@kontourai/flow-agents 3.5.0 → 3.6.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/.github/workflows/ci.yml +4 -0
- package/CHANGELOG.md +8 -0
- package/build/src/builder-flow-run-adapter.d.ts +10 -1
- package/build/src/builder-flow-run-adapter.js +29 -1
- package/build/src/builder-flow-runtime.d.ts +18 -0
- package/build/src/builder-flow-runtime.js +205 -13
- package/build/src/builder-lifecycle-authority.d.ts +35 -0
- package/build/src/builder-lifecycle-authority.js +219 -0
- package/build/src/cli/assignment-provider.d.ts +10 -0
- package/build/src/cli/assignment-provider.js +61 -52
- package/build/src/cli/builder-run.js +46 -5
- package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
- package/build/src/cli/workflow-sidecar.d.ts +3 -0
- package/build/src/cli/workflow-sidecar.js +28 -6
- package/build/src/cli/workflow.d.ts +2 -0
- package/build/src/cli/workflow.js +521 -0
- package/build/src/cli.js +2 -0
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +2 -0
- package/build/src/lib/package-version.d.ts +2 -0
- package/build/src/lib/package-version.js +13 -0
- package/build/src/lib/pinned-cli-command.d.ts +6 -0
- package/build/src/lib/pinned-cli-command.js +21 -0
- package/context/contracts/artifact-contract.md +1 -1
- package/context/scripts/hooks/config-protection.js +8 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/context/scripts/hooks/stop-goal-fit.js +1 -1
- package/docs/context-map.md +2 -0
- package/docs/public-workflow-cli.md +63 -0
- package/docs/spec/builder-flow-runtime.md +37 -0
- package/docs/workflow-usage-guide.md +5 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -4
- package/evals/integration/test_bundle_install.sh +59 -5
- package/evals/integration/test_public_workflow_cli.sh +259 -0
- package/package.json +2 -2
- package/schemas/builder-lifecycle-authorization.schema.json +57 -0
- package/schemas/lifecycle-authority-keys.schema.json +25 -0
- package/schemas/workflow-state.schema.json +1 -1
- package/scripts/hooks/config-protection.js +8 -1
- package/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/scripts/hooks/stop-goal-fit.js +1 -1
- package/src/builder-flow-run-adapter.ts +47 -0
- package/src/builder-flow-runtime.ts +216 -4
- package/src/builder-lifecycle-authority.ts +218 -0
- package/src/cli/assignment-provider.ts +29 -9
- package/src/cli/builder-flow-runtime.test.mjs +404 -1
- package/src/cli/builder-run.ts +56 -5
- package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
- package/src/cli/workflow-sidecar.ts +28 -6
- package/src/cli/workflow.ts +471 -0
- package/src/cli.ts +2 -0
- package/src/index.ts +14 -0
- package/src/lib/package-version.ts +15 -0
- package/src/lib/pinned-cli-command.ts +23 -0
|
@@ -3,17 +3,31 @@ 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
7
|
|
|
7
8
|
import { FLOW_RUN_EVIDENCE_MANIFEST_PATH, runDir } from "@kontourai/flow";
|
|
8
9
|
import {
|
|
10
|
+
archiveBuilderFlowSession,
|
|
11
|
+
cancelBuilderFlowSession,
|
|
12
|
+
pauseBuilderFlowSession,
|
|
9
13
|
recoverBuilderFlowSession,
|
|
14
|
+
releaseBuilderFlowAssignment,
|
|
15
|
+
resumeBuilderFlowSession,
|
|
10
16
|
startBuilderFlowSession,
|
|
11
17
|
syncBuilderFlowSession,
|
|
12
18
|
} from "../../build/src/builder-flow-runtime.js";
|
|
19
|
+
import { builderLifecycleAuthorizationPayload, loadBuilderLifecycleAuthorization, recordAuthorizationConsumed } from "../../build/src/builder-lifecycle-authority.js";
|
|
20
|
+
import { cancelBuilderBuildRun } from "../../build/src/builder-flow-run-adapter.js";
|
|
21
|
+
import { performLocalClaim, readLocalAssignmentStatus, resolveCurrentAssignmentActor } from "../../build/src/cli/assignment-provider.js";
|
|
13
22
|
import { main as builderRunMain } from "../../build/src/cli/builder-run.js";
|
|
14
23
|
|
|
15
24
|
const SUBJECT = "local:work-item/runtime-projection";
|
|
16
25
|
const NOW = "2026-07-09T20:00:00.000Z";
|
|
26
|
+
const PACKAGE_VERSION = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
27
|
+
const ACTOR = { runtime: "codex", session_id: "runtime-projection", host: "test-host", human: null };
|
|
28
|
+
const ACTOR_KEY = "codex:runtime-projection:test-host";
|
|
29
|
+
const AUTHORITY_KEY_ID = "runtime-test";
|
|
30
|
+
const AUTHORITY_KEYS = generateKeyPairSync("ed25519");
|
|
17
31
|
|
|
18
32
|
function makeSession(slug = "runtime-projection") {
|
|
19
33
|
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-builder-runtime-"));
|
|
@@ -33,6 +47,10 @@ function makeSession(slug = "runtime-projection") {
|
|
|
33
47
|
artifact_dir: `.kontourai/flow-agents/${slug}`,
|
|
34
48
|
updated_at: NOW,
|
|
35
49
|
});
|
|
50
|
+
writeJson(path.join(projectRoot, ".flow-agents", "lifecycle-authority-keys.json"), {
|
|
51
|
+
schema_version: "1.0",
|
|
52
|
+
keys: [{ id: AUTHORITY_KEY_ID, algorithm: "ed25519", public_key_pem: AUTHORITY_KEYS.publicKey.export({ type: "spki", format: "pem" }) }],
|
|
53
|
+
});
|
|
36
54
|
return { projectRoot, artifactRoot, sessionDir, slug };
|
|
37
55
|
}
|
|
38
56
|
|
|
@@ -45,6 +63,104 @@ function readJson(file) {
|
|
|
45
63
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
46
64
|
}
|
|
47
65
|
|
|
66
|
+
function consumedAuthorizationRecords(session) {
|
|
67
|
+
const directory = path.join(session.artifactRoot, "lifecycle-authority", "consumed");
|
|
68
|
+
if (!fs.existsSync(directory)) return [];
|
|
69
|
+
return fs.readdirSync(directory).sort().map((name) => readJson(path.join(directory, name)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function claimSessionAssignment(session) {
|
|
73
|
+
performLocalClaim(session.artifactRoot, session.slug, ACTOR, {
|
|
74
|
+
ttlSeconds: 1800,
|
|
75
|
+
actorKey: ACTOR_KEY,
|
|
76
|
+
branch: `agent/${session.slug}`,
|
|
77
|
+
artifactDir: session.sessionDir,
|
|
78
|
+
workItemRef: SUBJECT,
|
|
79
|
+
reason: "test",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function claimAmbientSessionAssignment(session) {
|
|
84
|
+
const ambient = resolveCurrentAssignmentActor();
|
|
85
|
+
performLocalClaim(session.artifactRoot, session.slug, ambient.actor, {
|
|
86
|
+
ttlSeconds: 1800,
|
|
87
|
+
actorKey: ambient.actorKey,
|
|
88
|
+
branch: `agent/${session.slug}`,
|
|
89
|
+
artifactDir: session.sessionDir,
|
|
90
|
+
workItemRef: SUBJECT,
|
|
91
|
+
reason: "test",
|
|
92
|
+
});
|
|
93
|
+
return ambient;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function lifecycleAuthorization(session, operation, name, overrides = {}) {
|
|
97
|
+
const file = path.join(session.projectRoot, `${name}.authorization.json`);
|
|
98
|
+
const unsigned = {
|
|
99
|
+
schema_version: "1.0",
|
|
100
|
+
operation,
|
|
101
|
+
run_id: session.slug,
|
|
102
|
+
subject: SUBJECT,
|
|
103
|
+
assignment_actor_key: ACTOR_KEY,
|
|
104
|
+
assignment_actor: ACTOR,
|
|
105
|
+
nonce: `${session.slug}:${name}`,
|
|
106
|
+
expires_at: "2026-07-09T21:00:00.000Z",
|
|
107
|
+
request: {
|
|
108
|
+
reason: `${name} requested by fixture`,
|
|
109
|
+
authority: {
|
|
110
|
+
kind: "user_request",
|
|
111
|
+
actor: "fixture-user",
|
|
112
|
+
request_ref: `fixture://request/${name}`,
|
|
113
|
+
requested_at: NOW,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
...overrides,
|
|
117
|
+
};
|
|
118
|
+
const value = {
|
|
119
|
+
...unsigned,
|
|
120
|
+
signature: {
|
|
121
|
+
algorithm: "ed25519",
|
|
122
|
+
key_id: AUTHORITY_KEY_ID,
|
|
123
|
+
value: sign(null, Buffer.from(builderLifecycleAuthorizationPayload(unsigned)), AUTHORITY_KEYS.privateKey).toString("base64"),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
writeJson(file, value);
|
|
127
|
+
return file;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function liveLifecycleAuthorization(session, operation, name, overrides = {}) {
|
|
131
|
+
const requestedAt = new Date();
|
|
132
|
+
return lifecycleAuthorization(session, operation, name, {
|
|
133
|
+
expires_at: new Date(requestedAt.getTime() + 60 * 60_000).toISOString(),
|
|
134
|
+
request: {
|
|
135
|
+
reason: `${name} requested by fixture`,
|
|
136
|
+
authority: {
|
|
137
|
+
kind: "user_request",
|
|
138
|
+
actor: "fixture-user",
|
|
139
|
+
request_ref: `fixture://request/${name}`,
|
|
140
|
+
requested_at: requestedAt.toISOString(),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
...overrides,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function expiredLifecycleAuthorization(session, operation, name, overrides = {}) {
|
|
148
|
+
const requestedAt = new Date(Date.now() - 120_000);
|
|
149
|
+
return lifecycleAuthorization(session, operation, name, {
|
|
150
|
+
expires_at: new Date(Date.now() - 60_000).toISOString(),
|
|
151
|
+
request: {
|
|
152
|
+
reason: `${name} requested by fixture`,
|
|
153
|
+
authority: {
|
|
154
|
+
kind: "user_request",
|
|
155
|
+
actor: "fixture-user",
|
|
156
|
+
request_ref: `fixture://request/${name}`,
|
|
157
|
+
requested_at: requestedAt.toISOString(),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
...overrides,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
48
164
|
function snapshotFile(file) {
|
|
49
165
|
return fs.existsSync(file) ? fs.readFileSync(file).toString("base64") : null;
|
|
50
166
|
}
|
|
@@ -152,7 +268,10 @@ test("small-model client can start and advance from projected actions without ch
|
|
|
152
268
|
assert.equal(started.run.state.current_step, "pull-work");
|
|
153
269
|
assert.deepEqual(started.projection.next_action.skills, ["pull-work"]);
|
|
154
270
|
assert.deepEqual(started.projection.next_action.operations, []);
|
|
155
|
-
assert.
|
|
271
|
+
assert.match(started.projection.next_action.command, /^sh -c /);
|
|
272
|
+
assert.match(started.projection.next_action.command, /--prefix "\$root"/);
|
|
273
|
+
assert.ok(started.projection.next_action.command.includes(`'@kontourai/flow-agents@${PACKAGE_VERSION}'`));
|
|
274
|
+
assert.ok(started.projection.next_action.command.includes(`'workflow' 'status' '--session-dir' '.kontourai/flow-agents/${session.slug}' '--json'`));
|
|
156
275
|
assert.ok(fs.existsSync(runDir(session.slug, session.projectRoot)));
|
|
157
276
|
assert.ok(!fs.existsSync(path.join(session.projectRoot, ".flow", "runs")), "retired runtime path must not be created");
|
|
158
277
|
|
|
@@ -172,6 +291,282 @@ test("small-model client can start and advance from projected actions without ch
|
|
|
172
291
|
assert.equal(duplicate.run.manifest.evidence.length, advanced.run.manifest.evidence.length);
|
|
173
292
|
});
|
|
174
293
|
|
|
294
|
+
test("pause and resume preserve the current Flow step and active assignment", async () => {
|
|
295
|
+
const session = makeSession("lifecycle-pause-resume");
|
|
296
|
+
claimAmbientSessionAssignment(session);
|
|
297
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
298
|
+
const before = readJson(path.join(runDir(session.slug, session.projectRoot), "state.json"));
|
|
299
|
+
|
|
300
|
+
const paused = await pauseBuilderFlowSession({
|
|
301
|
+
sessionDir: session.sessionDir,
|
|
302
|
+
reason: "fixture pause",
|
|
303
|
+
});
|
|
304
|
+
assert.equal(paused.run.state.status, "paused");
|
|
305
|
+
assert.equal(paused.run.state.current_step, before.current_step);
|
|
306
|
+
assert.deepEqual(paused.run.state.transitions, before.transitions);
|
|
307
|
+
assert.equal(paused.projection.status, "blocked");
|
|
308
|
+
assert.equal(readLocalAssignmentStatus(session.artifactRoot, session.slug).record.status, "claimed");
|
|
309
|
+
|
|
310
|
+
const resumed = await resumeBuilderFlowSession({
|
|
311
|
+
sessionDir: session.sessionDir,
|
|
312
|
+
reason: "fixture resume",
|
|
313
|
+
});
|
|
314
|
+
assert.equal(resumed.run.state.status, "active");
|
|
315
|
+
assert.equal(resumed.run.state.current_step, before.current_step);
|
|
316
|
+
assert.deepEqual(resumed.run.state.transitions, before.transitions);
|
|
317
|
+
assert.equal(readLocalAssignmentStatus(session.artifactRoot, session.slug).record.status, "claimed");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("authorized cancellation is canonical, terminal, and releases the assignment exactly once", async () => {
|
|
321
|
+
const session = makeSession("lifecycle-cancel");
|
|
322
|
+
claimSessionAssignment(session);
|
|
323
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
324
|
+
const authorizationFile = liveLifecycleAuthorization(session, "cancel", "cancel");
|
|
325
|
+
|
|
326
|
+
const canceled = await cancelBuilderFlowSession({
|
|
327
|
+
sessionDir: session.sessionDir,
|
|
328
|
+
authorizationFile,
|
|
329
|
+
});
|
|
330
|
+
assert.equal(canceled.run.state.status, "canceled");
|
|
331
|
+
assert.equal(canceled.projection.status, "canceled");
|
|
332
|
+
assert.equal(canceled.assignmentReleased, true);
|
|
333
|
+
assert.equal(canceled.idempotent, false);
|
|
334
|
+
const assignmentFile = path.join(session.artifactRoot, "assignment", `${session.slug}.json`);
|
|
335
|
+
assert.equal(readJson(assignmentFile).status, "released");
|
|
336
|
+
const firstAudit = readJson(assignmentFile).audit_trail;
|
|
337
|
+
|
|
338
|
+
await assert.rejects(() => cancelBuilderFlowSession({
|
|
339
|
+
sessionDir: session.sessionDir,
|
|
340
|
+
authorizationFile,
|
|
341
|
+
}), /nonce has already been consumed/);
|
|
342
|
+
assert.deepEqual(readJson(assignmentFile).audit_trail, firstAudit);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("assignment release is independent of Flow lifecycle", async () => {
|
|
346
|
+
const session = makeSession("lifecycle-release-assignment");
|
|
347
|
+
claimAmbientSessionAssignment(session);
|
|
348
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
349
|
+
const beforeFlow = snapshotTree(runDir(session.slug, session.projectRoot));
|
|
350
|
+
|
|
351
|
+
const released = await releaseBuilderFlowAssignment({
|
|
352
|
+
sessionDir: session.sessionDir,
|
|
353
|
+
reason: "fixture assignment release",
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
assert.equal(released.assignmentReleased, true);
|
|
357
|
+
assert.deepEqual(snapshotTree(runDir(session.slug, session.projectRoot)), beforeFlow);
|
|
358
|
+
assert.equal(readJson(path.join(session.artifactRoot, "assignment", `${session.slug}.json`)).status, "released");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("archive rejects active runs and retains canceled Flow and session artifacts", async () => {
|
|
362
|
+
const session = makeSession("lifecycle-archive");
|
|
363
|
+
claimSessionAssignment(session);
|
|
364
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
365
|
+
const cancelAuthorization = liveLifecycleAuthorization(session, "cancel", "archive-cancel");
|
|
366
|
+
const authorizationFile = liveLifecycleAuthorization(session, "archive", "archive");
|
|
367
|
+
const beforeReject = snapshotTree(session.sessionDir);
|
|
368
|
+
await assert.rejects(() => archiveBuilderFlowSession({
|
|
369
|
+
sessionDir: session.sessionDir,
|
|
370
|
+
authorizationFile,
|
|
371
|
+
}), /must be completed or canceled/);
|
|
372
|
+
assert.deepEqual(snapshotTree(session.sessionDir), beforeReject);
|
|
373
|
+
|
|
374
|
+
await cancelBuilderFlowSession({
|
|
375
|
+
sessionDir: session.sessionDir,
|
|
376
|
+
authorizationFile: cancelAuthorization,
|
|
377
|
+
});
|
|
378
|
+
const beforeFlow = snapshotTree(runDir(session.slug, session.projectRoot));
|
|
379
|
+
const beforeSession = snapshotTree(session.sessionDir);
|
|
380
|
+
const archived = await archiveBuilderFlowSession({
|
|
381
|
+
sessionDir: session.sessionDir,
|
|
382
|
+
authorizationFile,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
assert.equal(archived.archiveDir, path.join(session.artifactRoot, "archive", session.slug));
|
|
386
|
+
assert.equal(fs.existsSync(session.sessionDir), false);
|
|
387
|
+
assert.equal(readJson(path.join(archived.archiveDir, "state.json")).status, "archived");
|
|
388
|
+
assert.deepEqual(snapshotTree(runDir(session.slug, session.projectRoot)), beforeFlow);
|
|
389
|
+
const archivedFiles = snapshotTree(archived.archiveDir).map(([name]) => name);
|
|
390
|
+
for (const [name] of beforeSession) assert.ok(archivedFiles.includes(name), `archive retained ${name}`);
|
|
391
|
+
assert.deepEqual(consumedAuthorizationRecords(session).map((record) => record.operation).sort(), ["archive", "cancel"]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("archive rejects a symlinked archive root without moving the session", async () => {
|
|
395
|
+
const session = makeSession("lifecycle-archive-symlink");
|
|
396
|
+
claimSessionAssignment(session);
|
|
397
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
398
|
+
const cancelAuthorization = liveLifecycleAuthorization(session, "cancel", "archive-symlink-cancel");
|
|
399
|
+
const authorizationFile = liveLifecycleAuthorization(session, "archive", "archive-symlink");
|
|
400
|
+
await cancelBuilderFlowSession({
|
|
401
|
+
sessionDir: session.sessionDir,
|
|
402
|
+
authorizationFile: cancelAuthorization,
|
|
403
|
+
});
|
|
404
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-archive-outside-"));
|
|
405
|
+
fs.symlinkSync(outside, path.join(session.artifactRoot, "archive"), "dir");
|
|
406
|
+
const beforeSession = snapshotTree(session.sessionDir);
|
|
407
|
+
|
|
408
|
+
await assert.rejects(() => archiveBuilderFlowSession({
|
|
409
|
+
sessionDir: session.sessionDir,
|
|
410
|
+
authorizationFile,
|
|
411
|
+
}), /archive root.*symbolic link/);
|
|
412
|
+
|
|
413
|
+
assert.deepEqual(snapshotTree(session.sessionDir), beforeSession);
|
|
414
|
+
assert.deepEqual(fs.readdirSync(outside), []);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("archive retries the exact consumed authorization after an interrupted prepared move", async () => {
|
|
418
|
+
const session = makeSession("lifecycle-archive-recovery");
|
|
419
|
+
claimSessionAssignment(session);
|
|
420
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
421
|
+
await cancelBuilderFlowSession({
|
|
422
|
+
sessionDir: session.sessionDir,
|
|
423
|
+
authorizationFile: liveLifecycleAuthorization(session, "cancel", "archive-recovery-cancel"),
|
|
424
|
+
});
|
|
425
|
+
const authorizationFile = expiredLifecycleAuthorization(session, "archive", "archive-recovery");
|
|
426
|
+
const rawAuthorization = readJson(authorizationFile);
|
|
427
|
+
const authorization = loadBuilderLifecycleAuthorization(authorizationFile, {
|
|
428
|
+
projectRoot: session.projectRoot,
|
|
429
|
+
operation: "archive",
|
|
430
|
+
runId: session.slug,
|
|
431
|
+
subject: SUBJECT,
|
|
432
|
+
actorKey: ACTOR_KEY,
|
|
433
|
+
now: new Date(Date.parse(rawAuthorization.request.authority.requested_at) + 30_000).toISOString(),
|
|
434
|
+
});
|
|
435
|
+
const preparedState = readJson(path.join(session.sessionDir, "state.json"));
|
|
436
|
+
preparedState.status = "archived";
|
|
437
|
+
preparedState.phase = "done";
|
|
438
|
+
preparedState.next_action = { status: "done", summary: "Builder session archived; canonical Flow artifacts remain retained." };
|
|
439
|
+
writeJson(path.join(session.sessionDir, "state.json"), preparedState);
|
|
440
|
+
recordAuthorizationConsumed(session.artifactRoot, authorization);
|
|
441
|
+
|
|
442
|
+
const conflictingAuthorization = liveLifecycleAuthorization(session, "archive", "archive-recovery-conflict", {
|
|
443
|
+
nonce: authorization.nonce,
|
|
444
|
+
});
|
|
445
|
+
await assert.rejects(
|
|
446
|
+
() => archiveBuilderFlowSession({ sessionDir: session.sessionDir, authorizationFile: conflictingAuthorization }),
|
|
447
|
+
/does not match its integrity key/,
|
|
448
|
+
);
|
|
449
|
+
assert.equal(fs.existsSync(session.sessionDir), true);
|
|
450
|
+
|
|
451
|
+
const recovered = await archiveBuilderFlowSession({ sessionDir: session.sessionDir, authorizationFile });
|
|
452
|
+
assert.equal(fs.existsSync(session.sessionDir), false);
|
|
453
|
+
assert.equal(readJson(path.join(recovered.archiveDir, "state.json")).status, "archived");
|
|
454
|
+
assert.equal(consumedAuthorizationRecords(session).length, 2);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("mismatched and expired cancellation authority fail before Flow or sidecar mutation", async (t) => {
|
|
458
|
+
for (const [name, overrides, pattern] of [
|
|
459
|
+
["wrong-run", { run_id: "another-run" }, /run_id does not match/],
|
|
460
|
+
["wrong-subject", { subject: "local:other" }, /subject does not match/],
|
|
461
|
+
["wrong-actor", { assignment_actor_key: "another-actor" }, /assignment_actor_key does not match/],
|
|
462
|
+
["wrong-actor-struct", { assignment_actor: { ...ACTOR, session_id: "another-session" } }, /assignment_actor.*active assignment holder/],
|
|
463
|
+
["wrong-operation", { operation: "archive" }, /operation does not match/],
|
|
464
|
+
["expired", {}, /expired/],
|
|
465
|
+
]) {
|
|
466
|
+
await t.test(name, async () => {
|
|
467
|
+
const session = makeSession(`lifecycle-reject-${name}`);
|
|
468
|
+
claimSessionAssignment(session);
|
|
469
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
470
|
+
const beforeFlow = snapshotTree(runDir(session.slug, session.projectRoot));
|
|
471
|
+
const beforeProjection = snapshotProjectionTargets(session);
|
|
472
|
+
const beforeAssignment = snapshotFile(path.join(session.artifactRoot, "assignment", `${session.slug}.json`));
|
|
473
|
+
await assert.rejects(() => cancelBuilderFlowSession({
|
|
474
|
+
sessionDir: session.sessionDir,
|
|
475
|
+
authorizationFile: name === "expired"
|
|
476
|
+
? expiredLifecycleAuthorization(session, "cancel", name, overrides)
|
|
477
|
+
: liveLifecycleAuthorization(session, "cancel", name, overrides),
|
|
478
|
+
}), pattern);
|
|
479
|
+
assert.deepEqual(snapshotTree(runDir(session.slug, session.projectRoot)), beforeFlow);
|
|
480
|
+
assert.deepEqual(snapshotProjectionTargets(session), beforeProjection);
|
|
481
|
+
assert.equal(snapshotFile(path.join(session.artifactRoot, "assignment", `${session.slug}.json`)), beforeAssignment);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("conflicting cancellation request replay is rejected without a second transition or release", async () => {
|
|
487
|
+
const session = makeSession("lifecycle-conflicting-replay");
|
|
488
|
+
claimSessionAssignment(session);
|
|
489
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
490
|
+
await cancelBuilderFlowSession({
|
|
491
|
+
sessionDir: session.sessionDir,
|
|
492
|
+
authorizationFile: liveLifecycleAuthorization(session, "cancel", "cancel-original"),
|
|
493
|
+
});
|
|
494
|
+
const beforeFlow = snapshotTree(runDir(session.slug, session.projectRoot));
|
|
495
|
+
const assignmentFile = path.join(session.artifactRoot, "assignment", `${session.slug}.json`);
|
|
496
|
+
const beforeAssignment = snapshotFile(assignmentFile);
|
|
497
|
+
await assert.rejects(() => cancelBuilderFlowSession({
|
|
498
|
+
sessionDir: session.sessionDir,
|
|
499
|
+
authorizationFile: liveLifecycleAuthorization(session, "cancel", "cancel-conflict"),
|
|
500
|
+
}), /does not match the canonical cancellation/);
|
|
501
|
+
assert.deepEqual(snapshotTree(runDir(session.slug, session.projectRoot)), beforeFlow);
|
|
502
|
+
assert.equal(snapshotFile(assignmentFile), beforeAssignment);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("tampered or unsigned cancellation authority fails before mutation", async () => {
|
|
506
|
+
const session = makeSession("lifecycle-signature-reject");
|
|
507
|
+
claimSessionAssignment(session);
|
|
508
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
509
|
+
const authorizationFile = liveLifecycleAuthorization(session, "cancel", "signature-reject");
|
|
510
|
+
const tampered = readJson(authorizationFile);
|
|
511
|
+
tampered.request.reason = "agent-authored replacement";
|
|
512
|
+
writeJson(authorizationFile, tampered);
|
|
513
|
+
const beforeFlow = snapshotTree(runDir(session.slug, session.projectRoot));
|
|
514
|
+
const beforeAssignment = snapshotFile(path.join(session.artifactRoot, "assignment", `${session.slug}.json`));
|
|
515
|
+
|
|
516
|
+
await assert.rejects(() => cancelBuilderFlowSession({ sessionDir: session.sessionDir, authorizationFile }), /signature is invalid/);
|
|
517
|
+
assert.deepEqual(snapshotTree(runDir(session.slug, session.projectRoot)), beforeFlow);
|
|
518
|
+
assert.equal(snapshotFile(path.join(session.artifactRoot, "assignment", `${session.slug}.json`)), beforeAssignment);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("expired authority can finish side effects for its matching canonical cancellation", async () => {
|
|
522
|
+
const session = makeSession("lifecycle-cancel-recovery");
|
|
523
|
+
claimSessionAssignment(session);
|
|
524
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
525
|
+
const authorizationFile = expiredLifecycleAuthorization(session, "cancel", "cancel-recovery");
|
|
526
|
+
const authorization = readJson(authorizationFile);
|
|
527
|
+
await cancelBuilderBuildRun({
|
|
528
|
+
cwd: session.projectRoot,
|
|
529
|
+
runId: session.slug,
|
|
530
|
+
request: authorization.request,
|
|
531
|
+
at: new Date(Date.parse(authorization.expires_at) - 1_000).toISOString(),
|
|
532
|
+
});
|
|
533
|
+
assert.equal(readJson(path.join(runDir(session.slug, session.projectRoot), "state.json")).status, "canceled");
|
|
534
|
+
assert.equal(readJson(path.join(session.artifactRoot, "assignment", `${session.slug}.json`)).status, "claimed");
|
|
535
|
+
assert.deepEqual(consumedAuthorizationRecords(session), []);
|
|
536
|
+
|
|
537
|
+
const recovered = await cancelBuilderFlowSession({ sessionDir: session.sessionDir, authorizationFile });
|
|
538
|
+
assert.equal(recovered.idempotent, true);
|
|
539
|
+
assert.equal(recovered.assignmentReleased, true);
|
|
540
|
+
assert.equal(recovered.projection.status, "canceled");
|
|
541
|
+
assert.equal(consumedAuthorizationRecords(session).length, 1);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("builder-run exposes lifecycle actions without caller-selected Flow identity", async () => {
|
|
545
|
+
const session = makeSession("lifecycle-cli");
|
|
546
|
+
const ambient = resolveCurrentAssignmentActor();
|
|
547
|
+
performLocalClaim(session.artifactRoot, session.slug, ambient.actor, { actorKey: ambient.actorKey, ttlSeconds: 1800, branch: `agent/${session.slug}`, artifactDir: session.sessionDir, workItemRef: SUBJECT, reason: "test" });
|
|
548
|
+
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
549
|
+
assert.equal(await builderRunMain([
|
|
550
|
+
"pause", "--session-dir", session.sessionDir,
|
|
551
|
+
"--reason", "cli pause",
|
|
552
|
+
]), 0);
|
|
553
|
+
assert.equal(await builderRunMain([
|
|
554
|
+
"resume", "--session-dir", session.sessionDir,
|
|
555
|
+
"--reason", "cli resume",
|
|
556
|
+
]), 0);
|
|
557
|
+
const cliCancel = liveLifecycleAuthorization(session, "cancel", "cli-cancel", { assignment_actor_key: ambient.actorKey, assignment_actor: ambient.actor });
|
|
558
|
+
assert.equal(await builderRunMain([
|
|
559
|
+
"cancel", "--session-dir", session.sessionDir,
|
|
560
|
+
"--authorization-file", cliCancel,
|
|
561
|
+
]), 0);
|
|
562
|
+
assert.equal(readJson(path.join(runDir(session.slug, session.projectRoot), "state.json")).status, "canceled");
|
|
563
|
+
assert.equal(await builderRunMain([
|
|
564
|
+
"cancel", "--session-dir", session.sessionDir,
|
|
565
|
+
"--authorization-file", liveLifecycleAuthorization(session, "cancel", "cli-clock-override", { assignment_actor_key: ambient.actorKey, assignment_actor: ambient.actor }),
|
|
566
|
+
"--now", "2020-01-01T00:00:00.000Z",
|
|
567
|
+
]), 64);
|
|
568
|
+
});
|
|
569
|
+
|
|
175
570
|
test("automatic start refuses a slug-bound run for another Work Item without mutation", async () => {
|
|
176
571
|
const session = makeSession("start-subject-mismatch");
|
|
177
572
|
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
@@ -253,6 +648,7 @@ test("failed verification projects Flow-owned route-back attempt and budget", as
|
|
|
253
648
|
|
|
254
649
|
test("verified sidecar claims drive the composed publish and learning prefix to completion", async () => {
|
|
255
650
|
const session = makeSession("composed-completion");
|
|
651
|
+
claimSessionAssignment(session);
|
|
256
652
|
await startBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
257
653
|
const steps = [
|
|
258
654
|
[bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })],
|
|
@@ -297,6 +693,13 @@ test("verified sidecar claims drive the composed publish and learning prefix to
|
|
|
297
693
|
assert.equal(recovered.projection.phase, "done");
|
|
298
694
|
assert.deepEqual(recovered.projection.next_action, { status: "done", summary: "Canonical Flow run is complete." });
|
|
299
695
|
assert.deepEqual(snapshotTree(flowDirectory), beforeFlow);
|
|
696
|
+
|
|
697
|
+
const archived = await archiveBuilderFlowSession({
|
|
698
|
+
sessionDir: session.sessionDir,
|
|
699
|
+
authorizationFile: liveLifecycleAuthorization(session, "archive", "completed-archive"),
|
|
700
|
+
});
|
|
701
|
+
assert.equal(readJson(path.join(archived.archiveDir, "state.json")).status, "archived");
|
|
702
|
+
assert.deepEqual(snapshotTree(flowDirectory), beforeFlow);
|
|
300
703
|
});
|
|
301
704
|
|
|
302
705
|
test("recovery loads the slug-bound run, restores every matching projection, and preserves every Flow byte", async () => {
|
package/src/cli/builder-run.ts
CHANGED
|
@@ -1,36 +1,87 @@
|
|
|
1
1
|
import { flagString, parseArgs } from "../lib/args.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
cancelBuilderFlowSession,
|
|
4
|
+
archiveBuilderFlowSession,
|
|
5
|
+
pauseBuilderFlowSession,
|
|
6
|
+
recoverBuilderFlowSession,
|
|
7
|
+
releaseBuilderFlowAssignment,
|
|
8
|
+
resumeBuilderFlowSession,
|
|
9
|
+
startBuilderFlowSession,
|
|
10
|
+
syncBuilderFlowSession,
|
|
11
|
+
} from "../builder-flow-runtime.js";
|
|
12
|
+
|
|
13
|
+
const USAGE = "Usage: flow-agents builder-run <start|sync|recover|pause|resume|cancel|release-assignment|archive> --session-dir <path> [--reason <text> | --authorization-file <path>]";
|
|
3
14
|
|
|
4
15
|
export async function main(argv: string[]): Promise<number> {
|
|
5
16
|
const parsed = parseArgs(argv);
|
|
6
17
|
const action = parsed.positionals[0];
|
|
7
18
|
const sessionDir = flagString(parsed.flags, "session-dir");
|
|
19
|
+
const authorizationFile = flagString(parsed.flags, "authorization-file");
|
|
20
|
+
const reason = flagString(parsed.flags, "reason");
|
|
8
21
|
const validRecoveryArguments = parsed.positionals.length === 1
|
|
9
22
|
&& Object.keys(parsed.flags).length === 1
|
|
10
23
|
&& typeof parsed.flags["session-dir"] === "string";
|
|
11
24
|
if (action === "recover" && !validRecoveryArguments) {
|
|
12
|
-
console.error(
|
|
25
|
+
console.error(USAGE);
|
|
13
26
|
return 64;
|
|
14
27
|
}
|
|
15
28
|
if (!sessionDir) {
|
|
16
29
|
console.error("builder-run requires --session-dir .kontourai/flow-agents/<slug>");
|
|
17
30
|
return 64;
|
|
18
31
|
}
|
|
19
|
-
if (action
|
|
20
|
-
console.error(
|
|
32
|
+
if (!action || !["start", "sync", "recover", "pause", "resume", "cancel", "release-assignment", "archive"].includes(action)) {
|
|
33
|
+
console.error(USAGE);
|
|
34
|
+
return 64;
|
|
35
|
+
}
|
|
36
|
+
const agentLifecycle = action === "pause" || action === "resume" || action === "release-assignment";
|
|
37
|
+
const authorizedLifecycle = action === "cancel" || action === "archive";
|
|
38
|
+
const lifecycle = agentLifecycle || authorizedLifecycle;
|
|
39
|
+
const allowedLifecycleFlag = (name: string) => name === "session-dir" || (agentLifecycle ? name === "reason" : name === "authorization-file");
|
|
40
|
+
if (lifecycle && (parsed.positionals.length !== 1 || Object.keys(parsed.flags).some((name) => !allowedLifecycleFlag(name)))) {
|
|
41
|
+
console.error(USAGE);
|
|
42
|
+
return 64;
|
|
43
|
+
}
|
|
44
|
+
if (agentLifecycle && !reason) {
|
|
45
|
+
console.error(`builder-run ${action} requires --reason <text>`);
|
|
46
|
+
return 64;
|
|
47
|
+
}
|
|
48
|
+
if (authorizedLifecycle && !authorizationFile) {
|
|
49
|
+
console.error(`builder-run ${action} requires a signed --authorization-file <path>`);
|
|
50
|
+
return 64;
|
|
51
|
+
}
|
|
52
|
+
if (!lifecycle && (authorizationFile || reason)) {
|
|
53
|
+
console.error(USAGE);
|
|
21
54
|
return 64;
|
|
22
55
|
}
|
|
23
56
|
const result = action === "start"
|
|
24
57
|
? await startBuilderFlowSession({ sessionDir })
|
|
25
58
|
: action === "sync"
|
|
26
59
|
? await syncBuilderFlowSession({ sessionDir })
|
|
27
|
-
:
|
|
60
|
+
: action === "recover"
|
|
61
|
+
? await recoverBuilderFlowSession({ sessionDir })
|
|
62
|
+
: action === "pause"
|
|
63
|
+
? await pauseBuilderFlowSession({ sessionDir, reason: reason! })
|
|
64
|
+
: action === "resume"
|
|
65
|
+
? await resumeBuilderFlowSession({ sessionDir, reason: reason! })
|
|
66
|
+
: action === "cancel"
|
|
67
|
+
? await cancelBuilderFlowSession({ sessionDir, authorizationFile: authorizationFile! })
|
|
68
|
+
: action === "release-assignment"
|
|
69
|
+
? await releaseBuilderFlowAssignment({ sessionDir, reason: reason! })
|
|
70
|
+
: await archiveBuilderFlowSession({ sessionDir, authorizationFile: authorizationFile! });
|
|
28
71
|
console.log(JSON.stringify({
|
|
29
72
|
run_id: result.run.runId,
|
|
30
73
|
definition_id: result.run.definitionId,
|
|
31
74
|
current_step: result.run.state.current_step,
|
|
32
75
|
status: result.run.state.status,
|
|
33
76
|
attached: result.attached,
|
|
77
|
+
...(action === "cancel" ? {
|
|
78
|
+
assignment_released: "assignmentReleased" in result ? result.assignmentReleased : false,
|
|
79
|
+
idempotent: "idempotent" in result ? result.idempotent : false,
|
|
80
|
+
} : action === "release-assignment" ? {
|
|
81
|
+
assignment_released: "assignmentReleased" in result ? result.assignmentReleased : false,
|
|
82
|
+
} : action === "archive" ? {
|
|
83
|
+
archive_dir: "archiveDir" in result ? result.archiveDir : null,
|
|
84
|
+
} : {}),
|
|
34
85
|
next_action: result.projection.next_action,
|
|
35
86
|
}));
|
|
36
87
|
return 0;
|
|
@@ -264,6 +264,9 @@ function classifyWorkflow(slug: string, workflowPath: string): AuditItem {
|
|
|
264
264
|
if (status === "verified" && nextStatus === "done") {
|
|
265
265
|
return { ...base, classification: "cleanup_candidate", reasons: ["verified workflow has next_action.status done"] };
|
|
266
266
|
}
|
|
267
|
+
if (status === "canceled" && phase === "done") {
|
|
268
|
+
return { ...base, classification: "terminal_done", reasons: ["canceled workflow retains its artifacts without requiring delivery promotion"] };
|
|
269
|
+
}
|
|
267
270
|
if (["delivered", "accepted", "archived"].includes(status) && phase === "done") {
|
|
268
271
|
if (status !== "archived" && !hasPromotionClaim(workflowPath)) {
|
|
269
272
|
return { ...base, classification: "cleanup_candidate", reasons: [`${status} workflow reached phase done without a promotion claim; ${PROMOTE_REMEDY}`] };
|
|
@@ -10,6 +10,8 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { resolveActiveFlowStep, resolveAllFlowGateExpects, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy, type ActiveFlowStep } from "../lib/flow-resolver.js";
|
|
11
11
|
import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
12
12
|
import { ensureSafeDirectory } from "../lib/fs.js";
|
|
13
|
+
import { flowAgentsPackageVersion } from "../lib/package-version.js";
|
|
14
|
+
import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
|
|
13
15
|
import { startBuilderFlowSession, syncBuilderFlowSessionIfPresent } from "../builder-flow-runtime.js";
|
|
14
16
|
// #291 Wave 1 Task 1.1 exports: ensure-session's ownership guard reuses the EXACT same
|
|
15
17
|
// assignment ⋈ liveness join / claim / supersede logic #290 already ships for the
|
|
@@ -19,11 +21,12 @@ import { assignmentFilePath, computeEffectiveState, performLocalClaim, performLo
|
|
|
19
21
|
|
|
20
22
|
type AnyObj = Record<string, any>;
|
|
21
23
|
|
|
22
|
-
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
24
|
+
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "canceled", "accepted", "archived"]);
|
|
23
25
|
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
24
26
|
export const checkKinds = new Set(["build", "types", "lint", "test", "command", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
25
27
|
export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
26
28
|
export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
29
|
+
export const WORKFLOW_WRITER_CONTRACT_VERSION = "1.0";
|
|
27
30
|
|
|
28
31
|
function now(): string { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
29
32
|
function read(file: string): string { return fs.readFileSync(file, "utf8"); }
|
|
@@ -1678,6 +1681,11 @@ function currentDir(root: string, actorKey?: string): string | null {
|
|
|
1678
1681
|
}
|
|
1679
1682
|
return dir;
|
|
1680
1683
|
}
|
|
1684
|
+
|
|
1685
|
+
export function currentWorkflowSessionDir(root: string): string | null {
|
|
1686
|
+
const resolved = loadActorIdentityHelper().resolveActor(process.env);
|
|
1687
|
+
return currentDir(path.resolve(root), loadActorIdentityHelper().isUnresolvedActor(resolved.actor) ? undefined : resolved.actor);
|
|
1688
|
+
}
|
|
1681
1689
|
/**
|
|
1682
1690
|
* #291 Wave 2 Task 2.1 (§6): updates BOTH the legacy current.json (when IT points at `dir` — the
|
|
1683
1691
|
* exact, unchanged existing check/write) AND the resolved actor's own per-actor current.json
|
|
@@ -2096,13 +2104,24 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2096
2104
|
// takeover continuity true by construction (see Design Decision 3 in the plan).
|
|
2097
2105
|
const branch = resolveSessionBranch(p, slug);
|
|
2098
2106
|
const initialMarkdownStatus = entry.flowId ? "new" : "planning";
|
|
2099
|
-
|
|
2107
|
+
const acceptanceCriteria = opts(p, "criterion");
|
|
2108
|
+
if (acceptanceCriteria.length === 0) acceptanceCriteria.push(`Complete the requested outcome: ${opt(p, "summary", "Workflow session is durable.")}`);
|
|
2109
|
+
md = `# ${opt(p, "title", slug)}\n\nbranch: ${branch}\nworktree: main\ncreated: ${timestamp}\nstatus: ${initialMarkdownStatus}\ntype: deliver\niteration: 1\n\n## Plan\n\n${opt(p, "summary", "")}\n\n## Definition Of Done\n\n- **User outcome:** ${opt(p, "summary", "Workflow session is durable.")}\n- **Scope:** Workflow session artifacts and sidecars.\n- **Acceptance criteria:**\n${acceptanceCriteria.map((c) => ` - [ ] ${c} - Evidence: pending.`).join("\n")}\n- **Usefulness checks:**\n - [ ] User-facing workflow is documented or discoverable\n- **Stop-short risks:** Workflow artifacts could drift.\n- **Durable docs target:** not needed\n- **Sandbox mode:** local-edit\n\n## Execution Progress\n\n- [ ] Session initialized.\n\n## Verification Report\n\nBuild: [NOT_VERIFIED] Verification has not run yet.\n\n### Acceptance Criteria\n- [NOT_VERIFIED] Verification has not run yet - Evidence: pending workflow execution and checks.\n\n### Verdict: NOT_VERIFIED\n\n## Goal Fit Gate\n\n- [ ] Original user goal restated\n\n## Final Acceptance\n\n- [ ] CI/relevant checks passed or local equivalent recorded\n`;
|
|
2100
2110
|
fs.writeFileSync(path.join(dir, `${slug}--deliver.md`), md);
|
|
2101
2111
|
}
|
|
2102
2112
|
if (!fs.existsSync(path.join(dir, "state.json")) || !fs.existsSync(path.join(dir, "acceptance.json")) || !fs.existsSync(path.join(dir, "handoff.json"))) {
|
|
2103
2113
|
const phaseMap = entry.flowId ? resolvePhaseMap(entry.flowId, findRepoRootFromDir(dir)) : null;
|
|
2104
2114
|
const initialPhase = Object.entries(phaseMap ?? {}).find(([, step]) => step === entry.firstStep)?.[0];
|
|
2105
|
-
const startCommand =
|
|
2115
|
+
const startCommand = pinnedFlowAgentsCommand(flowAgentsPackageVersion(), [
|
|
2116
|
+
"workflow", "start",
|
|
2117
|
+
"--flow", "builder.build",
|
|
2118
|
+
"--work-item", workItem.ref,
|
|
2119
|
+
...(workItem.ref === `local:${slug}` ? ["--task-slug", slug] : []),
|
|
2120
|
+
"--artifact-root", root,
|
|
2121
|
+
"--title", opt(p, "title", slug),
|
|
2122
|
+
"--summary", opt(p, "summary", "Workflow session is durable."),
|
|
2123
|
+
...opts(p, "criterion").flatMap((criterion) => ["--criterion", criterion]),
|
|
2124
|
+
]);
|
|
2106
2125
|
const nextAction: string | AnyObj = entry.flowId
|
|
2107
2126
|
? entry.flowId === "builder.build"
|
|
2108
2127
|
? {
|
|
@@ -5421,7 +5440,7 @@ function loadLivenessReadHelper(): {
|
|
|
5421
5440
|
// init-plan claims the work-item; advance-state heartbeats (or releases on terminal),
|
|
5422
5441
|
// so the workflow lifecycle itself maintains the liveness claim — no manual liveness calls.
|
|
5423
5442
|
// Additive + fail-open: a liveness-emit failure never affects the workflow command.
|
|
5424
|
-
export const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
5443
|
+
export const LIVENESS_TERMINAL = new Set(["delivered", "canceled", "accepted", "archived"]);
|
|
5425
5444
|
/**
|
|
5426
5445
|
* Delegate to the shared pure-CJS resolver (scripts/hooks/lib/actor-identity.js), mirroring the
|
|
5427
5446
|
* createRequire pattern used by readLivenessEvents() above. Deliberately NO inline duplicate
|
|
@@ -5889,8 +5908,8 @@ Available claim ids:
|
|
|
5889
5908
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5890
5909
|
|
|
5891
5910
|
|
|
5892
|
-
async function main(): Promise<number> {
|
|
5893
|
-
const _rawArgv =
|
|
5911
|
+
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
5912
|
+
const _rawArgv = argv;
|
|
5894
5913
|
// #380: `record-check <dir> -- <command...>` — argv after the FIRST literal `--` token is the
|
|
5895
5914
|
// command to execute verbatim (never option-parsed: a command like `npm test -- --watch`
|
|
5896
5915
|
// legitimately contains its OWN `--`, so only the record-check dispatcher's own separator, the
|
|
@@ -5965,5 +5984,8 @@ async function main(): Promise<number> {
|
|
|
5965
5984
|
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
5966
5985
|
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
5967
5986
|
if (_selfRealPath === _argv1RealPath) {
|
|
5987
|
+
if (path.basename(process.argv[1] ?? "") === "flow-agents-workflow-sidecar") {
|
|
5988
|
+
process.stderr.write("flow-agents-workflow-sidecar is deprecated; use `flow-agents workflow` or an explicitly pinned `npx @kontourai/flow-agents@<version> workflow` command.\n");
|
|
5989
|
+
}
|
|
5968
5990
|
main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
|
|
5969
5991
|
}
|