@kontourai/flow-agents 3.9.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 (106) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +1 -1
  4. package/build/src/cli/assignment-provider.js +4 -18
  5. package/build/src/cli/continuation-adapter.d.ts +10 -8
  6. package/build/src/cli/continuation-adapter.js +22 -4
  7. package/build/src/cli/validate-workflow-artifacts.js +21 -1
  8. package/build/src/cli/workflow-sidecar.js +15 -20
  9. package/build/src/cli/workflow.js +58 -11
  10. package/build/src/continuation-driver.d.ts +27 -3
  11. package/build/src/continuation-driver.js +97 -14
  12. package/build/src/index.d.ts +2 -2
  13. package/build/src/index.js +1 -1
  14. package/build/src/tools/build-universal-bundles.js +52 -25
  15. package/build/src/tools/validate-source-tree.js +5 -2
  16. package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  17. package/context/scripts/hooks/stop-goal-fit.js +244 -73
  18. package/context/scripts/hooks/workflow-steering.js +24 -15
  19. package/context/scripts/telemetry/lib/pricing.sh +18 -6
  20. package/context/scripts/telemetry/lib/transport.sh +139 -37
  21. package/context/scripts/telemetry/lib/usage.sh +282 -32
  22. package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  23. package/context/scripts/telemetry/telemetry.sh +76 -1
  24. package/docs/coordination-guide.md +21 -5
  25. package/docs/fixture-ownership.md +2 -1
  26. package/docs/public-workflow-cli.md +44 -0
  27. package/docs/spec/runtime-hook-surface.md +8 -4
  28. package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
  29. package/evals/acceptance/prove-capture-teeth.sh +13 -0
  30. package/evals/ci/run-baseline.sh +2 -0
  31. package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
  32. package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
  33. package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
  34. package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
  35. package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
  36. package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
  37. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
  38. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
  39. package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
  40. package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
  41. package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
  42. package/evals/integration/test_actor_identity.sh +63 -0
  43. package/evals/integration/test_assignment_provider_local_file.sh +34 -0
  44. package/evals/integration/test_builder_entry_enforcement.sh +5 -1
  45. package/evals/integration/test_bundle_install.sh +106 -17
  46. package/evals/integration/test_bundle_lifecycle.sh +17 -3
  47. package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
  48. package/evals/integration/test_ci_actor_identity.sh +2 -2
  49. package/evals/integration/test_command_log_concurrency.sh +11 -0
  50. package/evals/integration/test_command_log_fork_classification.sh +11 -0
  51. package/evals/integration/test_command_log_integrity.sh +11 -0
  52. package/evals/integration/test_current_json_per_actor.sh +416 -21
  53. package/evals/integration/test_enforcer_expects_driven.sh +11 -0
  54. package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
  55. package/evals/integration/test_evidence_capture_hook.sh +25 -6
  56. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  57. package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
  58. package/evals/integration/test_gate_bypass_chain.sh +11 -0
  59. package/evals/integration/test_gate_lockdown.sh +10 -0
  60. package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
  61. package/evals/integration/test_goal_fit_hook.sh +257 -9
  62. package/evals/integration/test_goal_fit_rederive.sh +11 -0
  63. package/evals/integration/test_install_merge.sh +141 -3
  64. package/evals/integration/test_liveness_conflict_injection.sh +39 -3
  65. package/evals/integration/test_liveness_heartbeat.sh +46 -9
  66. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
  67. package/evals/integration/test_public_workflow_cli.sh +6 -6
  68. package/evals/integration/test_session_resume_roundtrip.sh +30 -0
  69. package/evals/integration/test_stop_hook_release.sh +111 -11
  70. package/evals/integration/test_telemetry_tool_usage.sh +394 -0
  71. package/evals/integration/test_verify_hold.sh +64 -1
  72. package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
  73. package/evals/integration/test_workflow_steering_hook.sh +20 -4
  74. package/evals/run.sh +2 -0
  75. package/evals/static/test_universal_bundles.sh +123 -0
  76. package/package.json +1 -1
  77. package/packaging/README.md +21 -0
  78. package/packaging/codex-legacy-agents-fingerprints.json +33 -0
  79. package/scripts/README.md +2 -1
  80. package/scripts/audit-codex-legacy-agents.js +43 -0
  81. package/scripts/classify-codex-legacy-agents.js +71 -0
  82. package/scripts/hooks/evidence-capture.js +18 -6
  83. package/scripts/hooks/lib/actor-identity.js +62 -29
  84. package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  85. package/scripts/hooks/lib/current-pointer.js +114 -12
  86. package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
  87. package/scripts/hooks/stop-goal-fit.js +244 -73
  88. package/scripts/hooks/workflow-steering.js +24 -15
  89. package/scripts/install-codex-home.sh +8 -7
  90. package/scripts/telemetry/lib/pricing.sh +18 -6
  91. package/scripts/telemetry/lib/transport.sh +71 -0
  92. package/scripts/telemetry/lib/usage.sh +282 -32
  93. package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  94. package/scripts/telemetry/telemetry.sh +76 -1
  95. package/src/cli/assignment-provider.ts +5 -26
  96. package/src/cli/builder-flow-runtime.test.mjs +93 -6
  97. package/src/cli/continuation-adapter.ts +33 -7
  98. package/src/cli/continuation-driver.test.mjs +741 -2
  99. package/src/cli/public-api.test.mjs +1 -0
  100. package/src/cli/validate-workflow-artifacts.ts +19 -1
  101. package/src/cli/workflow-sidecar.ts +18 -23
  102. package/src/cli/workflow.ts +71 -11
  103. package/src/continuation-driver.ts +125 -19
  104. package/src/index.ts +4 -0
  105. package/src/tools/build-universal-bundles.ts +61 -25
  106. package/src/tools/validate-source-tree.ts +5 -2
