Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2c0f86c
Record which added-member emulations hot reload takes (ADR 0011)
hatayama Oct 1, 2026
f367ccc
docs(hot-reload): Explain re-running an edited OnEnable by toggling e…
hatayama Oct 1, 2026
078e67d
feat(hot-reload): Raise and subscribe to field-like events added to a…
hatayama Oct 1, 2026
7cebb06
feat(hot-reload): Warn when a reload re-declares an added field or ev…
hatayama Oct 1, 2026
8863432
fix(hot-reload): Stop the introduced-type hint from listing subscript…
hatayama Oct 1, 2026
cbe6c92
docs(hot-reload): Describe field-like events added to a compiled class
hatayama Oct 1, 2026
9ca3798
refactor(hot-reload): Extract the field and event branches of the acc…
hatayama Oct 1, 2026
d3a7ee0
docs(hot-reload): Move the declaration lookup's summary back onto the…
hatayama Oct 1, 2026
e2f2e3d
test(hot-reload): Pin parenthesized subscriptions to a compiled event…
hatayama Oct 1, 2026
3c2bbd5
fix(hot-reload): Keep naming subscriptions to added events the store …
hatayama Oct 1, 2026
159c765
test(hot-reload): Require the declared-type warning itself to name th…
hatayama Oct 1, 2026
0d310b6
test(hot-reload): Pin lambda initializers of added fields and events
hatayama Oct 1, 2026
729f466
fix(hot-reload): Keep refusing events added to a generic class
hatayama Oct 1, 2026
581d2d9
test(hot-reload): Pin getters with private access raising an added ev…
hatayama Oct 1, 2026
195817e
test(hot-reload): Make the added-event and declared-type tests observ…
hatayama Oct 1, 2026
db3c179
fix(hot-reload): Say a delegate type change drops subscribers only wh…
hatayama Oct 1, 2026
d586263
docs(hot-reload): Correct the lifetime and scope of added events
hatayama Oct 2, 2026
caf6887
fix(hot-reload): Point subscriptions to added events outside the stor…
hatayama Oct 2, 2026
0506c50
test(hot-reload): Observe the handler a parenthesized subscription adds
hatayama Oct 2, 2026
547a339
fix(hot-reload): Name the step that applies when a method group handl…
hatayama Oct 2, 2026
b05ea42
fix(hot-reload): Tell how to re-run an edited OnEnable or OnDisable o…
hatayama Oct 2, 2026
3dadbbc
docs(hot-reload): Show the enabled toggle and the added-method workar…
hatayama Oct 2, 2026
7bb4899
fix(hot-reload): Keep the lambda advice for a private '+=' handler in…
hatayama Oct 2, 2026
417c602
docs(hot-reload): Say AddedFields is empty on --status and --revert-all
hatayama Oct 2, 2026
0e31b6a
docs(hot-reload): Separate re-running an edited OnEnable from an edit…
hatayama Oct 2, 2026
57bc2cd
refactor(hot-reload): Move the rescue-reason comment back onto its me…
hatayama Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .agents/skills/uloop-hot-reload/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ changed are patched (`UnchangedTotal` counts the rest).
## Scope in Brief

- Patched: ordinary method bodies and property getters with a body.
- Added members: new methods, fields, and supported properties apply as `Added` rows,
- Added members: new methods, fields, and supported properties apply as `Added` rows
(supported field-like events of a class are listed in `AddedFields`),
visible to edited code of the same reload within the same assembly (pass the declaring
file and its callers together), and gone on any compile or domain reload.
- New types: a top-level class, struct, enum, or interface declared in an edited
Expand All @@ -88,7 +89,10 @@ While hot-reload changes are active, `AutoRefreshHeld` is true so returning focu
recompile; `uloop compile` releases the hold, and `--revert-all` only when no introduced type
remains.
One-shot methods (`Awake`, `Start`, init helpers) patch but show no effect on the call that
already ran; the response marks them with `LifecycleNote`.
already ran; the response marks them with `LifecycleNote`. To re-run an edited compiled
`OnEnable`, set `enabled = false` with `execute-dynamic-code`, reload, then set it back to `true`
(Unity calls the old `OnDisable`, then the patched `OnEnable`). An edited `OnDisable` runs the
next time the component is disabled; to run it now, toggle `enabled` after the reload.
To tune a value while playing, expose a static property getter instead of a `const`; its
body is patched on compiled and introduced types alike.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,12 @@ include the file keep the type `AlreadyActive` and patch the body again. The fil
addition has to be in the reload: passed, or unchanged since it was last applied, which the
reload pulls back in on its own.

