@kontourai/flow-agents 3.0.0 → 3.1.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 (131) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +15 -0
  3. package/CONTEXT.md +67 -1
  4. package/README.md +4 -0
  5. package/build/src/cli/assignment-provider.d.ts +143 -0
  6. package/build/src/cli/assignment-provider.js +106 -49
  7. package/build/src/cli/workflow-sidecar.d.ts +2 -2
  8. package/build/src/cli/workflow-sidecar.js +392 -35
  9. package/build/src/lib/flow-resolver.d.ts +12 -6
  10. package/build/src/lib/flow-resolver.js +30 -14
  11. package/build/src/tools/validate-source-tree.js +2 -1
  12. package/context/contracts/assignment-provider-contract.md +9 -0
  13. package/context/contracts/probe-docs-write-contract.md +187 -0
  14. package/context/scripts/hooks/config-protection.js +14 -1
  15. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  16. package/context/scripts/hooks/stop-goal-fit.js +4 -2
  17. package/context/scripts/hooks/workflow-steering.js +42 -0
  18. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  19. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  20. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  21. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  22. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  23. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  24. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  25. package/docs/adr/0007-skill-audit.md +2 -0
  26. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  27. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  28. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  29. package/docs/adr/0011-mcp-posture.md +2 -0
  30. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  31. package/docs/adr/0013-context-lifecycle.md +2 -0
  32. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  33. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  34. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  35. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  36. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  37. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  38. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  39. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  40. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +2 -0
  41. package/docs/adr/README.md +49 -0
  42. package/docs/adr/index.md +34 -0
  43. package/docs/decisions/agent-coordination.md +20 -0
  44. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  45. package/docs/decisions/context-lifecycle.md +18 -0
  46. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  47. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  48. package/docs/decisions/flow-kit.md +20 -0
  49. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  50. package/docs/decisions/graph-knowledge-provider.md +63 -0
  51. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  52. package/docs/decisions/index.md +19 -0
  53. package/docs/decisions/kit-dependency-ownership.md +18 -0
  54. package/docs/decisions/kit-operation-boundary.md +18 -0
  55. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  56. package/docs/decisions/kontour-resource-contract.md +18 -0
  57. package/docs/decisions/mcp-posture.md +18 -0
  58. package/docs/decisions/three-hard-boundary-model.md +18 -0
  59. package/docs/decisions/trust-reconcile.md +20 -0
  60. package/docs/decisions/typescript-source-policy.md +48 -0
  61. package/docs/decisions/workflow-enforcement.md +18 -0
  62. package/docs/decisions/workflow-trust-state.md +20 -0
  63. package/docs/fixture-ownership.md +1 -1
  64. package/docs/workflow-usage-guide.md +1 -1
  65. package/evals/ci/run-baseline.sh +4 -0
  66. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  67. package/evals/integration/test_current_json_per_actor.sh +516 -0
  68. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  69. package/evals/integration/test_gate_lockdown.sh +10 -0
  70. package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
  71. package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
  72. package/evals/run.sh +4 -0
  73. package/evals/static/test_knowledge_providers.sh +13 -4
  74. package/evals/static/test_workflow_skills.sh +15 -2
  75. package/kits/builder/skills/deliver/SKILL.md +17 -0
  76. package/kits/builder/skills/design-probe/SKILL.md +37 -0
  77. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  78. package/kits/knowledge/adapters/default-store/index.js +92 -4
  79. package/kits/knowledge/adapters/obsidian-store/index.js +69 -4
  80. package/kits/knowledge/adapters/shared/codec.js +141 -0
  81. package/kits/knowledge/docs/README.md +121 -2
  82. package/kits/knowledge/docs/store-contract.md +112 -4
  83. package/kits/knowledge/evals/contract-suite/suite.test.js +176 -0
  84. package/kits/knowledge/flows/promote.flow.json +84 -0
  85. package/kits/knowledge/kit.json +15 -0
  86. package/kits/knowledge/promote/distill.js +96 -0
  87. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  88. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  89. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  90. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  91. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  92. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  93. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  94. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  95. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  96. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  97. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  98. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  99. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  100. package/kits/knowledge/promote/health.js +137 -0
  101. package/kits/knowledge/promote/index.js +176 -0
  102. package/kits/knowledge/promote/ingest.js +103 -0
  103. package/kits/knowledge/promote/lib.js +132 -0
  104. package/kits/knowledge/promote/link.js +84 -0
  105. package/kits/knowledge/promote/promote.test.js +174 -0
  106. package/kits/knowledge/providers/conformance/suite.test.js +18 -0
  107. package/kits/knowledge/providers/index.js +1 -0
  108. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  109. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  110. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  111. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  112. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  113. package/kits/knowledge/providers/neo4j/index.js +280 -0
  114. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  115. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  116. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  117. package/package.json +4 -2
  118. package/scripts/README.md +1 -0
  119. package/scripts/freeze-adrs.mjs +364 -0
  120. package/scripts/hooks/config-protection.js +14 -1
  121. package/scripts/hooks/evidence-capture.js +4 -1
  122. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  123. package/scripts/hooks/lib/current-pointer.js +123 -0
  124. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  125. package/scripts/hooks/stop-goal-fit.js +4 -2
  126. package/scripts/hooks/workflow-steering.js +42 -0
  127. package/scripts/statusline/flow-agents-statusline.js +3 -1
  128. package/src/cli/assignment-provider.ts +137 -55
  129. package/src/cli/workflow-sidecar.ts +430 -33
  130. package/src/lib/flow-resolver.ts +35 -14
  131. package/src/tools/validate-source-tree.ts +2 -1
@@ -161,6 +161,14 @@ jobs:
161
161
  continue-on-error: true
162
162
  run: bash evals/ci/run-baseline.sh --check pull-work-assignment-join-integration
163
163
 
164
+ - name: Ensure-session ownership guard integration
165
+ continue-on-error: true
166
+ run: bash evals/ci/run-baseline.sh --check ensure-session-ownership-guard-integration
167
+
168
+ - name: Current.json per-actor integration
169
+ continue-on-error: true
170
+ run: bash evals/ci/run-baseline.sh --check current-json-per-actor-integration
171
+
164
172
  - name: Finalize CI evidence
165
173
  if: always()
166
174
  run: bash evals/ci/run-baseline.sh --finalize
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.1.0](https://github.com/kontourai/flow-agents/compare/v3.0.0...v3.1.0) (2026-07-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * **docs:** probe docs-write contract — vocabulary + decision deltas ([#311](https://github.com/kontourai/flow-agents/issues/311)) ([#371](https://github.com/kontourai/flow-agents/issues/371)) ([867c3b5](https://github.com/kontourai/flow-agents/commit/867c3b5b645c896e5df6886177171b05e73e8fa9))
9
+ * **knowledge:** neo4j knowledge-store provider — Cypher-backed [#317](https://github.com/kontourai/flow-agents/issues/317) interface (opt-in default) ([#373](https://github.com/kontourai/flow-agents/issues/373)) ([2ef2523](https://github.com/kontourai/flow-agents/commit/2ef2523cc65a5e56dec8dcbf84edc3718cfe190c))
10
+ * **knowledge:** promote sub-flow — ingest→distill→link→health (flow within a flow) ([#313](https://github.com/kontourai/flow-agents/issues/313)) ([#372](https://github.com/kontourai/flow-agents/issues/372)) ([b345d89](https://github.com/kontourai/flow-agents/commit/b345d89873c99f65f00d0fe8ede6a8702b1a2b43))
11
+ * **knowledge:** stable record identity — short-id prefix + slug aliases ([#339](https://github.com/kontourai/flow-agents/issues/339)) ([ff54bc7](https://github.com/kontourai/flow-agents/commit/ff54bc74f893321f13276cd147d500f91e1fc133))
12
+
13
+
14
+ ### Documentation
15
+
16
+ * **adr:** ADR freeze cutover + liveness coordination experiment ([#332](https://github.com/kontourai/flow-agents/issues/332)) ([#368](https://github.com/kontourai/flow-agents/issues/368)) ([2c845b8](https://github.com/kontourai/flow-agents/commit/2c845b81675db02622fee8fa1efa6aa197ce96c0))
17
+
3
18
  ## [3.0.0](https://github.com/kontourai/flow-agents/compare/v2.4.0...v3.0.0) (2026-07-04)
4
19
 
5
20
 
package/CONTEXT.md CHANGED
@@ -66,6 +66,16 @@ _Avoid_: Graph database as the generic term (the model is provider-independent;
66
66
  A configured implementation of the knowledge-store capability behind the Knowledge Graph model. Providers expose a read interface (nodes, edges, query-by-type) and a proposals-only write interface (proposeWrite returns a proposal, never mutating a human-curated store). The reference providers are markdown-vault (the Obsidian-shaped vault), git-repo (decision registry, CONTEXT.md vocabulary, learnings), and work-item (GitHub issues as a source/sink adapter). See context/contracts/knowledge-store-contract.md.
67
67
  _Avoid_: Knowledge adapter when contrasting the provider interface with a single store adapter
68
68
 
69
+ ### Graph Knowledge Provider
70
+
71
+ The owner's opt-in Neo4j-backed Knowledge Store Provider (the `neo4j` provider). A queryable materialized view of the Knowledge Graph, synced from the file/work-item providers with idempotent MERGE semantics — the file stores stay the source of truth and the write side stays proposals-only. Selected via `KNOWLEDGE_PROVIDER=neo4j`; degrades to the file providers when no Neo4j is reachable. The file providers remain the portfolio default; the graph is a personal default only. See docs/decisions/graph-knowledge-provider.md.
72
+ _Avoid_: Graph database as the default store (it is opt-in and a view, not the source of truth)
73
+
74
+ ### Knowledge Promote Sub-Flow
75
+
76
+ The Knowledge Kit's codebase-facing pipeline (the "flow within a flow") that a delivered session's promotion runs through: ingest the session artifacts, distill schema-valid draft decision/vocabulary/learning deltas, link their provenance (PR, merge SHA, session archive, touched topics), and health-check the registry for contradictions with a merge-repair proposal. It is proposals-only — every output is a draft under the session's proposals directory that the promote step applies; the sub-flow never writes docs directly. Invokable standalone and composable from the Builder promote step. See docs/decisions/knowledge-promote-sub-flow.md.
77
+ _Avoid_: Promotion gate as a synonym (the gate is the recorded promote claim; this is the assisted pipeline)
78
+
69
79
  ### Kontour Resource Contract
70
80
 
71
81
  A versioned Kontour record shape for durable machine-readable configuration, scope, run state, evidence, provider output, and cross-product interchange. Kontour Resource Contracts are the default for new pre-public durable contracts unless a product records why a native shape is clearer.
@@ -225,7 +235,7 @@ A reason code for a Gate that cannot be evaluated or cannot pass because a requi
225
235
 
226
236
  ### Probe
227
237
 
228
- A Builder Kit Alignment Gate that explores context, challenges assumptions, and records aligned decisions before the process continues. A Probe asks one question at a time, recommends an answer, and uses repository context before asking when the answer can be discovered.
238
+ A Builder Kit Alignment Gate that explores context, challenges assumptions, and records aligned decisions before the process continues. A Probe asks one question at a time, recommends an answer, and uses repository context before asking when the answer can be discovered. As understanding crystallizes, a Probe writes it into durable docs in the same motion: a vocabulary delta into this glossary and a decision delta into the Decision Registry, per [context/contracts/probe-docs-write-contract.md](context/contracts/probe-docs-write-contract.md).
229
239
  _Avoid_: Grill, interrogation
230
240
 
231
241
  ### Flow
@@ -328,6 +338,62 @@ The Console overview for global setup, registered projects, cross-project usage,
328
338
 
329
339
  The shared tool layer used by the Console, CLI, AI agents, and automation. The Control API owns operations such as reading effective settings, explaining provider resolution, testing provider health, previewing config changes, writing config, inspecting workflow state, and reporting usage or eval outcomes.
330
340
 
341
+ ### Workflow trust state
342
+
343
+ The trust bundle a workflow gate reads to decide whether to advance — claims, evidence, verification events, and derived status expressed as a Hachure Trust Bundle — so gates consume inspectable trust state rather than raw tool output. Provenance lives in frozen ADRs; the subject is open in the Decision Registry as [docs/decisions/workflow-trust-state.md](docs/decisions/workflow-trust-state.md).
344
+
345
+ ### TypeScript-first source policy
346
+
347
+ The policy that Kontour product and runtime source defaults to TypeScript, with narrow JavaScript/MJS exceptions (config/tooling, generated assets, thin launchers, fixtures, historical artifacts) and staged per-repo migration for existing non-TypeScript source. Ratified in the Decision Registry as [docs/decisions/typescript-source-policy.md](docs/decisions/typescript-source-policy.md).
348
+
349
+ ### Flow / Skill / Kit / Tool boundary
350
+
351
+ The layering that separates a Flow (workflow semantics) from a Skill (agent-facing procedure), a Flow Kit (installable bundle), and a Tool (an executable operation), so each concern has one home and does not leak into the others. Subject open in the Decision Registry as [docs/decisions/flow-skill-kit-tool-boundary.md](docs/decisions/flow-skill-kit-tool-boundary.md).
352
+
353
+ ### Kit operation boundary
354
+
355
+ The rule for what a kit-owned operation may do versus what belongs to the core, keeping kit operations scoped to their capability and free of core enforcement responsibilities. Subject open in the Decision Registry as [docs/decisions/kit-operation-boundary.md](docs/decisions/kit-operation-boundary.md).
356
+
357
+ ### Hook core/kit boundary
358
+
359
+ The division between canonical hook behavior owned by the core and hook contributions owned by kits, so enforcement hooks have a single authoritative implementation. Subject open in the Decision Registry as [docs/decisions/hook-core-kit-boundary.md](docs/decisions/hook-core-kit-boundary.md).
360
+
361
+ ### MCP posture
362
+
363
+ Flow Agents' stance on the Model Context Protocol: enforcement stays in hooks, Surface owns any MCP projection, and no MCP configuration is auto-injected into a runtime. Subject open in the Decision Registry as [docs/decisions/mcp-posture.md](docs/decisions/mcp-posture.md).
364
+
365
+ ### Agent coordination
366
+
367
+ How concurrent agents avoid stepping on each other's work, modeled as Hachure liveness claims plus assignment leases with stale-claim takeover, so a work item's holder is advisory-visible and reclaimable when stale. Subject open in the Decision Registry as [docs/decisions/agent-coordination.md](docs/decisions/agent-coordination.md).
368
+
369
+ ### Context lifecycle
370
+
371
+ The lifecycle of an agent's working context — workflow-boundary compaction, freshness-gated reuse, and the split between durable learnings and ephemeral context — so context is refreshed rather than silently stale. Subject open in the Decision Registry as [docs/decisions/context-lifecycle.md](docs/decisions/context-lifecycle.md).
372
+
373
+ ### Core vs domain kit boundary
374
+
375
+ The generic/kit boundary that keeps the Flow Agents core domain-agnostic while domain behavior lives in kits, so the core carries no kit-specific knowledge. Subject open in the Decision Registry as [docs/decisions/core-domain-kit-boundary.md](docs/decisions/core-domain-kit-boundary.md).
376
+
377
+ ### Flow / Flow Agents boundary
378
+
379
+ The reconciled division of responsibility between Flow (the workflow engine consumed for enforcement) and Flow Agents (the product that consumes it), so each owns a distinct layer without duplicating the other. Subject open in the Decision Registry as [docs/decisions/flow-flow-agents-boundary.md](docs/decisions/flow-flow-agents-boundary.md).
380
+
381
+ ### Three-hard-boundary model
382
+
383
+ The FlowDefinition-driven, kit-agnostic model that names the three hard boundaries the core enforces, unifying the individual boundary decisions into one architecture. Subject open in the Decision Registry as [docs/decisions/three-hard-boundary-model.md](docs/decisions/three-hard-boundary-model.md).
384
+
385
+ ### Anti-gaming trust security
386
+
387
+ The layered-defense trust security model that assumes the local agent can be gamed and anchors enforcement in an external CI check, freezing the local shell-parsing heuristics and routing new enforcement to the CI anchor. Subject open in the Decision Registry as [docs/decisions/anti-gaming-trust-security.md](docs/decisions/anti-gaming-trust-security.md).
388
+
389
+ ### Kit dependency ownership
390
+
391
+ The rule for which layer owns a kit's runtime dependencies, keeping dependency declaration and installation with the kit that needs them rather than the core. Subject open in the Decision Registry as [docs/decisions/kit-dependency-ownership.md](docs/decisions/kit-dependency-ownership.md).
392
+
393
+ ### Trust-reconcile and delivery reconciliation
394
+
395
+ The CI-anchored reconciliation of a session's trust claims against a manifest — classifying command, session-local, and attested claims, honoring governed waivers — and the fail-closed delivery reconciliation that blocks publication on unreconciled residue unless an exemption is recorded. Subject open in the Decision Registry as [docs/decisions/trust-reconcile.md](docs/decisions/trust-reconcile.md).
396
+
331
397
  ### Model Routing
332
398
 
333
399
  The policy that maps a delegate role name (such as `delegate-mechanical`, `delegate-implementation`, `delegate-design`, `orchestrator`, `extraction-default`) to a specific `model@provider` ref. Model Routing is data, not code: it lives in `.datum/config.json` (read by the `@kontourai/datum` registry, schema `datum.schema.json`) and never in generated files or per-agent frontmatter. The orchestrator resolves the role at delegation time (`datum resolve <role> --json`) and passes the resolved model explicitly when spawning each delegate. See [context/contracts/execution-contract.md](context/contracts/execution-contract.md) § Delegation: Model Routing and [docs/decisions/model-routing.md](docs/decisions/model-routing.md).
package/README.md CHANGED
@@ -217,6 +217,10 @@ See [Repository Structure](docs/repository-structure.md) for the canonical map.
217
217
  - `kits/`, `schemas/`, `packaging/`, `evals/`, `docs/`, and `integrations/` hold Flow Kit assets, contracts, package metadata, evals, durable docs, and optional integrations.
218
218
  - `dist/`, `build/`, and `_site/` are generated output. Local `.kontourai/`, `.flow-agents/`, `.codex/`, `.claude/`, telemetry, promptfoo, Veritas, and cache directories stay ignored; durable outcomes belong in docs, source, schemas, or provider records.
219
219
 
220
+ ## Graph provider (opt-in)
221
+
222
+ The Knowledge Kit ships an optional `neo4j` graph provider — the owner's opt-in personal default, while the file providers remain the portfolio default. It is a queryable **materialized view** synced from the file/work-item stores (which stay the source of truth), with Cypher-backed health/query verbs when selected and graceful degradation to the file providers when no Neo4j is reachable (never a hard dependency). Opt in with `KNOWLEDGE_PROVIDER=neo4j` and see [Graph provider (opt-in)](kits/knowledge/docs/README.md#graph-provider-opt-in) for the `docker run` one-liner, env vars, sync command, and an example Cypher session.
223
+
220
224
  ## Documentation
221
225
 
222
226
  The [GitHub Pages site](https://kontourai.github.io/flow-agents/) is the product overview and quickstart. The repository docs are the developer reference for contracts, workflow behavior, evals, and integration boundaries. For the gate semantics underneath — definitions, runs, evidence, route-back — read the [Kontour Flow documentation](https://kontourai.github.io/flow/).
@@ -1 +1,144 @@
1
+ export type ActorStruct = {
2
+ runtime: string;
3
+ session_id: string;
4
+ host: string;
5
+ human?: string | null;
6
+ };
7
+ type AssignmentClaimRecordStatus = "claimed" | "released" | "superseded";
8
+ type AssignmentAuditEntry = {
9
+ at: string;
10
+ transition: "claim" | "release" | "supersede";
11
+ from_actor?: ActorStruct | null;
12
+ to_actor?: ActorStruct | null;
13
+ reason?: string;
14
+ };
15
+ /**
16
+ * The versioned claim-record shape from the contract doc's "Versioned claim-record format"
17
+ * section (Design Decision 2). `schema_version` is bumped only on an incompatible change, per
18
+ * artifact-contract.md's existing sidecar rule.
19
+ *
20
+ * `actor_key` (F1 fix, fix-plan iteration 1, HIGH — additive field, schema_version unchanged):
21
+ * the canonical `resolveActor(env).actor` string for the claiming actor — the SAME flat/bare
22
+ * token every other tool (`liveness whoami`, `liveness claim --actor`, per-actor current.json,
23
+ * pull-work's `--self-actor`) already uses. Optional so every pre-fix record and every #290 eval
24
+ * fixture with no `actor_key` still parses; `computeEffectiveState` falls back to
25
+ * `serializeActor(record.actor)` (today's behavior) whenever it's absent. Present, it is the ONLY
26
+ * correct self-recognition/liveness-join key — see computeEffectiveState's holderActorKey.
27
+ */
28
+ export type AssignmentClaimRecord = {
29
+ schema_version: "1.0";
30
+ role: "AssignmentClaimRecord";
31
+ subject_id: string;
32
+ actor: ActorStruct;
33
+ actor_key?: string;
34
+ claimed_at: string;
35
+ ttl_seconds: number;
36
+ branch: string;
37
+ artifact_dir: string;
38
+ status: AssignmentClaimRecordStatus;
39
+ audit_trail?: AssignmentAuditEntry[];
40
+ };
41
+ export type FreshHolder = {
42
+ actor: string;
43
+ lastAt: string;
44
+ ttlSeconds: number;
45
+ fresh: boolean;
46
+ };
47
+ export type EffectiveState = "held" | "reclaimable" | "human-held" | "free";
48
+ /** Provider-neutral assignment-layer read, before any liveness join (contract doc's status()). */
49
+ export type AssignmentStatus = {
50
+ subject_id: string;
51
+ provider: "local-file" | "github";
52
+ assignee: string | null;
53
+ record: AssignmentClaimRecord | null;
54
+ has_claim_label?: boolean;
55
+ };
56
+ export declare function assignmentFilePath(artifactRoot: string, subjectId: string): string;
57
+ export declare function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null;
58
+ export declare function writeLocalRecord(artifactRoot: string, subjectId: string, record: AssignmentClaimRecord): void;
59
+ /**
60
+ * F1 fix (fix-plan iteration 1, CRITICAL): claimLocalFile/releaseLocalFile/supersedeLocalFile were
61
+ * a plain read -> compare-actor -> write with no lock, so two concurrently-launched OS processes
62
+ * could both read "no conflicting claim" before either wrote, and the second write would silently
63
+ * clobber the first with zero error and zero audit-trail entry for the loser (reproduced 29/40
64
+ * races against the built CLI). This mirrors the EXACT mechanism `withLock` already uses in
65
+ * workflow-sidecar.ts:908 for the same class of shared-state mutation — atomic `fs.mkdirSync`
66
+ * lockdir create as the mutual-exclusion primitive, EEXIST-spin with a staleness-reclaim check
67
+ * (a lock directory older than the stale threshold is presumed abandoned by a crashed process and
68
+ * is reclaimed rather than waited on forever) and a bounded deadline, `finally` rmSync release —
69
+ * as a small LOCAL helper (not a cross-import of that private function, which would pull the
70
+ * entire workflow-sidecar module in for one primitive). Deliberately synchronous (sleepSync's
71
+ * Atomics.wait spin, not setTimeout/await) so claim/release/supersede can stay sync `number`
72
+ * -returning functions and the CLI dispatcher (src/cli.ts, `number | Promise<number>`) does not
73
+ * need any ripple to async. On lock-acquire failure (any error other than a live contested lock,
74
+ * or a timeout waiting one out) this THROWS — never a silent no-op — "fail loud, never fail-open"
75
+ * (artifact-contract.md). Wrap the ENTIRE read-modify-write body (the existing-claim check AND
76
+ * the write) of all three local-file mutators in this, since all three mutate the same record
77
+ * file for a given subject.
78
+ */
79
+ export declare function withSubjectLock<T>(artifactRoot: string, subjectId: string, body: () => T): T;
80
+ /**
81
+ * The assignment ⋈ liveness join (contract doc's "assignment ⋈ liveness join" section, ADR 0021
82
+ * §1). Pure function: `{ assignment, freshHoldersList, selfActor, nowMs }` -> one of five
83
+ * effective states (held/reclaimable/human-held/free — "held" covers both the plain and
84
+ * assignment-lagging rows, matching the contract table's own repeated "held" label).
85
+ *
86
+ * The human-assignee gate (AC11, Design Decision 3) reads `record.actor.human` being *present*,
87
+ * never a username heuristic — an idle human assignment is always `human-held`, regardless of
88
+ * idle duration, and is never auto-reclaimable by this function.
89
+ *
90
+ * `nowMs` (F3 fix, fix-plan iteration 1) is the SAME resolved "now" the caller already threads
91
+ * into `freshHolders()` (the `--now` override, when passed, else `Date.now()`) — passing it
92
+ * through here too means `--now` deterministically governs idle_days as well as liveness
93
+ * freshness, rather than idle_days silently reading the real wall clock regardless of `--now`.
94
+ */
95
+ export declare function computeEffectiveState(assignment: AssignmentStatus, freshHoldersList: FreshHolder[], selfActor: string | undefined, nowMs: number): {
96
+ effective_state: EffectiveState;
97
+ reason: string;
98
+ holder?: {
99
+ actor?: string;
100
+ assignee?: string | null;
101
+ idle_days?: number | null;
102
+ last_at?: string;
103
+ };
104
+ };
105
+ /**
106
+ * Wave 1 (#291) extraction: the durable-write body previously inlined inside claimLocalFile's
107
+ * withSubjectLock() closure, now a parameter-driven pure function so ensure-session's ownership
108
+ * guard (workflow-sidecar.ts, Wave 2) can reuse the EXACT same claim logic — same-actor idempotent
109
+ * refresh, different-actor throw, atomic write under withSubjectLock — rather than reimplementing
110
+ * a second, parallel claim path. claimLocalFile (CLI wrapper, below) is now a thin
111
+ * parse-args/print-envelope shell around this.
112
+ */
113
+ export declare function performLocalClaim(artifactRoot: string, subjectId: string, actor: ActorStruct, opts: {
114
+ ttlSeconds: number;
115
+ branch: string;
116
+ artifactDir: string;
117
+ reason?: string;
118
+ actorKey?: string;
119
+ }): AssignmentClaimRecord;
120
+ /**
121
+ * Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
122
+ * withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
123
+ * `--supersede-stale` takeover path (workflow-sidecar.ts, Wave 2) can reuse the EXACT same
124
+ * supersede logic — from-actor holder verification, ttl/branch/artifact_dir carry-forward,
125
+ * audit-trail append, atomic write under withSubjectLock — rather than reimplementing a second,
126
+ * parallel supersede path. supersedeLocalFile (CLI wrapper, below) is now a thin
127
+ * parse-args/print-envelope shell around this.
128
+ */
129
+ export declare function performLocalSupersede(artifactRoot: string, subjectId: string, fromActor: ActorStruct, toActor: ActorStruct, opts?: {
130
+ ttlSeconds?: number;
131
+ branch?: string;
132
+ artifactDir?: string;
133
+ reason?: string;
134
+ actorKey?: string;
135
+ }): AssignmentClaimRecord;
136
+ /**
137
+ * Wave 1 (#291) extraction: the local-file branch of statusCommand's assignment-layer read,
138
+ * mirrored exactly so ensure-session's ownership guard (workflow-sidecar.ts, Wave 2) derives an
139
+ * AssignmentStatus identically to the `assignment-provider status` CLI command — a single
140
+ * implementation, not a second parallel local-file read.
141
+ */
142
+ export declare function readLocalAssignmentStatus(artifactRoot: string, subjectId: string): AssignmentStatus;
1
143
  export declare function main(argv?: string[]): number;
144
+ export {};
@@ -64,22 +64,29 @@ function loadActorStructFromFile(file) {
64
64
  * fixture-friendly path (used by evals and any caller that already knows its own struct);
65
65
  * when omitted, auto-derive from the live environment via the shared resolver, mirroring
66
66
  * (never forking) the exact struct fields serializeActor() already defines.
67
+ *
68
+ * F1 fix (fix-plan iteration 1, HIGH): also returns `actorKey` — set to the canonical
69
+ * `resolveActor(env).actor` string ONLY on the auto-derive path (pull-work's real path, and any
70
+ * other caller with no --actor-json), so a claim made via `assignment-provider claim` and one
71
+ * made via ensure-session share the same canonical key. `--actor-json` explicit fixtures leave
72
+ * `actorKey` unset — `performLocalClaim`/`performLocalSupersede` then fall back to
73
+ * `serializeActor(actor)` for the record's `actor_key`, preserving existing fixture behavior.
67
74
  */
68
75
  function loadActorStruct(args) {
69
76
  const actorJsonPath = flagString(args.flags, "actor-json");
70
77
  if (actorJsonPath)
71
- return loadActorStructFromFile(actorJsonPath);
78
+ return { actor: loadActorStructFromFile(actorJsonPath) };
72
79
  const helper = loadActorIdentityHelper();
73
80
  const resolved = helper.resolveActor(process.env);
74
81
  if (helper.isUnresolvedActor(resolved.actor))
75
82
  throw new Error("could not resolve an actor identity (no --actor-json and no resolvable environment actor); pass --actor-json explicitly");
76
- return { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null };
83
+ return { actor: { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null }, actorKey: resolved.actor };
77
84
  }
78
- function assignmentFilePath(artifactRoot, subjectId) {
85
+ export function assignmentFilePath(artifactRoot, subjectId) {
79
86
  const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
80
87
  return path.join(artifactRoot, "assignment", `${sanitized}.json`);
81
88
  }
82
- function readLocalRecord(artifactRoot, subjectId) {
89
+ export function readLocalRecord(artifactRoot, subjectId) {
83
90
  const file = assignmentFilePath(artifactRoot, subjectId);
84
91
  if (!fs.existsSync(file))
85
92
  return null;
@@ -99,7 +106,7 @@ function readLocalRecord(artifactRoot, subjectId) {
99
106
  throw new Error(`${file}: unsupported schema_version ${String(record.schema_version)}`);
100
107
  return record;
101
108
  }
102
- function writeLocalRecord(artifactRoot, subjectId, record) {
109
+ export function writeLocalRecord(artifactRoot, subjectId, record) {
103
110
  // writeJson throws on any mkdir/writeFileSync failure; that error is intentionally allowed to
104
111
  // propagate to main()'s top-level try/catch and exit non-zero. Durable writes must fail loud,
105
112
  // never fail open (artifact-contract.md).
@@ -142,7 +149,7 @@ function subjectLockDir(artifactRoot, subjectId) {
142
149
  * the write) of all three local-file mutators in this, since all three mutate the same record
143
150
  * file for a given subject.
144
151
  */
145
- function withSubjectLock(artifactRoot, subjectId, body) {
152
+ export function withSubjectLock(artifactRoot, subjectId, body) {
146
153
  const lockDir = subjectLockDir(artifactRoot, subjectId);
147
154
  const staleMs = Number(process.env.FLOW_AGENTS_ASSIGNMENT_STALE_LOCK_MS ?? 5 * 60 * 1000);
148
155
  const deadline = Date.now() + 30000;
@@ -196,7 +203,7 @@ function withSubjectLock(artifactRoot, subjectId, body) {
196
203
  * through here too means `--now` deterministically governs idle_days as well as liveness
197
204
  * freshness, rather than idle_days silently reading the real wall clock regardless of `--now`.
198
205
  */
199
- function computeEffectiveState(assignment, freshHoldersList, selfActor, nowMs) {
206
+ export function computeEffectiveState(assignment, freshHoldersList, selfActor, nowMs) {
200
207
  const record = assignment.record && assignment.record.status === "claimed" ? assignment.record : null;
201
208
  const isAssigned = Boolean(assignment.assignee) || Boolean(record);
202
209
  if (!isAssigned) {
@@ -222,7 +229,14 @@ function computeEffectiveState(assignment, freshHoldersList, selfActor, nowMs) {
222
229
  // human's work (Design Decision 3 / ADR 0021 §6's "never auto-reclaim" non-goal).
223
230
  return { effective_state: "human-held", reason: "assignee_without_claim_record", holder: { assignee: assignment.assignee } };
224
231
  }
225
- const holderActorKey = loadActorIdentityHelper().serializeActor(record.actor);
232
+ // F1 fix (fix-plan iteration 1, HIGH): prefer the canonical actor_key over re-serializing
233
+ // record.actor. record.actor_key IS resolveActor(env).actor for records written by the fixed
234
+ // performLocalClaim/performLocalSupersede paths (below) — the same flat/bare string liveness
235
+ // whoami, `liveness claim --actor`, per-actor current.json, and pull-work's --self-actor all
236
+ // use — so for an explicit-override actor the self-check and the liveness join now agree with
237
+ // every other tool. BACK-COMPAT: records with no actor_key (every pre-fix record, every #290
238
+ // eval fixture) fall back to serializeActor(record.actor) exactly as before this fix.
239
+ const holderActorKey = record.actor_key || loadActorIdentityHelper().serializeActor(record.actor);
226
240
  if (selfActor && holderActorKey === selfActor)
227
241
  return { effective_state: "held", reason: "self_is_holder", holder: { actor: holderActorKey } };
228
242
  const fresh = freshHoldersList.find((holder) => holder.actor === holderActorKey);
@@ -349,22 +363,17 @@ function loadLivenessInputs(args) {
349
363
  }
350
364
  // ─── local-file: claim | release | supersede (the durable-write path; real I/O by design — no
351
365
  // external mutation to defer to a skill for this provider kind, per Design Decision 1) ─────────
352
- function claimLocalFile(argv) {
353
- const args = parseArgs(argv);
354
- const provider = flagString(args.flags, "provider", "local-file");
355
- if (provider !== "local-file")
356
- throw new Error(`claim: --provider must be local-file (use render-claim for github); got ${provider}`);
357
- const artifactRoot = requireFlag(args, "artifact-root");
358
- const subjectId = requireFlag(args, "subject-id");
359
- const actor = loadActorStruct(args);
366
+ /**
367
+ * Wave 1 (#291) extraction: the durable-write body previously inlined inside claimLocalFile's
368
+ * withSubjectLock() closure, now a parameter-driven pure function so ensure-session's ownership
369
+ * guard (workflow-sidecar.ts, Wave 2) can reuse the EXACT same claim logic — same-actor idempotent
370
+ * refresh, different-actor throw, atomic write under withSubjectLock rather than reimplementing
371
+ * a second, parallel claim path. claimLocalFile (CLI wrapper, below) is now a thin
372
+ * parse-args/print-envelope shell around this.
373
+ */
374
+ export function performLocalClaim(artifactRoot, subjectId, actor, opts) {
360
375
  const helper = loadActorIdentityHelper();
361
- const ttlSecondsRaw = flagString(args.flags, "ttl-seconds", "1800") ?? "1800";
362
- const ttlSeconds = Number(ttlSecondsRaw);
363
- if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
364
- throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
365
- const branch = requireFlag(args, "branch");
366
- const artifactDir = requireFlag(args, "artifact-dir");
367
- const reason = flagString(args.flags, "reason") ?? "claim";
376
+ const reason = opts.reason ?? "claim";
368
377
  // F1 fix (fix-plan iteration 1, CRITICAL): the existing-claim check AND the write must happen
369
378
  // atomically with respect to any other `assignment-provider` invocation on the same subject —
370
379
  // see withSubjectLock()'s doc comment for the full rationale.
@@ -384,18 +393,37 @@ function claimLocalFile(argv) {
384
393
  role: "AssignmentClaimRecord",
385
394
  subject_id: subjectId,
386
395
  actor,
396
+ ...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
387
397
  claimed_at: isoNow(),
388
- ttl_seconds: ttlSeconds,
389
- branch,
390
- artifact_dir: artifactDir,
398
+ ttl_seconds: opts.ttlSeconds,
399
+ branch: opts.branch,
400
+ artifact_dir: opts.artifactDir,
391
401
  status: "claimed",
392
402
  audit_trail: [...(existing?.audit_trail ?? []), { at: isoNow(), transition: "claim", from_actor: null, to_actor: actor, reason }],
393
403
  };
394
404
  writeLocalRecord(artifactRoot, subjectId, record);
395
- console.log(JSON.stringify({ role: "AssignmentClaimResult", subject_id: subjectId, record }, null, 2));
396
- return 0;
405
+ return record;
397
406
  });
398
407
  }
408
+ function claimLocalFile(argv) {
409
+ const args = parseArgs(argv);
410
+ const provider = flagString(args.flags, "provider", "local-file");
411
+ if (provider !== "local-file")
412
+ throw new Error(`claim: --provider must be local-file (use render-claim for github); got ${provider}`);
413
+ const artifactRoot = requireFlag(args, "artifact-root");
414
+ const subjectId = requireFlag(args, "subject-id");
415
+ const { actor, actorKey } = loadActorStruct(args);
416
+ const ttlSecondsRaw = flagString(args.flags, "ttl-seconds", "1800") ?? "1800";
417
+ const ttlSeconds = Number(ttlSecondsRaw);
418
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
419
+ throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
420
+ const branch = requireFlag(args, "branch");
421
+ const artifactDir = requireFlag(args, "artifact-dir");
422
+ const reason = flagString(args.flags, "reason") ?? "claim";
423
+ const record = performLocalClaim(artifactRoot, subjectId, actor, { ttlSeconds, branch, artifactDir, reason, actorKey });
424
+ console.log(JSON.stringify({ role: "AssignmentClaimResult", subject_id: subjectId, record }, null, 2));
425
+ return 0;
426
+ }
399
427
  function releaseLocalFile(argv) {
400
428
  const args = parseArgs(argv);
401
429
  const provider = flagString(args.flags, "provider", "local-file");
@@ -421,20 +449,18 @@ function releaseLocalFile(argv) {
421
449
  return 0;
422
450
  });
423
451
  }
424
- function supersedeLocalFile(argv) {
425
- const args = parseArgs(argv);
426
- const provider = flagString(args.flags, "provider", "local-file");
427
- if (provider !== "local-file")
428
- throw new Error(`supersede: --provider must be local-file (use render-supersede for github); got ${provider}`);
429
- const artifactRoot = requireFlag(args, "artifact-root");
430
- const subjectId = requireFlag(args, "subject-id");
452
+ /**
453
+ * Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
454
+ * withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
455
+ * `--supersede-stale` takeover path (workflow-sidecar.ts, Wave 2) can reuse the EXACT same
456
+ * supersede logic from-actor holder verification, ttl/branch/artifact_dir carry-forward,
457
+ * audit-trail append, atomic write under withSubjectLock — rather than reimplementing a second,
458
+ * parallel supersede path. supersedeLocalFile (CLI wrapper, below) is now a thin
459
+ * parse-args/print-envelope shell around this.
460
+ */
461
+ export function performLocalSupersede(artifactRoot, subjectId, fromActor, toActor, opts = {}) {
431
462
  const helper = loadActorIdentityHelper();
432
- const fromActor = loadActorStructFromFile(requireFlag(args, "from-actor-json"));
433
- const toActor = loadActorStructFromFile(requireFlag(args, "to-actor-json"));
434
- const reason = flagString(args.flags, "reason") ?? "supersede";
435
- const ttlSecondsOverride = flagString(args.flags, "ttl-seconds");
436
- const branchOverride = flagString(args.flags, "branch");
437
- const artifactDirOverride = flagString(args.flags, "artifact-dir");
463
+ const reason = opts.reason ?? "supersede";
438
464
  // F1 fix (fix-plan iteration 1, CRITICAL): supersede mutates the same record file claim/release
439
465
  // do, under the same per-subject lock (see withSubjectLock()'s doc comment).
440
466
  return withSubjectLock(artifactRoot, subjectId, () => {
@@ -444,7 +470,7 @@ function supersedeLocalFile(argv) {
444
470
  if (helper.serializeActor(existing.actor) !== helper.serializeActor(fromActor)) {
445
471
  throw new Error(`--from-actor-json does not match the current holder (${helper.serializeActor(existing.actor)}); refusing to supersede a claim held by someone else`);
446
472
  }
447
- const ttlSecondsRaw = ttlSecondsOverride ?? String(existing.ttl_seconds);
473
+ const ttlSecondsRaw = opts.ttlSeconds != null ? String(opts.ttlSeconds) : String(existing.ttl_seconds);
448
474
  const ttlSeconds = Number(ttlSecondsRaw);
449
475
  if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
450
476
  throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
@@ -453,18 +479,40 @@ function supersedeLocalFile(argv) {
453
479
  role: "AssignmentClaimRecord",
454
480
  subject_id: subjectId,
455
481
  actor: toActor,
482
+ ...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
456
483
  claimed_at: isoNow(),
457
484
  ttl_seconds: ttlSeconds,
458
- branch: branchOverride ?? existing.branch,
459
- artifact_dir: artifactDirOverride ?? existing.artifact_dir,
485
+ branch: opts.branch ?? existing.branch,
486
+ artifact_dir: opts.artifactDir ?? existing.artifact_dir,
460
487
  status: "claimed",
461
488
  audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "supersede", from_actor: fromActor, to_actor: toActor, reason }],
462
489
  };
463
490
  writeLocalRecord(artifactRoot, subjectId, record);
464
- console.log(JSON.stringify({ role: "AssignmentSupersedeResult", subject_id: subjectId, record }, null, 2));
465
- return 0;
491
+ return record;
466
492
  });
467
493
  }
494
+ function supersedeLocalFile(argv) {
495
+ const args = parseArgs(argv);
496
+ const provider = flagString(args.flags, "provider", "local-file");
497
+ if (provider !== "local-file")
498
+ throw new Error(`supersede: --provider must be local-file (use render-supersede for github); got ${provider}`);
499
+ const artifactRoot = requireFlag(args, "artifact-root");
500
+ const subjectId = requireFlag(args, "subject-id");
501
+ const fromActor = loadActorStructFromFile(requireFlag(args, "from-actor-json"));
502
+ const toActor = loadActorStructFromFile(requireFlag(args, "to-actor-json"));
503
+ const reason = flagString(args.flags, "reason") ?? "supersede";
504
+ const ttlSecondsOverride = flagString(args.flags, "ttl-seconds");
505
+ const branchOverride = flagString(args.flags, "branch");
506
+ const artifactDirOverride = flagString(args.flags, "artifact-dir");
507
+ const record = performLocalSupersede(artifactRoot, subjectId, fromActor, toActor, {
508
+ ttlSeconds: ttlSecondsOverride != null ? Number(ttlSecondsOverride) : undefined,
509
+ branch: branchOverride ?? undefined,
510
+ artifactDir: artifactDirOverride ?? undefined,
511
+ reason,
512
+ });
513
+ console.log(JSON.stringify({ role: "AssignmentSupersedeResult", subject_id: subjectId, record }, null, 2));
514
+ return 0;
515
+ }
468
516
  // ─── GitHub: render-claim | render-release | render-supersede (render, don't execute — Design
469
517
  // Decision 1). Pure functions: no I/O beyond reading --input-json/--actor-json. Never invoke
470
518
  // `gh` (or any process) here — the calling skill runs the emitted argv verbatim. ────────────────
@@ -624,6 +672,17 @@ function renderSupersede(argv) {
624
672
  return 0;
625
673
  }
626
674
  // ─── status | list (both provider kinds) ────────────────────────────────────────────────────
675
+ /**
676
+ * Wave 1 (#291) extraction: the local-file branch of statusCommand's assignment-layer read,
677
+ * mirrored exactly so ensure-session's ownership guard (workflow-sidecar.ts, Wave 2) derives an
678
+ * AssignmentStatus identically to the `assignment-provider status` CLI command — a single
679
+ * implementation, not a second parallel local-file read.
680
+ */
681
+ export function readLocalAssignmentStatus(artifactRoot, subjectId) {
682
+ const record = readLocalRecord(artifactRoot, subjectId);
683
+ const active = record && record.status === "claimed" ? record : null;
684
+ return { subject_id: subjectId, provider: "local-file", assignee: active ? loadActorIdentityHelper().serializeActor(active.actor) : null, record: active };
685
+ }
627
686
  function statusCommand(argv) {
628
687
  const args = parseArgs(argv);
629
688
  const provider = requireFlag(args, "provider");
@@ -633,9 +692,7 @@ function statusCommand(argv) {
633
692
  const artifactRoot = requireFlag(args, "artifact-root");
634
693
  if (!requestedSubjectId)
635
694
  throw new Error("--subject-id is required for status --provider local-file");
636
- const record = readLocalRecord(artifactRoot, requestedSubjectId);
637
- const active = record && record.status === "claimed" ? record : null;
638
- assignment = { subject_id: requestedSubjectId, provider: "local-file", assignee: active ? loadActorIdentityHelper().serializeActor(active.actor) : null, record: active };
695
+ assignment = readLocalAssignmentStatus(artifactRoot, requestedSubjectId);
639
696
  }
640
697
  else if (provider === "github") {
641
698
  const issueJsonPath = requireFlag(args, "issue-json");
@@ -145,7 +145,7 @@ type SurfaceModule = {
145
145
  * @param critiques Critique objects (from critique.json .critiques array)
146
146
  * @param commandLog Optional parsed command-log.jsonl entries (capture-authoritative fold)
147
147
  */
148
- export declare function buildTrustBundle(slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[], commandLog?: AnyObj[], flowAgentsDir?: string): Promise<AnyObj | null>;
148
+ export declare function buildTrustBundle(slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[], commandLog?: AnyObj[], flowAgentsDir?: string, actorKey?: string): Promise<AnyObj | null>;
149
149
  /**
150
150
  * Fail-open wrapper: builds (via Surface), validates, and writes a trust.bundle.
151
151
  * Accepts raw check/criterion/critique inputs directly (ADR 0010 Phase 4a).
@@ -159,7 +159,7 @@ export declare function buildTrustBundle(slug: string, timestamp: string, checks
159
159
  * @param criteria Acceptance criteria objects (same as buildTrustBundle)
160
160
  * @param critiques Critique objects (same as buildTrustBundle)
161
161
  */
162
- export declare function writeTrustBundle(dir: string, slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[]): Promise<{
162
+ export declare function writeTrustBundle(dir: string, slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[], actorKey?: string): Promise<{
163
163
  written: boolean;
164
164
  errors: string[];
165
165
  }>;