@kontourai/flow-agents 2.2.0 → 2.4.0

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 (250) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/AGENTS.md +4 -4
  4. package/CHANGELOG.md +46 -0
  5. package/CONTEXT.md +14 -0
  6. package/README.md +9 -3
  7. package/agents/dev.json +1 -1
  8. package/agents/tool-code-reviewer.json +1 -1
  9. package/agents/tool-planner.json +3 -3
  10. package/agents/tool-verifier.json +3 -3
  11. package/build/src/cli/console-learning-projection.js +3 -2
  12. package/build/src/cli/init.js +104 -21
  13. package/build/src/cli/kit.js +37 -6
  14. package/build/src/cli/pull-work-provider.js +1 -1
  15. package/build/src/cli/usage-feedback.js +3 -3
  16. package/build/src/cli/validate-hook-influence.js +1 -0
  17. package/build/src/cli/validate-workflow-artifacts.js +22 -6
  18. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  19. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  20. package/build/src/cli/workflow-sidecar.js +792 -69
  21. package/build/src/flow-kit/validate.d.ts +23 -0
  22. package/build/src/flow-kit/validate.js +67 -1
  23. package/build/src/index.d.ts +1 -1
  24. package/build/src/index.js +1 -1
  25. package/build/src/lib/flow-resolver.d.ts +6 -4
  26. package/build/src/lib/flow-resolver.js +125 -58
  27. package/build/src/lib/local-artifact-root.d.ts +14 -2
  28. package/build/src/lib/local-artifact-root.js +22 -5
  29. package/build/src/lib/workflow-learning-projection.js +2 -2
  30. package/build/src/runtime-adapters.d.ts +12 -0
  31. package/build/src/runtime-adapters.js +56 -16
  32. package/build/src/tools/build-universal-bundles.js +49 -16
  33. package/build/src/tools/generate-context-map.js +10 -8
  34. package/build/src/tools/validate-source-tree.js +23 -1
  35. package/console.telemetry.json +22 -0
  36. package/context/contracts/artifact-contract.md +38 -7
  37. package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
  38. package/context/contracts/decision-registry-contract.md +263 -0
  39. package/context/contracts/delivery-contract.md +1 -1
  40. package/context/contracts/planning-contract.md +2 -1
  41. package/context/contracts/review-contract.md +5 -3
  42. package/context/contracts/verification-contract.md +9 -1
  43. package/context/contracts/work-item-contract.md +1 -1
  44. package/context/deferred/parallelization.md +1 -1
  45. package/context/gate-awareness.md +4 -4
  46. package/context/scripts/git-status.sh +2 -2
  47. package/context/scripts/hooks/config-protection.js +502 -2
  48. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  49. package/context/scripts/hooks/run-hook.js +14 -1
  50. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  51. package/context/scripts/hooks/workflow-steering.js +212 -6
  52. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  54. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  55. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  56. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  57. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
  58. package/docs/agent-system-guidebook.md +3 -3
  59. package/docs/agent-usage-feedback-loop.md +6 -6
  60. package/docs/context-map.md +9 -7
  61. package/docs/decisions/decision-records.md +57 -0
  62. package/docs/decisions/index.md +14 -0
  63. package/docs/decisions/promotion-gate.md +52 -0
  64. package/docs/developer-architecture.md +1 -1
  65. package/docs/fixture-ownership.md +3 -0
  66. package/docs/flow-kit-repository-contract.md +5 -5
  67. package/docs/getting-started.md +12 -2
  68. package/docs/integrations/harness-install.md +2 -1
  69. package/docs/kit-authoring-guide.md +64 -5
  70. package/docs/knowledge-kit.md +5 -1
  71. package/docs/learnings/2026-07-improvement-program.md +203 -0
  72. package/docs/migrations.md +6 -5
  73. package/docs/north-star.md +2 -2
  74. package/docs/operating-layers.md +4 -3
  75. package/docs/repository-structure.md +5 -5
  76. package/docs/skills-map.md +8 -8
  77. package/docs/spec/runtime-hook-surface.md +19 -15
  78. package/docs/spikes/graph-provider-2026-07.md +155 -0
  79. package/docs/standards-register.md +7 -7
  80. package/docs/trust-anchor-adoption.md +105 -4
  81. package/docs/work-item-adapters.md +1 -1
  82. package/docs/workflow-artifact-lifecycle.md +72 -9
  83. package/docs/workflow-eval-strategy.md +3 -3
  84. package/docs/workflow-shared-contracts.md +1 -1
  85. package/docs/workflow-usage-guide.md +254 -21
  86. package/evals/acceptance/DEMO-false-completion.md +20 -11
  87. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  88. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  89. package/evals/acceptance/test_kiro_harness.sh +1 -1
  90. package/evals/ci/antigaming-suite.sh +5 -0
  91. package/evals/ci/run-baseline.sh +65 -1
  92. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  93. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  94. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  95. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  96. package/evals/fixtures/hook-influence/cases.json +32 -0
  97. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  98. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  99. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  100. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  101. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  102. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  103. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  104. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  105. package/evals/integration/test_actor_identity.sh +254 -0
  106. package/evals/integration/test_builder_step_producers.sh +33 -23
  107. package/evals/integration/test_bundle_install.sh +22 -16
  108. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  109. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  110. package/evals/integration/test_checkpoint_signing.sh +43 -0
  111. package/evals/integration/test_claim_lookup.sh +4 -4
  112. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  113. package/evals/integration/test_command_log_concurrency.sh +8 -8
  114. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  115. package/evals/integration/test_command_log_integrity.sh +16 -16
  116. package/evals/integration/test_console_learning_projection.sh +1 -1
  117. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  118. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  119. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  120. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  121. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  122. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  123. package/evals/integration/test_flow_kit_repository.sh +2 -0
  124. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  125. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  126. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  127. package/evals/integration/test_gate_lockdown.sh +97 -73
  128. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  129. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  130. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  131. package/evals/integration/test_goal_fit_hook.sh +32 -32
  132. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  133. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  134. package/evals/integration/test_hook_influence_cases.sh +1 -0
  135. package/evals/integration/test_install_merge.sh +262 -1
  136. package/evals/integration/test_kit_identity_trust.sh +6 -6
  137. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  138. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  139. package/evals/integration/test_liveness_verdict.sh +394 -0
  140. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  141. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  142. package/evals/integration/test_promote_gate.sh +178 -0
  143. package/evals/integration/test_publish_delivery.sh +2 -2
  144. package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
  145. package/evals/integration/test_pull_work_provider.sh +2 -2
  146. package/evals/integration/test_reconcile_soundness.sh +4 -4
  147. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  148. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  149. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  150. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  151. package/evals/integration/test_trust_reconcile.sh +2 -2
  152. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  153. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  154. package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
  155. package/evals/integration/test_usage_feedback_global.sh +5 -5
  156. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  157. package/evals/integration/test_verify_cli.sh +2 -2
  158. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  159. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  160. package/evals/integration/test_workflow_artifacts.sh +33 -0
  161. package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
  162. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  163. package/evals/lib/codex-provider.sh +1 -1
  164. package/evals/lib/node.sh +19 -1
  165. package/evals/run.sh +22 -0
  166. package/evals/static/test_decisions.sh +150 -0
  167. package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
  168. package/evals/static/test_package.sh +19 -10
  169. package/evals/static/test_universal_bundles.sh +48 -3
  170. package/evals/static/test_workflow_skills.sh +41 -8
  171. package/kits/builder/flows/build.flow.json +3 -74
  172. package/kits/builder/flows/publish-learn.flow.json +90 -0
  173. package/kits/builder/kit.json +11 -0
  174. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  175. package/kits/builder/skills/deliver/SKILL.md +13 -11
  176. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  177. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  178. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  179. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  180. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  181. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  182. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  183. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  184. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  185. package/kits/builder/skills/pull-work/SKILL.md +80 -5
  186. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  187. package/kits/builder/skills/review-work/SKILL.md +24 -1
  188. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  189. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  190. package/kits/catalog.json +6 -0
  191. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  192. package/kits/veritas-governance/docs/README.md +75 -0
  193. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  194. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  195. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  196. package/kits/veritas-governance/kit.json +16 -0
  197. package/package.json +8 -4
  198. package/packaging/README.md +2 -2
  199. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  200. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  201. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  202. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  203. package/packaging/manifest.json +9 -10
  204. package/schemas/decision-record.schema.json +109 -0
  205. package/schemas/workflow-evidence.schema.json +1 -1
  206. package/schemas/workflow-state.schema.json +5 -0
  207. package/scripts/README.md +8 -3
  208. package/scripts/check-content-boundary.cjs +1 -1
  209. package/scripts/check-decisions.cjs +356 -0
  210. package/scripts/ci/derive-claim-status.mjs +74 -0
  211. package/scripts/ci/trust-reconcile.js +436 -79
  212. package/scripts/git-status.sh +2 -2
  213. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  214. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  215. package/scripts/hooks/config-protection.js +102 -30
  216. package/scripts/hooks/evidence-capture.js +3 -3
  217. package/scripts/hooks/lib/actor-identity.js +368 -0
  218. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  219. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  220. package/scripts/hooks/lib/liveness-policy.js +127 -0
  221. package/scripts/hooks/lib/liveness-read.js +105 -12
  222. package/scripts/hooks/lib/liveness-write.js +46 -0
  223. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  224. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  225. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  226. package/scripts/hooks/stop-goal-fit.js +157 -26
  227. package/scripts/hooks/workflow-steering.js +77 -3
  228. package/scripts/install-codex-home.sh +138 -38
  229. package/scripts/statusline/flow-agents-statusline.js +2 -2
  230. package/src/cli/console-learning-projection.ts +3 -2
  231. package/src/cli/init.ts +101 -21
  232. package/src/cli/kit.ts +37 -6
  233. package/src/cli/public-api.test.mjs +58 -6
  234. package/src/cli/pull-work-provider.ts +1 -1
  235. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  236. package/src/cli/usage-feedback.ts +3 -3
  237. package/src/cli/validate-hook-influence.ts +1 -0
  238. package/src/cli/validate-workflow-artifacts.ts +22 -6
  239. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  240. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  241. package/src/cli/workflow-sidecar.ts +795 -71
  242. package/src/flow-kit/validate.ts +74 -1
  243. package/src/index.ts +6 -2
  244. package/src/lib/flow-resolver.ts +123 -55
  245. package/src/lib/local-artifact-root.ts +24 -5
  246. package/src/lib/workflow-learning-projection.ts +2 -2
  247. package/src/runtime-adapters.ts +61 -15
  248. package/src/tools/build-universal-bundles.ts +49 -16
  249. package/src/tools/generate-context-map.ts +10 -8
  250. package/src/tools/validate-source-tree.ts +23 -1
