@kontourai/flow-agents 3.8.0 → 3.10.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.
Files changed (109) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +1 -1
  4. package/build/src/builder-flow-runtime.js +16 -10
  5. package/build/src/cli/assignment-provider.js +4 -18
  6. package/build/src/cli/continuation-adapter.d.ts +26 -0
  7. package/build/src/cli/continuation-adapter.js +243 -0
  8. package/build/src/cli/validate-workflow-artifacts.js +21 -1
  9. package/build/src/cli/workflow-sidecar.js +15 -20
  10. package/build/src/cli/workflow.js +99 -6
  11. package/build/src/continuation-driver.d.ts +116 -0
  12. package/build/src/continuation-driver.js +527 -0
  13. package/build/src/index.d.ts +2 -0
  14. package/build/src/index.js +1 -0
  15. package/build/src/tools/build-universal-bundles.js +52 -25
  16. package/build/src/tools/validate-source-tree.js +5 -2
  17. package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  18. package/context/scripts/hooks/stop-goal-fit.js +244 -73
  19. package/context/scripts/hooks/workflow-steering.js +24 -15
  20. package/context/scripts/telemetry/lib/pricing.sh +18 -6
  21. package/context/scripts/telemetry/lib/transport.sh +139 -37
  22. package/context/scripts/telemetry/lib/usage.sh +282 -32
  23. package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  24. package/context/scripts/telemetry/telemetry.sh +76 -1
  25. package/docs/coordination-guide.md +21 -5
  26. package/docs/fixture-ownership.md +2 -1
  27. package/docs/public-workflow-cli.md +98 -0
  28. package/docs/spec/runtime-hook-surface.md +8 -4
  29. package/docs/workflow-usage-guide.md +7 -0
  30. package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
  31. package/evals/acceptance/prove-capture-teeth.sh +13 -0
  32. package/evals/ci/run-baseline.sh +2 -0
  33. package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
  34. package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
  35. package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
  36. package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
  37. package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
  38. package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
  39. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
  40. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
  41. package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
  42. package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
  43. package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
  44. package/evals/integration/test_actor_identity.sh +63 -0
  45. package/evals/integration/test_assignment_provider_local_file.sh +34 -0
  46. package/evals/integration/test_builder_entry_enforcement.sh +5 -1
  47. package/evals/integration/test_bundle_install.sh +106 -17
  48. package/evals/integration/test_bundle_lifecycle.sh +17 -3
  49. package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
  50. package/evals/integration/test_ci_actor_identity.sh +2 -2
  51. package/evals/integration/test_command_log_concurrency.sh +11 -0
  52. package/evals/integration/test_command_log_fork_classification.sh +11 -0
  53. package/evals/integration/test_command_log_integrity.sh +11 -0
  54. package/evals/integration/test_current_json_per_actor.sh +416 -21
  55. package/evals/integration/test_enforcer_expects_driven.sh +11 -0
  56. package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
  57. package/evals/integration/test_evidence_capture_hook.sh +25 -6
  58. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  59. package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
  60. package/evals/integration/test_gate_bypass_chain.sh +11 -0
  61. package/evals/integration/test_gate_lockdown.sh +10 -0
  62. package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
  63. package/evals/integration/test_goal_fit_hook.sh +257 -9
  64. package/evals/integration/test_goal_fit_rederive.sh +11 -0
  65. package/evals/integration/test_install_merge.sh +141 -3
  66. package/evals/integration/test_liveness_conflict_injection.sh +39 -3
  67. package/evals/integration/test_liveness_heartbeat.sh +46 -9
  68. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
  69. package/evals/integration/test_public_workflow_cli.sh +6 -6
  70. package/evals/integration/test_session_resume_roundtrip.sh +30 -0
  71. package/evals/integration/test_stop_hook_release.sh +111 -11
  72. package/evals/integration/test_telemetry_tool_usage.sh +394 -0
  73. package/evals/integration/test_verify_hold.sh +64 -1
  74. package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
  75. package/evals/integration/test_workflow_steering_hook.sh +20 -4
  76. package/evals/run.sh +2 -0
  77. package/evals/static/test_universal_bundles.sh +123 -0
  78. package/package.json +4 -4
  79. package/packaging/README.md +21 -0
  80. package/packaging/codex-legacy-agents-fingerprints.json +33 -0
  81. package/scripts/README.md +2 -1
  82. package/scripts/audit-codex-legacy-agents.js +43 -0
  83. package/scripts/classify-codex-legacy-agents.js +71 -0
  84. package/scripts/hooks/evidence-capture.js +18 -6
  85. package/scripts/hooks/lib/actor-identity.js +62 -29
  86. package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  87. package/scripts/hooks/lib/current-pointer.js +114 -12
  88. package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
  89. package/scripts/hooks/stop-goal-fit.js +244 -73
  90. package/scripts/hooks/workflow-steering.js +24 -15
  91. package/scripts/install-codex-home.sh +8 -7
  92. package/scripts/telemetry/lib/pricing.sh +18 -6
  93. package/scripts/telemetry/lib/transport.sh +71 -0
  94. package/scripts/telemetry/lib/usage.sh +282 -32
  95. package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  96. package/scripts/telemetry/telemetry.sh +76 -1
  97. package/src/builder-flow-runtime.ts +9 -3
  98. package/src/cli/assignment-provider.ts +5 -26
  99. package/src/cli/builder-flow-runtime.test.mjs +215 -2
  100. package/src/cli/continuation-adapter.ts +265 -0
  101. package/src/cli/continuation-driver.test.mjs +1303 -0
  102. package/src/cli/public-api.test.mjs +1 -0
  103. package/src/cli/validate-workflow-artifacts.ts +19 -1
  104. package/src/cli/workflow-sidecar.ts +18 -23
  105. package/src/cli/workflow.ts +113 -6
  106. package/src/continuation-driver.ts +638 -0
  107. package/src/index.ts +24 -0
  108. package/src/tools/build-universal-bundles.ts +61 -25
  109. package/src/tools/validate-source-tree.ts +5 -2
