@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.
- package/CHANGELOG.md +7 -0
- package/build/src/builder-flow-run-adapter.d.ts +22 -2
- package/build/src/builder-flow-run-adapter.js +93 -28
- package/build/src/builder-flow-runtime.d.ts +8 -3
- package/build/src/builder-flow-runtime.js +308 -47
- package/build/src/cli/assignment-provider.d.ts +4 -7
- package/build/src/cli/assignment-provider.js +67 -13
- package/build/src/cli/builder-run.js +14 -18
- package/build/src/cli/workflow-sidecar.d.ts +28 -4
- package/build/src/cli/workflow-sidecar.js +801 -97
- package/build/src/cli/workflow.js +290 -42
- package/build/src/flow-kit/validate.d.ts +17 -0
- package/build/src/flow-kit/validate.js +340 -2
- package/build/src/index.js +5 -5
- package/build/src/lib/observed-command.d.ts +7 -0
- package/build/src/lib/observed-command.js +100 -0
- package/build/src/tools/generate-context-map.js +5 -3
- package/context/scripts/hooks/lib/kit-catalog.js +1 -1
- package/context/scripts/hooks/stop-goal-fit.js +78 -9
- package/docs/context-map.md +22 -20
- package/docs/developer-architecture.md +1 -1
- package/docs/public-workflow-cli.md +76 -7
- package/docs/skills-map.md +51 -27
- package/docs/spec/builder-flow-runtime.md +41 -40
- package/docs/workflow-usage-guide.md +109 -42
- package/evals/fixtures/hook-influence/cases.json +2 -2
- package/evals/integration/test_builder_entry_enforcement.sh +52 -41
- package/evals/integration/test_builder_step_producers.sh +297 -6
- package/evals/integration/test_bundle_install.sh +212 -63
- package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
- package/evals/integration/test_current_json_per_actor.sh +12 -0
- package/evals/integration/test_dual_emit_flow_step.sh +62 -43
- package/evals/integration/test_flowdef_session_activation.sh +145 -25
- package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
- package/evals/integration/test_gate_lockdown.sh +3 -5
- package/evals/integration/test_goal_fit_hook.sh +60 -2
- package/evals/integration/test_liveness_verdict.sh +14 -17
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
- package/evals/integration/test_public_workflow_cli.sh +325 -11
- package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
- package/evals/integration/test_sidecar_field_preservation.sh +36 -11
- package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
- package/evals/integration/test_workflow_steering_hook.sh +15 -38
- package/evals/run.sh +2 -0
- package/evals/static/test_builder_skill_coherence.sh +247 -0
- package/evals/static/test_library_exports.sh +5 -2
- package/evals/static/test_workflow_skills.sh +13 -325
- package/kits/builder/flows/build.flow.json +22 -0
- package/kits/builder/flows/shape.flow.json +9 -9
- package/kits/builder/kit.json +70 -16
- package/kits/builder/skills/builder-shape/SKILL.md +75 -75
- package/kits/builder/skills/continue-work/SKILL.md +45 -106
- package/kits/builder/skills/deliver/SKILL.md +96 -442
- package/kits/builder/skills/design-probe/SKILL.md +40 -139
- package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
- package/kits/builder/skills/execute-plan/SKILL.md +54 -125
- package/kits/builder/skills/fix-bug/SKILL.md +42 -132
- package/kits/builder/skills/gate-review/SKILL.md +60 -211
- package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
- package/kits/builder/skills/learning-review/SKILL.md +63 -170
- package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
- package/kits/builder/skills/plan-work/SKILL.md +51 -185
- package/kits/builder/skills/pull-work/SKILL.md +136 -485
- package/kits/builder/skills/release-readiness/SKILL.md +66 -107
- package/kits/builder/skills/review-work/SKILL.md +89 -176
- package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
- package/kits/builder/skills/verify-work/SKILL.md +101 -113
- package/package.json +2 -2
- package/scripts/hooks/lib/kit-catalog.js +1 -1
- package/scripts/hooks/stop-goal-fit.js +78 -9
- package/src/builder-flow-run-adapter.ts +118 -32
- package/src/builder-flow-runtime.ts +331 -61
- package/src/cli/assignment-provider-lock.test.mjs +83 -0
- package/src/cli/assignment-provider.ts +62 -13
- package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
- package/src/cli/builder-flow-runtime.test.mjs +194 -8
- package/src/cli/builder-run.ts +3 -9
- package/src/cli/kit-metadata-security.test.mjs +232 -6
- package/src/cli/public-api.test.mjs +15 -0
- package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
- package/src/cli/workflow-sidecar.ts +724 -97
- package/src/cli/workflow.ts +277 -40
- package/src/flow-kit/validate.ts +320 -2
- package/src/index.ts +6 -5
- package/src/lib/observed-command.ts +96 -0
- package/src/tools/generate-context-map.ts +5 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
}
|
package/build/src/index.js
CHANGED
|
@@ -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
|
|
5
|
-
* writer/validator
|
|
6
|
-
* `flow-agents
|
|
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,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
|
|
27
|
-
["
|
|
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: `
|
|
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
|
|
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.`);
|