@kontourai/flow-agents 3.7.0 → 3.8.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 +12 -0
- package/build/src/builder-flow-runtime.js +102 -7
- package/build/src/cli/assignment-provider.js +13 -1
- package/build/src/cli/workflow-sidecar.js +29 -11
- package/build/src/cli/workflow.js +20 -10
- package/build/src/tools/build-universal-bundles.js +53 -3
- package/docs/getting-started.md +9 -1
- package/docs/spec/builder-flow-runtime.md +36 -2
- 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 +1 -1
- 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 +99 -7
- package/src/cli/assignment-provider.ts +13 -1
- package/src/cli/builder-flow-runtime.test.mjs +250 -18
- package/src/cli/workflow-sidecar.ts +27 -11
- package/src/cli/workflow.ts +20 -10
- package/src/tools/build-universal-bundles.ts +51 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.8.0](https://github.com/kontourai/flow-agents/compare/v3.7.0...v3.8.0) (2026-07-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **packaging:** install portable skills under .agents ([#551](https://github.com/kontourai/flow-agents/issues/551)) ([12f12e3](https://github.com/kontourai/flow-agents/commit/12f12e3d0a32b398e47a9ac01604179c58bf14ba))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
* **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))
|
|
14
|
+
|
|
3
15
|
## [3.7.0](https://github.com/kontourai/flow-agents/compare/v3.6.0...v3.7.0) (2026-07-12)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -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)
|
|
@@ -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`);
|
|
@@ -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,
|
|
@@ -200,7 +200,7 @@ async function evidence(sessionDir, argv, json) {
|
|
|
200
200
|
// session state cannot change mid-invocation.
|
|
201
201
|
const caller = assertMatchingAssignmentActor(sessionDir, slug);
|
|
202
202
|
const repaired = await recoverBuilderFlowSession({ sessionDir });
|
|
203
|
-
const beforeEvidence =
|
|
203
|
+
const beforeEvidence = manifestEvidenceIdentity(repaired.run.manifest);
|
|
204
204
|
await mainFromPublicWorkflow([
|
|
205
205
|
"record-gate-claim",
|
|
206
206
|
sessionDir,
|
|
@@ -208,27 +208,34 @@ async function evidence(sessionDir, argv, json) {
|
|
|
208
208
|
"--actor",
|
|
209
209
|
caller.actorKey,
|
|
210
210
|
]);
|
|
211
|
-
await syncBuilderFlowSession({ sessionDir });
|
|
211
|
+
const synchronized = await syncBuilderFlowSession({ sessionDir });
|
|
212
212
|
const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
|
|
213
213
|
const run = await loadBuilderFlowRun({ cwd: repaired.projectRoot, runId: slug });
|
|
214
|
-
const afterEvidence =
|
|
215
|
-
const
|
|
216
|
-
|
|
214
|
+
const afterEvidence = manifestEvidenceIdentity(run.manifest);
|
|
215
|
+
const beforeIds = new Set(beforeEvidence.map((entry) => entry.id));
|
|
216
|
+
const newEvidence = afterEvidence.filter((entry) => !beforeIds.has(entry.id));
|
|
217
|
+
if (synchronized.attached && (newEvidence.length !== 1 || newEvidence[0]?.sha256 !== digest)) {
|
|
217
218
|
throw new Error("workflow evidence did not attach exactly this invocation's resulting trust.bundle digest");
|
|
218
219
|
}
|
|
220
|
+
if (!synchronized.attached && newEvidence.length !== 0) {
|
|
221
|
+
throw new Error("workflow evidence changed the canonical manifest while synchronization reported no attachment");
|
|
222
|
+
}
|
|
219
223
|
const updatedSidecar = readBoundSession(sessionDir).sidecar;
|
|
220
224
|
return immutableReport({
|
|
221
225
|
run_id: run.runId,
|
|
222
226
|
status: run.state.status,
|
|
223
227
|
current_step: run.state.current_step,
|
|
224
|
-
attached:
|
|
228
|
+
attached: synchronized.attached,
|
|
229
|
+
awaiting_evidence: !synchronized.attached,
|
|
225
230
|
next_action: updatedSidecar.next_action ?? null,
|
|
226
231
|
});
|
|
227
232
|
});
|
|
228
233
|
if (json)
|
|
229
234
|
console.log(JSON.stringify(report));
|
|
230
235
|
else
|
|
231
|
-
console.log(
|
|
236
|
+
console.log(report.attached
|
|
237
|
+
? `Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`
|
|
238
|
+
: `Recorded evidence; canonical run is awaiting the remaining gate expectations at ${report.current_step}.`);
|
|
232
239
|
return 0;
|
|
233
240
|
}
|
|
234
241
|
async function critique(sessionDir, argv, json) {
|
|
@@ -541,11 +548,14 @@ function immutableReport(value) {
|
|
|
541
548
|
immutableReport(nested);
|
|
542
549
|
return Object.freeze(value);
|
|
543
550
|
}
|
|
544
|
-
function
|
|
551
|
+
function manifestEvidenceIdentity(manifest) {
|
|
545
552
|
const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
|
|
546
553
|
return evidence
|
|
547
|
-
.
|
|
548
|
-
|
|
554
|
+
.flatMap((entry) => entry && typeof entry === "object"
|
|
555
|
+
&& typeof entry.id === "string"
|
|
556
|
+
&& typeof entry.sha256 === "string"
|
|
557
|
+
? [{ id: String(entry.id), sha256: String(entry.sha256) }]
|
|
558
|
+
: []);
|
|
549
559
|
}
|
|
550
560
|
function optionalFileDigest(file) {
|
|
551
561
|
try {
|
|
@@ -137,6 +137,55 @@ function copyTree(src, dest, target, rootReplacement) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
const skillResourcePattern = /(?:^|[`("'\s])(context\/contracts\/[A-Za-z0-9._/-]+\.md)(?=$|[`),"'\s])/gm;
|
|
141
|
+
/** Export a skill as a self-contained package. Shared instruction resources
|
|
142
|
+
* retain the relative paths used by SKILL.md so runtime resolution is local. */
|
|
143
|
+
function exportSkillPackage(src, dest, target) {
|
|
144
|
+
const sourceDir = path.dirname(src);
|
|
145
|
+
copyTree(sourceDir, dest, target, "<bundle-root>");
|
|
146
|
+
const skillText = readText(src);
|
|
147
|
+
const resources = new Set();
|
|
148
|
+
for (const match of skillText.matchAll(skillResourcePattern))
|
|
149
|
+
resources.add(match[1]);
|
|
150
|
+
for (const rel of [...resources].sort()) {
|
|
151
|
+
if (path.isAbsolute(rel) || rel.split("/").includes("..")) {
|
|
152
|
+
throw new Error(`skill '${path.basename(sourceDir)}': unsafe local resource '${rel}'`);
|
|
153
|
+
}
|
|
154
|
+
const source = path.resolve(root, rel);
|
|
155
|
+
const output = path.resolve(dest, rel);
|
|
156
|
+
if (!source.startsWith(`${path.resolve(root)}${path.sep}`) || !output.startsWith(`${path.resolve(dest)}${path.sep}`)) {
|
|
157
|
+
throw new Error(`skill '${path.basename(sourceDir)}': local resource escapes package '${rel}'`);
|
|
158
|
+
}
|
|
159
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
|
|
160
|
+
throw new Error(`skill '${path.basename(sourceDir)}': missing local resource '${rel}'`);
|
|
161
|
+
}
|
|
162
|
+
if (fs.existsSync(output)) {
|
|
163
|
+
const existing = fs.readFileSync(output);
|
|
164
|
+
const incoming = fs.readFileSync(source);
|
|
165
|
+
if (!existing.equals(incoming))
|
|
166
|
+
throw new Error(`skill '${path.basename(sourceDir)}': resource collision '${rel}'`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
writeText(output, sanitizeText(readText(source), target, "<bundle-root>"));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function validateExportedSkillPackages(skillsRoot) {
|
|
173
|
+
if (!fs.existsSync(skillsRoot))
|
|
174
|
+
return;
|
|
175
|
+
for (const skillName of fs.readdirSync(skillsRoot).sort()) {
|
|
176
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
177
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
178
|
+
if (!fs.existsSync(skillFile))
|
|
179
|
+
continue;
|
|
180
|
+
for (const match of readText(skillFile).matchAll(skillResourcePattern)) {
|
|
181
|
+
const rel = match[1];
|
|
182
|
+
const resolved = path.resolve(skillDir, rel);
|
|
183
|
+
if (!resolved.startsWith(`${path.resolve(skillDir)}${path.sep}`) || !fs.existsSync(resolved)) {
|
|
184
|
+
throw new Error(`skill '${skillName}': unresolved exported resource '${rel}'`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
140
189
|
function resolveSourcePath(pathText) {
|
|
141
190
|
let normalized = pathText;
|
|
142
191
|
for (const alias of manifest.source_root_aliases)
|
|
@@ -473,9 +522,10 @@ function buildCodex(agents) {
|
|
|
473
522
|
writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
|
|
474
523
|
for (const spec of targetAgents)
|
|
475
524
|
writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
525
|
+
const skillsRoot = path.join(bundle, ".agents/skills");
|
|
526
|
+
for (const { name, src } of collectAllSkills())
|
|
527
|
+
exportSkillPackage(src, path.join(skillsRoot, name), "codex");
|
|
528
|
+
validateExportedSkillPackages(skillsRoot);
|
|
479
529
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
|
|
480
530
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace", CODEX_LIVE_HOOKS_README));
|
|
481
531
|
writeText(path.join(bundle, "install.sh"), installScript("Codex", "/path/to/workspace", undefined, undefined, { configRelPath: ".codex/hooks.json", managedConfigRelPath: ".codex/hooks.json", runtime: "codex", version: pkgVersion }));
|
package/docs/getting-started.md
CHANGED
|
@@ -30,7 +30,15 @@ For a normal Codex global install, target the Codex home instead of a project wo
|
|
|
30
30
|
npx @kontourai/flow-agents init --runtime codex --global --activate-kits --yes
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
That
|
|
33
|
+
That splits installation by ownership: Codex-only runtime assets go into `CODEX_HOME` when set (otherwise `~/.codex`), while portable skills go into Codex's documented user catalog at `$HOME/.agents/skills`. A repository bundle install similarly exposes skills at `<repo>/.agents/skills`.
|
|
34
|
+
|
|
35
|
+
Pass a positional runtime destination and `--skills-dir PATH` when both roots must be isolated, or set `FLOW_AGENTS_SKILLS_DIR` for headless environments:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bash scripts/install-codex-home.sh /tmp/codex-home --skills-dir /tmp/agents/skills
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Installer output reports both destinations. Reinstall preserves unknown user skills and migrates only unchanged Flow Agents-owned files from the former `CODEX_HOME/skills` layout; modified legacy content is retained. Flow Agents intentionally does not create a compatibility symlink because the Codex runtime home must not own or mask a universal user catalog, and the installer refuses to write through symlinked destinations.
|
|
34
42
|
|
|
35
43
|
Keep generated Codex base config lean. Put profile-specific model, provider, and approval settings in separate `<profile>.config.toml` files and select them with `codex --profile <name>`.
|
|
36
44
|
|
|
@@ -63,8 +63,36 @@ does not create a Flow run.
|
|
|
63
63
|
|
|
64
64
|
After a gate producer writes `trust.bundle`, the public evidence path
|
|
65
65
|
synchronizes the existing run while holding the assignment subject lock.
|
|
66
|
-
Synchronization
|
|
67
|
-
|
|
66
|
+
Synchronization selects only live claims: producer-superseded claims and claims
|
|
67
|
+
carrying `metadata.superseded_by` remain auditable history but never determine
|
|
68
|
+
the current outcome. Passing evidence is published atomically only when every
|
|
69
|
+
required expectation for the gate is present; this prevents sequential critique
|
|
70
|
+
writes from attaching an intermediate partial snapshot and consuming a
|
|
71
|
+
route-back attempt. Failed evidence may still synchronize immediately when it
|
|
72
|
+
carries a route reason declared by the gate; a disputed report-only critique is
|
|
73
|
+
not itself a routed gate decision and remains pending.
|
|
74
|
+
|
|
75
|
+
Attachments carry the exact expectation ids selected from the current bundle.
|
|
76
|
+
Digest idempotence applies only while an unsuperseded attachment for that gate
|
|
77
|
+
and expectation set remains live. After route-back, claims must be current for
|
|
78
|
+
the new gate visit before synchronization, and claim/evidence identities used
|
|
79
|
+
by any earlier attachment to that gate can never satisfy the later visit.
|
|
80
|
+
Gate claims, verified criteria, and critiques carry producer-recorded version
|
|
81
|
+
timestamps plus `identity_version: 2` in their identity derivation so legitimate
|
|
82
|
+
re-verification creates new identities. Unmarked pre-upgrade records retain the
|
|
83
|
+
legacy identity formula during rebuild; installing new code alone can never
|
|
84
|
+
manufacture a fresh identity. Because those identities are embedded in TrustBundle bytes, a
|
|
85
|
+
genuinely new identity necessarily changes the bundle SHA-256; byte-identical
|
|
86
|
+
replay remains pending rather than consuming another attempt.
|
|
87
|
+
|
|
88
|
+
Every gate visit has a canonical boundary: the latest Flow transition into the
|
|
89
|
+
step, or the run's initial timestamp for its entry step. Claim creation and
|
|
90
|
+
observation timestamps must fall within that visit and may not be more than 30
|
|
91
|
+
seconds ahead of synchronization. The sole pre-boundary allowance is the
|
|
92
|
+
30-second acquisition window for the assignment-backed `selected-work` claim,
|
|
93
|
+
which is intentionally produced immediately before the canonical run starts.
|
|
94
|
+
Previously attached claim and evidence identities are excluded from every later
|
|
95
|
+
visit to the same gate regardless of timestamp skew.
|
|
68
96
|
|
|
69
97
|
## Public Status And Recovery
|
|
70
98
|
|
|
@@ -119,6 +147,12 @@ Workflow steering surfaces these fields on session start and prompt submission.
|
|
|
119
147
|
Stop hook treats an unfinished canonical Flow run as active even during pickup or
|
|
120
148
|
planning, blocks a premature stop in block mode, and does not release its liveness
|
|
121
149
|
claim. A run is complete only when Flow reaches its terminal step.
|
|
150
|
+
|
|
151
|
+
Projection is always derived from the canonical run's `current_step`, including
|
|
152
|
+
composed steps that have no legacy sidecar phase (`merge-ready-ci` and `learn`).
|
|
153
|
+
Both the legacy `current.json` pointer and every matching per-actor pointer are
|
|
154
|
+
updated from that canonical step; `phase_map` is presentation metadata, not the
|
|
155
|
+
authority for pointer navigation.
|
|
122
156
|
# Builder Lifecycle Authority
|
|
123
157
|
|
|
124
158
|
The canonical Flow run owns pause, resume, and cancellation. The current assignment actor may
|
|
@@ -456,6 +456,9 @@ record_public_expectation() {
|
|
|
456
456
|
*) _fail "public producer fixture has no durable artifact for $expectation"; return ;;
|
|
457
457
|
esac
|
|
458
458
|
local -a args=(evidence --session-dir "$PUBLIC_SESSION" --expectation "$expectation" --status "$status" --summary "public producer fixture records a reviewable durable artifact" --evidence-ref-json "{\"kind\":\"artifact\",\"file\":\"$artifact\",\"summary\":\"Fixture artifact for $expectation.\"}")
|
|
459
|
+
if [ "$expectation" = "tests-evidence" ] && [ "$status" = "fail" ]; then
|
|
460
|
+
args+=(--route-reason implementation_defect)
|
|
461
|
+
fi
|
|
459
462
|
if [ "$expectation" = "tests-evidence" ] && [ "$status" = "pass" ]; then
|
|
460
463
|
local test_command criterion_one criterion_two command_ref
|
|
461
464
|
test_command="bash checks/check-public-session.sh .kontourai/flow-agents/$slug/state.json"
|
|
@@ -614,16 +617,46 @@ ROUTE_CRITIQUE_OUTPUT="$(public_review critique --session-dir "$PUBLIC_SESSION"
|
|
|
614
617
|
--artifact-ref "$PUBLIC_SESSION/$(basename "$PUBLIC_SESSION")--deliver.md" \
|
|
615
618
|
--lane-json "{\"id\":\"code-review\",\"status\":\"pass\",\"summary\":\"Public fixture code review completed.\",\"evidence_refs\":[{\"kind\":\"artifact\",\"file\":\"$PUBLIC_SESSION/$(basename "$PUBLIC_SESSION")--deliver.md\",\"summary\":\"Reviewed public fixture delivery artifact.\"}]}" 2>&1)" \
|
|
616
619
|
|| _fail "public authenticated critique failed before failed tests-evidence: $ROUTE_CRITIQUE_OUTPUT"
|
|
620
|
+
# Seed current verified acceptance prerequisites through the private producer
|
|
621
|
+
# without synchronizing Flow. The subsequent public failed tests claim replaces
|
|
622
|
+
# the provisional passing tests check, preserves these criteria + the clean
|
|
623
|
+
# critique, and is the only attachment/evaluation for this gate visit.
|
|
624
|
+
ROUTE_TEST_COMMAND="bash checks/check-public-session.sh .kontourai/flow-agents/$(basename "$PUBLIC_SESSION")/state.json"
|
|
625
|
+
ROUTE_COMMAND_REF="$(node - "$ROUTE_TEST_COMMAND" <<'NODE'
|
|
626
|
+
const command = process.argv[2];
|
|
627
|
+
process.stdout.write(JSON.stringify({ kind: 'command', excerpt: command, summary: 'Current route-back prerequisite command.' }));
|
|
628
|
+
NODE
|
|
629
|
+
)"
|
|
630
|
+
ROUTE_CRITERION_ONE="$(node - "$ROUTE_TEST_COMMAND" <<'NODE'
|
|
631
|
+
const command = process.argv[2];
|
|
632
|
+
process.stdout.write(JSON.stringify({ id: 'AC-1', status: 'pass', evidence_refs: [{ kind: 'command', excerpt: command, summary: 'Current AC-1 prerequisite.' }] }));
|
|
633
|
+
NODE
|
|
634
|
+
)"
|
|
635
|
+
ROUTE_CRITERION_TWO="$(node - "$ROUTE_TEST_COMMAND" <<'NODE'
|
|
636
|
+
const command = process.argv[2];
|
|
637
|
+
process.stdout.write(JSON.stringify({ id: 'AC-2', status: 'pass', evidence_refs: [{ kind: 'command', excerpt: command, summary: 'Current AC-2 prerequisite.' }] }));
|
|
638
|
+
NODE
|
|
639
|
+
)"
|
|
640
|
+
ROUTE_OBSERVED="$(node - "$ROUTE_TEST_COMMAND" <<'NODE'
|
|
641
|
+
const command = process.argv[2];
|
|
642
|
+
process.stdout.write(JSON.stringify({ command, exit_code: 0, test_count: 1, output_sha256: '0'.repeat(64) }));
|
|
643
|
+
NODE
|
|
644
|
+
)"
|
|
645
|
+
CODEX_SESSION_ID=builder-public-producers flow_agents_node "workflow-sidecar" record-gate-claim "$PUBLIC_SESSION" \
|
|
646
|
+
--expectation tests-evidence --status pass --summary "Seed complete current acceptance prerequisites before routed failure." \
|
|
647
|
+
--command "$ROUTE_TEST_COMMAND" --observed-command-json "$ROUTE_OBSERVED" \
|
|
648
|
+
--evidence-ref-json "$ROUTE_COMMAND_REF" --criterion-json "$ROUTE_CRITERION_ONE" --criterion-json "$ROUTE_CRITERION_TWO" >/dev/null 2>&1 \
|
|
649
|
+
|| _fail "failed to seed current acceptance prerequisites for routed failure"
|
|
617
650
|
record_public_expectation "tests-evidence" "fail"
|
|
618
651
|
ROUTE_REPORT="$(public_flow status --session-dir "$PUBLIC_SESSION" --json 2>/dev/null)"
|
|
619
652
|
if node - "$ROUTE_REPORT" <<'NODE'
|
|
620
653
|
const report = JSON.parse(process.argv[2]);
|
|
621
|
-
if (report.current_step !== '
|
|
622
|
-
if ((report.next_action?.skills || []).join(',') !== '
|
|
623
|
-
if (!/attempt 1\/3 returned to `
|
|
654
|
+
if (report.current_step !== 'execute') process.exit(1);
|
|
655
|
+
if ((report.next_action?.skills || []).join(',') !== 'execute-plan') process.exit(2);
|
|
656
|
+
if (!/attempt 1\/3 returned to `execute` for `implementation_defect`/.test(report.next_action?.summary || '')) process.exit(3);
|
|
624
657
|
NODE
|
|
625
658
|
then
|
|
626
|
-
_pass "failed verify evidence routes back through Flow
|
|
659
|
+
_pass "complete failed verify evidence routes back through Flow once to execute for implementation_defect"
|
|
627
660
|
else
|
|
628
661
|
_fail "public failed-verify route-back was not projected correctly: $ROUTE_REPORT"
|
|
629
662
|
fi
|