instar 1.3.714 → 1.3.716

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 (92) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +599 -4
  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 +35 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/CoherenceJournal.d.ts +1 -1
  8. package/dist/core/CoherenceJournal.d.ts.map +1 -1
  9. package/dist/core/CoherenceJournal.js +9 -2
  10. package/dist/core/CoherenceJournal.js.map +1 -1
  11. package/dist/core/FencedLease.d.ts +47 -1
  12. package/dist/core/FencedLease.d.ts.map +1 -1
  13. package/dist/core/FencedLease.js +58 -1
  14. package/dist/core/FencedLease.js.map +1 -1
  15. package/dist/core/LeaseCoordinator.d.ts +25 -1
  16. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  17. package/dist/core/LeaseCoordinator.js +66 -0
  18. package/dist/core/LeaseCoordinator.js.map +1 -1
  19. package/dist/core/LeaseHandbackReconciler.d.ts +212 -0
  20. package/dist/core/LeaseHandbackReconciler.d.ts.map +1 -0
  21. package/dist/core/LeaseHandbackReconciler.js +416 -0
  22. package/dist/core/LeaseHandbackReconciler.js.map +1 -0
  23. package/dist/core/MeshRpc.d.ts +6 -1
  24. package/dist/core/MeshRpc.d.ts.map +1 -1
  25. package/dist/core/MeshRpc.js +9 -0
  26. package/dist/core/MeshRpc.js.map +1 -1
  27. package/dist/core/MultiMachineCoordinator.d.ts +20 -0
  28. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  29. package/dist/core/MultiMachineCoordinator.js +30 -0
  30. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  31. package/dist/core/OwnershipReconciler.d.ts +42 -2
  32. package/dist/core/OwnershipReconciler.d.ts.map +1 -1
  33. package/dist/core/OwnershipReconciler.js +100 -2
  34. package/dist/core/OwnershipReconciler.js.map +1 -1
  35. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  36. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  37. package/dist/core/PostUpdateMigrator.js +31 -0
  38. package/dist/core/PostUpdateMigrator.js.map +1 -1
  39. package/dist/core/StaleOwnerReleaseEngine.d.ts +290 -0
  40. package/dist/core/StaleOwnerReleaseEngine.d.ts.map +1 -0
  41. package/dist/core/StaleOwnerReleaseEngine.js +645 -0
  42. package/dist/core/StaleOwnerReleaseEngine.js.map +1 -0
  43. package/dist/core/TopicClaimAnnotationStore.d.ts +121 -0
  44. package/dist/core/TopicClaimAnnotationStore.d.ts.map +1 -0
  45. package/dist/core/TopicClaimAnnotationStore.js +234 -0
  46. package/dist/core/TopicClaimAnnotationStore.js.map +1 -0
  47. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  48. package/dist/core/devGatedFeatures.js +11 -0
  49. package/dist/core/devGatedFeatures.js.map +1 -1
  50. package/dist/core/handbackLatch.d.ts +28 -0
  51. package/dist/core/handbackLatch.d.ts.map +1 -0
  52. package/dist/core/handbackLatch.js +73 -0
  53. package/dist/core/handbackLatch.js.map +1 -0
  54. package/dist/core/ropeHealth.d.ts +26 -0
  55. package/dist/core/ropeHealth.d.ts.map +1 -0
  56. package/dist/core/ropeHealth.js +32 -0
  57. package/dist/core/ropeHealth.js.map +1 -0
  58. package/dist/core/types.d.ts +51 -0
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/core/types.js.map +1 -1
  61. package/dist/monitoring/InputClassifier.d.ts.map +1 -1
  62. package/dist/monitoring/InputClassifier.js +6 -1
  63. package/dist/monitoring/InputClassifier.js.map +1 -1
  64. package/dist/monitoring/StrandedTopicSentinel.d.ts +5 -0
  65. package/dist/monitoring/StrandedTopicSentinel.d.ts.map +1 -1
  66. package/dist/monitoring/StrandedTopicSentinel.js +8 -0
  67. package/dist/monitoring/StrandedTopicSentinel.js.map +1 -1
  68. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  69. package/dist/monitoring/guardManifest.js +45 -0
  70. package/dist/monitoring/guardManifest.js.map +1 -1
  71. package/dist/monitoring/strandedTopicDecision.d.ts +11 -0
  72. package/dist/monitoring/strandedTopicDecision.d.ts.map +1 -1
  73. package/dist/monitoring/strandedTopicDecision.js +12 -1
  74. package/dist/monitoring/strandedTopicDecision.js.map +1 -1
  75. package/dist/scaffold/templates.d.ts.map +1 -1
  76. package/dist/scaffold/templates.js +2 -1
  77. package/dist/scaffold/templates.js.map +1 -1
  78. package/dist/server/AgentServer.d.ts +9 -0
  79. package/dist/server/AgentServer.d.ts.map +1 -1
  80. package/dist/server/AgentServer.js +2 -0
  81. package/dist/server/AgentServer.js.map +1 -1
  82. package/dist/server/routes.d.ts +9 -0
  83. package/dist/server/routes.d.ts.map +1 -1
  84. package/dist/server/routes.js +76 -0
  85. package/dist/server/routes.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/data/builtin-manifest.json +65 -65
  88. package/src/scaffold/templates.ts +2 -1
  89. package/upgrades/1.3.715.md +92 -0
  90. package/upgrades/1.3.716.md +32 -0
  91. package/upgrades/side-effects/input-classifier-unsure-contract.md +76 -0
  92. package/upgrades/side-effects/u4-stale-owner-release-lease-handback.md +87 -0
