@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
|
@@ -915,7 +915,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
915
915
|
if (!check.id) continue;
|
|
916
916
|
const subjectId = `${slug}/${check.id}`;
|
|
917
917
|
const fieldOrBehavior = String(check.summary ?? check.id);
|
|
918
|
-
const
|
|
918
|
+
const gateClaimIdentityVersion = check._gate_claim_identity_version === 2 ? 2 : 1;
|
|
919
|
+
const gateClaimRecordedAt = gateClaimIdentityVersion === 2 && typeof check._gate_claim_recorded_at === "string" ? check._gate_claim_recorded_at : null;
|
|
920
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", gateClaimRecordedAt ? `${fieldOrBehavior}::gate-visit::${gateClaimRecordedAt}` : fieldOrBehavior);
|
|
919
921
|
const evId = `ev:${claimId}`;
|
|
920
922
|
const legacyClaimType = `workflow.check.${check.kind ?? "external"}`;
|
|
921
923
|
|
|
@@ -1071,7 +1073,7 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1071
1073
|
// restored stamp) takes the currently-active step's id.
|
|
1072
1074
|
const declaredStepId = gateClaimDeclaredStepId ?? (activeStep ? activeStep.stepId : null);
|
|
1073
1075
|
const declaredMetadata: AnyObj = gateClaimExpectationId
|
|
1074
|
-
? { ...claimMetadata, gate_claim: { expectation_id: gateClaimExpectationId, claim_type: declared.claimType, subject_type: declared.subjectType, step_id: declaredStepId, ...(gateClaimRouteReason ? { route_reason: gateClaimRouteReason } : {}) } }
|
|
1076
|
+
? { ...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 } : {}) } }
|
|
1075
1077
|
: claimMetadata;
|
|
1076
1078
|
const declaredClaimObj: AnyObj = { 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 } : {}) };
|
|
1077
1079
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj as Record<string, unknown>, evidence: [evItem] as Record<string, unknown>[], events: claimEvents as Record<string, unknown>[], policies: [declaredPolicy] as Record<string, unknown>[] });
|
|
@@ -1089,7 +1091,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1089
1091
|
if (!criterion.id) continue;
|
|
1090
1092
|
const subjectId = `${slug}/${criterion.id}`;
|
|
1091
1093
|
const fieldOrBehavior = String(criterion.description ?? criterion.id);
|
|
1092
|
-
const
|
|
1094
|
+
const criterionIdentityVersion = criterion.identity_version === 2 ? 2 : 1;
|
|
1095
|
+
const criterionVerifiedAt = criterionIdentityVersion === 2 && typeof criterion.verified_at === "string" ? criterion.verified_at : null;
|
|
1096
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", criterionVerifiedAt ? `${fieldOrBehavior}::verified::${criterionVerifiedAt}` : fieldOrBehavior);
|
|
1093
1097
|
const legacyClaimType = "workflow.acceptance.criterion";
|
|
1094
1098
|
const policy = ensurePolicy(legacyClaimType, "high", []);
|
|
1095
1099
|
const evStatus = criterionStatusToEventStatus(String(criterion.status ?? ""));
|
|
@@ -1105,7 +1109,7 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1105
1109
|
if (declared) {
|
|
1106
1110
|
// Declared kit-typed claim only — no legacy shadow (ADR 0016 P-d).
|
|
1107
1111
|
const declaredPolicy = ensurePolicy(declared.claimType, "high", []);
|
|
1108
|
-
const declaredClaimObj: AnyObj = { 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 } : {}) } };
|
|
1112
|
+
const declaredClaimObj: AnyObj = { 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 } : {}) } };
|
|
1109
1113
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj as Record<string, unknown>, evidence: [], events: claimEvents as Record<string, unknown>[], policies: [declaredPolicy] as Record<string, unknown>[] });
|
|
1110
1114
|
claims.push({ ...declaredClaimObj, status: declaredStatus });
|
|
1111
1115
|
} else {
|
|
@@ -1128,10 +1132,12 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1128
1132
|
const supersededBy = typeof c.superseded_by === "string" && c.superseded_by.length > 0 ? c.superseded_by : null;
|
|
1129
1133
|
const critiqueReviewer = String(c.reviewer ?? "tool-code-reviewer");
|
|
1130
1134
|
const critiqueReviewedAt = String(c.reviewed_at ?? ts);
|
|
1135
|
+
const critiqueIdentityVersion = c.identity_version === 2 ? 2 : 1;
|
|
1131
1136
|
const critMeta: AnyObj = {
|
|
1132
1137
|
origin: "critique",
|
|
1133
1138
|
reviewer: critiqueReviewer,
|
|
1134
1139
|
reviewed_at: critiqueReviewedAt,
|
|
1140
|
+
...(critiqueIdentityVersion === 2 ? { identity_version: 2 } : {}),
|
|
1135
1141
|
findings: Array.isArray(c.findings) ? c.findings : [],
|
|
1136
1142
|
lanes: Array.isArray(c.lanes) ? c.lanes : [],
|
|
1137
1143
|
review_target: c.review_target && typeof c.review_target === "object" ? c.review_target : { artifacts: [] },
|
|
@@ -1143,7 +1149,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1143
1149
|
// A superseded historical write gets a distinct, stable claimId so it co-exists with the live
|
|
1144
1150
|
// claim of the same critique id (never overwrites or duplicates it). The salt is reproducible
|
|
1145
1151
|
// across rebuilds because superseded_by + reviewed_at are preserved in metadata.
|
|
1146
|
-
const claimIdSalt =
|
|
1152
|
+
const claimIdSalt = critiqueIdentityVersion === 2
|
|
1153
|
+
? `${fieldOrBehavior}::reviewed::${critiqueReviewedAt}${supersededBy ? `::superseded::${supersededBy}` : ""}`
|
|
1154
|
+
: (supersededBy ? `${fieldOrBehavior}::superseded::${supersededBy}::${critiqueReviewedAt}` : fieldOrBehavior);
|
|
1147
1155
|
const claimId = generateClaimId(subjectId, "flow-agents.workflow", claimIdSalt);
|
|
1148
1156
|
const legacyClaimType = "workflow.critique.review";
|
|
1149
1157
|
const policy = ensurePolicy(legacyClaimType, "medium", []);
|
|
@@ -2727,7 +2735,7 @@ function requireObservedCommandRefs(refs: AnyObj[], observedCommands: ReadonlySe
|
|
|
2727
2735
|
}
|
|
2728
2736
|
}
|
|
2729
2737
|
|
|
2730
|
-
function completePassingCriteria(existing: AnyObj[], raw: string[], observedCommands: ReadonlySet<string
|
|
2738
|
+
function completePassingCriteria(existing: AnyObj[], raw: string[], observedCommands: ReadonlySet<string>, verifiedAt: string): AnyObj[] {
|
|
2731
2739
|
if (raw.length === 0) die("record-gate-claim requires --criterion-json for a passing tests-evidence claim");
|
|
2732
2740
|
const incoming = raw.map((value) => parseJson(value, "--criterion-json"));
|
|
2733
2741
|
const expectedById = new Map<string, AnyObj>();
|
|
@@ -2746,7 +2754,7 @@ function completePassingCriteria(existing: AnyObj[], raw: string[], observedComm
|
|
|
2746
2754
|
const refs = normalizeEvidenceRefs(criterion.evidence_refs, `criterion ${ids[index]} evidence_refs`);
|
|
2747
2755
|
if (refs.length === 0) die(`criterion ${ids[index]} requires reviewable evidence_refs`);
|
|
2748
2756
|
requireObservedCommandRefs(refs, observedCommands, `criterion ${ids[index]}`);
|
|
2749
|
-
return { ...expectedById.get(ids[index])!, status: "pass", evidence_refs: refs };
|
|
2757
|
+
return { ...expectedById.get(ids[index])!, status: "pass", evidence_refs: refs, identity_version: 2, verified_at: verifiedAt };
|
|
2750
2758
|
});
|
|
2751
2759
|
}
|
|
2752
2760
|
|
|
@@ -3133,6 +3141,8 @@ function checksFromBundle(dir: string): AnyObj[] {
|
|
|
3133
3141
|
if (typeof gc.claim_type === "string") check._gate_claim_declared_type = gc.claim_type;
|
|
3134
3142
|
if (typeof gc.subject_type === "string") check._gate_claim_declared_subject = gc.subject_type;
|
|
3135
3143
|
if (typeof gc.step_id === "string") check._gate_claim_declared_step_id = gc.step_id;
|
|
3144
|
+
if (typeof gc.recorded_at === "string") check._gate_claim_recorded_at = gc.recorded_at;
|
|
3145
|
+
if (gc.identity_version === 2) check._gate_claim_identity_version = 2;
|
|
3136
3146
|
if (typeof gc.route_reason === "string") check._gate_claim_route_reason = gc.route_reason;
|
|
3137
3147
|
};
|
|
3138
3148
|
// #270 CRITICAL/HIGH fix: a claim that is gate-claim-SHAPED but carries NO metadata.gate_claim
|
|
@@ -3294,6 +3304,7 @@ function critiquesFromBundle(dir: string): AnyObj[] {
|
|
|
3294
3304
|
review_target: md.review_target && typeof md.review_target === "object" && !Array.isArray(md.review_target) ? md.review_target : { artifacts: [] },
|
|
3295
3305
|
reviewer: typeof md.reviewer === "string" ? md.reviewer : "tool-code-reviewer",
|
|
3296
3306
|
reviewed_at: typeof md.reviewed_at === "string" ? md.reviewed_at : (c.updatedAt || c.createdAt || now()),
|
|
3307
|
+
...(md.identity_version === 2 ? { identity_version: 2 } : {}),
|
|
3297
3308
|
artifact_refs: Array.isArray(md.artifact_refs) ? md.artifact_refs : [],
|
|
3298
3309
|
...(typeof md.superseded_by === "string" && md.superseded_by.length > 0 ? { superseded_by: md.superseded_by } : {}),
|
|
3299
3310
|
};
|
|
@@ -3314,6 +3325,8 @@ function criteriaFromBundle(dir: string): AnyObj[] {
|
|
|
3314
3325
|
description: typeof saved.description === "string" ? saved.description : (claim.fieldOrBehavior || ""),
|
|
3315
3326
|
status: typeof saved.status === "string" ? saved.status : (claim.value ?? "not_verified"),
|
|
3316
3327
|
evidence_refs: Array.isArray(saved.evidence_refs) ? saved.evidence_refs : [],
|
|
3328
|
+
...(typeof saved.verified_at === "string" ? { verified_at: saved.verified_at } : {}),
|
|
3329
|
+
...(saved.identity_version === 2 ? { identity_version: 2 } : {}),
|
|
3317
3330
|
};
|
|
3318
3331
|
})
|
|
3319
3332
|
.filter((criterion: AnyObj) => typeof criterion.id === "string" && criterion.id.length > 0);
|
|
@@ -3369,7 +3382,7 @@ async function recordEvidence(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3369
3382
|
if (!checks.length && opts(p, "surface-trust-json").length === 0) die("record-evidence requires at least one --check-json or --surface-trust-json");
|
|
3370
3383
|
validateAcceptanceEvidenceRefs(dir, p);
|
|
3371
3384
|
// Phase 4c: bundle is the sole verification artifact — stop writing evidence.json and acceptance.json update.
|
|
3372
|
-
const ts = opt(p, "timestamp",
|
|
3385
|
+
const ts = opt(p, "timestamp", new Date().toISOString());
|
|
3373
3386
|
// #298: readBundleState + merge-by-id instead of unconditionally replacing every prior check —
|
|
3374
3387
|
// record-evidence previously never called checksFromBundle(dir), so it dropped every check
|
|
3375
3388
|
// recorded by an earlier record-evidence/record-check/record-gate-claim call. A later check
|
|
@@ -3576,7 +3589,7 @@ function diagnostic(dir: string, code: string, summary: string): never {
|
|
|
3576
3589
|
async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
3577
3590
|
const dir = artifactDirFrom(p.positional[0] || die("artifact directory is required"));
|
|
3578
3591
|
const slug = taskSlugFor(dir, opt(p, "task-slug"));
|
|
3579
|
-
const ts = opt(p, "timestamp",
|
|
3592
|
+
const ts = opt(p, "timestamp", new Date().toISOString());
|
|
3580
3593
|
const statusVal = opt(p, "status");
|
|
3581
3594
|
if (!["pass", "fail", "not_verified"].includes(statusVal)) die("--status must be one of: pass, fail, not_verified");
|
|
3582
3595
|
const summary = opt(p, "summary") || die("--summary is required");
|
|
@@ -3652,6 +3665,8 @@ async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3652
3665
|
status: statusVal,
|
|
3653
3666
|
summary,
|
|
3654
3667
|
_gate_claim_expectation_id: targetExpectation.id,
|
|
3668
|
+
_gate_claim_identity_version: 2,
|
|
3669
|
+
_gate_claim_recorded_at: ts,
|
|
3655
3670
|
...(routeReason ? { _gate_claim_route_reason: routeReason } : {}),
|
|
3656
3671
|
};
|
|
3657
3672
|
|
|
@@ -3690,7 +3705,7 @@ async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3690
3705
|
// SAME expectation id supersedes the earlier check for that expectation (mergeChecksById); a
|
|
3691
3706
|
// gate claim against a different expectation is additive.
|
|
3692
3707
|
const _existingState = readBundleState(dir);
|
|
3693
|
-
const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames) : _existingState.criteria;
|
|
3708
|
+
const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames, ts) : _existingState.criteria;
|
|
3694
3709
|
if (mustRunTests) {
|
|
3695
3710
|
const liveCritiques = _existingState.critiques.filter((critique) => !critique.superseded_by);
|
|
3696
3711
|
if (liveCritiques.length === 0 || liveCritiques.some((critique) => !critiqueIsCleanAndCurrent(dir, critique))) {
|
|
@@ -3907,7 +3922,8 @@ async function recordCritique(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3907
3922
|
const critique = {
|
|
3908
3923
|
id: critiqueId,
|
|
3909
3924
|
reviewer: opt(p, "reviewer", "tool-code-reviewer"),
|
|
3910
|
-
reviewed_at: opt(p, "timestamp",
|
|
3925
|
+
reviewed_at: opt(p, "timestamp", new Date().toISOString()),
|
|
3926
|
+
identity_version: 2,
|
|
3911
3927
|
verdict,
|
|
3912
3928
|
summary,
|
|
3913
3929
|
lanes,
|
package/src/cli/workflow.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { validateDefinition } from "@kontourai/flow";
|
|
8
8
|
import { loadBuilderFlowRun } from "../builder-flow-run-adapter.js";
|
|
9
|
+
import { driveBuilderFlowSession, withContinuationDriverLock } from "../continuation-driver.js";
|
|
9
10
|
import { inspectBuilderFlowSession, recoverBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
|
|
10
11
|
import { flowAgentsPackageRoot, flowAgentsPackageVersion } from "../lib/package-version.js";
|
|
11
12
|
import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
|
|
@@ -14,6 +15,7 @@ import { flagBool, flagList, flagString, parseArgs } from "../lib/args.js";
|
|
|
14
15
|
import { main as builderRun } from "./builder-run.js";
|
|
15
16
|
import { currentWorkflowSessionDir, isMeaningfulTestCommand, mainFromPublicWorkflow, WORKFLOW_WRITER_CONTRACT_VERSION } from "./workflow-sidecar.js";
|
|
16
17
|
import { resolveCurrentAssignmentActor, withSubjectLock } from "./assignment-provider.js";
|
|
18
|
+
import { executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "./continuation-adapter.js";
|
|
17
19
|
|
|
18
20
|
type JsonRecord = Record<string, unknown>;
|
|
19
21
|
|
|
@@ -22,7 +24,7 @@ const PACKAGE_ROOT = flowAgentsPackageRoot();
|
|
|
22
24
|
const REQUIRE = createRequire(import.meta.url);
|
|
23
25
|
const PACKAGE_METADATA = readJsonFile(path.join(PACKAGE_ROOT, "package.json"), "Flow Agents package metadata");
|
|
24
26
|
const CLI_VERSION = flowAgentsPackageVersion();
|
|
25
|
-
const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "pause", "resume", "release", "cancel", "archive", "doctor"] as const;
|
|
27
|
+
const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "drive", "pause", "resume", "release", "cancel", "archive", "doctor"] as const;
|
|
26
28
|
|
|
27
29
|
function usage(): void {
|
|
28
30
|
console.log(`Usage: flow-agents workflow <verb> [options]
|
|
@@ -32,6 +34,7 @@ Public workflow verbs:
|
|
|
32
34
|
status Show the current canonical run and projected next action.
|
|
33
35
|
evidence Record evidence for the current Flow gate and synchronize it.
|
|
34
36
|
critique Record review critique directly into the current trust bundle.
|
|
37
|
+
drive Continue the canonical run through an explicit runtime adapter.
|
|
35
38
|
pause Pause the current run as its assignment actor.
|
|
36
39
|
resume Resume the current paused run as its assignment actor.
|
|
37
40
|
release Release the current assignment without canceling the run.
|
|
@@ -61,12 +64,41 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
61
64
|
if (verb === "status") return status(sessionDir, flagBool(parsed.flags, "json"));
|
|
62
65
|
if (verb === "evidence") return evidence(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
|
|
63
66
|
if (verb === "critique") return critique(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
|
|
67
|
+
if (verb === "drive") return drive(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
|
|
64
68
|
|
|
65
69
|
const forwarded = stripPublicFlags(argv.slice(1), new Set(["artifact-root", "session-dir", "json"]));
|
|
66
70
|
if (verb === "release" && !flagString(parsed.flags, "reason")) throw new Error("workflow release requires --reason <text>");
|
|
67
71
|
return builderRun([verb === "release" ? "release-assignment" : verb, "--session-dir", sessionDir, ...forwarded]);
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
async function drive(sessionDir: string, argv: string[], json: boolean): Promise<number> {
|
|
75
|
+
const parsed = parseArgs(argv);
|
|
76
|
+
assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "adapter-command-file", "max-turns", "turn-timeout-ms", "barrier-wait-ms", "barrier-poll-ms"]), "workflow drive");
|
|
77
|
+
const adapterCommandFile = flagString(parsed.flags, "adapter-command-file");
|
|
78
|
+
if (!adapterCommandFile) throw new Error("workflow drive requires --adapter-command-file <path>");
|
|
79
|
+
const maxTurns = integerFlag(parsed.flags, "max-turns", 4, 1, 100);
|
|
80
|
+
const turnTimeoutMs = integerFlag(parsed.flags, "turn-timeout-ms", 900_000, 1, 86_400_000);
|
|
81
|
+
const barrierWaitMs = integerFlag(parsed.flags, "barrier-wait-ms", 300_000, 0, 86_400_000);
|
|
82
|
+
const barrierPollMs = integerFlag(parsed.flags, "barrier-poll-ms", 1_000, 1, 60_000);
|
|
83
|
+
const { slug, projectRoot } = readBoundSession(sessionDir);
|
|
84
|
+
assertMatchingAssignmentActor(sessionDir, slug);
|
|
85
|
+
const adapterCommand = loadContinuationAdapterCommand(adapterCommandFile);
|
|
86
|
+
const result = await withContinuationDriverLock(sessionDir, async () => {
|
|
87
|
+
assertMatchingAssignmentActor(sessionDir, slug);
|
|
88
|
+
return driveBuilderFlowSession({
|
|
89
|
+
sessionDir,
|
|
90
|
+
maxTurns,
|
|
91
|
+
adapterCommandIdentity: adapterCommand.identity,
|
|
92
|
+
authorizeTurn: async () => { assertMatchingAssignmentActor(sessionDir, slug); },
|
|
93
|
+
execute: async (request) => executeLoadedContinuationAdapter(adapterCommand, request, { cwd: projectRoot, timeoutMs: turnTimeoutMs }),
|
|
94
|
+
waitForBarrier: async (barrier) => waitForContinuationBarrier(barrier, { maxWaitMs: barrierWaitMs, pollMs: barrierPollMs }),
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
if (json) console.log(JSON.stringify(result));
|
|
98
|
+
else console.log(`Continuation driver ${result.outcome} after ${result.turns_started} turn(s); canonical Flow is ${result.snapshot.status} at ${result.snapshot.current_step}.`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
async function start(argv: string[]): Promise<number> {
|
|
71
103
|
const parsed = parseArgs(argv);
|
|
72
104
|
assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion", "assignment-provider", "effective-state-json"]), "workflow start");
|
|
@@ -193,7 +225,7 @@ async function evidence(sessionDir: string, argv: string[], json: boolean): Prom
|
|
|
193
225
|
// session state cannot change mid-invocation.
|
|
194
226
|
const caller = assertMatchingAssignmentActor(sessionDir, slug);
|
|
195
227
|
const repaired = await recoverBuilderFlowSession({ sessionDir });
|
|
196
|
-
const beforeEvidence =
|
|
228
|
+
const beforeEvidence = manifestEvidenceIdentity(repaired.run.manifest);
|
|
197
229
|
await mainFromPublicWorkflow([
|
|
198
230
|
"record-gate-claim",
|
|
199
231
|
sessionDir,
|
|
@@ -202,26 +234,33 @@ async function evidence(sessionDir: string, argv: string[], json: boolean): Prom
|
|
|
202
234
|
caller.actorKey,
|
|
203
235
|
]);
|
|
204
236
|
|
|
205
|
-
await syncBuilderFlowSession({ sessionDir });
|
|
237
|
+
const synchronized = await syncBuilderFlowSession({ sessionDir });
|
|
206
238
|
|
|
207
239
|
const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
|
|
208
240
|
const run = await loadBuilderFlowRun({ cwd: repaired.projectRoot, runId: slug });
|
|
209
|
-
const afterEvidence =
|
|
210
|
-
const
|
|
211
|
-
|
|
241
|
+
const afterEvidence = manifestEvidenceIdentity(run.manifest);
|
|
242
|
+
const beforeIds = new Set(beforeEvidence.map((entry) => entry.id));
|
|
243
|
+
const newEvidence = afterEvidence.filter((entry) => !beforeIds.has(entry.id));
|
|
244
|
+
if (synchronized.attached && (newEvidence.length !== 1 || newEvidence[0]?.sha256 !== digest)) {
|
|
212
245
|
throw new Error("workflow evidence did not attach exactly this invocation's resulting trust.bundle digest");
|
|
213
246
|
}
|
|
247
|
+
if (!synchronized.attached && newEvidence.length !== 0) {
|
|
248
|
+
throw new Error("workflow evidence changed the canonical manifest while synchronization reported no attachment");
|
|
249
|
+
}
|
|
214
250
|
const updatedSidecar = readBoundSession(sessionDir).sidecar;
|
|
215
251
|
return immutableReport({
|
|
216
252
|
run_id: run.runId,
|
|
217
253
|
status: run.state.status,
|
|
218
254
|
current_step: run.state.current_step,
|
|
219
|
-
attached:
|
|
255
|
+
attached: synchronized.attached,
|
|
256
|
+
awaiting_evidence: !synchronized.attached,
|
|
220
257
|
next_action: updatedSidecar.next_action ?? null,
|
|
221
258
|
});
|
|
222
259
|
});
|
|
223
260
|
if (json) console.log(JSON.stringify(report));
|
|
224
|
-
else console.log(
|
|
261
|
+
else console.log(report.attached
|
|
262
|
+
? `Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`
|
|
263
|
+
: `Recorded evidence; canonical run is awaiting the remaining gate expectations at ${report.current_step}.`);
|
|
225
264
|
return 0;
|
|
226
265
|
}
|
|
227
266
|
|
|
@@ -511,11 +550,14 @@ function immutableReport<T>(value: T): T {
|
|
|
511
550
|
return Object.freeze(value);
|
|
512
551
|
}
|
|
513
552
|
|
|
514
|
-
function
|
|
553
|
+
function manifestEvidenceIdentity(manifest: JsonRecord): Array<{ id: string; sha256: string }> {
|
|
515
554
|
const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
|
|
516
555
|
return evidence
|
|
517
|
-
.
|
|
518
|
-
|
|
556
|
+
.flatMap((entry) => entry && typeof entry === "object"
|
|
557
|
+
&& typeof (entry as JsonRecord).id === "string"
|
|
558
|
+
&& typeof (entry as JsonRecord).sha256 === "string"
|
|
559
|
+
? [{ id: String((entry as JsonRecord).id), sha256: String((entry as JsonRecord).sha256) }]
|
|
560
|
+
: []);
|
|
519
561
|
}
|
|
520
562
|
|
|
521
563
|
function optionalFileDigest(file: string): string | null {
|
|
@@ -560,6 +602,21 @@ function assertOnlyFlags(flags: ReturnType<typeof parseArgs>["flags"], allowed:
|
|
|
560
602
|
if (unsupported) throw new Error(`${command} does not support --${unsupported}`);
|
|
561
603
|
}
|
|
562
604
|
|
|
605
|
+
function integerFlag(
|
|
606
|
+
flags: ReturnType<typeof parseArgs>["flags"],
|
|
607
|
+
name: string,
|
|
608
|
+
fallback: number,
|
|
609
|
+
min: number,
|
|
610
|
+
max: number,
|
|
611
|
+
): number {
|
|
612
|
+
const raw = flagString(flags, name);
|
|
613
|
+
if (raw === undefined) return fallback;
|
|
614
|
+
if (!/^(0|[1-9][0-9]*)$/.test(raw)) throw new Error(`workflow drive --${name} must be an integer from ${min} through ${max}`);
|
|
615
|
+
const value = Number(raw);
|
|
616
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`workflow drive --${name} must be an integer from ${min} through ${max}`);
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
|
|
563
620
|
function isWithin(candidate: string, root: string): boolean {
|
|
564
621
|
const relative = path.relative(root, candidate);
|
|
565
622
|
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|