@kontourai/flow-agents 3.6.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 (86) hide show
  1. package/CHANGELOG.md +7 -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 +308 -47
  6. package/build/src/cli/assignment-provider.d.ts +4 -7
  7. package/build/src/cli/assignment-provider.js +67 -13
  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 +801 -97
  11. package/build/src/cli/workflow.js +290 -42
  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/generate-context-map.js +5 -3
  18. package/context/scripts/hooks/lib/kit-catalog.js +1 -1
  19. package/context/scripts/hooks/stop-goal-fit.js +78 -9
  20. package/docs/context-map.md +22 -20
  21. package/docs/developer-architecture.md +1 -1
  22. package/docs/public-workflow-cli.md +76 -7
  23. package/docs/skills-map.md +51 -27
  24. package/docs/spec/builder-flow-runtime.md +41 -40
  25. package/docs/workflow-usage-guide.md +109 -42
  26. package/evals/fixtures/hook-influence/cases.json +2 -2
  27. package/evals/integration/test_builder_entry_enforcement.sh +52 -41
  28. package/evals/integration/test_builder_step_producers.sh +297 -6
  29. package/evals/integration/test_bundle_install.sh +212 -63
  30. package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
  31. package/evals/integration/test_current_json_per_actor.sh +12 -0
  32. package/evals/integration/test_dual_emit_flow_step.sh +62 -43
  33. package/evals/integration/test_flowdef_session_activation.sh +145 -25
  34. package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
  35. package/evals/integration/test_gate_lockdown.sh +3 -5
  36. package/evals/integration/test_goal_fit_hook.sh +60 -2
  37. package/evals/integration/test_liveness_verdict.sh +14 -17
  38. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
  39. package/evals/integration/test_public_workflow_cli.sh +325 -11
  40. package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
  41. package/evals/integration/test_sidecar_field_preservation.sh +36 -11
  42. package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
  43. package/evals/integration/test_workflow_steering_hook.sh +15 -38
  44. package/evals/run.sh +2 -0
  45. package/evals/static/test_builder_skill_coherence.sh +247 -0
  46. package/evals/static/test_library_exports.sh +5 -2
  47. package/evals/static/test_workflow_skills.sh +13 -325
  48. package/kits/builder/flows/build.flow.json +22 -0
  49. package/kits/builder/flows/shape.flow.json +9 -9
  50. package/kits/builder/kit.json +70 -16
  51. package/kits/builder/skills/builder-shape/SKILL.md +75 -75
  52. package/kits/builder/skills/continue-work/SKILL.md +45 -106
  53. package/kits/builder/skills/deliver/SKILL.md +96 -442
  54. package/kits/builder/skills/design-probe/SKILL.md +40 -139
  55. package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
  56. package/kits/builder/skills/execute-plan/SKILL.md +54 -125
  57. package/kits/builder/skills/fix-bug/SKILL.md +42 -132
  58. package/kits/builder/skills/gate-review/SKILL.md +60 -211
  59. package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
  60. package/kits/builder/skills/learning-review/SKILL.md +63 -170
  61. package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
  62. package/kits/builder/skills/plan-work/SKILL.md +51 -185
  63. package/kits/builder/skills/pull-work/SKILL.md +136 -485
  64. package/kits/builder/skills/release-readiness/SKILL.md +66 -107
  65. package/kits/builder/skills/review-work/SKILL.md +89 -176
  66. package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
  67. package/kits/builder/skills/verify-work/SKILL.md +101 -113
  68. package/package.json +2 -2
  69. package/scripts/hooks/lib/kit-catalog.js +1 -1
  70. package/scripts/hooks/stop-goal-fit.js +78 -9
  71. package/src/builder-flow-run-adapter.ts +118 -32
  72. package/src/builder-flow-runtime.ts +331 -61
  73. package/src/cli/assignment-provider-lock.test.mjs +83 -0
  74. package/src/cli/assignment-provider.ts +62 -13
  75. package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
  76. package/src/cli/builder-flow-runtime.test.mjs +194 -8
  77. package/src/cli/builder-run.ts +3 -9
  78. package/src/cli/kit-metadata-security.test.mjs +232 -6
  79. package/src/cli/public-api.test.mjs +15 -0
  80. package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
  81. package/src/cli/workflow-sidecar.ts +724 -97
  82. package/src/cli/workflow.ts +277 -40
  83. package/src/flow-kit/validate.ts +320 -2
  84. package/src/index.ts +6 -5
  85. package/src/lib/observed-command.ts +96 -0
  86. package/src/tools/generate-context-map.ts +5 -3
@@ -1,4 +1,4 @@
1
- import * as fs from "node:fs";
1
+ import fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { readJson } from "../lib/fs.js";
4
4
  // Extension-only asset classes: validated by Flow Agents. Flows are validated by @kontourai/flow.
@@ -14,7 +14,89 @@ const AGENT_EXTENSION_CLASSES = new Set(["skills", "docs", "adapters", "evals",
14
14
  // workflow_triggers / hook_influence_expectations declare kit-owned runtime influence,
15
15
  // and first_party is legacy catalog/marketplace metadata. It does not grant runtime
16
16
  // capability or steering privilege.
17
- const KNOWN_METADATA_FIELDS = new Set(["dependencies", "workflow_triggers", "hook_influence_expectations", "flow_step_actions", "first_party"]);
17
+ const KNOWN_METADATA_FIELDS = new Set(["dependencies", "workflow_triggers", "hook_influence_expectations", "flow_step_actions", "skill_roles", "first_party"]);
18
+ const MAX_FLOW_DEFINITION_BYTES = 1024 * 1024;
19
+ function sameFileIdentity(left, right) {
20
+ return left.dev === right.dev && left.ino === right.ino;
21
+ }
22
+ function lstatSafePath(root, relativePath) {
23
+ const parts = relativePath.split(path.sep);
24
+ let current = root;
25
+ for (const [index, part] of parts.entries()) {
26
+ if (!part || part === "." || part === "..")
27
+ return { error: "path must stay inside the kit directory" };
28
+ current = path.join(current, part);
29
+ const stat = fs.lstatSync(current);
30
+ if (stat.isSymbolicLink())
31
+ return { error: "path must not traverse a symbolic link" };
32
+ if (index < parts.length - 1 && !stat.isDirectory())
33
+ return { error: "path component must be a directory" };
34
+ if (index === parts.length - 1)
35
+ return { file: current, stat };
36
+ }
37
+ return { error: "path must reference a regular file" };
38
+ }
39
+ function readSafeFlowDefinition(kitDir, relativePath) {
40
+ const root = path.resolve(kitDir);
41
+ if (path.isAbsolute(relativePath))
42
+ return { error: "path must be relative" };
43
+ const lexicalFile = path.resolve(root, relativePath);
44
+ if (lexicalFile === root || !lexicalFile.startsWith(`${root}${path.sep}`))
45
+ return { error: "path must stay inside the kit directory" };
46
+ let rootDescriptor;
47
+ let descriptor;
48
+ try {
49
+ const realRoot = fs.realpathSync.native(root);
50
+ rootDescriptor = fs.openSync(realRoot, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
51
+ const rootIdentity = fs.fstatSync(rootDescriptor);
52
+ if (!rootIdentity.isDirectory() || !sameFileIdentity(rootIdentity, fs.lstatSync(realRoot)))
53
+ return { error: "kit directory identity changed while opening flow definition" };
54
+ const initialPath = lstatSafePath(realRoot, relativePath);
55
+ if (!initialPath.file || !initialPath.stat)
56
+ return { error: initialPath.error };
57
+ if (!initialPath.stat.isFile())
58
+ return { error: "path must reference a regular file" };
59
+ if (initialPath.stat.size > MAX_FLOW_DEFINITION_BYTES)
60
+ return { error: `file exceeds ${MAX_FLOW_DEFINITION_BYTES} bytes` };
61
+ // O_NOFOLLOW protects the terminal component. Identity checks before and after
62
+ // opening make a race on any intermediate component fail closed on macOS/Node,
63
+ // where openat-style traversal from a directory descriptor is not exposed.
64
+ descriptor = fs.openSync(initialPath.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
65
+ const stat = fs.fstatSync(descriptor);
66
+ if (!stat.isFile())
67
+ return { error: "path must reference a regular file" };
68
+ if (!sameFileIdentity(initialPath.stat, stat))
69
+ return { error: "flow definition identity changed while opening" };
70
+ if (stat.size > MAX_FLOW_DEFINITION_BYTES)
71
+ return { error: `file exceeds ${MAX_FLOW_DEFINITION_BYTES} bytes` };
72
+ const verifiedPath = lstatSafePath(realRoot, relativePath);
73
+ if (!verifiedPath.stat || !sameFileIdentity(stat, verifiedPath.stat))
74
+ return { error: "flow definition identity changed while opening" };
75
+ const buffer = Buffer.alloc(stat.size);
76
+ let offset = 0;
77
+ while (offset < buffer.length) {
78
+ const read = fs.readSync(descriptor, buffer, offset, buffer.length - offset, offset);
79
+ if (read === 0)
80
+ break;
81
+ offset += read;
82
+ }
83
+ const finalStat = fs.fstatSync(descriptor);
84
+ if (offset !== stat.size || finalStat.size !== stat.size || !sameFileIdentity(stat, finalStat))
85
+ return { error: "file changed while being read" };
86
+ return { definition: JSON.parse(buffer.toString("utf8")) };
87
+ }
88
+ catch (error) {
89
+ if (error.code === "ELOOP")
90
+ return { error: "path must not reference a symbolic link" };
91
+ return { error: `path is not readable: ${error.message}` };
92
+ }
93
+ finally {
94
+ if (descriptor !== undefined)
95
+ fs.closeSync(descriptor);
96
+ if (rootDescriptor !== undefined)
97
+ fs.closeSync(rootDescriptor);
98
+ }
99
+ }
18
100
  /**
19
101
  * Parse and shape-validate a kit manifest's `dependencies` field (Flow Agents
20
102
  * extension-layer metadata; see docs/adr/0019-kit-dependency-ownership.md).
@@ -249,6 +331,8 @@ export function parseKitFlowStepActions(manifest, manifestPath) {
249
331
  }
250
332
  const skills = record.skills;
251
333
  const operations = record.operations ?? [];
334
+ const expectationIds = record.expectation_ids;
335
+ const artifacts = record.artifacts ?? [];
252
336
  if (!Array.isArray(skills) || !skills.every(workflowTriggerIdentifier)) {
253
337
  errors.push(`${manifestPath}: flow_step_actions[${index}].skills must be an identifier list`);
254
338
  return;
@@ -265,6 +349,14 @@ export function parseKitFlowStepActions(manifest, manifestPath) {
265
349
  errors.push(`${manifestPath}: flow_step_actions[${index}].operations must not contain duplicates`);
266
350
  return;
267
351
  }
352
+ if (expectationIds !== undefined && (!Array.isArray(expectationIds) || !expectationIds.every(workflowTriggerIdentifier) || new Set(expectationIds).size !== expectationIds.length)) {
353
+ errors.push(`${manifestPath}: flow_step_actions[${index}].expectation_ids must be a unique identifier list when present`);
354
+ return;
355
+ }
356
+ if (!Array.isArray(artifacts) || !artifacts.every((artifact) => typeof artifact === "string" && artifact.trim().length > 0) || new Set(artifacts).size !== artifacts.length) {
357
+ errors.push(`${manifestPath}: flow_step_actions[${index}].artifacts must be a unique non-empty string list when present`);
358
+ return;
359
+ }
268
360
  const key = `${record.flow_id}/${record.step_id}`;
269
361
  if (seen.has(key)) {
270
362
  errors.push(`${manifestPath}: flow_step_actions[${index}] duplicates '${key}'`);
@@ -276,10 +368,78 @@ export function parseKitFlowStepActions(manifest, manifestPath) {
276
368
  step_id: record.step_id,
277
369
  skills: skills,
278
370
  operations: operations,
371
+ artifacts: artifacts,
372
+ ...(Array.isArray(expectationIds) ? { expectation_ids: expectationIds } : {}),
279
373
  });
280
374
  });
281
375
  return { entries, errors };
282
376
  }
377
+ export function parseKitSkillRoles(manifest, manifestPath) {
378
+ const entries = [];
379
+ const errors = [];
380
+ const raw = manifest.skill_roles;
381
+ if (raw === undefined)
382
+ return { entries, errors };
383
+ if (!Array.isArray(raw)) {
384
+ errors.push(`${manifestPath}: .skill_roles must be a list`);
385
+ return { entries, errors };
386
+ }
387
+ const roles = new Set(["entrypoint", "profile", "step", "shared-primitive", "extension"]);
388
+ const fields = new Set(["skill_id", "role", "flow_id", "step_ids", "artifacts", "expectation_ids"]);
389
+ const seen = new Set();
390
+ raw.forEach((entry, index) => {
391
+ if (typeof entry !== "object" || entry === null) {
392
+ errors.push(`${manifestPath}: skill_roles[${index}] must be an object`);
393
+ return;
394
+ }
395
+ const record = entry;
396
+ const unknown = Object.keys(record).filter((field) => !fields.has(field));
397
+ if (unknown.length > 0)
398
+ errors.push(`${manifestPath}: skill_roles[${index}] contains unsupported field(s): ${unknown.join(", ")}`);
399
+ if (!workflowTriggerIdentifier(record.skill_id)) {
400
+ errors.push(`${manifestPath}: skill_roles[${index}].skill_id must be an identifier`);
401
+ return;
402
+ }
403
+ if (seen.has(record.skill_id))
404
+ errors.push(`${manifestPath}: skill_roles[${index}].skill_id duplicates '${record.skill_id}'`);
405
+ seen.add(record.skill_id);
406
+ if (typeof record.role !== "string" || !roles.has(record.role)) {
407
+ errors.push(`${manifestPath}: skill_roles[${index}].role must be entrypoint, profile, step, shared-primitive, or extension`);
408
+ return;
409
+ }
410
+ const role = record.role;
411
+ if (record.flow_id !== undefined && !workflowTriggerIdentifier(record.flow_id)) {
412
+ errors.push(`${manifestPath}: skill_roles[${index}].flow_id must be a Flow identifier when present`);
413
+ }
414
+ for (const field of ["step_ids", "expectation_ids"]) {
415
+ const value = record[field];
416
+ if (!Array.isArray(value) || !value.every(workflowTriggerIdentifier) || new Set(value).size !== value.length) {
417
+ errors.push(`${manifestPath}: skill_roles[${index}].${field} must be a unique identifier list`);
418
+ }
419
+ }
420
+ if (!Array.isArray(record.artifacts) || !record.artifacts.every((artifact) => typeof artifact === "string" && artifact.trim().length > 0) || new Set(record.artifacts).size !== record.artifacts.length) {
421
+ errors.push(`${manifestPath}: skill_roles[${index}].artifacts must be a unique non-empty string list`);
422
+ }
423
+ const stepIds = Array.isArray(record.step_ids) ? record.step_ids.filter(workflowTriggerIdentifier) : [];
424
+ const expectationIds = Array.isArray(record.expectation_ids) ? record.expectation_ids.filter(workflowTriggerIdentifier) : [];
425
+ const artifacts = Array.isArray(record.artifacts) ? record.artifacts.filter((artifact) => typeof artifact === "string" && artifact.trim().length > 0) : [];
426
+ const flowId = typeof record.flow_id === "string" && workflowTriggerIdentifier(record.flow_id) ? record.flow_id : undefined;
427
+ if ((role === "entrypoint" || role === "profile") && (!flowId || stepIds.length > 0 || expectationIds.length > 0 || artifacts.length > 0)) {
428
+ errors.push(`${manifestPath}: skill_roles[${index}] ${role} must select one flow and own no steps, artifacts, or expectations`);
429
+ }
430
+ if (role === "step" && (!flowId || stepIds.length === 0 || artifacts.length === 0)) {
431
+ errors.push(`${manifestPath}: skill_roles[${index}] step must bind one flow, at least one step, and at least one artifact`);
432
+ }
433
+ if (role === "shared-primitive" && (flowId || stepIds.length > 0 || expectationIds.length > 0 || artifacts.length === 0)) {
434
+ errors.push(`${manifestPath}: skill_roles[${index}] ${role} must own artifacts but no Builder flow, steps, or expectations`);
435
+ }
436
+ if (role === "extension" && (flowId || stepIds.length > 0 || expectationIds.length > 0)) {
437
+ errors.push(`${manifestPath}: skill_roles[${index}] extension must own no Builder flow, steps, or expectations`);
438
+ }
439
+ entries.push({ skill_id: record.skill_id, role, ...(flowId ? { flow_id: flowId } : {}), step_ids: stepIds, artifacts, expectation_ids: expectationIds });
440
+ });
441
+ return { entries, errors };
442
+ }
283
443
  /**
284
444
  * Derive the trust level for a kit id.
285
445
  * "verified" is reserved for future third-party verification.
@@ -461,6 +621,184 @@ export async function validateKitRepository(kitDir) {
461
621
  const flowStepActionResult = parseKitFlowStepActions(manifest, manifestPath);
462
622
  for (const err of flowStepActionResult.errors)
463
623
  errors.push(err);
624
+ const skillRoleResult = parseKitSkillRoles(manifest, manifestPath);
625
+ for (const err of skillRoleResult.errors)
626
+ errors.push(err);
627
+ if (manifest.skill_roles !== undefined && skillRoleResult.errors.length === 0) {
628
+ const declaredSkillIds = new Set(Array.isArray(manifest.skills)
629
+ ? manifest.skills.flatMap((entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" ? [entry.id] : [])
630
+ : []);
631
+ const roleSkillIds = new Set(skillRoleResult.entries.map((entry) => entry.skill_id));
632
+ for (const id of declaredSkillIds)
633
+ if (!roleSkillIds.has(id))
634
+ errors.push(`${manifestPath}: skill_roles is missing declared skill '${id}'`);
635
+ for (const id of roleSkillIds)
636
+ if (!declaredSkillIds.has(id))
637
+ errors.push(`${manifestPath}: skill_roles references undeclared skill '${id}'`);
638
+ const flows = new Map();
639
+ if (Array.isArray(manifest.flows)) {
640
+ for (const entry of manifest.flows) {
641
+ if (typeof entry !== "object" || entry === null)
642
+ continue;
643
+ const flow = entry;
644
+ if (typeof flow.id !== "string" || typeof flow.path !== "string")
645
+ continue;
646
+ const safeDefinition = readSafeFlowDefinition(kitDir, flow.path);
647
+ if (!safeDefinition.definition) {
648
+ errors.push(`${manifestPath}: flows '${flow.id}' ${safeDefinition.error}`);
649
+ continue;
650
+ }
651
+ try {
652
+ const definition = safeDefinition.definition;
653
+ const steps = new Set(Array.isArray(definition.steps) ? definition.steps.flatMap((step) => typeof step === "object" && step !== null && typeof step.id === "string" ? [step.id] : []) : []);
654
+ const usesFlowByStep = new Map();
655
+ if (Array.isArray(definition.steps)) {
656
+ for (const step of definition.steps) {
657
+ if (typeof step !== "object" || step === null)
658
+ continue;
659
+ const stepRecord = step;
660
+ if (typeof stepRecord.id === "string" && typeof stepRecord.uses_flow === "string")
661
+ usesFlowByStep.set(stepRecord.id, stepRecord.uses_flow);
662
+ }
663
+ }
664
+ const expectationsByStep = new Map();
665
+ if (typeof definition.gates === "object" && definition.gates !== null) {
666
+ for (const gate of Object.values(definition.gates)) {
667
+ if (typeof gate !== "object" || gate === null)
668
+ continue;
669
+ const gateRecord = gate;
670
+ if (typeof gateRecord.step !== "string" || !Array.isArray(gateRecord.expects))
671
+ continue;
672
+ const expectations = expectationsByStep.get(gateRecord.step) ?? new Map();
673
+ for (const expectation of gateRecord.expects) {
674
+ if (typeof expectation !== "object" || expectation === null)
675
+ continue;
676
+ const expectationRecord = expectation;
677
+ if (typeof expectationRecord.id !== "string")
678
+ continue;
679
+ const exportKeys = new Set([expectationRecord.id]);
680
+ const claimType = expectationRecord.bundle_claim?.claimType;
681
+ if (typeof claimType === "string")
682
+ exportKeys.add(claimType);
683
+ expectations.set(expectationRecord.id, { id: expectationRecord.id, exportKeys });
684
+ }
685
+ expectationsByStep.set(gateRecord.step, expectations);
686
+ }
687
+ }
688
+ flows.set(flow.id, {
689
+ steps,
690
+ expectationsByStep,
691
+ usesFlowByStep,
692
+ exports: new Set(Array.isArray(definition.exports) ? definition.exports.filter((entry) => typeof entry === "string" && entry.length > 0) : []),
693
+ });
694
+ }
695
+ catch {
696
+ // Core Flow validation reports missing or malformed definitions.
697
+ }
698
+ }
699
+ }
700
+ const resolveEffectiveFlowStep = (flowId, stepId, seen = new Set()) => {
701
+ const cycleKey = `${flowId}\u0000${stepId}`;
702
+ if (seen.has(cycleKey))
703
+ return undefined;
704
+ seen.add(cycleKey);
705
+ const flow = flows.get(flowId);
706
+ if (!flow)
707
+ return undefined;
708
+ const direct = flow.expectationsByStep.get(stepId);
709
+ if (direct)
710
+ return { sourceFlowId: flowId, stepId, expectations: direct, flowIds: new Set([flowId]) };
711
+ const childFlowId = flow.usesFlowByStep.get(stepId);
712
+ if (!childFlowId)
713
+ return { sourceFlowId: flowId, stepId, expectations: new Map(), flowIds: new Set([flowId]) };
714
+ const child = resolveEffectiveFlowStep(childFlowId, stepId, seen);
715
+ const childFlow = flows.get(childFlowId);
716
+ if (!child || !childFlow || [...child.expectations.values()].some((expectation) => ![...expectation.exportKeys].some((key) => childFlow.exports.has(key))))
717
+ return undefined;
718
+ child.flowIds.add(flowId);
719
+ return child;
720
+ };
721
+ for (const row of skillRoleResult.entries) {
722
+ if (!row.flow_id)
723
+ continue;
724
+ const flow = flows.get(row.flow_id);
725
+ if (!flow) {
726
+ errors.push(`${manifestPath}: skill_roles '${row.skill_id}' references unknown flow '${row.flow_id}'`);
727
+ continue;
728
+ }
729
+ for (const stepId of row.step_ids)
730
+ if (!flow.steps.has(stepId))
731
+ errors.push(`${manifestPath}: skill_roles '${row.skill_id}' references unknown step '${row.flow_id}/${stepId}'`);
732
+ const allowedExpectations = new Set(row.step_ids.flatMap((stepId) => [...(resolveEffectiveFlowStep(row.flow_id, stepId)?.expectations.keys() ?? [])]));
733
+ for (const expectationId of row.expectation_ids)
734
+ if (!allowedExpectations.has(expectationId))
735
+ errors.push(`${manifestPath}: skill_roles '${row.skill_id}' expectation '${expectationId}' is not owned by its bound step(s)`);
736
+ }
737
+ const roleByShortId = new Map(skillRoleResult.entries.map((entry) => [entry.skill_id.replace(`${String(manifest.id)}.`, ""), entry]));
738
+ const producerOwners = new Map();
739
+ for (const row of skillRoleResult.entries) {
740
+ if (row.role !== "step" || !row.flow_id)
741
+ continue;
742
+ for (const stepId of row.step_ids) {
743
+ const effectiveStep = resolveEffectiveFlowStep(row.flow_id, stepId);
744
+ if (!effectiveStep)
745
+ continue;
746
+ for (const expectationId of row.expectation_ids) {
747
+ if (!effectiveStep.expectations.has(expectationId))
748
+ continue;
749
+ const key = `${effectiveStep.sourceFlowId}\u0000${effectiveStep.stepId}\u0000${expectationId}`;
750
+ producerOwners.set(key, [...(producerOwners.get(key) ?? []), `skill:${row.skill_id}`]);
751
+ }
752
+ }
753
+ }
754
+ for (const action of flowStepActionResult.entries) {
755
+ const containingFlow = flows.get(action.flow_id);
756
+ if (!containingFlow || !containingFlow.steps.has(action.step_id)) {
757
+ errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' references an unknown flow step`);
758
+ continue;
759
+ }
760
+ const effectiveStep = resolveEffectiveFlowStep(action.flow_id, action.step_id);
761
+ if (!effectiveStep) {
762
+ errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' cannot resolve its composed Flow step`);
763
+ continue;
764
+ }
765
+ const expectedAtStep = effectiveStep.expectations;
766
+ for (const expectationId of action.expectation_ids ?? []) {
767
+ if (!expectedAtStep.has(expectationId))
768
+ errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' operation expectation '${expectationId}' is not owned by its resolved Flow step`);
769
+ }
770
+ if (action.skills.length === 0 && action.operations.length > 0 && expectedAtStep.size > 0 && !(action.expectation_ids?.length)) {
771
+ errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' operation-only action must explicitly declare expectation_ids`);
772
+ }
773
+ const actionRows = [];
774
+ for (const skill of action.skills) {
775
+ const row = roleByShortId.get(skill);
776
+ if (!row || row.role !== "step" || !row.flow_id || !effectiveStep.flowIds.has(row.flow_id) || !row.step_ids.includes(action.step_id)) {
777
+ errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' skill '${skill}' must match one step-role binding`);
778
+ }
779
+ else {
780
+ actionRows.push(row);
781
+ }
782
+ }
783
+ for (const expectationId of expectedAtStep.keys()) {
784
+ const key = `${effectiveStep.sourceFlowId}\u0000${effectiveStep.stepId}\u0000${expectationId}`;
785
+ const owners = producerOwners.get(key) ?? [];
786
+ if (action.operations.length > 0 && action.expectation_ids?.includes(expectationId))
787
+ owners.push(`operation:${action.flow_id}/${action.step_id}`);
788
+ producerOwners.set(key, owners);
789
+ }
790
+ }
791
+ for (const [flowId, flow] of flows) {
792
+ for (const [stepId, expectations] of flow.expectationsByStep) {
793
+ for (const expectationId of expectations.keys()) {
794
+ const owners = producerOwners.get(`${flowId}\u0000${stepId}\u0000${expectationId}`) ?? [];
795
+ if (owners.length !== 1) {
796
+ errors.push(`${manifestPath}: flow expectation '${flowId}/${stepId}/${expectationId}' must have exactly one producer owner; found ${owners.length}`);
797
+ }
798
+ }
799
+ }
800
+ }
801
+ }
464
802
  if (manifest.first_party !== undefined && typeof manifest.first_party !== "boolean") {
465
803
  errors.push(`${manifestPath}: .first_party must be a boolean when present`);
466
804
  }
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * Public library surface for `@kontourai/flow-agents`.
3
3
  *
4
- * Native orchestration hosts can import the canonical workflow-sidecar
5
- * writer/validator here instead of shelling out to the
6
- * `flow-agents-workflow-sidecar` CLI or reimplementing validated
7
- * read / merge / write of workflow evidence. This is the same code the CLI
8
- * runs — importing it does not execute the CLI.
4
+ * Native orchestration hosts can import the canonical workflow runtime and
5
+ * sidecar writer/validator instead of reimplementing validated state changes.
6
+ * Agent-facing Kit guidance uses the public `flow-agents workflow` CLI.
9
7
  *
10
8
  * The sidecar JSON Schemas ship under `schemas/` and can be validated against
11
9
  * directly; the helpers below are the canonical writer/validator that produce
@@ -17,6 +15,8 @@ import * as path from "node:path";
17
15
  import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
18
16
  export { BUILDER_BUILD_FLOW_ID, BUILDER_BUILD_FLOW_RELATIVE_PATH, BuilderBuildRunInputError, BuilderBuildRunIdentityError, evaluateBuilderBuildRun, loadBuilderBuildRun, resolveBuilderBuildFlowDefinitionPath, startBuilderBuildRun, } from "./builder-flow-run-adapter.js";
19
17
  export { archiveBuilderFlowSession, cancelBuilderFlowSession, pauseBuilderFlowSession, recoverBuilderFlowSession, releaseBuilderFlowAssignment, resumeBuilderFlowSession, startBuilderFlowSession, syncBuilderFlowSession, } from "./builder-flow-runtime.js";
18
+ // Pure serialization contract used by external lifecycle authorities when
19
+ // signing requests. This does not load, create, or mutate a Flow run.
20
20
  export { builderLifecycleAuthorizationPayload, loadBuilderLifecycleAuthorization } from "./builder-lifecycle-authority.js";
21
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";
22
22
  export {
@@ -0,0 +1,7 @@
1
+ export type ObservedProcessResult = {
2
+ command: string;
3
+ exit_code: number | null;
4
+ output_sha256: string;
5
+ output: string;
6
+ };
7
+ export declare function runObservedCommand(command: string, projectRoot: string): Promise<ObservedProcessResult>;
@@ -0,0 +1,100 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ function configuredTimeout(variable, fallback) {
4
+ const value = Number(process.env[variable] ?? fallback);
5
+ return Number.isSafeInteger(value) && value > 0 ? value : fallback;
6
+ }
7
+ export async function runObservedCommand(command, projectRoot) {
8
+ const timeoutMs = configuredTimeout("FLOW_AGENTS_EVIDENCE_COMMAND_TIMEOUT_MS", 600000);
9
+ const killGraceMs = configuredTimeout("FLOW_AGENTS_EVIDENCE_COMMAND_KILL_GRACE_MS", 5000);
10
+ const result = await new Promise((resolve, reject) => {
11
+ const child = spawn("bash", ["-lc", command], {
12
+ cwd: projectRoot,
13
+ detached: process.platform !== "win32",
14
+ stdio: ["ignore", "pipe", "pipe"],
15
+ });
16
+ const stdoutHash = createHash("sha256");
17
+ const stderrHash = createHash("sha256");
18
+ let settled = false;
19
+ let timeout;
20
+ let killTimer;
21
+ let cleanupStarted = false;
22
+ let cleanupComplete = false;
23
+ let streamsClosed = false;
24
+ let closedCode = null;
25
+ let output = "";
26
+ const captureOutput = (chunk) => {
27
+ if (output.length >= 64 * 1024)
28
+ return;
29
+ output += chunk.toString("utf8").slice(0, 64 * 1024 - output.length);
30
+ };
31
+ const terminateProcessGroup = (signal) => {
32
+ try {
33
+ if (process.platform !== "win32" && child.pid)
34
+ process.kill(-child.pid, signal);
35
+ else
36
+ child.kill(signal);
37
+ return true;
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ESRCH")
41
+ return false;
42
+ throw error;
43
+ }
44
+ };
45
+ const fail = (error) => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
49
+ if (timeout)
50
+ clearTimeout(timeout);
51
+ if (killTimer)
52
+ clearTimeout(killTimer);
53
+ reject(error);
54
+ };
55
+ const complete = () => {
56
+ if (settled || !cleanupComplete || !streamsClosed)
57
+ return;
58
+ settled = true;
59
+ if (timeout)
60
+ clearTimeout(timeout);
61
+ const outputHash = createHash("sha256")
62
+ .update("stdout\0").update(stdoutHash.digest())
63
+ .update("stderr\0").update(stderrHash.digest());
64
+ resolve({ code: closedCode, outputSha256: outputHash.digest("hex"), output });
65
+ };
66
+ const beginCleanup = () => {
67
+ if (settled || cleanupStarted)
68
+ return;
69
+ cleanupStarted = true;
70
+ try {
71
+ if (!terminateProcessGroup("SIGTERM")) {
72
+ cleanupComplete = true;
73
+ complete();
74
+ return;
75
+ }
76
+ killTimer = setTimeout(() => {
77
+ killTimer = undefined;
78
+ try {
79
+ terminateProcessGroup("SIGKILL");
80
+ cleanupComplete = true;
81
+ complete();
82
+ }
83
+ catch (error) {
84
+ fail(error);
85
+ }
86
+ }, killGraceMs);
87
+ }
88
+ catch (error) {
89
+ fail(error);
90
+ }
91
+ };
92
+ timeout = setTimeout(beginCleanup, timeoutMs);
93
+ child.stdout.on("data", (chunk) => { stdoutHash.update(chunk); captureOutput(chunk); });
94
+ child.stderr.on("data", (chunk) => { stderrHash.update(chunk); captureOutput(chunk); });
95
+ child.once("error", fail);
96
+ child.once("exit", (code) => { closedCode = code; beginCleanup(); });
97
+ child.once("close", () => { streamsClosed = true; complete(); });
98
+ });
99
+ return { command, exit_code: result.code, output_sha256: result.outputSha256, output: result.output };
100
+ }
@@ -23,8 +23,10 @@ const commands = [
23
23
  ["Static suite", "bash evals/run.sh static"],
24
24
  ["Integration suite", "bash evals/run.sh integration"],
25
25
  ["Workflow artifacts", `npm run workflow:validate-artifacts -- --require-sidecars --require-critique ${FLOW_AGENTS_RUNTIME_DIR}/<slug>`],
26
- ["Workflow sidecars", "npm run workflow:sidecar -- --help"],
27
- ["Claim lookup", "npm run workflow:sidecar -- claim <id> <dir>"],
26
+ ["Workflow control", "flow-agents workflow --help"],
27
+ ["Workflow status", "flow-agents workflow status --session-dir .kontourai/flow-agents/<slug> --json"],
28
+ ["Workflow critique", "flow-agents workflow critique --session-dir .kontourai/flow-agents/<slug> --verdict <pass|fail|not_verified> --summary <text>"],
29
+ ["Workflow verification", "flow-agents workflow evidence --session-dir .kontourai/flow-agents/<slug> --expectation tests-evidence --status <pass|fail|not_verified> --command \"bash evals/static/test_builder_skill_coherence.sh\" --criterion-json '{\"id\":\"AC-1\",\"status\":\"pass\",\"evidence_refs\":[{\"kind\":\"command\",\"excerpt\":\"bash evals/static/test_builder_skill_coherence.sh\",\"summary\":\"Exact substantive command run for AC-1.\"}]}' --evidence-ref-json '{\"kind\":\"artifact\",\"file\":\".kontourai/flow-agents/<slug>/<slug>--plan-work.md\",\"summary\":\"Reviewable criterion mapping.\"}'"],
28
30
  ["Context map drift", "npm run context-map:check"],
29
31
  ["Bundle build", "npm run build:bundles"],
30
32
  ["Skill drift check", "flow-agents skill-drift-check"],
@@ -193,7 +195,7 @@ function render(includeRuntime) {
193
195
  "## Workflow Sidecars", "",
194
196
  `Machine-readable workflow state lives beside Markdown artifacts in \`${FLOW_AGENTS_RUNTIME_DIR}/<slug>/\`.`, "",
195
197
  ...markdownTable(["Schema", "Title", "ID"], schemas()), "",
196
- "Primary tools: `npm run workflow:sidecar`, `npm run workflow:validate-artifacts`, `scripts/hooks/stop-goal-fit.js`, and `scripts/hooks/workflow-steering.js`.", "",
198
+ "Primary tools: public `flow-agents workflow`, `trust.bundle`, artifact validation, `scripts/hooks/stop-goal-fit.js`, and `scripts/hooks/workflow-steering.js`. The package-internal writer is not an agent or consumer interface.", "",
197
199
  "## Workflow Skills", "", ...markdownTable(["Skill", "Source", "When To Load"], workflowRows), "",
198
200
  "## Support Skills", "", ...markdownTable(["Skill", "Source", "When To Load"], supportRows), "",
199
201
  "## Agents", "", ...markdownTable(["Agent", "Model", "Tools", "Role"], agents()), "",
@@ -137,7 +137,7 @@ function renderKitSteering(trigger) {
137
137
  else lines.push(`Activate \`${defaultSkill}\`.`);
138
138
  }
139
139
  if (targetFlowId) {
140
- lines.push(`Keep the session on \`${targetFlowId}\` and use \`npm run workflow:sidecar -- ensure-session --flow-id ${targetFlowId} ...\` when the repo provides the sidecar writer.`);
140
+ lines.push(`Keep the session on \`${targetFlowId}\`. Use the public \`flow-agents workflow\` interface only when it supports that Flow and its Work Item binding; otherwise report an unsupported-runtime blocker. Never call the package-internal writer from skill guidance.`);
141
141
  }
142
142
  if (requiredSequence.length) {
143
143
  lines.push(`Do not bypass ${requiredSequence.join(' -> ')} for matching work.`);