@kontourai/flow-agents 3.4.2 → 3.4.3
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/CHANGELOG.md +7 -0
- package/agents/dev.json +0 -5
- package/build/src/builder-flow-runtime.js +18 -12
- package/build/src/cli/workflow-sidecar.js +77 -5
- package/build/src/tools/build-universal-bundles.js +0 -2
- package/context/scripts/hooks/workflow-steering.js +2 -40
- package/docs/spec/builder-flow-runtime.md +12 -7
- package/docs/spec/runtime-hook-surface.md +4 -4
- package/evals/integration/test_builder_entry_enforcement.sh +138 -7
- package/evals/integration/test_builder_step_producers.sh +1 -1
- package/evals/integration/test_bundle_install.sh +3 -3
- package/evals/integration/test_current_json_per_actor.sh +2 -2
- package/evals/integration/test_dual_emit_flow_step.sh +41 -9
- package/evals/integration/test_flowdef_session_activation.sh +11 -5
- package/evals/integration/test_install_merge.sh +24 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -10
- package/evals/integration/test_resolvefirststep_security.sh +1 -1
- package/evals/integration/test_sidecar_field_preservation.sh +4 -2
- package/evals/integration/test_takeover_protocol.sh +1 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +17 -6
- package/evals/integration/test_workflow_steering_hook.sh +0 -72
- package/package.json +1 -1
- package/schemas/workflow-state.schema.json +1 -0
- package/scripts/hooks/workflow-steering.js +2 -40
- package/scripts/install-merge.js +2 -0
- package/src/builder-flow-runtime.ts +23 -12
- package/src/cli/builder-flow-runtime.test.mjs +32 -0
- package/src/cli/workflow-sidecar.ts +76 -6
- package/src/tools/build-universal-bundles.ts +0 -2
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
// ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
|
|
10
10
|
import { resolveActiveFlowStep, resolveAllFlowGateExpects, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy, type ActiveFlowStep } from "../lib/flow-resolver.js";
|
|
11
11
|
import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
12
|
-
import { syncBuilderFlowSessionIfPresent } from "../builder-flow-runtime.js";
|
|
12
|
+
import { startBuilderFlowSession, syncBuilderFlowSessionIfPresent } from "../builder-flow-runtime.js";
|
|
13
13
|
// #291 Wave 1 Task 1.1 exports: ensure-session's ownership guard reuses the EXACT same
|
|
14
14
|
// assignment ⋈ liveness join / claim / supersede logic #290 already ships for the
|
|
15
15
|
// `assignment-provider` CLI, rather than reimplementing a second, parallel join (static ESM
|
|
@@ -1312,8 +1312,15 @@ async function withLock<T>(dir: string, create: boolean, command: string, body:
|
|
|
1312
1312
|
const lockDir = path.join(dir, ".workflow-sidecar.lockdir");
|
|
1313
1313
|
const staleMs = Number(process.env.FLOW_AGENTS_WORKFLOW_SIDECAR_STALE_LOCK_MS ?? 5 * 60 * 1000);
|
|
1314
1314
|
const deadline = Date.now() + 30000;
|
|
1315
|
+
let acquiredRoot: fs.Stats | null = null;
|
|
1316
|
+
let acquiredLock: fs.Stats | null = null;
|
|
1315
1317
|
while (true) {
|
|
1316
|
-
try {
|
|
1318
|
+
try {
|
|
1319
|
+
fs.mkdirSync(lockDir);
|
|
1320
|
+
acquiredRoot = fs.lstatSync(dir);
|
|
1321
|
+
acquiredLock = fs.lstatSync(lockDir);
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1317
1324
|
catch (error) {
|
|
1318
1325
|
const lockError = error as NodeJS.ErrnoException;
|
|
1319
1326
|
if (lockError.code !== "EEXIST") {
|
|
@@ -1338,7 +1345,28 @@ async function withLock<T>(dir: string, create: boolean, command: string, body:
|
|
|
1338
1345
|
if (delay) await new Promise((resolve) => setTimeout(resolve, Number(delay) * 1000));
|
|
1339
1346
|
return await body();
|
|
1340
1347
|
} finally {
|
|
1341
|
-
|
|
1348
|
+
try {
|
|
1349
|
+
const currentRoot = fs.lstatSync(dir);
|
|
1350
|
+
const currentLock = fs.lstatSync(lockDir);
|
|
1351
|
+
const sameIdentity = (left: fs.Stats, right: fs.Stats): boolean =>
|
|
1352
|
+
left.dev === right.dev && left.ino === right.ino;
|
|
1353
|
+
if (acquiredRoot
|
|
1354
|
+
&& acquiredLock
|
|
1355
|
+
&& !currentRoot.isSymbolicLink()
|
|
1356
|
+
&& currentRoot.isDirectory()
|
|
1357
|
+
&& !currentLock.isSymbolicLink()
|
|
1358
|
+
&& currentLock.isDirectory()
|
|
1359
|
+
&& sameIdentity(currentRoot, acquiredRoot)
|
|
1360
|
+
&& sameIdentity(currentLock, acquiredLock)) {
|
|
1361
|
+
fs.rmdirSync(lockDir);
|
|
1362
|
+
} else {
|
|
1363
|
+
process.stderr.write(`[workflow-sidecar] lock cleanup skipped because root or lock identity changed: ${lockDir}\n`);
|
|
1364
|
+
}
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
1367
|
+
process.stderr.write(`[workflow-sidecar] lock cleanup skipped: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1342
1370
|
}
|
|
1343
1371
|
}
|
|
1344
1372
|
|
|
@@ -1947,19 +1975,53 @@ function resolveEnsureSessionEntry(p: ReturnType<typeof parseArgs>, dir: string)
|
|
|
1947
1975
|
return { flowId, stepId: explicitStep || firstStep, firstStep };
|
|
1948
1976
|
}
|
|
1949
1977
|
|
|
1978
|
+
function assertCanonicalBuilderArtifactRoot(root: string): void {
|
|
1979
|
+
const kontouraiRoot = path.dirname(root);
|
|
1980
|
+
const projectRoot = path.dirname(kontouraiRoot);
|
|
1981
|
+
if (path.basename(root) !== "flow-agents" || path.basename(kontouraiRoot) !== ".kontourai") {
|
|
1982
|
+
die("ensure-session --flow-id builder.build requires --artifact-root <project>/.kontourai/flow-agents");
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const statIfPresent = (candidate: string): fs.Stats | null => {
|
|
1986
|
+
try {
|
|
1987
|
+
return fs.lstatSync(candidate);
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
1990
|
+
throw error;
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
const projectStat = statIfPresent(projectRoot);
|
|
1994
|
+
if (projectStat && (projectStat.isSymbolicLink() || !projectStat.isDirectory())) {
|
|
1995
|
+
die(`ensure-session --flow-id builder.build requires a non-symlink project root: ${projectRoot}`);
|
|
1996
|
+
}
|
|
1997
|
+
const realProjectRoot = projectStat ? fs.realpathSync(projectRoot) : projectRoot;
|
|
1998
|
+
for (const [candidate, expected, label] of [
|
|
1999
|
+
[kontouraiRoot, path.join(realProjectRoot, ".kontourai"), ".kontourai root"],
|
|
2000
|
+
[root, path.join(realProjectRoot, ".kontourai", "flow-agents"), "Flow Agents artifact root"],
|
|
2001
|
+
] as const) {
|
|
2002
|
+
const stat = statIfPresent(candidate);
|
|
2003
|
+
if (!stat) continue;
|
|
2004
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || fs.realpathSync(candidate) !== expected) {
|
|
2005
|
+
die(`ensure-session --flow-id builder.build requires a non-symlink ${label}: ${candidate}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1950
2010
|
function preflightEnsureSession(p: ReturnType<typeof parseArgs>): void {
|
|
1951
2011
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
1952
2012
|
const slug = opt(p, "task-slug") || (opt(p, "work-item") ? workItemSlug(opt(p, "work-item")) : die("--task-slug is required (or pass --work-item to derive it)"));
|
|
1953
2013
|
const dir = sessionDirFor(root, slug);
|
|
1954
|
-
resolveEnsureSessionEntry(p, dir);
|
|
2014
|
+
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2015
|
+
if (entry.flowId === "builder.build") assertCanonicalBuilderArtifactRoot(root);
|
|
1955
2016
|
sessionWorkItem(p, slug, dir);
|
|
1956
2017
|
}
|
|
1957
2018
|
|
|
1958
|
-
function ensureSession(p: ReturnType<typeof parseArgs>): number {
|
|
2019
|
+
async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
1959
2020
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
1960
2021
|
const slug = opt(p, "task-slug") || (opt(p, "work-item") ? workItemSlug(opt(p, "work-item")) : die("--task-slug is required (or pass --work-item to derive it)"));
|
|
1961
2022
|
const dir = sessionDirFor(root, slug);
|
|
1962
2023
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2024
|
+
if (entry.flowId === "builder.build") assertCanonicalBuilderArtifactRoot(root);
|
|
1963
2025
|
const workItem = sessionWorkItem(p, slug, dir);
|
|
1964
2026
|
// #291 Wave 2 Task 2.1 (§3, §4): resolve the actor ONCE, then run the ownership guard BEFORE
|
|
1965
2027
|
// any directory/file is created — a refusal must never leave a stray empty session dir. Reused
|
|
@@ -1994,7 +2056,6 @@ function ensureSession(p: ReturnType<typeof parseArgs>): number {
|
|
|
1994
2056
|
summary: `Start the canonical Flow run; activate \`pull-work\` for work item ${JSON.stringify(workItem.ref)}, and satisfy the declared gate before advancing.`,
|
|
1995
2057
|
skills: ["pull-work"],
|
|
1996
2058
|
command: startCommand,
|
|
1997
|
-
enforcement: "before_tool_use",
|
|
1998
2059
|
}
|
|
1999
2060
|
: `Continue at Flow step ${JSON.stringify(entry.stepId)} for work item ${JSON.stringify(workItem.ref)}; satisfy its declared gate before advancing.`
|
|
2000
2061
|
: opt(p, "next-action", "Continue.");
|
|
@@ -2025,6 +2086,15 @@ function ensureSession(p: ReturnType<typeof parseArgs>): number {
|
|
|
2025
2086
|
? persistedCurrent.active_step_id
|
|
2026
2087
|
: entry.stepId;
|
|
2027
2088
|
writeCurrent(root, dir, timestamp, "workflow-sidecar", "ensure-session", entry.flowId || undefined, resumedStep || undefined, actorResolution.unresolved ? undefined : actorResolution.branchActorKey);
|
|
2089
|
+
if (entry.flowId === "builder.build") {
|
|
2090
|
+
try {
|
|
2091
|
+
await startBuilderFlowSession({ sessionDir: dir });
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
const retry = `flow-agents builder-run start --session-dir .kontourai/flow-agents/${slug}`;
|
|
2094
|
+
process.stderr.write(`[ensure-session] canonical Builder Flow run did not start; the sidecar remains retryable. Run: ${retry}\n`);
|
|
2095
|
+
throw error;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2028
2098
|
console.log(dir);
|
|
2029
2099
|
return 0;
|
|
2030
2100
|
}
|
|
@@ -337,7 +337,6 @@ function exportClaudeSettings(): string {
|
|
|
337
337
|
hooks.UserPromptSubmit.push({ hooks: [shellHook(claudePolicy("UserPromptSubmit", "workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
|
|
338
338
|
hooks.PostToolUse.push({ hooks: [shellHook(claudePolicy("PostToolUse", "quality-gate.js"), 30, "Running Flow Agents hook policy")] });
|
|
339
339
|
hooks.PostToolUse.push({ hooks: [shellHook(claudePolicy("PostToolUse", "evidence-capture.js"), 30, "Capturing Flow Agents command evidence")] });
|
|
340
|
-
hooks.PreToolUse.push({ hooks: [shellHook(claudePolicy("PreToolUse", "workflow-steering.js"), 30, "Enforcing Flow Agents projected action")] });
|
|
341
340
|
hooks.PreToolUse.push({ hooks: [shellHook(claudePolicy("PreToolUse", "config-protection.js"), 30, "Running Flow Agents hook policy")] });
|
|
342
341
|
return `${JSON.stringify({
|
|
343
342
|
statusLine: { type: "command", command: 'bash -lc \'root="${CLAUDE_PROJECT_DIR:-$(pwd)}"; node "$root/scripts/statusline/flow-agents-statusline.js"\'' },
|
|
@@ -355,7 +354,6 @@ function exportCodexHooks(): string {
|
|
|
355
354
|
hooks.SessionStart.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
|
|
356
355
|
hooks.UserPromptSubmit.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
|
|
357
356
|
hooks.PostToolUse.push({ hooks: [shellHook(codexPolicy("evidence-capture.js"), 30, "Capturing Flow Agents command evidence")] });
|
|
358
|
-
hooks.PreToolUse.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Enforcing Flow Agents projected action")] });
|
|
359
357
|
return `${JSON.stringify({ hooks }, null, 2)}\n`;
|
|
360
358
|
}
|
|
361
359
|
|