@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
@@ -37,6 +37,7 @@ test("public API retains the documented native-host compatibility surface", asyn
37
37
  "startBuilderBuildRun", "evaluateBuilderBuildRun", "startBuilderFlowSession",
38
38
  "pauseBuilderFlowSession", "resumeBuilderFlowSession", "cancelBuilderFlowSession",
39
39
  "archiveBuilderFlowSession", "recoverBuilderFlowSession", "releaseBuilderFlowAssignment",
40
+ "ContinuationAdapterTimeoutError",
40
41
  "writeJson", "appendJsonl", "sidecarBase", "writeState", "writeSidecar",
41
42
  ]) {
42
43
  assert.equal(typeof lib[name], "function", `${name} must remain package-root exported`);
@@ -22,6 +22,10 @@ const sidecarSchemas: Record<string, string> = {
22
22
  "release.json": "schemas/workflow-release.schema.json",
23
23
  "learning.json": "schemas/workflow-learning.schema.json",
24
24
  };
25
+ // Runtime coordination records live below a session but are not workflow
26
+ // sidecars. Recursing into them would validate continuation-driver/state.json
27
+ // against the public workflow-state schema and turn an active driver into a
28
+ // false Goal Fit hard block.
25
29
 
26
30
  function readText(file: string): string {
27
31
  return fs.readFileSync(file, "utf8");
@@ -91,11 +95,25 @@ function hasExplicitAcceptance(text: string): boolean {
91
95
  return /explicitly accepted|accepted by user|user accepted|accepted gap/i.test(text);
92
96
  }
93
97
 
98
+ function isPrivateRuntimeChild(dir: string): boolean {
99
+ if (path.basename(dir) !== "continuation-driver") return false;
100
+ const sessionDir = path.dirname(dir);
101
+ const sessionState = path.join(sessionDir, "state.json");
102
+ try {
103
+ const stat = fs.lstatSync(sessionState);
104
+ if (stat.isSymbolicLink() || !stat.isFile()) return false;
105
+ const state = JSON.parse(readText(sessionState)) as { task_slug?: unknown };
106
+ return state.task_slug === path.basename(sessionDir);
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
94
112
  function walk(dir: string): string[] {
95
113
  const out: string[] = [];
96
114
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
97
115
  const p = path.join(dir, entry.name);
98
- if (entry.isDirectory()) out.push(...walk(p));
116
+ if (entry.isDirectory() && !isPrivateRuntimeChild(p)) out.push(...walk(p));
99
117
  else out.push(p);
100
118
  }
101
119
  return out;
@@ -275,7 +275,9 @@ function resolveEnsureSessionActor(p: ReturnType<typeof parseArgs>): { actorStru
275
275
  die("ensure-session --actor value strips to empty under the allowed actor charset ([A-Za-z0-9_.-]) — pass a value containing at least one letter, digit, underscore, period, or hyphen.");
276
276
  }
277
277
  const explicitActor = explicitActorRaw ? helper.sanitizeSegment(explicitActorRaw) : "";
278
- const resolved = explicitActor ? { actor: explicitActor, source: "explicit-override" } : helper.resolveActor(process.env);
278
+ const resolved = explicitActor
279
+ ? { actor: explicitActor, source: "explicit-override", actorStruct: { runtime: "explicit-override", session_id: explicitActor, host: os.hostname() } }
280
+ : helper.resolveActorIdentity(process.env);
279
281
  const unresolved = helper.isUnresolvedActor(resolved.actor);
280
282
  const branchActorKey = unresolved ? `unknown-actor-${unknownDisambiguator(resolved.actor)}` : resolved.actor;
281
283
 
@@ -286,17 +288,8 @@ function resolveEnsureSessionActor(p: ReturnType<typeof parseArgs>): { actorStru
286
288
  return { actorStruct: { runtime: "unresolved", session_id: branchActorKey, host: os.hostname() }, actorKey: resolved.actor, branchActorKey, unresolved: true };
287
289
  }
288
290
 
289
- // #398: the CI-runtime tier must reconstruct the SAME struct resolveActor serialized, or
290
- // `serializeActor(actorStruct)` would diverge from `resolved.actor` (the else-branch would rebuild
291
- // an ANCESTRY struct — detectRuntime→unknown, runtimeSessionId→'' — so the claim's stored
292
- // actor_key would not match the CI actor at publish → self not recognized → false-block, the exact
293
- // bug this issue removes). Uses the SAME helper.detectCiActor as resolveActor, single-sourced.
294
- const ciActor = resolved.source.startsWith("ci-runtime") ? helper.detectCiActor(process.env) : null;
295
- const actorStruct: ActorStruct = resolved.source === "explicit-override"
296
- ? { runtime: "explicit-override", session_id: resolved.actor, host: os.hostname() }
297
- : ciActor && ciActor.session_id
298
- ? { runtime: ciActor.runtime, session_id: ciActor.session_id, host: os.hostname() }
299
- : { runtime: helper.detectRuntime(process.env), session_id: helper.runtimeSessionId(process.env) || (() => { const seed = helper.ancestorActorSeed(); return seed ? `anc-${seed}` : ""; })(), host: os.hostname() };
291
+ if (!resolved.actorStruct) die("ensure-session could not resolve a canonical actor struct");
292
+ const actorStruct: ActorStruct = resolved.actorStruct;
300
293
  const actorKey = helper.serializeActor(actorStruct);
301
294
  return { actorStruct, actorKey, branchActorKey, unresolved: false };
302
295
  }
@@ -1613,6 +1606,7 @@ function resolveFirstStep(flowId: string, repoRoot: string): string | null {
1613
1606
  function loadCurrentPointerHelper(): {
1614
1607
  perActorCurrentFile: (flowAgentsDir: string, actorKey: string) => string;
1615
1608
  readCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
1609
+ readOwnCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
1616
1610
  writePerActorCurrent: (flowAgentsDir: string, actorKey: string, payload: AnyObj) => void;
1617
1611
  } {
1618
1612
  const _req = createRequire(import.meta.url);
@@ -1620,6 +1614,7 @@ function loadCurrentPointerHelper(): {
1620
1614
  return _req(helperPath) as {
1621
1615
  perActorCurrentFile: (flowAgentsDir: string, actorKey: string) => string;
1622
1616
  readCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
1617
+ readOwnCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
1623
1618
  writePerActorCurrent: (flowAgentsDir: string, actorKey: string, payload: AnyObj) => void;
1624
1619
  };
1625
1620
  }
@@ -1758,9 +1753,15 @@ function updateCurrentAgent(root: string, dir: string, agentId: string, status:
1758
1753
 
1759
1754
  if (actorKey && !loadActorIdentityHelper().isUnresolvedActor(actorKey)) {
1760
1755
  const helper = loadCurrentPointerHelper();
1761
- const perActorFile = helper.perActorCurrentFile(root, actorKey);
1762
- let perActor: AnyObj | null = null;
1763
- try { perActor = fs.existsSync(perActorFile) ? (JSON.parse(fs.readFileSync(perActorFile, "utf8")) as AnyObj) : null; } catch { perActor = null; }
1756
+ // #440 fix-wave 3: read through the legacy-aware path (readOwnCurrentPointer tries the new
1757
+ // collision-resistant filename first, then falls back to the pre-fix-wave-2 legacy filename)
1758
+ // instead of a direct fs read of perActorCurrentFile() alone a direct read of ONLY the new
1759
+ // filename would silently skip this active_agents/updated_at projection for a pointer a
1760
+ // still-running pre-fix-wave-2 sidecar wrote under the old name. The write below always goes
1761
+ // through writePerActorCurrent (new filename only), so the pointer migrates to the new name
1762
+ // on first touch.
1763
+ const pointer = helper.readOwnCurrentPointer(root, actorKey);
1764
+ const perActor = pointer.payload;
1764
1765
  if (perActor && path.resolve(root, perActor.artifact_dir ?? "") === path.resolve(dir)) {
1765
1766
  helper.writePerActorCurrent(root, actorKey, applyAgentUpdate(perActor));
1766
1767
  }
@@ -6088,6 +6089,7 @@ export const LIVENESS_TERMINAL = new Set(["delivered", "canceled", "accepted", "
6088
6089
  */
6089
6090
  function loadActorIdentityHelper(): {
6090
6091
  resolveActor: (env: NodeJS.ProcessEnv) => { actor: string; source: string };
6092
+ resolveActorIdentity: (env: NodeJS.ProcessEnv) => { actor: string; source: string; actorStruct: ActorStruct | null };
6091
6093
  sanitizeSegment: (value: unknown) => string;
6092
6094
  isUnresolvedActor: (actor: string) => boolean;
6093
6095
  // #291 Wave 2 Task 2.1: widened (additive only — every existing caller above keeps using only
@@ -6097,22 +6099,15 @@ function loadActorIdentityHelper(): {
6097
6099
  // assignment-claim identity (a DIFFERENT identity concept from the flat actorKey used for
6098
6100
  // branch-naming/liveness — see resolveEnsureSessionActor's doc comment for why both exist).
6099
6101
  serializeActor: (actor: Partial<ActorStruct> | undefined) => string;
6100
- detectRuntime: (env: NodeJS.ProcessEnv) => string;
6101
- runtimeSessionId: (env: NodeJS.ProcessEnv) => string;
6102
- detectCiActor: (env: NodeJS.ProcessEnv) => { runtime: string; session_id: string } | null;
6103
- ancestorActorSeed: () => string;
6104
6102
  } {
6105
6103
  const _req = createRequire(import.meta.url);
6106
6104
  const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../scripts/hooks/lib/actor-identity.js");
6107
6105
  return _req(helperPath) as {
6108
6106
  resolveActor: (env: NodeJS.ProcessEnv) => { actor: string; source: string };
6107
+ resolveActorIdentity: (env: NodeJS.ProcessEnv) => { actor: string; source: string; actorStruct: ActorStruct | null };
6109
6108
  sanitizeSegment: (value: unknown) => string;
6110
6109
  isUnresolvedActor: (actor: string) => boolean;
6111
6110
  serializeActor: (actor: Partial<ActorStruct> | undefined) => string;
6112
- detectRuntime: (env: NodeJS.ProcessEnv) => string;
6113
- runtimeSessionId: (env: NodeJS.ProcessEnv) => string;
6114
- detectCiActor: (env: NodeJS.ProcessEnv) => { runtime: string; session_id: string } | null;
6115
- ancestorActorSeed: () => string;
6116
6111
  };
6117
6112
  }
6118
6113
  function resolveLivenessActor(): string {
@@ -6,6 +6,7 @@ import { isDeepStrictEqual } from "node:util";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { validateDefinition } from "@kontourai/flow";
8
8
  import { loadBuilderFlowRun } from "../builder-flow-run-adapter.js";
9
+ import { driveBuilderFlowSession, withContinuationDriverLock } from "../continuation-driver.js";
9
10
  import { inspectBuilderFlowSession, recoverBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
10
11
  import { flowAgentsPackageRoot, flowAgentsPackageVersion } from "../lib/package-version.js";
11
12
  import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
@@ -14,6 +15,7 @@ import { flagBool, flagList, flagString, parseArgs } from "../lib/args.js";
14
15
  import { main as builderRun } from "./builder-run.js";
15
16
  import { currentWorkflowSessionDir, isMeaningfulTestCommand, mainFromPublicWorkflow, WORKFLOW_WRITER_CONTRACT_VERSION } from "./workflow-sidecar.js";
16
17
  import { resolveCurrentAssignmentActor, withSubjectLock } from "./assignment-provider.js";
18
+ import { assertLoadedContinuationAdapterIntegrity, executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "./continuation-adapter.js";
17
19
 
18
20
  type JsonRecord = Record<string, unknown>;
19
21
 
@@ -22,7 +24,7 @@ const PACKAGE_ROOT = flowAgentsPackageRoot();
22
24
  const REQUIRE = createRequire(import.meta.url);
23
25
  const PACKAGE_METADATA = readJsonFile(path.join(PACKAGE_ROOT, "package.json"), "Flow Agents package metadata");
24
26
  const CLI_VERSION = flowAgentsPackageVersion();
25
- const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "pause", "resume", "release", "cancel", "archive", "doctor"] as const;
27
+ const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "drive", "pause", "resume", "release", "cancel", "archive", "doctor"] as const;
26
28
 
27
29
  function usage(): void {
28
30
  console.log(`Usage: flow-agents workflow <verb> [options]
@@ -32,6 +34,7 @@ Public workflow verbs:
32
34
  status Show the current canonical run and projected next action.
33
35
  evidence Record evidence for the current Flow gate and synchronize it.
34
36
  critique Record review critique directly into the current trust bundle.
37
+ drive Continue the canonical run through an explicit runtime adapter.
35
38
  pause Pause the current run as its assignment actor.
36
39
  resume Resume the current paused run as its assignment actor.
37
40
  release Release the current assignment without canceling the run.
@@ -61,12 +64,74 @@ export async function main(argv: string[]): Promise<number> {
61
64
  if (verb === "status") return status(sessionDir, flagBool(parsed.flags, "json"));
62
65
  if (verb === "evidence") return evidence(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
63
66
  if (verb === "critique") return critique(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
67
+ if (verb === "drive") return drive(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
64
68
 
65
69
  const forwarded = stripPublicFlags(argv.slice(1), new Set(["artifact-root", "session-dir", "json"]));
66
70
  if (verb === "release" && !flagString(parsed.flags, "reason")) throw new Error("workflow release requires --reason <text>");
67
71
  return builderRun([verb === "release" ? "release-assignment" : verb, "--session-dir", sessionDir, ...forwarded]);
68
72
  }
69
73
 
74
+ async function drive(sessionDir: string, argv: string[], json: boolean): Promise<number> {
75
+ const parsed = parseArgs(argv);
76
+ assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "adapter-command-file", "max-turns", "turn-timeout-ms", "barrier-wait-ms", "barrier-poll-ms"]), "workflow drive");
77
+ const adapterCommandFile = flagString(parsed.flags, "adapter-command-file");
78
+ if (!adapterCommandFile) throw new Error("workflow drive requires --adapter-command-file <path>");
79
+ const maxTurns = integerFlag(parsed.flags, "max-turns", 4, 1, 100);
80
+ const turnTimeoutMs = integerFlag(parsed.flags, "turn-timeout-ms", 900_000, 1, 86_400_000);
81
+ const barrierWaitMs = integerFlag(parsed.flags, "barrier-wait-ms", 300_000, 0, 86_400_000);
82
+ const barrierPollMs = integerFlag(parsed.flags, "barrier-poll-ms", 1_000, 1, 60_000);
83
+ const { slug, projectRoot } = readBoundSession(sessionDir);
84
+ assertOrdinaryMatchingAssignmentActor(sessionDir, slug);
85
+ const adapterCommand = loadContinuationAdapterCommand(adapterCommandFile);
86
+ const result = await withContinuationDriverLock(sessionDir, async (lock) => {
87
+ assertOrdinaryMatchingAssignmentActor(sessionDir, slug);
88
+ return driveBuilderFlowSession({
89
+ sessionDir,
90
+ maxTurns,
91
+ adapterCommandIdentity: adapterCommand.identity,
92
+ authorizeTurn: async () => { assertOrdinaryMatchingAssignmentActor(sessionDir, slug); },
93
+ issueTurnAuthority: async (request) => {
94
+ const currentAssignment = assertOrdinaryMatchingAssignmentActor(sessionDir, slug);
95
+ assertLoadedContinuationAdapterIntegrity(adapterCommand);
96
+ const authority = loadContinuationTurnAuthority();
97
+ return authority.issueActiveTurnAuthority({
98
+ sessionDir,
99
+ runId: request.run_id,
100
+ definitionId: request.definition_id,
101
+ currentStep: request.current_step,
102
+ iteration: request.iteration,
103
+ maxTurns: request.max_turns,
104
+ adapterCommandIdentity: adapterCommand.identity,
105
+ assignmentActor: currentAssignment.actorKey,
106
+ assignmentActorStruct: currentAssignment.actor,
107
+ lock,
108
+ timeoutMs: turnTimeoutMs,
109
+ });
110
+ },
111
+ execute: async (request, context) => executeLoadedContinuationAdapter(adapterCommand, request, {
112
+ cwd: projectRoot,
113
+ timeoutMs: turnTimeoutMs,
114
+ continuationTurnSecret: context?.continuationTurnSecret,
115
+ continuationRunId: context?.continuationRunId,
116
+ }),
117
+ waitForBarrier: async (barrier) => waitForContinuationBarrier(barrier, { maxWaitMs: barrierWaitMs, pollMs: barrierPollMs }),
118
+ });
119
+ });
120
+ if (json) console.log(JSON.stringify(result));
121
+ else console.log(`Continuation driver ${result.outcome} after ${result.turns_started} turn(s); canonical Flow is ${result.snapshot.status} at ${result.snapshot.current_step}.`);
122
+ return 0;
123
+ }
124
+
125
+ function loadContinuationTurnAuthority(): {
126
+ issueActiveTurnAuthority(input: Record<string, unknown>): { runId: string; turnSecret: string; publicKeyDigest: string; cleanup(): boolean };
127
+ validateSignedActiveTurnAssignmentAuthority(input: Record<string, unknown>): { valid: boolean; record?: { assignment_actor: string; assignment_actor_struct: Record<string, unknown> } };
128
+ } {
129
+ return REQUIRE(path.resolve(PACKAGE_ROOT, "scripts", "hooks", "lib", "continuation-turn-authority.js")) as {
130
+ issueActiveTurnAuthority(input: Record<string, unknown>): { runId: string; turnSecret: string; publicKeyDigest: string; cleanup(): boolean };
131
+ validateSignedActiveTurnAssignmentAuthority(input: Record<string, unknown>): { valid: boolean; record?: { assignment_actor: string; assignment_actor_struct: Record<string, unknown> } };
132
+ };
133
+ }
134
+
70
135
  async function start(argv: string[]): Promise<number> {
71
136
  const parsed = parseArgs(argv);
72
137
  assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion", "assignment-provider", "effective-state-json"]), "workflow start");
@@ -486,15 +551,42 @@ function readAssignment(sessionDir: string, slug: string): JsonRecord {
486
551
  }
487
552
 
488
553
  function assertMatchingAssignmentActor(sessionDir: string, slug: string): ReturnType<typeof resolveCurrentAssignmentActor> {
489
- const assignment = readActiveAssignment(sessionDir, slug);
490
- const caller = resolveCurrentAssignmentActor();
491
- const normalizeActor = (value: unknown): JsonRecord | null => value && typeof value === "object" ? { ...(value as JsonRecord), human: (value as JsonRecord).human ?? null } : null;
492
- if (assignment.actor_key !== caller.actorKey || !isDeepStrictEqual(normalizeActor(assignment.actor), normalizeActor(caller.actor))) {
493
- throw new Error("workflow mutation requires the session's active, matching assignment actor");
554
+ const { assignment, caller, matches } = assignmentActorContext(sessionDir, slug);
555
+ if (matches) return caller;
556
+
557
+ const authority = loadContinuationTurnAuthority().validateSignedActiveTurnAssignmentAuthority({
558
+ sessionDir,
559
+ runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID,
560
+ turnSecret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET,
561
+ });
562
+ if (authority.valid && authority.record
563
+ && assignment.actor_key === authority.record.assignment_actor
564
+ && isDeepStrictEqual(normalizeAssignmentActor(assignment.actor), normalizeAssignmentActor(authority.record.assignment_actor_struct))) {
565
+ return { actorKey: authority.record.assignment_actor, actor: normalizeAssignmentActor(authority.record.assignment_actor_struct)! as ReturnType<typeof resolveCurrentAssignmentActor>["actor"] };
494
566
  }
567
+ throw new Error("workflow mutation requires the session's active, matching assignment actor");
568
+ }
569
+
570
+ function assertOrdinaryMatchingAssignmentActor(sessionDir: string, slug: string): ReturnType<typeof resolveCurrentAssignmentActor> {
571
+ const { caller, matches } = assignmentActorContext(sessionDir, slug);
572
+ if (!matches) throw new Error("workflow mutation requires the session's active, matching assignment actor");
495
573
  return caller;
496
574
  }
497
575
 
576
+ function assignmentActorContext(sessionDir: string, slug: string): {
577
+ assignment: JsonRecord;
578
+ caller: ReturnType<typeof resolveCurrentAssignmentActor>;
579
+ matches: boolean;
580
+ } {
581
+ const assignment = readActiveAssignment(sessionDir, slug);
582
+ const caller = resolveCurrentAssignmentActor();
583
+ return { assignment, caller, matches: assignment.actor_key === caller.actorKey && isDeepStrictEqual(normalizeAssignmentActor(assignment.actor), normalizeAssignmentActor(caller.actor)) };
584
+ }
585
+
586
+ function normalizeAssignmentActor(value: unknown): JsonRecord | null {
587
+ return value && typeof value === "object" ? { ...(value as JsonRecord), human: (value as JsonRecord).human ?? null } : null;
588
+ }
589
+
498
590
  function readActiveAssignment(sessionDir: string, slug: string): JsonRecord {
499
591
  const assignment = readAssignment(sessionDir, slug);
500
592
  if (assignment.status !== "claimed" || assignment.artifact_dir !== slug || typeof assignment.actor_key !== "string" || !assignment.actor_key || !assignment.actor || typeof assignment.actor !== "object" || Array.isArray(assignment.actor)) {
@@ -570,6 +662,21 @@ function assertOnlyFlags(flags: ReturnType<typeof parseArgs>["flags"], allowed:
570
662
  if (unsupported) throw new Error(`${command} does not support --${unsupported}`);
571
663
  }
572
664
 
665
+ function integerFlag(
666
+ flags: ReturnType<typeof parseArgs>["flags"],
667
+ name: string,
668
+ fallback: number,
669
+ min: number,
670
+ max: number,
671
+ ): number {
672
+ const raw = flagString(flags, name);
673
+ if (raw === undefined) return fallback;
674
+ if (!/^(0|[1-9][0-9]*)$/.test(raw)) throw new Error(`workflow drive --${name} must be an integer from ${min} through ${max}`);
675
+ const value = Number(raw);
676
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`workflow drive --${name} must be an integer from ${min} through ${max}`);
677
+ return value;
678
+ }
679
+
573
680
  function isWithin(candidate: string, root: string): boolean {
574
681
  const relative = path.relative(root, candidate);
575
682
  return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));