@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
|
@@ -18,10 +18,12 @@ import {
|
|
|
18
18
|
syncBuilderFlowSession,
|
|
19
19
|
} from "../../build/src/builder-flow-runtime.js";
|
|
20
20
|
import { builderLifecycleAuthorizationPayload, loadBuilderLifecycleAuthorization, recordAuthorizationConsumed } from "../../build/src/builder-lifecycle-authority.js";
|
|
21
|
+
import { driveBuilderFlowSession } from "../../build/src/continuation-driver.js";
|
|
21
22
|
import { cancelBuilderBuildRun } from "../../build/src/builder-flow-run-adapter.js";
|
|
22
|
-
import { performLocalClaim, readLocalAssignmentStatus, resolveCurrentAssignmentActor } from "../../build/src/cli/assignment-provider.js";
|
|
23
|
+
import { performLocalClaim, performLocalRelease, readLocalAssignmentStatus, resolveCurrentAssignmentActor } from "../../build/src/cli/assignment-provider.js";
|
|
23
24
|
import { main as builderRunMain } from "../../build/src/cli/builder-run.js";
|
|
24
|
-
import {
|
|
25
|
+
import { main as workflowMain } from "../../build/src/cli/workflow.js";
|
|
26
|
+
import { buildTrustBundle, inferExecutedTestCount, main as workflowSidecarMain } from "../../build/src/cli/workflow-sidecar.js";
|
|
25
27
|
|
|
26
28
|
const SUBJECT = "local:work-item/runtime-projection";
|
|
27
29
|
const NOW = "2026-07-09T20:00:00.000Z";
|
|
@@ -88,7 +90,7 @@ function claimSessionAssignment(session) {
|
|
|
88
90
|
ttlSeconds: 1800,
|
|
89
91
|
actorKey: ACTOR_KEY,
|
|
90
92
|
branch: `agent/${session.slug}`,
|
|
91
|
-
artifactDir: session.
|
|
93
|
+
artifactDir: session.slug,
|
|
92
94
|
workItemRef: SUBJECT,
|
|
93
95
|
reason: "test",
|
|
94
96
|
});
|
|
@@ -100,7 +102,7 @@ function claimAmbientSessionAssignment(session) {
|
|
|
100
102
|
ttlSeconds: 1800,
|
|
101
103
|
actorKey: ambient.actorKey,
|
|
102
104
|
branch: `agent/${session.slug}`,
|
|
103
|
-
artifactDir: session.
|
|
105
|
+
artifactDir: session.slug,
|
|
104
106
|
workItemRef: SUBJECT,
|
|
105
107
|
reason: "test",
|
|
106
108
|
});
|
|
@@ -214,7 +216,7 @@ async function assertRecoveryRejectsWithoutWrites(session, pattern) {
|
|
|
214
216
|
assert.deepEqual(snapshotProjectionTargets(session), beforeProjection);
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
function bundleClaim({ expectation, claimType, subjectType, status = "pass", routeReason, subject = SUBJECT, testCount = 1, timestamp =
|
|
219
|
+
function bundleClaim({ expectation, claimType, subjectType, status = "pass", routeReason, subject = SUBJECT, testCount = 1, timestamp = new Date().toISOString() }) {
|
|
218
220
|
const claimId = `claim.${expectation}`;
|
|
219
221
|
return {
|
|
220
222
|
claim: {
|
|
@@ -264,7 +266,7 @@ function bundleClaim({ expectation, claimType, subjectType, status = "pass", rou
|
|
|
264
266
|
};
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
function verifiedTestsPrerequisites(session, timestamp =
|
|
269
|
+
function verifiedTestsPrerequisites(session, timestamp = new Date().toISOString()) {
|
|
268
270
|
const reviewArtifact = path.join(session.projectRoot, "review-target", "delivery.md");
|
|
269
271
|
const implementation = path.join(session.projectRoot, "review-target", "implementation.txt");
|
|
270
272
|
const implementationFile = path.relative(session.projectRoot, implementation);
|
|
@@ -312,6 +314,24 @@ function writeBundle(sessionDir, entries) {
|
|
|
312
314
|
});
|
|
313
315
|
}
|
|
314
316
|
|
|
317
|
+
function historicalProducerSuperseded(entry, suffix = "historical") {
|
|
318
|
+
const copy = withIdentitySuffix(entry, suffix);
|
|
319
|
+
copy.claim.producerStatus = "superseded";
|
|
320
|
+
return copy;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function withIdentitySuffix(entry, suffix) {
|
|
324
|
+
const copy = structuredClone(entry);
|
|
325
|
+
const priorId = copy.claim.id;
|
|
326
|
+
copy.claim.id = `${priorId}.${suffix}`;
|
|
327
|
+
copy.evidence.id = `${copy.evidence.id}.${suffix}`;
|
|
328
|
+
copy.evidence.claimId = copy.claim.id;
|
|
329
|
+
copy.event.id = `${copy.event.id}.${suffix}`;
|
|
330
|
+
copy.event.claimId = copy.claim.id;
|
|
331
|
+
copy.event.evidenceIds = [copy.evidence.id];
|
|
332
|
+
return copy;
|
|
333
|
+
}
|
|
334
|
+
|
|
315
335
|
async function writeAndSync(session, entries) {
|
|
316
336
|
writeBundle(session.sessionDir, entries);
|
|
317
337
|
return syncBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
@@ -347,6 +367,130 @@ test("small-model client can start and advance from projected actions without ch
|
|
|
347
367
|
assert.equal(duplicate.run.manifest.evidence.length, advanced.run.manifest.evidence.length);
|
|
348
368
|
});
|
|
349
369
|
|
|
370
|
+
test("Builder projection preserves Flow continuation semantics for exceptional statuses", async () => {
|
|
371
|
+
const session = makeSession("continuation-status-projection");
|
|
372
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
373
|
+
const stateFile = path.join(runDir(session.slug, session.projectRoot), "state.json");
|
|
374
|
+
const original = readJson(stateFile);
|
|
375
|
+
for (const [status, expected] of [
|
|
376
|
+
["blocked", "continue"],
|
|
377
|
+
["accepted_by_exception", "continue"],
|
|
378
|
+
["needs_decision", "blocked"],
|
|
379
|
+
["failed", "failed"],
|
|
380
|
+
]) {
|
|
381
|
+
writeJson(stateFile, { ...original, status, next_action: `fixture ${status}` });
|
|
382
|
+
const recovered = await recoverBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
383
|
+
assert.equal(recovered.projection.next_action.status, expected, status);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("continuation driver advances two real FlowDefinition steps unattended", async () => {
|
|
388
|
+
const session = makeSession("continuation-driver-two-steps");
|
|
389
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
390
|
+
const visited = [];
|
|
391
|
+
|
|
392
|
+
const result = await driveBuilderFlowSession({
|
|
393
|
+
sessionDir: session.sessionDir,
|
|
394
|
+
maxTurns: 2,
|
|
395
|
+
execute: async (request) => {
|
|
396
|
+
visited.push(request.current_step);
|
|
397
|
+
if (request.current_step === "pull-work") {
|
|
398
|
+
writeBundle(session.sessionDir, [bundleClaim({
|
|
399
|
+
expectation: "selected-work",
|
|
400
|
+
claimType: "builder.pull-work.selected",
|
|
401
|
+
subjectType: "work-item",
|
|
402
|
+
})]);
|
|
403
|
+
} else if (request.current_step === "design-probe") {
|
|
404
|
+
writeBundle(session.sessionDir, [
|
|
405
|
+
bundleClaim({
|
|
406
|
+
expectation: "pickup-probe-readiness",
|
|
407
|
+
claimType: "builder.design-probe.pickup-readiness",
|
|
408
|
+
subjectType: "work-item",
|
|
409
|
+
}),
|
|
410
|
+
bundleClaim({
|
|
411
|
+
expectation: "probe-decisions-or-accepted-gaps",
|
|
412
|
+
claimType: "builder.design-probe.decisions",
|
|
413
|
+
subjectType: "decision",
|
|
414
|
+
}),
|
|
415
|
+
]);
|
|
416
|
+
} else {
|
|
417
|
+
assert.fail(`unexpected continuation step ${request.current_step}`);
|
|
418
|
+
}
|
|
419
|
+
return { status: "completed", summary: `completed ${request.current_step}` };
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
assert.equal(result.outcome, "budget_exhausted");
|
|
424
|
+
assert.equal(result.turns_started, 2);
|
|
425
|
+
assert.deepEqual(visited, ["pull-work", "design-probe"]);
|
|
426
|
+
assert.equal(result.snapshot.current_step, "plan");
|
|
427
|
+
assert.equal(result.snapshot.next_action.skills[0], "plan-work");
|
|
428
|
+
const driverState = readJson(path.join(session.sessionDir, "continuation-driver", "state.json"));
|
|
429
|
+
assert.equal(driverState.status, "budget_exhausted");
|
|
430
|
+
assert.equal(driverState.turns_started, 2);
|
|
431
|
+
const events = fs.readFileSync(path.join(session.sessionDir, "continuation-driver", "events.jsonl"), "utf8").trim().split("\n").map(JSON.parse);
|
|
432
|
+
assert.deepEqual(events.filter((event) => event.type === "turn_started").map((event) => event.current_step), ["pull-work", "design-probe"]);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("public workflow drive invokes an explicit adapter but preserves canonical completion authority", async () => {
|
|
436
|
+
const session = makeSession("continuation-driver-cli");
|
|
437
|
+
claimAmbientSessionAssignment(session);
|
|
438
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
439
|
+
const adapter = path.join(session.projectRoot, "adapter.mjs");
|
|
440
|
+
const commandFile = path.join(session.projectRoot, "adapter-command.json");
|
|
441
|
+
fs.writeFileSync(adapter, `
|
|
442
|
+
import fs from "node:fs";
|
|
443
|
+
let input = "";
|
|
444
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
445
|
+
fs.writeFileSync("adapter-request.json", input);
|
|
446
|
+
process.stdout.write(JSON.stringify({ status: "completed", summary: "adapter says done" }));
|
|
447
|
+
`);
|
|
448
|
+
writeJson(commandFile, { argv: [process.execPath, adapter] });
|
|
449
|
+
|
|
450
|
+
const rc = await workflowMain([
|
|
451
|
+
"drive",
|
|
452
|
+
"--session-dir", session.sessionDir,
|
|
453
|
+
"--adapter-command-file", commandFile,
|
|
454
|
+
"--max-turns", "1",
|
|
455
|
+
"--turn-timeout-ms", "5000",
|
|
456
|
+
"--barrier-wait-ms", "0",
|
|
457
|
+
"--json",
|
|
458
|
+
]);
|
|
459
|
+
|
|
460
|
+
assert.equal(rc, 0);
|
|
461
|
+
const request = readJson(path.join(session.projectRoot, "adapter-request.json"));
|
|
462
|
+
assert.equal(request.current_step, "pull-work");
|
|
463
|
+
assert.deepEqual(request.next_action.skills, ["pull-work"]);
|
|
464
|
+
assert.equal(Object.hasOwn(request, "system_prompt"), false);
|
|
465
|
+
const driverState = readJson(path.join(session.sessionDir, "continuation-driver", "state.json"));
|
|
466
|
+
assert.equal(driverState.status, "budget_exhausted");
|
|
467
|
+
const canonical = await recoverBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
468
|
+
assert.equal(canonical.run.state.status, "active");
|
|
469
|
+
assert.equal(canonical.run.state.current_step, "pull-work");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("public workflow drive rejects a non-owner before adapter execution", async () => {
|
|
473
|
+
const session = makeSession("continuation-driver-owner");
|
|
474
|
+
claimSessionAssignment(session);
|
|
475
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
476
|
+
const marker = path.join(session.projectRoot, "adapter-ran");
|
|
477
|
+
const adapter = path.join(session.projectRoot, "adapter.mjs");
|
|
478
|
+
const commandFile = path.join(session.projectRoot, "adapter-command.json");
|
|
479
|
+
fs.writeFileSync(adapter, `
|
|
480
|
+
import fs from "node:fs";
|
|
481
|
+
fs.writeFileSync(${JSON.stringify(marker)}, "ran");
|
|
482
|
+
process.stdout.write(JSON.stringify({ status: "completed" }));
|
|
483
|
+
`);
|
|
484
|
+
writeJson(commandFile, { argv: [process.execPath, adapter] });
|
|
485
|
+
|
|
486
|
+
await assert.rejects(
|
|
487
|
+
workflowMain(["drive", "--session-dir", session.sessionDir, "--adapter-command-file", commandFile]),
|
|
488
|
+
/active, matching assignment actor/,
|
|
489
|
+
);
|
|
490
|
+
assert.equal(fs.existsSync(marker), false);
|
|
491
|
+
assert.equal(fs.existsSync(path.join(session.sessionDir, "continuation-driver")), false);
|
|
492
|
+
});
|
|
493
|
+
|
|
350
494
|
test("sync attaches the staged trust.bundle bytes when the session bundle is replaced", async () => {
|
|
351
495
|
const session = makeSession("snapshot-attachment");
|
|
352
496
|
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
@@ -736,13 +880,15 @@ test("failed verification projects Flow-owned route-back attempt and budget", as
|
|
|
736
880
|
const verify = await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
737
881
|
assert.equal(verify.run.state.current_step, "verify");
|
|
738
882
|
|
|
883
|
+
const failureTimestamp = new Date().toISOString();
|
|
739
884
|
const routed = await writeAndSync(session, [bundleClaim({
|
|
740
885
|
expectation: "tests-evidence",
|
|
741
886
|
claimType: "builder.verify.tests",
|
|
742
887
|
subjectType: "flow-step",
|
|
743
888
|
status: "fail",
|
|
744
889
|
routeReason: "implementation_defect",
|
|
745
|
-
|
|
890
|
+
timestamp: failureTimestamp,
|
|
891
|
+
}), ...verifiedTestsPrerequisites(session, failureTimestamp)]);
|
|
746
892
|
|
|
747
893
|
assert.equal(routed.run.state.current_step, "execute");
|
|
748
894
|
assert.equal(routed.projection.flow_run.route_back_attempt, 1);
|
|
@@ -750,17 +896,28 @@ test("failed verification projects Flow-owned route-back attempt and budget", as
|
|
|
750
896
|
assert.match(routed.projection.next_action.summary, /Route-back history: attempt 1\/3 returned to `execute` for `implementation_defect`/);
|
|
751
897
|
assert.deepEqual(routed.projection.next_action.skills, ["execute-plan"]);
|
|
752
898
|
|
|
753
|
-
const reentered = await writeAndSync(session, [bundleClaim({
|
|
899
|
+
const reentered = await writeAndSync(session, [withIdentitySuffix(bundleClaim({
|
|
754
900
|
expectation: "implementation-scope",
|
|
755
901
|
claimType: "builder.execute.scope",
|
|
756
902
|
subjectType: "change",
|
|
757
903
|
timestamp: new Date().toISOString(),
|
|
758
|
-
})]);
|
|
904
|
+
}), "reentry")]);
|
|
759
905
|
assert.equal(reentered.run.state.current_step, "verify");
|
|
906
|
+
const staleRetry = await writeAndSync(session, [bundleClaim({
|
|
907
|
+
expectation: "tests-evidence",
|
|
908
|
+
claimType: "builder.verify.tests",
|
|
909
|
+
subjectType: "flow-step",
|
|
910
|
+
status: "fail",
|
|
911
|
+
routeReason: "implementation_defect",
|
|
912
|
+
timestamp: NOW,
|
|
913
|
+
})]);
|
|
914
|
+
assert.equal(staleRetry.attached, false);
|
|
915
|
+
assert.equal(staleRetry.run.state.current_step, "verify");
|
|
916
|
+
assert.equal(staleRetry.run.state.transitions.filter((transition) => transition.type === "route_back").length, 1);
|
|
760
917
|
const correctedAt = new Date(Date.parse(reentered.run.state.transitions.at(-1).at) + 1).toISOString();
|
|
761
918
|
const corrected = await writeAndSync(session, [
|
|
762
|
-
bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step", timestamp: correctedAt }),
|
|
763
|
-
...verifiedTestsPrerequisites(session, correctedAt),
|
|
919
|
+
withIdentitySuffix(bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step", timestamp: correctedAt }), "corrected"),
|
|
920
|
+
...verifiedTestsPrerequisites(session, correctedAt).map((entry, index) => withIdentitySuffix(entry, `corrected-${index}`)),
|
|
764
921
|
]);
|
|
765
922
|
assert.equal(corrected.run.state.current_step, "merge-ready");
|
|
766
923
|
const verifyEvidence = corrected.run.manifest.evidence.filter((entry) => entry.gate_id === "verify-gate");
|
|
@@ -768,6 +925,190 @@ test("failed verification projects Flow-owned route-back attempt and budget", as
|
|
|
768
925
|
assert.equal(verifyEvidence[0].superseded_by, verifyEvidence[1].id);
|
|
769
926
|
});
|
|
770
927
|
|
|
928
|
+
test("producer-superseded FAIL is audit history and live PASS drives verify", async () => {
|
|
929
|
+
const session = makeSession("producer-superseded-verify");
|
|
930
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
931
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
932
|
+
await writeAndSync(session, [
|
|
933
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
934
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
935
|
+
]);
|
|
936
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
937
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
938
|
+
|
|
939
|
+
const livePass = bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" });
|
|
940
|
+
const historicalFail = historicalProducerSuperseded(bundleClaim({
|
|
941
|
+
expectation: "tests-evidence",
|
|
942
|
+
claimType: "builder.verify.tests",
|
|
943
|
+
subjectType: "flow-step",
|
|
944
|
+
status: "fail",
|
|
945
|
+
routeReason: "implementation_defect",
|
|
946
|
+
}));
|
|
947
|
+
const result = await writeAndSync(session, [historicalFail, livePass, ...verifiedTestsPrerequisites(session)]);
|
|
948
|
+
|
|
949
|
+
assert.equal(result.run.state.current_step, "merge-ready");
|
|
950
|
+
const attached = result.run.manifest.evidence.filter((entry) => entry.gate_id === "verify-gate" && !entry.superseded_by);
|
|
951
|
+
assert.equal(attached.length, 1);
|
|
952
|
+
assert.deepEqual(attached[0].expectation_ids.sort(), ["acceptance-criteria", "clean-critique", "tests-evidence"]);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("partial passing review snapshot waits for the complete verify expectation set", async () => {
|
|
956
|
+
const session = makeSession("atomic-review-sync");
|
|
957
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
958
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
959
|
+
await writeAndSync(session, [
|
|
960
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
961
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
962
|
+
]);
|
|
963
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
964
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
965
|
+
|
|
966
|
+
const disputedCritique = verifiedTestsPrerequisites(session)[0];
|
|
967
|
+
disputedCritique.claim.value = "fail";
|
|
968
|
+
disputedCritique.claim.status = "disputed";
|
|
969
|
+
disputedCritique.event.status = "disputed";
|
|
970
|
+
const disputedPartial = await writeAndSync(session, [disputedCritique]);
|
|
971
|
+
assert.equal(disputedPartial.attached, false);
|
|
972
|
+
assert.equal(disputedPartial.run.state.transitions.filter((transition) => transition.type === "route_back").length, 0);
|
|
973
|
+
|
|
974
|
+
const partial = await writeAndSync(session, verifiedTestsPrerequisites(session).slice(0, 1));
|
|
975
|
+
assert.equal(partial.attached, false);
|
|
976
|
+
assert.equal(partial.run.state.current_step, "verify");
|
|
977
|
+
assert.equal(partial.run.state.transitions.filter((transition) => transition.type === "route_back").length, 0);
|
|
978
|
+
|
|
979
|
+
const complete = await writeAndSync(session, [
|
|
980
|
+
bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" }),
|
|
981
|
+
...verifiedTestsPrerequisites(session),
|
|
982
|
+
]);
|
|
983
|
+
assert.equal(complete.run.state.current_step, "merge-ready");
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test("initial gate boundary rejects pre-run and far-future claims", async () => {
|
|
987
|
+
for (const [name, timestamp] of [
|
|
988
|
+
["pre-run", NOW],
|
|
989
|
+
["far-future", new Date(Date.now() + 60 * 60_000).toISOString()],
|
|
990
|
+
]) {
|
|
991
|
+
const session = makeSession(`freshness-${name}`);
|
|
992
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
993
|
+
const result = await writeAndSync(session, [bundleClaim({
|
|
994
|
+
expectation: "selected-work",
|
|
995
|
+
claimType: "builder.pull-work.selected",
|
|
996
|
+
subjectType: "work-item",
|
|
997
|
+
timestamp,
|
|
998
|
+
})]);
|
|
999
|
+
assert.equal(result.attached, false, name);
|
|
1000
|
+
assert.equal(result.run.state.current_step, "pull-work", name);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("prior claim identity is rejected after re-entry and a new identity can recover", async () => {
|
|
1005
|
+
const session = makeSession("same-digest-reentry");
|
|
1006
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
1007
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
1008
|
+
await writeAndSync(session, [
|
|
1009
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
1010
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
1011
|
+
]);
|
|
1012
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
1013
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
1014
|
+
const future = new Date(Date.now() + 10_000).toISOString();
|
|
1015
|
+
const failure = [bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step", status: "fail", routeReason: "implementation_defect", timestamp: future }), ...verifiedTestsPrerequisites(session, future)];
|
|
1016
|
+
const first = await writeAndSync(session, failure);
|
|
1017
|
+
assert.equal(first.run.state.current_step, "execute");
|
|
1018
|
+
await writeAndSync(session, [withIdentitySuffix(bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change", timestamp: future }), "reentry")]);
|
|
1019
|
+
const replay = await writeAndSync(session, failure);
|
|
1020
|
+
assert.equal(replay.attached, false);
|
|
1021
|
+
assert.equal(replay.run.state.current_step, "verify");
|
|
1022
|
+
assert.equal(replay.run.state.transitions.filter((transition) => transition.type === "route_back").length, 1);
|
|
1023
|
+
const newIdentity = failure.map((entry, index) => withIdentitySuffix(entry, `visit-2-${index}`));
|
|
1024
|
+
const second = await writeAndSync(session, newIdentity);
|
|
1025
|
+
assert.equal(second.run.state.current_step, "execute");
|
|
1026
|
+
const verifyEvidence = second.run.manifest.evidence.filter((entry) => entry.gate_id === "verify-gate");
|
|
1027
|
+
assert.equal(verifyEvidence.length, 2);
|
|
1028
|
+
assert.equal(verifyEvidence[0].superseded_by, verifyEvidence[1].id);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test("legacy colon-bearing assignment actor releases only through its normalized equivalent", () => {
|
|
1032
|
+
const session = makeSession("legacy-actor-release");
|
|
1033
|
+
const legacyActor = { runtime: "unknown", session_id: "legacy-session", host: "Kontour", human: null };
|
|
1034
|
+
performLocalClaim(session.artifactRoot, session.slug, legacyActor, {
|
|
1035
|
+
ttlSeconds: 1800,
|
|
1036
|
+
actorKey: "unknown:legacy-session:Kontour",
|
|
1037
|
+
branch: `agent/${session.slug}`,
|
|
1038
|
+
artifactDir: session.sessionDir,
|
|
1039
|
+
workItemRef: SUBJECT,
|
|
1040
|
+
reason: "legacy fixture",
|
|
1041
|
+
});
|
|
1042
|
+
assert.throws(() => performLocalRelease(session.artifactRoot, session.slug, legacyActor, {
|
|
1043
|
+
actorKey: "different-actor",
|
|
1044
|
+
}), /refusing to release/);
|
|
1045
|
+
assert.throws(() => performLocalRelease(session.artifactRoot, session.slug, { ...legacyActor, session_id: "different" }, {
|
|
1046
|
+
actorKey: "unknownlegacy-sessionKontour",
|
|
1047
|
+
}), /refusing to release/);
|
|
1048
|
+
const released = performLocalRelease(session.artifactRoot, session.slug, legacyActor, {
|
|
1049
|
+
actorKey: "unknownlegacy-sessionKontour",
|
|
1050
|
+
});
|
|
1051
|
+
assert.equal(released.status, "released");
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
test("string-only legacy liveness identity cannot be released by a colliding modern actor", async () => {
|
|
1055
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-legacy-liveness-"));
|
|
1056
|
+
const eventsFile = path.join(root, "liveness", "events.jsonl");
|
|
1057
|
+
fs.mkdirSync(path.dirname(eventsFile), { recursive: true });
|
|
1058
|
+
fs.writeFileSync(eventsFile, `${JSON.stringify({
|
|
1059
|
+
type: "claim",
|
|
1060
|
+
subjectId: "legacy-subject",
|
|
1061
|
+
actor: "unknown:anc-123456789abc:Kontour",
|
|
1062
|
+
at: NOW,
|
|
1063
|
+
ttlSeconds: 1800,
|
|
1064
|
+
})}\n`);
|
|
1065
|
+
|
|
1066
|
+
await assert.rejects(() => workflowSidecarMain([
|
|
1067
|
+
"liveness", "release", "legacy-subject",
|
|
1068
|
+
"--actor", "unknownanc-123456789abcKontour",
|
|
1069
|
+
"--artifact-root", root,
|
|
1070
|
+
"--at", "2026-07-09T20:01:00.000Z",
|
|
1071
|
+
]), /prior claim/);
|
|
1072
|
+
const events = fs.readFileSync(eventsFile, "utf8").trim().split("\n").map(JSON.parse);
|
|
1073
|
+
assert.equal(events.length, 1);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
test("lossy modern liveness key collision cannot release a non-reversible legacy actor", async () => {
|
|
1077
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-lossy-liveness-"));
|
|
1078
|
+
const eventsFile = path.join(root, "liveness", "events.jsonl");
|
|
1079
|
+
fs.mkdirSync(path.dirname(eventsFile), { recursive: true });
|
|
1080
|
+
fs.writeFileSync(eventsFile, `${JSON.stringify({ type: "claim", subjectId: "collision", actor: "a:b", at: NOW, ttlSeconds: 1800 })}\n`);
|
|
1081
|
+
await assert.rejects(() => workflowSidecarMain([
|
|
1082
|
+
"liveness", "release", "collision", "--actor", "ab", "--artifact-root", root,
|
|
1083
|
+
]), /prior claim/);
|
|
1084
|
+
assert.equal(fs.readFileSync(eventsFile, "utf8").trim().split("\n").length, 1);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test("upgrade rebuild preserves legacy critique identity until a versioned new review", async () => {
|
|
1088
|
+
const legacy = {
|
|
1089
|
+
id: "upgrade-review",
|
|
1090
|
+
reviewer: "reviewer",
|
|
1091
|
+
reviewed_at: "2026-07-01T00:00:00Z",
|
|
1092
|
+
verdict: "pass",
|
|
1093
|
+
summary: "Legacy clean review",
|
|
1094
|
+
findings: [],
|
|
1095
|
+
lanes: [{ id: "code", status: "pass" }],
|
|
1096
|
+
review_target: { artifacts: [] },
|
|
1097
|
+
artifact_refs: [],
|
|
1098
|
+
};
|
|
1099
|
+
const beforeUpgrade = await buildTrustBundle("upgrade-fixture", "2026-07-01T00:00:00Z", [], [], [legacy]);
|
|
1100
|
+
const rebuiltAfterUpgrade = await buildTrustBundle("upgrade-fixture", "2026-07-12T00:00:00Z", [], [], [legacy]);
|
|
1101
|
+
const newReview = await buildTrustBundle("upgrade-fixture", "2026-07-12T00:01:00Z", [], [], [{
|
|
1102
|
+
...legacy,
|
|
1103
|
+
reviewed_at: "2026-07-12T00:01:00Z",
|
|
1104
|
+
identity_version: 2,
|
|
1105
|
+
}]);
|
|
1106
|
+
assert.equal(beforeUpgrade.claims[0].id, rebuiltAfterUpgrade.claims[0].id);
|
|
1107
|
+
assert.notEqual(newReview.claims[0].id, beforeUpgrade.claims[0].id);
|
|
1108
|
+
assert.equal(rebuiltAfterUpgrade.claims[0].metadata.identity_version, undefined);
|
|
1109
|
+
assert.equal(newReview.claims[0].metadata.identity_version, 2);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
771
1112
|
test("passing tests-evidence rejects a critique whose reviewed artifact changed", async () => {
|
|
772
1113
|
const session = makeSession("stale-critique-artifact");
|
|
773
1114
|
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
@@ -837,25 +1178,30 @@ test("verified sidecar claims drive the composed publish and learning prefix to
|
|
|
837
1178
|
claimSessionAssignment(session);
|
|
838
1179
|
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
839
1180
|
const steps = [
|
|
840
|
-
[bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })],
|
|
841
|
-
[
|
|
1181
|
+
() => [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })],
|
|
1182
|
+
() => [
|
|
842
1183
|
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
843
1184
|
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
844
1185
|
],
|
|
845
|
-
[bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })],
|
|
846
|
-
[bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })],
|
|
847
|
-
[bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" }), ...verifiedTestsPrerequisites(session)],
|
|
848
|
-
[bundleClaim({ expectation: "merge-readiness", claimType: "builder.merge-ready.readiness", subjectType: "change" })],
|
|
1186
|
+
() => [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })],
|
|
1187
|
+
() => [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })],
|
|
1188
|
+
() => [bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" }), ...verifiedTestsPrerequisites(session)],
|
|
1189
|
+
() => [bundleClaim({ expectation: "merge-readiness", claimType: "builder.merge-ready.readiness", subjectType: "change" })],
|
|
849
1190
|
];
|
|
850
|
-
for (const entries of steps) await writeAndSync(session, entries);
|
|
1191
|
+
for (const entries of steps) await writeAndSync(session, entries());
|
|
851
1192
|
|
|
852
1193
|
const prOpen = readJson(path.join(session.sessionDir, "state.json"));
|
|
853
1194
|
assert.equal(prOpen.flow_run.current_step, "pr-open");
|
|
1195
|
+
assert.equal(readJson(path.join(session.artifactRoot, "current.json")).active_step_id, "pr-open");
|
|
854
1196
|
assert.deepEqual(prOpen.next_action.skills, []);
|
|
855
1197
|
assert.deepEqual(prOpen.next_action.operations, ["publish-change"]);
|
|
856
1198
|
|
|
857
|
-
await writeAndSync(session, [bundleClaim({ expectation: "pull-request-opened", claimType: "builder.pr-open.pull-request", subjectType: "pull-request" })]);
|
|
858
|
-
|
|
1199
|
+
const mergeReadyCi = await writeAndSync(session, [bundleClaim({ expectation: "pull-request-opened", claimType: "builder.pr-open.pull-request", subjectType: "pull-request" })]);
|
|
1200
|
+
assert.equal(mergeReadyCi.run.state.current_step, "merge-ready-ci");
|
|
1201
|
+
assert.equal(readJson(path.join(session.artifactRoot, "current.json")).active_step_id, "merge-ready-ci");
|
|
1202
|
+
const learn = await writeAndSync(session, [bundleClaim({ expectation: "ci-merge-readiness", claimType: "builder.merge-ready-ci.readiness", subjectType: "pull-request" })]);
|
|
1203
|
+
assert.equal(learn.run.state.current_step, "learn");
|
|
1204
|
+
assert.equal(readJson(path.join(session.artifactRoot, "current.json")).active_step_id, "learn");
|
|
859
1205
|
const completed = await writeAndSync(session, [
|
|
860
1206
|
bundleClaim({ expectation: "decision-evidence", claimType: "builder.learn.decisions", subjectType: "decision" }),
|
|
861
1207
|
bundleClaim({ expectation: "learning-evidence", claimType: "builder.learn.evidence", subjectType: "release" }),
|
|
@@ -865,6 +1211,18 @@ test("verified sidecar claims drive the composed publish and learning prefix to
|
|
|
865
1211
|
assert.equal(completed.run.state.status, "completed");
|
|
866
1212
|
assert.equal(completed.projection.status, "delivered");
|
|
867
1213
|
assert.deepEqual(completed.projection.next_action, { status: "done", summary: "Canonical Flow run is complete." });
|
|
1214
|
+
const liveEvidence = completed.run.manifest.evidence.filter((entry) => !entry.superseded_by);
|
|
1215
|
+
assert.deepEqual(liveEvidence.map((entry) => entry.expectation_ids), [
|
|
1216
|
+
["selected-work"],
|
|
1217
|
+
["pickup-probe-readiness", "probe-decisions-or-accepted-gaps"],
|
|
1218
|
+
["implementation-plan"],
|
|
1219
|
+
["implementation-scope"],
|
|
1220
|
+
["clean-critique", "acceptance-criteria", "tests-evidence"],
|
|
1221
|
+
["merge-readiness"],
|
|
1222
|
+
["pull-request-opened"],
|
|
1223
|
+
["ci-merge-readiness"],
|
|
1224
|
+
["decision-evidence", "learning-evidence"],
|
|
1225
|
+
]);
|
|
868
1226
|
|
|
869
1227
|
const flowDirectory = runDir(session.slug, session.projectRoot);
|
|
870
1228
|
const beforeFlow = snapshotTree(flowDirectory);
|