@kontourai/flow-agents 3.4.3 → 3.6.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 (65) hide show
  1. package/.github/workflows/ci.yml +4 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/src/builder-flow-run-adapter.d.ts +10 -1
  4. package/build/src/builder-flow-run-adapter.js +29 -1
  5. package/build/src/builder-flow-runtime.d.ts +18 -0
  6. package/build/src/builder-flow-runtime.js +205 -13
  7. package/build/src/builder-lifecycle-authority.d.ts +35 -0
  8. package/build/src/builder-lifecycle-authority.js +219 -0
  9. package/build/src/cli/assignment-provider.d.ts +13 -0
  10. package/build/src/cli/assignment-provider.js +120 -62
  11. package/build/src/cli/builder-run.js +46 -5
  12. package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
  13. package/build/src/cli/workflow-sidecar.d.ts +3 -0
  14. package/build/src/cli/workflow-sidecar.js +140 -30
  15. package/build/src/cli/workflow.d.ts +2 -0
  16. package/build/src/cli/workflow.js +521 -0
  17. package/build/src/cli.js +2 -0
  18. package/build/src/index.d.ts +4 -0
  19. package/build/src/index.js +2 -0
  20. package/build/src/lib/flow-resolver.js +7 -2
  21. package/build/src/lib/package-version.d.ts +2 -0
  22. package/build/src/lib/package-version.js +13 -0
  23. package/build/src/lib/pinned-cli-command.d.ts +6 -0
  24. package/build/src/lib/pinned-cli-command.js +21 -0
  25. package/context/contracts/artifact-contract.md +1 -1
  26. package/context/contracts/assignment-provider-contract.md +5 -2
  27. package/context/scripts/hooks/config-protection.js +8 -1
  28. package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
  29. package/context/scripts/hooks/stop-goal-fit.js +1 -1
  30. package/docs/context-map.md +2 -0
  31. package/docs/public-workflow-cli.md +63 -0
  32. package/docs/spec/builder-flow-runtime.md +49 -5
  33. package/docs/workflow-usage-guide.md +5 -0
  34. package/evals/ci/run-baseline.sh +2 -0
  35. package/evals/integration/test_assignment_provider_local_file.sh +54 -0
  36. package/evals/integration/test_builder_entry_enforcement.sh +241 -24
  37. package/evals/integration/test_bundle_install.sh +97 -0
  38. package/evals/integration/test_current_json_per_actor.sh +1 -0
  39. package/evals/integration/test_dual_emit_flow_step.sh +2 -0
  40. package/evals/integration/test_flowdef_session_activation.sh +6 -3
  41. package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
  42. package/evals/integration/test_phase_map_and_gate_claim.sh +4 -0
  43. package/evals/integration/test_public_workflow_cli.sh +259 -0
  44. package/evals/integration/test_workflow_sidecar_writer.sh +1 -0
  45. package/package.json +2 -2
  46. package/schemas/builder-lifecycle-authorization.schema.json +57 -0
  47. package/schemas/lifecycle-authority-keys.schema.json +25 -0
  48. package/schemas/workflow-state.schema.json +1 -1
  49. package/scripts/hooks/config-protection.js +8 -1
  50. package/scripts/hooks/lib/config-protection-remedies.js +3 -0
  51. package/scripts/hooks/stop-goal-fit.js +1 -1
  52. package/src/builder-flow-run-adapter.ts +47 -0
  53. package/src/builder-flow-runtime.ts +216 -4
  54. package/src/builder-lifecycle-authority.ts +218 -0
  55. package/src/cli/assignment-provider.ts +84 -20
  56. package/src/cli/builder-flow-runtime.test.mjs +404 -1
  57. package/src/cli/builder-run.ts +56 -5
  58. package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
  59. package/src/cli/workflow-sidecar.ts +138 -31
  60. package/src/cli/workflow.ts +471 -0
  61. package/src/cli.ts +2 -0
  62. package/src/index.ts +14 -0
  63. package/src/lib/flow-resolver.ts +6 -2
  64. package/src/lib/package-version.ts +15 -0
  65. package/src/lib/pinned-cli-command.ts +23 -0
