@kontourai/flow-agents 3.7.0 → 3.9.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/CHANGELOG.md +19 -0
- package/build/src/builder-flow-runtime.js +118 -17
- package/build/src/cli/assignment-provider.js +13 -1
- package/build/src/cli/continuation-adapter.d.ts +24 -0
- package/build/src/cli/continuation-adapter.js +225 -0
- package/build/src/cli/workflow-sidecar.js +29 -11
- package/build/src/cli/workflow.js +67 -11
- package/build/src/continuation-driver.d.ts +92 -0
- package/build/src/continuation-driver.js +444 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/tools/build-universal-bundles.js +53 -3
- package/docs/getting-started.md +9 -1
- package/docs/public-workflow-cli.md +54 -0
- package/docs/spec/builder-flow-runtime.md +36 -2
- package/docs/workflow-usage-guide.md +7 -0
- package/evals/integration/test_builder_step_producers.sh +37 -4
- package/evals/integration/test_bundle_install.sh +108 -4
- package/evals/integration/test_bundle_lifecycle.sh +4 -6
- package/evals/integration/test_install_merge.sh +18 -8
- package/evals/integration/test_public_workflow_cli.sh +11 -5
- package/evals/static/test_universal_bundles.sh +44 -1
- package/package.json +4 -4
- package/packaging/README.md +1 -1
- package/scripts/README.md +1 -1
- package/scripts/install-codex-home.sh +63 -23
- package/scripts/install-owned-files.js +18 -2
- package/src/builder-flow-runtime.ts +108 -10
- package/src/cli/assignment-provider.ts +13 -1
- package/src/cli/builder-flow-runtime.test.mjs +378 -20
- package/src/cli/continuation-adapter.ts +239 -0
- package/src/cli/continuation-driver.test.mjs +564 -0
- package/src/cli/workflow-sidecar.ts +27 -11
- package/src/cli/workflow.ts +68 -11
- package/src/continuation-driver.ts +532 -0
- package/src/index.ts +20 -0
- package/src/tools/build-universal-bundles.ts +51 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.9.0](https://github.com/kontourai/flow-agents/compare/v3.8.0...v3.9.0) (2026-07-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **workflow:** add bounded continuation driver ([#560](https://github.com/kontourai/flow-agents/issues/560)) ([e6365ab](https://github.com/kontourai/flow-agents/commit/e6365aba324c76ee164681c987d20916c315444e))
|
|
9
|
+
|
|
10
|
+
## [3.8.0](https://github.com/kontourai/flow-agents/compare/v3.7.0...v3.8.0) (2026-07-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **packaging:** install portable skills under .agents ([#551](https://github.com/kontourai/flow-agents/issues/551)) ([12f12e3](https://github.com/kontourai/flow-agents/commit/12f12e3d0a32b398e47a9ac01604179c58bf14ba))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
* **builder:** make gate evidence sync visit-safe ([#558](https://github.com/kontourai/flow-agents/issues/558)) ([1c0e8dc](https://github.com/kontourai/flow-agents/commit/1c0e8dcea6bdfd7d54484f950b2432fcf727b0ea))
|
|
21
|
+
|
|
3
22
|
## [3.7.0](https://github.com/kontourai/flow-agents/compare/v3.6.0...v3.7.0) (2026-07-12)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -277,9 +277,13 @@ async function syncAndProject(context, initial, sidecarSnapshot) {
|
|
|
277
277
|
const snapshot = stageTrustBundleSnapshot(context);
|
|
278
278
|
try {
|
|
279
279
|
const rawBundle = JSON.parse(snapshot.raw.toString("utf8"));
|
|
280
|
-
const gateEvidence = await bundleGateEvidence(rawBundle, gates[0], run.state.subject, context.projectRoot);
|
|
280
|
+
const gateEvidence = await bundleGateEvidence(rawBundle, gates[0], run.state, run.state.subject, context.projectRoot, manifestEvidence(run.manifest));
|
|
281
281
|
if (gateEvidence) {
|
|
282
|
-
const alreadyAttached = manifestEvidence(run.manifest).some((entry) => entry.gate_id === gates[0].id
|
|
282
|
+
const alreadyAttached = manifestEvidence(run.manifest).some((entry) => entry.gate_id === gates[0].id
|
|
283
|
+
&& entry.sha256 === snapshot.sha256
|
|
284
|
+
&& typeof entry.superseded_by !== "string"
|
|
285
|
+
&& timestampAtOrAfter(entry.attached_at, gateEvidence.visitEnteredAt)
|
|
286
|
+
&& gateEvidence.expectationIds.every((expectationId) => Array.isArray(entry.expectation_ids) && entry.expectation_ids.includes(expectationId)));
|
|
283
287
|
if (!alreadyAttached) {
|
|
284
288
|
const supersede = manifestEvidence(run.manifest)
|
|
285
289
|
.filter((entry) => entry.gate_id === gates[0].id && typeof entry.superseded_by !== "string")
|
|
@@ -294,6 +298,7 @@ async function syncAndProject(context, initial, sidecarSnapshot) {
|
|
|
294
298
|
...(supersede.length > 0 ? { supersede } : {}),
|
|
295
299
|
...(gateEvidence.failed ? { status: "failed" } : {}),
|
|
296
300
|
...(gateEvidence.routeReason ? { routeReason: gateEvidence.routeReason } : {}),
|
|
301
|
+
expectationIds: gateEvidence.expectationIds,
|
|
297
302
|
},
|
|
298
303
|
});
|
|
299
304
|
attached = true;
|
|
@@ -377,15 +382,67 @@ function persistedFlowId(state) {
|
|
|
377
382
|
function openGatesForResult(run) {
|
|
378
383
|
return openGates(JSON.parse(fs.readFileSync(path.join(run.dir, "definition.json"), "utf8")), run.state);
|
|
379
384
|
}
|
|
380
|
-
async function bundleGateEvidence(bundle, gate, subject, projectRoot) {
|
|
385
|
+
async function bundleGateEvidence(bundle, gate, state, subject, projectRoot, manifest) {
|
|
381
386
|
if (!isRecord(bundle) || !Array.isArray(bundle.claims))
|
|
382
387
|
return null;
|
|
383
|
-
const
|
|
388
|
+
const expectations = expectationsForGate(gate);
|
|
389
|
+
const visit = currentGateVisit(state, String(gate.step));
|
|
390
|
+
const enteredAt = visit.enteredAt;
|
|
391
|
+
const synchronizedAt = Date.now();
|
|
392
|
+
const maxClockSkewMs = 30_000;
|
|
393
|
+
const priorVisitClaimIds = new Set();
|
|
394
|
+
const priorVisitEvidenceIds = new Set();
|
|
395
|
+
for (const entry of manifest) {
|
|
396
|
+
if (entry.gate_id !== String(gate.id))
|
|
397
|
+
continue;
|
|
398
|
+
const claims = isRecord(entry.bundle) && Array.isArray(entry.bundle.claims) ? entry.bundle.claims : [];
|
|
399
|
+
for (const historical of claims) {
|
|
400
|
+
if (isRecord(historical) && typeof historical.id === "string")
|
|
401
|
+
priorVisitClaimIds.add(historical.id);
|
|
402
|
+
}
|
|
403
|
+
const evidence = isRecord(entry.bundle) && Array.isArray(entry.bundle.evidence) ? entry.bundle.evidence : [];
|
|
404
|
+
for (const historical of evidence) {
|
|
405
|
+
if (isRecord(historical) && typeof historical.id === "string")
|
|
406
|
+
priorVisitEvidenceIds.add(historical.id);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const claimIsCurrent = (claim) => {
|
|
410
|
+
if (typeof claim.id !== "string" || priorVisitClaimIds.has(claim.id))
|
|
411
|
+
return false;
|
|
412
|
+
const timestamps = [];
|
|
413
|
+
const createdAt = parseTimestamp(claim.createdAt);
|
|
414
|
+
if (createdAt !== null)
|
|
415
|
+
timestamps.push(createdAt);
|
|
416
|
+
if (Array.isArray(bundle.evidence))
|
|
417
|
+
for (const evidence of bundle.evidence) {
|
|
418
|
+
if (!isRecord(evidence) || evidence.claimId !== claim.id)
|
|
419
|
+
continue;
|
|
420
|
+
if (typeof evidence.id !== "string" || priorVisitEvidenceIds.has(evidence.id))
|
|
421
|
+
return false;
|
|
422
|
+
const observedAt = parseTimestamp(evidence.observedAt);
|
|
423
|
+
if (observedAt !== null)
|
|
424
|
+
timestamps.push(observedAt);
|
|
425
|
+
}
|
|
426
|
+
const initialAcquisitionSkew = visit.initial && claim.claimType === "builder.pull-work.selected" ? maxClockSkewMs : 0;
|
|
427
|
+
return timestamps.some((timestamp) => timestamp >= enteredAt - initialAcquisitionSkew
|
|
428
|
+
&& timestamp <= synchronizedAt + maxClockSkewMs);
|
|
429
|
+
};
|
|
384
430
|
const relevant = bundle.claims.filter((claim) => {
|
|
385
431
|
if (!isRecord(claim))
|
|
386
432
|
return false;
|
|
387
|
-
|
|
388
|
-
|
|
433
|
+
if (claim.producerStatus === "superseded")
|
|
434
|
+
return false;
|
|
435
|
+
const metadata = isRecord(claim.metadata) ? claim.metadata : null;
|
|
436
|
+
if (metadata && typeof metadata.superseded_by === "string")
|
|
437
|
+
return false;
|
|
438
|
+
return expectations.some((expectation) => {
|
|
439
|
+
const candidate = expectation.bundle_claim;
|
|
440
|
+
return candidate
|
|
441
|
+
&& claimIsCurrent(claim)
|
|
442
|
+
&&
|
|
443
|
+
candidate.claimType === claim.claimType
|
|
444
|
+
&& (!candidate.subjectType || candidate.subjectType === claim.subjectType);
|
|
445
|
+
});
|
|
389
446
|
});
|
|
390
447
|
if (relevant.length === 0)
|
|
391
448
|
return null;
|
|
@@ -393,6 +450,11 @@ async function bundleGateEvidence(bundle, gate, subject, projectRoot) {
|
|
|
393
450
|
throw new BuilderBuildRunInputError("evidence.claims.metadata.workflow_subject_ref", "must match the persisted run subject");
|
|
394
451
|
}
|
|
395
452
|
const failed = relevant.some((claim) => claim.value === "fail" || claim.status === "disputed");
|
|
453
|
+
const expectationIds = expectations.filter((expectation) => relevant.some((claim) => {
|
|
454
|
+
const selector = expectation.bundle_claim;
|
|
455
|
+
return selector.claimType === claim.claimType && (!selector.subjectType || selector.subjectType === claim.subjectType);
|
|
456
|
+
})).map((expectation) => expectation.id);
|
|
457
|
+
const missingRequired = expectations.filter((expectation) => expectation.required && !expectationIds.includes(expectation.id));
|
|
396
458
|
const routeReasons = [...new Set(relevant.flatMap((claim) => {
|
|
397
459
|
const metadata = isRecord(claim.metadata) ? claim.metadata : null;
|
|
398
460
|
const gateClaim = metadata && isRecord(metadata.gate_claim) ? metadata.gate_claim : null;
|
|
@@ -402,6 +464,13 @@ async function bundleGateEvidence(bundle, gate, subject, projectRoot) {
|
|
|
402
464
|
throw new BuilderBuildRunInputError("evidence.claims.metadata.gate_claim.route_reason", "must agree across current-gate claims");
|
|
403
465
|
}
|
|
404
466
|
const routeReason = routeReasons[0] ?? null;
|
|
467
|
+
if (failed && !routeReason)
|
|
468
|
+
return null;
|
|
469
|
+
// Passing evidence waits for the complete expectation set. A failing
|
|
470
|
+
// snapshot is complete only when a gate producer explicitly declares its
|
|
471
|
+
// route reason; report-only disputed critique state remains pending.
|
|
472
|
+
if (!failed && missingRequired.length > 0)
|
|
473
|
+
return null;
|
|
405
474
|
if (routeReason && !failed) {
|
|
406
475
|
throw new BuilderBuildRunInputError("evidence.claims.metadata.gate_claim.route_reason", "requires failed current-gate evidence");
|
|
407
476
|
}
|
|
@@ -412,7 +481,33 @@ async function bundleGateEvidence(bundle, gate, subject, projectRoot) {
|
|
|
412
481
|
if (String(gate.id) === "verify-gate" && relevant.some((claim) => claim.claimType === "builder.verify.tests" && claim.value === "pass")) {
|
|
413
482
|
await assertVerifiedTestsTrust(bundle.claims, projectRoot);
|
|
414
483
|
}
|
|
415
|
-
return { failed, routeReason };
|
|
484
|
+
return { failed, routeReason, expectationIds, visitEnteredAt: enteredAt };
|
|
485
|
+
}
|
|
486
|
+
function currentGateVisit(state, step) {
|
|
487
|
+
let enteredAt = null;
|
|
488
|
+
for (const transition of state.transitions ?? []) {
|
|
489
|
+
if (transition.to_step !== step)
|
|
490
|
+
continue;
|
|
491
|
+
const parsed = parseTimestamp(transition.at);
|
|
492
|
+
if (parsed !== null)
|
|
493
|
+
enteredAt = parsed;
|
|
494
|
+
}
|
|
495
|
+
const initial = parseTimestamp(state.updated_at);
|
|
496
|
+
if (enteredAt !== null)
|
|
497
|
+
return { enteredAt, initial: false };
|
|
498
|
+
if (initial !== null)
|
|
499
|
+
return { enteredAt: initial, initial: true };
|
|
500
|
+
throw new BuilderBuildRunInputError("flow_run.state.updated_at", "must establish the current gate visit boundary");
|
|
501
|
+
}
|
|
502
|
+
function parseTimestamp(value) {
|
|
503
|
+
if (typeof value !== "string")
|
|
504
|
+
return null;
|
|
505
|
+
const parsed = Date.parse(value);
|
|
506
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
507
|
+
}
|
|
508
|
+
function timestampAtOrAfter(value, boundary) {
|
|
509
|
+
const parsed = parseTimestamp(value);
|
|
510
|
+
return parsed !== null && parsed >= boundary;
|
|
416
511
|
}
|
|
417
512
|
async function assertVerifiedTestsTrust(claims, projectRoot) {
|
|
418
513
|
const testClaims = claims.filter((claim) => isRecord(claim)
|
|
@@ -665,7 +760,9 @@ function projectFlowRun(context, run, sidecar) {
|
|
|
665
760
|
const complete = run.state.status === "completed";
|
|
666
761
|
const paused = run.state.status === "paused";
|
|
667
762
|
const canceled = run.state.status === "canceled";
|
|
668
|
-
const
|
|
763
|
+
const needsDecision = run.state.status === "needs_decision";
|
|
764
|
+
const failed = run.state.status === "failed";
|
|
765
|
+
const action = complete || paused || canceled || needsDecision || failed ? { skills: [], operations: [] } : stepAction(run.definitionId, run.state.current_step);
|
|
669
766
|
if (!action) {
|
|
670
767
|
throw new BuilderBuildRunInputError("kit.flow_step_actions", `does not declare Builder step ${run.state.current_step}`);
|
|
671
768
|
}
|
|
@@ -690,18 +787,22 @@ function projectFlowRun(context, run, sidecar) {
|
|
|
690
787
|
? { status: "done", summary: "Canonical Flow run was canceled by an authorized external request. Artifacts are retained until separately archived." }
|
|
691
788
|
: paused
|
|
692
789
|
? { status: "blocked", summary: "Canonical Flow run is paused. The current assignment actor may resume it with a reason." }
|
|
693
|
-
:
|
|
694
|
-
status: "
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
790
|
+
: needsDecision
|
|
791
|
+
? { status: "blocked", summary: "Canonical Flow requires an external decision before continuation." }
|
|
792
|
+
: failed
|
|
793
|
+
? { status: "failed", summary: "Canonical Flow run failed; no continuation turn is allowed." }
|
|
794
|
+
: {
|
|
795
|
+
status: "continue",
|
|
796
|
+
summary: `Flow step \`${run.state.current_step}\`: ${skillText}${operationText} ${gateText}${routeText} Then synchronize the recorded evidence.`,
|
|
797
|
+
skills,
|
|
798
|
+
operations,
|
|
799
|
+
command: syncCommand,
|
|
800
|
+
};
|
|
700
801
|
const phase = phaseForStep(definition.phase_map, run.state.current_step) ?? sidecar.phase;
|
|
701
802
|
return {
|
|
702
803
|
...sidecar,
|
|
703
|
-
status: complete ? "delivered" : canceled ? "canceled" : paused ? "blocked" : (run.state.transitions.length > 0 ? "in_progress" : sidecar.status),
|
|
704
|
-
phase: complete || canceled ? "done" : phase,
|
|
804
|
+
status: complete ? "delivered" : canceled ? "canceled" : failed ? "failed" : (paused || needsDecision) ? "blocked" : (run.state.transitions.length > 0 ? "in_progress" : sidecar.status),
|
|
805
|
+
phase: complete || canceled || failed ? "done" : phase,
|
|
705
806
|
updated_at: run.state.updated_at,
|
|
706
807
|
flow_run: {
|
|
707
808
|
run_id: run.runId,
|
|
@@ -626,7 +626,19 @@ export function performLocalReleaseUnderLock(artifactRoot, subjectId, releasedBy
|
|
|
626
626
|
// seam, relocated to this write path).
|
|
627
627
|
const holderActorKey = existing.actor_key || helper.serializeActor(existing.actor);
|
|
628
628
|
const releasedByActorKey = opts.actorKey || helper.serializeActor(releasedBy);
|
|
629
|
-
|
|
629
|
+
// Pre-3.7 lifecycle events could persist the derived ancestry actor before
|
|
630
|
+
// sanitizeSegment removed ':' separators. Modern explicit/env release paths
|
|
631
|
+
// always use the sanitized form. Accept only that one-way legacy migration;
|
|
632
|
+
// never normalize two modern keys or relax ownership to a prefix match.
|
|
633
|
+
const sameActorStruct = existing.actor.runtime === releasedBy.runtime
|
|
634
|
+
&& existing.actor.session_id === releasedBy.session_id
|
|
635
|
+
&& existing.actor.host === releasedBy.host
|
|
636
|
+
&& (existing.actor.human ?? null) === (releasedBy.human ?? null);
|
|
637
|
+
const legacyActorKeyMatches = holderActorKey.includes(":")
|
|
638
|
+
&& holderActorKey === helper.serializeActor(existing.actor)
|
|
639
|
+
&& helper.sanitizeSegment(holderActorKey) === releasedByActorKey
|
|
640
|
+
&& sameActorStruct;
|
|
641
|
+
if (holderActorKey !== releasedByActorKey && !legacyActorKeyMatches) {
|
|
630
642
|
if (tolerateNoActiveClaim)
|
|
631
643
|
return null;
|
|
632
644
|
throw new Error(`--actor-json does not match the current holder (${holderActorKey}); refusing to release a claim held by someone else`);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ContinuationBarrier, ContinuationTurnRequest, ContinuationTurnResult } from "../continuation-driver.js";
|
|
2
|
+
export type ContinuationAdapterCommand = {
|
|
3
|
+
argv: string[];
|
|
4
|
+
identity: string;
|
|
5
|
+
integrity: Array<{
|
|
6
|
+
file: string;
|
|
7
|
+
sha256: string;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
export declare function loadContinuationAdapterCommand(commandFileInput: string): ContinuationAdapterCommand;
|
|
11
|
+
export declare function executeContinuationAdapter(commandFileInput: string, request: ContinuationTurnRequest, options: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
}): Promise<ContinuationTurnResult>;
|
|
15
|
+
export declare function executeLoadedContinuationAdapter(command: ContinuationAdapterCommand, request: ContinuationTurnRequest, options: {
|
|
16
|
+
cwd: string;
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
}): Promise<ContinuationTurnResult>;
|
|
19
|
+
export declare function waitForContinuationBarrier(barrier: ContinuationBarrier, options: {
|
|
20
|
+
maxWaitMs: number;
|
|
21
|
+
pollMs: number;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
sleep?: (ms: number) => Promise<void>;
|
|
24
|
+
}): Promise<"ready" | "pending">;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
export function loadContinuationAdapterCommand(commandFileInput) {
|
|
6
|
+
const commandFile = path.resolve(commandFileInput);
|
|
7
|
+
const command = validateAdapterCommand(JSON.parse(readRegularFileNoFollow(commandFile, "continuation adapter command file")));
|
|
8
|
+
if (!path.isAbsolute(command.argv[0]))
|
|
9
|
+
throw new Error("continuation adapter executable must be an absolute path");
|
|
10
|
+
const integrity = [...new Set(command.argv.filter((entry) => path.isAbsolute(entry) && regularFileExists(entry)))]
|
|
11
|
+
.map((file) => ({ file, sha256: sha256File(file, "continuation adapter integrity file") }));
|
|
12
|
+
if (!integrity.some((entry) => entry.file === command.argv[0]))
|
|
13
|
+
throw new Error("continuation adapter executable must be a regular file");
|
|
14
|
+
const identity = createHash("sha256").update(JSON.stringify({ ...command, integrity })).digest("hex");
|
|
15
|
+
return { ...command, identity, integrity };
|
|
16
|
+
}
|
|
17
|
+
export async function executeContinuationAdapter(commandFileInput, request, options) {
|
|
18
|
+
const command = loadContinuationAdapterCommand(commandFileInput);
|
|
19
|
+
return executeLoadedContinuationAdapter(command, request, options);
|
|
20
|
+
}
|
|
21
|
+
export async function executeLoadedContinuationAdapter(command, request, options) {
|
|
22
|
+
for (const entry of command.integrity) {
|
|
23
|
+
if (sha256File(entry.file, "continuation adapter integrity file") !== entry.sha256) {
|
|
24
|
+
throw new Error(`continuation adapter integrity changed after mission binding: ${entry.file}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
assertPositiveInteger(options.timeoutMs, "continuation adapter timeoutMs", 1, 86_400_000);
|
|
28
|
+
return await spawnAdapter(command, request, options);
|
|
29
|
+
}
|
|
30
|
+
export async function waitForContinuationBarrier(barrier, options) {
|
|
31
|
+
assertPositiveInteger(options.maxWaitMs, "continuation barrier maxWaitMs", 0, 86_400_000);
|
|
32
|
+
assertPositiveInteger(options.pollMs, "continuation barrier pollMs", 1, 60_000);
|
|
33
|
+
const now = options.now ?? Date.now;
|
|
34
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
35
|
+
const stopAt = now() + options.maxWaitMs;
|
|
36
|
+
if (barrier.kind === "deadline") {
|
|
37
|
+
const deadline = Date.parse(barrier.at);
|
|
38
|
+
if (deadline <= now())
|
|
39
|
+
return "ready";
|
|
40
|
+
const waitMs = Math.min(deadline - now(), Math.max(0, stopAt - now()));
|
|
41
|
+
if (waitMs > 0)
|
|
42
|
+
await sleep(waitMs);
|
|
43
|
+
return Date.parse(barrier.at) <= now() ? "ready" : "pending";
|
|
44
|
+
}
|
|
45
|
+
while (pidAlive(barrier.pid)) {
|
|
46
|
+
const remaining = stopAt - now();
|
|
47
|
+
if (remaining <= 0)
|
|
48
|
+
return "pending";
|
|
49
|
+
await sleep(Math.min(options.pollMs, remaining));
|
|
50
|
+
}
|
|
51
|
+
return "ready";
|
|
52
|
+
}
|
|
53
|
+
function spawnAdapter(command, request, options) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(command.argv[0], command.argv.slice(1), {
|
|
56
|
+
cwd: options.cwd,
|
|
57
|
+
env: process.env,
|
|
58
|
+
detached: process.platform !== "win32",
|
|
59
|
+
shell: false,
|
|
60
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
const stdout = [];
|
|
63
|
+
const stderr = [];
|
|
64
|
+
let stdoutBytes = 0;
|
|
65
|
+
let stderrBytes = 0;
|
|
66
|
+
let settled = false;
|
|
67
|
+
let forcedKill;
|
|
68
|
+
let terminationError;
|
|
69
|
+
const maxBytes = 4 * 1024 * 1024;
|
|
70
|
+
const timeout = setTimeout(() => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
terminationError = new Error(`continuation adapter timed out after ${options.timeoutMs}ms`);
|
|
74
|
+
terminateProcessGroup(child.pid, "SIGTERM");
|
|
75
|
+
forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
|
|
76
|
+
}, options.timeoutMs);
|
|
77
|
+
child.stdout.on("data", (chunk) => {
|
|
78
|
+
stdoutBytes += chunk.length;
|
|
79
|
+
if (stdoutBytes > maxBytes) {
|
|
80
|
+
if (!terminationError) {
|
|
81
|
+
terminationError = new Error("continuation adapter stdout exceeded 4 MiB");
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
terminateProcessGroup(child.pid, "SIGTERM");
|
|
84
|
+
forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
stdout.push(chunk);
|
|
89
|
+
});
|
|
90
|
+
child.stderr.on("data", (chunk) => {
|
|
91
|
+
stderrBytes += chunk.length;
|
|
92
|
+
if (stderrBytes <= maxBytes)
|
|
93
|
+
stderr.push(chunk);
|
|
94
|
+
});
|
|
95
|
+
child.on("error", (error) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
if (forcedKill)
|
|
101
|
+
clearTimeout(forcedKill);
|
|
102
|
+
reject(error);
|
|
103
|
+
});
|
|
104
|
+
child.on("close", (code, signal) => {
|
|
105
|
+
if (settled)
|
|
106
|
+
return;
|
|
107
|
+
settled = true;
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
if (forcedKill)
|
|
110
|
+
clearTimeout(forcedKill);
|
|
111
|
+
if (terminationError) {
|
|
112
|
+
reject(terminationError);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const stderrText = Buffer.concat(stderr).toString("utf8").trim();
|
|
116
|
+
if (code !== 0) {
|
|
117
|
+
scheduleProcessGroupTermination(child.pid);
|
|
118
|
+
reject(new Error(`continuation adapter exited ${code ?? signal ?? "unknown"}${stderrText ? `: ${stderrText}` : ""}`));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const output = Buffer.concat(stdout).toString("utf8").trim();
|
|
122
|
+
try {
|
|
123
|
+
const result = JSON.parse(output);
|
|
124
|
+
if (!isValidWaitResult(result))
|
|
125
|
+
scheduleProcessGroupTermination(child.pid);
|
|
126
|
+
resolve(result);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
scheduleProcessGroupTermination(child.pid);
|
|
130
|
+
reject(new Error("continuation adapter must emit exactly one JSON result on stdout"));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
child.stdin.on("error", (error) => {
|
|
134
|
+
if (error.code !== "EPIPE" && !settled) {
|
|
135
|
+
settled = true;
|
|
136
|
+
clearTimeout(timeout);
|
|
137
|
+
if (forcedKill)
|
|
138
|
+
clearTimeout(forcedKill);
|
|
139
|
+
scheduleProcessGroupTermination(child.pid);
|
|
140
|
+
reject(error);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
child.stdin.end(`${JSON.stringify(request)}\n`);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function validateAdapterCommand(value) {
|
|
147
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
148
|
+
throw new Error("continuation adapter command file must contain an object");
|
|
149
|
+
const argv = value.argv;
|
|
150
|
+
if (!Array.isArray(argv) || argv.length === 0 || argv.length > 128 || argv.some((entry) => typeof entry !== "string" || entry.length === 0 || entry.includes("\0"))) {
|
|
151
|
+
throw new Error("continuation adapter command argv must contain 1 through 128 non-empty strings");
|
|
152
|
+
}
|
|
153
|
+
return { argv: [...argv] };
|
|
154
|
+
}
|
|
155
|
+
function assertRegularFile(file, label) {
|
|
156
|
+
const stat = fs.lstatSync(file);
|
|
157
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
158
|
+
throw new Error(`${label} must be a regular file`);
|
|
159
|
+
}
|
|
160
|
+
function readRegularFileNoFollow(file, label) {
|
|
161
|
+
return readRegularFileBufferNoFollow(file, label).toString("utf8");
|
|
162
|
+
}
|
|
163
|
+
function readRegularFileBufferNoFollow(file, label) {
|
|
164
|
+
assertRegularFile(file, label);
|
|
165
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
166
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
167
|
+
try {
|
|
168
|
+
if (!fs.fstatSync(fd).isFile())
|
|
169
|
+
throw new Error(`${label} must be a regular file`);
|
|
170
|
+
return fs.readFileSync(fd);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
fs.closeSync(fd);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function assertPositiveInteger(value, label, min, max) {
|
|
177
|
+
if (!Number.isSafeInteger(value) || value < min || value > max)
|
|
178
|
+
throw new Error(`${label} must be an integer from ${min} through ${max}`);
|
|
179
|
+
}
|
|
180
|
+
function pidAlive(pid) {
|
|
181
|
+
try {
|
|
182
|
+
process.kill(pid, 0);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return error.code === "EPERM";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function terminateProcessGroup(pid, signal) {
|
|
190
|
+
if (!pid)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
process.kill(process.platform === "win32" ? pid : -pid, signal);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (error.code !== "ESRCH")
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function isValidWaitResult(result) {
|
|
201
|
+
if (result?.status !== "wait" || !result.barrier || typeof result.barrier !== "object")
|
|
202
|
+
return false;
|
|
203
|
+
if (result.barrier.kind === "pid")
|
|
204
|
+
return Number.isSafeInteger(result.barrier.pid) && result.barrier.pid > 0;
|
|
205
|
+
return result.barrier.kind === "deadline" && typeof result.barrier.at === "string" && Number.isFinite(Date.parse(result.barrier.at));
|
|
206
|
+
}
|
|
207
|
+
function scheduleProcessGroupTermination(pid) {
|
|
208
|
+
terminateProcessGroup(pid, "SIGTERM");
|
|
209
|
+
const forcedKill = setTimeout(() => terminateProcessGroup(pid, "SIGKILL"), 250);
|
|
210
|
+
forcedKill.unref();
|
|
211
|
+
}
|
|
212
|
+
function regularFileExists(file) {
|
|
213
|
+
try {
|
|
214
|
+
const stat = fs.lstatSync(file);
|
|
215
|
+
return !stat.isSymbolicLink() && stat.isFile();
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error.code === "ENOENT")
|
|
219
|
+
return false;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function sha256File(file, label) {
|
|
224
|
+
return createHash("sha256").update(readRegularFileBufferNoFollow(file, label)).digest("hex");
|
|
225
|
+
}
|
|
@@ -880,7 +880,9 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
880
880
|
continue;
|
|
881
881
|
const subjectId = `${slug}/${check.id}`;
|
|
882
882
|
const fieldOrBehavior = String(check.summary ?? check.id);
|
|
883
|
-
const
|
|
883
|
+
const gateClaimIdentityVersion = check._gate_claim_identity_version === 2 ? 2 : 1;
|
|
884
|
+
const gateClaimRecordedAt = gateClaimIdentityVersion === 2 && typeof check._gate_claim_recorded_at === "string" ? check._gate_claim_recorded_at : null;
|
|
885
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", gateClaimRecordedAt ? `${fieldOrBehavior}::gate-visit::${gateClaimRecordedAt}` : fieldOrBehavior);
|
|
884
886
|
const evId = `ev:${claimId}`;
|
|
885
887
|
const legacyClaimType = `workflow.check.${check.kind ?? "external"}`;
|
|
886
888
|
const cmd = typeof check.command === "string" ? check.command.replace(/\s+/g, " ").trim() : "";
|
|
@@ -1034,7 +1036,7 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1034
1036
|
// restored stamp) takes the currently-active step's id.
|
|
1035
1037
|
const declaredStepId = gateClaimDeclaredStepId ?? (activeStep ? activeStep.stepId : null);
|
|
1036
1038
|
const declaredMetadata = gateClaimExpectationId
|
|
1037
|
-
? { ...claimMetadata, gate_claim: { expectation_id: gateClaimExpectationId, claim_type: declared.claimType, subject_type: declared.subjectType, step_id: declaredStepId, ...(gateClaimRouteReason ? { route_reason: gateClaimRouteReason } : {}) } }
|
|
1039
|
+
? { ...claimMetadata, gate_claim: { expectation_id: gateClaimExpectationId, claim_type: declared.claimType, subject_type: declared.subjectType, step_id: declaredStepId, ...(gateClaimIdentityVersion === 2 ? { identity_version: 2 } : {}), ...(gateClaimRecordedAt ? { recorded_at: gateClaimRecordedAt } : {}), ...(gateClaimRouteReason ? { route_reason: gateClaimRouteReason } : {}) } }
|
|
1038
1040
|
: claimMetadata;
|
|
1039
1041
|
const declaredClaimObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: effectiveStatus, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, ...(declaredMetadata ? { metadata: declaredMetadata } : {}) };
|
|
1040
1042
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj, evidence: [evItem], events: claimEvents, policies: [declaredPolicy] });
|
|
@@ -1053,7 +1055,9 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1053
1055
|
continue;
|
|
1054
1056
|
const subjectId = `${slug}/${criterion.id}`;
|
|
1055
1057
|
const fieldOrBehavior = String(criterion.description ?? criterion.id);
|
|
1056
|
-
const
|
|
1058
|
+
const criterionIdentityVersion = criterion.identity_version === 2 ? 2 : 1;
|
|
1059
|
+
const criterionVerifiedAt = criterionIdentityVersion === 2 && typeof criterion.verified_at === "string" ? criterion.verified_at : null;
|
|
1060
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", criterionVerifiedAt ? `${fieldOrBehavior}::verified::${criterionVerifiedAt}` : fieldOrBehavior);
|
|
1057
1061
|
const legacyClaimType = "workflow.acceptance.criterion";
|
|
1058
1062
|
const policy = ensurePolicy(legacyClaimType, "high", []);
|
|
1059
1063
|
const evStatus = criterionStatusToEventStatus(String(criterion.status ?? ""));
|
|
@@ -1068,7 +1072,7 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1068
1072
|
if (declared) {
|
|
1069
1073
|
// Declared kit-typed claim only — no legacy shadow (ADR 0016 P-d).
|
|
1070
1074
|
const declaredPolicy = ensurePolicy(declared.claimType, "high", []);
|
|
1071
|
-
const declaredClaimObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [] }, ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
|
|
1075
|
+
const declaredClaimObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [], ...(criterionIdentityVersion === 2 ? { identity_version: 2 } : {}), ...(criterionVerifiedAt ? { verified_at: criterionVerifiedAt } : {}) }, ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
|
|
1072
1076
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj, evidence: [], events: claimEvents, policies: [declaredPolicy] });
|
|
1073
1077
|
claims.push({ ...declaredClaimObj, status: declaredStatus });
|
|
1074
1078
|
}
|
|
@@ -1092,10 +1096,12 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1092
1096
|
const supersededBy = typeof c.superseded_by === "string" && c.superseded_by.length > 0 ? c.superseded_by : null;
|
|
1093
1097
|
const critiqueReviewer = String(c.reviewer ?? "tool-code-reviewer");
|
|
1094
1098
|
const critiqueReviewedAt = String(c.reviewed_at ?? ts);
|
|
1099
|
+
const critiqueIdentityVersion = c.identity_version === 2 ? 2 : 1;
|
|
1095
1100
|
const critMeta = {
|
|
1096
1101
|
origin: "critique",
|
|
1097
1102
|
reviewer: critiqueReviewer,
|
|
1098
1103
|
reviewed_at: critiqueReviewedAt,
|
|
1104
|
+
...(critiqueIdentityVersion === 2 ? { identity_version: 2 } : {}),
|
|
1099
1105
|
findings: Array.isArray(c.findings) ? c.findings : [],
|
|
1100
1106
|
lanes: Array.isArray(c.lanes) ? c.lanes : [],
|
|
1101
1107
|
review_target: c.review_target && typeof c.review_target === "object" ? c.review_target : { artifacts: [] },
|
|
@@ -1107,7 +1113,9 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1107
1113
|
// A superseded historical write gets a distinct, stable claimId so it co-exists with the live
|
|
1108
1114
|
// claim of the same critique id (never overwrites or duplicates it). The salt is reproducible
|
|
1109
1115
|
// across rebuilds because superseded_by + reviewed_at are preserved in metadata.
|
|
1110
|
-
const claimIdSalt =
|
|
1116
|
+
const claimIdSalt = critiqueIdentityVersion === 2
|
|
1117
|
+
? `${fieldOrBehavior}::reviewed::${critiqueReviewedAt}${supersededBy ? `::superseded::${supersededBy}` : ""}`
|
|
1118
|
+
: (supersededBy ? `${fieldOrBehavior}::superseded::${supersededBy}::${critiqueReviewedAt}` : fieldOrBehavior);
|
|
1111
1119
|
const claimId = generateClaimId(subjectId, "flow-agents.workflow", claimIdSalt);
|
|
1112
1120
|
const legacyClaimType = "workflow.critique.review";
|
|
1113
1121
|
const policy = ensurePolicy(legacyClaimType, "medium", []);
|
|
@@ -2770,7 +2778,7 @@ function requireObservedCommandRefs(refs, observedCommands, label, requireAll =
|
|
|
2770
2778
|
die(`${label} requires a top-level command evidence ref for every successful observed command`);
|
|
2771
2779
|
}
|
|
2772
2780
|
}
|
|
2773
|
-
function completePassingCriteria(existing, raw, observedCommands) {
|
|
2781
|
+
function completePassingCriteria(existing, raw, observedCommands, verifiedAt) {
|
|
2774
2782
|
if (raw.length === 0)
|
|
2775
2783
|
die("record-gate-claim requires --criterion-json for a passing tests-evidence claim");
|
|
2776
2784
|
const incoming = raw.map((value) => parseJson(value, "--criterion-json"));
|
|
@@ -2794,7 +2802,7 @@ function completePassingCriteria(existing, raw, observedCommands) {
|
|
|
2794
2802
|
if (refs.length === 0)
|
|
2795
2803
|
die(`criterion ${ids[index]} requires reviewable evidence_refs`);
|
|
2796
2804
|
requireObservedCommandRefs(refs, observedCommands, `criterion ${ids[index]}`);
|
|
2797
|
-
return { ...expectedById.get(ids[index]), status: "pass", evidence_refs: refs };
|
|
2805
|
+
return { ...expectedById.get(ids[index]), status: "pass", evidence_refs: refs, identity_version: 2, verified_at: verifiedAt };
|
|
2798
2806
|
});
|
|
2799
2807
|
}
|
|
2800
2808
|
const critiqueStatuses = new Set(["pass", "fail", "not_verified"]);
|
|
@@ -3234,6 +3242,10 @@ function checksFromBundle(dir) {
|
|
|
3234
3242
|
check._gate_claim_declared_subject = gc.subject_type;
|
|
3235
3243
|
if (typeof gc.step_id === "string")
|
|
3236
3244
|
check._gate_claim_declared_step_id = gc.step_id;
|
|
3245
|
+
if (typeof gc.recorded_at === "string")
|
|
3246
|
+
check._gate_claim_recorded_at = gc.recorded_at;
|
|
3247
|
+
if (gc.identity_version === 2)
|
|
3248
|
+
check._gate_claim_identity_version = 2;
|
|
3237
3249
|
if (typeof gc.route_reason === "string")
|
|
3238
3250
|
check._gate_claim_route_reason = gc.route_reason;
|
|
3239
3251
|
};
|
|
@@ -3423,6 +3435,7 @@ function critiquesFromBundle(dir) {
|
|
|
3423
3435
|
review_target: md.review_target && typeof md.review_target === "object" && !Array.isArray(md.review_target) ? md.review_target : { artifacts: [] },
|
|
3424
3436
|
reviewer: typeof md.reviewer === "string" ? md.reviewer : "tool-code-reviewer",
|
|
3425
3437
|
reviewed_at: typeof md.reviewed_at === "string" ? md.reviewed_at : (c.updatedAt || c.createdAt || now()),
|
|
3438
|
+
...(md.identity_version === 2 ? { identity_version: 2 } : {}),
|
|
3426
3439
|
artifact_refs: Array.isArray(md.artifact_refs) ? md.artifact_refs : [],
|
|
3427
3440
|
...(typeof md.superseded_by === "string" && md.superseded_by.length > 0 ? { superseded_by: md.superseded_by } : {}),
|
|
3428
3441
|
};
|
|
@@ -3444,6 +3457,8 @@ function criteriaFromBundle(dir) {
|
|
|
3444
3457
|
description: typeof saved.description === "string" ? saved.description : (claim.fieldOrBehavior || ""),
|
|
3445
3458
|
status: typeof saved.status === "string" ? saved.status : (claim.value ?? "not_verified"),
|
|
3446
3459
|
evidence_refs: Array.isArray(saved.evidence_refs) ? saved.evidence_refs : [],
|
|
3460
|
+
...(typeof saved.verified_at === "string" ? { verified_at: saved.verified_at } : {}),
|
|
3461
|
+
...(saved.identity_version === 2 ? { identity_version: 2 } : {}),
|
|
3447
3462
|
};
|
|
3448
3463
|
})
|
|
3449
3464
|
.filter((criterion) => typeof criterion.id === "string" && criterion.id.length > 0);
|
|
@@ -3503,7 +3518,7 @@ async function recordEvidence(p) {
|
|
|
3503
3518
|
die("record-evidence requires at least one --check-json or --surface-trust-json");
|
|
3504
3519
|
validateAcceptanceEvidenceRefs(dir, p);
|
|
3505
3520
|
// Phase 4c: bundle is the sole verification artifact — stop writing evidence.json and acceptance.json update.
|
|
3506
|
-
const ts = opt(p, "timestamp",
|
|
3521
|
+
const ts = opt(p, "timestamp", new Date().toISOString());
|
|
3507
3522
|
// #298: readBundleState + merge-by-id instead of unconditionally replacing every prior check —
|
|
3508
3523
|
// record-evidence previously never called checksFromBundle(dir), so it dropped every check
|
|
3509
3524
|
// recorded by an earlier record-evidence/record-check/record-gate-claim call. A later check
|
|
@@ -3710,7 +3725,7 @@ function diagnostic(dir, code, summary) {
|
|
|
3710
3725
|
async function recordGateClaim(p) {
|
|
3711
3726
|
const dir = artifactDirFrom(p.positional[0] || die("artifact directory is required"));
|
|
3712
3727
|
const slug = taskSlugFor(dir, opt(p, "task-slug"));
|
|
3713
|
-
const ts = opt(p, "timestamp",
|
|
3728
|
+
const ts = opt(p, "timestamp", new Date().toISOString());
|
|
3714
3729
|
const statusVal = opt(p, "status");
|
|
3715
3730
|
if (!["pass", "fail", "not_verified"].includes(statusVal))
|
|
3716
3731
|
die("--status must be one of: pass, fail, not_verified");
|
|
@@ -3795,6 +3810,8 @@ async function recordGateClaim(p) {
|
|
|
3795
3810
|
status: statusVal,
|
|
3796
3811
|
summary,
|
|
3797
3812
|
_gate_claim_expectation_id: targetExpectation.id,
|
|
3813
|
+
_gate_claim_identity_version: 2,
|
|
3814
|
+
_gate_claim_recorded_at: ts,
|
|
3798
3815
|
...(routeReason ? { _gate_claim_route_reason: routeReason } : {}),
|
|
3799
3816
|
};
|
|
3800
3817
|
// Include structured evidence refs if provided
|
|
@@ -3836,7 +3853,7 @@ async function recordGateClaim(p) {
|
|
|
3836
3853
|
// SAME expectation id supersedes the earlier check for that expectation (mergeChecksById); a
|
|
3837
3854
|
// gate claim against a different expectation is additive.
|
|
3838
3855
|
const _existingState = readBundleState(dir);
|
|
3839
|
-
const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames) : _existingState.criteria;
|
|
3856
|
+
const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames, ts) : _existingState.criteria;
|
|
3840
3857
|
if (mustRunTests) {
|
|
3841
3858
|
const liveCritiques = _existingState.critiques.filter((critique) => !critique.superseded_by);
|
|
3842
3859
|
if (liveCritiques.length === 0 || liveCritiques.some((critique) => !critiqueIsCleanAndCurrent(dir, critique))) {
|
|
@@ -4062,7 +4079,8 @@ async function recordCritique(p) {
|
|
|
4062
4079
|
const critique = {
|
|
4063
4080
|
id: critiqueId,
|
|
4064
4081
|
reviewer: opt(p, "reviewer", "tool-code-reviewer"),
|
|
4065
|
-
reviewed_at: opt(p, "timestamp",
|
|
4082
|
+
reviewed_at: opt(p, "timestamp", new Date().toISOString()),
|
|
4083
|
+
identity_version: 2,
|
|
4066
4084
|
verdict,
|
|
4067
4085
|
summary,
|
|
4068
4086
|
lanes,
|