@kontourai/flow-agents 3.5.0 → 3.7.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 (111) 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 +32 -3
  4. package/build/src/builder-flow-run-adapter.js +113 -20
  5. package/build/src/builder-flow-runtime.d.ts +26 -3
  6. package/build/src/builder-flow-runtime.js +502 -49
  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 +14 -7
  10. package/build/src/cli/assignment-provider.js +128 -65
  11. package/build/src/cli/builder-run.js +46 -9
  12. package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
  13. package/build/src/cli/workflow-sidecar.d.ts +30 -3
  14. package/build/src/cli/workflow-sidecar.js +825 -99
  15. package/build/src/cli/workflow.d.ts +2 -0
  16. package/build/src/cli/workflow.js +769 -0
  17. package/build/src/cli.js +2 -0
  18. package/build/src/flow-kit/validate.d.ts +17 -0
  19. package/build/src/flow-kit/validate.js +340 -2
  20. package/build/src/index.d.ts +4 -0
  21. package/build/src/index.js +7 -5
  22. package/build/src/lib/observed-command.d.ts +7 -0
  23. package/build/src/lib/observed-command.js +100 -0
  24. package/build/src/lib/package-version.d.ts +2 -0
  25. package/build/src/lib/package-version.js +13 -0
  26. package/build/src/lib/pinned-cli-command.d.ts +6 -0
  27. package/build/src/lib/pinned-cli-command.js +21 -0
  28. package/build/src/tools/generate-context-map.js +5 -3
  29. package/context/contracts/artifact-contract.md +1 -1
  30. package/context/scripts/hooks/config-protection.js +8 -1
  31. package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
  32. package/context/scripts/hooks/lib/kit-catalog.js +1 -1
  33. package/context/scripts/hooks/stop-goal-fit.js +79 -10
  34. package/docs/context-map.md +24 -20
  35. package/docs/developer-architecture.md +1 -1
  36. package/docs/public-workflow-cli.md +132 -0
  37. package/docs/skills-map.md +51 -27
  38. package/docs/spec/builder-flow-runtime.md +78 -40
  39. package/docs/workflow-usage-guide.md +110 -38
  40. package/evals/ci/run-baseline.sh +2 -0
  41. package/evals/fixtures/hook-influence/cases.json +2 -2
  42. package/evals/integration/test_builder_entry_enforcement.sh +57 -45
  43. package/evals/integration/test_builder_step_producers.sh +297 -6
  44. package/evals/integration/test_bundle_install.sh +258 -55
  45. package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
  46. package/evals/integration/test_current_json_per_actor.sh +12 -0
  47. package/evals/integration/test_dual_emit_flow_step.sh +62 -43
  48. package/evals/integration/test_flowdef_session_activation.sh +145 -25
  49. package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
  50. package/evals/integration/test_gate_lockdown.sh +3 -5
  51. package/evals/integration/test_goal_fit_hook.sh +60 -2
  52. package/evals/integration/test_liveness_verdict.sh +14 -17
  53. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
  54. package/evals/integration/test_public_workflow_cli.sh +573 -0
  55. package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
  56. package/evals/integration/test_sidecar_field_preservation.sh +36 -11
  57. package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
  58. package/evals/integration/test_workflow_steering_hook.sh +15 -38
  59. package/evals/run.sh +2 -0
  60. package/evals/static/test_builder_skill_coherence.sh +247 -0
  61. package/evals/static/test_library_exports.sh +5 -2
  62. package/evals/static/test_workflow_skills.sh +13 -325
  63. package/kits/builder/flows/build.flow.json +22 -0
  64. package/kits/builder/flows/shape.flow.json +9 -9
  65. package/kits/builder/kit.json +70 -16
  66. package/kits/builder/skills/builder-shape/SKILL.md +75 -75
  67. package/kits/builder/skills/continue-work/SKILL.md +45 -106
  68. package/kits/builder/skills/deliver/SKILL.md +96 -442
  69. package/kits/builder/skills/design-probe/SKILL.md +40 -139
  70. package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
  71. package/kits/builder/skills/execute-plan/SKILL.md +54 -125
  72. package/kits/builder/skills/fix-bug/SKILL.md +42 -132
  73. package/kits/builder/skills/gate-review/SKILL.md +60 -211
  74. package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
  75. package/kits/builder/skills/learning-review/SKILL.md +63 -170
  76. package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
  77. package/kits/builder/skills/plan-work/SKILL.md +51 -185
  78. package/kits/builder/skills/pull-work/SKILL.md +136 -485
  79. package/kits/builder/skills/release-readiness/SKILL.md +66 -107
  80. package/kits/builder/skills/review-work/SKILL.md +89 -176
  81. package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
  82. package/kits/builder/skills/verify-work/SKILL.md +101 -113
  83. package/package.json +2 -2
  84. package/schemas/builder-lifecycle-authorization.schema.json +57 -0
  85. package/schemas/lifecycle-authority-keys.schema.json +25 -0
  86. package/schemas/workflow-state.schema.json +1 -1
  87. package/scripts/hooks/config-protection.js +8 -1
  88. package/scripts/hooks/lib/config-protection-remedies.js +3 -0
  89. package/scripts/hooks/lib/kit-catalog.js +1 -1
  90. package/scripts/hooks/stop-goal-fit.js +79 -10
  91. package/src/builder-flow-run-adapter.ts +156 -23
  92. package/src/builder-flow-runtime.ts +535 -53
  93. package/src/builder-lifecycle-authority.ts +218 -0
  94. package/src/cli/assignment-provider-lock.test.mjs +83 -0
  95. package/src/cli/assignment-provider.ts +91 -22
  96. package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
  97. package/src/cli/builder-flow-runtime.test.mjs +597 -8
  98. package/src/cli/builder-run.ts +54 -9
  99. package/src/cli/kit-metadata-security.test.mjs +232 -6
  100. package/src/cli/public-api.test.mjs +15 -0
  101. package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
  102. package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
  103. package/src/cli/workflow-sidecar.ts +748 -99
  104. package/src/cli/workflow.ts +708 -0
  105. package/src/cli.ts +2 -0
  106. package/src/flow-kit/validate.ts +320 -2
  107. package/src/index.ts +20 -5
  108. package/src/lib/observed-command.ts +96 -0
  109. package/src/lib/package-version.ts +15 -0
  110. package/src/lib/pinned-cli-command.ts +23 -0
  111. package/src/tools/generate-context-map.ts +5 -3
@@ -0,0 +1,769 @@
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 { fileURLToPath } from "node:url";
7
+ import { validateDefinition } from "@kontourai/flow";
8
+ import { loadBuilderFlowRun } from "../builder-flow-run-adapter.js";
9
+ import { inspectBuilderFlowSession, recoverBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
10
+ import { flowAgentsPackageRoot, flowAgentsPackageVersion } from "../lib/package-version.js";
11
+ import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
12
+ import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
13
+ import { flagBool, flagList, flagString, parseArgs } from "../lib/args.js";
14
+ import { main as builderRun } from "./builder-run.js";
15
+ import { currentWorkflowSessionDir, isMeaningfulTestCommand, mainFromPublicWorkflow, WORKFLOW_WRITER_CONTRACT_VERSION } from "./workflow-sidecar.js";
16
+ import { resolveCurrentAssignmentActor, withSubjectLock } from "./assignment-provider.js";
17
+ export const WORKFLOW_CONTRACT_VERSION = "1.0";
18
+ const PACKAGE_ROOT = flowAgentsPackageRoot();
19
+ const REQUIRE = createRequire(import.meta.url);
20
+ const PACKAGE_METADATA = readJsonFile(path.join(PACKAGE_ROOT, "package.json"), "Flow Agents package metadata");
21
+ const CLI_VERSION = flowAgentsPackageVersion();
22
+ const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "pause", "resume", "release", "cancel", "archive", "doctor"];
23
+ function usage() {
24
+ console.log(`Usage: flow-agents workflow <verb> [options]
25
+
26
+ Public workflow verbs:
27
+ start Start or resume a workflow for a Work Item.
28
+ status Show the current canonical run and projected next action.
29
+ evidence Record evidence for the current Flow gate and synchronize it.
30
+ critique Record review critique directly into the current trust bundle.
31
+ pause Pause the current run as its assignment actor.
32
+ resume Resume the current paused run as its assignment actor.
33
+ release Release the current assignment without canceling the run.
34
+ cancel Cancel through a signed user/operator authorization record.
35
+ archive Archive a terminal session through a signed authorization record.
36
+ doctor Report CLI, install, Kit, Flow, and artifact compatibility.
37
+
38
+ Use the isolated exact-package command emitted by workflow status and doctor in automation.`);
39
+ }
40
+ export async function main(argv) {
41
+ const parsed = parseArgs(argv);
42
+ const verb = parsed.positionals[0];
43
+ if (!verb || verb === "help" || verb === "--help" || verb === "-h") {
44
+ usage();
45
+ return 0;
46
+ }
47
+ if (!PUBLIC_VERBS.includes(verb)) {
48
+ console.error(`Unknown workflow verb: ${verb}`);
49
+ usage();
50
+ return 64;
51
+ }
52
+ if (verb === "start")
53
+ return start(argv.slice(1));
54
+ if (verb === "doctor")
55
+ return doctor(argv.slice(1));
56
+ const sessionDir = resolveSessionDir(parsed.flags);
57
+ if (verb === "status")
58
+ return status(sessionDir, flagBool(parsed.flags, "json"));
59
+ if (verb === "evidence")
60
+ return evidence(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
61
+ if (verb === "critique")
62
+ return critique(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
63
+ const forwarded = stripPublicFlags(argv.slice(1), new Set(["artifact-root", "session-dir", "json"]));
64
+ if (verb === "release" && !flagString(parsed.flags, "reason"))
65
+ throw new Error("workflow release requires --reason <text>");
66
+ return builderRun([verb === "release" ? "release-assignment" : verb, "--session-dir", sessionDir, ...forwarded]);
67
+ }
68
+ async function start(argv) {
69
+ const parsed = parseArgs(argv);
70
+ assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion", "assignment-provider", "effective-state-json"]), "workflow start");
71
+ const flow = flagString(parsed.flags, "flow", "builder.build");
72
+ if (flow !== "builder.build" && flow !== "builder.shape")
73
+ throw new Error("workflow start supports only --flow builder.build or builder.shape");
74
+ const workItem = flagString(parsed.flags, "work-item");
75
+ const artifactRoot = path.resolve(flagString(parsed.flags, "artifact-root", flowAgentsArtifactRoot()));
76
+ const taskSlug = flagString(parsed.flags, "task-slug");
77
+ if (flow === "builder.shape") {
78
+ if (!taskSlug || !isSafeSlug(taskSlug))
79
+ throw new Error("workflow start --flow builder.shape requires an explicit safe --task-slug");
80
+ if (workItem)
81
+ throw new Error("workflow start --flow builder.shape creates a local Work Item; omit --work-item");
82
+ }
83
+ else if (!workItem) {
84
+ throw new Error("workflow start requires --work-item <provider-ref>");
85
+ }
86
+ const assignmentProvider = flagString(parsed.flags, "assignment-provider", flow === "builder.shape" || workItem?.startsWith("local:") ? "local-file" : undefined);
87
+ const effectiveStateJson = flagString(parsed.flags, "effective-state-json");
88
+ if (flow === "builder.build" && workItem && !workItem.startsWith("local:") && !assignmentProvider) {
89
+ throw new Error("workflow start requires --assignment-provider <kind> for a provider-backed Work Item; provider identity is never inferred from its reference");
90
+ }
91
+ if (assignmentProvider !== "local-file" && !effectiveStateJson) {
92
+ throw new Error(`workflow start requires --effective-state-json <path> for assignment provider ${assignmentProvider}`);
93
+ }
94
+ if (assignmentProvider === "local-file" && effectiveStateJson) {
95
+ throw new Error("workflow start --effective-state-json is only valid for a non-local assignment provider");
96
+ }
97
+ if (workItem?.startsWith("local:")) {
98
+ const localSlug = workItem.slice("local:".length);
99
+ if (!taskSlug || taskSlug !== localSlug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(localSlug)) {
100
+ throw new Error("local Work Item retries require the exact existing --task-slug binding");
101
+ }
102
+ const sessionDir = validateCanonicalSessionDir(path.join(artifactRoot, taskSlug));
103
+ const localRecord = readJsonFile(path.join(sessionDir, "work-item.json"), "local Work Item binding");
104
+ if (localRecord.id !== taskSlug || localRecord.source_provider?.kind !== "local") {
105
+ throw new Error("local Work Item retry does not match the existing session binding");
106
+ }
107
+ }
108
+ else if (taskSlug && flow !== "builder.shape") {
109
+ throw new Error("--task-slug is reserved for an existing local Work Item retry");
110
+ }
111
+ const existingSlug = taskSlug ?? (workItem ? workItemSlug(workItem) : null);
112
+ if (existingSlug && fs.existsSync(path.join(artifactRoot, existingSlug, "state.json"))) {
113
+ try {
114
+ const existing = await loadBuilderFlowRun({ cwd: path.dirname(path.dirname(artifactRoot)), runId: existingSlug });
115
+ if (existing.definitionId !== flow) {
116
+ throw new Error(`workflow start cannot resume ${existing.definitionId} as ${flow}; local shape sessions are not build retries. Create or claim a provider Work Item for the builder.build handoff.`);
117
+ }
118
+ }
119
+ catch (error) {
120
+ if (error.code !== "flow.run_location.not_found")
121
+ throw error;
122
+ }
123
+ }
124
+ if (flow === "builder.build" && workItem && !workItem.startsWith("local:")) {
125
+ const slug = workItemSlug(workItem);
126
+ const report = path.join(artifactRoot, slug, `${slug}--pull-work.md`);
127
+ try {
128
+ const stat = fs.lstatSync(report);
129
+ if (stat.isSymbolicLink() || !stat.isFile() || !fs.readFileSync(report, "utf8").includes(workItem)) {
130
+ throw new Error("invalid");
131
+ }
132
+ }
133
+ catch {
134
+ throw new Error(`workflow start requires concrete pull-work selection evidence at ${report} naming ${workItem} before it can produce selected-work`);
135
+ }
136
+ }
137
+ const sourceRequest = flagString(parsed.flags, "source-request", `Start ${flow} for ${workItem ?? `local:${taskSlug}`}`);
138
+ const summary = flagString(parsed.flags, "summary", `Deliver ${workItem ?? `local:${taskSlug}`} through ${flow}.`);
139
+ const forwarded = keepFlags(argv, new Set(["title", "criterion"]));
140
+ return mainFromPublicWorkflow([
141
+ "ensure-session",
142
+ "--artifact-root", artifactRoot,
143
+ "--flow-id", flow,
144
+ ...(workItem ? ["--work-item", workItem] : []),
145
+ ...(taskSlug ? ["--task-slug", taskSlug] : []),
146
+ ...(assignmentProvider ? ["--assignment-provider", assignmentProvider] : []),
147
+ ...(effectiveStateJson ? ["--effective-state-json", path.resolve(effectiveStateJson)] : []),
148
+ "--source-request", sourceRequest,
149
+ "--summary", summary,
150
+ ...forwarded,
151
+ ]);
152
+ }
153
+ function workItemSlug(workItem) {
154
+ return workItem.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
155
+ }
156
+ async function status(sessionDir, json) {
157
+ const inspected = await inspectBuilderFlowSession({ sessionDir });
158
+ const result = inspected.run;
159
+ const report = {
160
+ run_id: result.runId,
161
+ definition_id: result.definitionId,
162
+ definition_version: result.definitionVersion,
163
+ status: result.state.status,
164
+ current_step: result.state.current_step,
165
+ session_dir: sessionDir,
166
+ next_action: inspected.projection.next_action ?? null,
167
+ };
168
+ if (json)
169
+ console.log(JSON.stringify(report));
170
+ else {
171
+ console.log(`${report.definition_id}@${report.definition_version} ${report.run_id}`);
172
+ console.log(`Status: ${report.status}`);
173
+ console.log(`Step: ${report.current_step}`);
174
+ console.log(`Next: ${String(report.next_action && typeof report.next_action === "object" ? report.next_action.summary ?? "" : "")}`);
175
+ }
176
+ return 0;
177
+ }
178
+ async function evidence(sessionDir, argv, json) {
179
+ const parsed = parseArgs(argv);
180
+ assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "expectation", "status", "summary", "route-reason", "evidence-ref-json", "criterion-json", "accepted-gap-reason", "waived-by", "command"]), "workflow evidence");
181
+ if (!flagString(parsed.flags, "expectation"))
182
+ throw new Error("workflow evidence requires --expectation <gate-expectation-id>");
183
+ if (!flagString(parsed.flags, "status"))
184
+ throw new Error("workflow evidence requires --status <pass|fail|not_verified>");
185
+ if (!flagString(parsed.flags, "summary"))
186
+ throw new Error("workflow evidence requires --summary <text>");
187
+ const forwarded = stripPublicFlags(argv, new Set(["artifact-root", "session-dir", "json"]));
188
+ const { slug, projectRoot } = readBoundSession(sessionDir);
189
+ const commands = flagList(parsed.flags, "command");
190
+ if (Object.hasOwn(parsed.flags, "command") && commands.length === 0) {
191
+ throw new Error("workflow evidence --command requires a shell command value");
192
+ }
193
+ const requiresTestEvidence = flagString(parsed.flags, "expectation") === "tests-evidence" && flagString(parsed.flags, "status") === "pass";
194
+ // Argument and command-shape rejection must be read-only. Recovery below may
195
+ // repair stale projections, so it runs only after every command is accepted.
196
+ assertRunnableEvidenceCommands(commands, projectRoot, requiresTestEvidence);
197
+ const report = await withSubjectLock(path.dirname(sessionDir), slug, async () => {
198
+ // Validate the owner after the lock is held, then keep the lock through command
199
+ // execution, evidence recording, and postcondition capture so assignment and
200
+ // session state cannot change mid-invocation.
201
+ const caller = assertMatchingAssignmentActor(sessionDir, slug);
202
+ const repaired = await recoverBuilderFlowSession({ sessionDir });
203
+ const beforeEvidence = manifestEvidenceDigests(repaired.run.manifest);
204
+ await mainFromPublicWorkflow([
205
+ "record-gate-claim",
206
+ sessionDir,
207
+ ...forwarded,
208
+ "--actor",
209
+ caller.actorKey,
210
+ ]);
211
+ await syncBuilderFlowSession({ sessionDir });
212
+ const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
213
+ const run = await loadBuilderFlowRun({ cwd: repaired.projectRoot, runId: slug });
214
+ const afterEvidence = manifestEvidenceDigests(run.manifest);
215
+ const newEvidence = afterEvidence.filter((value) => !beforeEvidence.includes(value));
216
+ if (newEvidence.length !== 1 || newEvidence[0] !== digest) {
217
+ throw new Error("workflow evidence did not attach exactly this invocation's resulting trust.bundle digest");
218
+ }
219
+ const updatedSidecar = readBoundSession(sessionDir).sidecar;
220
+ return immutableReport({
221
+ run_id: run.runId,
222
+ status: run.state.status,
223
+ current_step: run.state.current_step,
224
+ attached: true,
225
+ next_action: updatedSidecar.next_action ?? null,
226
+ });
227
+ });
228
+ if (json)
229
+ console.log(JSON.stringify(report));
230
+ else
231
+ console.log(`Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`);
232
+ return 0;
233
+ }
234
+ async function critique(sessionDir, argv, json) {
235
+ const parsed = parseArgs(argv);
236
+ assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "id", "verdict", "summary", "artifact-ref", "finding-json", "lane-json", "timestamp"]), "workflow critique");
237
+ if (!flagString(parsed.flags, "summary"))
238
+ throw new Error("workflow critique requires --summary <text>");
239
+ if (Object.hasOwn(parsed.flags, "reviewer"))
240
+ throw new Error("workflow critique derives reviewer identity from the authenticated assignment actor; --reviewer is not accepted");
241
+ const { slug, projectRoot } = readBoundSession(sessionDir);
242
+ const forwarded = stripPublicFlags(argv, new Set(["artifact-root", "session-dir", "json"]));
243
+ const report = await withSubjectLock(path.dirname(sessionDir), slug, async () => {
244
+ const caller = assertDistinctReviewActor(sessionDir, slug);
245
+ const current = await loadBuilderFlowRun({ cwd: projectRoot, runId: slug });
246
+ if (current.definitionId !== "builder.build" || current.state.current_step !== "verify")
247
+ throw new Error("workflow critique is allowed only for the canonical builder.build verify step");
248
+ const beforeManifest = JSON.parse(JSON.stringify(current.manifest));
249
+ const beforeTrustBundle = optionalFileDigest(path.join(sessionDir, "trust.bundle"));
250
+ const legacySidecars = ["critique.json", "evidence.json"].map((name) => ({ name, digest: optionalFileDigest(path.join(sessionDir, name)) }));
251
+ await mainFromPublicWorkflow(["record-critique", sessionDir, ...forwarded, "--reviewer", caller.actorKey]);
252
+ const result = await recoverBuilderFlowSession({ sessionDir });
253
+ const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
254
+ if (!isDeepStrictEqual(result.run.manifest, beforeManifest)) {
255
+ throw new Error("workflow critique must not attach or otherwise mutate the Flow manifest");
256
+ }
257
+ if (beforeTrustBundle === digest) {
258
+ throw new Error("workflow critique did not change trust.bundle");
259
+ }
260
+ if (legacySidecars.some(({ name, digest: legacyDigest }) => optionalFileDigest(path.join(sessionDir, name)) !== legacyDigest)) {
261
+ throw new Error("workflow critique must persist only through trust.bundle");
262
+ }
263
+ return immutableReport({ run_id: slug, recorded: true });
264
+ });
265
+ if (json)
266
+ console.log(JSON.stringify(report));
267
+ else
268
+ console.log("Recorded critique in the trust bundle.");
269
+ return 0;
270
+ }
271
+ function assertRunnableEvidenceCommands(commands, projectRoot, requiresTestEvidence) {
272
+ const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../scripts/hooks/lib/runnable-command.js");
273
+ const { isRunnableCommandText } = REQUIRE(helperPath);
274
+ if (commands.length > 1 && new Set(commands).size !== commands.length) {
275
+ throw new Error("workflow evidence --command values must be unique because observations are matched by exact command text");
276
+ }
277
+ for (const command of commands) {
278
+ if (!isRunnableCommandText(command)) {
279
+ throw new Error(`workflow evidence --command ${JSON.stringify(command)} is not a runnable shell command — prose belongs in --summary, which is never executed.`);
280
+ }
281
+ if (requiresTestEvidence && !isMeaningfulTestCommand(command, projectRoot)) {
282
+ throw new Error("workflow evidence tests-evidence command must resolve through a non-vacuous package script or a known test/check/verify/eval runner or project-local test path; shell wrappers, no-ops, version/help commands, and arbitrary node -e commands are not evidence");
283
+ }
284
+ }
285
+ }
286
+ function resolveSessionDir(flags) {
287
+ const explicit = flagString(flags, "session-dir");
288
+ if (explicit)
289
+ return validateCanonicalSessionDir(path.resolve(explicit));
290
+ const artifactRoot = path.resolve(flagString(flags, "artifact-root", defaultArtifactRootForRead()));
291
+ const candidate = currentWorkflowSessionDir(artifactRoot);
292
+ if (!candidate || !isWithin(candidate, artifactRoot) || !fs.existsSync(path.join(candidate, "state.json"))) {
293
+ throw new Error("current workflow pointer does not resolve to a valid session; pass --session-dir explicitly");
294
+ }
295
+ return validateCanonicalSessionDir(candidate);
296
+ }
297
+ function doctor(argv) {
298
+ const parsed = parseArgs(argv);
299
+ const projectRoot = path.resolve(flagString(parsed.flags, "project-root", process.cwd()));
300
+ const artifactRoot = path.resolve(flagString(parsed.flags, "artifact-root", defaultArtifactRootForRead(projectRoot)));
301
+ const packageRoot = PACKAGE_ROOT;
302
+ const packageJson = PACKAGE_METADATA;
303
+ const cliVersion = CLI_VERSION;
304
+ const installFile = path.join(projectRoot, ".flow-agents", "install.json");
305
+ const install = readOptionalJson(installFile);
306
+ const state = readCurrentState(artifactRoot);
307
+ const packageKit = readOptionalJson(path.join(packageRoot, "kits", "builder", "kit.json"));
308
+ const installedKitFile = path.join(projectRoot, "kits", "builder", "kit.json");
309
+ const installedKit = readOptionalJson(installedKitFile);
310
+ const definitions = [
311
+ { id: "builder.build", file: "build.flow.json" },
312
+ { id: "builder.shape", file: "shape.flow.json" },
313
+ ].map(({ id, file }) => {
314
+ const packageFile = path.join(packageRoot, "kits", "builder", "flows", file);
315
+ const installedFile = path.join(projectRoot, "kits", "builder", "flows", file);
316
+ return { id, packageFile, installedFile, packageDefinition: readOptionalJson(packageFile), installedDefinition: readOptionalJson(installedFile) };
317
+ });
318
+ const resolvedFlowPackage = readOptionalJson(resolveDependencyPackageJson("@kontourai/flow"));
319
+ const installedVersion = typeof install?.version === "string" ? install.version : null;
320
+ const staleInstall = installedVersion !== null && installedVersion !== cliVersion;
321
+ const activeKitIds = Array.isArray(install?.active_kit_ids) ? install.active_kit_ids.map(String) : (installedKit ? ["builder"] : []);
322
+ const runtime = typeof install?.runtime === "string" ? install.runtime : "base";
323
+ const remediation = pinnedFlowAgentsCommand(cliVersion, ["init", "--runtime", runtime, "--dest", projectRoot, ...activeKitIds.flatMap((id) => ["--activate-kit", id]), "--yes"]);
324
+ const localDependencyFile = path.join(projectRoot, "node_modules", "@kontourai", "flow-agents", "package.json");
325
+ const localDependency = readOptionalJson(localDependencyFile);
326
+ const installIntegrity = verifyInstalledAssets(projectRoot, packageRoot, runtime);
327
+ const warnings = [];
328
+ if (!install)
329
+ warnings.push(`No installed hook/bundle version found at ${installFile}. Run: ${remediation}`);
330
+ else if (staleInstall)
331
+ warnings.push(`Installed hook/writer version ${installedVersion} is incompatible with CLI ${cliVersion}. Run: ${remediation}`);
332
+ if (!installIntegrity.ok)
333
+ warnings.push(`Installed hook/writer assets failed integrity verification: ${installIntegrity.problems.join("; ")}. Run: ${remediation}`);
334
+ if (localDependency?.version && localDependency.version !== cliVersion)
335
+ warnings.push(`Repository-local Flow Agents ${String(localDependency.version)} differs from executing CLI ${cliVersion}; keep automation explicitly versioned.`);
336
+ if (activeKitIds.includes("builder") && !installedKit)
337
+ warnings.push(`Activated Builder Kit is missing at ${installedKitFile}. Run: ${remediation}`);
338
+ for (const definition of definitions) {
339
+ if (activeKitIds.includes("builder") && !definition.installedDefinition) {
340
+ warnings.push(`Activated ${definition.id} definition is missing at ${definition.installedFile}. Run: ${remediation}`);
341
+ continue;
342
+ }
343
+ if (!definition.installedDefinition)
344
+ continue;
345
+ const validationError = validateFlowDefinition(definition.id, definition.installedDefinition);
346
+ if (validationError)
347
+ warnings.push(`Installed ${definition.id} definition is invalid: ${validationError}. Run: ${remediation}`);
348
+ if (definition.installedDefinition.id !== definition.packageDefinition?.id) {
349
+ warnings.push(`Installed ${definition.id} definition id ${String(definition.installedDefinition.id)} differs from CLI definition ${String(definition.packageDefinition?.id)}. Run: ${remediation}`);
350
+ }
351
+ if (definition.installedDefinition.version !== definition.packageDefinition?.version) {
352
+ warnings.push(`Installed ${definition.id} version ${String(definition.installedDefinition.version)} differs from CLI version ${String(definition.packageDefinition?.version)}. Run: ${remediation}`);
353
+ }
354
+ else if (fileSha256(definition.installedFile) !== fileSha256(definition.packageFile)) {
355
+ warnings.push(`Installed ${definition.id} content differs from Flow Agents ${cliVersion}. Run: ${remediation}`);
356
+ }
357
+ }
358
+ if (installedKit && installedKit.schema_version !== packageKit?.schema_version) {
359
+ warnings.push(`Installed Builder Kit schema ${String(installedKit.schema_version)} differs from CLI schema ${String(packageKit?.schema_version)}. Run: ${remediation}`);
360
+ }
361
+ if (installedKit && fileSha256(installedKitFile) !== fileSha256(path.join(packageRoot, "kits", "builder", "kit.json"))) {
362
+ warnings.push(`Installed Builder Kit content differs from Flow Agents ${cliVersion}. Run: ${remediation}`);
363
+ }
364
+ const currentRun = state?.flow_run && typeof state.flow_run === "object" ? state.flow_run : null;
365
+ const currentDefinition = definitions.find((definition) => definition.id === currentRun?.definition_id);
366
+ if (currentRun?.definition_id && !currentDefinition) {
367
+ warnings.push(`Current run uses unsupported Flow definition ${String(currentRun.definition_id)}; recover or migrate before mutation.`);
368
+ }
369
+ else if (currentRun && currentDefinition && currentRun.definition_version !== currentDefinition.packageDefinition?.version) {
370
+ warnings.push(`Current run uses ${currentDefinition.id}@${String(currentRun.definition_version)} while CLI resolves ${currentDefinition.id}@${String(currentDefinition.packageDefinition?.version)}; recover or migrate before mutation.`);
371
+ }
372
+ if (state && state.schema_version !== "1.0")
373
+ warnings.push(`Artifact schema ${String(state.schema_version)} is unsupported; recreate or migrate the session with CLI ${cliVersion}.`);
374
+ const trustBundleSchema = readCurrentTrustBundleSchema(artifactRoot);
375
+ if (trustBundleSchema !== null && trustBundleSchema !== "1.0")
376
+ warnings.push(`Trust bundle schema ${String(trustBundleSchema)} is unsupported; recreate or migrate the session with CLI ${cliVersion}.`);
377
+ const resolvedFlowVersion = typeof resolvedFlowPackage?.version === "string" ? resolvedFlowPackage.version : null;
378
+ const expectedFlowRange = packageJson.dependencies && typeof packageJson.dependencies === "object" ? String(packageJson.dependencies["@kontourai/flow"] ?? "") : "";
379
+ if (!resolvedFlowVersion || !flowVersionCompatible(resolvedFlowVersion, expectedFlowRange)) {
380
+ warnings.push(`Resolved Flow runtime ${resolvedFlowVersion ?? "missing"} does not satisfy ${expectedFlowRange || "the package contract"}. Reinstall Flow Agents ${cliVersion}.`);
381
+ }
382
+ const report = {
383
+ ok: warnings.length === 0,
384
+ project_root: projectRoot,
385
+ cli: { version: cliVersion, workflow_contract_version: WORKFLOW_CONTRACT_VERSION, package_root: packageRoot },
386
+ writer: { contract_version: WORKFLOW_WRITER_CONTRACT_VERSION, package_version: cliVersion },
387
+ hook: { contract_version: install && !staleInstall && installIntegrity.ok ? WORKFLOW_CONTRACT_VERSION : null, install_version: installedVersion, path: installIntegrity.hook_config, integrity: installIntegrity },
388
+ local_dependency: {
389
+ version: localDependency?.version ?? null,
390
+ path: localDependency ? path.dirname(localDependencyFile) : null,
391
+ selected: localDependency ? fs.realpathSync(path.dirname(localDependencyFile)) === fs.realpathSync(packageRoot) : false,
392
+ },
393
+ installed: {
394
+ hooks: { version: installedVersion, source: installIntegrity.hook_config, compatible: Boolean(install && !staleInstall && installIntegrity.ok) },
395
+ writer: { version: cliVersion, source: packageRoot, compatible: true },
396
+ runtime: install?.runtime ?? null,
397
+ active_kit_ids: activeKitIds,
398
+ },
399
+ kit: {
400
+ id: packageKit?.id ?? null,
401
+ resolved_schema_version: packageKit?.schema_version ?? null,
402
+ installed_schema_version: installedKit?.schema_version ?? null,
403
+ resolved_content_sha256: fileSha256(path.join(packageRoot, "kits", "builder", "kit.json")),
404
+ installed_content_sha256: installedKit ? fileSha256(installedKitFile) : null,
405
+ source: installedKit ? installedKitFile : path.join(packageRoot, "kits", "builder", "kit.json"),
406
+ },
407
+ flow_runtime: { package_version: resolvedFlowVersion, expected_range: expectedFlowRange, compatible: resolvedFlowVersion ? flowVersionCompatible(resolvedFlowVersion, expectedFlowRange) : false },
408
+ definition: { id: currentRun?.definition_id ?? definitions[0].packageDefinition?.id ?? null, version: currentRun?.definition_version ?? definitions[0].packageDefinition?.version ?? null },
409
+ definitions: definitions.map((definition) => ({
410
+ id: definition.id,
411
+ resolved_version: definition.packageDefinition?.version ?? null,
412
+ installed_version: definition.installedDefinition?.version ?? null,
413
+ installed_valid: definition.installedDefinition ? validateFlowDefinition(definition.id, definition.installedDefinition) === null : false,
414
+ })),
415
+ artifact: {
416
+ state_schema_version: state?.schema_version ?? null,
417
+ trust_bundle_schema_version: trustBundleSchema,
418
+ session: state ? resolveStateSession(artifactRoot) : null,
419
+ },
420
+ warnings,
421
+ remediation: warnings.length ? remediation : null,
422
+ };
423
+ if (flagBool(parsed.flags, "json"))
424
+ console.log(JSON.stringify(report));
425
+ else {
426
+ console.log(`Flow Agents CLI: ${cliVersion}`);
427
+ console.log(`Installed hooks/writer: ${installedVersion ?? "missing"}`);
428
+ console.log(`Builder Kit schema: installed=${String(installedKit?.schema_version ?? "missing")} resolved=${String(packageKit?.schema_version ?? "missing")}`);
429
+ console.log(`Flow: ${String(report.definition.id ?? "none")}@${String(report.definition.version ?? "unknown")}`);
430
+ console.log(`Artifact schema: state=${String(report.artifact.state_schema_version ?? "none")} trust=${String(report.artifact.trust_bundle_schema_version ?? "none")}`);
431
+ for (const warning of warnings)
432
+ console.log(`WARNING: ${warning}`);
433
+ }
434
+ return report.ok ? 0 : 2;
435
+ }
436
+ function readCurrentState(artifactRoot) {
437
+ try {
438
+ const session = resolveSessionDir({ "artifact-root": artifactRoot });
439
+ return readJsonFile(path.join(session, "state.json"), "workflow state");
440
+ }
441
+ catch {
442
+ return null;
443
+ }
444
+ }
445
+ function resolveStateSession(artifactRoot) {
446
+ try {
447
+ return resolveSessionDir({ "artifact-root": artifactRoot });
448
+ }
449
+ catch {
450
+ return null;
451
+ }
452
+ }
453
+ function readJsonFile(file, label) {
454
+ const descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
455
+ try {
456
+ const stat = fs.fstatSync(descriptor);
457
+ if (!stat.isFile() || stat.size > 1024 * 1024)
458
+ throw new Error(`${label} must be a regular file no larger than 1 MiB`);
459
+ const value = JSON.parse(fs.readFileSync(descriptor, "utf8"));
460
+ if (!value || typeof value !== "object" || Array.isArray(value))
461
+ throw new Error(`${label} must be a JSON object`);
462
+ return value;
463
+ }
464
+ finally {
465
+ fs.closeSync(descriptor);
466
+ }
467
+ }
468
+ function readOptionalJson(file) {
469
+ try {
470
+ return readJsonFile(file, file);
471
+ }
472
+ catch (error) {
473
+ if (error.code === "ENOENT")
474
+ return null;
475
+ throw error;
476
+ }
477
+ }
478
+ function validateFlowDefinition(expectedId, definition) {
479
+ if (definition.id !== expectedId)
480
+ return `expected id ${expectedId}, received ${String(definition.id)}`;
481
+ try {
482
+ validateDefinition(definition);
483
+ return null;
484
+ }
485
+ catch (error) {
486
+ return error instanceof Error ? error.message : String(error);
487
+ }
488
+ }
489
+ function isSafeSlug(value) {
490
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
491
+ }
492
+ function readBoundSession(sessionDir) {
493
+ const slug = path.basename(sessionDir);
494
+ if (!isSafeSlug(slug))
495
+ throw new Error("workflow session basename must be a safe task slug");
496
+ const sidecar = readJsonFile(path.join(sessionDir, "state.json"), "workflow state");
497
+ if (sidecar.task_slug !== slug)
498
+ throw new Error("workflow state task_slug must exactly match the safe session basename");
499
+ return { sidecar, slug, projectRoot: path.dirname(path.dirname(path.dirname(sessionDir))) };
500
+ }
501
+ function readAssignment(sessionDir, slug) {
502
+ const artifactRoot = path.dirname(sessionDir);
503
+ const assignmentRoot = path.join(artifactRoot, "assignment");
504
+ const stat = fs.lstatSync(assignmentRoot);
505
+ if (stat.isSymbolicLink() || !stat.isDirectory())
506
+ throw new Error("workflow assignment directory must be a non-symlink directory");
507
+ const file = path.join(assignmentRoot, `${slug}.json`);
508
+ const fileStat = fs.lstatSync(file);
509
+ if (fileStat.isSymbolicLink() || !fileStat.isFile())
510
+ throw new Error("workflow assignment must be a non-symlink regular file");
511
+ return readJsonFile(file, "workflow assignment");
512
+ }
513
+ function assertMatchingAssignmentActor(sessionDir, slug) {
514
+ const assignment = readActiveAssignment(sessionDir, slug);
515
+ const caller = resolveCurrentAssignmentActor();
516
+ const normalizeActor = (value) => value && typeof value === "object" ? { ...value, human: value.human ?? null } : null;
517
+ if (assignment.actor_key !== caller.actorKey || !isDeepStrictEqual(normalizeActor(assignment.actor), normalizeActor(caller.actor))) {
518
+ throw new Error("workflow mutation requires the session's active, matching assignment actor");
519
+ }
520
+ return caller;
521
+ }
522
+ function readActiveAssignment(sessionDir, slug) {
523
+ const assignment = readAssignment(sessionDir, slug);
524
+ 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)) {
525
+ throw new Error("workflow mutation requires the session's active implementation assignment");
526
+ }
527
+ return assignment;
528
+ }
529
+ function assertDistinctReviewActor(sessionDir, slug) {
530
+ const assignment = readActiveAssignment(sessionDir, slug);
531
+ const caller = resolveCurrentAssignmentActor();
532
+ if (assignment.actor_key === caller.actorKey) {
533
+ throw new Error("workflow critique requires a reviewer identity distinct from the active implementation assignment actor");
534
+ }
535
+ return caller;
536
+ }
537
+ function immutableReport(value) {
538
+ if (!value || typeof value !== "object")
539
+ return value;
540
+ for (const nested of Object.values(value))
541
+ immutableReport(nested);
542
+ return Object.freeze(value);
543
+ }
544
+ function manifestEvidenceDigests(manifest) {
545
+ const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
546
+ return evidence
547
+ .map((entry) => entry && typeof entry === "object" ? entry.sha256 : null)
548
+ .filter((digest) => typeof digest === "string");
549
+ }
550
+ function optionalFileDigest(file) {
551
+ try {
552
+ return fileSha256(file);
553
+ }
554
+ catch (error) {
555
+ if (error.code === "ENOENT")
556
+ return null;
557
+ throw error;
558
+ }
559
+ }
560
+ function stripPublicFlags(argv, removed) {
561
+ const result = [];
562
+ for (let index = 0; index < argv.length; index += 1) {
563
+ const token = argv[index];
564
+ if (!token.startsWith("--")) {
565
+ result.push(token);
566
+ continue;
567
+ }
568
+ const equals = token.indexOf("=");
569
+ const key = token.slice(2, equals === -1 ? undefined : equals);
570
+ if (!removed.has(key)) {
571
+ result.push(token);
572
+ continue;
573
+ }
574
+ if (equals === -1 && argv[index + 1] !== undefined && !argv[index + 1].startsWith("--"))
575
+ index += 1;
576
+ }
577
+ return result;
578
+ }
579
+ function keepFlags(argv, kept) {
580
+ const result = [];
581
+ for (let index = 0; index < argv.length; index += 1) {
582
+ const token = argv[index];
583
+ if (!token.startsWith("--"))
584
+ continue;
585
+ const equals = token.indexOf("=");
586
+ const key = token.slice(2, equals === -1 ? undefined : equals);
587
+ const hasValue = equals === -1 && argv[index + 1] !== undefined && !argv[index + 1].startsWith("--");
588
+ if (kept.has(key)) {
589
+ result.push(token);
590
+ if (hasValue)
591
+ result.push(argv[index + 1]);
592
+ }
593
+ if (hasValue)
594
+ index += 1;
595
+ }
596
+ return result;
597
+ }
598
+ function assertOnlyFlags(flags, allowed, command) {
599
+ const unsupported = Object.keys(flags).find((key) => !allowed.has(key));
600
+ if (unsupported)
601
+ throw new Error(`${command} does not support --${unsupported}`);
602
+ }
603
+ function isWithin(candidate, root) {
604
+ const relative = path.relative(root, candidate);
605
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
606
+ }
607
+ function fileSha256(file) {
608
+ return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
609
+ }
610
+ function readCurrentTrustBundleSchema(artifactRoot) {
611
+ const session = resolveStateSession(artifactRoot);
612
+ if (!session)
613
+ return null;
614
+ return readOptionalJson(path.join(session, "trust.bundle"))?.schema_version ?? null;
615
+ }
616
+ function validateCanonicalSessionDir(candidate) {
617
+ const sessionDir = path.resolve(candidate);
618
+ const artifactRoot = path.dirname(sessionDir);
619
+ const kontouraiRoot = path.dirname(artifactRoot);
620
+ const projectRoot = path.dirname(kontouraiRoot);
621
+ if (path.basename(artifactRoot) !== "flow-agents" || path.basename(kontouraiRoot) !== ".kontourai" || path.dirname(sessionDir) !== artifactRoot) {
622
+ throw new Error("workflow session must be .kontourai/flow-agents/<slug>");
623
+ }
624
+ for (const [label, entry, kind] of [
625
+ ["project root", projectRoot, "directory"],
626
+ [".kontourai root", kontouraiRoot, "directory"],
627
+ ["artifact root", artifactRoot, "directory"],
628
+ ["session directory", sessionDir, "directory"],
629
+ ["workflow state", path.join(sessionDir, "state.json"), "file"],
630
+ ]) {
631
+ const stat = fs.lstatSync(entry);
632
+ if (stat.isSymbolicLink() || (kind === "directory" ? !stat.isDirectory() : !stat.isFile()))
633
+ throw new Error(`${label} must be a non-symlink ${kind}`);
634
+ }
635
+ const bundle = path.join(sessionDir, "trust.bundle");
636
+ if (fs.existsSync(bundle)) {
637
+ const stat = fs.lstatSync(bundle);
638
+ if (stat.isSymbolicLink() || !stat.isFile())
639
+ throw new Error("workflow trust bundle must be a non-symlink file");
640
+ }
641
+ return sessionDir;
642
+ }
643
+ function verifyInstalledAssets(projectRoot, packageRoot, runtime) {
644
+ const problems = [];
645
+ const bundleRoot = path.join(packageRoot, "dist", runtime);
646
+ for (const relativeRoot of ["build/src", "scripts/hooks"]) {
647
+ const expectedRoot = path.join(bundleRoot, relativeRoot);
648
+ if (!fs.existsSync(expectedRoot)) {
649
+ problems.push(`package bundle is missing: ${relativeRoot}`);
650
+ continue;
651
+ }
652
+ const pending = [expectedRoot];
653
+ while (pending.length > 0) {
654
+ const current = pending.pop();
655
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
656
+ const expected = path.join(current, entry.name);
657
+ if (entry.isDirectory()) {
658
+ pending.push(expected);
659
+ continue;
660
+ }
661
+ if (!entry.isFile())
662
+ continue;
663
+ const relative = path.relative(bundleRoot, expected);
664
+ const installed = path.join(projectRoot, relative);
665
+ try {
666
+ const stat = fs.lstatSync(installed);
667
+ const matches = runtime === "kiro"
668
+ ? fs.readFileSync(installed, "utf8") === fs.readFileSync(expected, "utf8").replaceAll("__KIRO_PACKAGE_ROOT__", projectRoot)
669
+ : fileSha256(installed) === fileSha256(expected);
670
+ if (stat.isSymbolicLink() || !stat.isFile() || !matches)
671
+ problems.push(`asset mismatch: ${relative}`);
672
+ }
673
+ catch {
674
+ problems.push(`asset missing: ${relative}`);
675
+ }
676
+ }
677
+ }
678
+ }
679
+ const verifyExactRuntimeFile = (relative, expectedContent) => {
680
+ const expected = path.join(bundleRoot, relative);
681
+ const installed = path.join(projectRoot, relative);
682
+ try {
683
+ const stat = fs.lstatSync(installed);
684
+ const matches = expectedContent === undefined ? fileSha256(installed) === fileSha256(expected) : fs.readFileSync(installed, "utf8") === expectedContent;
685
+ if (stat.isSymbolicLink() || !stat.isFile() || !matches)
686
+ problems.push(`runtime wiring mismatch: ${relative}`);
687
+ }
688
+ catch {
689
+ problems.push(`runtime wiring missing: ${relative}`);
690
+ }
691
+ };
692
+ const verifyManagedHooks = (relative) => {
693
+ const installedFile = path.join(projectRoot, relative);
694
+ try {
695
+ const stat = fs.lstatSync(installedFile);
696
+ const installed = readJsonFile(installedFile, "installed runtime hook configuration");
697
+ const expected = readJsonFile(path.join(bundleRoot, relative), "packaged runtime hook configuration");
698
+ const installedHooks = installed.hooks;
699
+ const expectedHooks = expected.hooks;
700
+ const complete = !stat.isSymbolicLink() && stat.isFile() && installedHooks && expectedHooks && Object.entries(expectedHooks).every(([event, groups]) => {
701
+ const actualGroups = installedHooks[event];
702
+ return Array.isArray(groups) && Array.isArray(actualGroups) && groups.every((group) => actualGroups.some((actual) => isDeepStrictEqual(actual, group)));
703
+ });
704
+ if (!complete)
705
+ problems.push("runtime hook configuration does not contain the packaged managed hooks");
706
+ }
707
+ catch {
708
+ problems.push("runtime hook configuration is missing");
709
+ }
710
+ };
711
+ let hookConfig = null;
712
+ if (runtime === "codex") {
713
+ hookConfig = path.join(projectRoot, ".codex", "hooks.json");
714
+ verifyManagedHooks(".codex/hooks.json");
715
+ }
716
+ else if (runtime === "claude-code") {
717
+ hookConfig = path.join(projectRoot, ".claude", "settings.json");
718
+ verifyManagedHooks(".claude/settings.json");
719
+ }
720
+ else if (runtime === "opencode") {
721
+ hookConfig = path.join(projectRoot, ".opencode", "plugins", "flow-agents.js");
722
+ verifyExactRuntimeFile(".opencode/plugins/flow-agents.js");
723
+ }
724
+ else if (runtime === "pi") {
725
+ hookConfig = path.join(projectRoot, ".pi", "extensions", "flow-agents.ts");
726
+ verifyExactRuntimeFile(".pi/extensions/flow-agents.ts");
727
+ }
728
+ else if (runtime === "kiro") {
729
+ hookConfig = path.join(projectRoot, "agents", "dev.json");
730
+ const expectedAgentsRoot = path.join(bundleRoot, "agents");
731
+ try {
732
+ for (const entry of fs.readdirSync(expectedAgentsRoot, { withFileTypes: true })) {
733
+ if (!entry.isFile() || path.extname(entry.name) !== ".json")
734
+ continue;
735
+ const relative = path.join("agents", entry.name);
736
+ const rendered = fs.readFileSync(path.join(expectedAgentsRoot, entry.name), "utf8").replaceAll("__KIRO_PACKAGE_ROOT__", projectRoot);
737
+ verifyExactRuntimeFile(relative, rendered);
738
+ }
739
+ }
740
+ catch {
741
+ problems.push("runtime wiring missing: agents");
742
+ }
743
+ }
744
+ return { ok: problems.length === 0, problems, hook_config: hookConfig };
745
+ }
746
+ function flowVersionCompatible(version, range) {
747
+ const actual = version.match(/^(\d+)\.(\d+)\.(\d+)/);
748
+ const minimum = range.match(/(\d+)\.(\d+)\.(\d+)/);
749
+ if (!actual || !minimum)
750
+ return false;
751
+ const a = actual.slice(1).map(Number);
752
+ const m = minimum.slice(1).map(Number);
753
+ if (a[0] !== m[0])
754
+ return false;
755
+ return a[1] > m[1] || (a[1] === m[1] && a[2] >= m[2]);
756
+ }
757
+ function resolveDependencyPackageJson(packageName) {
758
+ let candidate = path.dirname(REQUIRE.resolve(packageName));
759
+ for (let depth = 0; depth < 8; depth += 1) {
760
+ const metadata = path.join(candidate, "package.json");
761
+ if (fs.existsSync(metadata))
762
+ return metadata;
763
+ const parent = path.dirname(candidate);
764
+ if (parent === candidate)
765
+ break;
766
+ candidate = parent;
767
+ }
768
+ throw new Error(`could not resolve ${packageName} package metadata`);
769
+ }