@kontourai/flow-agents 3.6.0 → 3.8.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 (96) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/src/builder-flow-run-adapter.d.ts +22 -2
  3. package/build/src/builder-flow-run-adapter.js +93 -28
  4. package/build/src/builder-flow-runtime.d.ts +8 -3
  5. package/build/src/builder-flow-runtime.js +407 -51
  6. package/build/src/cli/assignment-provider.d.ts +4 -7
  7. package/build/src/cli/assignment-provider.js +80 -14
  8. package/build/src/cli/builder-run.js +14 -18
  9. package/build/src/cli/workflow-sidecar.d.ts +28 -4
  10. package/build/src/cli/workflow-sidecar.js +825 -103
  11. package/build/src/cli/workflow.js +301 -43
  12. package/build/src/flow-kit/validate.d.ts +17 -0
  13. package/build/src/flow-kit/validate.js +340 -2
  14. package/build/src/index.js +5 -5
  15. package/build/src/lib/observed-command.d.ts +7 -0
  16. package/build/src/lib/observed-command.js +100 -0
  17. package/build/src/tools/build-universal-bundles.js +53 -3
  18. package/build/src/tools/generate-context-map.js +5 -3
  19. package/context/scripts/hooks/lib/kit-catalog.js +1 -1
  20. package/context/scripts/hooks/stop-goal-fit.js +78 -9
  21. package/docs/context-map.md +22 -20
  22. package/docs/developer-architecture.md +1 -1
  23. package/docs/getting-started.md +9 -1
  24. package/docs/public-workflow-cli.md +76 -7
  25. package/docs/skills-map.md +51 -27
  26. package/docs/spec/builder-flow-runtime.md +75 -40
  27. package/docs/workflow-usage-guide.md +109 -42
  28. package/evals/fixtures/hook-influence/cases.json +2 -2
  29. package/evals/integration/test_builder_entry_enforcement.sh +52 -41
  30. package/evals/integration/test_builder_step_producers.sh +330 -6
  31. package/evals/integration/test_bundle_install.sh +318 -65
  32. package/evals/integration/test_bundle_lifecycle.sh +4 -6
  33. package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
  34. package/evals/integration/test_current_json_per_actor.sh +12 -0
  35. package/evals/integration/test_dual_emit_flow_step.sh +62 -43
  36. package/evals/integration/test_flowdef_session_activation.sh +145 -25
  37. package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
  38. package/evals/integration/test_gate_lockdown.sh +3 -5
  39. package/evals/integration/test_goal_fit_hook.sh +60 -2
  40. package/evals/integration/test_install_merge.sh +18 -8
  41. package/evals/integration/test_liveness_verdict.sh +14 -17
  42. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
  43. package/evals/integration/test_public_workflow_cli.sh +334 -14
  44. package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
  45. package/evals/integration/test_sidecar_field_preservation.sh +36 -11
  46. package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
  47. package/evals/integration/test_workflow_steering_hook.sh +15 -38
  48. package/evals/run.sh +2 -0
  49. package/evals/static/test_builder_skill_coherence.sh +247 -0
  50. package/evals/static/test_library_exports.sh +5 -2
  51. package/evals/static/test_universal_bundles.sh +44 -1
  52. package/evals/static/test_workflow_skills.sh +13 -325
  53. package/kits/builder/flows/build.flow.json +22 -0
  54. package/kits/builder/flows/shape.flow.json +9 -9
  55. package/kits/builder/kit.json +70 -16
  56. package/kits/builder/skills/builder-shape/SKILL.md +75 -75
  57. package/kits/builder/skills/continue-work/SKILL.md +45 -106
  58. package/kits/builder/skills/deliver/SKILL.md +96 -442
  59. package/kits/builder/skills/design-probe/SKILL.md +40 -139
  60. package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
  61. package/kits/builder/skills/execute-plan/SKILL.md +54 -125
  62. package/kits/builder/skills/fix-bug/SKILL.md +42 -132
  63. package/kits/builder/skills/gate-review/SKILL.md +60 -211
  64. package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
  65. package/kits/builder/skills/learning-review/SKILL.md +63 -170
  66. package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
  67. package/kits/builder/skills/plan-work/SKILL.md +51 -185
  68. package/kits/builder/skills/pull-work/SKILL.md +136 -485
  69. package/kits/builder/skills/release-readiness/SKILL.md +66 -107
  70. package/kits/builder/skills/review-work/SKILL.md +89 -176
  71. package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
  72. package/kits/builder/skills/verify-work/SKILL.md +101 -113
  73. package/package.json +2 -2
  74. package/packaging/README.md +1 -1
  75. package/scripts/README.md +1 -1
  76. package/scripts/hooks/lib/kit-catalog.js +1 -1
  77. package/scripts/hooks/stop-goal-fit.js +78 -9
  78. package/scripts/install-codex-home.sh +63 -23
  79. package/scripts/install-owned-files.js +18 -2
  80. package/src/builder-flow-run-adapter.ts +118 -32
  81. package/src/builder-flow-runtime.ts +426 -64
  82. package/src/cli/assignment-provider-lock.test.mjs +83 -0
  83. package/src/cli/assignment-provider.ts +75 -14
  84. package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
  85. package/src/cli/builder-flow-runtime.test.mjs +436 -18
  86. package/src/cli/builder-run.ts +3 -9
  87. package/src/cli/kit-metadata-security.test.mjs +232 -6
  88. package/src/cli/public-api.test.mjs +15 -0
  89. package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
  90. package/src/cli/workflow-sidecar.ts +746 -103
  91. package/src/cli/workflow.ts +288 -41
  92. package/src/flow-kit/validate.ts +320 -2
  93. package/src/index.ts +6 -5
  94. package/src/lib/observed-command.ts +96 -0
  95. package/src/tools/build-universal-bundles.ts +51 -3
  96. package/src/tools/generate-context-map.ts +5 -3
@@ -3,11 +3,11 @@ import assert from "node:assert/strict";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { createRequire } from "node:module";
6
+ import { createRequire, syncBuiltinESMExports } from "node:module";
7
7
 
8
8
  import { readKitInventory } from "../../build/src/runtime-adapters.js";
9
9
  import { main as validateHookInfluence } from "../../build/src/cli/validate-hook-influence.js";
10
- import { parseKitFlowStepActions } from "../../build/src/flow-kit/validate.js";
10
+ import { parseKitFlowStepActions, parseKitSkillRoles, validateKitRepository } from "../../build/src/flow-kit/validate.js";
11
11
 
12
12
  const require = createRequire(import.meta.url);
13
13
  const { workflowTriggersFor } = require("../../scripts/hooks/lib/kit-catalog.js");
@@ -68,13 +68,15 @@ test("Builder flow step actions are structured, complete, and operation-aware",
68
68
  const result = parseKitFlowStepActions(manifest, "kits/builder/kit.json");
69
69
 
70
70
  assert.deepEqual(result.errors, []);
71
- assert.equal(result.entries.length, 10);
71
+ assert.equal(result.entries.length, 14);
72
72
  assert.deepEqual(result.entries.find((entry) => entry.step_id === "plan")?.skills, ["plan-work"]);
73
73
  assert.deepEqual(result.entries.find((entry) => entry.step_id === "pr-open"), {
74
74
  flow_id: "builder.build",
75
75
  step_id: "pr-open",
76
76
  skills: [],
77
77
  operations: ["publish-change"],
78
+ artifacts: ["release.json"],
79
+ expectation_ids: ["pull-request-opened"],
78
80
  });
79
81
  });
80
82
 
@@ -84,11 +86,231 @@ test("flow step action metadata rejects malformed and duplicate entries", () =>
84
86
  { flow_id: "builder.build", step_id: "plan", skills: ["plan-work"] },
85
87
  { flow_id: "builder.build", step_id: "plan", skills: [] },
86
88
  { flow_id: "builder.build", step_id: "verify", skills: "verify-work" },
89
+ { flow_id: "builder.build", step_id: "publish", skills: [], operations: ["publish-change"], expectation_ids: ["pull-request-opened", "pull-request-opened"] },
87
90
  ],
88
91
  }, "fixture/kit.json");
89
92
 
90
93
  assert.match(result.errors.join("\n"), /duplicates 'builder\.build\/plan'/);
91
94
  assert.match(result.errors.join("\n"), /skills must be an identifier list/);
95
+ assert.match(result.errors.join("\n"), /expectation_ids must be a unique identifier list when present/);
96
+ });
97
+
98
+ test("flow step action metadata preserves explicit operation expectation ownership", () => {
99
+ const result = parseKitFlowStepActions({
100
+ flow_step_actions: [
101
+ { flow_id: "builder.build", step_id: "pr-open", skills: [], operations: ["publish-change"], expectation_ids: ["pull-request-opened"] },
102
+ ],
103
+ }, "fixture/kit.json");
104
+
105
+ assert.deepEqual(result.errors, []);
106
+ assert.deepEqual(result.entries[0]?.expectation_ids, ["pull-request-opened"]);
107
+ assert.deepEqual(result.entries[0]?.artifacts, []);
108
+ });
109
+
110
+ test("Builder skill roles form one complete role and producer matrix", async () => {
111
+ const manifest = JSON.parse(fs.readFileSync("kits/builder/kit.json", "utf8"));
112
+ const result = parseKitSkillRoles(manifest, "kits/builder/kit.json");
113
+
114
+ assert.deepEqual(result.errors, []);
115
+ assert.equal(result.entries.length, 17);
116
+ assert.deepEqual(Object.fromEntries([...new Set(result.entries.map((entry) => entry.role))].map((role) => [role, result.entries.filter((entry) => entry.role === role).length])), {
117
+ entrypoint: 2,
118
+ profile: 2,
119
+ step: 10,
120
+ "shared-primitive": 1,
121
+ extension: 2,
122
+ });
123
+ assert.deepEqual(result.entries.find((entry) => entry.skill_id === "builder.review-work")?.expectation_ids, ["clean-critique"]);
124
+ assert.deepEqual(result.entries.find((entry) => entry.skill_id === "builder.verify-work")?.expectation_ids, ["acceptance-criteria", "tests-evidence", "policy-compliance"]);
125
+ assert.deepEqual(await validateKitRepository("kits/builder"), []);
126
+ });
127
+
128
+ test("skill role metadata rejects hidden semantics and role boundary violations", () => {
129
+ const result = parseKitSkillRoles({
130
+ skill_roles: [
131
+ { skill_id: "builder.deliver", role: "entrypoint", flow_id: "builder.build", step_ids: [], artifacts: [], expectation_ids: [], command: "private-bypass" },
132
+ { skill_id: "builder.deliver", role: "step", flow_id: "builder.build", step_ids: ["execute"], artifacts: ["state.json"], expectation_ids: [] },
133
+ { skill_id: "builder.profile", role: "profile", flow_id: "builder.build", step_ids: [], artifacts: [], expectation_ids: ["tests-evidence"] },
134
+ { skill_id: "builder.extension", role: "extension", flow_id: "builder.build", step_ids: [], artifacts: ["report.json"], expectation_ids: [] },
135
+ { skill_id: "builder.empty-step", role: "step", flow_id: "builder.build", step_ids: ["execute"], artifacts: [], expectation_ids: [] },
136
+ { skill_id: "builder.unknown", role: "orchestrator", step_ids: [], artifacts: [], expectation_ids: [] },
137
+ ],
138
+ }, "fixture/kit.json");
139
+
140
+ const errors = result.errors.join("\n");
141
+ assert.match(errors, /unsupported field\(s\): command/);
142
+ assert.match(errors, /duplicates 'builder\.deliver'/);
143
+ assert.match(errors, /profile must select one flow and own no steps, artifacts, or expectations/);
144
+ assert.match(errors, /extension must own no Builder flow, steps, or expectations/);
145
+ assert.match(errors, /step must bind one flow, at least one step, and at least one artifact/);
146
+ assert.match(errors, /role must be entrypoint, profile, step, shared-primitive, or extension/);
147
+ });
148
+
149
+ test("skill role repository validation rejects unknown steps and misplaced expectations", async () => {
150
+ const root = tempRoot("flow-agents-skill-role-cross-reference-");
151
+ const kit = path.join(root, "builder");
152
+ fs.cpSync("kits/builder", kit, { recursive: true });
153
+ const manifestFile = path.join(kit, "kit.json");
154
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
155
+ const execute = manifest.skill_roles.find((entry) => entry.skill_id === "builder.execute-plan");
156
+ execute.step_ids = ["missing-step"];
157
+ execute.expectation_ids = ["tests-evidence"];
158
+ writeJson(manifestFile, manifest);
159
+
160
+ const errors = (await validateKitRepository(kit)).join("\n");
161
+ assert.match(errors, /references unknown step 'builder\.build\/missing-step'/);
162
+ assert.match(errors, /expectation 'tests-evidence' is not owned by its bound step/);
163
+ assert.match(errors, /flow_step_actions 'builder\.build\/execute' skill 'execute-plan' must match one step-role binding/);
164
+ });
165
+
166
+ test("skill role repository validation requires exactly one producer per skill-owned expectation", async () => {
167
+ const root = tempRoot("flow-agents-skill-role-producer-completeness-");
168
+ const kit = path.join(root, "builder");
169
+ fs.cpSync("kits/builder", kit, { recursive: true });
170
+ const manifestFile = path.join(kit, "kit.json");
171
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
172
+ const verify = manifest.skill_roles.find((entry) => entry.skill_id === "builder.verify-work");
173
+ const review = manifest.skill_roles.find((entry) => entry.skill_id === "builder.review-work");
174
+ const shape = manifest.skill_roles.find((entry) => entry.skill_id === "builder.idea-to-backlog");
175
+
176
+ verify.expectation_ids = ["policy-compliance"];
177
+ review.expectation_ids = ["policy-compliance"];
178
+ shape.expectation_ids = shape.expectation_ids.filter((id) => id !== "shaped-problem");
179
+ manifest.flow_step_actions.find((entry) => entry.flow_id === "builder.build" && entry.step_id === "verify").skills = ["verify-work"];
180
+ writeJson(manifestFile, manifest);
181
+
182
+ const errors = (await validateKitRepository(kit)).join("\n");
183
+ assert.match(errors, /flow expectation 'builder\.build\/verify\/tests-evidence' must have exactly one producer owner; found 0/);
184
+ assert.match(errors, /flow expectation 'builder\.build\/verify\/policy-compliance' must have exactly one producer owner; found 2/);
185
+ assert.match(errors, /flow expectation 'builder\.shape\/shape\/shaped-problem' must have exactly one producer owner; found 0/);
186
+ });
187
+
188
+ test("flow expectation ownership cannot be bypassed by reclassifying a producer and emptying its action", async () => {
189
+ const root = tempRoot("flow-agents-reclassified-producer-");
190
+ const kit = path.join(root, "builder");
191
+ fs.cpSync("kits/builder", kit, { recursive: true });
192
+ const manifestFile = path.join(kit, "kit.json");
193
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
194
+ const producer = manifest.skill_roles.find((entry) => entry.skill_id === "builder.evidence-gate");
195
+ producer.role = "extension";
196
+ delete producer.flow_id;
197
+ producer.step_ids = [];
198
+ producer.expectation_ids = [];
199
+ producer.artifacts = [];
200
+ const action = manifest.flow_step_actions.find((entry) => entry.flow_id === "builder.build" && entry.step_id === "merge-ready");
201
+ action.skills = [];
202
+ delete action.operations;
203
+ writeJson(manifestFile, manifest);
204
+
205
+ const errors = (await validateKitRepository(kit)).join("\n");
206
+ assert.doesNotMatch(errors, /extension must own artifacts/);
207
+ assert.match(errors, /flow expectation 'builder\.build\/merge-ready\/merge-readiness' must have exactly one producer owner; found 0/);
208
+ });
209
+
210
+ test("operation-only composed actions must explicitly and exclusively own expectations", async () => {
211
+ async function errorsFor(name, mutate) {
212
+ const root = tempRoot(`flow-agents-operation-owner-${name}-`);
213
+ const kit = path.join(root, "builder");
214
+ fs.cpSync("kits/builder", kit, { recursive: true });
215
+ const manifestFile = path.join(kit, "kit.json");
216
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
217
+ mutate(manifest);
218
+ writeJson(manifestFile, manifest);
219
+ return (await validateKitRepository(kit)).join("\n");
220
+ }
221
+
222
+ const missing = await errorsFor("missing", (manifest) => {
223
+ const action = manifest.flow_step_actions.find((entry) => entry.flow_id === "builder.build" && entry.step_id === "pr-open");
224
+ action.skills = [];
225
+ action.operations = ["publish-change"];
226
+ delete action.expectation_ids;
227
+ });
228
+ assert.match(missing, /operation-only action must explicitly declare expectation_ids/);
229
+ assert.match(missing, /flow expectation 'builder\.publish-learn\/pr-open\/pull-request-opened' must have exactly one producer owner; found 0/);
230
+
231
+ const duplicate = await errorsFor("duplicate", (manifest) => {
232
+ const action = manifest.flow_step_actions.find((entry) => entry.flow_id === "builder.build" && entry.step_id === "pr-open");
233
+ action.skills = ["release-readiness"];
234
+ action.operations = ["publish-change"];
235
+ action.expectation_ids = ["pull-request-opened"];
236
+ const role = manifest.skill_roles.find((entry) => entry.skill_id === "builder.release-readiness");
237
+ role.step_ids = ["pr-open", "merge-ready-ci"];
238
+ role.expectation_ids = ["pull-request-opened", "ci-merge-readiness"];
239
+ });
240
+ assert.match(duplicate, /flow expectation 'builder\.publish-learn\/pr-open\/pull-request-opened' must have exactly one producer owner; found 2/);
241
+ });
242
+
243
+ test("skill role cross-reference never reads Flow definitions through traversal, symlinks, or oversized files", async () => {
244
+ async function errorsFor(name, mutate) {
245
+ const root = tempRoot(`flow-agents-skill-role-safe-flow-${name}-`);
246
+ const kit = path.join(root, "builder");
247
+ fs.cpSync("kits/builder", kit, { recursive: true });
248
+ const manifestFile = path.join(kit, "kit.json");
249
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
250
+ mutate({ root, kit, manifest });
251
+ writeJson(manifestFile, manifest);
252
+ return (await validateKitRepository(kit)).join("\n");
253
+ }
254
+
255
+ const traversal = await errorsFor("traversal", ({ root, manifest }) => {
256
+ writeJson(path.join(root, "outside.flow.json"), { id: "outside", steps: [], gates: {} });
257
+ manifest.flows[0].path = "../outside.flow.json";
258
+ });
259
+ assert.match(traversal, /path must stay inside the kit directory/);
260
+
261
+ const symlink = await errorsFor("symlink", ({ kit, manifest }) => {
262
+ const link = path.join(kit, "flows", "linked.flow.json");
263
+ fs.symlinkSync(path.join(kit, "flows", "build.flow.json"), link);
264
+ manifest.flows[0].path = "flows/linked.flow.json";
265
+ });
266
+ assert.match(symlink, /path must not traverse a symbolic link/);
267
+
268
+ const intermediateSymlink = await errorsFor("intermediate-symlink", ({ kit, manifest }) => {
269
+ const link = path.join(kit, "linked-flows");
270
+ fs.symlinkSync(path.join(kit, "flows"), link);
271
+ manifest.flows[0].path = "linked-flows/build.flow.json";
272
+ });
273
+ assert.match(intermediateSymlink, /path must not traverse a symbolic link/);
274
+
275
+ const oversized = await errorsFor("oversized", ({ kit, manifest }) => {
276
+ fs.writeFileSync(path.join(kit, "flows", "oversized.flow.json"), " ".repeat(1024 * 1024 + 1));
277
+ manifest.flows[0].path = "flows/oversized.flow.json";
278
+ });
279
+ assert.match(oversized, /file exceeds 1048576 bytes/);
280
+ });
281
+
282
+ test("flow definition descriptor identity changes fail closed", async () => {
283
+ const root = tempRoot("flow-agents-flow-identity-race-");
284
+ const kit = path.join(root, "builder");
285
+ fs.cpSync("kits/builder", kit, { recursive: true });
286
+ const manifestFile = path.join(kit, "kit.json");
287
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
288
+ const target = path.join(kit, "flows", "raced.flow.json");
289
+ const replacement = path.join(kit, "flows", "replacement.flow.json");
290
+ fs.copyFileSync(path.join(kit, "flows", "build.flow.json"), target);
291
+ fs.copyFileSync(path.join(kit, "flows", "shape.flow.json"), replacement);
292
+ manifest.flows[0].path = "flows/raced.flow.json";
293
+ writeJson(manifestFile, manifest);
294
+ const realTarget = path.join(fs.realpathSync(path.dirname(target)), path.basename(target));
295
+
296
+ const originalOpen = fs.openSync;
297
+ let swapped = false;
298
+ fs.openSync = (file, flags, mode) => {
299
+ if (!swapped && file === realTarget) {
300
+ fs.renameSync(replacement, target);
301
+ swapped = true;
302
+ }
303
+ return originalOpen(file, flags, mode);
304
+ };
305
+ syncBuiltinESMExports();
306
+ try {
307
+ const errors = (await validateKitRepository(kit)).join("\n");
308
+ assert.equal(swapped, true);
309
+ assert.match(errors, /flow definition identity changed while opening/);
310
+ } finally {
311
+ fs.openSync = originalOpen;
312
+ syncBuiltinESMExports();
313
+ }
92
314
  });
93
315
 
94
316
  test("hook influence: kit expectation cannot override engine-required case id", () => {
@@ -267,7 +489,9 @@ test("workflowTriggersFor uses one structured renderer for any id and provenance
267
489
  assert.match(trigger.steering, /^KIT WORKFLOW ROUTE:/);
268
490
  assert.match(trigger.steering, /use the `hostile-kit` kit's `hostile\.build` workflow/);
269
491
  assert.match(trigger.steering, /If user-requested-tdd, activate `hostile\.tdd`; otherwise activate `hostile\.run`/);
270
- assert.match(trigger.steering, /--flow-id hostile\.build/);
492
+ assert.match(trigger.steering, /Keep the session on `hostile\.build`/);
493
+ assert.match(trigger.steering, /public `flow-agents workflow` interface/);
494
+ assert.doesNotMatch(trigger.steering, /workflow:sidecar|ensure-session/);
271
495
  assert.match(trigger.steering, /hostile\.plan -> hostile\.verify/);
272
496
  assert.match(trigger.steering, /hostile\.release/);
273
497
  });
@@ -299,7 +523,8 @@ test("workflowTriggersFor ignores invalid workflow_triggers and still returns Bu
299
523
  assert.deepEqual(triggers.map((trigger) => trigger.kit_id), ["builder"]);
300
524
  assert.match(triggers[0].steering, /^KIT WORKFLOW ROUTE:/);
301
525
  assert.match(triggers[0].steering, /use the `builder` kit's `builder\.build` workflow/);
302
- assert.match(triggers[0].steering, /--flow-id builder\.build/);
526
+ assert.match(triggers[0].steering, /Keep the session on `builder\.build`/);
527
+ assert.match(triggers[0].steering, /public `flow-agents workflow` interface/);
303
528
  });
304
529
 
305
530
  test("workflowTriggersFor rejects duplicate trigger ids for a kit", () => {
@@ -439,5 +664,6 @@ test("workflowTriggersFor returns Knowledge's structured capture trigger", () =>
439
664
  assert.equal(trigger.kit_id, "knowledge");
440
665
  assert.match(trigger.steering, /use the `knowledge` kit's `knowledge\.ingest` workflow/);
441
666
  assert.match(trigger.steering, /`knowledge\.knowledge-capture`/);
442
- assert.match(trigger.steering, /--flow-id knowledge\.ingest/);
667
+ assert.match(trigger.steering, /Keep the session on `knowledge\.ingest`/);
668
+ assert.match(trigger.steering, /unsupported-runtime blocker/);
443
669
  });
@@ -31,6 +31,21 @@ test("public API exports local artifact root helpers", () => {
31
31
  assert.notEqual(defaultArtifactRootForRead(cwd), durableFlowAgentsRoot(cwd));
32
32
  });
33
33
 
34
+ test("public API retains the documented native-host compatibility surface", async () => {
35
+ const lib = await import("../../build/src/index.js");
36
+ for (const name of [
37
+ "startBuilderBuildRun", "evaluateBuilderBuildRun", "startBuilderFlowSession",
38
+ "pauseBuilderFlowSession", "resumeBuilderFlowSession", "cancelBuilderFlowSession",
39
+ "archiveBuilderFlowSession", "recoverBuilderFlowSession", "releaseBuilderFlowAssignment",
40
+ "writeJson", "appendJsonl", "sidecarBase", "writeState", "writeSidecar",
41
+ ]) {
42
+ assert.equal(typeof lib[name], "function", `${name} must remain package-root exported`);
43
+ }
44
+ assert.equal(typeof lib.loadJson, "function");
45
+ assert.equal(typeof lib.validateTrustBundle, "function");
46
+ assert.equal(typeof lib.builderLifecycleAuthorizationPayload, "function");
47
+ });
48
+
34
49
  test("TS and CJS artifact helpers stay in parity without durable-root fallback", () => {
35
50
  const require = createRequire(import.meta.url);
36
51
  const cjs = require("../../scripts/hooks/lib/local-artifact-paths.js");
@@ -0,0 +1,90 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { inferExecutedTestCount, isMeaningfulTestCommand, testExecutionProof } from "../../build/src/cli/workflow-sidecar.js";
8
+
9
+ function fixture(files) {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-test-proof-"));
11
+ for (const [name, content] of Object.entries(files)) {
12
+ const file = path.join(root, name);
13
+ fs.mkdirSync(path.dirname(file), { recursive: true });
14
+ fs.writeFileSync(file, content);
15
+ }
16
+ return root;
17
+ }
18
+
19
+ test("fake Vitest-looking stdout is not test execution proof", () => {
20
+ const root = fixture({
21
+ "package.json": JSON.stringify({ scripts: { test: "node fake-vitest.mjs" } }),
22
+ "fake-vitest.mjs": 'console.log("Tests 999 passed");\n',
23
+ });
24
+
25
+ assert.equal(isMeaningfulTestCommand("npm test", root), false);
26
+ assert.equal(testExecutionProof("npm test", root), null);
27
+ assert.equal(inferExecutedTestCount("npm test", root, "Tests 999 passed\n"), 0);
28
+ });
29
+
30
+ test("package-script output cannot manufacture a positive test count", () => {
31
+ const root = fixture({
32
+ "package.json": JSON.stringify({ scripts: { test: 'echo "# tests 999"' } }),
33
+ });
34
+
35
+ assert.equal(isMeaningfulTestCommand("npm test", root), false);
36
+ assert.equal(inferExecutedTestCount("npm test", root, "# tests 999\n"), 0);
37
+ });
38
+
39
+ test("supported node test workflows produce source-derived local proof", () => {
40
+ const root = fixture({
41
+ "package.json": JSON.stringify({ scripts: { test: "node --test test/contract.test.mjs" } }),
42
+ "test/contract.test.mjs": 'import test from "node:test";\ntest("contract", () => {});\n',
43
+ });
44
+
45
+ const proof = testExecutionProof("npm test", root);
46
+ assert.deepEqual(proof, { kind: "local-process-exit", runner: "node --test", static_test_units: 1 });
47
+ assert.equal(inferExecutedTestCount("npm test", root, "# tests 0\n"), 0);
48
+ assert.equal(inferExecutedTestCount("npm test", root, "# tests 1\n"), 1);
49
+ assert.equal(inferExecutedTestCount("npm test", root, "ℹ tests 1\n"), 1);
50
+ });
51
+
52
+ test("empty suite declarations are not counted as executed test cases", () => {
53
+ const root = fixture({
54
+ "package.json": JSON.stringify({ scripts: { test: "node --test test/empty.test.mjs" } }),
55
+ "test/empty.test.mjs": 'import { describe } from "node:test";\ndescribe("empty", () => {});\n',
56
+ });
57
+
58
+ assert.equal(testExecutionProof("npm test", root), null);
59
+ assert.equal(inferExecutedTestCount("npm test", root, "# tests 0\n"), 0);
60
+ });
61
+
62
+ test("runner-shaped executable names require explicit files with test cases", () => {
63
+ const root = fixture({
64
+ "pytest": "#!/bin/sh\nexit 0\n",
65
+ "test/contract_test.py": "def test_contract():\n assert True\n",
66
+ });
67
+
68
+ assert.equal(testExecutionProof("./pytest", root), null);
69
+ assert.equal(testExecutionProof("./pytest test/contract_test.py", root), null);
70
+ assert.deepEqual(testExecutionProof("pytest test/contract_test.py", root), {
71
+ kind: "local-process-exit",
72
+ runner: "pytest",
73
+ static_test_units: 1,
74
+ });
75
+ });
76
+
77
+ test("cargo and go require substantive local test sources", () => {
78
+ const empty = fixture({ "Cargo.toml": "[package]\nname='empty'\nversion='0.1.0'\n", "go.mod": "module example.test/empty\n" });
79
+ assert.equal(testExecutionProof("cargo test", empty), null);
80
+ assert.equal(testExecutionProof("go test ./...", empty), null);
81
+
82
+ const covered = fixture({
83
+ "Cargo.toml": "[package]\nname='covered'\nversion='0.1.0'\n",
84
+ "src/lib.rs": "#[cfg(test)]\nmod tests { #[test] fn contract() { assert!(true); } }\n",
85
+ "go.mod": "module example.test/covered\n",
86
+ "contract_test.go": "package covered\nimport \"testing\"\nfunc TestContract(t *testing.T) {}\n",
87
+ });
88
+ assert.equal(testExecutionProof("cargo test", covered)?.static_test_units, 1);
89
+ assert.equal(testExecutionProof("go test ./...", covered)?.static_test_units, 1);
90
+ });