@@ -0,0 +1,521 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { createRequire } from "node:module";
5
+ import { isDeepStrictEqual } from "node:util";
6
+ import { loadBuilderBuildRun } from "../builder-flow-run-adapter.js";
7
+ import { flowAgentsPackageRoot, flowAgentsPackageVersion } from "../lib/package-version.js";
8
+ import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
9
+ import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
10
+ import { flagBool, flagString, parseArgs } from "../lib/args.js";
11
+ import { main as builderRun } from "./builder-run.js";
12
+ import { currentWorkflowSessionDir, main as workflowWriter, WORKFLOW_WRITER_CONTRACT_VERSION } from "./workflow-sidecar.js";
13
+ export const WORKFLOW_CONTRACT_VERSION = "1.0";
14
+ const PACKAGE_ROOT = flowAgentsPackageRoot();
15
+ const REQUIRE = createRequire(import.meta.url);
16
+ const PACKAGE_METADATA = readJsonFile(path.join(PACKAGE_ROOT, "package.json"), "Flow Agents package metadata");
17
+ const CLI_VERSION = flowAgentsPackageVersion();
18
+ const PUBLIC_VERBS = ["start", "status", "evidence", "pause", "resume", "release", "cancel", "archive", "doctor"];
19
+ function usage() {
20
+ console.log(`Usage: flow-agents workflow <verb> [options]
21
+
22
+ Public workflow verbs:
23
+ start Start or resume a workflow for a Work Item.
24
+ status Show the current canonical run and projected next action.
25
+ evidence Record evidence for the current Flow gate and synchronize it.
26
+ pause Pause the current run as its assignment actor.
27
+ resume Resume the current paused run as its assignment actor.
28
+ release Release the current assignment without canceling the run.
29
+ cancel Cancel through a signed user/operator authorization record.
30
+ archive Archive a terminal session through a signed authorization record.
31
+ doctor Report CLI, install, Kit, Flow, and artifact compatibility.
32
+
33
+ Use the isolated exact-package command emitted by workflow status and doctor in automation.`);
34
+ }
35
+ export async function main(argv) {
36
+ const parsed = parseArgs(argv);
37
+ const verb = parsed.positionals[0];
38
+ if (!verb || verb === "help" || verb === "--help" || verb === "-h") {
39
+ usage();
40
+ return 0;
41
+ }
42
+ if (!PUBLIC_VERBS.includes(verb)) {
43
+ console.error(`Unknown workflow verb: ${verb}`);
44
+ usage();
45
+ return 64;
46
+ }
47
+ if (verb === "start")
48
+ return start(argv.slice(1));
49
+ if (verb === "doctor")
50
+ return doctor(argv.slice(1));
51
+ const sessionDir = resolveSessionDir(parsed.flags);
52
+ if (verb === "status")
53
+ return status(sessionDir, flagBool(parsed.flags, "json"));
54
+ if (verb === "evidence")
55
+ return evidence(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
56
+ const forwarded = stripPublicFlags(argv.slice(1), new Set(["artifact-root", "session-dir", "json"]));
57
+ if (verb === "release" && !flagString(parsed.flags, "reason"))
58
+ throw new Error("workflow release requires --reason <text>");
59
+ return builderRun([verb === "release" ? "release-assignment" : verb, "--session-dir", sessionDir, ...forwarded]);
60
+ }
61
+ async function start(argv) {
62
+ const parsed = parseArgs(argv);
63
+ assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion"]), "workflow start");
64
+ const flow = flagString(parsed.flags, "flow", "builder.build");
65
+ if (flow !== "builder.build")
66
+ throw new Error("workflow start currently supports only --flow builder.build");
67
+ const workItem = flagString(parsed.flags, "work-item");
68
+ if (!workItem)
69
+ throw new Error("workflow start requires --work-item <owner/repo#id>");
70
+ const artifactRoot = path.resolve(flagString(parsed.flags, "artifact-root", flowAgentsArtifactRoot()));
71
+ const taskSlug = flagString(parsed.flags, "task-slug");
72
+ if (workItem.startsWith("local:")) {
73
+ const localSlug = workItem.slice("local:".length);
74
+ if (!taskSlug || taskSlug !== localSlug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(localSlug)) {
75
+ throw new Error("local Work Item retries require the exact existing --task-slug binding");
76
+ }
77
+ const sessionDir = validateCanonicalSessionDir(path.join(artifactRoot, taskSlug));
78
+ const localRecord = readJsonFile(path.join(sessionDir, "work-item.json"), "local Work Item binding");
79
+ if (localRecord.id !== taskSlug || localRecord.source_provider?.kind !== "local") {
80
+ throw new Error("local Work Item retry does not match the existing session binding");
81
+ }
82
+ }
83
+ else if (taskSlug) {
84
+ throw new Error("--task-slug is reserved for an existing local Work Item retry");
85
+ }
86
+ const sourceRequest = flagString(parsed.flags, "source-request", `Start ${flow} for ${workItem}`);
87
+ const summary = flagString(parsed.flags, "summary", `Deliver ${workItem} through ${flow}.`);
88
+ const forwarded = keepFlags(argv, new Set(["title", "criterion"]));
89
+ return workflowWriter([
90
+ "ensure-session",
91
+ "--artifact-root", artifactRoot,
92
+ "--flow-id", flow,
93
+ "--work-item", workItem,
94
+ ...(taskSlug ? ["--task-slug", taskSlug] : []),
95
+ "--source-request", sourceRequest,
96
+ "--summary", summary,
97
+ ...forwarded,
98
+ ]);
99
+ }
100
+ async function status(sessionDir, json) {
101
+ const sidecar = readJsonFile(path.join(sessionDir, "state.json"), "workflow state");
102
+ const slug = String(sidecar.task_slug ?? path.basename(sessionDir));
103
+ const projectRoot = path.dirname(path.dirname(path.dirname(sessionDir)));
104
+ const result = await loadBuilderBuildRun({ cwd: projectRoot, runId: slug });
105
+ const report = {
106
+ run_id: result.runId,
107
+ definition_id: result.definitionId,
108
+ definition_version: result.definitionVersion,
109
+ status: result.state.status,
110
+ current_step: result.state.current_step,
111
+ session_dir: sessionDir,
112
+ next_action: sidecar.next_action ?? null,
113
+ };
114
+ if (json)
115
+ console.log(JSON.stringify(report));
116
+ else {
117
+ console.log(`${report.definition_id}@${report.definition_version} ${report.run_id}`);
118
+ console.log(`Status: ${report.status}`);
119
+ console.log(`Step: ${report.current_step}`);
120
+ console.log(`Next: ${String(report.next_action && typeof report.next_action === "object" ? report.next_action.summary ?? "" : "")}`);
121
+ }
122
+ return 0;
123
+ }
124
+ async function evidence(sessionDir, argv, json) {
125
+ const parsed = parseArgs(argv);
126
+ assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "expectation", "status", "summary", "evidence-ref-json", "accepted-gap-reason", "waived-by"]), "workflow evidence");
127
+ if (!flagString(parsed.flags, "expectation"))
128
+ throw new Error("workflow evidence requires --expectation <gate-expectation-id>");
129
+ if (!flagString(parsed.flags, "status"))
130
+ throw new Error("workflow evidence requires --status <pass|fail|not_verified>");
131
+ if (!flagString(parsed.flags, "summary"))
132
+ throw new Error("workflow evidence requires --summary <text>");
133
+ const forwarded = stripPublicFlags(argv, new Set(["artifact-root", "session-dir", "json"]));
134
+ await workflowWriter(["record-gate-claim", sessionDir, ...forwarded]);
135
+ const sidecar = readJsonFile(path.join(sessionDir, "state.json"), "workflow state");
136
+ const projectRoot = path.dirname(path.dirname(path.dirname(sessionDir)));
137
+ const result = await loadBuilderBuildRun({ cwd: projectRoot, runId: String(sidecar.task_slug ?? path.basename(sessionDir)) });
138
+ const report = {
139
+ run_id: result.runId,
140
+ status: result.state.status,
141
+ current_step: result.state.current_step,
142
+ attached: result.attachedEvidence.length > 0,
143
+ next_action: sidecar.next_action ?? null,
144
+ };
145
+ if (json)
146
+ console.log(JSON.stringify(report));
147
+ else
148
+ console.log(`Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`);
149
+ return 0;
150
+ }
151
+ function resolveSessionDir(flags) {
152
+ const explicit = flagString(flags, "session-dir");
153
+ if (explicit)
154
+ return validateCanonicalSessionDir(path.resolve(explicit));
155
+ const artifactRoot = path.resolve(flagString(flags, "artifact-root", defaultArtifactRootForRead()));
156
+ const candidate = currentWorkflowSessionDir(artifactRoot);
157
+ if (!candidate || !isWithin(candidate, artifactRoot) || !fs.existsSync(path.join(candidate, "state.json"))) {
158
+ throw new Error("current workflow pointer does not resolve to a valid session; pass --session-dir explicitly");
159
+ }
160
+ return validateCanonicalSessionDir(candidate);
161
+ }
162
+ function doctor(argv) {
163
+ const parsed = parseArgs(argv);
164
+ const projectRoot = path.resolve(flagString(parsed.flags, "project-root", process.cwd()));
165
+ const artifactRoot = path.resolve(flagString(parsed.flags, "artifact-root", defaultArtifactRootForRead(projectRoot)));
166
+ const packageRoot = PACKAGE_ROOT;
167
+ const packageJson = PACKAGE_METADATA;
168
+ const cliVersion = CLI_VERSION;
169
+ const installFile = path.join(projectRoot, ".flow-agents", "install.json");
170
+ const install = readOptionalJson(installFile);
171
+ const state = readCurrentState(artifactRoot);
172
+ const packageKit = readOptionalJson(path.join(packageRoot, "kits", "builder", "kit.json"));
173
+ const packageFlow = readOptionalJson(path.join(packageRoot, "kits", "builder", "flows", "build.flow.json"));
174
+ const installedKitFile = path.join(projectRoot, "kits", "builder", "kit.json");
175
+ const installedFlowFile = path.join(projectRoot, "kits", "builder", "flows", "build.flow.json");
176
+ const installedKit = readOptionalJson(installedKitFile);
177
+ const installedFlow = readOptionalJson(installedFlowFile);
178
+ const resolvedFlowPackage = readOptionalJson(resolveDependencyPackageJson("@kontourai/flow"));
179
+ const installedVersion = typeof install?.version === "string" ? install.version : null;
180
+ const staleInstall = installedVersion !== null && installedVersion !== cliVersion;
181
+ const activeKitIds = Array.isArray(install?.active_kit_ids) ? install.active_kit_ids.map(String) : (installedKit ? ["builder"] : []);
182
+ const runtime = typeof install?.runtime === "string" ? install.runtime : "base";
183
+ const remediation = pinnedFlowAgentsCommand(cliVersion, ["init", "--runtime", runtime, "--dest", projectRoot, ...activeKitIds.flatMap((id) => ["--activate-kit", id]), "--yes"]);
184
+ const localDependencyFile = path.join(projectRoot, "node_modules", "@kontourai", "flow-agents", "package.json");
185
+ const localDependency = readOptionalJson(localDependencyFile);
186
+ const installIntegrity = verifyInstalledAssets(projectRoot, packageRoot, runtime);
187
+ const warnings = [];
188
+ if (!install)
189
+ warnings.push(`No installed hook/bundle version found at ${installFile}. Run: ${remediation}`);
190
+ else if (staleInstall)
191
+ warnings.push(`Installed hook/writer version ${installedVersion} is incompatible with CLI ${cliVersion}. Run: ${remediation}`);
192
+ if (!installIntegrity.ok)
193
+ warnings.push(`Installed hook/writer assets failed integrity verification: ${installIntegrity.problems.join("; ")}. Run: ${remediation}`);
194
+ if (localDependency?.version && localDependency.version !== cliVersion)
195
+ warnings.push(`Repository-local Flow Agents ${String(localDependency.version)} differs from executing CLI ${cliVersion}; keep automation explicitly versioned.`);
196
+ if (activeKitIds.includes("builder") && !installedKit)
197
+ warnings.push(`Activated Builder Kit is missing at ${installedKitFile}. Run: ${remediation}`);
198
+ if (activeKitIds.includes("builder") && !installedFlow)
199
+ warnings.push(`Activated builder.build definition is missing at ${installedFlowFile}. Run: ${remediation}`);
200
+ if (installedKit && installedKit.schema_version !== packageKit?.schema_version) {
201
+ warnings.push(`Installed Builder Kit schema ${String(installedKit.schema_version)} differs from CLI schema ${String(packageKit?.schema_version)}. Run: ${remediation}`);
202
+ }
203
+ if (installedKit && fileSha256(installedKitFile) !== fileSha256(path.join(packageRoot, "kits", "builder", "kit.json"))) {
204
+ warnings.push(`Installed Builder Kit content differs from Flow Agents ${cliVersion}. Run: ${remediation}`);
205
+ }
206
+ if (installedFlow && installedFlow.version !== packageFlow?.version) {
207
+ warnings.push(`Installed builder.build version ${String(installedFlow.version)} differs from CLI version ${String(packageFlow?.version)}. Run: ${remediation}`);
208
+ }
209
+ if (state?.flow_run && state.flow_run.definition_version !== packageFlow?.version) {
210
+ warnings.push(`Current run uses builder.build@${String(state.flow_run.definition_version)} while CLI resolves ${String(packageFlow?.version)}; recover or migrate before mutation.`);
211
+ }
212
+ if (state && state.schema_version !== "1.0")
213
+ warnings.push(`Artifact schema ${String(state.schema_version)} is unsupported; recreate or migrate the session with CLI ${cliVersion}.`);
214
+ const trustBundleSchema = readCurrentTrustBundleSchema(artifactRoot);
215
+ if (trustBundleSchema !== null && trustBundleSchema !== "1.0")
216
+ warnings.push(`Trust bundle schema ${String(trustBundleSchema)} is unsupported; recreate or migrate the session with CLI ${cliVersion}.`);
217
+ const resolvedFlowVersion = typeof resolvedFlowPackage?.version === "string" ? resolvedFlowPackage.version : null;
218
+ const expectedFlowRange = packageJson.dependencies && typeof packageJson.dependencies === "object" ? String(packageJson.dependencies["@kontourai/flow"] ?? "") : "";
219
+ if (!resolvedFlowVersion || !flowVersionCompatible(resolvedFlowVersion, expectedFlowRange)) {
220
+ warnings.push(`Resolved Flow runtime ${resolvedFlowVersion ?? "missing"} does not satisfy ${expectedFlowRange || "the package contract"}. Reinstall Flow Agents ${cliVersion}.`);
221
+ }
222
+ const report = {
223
+ ok: warnings.length === 0,
224
+ project_root: projectRoot,
225
+ cli: { version: cliVersion, workflow_contract_version: WORKFLOW_CONTRACT_VERSION, package_root: packageRoot },
226
+ writer: { contract_version: WORKFLOW_WRITER_CONTRACT_VERSION, package_version: cliVersion },
227
+ hook: { contract_version: install && !staleInstall && installIntegrity.ok ? WORKFLOW_CONTRACT_VERSION : null, install_version: installedVersion, path: installIntegrity.hook_config, integrity: installIntegrity },
228
+ local_dependency: {
229
+ version: localDependency?.version ?? null,
230
+ path: localDependency ? path.dirname(localDependencyFile) : null,
231
+ selected: localDependency ? fs.realpathSync(path.dirname(localDependencyFile)) === fs.realpathSync(packageRoot) : false,
232
+ },
233
+ installed: {
234
+ hooks: { version: installedVersion, source: installIntegrity.hook_config, compatible: Boolean(install && !staleInstall && installIntegrity.ok) },
235
+ writer: { version: cliVersion, source: packageRoot, compatible: true },
236
+ runtime: install?.runtime ?? null,
237
+ active_kit_ids: activeKitIds,
238
+ },
239
+ kit: {
240
+ id: packageKit?.id ?? null,
241
+ resolved_schema_version: packageKit?.schema_version ?? null,
242
+ installed_schema_version: installedKit?.schema_version ?? null,
243
+ resolved_content_sha256: fileSha256(path.join(packageRoot, "kits", "builder", "kit.json")),
244
+ installed_content_sha256: installedKit ? fileSha256(installedKitFile) : null,
245
+ source: installedKit ? installedKitFile : path.join(packageRoot, "kits", "builder", "kit.json"),
246
+ },
247
+ flow_runtime: { package_version: resolvedFlowVersion, expected_range: expectedFlowRange, compatible: resolvedFlowVersion ? flowVersionCompatible(resolvedFlowVersion, expectedFlowRange) : false },
248
+ definition: { id: state?.flow_run?.definition_id ?? packageFlow?.id ?? null, version: state?.flow_run?.definition_version ?? packageFlow?.version ?? null },
249
+ artifact: {
250
+ state_schema_version: state?.schema_version ?? null,
251
+ trust_bundle_schema_version: trustBundleSchema,
252
+ session: state ? resolveStateSession(artifactRoot) : null,
253
+ },
254
+ warnings,
255
+ remediation: warnings.length ? remediation : null,
256
+ };
257
+ if (flagBool(parsed.flags, "json"))
258
+ console.log(JSON.stringify(report));
259
+ else {
260
+ console.log(`Flow Agents CLI: ${cliVersion}`);
261
+ console.log(`Installed hooks/writer: ${installedVersion ?? "missing"}`);
262
+ console.log(`Builder Kit schema: installed=${String(installedKit?.schema_version ?? "missing")} resolved=${String(packageKit?.schema_version ?? "missing")}`);
263
+ console.log(`Flow: ${String(report.definition.id ?? "none")}@${String(report.definition.version ?? "unknown")}`);
264
+ console.log(`Artifact schema: state=${String(report.artifact.state_schema_version ?? "none")} trust=${String(report.artifact.trust_bundle_schema_version ?? "none")}`);
265
+ for (const warning of warnings)
266
+ console.log(`WARNING: ${warning}`);
267
+ }
268
+ return report.ok ? 0 : 2;
269
+ }
270
+ function readCurrentState(artifactRoot) {
271
+ try {
272
+ const session = resolveSessionDir({ "artifact-root": artifactRoot });
273
+ return readJsonFile(path.join(session, "state.json"), "workflow state");
274
+ }
275
+ catch {
276
+ return null;
277
+ }
278
+ }
279
+ function resolveStateSession(artifactRoot) {
280
+ try {
281
+ return resolveSessionDir({ "artifact-root": artifactRoot });
282
+ }
283
+ catch {
284
+ return null;
285
+ }
286
+ }
287
+ function readJsonFile(file, label) {
288
+ const descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
289
+ try {
290
+ const stat = fs.fstatSync(descriptor);
291
+ if (!stat.isFile() || stat.size > 1024 * 1024)
292
+ throw new Error(`${label} must be a regular file no larger than 1 MiB`);
293
+ const value = JSON.parse(fs.readFileSync(descriptor, "utf8"));
294
+ if (!value || typeof value !== "object" || Array.isArray(value))
295
+ throw new Error(`${label} must be a JSON object`);
296
+ return value;
297
+ }
298
+ finally {
299
+ fs.closeSync(descriptor);
300
+ }
301
+ }
302
+ function readOptionalJson(file) {
303
+ try {
304
+ return readJsonFile(file, file);
305
+ }
306
+ catch (error) {
307
+ if (error.code === "ENOENT")
308
+ return null;
309
+ throw error;
310
+ }
311
+ }
312
+ function stripPublicFlags(argv, removed) {
313
+ const result = [];
314
+ for (let index = 0; index < argv.length; index += 1) {
315
+ const token = argv[index];
316
+ if (!token.startsWith("--")) {
317
+ result.push(token);
318
+ continue;
319
+ }
320
+ const equals = token.indexOf("=");
321
+ const key = token.slice(2, equals === -1 ? undefined : equals);
322
+ if (!removed.has(key)) {
323
+ result.push(token);
324
+ continue;
325
+ }
326
+ if (equals === -1 && argv[index + 1] !== undefined && !argv[index + 1].startsWith("--"))
327
+ index += 1;
328
+ }
329
+ return result;
330
+ }
331
+ function keepFlags(argv, kept) {
332
+ const result = [];
333
+ for (let index = 0; index < argv.length; index += 1) {
334
+ const token = argv[index];
335
+ if (!token.startsWith("--"))
336
+ continue;
337
+ const equals = token.indexOf("=");
338
+ const key = token.slice(2, equals === -1 ? undefined : equals);
339
+ const hasValue = equals === -1 && argv[index + 1] !== undefined && !argv[index + 1].startsWith("--");
340
+ if (kept.has(key)) {
341
+ result.push(token);
342
+ if (hasValue)
343
+ result.push(argv[index + 1]);
344
+ }
345
+ if (hasValue)
346
+ index += 1;
347
+ }
348
+ return result;
349
+ }
350
+ function assertOnlyFlags(flags, allowed, command) {
351
+ const unsupported = Object.keys(flags).find((key) => !allowed.has(key));
352
+ if (unsupported)
353
+ throw new Error(`${command} does not support --${unsupported}`);
354
+ }
355
+ function isWithin(candidate, root) {
356
+ const relative = path.relative(root, candidate);
357
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
358
+ }
359
+ function fileSha256(file) {
360
+ return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
361
+ }
362
+ function readCurrentTrustBundleSchema(artifactRoot) {
363
+ const session = resolveStateSession(artifactRoot);
364
+ if (!session)
365
+ return null;
366
+ return readOptionalJson(path.join(session, "trust.bundle"))?.schema_version ?? null;
367
+ }
368
+ function validateCanonicalSessionDir(candidate) {
369
+ const sessionDir = path.resolve(candidate);
370
+ const artifactRoot = path.dirname(sessionDir);
371
+ const kontouraiRoot = path.dirname(artifactRoot);
372
+ const projectRoot = path.dirname(kontouraiRoot);
373
+ if (path.basename(artifactRoot) !== "flow-agents" || path.basename(kontouraiRoot) !== ".kontourai" || path.dirname(sessionDir) !== artifactRoot) {
374
+ throw new Error("workflow session must be .kontourai/flow-agents/<slug>");
375
+ }
376
+ for (const [label, entry, kind] of [
377
+ ["project root", projectRoot, "directory"],
378
+ [".kontourai root", kontouraiRoot, "directory"],
379
+ ["artifact root", artifactRoot, "directory"],
380
+ ["session directory", sessionDir, "directory"],
381
+ ["workflow state", path.join(sessionDir, "state.json"), "file"],
382
+ ]) {
383
+ const stat = fs.lstatSync(entry);
384
+ if (stat.isSymbolicLink() || (kind === "directory" ? !stat.isDirectory() : !stat.isFile()))
385
+ throw new Error(`${label} must be a non-symlink ${kind}`);
386
+ }
387
+ const bundle = path.join(sessionDir, "trust.bundle");
388
+ if (fs.existsSync(bundle)) {
389
+ const stat = fs.lstatSync(bundle);
390
+ if (stat.isSymbolicLink() || !stat.isFile())
391
+ throw new Error("workflow trust bundle must be a non-symlink file");
392
+ }
393
+ return sessionDir;
394
+ }
395
+ function verifyInstalledAssets(projectRoot, packageRoot, runtime) {
396
+ const problems = [];
397
+ const bundleRoot = path.join(packageRoot, "dist", runtime);
398
+ for (const relativeRoot of ["build/src", "scripts/hooks"]) {
399
+ const expectedRoot = path.join(bundleRoot, relativeRoot);
400
+ if (!fs.existsSync(expectedRoot)) {
401
+ problems.push(`package bundle is missing: ${relativeRoot}`);
402
+ continue;
403
+ }
404
+ const pending = [expectedRoot];
405
+ while (pending.length > 0) {
406
+ const current = pending.pop();
407
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
408
+ const expected = path.join(current, entry.name);
409
+ if (entry.isDirectory()) {
410
+ pending.push(expected);
411
+ continue;
412
+ }
413
+ if (!entry.isFile())
414
+ continue;
415
+ const relative = path.relative(bundleRoot, expected);
416
+ const installed = path.join(projectRoot, relative);
417
+ try {
418
+ const stat = fs.lstatSync(installed);
419
+ const matches = runtime === "kiro"
420
+ ? fs.readFileSync(installed, "utf8") === fs.readFileSync(expected, "utf8").replaceAll("__KIRO_PACKAGE_ROOT__", projectRoot)
421
+ : fileSha256(installed) === fileSha256(expected);
422
+ if (stat.isSymbolicLink() || !stat.isFile() || !matches)
423
+ problems.push(`asset mismatch: ${relative}`);
424
+ }
425
+ catch {
426
+ problems.push(`asset missing: ${relative}`);
427
+ }
428
+ }
429
+ }
430
+ }
431
+ const verifyExactRuntimeFile = (relative, expectedContent) => {
432
+ const expected = path.join(bundleRoot, relative);
433
+ const installed = path.join(projectRoot, relative);
434
+ try {
435
+ const stat = fs.lstatSync(installed);
436
+ const matches = expectedContent === undefined ? fileSha256(installed) === fileSha256(expected) : fs.readFileSync(installed, "utf8") === expectedContent;
437
+ if (stat.isSymbolicLink() || !stat.isFile() || !matches)
438
+ problems.push(`runtime wiring mismatch: ${relative}`);
439
+ }
440
+ catch {
441
+ problems.push(`runtime wiring missing: ${relative}`);
442
+ }
443
+ };
444
+ const verifyManagedHooks = (relative) => {
445
+ const installedFile = path.join(projectRoot, relative);
446
+ try {
447
+ const stat = fs.lstatSync(installedFile);
448
+ const installed = readJsonFile(installedFile, "installed runtime hook configuration");
449
+ const expected = readJsonFile(path.join(bundleRoot, relative), "packaged runtime hook configuration");
450
+ const installedHooks = installed.hooks;
451
+ const expectedHooks = expected.hooks;
452
+ const complete = !stat.isSymbolicLink() && stat.isFile() && installedHooks && expectedHooks && Object.entries(expectedHooks).every(([event, groups]) => {
453
+ const actualGroups = installedHooks[event];
454
+ return Array.isArray(groups) && Array.isArray(actualGroups) && groups.every((group) => actualGroups.some((actual) => isDeepStrictEqual(actual, group)));
455
+ });
456
+ if (!complete)
457
+ problems.push("runtime hook configuration does not contain the packaged managed hooks");
458
+ }
459
+ catch {
460
+ problems.push("runtime hook configuration is missing");
461
+ }
462
+ };
463
+ let hookConfig = null;
464
+ if (runtime === "codex") {
465
+ hookConfig = path.join(projectRoot, ".codex", "hooks.json");
466
+ verifyManagedHooks(".codex/hooks.json");
467
+ }
468
+ else if (runtime === "claude-code") {
469
+ hookConfig = path.join(projectRoot, ".claude", "settings.json");
470
+ verifyManagedHooks(".claude/settings.json");
471
+ }
472
+ else if (runtime === "opencode") {
473
+ hookConfig = path.join(projectRoot, ".opencode", "plugins", "flow-agents.js");
474
+ verifyExactRuntimeFile(".opencode/plugins/flow-agents.js");
475
+ }
476
+ else if (runtime === "pi") {
477
+ hookConfig = path.join(projectRoot, ".pi", "extensions", "flow-agents.ts");
478
+ verifyExactRuntimeFile(".pi/extensions/flow-agents.ts");
479
+ }
480
+ else if (runtime === "kiro") {
481
+ hookConfig = path.join(projectRoot, "agents", "dev.json");
482
+ const expectedAgentsRoot = path.join(bundleRoot, "agents");
483
+ try {
484
+ for (const entry of fs.readdirSync(expectedAgentsRoot, { withFileTypes: true })) {
485
+ if (!entry.isFile() || path.extname(entry.name) !== ".json")
486
+ continue;
487
+ const relative = path.join("agents", entry.name);
488
+ const rendered = fs.readFileSync(path.join(expectedAgentsRoot, entry.name), "utf8").replaceAll("__KIRO_PACKAGE_ROOT__", projectRoot);
489
+ verifyExactRuntimeFile(relative, rendered);
490
+ }
491
+ }
492
+ catch {
493
+ problems.push("runtime wiring missing: agents");
494
+ }
495
+ }
496
+ return { ok: problems.length === 0, problems, hook_config: hookConfig };
497
+ }
498
+ function flowVersionCompatible(version, range) {
499
+ const actual = version.match(/^(\d+)\.(\d+)\.(\d+)/);
500
+ const minimum = range.match(/(\d+)\.(\d+)\.(\d+)/);
501
+ if (!actual || !minimum)
502
+ return false;
503
+ const a = actual.slice(1).map(Number);
504
+ const m = minimum.slice(1).map(Number);
505
+ if (a[0] !== m[0])
506
+ return false;
507
+ return a[1] > m[1] || (a[1] === m[1] && a[2] >= m[2]);
508
+ }
509
+ function resolveDependencyPackageJson(packageName) {
510
+ let candidate = path.dirname(REQUIRE.resolve(packageName));
511
+ for (let depth = 0; depth < 8; depth += 1) {
512
+ const metadata = path.join(candidate, "package.json");
513
+ if (fs.existsSync(metadata))
514
+ return metadata;
515
+ const parent = path.dirname(candidate);
516
+ if (parent === candidate)
517
+ break;
518
+ candidate = parent;
519
+ }
520
+ throw new Error(`could not resolve ${packageName} package metadata`);
521
+ }
package/build/src/cli.js CHANGED
@@ -15,6 +15,7 @@ import { main as telemetryDoctor } from "./cli/telemetry-doctor.js";
15
15
  import { main as usageFeedback } from "./cli/usage-feedback.js";
