@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
@@ -149,6 +149,10 @@ jobs:
149
149
  continue-on-error: true
150
150
  run: bash evals/ci/run-baseline.sh --check actor-identity-resolver-integration
151
151
 
152
+ - name: CI-runtime actor identity integration
153
+ continue-on-error: true
154
+ run: bash evals/ci/run-baseline.sh --check ci-runtime-actor-identity-integration
155
+
152
156
  - name: Assignment provider local-file integration
153
157
  continue-on-error: true
154
158
  run: bash evals/ci/run-baseline.sh --check assignment-provider-local-file-integration
@@ -157,6 +161,10 @@ jobs:
157
161
  continue-on-error: true
158
162
  run: bash evals/ci/run-baseline.sh --check assignment-provider-github-integration
159
163
 
164
+ - name: Stop hook release-with-handoff integration
165
+ continue-on-error: true
166
+ run: bash evals/ci/run-baseline.sh --check stop-hook-release-with-handoff-integration
167
+
160
168
  - name: Pull work assignment join integration
161
169
  continue-on-error: true
162
170
  run: bash evals/ci/run-baseline.sh --check pull-work-assignment-join-integration
@@ -165,6 +173,14 @@ jobs:
165
173
  continue-on-error: true
166
174
  run: bash evals/ci/run-baseline.sh --check ensure-session-ownership-guard-integration
167
175
 
176
+ - name: Verify-hold gate integration
177
+ continue-on-error: true
178
+ run: bash evals/ci/run-baseline.sh --check verify-hold-gate-integration
179
+
180
+ - name: Takeover protocol integration
181
+ continue-on-error: true
182
+ run: bash evals/ci/run-baseline.sh --check takeover-protocol-integration
183
+
168
184
  - name: Current.json per-actor integration
169
185
  continue-on-error: true