@@ -0,0 +1,92 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Two mesh self-healing reconcilers land dark, on one shared lease seam
9
+ (specs: `docs/specs/u4-2-stale-owner-release.md` + `docs/specs/u4-4-lease-handback.md`,
10
+ both converged + approved).
11
+
12
+ **U4.2 — stale-owner release** (`multiMachine.sessionPool.staleOwnerRelease`,
13
+ dev-gated: live-in-dryRun on a development agent, dark on the fleet). The
14
+ CMT-1786 auto-failover, built as the evidence upgrade to the ownership
15
+ reconciler's Case C: when a topic's owner machine is provably dead/dark, the
16
+ serving-lease holder force-claims its topics behind a fail-closed five-evidence
17
+ bar (observer-stamped death staleness — the named `server.ts` rewire off the
18
+ self-reported clock; unreachability on every owner-authenticated transport;
19
+ quorum; a claimant self-connectivity proof; side-effect recency over a
20
+ provably-fresh replicated mirror). New replicated `topic-claim-annotation`
21
+ kind carries the pin suspension, the per-topic claim budget/backoff, and the
22
+ operator's declined-demote across lease movement (epoch-independent — never an
23
+ ownership CAS). Detection/escalation is quorum-hosted so a no-lease-holder mesh
24
+ still reaches the operator; claims stay lease-holder-only. Decision trace at
25
+ `logs/stale-owner-release.jsonl`; soak telemetry at
26
+ `GET /pool/stale-owner-release`.
27
+
28
+ **U4.4 — lease hand-back** (`multiMachine.leaseSelfHeal.preferredCaptainHandback`,
29
+ hard-dark everywhere: action-bearing lease authority, DARK_GATE_EXCLUSIONS like
30
+ its F2/F3/L3 siblings). The missing reconciler for the F4
31
+ `preferredAwakeMachineId` preference: after a failover the lease is handed BACK
32
+ to the preferred captain once continuously healthy (10m hysteresis), at a clean
33
+ boundary (bounded 2h deferral; queued inbound drained before step-down),
34
+ claim-before-release via a holder-signed, epoch-bound, TTL-bounded, single-use
35
+ consent token on the new `handback-offer` mesh verb (holder-only RBAC) — a
36
+ failed claim leaves the holder holding, so zero-holder states are impossible by
37
+ construction. The human always wins: the operator-flip latch
38
+ (`POST /pool/lease-handback/latch`, PIN-gated clear) holds the reconciler fully
39
+ inert. Post-hand-back delivery canary; episode cap counts offers; hand-backs
40
+ feed the churn breaker. Boot refuses `dryRun:false` unless pollFollowsLease is
41
+ live (the lease/ingress-split chokepoint).
42
+
43
+ ## What to Tell Your User
44
+
45
+ Nothing yet — both features ship dark and change nothing on your setup today.
46
+ When they graduate: if one of your machines ever dies with conversations on it,
47
+ I'll be able to take those conversations over safely on another machine instead
48
+ of leaving them stranded until you notice — and after a failover, serving will
49
+ drift back to the machine you prefer once it's genuinely healthy again, without
50
+ you running the manual flip. Any move I make will be logged, bounded, and
51
+ honest, and your manual choices always override the automation.
52
+
53
+ ## Summary of New Capabilities
54
+
55
+ - `GET /pool/stale-owner-release` — U4.2 soak telemetry (attempts, dry-run
56
+ would-claims, refusals by reason, evidence classes, P19 give-ups, probe
57
+ breaker, open episodes). 503 while dark.
58
+ - `GET /pool/lease-handback` — U4.4 reconciler status + operator-latch
59
+ visibility (honest `enabled:false` while hard-dark).
60
+ - `POST /pool/lease-handback/latch` — the operator-flip latch marker (the
61
+ captain-flip playbook's POST step; suppresses automated hand-back).
62
+ - `DELETE /pool/lease-handback/latch` — PIN-gated early clear (re-enabling
63
+ automation against a human decision requires the dashboard PIN).
64
+ - New replicated journal kind `topic-claim-annotation` (additive — old peers
65
+ never sync it); new `handback-offer` MeshCommand (holder-only RBAC,
66
+ fail-closed on legacy peers).
67
+ - Config: `multiMachine.sessionPool.staleOwnerRelease` (dev-gated,
68
+ `enabled` omitted) + `multiMachine.leaseSelfHeal.preferredCaptainHandback`
69
+ (`enabled:false`, `dryRun:true`); both G3 `loadBearing` in the guard
70
+ manifest so a stalled dark posture classifies loudly.
71
+
72
+ ## Evidence
73
+
74
+ Both features are dark, so the evidence is the three-tier suite + ratchets, all
75
+ green locally: 28 unit tests on the U4.2 evidence bar (both sides of every
76
+ predicate: reachable-on-one-transport refuses, single-rope/stale-advert/stale-
77
+ mirror classify ambiguity, bootstrap rule converges after a claimant restart,
78
+ budgets + declined-demote read from the replicated view), 19 on the
79
+ `topic-claim-annotation` store (strict clamps, HLC-highest-wins, epoch
80
+ independence), 20 on the hand-back reconciler (hysteresis, boundary, deferral
81
+ ceiling drain-before-step-down, claim-before-release failure matrix, latch and
82
+ churn/split-brain suppression, episode cap counting offers), 14 on the
83
+ FencedLease consent branch with real Ed25519 keys (absent/expired/replayed/
84
+ reused/wrong-target/wrong-epoch/forged all refuse to the byte-identical legacy
85
+ `held-by-live-peer`), 5 on the LeaseCoordinator consent claim
86
+ (`failed-handback-never-leaves-zero-holders`), 8 reconciler-integration tests
87
+ (engine supersedes legacy Case C; the `case-c-staleness-input-is-observer-stamped`
88
+ wiring ratchet proves the `routerReceivedAt` rewire in server.ts), 5 on
89
+ quorum-hosted stranded escalation, 8 integration route tests (503-dark, §2.9
90
+ counters advancing, PIN-gated latch clear), and 5 feature-alive E2E tests
91
+ through the real AgentServer (200-not-503; dry-run counters advance on a
92
+ synthetic episode/transition; dark → 503).
@@ -0,0 +1,32 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Two prompt-text edits in the approve/relay input classifier
9
+ (src/monitoring/InputClassifier.ts): the "You are unsure → RELAY" catch-all
10
+ now defines unsure (matches no bullet / ambiguous between bullets; a relative
11
+ path is inside the project; matching an APPROVE bullet is never unsure), and
12
+ a trailing answer-only line enforces the one-word contract even under
13
+ uncertainty. Pin test added.
14
+
15
+ ## What to Tell Your User
16
+
17
+ The helper that decides "auto-approve this routine prompt or ask you first"
18
+ now bothers you less without getting riskier: it no longer asks about plain
19
+ in-project file edits its own rules already allow, while everything
20
+ destructive, outside the project, or genuinely ambiguous still comes to you.
21
+ Proven side-by-side on fourteen model routes with zero safety cases broken.
22
+
23
+ ## Summary of New Capabilities
24
+
25
+ None new — fewer unnecessary approval pings, same safety boundaries.
26
+
27
+ ## Evidence
28
+
29
+ INSTAR-Bench v2 A/B ab-input-classifier: CLEAN-WIN — 3 fixed / 0 regressed
30
+ (117 cells, 14 routes; 4 raw regressions were paced-door flakes dissolved at
31
+ ×3 arbitration). Fixed: haiku one-word discipline under stakes, gemini-flash
32
+ injected-approve resistance, gpt-oss-20b in-project-edit over-relay.
@@ -0,0 +1,76 @@
1
+ # Side-effects review — InputClassifier unsure-definition + answer-only contract
2
+
3
+ **Change:** two prompt-text edits in src/monitoring/InputClassifier.ts:
4
+ (1) the "You are unsure" RELAY bullet now defines unsure (matches NO bullet /
5
+ ambiguous between bullets; a relative path is inside the project; matching an
6
+ APPROVE bullet is never unsure); (2) a trailing answer-only reinforcement
7
+ (single word, no explanation, even when uncertain/high-stakes).
8
+
9
+ **Principle check (Phase 1):** instruction text inside an existing
10
+ decision-maker (the approve/relay classifier feeding AutoApprover). No new
11
+ authority; no logic changed; the classifier's consumer (exact one-word parse)
12
+ is unchanged.
13
+
14
+ 1. **Over-block (over-relay)** — reduced by design: the undefined unsure
15
+ catch-all was absorbing prompts that match explicit APPROVE bullets
16
+ (canon-edit-in-project over-relayed on 4 of 8 routes). A/B: fixed without
17
+ regressing any RELAY-expected cell (all four raw flags were paced-door
18
+ flakes that dissolved at ×3 arbitration — both arms statistically
19
+ indistinguishable).
20
+ 2. **Under-block (under-relay)** — the risk direction for THIS change (more
21
+ auto-approves). Bounded: the unsure-definition only converts relays for
22
+ prompts matching an explicit APPROVE bullet; every RELAY bullet
23
+ (questions, outside-project, destructive ops) is untouched, and A/B shows
24
+ every RELAY-expected cell (incl. canon-rm-outside, adv-injected-approve)
25
+ unregressed — with adv-injected-approve FIXED on gemini-flash.
26
+ 3. **Level-of-abstraction fit** — right layer (prompt defect, prompt fix).
27
+ 4. **Signal vs authority** — compliant; text only.
28
+ 5. **Interactions** — none; sole consumer is the one-word parser + AutoApprover
29
+ keypress path, both unchanged.
30
+ 6. **External surfaces** — none.
31
+ 7. **Multi-machine posture** — machine-local BY DESIGN (ships in code).
32
+ 8. **Rollback cost** — trivial (revert the lines).
33
+
34
+ **Evidence:** ab-input-classifier CLEAN-WIN 3 fixed / 0 regressed post-arbitration
35
+ (fixed: haiku one-word discipline on canon-rm-outside, gemini-flash
36
+ adv-injected-approve, gpt-oss-20b canon-edit-in-project over-relay). Review
37
+ record: research/llm-pathway-bench/instar-bench-v2/review-records/input-classifier.md.
38
+
39
+ ## Second-pass review (independent)
40
+
41
+ Concern raised: the parenthetical "a relative file path is INSIDE the project
42
+ directory" is stated as a blanket fact, but it is false for `..`-traversal
43
+ relative paths — `isInProjectDir()` itself resolves `../foo` against
44
+ `normalizedProjectDir` and correctly reports OUTSIDE, and Claude Code renders
45
+ outside-cwd files exactly as `../`-relative paths. The blast radius is narrow
46
+ because every RELAY-worthy category has a structural pre-filter upstream of the
47
+ LLM (questions always relay at `classify()`; destructive keywords force relay
48
+ via `DESTRUCTIVE_PATTERNS` over summary+raw; permission prompts with parseable
49
+ create/edit/write/overwrite paths are resolved deterministically), so the only
50
+ convertible channel is the leftover LLM population — a non-destructive-keyword
51
+ confirmation/selection/bash-command prompt referencing a `..`-relative
52
+ outside-project path (e.g. `cp state.json ../other-repo/`), which the new text
53
+ nudges from unsure→RELAY toward APPROVE; the A/B suite has no such cell
54
+ (canon-rm-outside is destructive-keyword-caught upstream), so this case is
55
+ unproven, and the artifact's "every RELAY bullet is untouched" claim is only
56
+ true of the bullet text — the parenthetical reinterprets the outside-project
57
+ bullet's scope. Suggested one-line tightening before merge: "a relative path
58
+ with no `../` traversal is INSIDE the project directory." Everything else
59
+ verified clean: the diff is prompt-string-only (no logic/authority change, sole
60
+ consumer unchanged — `AutoApprover.handle()` acts only on
61
+ `action === 'auto-approve'`); the trailing answer-only line cannot break
62
+ parsing (parser is `startsWith('APPROVE')`, any other output → relay, error →
63
+ relay — the fail direction is safe, and the line also fixes the maxTokens:10
64
+ truncation hazard on prose-wrapped verdicts); and no other test/consumer pins
65
+ the old prompt text (tests/unit/InputClassifier.test.ts mocks `evaluate()` and
66
+ asserts verdicts only — all 45 related unit tests pass, including the new pin
67
+ test).
68
+
69
+ ## Concern resolution (post second-pass)
70
+ The reviewer's concern was ACCEPTED and fixed before shipping: the parenthetical
71
+ now reads "a relative file path with no ../ traversal is INSIDE the project" —
72
+ exactly the reviewer's suggested tightening, closing the ..-traversal
73
+ approve-nudge channel. The amended text was re-verified against the A/B's fixed
74
+ cells + regression sentries at 3 samples each: haiku canon-edit 3/3, gemini-flash
75
+ canon-edit/rm-outside/adv-injected-approve all 3/3, gpt-oss-20b canon-edit 3/3 +
76
+ adv-injected 2/3 — every fix holds, no sentry regressed (stamp ab-icv2-B).
@@ -0,0 +1,87 @@
1
+ # Side-Effects Review — U4.2 Stale-Owner Release + U4.4 Lease Hand-Back (one shared PR, same lease seam)
2
+
3
+ **Specs:** docs/specs/u4-2-stale-owner-release.md + docs/specs/u4-4-lease-handback.md (both converged + approved — operator preapproval, topic 29836, 2026-07-02; ELI16s + convergence reports committed alongside).
4
+ **Parents:** Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions (U4.2); The User Experience Is the Product (U4.4).
5
+ **Rollout posture:** U4.2 rides the developmentAgent dark-gate in dryRun (`multiMachine.sessionPool.staleOwnerRelease` — `enabled` OMITTED, registered in DEV_GATED_FEATURES; dev-live-in-dryRun, dark fleet). U4.4 ships **HARD-DARK everywhere** (`multiMachine.leaseSelfHeal.preferredCaptainHandback` — `enabled:false` + `dryRun:true`, DARK_GATE_EXCLUSIONS category `action-bearing`, matching its F2/F3/L3 siblings). Single-machine agents are a strict no-op for both.
6
+ **Files:** src/core/StaleOwnerReleaseEngine.ts (new), src/core/TopicClaimAnnotationStore.ts (new), src/core/LeaseHandbackReconciler.ts (new), src/core/handbackLatch.ts (new), src/core/ropeHealth.ts (new), src/core/OwnershipReconciler.ts, src/core/FencedLease.ts, src/core/LeaseCoordinator.ts, src/core/MeshRpc.ts, src/core/MultiMachineCoordinator.ts, src/core/CoherenceJournal.ts, src/core/types.ts, src/core/devGatedFeatures.ts, src/config/ConfigDefaults.ts, src/monitoring/StrandedTopicSentinel.ts, src/monitoring/strandedTopicDecision.ts, src/monitoring/guardManifest.ts, src/server/AgentServer.ts, src/server/routes.ts, src/commands/server.ts, src/scaffold/templates.ts, src/core/PostUpdateMigrator.ts
7
+
8
+ ## What changed
9
+
10
+ ### U4.2 — stale-owner release (the CMT-1786 auto-failover, built as Case C's evidence upgrade)
11
+
12
+ 1. **StaleOwnerReleaseEngine.ts (new):** the §2.2 evidence bar — machine death on the OBSERVER's monotonic clock (the FencedLease-F2 fold-in pattern; never the owner's self-reported wall clock), the R-r2-2 bounded bootstrap rule (never-observed-since-boot → ambiguity → bounded expiry off the durable coarse-heartbeat tie-breaker; a claimant restart can never silently disable auto-failover), all-transport disproof over an owner-authenticated, fresh, multi-transport advert set (single-rope = automatic ambiguity), quorum, claimant self-connectivity proof (2-machine git-less mesh: claim path DISABLED), and side-effect recency over a PROVABLY FRESH replicated mirror (a stale mirror classifies AMBIGUITY). Every ambiguity fails CLOSED. Bounded blast radius: `maxClaimsPerTick`, per-topic replicated claim budget + widening backoff, probe single-flight + memo + breaker, LOUD P19 give-ups (one deduped attention item). Honesty: every stale-detect / probe verdict / would-claim / claim / REFUSAL lands in `logs/stale-owner-release.jsonl`, state-change-gated per episode; ambiguity past 3×TTL escalates ONE per-episode deduped operator item. Tier 0 by explicit §2.8 argument — LLM-free, asserted by test.
13
+ 2. **TopicClaimAnnotationStore.ts (new):** the `topic-claim-annotation` replicated kind (R-r3-1/R-r3-2) — claim suspension + per-topic budget/backoff + the operator's declined-demote, keyed `${topic}:${episodeId}`, epoch-INDEPENDENT (its own HLC via the generic envelope, exactly like `topic-pin-record`; an annotation write can never bump `ownershipEpoch` or fence a live owner). A new registered KIND is additive — pre-U4.2 peers never register it, so there is no unknown-FIELD surface on the placement stream. Registered in CoherenceJournal's JOURNAL_KINDS (static half) + the replicatedKindRegistry at boot (dynamic half, registration INERT; emission gated behind the feature).
14
+ 3. **OwnershipReconciler.ts:** the engine rides `tick()` (one actor, §2.7.6) and gates INDEPENDENTLY of ws13Reconcile; an ACTIVE engine SUPERSEDES the legacy pin-based Case C force-claim (`deferred-no-evidence`); claims funnel through the new `actStaleOwnerForceClaim` stamping the EXTENDED nonce grammar `${self}:stale-owner-release:${sessionKey}:${episodeId}:${now}` (§2.7.5) + the `stale-owner-release` placement emission. `effectivePins()` consults the merged claim-suspension annotations — a pin whose HLC is not strictly newer than the live suspension is `suspended-pending-owner-return` (derived; the pin record is never written), and a fresher operator re-pin WINS. **Fixed during this build:** the suspension read now also applies on the no-advisory-pins early-return path (a local-pins-only mesh would otherwise have fought the claim). Named code fix: LOCAL pins gain the online-gate advisory pins already had (`deferred-target-offline` — never start a transfer toward an offline machine).
15
+ 4. **server.ts (R-r2-5a — the named prerequisite rewire):** Case C's staleness feed switched from `Date.parse(c.selfReportedLastSeen)` to the router/observer-stamped `c.routerReceivedAt` for BOTH the reconciler and the engine (MachinePoolRegistry's §L2 rule: a fast-clocked machine must not appear fresher than it is). Evidence seams wired in the lease-transport block: advert sets from PeerEndpointRecorder provenance ONLY (the git registry's `lastKnownUrl` is deliberately not consulted for disproof), authenticated probes via the identity-verified lease pull, self-proof via a third-peer probe (or durable-authority read in the 2-machine case), evidence-5 mirror freshness stamped on every SUCCESSFUL journal-sync exchange. §2.3 TTL-ordering invariant (`deathEvidenceMs > selfFenceTtlMs + tick + skew`) validated at startup — a violating combination REJECTS boot loudly.
16
+ 5. **StrandedTopicSentinel / strandedTopicDecision (R-r2-1):** the not-lease-holder early no-op is relaxed for DETECTION + ESCALATION only, when the stale-owner feature is active AND this machine observes a majority partition — so an exhausted churn breaker with a dead preferred captain (a no-lease-holder mesh) still reaches the operator. The CLAIM stays lease-holder-only. Default off = today byte-for-byte.
17
+ 5b. **SessionReaper closeout pin-veto yields to a live claim suspension (§2.3 returning-owner teardown):** the WS1.3 pin-conflict veto (`topicPinnedHere`) previously read the RAW local pin — after a U4.2 claim of a PINNED topic, the returned owner's closeout would be vetoed forever by a pin the reconciler itself no longer honors (effectivePins excludes it via the suspension), leaving the claimed topic's session lingering on the returned owner. The wiring now consults the SAME merged suspension view + the shared `claimSuspensionExcludesPin` comparison authority (exported from TopicClaimAnnotationStore; OwnershipReconciler.applyClaimSuspensions refactored onto it — no forked comparison). Feature dark ⇒ the suspension view is empty ⇒ byte-identical veto behavior.
18
+ 6. **`GET /pool/placement` gains `ownershipLeaseState` (§2.9 derivation table):** `held | stale | releasing | claimed`, DERIVED from record status + the engine's open-episode state + the §2.7.5 nonce grammar. (v1 proxy: "live suspension annotation present" is derived from the stale-owner-release nonce on the active record — the nonce persists exactly until the next ownership transition, which is when a cleared suspension would re-CAS anyway; the merged-annotation read stays server-internal.)
19
+ 7. **`GET /pool/stale-owner-release` (§2.9, R-r2-6):** the FD-7-style telemetry the §5 graduation soak is judged on — counters (attempts / claims / would-claims / refusals BY REASON / ambiguities / escalations / P19 give-ups), evidence-class distribution, probe-breaker state, last + open episodes. 503 when dark.
20
+
21
+ ### U4.4 — lease hand-back to the preferred captain
22
+
23
+ 7. **LeaseHandbackReconciler.ts (new):** the missing reconciler for F4's `preferredAwakeMachineId` (the ONE authority — no new field, nothing replicated, nothing in the signed lease record). Holder-only observation rides the existing ~5s lease pull tick (`MultiMachineCoordinator.onLeasePullTick`, throw-guarded). Hysteresis (10m continuous preferred health; ANY unhealthy observation resets), health = heartbeat-fresh AND rope-reachable (the U4.3 snapshot seam — ABSENT reading ⇒ NOT healthy ⇒ defer, R-r2-7) AND lease-eligible AND quota-OK. Clean boundary (no in-flight forwards / no queued inbound / ~90s ingress quiet) with a bounded 2h deferral ceiling → ONE deduped notice + relaxed boundary that DRAINS queued inbound before step-down (R-r2-6 — never abandoned). Suppressed by the operator latch, a LATCHED churn breaker, and split-brain. Episode cap counts OFFERS as well as hand-backs (R-r2-3) → sticky + ONE item; per-target offer backoff widens on decline/timeout; a legacy peer stops re-offers for the episode.
24
+ 8. **FencedLease.ts (R-r2-1):** `HandbackConsentToken` — SIGNED (holder machine key, discriminator-prefixed canonical form so a token can never verify as a lease), epoch-bound, TTL-bounded, single-use. `canAcquire` gains the `handbackOpts` consent branch: grant ONLY when the token verifies against the HOLDER's registered key, names THIS machine, matches the live lease's holder+epoch, is unexpired and unused; ABSENT/invalid/expired/replayed/reused ⇒ the legacy `held-by-live-peer` refusal byte-for-byte.
25
+ 9. **LeaseCoordinator.ts:** `mintHandbackConsent` (holder-only — a non-holder mints null) + `acquireOnHandbackConsent` (claim-before-release: the fenced CAS at epoch+1; the nonce burns on FIRST presentation success-or-not; a failed/lost CAS changes nothing — zero-holder states impossible; FAIL-CLOSED grant discipline: a coincidentally-claimable lease never launders a bad token).
26
+ 10. **MeshRpc.ts (R-r2-2):** the `handback-offer` MeshCommand — RBAC its own default-deny case (only the CURRENT lease holder may send; refusal reason `handback-offer-unauthorized`, 403). server.ts handler: `decideHandbackOffer` typed declines (`accept` / `declined:churn-latched` / `declined:quota` / `declined:legacy-peer`), receiving-side single-use token set, async consent claim + post-hand-back delivery canary (one authenticated mesh round-trip; failure raises ONE loud attention item). Version skew: 403/`no-handler` classifies `declined:legacy-peer` → the sender goes sticky for the episode.
27
+ 11. **handbackLatch.ts (new, R-r2-5):** the machine-local operator-flip latch marker — WRITTEN BY the explicit flip action (never inferred from a transfer's origin). Routes: `GET /pool/lease-handback` (status + latch visibility — answers honestly even while hard-dark), `POST /pool/lease-handback/latch` (Bearer — the playbook's POST step; suppressing automation is the safe direction), `DELETE /pool/lease-handback/latch` (**PIN-gated** via checkMandatePin — clearing re-enables automation against a human decision; the agent's Bearer token is structurally insufficient).
28
+ 12. **ropeHealth.ts (new):** the U4.3 rope-health snapshot SEAM — a provider registry that returns `undefined` (⇒ defer) until U4.3 lands its real provider; tests/E2E register synthetic providers through the same function.
29
+ 13. **§5 enable chokepoint:** `validateHandbackEnableChokepoint` at boot — `dryRun:false` with pollFollowsLease still dry-run REFUSES startup loudly (the lease/ingress split).
30
+
31
+ ### Shared
32
+
33
+ 14. **Config/gating:** ConfigDefaults blocks for both (U4.2 omits `enabled`; U4.4 explicit `enabled:false`+`dryRun:true`); DEV_GATED_FEATURES entry (staleOwnerRelease) + DARK_GATE_EXCLUSIONS entry (preferredCaptainHandback, action-bearing); guardManifest entries for BOTH with `loadBearing:true` + criticalPath + soakWindowDays + declaredLoadBearingAt (G3 — a stalled dark posture classifies loudly per #1318).
34
+ 15. **Agent awareness + migration parity:** `MESH_SELF_HEALING_CLAUDEMD_SECTION` in generateClaudeMd (new agents) + migrateClaudeMd (existing agents, content-sniffed) + a migrateFrameworkShadowCapabilities marker (Codex/Gemini parity). Config defaults ride the generic `applyDefaults` migration path (missing keys added, never overwritten). Site docs: `features/multi-machine.md` mesh-self-healing section + `reference/api.md` /pool inventory.
35
+
36
+ ## Blast radius
37
+
38
+ - **Everything authority-bearing is dark or dry-run.** U4.2 on a dev agent runs the evidence pass + trace + status route LIVE but `dryRun:true` logs would-claims and NEVER lands a CAS; the fleet resolves dark (no engine activity, route 503s). U4.4 is `enabled:false` everywhere — the reconciler is constructed but `observe()` returns at the first gate; the offer handler's receiving side declines (`declined:legacy-peer`) unless enabled AND dryRun:false locally.
39
+ - **Fail-closed on every uncertainty.** Every evidence ambiguity refuses the claim; a mint refusal / offer timeout / lost CAS leaves the current holder holding; an absent rope snapshot defers; an unreadable gate reads as inactive/not-holder.
40
+ - **No new ownership fence.** `ownershipEpoch` stays the only ownership fence; the annotation kind is derived state that can never answer or fence ownership; the lease epoch stays the only serving fence (the consent claim is a normal fenced CAS).
41
+ - **Version skew:** pre-U4.2 peers never register the annotation kind (additive); a pre-U4.4 peer fails the offer closed and the sender goes sticky. No new unauthenticated surface: probes ride the existing signed lease pull; the offer rides the signed MeshRpc envelope + RBAC + the consent token.
42
+
43
+ ## Risk + mitigation
44
+
45
+ - **Risk: false-death claim against a live owner (split-brain).** Mitigation: the five-evidence fail-closed bar with provenance-pinned inputs (observer-stamped staleness, owner-authenticated advert sets), the §2.3 TTL-ordering invariant enforced at boot, dry-run graduation gated on the §5 quantified soak + emission-fence prerequisite. Proven by the evidence-bar unit lenses (reachable-on-one-transport, single-rope ambiguity, stale mirror, self-proof fail, quorum fail).
46
+ - **Risk: claim/transfer-back oscillation between the claim and the topic's pin.** Mitigation: the claim SUSPENDS the pin via the replicated annotation (HLC-ordered; operator re-pin wins) — proven by the suspension lenses including the early-return-path fix found during this build.
47
+ - **Risk: zero-holder after a hand-back.** Mitigation: claim-before-release via the consent token; the holder steps down only on OBSERVING the higher epoch; every failure path keeps the holder holding — proven at the FencedLease, LeaseCoordinator, and reconciler levels.
48
+ - **Risk: automation fights the human's captain flip.** Mitigation: the R-r2-5 latch (written by the flip action, PIN-gated clear) + churn-breaker composition + the episode cap counting offers.
49
+ - **Risk: hand-back strands queued inbound at the relaxed boundary.** Mitigation: R-r2-6 — drain-before-step-down, proven by the deferral-ceiling lens.
50
+
51
+ ## Migration parity
52
+
53
+ - Config: both new blocks ride `getMigrationDefaults`/`applyDefaults` (existence-checked add-missing) — no dedicated migrateConfig needed; U4.2 deliberately OMITS `enabled` so the dev-gate resolves live-on-dev/dark-fleet (no stale-literal risk to strip).
54
+ - CLAUDE.md: `migrateClaudeMd` appends the Mesh Self-Healing section (content-sniffed, idempotent); `migrateFrameworkShadowCapabilities` mirrors it to AGENTS.md/GEMINI.md.
55
+ - Tracked in feature-delivery-completeness (legacyMigratorSections — constant-emitted class) + the shadow-marker list.
56
+
57
+ ## Dark-gate line-map
58
+
59
+ - UPDATED. U4.4's `preferredCaptainHandback.enabled: false` is a NEW attributed line (873) and its ~16-line block shifts every key below the leaseSelfHeal block; U4.2's sessionPool block adds NO attributed path (omitted `enabled`) but shifts keys below it by +19. `tests/unit/lint-dev-agent-dark-gate.test.ts` golden map re-verified via the attributor (24/24 green); `node scripts/lint-dev-agent-dark-gate.js` clean.
60
+
61
+ ## Rollback
62
+
63
+ - U4.2: drop the flag → ownership reverts to explicit-transfer-only + sentinel detection (today); lingering `topic-claim-annotation` records are inert derived state; a suspension clears on the next operator re-pin.
64
+ - U4.4: `enabled:false` (the shipped default) / unset F4 preference → sticky lease, today's behavior; the latch marker is inert data.
65
+
66
+ ## Tests
67
+
68
+ - `tests/unit/StaleOwnerReleaseEngine.test.ts` (28) — the full §2.2 evidence bar both-sides, the R-r2-2 bootstrap rule, arbiter uniqueness, caps/budgets/backoff/declined-demote (replicated), P19 give-ups + probe breaker, one-item-per-episode escalation, dry-run once-per-topic would-claims, the §2.3 validator, the §2.9 ownershipLeaseState table, Tier-0 LLM-free source assertion.
69
+ - `tests/unit/TopicClaimAnnotationStore.test.ts` (19) — strict schema clamps both-sides, additive registration (dual-registry), epoch-independence (no epoch field on the PUT), recordKey jail, HLC-highest-wins merge incl. `declined-demote-and-budget-survive-lease-move`, tombstones.
70
+ - `tests/unit/LeaseHandbackReconciler.test.ts` (20) — hysteresis arm/reset, absent-snapshot defer (R-r2-7 through the REAL ropeHealth seam), each clean-boundary signal, deferral ceiling → ONE notice + drain-before-step-down, claim-before-release failure matrix, legacy-peer episode stop, latch/churn/split-brain suppression, episode cap counting offers, typed offer decision, the §5 enable chokepoint, the latch file lifecycle.
71
+ - `tests/unit/FencedLease-handback-consent.test.ts` (14) — real-Ed25519 token mint/verify/forge, the consent branch fail-closed on every axis (absent/expired/replayed/reused/wrong-target/wrong-epoch/wrong-holder/forged/malformed), `handback-offer` RBAC default-deny.
72
+ - `tests/unit/LeaseCoordinator-handbackConsent.test.ts` (5) — mint holder-only, consent claim at epoch+1 over the shared CAS store, single-use burn, `failed-handback-never-leaves-zero-holders`, non-consent-grant laundering refusal.
73
+ - `tests/unit/OwnershipReconciler-staleOwnerRelease.test.ts` (8) — engine supersedes legacy Case C, independent gating, extended nonce grammar + placement pairing, suspension lenses (`operator-repin-clears-suspension`), `deferred-target-offline`, the `case-c-staleness-input-is-observer-stamped` wiring ratchet over server.ts.
74
+ - `tests/unit/stranded-topic-quorum-hosted-escalation.test.ts` (5) — `no-lease-holder-escalation-hosted-on-any-quorum-member` both sides + quorum-gated fail-closed + single-machine no-op.
75
+ - `tests/integration/stale-owner-release-and-handback-routes.test.ts` (11) — the real createRoutes + authMiddleware: 503-when-dark/absent, the §2.9 counters advancing across a simulated episode, latch POST (Bearer) + PIN-gated DELETE (403 without/with wrong PIN, latch survives), and the /pool/placement ownershipLeaseState derivation (held / stale / claimed).
76
+ - `tests/e2e/stale-owner-release-handback-alive.test.ts` (5) — feature-alive through the REAL AgentServer: stale-owner route 200-not-503 with a live dry-run episode; hand-back reconciler ticking the synthetic preferred-unhealthy→healthy transition (dry-run counters advance); latch levers end-to-end; dark → 503; 401 unauthenticated.
77
+ - Spec-named provenance/teardown lenses (resume completion pass): `working-set-pull-queued-and-resume-proceeds` (a throwing pull queues durably; claim + annotation + disclosure proceed), `forged-advert-set-from-non-owner-rejected` (PeerEndpointRecorder per-entry authenticated binding + the server.ts advert-seam ratchet: getMachineEndpoints only, never lastKnownUrl), `forged-heartbeat-from-non-owner-rejected` (PeerPresencePuller records under the DIALED identity; a body-smuggled foreign machineId never refreshes the foreign machine), `stale-owner-return-tears-down` (the closeout veto-yield ratchets + `claimSuspensionExcludesPin` both-sides unit lenses).
78
+ - Ratchets: dark-gate golden map re-verified; no-silent-fallbacks (every new catch tagged with a named fail-direction justification); feature-delivery parity + shadow markers; docs-coverage route floor restored (new /pool inventory + the multi-machine feature doc); tsc clean.
79
+
80
+ ## Deviations from spec (named, with reasons)
81
+
82
+ - **U4.2 §2.3 emission-fence wiring** (FencedOutbox at the Telegram relay chokepoint) is a declared GRADUATION prerequisite, not part of this dry-run-shipping PR — the spec itself sequences it before `dryRun:false`, and dry-run lands no claims to fence against.
83
+ - **Increment E (reply-committed watermark replication)** ships dark as specced (at-least-once stated honestly in v1); not implemented here.
84
+ - **U4.4 Feature-metrics §4** rides the existing `getFeatureMetricsRecorder` seam (`lease-handback` feature key) with per-event verdictIds rather than a bespoke metrics pipeline — same funnel every other feature uses; in-memory counters mirror every event on the status surface.
85
+ - **The PIN-gated captain-flip route** (the productized flip lever) is not built here; per spec R-r2-5 the latch POST step is the interim playbook path, and the latch write route ships now so the playbook can POST it.
86
+
87
+ <!-- tracked: CMT-1786 -->