16
16
  import { main as veritasGovernance } from "./cli/veritas-governance.js";
17
17
  import { main as workflowArtifactCleanupAudit } from "./cli/workflow-artifact-cleanup-audit.js";
18
+ import { main as workflow } from "./cli/workflow.js";
18
19
  import { main as buildBundles } from "./tools/build-universal-bundles.js";
19
20
  import { main as contextMap } from "./tools/generate-context-map.js";
20
21
  import { main as validateSource } from "./tools/validate-source-tree.js";
@@ -47,6 +48,7 @@ const availableCommands = new Map([
47
48
  ["validate-package", validatePackage],
48
49
  ["validate-hook-influence", validateHookInfluence],
49
50
  ["verify", verify],
51
+ ["workflow", workflow],
50
52
  ["validate-source", validateSource],
51
53
  ["workflow-artifact-cleanup-audit", workflowArtifactCleanupAudit],
52
54
  ]);
@@ -1,5 +1,9 @@
1
1
  export { BUILDER_BUILD_FLOW_ID, BUILDER_BUILD_FLOW_RELATIVE_PATH, BuilderBuildRunInputError, BuilderBuildRunIdentityError, evaluateBuilderBuildRun, loadBuilderBuildRun, resolveBuilderBuildFlowDefinitionPath, startBuilderBuildRun, } from "./builder-flow-run-adapter.js";