170
186
  run: bash evals/ci/run-baseline.sh --check current-json-per-actor-integration
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.3.0](https://github.com/kontourai/flow-agents/compare/v3.2.0...v3.3.0) (2026-07-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cleanup-audit:** apply mode — safe, reversible session archival ([#406](https://github.com/kontourai/flow-agents/issues/406)) ([3dd7550](https://github.com/kontourai/flow-agents/commit/3dd7550a664c01ee5afb6eabb75325a02de28b59))
9
+ * **veritas-governance:** exemption-issuance flow — governed path for delivery/DECLARED (ADR 0022 §3) ([#370](https://github.com/kontourai/flow-agents/issues/370)) ([50f8f3b](https://github.com/kontourai/flow-agents/commit/50f8f3b0aa65a7b18c6d736360e93f43bb057145))
10
+
11
+
12
+ ### Documentation
13
+
14
+ * unified coordination guide + console integration doc + status hygiene ([#399](https://github.com/kontourai/flow-agents/issues/399)) ([#400](https://github.com/kontourai/flow-agents/issues/400)) ([b60331e](https://github.com/kontourai/flow-agents/commit/b60331e1b10d3a417a941de836bbac01bee490bc))
15
+
16
+ ## [3.2.0](https://github.com/kontourai/flow-agents/compare/v3.1.0...v3.2.0) (2026-07-04)
17
+
18
+
19
+ ### Features
20
+
21
+ * **builder:** per-step model-routing hints + escalate-on-gate-failure ladder ([#376](https://github.com/kontourai/flow-agents/issues/376)) ([8182dc6](https://github.com/kontourai/flow-agents/commit/8182dc66da10f3bfd731aef93d7cbd38b63ddd06))
22
+ * **knowledge:** inbound-reference integrity check, fail closed ([#340](https://github.com/kontourai/flow-agents/issues/340)) ([02932ab](https://github.com/kontourai/flow-agents/commit/02932abec98fcc43ae3755d2281697e21c7451a6))
23
+ * **knowledge:** incremental append-mode consolidate (Closes [#343](https://github.com/kontourai/flow-agents/issues/343)) ([9b972f5](https://github.com/kontourai/flow-agents/commit/9b972f5bf9d7a41d1de4af23d5e27619db3b1635))
24
+ * **knowledge:** incremental append-mode consolidate, regenerate snapshot from records ([#343](https://github.com/kontourai/flow-agents/issues/343)) ([32cc263](https://github.com/kontourai/flow-agents/commit/32cc263aedc4c3727b144ced24872bc102a13383))
25
+ * **knowledge:** record-carried freshness + Hachure-aligned status semantics ([#341](https://github.com/kontourai/flow-agents/issues/341)) ([32ff45e](https://github.com/kontourai/flow-agents/commit/32ff45eb9244fa1879a0b32784e95e5e5c80de71))
26
+
27
+
28
+ ### Fixes
29
+
30
+ * **delivery:** per-session delivery paths so concurrent deliveries stop contending ([#379](https://github.com/kontourai/flow-agents/issues/379)) ([fedf4db](https://github.com/kontourai/flow-agents/commit/fedf4db5c41c7e45dcf9db949176be076131a09e))
31
+ * **delivery:** per-session delivery paths so concurrent deliveries stop contending ([#379](https://github.com/kontourai/flow-agents/issues/379)) ([a03026f](https://github.com/kontourai/flow-agents/commit/a03026fccf940da2129a3b82e979443c37b87456))
32
+
3
33
  ## [3.1.0](https://github.com/kontourai/flow-agents/compare/v3.0.0...v3.1.0) (2026-07-04)
4
34
 
5
35
 
@@ -117,6 +117,51 @@ export declare function performLocalClaim(artifactRoot: string, subjectId: strin
117
117
  reason?: string;
118
118
  actorKey?: string;
119
119
  }): AssignmentClaimRecord;
120
+ /**
121
+ * Wave 1 (#292) extraction: the durable-write body previously inlined inside releaseLocalFile's
122
+ * withSubjectLock() closure, now a parameter-driven pure function so the Stop hook's non-terminal
123
+ * release lifecycle (scripts/hooks/stop-goal-fit.js, #292 Wave 2) can reuse the EXACT same release
124
+ * logic — actor-ownership verification, audit-trail append, atomic write under withSubjectLock —
125
+ * rather than reimplementing a second, parallel release path. releaseLocalFile (CLI wrapper,
126
+ * below) is now a thin parse-args/print-envelope shell around this, mirroring the
127
+ * performLocalSupersede/supersedeLocalFile extraction shape exactly.
128
+ *
129
+ * Two behaviors are deliberately DIFFERENT from a naive inline release, both required for the
130
+ * Stop hook's idempotent, actor-scoped lifecycle release (never for the interactive CLI, which
131
+ * keeps `tolerateNoActiveClaim` unset/false and therefore 100% of its prior throw-on-error shape):
132
+ *
133
+ * - `opts.tolerateNoActiveClaim === true` and there is no existing record, or the existing
134
+ * record's status is not `"claimed"`: return `null` (a tolerated no-op) instead of throwing
135
+ * "no active claim to release". This is the one deliberate idempotency change vs today's
136
+ * releaseLocalFile — a second release call (e.g. a double Stop event) must be a safe no-op.
137
+ * - `releasedBy` is provided and does not match the existing record's holder: never force-release
138
+ * a claim held by a different actor — return `null` (if tolerateNoActiveClaim) or throw
139
+ * (otherwise), same as the no-active-claim case. The comparison mirrors computeEffectiveState()'s
140
+ * `record.actor_key || helper.serializeActor(record.actor)` canonical-key preference EXACTLY
141
+ * (actor_key-first, falling back to serializeActor only when actor_key is absent) — the read
142
+ * path (status/effective-state) and this write path (release) must use the identical
143
+ * canonical-key comparison, or a claim written under an explicit-override actor (`actor_key`
144
+ * bare, e.g. `"canonical-x"`, but `serializeActor(record.actor)` a DIFFERENT triple, e.g.
145
+ * `"explicit-override:canonical-x:host"`) can be self-recognized by computeEffectiveState() yet
146
+ * fail to release here because the releaser's canonical key was compared against the wrong
147
+ * (re-derived, triple) form instead of the stored actor_key. Comparing two serializeActor()
148
+ * calls unconditionally — as a prior version of this function did — is NOT correct for override
149
+ * actors and reintroduces the exact #291 seam on the release path.
150
+ *
151
+ * Contract: when `releasedBy` is provided AND the existing record is `actor_key`-stamped,
152
+ * `opts.actorKey` is REQUIRED (the canonical `resolveActor(env).actor` string) — otherwise
153
+ * ownership cannot be verified. A caller that passes `releasedBy` without `opts.actorKey` against
154
+ * an `actor_key`-stamped record would have its ownership compared as
155
+ * `existing.actor_key` (bare canonical) vs `serializeActor(releasedBy)` (re-derived triple), which
156
+ * can NEVER match even for the legitimate holder — a silent-failure trap, not a real ownership
157
+ * check. This is refused loudly (see the guard at the top of the `releasedBy` branch below) rather
158
+ * than allowed to silently no-op or wrongly refuse.
159
+ */
160
+ export declare function performLocalRelease(artifactRoot: string, subjectId: string, releasedBy: ActorStruct | null, opts?: {
161
+ reason?: string;
162
+ actorKey?: string;
163
+ tolerateNoActiveClaim?: boolean;
164
+ }): AssignmentClaimRecord | null;
120
165
  /**
121
166
  * Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
122
167
  * withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
@@ -80,7 +80,16 @@ function loadActorStruct(args) {
80
80
  const resolved = helper.resolveActor(process.env);
81
81
  if (helper.isUnresolvedActor(resolved.actor))
82
82
  throw new Error("could not resolve an actor identity (no --actor-json and no resolvable environment actor); pass --actor-json explicitly");
83
- return { actor: { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null }, actorKey: resolved.actor };
83
+ // #398: reconstruct the SAME struct resolveActor serialized for a CI actor, mirroring
84
+ // resolveEnsureSessionActor (workflow-sidecar.ts) via the shared detectCiActor. Without this the
85
+ // else-branch would write `record.actor = {runtime:"unknown", session_id:<the whole triple>}` for a
86
+ // CI session — actor_key stays correct (so no false-block), but record.actor is malformed and the
87
+ // audit-trail / `assignment-provider status` output for CI sessions would be corrupt.
88
+ const ci = resolved.source.startsWith("ci-runtime") ? helper.detectCiActor(process.env) : null;
89
+ const actor = ci && ci.session_id
90
+ ? { runtime: ci.runtime, session_id: ci.session_id, host: os.hostname(), human: null }
91
+ : { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null };
92
+ return { actor, actorKey: resolved.actor };
84
93
  }
85
94
  export function assignmentFilePath(artifactRoot, subjectId) {
86
95
  const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
@@ -424,31 +433,116 @@ function claimLocalFile(argv) {
424
433
  console.log(JSON.stringify({ role: "AssignmentClaimResult", subject_id: subjectId, record }, null, 2));
425
434
  return 0;
426
435
  }
427
- function releaseLocalFile(argv) {
428
- const args = parseArgs(argv);
429
- const provider = flagString(args.flags, "provider", "local-file");
430
- if (provider !== "local-file")
431
- throw new Error(`release: --provider must be local-file (use render-release for github); got ${provider}`);
432
- const artifactRoot = requireFlag(args, "artifact-root");
433
- const subjectId = requireFlag(args, "subject-id");
434
- const releasedBy = flagString(args.flags, "actor-json") ? loadActorStructFromFile(requireFlag(args, "actor-json")) : null;
435
- const reason = flagString(args.flags, "reason") ?? "released";
436
+ /**
437
+ * Wave 1 (#292) extraction: the durable-write body previously inlined inside releaseLocalFile's
438
+ * withSubjectLock() closure, now a parameter-driven pure function so the Stop hook's non-terminal
439
+ * release lifecycle (scripts/hooks/stop-goal-fit.js, #292 Wave 2) can reuse the EXACT same release
440
+ * logic actor-ownership verification, audit-trail append, atomic write under withSubjectLock
441
+ * rather than reimplementing a second, parallel release path. releaseLocalFile (CLI wrapper,
442
+ * below) is now a thin parse-args/print-envelope shell around this, mirroring the
443
+ * performLocalSupersede/supersedeLocalFile extraction shape exactly.
444
+ *
445
+ * Two behaviors are deliberately DIFFERENT from a naive inline release, both required for the
446
+ * Stop hook's idempotent, actor-scoped lifecycle release (never for the interactive CLI, which
447
+ * keeps `tolerateNoActiveClaim` unset/false and therefore 100% of its prior throw-on-error shape):
448
+ *
449
+ * - `opts.tolerateNoActiveClaim === true` and there is no existing record, or the existing
450
+ * record's status is not `"claimed"`: return `null` (a tolerated no-op) instead of throwing
451
+ * "no active claim to release". This is the one deliberate idempotency change vs today's
452
+ * releaseLocalFile — a second release call (e.g. a double Stop event) must be a safe no-op.
453
+ * - `releasedBy` is provided and does not match the existing record's holder: never force-release
454
+ * a claim held by a different actor — return `null` (if tolerateNoActiveClaim) or throw
455
+ * (otherwise), same as the no-active-claim case. The comparison mirrors computeEffectiveState()'s
456
+ * `record.actor_key || helper.serializeActor(record.actor)` canonical-key preference EXACTLY
457
+ * (actor_key-first, falling back to serializeActor only when actor_key is absent) — the read
458
+ * path (status/effective-state) and this write path (release) must use the identical
459
+ * canonical-key comparison, or a claim written under an explicit-override actor (`actor_key`
460
+ * bare, e.g. `"canonical-x"`, but `serializeActor(record.actor)` a DIFFERENT triple, e.g.
461
+ * `"explicit-override:canonical-x:host"`) can be self-recognized by computeEffectiveState() yet
462
+ * fail to release here because the releaser's canonical key was compared against the wrong
463
+ * (re-derived, triple) form instead of the stored actor_key. Comparing two serializeActor()
464
+ * calls unconditionally — as a prior version of this function did — is NOT correct for override
465
+ * actors and reintroduces the exact #291 seam on the release path.
466
+ *
467
+ * Contract: when `releasedBy` is provided AND the existing record is `actor_key`-stamped,
468
+ * `opts.actorKey` is REQUIRED (the canonical `resolveActor(env).actor` string) — otherwise
469
+ * ownership cannot be verified. A caller that passes `releasedBy` without `opts.actorKey` against
470
+ * an `actor_key`-stamped record would have its ownership compared as
471
+ * `existing.actor_key` (bare canonical) vs `serializeActor(releasedBy)` (re-derived triple), which
472
+ * can NEVER match even for the legitimate holder — a silent-failure trap, not a real ownership
473
+ * check. This is refused loudly (see the guard at the top of the `releasedBy` branch below) rather
474
+ * than allowed to silently no-op or wrongly refuse.
475
+ */
476
+ export function performLocalRelease(artifactRoot, subjectId, releasedBy, opts = {}) {
477
+ const helper = loadActorIdentityHelper();
478
+ const reason = opts.reason ?? "released";
479
+ const tolerateNoActiveClaim = opts.tolerateNoActiveClaim ?? false;
436
480
  // F1 fix (fix-plan iteration 1, CRITICAL): release mutates the same record file claim/supersede
437
481
  // do, under the same per-subject lock (see withSubjectLock()'s doc comment).
438
482
  return withSubjectLock(artifactRoot, subjectId, () => {
439
483
  const existing = readLocalRecord(artifactRoot, subjectId);
440
- if (!existing || existing.status !== "claimed")
484
+ if (!existing || existing.status !== "claimed") {
485
+ if (tolerateNoActiveClaim)
486
+ return null;
441
487
  throw new Error(`no active claim to release for subject: ${subjectId}`);
488
+ }
489
+ if (releasedBy) {
490
+ // Contract guard (hardening fix, #292 review): a caller that supplies `releasedBy` but NOT
491
+ // `opts.actorKey` against a record that already carries `actor_key` cannot reliably prove
492
+ // ownership — see this function's doc comment. This is the ONLY combination that fires: it
493
+ // does NOT fire when `existing.actor_key` is absent (the CLI/fixture path, where both sides
494
+ // fall back to serializeActor() and legitimately compare equal). Fail loudly rather than
495
+ // silently no-op (tolerant callers) or wrongly refuse (throwing callers) — never silent.
496
+ if (!opts.actorKey && existing.actor_key) {
497
+ if (tolerateNoActiveClaim) {
498
+ console.error(`[performLocalRelease] cannot verify ownership of an actor_key-stamped record without opts.actorKey; skipping release for ${subjectId}`);
499
+ return null;
500
+ }
501
+ throw new Error("performLocalRelease: pass opts.actorKey (the canonical resolveActor().actor string) when releasedBy is set and the record carries actor_key — serializeActor(releasedBy) is not a valid ownership key for actor_key-stamped records");
502
+ }
503
+ // AC6: never force-release a claim held by a different actor. Mirrors
504
+ // computeEffectiveState()'s canonical self-recognition comparison EXACTLY —
505
+ // `holderActorKey` prefers the stored `actor_key` (the canonical resolveActor(env).actor
506
+ // string, present on records written by the fixed performLocalClaim/performLocalSupersede
507
+ // paths) and only falls back to `serializeActor(existing.actor)` when `actor_key` is
508
+ // absent (every pre-fix record, every #290 eval fixture). The releaser's side must use the
509
+ // SAME canonical form: `opts.actorKey` (the caller's resolveActor(env).actor string, e.g.
510
+ // scripts/hooks/stop-goal-fit.js's Stop hook) when provided, else re-derived via
511
+ // serializeActor(releasedBy) — never serializeActor() unconditionally on both sides, which
512
+ // would compare the bare actor_key form against a re-derived triple form for an
513
+ // explicit-override actor and spuriously reject a legitimate same-actor release (the #291
514
+ // seam, relocated to this write path).
515
+ const holderActorKey = existing.actor_key || helper.serializeActor(existing.actor);
516
+ const releasedByActorKey = opts.actorKey || helper.serializeActor(releasedBy);
517
+ if (holderActorKey !== releasedByActorKey) {
518
+ if (tolerateNoActiveClaim)
519
+ return null;
520
+ throw new Error(`--actor-json does not match the current holder (${holderActorKey}); refusing to release a claim held by someone else`);
521
+ }
522
+ }
442
523
  const record = {
443
524
  ...existing,
525
+ ...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
444
526
  status: "released",
445
527
  audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "release", from_actor: existing.actor, to_actor: releasedBy, reason }],
446
528
  };
447
529
  writeLocalRecord(artifactRoot, subjectId, record);
448
- console.log(JSON.stringify({ role: "AssignmentReleaseResult", subject_id: subjectId, record }, null, 2));
449
- return 0;
530
+ return record;
450
531
  });
451
532
  }
533
+ function releaseLocalFile(argv) {
534
+ const args = parseArgs(argv);
535
+ const provider = flagString(args.flags, "provider", "local-file");
536
+ if (provider !== "local-file")
537
+ throw new Error(`release: --provider must be local-file (use render-release for github); got ${provider}`);
538
+ const artifactRoot = requireFlag(args, "artifact-root");
539
+ const subjectId = requireFlag(args, "subject-id");
540
+ const releasedBy = flagString(args.flags, "actor-json") ? loadActorStructFromFile(requireFlag(args, "actor-json")) : null;
541
+ const reason = flagString(args.flags, "reason") ?? "released";
542
+ const record = performLocalRelease(artifactRoot, subjectId, releasedBy, { reason, tolerateNoActiveClaim: false });
543
+ console.log(JSON.stringify({ role: "AssignmentReleaseResult", subject_id: subjectId, record }, null, 2));
544
+ return 0;
545
+ }
452
546
  /**
453
547
  * Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
454
548
  * withSubjectLock() closure, now a parameter-driven pure function so ensure-session's