instar 1.3.550 → 1.3.552

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +25 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +24 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  8. package/dist/core/PostUpdateMigrator.js +32 -0
  9. package/dist/core/PostUpdateMigrator.js.map +1 -1
  10. package/dist/core/RemoteAckStore.d.ts +71 -0
  11. package/dist/core/RemoteAckStore.d.ts.map +1 -0
  12. package/dist/core/RemoteAckStore.js +144 -0
  13. package/dist/core/RemoteAckStore.js.map +1 -0
  14. package/dist/core/types.d.ts +24 -1
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/core/types.js.map +1 -1
  17. package/dist/scaffold/templates.d.ts.map +1 -1
  18. package/dist/scaffold/templates.js +2 -0
  19. package/dist/scaffold/templates.js.map +1 -1
  20. package/dist/scheduler/JobScheduler.d.ts +33 -0
  21. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  22. package/dist/scheduler/JobScheduler.js +75 -0
  23. package/dist/scheduler/JobScheduler.js.map +1 -1
  24. package/dist/server/routes.d.ts.map +1 -1
  25. package/dist/server/routes.js +186 -0
  26. package/dist/server/routes.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/data/builtin-manifest.json +64 -64
  29. package/src/data/state-coherence-registry.json +12 -0
  30. package/src/scaffold/templates.ts +2 -0
  31. package/upgrades/1.3.551.md +23 -0
  32. package/upgrades/1.3.552.md +23 -0
  33. package/upgrades/side-effects/ws41-durable-ack.md +37 -0
  34. package/upgrades/side-effects/ws43-role-guard.md +40 -0
  35. package/upgrades/ws43-role-guard.eli16.md +47 -0