2
2
  export type { BuilderBuildRunResult, BuilderBuildRunIdentityMismatch, BuilderBuildTrustBundleEvidenceInput, EvaluateBuilderBuildRunInput, LoadBuilderBuildRunInput, StartBuilderBuildRunInput, } from "./builder-flow-run-adapter.js";
3
+ export { archiveBuilderFlowSession, cancelBuilderFlowSession, pauseBuilderFlowSession, recoverBuilderFlowSession, releaseBuilderFlowAssignment, resumeBuilderFlowSession, startBuilderFlowSession, syncBuilderFlowSession, } from "./builder-flow-runtime.js";
4
+ export type { BuilderFlowAgentLifecycleInput, BuilderFlowAuthorizedLifecycleInput, BuilderFlowSessionInput, BuilderFlowSessionResult } from "./builder-flow-runtime.js";
5
+ export { builderLifecycleAuthorizationPayload, loadBuilderLifecycleAuthorization } from "./builder-lifecycle-authority.js";
6
+ export type { BuilderLifecycleAuthorization } from "./builder-lifecycle-authority.js";
3
7
  export { defaultArtifactRootForRead, defaultCodexHome, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, durableFlowAgentsRoot, durableInstallRecordPath, DURABLE_FLOW_AGENTS_DIR, FLOW_AGENTS_RUNTIME_DIR, FLOW_AGENTS_RUNTIME_SUBDIR, firstExistingPath, flowAgentsArtifactRoot, KONTOURAI_DIR, legacyTelemetryDataDir, LEGACY_TELEMETRY_DIR, telemetryDataDir, } from "./lib/local-artifact-root.js";
4
8
  export { validateTrustBundle, normalizeCheck, normalizeFinding, normalizeLearning, normalizeEvidenceRefs, validateEvidenceRef, validateLearningCorrection, loadJson, writeJson, appendJsonl, sidecarBase, writeState, statuses, phases, checkKinds, checkStatuses, verdicts, } from "./cli/workflow-sidecar.js";
5
9
  /** Read a sidecar JSON file from a workflow artifact directory; returns `{}` if absent. */
@@ -16,6 +16,8 @@
16
16
  import * as path from "node:path";
17
17
  import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
18
18
  export { BUILDER_BUILD_FLOW_ID, BUILDER_BUILD_FLOW_RELATIVE_PATH, BuilderBuildRunInputError, BuilderBuildRunIdentityError, evaluateBuilderBuildRun, loadBuilderBuildRun, resolveBuilderBuildFlowDefinitionPath, startBuilderBuildRun, } from "./builder-flow-run-adapter.js";
19
+ export { archiveBuilderFlowSession, cancelBuilderFlowSession, pauseBuilderFlowSession, recoverBuilderFlowSession, releaseBuilderFlowAssignment, resumeBuilderFlowSession, startBuilderFlowSession, syncBuilderFlowSession, } from "./builder-flow-runtime.js";
20
+ export { builderLifecycleAuthorizationPayload, loadBuilderLifecycleAuthorization } from "./builder-lifecycle-authority.js";
19
21
  export { defaultArtifactRootForRead, defaultCodexHome, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, durableFlowAgentsRoot, durableInstallRecordPath, DURABLE_FLOW_AGENTS_DIR, FLOW_AGENTS_RUNTIME_DIR, FLOW_AGENTS_RUNTIME_SUBDIR, firstExistingPath, flowAgentsArtifactRoot, KONTOURAI_DIR, legacyTelemetryDataDir, LEGACY_TELEMETRY_DIR, telemetryDataDir, } from "./lib/local-artifact-root.js";
20
22
  export {
21
23
  // Trust-bundle (Hachure) validation — the same validator the writer uses.
@@ -392,8 +392,8 @@ export function resolvePhaseMap(flowId, repoRoot) {
392
392
  }
393
393
  /**
394
394
  * Find the repository root from a starting directory by walking upward to locate
395
- * the nearest ancestor that contains a `kits/` subdirectory. If none is found,
396
- * falls back to `process.cwd()` so the default "run from repo root" case still works.
395
+ * the nearest ancestor that contains a `kits/` subdirectory. A canonical `.kontourai`
396
+ * artifact path falls back to its owning project; other layouts fall back to cwd.
397
397
  *
398
398
  * This is required because the runtime artifact directory can live anywhere (temp dirs,
399
399
  * subprojects, CI workspaces) while the kits/ directory is always at the repo root.
@@ -409,6 +409,11 @@ function findRepoRoot(startDir) {
409
409
  break; // reached filesystem root
410
410
  dir = parent;
411
411
  }
412
+ // A canonical product artifact root identifies its owning project even when that
413
+ // consumer has no source-tree kits/. resolveFlowFilePath can then use the installed
414
+ // package fallback without consulting an unrelated ambient cwd.
415
+ if (path.basename(startDir) === ".kontourai")
416
+ return path.dirname(startDir);
412
417
  // Fallback: process.cwd() covers the common "run from repo root" case
413
418
  return process.cwd();
414
419
  }
@@ -0,0 +1,2 @@
1
+ export declare function flowAgentsPackageRoot(): string;
2
+ export declare function flowAgentsPackageVersion(): string;
@@ -0,0 +1,13 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export function flowAgentsPackageRoot() {
5
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
6
+ }
7
+ export function flowAgentsPackageVersion() {
8
+ const metadata = JSON.parse(fs.readFileSync(path.join(flowAgentsPackageRoot(), "package.json"), "utf8"));
9
+ if (typeof metadata.version !== "string" || !/^\d+\.\d+\.\d+(?:[-+].*)?$/.test(metadata.version)) {
10
+ throw new Error("Flow Agents package metadata does not contain a valid version");
11
+ }
12
+ return metadata.version;
13
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Build a portable command that cannot reuse a repository-local package with
3
+ * the same name and version. The temporary prefix is removed on every exit.
4
+ */
5
+ export declare function isolatedPackageCommand(packageSpec: string, binary: string, args: string[]): string;
6
+ export declare function pinnedFlowAgentsCommand(version: string, args: string[]): string;
@@ -0,0 +1,21 @@
1
+ function shellQuote(value) {
2
+ return `'${value.replaceAll("'", `'\\''`)}'`;
3
+ }
4
+ /**
5
+ * Build a portable command that cannot reuse a repository-local package with
6
+ * the same name and version. The temporary prefix is removed on every exit.
7
+ */
8
+ export function isolatedPackageCommand(packageSpec, binary, args) {
9
+ const script = [
10
+ "root=$(mktemp -d) || exit 1",
11
+ `trap 'rm -rf "$root"' EXIT HUP INT TERM`,
12
+ "package=$1",
13
+ "binary=$2",
14
+ "shift 2",
15
+ `npm exec --yes --prefix "$root" --package="$package" -- "$binary" "$@"`,
16
+ ].join("; ");
17
+ return `sh -c ${shellQuote(script)} sh ${[packageSpec, binary, ...args].map(shellQuote).join(" ")}`;
18
+ }
19
+ export function pinnedFlowAgentsCommand(version, args) {
20
+ return isolatedPackageCommand(`@kontourai/flow-agents@${version}`, "flow-agents", args);
21
+ }
@@ -137,7 +137,7 @@ Canonical vocabulary:
137
137
 
138
138
  | Field | Values |
139
139
  | --- | --- |
140
- | `state.status` | `new`, `planning`, `planned`, `in_progress`, `blocked`, `verifying`, `verified`, `needs_decision`, `not_verified`, `failed`, `delivered`, `accepted`, `archived` |
140
+ | `state.status` | `new`, `planning`, `planned`, `in_progress`, `blocked`, `verifying`, `verified`, `needs_decision`, `not_verified`, `failed`, `delivered`, `canceled`, `accepted`, `archived` |
141
141
  | `state.phase` | `idea`, `backlog`, `pickup`, `planning`, `execution`, `verification`, `goal_fit`, `evidence`, `release`, `learning`, `done` |
142
142
  | `next_action.status` | `continue`, `needs_user`, `blocked`, `done` |
143
143
  | `acceptance.criteria[].status` | `pending`, `pass`, `fail`, `not_verified`, `accepted_gap` |