@kontourai/flow-agents 3.4.3 → 3.6.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 (65) hide show
  1. package/.github/workflows/ci.yml +4 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/src/builder-flow-run-adapter.d.ts +10 -1
  4. package/build/src/builder-flow-run-adapter.js +29 -1
  5. package/build/src/builder-flow-runtime.d.ts +18 -0
  6. package/build/src/builder-flow-runtime.js +205 -13
  7. package/build/src/builder-lifecycle-authority.d.ts +35 -0
  8. package/build/src/builder-lifecycle-authority.js +219 -0
  9. package/build/src/cli/assignment-provider.d.ts +13 -0
  10. package/build/src/cli/assignment-provider.js +120 -62
  11. package/build/src/cli/builder-run.js +46 -5
  12. package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
  13. package/build/src/cli/workflow-sidecar.d.ts +3 -0
  14. package/build/src/cli/workflow-sidecar.js +140 -30
  15. package/build/src/cli/workflow.d.ts +2 -0
  16. package/build/src/cli/workflow.js +521 -0
  17. package/build/src/cli.js +2 -0
  18. package/build/src/index.d.ts +4 -0
  19. package/build/src/index.js +2 -0
  20. package/build/src/lib/flow-resolver.js +7 -2
  21. package/build/src/lib/package-version.d.ts +2 -0
  22. package/build/src/lib/package-version.js +13 -0
  23. package/build/src/lib/pinned-cli-command.d.ts +6 -0
  24. package/build/src/lib/pinned-cli-command.js +21 -0
  25. package/context/contracts/artifact-contract.md +1 -1
  26. package/context/contracts/assignment-provider-contract.md +5 -2
  27. package/context/scripts/hooks/config-protection.js +8 -1
  28. package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
  29. package/context/scripts/hooks/stop-goal-fit.js +1 -1
  30. package/docs/context-map.md +2 -0
  31. package/docs/public-workflow-cli.md +63 -0
  32. package/docs/spec/builder-flow-runtime.md +49 -5
  33. package/docs/workflow-usage-guide.md +5 -0
  34. package/evals/ci/run-baseline.sh +2 -0
  35. package/evals/integration/test_assignment_provider_local_file.sh +54 -0
  36. package/evals/integration/test_builder_entry_enforcement.sh +241 -24
  37. package/evals/integration/test_bundle_install.sh +97 -0
  38. package/evals/integration/test_current_json_per_actor.sh +1 -0
  39. package/evals/integration/test_dual_emit_flow_step.sh +2 -0
  40. package/evals/integration/test_flowdef_session_activation.sh +6 -3
  41. package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
  42. package/evals/integration/test_phase_map_and_gate_claim.sh +4 -0
  43. package/evals/integration/test_public_workflow_cli.sh +259 -0
  44. package/evals/integration/test_workflow_sidecar_writer.sh +1 -0
  45. package/package.json +2 -2
  46. package/schemas/builder-lifecycle-authorization.schema.json +57 -0
  47. package/schemas/lifecycle-authority-keys.schema.json +25 -0
  48. package/schemas/workflow-state.schema.json +1 -1
  49. package/scripts/hooks/config-protection.js +8 -1
  50. package/scripts/hooks/lib/config-protection-remedies.js +3 -0
  51. package/scripts/hooks/stop-goal-fit.js +1 -1
  52. package/src/builder-flow-run-adapter.ts +47 -0
  53. package/src/builder-flow-runtime.ts +216 -4
  54. package/src/builder-lifecycle-authority.ts +218 -0
  55. package/src/cli/assignment-provider.ts +84 -20
  56. package/src/cli/builder-flow-runtime.test.mjs +404 -1
  57. package/src/cli/builder-run.ts +56 -5
  58. package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
  59. package/src/cli/workflow-sidecar.ts +138 -31
  60. package/src/cli/workflow.ts +471 -0
  61. package/src/cli.ts +2 -0
  62. package/src/index.ts +14 -0
  63. package/src/lib/flow-resolver.ts +6 -2
  64. package/src/lib/package-version.ts +15 -0
  65. package/src/lib/pinned-cli-command.ts +23 -0