@@ -0,0 +1,290 @@
1
+ ---
2
+ title: "ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers"
3
+ ---
4
+
5
+ # ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers
6
+
7
+ Status: Accepted
8
+
9
+ Relates to: ADR 0004 (self-grading gate), ADR 0010 (trust.bundle as the primary
10
+ verification artifact), ADR 0017 (the anti-gaming trust security model + external CI
11
+ anchor). This ADR refines the CI anchor's *reconcile* step; it does not change the
12
+ layered-defense posture ADR 0017 established.
13
+
14
+ ## Context
15
+
16
+ PR #264 was this repository's first real `trust.bundle` publication through the CI trust
17
+ anchor (`scripts/ci/trust-reconcile.js`, ADR 0017). It could not pass, and the failure was
18
+ architectural, not incidental:
19
+
20
+ 1. **Single opaque composite command.** The reconciler resolved exactly one canonical
21
+ verify command — `package.json scripts["trust-reconcile-verify"]` = `npm run build &&
22
+ npm run eval:static` — and reconciled a claimed pass only when the claim's
23
+ `evidence.execution.label` was byte-identical to that composite string. Any honest,
24
+ granular, per-acceptance-criterion command (a single lane check, a narrower test, a
25
+ browser check) was, by construction, "a claimed pass CI never ran" → divergence.
26
+
27
+ 2. **Every check hardcoded as `test_output`.** `buildTrustBundle`
28
+ (`src/cli/workflow-sidecar.ts`) stamped `evidenceType: "test_output"` on *every* check
29
+ regardless of `check.kind`, and only stamped `execution.label` when the local
30
+ command-log capture happened to run it. A browser check, a manual verification, a
31
+ provider/CI check, or a diff review therefore looked identical to a `test_output`
32
+ claim CI never ran.
33
+
34
+ 3. **No waiver path.** An honestly-accepted gap had no sanctioned, visible way through the
35
+ anchor.
36
+
37
+ The net effect: no bundle carrying honest, granular, mixed evidence could pass, and the
38
+ deliver skill's own publish step was unsatisfiable for any session reporting more than one
39
+ composite command. PR #264's bundle was withdrawn.
40
+
41
+ ## Decision
42
+
43
+ Redesign the producer/reconciler contract. **Consume, never fork** — every new concept
44
+ reuses an existing canonical vocabulary rather than inventing a parallel one.
45
+
46
+ ### 1. The manifest is the existing CI check registry, not a new file
47
+
48
+ `evals/ci/run-baseline.sh` already defines a CI-verified, named, individually-re-runnable
49
+ command registry: the `CHECKS` array of `"Label|command"` pairs, partitioned into `LANE_*`
50
+ arrays that `.github/workflows/ci.yml`'s jobs invoke via `--check <slug>`. The merge-gating
51
+ lanes (`LANE_SOURCE_AND_STATIC`, `LANE_WORKFLOW_CONTRACTS`, `LANE_RUNTIME_AND_KIT`) are
52
+ *structurally* wired into a required job — the anti-gaming property ("a manifest command must
53
+ actually run in a required lane") is achieved **by construction** rather than by a new rule
54
+ that itself needs policing. The advisory `LANE_USAGE_FEEDBACK` lane runs `continue-on-error`
55
+ and does **not** gate a merge, so its checks are **excluded** from `--manifest-json`: a
56
+ `test_output` claim must never be able to reconcile against a non-gating command.
57
+ (`test_trust_reconcile_manifest.sh` asserts the emitted manifest equals the *required-lane*
58
+ `--check` set, usage-feedback excluded from both sides.)
59
+
60
+ `run-baseline.sh` gains a machine-readable `--manifest-json` emit mode (lane-covered checks
61
+ only, as `{id, command, lanes[]}`). `trust-reconcile.js` gains a manifest resolver with the
62
+ priority order:
63
+
64
+ 1. CLI `--manifest <json>`
65
+ 2. env `TRUST_RECONCILE_MANIFEST <json>`
66
+ 3. `package.json "trust-reconcile-manifest"` — an inline `{id, command}[]` array (for
67
+ adopters without a `run-baseline.sh`-shaped registry), **or** a string command that
68
+ emits that JSON. This repo declares it as `bash evals/ci/run-baseline.sh
69
+ --manifest-json`, so its live manifest *is* the run-baseline registry.
70
+ 4. `evals/ci/run-baseline.sh --manifest-json` (auto-detected — this repo's dogfood path).
71
+ 5. Legacy fallback: the resolved single fresh-verify command(s) as a manifest of size N —
72
+ so the historical single-command behavior is a strict subset (backward compat by
73
+ construction).
74
+
75
+ The standalone fresh-verify (Step 1, the anchor's own independent CI truth) is unchanged.
76
+ The manifest governs Step 2 (per-command reconcile): a `test_output` claim may reconcile
77
+ only against a manifest command, and a manifest command not already run in Step 1 is
78
+ re-run **fresh on demand** (bounded by the registry) so the reconcile is against real CI
79
+ results, never the agent's self-report.
80
+
81
+ ### 2. Claim classification reuses Surface's `evidenceType`/`method`, not a new taxonomy
82
+
83
+ `@kontourai/surface`'s `evidence.schema.json` already defines
84
+ `evidenceType: [source_excerpt, test_output, human_attestation, attestation,
85
+ calculation_trace, document_citation, crawl_observation, policy_rule]` and a `method`
86
+ enum. `buildTrustBundle` now derives `evidenceType`/`method` from `check.kind`:
87
+
88
+ | `check.kind` | `evidenceType` | `method` | reconcilable? |
89
+ | --- | --- | --- | --- |
90
+ | build, types, lint, test, command | `test_output` | `validation` | yes |
91
+ | security (command-backed) | `test_output` | `validation` | yes |
92
+ | security (no command) | `attestation` | `corroboration` | no |
93
+ | browser (command-backed) | `test_output` | `validation` | yes |
94
+ | browser (no command) | `crawl_observation` | `observation` | no |
95
+ | runtime (command-backed) | `test_output` | `validation` | yes |
96
+ | runtime (no command) | `attestation` | `attestation` | no |
97
+ | diff | `source_excerpt` | `extraction` | no |
98
+ | policy | `policy_rule` | `auditability` | no |
99
+ | external | `attestation` | `corroboration` | no |
100
+
101
+ A command-backed check now **always** carries `execution.label` (even when the local
102
+ command-log capture did not run it) so the reconciler has a stable match key. A claim is
103
+ **reconcilable** iff its evidence is `evidenceType: "test_output"` **and** its command
104
+ matches a manifest entry; everything else is **session-local by construction** — no new
105
+ boolean, no new enum. A `test_output` claim whose command is *not* in the manifest is
106
+ still a divergence (an agent cannot self-label an arbitrary command `test_output` to dodge
107
+ the manifest). The existing exit-code-laundering check runs *before* classification and is
108
+ unchanged.
109
+
110
+ **Iteration-2 hardening (soundness fixes found in review).**
111
+
112
+ - **A `test_output` claim reconciles or it is a divergence — never session-local (finding 1).**
113
+ The not-run guard triggers for ANY pass-asserting claim whose evidence is
114
+ `evidenceType: test_output` (Surface's default when unset) that lacks a manifest-matched
115
+ `execution.label` — not only the literal claimType `workflow.check.command`. A fabricated
116
+ `kind:"test"` claim with no command still emits `test_output` evidence with no label; it is
117
+ now a `not-run` divergence instead of silently passing as session-local.
118
+ - **The reconciler re-derives status CI-side and never trusts `claim.status` (finding 3).**
119
+ For every session-local claim, `trust-reconcile.js` re-runs Surface's canonical
120
+ `deriveClaimStatus` over the bundle's OWN evidence/events/policies (via
121
+ `scripts/ci/derive-claim-status.mjs`, resolving Surface from the anchor's own node_modules)
122
+ and compares the result to the asserted status. A mismatch is a `status-misassertion`
123
+ divergence; if re-derivation is unavailable the claim fails closed (`status-underivable`).
124
+ The bundle's self-reported `status` field is never the basis for a pass.
125
+ - **Policy construction is order-independent (AC1 cache defect).** `ensurePolicy` keys its
126
+ cache by `(claimType, requiredEvidence)` rather than by `claimType` alone, so two checks of
127
+ the same legacy claimType that differ in command-presence (a command-backed browser check →
128
+ `test_output` vs a no-command browser check → `crawl_observation`) get distinct policies
129
+ instead of the first-seen `requiredEvidence` silently corrupting the second claim's derived
130
+ status. Merging is deliberately NOT used — Surface's `requiredEvidence` is all-of, so a union
131
+ would over-constrain both claims. `trust-bundle-policy-order.test.mjs` proves two same-kind
132
+ checks produce identical bundles in both record orders.
133
+
134
+ ### 3. Waivers reuse the existing `accepted_gap` status
135
+
136
+ `record-evidence`/`record-gate-claim` gain `--accepted-gap-reason <text>` and `--waived-by
137
+ <actor>` (both required together — an accepted gap with no justification or no approver is
138
+ refused; no silent/default waiver). When present, the recorded claim is classified
139
+ session-local (`attestation`), its status is the existing `accepted_gap → assumed` mapping,
140
+ and `claim.metadata.waiver = {reason, approved_by, approved_at}` is stamped
141
+ (`claim.metadata` is free-form per `claim.schema.json` — no schema fork). The reconciler
142
+ prints every waived claim on a distinct, un-suppressible `[trust-reconcile] WAIVED: ...`
143
+ line so it is visible in the required job's own log — reviewed, justified, never silent.
144
+
145
+ A session-local claim that is **not** waived must resolve a real, CI-RE-DERIVED `verified`
146
+ status to pass; a `disputed`/`rejected` claim still blocks. **`assumed` alone is no longer a
147
+ silent pass (finding 2):** because the `skip → assumed` mapping means an unjustified skip
148
+ resolves `assumed`, an `assumed` claim is accepted ONLY when it also carries a waiver (printed
149
+ on the loud `WAIVED` line). An unwaived `assumed` claim is an `unwaived-assumed` divergence —
150
+ restoring the pre-WS8 semantics where `assumed` alone never satisfied a claimed pass. An
151
+ unwaived session-local claim asserting pass without a re-derived `verified` status is an
152
+ `unwaived-session-local` divergence. Session-local classification is **not** a pass bypass.
153
+
154
+ **A command-backed check cannot be waived (finding 4).** A waiver documents an *accepted gap*
155
+ in something that was not re-runnably verified; a `test_output` (command-backed) check
156
+ reconciles against CI or fails, so waiving it would let an agent skip the real run. Both the
157
+ producer (`record-evidence`/`record-gate-claim` reject `--accepted-gap-reason`/`--waived-by`
158
+ on any check whose evidence classifies as `test_output`) and the reconciler (a waiver on a
159
+ claim backed by `test_output` evidence is a `waiver-on-command-check` divergence) enforce this.
160
+
161
+ **Authority-binding residual (honest).** `approved_by` is currently free text — the reconciler
162
+ verifies a waiver is *present, justified, and named*, and prints it loudly for human review,
163
+ but it does NOT yet cryptographically bind the approver to an authenticated identity. Tying
164
+ `approved_by` to an Assurance-profile identity (Surface's authority/identity model), so a
165
+ waiver's approver is verifiable rather than self-asserted, is deferred to that profile's
166
+ adoption and tracked as follow-up; until then a waiver's accountability rests on the required
167
+ job's visible `WAIVED` line and CODEOWNERS review, not on identity attestation. (See Residuals
168
+ below — this is the same authenticated-identity gap that also underlies the fabricated-
169
+ attestation residual.)
170
+
171
+ ### 4. Iteration-4 hardening: loud disclosure of non-command-backed passes (converged finding)
172
+
173
+ Both iteration-3 gates (the review critique and an independently-corroborating adversarial
174
+ re-verification) converged on one finding against the redesign above: a session-local claim
175
+ that re-derives `verified` with **no waiver** — i.e. every `human_attestation` / `attestation`
176
+ / `external`-evidenced claim for a no-`command` check (§2's table: `security`/`browser`/
177
+ `runtime`/`external` without a command, `diff`, `policy`) — was printed as a quiet
178
+ `SESSION-LOCAL OK` line, identical in weight and format to a genuinely CI-reconciled
179
+ `RECONCILED` line. `deriveClaimStatus` (finding 3, §"Iteration-2 hardening") re-derives status
180
+ from the bundle's OWN evidence/events/policies — it proves the bundle is **internally
181
+ self-consistent**, never that the underlying real-world attestation is **true**. A fully
182
+ hand-fabricated claim+evidence+event triple (a single `record-evidence --check-json
183
+ '{"kind":"security","status":"pass",...}'` call with no `command` field, run through the real
184
+ producer CLI) is, at this re-derivation layer, indistinguishable from a genuine one — and
185
+ because pre-WS8 every check was hardcoded `test_output` (§ Context, point 2), this specific
186
+ silent-pass path did not exist before this redesign; WS8 newly opened it for exactly the
187
+ high-impact, no-command kinds this ADR added first-class support for.
188
+
189
+ The fix is **disclosure, not a new block**: blocking every non-command-backed claim at this
190
+ layer would break every honest human-attestation/external-check use the manifest+
191
+ classification redesign exists to support (§1–§2's whole point). Instead, `trust-reconcile.js`
192
+ now prints every such claim on a loud, distinct, `grep`-stable line —
193
+
194
+ ```
195
+ [trust-reconcile] ATTESTED (not independently verifiable at L0): '<claimId>' (<claimType>) evidenceType=<type> — accepted on bundle-internal consistency only; see ADR 0020 Residuals
196
+ ```
197
+
198
+ — and a summary count after the reconcile step (`N attested claim(s) accepted without
199
+ independent verification`), always emitted (including `0`) so its absence is never mistaken
200
+ for "count line not implemented." Exit codes are unchanged — this is visibility, not a new
201
+ gate. `evals/integration/test_trust_reconcile_negatives.sh` freezes a fabricated-attestation
202
+ fixture asserting BOTH the pass (exit 0) AND the loud marker + summary line, and
203
+ `test_trust_reconcile_mixed_bundle.sh`'s shipped honest bundle asserts its own `ATTESTED` line
204
+ so the honest path is proven, not just the exploit.
205
+
206
+ ## Residuals (honest — ADR 0017 §Residuals style; a bar-raiser, not a wall)
207
+
208
+ - **Fabricated self-consistent attestation bundles pass, loudly marked (inherent to L0).**
209
+ "L0" here means this reconciler's baseline reconcile/re-derivation layer — distinct from
210
+ ADR 0017's Layer 1–4 defense-in-depth naming. At L0, a non-command-backed claim
211
+ (`human_attestation`/`attestation`/`external`/etc.) can only be checked for *internal*
212
+ self-consistency (does the bundle's own evidence/event agree with the asserted status);
213
+ L0 has no independent oracle for whether the underlying real-world review/attestation
214
+ actually happened. A hand-fabricated bundle that is internally consistent therefore passes
215
+ — by design, since L0 cannot tell it apart from a genuine one. **The remedy is a higher
216
+ Assurance profile, not a stronger L0 heuristic:** an L1/L2 profile in which
217
+ non-command-backed evidence is cryptographically signed by an authenticated identity at
218
+ capture time (extending Layer 3's keyless-Sigstore minting, ADR 0017, from CI's own fresh
219
+ results to individual human-attestation events) would let the reconciler verify the
220
+ attestation's *provenance*, not merely the bundle's internal arithmetic. That profile does
221
+ not exist yet; until it does, every `ATTESTED` line is this ADR's explicit, permanent
222
+ disclosure that the claim rests on bundle-internal consistency only — a human reviewer,
223
+ not the reconciler, is the check.
224
+ - **Free-text `approved_by` on waivers is unauthenticated (existing disclosure, folded in
225
+ here).** As noted in §3 above: a waiver's `approved_by` is self-asserted free text, not
226
+ bound to an authenticated identity. *Mitigated by:* the loud, un-suppressible `WAIVED` line
227
+ in the required job's own log, plus CODEOWNERS review — not by identity attestation. Same
228
+ underlying gap and same Assurance-profile remedy as the attestation residual above.
229
+ - **The evidenceType-laundering route (explicitly named).** §2's classification table routes
230
+ `security`/`browser`/`runtime` checks to `test_output` (CI-reconcilable) only `hasCommand`;
231
+ otherwise they fall to `attestation`/`crawl_observation` (session-local). An agent that
232
+ wants a high-impact check to avoid CI re-execution can simply omit `command` from the
233
+ `record-evidence --check-json` call — the check is not "downgraded," it legitimately *is*
234
+ session-local (no command to reconcile), but the practical effect is the same evasion
235
+ surface as the fabricated-attestation residual above: a `kind:"security"`/`"runtime"`/
236
+ `"external"` claim with no command was, pre-iteration-4, indistinguishable from a
237
+ test_output-equivalent pass. *Mitigated by:* the iteration-4 `ATTESTED` marker makes every
238
+ claim that took this route visible and countable in the required job's own log — a reviewer
239
+ (or a future CI policy keyed on the summary count) can now see exactly how many high-impact
240
+ claims in a passing bundle went through the no-command/session-local path rather than a
241
+ fresh CI re-run. Not closed; disclosed. Closing it fully requires either (a) the
242
+ Assurance-profile signing remedy above, or (b) a policy decision to require `command` for
243
+ specific `impactLevel`/`kind` combinations (a future, separate ADR — not implemented here,
244
+ as it would need to weigh against the honest no-command use cases §1–§2 exist to support).
245
+
246
+ The consistent principle (same as ADR 0017): **the reconciler's L0 baseline cannot manufacture
247
+ independent truth for non-command-backed evidence; disclosure (the loud `ATTESTED` marker +
248
+ summary count) and human review are the closing mechanism until a signed-attestation
249
+ Assurance profile exists.**
250
+
251
+ ## Consequences
252
+
253
+ - The exact capability PR #264 lacked now exists and is proven end-to-end:
254
+ `evals/integration/test_trust_reconcile_mixed_bundle.sh` builds a bundle with one
255
+ manifest-matched `test_output` claim, one honest `human_attestation` session-local claim,
256
+ and one waived `accepted_gap` claim, and passes the *real* `trust-reconcile.js`
257
+ entrypoint (exit 0) with distinct `RECONCILED` / `ATTESTED (not independently verifiable at
258
+ L0)` / `WAIVED` lines (the `SESSION-LOCAL OK` line from earlier iterations was replaced by
259
+ the loud `ATTESTED` marker in iteration-4 — see §4 above).
260
+ - The anti-gaming self-check `evals/integration/test_trust_reconcile_manifest.sh` asserts
261
+ the manifest id set and ci.yml's required `--check` set are identical, and runs in a
262
+ required lane (`antigaming-suite.sh`). Removing a check from every `LANE_*` array fails
263
+ it.
264
+ - Every live exploit reproduced in review is frozen as a permanent negative regression in
265
+ `evals/integration/test_trust_reconcile_negatives.sh` (wired into the required
266
+ `antigaming-suite.sh`): a no-label `test_output` bypass, a `skip → assumed` silent pass, a
267
+ `status-misassertion`, a waived command-backed check, the real ws3 old-style bundle
268
+ (AC6), and (iteration-4) a fabricated-attestation bundle asserting it PASSES (exit 0) while
269
+ emitting the loud `ATTESTED` marker and summary count — a visibility assertion, not a
270
+ divergence assertion — each of the first five asserting a non-zero exit and its specific
271
+ divergence string.
272
+ - Backward compat holds by construction: single-command adopters and old all-`test_output`
273
+ bundles reconcile identically (the legacy path is a manifest of size 1). The existing
274
+ laundering, checkpoint-bypass, and never-captured-command divergences are unchanged.
275
+ - CODEOWNERS protection extends to the new `trust-reconcile-manifest` declaration, matching
276
+ ADR 0017's owner-review posture for the verify config.
277
+ - **Trade-off (honest):** the reconciler re-runs matched manifest commands fresh on demand.
278
+ This is bounded by the registry and only the claimed commands are run. Reusing ci.yml's
279
+ already-produced `status.tsv` (rather than re-running) is a future optimization the
280
+ manifest design anticipates but this ADR does not implement.
281
+
282
+ ## Alternatives Considered
283
+
284
+ - **A new manifest file / a new `claimType` taxonomy / a new waiver status value** — all
285
+ rejected as forks of existing canonical vocabulary (`run-baseline.sh`'s registry,
286
+ Surface's `evidenceType`, the `accepted_gap` status). Each would create a parallel
287
+ concept needing its own policing.
288
+ - **Making the reconciler re-run the full registry in `trust-reconcile.yml`** — rejected:
289
+ it would duplicate the entire ci.yml suite in a second required job. On-demand fresh runs
290
+ of only the claimed manifest commands give the same soundness at bounded cost.
@@ -0,0 +1,221 @@
1
+ ---
2
+ title: "ADR 0021: Assignment Leases and Stale-Claim Takeover"
3
+ ---
4
+
5
+ # ADR 0021: Assignment Leases and Stale-Claim Takeover
6
+
7
+ **Date:** 2026-07-02
8
+ **Status:** Draft (shaped with Brian Anderson, 2026-07-02). Completes the coordination loop
9
+ designed in [ADR 0012](./0012-agent-coordination-as-liveness-claims.md); gated on the same
10
+ Surface ≥1.2.x bump.
11
+
12
+ ---
13
+
14
+ ## Context
15
+
16
+ ADR 0012 designed the primitive — an advisory liveness claim stream (`claim` / `heartbeat` /
17
+ `release`, TTL-reaped, recomputed via the Surface status function) — and the implementation
18
+ exists (`src/cli/workflow-sidecar.ts` `liveness` command, `scripts/hooks/lib/liveness-read.js`,
19
+ the `workflow-steering.js` SessionStart warning). But the loop is unwired in practice:
20
+
21
+ 1. **Nothing emits.** Lifecycle auto-emit is gated behind `FLOW_AGENTS_LIVENESS` (default off);
22
+ no skill calls `liveness claim`; the live runtime has no `liveness/` stream at all.
23
+ 2. **`pull-work` never reads.** Selection writes no claim, label, or assignee (provider
24
+ mutation is forbidden by the skill), so N concurrent sessions independently classify the
25
+ same issue "ready" and all take it (#166, under epic #151).
26
+ 3. **Actor identity collapses.** `FLOW_AGENTS_ACTOR` defaults to the shared literal `"local"`,
27
+ so co-located sessions are indistinguishable — held/conflict detection is structurally
28
+ defeated on exactly the machine where it matters most.
29
+ 4. **The sidecar directory is a collision point.** `workItemSlug()` is deterministic per work
30
+ item, so two sessions on one issue race last-writer-wins on the same `state.json` and the
31
+ single global `current.json` pointer.
32
+ 5. **The merge race is unguarded.** No branch convention (templates default `branch: main`),
33
+ no hold check before push/PR/merge — a lapsed-and-superseded session can still publish over
34
+ its successor's work.
35
+ 6. **Durable assignment is missing entirely.** The liveness stream is local and ephemeral by
36
+ design; there is no cross-machine, human-visible, crash-surviving record of *who owns what*.
37
+
38
+ The user-level requirement: run X concurrent sessions unattended, with no duplicate pickup, no
39
+ clobbered merges, visibility into who holds what — and **no lost locks**: a claim must never
40
+ outlive its holder's usefulness just because a terminal window was forgotten.
41
+
42
+ ## Decision
43
+
44
+ ### 1. Effective claim state is a join: `assignment ⋈ liveness`. Neither layer alone is truth.
45
+
46
+ Two layers with distinct jobs, never conflated:
47
+
48
+ | Layer | Records | Lifetime | Medium |
49
+ |---|---|---|---|
50
+ | **Assignment** | intent / ownership | durable, survives crashes | provider (issue assignee/label/comment) |
51
+ | **Liveness** | presence / freshness | ephemeral, TTL-reaped | ADR 0012 claim stream |
52
+
53
+ Readers always compute the join; provider state is **never trusted alone**:
54
+
55
+ | Assignment | Liveness | Effective state | `pull-work` treatment |
56
+ |---|---|---|---|
57
+ | assigned | fresh heartbeat | **held** | excluded |
58
+ | assigned | stale / absent | **reclaimable** | offered, via takeover protocol (§5) |
59
+ | assigned (human) | n/a (humans don't heartbeat) | **human-held** | surfaced, never auto-taken (§6) |
60
+ | unassigned | fresh (claim only) | **held** (assignment lagging) | excluded |
61
+ | unassigned | absent | **free** | offered |
62
+
63
+ This join rule is what makes lost locks structurally impossible: staleness — not assignment —
64
+ is what excludes, so an orphaned assignee/label from a dead session can never gate work.
65
+
66
+ ### 2. Assignment is a provider abstraction — GitHub is an implementation, not the model.
67
+
68
+ A third provider leg beside the existing backlog and change providers
69
+ (`backlog-provider-settings.schema.json`, `ChangeProvider`):
70
+
71
+ ```
72
+ AssignmentProvider:
73
+ claim(subjectId, actor, meta) # assign + attach machine-readable claim record
74
+ release(subjectId, actor, meta) # unassign + handoff note
75
+ supersede(subjectId, from, to, meta) # reassign with audit trail
76
+ status(subjectId) -> {assignee?, actor?, claimedAt?, meta?}
77
+ list(actor?) -> [subjectId]
78
+ ```
79
+
80
+ The **GitHub implementation**: assignee (notification/board hook) + a single `agent:claimed`
81
+ label (board filter) + a **machine-readable claim comment** carrying the full actor struct,
82
+ `claimed_at`, TTL, branch, and artifact-dir pointer. The comment carries identity because the
83
+ assignee field cannot: N agent sessions typically share one GitHub account, so per-session
84
+ identity lives in the attached record, and each provider decides what it can natively
85
+ represent versus what goes in the record. Other implementations (Linear, Jira, GitLab,
86
+ local-file for tracker-less repos) map the same operations.
87
+
88
+ **Actor identity is runtime-agnostic**: `{runtime, session_id, host, human?}` — Claude Code,
89
+ codex, opencode, and pi sessions (all of which already have telemetry hook adapters) and
90
+ humans are all actors. The SessionStart hook derives and exports `FLOW_AGENTS_ACTOR` from the
91
+ runtime session id automatically; the `"local"` default is retired as an error, not a fallback.
92
+
93
+ ### 3. Liveness is cross-cutting: ambient reads, lifecycle writes, one hard gate.
94
+
95
+ Five touchpoints, one policy:
96
+
97
+ | Touchpoint | Direction | Behavior |
98
+ |---|---|---|
99
+ | `workflow-steering.js`, every turn | read | one-line liveness digest; "superseded" interrupt for the held subject |
100
+ | `pull-work` | read + write | exclude held; claim (liveness + provider) on selection |
101
+ | `ensure-session` / pickup | read + write | refuse entry to a session dir under a fresh other-actor claim; supersede-then-enter if stale |
102
+ | `advance-state` + tool activity | write | heartbeat rides existing writes; no bespoke timer |
103
+ | **publish (push / PR / merge)** | **read, blocking** | **verify-hold**: hard-stop if not the fresh, non-superseded holder |
104
+
105
+ Once an actor holds a fresh claim it is "heads down": the ambient check narrows from "what is
106
+ everyone doing" to "have I been superseded". The verify-hold gate is the fencing token — the
107
+ advisory layer's single mutex moment, placed at the integration layer exactly where ADR 0012 §4
108
+ assigns real serialization. It is what defeats the zombie: a session that sleeps past TTL, gets
109
+ superseded, and wakes is caught deterministically before it can publish over its successor.
110
+
111
+ `FLOW_AGENTS_LIVENESS` flips to default-on. Branch becomes a first-class routing field in
112
+ `state.json` with the convention `agent/<actor>/<slug>`; `current.json` becomes per-actor
113
+ rather than one global last-writer-wins pointer.
114
+
115
+ ### 4. Provider state is corrected lazily at claim transitions — plus a janitor for humans.
116
+
117
+ Nothing updates the provider on a timer; every mutation has a responsible actor:
118
+
119
+ | Transition | Mutator | When |
120
+ |---|---|---|
121
+ | claim | the claiming session | at selection |
122
+ | clean release | the incumbent (Stop hook / terminal `advance-state`) | session end — unassign + handoff comment |
123
+ | supersede | the **successor**, inside the takeover protocol | after the grace beat |
124
+ | crash, no successor | nobody, initially | corrected by the next actor to want the subject, or the janitor |
125
+
126
+ Lazy correction is *safe* by §1 (stale assignment excludes nothing) but leaves the human board
127
+ lying in the crash case. The supplement is a **janitor**: a sweep that joins `agent:claimed`
128
+ subjects against the liveness stream and relabels/unassigns past-TTL claims with an explanatory
129
+ comment. Locally this is a cron sweep; for fleets it is the natural first coordination duty of
130
+ the hosted Console relay (§7), which is the one vantage with global heartbeat visibility.
131
+ Heartbeats are **never** written to the provider (rate-limit abuse for no benefit); at most the
132
+ claim comment refreshes on phase transitions, doubling as board-level progress.
133
+
134
+ ### 5. Takeover is resumption, not restart — `continue-work` triggered by staleness.
135
+
136
+ ADR 0012 §3 established that durable evidence outlives the ephemeral claim. This ADR makes the
137
+ resume path concrete. On seeing `assigned + stale`:
138
+
139
+ 1. Emit a supersede claim on the subject.
140
+ 2. **Grace beat**: wait one heartbeat interval; if the incumbent revives, back off (the
141
+ "laptop just woke" race resolves in the incumbent's favor).
142
+ 3. `supersede()` via the provider — reassign with the audit-trail comment ("superseded actor X,
143
+ last seen T, resuming from trust bundle").
144
+ 4. Resume through the existing handoff/trust-bundle machinery — the deterministic work-item
145
+ slug means the successor inherits the same artifact dir by construction (the prior
146
+ collision hazard becomes the resume feature once claims gate entry).
147
+ 5. **Continue the incumbent's branch** (fetched from `state.json`'s branch field), never a
148
+ parallel one — otherwise takeover recreates the duplicate work it exists to prevent.
149
+
150
+ The woken zombie is then caught twice: its next turn's steering digest says "superseded", and
151
+ the verify-hold gate blocks it at publish regardless.
152
+
153
+ ### 6. Human assignees are never auto-superseded.
154
+
155
+ Humans do not heartbeat, so `assigned-to-human + no-liveness` is normal, not stale. Agents
156
+ **surface** idle human assignments ("assigned to brian 3 days ago, no linked activity —
157
+ reclaim?") and act only on explicit confirmation. The threshold and behavior are a policy knob
158
+ in the assignment-provider settings; the default is ask-first.
159
+
160
+ ### 7. Local-first stands; the Console is the fleet tier, never required.
161
+
162
+ Per ADR 0012 §5 the sink ladder is local file → git ref → optional hosted relay. Everything in
163
+ §§1–6 works on a single machine with the local JSONL stream and the GitHub provider. The hosted
164
+ Console (console.kontourai.io) adds, strictly as a relay/projection of the advisory stream —
165
+ consistent with its "never becomes the authority" stance:
166
+
167
+ - **liveness relay**: sessions mirror claim/heartbeat/release events (its existing idempotent
168
+ ingest + tenant auth + SSE substrate fits without new authority semantics);
169
+ - **fleet view**: active actors, held/reclaimable subjects, per-session cost it already has;
170
+ - **the janitor** (§4), the first genuinely cross-machine duty.
171
+
172
+ This is the deliberate product boundary: single-machine parallelism is fully unlocked locally;
173
+ multi-machine fleets, human oversight, and history are what the hosted tier sells.
174
+
175
+ ## Consequences
176
+
177
+ - Running X unattended concurrent sessions becomes safe by construction: duplicate pickup is
178
+ excluded by the join rule, clobbered merges by verify-hold, lost locks by TTL + Stop-hook
179
+ release, and abandoned work is resumed — not restarted — via takeover.
180
+ - The Stop hook gains a duty: emit `release`-with-handoff on every non-terminal session end, so
181
+ the common case (closed terminal) frees work instantly and TTL only covers real crashes. This
182
+ keeps TTLs generous, which ADR 0012 §4 identifies as the operational risk.
183
+ - `pull-work` loses its blanket no-provider-mutation rule in favor of one narrow, audited
184
+ mutation: the claim at selection.
185
+ - New surface to maintain: the `AssignmentProvider` contract + settings schema, the actor
186
+ struct, a branch-naming convention, per-actor `current.json`, and the janitor.
187
+ - Cross-repo dependency: the Console relay/fleet-view/janitor land in the console repo; this
188
+ repo only gains the optional relay sink.
189
+
190
+ ## Alternatives Considered
191
+
192
+ - **Liveness-only (no durable assignment).** Fails cross-machine and human visibility; a crash
193
+ leaves no record of ownership anywhere a human looks. Rejected — it is the presence layer,
194
+ not the ownership layer.
195
+ - **Provider-only (assignee/label, no liveness).** ADR 0012 already rejected as insufficient;
196
+ additionally it has no freshness, so it *creates* the lost-lock problem this ADR exists to
197
+ prevent. TTL logic bolted onto provider comments is the same design with worse atomicity.
198
+ - **Hard leases (provider-side CAS, lock server).** Re-rejected per ADR 0012 — stale leases
199
+ orphan work; the verify-hold gate gets mutex safety at the single point that needs it.
200
+ - **Heartbeating to the provider.** Rejected: rate limits, comment spam, and it duplicates the
201
+ stream that already exists.
202
+ - **Auto-superseding humans symmetrically with agents.** Rejected: humans lack heartbeats, so
203
+ symmetry manufactures false reclaims against exactly the actors least able to defend a claim.
204
+
205
+ ## References
206
+
207
+ - [ADR 0012: Agent Coordination as Hachure Liveness Claims](./0012-agent-coordination-as-liveness-claims.md) — the primitive this ADR wires and completes. (Its reference to
208
+ #137 for pull-work claim wiring is stale; the live tracking is #166 under epic #151.)
209
+ - #151 — the ADR 0012 coordination epic; this ADR shapes its remaining slices. #166
210
+ (pull-work claim emit/exclude) is extended by §1's join rule; #167 (advisory surface,
211
+ warn-never-block) is deliberately upgraded by §3 at exactly one point — the verify-hold
212
+ publish gate; #153 (resume from the durable record) is the substrate §5's takeover
213
+ builds on; #161 (deterministic work-item slug) is the landed foundation.
214
+ - `src/cli/workflow-sidecar.ts` (`liveness`, `withLock`, `writeCurrent`, `workItemSlug`);
215
+ `scripts/hooks/workflow-steering.js` (ambient read side, already live);
216
+ `scripts/hooks/stop-goal-fit.js` (release-with-handoff injection point).
217
+ - `docs/kontour-resource-contract.md` `ScopeOverlap` — the declared-file-scope seam for
218
+ work-*area* (vs work-item) conflict detection; claims should carry declared scope so
219
+ `pull-work`'s `global_conflicts` can consult live claims, closing the gap ADR 0012 names.
220
+ - Console: `docs/integrations/flow-agents-console.md`, console repo `POST /records` +
221
+ `/ingest/flow` + SSE (the relay substrate).