@kontourai/flow-agents 3.1.0 → 3.3.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 (86) hide show
  1. package/.github/workflows/ci.yml +16 -0
  2. package/CHANGELOG.md +30 -0
  3. package/build/src/cli/assignment-provider.d.ts +45 -0
  4. package/build/src/cli/assignment-provider.js +107 -13
  5. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  6. package/build/src/cli/workflow-sidecar.d.ts +238 -4
  7. package/build/src/cli/workflow-sidecar.js +875 -14
  8. package/build/src/tools/validate-source-tree.js +3 -2
  9. package/context/contracts/artifact-contract.md +16 -2
  10. package/context/contracts/assignment-provider-contract.md +1 -1
  11. package/context/contracts/execution-contract.md +78 -0
  12. package/context/scripts/hooks/config-protection.js +11 -4
  13. package/context/scripts/hooks/stop-goal-fit.js +259 -4
  14. package/context/scripts/hooks/workflow-steering.js +73 -1
  15. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +111 -0
  16. package/docs/coordination-guide.md +370 -0
  17. package/docs/decisions/agent-coordination.md +26 -9
  18. package/docs/decisions/index.md +2 -2
  19. package/docs/decisions/trust-reconcile.md +42 -9
  20. package/docs/fixture-ownership.md +3 -2
  21. package/docs/index.md +4 -0
  22. package/docs/integrations/flow-agents-console.md +108 -0
  23. package/docs/integrations/index.md +4 -0
  24. package/docs/workflow-artifact-lifecycle.md +38 -1
  25. package/evals/ci/antigaming-suite.sh +1 -0
  26. package/evals/ci/run-baseline.sh +8 -0
  27. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  28. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  29. package/evals/integration/test_checkpoint_signing.sh +14 -5
  30. package/evals/integration/test_ci_actor_identity.sh +221 -0
  31. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  32. package/evals/integration/test_gate_lockdown.sh +36 -0
  33. package/evals/integration/test_model_routing_escalation.sh +145 -0
  34. package/evals/integration/test_publish_delivery.sh +73 -8
  35. package/evals/integration/test_reconcile_preflight.sh +304 -0
  36. package/evals/integration/test_stop_hook_release.sh +552 -0
  37. package/evals/integration/test_takeover_protocol.sh +340 -0
  38. package/evals/integration/test_trust_reconcile_negatives.sh +261 -0
  39. package/evals/integration/test_verify_hold.sh +910 -0
  40. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  41. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  42. package/evals/run.sh +14 -0
  43. package/evals/static/test_model_routing_hints.sh +107 -0
  44. package/kits/builder/skills/builder-shape/SKILL.md +10 -0
  45. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  46. package/kits/builder/skills/deliver/SKILL.md +125 -11
  47. package/kits/builder/skills/design-probe/SKILL.md +10 -0
  48. package/kits/builder/skills/execute-plan/SKILL.md +13 -0
  49. package/kits/builder/skills/fix-bug/SKILL.md +17 -0
  50. package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
  51. package/kits/builder/skills/plan-work/SKILL.md +9 -0
  52. package/kits/builder/skills/pull-work/SKILL.md +22 -2
  53. package/kits/builder/skills/review-work/SKILL.md +11 -0
  54. package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
  55. package/kits/builder/skills/verify-work/SKILL.md +11 -0
  56. package/kits/knowledge/adapters/default-store/index.js +56 -15
  57. package/kits/knowledge/adapters/flow-runner/index.js +912 -16
  58. package/kits/knowledge/adapters/obsidian-store/index.js +29 -11
  59. package/kits/knowledge/adapters/shared/codec.js +124 -0
  60. package/kits/knowledge/docs/store-contract.md +405 -3
  61. package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
  62. package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
  63. package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
  64. package/kits/knowledge/evals/contract-suite/suite.test.js +36 -0
  65. package/kits/knowledge/evals/freshness/suite.test.js +339 -0
  66. package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
  67. package/kits/knowledge/evals/retirement/suite.test.js +1 -1
  68. package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
  69. package/kits/veritas-governance/docs/README.md +81 -3
  70. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  71. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  72. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  73. package/kits/veritas-governance/kit.json +5 -0
  74. package/package.json +1 -1
  75. package/schemas/workflow-handoff.schema.json +6 -0
  76. package/scripts/ci/mint-attestation.js +33 -6
  77. package/scripts/ci/trust-reconcile.js +222 -279
  78. package/scripts/hooks/config-protection.js +11 -4
  79. package/scripts/hooks/lib/actor-identity.js +82 -0
  80. package/scripts/hooks/stop-goal-fit.js +259 -4
  81. package/scripts/hooks/workflow-steering.js +73 -1
  82. package/scripts/lib/reconcile-shape.js +381 -0
  83. package/src/cli/assignment-provider.ts +122 -13
  84. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  85. package/src/cli/workflow-sidecar.ts +965 -14
  86. package/src/tools/validate-source-tree.ts +3 -2