@@ -138,8 +138,10 @@ detect an incompatible future shape before parsing fields it does not understand
138
138
  {
139
139
  "schema_version": "1.0",
140
140
  "role": "AssignmentClaimRecord",
141
- "subject_id": "kontourai/flow-agents#290",
141
+ "subject_id": "kontourai-flow-agents-290",
142
142
  "actor": { "runtime": "claude-code", "session_id": "...", "host": "...", "human": null },
143
+ "actor_key": "claude-code:...:host",
144
+ "work_item_ref": "kontourai/flow-agents#290",
143
145
  "claimed_at": "2026-07-02T00:00:00Z",
144
146
  "ttl_seconds": 1800,
145
147
  "branch": "agent/<actor>/<slug>",
@@ -152,9 +154,10 @@ detect an incompatible future shape before parsing fields it does not understand
152
154
  | --- | --- | --- |
153
155
  | `schema_version` | yes | Version of this record shape, `"1.0"`. |
154
156
  | `role` | yes | Constant `"AssignmentClaimRecord"`, for readers scanning mixed content (e.g. a GitHub comment thread) for this record type. |
155
- | `subject_id` | yes | The claimed work item, in `owner/repo#id` form the same string `workItemSlug` derives the deterministic session slug from. |
157
+ | `subject_id` | yes | The provider subject key. The local-file provider uses the deterministic session slug; provider-backed records may use their native subject identifier. |
156
158
  | `actor` | yes | `{ runtime, session_id, host, human? }` — the exact struct `actor-identity.js` defines. `human` is set (non-null) only for a human assignee; its presence, not a username heuristic, gates the human-held join state. |
157
159
  | `actor_key` | no (additive, #291) | The canonical `resolveActor(env).actor` string for the claiming actor — the same flat/bare token `liveness whoami`, `liveness claim --actor`, per-actor `current.json`, and pull-work's `--self-actor` all use. When present, `computeEffectiveState` compares against THIS (not a re-serialization of `actor`) for both self-recognition and the liveness join, because `serializeActor(actor)` and `resolveActor(env).actor` diverge for an explicit-override actor (a bare token vs. a `explicit-override:<value>:<host>` triple) while agreeing for a derived actor. Absent on any pre-#291 record or fixture — `computeEffectiveState` falls back to `serializeActor(actor)` in that case, reproducing pre-#291 behavior exactly. |
160
+ | `work_item_ref` | no (additive, #541) | Exact Work Item reference acquired by Builder `ensure-session`. This preserves identity beyond the local provider's lossy filesystem slug and allows an interrupted acquisition to retry safely. Older records and ordinary assignment-provider claims omit it and cannot automatically satisfy `builder.pull-work.selected`. |
158
161
  | `claimed_at` | yes | ISO-8601 timestamp the claim was recorded. Mirrors the liveness stream's own claim-event field so a reader compares freshness with one mental model across both layers, even though the two are stored in different media. |
159
162
  | `ttl_seconds` | yes | Same field name/semantics as the liveness stream's `ttlSeconds` (default `1800`). |
160
163
  | `branch` | yes | The branch this actor is working on, per the `agent/<actor>/<slug>` convention. |
@@ -120,6 +120,13 @@ function checkProtectedPathPattern(filePath) {
120
120
  };
121
121
  }
122
122
 
123
+ if (/(?:^|\/)\.flow-agents\/lifecycle-authority-keys\.json$/.test(norm)) {
124
+ return {
125
+ name: '.flow-agents/lifecycle-authority-keys.json',
126
+ reason: 'an agent could replace the pinned lifecycle authority key and forge cancellation authority',
127
+ };
128
+ }
129
+
123
130
  // .kontourai/flow-agents/current.json — an agent could forge active_flow_id / active_step_id
124
131
  // to route the gate to a permissive or empty-expects FlowDefinition.
125
132
  // SAFE: the workflow CLI writes current.json via fs (writeJson → fs.writeFileSync),
@@ -426,7 +433,7 @@ function checkCommandForBypass(command) {
426
433
  */
427
434
  // #379: the delivery/ arms carry an optional (?:[^/]+\/)? segment so redirects/tee to the
428
435
  // per-session path delivery/<slug>/trust.bundle (+ checkpoint) are caught, not just the flat path.
429
- const REDIRECT_PROTECTED_RE = /(?:^|\/|~\/)(\.bash_profile|\.bashrc|\.profile|\.zprofile|\.zshrc)$|(?:^|\/)\.claude\/settings(?:\.local)?\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\/[^/]+\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/\.goal-fit-block-streak\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/state\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/trust\.bundle$|(?:^|\/)delivery\/(?:[^/]+\/)?trust\.bundle$|(?:^|\/)delivery\/(?:[^/]+\/)?trust\.checkpoint\.json$/;
436
+ const REDIRECT_PROTECTED_RE = /(?:^|\/|~\/)(\.bash_profile|\.bashrc|\.profile|\.zprofile|\.zshrc)$|(?:^|\/)\.claude\/settings(?:\.local)?\.json$|(?:^|\/)\.flow-agents\/lifecycle-authority-keys\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\/[^/]+\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/\.goal-fit-block-streak\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/state\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/trust\.bundle$|(?:^|\/)delivery\/(?:[^/]+\/)?trust\.bundle$|(?:^|\/)delivery\/(?:[^/]+\/)?trust\.checkpoint\.json$/;
430
437
 
431
438
  /**
432
439
  * Return true when a token (an unquoted redirect target or tee argument) matches
@@ -20,6 +20,8 @@ const SANCTIONED_REMEDIES = {
20
20
  'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
21
21
  '.claude/settings.local.json':
22
22
  'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
23
+ '.flow-agents/lifecycle-authority-keys.json':
24
+ 'Only the trusted harness installer or a human maintainer may rotate lifecycle authority keys. Never let an agent replace this trust root.',
23
25
  '.kontourai/flow-agents/current.json':
24
26
  'Use `npm run workflow:sidecar -- ensure-session` (or `advance-state`), which writes this file for you. Never disable this hook to make the write.',
25
27
  '.kontourai/flow-agents/current/<actor>.json':
@@ -58,6 +60,7 @@ function remedyFor(name) {
58
60
  * basename 'trust.bundle') -- first match wins, deterministically.
59
61
  */
60
62
  const REMEDY_COMMAND_CANDIDATES = [
63
+ { name: '.flow-agents/lifecycle-authority-keys.json', needles: ['lifecycle-authority-keys.json'] },
61
64
  { name: 'delivery/trust.checkpoint.json', needles: ['delivery/trust.checkpoint.json', 'trust.checkpoint.json'] },
62
65
  { name: 'delivery/trust.bundle', needles: ['delivery/trust.bundle'] },
63
66
  { name: '.kontourai/flow-agents/<slug>/trust.bundle', needles: ['trust.bundle'] },
@@ -77,7 +77,7 @@ const PRE_EXECUTION_PHASES = new Set(['idea', 'backlog', 'pickup', 'planning']);
77
77
  // Terminal tasks are complete — they must never gate a stop or count as "active".
78
78
  // A stale current.json pointing at one, or a graveyard of finished states, must
79
79
  // not block an unrelated session.
80
- const TERMINAL_STATUSES = new Set(['done', 'delivered', 'accepted', 'archived', 'complete', 'completed']);
80
+ const TERMINAL_STATUSES = new Set(['done', 'delivered', 'canceled', 'accepted', 'archived', 'complete', 'completed']);
81
81
 
82
82
  function isTerminalDeliveredState(state) {
83
83
  if (!state || typeof state !== 'object') return false;
@@ -54,8 +54,10 @@ Machine-readable workflow state lives beside Markdown artifacts in `.kontourai/f
54
54
  | --- | --- | --- |
55
55
  | assignment-provider-settings.schema.json | Flow Agents Assignment Provider Settings | https://flow-agents.dev/schemas/assignment-provider-settings.schema.json |
56
56
  | backlog-provider-settings.schema.json | Flow Agents Backlog Provider Settings | https://flow-agents.dev/schemas/backlog-provider-settings.schema.json |
57
+ | builder-lifecycle-authorization.schema.json | Builder Lifecycle Authorization | https://kontourai.dev/schemas/builder-lifecycle-authorization.schema.json |
57
58
  | decision-record.schema.json | Flow Agents Decision Record | https://flow-agents.dev/schemas/decision-record.schema.json |
58
59
  | flow-agents-settings.schema.json | Flow Agents Settings | https://flow-agents.dev/schemas/flow-agents-settings.schema.json |
60
+ | lifecycle-authority-keys.schema.json | Lifecycle Authority Key Registry | https://kontourai.dev/schemas/lifecycle-authority-keys.schema.json |
59
61
  | workflow-acceptance.schema.json | Flow Agents Workflow Acceptance | https://flow-agents.dev/schemas/workflow-acceptance.schema.json |
60
62
  | workflow-critique.schema.json | Flow Agents Workflow Critique | https://flow-agents.dev/schemas/workflow-critique.schema.json |
61
63
  | workflow-evidence.schema.json | Flow Agents Workflow Evidence | https://flow-agents.dev/schemas/workflow-evidence.schema.json |
@@ -0,0 +1,63 @@
1
+ # Public Workflow CLI
2
+
3
+ Flow Agents exposes its supported consumer workflow surface through the primary package binary.
4
+ Consumer repositories do not need a `package.json`, a local dependency, or a repository-owned
5
+ writer script.
6
+
7
+ Use an exact package version from an isolated npm prefix. This prevents a repository-local
8
+ dependency with the same version from intercepting the command. Generated workflow actions and
9
+ doctor remediation include this isolation automatically:
10
+
11
+ ```bash
12
+ flow_agents() (
13
+ root=$(mktemp -d) || exit 1
14
+ trap 'rm -rf "$root"' EXIT HUP INT TERM
15
+ npm exec --yes --prefix "$root" \
16
+ --package=@kontourai/flow-agents@3.5.0 -- flow-agents "$@"
17
+ )
18
+
19
+ flow_agents workflow start \
20
+ --flow builder.build \
21
+ --work-item owner/repository#123
22
+
23
+ flow_agents workflow status --json
24
+
25
+ flow_agents workflow evidence \
26
+ --expectation implementation-plan \
27
+ --status pass \
28
+ --summary "Implementation plan recorded." \
29
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/example/example--plan.md"}'
30
+ ```
31
+
32
+ The public lifecycle verbs are `pause`, `resume`, `release`, `cancel`, and `archive`. Pause,
33
+ resume, and release require the current assignment actor and an explicit reason. Cancel and
34
+ archive require a signed user/operator authorization file. Flow owns the canonical run
35
+ transition; Flow Agents validates assignment and request binding and projects the resulting run.
36
+
37
+ ```bash
38
+ flow_agents workflow pause --reason "Waiting for a decision"
39
+ flow_agents workflow resume --reason "Decision received"
40
+ flow_agents workflow release --reason "Handing work back"
41
+ flow_agents workflow cancel --authorization-file cancel.json
42
+ flow_agents workflow archive --authorization-file archive.json
43
+ ```
44
+
45
+ `workflow status` is read-only. It reads the actor-scoped current-session pointer, the projected
46
+ state, and the canonical Flow run without rewriting either store.
47
+
48
+ ## Compatibility Doctor
49
+
50
+ Run doctor through the exact isolated package helper defined above:
51
+
52
+ ```bash
53
+ flow_agents workflow doctor --json
54
+ ```
55
+
56
+ The report distinguishes the executing CLI from a repository-local dependency and reports the
57
+ workflow/writer contract, installed hook and writer package, active Kits, Builder Kit content,
58
+ Flow runtime and definition, workflow-state schema, and trust-bundle schema. Incompatible or
59
+ missing installed components produce a nonzero exit and an exact, version-pinned `init` command
60
+ that preserves the recorded runtime and active Kits.
61
+
62
+ The `flow-agents-workflow-sidecar` binary is deprecated and retained only while package-internal
63
+ callers migrate. New consumer guidance must use `flow-agents workflow`.
@@ -36,11 +36,18 @@ flow-agents builder-run start --session-dir .kontourai/flow-agents/<slug>
36
36
  ```
37
37
 
38
38
  `ensure-session --flow-id builder.build` starts or loads this canonical run before
39
- returning, then projects the first Flow step into the sidecar. The command remains
40
- the explicit recovery path if Flow startup fails after sidecar creation; the
41
- failure is returned to the caller and no substitute run state is invented. Runtime
42
- hooks keep projected actions advisory while the agent performs their declared
43
- skills and operations.
39
+ returning. When the local assignment provider durably assigns the exact Work Item
40
+ to the resolved workflow actor, `ensure-session` rereads that record and produces
41
+ the declared `builder.pull-work.selected` claim through the normal Surface trust
42
+ bundle path. Flow evaluates that subject-bound evidence and advances to
43
+ `design-probe`; Flow Agents does not write a transition or gate outcome directly.
44
+ Skipped ownership, unresolved actors, precomputed state, and non-local providers do
45
+ not produce selection evidence and remain at `pull-work`.
46
+
47
+ The command remains the explicit recovery path if Flow startup fails after sidecar
48
+ creation; the failure is returned to the caller and no substitute run state is
49
+ invented. Runtime hooks keep projected actions advisory while the agent performs
50
+ their declared skills and operations.
44
51
 
45
52
  Sidecars written by 3.4.2 may still contain `next_action.enforcement`. The 1.0
46
53
  schema accepts that deprecated field for artifact compatibility, but current
@@ -111,3 +118,40 @@ Workflow steering surfaces these fields on session start and prompt submission.
111
118
  Stop hook treats an unfinished canonical Flow run as active even during pickup or
112
119
  planning, blocks a premature stop in block mode, and does not release its liveness
113
120
  claim. A run is complete only when Flow reaches its terminal step.
121
+ # Builder Lifecycle Authority
122
+
123
+ The canonical Flow run owns pause, resume, and cancellation. The current assignment actor may
124
+ pause, resume, or release its own assignment with a reason. Cancellation and archival require
125
+ an Ed25519-signed authorization record conforming to
126
+ `schemas/builder-lifecycle-authorization.schema.json`. The record is operation-bound and binds
127
+ the request to the run id, selected Work Item, current assignment actor, immutable external
128
+ request reference, nonce, and expiry. Its signing key must be pinned in the durable
129
+ `.flow-agents/lifecycle-authority-keys.json` registry. Runtime or harness adapters hold the
130
+ private key and capture the signed record from a user/operator channel they trust; agent-authored
131
+ prose or an unsigned model-written file is not cancellation authority.
132
+
133
+ This is an audit and policy boundary, not authentication against a process with unrestricted
134
+ access as the same operating-system user. The harness must keep its signing key outside the
135
+ agent process and enforce its own filesystem or process isolation when the agent is adversarial.
136
+ The repository hooks protect the pinned public-key registry from ordinary agent writes, but are
137
+ explicitly not an operating-system security boundary.
138
+ Adversarial-runtime authentication is tracked separately in Flow Agents issue #545. Flow's
139
+ current lifecycle authority vocabulary also requires agent-owned pause/resume events to use the
140
+ closest available `operator_request` shape; a distinct canonical runtime authority is tracked in
141
+ Flow issue #118.
142
+
143
+ ```text
144
+ flow-agents builder-run pause --session-dir <dir> --reason <text>
145
+ flow-agents builder-run resume --session-dir <dir> --reason <text>
146
+ flow-agents builder-run cancel --session-dir <dir> --authorization-file <record.json>
147
+ flow-agents builder-run release-assignment --session-dir <dir> --reason <text>
148
+ flow-agents builder-run archive --session-dir <dir> --authorization-file <record.json>
149
+ ```
150
+
151
+ Pause and resume verify the live assignment actor under the assignment lock, and preserve the
152
+ current Flow step and assignment. Assignment release does not
153
+ change the Flow run. Cancellation changes Flow first and then idempotently releases the owning
154
+ assignment while holding the same lock; a successfully consumed cancellation nonce cannot be
155
+ replayed. Archive accepts only completed or canceled runs, moves the session under
156
+ `.kontourai/flow-agents/archive/<slug>/`, and retains the canonical Flow run. None of these
157
+ operations deletes a branch or worktree; cleanup requires a separate provider-aware action.
@@ -4,6 +4,11 @@ title: Workflow Usage Guide
4
4
 
5
5
  # Workflow Usage Guide
6
6
 
7
+ > Consumer repositories should use the supported public workflow surface and isolated exact-package
8
+ > launcher documented in [Public Workflow CLI](public-workflow-cli.md). The lower-level writer
9
+ > commands retained later in this guide describe internal/package migration mechanics and are not a
10
+ > consumer prerequisite.
11
+
7
12
  This guide shows how to use the Builder Kit workflow skills in normal chats.
8
13
 
9
14
  > **Which doc do I want?** This page is the *driver's manual* — what to say at each stage and what should happen. If you want the conceptual map first — layers, sidecars, hooks, evidence, and why the system is shaped this way — read the [Agent System Guidebook](agent-system-guidebook.md). For a one-line summary of every skill and gate, use the [Skills Map](skills-map.md). Flow Agents coordinates the local runtime, installs Flow Kits, and records artifacts; Flow owns gate semantics, including typed `expects` entries with `kind: "trust.bundle"`, trusted producer config, and gate overrides.
@@ -41,6 +41,7 @@ CHECKS=(
41
41
  "Flow Kit repository integration|bash evals/integration/test_flow_kit_repository.sh"
42
42
  "Runtime adapter activation integration|bash evals/integration/test_runtime_adapter_activation.sh"
43
43
  "Bundle install integration|bash evals/integration/test_bundle_install.sh"
44
+ "Public workflow CLI integration|bash evals/integration/test_public_workflow_cli.sh"
44
45
  "Bundle lifecycle integration|bash evals/integration/test_bundle_lifecycle.sh"
45
46
  "Activate npx context integration|bash evals/integration/test_activate_npx_context.sh"
46
47
  "Kit conformance levels integration|bash evals/integration/test_kit_conformance_levels.sh"
@@ -133,6 +134,7 @@ LANE_RUNTIME_AND_KIT=(
133
134
  "Flow Kit repository integration"
134
135
  "Runtime adapter activation integration"
135
136
  "Bundle install integration"
137
+ "Public workflow CLI integration"
136
138
  "Bundle lifecycle integration"
137
139
  "Activate npx context integration"
138
140
  "Kit conformance levels integration"
@@ -69,6 +69,60 @@ node "$CLI" assignment-provider status \
69
69
  [[ "$(json_query "$TMPDIR_EVAL/status-a.json" "assignment.assignee")" == "claude-code:eval-actor-a-session:eval-host" ]] && pass "status reports actor A as assignee" || fail "status reports actor A as assignee"
70
70
  [[ "$(json_query "$TMPDIR_EVAL/status-a.json" "assignment.record.claimed_at")" == "$CLAIMED_AT" ]] && pass "status claimed_at matches claim's claimed_at" || fail "status claimed_at matches claim's claimed_at"
71
71
 
72
+ # A generic same-actor refresh must preserve exact Builder provenance that an interrupted
73
+ # ensure-session recorded, even though the assignment-provider CLI does not accept that field.
74
+ PROVENANCE_SUBJECT="builder-provenance"
75
+ node "$CLI" assignment-provider claim \
76
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
77
+ --subject-id "$PROVENANCE_SUBJECT" \
78
+ --actor-json "$ACTOR_A" \
79
+ --branch "$BRANCH_A" \
80
+ --artifact-dir "$ARTIFACT_DIR_A" \
81
+ --ttl-seconds 1800 \
82
+ > "$TMPDIR_EVAL/provenance-claim.json"
83
+ PROVENANCE_FILE="$(node - "$ARTIFACT_ROOT/assignment" "$PROVENANCE_SUBJECT" <<'NODE'
84
+ const fs = require('node:fs');
85
+ const path = require('node:path');
86
+ const [dir, subject] = process.argv.slice(2);
87
+ for (const name of fs.readdirSync(dir)) {
88
+ if (!name.endsWith('.json')) continue;
89
+ const file = path.join(dir, name);
90
+ if (JSON.parse(fs.readFileSync(file, 'utf8')).subject_id === subject) {
91
+ process.stdout.write(file);
92
+ process.exit(0);
93
+ }
94
+ }
95
+ process.exit(1);
96
+ NODE
97
+ )"
98
+ node - "$PROVENANCE_FILE" <<'NODE'
99
+ const fs = require('node:fs');
100
+ const file = process.argv[2];
101
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
102
+ record.work_item_ref = 'kontourai/flow-agents#9001';
103
+ fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`);
104
+ NODE
105
+ node "$CLI" assignment-provider claim \
106
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
107
+ --subject-id "$PROVENANCE_SUBJECT" \
108
+ --actor-json "$ACTOR_A" \
109
+ --branch "$BRANCH_A" \
110
+ --artifact-dir "$ARTIFACT_DIR_A" \
111
+ --ttl-seconds 1800 \
112
+ > "$TMPDIR_EVAL/provenance-refresh.json"
113
+ [[ "$(json_query "$TMPDIR_EVAL/provenance-refresh.json" "record.work_item_ref")" == "$SUBJECT_ID" ]] && pass "same-actor refresh preserves exact Work Item provenance" || fail "same-actor refresh preserves exact Work Item provenance"
114
+ node "$CLI" assignment-provider supersede \
115
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
116
+ --subject-id "$PROVENANCE_SUBJECT" \
117
+ --from-actor-json "$ACTOR_A" --to-actor-json "$ACTOR_B" \
118
+ --reason "provenance handoff" >"$TMPDIR_EVAL/provenance-supersede.json"
119
+ [[ "$(json_query "$TMPDIR_EVAL/provenance-supersede.json" "record.work_item_ref")" == "$SUBJECT_ID" ]] && pass "generic supersede preserves exact Work Item provenance" || fail "generic supersede preserves exact Work Item provenance"
120
+ node "$CLI" assignment-provider release \
121
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
122
+ --subject-id "$PROVENANCE_SUBJECT" \
123
+ --actor-json "$ACTOR_B" \
124
+ --reason "provenance fixture cleanup" >/dev/null
125
+
72
126
  # 3. supersede A -> B updates the actor and records an audit trail entry.
73
127
  node "$CLI" assignment-provider supersede \
74
128
  --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \