@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/agents/dev.json +0 -5
  3. package/build/src/builder-flow-runtime.js +18 -12
  4. package/build/src/cli/workflow-sidecar.js +77 -5
  5. package/build/src/tools/build-universal-bundles.js +0 -2
  6. package/context/scripts/hooks/workflow-steering.js +2 -40
  7. package/docs/spec/builder-flow-runtime.md +12 -7
  8. package/docs/spec/runtime-hook-surface.md +4 -4
  9. package/evals/integration/test_builder_entry_enforcement.sh +138 -7
  10. package/evals/integration/test_builder_step_producers.sh +1 -1
  11. package/evals/integration/test_bundle_install.sh +3 -3
  12. package/evals/integration/test_current_json_per_actor.sh +2 -2
  13. package/evals/integration/test_dual_emit_flow_step.sh +41 -9
  14. package/evals/integration/test_flowdef_session_activation.sh +11 -5
  15. package/evals/integration/test_install_merge.sh +24 -0
  16. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -10
  17. package/evals/integration/test_resolvefirststep_security.sh +1 -1
  18. package/evals/integration/test_sidecar_field_preservation.sh +4 -2
  19. package/evals/integration/test_takeover_protocol.sh +1 -1
  20. package/evals/integration/test_workflow_sidecar_writer.sh +17 -6
  21. package/evals/integration/test_workflow_steering_hook.sh +0 -72
  22. package/package.json +1 -1
  23. package/schemas/workflow-state.schema.json +1 -0
  24. package/scripts/hooks/workflow-steering.js +2 -40
  25. package/scripts/install-merge.js +2 -0
  26. package/src/builder-flow-runtime.ts +23 -12
  27. package/src/cli/builder-flow-runtime.test.mjs +32 -0
  28. package/src/cli/workflow-sidecar.ts +76 -6
  29. 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 { fs.mkdirSync(lockDir); break; }
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
- fs.rmSync(lockDir, { recursive: true, force: true });
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