@kontourai/flow-agents 3.6.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 +19 -0
- package/build/src/builder-flow-run-adapter.d.ts +22 -2
- package/build/src/builder-flow-run-adapter.js +93 -28
- package/build/src/builder-flow-runtime.d.ts +8 -3
- package/build/src/builder-flow-runtime.js +407 -51
- package/build/src/cli/assignment-provider.d.ts +4 -7
- package/build/src/cli/assignment-provider.js +80 -14
- package/build/src/cli/builder-run.js +14 -18
- package/build/src/cli/workflow-sidecar.d.ts +28 -4
- package/build/src/cli/workflow-sidecar.js +825 -103
- package/build/src/cli/workflow.js +301 -43
- package/build/src/flow-kit/validate.d.ts +17 -0
- package/build/src/flow-kit/validate.js +340 -2
- package/build/src/index.js +5 -5
- package/build/src/lib/observed-command.d.ts +7 -0
- package/build/src/lib/observed-command.js +100 -0
- package/build/src/tools/build-universal-bundles.js +53 -3
- package/build/src/tools/generate-context-map.js +5 -3
- package/context/scripts/hooks/lib/kit-catalog.js +1 -1
- package/context/scripts/hooks/stop-goal-fit.js +78 -9
- package/docs/context-map.md +22 -20
- package/docs/developer-architecture.md +1 -1
- package/docs/getting-started.md +9 -1
- package/docs/public-workflow-cli.md +76 -7
- package/docs/skills-map.md +51 -27
- package/docs/spec/builder-flow-runtime.md +75 -40
- package/docs/workflow-usage-guide.md +109 -42
- package/evals/fixtures/hook-influence/cases.json +2 -2
- package/evals/integration/test_builder_entry_enforcement.sh +52 -41
- package/evals/integration/test_builder_step_producers.sh +330 -6
- package/evals/integration/test_bundle_install.sh +318 -65
- package/evals/integration/test_bundle_lifecycle.sh +4 -6
- package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
- package/evals/integration/test_current_json_per_actor.sh +12 -0
- package/evals/integration/test_dual_emit_flow_step.sh +62 -43
- package/evals/integration/test_flowdef_session_activation.sh +145 -25
- package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
- package/evals/integration/test_gate_lockdown.sh +3 -5
- package/evals/integration/test_goal_fit_hook.sh +60 -2
- package/evals/integration/test_install_merge.sh +18 -8
- package/evals/integration/test_liveness_verdict.sh +14 -17
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
- package/evals/integration/test_public_workflow_cli.sh +334 -14
- package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
- package/evals/integration/test_sidecar_field_preservation.sh +36 -11
- package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
- package/evals/integration/test_workflow_steering_hook.sh +15 -38
- package/evals/run.sh +2 -0
- package/evals/static/test_builder_skill_coherence.sh +247 -0
- package/evals/static/test_library_exports.sh +5 -2
- package/evals/static/test_universal_bundles.sh +44 -1
- package/evals/static/test_workflow_skills.sh +13 -325
- package/kits/builder/flows/build.flow.json +22 -0
- package/kits/builder/flows/shape.flow.json +9 -9
- package/kits/builder/kit.json +70 -16
- package/kits/builder/skills/builder-shape/SKILL.md +75 -75
- package/kits/builder/skills/continue-work/SKILL.md +45 -106
- package/kits/builder/skills/deliver/SKILL.md +96 -442
- package/kits/builder/skills/design-probe/SKILL.md +40 -139
- package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
- package/kits/builder/skills/execute-plan/SKILL.md +54 -125
- package/kits/builder/skills/fix-bug/SKILL.md +42 -132
- package/kits/builder/skills/gate-review/SKILL.md +60 -211
- package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
- package/kits/builder/skills/learning-review/SKILL.md +63 -170
- package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
- package/kits/builder/skills/plan-work/SKILL.md +51 -185
- package/kits/builder/skills/pull-work/SKILL.md +136 -485
- package/kits/builder/skills/release-readiness/SKILL.md +66 -107
- package/kits/builder/skills/review-work/SKILL.md +89 -176
- package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
- package/kits/builder/skills/verify-work/SKILL.md +101 -113
- package/package.json +2 -2
- package/packaging/README.md +1 -1
- package/scripts/README.md +1 -1
- package/scripts/hooks/lib/kit-catalog.js +1 -1
- package/scripts/hooks/stop-goal-fit.js +78 -9
- package/scripts/install-codex-home.sh +63 -23
- package/scripts/install-owned-files.js +18 -2
- package/src/builder-flow-run-adapter.ts +118 -32
- package/src/builder-flow-runtime.ts +426 -64
- package/src/cli/assignment-provider-lock.test.mjs +83 -0
- package/src/cli/assignment-provider.ts +75 -14
- package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
- package/src/cli/builder-flow-runtime.test.mjs +436 -18
- package/src/cli/builder-run.ts +3 -9
- package/src/cli/kit-metadata-security.test.mjs +232 -6
- package/src/cli/public-api.test.mjs +15 -0
- package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
- package/src/cli/workflow-sidecar.ts +746 -103
- package/src/cli/workflow.ts +288 -41
- package/src/flow-kit/validate.ts +320 -2
- package/src/index.ts +6 -5
- package/src/lib/observed-command.ts +96 -0
- package/src/tools/build-universal-bundles.ts +51 -3
- package/src/tools/generate-context-map.ts +5 -3
|
@@ -3,12 +3,13 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { generateKeyPairSync, sign } from "node:crypto";
|
|
6
|
+
import { createHash, generateKeyPairSync, sign } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
import { FLOW_RUN_EVIDENCE_MANIFEST_PATH, runDir } from "@kontourai/flow";
|
|
9
9
|
import {
|
|
10
10
|
archiveBuilderFlowSession,
|
|
11
11
|
cancelBuilderFlowSession,
|
|
12
|
+
captureReviewWorkspaceSnapshot,
|
|
12
13
|
pauseBuilderFlowSession,
|
|
13
14
|
recoverBuilderFlowSession,
|
|
14
15
|
releaseBuilderFlowAssignment,
|
|
@@ -18,8 +19,9 @@ import {
|
|
|
18
19
|
} from "../../build/src/builder-flow-runtime.js";
|
|
19
20
|
import { builderLifecycleAuthorizationPayload, loadBuilderLifecycleAuthorization, recordAuthorizationConsumed } from "../../build/src/builder-lifecycle-authority.js";
|
|
20
21
|
import { cancelBuilderBuildRun } from "../../build/src/builder-flow-run-adapter.js";
|
|
21
|
-
import { performLocalClaim, readLocalAssignmentStatus, resolveCurrentAssignmentActor } from "../../build/src/cli/assignment-provider.js";
|
|
22
|
+
import { performLocalClaim, performLocalRelease, readLocalAssignmentStatus, resolveCurrentAssignmentActor } from "../../build/src/cli/assignment-provider.js";
|
|
22
23
|
import { main as builderRunMain } from "../../build/src/cli/builder-run.js";
|
|
24
|
+
import { buildTrustBundle, inferExecutedTestCount, main as workflowSidecarMain } from "../../build/src/cli/workflow-sidecar.js";
|
|
23
25
|
|
|
24
26
|
const SUBJECT = "local:work-item/runtime-projection";
|
|
25
27
|
const NOW = "2026-07-09T20:00:00.000Z";
|
|
@@ -51,6 +53,9 @@ function makeSession(slug = "runtime-projection") {
|
|
|
51
53
|
schema_version: "1.0",
|
|
52
54
|
keys: [{ id: AUTHORITY_KEY_ID, algorithm: "ed25519", public_key_pem: AUTHORITY_KEYS.publicKey.export({ type: "spki", format: "pem" }) }],
|
|
53
55
|
});
|
|
56
|
+
fs.mkdirSync(path.join(projectRoot, "review-target"), { recursive: true });
|
|
57
|
+
fs.writeFileSync(path.join(projectRoot, "review-target", "implementation.txt"), "reviewed implementation\n");
|
|
58
|
+
fs.writeFileSync(path.join(projectRoot, "review-target", "delivery.md"), "reviewed delivery artifact\n");
|
|
54
59
|
return { projectRoot, artifactRoot, sessionDir, slug };
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -59,6 +64,15 @@ function writeJson(file, value) {
|
|
|
59
64
|
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
test("shell output cannot spoof an executed-test count", () => {
|
|
68
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-test-count-"));
|
|
69
|
+
fs.mkdirSync(path.join(root, "checks"), { recursive: true });
|
|
70
|
+
fs.writeFileSync(path.join(root, "checks", "fake-test.sh"), "#!/bin/sh\nset -e\nprintf '1 passed\\n'\n");
|
|
71
|
+
fs.writeFileSync(path.join(root, "checks", "real-test.sh"), "#!/bin/sh\nset -e\ntest -f checks/real-test.sh\n");
|
|
72
|
+
assert.equal(inferExecutedTestCount("sh checks/fake-test.sh", root, "1 passed\n"), 0);
|
|
73
|
+
assert.equal(inferExecutedTestCount("sh checks/real-test.sh", root, "1..1\nok 1 - file exists\n"), 1);
|
|
74
|
+
});
|
|
75
|
+
|
|
62
76
|
function readJson(file) {
|
|
63
77
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
64
78
|
}
|
|
@@ -200,7 +214,7 @@ async function assertRecoveryRejectsWithoutWrites(session, pattern) {
|
|
|
200
214
|
assert.deepEqual(snapshotProjectionTargets(session), beforeProjection);
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
function bundleClaim({ expectation, claimType, subjectType, status = "pass", routeReason, subject = SUBJECT }) {
|
|
217
|
+
function bundleClaim({ expectation, claimType, subjectType, status = "pass", routeReason, subject = SUBJECT, testCount = 1, timestamp = new Date().toISOString() }) {
|
|
204
218
|
const claimId = `claim.${expectation}`;
|
|
205
219
|
return {
|
|
206
220
|
claim: {
|
|
@@ -212,6 +226,11 @@ function bundleClaim({ expectation, claimType, subjectType, status = "pass", rou
|
|
|
212
226
|
value: status,
|
|
213
227
|
metadata: {
|
|
214
228
|
workflow_subject_ref: subject,
|
|
229
|
+
...(expectation === "tests-evidence" ? {
|
|
230
|
+
recorded_by: "implementation-actor",
|
|
231
|
+
artifact_refs: [{ kind: "command", excerpt: "node --test src/cli/builder-flow-runtime.test.mjs", summary: "Runtime fixture assertion." }],
|
|
232
|
+
observed_commands: [{ command: "node --test src/cli/builder-flow-runtime.test.mjs", exit_code: 0, test_count: testCount, output_sha256: "0".repeat(64) }],
|
|
233
|
+
} : {}),
|
|
215
234
|
gate_claim: {
|
|
216
235
|
expectation_id: expectation,
|
|
217
236
|
claim_type: claimType,
|
|
@@ -219,8 +238,8 @@ function bundleClaim({ expectation, claimType, subjectType, status = "pass", rou
|
|
|
219
238
|
...(routeReason ? { route_reason: routeReason } : {}),
|
|
220
239
|
},
|
|
221
240
|
},
|
|
222
|
-
createdAt:
|
|
223
|
-
updatedAt:
|
|
241
|
+
createdAt: timestamp,
|
|
242
|
+
updatedAt: timestamp,
|
|
224
243
|
},
|
|
225
244
|
evidence: {
|
|
226
245
|
id: `evidence.${expectation}`,
|
|
@@ -229,7 +248,7 @@ function bundleClaim({ expectation, claimType, subjectType, status = "pass", rou
|
|
|
229
248
|
method: "attestation",
|
|
230
249
|
sourceRef: "src/cli/builder-flow-runtime.test.mjs",
|
|
231
250
|
excerptOrSummary: `${expectation} fixture`,
|
|
232
|
-
observedAt:
|
|
251
|
+
observedAt: timestamp,
|
|
233
252
|
collectedBy: "flow-agents-test",
|
|
234
253
|
},
|
|
235
254
|
event: {
|
|
@@ -239,10 +258,47 @@ function bundleClaim({ expectation, claimType, subjectType, status = "pass", rou
|
|
|
239
258
|
actor: "flow-agents-test",
|
|
240
259
|
method: "attestation",
|
|
241
260
|
evidenceIds: [`evidence.${expectation}`],
|
|
242
|
-
createdAt:
|
|
243
|
-
verifiedAt:
|
|
261
|
+
createdAt: timestamp,
|
|
262
|
+
verifiedAt: timestamp,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function verifiedTestsPrerequisites(session, timestamp = new Date().toISOString()) {
|
|
268
|
+
const reviewArtifact = path.join(session.projectRoot, "review-target", "delivery.md");
|
|
269
|
+
const implementation = path.join(session.projectRoot, "review-target", "implementation.txt");
|
|
270
|
+
const implementationFile = path.relative(session.projectRoot, implementation);
|
|
271
|
+
const implementationBytes = fs.readFileSync(implementation);
|
|
272
|
+
const implementationSha256 = createHash("sha256").update(implementationBytes).digest("hex");
|
|
273
|
+
const workspaceDigest = createHash("sha256")
|
|
274
|
+
.update("flow-agents:reviewed-files:v1\0")
|
|
275
|
+
.update(implementationFile).update("\0").update(implementationBytes).update("\0")
|
|
276
|
+
.digest("hex");
|
|
277
|
+
const critique = bundleClaim({ expectation: "clean-critique", claimType: "workflow.critique.review", subjectType: "workflow-critique", timestamp });
|
|
278
|
+
critique.claim.metadata = {
|
|
279
|
+
workflow_subject_ref: SUBJECT,
|
|
280
|
+
origin: "critique",
|
|
281
|
+
reviewer: "reviewer-actor",
|
|
282
|
+
findings: [],
|
|
283
|
+
lanes: [{ id: "code", status: "pass" }],
|
|
284
|
+
review_target: {
|
|
285
|
+
artifacts: [{ file: path.relative(session.projectRoot, reviewArtifact), sha256: createHash("sha256").update(fs.readFileSync(reviewArtifact)).digest("hex") }],
|
|
286
|
+
workspace_snapshot: {
|
|
287
|
+
version: 1,
|
|
288
|
+
kind: "reviewed-files",
|
|
289
|
+
algorithm: "sha256",
|
|
290
|
+
digest: workspaceDigest,
|
|
291
|
+
files: [{ file: implementationFile, sha256: implementationSha256 }],
|
|
292
|
+
},
|
|
244
293
|
},
|
|
245
294
|
};
|
|
295
|
+
const criterion = bundleClaim({ expectation: "verified-criterion", claimType: "workflow.acceptance.criterion", subjectType: "flow-step", timestamp });
|
|
296
|
+
criterion.claim.metadata = {
|
|
297
|
+
workflow_subject_ref: SUBJECT,
|
|
298
|
+
origin: "acceptance",
|
|
299
|
+
criterion: { id: "ac-runtime", status: "pass", evidence_refs: [{ kind: "command", excerpt: "node --test src/cli/builder-flow-runtime.test.mjs", summary: "Runtime fixture assertion." }] },
|
|
300
|
+
};
|
|
301
|
+
return [critique, criterion];
|
|
246
302
|
}
|
|
247
303
|
|
|
248
304
|
function writeBundle(sessionDir, entries) {
|
|
@@ -256,6 +312,24 @@ function writeBundle(sessionDir, entries) {
|
|
|
256
312
|
});
|
|
257
313
|
}
|
|
258
314
|
|
|
315
|
+
function historicalProducerSuperseded(entry, suffix = "historical") {
|
|
316
|
+
const copy = withIdentitySuffix(entry, suffix);
|
|
317
|
+
copy.claim.producerStatus = "superseded";
|
|
318
|
+
return copy;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function withIdentitySuffix(entry, suffix) {
|
|
322
|
+
const copy = structuredClone(entry);
|
|
323
|
+
const priorId = copy.claim.id;
|
|
324
|
+
copy.claim.id = `${priorId}.${suffix}`;
|
|
325
|
+
copy.evidence.id = `${copy.evidence.id}.${suffix}`;
|
|
326
|
+
copy.evidence.claimId = copy.claim.id;
|
|
327
|
+
copy.event.id = `${copy.event.id}.${suffix}`;
|
|
328
|
+
copy.event.claimId = copy.claim.id;
|
|
329
|
+
copy.event.evidenceIds = [copy.evidence.id];
|
|
330
|
+
return copy;
|
|
331
|
+
}
|
|
332
|
+
|
|
259
333
|
async function writeAndSync(session, entries) {
|
|
260
334
|
writeBundle(session.sessionDir, entries);
|
|
261
335
|
return syncBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
@@ -291,6 +365,53 @@ test("small-model client can start and advance from projected actions without ch
|
|
|
291
365
|
assert.equal(duplicate.run.manifest.evidence.length, advanced.run.manifest.evidence.length);
|
|
292
366
|
});
|
|
293
367
|
|
|
368
|
+
test("sync attaches the staged trust.bundle bytes when the session bundle is replaced", async () => {
|
|
369
|
+
const session = makeSession("snapshot-attachment");
|
|
370
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
371
|
+
const originalEntries = [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })];
|
|
372
|
+
writeBundle(session.sessionDir, originalEntries);
|
|
373
|
+
const bundleFile = path.join(session.sessionDir, "trust.bundle");
|
|
374
|
+
const originalDigest = createHash("sha256").update(fs.readFileSync(bundleFile)).digest("hex");
|
|
375
|
+
let replaceBundle;
|
|
376
|
+
const replaced = new Promise((resolve) => { replaceBundle = resolve; });
|
|
377
|
+
const watcher = fs.watch(session.sessionDir, (_event, filename) => {
|
|
378
|
+
if (String(filename) !== ".trust-bundle-snapshots") return;
|
|
379
|
+
fs.writeFileSync(bundleFile, "{\"claims\":[]}");
|
|
380
|
+
replaceBundle();
|
|
381
|
+
});
|
|
382
|
+
try {
|
|
383
|
+
const syncing = syncBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
384
|
+
await Promise.race([
|
|
385
|
+
replaced,
|
|
386
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("trust.bundle snapshot was not staged")), 2_000)),
|
|
387
|
+
]);
|
|
388
|
+
const synced = await syncing;
|
|
389
|
+
assert.equal(synced.attached, true);
|
|
390
|
+
} finally {
|
|
391
|
+
watcher.close();
|
|
392
|
+
}
|
|
393
|
+
const manifest = readJson(path.join(runDir(session.slug, session.projectRoot), FLOW_RUN_EVIDENCE_MANIFEST_PATH));
|
|
394
|
+
assert.equal(manifest.evidence.at(-1).sha256, originalDigest);
|
|
395
|
+
assert.equal(fs.existsSync(path.join(session.sessionDir, ".trust-bundle-snapshots")), false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("start rejects a requested Builder flow that differs from the existing run before projection mutation", async (t) => {
|
|
399
|
+
for (const [existingFlowId, requestedFlowId] of [["builder.shape", "builder.build"], ["builder.build", "builder.shape"]]) {
|
|
400
|
+
await t.test(`${existingFlowId} cannot resume as ${requestedFlowId}`, async () => {
|
|
401
|
+
const session = makeSession(`flow-mismatch-${existingFlowId.replace('.', '-')}`);
|
|
402
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir, flowId: existingFlowId });
|
|
403
|
+
const beforeFlow = snapshotTree(runDir(session.slug, session.projectRoot));
|
|
404
|
+
const beforeProjection = snapshotProjectionTargets(session);
|
|
405
|
+
await assert.rejects(
|
|
406
|
+
() => startBuilderFlowSession({ sessionDir: session.sessionDir, flowId: requestedFlowId }),
|
|
407
|
+
new RegExp(`requested ${requestedFlowId} does not match the existing ${existingFlowId} run`),
|
|
408
|
+
);
|
|
409
|
+
assert.deepEqual(snapshotTree(runDir(session.slug, session.projectRoot)), beforeFlow);
|
|
410
|
+
assert.deepEqual(snapshotProjectionTargets(session), beforeProjection);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
294
415
|
test("pause and resume preserve the current Flow step and active assignment", async () => {
|
|
295
416
|
const session = makeSession("lifecycle-pause-resume");
|
|
296
417
|
claimAmbientSessionAssignment(session);
|
|
@@ -546,6 +667,8 @@ test("builder-run exposes lifecycle actions without caller-selected Flow identit
|
|
|
546
667
|
const ambient = resolveCurrentAssignmentActor();
|
|
547
668
|
performLocalClaim(session.artifactRoot, session.slug, ambient.actor, { actorKey: ambient.actorKey, ttlSeconds: 1800, branch: `agent/${session.slug}`, artifactDir: session.sessionDir, workItemRef: SUBJECT, reason: "test" });
|
|
548
669
|
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
670
|
+
assert.equal(await builderRunMain(["start", "--session-dir", session.sessionDir]), 64);
|
|
671
|
+
assert.equal(await builderRunMain(["sync", "--session-dir", session.sessionDir]), 64);
|
|
549
672
|
assert.equal(await builderRunMain([
|
|
550
673
|
"pause", "--session-dir", session.sessionDir,
|
|
551
674
|
"--reason", "cli pause",
|
|
@@ -631,19 +754,297 @@ test("failed verification projects Flow-owned route-back attempt and budget", as
|
|
|
631
754
|
const verify = await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
632
755
|
assert.equal(verify.run.state.current_step, "verify");
|
|
633
756
|
|
|
757
|
+
const failureTimestamp = new Date().toISOString();
|
|
634
758
|
const routed = await writeAndSync(session, [bundleClaim({
|
|
635
759
|
expectation: "tests-evidence",
|
|
636
760
|
claimType: "builder.verify.tests",
|
|
637
761
|
subjectType: "flow-step",
|
|
638
762
|
status: "fail",
|
|
639
763
|
routeReason: "implementation_defect",
|
|
640
|
-
|
|
764
|
+
timestamp: failureTimestamp,
|
|
765
|
+
}), ...verifiedTestsPrerequisites(session, failureTimestamp)]);
|
|
641
766
|
|
|
642
767
|
assert.equal(routed.run.state.current_step, "execute");
|
|
643
768
|
assert.equal(routed.projection.flow_run.route_back_attempt, 1);
|
|
644
769
|
assert.equal(routed.projection.flow_run.route_back_max_attempts, 3);
|
|
645
770
|
assert.match(routed.projection.next_action.summary, /Route-back history: attempt 1\/3 returned to `execute` for `implementation_defect`/);
|
|
646
771
|
assert.deepEqual(routed.projection.next_action.skills, ["execute-plan"]);
|
|
772
|
+
|
|
773
|
+
const reentered = await writeAndSync(session, [withIdentitySuffix(bundleClaim({
|
|
774
|
+
expectation: "implementation-scope",
|
|
775
|
+
claimType: "builder.execute.scope",
|
|
776
|
+
subjectType: "change",
|
|
777
|
+
timestamp: new Date().toISOString(),
|
|
778
|
+
}), "reentry")]);
|
|
779
|
+
assert.equal(reentered.run.state.current_step, "verify");
|
|
780
|
+
const staleRetry = await writeAndSync(session, [bundleClaim({
|
|
781
|
+
expectation: "tests-evidence",
|
|
782
|
+
claimType: "builder.verify.tests",
|
|
783
|
+
subjectType: "flow-step",
|
|
784
|
+
status: "fail",
|
|
785
|
+
routeReason: "implementation_defect",
|
|
786
|
+
timestamp: NOW,
|
|
787
|
+
})]);
|
|
788
|
+
assert.equal(staleRetry.attached, false);
|
|
789
|
+
assert.equal(staleRetry.run.state.current_step, "verify");
|
|
790
|
+
assert.equal(staleRetry.run.state.transitions.filter((transition) => transition.type === "route_back").length, 1);
|
|
791
|
+
const correctedAt = new Date(Date.parse(reentered.run.state.transitions.at(-1).at) + 1).toISOString();
|
|
792
|
+
const corrected = await writeAndSync(session, [
|
|
793
|
+
withIdentitySuffix(bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step", timestamp: correctedAt }), "corrected"),
|
|
794
|
+
...verifiedTestsPrerequisites(session, correctedAt).map((entry, index) => withIdentitySuffix(entry, `corrected-${index}`)),
|
|
795
|
+
]);
|
|
796
|
+
assert.equal(corrected.run.state.current_step, "merge-ready");
|
|
797
|
+
const verifyEvidence = corrected.run.manifest.evidence.filter((entry) => entry.gate_id === "verify-gate");
|
|
798
|
+
assert.equal(verifyEvidence.length, 2);
|
|
799
|
+
assert.equal(verifyEvidence[0].superseded_by, verifyEvidence[1].id);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("producer-superseded FAIL is audit history and live PASS drives verify", async () => {
|
|
803
|
+
const session = makeSession("producer-superseded-verify");
|
|
804
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
805
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
806
|
+
await writeAndSync(session, [
|
|
807
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
808
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
809
|
+
]);
|
|
810
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
811
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
812
|
+
|
|
813
|
+
const livePass = bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" });
|
|
814
|
+
const historicalFail = historicalProducerSuperseded(bundleClaim({
|
|
815
|
+
expectation: "tests-evidence",
|
|
816
|
+
claimType: "builder.verify.tests",
|
|
817
|
+
subjectType: "flow-step",
|
|
818
|
+
status: "fail",
|
|
819
|
+
routeReason: "implementation_defect",
|
|
820
|
+
}));
|
|
821
|
+
const result = await writeAndSync(session, [historicalFail, livePass, ...verifiedTestsPrerequisites(session)]);
|
|
822
|
+
|
|
823
|
+
assert.equal(result.run.state.current_step, "merge-ready");
|
|
824
|
+
const attached = result.run.manifest.evidence.filter((entry) => entry.gate_id === "verify-gate" && !entry.superseded_by);
|
|
825
|
+
assert.equal(attached.length, 1);
|
|
826
|
+
assert.deepEqual(attached[0].expectation_ids.sort(), ["acceptance-criteria", "clean-critique", "tests-evidence"]);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("partial passing review snapshot waits for the complete verify expectation set", async () => {
|
|
830
|
+
const session = makeSession("atomic-review-sync");
|
|
831
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
832
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
833
|
+
await writeAndSync(session, [
|
|
834
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
835
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
836
|
+
]);
|
|
837
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
838
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
839
|
+
|
|
840
|
+
const disputedCritique = verifiedTestsPrerequisites(session)[0];
|
|
841
|
+
disputedCritique.claim.value = "fail";
|
|
842
|
+
disputedCritique.claim.status = "disputed";
|
|
843
|
+
disputedCritique.event.status = "disputed";
|
|
844
|
+
const disputedPartial = await writeAndSync(session, [disputedCritique]);
|
|
845
|
+
assert.equal(disputedPartial.attached, false);
|
|
846
|
+
assert.equal(disputedPartial.run.state.transitions.filter((transition) => transition.type === "route_back").length, 0);
|
|
847
|
+
|
|
848
|
+
const partial = await writeAndSync(session, verifiedTestsPrerequisites(session).slice(0, 1));
|
|
849
|
+
assert.equal(partial.attached, false);
|
|
850
|
+
assert.equal(partial.run.state.current_step, "verify");
|
|
851
|
+
assert.equal(partial.run.state.transitions.filter((transition) => transition.type === "route_back").length, 0);
|
|
852
|
+
|
|
853
|
+
const complete = await writeAndSync(session, [
|
|
854
|
+
bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" }),
|
|
855
|
+
...verifiedTestsPrerequisites(session),
|
|
856
|
+
]);
|
|
857
|
+
assert.equal(complete.run.state.current_step, "merge-ready");
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test("initial gate boundary rejects pre-run and far-future claims", async () => {
|
|
861
|
+
for (const [name, timestamp] of [
|
|
862
|
+
["pre-run", NOW],
|
|
863
|
+
["far-future", new Date(Date.now() + 60 * 60_000).toISOString()],
|
|
864
|
+
]) {
|
|
865
|
+
const session = makeSession(`freshness-${name}`);
|
|
866
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
867
|
+
const result = await writeAndSync(session, [bundleClaim({
|
|
868
|
+
expectation: "selected-work",
|
|
869
|
+
claimType: "builder.pull-work.selected",
|
|
870
|
+
subjectType: "work-item",
|
|
871
|
+
timestamp,
|
|
872
|
+
})]);
|
|
873
|
+
assert.equal(result.attached, false, name);
|
|
874
|
+
assert.equal(result.run.state.current_step, "pull-work", name);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("prior claim identity is rejected after re-entry and a new identity can recover", async () => {
|
|
879
|
+
const session = makeSession("same-digest-reentry");
|
|
880
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
881
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
882
|
+
await writeAndSync(session, [
|
|
883
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
884
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
885
|
+
]);
|
|
886
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
887
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
888
|
+
const future = new Date(Date.now() + 10_000).toISOString();
|
|
889
|
+
const failure = [bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step", status: "fail", routeReason: "implementation_defect", timestamp: future }), ...verifiedTestsPrerequisites(session, future)];
|
|
890
|
+
const first = await writeAndSync(session, failure);
|
|
891
|
+
assert.equal(first.run.state.current_step, "execute");
|
|
892
|
+
await writeAndSync(session, [withIdentitySuffix(bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change", timestamp: future }), "reentry")]);
|
|
893
|
+
const replay = await writeAndSync(session, failure);
|
|
894
|
+
assert.equal(replay.attached, false);
|
|
895
|
+
assert.equal(replay.run.state.current_step, "verify");
|
|
896
|
+
assert.equal(replay.run.state.transitions.filter((transition) => transition.type === "route_back").length, 1);
|
|
897
|
+
const newIdentity = failure.map((entry, index) => withIdentitySuffix(entry, `visit-2-${index}`));
|
|
898
|
+
const second = await writeAndSync(session, newIdentity);
|
|
899
|
+
assert.equal(second.run.state.current_step, "execute");
|
|
900
|
+
const verifyEvidence = second.run.manifest.evidence.filter((entry) => entry.gate_id === "verify-gate");
|
|
901
|
+
assert.equal(verifyEvidence.length, 2);
|
|
902
|
+
assert.equal(verifyEvidence[0].superseded_by, verifyEvidence[1].id);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test("legacy colon-bearing assignment actor releases only through its normalized equivalent", () => {
|
|
906
|
+
const session = makeSession("legacy-actor-release");
|
|
907
|
+
const legacyActor = { runtime: "unknown", session_id: "legacy-session", host: "Kontour", human: null };
|
|
908
|
+
performLocalClaim(session.artifactRoot, session.slug, legacyActor, {
|
|
909
|
+
ttlSeconds: 1800,
|
|
910
|
+
actorKey: "unknown:legacy-session:Kontour",
|
|
911
|
+
branch: `agent/${session.slug}`,
|
|
912
|
+
artifactDir: session.sessionDir,
|
|
913
|
+
workItemRef: SUBJECT,
|
|
914
|
+
reason: "legacy fixture",
|
|
915
|
+
});
|
|
916
|
+
assert.throws(() => performLocalRelease(session.artifactRoot, session.slug, legacyActor, {
|
|
917
|
+
actorKey: "different-actor",
|
|
918
|
+
}), /refusing to release/);
|
|
919
|
+
assert.throws(() => performLocalRelease(session.artifactRoot, session.slug, { ...legacyActor, session_id: "different" }, {
|
|
920
|
+
actorKey: "unknownlegacy-sessionKontour",
|
|
921
|
+
}), /refusing to release/);
|
|
922
|
+
const released = performLocalRelease(session.artifactRoot, session.slug, legacyActor, {
|
|
923
|
+
actorKey: "unknownlegacy-sessionKontour",
|
|
924
|
+
});
|
|
925
|
+
assert.equal(released.status, "released");
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test("string-only legacy liveness identity cannot be released by a colliding modern actor", async () => {
|
|
929
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-legacy-liveness-"));
|
|
930
|
+
const eventsFile = path.join(root, "liveness", "events.jsonl");
|
|
931
|
+
fs.mkdirSync(path.dirname(eventsFile), { recursive: true });
|
|
932
|
+
fs.writeFileSync(eventsFile, `${JSON.stringify({
|
|
933
|
+
type: "claim",
|
|
934
|
+
subjectId: "legacy-subject",
|
|
935
|
+
actor: "unknown:anc-123456789abc:Kontour",
|
|
936
|
+
at: NOW,
|
|
937
|
+
ttlSeconds: 1800,
|
|
938
|
+
})}\n`);
|
|
939
|
+
|
|
940
|
+
await assert.rejects(() => workflowSidecarMain([
|
|
941
|
+
"liveness", "release", "legacy-subject",
|
|
942
|
+
"--actor", "unknownanc-123456789abcKontour",
|
|
943
|
+
"--artifact-root", root,
|
|
944
|
+
"--at", "2026-07-09T20:01:00.000Z",
|
|
945
|
+
]), /prior claim/);
|
|
946
|
+
const events = fs.readFileSync(eventsFile, "utf8").trim().split("\n").map(JSON.parse);
|
|
947
|
+
assert.equal(events.length, 1);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("lossy modern liveness key collision cannot release a non-reversible legacy actor", async () => {
|
|
951
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-lossy-liveness-"));
|
|
952
|
+
const eventsFile = path.join(root, "liveness", "events.jsonl");
|
|
953
|
+
fs.mkdirSync(path.dirname(eventsFile), { recursive: true });
|
|
954
|
+
fs.writeFileSync(eventsFile, `${JSON.stringify({ type: "claim", subjectId: "collision", actor: "a:b", at: NOW, ttlSeconds: 1800 })}\n`);
|
|
955
|
+
await assert.rejects(() => workflowSidecarMain([
|
|
956
|
+
"liveness", "release", "collision", "--actor", "ab", "--artifact-root", root,
|
|
957
|
+
]), /prior claim/);
|
|
958
|
+
assert.equal(fs.readFileSync(eventsFile, "utf8").trim().split("\n").length, 1);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
test("upgrade rebuild preserves legacy critique identity until a versioned new review", async () => {
|
|
962
|
+
const legacy = {
|
|
963
|
+
id: "upgrade-review",
|
|
964
|
+
reviewer: "reviewer",
|
|
965
|
+
reviewed_at: "2026-07-01T00:00:00Z",
|
|
966
|
+
verdict: "pass",
|
|
967
|
+
summary: "Legacy clean review",
|
|
968
|
+
findings: [],
|
|
969
|
+
lanes: [{ id: "code", status: "pass" }],
|
|
970
|
+
review_target: { artifacts: [] },
|
|
971
|
+
artifact_refs: [],
|
|
972
|
+
};
|
|
973
|
+
const beforeUpgrade = await buildTrustBundle("upgrade-fixture", "2026-07-01T00:00:00Z", [], [], [legacy]);
|
|
974
|
+
const rebuiltAfterUpgrade = await buildTrustBundle("upgrade-fixture", "2026-07-12T00:00:00Z", [], [], [legacy]);
|
|
975
|
+
const newReview = await buildTrustBundle("upgrade-fixture", "2026-07-12T00:01:00Z", [], [], [{
|
|
976
|
+
...legacy,
|
|
977
|
+
reviewed_at: "2026-07-12T00:01:00Z",
|
|
978
|
+
identity_version: 2,
|
|
979
|
+
}]);
|
|
980
|
+
assert.equal(beforeUpgrade.claims[0].id, rebuiltAfterUpgrade.claims[0].id);
|
|
981
|
+
assert.notEqual(newReview.claims[0].id, beforeUpgrade.claims[0].id);
|
|
982
|
+
assert.equal(rebuiltAfterUpgrade.claims[0].metadata.identity_version, undefined);
|
|
983
|
+
assert.equal(newReview.claims[0].metadata.identity_version, 2);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test("passing tests-evidence rejects a critique whose reviewed artifact changed", async () => {
|
|
987
|
+
const session = makeSession("stale-critique-artifact");
|
|
988
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
989
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
990
|
+
await writeAndSync(session, [
|
|
991
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
992
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
993
|
+
]);
|
|
994
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
995
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
996
|
+
const prerequisites = verifiedTestsPrerequisites(session);
|
|
997
|
+
fs.writeFileSync(path.join(session.projectRoot, "review-target", "delivery.md"), "changed after review\n");
|
|
998
|
+
await assert.rejects(
|
|
999
|
+
() => writeAndSync(session, [bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" }), ...prerequisites]),
|
|
1000
|
+
/review_target\.artifacts\.sha256.*does not match/,
|
|
1001
|
+
);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("passing tests-evidence rejects a successful command that executed zero tests", async () => {
|
|
1005
|
+
const session = makeSession("zero-test-evidence");
|
|
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
|
+
await assert.rejects(
|
|
1015
|
+
() => writeAndSync(session, [
|
|
1016
|
+
bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step", testCount: 0 }),
|
|
1017
|
+
...verifiedTestsPrerequisites(session),
|
|
1018
|
+
]),
|
|
1019
|
+
/positive executed-test count/,
|
|
1020
|
+
);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
test("passing tests-evidence rejects a critique after implementation source changed", async () => {
|
|
1024
|
+
const session = makeSession("stale-critique-workspace");
|
|
1025
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
1026
|
+
await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
|
|
1027
|
+
await writeAndSync(session, [
|
|
1028
|
+
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
1029
|
+
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
1030
|
+
]);
|
|
1031
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
|
|
1032
|
+
await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
|
|
1033
|
+
const prerequisites = verifiedTestsPrerequisites(session);
|
|
1034
|
+
fs.writeFileSync(path.join(session.projectRoot, "review-target", "implementation.txt"), "source changed after review\n");
|
|
1035
|
+
await assert.rejects(
|
|
1036
|
+
() => writeAndSync(session, [bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" }), ...prerequisites]),
|
|
1037
|
+
/review_target\.workspace_snapshot\.digest.*does not match/,
|
|
1038
|
+
);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test("workspace review fails closed when a Git worktree marker cannot be inspected", () => {
|
|
1042
|
+
const session = makeSession("git-inspection-unavailable");
|
|
1043
|
+
fs.mkdirSync(path.join(session.projectRoot, ".git"));
|
|
1044
|
+
assert.throws(
|
|
1045
|
+
() => captureReviewWorkspaceSnapshot(session.projectRoot, [{ file: "review-target/delivery.md", sha256: createHash("sha256").update("reviewed delivery\n").digest("hex") }]),
|
|
1046
|
+
/could not inspect the Git worktree/,
|
|
1047
|
+
);
|
|
647
1048
|
});
|
|
648
1049
|
|
|
649
1050
|
test("verified sidecar claims drive the composed publish and learning prefix to completion", async () => {
|
|
@@ -651,25 +1052,30 @@ test("verified sidecar claims drive the composed publish and learning prefix to
|
|
|
651
1052
|
claimSessionAssignment(session);
|
|
652
1053
|
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
653
1054
|
const steps = [
|
|
654
|
-
[bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })],
|
|
655
|
-
[
|
|
1055
|
+
() => [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })],
|
|
1056
|
+
() => [
|
|
656
1057
|
bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
|
|
657
1058
|
bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
|
|
658
1059
|
],
|
|
659
|
-
[bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })],
|
|
660
|
-
[bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })],
|
|
661
|
-
[bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" })],
|
|
662
|
-
[bundleClaim({ expectation: "merge-readiness", claimType: "builder.merge-ready.readiness", subjectType: "change" })],
|
|
1060
|
+
() => [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })],
|
|
1061
|
+
() => [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })],
|
|
1062
|
+
() => [bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" }), ...verifiedTestsPrerequisites(session)],
|
|
1063
|
+
() => [bundleClaim({ expectation: "merge-readiness", claimType: "builder.merge-ready.readiness", subjectType: "change" })],
|
|
663
1064
|
];
|
|
664
|
-
for (const entries of steps) await writeAndSync(session, entries);
|
|
1065
|
+
for (const entries of steps) await writeAndSync(session, entries());
|
|
665
1066
|
|
|
666
1067
|
const prOpen = readJson(path.join(session.sessionDir, "state.json"));
|
|
667
1068
|
assert.equal(prOpen.flow_run.current_step, "pr-open");
|
|
1069
|
+
assert.equal(readJson(path.join(session.artifactRoot, "current.json")).active_step_id, "pr-open");
|
|
668
1070
|
assert.deepEqual(prOpen.next_action.skills, []);
|
|
669
1071
|
assert.deepEqual(prOpen.next_action.operations, ["publish-change"]);
|
|
670
1072
|
|
|
671
|
-
await writeAndSync(session, [bundleClaim({ expectation: "pull-request-opened", claimType: "builder.pr-open.pull-request", subjectType: "pull-request" })]);
|
|
672
|
-
|
|
1073
|
+
const mergeReadyCi = await writeAndSync(session, [bundleClaim({ expectation: "pull-request-opened", claimType: "builder.pr-open.pull-request", subjectType: "pull-request" })]);
|
|
1074
|
+
assert.equal(mergeReadyCi.run.state.current_step, "merge-ready-ci");
|
|
1075
|
+
assert.equal(readJson(path.join(session.artifactRoot, "current.json")).active_step_id, "merge-ready-ci");
|
|
1076
|
+
const learn = await writeAndSync(session, [bundleClaim({ expectation: "ci-merge-readiness", claimType: "builder.merge-ready-ci.readiness", subjectType: "pull-request" })]);
|
|
1077
|
+
assert.equal(learn.run.state.current_step, "learn");
|
|
1078
|
+
assert.equal(readJson(path.join(session.artifactRoot, "current.json")).active_step_id, "learn");
|
|
673
1079
|
const completed = await writeAndSync(session, [
|
|
674
1080
|
bundleClaim({ expectation: "decision-evidence", claimType: "builder.learn.decisions", subjectType: "decision" }),
|
|
675
1081
|
bundleClaim({ expectation: "learning-evidence", claimType: "builder.learn.evidence", subjectType: "release" }),
|
|
@@ -679,6 +1085,18 @@ test("verified sidecar claims drive the composed publish and learning prefix to
|
|
|
679
1085
|
assert.equal(completed.run.state.status, "completed");
|
|
680
1086
|
assert.equal(completed.projection.status, "delivered");
|
|
681
1087
|
assert.deepEqual(completed.projection.next_action, { status: "done", summary: "Canonical Flow run is complete." });
|
|
1088
|
+
const liveEvidence = completed.run.manifest.evidence.filter((entry) => !entry.superseded_by);
|
|
1089
|
+
assert.deepEqual(liveEvidence.map((entry) => entry.expectation_ids), [
|
|
1090
|
+
["selected-work"],
|
|
1091
|
+
["pickup-probe-readiness", "probe-decisions-or-accepted-gaps"],
|
|
1092
|
+
["implementation-plan"],
|
|
1093
|
+
["implementation-scope"],
|
|
1094
|
+
["clean-critique", "acceptance-criteria", "tests-evidence"],
|
|
1095
|
+
["merge-readiness"],
|
|
1096
|
+
["pull-request-opened"],
|
|
1097
|
+
["ci-merge-readiness"],
|
|
1098
|
+
["decision-evidence", "learning-evidence"],
|
|
1099
|
+
]);
|
|
682
1100
|
|
|
683
1101
|
const flowDirectory = runDir(session.slug, session.projectRoot);
|
|
684
1102
|
const beforeFlow = snapshotTree(flowDirectory);
|
package/src/cli/builder-run.ts
CHANGED
|
@@ -6,11 +6,9 @@ import {
|
|
|
6
6
|
recoverBuilderFlowSession,
|
|
7
7
|
releaseBuilderFlowAssignment,
|
|
8
8
|
resumeBuilderFlowSession,
|
|
9
|
-
startBuilderFlowSession,
|
|
10
|
-
syncBuilderFlowSession,
|
|
11
9
|
} from "../builder-flow-runtime.js";
|
|
12
10
|
|
|
13
|
-
const USAGE = "Usage: flow-agents builder-run <
|
|
11
|
+
const USAGE = "Usage: flow-agents builder-run <recover|pause|resume|cancel|release-assignment|archive> --session-dir <path> [--reason <text> | --authorization-file <path>]";
|
|
14
12
|
|
|
15
13
|
export async function main(argv: string[]): Promise<number> {
|
|
16
14
|
const parsed = parseArgs(argv);
|
|
@@ -29,7 +27,7 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
29
27
|
console.error("builder-run requires --session-dir .kontourai/flow-agents/<slug>");
|
|
30
28
|
return 64;
|
|
31
29
|
}
|
|
32
|
-
if (!action || !["
|
|
30
|
+
if (!action || !["recover", "pause", "resume", "cancel", "release-assignment", "archive"].includes(action)) {
|
|
33
31
|
console.error(USAGE);
|
|
34
32
|
return 64;
|
|
35
33
|
}
|
|
@@ -53,11 +51,7 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
53
51
|
console.error(USAGE);
|
|
54
52
|
return 64;
|
|
55
53
|
}
|
|
56
|
-
const result = action === "
|
|
57
|
-
? await startBuilderFlowSession({ sessionDir })
|
|
58
|
-
: action === "sync"
|
|
59
|
-
? await syncBuilderFlowSession({ sessionDir })
|
|
60
|
-
: action === "recover"
|
|
54
|
+
const result = action === "recover"
|
|
61
55
|
? await recoverBuilderFlowSession({ sessionDir })
|
|
62
56
|
: action === "pause"
|
|
63
57
|
? await pauseBuilderFlowSession({ sessionDir, reason: reason! })
|