@@ -463,3 +463,114 @@ the workflow to fake the post-merge no-op path; this is closed by required code-
463
463
  review on `.github/workflows/trust-reconcile.yml` (#225, not yet server-side enforced),
464
464
  the same residual class as every other self-asserted CI input this ADR already carries.
465
465
  Step 1 (fresh verify) is unaffected by event scoping either way and always runs.
466
+
467
+ ## Addendum (2026-07-04, part 5): per-session delivery paths — concurrent deliveries stop contending (#379)
468
+
469
+ Owner-approved follow-up closing a structural defect the fail-closed gate's first real
470
+ deliveries surfaced repeatedly: the delivery transport used a SINGLE shared path
471
+ (`delivery/trust.bundle` + `delivery/trust.checkpoint.json`), so every sealed delivery
472
+ force-committed to the SAME two files. Any two concurrent deliveries therefore
473
+ merge-conflict by construction — three seal collisions inside 24h (#330, #358, #378) each
474
+ needed manual conflict resolution. Worse than the conflict itself: **GitHub schedules NO
475
+ `pull_request` workflows for a conflicting (DIRTY) PR** — zero checks, no error — so the
476
+ required Trust Reconcile check silently never re-runs. The symptom reads as "CI vanished,"
477
+ not "conflict." This collision class scales with delivery frequency; ADR 0021's own "work
478
+ area" vocabulary predicted it, and #335 named the `resolveDeliveryCandidates()` seam as the
479
+ fix site.
480
+
481
+ **Decision (structural): per-session delivery paths.** `publishDelivery()`
482
+ (`src/cli/workflow-sidecar.ts`) writes to `delivery/<slug>/trust.bundle` (+ checkpoint
483
+ companions), where `<slug>` is the session artifact dir's basename, instead of the shared
484
+ flat path. Concurrent deliveries write to DISTINCT files and cannot contend: two deliveries
485
+ add different `delivery/<slug>/` dirs (add/add of different paths — not a conflict), and both
486
+ deleting the same inherited flat/legacy file is a delete/delete (auto-merges — not a
487
+ conflict). The per-session dir NAME is only a collision-avoidance handle; it carries no
488
+ trust weight.
489
+
490
+ **Reconciler side: ownership-aware discovery, prefer-newest, not first-match.**
491
+ `resolveDeliveryCandidates()` now returns the flat path (FIRST, for full back-compat)
492
+ followed by every `delivery/<slug>/<filename>` (sorted). `discoverBundle()` no longer returns
493
+ the first-existing candidate; it collects every candidate that attests THIS change
494
+ (ancestor-or-equal, the SAME `bundleAttestsThisChange()` binding Addendum part 2 defined) and
495
+ selects the one attesting the **NEWEST** (descendant-most) commit. This prefer-newest rule is
496
+ load-bearing in a **merge-commit** repo (this repo's own history has merge commits, e.g.
497
+ release-please merges): an inherited FLAT bundle's `commit_sha` can be a REAL ancestor of HEAD
498
+ — it was committed on the trunk's linear history before this branch point — so it legitimately
499
+ "owns" the change too, and a naive first-fresh-wins would select that STALE inherited bundle
500
+ purely because it sorts first, reconciling the PREVIOUS delivery's claims against THIS change's
501
+ CI. Prefer-newest makes the fresh per-session bundle win on recency, not on being deleted
502
+ first — which in turn is what lets the cleanup policy leave the flat path in place (below)
503
+ without corrupting selection. (Addendum parts 2/4's squash-merge reasoning still holds for
504
+ squash repos; prefer-newest is the strict generalization that also covers merge-commit repos,
505
+ where "inherited ⇒ non-ancestor" is NOT guaranteed.) `extractBundleCommitSha()` now resolves a
506
+ bundle's sibling checkpoint from the bundle's OWN directory (`path.dirname(bundlePath)`), not a
507
+ global scan — a global scan would pair a per-session bundle with the wrong session's (or the
508
+ flat) checkpoint and read the wrong commit binding. For the flat layout this is byte-identical
509
+ to the prior behavior; for per-session it is the only correct pairing.
510
+
511
+ **Back-compat (retained, deprecation-noted).** The flat `delivery/trust.bundle` path stays
512
+ fully supported on the READ side: an already-committed flat bundle from before this change,
513
+ or an external adopter that has not migrated, still resolves and reconciles exactly as
514
+ before (regression-locked by the negatives suite's flat-owner-coexist case and the
515
+ unchanged `test_publish_delivery.sh` TEST 3/4 flat-fixture cases). Only the WRITE side moved
516
+ to per-session — writing to both flat and per-session would re-introduce the very contention
517
+ this closes, so the flat path is write-deprecated, read-supported.
518
+
519
+ **Cleanup policy: supersede-on-publish, per-session dirs only (delivery/ stays bounded).** A
520
+ publishing session prunes every inherited **per-session** seal dir except its own, then
521
+ commits only `delivery/<slug>/`. Per-session dirs are the growth vector (one per delivery) and
522
+ are UNIQUELY named, so pruning one can never conflict with a concurrent PR: two branches
523
+ deleting the same inherited dir is a delete/delete (auto-merges), and each delivery adds its
524
+ own distinct dir. Leaving an inherited per-session dir would be harmless anyway (prefer-newest
525
+ ignores it) — pruning is purely to stop unbounded accumulation. The alternative,
526
+ retain-as-history, was rejected as unbounded growth of permanently-superseded dirs with no
527
+ reader. Pruning is best-effort: a prune failure is logged, never fatal to the delivery.
528
+
529
+ **The shared flat path is deliberately NOT pruned per-delivery.** An earlier iteration of this
530
+ addendum also pruned the flat `delivery/trust.bundle` legacy seals on every publish (to
531
+ "migrate off" the shared path). That was reverted: during the migration window a concurrent PR
532
+ may still seal to the flat path (this was written while PR #370 had an open flat-path seal),
533
+ and a per-delivery deletion of that file is a **modify/delete conflict** against such a PR → a
534
+ DIRTY PR → precisely the no-CI failure mode this whole change exists to remove. Because the
535
+ flat path is a single fixed location (not a growth vector) and prefer-newest selection makes a
536
+ lingering flat bundle harmless, retaining it costs nothing. Removing the flat legacy seals is a
537
+ one-time cleanup for a **dedicated** PR once no open PR still seals to the flat path — not
538
+ something safely bundled into every delivery.
539
+
540
+ **The SILENT failure mode (documented, not "solved").** No repo-side code can make GitHub
541
+ run `pull_request` workflows on a conflicted PR — that is platform behavior. Per-session
542
+ paths remove the STRUCTURAL cause (the shared-path conflict) for agent-vs-agent delivery
543
+ contention, which is the overwhelming majority of the incidents. The residual — a PR that
544
+ goes DIRTY for some OTHER reason (a genuine same-file edit conflict with `main`) still gets
545
+ no CI silently — is addressed by making the failure mode LOUD where we can: the deliver
546
+ skill now documents the DIRTY→no-CI symptom and its diagnosis (`gh pr view --json
547
+ mergeStateStatus`), and `discoverBundle()` emits a grep-stable `#379: examined N delivery
548
+ candidate(s) … none attests this change …` concurrency hint so the next agent can tell a
549
+ per-session collision apart from a plain stale/absent bundle. #335's detectability half
550
+ (treating `mergeStateStatus=DIRTY` as a first-class steering/doctor input) remains open and
551
+ is explicitly NOT claimed closed here.
552
+
553
+ **Security: the forgery surface moved with the write path.** `scripts/hooks/config-protection.js`
554
+ (and its `context/` mirror) protected only the flat `delivery/trust.bundle` /
555
+ `delivery/trust.checkpoint.json` from direct agent Write/Edit/cp/redirect. Its three
556
+ delivery regexes now carry an optional `(?:[^/]+\/)?` segment so `delivery/<slug>/trust.*`
557
+ is equally blocked — otherwise moving the write path would have opened a hand-forgery hole
558
+ one directory down. Regression-locked by `test_gate_lockdown.sh` AC1.26b/c/d. The
559
+ `delivery/*` gitignore already covers per-session dirs (they are force-added deliberately per
560
+ delivery exactly like the flat path).
561
+
562
+ **Residuals (honest):** (1) the DIRTY→no-CI platform behavior for non-per-session conflicts
563
+ is documented, not eliminated (above). (2) The per-session dir name is derived from the
564
+ local session slug; a colliding slug across two sessions would re-share a path — acceptable
565
+ because ownership is still decided by commit ancestry (a stale same-named sibling is ignored,
566
+ not trusted), and slugs are session-unique in practice. (3) Selection correctness does NOT
567
+ depend on merge strategy: prefer-newest resolves the owning candidate for both squash-merge
568
+ (inherited seals are non-ancestors, trivially not-owning) and merge-commit (inherited seals
569
+ can be ancestors, but attest an OLDER commit than this session's, so they lose on recency)
570
+ histories — the merge-commit case was the concrete defect that forced prefer-newest and is
571
+ regression-locked by `test_trust_reconcile_negatives.sh` §8d against a real git repo. (4) The
572
+ flat legacy seals still on `main` are retained by design (see cleanup policy above) and remain
573
+ until a dedicated one-time cleanup PR; they are harmless (prefer-newest) but do accumulate as a
574
+ single fixed path, not a growth vector. This addendum changes WHERE seals live and HOW the
575
+ owning one is chosen; it does not touch Step 1 (fresh verify), the DECLARED exemption path, or
576
+ any fail-closed verdict — all of which are regression-locked unchanged.
@@ -0,0 +1,370 @@
1
+ ---
2
+ title: Parallel-Session Coordination Guide
3
+ ---
4
+
5
+ # Parallel-Session Coordination Guide
6
+
7
+ This is the plain-language, end-to-end explanation of how Flow Agents lets **many sessions work the
8
+ same repository at once** without two of them silently taking the same work, redoing each other's
9
+ effort, or clobbering one another's changes at publish time.
10
+
11
+ > **Which doc do I want?** This page is the *living reference* for coordination — the model and the
12
+ > four guard points, as actually shipped. The frozen [ADRs](adr/) (0012, 0021, 0020, 0022) are the
13
+ > immutable record of *why* each decision was made; this guide is where you look to understand *what
14
+ > runs today*. The [System Guidebook](agent-system-guidebook.md) is the broader map of the whole
15
+ > operating layer. For the optional hosted tier that turns this local substrate into a team-wide
16
+ > fleet view, see [Flow Agents × Console](integrations/flow-agents-console.md).
17
+
18
+ ---
19
+
20
+ ## The problem, in one paragraph
21
+
22
+ You want to run *X* parallel Builder sessions and never think about collisions. That means four
23
+ moments in a session's life must be fenced: **selection** (don't pick work someone already holds),
24
+ **entry** (don't re-enter a subject someone else owns), **mid-flight** (notice fast if an overlap
25
+ happened anyway, and correct it), and **publish** (never let a session that was superseded while it
26
+ slept push over the session that replaced it). Everything else is convenience; those four are the
27
+ safety. Flow Agents makes all four advisory-by-default and cheap, with exactly **one hard fence** —
28
+ at publish — because a false block anywhere else would just annoy you, while a missing block at
29
+ publish loses work.
30
+
31
+ ## The mental model: two independent streams, joined
32
+
33
+ Coordination is not one lock. It's **two independent, append-only signals about a subject** (a work
34
+ item / slug), joined at read time into a single *effective state*:
35
+
36
+ | Stream | Question it answers | Lifetime | Where it lives |
37
+ | --- | --- | --- | --- |
38
+ | **Liveness** | "Is someone *actively working* right now?" | Ephemeral — TTL-reaped, heartbeat-refreshed | `liveness/` event stream ([ADR 0012](adr/0012-agent-coordination-as-liveness-claims.md)) |
39
+ | **Assignment** | "Who *durably owns* this subject?" | Durable — an explicit claim record / GitHub assignee | `AssignmentProvider` ([ADR 0021](adr/0021-assignment-leases-and-stale-claim-takeover.md)) |
40
+
41
+ Neither alone is enough. Liveness without assignment can't tell a crashed session from a finished
42
+ one. Assignment without liveness can't tell an actively-working owner from a stale lease nobody is
43
+ servicing. **Joined**, they answer the only question that matters — *can I safely take this?* — and
44
+ the answer degrades gracefully when one signal is missing.
45
+
46
+ Here is the lifecycle, mapped to the code that fences each moment:
47
+
48
+ | Session phase | Guard point | Command / hook | Effect |
49
+ | --- | --- | --- | --- |
50
+ | **Selection** | pull-work preflight | `pull-work` skill → `assignment-provider status` | Excludes `held`/`reclaimable`; asks first on `human-held` |
51
+ | **Entry** | ensure-session ownership guard | `workflow-sidecar ensure-session` | `free`→claim; `reclaimable`+`--supersede-stale`→take over; `human-held`→ask |
52
+ | **Mid-flight** | liveness heartbeat + supersession steering | Activity hooks + `workflow-steering.js` | Refreshes the claim; warns every turn if you were superseded |
53
+ | **Exit** | Stop-hook clean release | Stop hook → `liveness release` + assignment release | Frees both streams, leaves a handoff note |
54
+ | **Publish** | verify-hold gate (**the one hard fence**) | `workflow-sidecar verify-hold` inside `publishDelivery` | Blocks a superseded/zombie session from pushing |
55
+
56
+ The rest of this guide walks each layer bottom-up: first *who am I* (the actor model — the part that
57
+ caused the most bugs, so it goes first), then the two streams, then the join, then each guard point,
58
+ then how delivery itself is made tamper-resistant.
59
+
60
+ ---
61
+
62
+ ## 1. The actor model — "who am I?"
63
+
64
+ Every claim, heartbeat, and ownership check is attributed to an **actor**. Getting actor identity
65
+ subtly wrong is what caused nearly every hard bug in this substrate (#291, #292, #293), so understand
66
+ this section before the rest.
67
+
68
+ ### The resolution chain
69
+
70
+ `resolveActor(env)` (in `scripts/hooks/lib/actor-identity.js`, mirrored in `src/cli`) returns
71
+ `{ actor, source }` by trying four sources in strict priority order:
72
+
73
+ 1. **`explicit-override`** — `FLOW_AGENTS_ACTOR` is set. The actor is the **bare token** you provided,
74
+ verbatim. Source string: `"explicit-override"`.
75
+ 2. **`runtime-session-id:<runtime>`** — the host runtime exposes a native session id (e.g. Claude
76
+ Code). The actor is a **serialized triple** `runtime:session:host`. Source string:
77
+ `"runtime-session-id:<runtime>"`.
78
+ 3. **`process-ancestry`** — no session id; identity is derived by walking the process tree. Also a
79
+ serialized triple. Source string: `"process-ancestry"`.
80
+ 4. **`unresolved`** — nothing worked. Source string: `"unresolved"`.
81
+
82
+ The `"local"` literal default from the old design is **retired as an error, not a fallback** — a
83
+ shared default actor structurally defeats collision detection on the one machine (your laptop) where
84
+ co-located sessions are most likely.
85
+
86
+ ### The seam that caused the bugs: flat token vs. serialized triple
87
+
88
+ Here is the trap. An **override** actor is a *bare token* (`alice`). A **derived** actor is a
89
+ *serialized triple* (`claude:sess-abc:host`). So there are two different string forms for "the
90
+ actor," and they **diverge for override actors but agree for derived actors**:
91
+
92
+ - `resolveActor(env).actor` → the canonical form (bare token for overrides; triple for derived).
93
+ - `serializeActor(actorStruct)` → **always** a triple `<runtime>:<session>:<host>`. For an override
94
+ actor the struct's `<runtime>` is whatever the reconstructing site fills in (`detectRuntime()`,
95
+ typically `unknown`, in `assignment-provider.ts`), so it serializes to something like
96
+ `unknown:alice:host` — **not** equal to the bare `alice` the rest of the system uses. The exact
97
+ prefix doesn't matter; what matters is that the serialized form is a triple and the canonical form
98
+ is a bare token, so the two **diverge for override actors** (and happen to agree for derived ones).
99
+
100
+ Every other surface — `liveness whoami`, `liveness claim --actor`, per-actor `current.json`,
101
+ pull-work's `--self-actor` — uses the canonical `resolveActor().actor` form. If a self-recognition
102
+ check compared the *serialized* form of a stored claim against the *canonical* self, an override
103
+ session would **fail to recognize its own claim** and either double-claim or false-block itself.
104
+
105
+ ### The fix: a canonical `actor_key` on every record
106
+
107
+ Claim records carry an explicit `actor_key` field (schema ≥ 1.0): the canonical
108
+ `resolveActor(env).actor` string of the claiming actor. Self-recognition and the liveness join always
109
+ compare against **this**, with a backward-compatible fallback for pre-#291 records:
110
+
111
+ ```
112
+ holderActorKey = record.actor_key || serializeActor(record.actor)
113
+ ```
114
+
115
+ A record written by a current session has `actor_key` and compares canonically; an old record without
116
+ it falls back to `serializeActor`, reproducing pre-#291 behavior exactly. This one field is why the
117
+ override/derived divergence no longer bites.
118
+
119
+ ### Stable vs. unstable identity (this powers the publish gate)
120
+
121
+ The `source` matters beyond attribution. An identity is **stable** if it came from
122
+ `explicit-override` or `runtime-session-id:*` — those are reproducible across a session's lifetime. It
123
+ is **unstable** if it came from `process-ancestry` or `unresolved` — those can shift (e.g. a CI job
124
+ with a different process tree on each step). The publish gate (§8) *enforces* only for stable
125
+ identities and degrades to advisory for unstable ones, because a hard block keyed on an identity that
126
+ might not reproduce would false-block legitimate work. Making CI identity stable is the subject of the
127
+ forthcoming CI-runtime identity tier (#398).
128
+
129
+ ---
130
+
131
+ ## 2. The liveness stream — "is someone working right now?"
132
+
133
+ Liveness ([ADR 0012](adr/0012-agent-coordination-as-liveness-claims.md)) is an **advisory,
134
+ append-only** stream of `claim` / `heartbeat` / `release` events, one file under `liveness/`, reaped
135
+ by TTL. It is computed, never authoritative:
136
+
137
+ - A session **claims** a subject when it starts working it, and **heartbeats** on activity to refresh
138
+ freshness. Auto-emit is wired into the lifecycle (default-on since #288).
139
+ - A holder is **fresh** if its most recent event is within the TTL window; **stale** otherwise.
140
+ - On clean exit the session emits **release**; on a crash it simply stops heartbeating and ages out.
141
+
142
+ Liveness answers "active *now*," cheaply and without provider mutation. It cannot, alone, distinguish
143
+ "crashed mid-work" from "finished and moved on" — that's what assignment adds.
144
+
145
+ Helpers: `scripts/hooks/lib/liveness-read.js` reads the stream; `liveness whoami` prints the current
146
+ actor; `workflow-sidecar liveness claim|heartbeat|release` manage events.
147
+
148
+ ---
149
+
150
+ ## 3. The assignment layer — "who durably owns this?"
151
+
152
+ The `AssignmentProvider` ([ADR 0021](adr/0021-assignment-leases-and-stale-claim-takeover.md),
153
+ `src/cli/assignment-provider.ts`) is the durable half. It's an abstraction with two implementations
154
+ behind one contract ([`context/contracts/assignment-provider-contract.md`](https://github.com/kontourai/flow-agents/blob/main/context/contracts/assignment-provider-contract.md)):
155
+
156
+ - **`github`** — maps ownership onto native GitHub primitives: **assignee** (the durable claim),
157
+ **label** (the state), **comment** (the audit trail). This is the shared-truth provider for a team.
158
+ - **`local-file`** — a per-repo claim record on disk, for solo/offline use.
159
+
160
+ Operations: `claim`, `release`, `supersede`, `status`, `list`. A claim record is versioned
161
+ (`schema_version: "1.0"`) and carries the canonical `actor_key` (§1), a `claimed_at`, and an
162
+ `audit_trail` of `claim`/`release`/`supersede` transitions.
163
+
164
+ ### Render-don't-execute
165
+
166
+ A crucial design rule ([ADR 0021](adr/0021-assignment-leases-and-stale-claim-takeover.md), Decision
167
+ 1): **the CLI never runs `gh` itself.** For the `github` provider it emits the exact `gh` argv and
168
+ comment bodies as pure data; the skill layer executes them via the harness Bash tool (argv arrays,
169
+ never shell strings). No `gh` subprocess exists in `src/`. This keeps the trust boundary clean and the
170
+ provider testable.
171
+
172
+ ---
173
+
174
+ ## 4. The join — assignment ⋈ liveness → effective state
175
+
176
+ `computeEffectiveState()` (`src/cli/assignment-provider.ts`) joins the two streams into one of four
177
+ **effective states**, each with a specific machine-readable `reason`. This table *is* the coordination
178
+ logic — everything downstream is a policy over these outcomes:
179
+
180
+ | effective_state | reason | Meaning |
181
+ | --- | --- | --- |
182
+ | `free` | `no_assignment_no_liveness` | Nobody owns it and nobody is working it. Take it. |
183
+ | `held` | `self_is_holder` | *You* hold it (canonical `actor_key` matches self). Proceed. |
184
+ | `held` | `fresh_liveness_heartbeat` | Someone else holds it **and** is actively heartbeating. Hands off. |
185
+ | `held` | `liveness_claim_present_assignment_lagging` | Liveness present but **no durable assignment yet** — an actively-working session that hasn't recorded a durable claim. |
186
+ | `reclaimable` | `assignment_present_liveness_stale_or_absent` | A durable assignment exists but **nobody is heartbeating** — a stale lease / crashed or finished session. Eligible for takeover. |
187
+ | `human-held` | `assignee_is_human` | A human is assigned. **Ask first**, always. |
188
+ | `human-held` | `assignee_without_claim_record` | A human assignee with no agent claim record. **Ask first.** |
189
+
190
+ Two subtleties worth internalizing:
191
+
192
+ - **`held / liveness_claim_present_assignment_lagging`** is "working but not yet durably claimed." It
193
+ is a *hold* (don't barge in) but it is **not** an assignment-backed conflict — which is exactly why
194
+ the publish gate treats it as a pass (§8).
195
+ - **`reclaimable`** is the takeover-eligible state: the durable owner is gone (stale/absent liveness)
196
+ but left a lease. Reclaiming it is deliberate and gated (§7, §10), never automatic.
197
+
198
+ ---
199
+
200
+ ## 5. Guard point 1 — pull-work selection
201
+
202
+ When a session selects work, the `pull-work` skill computes the join for each candidate and **excludes
203
+ anything not takeable**: `held` and `reclaimable` subjects are filtered out; `human-held` triggers an
204
+ ask-first prompt rather than a silent skip. Only after selection does it **emit** — first a liveness
205
+ claim, then (via render-don't-execute) the assignment claim. This is what stops *N* concurrent sessions
206
+ from all classifying the same issue "ready" and all taking it. Selection reads and writes both streams;
207
+ it never mutates a provider it hasn't been told to.
208
+
209
+ ## 6. Guard point 2 — ensure-session ownership guard
210
+
211
+ `workflow-sidecar ensure-session` (`src/cli/workflow-sidecar.ts`, `enforceEnsureSessionOwnership`) is
212
+ the fence at **entry** — the moment a session commits to a subject. It computes the effective state and
213
+ branches:
214
+
215
+ - **`free`** → claim and enter.
216
+ - **`self_is_holder`** → already yours; enter.
217
+ - **`reclaimable`** → enter **only** with an explicit `--supersede-stale`; that performs a `supersede`
218
+ (recording the takeover in the audit trail) and enters. Without the flag, it stops and explains.
219
+ - **`held` (someone else, fresh)** → stop; someone is actively working it.
220
+ - **`human-held`** → stop and ask first.
221
+
222
+ It resolves *self* through the same `resolveActor` path and compares on the canonical `actor_key`, so
223
+ an override session recognizes its own in-progress claim (this is the seam from §1). Every untrusted
224
+ field it echoes back (holder actor, `last_at`, branch) is sanitized at construction — see §9.
225
+
226
+ ## 7. Guard point 4 — Stop-hook clean release
227
+
228
+ On clean session end, the **Stop hook** releases *both* streams: `liveness release` plus an assignment
229
+ release, and writes a **handoff note** so the next session (human or agent) picks up from recorded
230
+ state rather than guesswork. A crash skips this — and that's fine: the liveness claim simply ages out
231
+ to `stale`, the assignment becomes `reclaimable`, and takeover (§10) handles it. Clean release just
232
+ makes the common case instant instead of TTL-delayed.
233
+
234
+ ## 8. Guard point 3 — the verify-hold publish gate (the one hard fence)
235
+
236
+ This is the **only** place coordination *blocks*, and it earned three fix iterations, so its design is
237
+ the most carefully tuned in the system. It lives in `runVerifyHold` and composes into `publishDelivery`
238
+ (`src/cli/workflow-sidecar.ts`) as a distinct fail-closed tier.
239
+
240
+ **The scenario it exists for:** a session claims a subject, goes idle (laptop sleeps), gets
241
+ legitimately superseded by another session that finishes and merges. The first session wakes and tries
242
+ to push — over the top of the work that replaced it. Nothing upstream catches this, because the zombie
243
+ *was* the valid holder when it started. The publish gate is the backstop.
244
+
245
+ **The converged rule — enforce narrowly, degrade safely:**
246
+
247
+ > The gate **blocks (hard)** only when **(1) the session's identity is stable** (`explicit-override`
248
+ > or `runtime-session-id:*`, or an explicitly-passed `actorKey`) **AND (2) there is a durable
249
+ > assignment conflict** — the effective state is `reclaimable`, an assignment-backed `held`-by-another,
250
+ > or `human-held`. In every other case it **degrades to advisory** and passes.
251
+
252
+ Concretely:
253
+
254
+ - **Unstable identity** (`process-ancestry` / `unresolved`) → short-circuits to
255
+ `{ ok: true, effective_state: "not_evaluated", reason: "actor-identity-unstable-advisory-only" }`.
256
+ It never hard-blocks on an identity that might not reproduce. (This is why CI, which currently
257
+ resolves via ancestry, gets advisory treatment — and why #398, giving CI a stable identity, upgrades
258
+ it to enforcing.)
259
+ - **Liveness-only hold** (`held / liveness_claim_present_assignment_lagging`) → **passes.** There is no
260
+ durable assignment conflict; blocking here would false-block a legitimate publish.
261
+ - **Genuine durable conflict + stable identity** → throws `NotFreshHolderError`
262
+ (`.code === "VERIFY_HOLD_NOT_FRESH_HOLDER"`), which `publishDelivery` refuses to swallow.
263
+
264
+ **Why this doesn't weaken zombie protection:** a superseded session *always* leaves a durable
265
+ assignment record (supersede writes one), and a real interactive session *always* carries a stable
266
+ identity. So the exact case the gate must catch — a stable session whose durable claim was superseded —
267
+ always lands in the enforcing branch. The advisory degradations only ever apply to cases that were
268
+ never a real conflict.
269
+
270
+ **The load-bearing lesson** (captured as a learning + a deliver-guidance correction): a hard block
271
+ added to a *shared* code path (`publishDelivery` is traversed by every delivery and many tests) must
272
+ **default to advisory and enforce only on high-confidence signals.** Two of the three defects were
273
+ *false-blocks* invisible to all eight acceptance criteria and two review passes — because every AC
274
+ scenario naturally used a stable identity with an assignment record. They only surfaced when CI's broad
275
+ suite ran under a *neutral* (ancestry) identity. Targeted ACs and a broad-suite-under-neutral-identity
276
+ are **different safety nets**; a hard-block-on-shared-path change needs both.
277
+
278
+ ## 9. The injection-safety invariant (everywhere untrusted fields are echoed)
279
+
280
+ Coordination reads fields written by *other* sessions — actor, holder, `last_at`, branch,
281
+ `artifact_dir`, reason. These are **untrusted** and flow into agent-facing output, so a hostile value
282
+ could inject instructions or blow up a display. The invariant, applied at *construction* (not at print
283
+ time), everywhere such a field crosses into output:
284
+
285
+ ```
286
+ stripControlCharsForDisplay(value).slice(0, 64) // (larger caps for known-long fields, e.g. reason → 240)
287
+ ```
288
+
289
+ This class recurred across #287/#320/#290/#291/#293. The subtlest miss: sanitizing only the
290
+ *discriminated* field (`actor`) while a sibling (`last_at`) slipped through raw — fixed by sanitizing
291
+ the *whole holder object* at construction. `safeStateText` in `workflow-steering.js` was found to only
292
+ collapse whitespace (not strip control chars) and was hardened. When you add a new field to any
293
+ coordination output, sanitize it at construction — no exceptions.
294
+
295
+ ## 10. Takeover protocol (forthcoming — #294)
296
+
297
+ Takeover is the deliberate reclaiming of a `reclaimable` subject. The pieces already exist —
298
+ `reclaimable` detection (§4), `--supersede-stale` entry (§6), supersede audit records (§3), and the
299
+ publish gate that catches the woken zombie (§8). #294 is the remaining slice that ties them into a
300
+ first-class protocol: **stale detect → grace beat → supersede → resume the branch**, with the original
301
+ session caught cleanly at publish if it ever wakes. Until it lands, takeover works via the manual
302
+ `ensure-session --supersede-stale` path. *(This section describes intended behavior; #294 is not yet
303
+ shipped.)*
304
+
305
+ ---
306
+
307
+ ## How delivery itself is made tamper-resistant
308
+
309
+ Coordination decides *who* may publish; the **delivery machinery** makes the publish itself honest and
310
+ non-colliding. Three mechanisms
311
+ ([ADR 0020](adr/0020-trust-reconcile-manifest-and-claim-classification.md),
312
+ [ADR 0022](adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md)):
313
+
314
+ **Per-session delivery paths (#379).** Each session writes its bundle under `delivery/<slug>/` instead
315
+ of a shared `delivery/trust.bundle`. Two sessions delivering concurrently no longer overwrite each
316
+ other; CI selects prefer-newest. (A recurring publish trap: after a soft-reset, `git add -A` would
317
+ stage the *deletion* of other sessions' `delivery/<slug>/` dirs — always restore sibling delivery dirs
318
+ from `origin/main` before committing.)
319
+
320
+ **Three-tier fail-closed `publishDelivery`.** Publishing passes through, in order:
321
+ 1. **fail-soft** — absent/repo-root bundle is tolerated (nothing to publish).
322
+ 2. **shape gate (#356)** — `InvalidBundleShapeError` (`.code === "RECONCILE_PREFLIGHT_INVALID_SHAPE"`):
323
+ an invalid *bundle shape* is refused before anything is copied into `delivery/`.
324
+ 3. **hold gate (#293)** — `NotFreshHolderError` (§8): a valid-shaped bundle from a superseded holder is
325
+ refused.
326
+
327
+ These are **distinct error types**, asserted by `.code`/`instanceof` (not message text) so a future
328
+ refactor can't silently collapse them.
329
+
330
+ **reconcile-preflight (#356).** A local, pre-push mirror of the CI reconcile check, sharing
331
+ `scripts/lib/reconcile-shape.js` so it can never drift from CI. It runs the *same* shape classification
332
+ CI enforces, catching an invalid bundle on your laptop instead of in a red CI run. Its one deliberate
333
+ difference is the `onUnderivable` mode: CI passes `'fail'` (fail-closed — an underivable status fails
334
+ the run); the local preflight may pass `'reduce'` (trust a session-local self-reported status when
335
+ re-derivation is unavailable). The default is `'fail'`, so nothing degrades to fail-open by accident.
336
+
337
+ **Staleness binding (ADR 0022).** A checkpoint's `commit_sha` must be an ancestor of `HEAD`, achieved
338
+ by *sealing at the parent commit*: soft-reset `HEAD~1` → seal → publish → recommit. This binds a bundle
339
+ to the exact history it attests, so a stale bundle can't be replayed onto a moved branch.
340
+
341
+ The canonical publish sequence that ties coordination + delivery together:
342
+
343
+ ```
344
+ rebase early → seal at parent (commit_sha = HEAD~1) → reconcile-preflight (local backstop)
345
+ → restore sibling delivery/<slug>/ dirs → verify-hold → push
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Where this goes next: the Console tier
351
+
352
+ Everything above runs **locally** and needs no server — that's the design invariant (the Console is
353
+ never required). The optional [Kontour Console](integrations/flow-agents-console.md) tier turns this
354
+ per-repo substrate into a **team-wide fleet view**: a liveness relay so you can see every teammate's
355
+ active sessions, project/team economics and insight views alongside your own, an in-hub janitor that
356
+ reaps stale claims centrally, and — eventually — a Console-backed `AssignmentProvider` so assignment is
357
+ authoritative across machines. It also makes CI a first-class, attributable participant (paired with
358
+ #398). See the [Flow Agents × Console integration doc](integrations/flow-agents-console.md) for the
359
+ phased plan.
360
+
361
+ ---
362
+
363
+ ## Provenance
364
+
365
+ This guide is the living reference. The decisions behind it are recorded, immutable, in the ADRs it
366
+ links; the ratified subject decisions live in [`docs/decisions/`](decisions/index.md)
367
+ ([agent-coordination](decisions/agent-coordination.md), [trust-reconcile](decisions/trust-reconcile.md)).
368
+ Shipped across issues #287 (actor identity), #288 (liveness), #289 (branches), #166 (pull-work), #320
369
+ (overlap), #290 (AssignmentProvider), #291 (ensure-session guard), #292 (stop-release), #356
370
+ (reconcile-preflight), #293 (verify-hold); #294 (takeover) and #398 (CI identity) forthcoming.
@@ -1,20 +1,37 @@
1
1
  ---
2
- status: needs-decision
2
+ status: current
3
3
  subject: Agent coordination
4
- decided: 2026-07-03
4
+ decided: 2026-07-04
5
5
  evidence:
6
6
  - kind: adr
7
7
  ref: docs/adr/0012-agent-coordination-as-liveness-claims.md
8
8
  - kind: adr
9
9
  ref: docs/adr/0021-assignment-leases-and-stale-claim-takeover.md
10
+ - kind: doc
11
+ ref: docs/coordination-guide.md
10
12
  ---
11
13
  # Agent coordination
12
14
 
13
- This subject has provenance in frozen ADR history ([0012-agent-coordination-as-liveness-claims.md](../adr/0012-agent-coordination-as-liveness-claims.md), [0021-assignment-leases-and-stale-claim-takeover.md](../adr/0021-assignment-leases-and-stale-claim-takeover.md)) but no living
14
- decision has been ratified yet under the topic-keyed decision registry
15
- (`context/contracts/decision-registry-contract.md`). This stub records that the
16
- subject is open and links the frozen ADR(s) as provenance; it is not a decision.
15
+ **Decision.** Parallel-session coordination is a **two-stream join**: an ephemeral,
16
+ TTL-reaped **liveness** stream ("who is working now") and a durable **assignment** record
17
+ ("who owns this") are joined at read time by `computeEffectiveState()` into one of four
18
+ effective states `free`, `held`, `reclaimable`, `human-held`. Selection, entry, and publish
19
+ are policies over that join. The model is **advisory by default with exactly one hard fence**
20
+ (the verify-hold publish gate), which itself enforces only for stable identities against durable
21
+ conflicts and degrades to advisory otherwise.
17
22
 
18
- When a living decision is ratified for agent coordination, update this
19
- file's `status` to `current`, add rationale, and keep the `adr` evidence
20
- links as provenance for the history that led here.
23
+ This is the **living reference**, kept current: [`docs/coordination-guide.md`](../coordination-guide.md).
24
+ The frozen ADRs ([0012](../adr/0012-agent-coordination-as-liveness-claims.md),
25
+ [0021](../adr/0021-assignment-leases-and-stale-claim-takeover.md)) remain immutable provenance
26
+ for *why* each decision was made.
27
+
28
+ **Rationale.** Two independent signals degrade gracefully when one is missing (liveness alone
29
+ can't tell a crash from a finish; assignment alone can't tell an active owner from a stale
30
+ lease), and an advisory-by-default posture keeps false blocks cheap while concentrating the one
31
+ place a false *miss* is expensive — publish — into a single, carefully-tuned gate.
32
+
33
+ **Shipped as** #287 (actor identity), #288 (liveness default-on), #289 (branches), #166
34
+ (pull-work), #320 (overlap detect-and-correct), #290 (AssignmentProvider), #291 (ensure-session
35
+ ownership guard), #292 (stop-hook clean release), #293 (verify-hold publish gate). Forthcoming:
36
+ #294 (takeover protocol), #398 (CI-runtime actor identity). The optional fleet tier is
37
+ [Flow Agents × Console](../integrations/flow-agents-console.md).
@@ -10,7 +10,7 @@ Numbered ADRs under `docs/adr/` are frozen history and are not listed here.
10
10
 
11
11
  | Topic | Status | Decision |
12
12
  | --- | --- | --- |
13
- | [agent-coordination](./agent-coordination.md) | needs-decision | Agent coordination |
13
+ | [agent-coordination](./agent-coordination.md) | current | Agent coordination |
14
14
  | [anti-gaming-trust-security](./anti-gaming-trust-security.md) | needs-decision | Anti-gaming trust security |
15
15
  | [context-lifecycle](./context-lifecycle.md) | needs-decision | Context lifecycle |
16
16
  | [core-domain-kit-boundary](./core-domain-kit-boundary.md) | needs-decision | Core vs domain kit boundary |
@@ -30,7 +30,7 @@ Numbered ADRs under `docs/adr/` are frozen history and are not listed here.
30
30
  | [promotion-gate](./promotion-gate.md) | current | Promotion gate |
31
31
  | [standing-directives](./standing-directives.md) | current | Standing directives |
32
32
  | [three-hard-boundary-model](./three-hard-boundary-model.md) | needs-decision | Three-hard-boundary model |
33
- | [trust-reconcile](./trust-reconcile.md) | needs-decision | Trust-reconcile and delivery reconciliation |
33
+ | [trust-reconcile](./trust-reconcile.md) | current | Trust-reconcile and delivery reconciliation |
34
34
  | [typescript-source-policy](./typescript-source-policy.md) | current | TypeScript-first source policy |
35
35
  | [workflow-enforcement](./workflow-enforcement.md) | needs-decision | Workflow Enforcement |
36
36
  | [workflow-trust-state](./workflow-trust-state.md) | needs-decision | Workflow trust state |
@@ -1,20 +1,53 @@
1
1
  ---
2
- status: needs-decision
2
+ status: current
3
3
  subject: Trust-reconcile and delivery reconciliation
4
- decided: 2026-07-03
4
+ decided: 2026-07-04
5
5
  evidence:
6
6
  - kind: adr
7
7
  ref: docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md
8
8
  - kind: adr
9
9
  ref: docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md
10
+ - kind: doc
11
+ ref: docs/coordination-guide.md
10
12
  ---
11
13
  # Trust-reconcile and delivery reconciliation
12
14
 
13
- This subject has provenance in frozen ADR history ([0020-trust-reconcile-manifest-and-claim-classification.md](../adr/0020-trust-reconcile-manifest-and-claim-classification.md), [0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md](../adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md)) but no living
14
- decision has been ratified yet under the topic-keyed decision registry
15
- (`context/contracts/decision-registry-contract.md`). This stub records that the
16
- subject is open and links the frozen ADR(s) as provenance; it is not a decision.
15
+ **Decision.** Delivery is **fail-closed with governed exemptions**. `publishDelivery()` passes a
16
+ bundle through three distinct, type-discriminated tiers fail-soft (absent bundle tolerated) →
17
+ shape gate (`InvalidBundleShapeError`, #356) hold gate (`NotFreshHolderError`, #293) — and CI's
18
+ reconcile is fail-closed by default (`onUnderivable: 'fail'`). A local `reconcile-preflight`
19
+ mirrors CI's shape check via the *shared* `scripts/lib/reconcile-shape.js` so the two cannot
20
+ drift. Concurrent deliveries are isolated by **per-session paths** (`delivery/<slug>/`, #379), and
21
+ a checkpoint's `commit_sha` must be an ancestor of HEAD (seal-at-parent) so a stale bundle can't be
22
+ replayed onto a moved branch. See the [coordination guide](../coordination-guide.md#how-delivery-itself-is-made-tamper-resistant).
17
23
 
18
- When a living decision is ratified for Trust-reconcile and delivery reconciliation, update this
19
- file's `status` to `current`, add rationale, and keep the `adr` evidence
20
- links as provenance for the history that led here.
24
+ **Rationale.** The default must be safe: an underivable status fails the run rather than passing
25
+ it, and a hard block on the shared publish path is applied only on high-confidence signals so it
26
+ never false-blocks legitimate work. The frozen ADRs ([0020](../adr/0020-trust-reconcile-manifest-and-claim-classification.md),
27
+ [0022](../adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md)) hold the
28
+ immutable rationale.
29
+
30
+ ## Implementation note (#356, iteration 1)
31
+
32
+ A local, pre-push `reconcile-preflight` now exists (`workflow-sidecar reconcile-preflight
33
+ <artifact-dir>`), reusing — never forking — the shape-classification logic CI's
34
+ `scripts/ci/trust-reconcile.js` enforces via the shared `scripts/lib/reconcile-shape.js`
35
+ module. `publishDelivery()` is itself fail-closed on shape-invalidity: it calls the same
36
+ preflight before copying anything into `delivery/`, and refuses to publish a bundle that
37
+ fails the shape check.
38
+
39
+ The reduced-coverage degradation (trusting a session-local claim's self-reported status when
40
+ CI-side re-derivation is unavailable) is a **LOCAL-preflight-only** opt-in
41
+ (`sessionLocalShapeIssues(..., { onUnderivable: 'reduce' })`). CI's `trust-reconcile.js`
42
+ always calls the shared function with `{ onUnderivable: 'fail' }` (also the function's
43
+ default when no mode is given) and remains fail-closed: when status re-derivation is
44
+ unavailable, every session-local pass-asserting claim becomes a `status-underivable`
45
+ divergence and the run fails, exactly as before the shape logic was extracted.
46
+
47
+ ## Implementation note (#379, per-session delivery paths)
48
+
49
+ Concurrent deliveries no longer contend on a shared `delivery/trust.bundle`. Each session writes
50
+ its bundle under `delivery/<slug>/`; CI selects prefer-newest. Publishing must **restore sibling
51
+ `delivery/<slug>/` directories** from `origin/main` before committing — after a soft-reset,
52
+ `git add -A` would otherwise stage the deletion of other sessions' delivery dirs that the branch
53
+ predates. This is the standard step in the publish sequence documented in the coordination guide.
@@ -22,9 +22,10 @@ run `npm run validate:source --` and `npm run fixture:retirement-audit --`.
22
22
  | `evals/fixtures/hook-influence` | hook influence behavioral cases | `evals/integration/test_hook_influence_cases.sh`, `evals/static/test_workflow_skills.sh`, `scripts/validate-hook-influence-cases.js` | Keep while hook influence cases define agent guidance behavior. |
23
23
  | `evals/fixtures/pull-work-provider` | work item provider normalization fixtures | `evals/integration/test_pull_work_provider.sh` | Keep while provider normalization preserves blockers, artifact refs, board membership, and freshness metadata. |
24
24
  | `evals/fixtures/pull-work-wip-shepherding` | WIP shepherding state fixtures | `evals/static/test_workflow_skills.sh` | Keep while pull-work documents personal versus global WIP behavior. |
25
+ | `evals/fixtures/reconcile-preflight` | #356 reconcile-preflight shape fixtures not already covered by trust-reconcile-exploits (un-superseded disputed critique, standalone disputed session-local claim) | `evals/integration/test_reconcile_preflight.sh` | Keep while the local `reconcile-preflight` subcommand (#356) is proven against the two shapes trust-reconcile-exploits does not already fixture (un-superseded disputed critique, standalone disputed session-local claim); the other four shapes reuse trust-reconcile-exploits/trust-reconcile-mixed-bundle directly rather than forking near-duplicates. |
25
26
  | `evals/fixtures/surface-trust` | Surface trust evidence fixtures | `evals/integration/test_workflow_sidecar_writer.sh` | Keep while sidecar writer maps Surface trust evidence into workflow records. |
26
- | `evals/fixtures/trust-reconcile-exploits` | WS8 trust-reconcile anti-gaming exploit fixtures (frozen negative regressions) | `evals/integration/test_trust_reconcile_negatives.sh` | Keep while trust-reconcile.js enforces the WS8 iteration-2 soundness properties (no-label test_output, unwaived-assumed, status-misassertion, waiver-on-command); each fixture is a permanent negative regression. |
27
- | `evals/fixtures/trust-reconcile-mixed-bundle` | WS8 trust-reconcile mixed-evidence end-to-end proof fixture | `evals/integration/test_trust_reconcile_mixed_bundle.sh` | Keep while the trust-reconcile manifest/classification/waiver contract (ADR 0020) is enforced; proves a mixed test_output + session-local + waived bundle passes the CI anchor. |
27
+ | `evals/fixtures/trust-reconcile-exploits` | WS8 trust-reconcile anti-gaming exploit fixtures (frozen negative regressions); also reused by the #356 local reconcile-preflight eval (same shapes, no forked copies) | `evals/integration/test_trust_reconcile_negatives.sh`, `evals/integration/test_reconcile_preflight.sh` | Keep while trust-reconcile.js enforces the WS8 iteration-2 soundness properties (no-label test_output, unwaived-assumed, status-misassertion, waiver-on-command); each fixture is a permanent negative regression. |
28
+ | `evals/fixtures/trust-reconcile-mixed-bundle` | WS8 trust-reconcile mixed-evidence end-to-end proof fixture; also reused by the #356 reconcile-preflight eval as its CLEAN-BUNDLE (AC4) case | `evals/integration/test_trust_reconcile_mixed_bundle.sh`, `evals/integration/test_reconcile_preflight.sh` | Keep while the trust-reconcile manifest/classification/waiver contract (ADR 0020) is enforced; proves a mixed test_output + session-local + waived bundle passes the CI anchor. |
28
29
  | `evals/fixtures/trust-reconcile-ws3` | WS8 AC6 backward-compat fixture: real ws3-kit-dependencies-namespacing old-style bundle | `evals/integration/test_trust_reconcile_negatives.sh` | Keep while backward compatibility with pre-classification (all-test_output) bundles is asserted; proves an old-style bundle still FAILS the same way (no silent pass). |
29
30
  | `evals/fixtures/usage-feedback` | usage feedback import/outcome fixtures | `evals/integration/test_usage_feedback_import.sh`, `evals/integration/test_usage_feedback_outcomes.sh`, `evals/integration/test_usage_feedback_report.sh` | Keep while usage feedback import, outcome, and report flows accept JSONL fixture input. |
30
31
  | `evals/fixtures/veritas-governance-adapter` | Veritas governance adapter fixtures | `evals/integration/test_veritas_governance_adapter.sh` | Keep while the Veritas adapter supports pass, unconfigured, and secret-failure governance paths. |