Constructors, initializers, setters, indexers, operators, event accessors and subscriptions to an
added event cannot be patched, so a call from them still fails the artifact compile (CS1061 or
Constructors, initializers, setters, indexers, operators and event accessors cannot be patched, so a call from them still fails the artifact compile (CS1061 or
CS0117) with a hint saying where such a call works. So does a call to an addition in another
assembly, or in a file that changed since it was last applied and is not passed.
assembly, or in a file that changed since it was last applied and is not passed. A subscription
to an added event the added-field store cannot hold (custom `add`/`remove` accessors, or a
delegate type not visible outside the assembly) fails the same way wherever it is written, even
in a method; only `uloop compile` makes it bind.

- When this reload does not patch a stubbed body (a generic method, or a method of a struct, is
`Skipped`), no type of that artifact is introduced: the stubbed type's row reads `Not
Expand Down
6 changes: 3 additions & 3 deletions .agents/skills/uloop-hot-reload/references/output.md

Large diffs are not rendered by default.

49 changes: 41 additions & 8 deletions .agents/skills/uloop-hot-reload/references/scope-and-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ scope and is reported as `Skipped`, same as edits to them. With a verified
baseline, event declarations are compared per accessor, so only the edited
add or remove appears as a `Skipped` row. A newly added explicit event, or
an edit before the first compile snapshot, still reports both accessors.
Adding a nested type, an event, or an indexer to a compiled type is still out of scope.
Adding a nested type or an indexer to a compiled type is still out of scope; an added
field-like event applies within the limits described below.
A new top-level `class`, `struct`, `enum`, or `interface` is introduced instead, within the
limits in [introduced-types.md](introduced-types.md); a `record` is refused there.
A member added to a compiled enum is out of scope too: it is not folded like an added
Expand Down Expand Up @@ -156,7 +157,8 @@ pattern that matches the property, `nameof`, `ref`/`out`/`in`, and conditional a
on the property itself. A compound assignment or increment keeps hot reloading when it is
rewritten as a plain assignment statement (`X = X + 1;`).

Types, events, and indexers are not reported per member — no `Skipped` row names them;
Types, indexers, and added events the store cannot hold (a struct host, a delegate type not
visible outside the assembly) are not reported per member — no `Skipped` row names them;
at most they surface as outside-body drift in `Warnings`. Treat their silence
as "not applied" and land them with `uloop compile`.

Expand Down Expand Up @@ -258,6 +260,15 @@ method's row says which answer it got in `LifecycleNote` (see Output).
its `Skipped` row names `uloop compile` as the only step: no rewrite of the body would make
the engine call it.

To re-run an edited compiled `OnEnable` on live objects — for example after adding a
subscription to it — toggle the component around the reload instead of compiling: set
`enabled = false` with `execute-dynamic-code`, run `hot-reload`, then set `enabled = true`.
Unity calls the old `OnDisable` (so the old subscription is removed) and then the patched
`OnEnable`; coroutines keep running because only the component is toggled. An edited
`OnDisable` runs the next time the component is disabled; to run it now, toggle `enabled`
after the reload. This works only for an `OnEnable` / `OnDisable` the compiled class already
has; an added one is not called.

The proxies exist only for the running session: nothing is attached outside Play Mode, and a
compile or a domain reload drops them along with every other patch. Execution order relative
to other components is not guaranteed — a proxy is its own component, so an added `Update`
Expand Down Expand Up @@ -347,12 +358,34 @@ Harmony accessor, which puts that method on the delegation path. Four shapes hav
backing field to reach and stay `Skipped` (see the table below): an event with custom
`add`/`remove` accessors, an `abstract`/`extern`/interface event, an event whose
delegate type is not visible outside the assembly, and an event added in this edit
(including one that had custom accessors when the assembly was last compiled).
that the added-field store cannot hold (see below).
Raising through a conditional receiver (`other?.E?.Invoke(x)`) and `nameof(E)` also
stay `Skipped`.
Subscribing to an event added in this edit, in any file of the run, is `Skipped`
too, whether the handler is a method group or a lambda: the compiled assembly has no
such event for the subscription to bind to until `uloop compile`.

A field-like event added in this edit to a compiled, non-generic class that is visible
outside the assembly keeps its delegate in the added-field store, like an added field, so edited bodies in any file of the same reload
can subscribe, unsubscribe, raise, read, and assign it (instance or static). It is listed in
`AddedFields`, shares the added-field lifetime, and compiled code that is not patched
cannot see it. These stay `Skipped`: a struct host (the struct-field reason), a generic
host or one nested in a generic type, a delegate type not visible outside the assembly, custom `add`/`remove` accessors, a name the
compiled class already uses for another member, `E ??= h`, `nameof(E)`, `a?.E += h`, passing it by `ref`, an initializer an added
field could not have, and `Get().E += h` (the receiver would be evaluated twice).
A handler that is a method group of an added method or of a compiled private method is
`Skipped` too; subscribe a lambda that calls it instead (`E += x => OnValue(x);`). When a
compiled method already holds `E += OnValue;` and compiled code removes it with `-= OnValue`,
do not wrap that line in a lambda (the `-=` would stop removing it): leave it as it is and
put the new lambda subscription in a method this reload adds, called from the edited method.
Subscriptions live on the store's delegate, not on Unity objects: `+=` is not atomic, so
subscribe and raise on the main thread only. A handler subscribed by an earlier reload
keeps running the body it was subscribed with until it is removed and subscribed again
(for a subscription made in `OnEnable`, toggle `enabled` as described under "Added Unity
messages"). Changing the event's delegate type in a later reload drops its subscribers unless the
new type can still read the old list (a variance-compatible change), and that reload names
it in `Warnings`. The store keeps subscriptions until `--revert-all`, a compile, or a domain
reload: an instance event's subscriptions go with the object, while a static event's outlive
Play Mode when Domain Reload is off, as a compiled static event's do. Deleting the event from
the source removes it from `AddedFields`, but its subscribers stay in the store until
`--revert-all`, so adding it back delivers to them again.

A `Skipped` row never undoes what an earlier reload applied to the same method: that
patch keeps running, so the method matches neither the compiled assembly nor the
Expand All @@ -374,10 +407,10 @@ source on disk. When a run skips a method it had patched before, `Warnings` name
| An added member's body cannot be fully bound in the hot-reload compilation | Hot reload cannot verify a member it cannot bind. A common cause: another file of the same reload, passed or pulled back in because it holds active patches, declares a compiled type from source, while a compiled API the body calls still names the compiled copy (for example, a lambda handed to a compiled `Register(Action<T>)`). The reason then names both types and the file declaring the compiled API. When the called member belongs to a type an earlier reload introduced and its signature was bound to the compiled copy, the reason names the introduced type and that compiled type instead. Either way the `Skipped` row names the step for that run (pass the file declaring the compiled API, leave the file declaring the type out, undo its edit and leave it out, or `uloop compile`), chosen from whether each file was passed, carried in, or already holds patches; a row about an added property's body points to the row of its accessor instead. A file passed this way is brought back by every later reload of the assembly while it stays unchanged, including the reload that re-applies after `--revert-all` or Play entry, until the next successful compile. When the skip deactivated added members an earlier reload applied, the next reload of the assembly retries their unchanged file once, so passing only the declaring file applies them again |
| Edited setter, init, or indexer accessor of a *compiled* property | Accessor patching covers getters only; `uloop compile` applies these edits. Accessors of a property added in this edit are emitted instead |
| Constructor (instance or static), operator, conversion operator, or explicit event accessor (add/remove) | Skipped; `uloop compile` applies these edits |
| Method raises or reads a field-like event that has no reachable backing field | Custom `add`/`remove` accessors, an `abstract`/`extern`/interface event, a delegate type that is not visible outside the assembly, or an event added in this edit leave nothing for the shim's Harmony accessor to bind |
| Method raises or reads a field-like event that has no reachable backing field | Custom `add`/`remove` accessors, an `abstract`/`extern`/interface event, a delegate type that is not visible outside the assembly, or an added event the added-field store cannot hold leave nothing for the shim's Harmony accessor to bind |
| Method raises or reads a field-like event through a conditional receiver (`other?.E`) | The shim has no name for the conditional receiver to pass to the accessor call |
| Method names a field-like event inside `nameof` | The shim is a different type and cannot keep the bare event name |
| Method subscribes (`+=`/`-=`) to an event added in this edit | The shim binds the subscription against the compiled assembly, which has no such event yet |
| Method subscribes (`+=`/`-=`) to an event added in this edit that the added-field store cannot hold | The shim binds the subscription against the compiled assembly, which has no such event yet; an added field-like event of a class is subscribed through the store instead |

## Failed — flips `Success` to `false`

Expand Down
8 changes: 6 additions & 2 deletions .claude/skills/uloop-hot-reload/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ changed are patched (`UnchangedTotal` counts the rest).
## Scope in Brief

- Patched: ordinary method bodies and property getters with a body.
- Added members: new methods, fields, and supported properties apply as `Added` rows,
- Added members: new methods, fields, and supported properties apply as `Added` rows
(supported field-like events of a class are listed in `AddedFields`),
visible to edited code of the same reload within the same assembly (pass the declaring
file and its callers together), and gone on any compile or domain reload.
- New types: a top-level class, struct, enum, or interface declared in an edited
Expand All @@ -88,7 +89,10 @@ While hot-reload changes are active, `AutoRefreshHeld` is true so returning focu
recompile; `uloop compile` releases the hold, and `--revert-all` only when no introduced type
remains.
One-shot methods (`Awake`, `Start`, init helpers) patch but show no effect on the call that
already ran; the response marks them with `LifecycleNote`.
already ran; the response marks them with `LifecycleNote`. To re-run an edited compiled
`OnEnable`, set `enabled = false` with `execute-dynamic-code`, reload, then set it back to `true`
(Unity calls the old `OnDisable`, then the patched `OnEnable`). An edited `OnDisable` runs the
next time the component is disabled; to run it now, toggle `enabled` after the reload.
To tune a value while playing, expose a static property getter instead of a `const`; its
body is patched on compiled and introduced types alike.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,12 @@ include the file keep the type `AlreadyActive` and patch the body again. The fil
addition has to be in the reload: passed, or unchanged since it was last applied, which the
reload pulls back in on its own.

Constructors, initializers, setters, indexers, operators, event accessors and subscriptions to an
added event cannot be patched, so a call from them still fails the artifact compile (CS1061 or
Constructors, initializers, setters, indexers, operators and event accessors cannot be patched, so a call from them still fails the artifact compile (CS1061 or
CS0117) with a hint saying where such a call works. So does a call to an addition in another
assembly, or in a file that changed since it was last applied and is not passed.
assembly, or in a file that changed since it was last applied and is not passed. A subscription
to an added event the added-field store cannot hold (custom `add`/`remove` accessors, or a
delegate type not visible outside the assembly) fails the same way wherever it is written, even
in a method; only `uloop compile` makes it bind.

- When this reload does not patch a stubbed body (a generic method, or a method of a struct, is
`Skipped`), no type of that artifact is introduced: the stubbed type's row reads `Not
Expand Down
Loading
Loading