@@ -3,10 +3,18 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
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";
6
9
 
7
- import { createFileContinuationStore, runContinuationDriver, withContinuationDriverLock } from "../../build/src/continuation-driver.js";
10
+ import { ContinuationAdapterTimeoutError, createFileContinuationStore, runContinuationDriver, withContinuationDriverLock } from "../../build/src/continuation-driver.js";
8
11
  import { executeContinuationAdapter, executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "../../build/src/cli/continuation-adapter.js";
9
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
+
10
18
  function snapshot(step, status = "active") {
11
19
  const disposition = status === "completed" || status === "canceled"
12
20
  ? "done"
@@ -39,6 +47,37 @@ function memoryStore(initial = null) {
39
47
  };
40
48
  }
41
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
+
42
81
  test("driver advances multiple canonical Flow steps without a human continuation", async () => {
43
82
  const states = [snapshot("plan"), snapshot("execute"), snapshot("done", "completed")];
44
83
  let index = 0;
@@ -244,6 +283,626 @@ test("adapter errors fail open to another bounded turn and remain auditable", as
244
283
  "runtime unavailable",
245
284
  "runtime unavailable",
246
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");
247
906
  });
248
907
 
249
908
  test("driver binds the adapter command identity for the mission", async () => {
@@ -335,6 +994,62 @@ test("explicit adapter argv receives one structured action without a shell", asy
335
994
  assert.deepEqual(result, { status: "completed", summary: `${fs.realpathSync(root)}|plan` });
336
995
  });
337
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
+
338
1053
  test("loaded adapter binds executable and script content for every turn", async (t) => {
339
1054
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-integrity-"));
340
1055
  t.after(() => fs.rmSync(root, { recursive: true, force: true }));
@@ -408,12 +1123,36 @@ test("adapter timeout terminates its process group", { skip: process.platform ==
408
1123
  max_turns: 3,
409
1124
  next_action: null,
410
1125
  }, { cwd: root, timeoutMs: 50 }),
411
- /timed out after 50ms/,
1126
+ (error) => error instanceof ContinuationAdapterTimeoutError && /timed out after 50ms/.test(error.message),
412
1127
  );
413
1128
  await new Promise((resolve) => setTimeout(resolve, 400));
414
1129
  assert.equal(fs.existsSync(marker), false);
415
1130
  });
416
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
+
417
1156
  test("completed adapter forcibly terminates background descendants", { skip: process.platform === "win32" }, async (t) => {
418
1157
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "continuation-completed-child-"));
419
1158
  t.after(() => fs.rmSync(root, { recursive: true, force: true }));