@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.
- package/.github/workflows/ci.yml +16 -0
- package/CHANGELOG.md +30 -0
- package/build/src/cli/assignment-provider.d.ts +45 -0
- package/build/src/cli/assignment-provider.js +107 -13
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +238 -4
- package/build/src/cli/workflow-sidecar.js +875 -14
- package/build/src/tools/validate-source-tree.js +3 -2
- package/context/contracts/artifact-contract.md +16 -2
- package/context/contracts/assignment-provider-contract.md +1 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/scripts/hooks/config-protection.js +11 -4
- package/context/scripts/hooks/stop-goal-fit.js +259 -4
- package/context/scripts/hooks/workflow-steering.js +73 -1
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +111 -0
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/index.md +2 -2
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/fixture-ownership.md +3 -2
- package/docs/index.md +4 -0
- package/docs/integrations/flow-agents-console.md +108 -0
- package/docs/integrations/index.md +4 -0
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +8 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/integration/test_checkpoint_signing.sh +14 -5
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_gate_lockdown.sh +36 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +73 -8
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +261 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/run.sh +14 -0
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +125 -11
- package/kits/builder/skills/design-probe/SKILL.md +10 -0
- package/kits/builder/skills/execute-plan/SKILL.md +13 -0
- package/kits/builder/skills/fix-bug/SKILL.md +17 -0
- package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
- package/kits/builder/skills/plan-work/SKILL.md +9 -0
- package/kits/builder/skills/pull-work/SKILL.md +22 -2
- package/kits/builder/skills/review-work/SKILL.md +11 -0
- package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
- package/kits/builder/skills/verify-work/SKILL.md +11 -0
- package/kits/knowledge/adapters/default-store/index.js +56 -15
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +29 -11
- package/kits/knowledge/adapters/shared/codec.js +124 -0
- package/kits/knowledge/docs/store-contract.md +405 -3
- package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
- package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
- package/kits/knowledge/evals/contract-suite/suite.test.js +36 -0
- package/kits/knowledge/evals/freshness/suite.test.js +339 -0
- package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1 -1
- package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
- package/kits/veritas-governance/docs/README.md +81 -3
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +5 -0
- package/package.json +1 -1
- package/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +222 -279
- package/scripts/hooks/config-protection.js +11 -4
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/stop-goal-fit.js +259 -4
- package/scripts/hooks/workflow-steering.js +73 -1
- package/scripts/lib/reconcile-shape.js +381 -0
- package/src/cli/assignment-provider.ts +122 -13
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +965 -14
- package/src/tools/validate-source-tree.ts +3 -2
package/.github/workflows/ci.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|