@@ -0,0 +1,23 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The **role-guard-at-spawn** closes the deferred follow-up to the merged WS4.3 jobs read-side (PR #1104, CMT-1416). The scheduler only starts on the in-charge (lease-holding) machine — but it is never torn down if that machine loses the lease mid-run, so its cron timers keep firing. That left a window where a STATE-WRITING scheduled job could spawn on a machine that has since become a read-only standby (the double-writer corruption the standby rule exists to prevent). Now `JobScheduler.triggerJob` re-checks the lease at the spawn boundary — read LIVE, not cached — and refuses a job marked `"writesState": true` when this machine does not hold the lease. The cron fires on every machine, so the lease-holder's pass runs the job; the refusal re-routes by construction, and raises one calm deduped heads-up. Ships behind `multiMachine.seamlessness.ws43RoleGuard` (default false) per `docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md`; a single-machine or flag-off agent is a strict no-op.
9
+
10
+ ## What to Tell Your User
11
+
12
+ None while dark — internal multi-machine plumbing. The user-visible behaviour, once an operator turns it on across more than one machine: a scheduled job that writes shared data will no longer accidentally run on the standby machine if the in-charge machine hands off control while that job's timer is ticking — it runs on whichever machine is actually in charge, and you get one quiet note if a job had to be redirected. On a single-machine setup nothing changes.
13
+
14
+ ## Summary of New Capabilities
15
+
16
+ None user-facing while dark. One new opt-in job field: `writesState` in `.instar/jobs.json`. One new scheduler injector: `setRoleGuard`. Migration parity: the `ws43RoleGuard` config default and the Job-Scheduler awareness bullet reach already-deployed agents on update (config add-missing + idempotent migrateClaudeMd splice), so existing agents receive the capability and its prose, not just new installs.
17
+
18
+ ## Evidence
19
+
20
+ - `tests/unit/job-scheduler-role-guard.test.ts` — 8/8: refuses on standby + raises attention; records role-guard skip; ALLOWS on lease-holder; ignores non-state-writing jobs; strict no-op when flag off / no provider; degrades to spawn-proceeds on a throwing provider; refusal survives a throwing attention callback.
21
+ - `tests/integration/scheduler-role-guard-integration.test.ts` — 4/4: full triggerJob pipeline with a config-flag-driven provider + real SkipLedger/StateManager — refusal lands a role-guard skip + job_skipped event; re-route-by-construction (lease-holder spawns the same job); flag-off no-op; live re-read of a mid-run demotion.
22
+ - `tests/e2e/scheduler-role-guard-alive.test.ts` — 2/2: the Phase-1 "feature is alive" E2E — a REAL MultiMachineCoordinator.holdsLease() (no mock) drives a real refusal when the flag is on; strict no-op when off.
23
+ - Gate suite green: tsc 0, dark-gate 24/24 (golden line-map recomputed), no-silent-fallbacks 5/5, feature-delivery-completeness 97/97, all 15 lints clean.
@@ -0,0 +1,37 @@
1
+ # Side-Effects Review — WS4.1 follow-up: durable operator-bound /ack (an ack survives the owner machine being briefly offline)
2
+
3
+ **Spec:** docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md (review-convergence + approved:true). **Parent:** Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions.
4
+ **Ships DARK** behind `multiMachine.seamlessness.ws41DurableAck` (default false). Single-machine / flag-off agents are a strict no-op.
5
+ **Files:** src/core/RemoteAckStore.ts (new), src/server/routes.ts, src/config/ConfigDefaults.ts, src/core/PostUpdateMigrator.ts, src/scaffold/templates.ts, site/src/content/docs/features/multi-machine.md, site/src/content/docs/reference/api.md
6
+
7
+ ## What changed
8
+
9
+ 1. **RemoteAckStore.ts (new):** durable queue for an operator's ack/resolve of a POOLED attention item whose OWNER is briefly offline. Persists the ack INTENT — with the AUTHENTICATED operator principal that performed it — so the intent survives the owner being dark; a drain tick + a boot sweep re-deliver it when the owner returns. Append-then-compact JSONL under `logs/` (no DB dep, mirrors RemoteCloseAudit / PendingInboundStore). Idempotent on `(itemId, targetMachineId)` — a re-ack refreshes intent, never stacks duplicates. Best-effort writes: a failed append/compact logs loudly but never throws into the ack path. **The store never authorizes anything — it only remembers intent; the OWNER revalidates the carried principal at apply time.**
10
+ 2. **routes.ts (`POST /attention/:id/remote-ack`):** the operator-facing leg. When `ws41DurableAck` is off it 503s; when on, a remote-owned item's ack is persisted to the store (with the Bearer-authenticated principal) instead of evaporating against the offline owner. The receiver-side precedence guard (an owner applying a remote-ack intent on return) is gated on the same flag — a no-op when off.
11
+ 3. **ConfigDefaults.ts:** new `ws41DurableAck: false` sibling under `multiMachine.seamlessness`. Deliberately NOT named `enabled` → it is outside the dev-agent dark-gate lint by construction (no inline `enabled:` line added).
12
+ 4. **PostUpdateMigrator.ts + templates.ts:** an Attention-Queue awareness bullet — `generateClaudeMd` (new agents) + an idempotent content-sniffed `migrateClaudeMd` additive patcher (existing agents).
13
+ 5. **docs:** multi-machine.md + api.md note the route + the dark posture.
14
+
15
+ ## Blast radius
16
+
17
+ - **Config-gated, not wiring-gated.** With `ws41DurableAck` false (the fleet default) the route 503s, the store is never constructed, and the receiver precedence guard is inert. A single-machine agent never has a remote owner, so the path is dead by topology too.
18
+ - **No authorization surface.** The store carries the operator principal as DATA the owner revalidates at apply — it never grants anything. The Bearer auth on the route is the only authority; the durable intent is replayed THROUGH the owner's normal ack authorization, not around it.
19
+ - **No new MeshRpc verb / no broadcast.** The intent re-delivers on the owner's existing return/boot path. N-machine-safe, no LAN assumption.
20
+
21
+ ## Risk + mitigation
22
+
23
+ - **Risk:** a stale or forged ack intent resolves an item it shouldn't. **Mitigation:** the owner revalidates the carried authenticated principal at apply time; an intent it can't authorize is dropped. The store is idempotent on (itemId,targetMachineId), so a redelivery can't double-apply.
24
+ - **Risk:** the store grows unbounded if an owner stays dark. **Mitigation:** append-then-compact keyed on (itemId,targetMachineId) collapses re-acks; a delivered/applied intent is compacted out. (A TTL horizon is a tracked follow-up if dark-peer accumulation proves real — CMT-1416.)
25
+ - **Risk:** a store write error breaks the operator's ack path. **Mitigation:** every write is best-effort try/catch — a failure logs loudly (an unrecorded ack-intent deserves a trace) but the route still returns; the operator's ack is never blocked by the durability layer.
26
+
27
+ ## Migration parity
28
+
29
+ - `ws41DurableAck: false` reaches existing agents via the generic config add-missing path (sibling under the already-migrated `multiMachine.seamlessness` block). The CLAUDE.md awareness bullet ships in `generateClaudeMd` + an idempotent content-sniffed `migrateClaudeMd` patcher. The Attention-Queue section heading is UNCHANGED → feature-delivery-completeness stays green (sub-bullet into an already-tracked section, the WS5.3/WS4.2 precedent).
30
+
31
+ ## Dark-gate line-map
32
+
33
+ - The `ws41DurableAck` flag is NOT an inline `enabled:` line, so the attributor sees no NEW attributed path. Any cartographer line shift came from main advancing; the EXPECTED map was recomputed. Verified: `tests/unit/lint-dev-agent-dark-gate.test.ts` → 24/24 green.
34
+
35
+ ## Rollback
36
+
37
+ - Revert the squash commit. Dark-by-default means nothing was live; no data migration, no state repair. The `logs/` JSONL (only written when an operator enabled the flag AND a remote-owned ack happened) is inert append-only data that simply stops being read.
@@ -0,0 +1,40 @@
1
+ # Side-Effects Review — WS4.3 role-guard-at-spawn (a state-writing job is refused on a read-only standby)
2
+
3
+ **Spec:** docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md (review-convergence + approved:true). **Parent:** Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions.
4
+ **Ships DARK** behind `multiMachine.seamlessness.ws43RoleGuard` (default false). Single-machine / flag-off agents are a strict no-op.
5
+ **Files:** src/scheduler/JobScheduler.ts, src/commands/server.ts, src/core/types.ts, src/config/ConfigDefaults.ts, src/core/PostUpdateMigrator.ts, src/scaffold/templates.ts, tests/unit/lint-dev-agent-dark-gate.test.ts (golden line-map)
6
+
7
+ ## What changed
8
+
9
+ 1. **JobScheduler.ts (the spawn-boundary re-check):** `triggerJob` gains a role-guard check, placed right after the machine-scope check and BEFORE the claim/capacity/spawn path. When a role-guard provider is wired AND `job.writesState` AND the provider reports `enabled && !holdsLease`, the trigger is REFUSED (`'skipped'`, no throw, no spawn): a `role-guard` skip-ledger row + a `job_skipped` state event (`gateReason: 'role-guard'`) + a best-effort deduped attention callback. The provider is read LIVE on every trigger (never cached), so a mid-run demotion takes effect on the next tick — the TOCTOU fix, same family as WS1.1's `_ownershipReadForDrain`. Wired via a new `setRoleGuard(provider, onRefused?)` injector.
10
+ 2. **server.ts (the wiring):** at the existing scheduler-Telegram wiring point (inside `if (scheduler)`, where `coordinator` + `config` + `telegram` are all in scope), `scheduler.setRoleGuard(...)` is called with a provider that reads `multiMachine.seamlessness.ws43RoleGuard` from config and `coordinator.holdsLease()` LIVE, and an `onRefused` that raises ONE per-slug deduped attention item (Agent-Health lane, LOW) so the operator learns a state-writing job couldn't run on this read-only standby.
11
+ 3. **types.ts:** new optional `JobDefinition.writesState?: boolean` (additive, default-absent ⇒ not guarded), new `SkipReason` member `'role-guard'`, and the `seamlessness.ws43RoleGuard?: boolean` config-type field.
12
+ 4. **ConfigDefaults.ts:** new `ws43RoleGuard: false` sibling under `multiMachine.seamlessness`. Deliberately NOT named `enabled` → outside the dev-agent dark-gate lint by construction.
13
+ 5. **PostUpdateMigrator.ts + templates.ts:** a Job-Scheduler awareness bullet — `generateClaudeMd` (new agents) + an idempotent content-sniffed (`ws43RoleGuard`) `migrateClaudeMd` additive patcher anchored after the `/jobs` Trigger line (existing agents).
14
+
15
+ ## Blast radius
16
+
17
+ - **Config-gated AND opt-in per job.** With `ws43RoleGuard` false (the fleet default) the provider returns `enabled:false` and the guard is a strict no-op. Even with the flag ON, ONLY jobs that explicitly carry `writesState: true` are ever considered — no existing job is affected until it opts in.
18
+ - **Topology-dead on a single machine.** A single-machine agent always holds the lease (`holdsLease()` ⇒ true), so the guard never fires regardless of the flag.
19
+ - **No new route, no new MeshRpc verb, no broadcast.** The re-route is by construction — the cron fires on every machine and only the lease-holder's pass clears the guard. The attention item rides the existing Agent-Health lane (calm, deduped, never a per-event topic).
20
+ - **Cannot block safe work.** The guard can only ever REFUSE a state-writing job that would have run on a read-only standby; a throwing provider degrades to spawn-proceeds (today's behavior).
21
+
22
+ ## Risk + mitigation
23
+
24
+ - **Risk:** a broken/throwing provider wedges the scheduler. **Mitigation:** the provider call is try/caught; a throw resolves to `{ enabled:false, holdsLease:true }` so the spawn proceeds — the safe direction (never gate on a broken signal). Unit-tested.
25
+ - **Risk:** the attention callback errors and swallows the refusal. **Mitigation:** the refusal (skip + ledger + event) happens BEFORE the callback; the callback is in its own try/catch. The refusal is the load-bearing safety; the heads-up is best-effort. Unit-tested.
26
+ - **Risk:** the deduped attention item floods on a flapping lease. **Mitigation:** per-slug dedup id (`agent:ws43-role-guard:<slug>`) + Agent-Health lane (suppression-deduped, single calm topic) + the universal topic-creation budget backstop.
27
+ - **Risk:** a legitimately state-writing job never runs because no machine holds the lease. **Mitigation:** exactly one machine always holds the lease in a healthy mesh; the heads-up names the situation so the operator sees it if a genuine no-writable-machine condition persists.
28
+
29
+ ## Migration parity
30
+
31
+ - `ws43RoleGuard: false` reaches existing agents via the generic config add-missing path (sibling under the already-migrated `multiMachine.seamlessness` block). The CLAUDE.md awareness bullet ships in `generateClaudeMd` + an idempotent content-sniffed (`ws43RoleGuard`) `migrateClaudeMd` patcher anchored after the `/jobs` Trigger line. The Job-Scheduler section heading is UNCHANGED → feature-delivery-completeness stays green (sub-bullet into an already-tracked section, the WS4.1 precedent).
32
+ - `JobDefinition.writesState` is additive — older agents' job parsers ignore the unknown field, so a jobs.json carrying it is back-compatible.
33
+
34
+ ## Dark-gate line-map
35
+
36
+ - `ws43RoleGuard` is NOT an inline `enabled:` line, so the attributor sees no NEW attributed path. BUT inserting the flag's comment block into ConfigDefaults.ts shifted every `enabled: false` line from `sessionPool.enabled` onward by +13. The EXPECTED golden map in `tests/unit/lint-dev-agent-dark-gate.test.ts` was recomputed via `attributeEnabledFalsePaths('src/config/ConfigDefaults.ts')` on the merged tree and updated by hand. Verified: 24/24 green.
37
+
38
+ ## Rollback
39
+
40
+ - Revert the squash commit. Dark-by-default means nothing was live; no data migration, no state repair. The `role-guard` skip-ledger rows + `job_skipped` events (only written when an operator enabled the flag AND a state-writing job was refused) are inert append-only observability data.
@@ -0,0 +1,47 @@
1
+ # ELI16 — WS4.3 role-guard-at-spawn (a state-writing job can't run on the wrong machine)
2
+
3
+ ## The problem
4
+
5
+ When I run on more than one computer, exactly one of them is "in charge" at a
6
+ time (it holds a token called the *lease*). The other is a standby — it is
7
+ deliberately read-only, so two computers can't both scribble on the same shared
8
+ notebook and corrupt it.
9
+
10
+ My scheduled jobs are a problem here. The job scheduler only starts up on the
11
+ in-charge computer. But here's the gap: if that computer LOSES the lease while
12
+ it's still running (it gets demoted to standby mid-shift), nobody turns its
13
+ scheduler off. Its cron timers keep ticking. So a job that WRITES to the shared
14
+ notebook could fire on a computer that is now supposed to be read-only — the
15
+ exact double-writer corruption the standby rule exists to prevent. It's a
16
+ classic timing hole: the scheduler checked "am I in charge?" at startup, but the
17
+ answer can go stale before the job actually runs.
18
+
19
+ ## What this change does
20
+
21
+ Right before the scheduler spawns a job, it re-checks — at that very moment, not
22
+ at startup — two things:
23
+
24
+ 1. Is this a STATE-WRITING job? (The job opts in by marking itself
25
+ `"writesState": true`.)
26
+ 2. Do I currently hold the lease?
27
+
28
+ If it's a state-writing job AND I do NOT hold the lease, the scheduler refuses
29
+ to start it, writes down why ("role-guard" skip), and raises one calm heads-up
30
+ note. The job isn't lost: the cron timer fires on EVERY computer, and the one
31
+ that actually holds the lease passes the check and runs it. So the refusal
32
+ re-routes the work to the right machine all by itself.
33
+
34
+ It's the same kind of just-in-time re-check the message router already does
35
+ before it spawns a session — catch the stale answer at the last possible moment.
36
+
37
+ ## Why it's safe to ship
38
+
39
+ It's dark by default (behind a flag), and it only ever affects jobs that
40
+ explicitly mark themselves state-writing. On a single computer you always hold
41
+ the lease, so it never fires. If the flag is off, or the check itself errors, the
42
+ job spawns exactly like today — the guard can only ever REFUSE work that would
43
+ have been unsafe, never block work that was fine.
44
+
45
+ ## Parent principle
46
+
47
+ Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions.