@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.
- package/.github/workflows/ci.yml +8 -0
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/build/src/builder-flow-runtime.js +16 -10
- package/build/src/cli/assignment-provider.js +4 -18
- package/build/src/cli/continuation-adapter.d.ts +26 -0
- package/build/src/cli/continuation-adapter.js +243 -0
- package/build/src/cli/validate-workflow-artifacts.js +21 -1
- package/build/src/cli/workflow-sidecar.js +15 -20
- package/build/src/cli/workflow.js +99 -6
- package/build/src/continuation-driver.d.ts +116 -0
- package/build/src/continuation-driver.js +527 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/tools/build-universal-bundles.js +52 -25
- package/build/src/tools/validate-source-tree.js +5 -2
- package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/context/scripts/hooks/stop-goal-fit.js +244 -73
- package/context/scripts/hooks/workflow-steering.js +24 -15
- package/context/scripts/telemetry/lib/pricing.sh +18 -6
- package/context/scripts/telemetry/lib/transport.sh +139 -37
- package/context/scripts/telemetry/lib/usage.sh +282 -32
- package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/context/scripts/telemetry/telemetry.sh +76 -1
- package/docs/coordination-guide.md +21 -5
- package/docs/fixture-ownership.md +2 -1
- package/docs/public-workflow-cli.md +98 -0
- package/docs/spec/runtime-hook-surface.md +8 -4
- package/docs/workflow-usage-guide.md +7 -0
- package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
- package/evals/acceptance/prove-capture-teeth.sh +13 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
- package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
- package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
- package/evals/integration/test_actor_identity.sh +63 -0
- package/evals/integration/test_assignment_provider_local_file.sh +34 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -1
- package/evals/integration/test_bundle_install.sh +106 -17
- package/evals/integration/test_bundle_lifecycle.sh +17 -3
- package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
- package/evals/integration/test_ci_actor_identity.sh +2 -2
- package/evals/integration/test_command_log_concurrency.sh +11 -0
- package/evals/integration/test_command_log_fork_classification.sh +11 -0
- package/evals/integration/test_command_log_integrity.sh +11 -0
- package/evals/integration/test_current_json_per_actor.sh +416 -21
- package/evals/integration/test_enforcer_expects_driven.sh +11 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
- package/evals/integration/test_evidence_capture_hook.sh +25 -6
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
- package/evals/integration/test_gate_bypass_chain.sh +11 -0
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
- package/evals/integration/test_goal_fit_hook.sh +257 -9
- package/evals/integration/test_goal_fit_rederive.sh +11 -0
- package/evals/integration/test_install_merge.sh +141 -3
- package/evals/integration/test_liveness_conflict_injection.sh +39 -3
- package/evals/integration/test_liveness_heartbeat.sh +46 -9
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
- package/evals/integration/test_public_workflow_cli.sh +6 -6
- package/evals/integration/test_session_resume_roundtrip.sh +30 -0
- package/evals/integration/test_stop_hook_release.sh +111 -11
- package/evals/integration/test_telemetry_tool_usage.sh +394 -0
- package/evals/integration/test_verify_hold.sh +64 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
- package/evals/integration/test_workflow_steering_hook.sh +20 -4
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +123 -0
- package/package.json +4 -4
- package/packaging/README.md +21 -0
- package/packaging/codex-legacy-agents-fingerprints.json +33 -0
- package/scripts/README.md +2 -1
- package/scripts/audit-codex-legacy-agents.js +43 -0
- package/scripts/classify-codex-legacy-agents.js +71 -0
- package/scripts/hooks/evidence-capture.js +18 -6
- package/scripts/hooks/lib/actor-identity.js +62 -29
- package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/scripts/hooks/lib/current-pointer.js +114 -12
- package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
- package/scripts/hooks/stop-goal-fit.js +244 -73
- package/scripts/hooks/workflow-steering.js +24 -15
- package/scripts/install-codex-home.sh +8 -7
- package/scripts/telemetry/lib/pricing.sh +18 -6
- package/scripts/telemetry/lib/transport.sh +71 -0
- package/scripts/telemetry/lib/usage.sh +282 -32
- package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/scripts/telemetry/telemetry.sh +76 -1
- package/src/builder-flow-runtime.ts +9 -3
- package/src/cli/assignment-provider.ts +5 -26
- package/src/cli/builder-flow-runtime.test.mjs +215 -2
- package/src/cli/continuation-adapter.ts +265 -0
- package/src/cli/continuation-driver.test.mjs +1303 -0
- package/src/cli/public-api.test.mjs +1 -0
- package/src/cli/validate-workflow-artifacts.ts +19 -1
- package/src/cli/workflow-sidecar.ts +18 -23
- package/src/cli/workflow.ts +113 -6
- package/src/continuation-driver.ts +638 -0
- package/src/index.ts +24 -0
- package/src/tools/build-universal-bundles.ts +61 -25
- 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
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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 {
|
package/src/cli/workflow.ts
CHANGED
|
@@ -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 =
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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));
|