@@ -0,0 +1,1303 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { createRequire } from "node:module";
7
+ import { execFileSync, spawnSync } from "node:child_process";
8
+ import { createHash, generateKeyPairSync, randomBytes, sign } from "node:crypto";
9
+
10
+ import { ContinuationAdapterTimeoutError, createFileContinuationStore, runContinuationDriver, withContinuationDriverLock } from "../../build/src/continuation-driver.js";
11
+ import { executeContinuationAdapter, executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "../../build/src/cli/continuation-adapter.js";
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const activeTurnAuthority = require("../../scripts/hooks/lib/continuation-turn-authority.js");
15
+ const stopGoalFit = require("../../scripts/hooks/stop-goal-fit.js");
16
+ const authorityActorStruct = { runtime: "test", session_id: "authority", host: "localhost", human: null };
17
+
18
+ function snapshot(step, status = "active") {
19
+ const disposition = status === "completed" || status === "canceled"
20
+ ? "done"
21
+ : status === "failed"
22
+ ? "failed"
23
+ : status === "paused" || status === "needs_decision"
24
+ ? "waiting"
25
+ : "continue";
26
+ return {
27
+ run_id: "run-251",
28
+ definition_id: "builder.build",
29
+ status,
30
+ disposition,
31
+ current_step: step,
32
+ next_action: status === "active"
33
+ ? { status: "continue", summary: `Execute ${step}`, skills: [`skill-${step}`], operations: [] }
34
+ : { status: "done", summary: "Workflow complete" },
35
+ };
36
+ }
37
+
38
+ function memoryStore(initial = null) {
39
+ let value = initial;
40
+ const events = [];
41
+ return {
42
+ load: () => value,
43
+ save: (next) => { value = structuredClone(next); },
44
+ append: (event) => { events.push(structuredClone(event)); },
45
+ value: () => value,
46
+ events,
47
+ };
48
+ }
49
+
50
+ function writeAuthorityAssignment(sessionDir, actorKey, extra = {}) {
51
+ const slug = path.basename(sessionDir);
52
+ const assignment = path.join(path.dirname(sessionDir), "assignment", `${slug}.json`);
53
+ fs.mkdirSync(path.dirname(assignment), { recursive: true });
54
+ fs.writeFileSync(assignment, JSON.stringify({
55
+ schema_version: "1.0",
56
+ role: "AssignmentClaimRecord",
57
+ subject_id: slug,
58
+ actor: authorityActorStruct,
59
+ actor_key: actorKey,
60
+ artifact_dir: slug,
61
+ status: "claimed",
62
+ ...extra,
63
+ }));
64
+ }
65
+
66
+ function bindAuthoritySigner(sessionDir, issued) {
67
+ const missionFile = path.join(sessionDir, "continuation-driver", "state.json");
68
+ const mission = JSON.parse(fs.readFileSync(missionFile, "utf8"));
69
+ fs.writeFileSync(missionFile, JSON.stringify({ ...mission, active_turn_public_key_digest: issued.publicKeyDigest }));
70
+ }
71
+
72
+ function canonicalBytes(value) {
73
+ const canonicalize = (entry) => Array.isArray(entry)
74
+ ? entry.map(canonicalize)
75
+ : entry && typeof entry === "object"
76
+ ? Object.fromEntries(Object.keys(entry).sort().map((key) => [key, canonicalize(entry[key])]))
77
+ : entry;
78
+ return Buffer.from(JSON.stringify(canonicalize(value)), "utf8");
79
+ }
80
+
81
+ test("driver advances multiple canonical Flow steps without a human continuation", async () => {
82
+ const states = [snapshot("plan"), snapshot("execute"), snapshot("done", "completed")];
83
+ let index = 0;
84
+ const requests = [];
85
+ const store = memoryStore();
86
+
87
+ const result = await runContinuationDriver({
88
+ maxTurns: 4,
89
+ store,
90
+ runtime: {
91
+ inspect: async () => states[index],
92
+ synchronize: async () => states[index],
93
+ execute: async (request) => {
94
+ requests.push(request);
95
+ index += 1;
96
+ return { status: "completed", summary: "turn complete" };
97
+ },
98
+ },
99
+ });
100
+
101
+ assert.equal(result.outcome, "done");
102
+ assert.equal(result.turns_started, 2);
103
+ assert.deepEqual(requests.map((request) => request.current_step), ["plan", "execute"]);
104
+ assert.deepEqual(requests.map((request) => request.next_action.skills), [["skill-plan"], ["skill-execute"]]);
105
+ assert.equal(requests.some((request) => Object.hasOwn(request, "system_prompt")), false);
106
+ assert.equal(store.value().status, "done");
107
+ });
108
+
109
+ test("driver parks on a wait barrier without burning another turn and resumes durably", async () => {
110
+ const barrier = { kind: "deadline", at: "2026-07-12T12:00:00.000Z" };
111
+ const store = memoryStore();
112
+ let executeCalls = 0;
113
+ let ready = false;
114
+ const runtime = {
115
+ inspect: async () => snapshot("verify"),
116
+ synchronize: async () => ready ? snapshot("done", "completed") : snapshot("verify"),
117
+ execute: async () => {
118
+ executeCalls += 1;
119
+ return { status: "wait", barrier, summary: "waiting for CI" };
120
+ },
121
+ };
122
+
123
+ const parked = await runContinuationDriver({
124
+ maxTurns: 3,
125
+ store,
126
+ runtime,
127
+ waitForBarrier: async () => "pending",
128
+ });
129
+
130
+ assert.equal(parked.outcome, "waiting");
131
+ assert.equal(parked.turns_started, 1);
132
+ assert.deepEqual(store.value().pending_barrier, barrier);
133
+ assert.equal(executeCalls, 1);
134
+
135
+ ready = true;
136
+ const resumed = await runContinuationDriver({
137
+ maxTurns: 3,
138
+ store,
139
+ runtime,
140
+ waitForBarrier: async () => "ready",
141
+ });
142
+
143
+ assert.equal(resumed.outcome, "done");
144
+ assert.equal(resumed.turns_started, 1);
145
+ assert.equal(executeCalls, 1);
146
+ assert.equal(store.value().pending_barrier, null);
147
+ assert.ok(store.events.some((event) => event.type === "parked"));
148
+ assert.ok(store.events.some((event) => event.type === "resumed"));
149
+ });
150
+
151
+ test("driver parks and resumes within one unattended invocation when its trigger fires", async () => {
152
+ const barrier = { kind: "deadline", at: "2026-07-12T12:00:01.000Z" };
153
+ const store = memoryStore();
154
+ let ready = false;
155
+ let executeCalls = 0;
156
+ const result = await runContinuationDriver({
157
+ maxTurns: 2,
158
+ store,
159
+ runtime: {
160
+ inspect: async () => snapshot("verify"),
161
+ synchronize: async () => ready ? snapshot("done", "completed") : snapshot("verify"),
162
+ execute: async () => {
163
+ executeCalls += 1;
164
+ return { status: "wait", barrier, summary: "waiting for trigger" };
165
+ },
166
+ },
167
+ waitForBarrier: async () => {
168
+ ready = true;
169
+ return "ready";
170
+ },
171
+ });
172
+
173
+ assert.equal(result.outcome, "done");
174
+ assert.equal(result.turns_started, 1);
175
+ assert.equal(executeCalls, 1);
176
+ assert.deepEqual(store.events.filter((event) => event.type === "parked" || event.type === "resumed").map((event) => event.type), ["parked", "resumed"]);
177
+ });
178
+
179
+ test("driver stops cleanly when the mission turn budget is exhausted", async () => {
180
+ const store = memoryStore();
181
+ let executeCalls = 0;
182
+ const runtime = {
183
+ inspect: async () => snapshot("plan"),
184
+ synchronize: async () => snapshot("plan"),
185
+ execute: async () => {
186
+ executeCalls += 1;
187
+ return { status: "completed" };
188
+ },
189
+ };
190
+
191
+ const first = await runContinuationDriver({ maxTurns: 2, store, runtime });
192
+ assert.equal(first.outcome, "budget_exhausted");
193
+ assert.equal(first.turns_started, 2);
194
+ assert.equal(executeCalls, 2);
195
+
196
+ const resumed = await runContinuationDriver({ maxTurns: 2, store, runtime });
197
+ assert.equal(resumed.outcome, "budget_exhausted");
198
+ assert.equal(resumed.turns_started, 2);
199
+ assert.equal(executeCalls, 2);
200
+ assert.equal(store.value().status, "budget_exhausted");
201
+ });
202
+
203
+ test("driver refuses to change a persisted mission budget on resume", async () => {
204
+ const store = memoryStore();
205
+ const runtime = {
206
+ inspect: async () => snapshot("plan"),
207
+ synchronize: async () => snapshot("plan"),
208
+ execute: async () => ({ status: "completed" }),
209
+ };
210
+
211
+ await runContinuationDriver({ maxTurns: 1, store, runtime });
212
+ await assert.rejects(
213
+ runContinuationDriver({ maxTurns: 2, store, runtime }),
214
+ /does not match the persisted mission budget 1/,
215
+ );
216
+ });
217
+
218
+ test("canonical completion clears a stale barrier without waiting", async () => {
219
+ const store = memoryStore({
220
+ schema_version: "1.0",
221
+ run_id: "run-251",
222
+ definition_id: "builder.build",
223
+ max_turns: 3,
224
+ adapter_command_identity: null,
225
+ status: "waiting",
226
+ turns_started: 1,
227
+ pending_barrier: { kind: "pid", pid: process.pid },
228
+ updated_at: "2026-07-12T12:00:00.000Z",
229
+ });
230
+ let waitCalls = 0;
231
+
232
+ const result = await runContinuationDriver({
233
+ maxTurns: 3,
234
+ store,
235
+ runtime: {
236
+ inspect: async () => snapshot("done", "completed"),
237
+ synchronize: async () => assert.fail("terminal inspection must not synchronize"),
238
+ execute: async () => assert.fail("terminal inspection must not execute"),
239
+ },
240
+ waitForBarrier: async () => {
241
+ waitCalls += 1;
242
+ return "pending";
243
+ },
244
+ });
245
+
246
+ assert.equal(result.outcome, "done");
247
+ assert.equal(waitCalls, 0);
248
+ assert.equal(store.value().pending_barrier, null);
249
+ });
250
+
251
+ test("adapter completion cannot declare the workflow done while canonical Flow remains active", async () => {
252
+ const store = memoryStore();
253
+ const runtime = {
254
+ inspect: async () => snapshot("plan"),
255
+ synchronize: async () => snapshot("plan"),
256
+ execute: async () => ({ status: "completed", summary: "I am done" }),
257
+ };
258
+
259
+ const result = await runContinuationDriver({ maxTurns: 1, store, runtime });
260
+ assert.equal(result.outcome, "budget_exhausted");
261
+ assert.equal(result.snapshot.status, "active");
262
+ });
263
+
264
+ test("adapter errors fail open to another bounded turn and remain auditable", async () => {
265
+ const store = memoryStore();
266
+ let executeCalls = 0;
267
+ const result = await runContinuationDriver({
268
+ maxTurns: 2,
269
+ store,
270
+ runtime: {
271
+ inspect: async () => snapshot("plan"),
272
+ synchronize: async () => snapshot("plan"),
273
+ execute: async () => {
274
+ executeCalls += 1;
275
+ throw new Error("runtime unavailable");
276
+ },
277
+ },
278
+ });
279
+
280
+ assert.equal(result.outcome, "budget_exhausted");
281
+ assert.equal(executeCalls, 2);
282
+ assert.deepEqual(store.events.filter((event) => event.type === "turn_failed").map((event) => event.summary), [
283
+ "runtime unavailable",
284
+ "runtime unavailable",
285
+ ]);
286
+ assert.deepEqual(store.events.filter((event) => event.type === "turn_failed").map((event) => event.failure_kind), ["adapter_error", "adapter_error"]);
287
+ });
288
+
289
+ test("authority cleanup failures are audited and cannot replace the adapter outcome", async () => {
290
+ const store = memoryStore();
291
+ const result = await runContinuationDriver({
292
+ maxTurns: 1,
293
+ store,
294
+ issueTurnAuthority: async () => ({
295
+ runId: "run-251",
296
+ turnSecret: "a".repeat(43),
297
+ publicKeyDigest: "a".repeat(64),
298
+ cleanup: () => { throw new Error("authority cleanup failed"); },
299
+ }),
300
+ runtime: {
301
+ inspect: async () => snapshot("plan"),
302
+ synchronize: async () => snapshot("plan"),
303
+ execute: async () => ({ status: "completed", summary: "adapter result survives cleanup" }),
304
+ },
305
+ });
306
+
307
+ assert.equal(result.outcome, "budget_exhausted");
308
+ assert.equal(store.value().active_turn_step, null);
309
+ assert.equal(store.value().active_turn_public_key_digest, null);
310
+ assert.equal(store.events.find((event) => event.type === "authority_cleanup_failed")?.summary, "authority cleanup failed");
311
+ });
312
+
313
+ test("false authority cleanup results are audited and cannot replace the adapter outcome", async () => {
314
+ const store = memoryStore();
315
+ const result = await runContinuationDriver({
316
+ maxTurns: 1,
317
+ store,
318
+ issueTurnAuthority: async () => ({
319
+ runId: "run-251",
320
+ turnSecret: "a".repeat(43),
321
+ publicKeyDigest: "a".repeat(64),
322
+ cleanup: () => false,
323
+ }),
324
+ runtime: {
325
+ inspect: async () => snapshot("plan"),
326
+ synchronize: async () => snapshot("plan"),
327
+ execute: async () => ({ status: "completed", summary: "adapter result survives cleanup" }),
328
+ },
329
+ });
330
+
331
+ assert.equal(result.outcome, "budget_exhausted");
332
+ assert.equal(store.value().active_turn_step, null);
333
+ assert.equal(store.value().active_turn_public_key_digest, null);
334
+ assert.equal(store.events.find((event) => event.type === "authority_cleanup_failed")?.summary, "continuation turn authority cleanup returned false");
335
+ });
336
+
337
+ test("terminal synchronization after an adapter error preserves the canonical done state", async () => {
338
+ const store = memoryStore();
339
+ let synchronizations = 0;
340
+ const result = await runContinuationDriver({
341
+ maxTurns: 2,
342
+ store,
343
+ runtime: {
344
+ inspect: async () => snapshot("plan"),
345
+ synchronize: async () => ++synchronizations === 1 ? snapshot("plan") : snapshot("done", "completed"),
346
+ execute: async () => { throw new Error("adapter transport failed"); },
347
+ },
348
+ });
349
+
350
+ assert.equal(result.outcome, "done");
351
+ assert.equal(store.value().status, "done");
352
+ assert.equal(store.value().active_turn_step, null);
353
+ assert.equal(store.events.find((event) => event.type === "turn_failed").failure_kind, "adapter_error");
354
+ });
355
+
356
+ test("terminal synchronization after an adapter timeout preserves the canonical failed state", async () => {
357
+ const store = memoryStore();
358
+ let synchronizations = 0;
359
+ const result = await runContinuationDriver({
360
+ maxTurns: 2,
361
+ store,
362
+ runtime: {
363
+ inspect: async () => snapshot("plan"),
364
+ synchronize: async () => ++synchronizations === 1 ? snapshot("plan") : snapshot("plan", "failed"),
365
+ execute: async () => { throw new ContinuationAdapterTimeoutError(10); },
366
+ },
367
+ });
368
+
369
+ assert.equal(result.outcome, "failed");
370
+ assert.equal(store.value().status, "failed");
371
+ assert.equal(store.value().active_turn_step, null);
372
+ assert.equal(store.events.find((event) => event.type === "turn_failed").failure_kind, "timeout");
373
+ });
374
+
375
+ test("driver classifies adapter timeout failures and records canonical non-advancement", async () => {
376
+ const store = memoryStore();
377
+ const result = await runContinuationDriver({
378
+ maxTurns: 1,
379
+ store,
380
+ runtime: {
381
+ inspect: async () => snapshot("plan"),
382
+ synchronize: async () => snapshot("plan"),
383
+ execute: async () => { throw new ContinuationAdapterTimeoutError(10); },
384
+ },
385
+ });
386
+ assert.equal(result.outcome, "budget_exhausted");
387
+ assert.equal(store.events.find((event) => event.type === "turn_failed").failure_kind, "timeout");
388
+
389
+ const forgedTextStore = memoryStore();
390
+ await runContinuationDriver({
391
+ maxTurns: 1,
392
+ store: forgedTextStore,
393
+ runtime: {
394
+ inspect: async () => snapshot("plan"),
395
+ synchronize: async () => snapshot("plan"),
396
+ execute: async () => { throw new Error("adapter stderr: timed out after 1ms"); },
397
+ },
398
+ });
399
+ assert.equal(forgedTextStore.events.find((event) => event.type === "turn_failed").failure_kind, "adapter_error");
400
+
401
+ const nonAdvancingStore = memoryStore();
402
+ const completed = await runContinuationDriver({
403
+ maxTurns: 1,
404
+ store: nonAdvancingStore,
405
+ runtime: {
406
+ inspect: async () => snapshot("plan"),
407
+ synchronize: async () => snapshot("plan"),
408
+ execute: async () => ({ status: "completed" }),
409
+ },
410
+ });
411
+ assert.equal(completed.outcome, "budget_exhausted");
412
+ assert.equal(nonAdvancingStore.events.some((event) => event.type === "gate_not_advanced"), true);
413
+ });
414
+
415
+ test("active turn authority is lock-bound, short-lived, and fails closed", async (t) => {
416
+ const sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-authority-"));
417
+ t.after(() => fs.rmSync(sessionDir, { recursive: true, force: true }));
418
+ const runId = path.basename(sessionDir);
419
+ const canonicalState = { run_id: runId, definition_id: "builder.build", current_step: "plan", status: "active" };
420
+ writeAuthorityAssignment(sessionDir, "codex:authority-test", { audit_trail: "x".repeat(20 * 1024) });
421
+ let issued;
422
+
423
+ await withContinuationDriverLock(sessionDir, async (lock) => {
424
+ const driverDir = path.join(sessionDir, "continuation-driver");
425
+ fs.writeFileSync(path.join(driverDir, "state.json"), JSON.stringify({
426
+ schema_version: "1.0",
427
+ run_id: runId,
428
+ definition_id: "builder.build",
429
+ max_turns: 2,
430
+ adapter_command_identity: "adapter-identity",
431
+ status: "active",
432
+ turns_started: 1,
433
+ active_turn_step: "plan",
434
+ pending_barrier: null,
435
+ audit_padding: "x".repeat(20 * 1024),
436
+ }));
437
+ issued = activeTurnAuthority.issueActiveTurnAuthority({
438
+ sessionDir,
439
+ runId,
440
+ definitionId: "builder.build",
441
+ currentStep: "plan",
442
+ iteration: 1,
443
+ maxTurns: 2,
444
+ adapterCommandIdentity: "adapter-identity",
445
+ assignmentActor: "codex:authority-test",
446
+ assignmentActorStruct: authorityActorStruct,
447
+ lock,
448
+ timeoutMs: 10_000,
449
+ });
450
+ bindAuthoritySigner(sessionDir, issued);
451
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
452
+ sessionDir,
453
+ runId: issued.runId,
454
+ turnSecret: issued.turnSecret,
455
+ assignmentActor: "codex:authority-test",
456
+ canonicalState,
457
+ }).valid, true);
458
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
459
+ sessionDir,
460
+ runId: issued.runId,
461
+ turnSecret: "A".repeat(43),
462
+ assignmentActor: "codex:authority-test",
463
+ canonicalState,
464
+ }).valid, false);
465
+ assert.equal(activeTurnAuthority.validateSignedActiveTurnAssignmentAuthority({
466
+ sessionDir,
467
+ runId: issued.runId,
468
+ turnSecret: issued.turnSecret,
469
+ }).valid, true, "the base validator returns the signed assignment without trusting the caller identity");
470
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
471
+ sessionDir,
472
+ runId: issued.runId,
473
+ turnSecret: issued.turnSecret,
474
+ assignmentActor: "codex:authority-test",
475
+ canonicalState: { ...canonicalState, current_step: "verify" },
476
+ }).valid, true, "canonical Flow-owned progression remains authorized");
477
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
478
+ sessionDir,
479
+ runId: issued.runId,
480
+ turnSecret: issued.turnSecret,
481
+ assignmentActor: "codex:authority-test",
482
+ canonicalState,
483
+ now: new Date(Date.now() + 20_000),
484
+ }).valid, false, "expired authority is rejected");
485
+ const missionFile = path.join(driverDir, "state.json");
486
+ const mission = JSON.parse(fs.readFileSync(missionFile, "utf8"));
487
+ fs.writeFileSync(missionFile, JSON.stringify({ ...mission, adapter_command_identity: "other-adapter" }));
488
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
489
+ sessionDir,
490
+ runId: issued.runId,
491
+ turnSecret: issued.turnSecret,
492
+ assignmentActor: "codex:authority-test",
493
+ canonicalState,
494
+ }).valid, false, "adapter identity mismatch is rejected");
495
+ fs.writeFileSync(missionFile, JSON.stringify(mission));
496
+ const authorityFile = activeTurnAuthority.activeTurnFile(sessionDir);
497
+ const record = JSON.parse(fs.readFileSync(authorityFile, "utf8"));
498
+ assert.equal(Object.hasOwn(record, "turnSecret"), false, "the raw turn secret is never persisted");
499
+ assert.match(record.turn_secret_sha256, /^[0-9a-f]{64}$/);
500
+ fs.writeFileSync(authorityFile, JSON.stringify({ ...record, max_turns: 3 }));
501
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
502
+ sessionDir, runId: issued.runId, turnSecret: issued.turnSecret, assignmentActor: "codex:authority-test", canonicalState,
503
+ }).valid, false, "a modified signed authority is rejected");
504
+ fs.writeFileSync(authorityFile, JSON.stringify(record));
505
+
506
+ const attacker = generateKeyPairSync("ed25519");
507
+ const attackerSecret = randomBytes(32).toString("base64url");
508
+ const attackerPublicKey = attacker.publicKey.export({ type: "spki", format: "der" }).toString("base64");
509
+ const forged = {
510
+ ...record,
511
+ turn_secret_sha256: createHash("sha256").update(attackerSecret).digest("hex"),
512
+ public_key_spki_b64: attackerPublicKey,
513
+ public_key_digest: createHash("sha256").update(attackerPublicKey).digest("hex"),
514
+ };
515
+ delete forged.signature_b64;
516
+ forged.signature_b64 = sign(null, canonicalBytes(forged), attacker.privateKey).toString("base64");
517
+ fs.writeFileSync(authorityFile, JSON.stringify(forged));
518
+ const forgedValidation = activeTurnAuthority.validateSignedActiveTurnAssignmentAuthority({
519
+ sessionDir, runId: issued.runId, turnSecret: attackerSecret,
520
+ });
521
+ assert.equal(forgedValidation.valid, false, "a self-consistent attacker record is rejected by the mission signer anchor");
522
+ assert.match(forgedValidation.reason, /driver mission does not match/);
523
+ fs.writeFileSync(authorityFile, JSON.stringify(record));
524
+ });
525
+
526
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
527
+ sessionDir,
528
+ runId: issued.runId,
529
+ turnSecret: issued.turnSecret,
530
+ assignmentActor: "codex:authority-test",
531
+ canonicalState,
532
+ }).valid, false, "a dead driver lock must not authorize Stop");
533
+ assert.equal(issued.cleanup(), true);
534
+ assert.equal(fs.existsSync(activeTurnAuthority.activeTurnFile(sessionDir)), false, "cleanup removes only the matching authority");
535
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
536
+ sessionDir,
537
+ runId: issued.runId,
538
+ turnSecret: issued.turnSecret,
539
+ assignmentActor: "codex:authority-test",
540
+ canonicalState,
541
+ }).valid, false, "replayed authority is rejected");
542
+ fs.symlinkSync(path.join(os.tmpdir(), "outside-authority"), activeTurnAuthority.activeTurnFile(sessionDir));
543
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
544
+ sessionDir,
545
+ runId: issued.runId,
546
+ turnSecret: issued.turnSecret,
547
+ assignmentActor: "codex:authority-test",
548
+ canonicalState,
549
+ }).valid, false, "symlinked authority is rejected");
550
+ });
551
+
552
+ test("authority cleanup leaves an expiring record when a parent is replaced", (t) => {
553
+ const sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-authority-parent-"));
554
+ t.after(() => fs.rmSync(sessionDir, { recursive: true, force: true }));
555
+ const runId = path.basename(sessionDir);
556
+ writeAuthorityAssignment(sessionDir, "codex:authority-test");
557
+ const driverDir = path.join(sessionDir, "continuation-driver");
558
+ const lock = { pid: process.pid, token: "parent-replacement", created_at: new Date().toISOString() };
559
+ fs.mkdirSync(path.join(driverDir, "locks"), { recursive: true });
560
+ fs.writeFileSync(path.join(driverDir, "locks", `${lock.pid}-${lock.token}.lock`), JSON.stringify({ schema_version: "1.0", ...lock }));
561
+ fs.writeFileSync(path.join(driverDir, "state.json"), JSON.stringify({
562
+ schema_version: "1.0", run_id: runId, definition_id: "builder.build", max_turns: 1,
563
+ adapter_command_identity: "adapter-identity", status: "active", turns_started: 1, active_turn_step: "plan", pending_barrier: null,
564
+ }));
565
+ const issued = activeTurnAuthority.issueActiveTurnAuthority({
566
+ sessionDir, runId, definitionId: "builder.build", currentStep: "plan", iteration: 1, maxTurns: 1,
567
+ adapterCommandIdentity: "adapter-identity", assignmentActor: "codex:authority-test", assignmentActorStruct: authorityActorStruct, lock, timeoutMs: 10_000,
568
+ });
569
+ bindAuthoritySigner(sessionDir, issued);
570
+ const replaced = `${driverDir}.replaced`;
571
+ fs.renameSync(driverDir, replaced);
572
+ fs.mkdirSync(driverDir, { recursive: true });
573
+ assert.equal(issued.cleanup(), false);
574
+ assert.equal(fs.existsSync(path.join(replaced, "active-turn.json")), true, "cleanup never unlinks through a replaced parent");
575
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
576
+ sessionDir, runId: issued.runId, turnSecret: issued.turnSecret, assignmentActor: "codex:authority-test",
577
+ canonicalState: { run_id: runId, definition_id: "builder.build", current_step: "plan", status: "active" },
578
+ }).valid, false, "parent replacement fails closed");
579
+ });
580
+
581
+ test("authority write leaves its temporary record when a parent is replaced", (t) => {
582
+ const sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-authority-write-parent-"));
583
+ t.after(() => fs.rmSync(sessionDir, { recursive: true, force: true }));
584
+ const runId = path.basename(sessionDir);
585
+ writeAuthorityAssignment(sessionDir, "codex:authority-test");
586
+ const driverDir = path.join(sessionDir, "continuation-driver");
587
+ const lock = { pid: process.pid, token: "write-parent-replacement", created_at: new Date().toISOString() };
588
+ fs.mkdirSync(path.join(driverDir, "locks"), { recursive: true });
589
+ fs.writeFileSync(path.join(driverDir, "locks", `${lock.pid}-${lock.token}.lock`), JSON.stringify({ schema_version: "1.0", ...lock }));
590
+ fs.writeFileSync(path.join(driverDir, "state.json"), JSON.stringify({
591
+ schema_version: "1.0", run_id: runId, definition_id: "builder.build", max_turns: 1,
592
+ adapter_command_identity: "adapter-identity", status: "active", turns_started: 1, active_turn_step: "plan", pending_barrier: null,
593
+ }));
594
+ const originalRename = fs.renameSync;
595
+ const replaced = `${driverDir}.replaced`;
596
+ fs.renameSync = (source, target) => {
597
+ if (target === activeTurnAuthority.activeTurnFile(sessionDir)) {
598
+ originalRename(driverDir, replaced);
599
+ fs.mkdirSync(driverDir, { recursive: true });
600
+ throw new Error("injected parent replacement");
601
+ }
602
+ return originalRename(source, target);
603
+ };
604
+ try {
605
+ assert.throws(() => activeTurnAuthority.issueActiveTurnAuthority({
606
+ sessionDir, runId, definitionId: "builder.build", currentStep: "plan", iteration: 1, maxTurns: 1,
607
+ adapterCommandIdentity: "adapter-identity", assignmentActor: "codex:authority-test", assignmentActorStruct: authorityActorStruct, lock, timeoutMs: 10_000,
608
+ }), /injected parent replacement/);
609
+ } finally {
610
+ fs.renameSync = originalRename;
611
+ }
612
+ assert.equal(fs.readdirSync(replaced).some((name) => /^\.active-turn\.json\..+\.tmp$/.test(name)), true, "temporary authority record is left when its parent identity changes");
613
+ });
614
+
615
+ test("a signed authority rejects a stale lock from an exited driver", (t) => {
616
+ const sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-authority-stale-lock-"));
617
+ t.after(() => fs.rmSync(sessionDir, { recursive: true, force: true }));
618
+ const runId = path.basename(sessionDir);
619
+ writeAuthorityAssignment(sessionDir, "codex:authority-test");
620
+ const exited = spawnSync(process.execPath, ["-e", "process.exit(0)"]);
621
+ assert.ok(exited.pid);
622
+ const lock = { pid: exited.pid, token: "stale-lock", created_at: new Date().toISOString() };
623
+ const driverDir = path.join(sessionDir, "continuation-driver");
624
+ fs.mkdirSync(path.join(driverDir, "locks"), { recursive: true });
625
+ fs.writeFileSync(path.join(driverDir, "locks", `${lock.pid}-${lock.token}.lock`), JSON.stringify({ schema_version: "1.0", ...lock }));
626
+ fs.writeFileSync(path.join(driverDir, "state.json"), JSON.stringify({
627
+ schema_version: "1.0", run_id: runId, definition_id: "builder.build", max_turns: 1,
628
+ adapter_command_identity: "adapter-identity", status: "active", turns_started: 1, active_turn_step: "plan", pending_barrier: null,
629
+ }));
630
+ const issued = activeTurnAuthority.issueActiveTurnAuthority({
631
+ sessionDir, runId, definitionId: "builder.build", currentStep: "plan", iteration: 1, maxTurns: 1,
632
+ adapterCommandIdentity: "adapter-identity", assignmentActor: "codex:authority-test", assignmentActorStruct: authorityActorStruct, lock, timeoutMs: 10_000,
633
+ });
634
+ bindAuthoritySigner(sessionDir, issued);
635
+ assert.equal(activeTurnAuthority.validateActiveTurnAuthority({
636
+ sessionDir, runId: issued.runId, turnSecret: issued.turnSecret, assignmentActor: "codex:authority-test",
637
+ canonicalState: { run_id: runId, definition_id: "builder.build", current_step: "plan", status: "active" },
638
+ }).valid, false, "a signed record alone cannot authorize after its driver exits");
639
+ });
640
+
641
+ test("authority validation rejects an active-turn file atomically replaced after descriptor read", (t) => {
642
+ const sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-authority-final-race-"));
643
+ t.after(() => fs.rmSync(sessionDir, { recursive: true, force: true }));
644
+ const runId = path.basename(sessionDir);
645
+ writeAuthorityAssignment(sessionDir, "codex:authority-test");
646
+ const lock = { pid: process.pid, token: "final-file-replacement", created_at: new Date().toISOString() };
647
+ const driverDir = path.join(sessionDir, "continuation-driver");
648
+ fs.mkdirSync(path.join(driverDir, "locks"), { recursive: true });
649
+ fs.writeFileSync(path.join(driverDir, "locks", `${lock.pid}-${lock.token}.lock`), JSON.stringify({ schema_version: "1.0", ...lock }));
650
+ fs.writeFileSync(path.join(driverDir, "state.json"), JSON.stringify({
651
+ schema_version: "1.0", run_id: runId, definition_id: "builder.build", max_turns: 1,
652
+ adapter_command_identity: "adapter-identity", status: "active", turns_started: 1, active_turn_step: "plan", pending_barrier: null,
653
+ }));
654
+ const issued = activeTurnAuthority.issueActiveTurnAuthority({
655
+ sessionDir, runId, definitionId: "builder.build", currentStep: "plan", iteration: 1, maxTurns: 1,
656
+ adapterCommandIdentity: "adapter-identity", assignmentActor: "codex:authority-test", assignmentActorStruct: authorityActorStruct, lock, timeoutMs: 10_000,
657
+ });
658
+ bindAuthoritySigner(sessionDir, issued);
659
+ const authorityFile = activeTurnAuthority.activeTurnFile(sessionDir);
660
+ const replacement = path.join(driverDir, "replacement-active-turn.json");
661
+ fs.copyFileSync(authorityFile, replacement);
662
+ const originalReadFile = fs.readFileSync;
663
+ let replaced = false;
664
+ fs.readFileSync = (target, ...args) => {
665
+ const bytes = originalReadFile(target, ...args);
666
+ if (!replaced && typeof target === "number") {
667
+ replaced = true;
668
+ fs.renameSync(replacement, authorityFile);
669
+ }
670
+ return bytes;
671
+ };
672
+ try {
673
+ const validation = activeTurnAuthority.validateActiveTurnAuthority({
674
+ sessionDir, runId: issued.runId, turnSecret: issued.turnSecret, assignmentActor: "codex:authority-test",
675
+ canonicalState: { run_id: runId, definition_id: "builder.build", current_step: "plan", status: "active" },
676
+ });
677
+ assert.equal(replaced, true, "test replaced the final pathname after descriptor read");
678
+ assert.equal(validation.valid, false, "descriptor bytes are rejected when the final pathname inode changed");
679
+ assert.match(validation.reason, /identity changed after reading/);
680
+ } finally {
681
+ fs.readFileSync = originalReadFile;
682
+ }
683
+ });
684
+
685
+ test("canonical Flow state rejects final-path replacement after descriptor read", (t) => {
686
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-canonical-race-"));
687
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
688
+ const slug = "canonical-race-run";
689
+ const sessionDir = path.join(root, ".kontourai", "flow-agents", slug);
690
+ const runDir = path.join(root, ".kontourai", "flow", "runs", slug);
691
+ fs.mkdirSync(sessionDir, { recursive: true });
692
+ fs.mkdirSync(runDir, { recursive: true });
693
+ const stateFile = path.join(runDir, "state.json");
694
+ const replacement = path.join(runDir, "replacement-state.json");
695
+ const state = { run_id: slug, definition_id: "builder.build", definition_version: "1.0", status: "active", current_step: "plan" };
696
+ fs.writeFileSync(stateFile, JSON.stringify(state));
697
+ fs.writeFileSync(replacement, JSON.stringify(state));
698
+ fs.writeFileSync(path.join(runDir, "definition.json"), JSON.stringify({ id: "builder.build", version: "1.0", steps: [{ id: "plan" }] }));
699
+ const originalReadFile = fs.readFileSync;
700
+ let replaced = false;
701
+ fs.readFileSync = (target, ...args) => {
702
+ const bytes = originalReadFile(target, ...args);
703
+ if (!replaced && typeof target === "number") {
704
+ replaced = true;
705
+ fs.renameSync(replacement, stateFile);
706
+ }
707
+ return bytes;
708
+ };
709
+ try {
710
+ const result = stopGoalFit.canonicalFlowState(root, sessionDir);
711
+ assert.equal(replaced, true, "test replaced the canonical final pathname after descriptor read");
712
+ assert.equal(result.state, null);
713
+ assert.match(result.error, /identity changed after reading/);
714
+ } finally {
715
+ fs.readFileSync = originalReadFile;
716
+ }
717
+ });
718
+
719
+ test("canonical Flow state rejects an unknown definition step and a replaced run parent", (t) => {
720
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-canonical-definition-"));
721
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
722
+ const slug = "canonical-definition-run";
723
+ const sessionDir = path.join(root, ".kontourai", "flow-agents", slug);
724
+ const runDir = path.join(root, ".kontourai", "flow", "runs", slug);
725
+ fs.mkdirSync(sessionDir, { recursive: true });
726
+ fs.mkdirSync(runDir, { recursive: true });
727
+ const state = { run_id: slug, definition_id: "builder.build", definition_version: "1.0", status: "active", current_step: "unknown" };
728
+ const definition = { id: "builder.build", version: "1.0", steps: [{ id: "plan" }] };
729
+ fs.writeFileSync(path.join(runDir, "state.json"), JSON.stringify(state));
730
+ fs.writeFileSync(path.join(runDir, "definition.json"), JSON.stringify(definition));
731
+ assert.match(stopGoalFit.canonicalFlowState(root, sessionDir).error, /current_step is not present/);
732
+
733
+ fs.writeFileSync(path.join(runDir, "state.json"), JSON.stringify({ ...state, current_step: "plan" }));
734
+ const originalReadFile = fs.readFileSync;
735
+ const replacedDir = `${runDir}.replaced`;
736
+ let descriptorReads = 0;
737
+ let replaced = false;
738
+ fs.readFileSync = (target, ...args) => {
739
+ const bytes = originalReadFile(target, ...args);
740
+ if (typeof target === "number" && ++descriptorReads === 2) {
741
+ fs.renameSync(runDir, replacedDir);
742
+ fs.mkdirSync(runDir);
743
+ fs.writeFileSync(path.join(runDir, "state.json"), JSON.stringify({ ...state, current_step: "plan" }));
744
+ fs.writeFileSync(path.join(runDir, "definition.json"), JSON.stringify(definition));
745
+ replaced = true;
746
+ }
747
+ return bytes;
748
+ };
749
+ try {
750
+ const result = stopGoalFit.canonicalFlowState(root, sessionDir);
751
+ assert.equal(replaced, true, "test replaced the canonical run parent during definition read");
752
+ assert.equal(result.state, null);
753
+ assert.match(result.error, /identity changed|parent identity changed/);
754
+ } finally {
755
+ fs.readFileSync = originalReadFile;
756
+ }
757
+ });
758
+
759
+ test("Stop ignores an unvalidated capability locator and preserves ordinary pointer selection", async (t) => {
760
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-invalid-locator-"));
761
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
762
+ fs.writeFileSync(path.join(root, "AGENTS.md"), "# Test Repo\n");
763
+ const artifactRoot = path.join(root, ".kontourai", "flow-agents");
764
+ const ordinarySlug = "ordinary-pointer-run";
765
+ const unvalidatedSlug = "unvalidated-env-run";
766
+ for (const slug of [ordinarySlug, unvalidatedSlug]) {
767
+ const dir = path.join(artifactRoot, slug);
768
+ fs.mkdirSync(dir, { recursive: true });
769
+ fs.writeFileSync(path.join(dir, `${slug}--deliver.md`), `# ${slug}\n\nstatus: planned\ntype: deliver\n`);
770
+ fs.writeFileSync(path.join(dir, "state.json"), JSON.stringify({
771
+ schema_version: "1.0", task_slug: slug, status: "planned", phase: "planning",
772
+ updated_at: new Date().toISOString(), next_action: { status: "continue", summary: "Continue" },
773
+ }));
774
+ }
775
+ fs.writeFileSync(path.join(artifactRoot, "current.json"), JSON.stringify({ active_slug: ordinarySlug, artifact_dir: ordinarySlug }));
776
+ const priorSecret = process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET;
777
+ const priorRunId = process.env.FLOW_AGENTS_CONTINUATION_RUN_ID;
778
+ // #440 FIXTURE-GAP (post-rebase, #589 merge): this fixture only ever writes the legacy
779
+ // current.json, never a per-actor pointer -- under a RESOLVED ambient actor,
780
+ // stop-goal-fit.js's ownership-scoped analyze() now scopes to that actor's own (nonexistent)
781
+ // pointer and never falls through to the legacy-named ordinary session at all. This test is
782
+ // about continuation-locator validation (an unvalidated locator must not override ordinary
783
+ // pointer selection), not #440's per-actor ownership scoping, so forcing the documented
784
+ // test-only unresolved-actor escape hatch restores the legacy-fallback path this fixture was
785
+ // written against (D3 compat), matching the same fix applied throughout evals/ for this exact
786
+ // class of fixture gap.
787
+ const priorForceUnresolved = process.env.FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED;
788
+ const priorNodeEnv = process.env.NODE_ENV;
789
+ process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET = "A".repeat(43);
790
+ process.env.FLOW_AGENTS_CONTINUATION_RUN_ID = unvalidatedSlug;
791
+ process.env.FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED = "1";
792
+ process.env.NODE_ENV = "test";
793
+ try {
794
+ const result = await stopGoalFit.analyze(root);
795
+ assert.equal(result.latestArtifactDir, path.join(artifactRoot, ordinarySlug));
796
+ } finally {
797
+ if (priorSecret === undefined) delete process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET;
798
+ else process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET = priorSecret;
799
+ if (priorRunId === undefined) delete process.env.FLOW_AGENTS_CONTINUATION_RUN_ID;
800
+ else process.env.FLOW_AGENTS_CONTINUATION_RUN_ID = priorRunId;
801
+ if (priorForceUnresolved === undefined) delete process.env.FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED;
802
+ else process.env.FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED = priorForceUnresolved;
803
+ if (priorNodeEnv === undefined) delete process.env.NODE_ENV;
804
+ else process.env.NODE_ENV = priorNodeEnv;
805
+ }
806
+ });
807
+
808
+ test("Stop keeps a base-valid signed session selected across paused and completed canonical dispositions", async (t) => {
809
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-exact-scope-"));
810
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
811
+ fs.writeFileSync(path.join(root, "AGENTS.md"), "# Test Repo\n");
812
+ const artifactRoot = path.join(root, ".kontourai", "flow-agents");
813
+ const exactSlug = "signed-exact-run";
814
+ const unrelatedSlug = "unrelated-pointer-run";
815
+ const exactDir = path.join(artifactRoot, exactSlug);
816
+ const unrelatedDir = path.join(artifactRoot, unrelatedSlug);
817
+ const runDir = path.join(root, ".kontourai", "flow", "runs", exactSlug);
818
+ fs.mkdirSync(path.join(exactDir, "continuation-driver", "locks"), { recursive: true });
819
+ fs.mkdirSync(unrelatedDir, { recursive: true });
820
+ fs.mkdirSync(runDir, { recursive: true });
821
+ fs.writeFileSync(path.join(exactDir, `${exactSlug}--deliver.md`), `# Exact\n\nstatus: planned\ntype: deliver\n`);
822
+ fs.writeFileSync(path.join(unrelatedDir, `${unrelatedSlug}--deliver.md`), `# Unrelated\n\nstatus: executing\ntype: deliver\n`);
823
+ const sidecar = (slug, flowStatus, status = "planned") => ({
824
+ schema_version: "1.0", task_slug: slug, status, phase: status === "delivered" ? "release" : "planning",
825
+ updated_at: new Date().toISOString(),
826
+ flow_run: { status: flowStatus, run_id: slug, definition_id: "builder.build", definition_version: "1.0", current_step: "plan" },
827
+ next_action: { status: flowStatus === "completed" ? "done" : "wait", summary: "Canonical disposition" },
828
+ });
829
+ fs.writeFileSync(path.join(unrelatedDir, "state.json"), JSON.stringify(sidecar(unrelatedSlug, "active", "executing")));
830
+ fs.writeFileSync(path.join(artifactRoot, "current.json"), JSON.stringify({ active_slug: unrelatedSlug }));
831
+ fs.mkdirSync(path.join(artifactRoot, "current"), { recursive: true });
832
+ fs.writeFileSync(path.join(artifactRoot, "current", "pointer-actor.json"), JSON.stringify({ active_slug: unrelatedSlug }));
833
+ fs.writeFileSync(path.join(runDir, "definition.json"), JSON.stringify({ id: "builder.build", version: "1.0", steps: [{ id: "plan" }] }));
834
+ writeAuthorityAssignment(exactDir, "driver-actor");
835
+ writeAuthorityAssignment(unrelatedDir, "driver-actor");
836
+ const lock = { pid: process.pid, token: "exact-scope", created_at: new Date().toISOString() };
837
+ fs.writeFileSync(path.join(exactDir, "continuation-driver", "locks", `${lock.pid}-${lock.token}.lock`), JSON.stringify({ schema_version: "1.0", ...lock }));
838
+ fs.writeFileSync(path.join(exactDir, "continuation-driver", "state.json"), JSON.stringify({
839
+ schema_version: "1.0", run_id: exactSlug, definition_id: "builder.build", max_turns: 2,
840
+ adapter_command_identity: "adapter-identity", status: "active", turns_started: 1,
841
+ active_turn_step: "plan", pending_barrier: null,
842
+ }));
843
+ const issued = activeTurnAuthority.issueActiveTurnAuthority({
844
+ sessionDir: exactDir, runId: exactSlug, definitionId: "builder.build", currentStep: "plan", iteration: 1, maxTurns: 2,
845
+ adapterCommandIdentity: "adapter-identity", assignmentActor: "driver-actor", assignmentActorStruct: authorityActorStruct,
846
+ lock, timeoutMs: 30_000,
847
+ });
848
+ bindAuthoritySigner(exactDir, issued);
849
+ const prior = {
850
+ actor: process.env.FLOW_AGENTS_ACTOR,
851
+ secret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET,
852
+ runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID,
853
+ };
854
+ process.env.FLOW_AGENTS_ACTOR = "pointer-actor";
855
+ process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET = issued.turnSecret;
856
+ process.env.FLOW_AGENTS_CONTINUATION_RUN_ID = issued.runId;
857
+ try {
858
+ for (const canonicalStatus of ["paused", "needs_decision", "completed"]) {
859
+ const terminal = canonicalStatus === "completed";
860
+ fs.writeFileSync(path.join(runDir, "state.json"), JSON.stringify({
861
+ run_id: exactSlug, definition_id: "builder.build", definition_version: "1.0", status: canonicalStatus, current_step: "plan",
862
+ }));
863
+ fs.writeFileSync(path.join(exactDir, "state.json"), JSON.stringify(sidecar(exactSlug, canonicalStatus, terminal ? "delivered" : "planned")));
864
+ const analyzed = await stopGoalFit.analyze(root);
865
+ assert.equal(analyzed.latestArtifactDir, exactDir, `${canonicalStatus} remains scoped to the signed run`);
866
+ assert.equal(analyzed.activeTurnAuthority, false, `${canonicalStatus} cannot relax the active gate`);
867
+ stopGoalFit.releaseOnNonTerminalStop(root, analyzed.latestArtifactDir);
868
+ assert.equal(JSON.parse(fs.readFileSync(path.join(artifactRoot, "assignment", `${exactSlug}.json`), "utf8")).status, "claimed");
869
+ assert.equal(JSON.parse(fs.readFileSync(path.join(artifactRoot, "assignment", `${unrelatedSlug}.json`), "utf8")).status, "claimed");
870
+ }
871
+ const liveness = path.join(artifactRoot, "liveness", "events.jsonl");
872
+ assert.equal(fs.existsSync(liveness) ? fs.readFileSync(liveness, "utf8").includes('"type":"release"') : false, false);
873
+ } finally {
874
+ if (prior.actor === undefined) delete process.env.FLOW_AGENTS_ACTOR; else process.env.FLOW_AGENTS_ACTOR = prior.actor;
875
+ if (prior.secret === undefined) delete process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET; else process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET = prior.secret;
876
+ if (prior.runId === undefined) delete process.env.FLOW_AGENTS_CONTINUATION_RUN_ID; else process.env.FLOW_AGENTS_CONTINUATION_RUN_ID = prior.runId;
877
+ }
878
+ });
879
+
880
+ test("artifact validation skips only a private continuation driver child", (t) => {
881
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-validator-"));
882
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
883
+ const session = path.join(root, "session-a");
884
+ fs.mkdirSync(path.join(session, "continuation-driver"), { recursive: true });
885
+ fs.writeFileSync(path.join(session, "state.json"), JSON.stringify({
886
+ schema_version: "1.0", task_slug: "session-a", status: "planned", phase: "planning",
887
+ updated_at: "2026-07-13T00:00:00Z", next_action: { status: "continue", summary: "Continue" },
888
+ }));
889
+ fs.writeFileSync(path.join(session, "continuation-driver", "state.json"), "{}\n");
890
+ execFileSync(process.execPath, ["build/src/cli/validate-workflow-artifacts.js", "--skip-markdown-validation", session], { cwd: path.resolve(import.meta.dirname, "..", ".."), stdio: "pipe" });
891
+ const topLevel = path.join(root, "continuation-driver");
892
+ fs.mkdirSync(topLevel, { recursive: true });
893
+ fs.writeFileSync(path.join(topLevel, "state.json"), "{}\n");
894
+ assert.throws(() => execFileSync(process.execPath, ["build/src/cli/validate-workflow-artifacts.js", "--skip-markdown-validation", topLevel], {
895
+ cwd: path.resolve(import.meta.dirname, "..", ".."), stdio: "pipe",
896
+ }), /Command failed/);
897
+ });
898
+
899
+ test("max-block hard classification preserves the no-authority baseline and tightens only authorized turns", () => {
900
+ const ordinary = ".kontourai/flow-agents/session-a is still status:planned (1m old). Do not final-answer as complete unless the next step is explicit.";
901
+ const evidenceFailure = "evidence verdict:fail";
902
+ assert.equal(stopGoalFit.isHardStopWarning(ordinary, ".kontourai/flow-agents/session-a", false), false, "ordinary FULL_BLOCK stays releasable without authority");
903
+ assert.equal(stopGoalFit.isHardStopWarning(evidenceFailure, ".kontourai/flow-agents/session-a", false), true, "existing HARD_BLOCK stays permanent without authority");
904
+ assert.equal(stopGoalFit.isHardStopWarning(ordinary, ".kontourai/flow-agents/session-a", true), false, "ordinary active-gate warning is advisory for a valid authority");
905
+ assert.equal(stopGoalFit.isHardStopWarning("Definition Of Done is incomplete", ".kontourai/flow-agents/session-a", true), true, "nonordinary FULL_BLOCK stays permanent for a valid authority");
906
+ });
907
+
908
+ test("driver binds the adapter command identity for the mission", async () => {
909
+ const store = memoryStore();
910
+ const runtime = {
911
+ inspect: async () => snapshot("plan"),
912
+ synchronize: async () => snapshot("plan"),
913
+ execute: async () => ({ status: "completed" }),
914
+ };
915
+ await runContinuationDriver({ maxTurns: 1, adapterCommandIdentity: "adapter-a", store, runtime });
916
+ await assert.rejects(
917
+ runContinuationDriver({ maxTurns: 1, adapterCommandIdentity: "adapter-b", store, runtime }),
918
+ /adapter command identity does not match/,
919
+ );
920
+ });
921
+
922
+ test("driver honors adapter dispositions across canonical Flow statuses", async (t) => {
923
+ for (const [status, expected] of [
924
+ ["blocked", "budget_exhausted"],
925
+ ["needs_decision", "waiting"],
926
+ ["paused", "waiting"],
927
+ ["canceled", "done"],
928
+ ["completed", "done"],
929
+ ["accepted_by_exception", "budget_exhausted"],
930
+ ["failed", "failed"],
931
+ ]) {
932
+ await t.test(status, async () => {
933
+ let executeCalls = 0;
934
+ const result = await runContinuationDriver({
935
+ maxTurns: 1,
936
+ store: memoryStore(),
937
+ runtime: {
938
+ inspect: async () => snapshot("plan", status),
939
+ synchronize: async () => snapshot("plan", status),
940
+ execute: async () => { executeCalls += 1; return { status: "completed" }; },
941
+ },
942
+ });
943
+ assert.equal(result.outcome, expected);
944
+ assert.equal(executeCalls, expected === "budget_exhausted" ? 1 : 0);
945
+ });
946
+ }
947
+ });
948
+
949
+ test("driver reauthorizes immediately before each adapter turn", async () => {
950
+ let authorizations = 0;
951
+ let executions = 0;
952
+ await assert.rejects(
953
+ runContinuationDriver({
954
+ maxTurns: 3,
955
+ store: memoryStore(),
956
+ authorizeTurn: async () => {
957
+ authorizations += 1;
958
+ if (authorizations === 2) throw new Error("assignment ownership changed");
959
+ },
960
+ runtime: {
961
+ inspect: async () => snapshot("plan"),
962
+ synchronize: async () => snapshot("plan"),
963
+ execute: async () => { executions += 1; return { status: "completed" }; },
964
+ },
965
+ }),
966
+ /assignment ownership changed/,
967
+ );
968
+ assert.equal(authorizations, 2);
969
+ assert.equal(executions, 1);
970
+ });
971
+
972
+ test("explicit adapter argv receives one structured action without a shell", async (t) => {
973
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-adapter-"));
974
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
975
+ const adapter = path.join(root, "adapter.mjs");
976
+ const command = path.join(root, "command.json");
977
+ fs.writeFileSync(adapter, `
978
+ let input = "";
979
+ for await (const chunk of process.stdin) input += chunk;
980
+ const request = JSON.parse(input);
981
+ process.stdout.write(JSON.stringify({ status: "completed", summary: process.cwd() + "|" + request.current_step }));
982
+ `);
983
+ fs.writeFileSync(command, JSON.stringify({ argv: [process.execPath, adapter] }));
984
+ const result = await executeContinuationAdapter(command, {
985
+ schema_version: "1.0",
986
+ run_id: "run-251",
987
+ definition_id: "builder.build",
988
+ current_step: "plan",
989
+ iteration: 1,
990
+ max_turns: 3,
991
+ next_action: { status: "continue", skills: ["plan-work"] },
992
+ }, { cwd: root, timeoutMs: 5_000 });
993
+
994
+ assert.deepEqual(result, { status: "completed", summary: `${fs.realpathSync(root)}|plan` });
995
+ });
996
+
997
+ test("adapter descendants preserve ordinary actor resolution and receive only fresh turn capability companions", async (t) => {
998
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-adapter-actor-"));
999
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1000
+ const adapter = path.join(root, "adapter.mjs");
1001
+ const command = path.join(root, "command.json");
1002
+ fs.writeFileSync(adapter, `
1003
+ process.stdout.write(JSON.stringify({ status: "completed", summary: JSON.stringify({ ambientActor: process.env.FLOW_AGENTS_ACTOR || null, continuation: {
1004
+ turnSecret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET || null,
1005
+ runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID || null,
1006
+ obsoleteNonce: process.env.FLOW_AGENTS_CONTINUATION_TURN_NONCE || null,
1007
+ obsoletePublicKeyDigest: process.env.FLOW_AGENTS_CONTINUATION_TURN_PUBLIC_KEY_DIGEST || null,
1008
+ actorB64: process.env.FLOW_AGENTS_CONTINUATION_ACTOR_B64 || null,
1009
+ } }) }));
1010
+ `);
1011
+ fs.writeFileSync(command, JSON.stringify({ argv: [process.execPath, adapter] }));
1012
+ const variables = ["FLOW_AGENTS_ACTOR", "CODEX_THREAD_ID", "CODEX_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "OPENCODE_SESSION_ID", "PI_SESSION_ID", "FLOW_AGENTS_CONTINUATION_TURN_SECRET", "FLOW_AGENTS_CONTINUATION_RUN_ID", "FLOW_AGENTS_CONTINUATION_TURN_NONCE", "FLOW_AGENTS_CONTINUATION_TURN_PUBLIC_KEY_DIGEST", "FLOW_AGENTS_CONTINUATION_ACTOR_B64"];
1013
+ const prior = Object.fromEntries(variables.map((name) => [name, process.env[name]]));
1014
+ for (const name of variables) delete process.env[name];
1015
+ process.env.FLOW_AGENTS_ACTOR = "stale-parent-actor";
1016
+ process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET = "z".repeat(43);
1017
+ process.env.FLOW_AGENTS_CONTINUATION_RUN_ID = "stale-run";
1018
+ process.env.FLOW_AGENTS_CONTINUATION_TURN_NONCE = "00000000-0000-4000-8000-000000000099";
1019
+ process.env.FLOW_AGENTS_CONTINUATION_TURN_PUBLIC_KEY_DIGEST = "b".repeat(64);
1020
+ process.env.FLOW_AGENTS_CONTINUATION_ACTOR_B64 = "stale-capability";
1021
+ try {
1022
+ const inherited = await executeContinuationAdapter(command, {
1023
+ schema_version: "1.0", run_id: "run-251", definition_id: "builder.build", current_step: "plan", iteration: 1, max_turns: 1, next_action: null,
1024
+ }, { cwd: root, timeoutMs: 5_000 });
1025
+ const directChild = JSON.parse(inherited.summary);
1026
+ assert.equal(directChild.ambientActor, "stale-parent-actor", "direct adapter calls preserve their existing actor when no signed turn actor is supplied");
1027
+ assert.deepEqual(directChild.continuation, { turnSecret: null, runId: null, obsoleteNonce: null, obsoletePublicKeyDigest: null, actorB64: null }, "direct adapter calls never inherit a stale continuation capability");
1028
+ const result = await executeContinuationAdapter(command, {
1029
+ schema_version: "1.0", run_id: "run-251", definition_id: "builder.build", current_step: "plan", iteration: 1, max_turns: 1, next_action: null,
1030
+ }, {
1031
+ cwd: root,
1032
+ timeoutMs: 5_000,
1033
+ continuationTurnSecret: "a".repeat(43),
1034
+ continuationRunId: "run-251",
1035
+ });
1036
+ const child = JSON.parse(result.summary);
1037
+ assert.equal(child.ambientActor, "stale-parent-actor", "signed propagation does not overwrite FLOW_AGENTS_ACTOR");
1038
+ assert.deepEqual(child.continuation, {
1039
+ turnSecret: "a".repeat(43),
1040
+ runId: "run-251",
1041
+ obsoleteNonce: null,
1042
+ obsoletePublicKeyDigest: null,
1043
+ actorB64: null,
1044
+ });
1045
+ } finally {
1046
+ for (const name of variables) {
1047
+ if (prior[name] === undefined) delete process.env[name];
1048
+ else process.env[name] = prior[name];
1049
+ }
1050
+ }
1051
+ });
1052
+
1053
+ test("loaded adapter binds executable and script content for every turn", async (t) => {
1054
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-integrity-"));
1055
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1056
+ const adapter = path.join(root, "adapter.mjs");
1057
+ const command = path.join(root, "command.json");
1058
+ fs.writeFileSync(adapter, 'process.stdout.write(JSON.stringify({ status: "completed" }));\n');
1059
+ fs.writeFileSync(command, JSON.stringify({ argv: [process.execPath, adapter] }));
1060
+ const loaded = loadContinuationAdapterCommand(command);
1061
+ fs.writeFileSync(adapter, 'process.stdout.write(JSON.stringify({ status: "wait" }));\n');
1062
+
1063
+ await assert.rejects(
1064
+ executeLoadedContinuationAdapter(loaded, {
1065
+ schema_version: "1.0",
1066
+ run_id: "run-251",
1067
+ definition_id: "builder.build",
1068
+ current_step: "execute",
1069
+ iteration: 1,
1070
+ max_turns: 3,
1071
+ next_action: null,
1072
+ }, { cwd: root, timeoutMs: 5_000 }),
1073
+ /integrity changed after mission binding/,
1074
+ );
1075
+ });
1076
+
1077
+ test("adapter integrity hashes raw file bytes", async (t) => {
1078
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-binary-integrity-"));
1079
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1080
+ const adapter = path.join(root, "adapter.mjs");
1081
+ const data = path.join(root, "binary.dat");
1082
+ const command = path.join(root, "command.json");
1083
+ fs.writeFileSync(adapter, 'process.stdout.write(JSON.stringify({ status: "completed" }));\n');
1084
+ fs.writeFileSync(data, Buffer.from([0x80]));
1085
+ fs.writeFileSync(command, JSON.stringify({ argv: [process.execPath, adapter, data] }));
1086
+ const loaded = loadContinuationAdapterCommand(command);
1087
+ fs.writeFileSync(data, Buffer.from([0x81]));
1088
+
1089
+ await assert.rejects(
1090
+ executeLoadedContinuationAdapter(loaded, {
1091
+ schema_version: "1.0",
1092
+ run_id: "run-251",
1093
+ definition_id: "builder.build",
1094
+ current_step: "execute",
1095
+ iteration: 1,
1096
+ max_turns: 3,
1097
+ next_action: null,
1098
+ }, { cwd: root, timeoutMs: 5_000 }),
1099
+ /integrity changed after mission binding/,
1100
+ );
1101
+ });
1102
+
1103
+ test("adapter timeout terminates its process group", { skip: process.platform === "win32" }, async (t) => {
1104
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-timeout-"));
1105
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1106
+ const adapter = path.join(root, "adapter.mjs");
1107
+ const command = path.join(root, "command.json");
1108
+ const marker = path.join(root, "orphaned-child-ran");
1109
+ fs.writeFileSync(adapter, `
1110
+ import { spawn } from "node:child_process";
1111
+ spawn(process.execPath, ["-e", ${JSON.stringify(`setTimeout(() => require("node:fs").writeFileSync(${JSON.stringify(marker)}, "ran"), 300)`) }], { stdio: "ignore" });
1112
+ setInterval(() => {}, 1000);
1113
+ `);
1114
+ fs.writeFileSync(command, JSON.stringify({ argv: [process.execPath, adapter] }));
1115
+
1116
+ await assert.rejects(
1117
+ executeContinuationAdapter(command, {
1118
+ schema_version: "1.0",
1119
+ run_id: "run-251",
1120
+ definition_id: "builder.build",
1121
+ current_step: "execute",
1122
+ iteration: 1,
1123
+ max_turns: 3,
1124
+ next_action: null,
1125
+ }, { cwd: root, timeoutMs: 50 }),
1126
+ (error) => error instanceof ContinuationAdapterTimeoutError && /timed out after 50ms/.test(error.message),
1127
+ );
1128
+ await new Promise((resolve) => setTimeout(resolve, 400));
1129
+ assert.equal(fs.existsSync(marker), false);
1130
+ });
1131
+
1132
+ test("adapter stderr with timeout-like text remains an adapter_error", async (t) => {
1133
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-timeout-stderr-"));
1134
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1135
+ const adapter = path.join(root, "adapter.mjs");
1136
+ const command = path.join(root, "command.json");
1137
+ fs.writeFileSync(adapter, 'process.stderr.write("timed out after 1ms\\n"); process.exit(7);\n');
1138
+ fs.writeFileSync(command, JSON.stringify({ argv: [process.execPath, adapter] }));
1139
+ const store = memoryStore();
1140
+
1141
+ await runContinuationDriver({
1142
+ maxTurns: 1,
1143
+ store,
1144
+ runtime: {
1145
+ inspect: async () => snapshot("plan"),
1146
+ synchronize: async () => snapshot("plan"),
1147
+ execute: async (request) => executeContinuationAdapter(command, request, { cwd: root, timeoutMs: 5_000 }),
1148
+ },
1149
+ });
1150
+
1151
+ const failure = store.events.find((event) => event.type === "turn_failed");
1152
+ assert.equal(failure.failure_kind, "adapter_error");
1153
+ assert.match(failure.summary, /timed out after 1ms/);
1154
+ });
1155
+
1156
+ test("completed adapter forcibly terminates background descendants", { skip: process.platform === "win32" }, async (t) => {
1157
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-completed-child-"));
1158
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1159
+ const adapter = path.join(root, "adapter.mjs");
1160
+ const command = path.join(root, "command.json");
1161
+ const marker = path.join(root, "background-child-ran");
1162
+ const childSource = `
1163
+ process.on("SIGTERM", () => {});
1164
+ setTimeout(() => require("node:fs").writeFileSync(${JSON.stringify(marker)}, "ran"), 600);
1165
+ setInterval(() => {}, 1000);
1166
+ `;
1167
+ fs.writeFileSync(adapter, `
1168
+ import { spawn } from "node:child_process";
1169
+ spawn(process.execPath, ["-e", ${JSON.stringify(childSource)}], { stdio: "ignore" }).unref();
1170
+ process.stdout.write(JSON.stringify({ status: "completed" }));
1171
+ `);
1172
+ fs.writeFileSync(command, JSON.stringify({ argv: [process.execPath, adapter] }));
1173
+
1174
+ await executeContinuationAdapter(command, {
1175
+ schema_version: "1.0",
1176
+ run_id: "run-251",
1177
+ definition_id: "builder.build",
1178
+ current_step: "execute",
1179
+ iteration: 1,
1180
+ max_turns: 3,
1181
+ next_action: null,
1182
+ }, { cwd: root, timeoutMs: 5_000 });
1183
+ await new Promise((resolve) => setTimeout(resolve, 750));
1184
+ assert.equal(fs.existsSync(marker), false);
1185
+ });
1186
+
1187
+ test("deadline and pid barriers report ready or pending without consuming turns", async () => {
1188
+ let clock = Date.parse("2026-07-12T12:00:00.000Z");
1189
+ const deadline = await waitForContinuationBarrier(
1190
+ { kind: "deadline", at: "2026-07-12T12:00:01.000Z" },
1191
+ { maxWaitMs: 1_000, pollMs: 10, now: () => clock, sleep: async (ms) => { clock += ms; } },
1192
+ );
1193
+ assert.equal(deadline, "ready");
1194
+
1195
+ const alive = await waitForContinuationBarrier(
1196
+ { kind: "pid", pid: process.pid },
1197
+ { maxWaitMs: 0, pollMs: 10 },
1198
+ );
1199
+ assert.equal(alive, "pending");
1200
+
1201
+ const gone = await waitForContinuationBarrier(
1202
+ { kind: "pid", pid: 2_147_483_647 },
1203
+ { maxWaitMs: 0, pollMs: 10 },
1204
+ );
1205
+ assert.equal(gone, "ready");
1206
+ });
1207
+
1208
+ test("file continuation store refuses a symlinked state target", (t) => {
1209
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-store-"));
1210
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1211
+ const store = createFileContinuationStore(root);
1212
+ const outside = path.join(root, "outside.json");
1213
+ fs.writeFileSync(outside, "{}\n");
1214
+ fs.symlinkSync(outside, path.join(root, "continuation-driver", "state.json"));
1215
+ assert.throws(() => store.load(), /must be a regular file/);
1216
+ });
1217
+
1218
+ test("file continuation store detects deleted and rolled-back mission state", (t) => {
1219
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-rollback-"));
1220
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1221
+ const store = createFileContinuationStore(root);
1222
+ store.save({
1223
+ schema_version: "1.0",
1224
+ run_id: "run-251",
1225
+ definition_id: "builder.build",
1226
+ max_turns: 3,
1227
+ adapter_command_identity: null,
1228
+ status: "active",
1229
+ turns_started: 1,
1230
+ pending_barrier: null,
1231
+ updated_at: "2026-07-12T12:00:00.000Z",
1232
+ });
1233
+ store.append({
1234
+ schema_version: "1.0",
1235
+ type: "turn_started",
1236
+ run_id: "run-251",
1237
+ definition_id: "builder.build",
1238
+ current_step: "plan",
1239
+ turns_started: 1,
1240
+ at: "2026-07-12T12:00:00.000Z",
1241
+ });
1242
+ const stateFile = path.join(root, "continuation-driver", "state.json");
1243
+ fs.unlinkSync(stateFile);
1244
+ assert.throws(() => store.load(), /state is missing while mission events exist/);
1245
+
1246
+ store.save({
1247
+ schema_version: "1.0",
1248
+ run_id: "run-251",
1249
+ definition_id: "builder.build",
1250
+ max_turns: 3,
1251
+ adapter_command_identity: null,
1252
+ status: "active",
1253
+ turns_started: 0,
1254
+ pending_barrier: null,
1255
+ updated_at: "2026-07-12T12:00:00.000Z",
1256
+ });
1257
+ assert.throws(() => store.load(), /rolled back behind its event history/);
1258
+ });
1259
+
1260
+ test("file continuation store restores a parked barrier from mission history", (t) => {
1261
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-barrier-rollback-"));
1262
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1263
+ const store = createFileContinuationStore(root);
1264
+ const barrier = { kind: "pid", pid: process.pid };
1265
+ store.save({
1266
+ schema_version: "1.0",
1267
+ run_id: "run-251",
1268
+ definition_id: "builder.build",
1269
+ max_turns: 3,
1270
+ adapter_command_identity: null,
1271
+ status: "active",
1272
+ turns_started: 1,
1273
+ pending_barrier: null,
1274
+ updated_at: "2026-07-12T12:00:00.000Z",
1275
+ });
1276
+ store.append({ schema_version: "1.0", type: "turn_started", run_id: "run-251", definition_id: "builder.build", current_step: "verify", turns_started: 1, at: "2026-07-12T12:00:00.000Z" });
1277
+ store.append({ schema_version: "1.0", type: "parked", run_id: "run-251", definition_id: "builder.build", current_step: "verify", turns_started: 1, at: "2026-07-12T12:00:01.000Z", barrier });
1278
+
1279
+ const restored = store.load();
1280
+ assert.equal(restored.status, "waiting");
1281
+ assert.deepEqual(restored.pending_barrier, barrier);
1282
+ });
1283
+
1284
+ test("session lock rejects concurrent drivers and reclaims a dead owner", async (t) => {
1285
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-lock-"));
1286
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
1287
+
1288
+ await withContinuationDriverLock(root, async () => {
1289
+ await assert.rejects(
1290
+ withContinuationDriverLock(root, async () => assert.fail("concurrent driver must not run")),
1291
+ /already running under pid/,
1292
+ );
1293
+ });
1294
+
1295
+ const locksDir = path.join(root, "continuation-driver", "locks");
1296
+ const lockFile = path.join(locksDir, "dead.lock");
1297
+ fs.writeFileSync(lockFile, JSON.stringify({ schema_version: "1.0", pid: 2_147_483_647, token: "dead", created_at: "2026-07-12T12:00:00.000Z" }));
1298
+ let ran = false;
1299
+ await withContinuationDriverLock(root, async () => { ran = true; });
1300
+ assert.equal(ran, true);
1301
+ assert.equal(fs.existsSync(lockFile), false);
1302
+ assert.deepEqual(fs.readdirSync(locksDir), []);
1303
+ });