@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.
- package/CHANGELOG.md +19 -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 +407 -51
- package/build/src/cli/assignment-provider.d.ts +4 -7
- package/build/src/cli/assignment-provider.js +80 -14
- 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 +825 -103
- package/build/src/cli/workflow.js +301 -43
- 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/build-universal-bundles.js +53 -3
- 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/getting-started.md +9 -1
- package/docs/public-workflow-cli.md +76 -7
- package/docs/skills-map.md +51 -27
- package/docs/spec/builder-flow-runtime.md +75 -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 +330 -6
- package/evals/integration/test_bundle_install.sh +318 -65
- package/evals/integration/test_bundle_lifecycle.sh +4 -6
- 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_install_merge.sh +18 -8
- 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 +334 -14
- 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_universal_bundles.sh +44 -1
- 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/packaging/README.md +1 -1
- package/scripts/README.md +1 -1
- package/scripts/hooks/lib/kit-catalog.js +1 -1
- package/scripts/hooks/stop-goal-fit.js +78 -9
- package/scripts/install-codex-home.sh +63 -23
- package/scripts/install-owned-files.js +18 -2
- package/src/builder-flow-run-adapter.ts +118 -32
- package/src/builder-flow-runtime.ts +426 -64
- package/src/cli/assignment-provider-lock.test.mjs +83 -0
- package/src/cli/assignment-provider.ts +75 -14
- package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
- package/src/cli/builder-flow-runtime.test.mjs +436 -18
- 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 +746 -103
- package/src/cli/workflow.ts +288 -41
- 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/build-universal-bundles.ts +51 -3
- package/src/tools/generate-context-map.ts +5 -3
|
@@ -3,19 +3,23 @@ import * as path from "node:path";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
|
-
import {
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { validateDefinition } from "@kontourai/flow";
|
|
8
|
+
import { loadBuilderFlowRun } from "../builder-flow-run-adapter.js";
|
|
9
|
+
import { inspectBuilderFlowSession, recoverBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
|
|
7
10
|
import { flowAgentsPackageRoot, flowAgentsPackageVersion } from "../lib/package-version.js";
|
|
8
11
|
import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
|
|
9
12
|
import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
10
|
-
import { flagBool, flagString, parseArgs } from "../lib/args.js";
|
|
13
|
+
import { flagBool, flagList, flagString, parseArgs } from "../lib/args.js";
|
|
11
14
|
import { main as builderRun } from "./builder-run.js";
|
|
12
|
-
import { currentWorkflowSessionDir,
|
|
15
|
+
import { currentWorkflowSessionDir, isMeaningfulTestCommand, mainFromPublicWorkflow, WORKFLOW_WRITER_CONTRACT_VERSION } from "./workflow-sidecar.js";
|
|
16
|
+
import { resolveCurrentAssignmentActor, withSubjectLock } from "./assignment-provider.js";
|
|
13
17
|
export const WORKFLOW_CONTRACT_VERSION = "1.0";
|
|
14
18
|
const PACKAGE_ROOT = flowAgentsPackageRoot();
|
|
15
19
|
const REQUIRE = createRequire(import.meta.url);
|
|
16
20
|
const PACKAGE_METADATA = readJsonFile(path.join(PACKAGE_ROOT, "package.json"), "Flow Agents package metadata");
|
|
17
21
|
const CLI_VERSION = flowAgentsPackageVersion();
|
|
18
|
-
const PUBLIC_VERBS = ["start", "status", "evidence", "pause", "resume", "release", "cancel", "archive", "doctor"];
|
|
22
|
+
const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "pause", "resume", "release", "cancel", "archive", "doctor"];
|
|
19
23
|
function usage() {
|
|
20
24
|
console.log(`Usage: flow-agents workflow <verb> [options]
|
|
21
25
|
|
|
@@ -23,6 +27,7 @@ Public workflow verbs:
|
|
|
23
27
|
start Start or resume a workflow for a Work Item.
|
|
24
28
|
status Show the current canonical run and projected next action.
|
|
25
29
|
evidence Record evidence for the current Flow gate and synchronize it.
|
|
30
|
+
critique Record review critique directly into the current trust bundle.
|
|
26
31
|
pause Pause the current run as its assignment actor.
|
|
27
32
|
resume Resume the current paused run as its assignment actor.
|
|
28
33
|
release Release the current assignment without canceling the run.
|
|
@@ -53,6 +58,8 @@ export async function main(argv) {
|
|
|
53
58
|
return status(sessionDir, flagBool(parsed.flags, "json"));
|
|
54
59
|
if (verb === "evidence")
|
|
55
60
|
return evidence(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
|
|
61
|
+
if (verb === "critique")
|
|
62
|
+
return critique(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
|
|
56
63
|
const forwarded = stripPublicFlags(argv.slice(1), new Set(["artifact-root", "session-dir", "json"]));
|
|
57
64
|
if (verb === "release" && !flagString(parsed.flags, "reason"))
|
|
58
65
|
throw new Error("workflow release requires --reason <text>");
|
|
@@ -60,16 +67,34 @@ export async function main(argv) {
|
|
|
60
67
|
}
|
|
61
68
|
async function start(argv) {
|
|
62
69
|
const parsed = parseArgs(argv);
|
|
63
|
-
assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion"]), "workflow start");
|
|
70
|
+
assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion", "assignment-provider", "effective-state-json"]), "workflow start");
|
|
64
71
|
const flow = flagString(parsed.flags, "flow", "builder.build");
|
|
65
|
-
if (flow !== "builder.build")
|
|
66
|
-
throw new Error("workflow start
|
|
72
|
+
if (flow !== "builder.build" && flow !== "builder.shape")
|
|
73
|
+
throw new Error("workflow start supports only --flow builder.build or builder.shape");
|
|
67
74
|
const workItem = flagString(parsed.flags, "work-item");
|
|
68
|
-
if (!workItem)
|
|
69
|
-
throw new Error("workflow start requires --work-item <owner/repo#id>");
|
|
70
75
|
const artifactRoot = path.resolve(flagString(parsed.flags, "artifact-root", flowAgentsArtifactRoot()));
|
|
71
76
|
const taskSlug = flagString(parsed.flags, "task-slug");
|
|
72
|
-
if (
|
|
77
|
+
if (flow === "builder.shape") {
|
|
78
|
+
if (!taskSlug || !isSafeSlug(taskSlug))
|
|
79
|
+
throw new Error("workflow start --flow builder.shape requires an explicit safe --task-slug");
|
|
80
|
+
if (workItem)
|
|
81
|
+
throw new Error("workflow start --flow builder.shape creates a local Work Item; omit --work-item");
|
|
82
|
+
}
|
|
83
|
+
else if (!workItem) {
|
|
84
|
+
throw new Error("workflow start requires --work-item <provider-ref>");
|
|
85
|
+
}
|
|
86
|
+
const assignmentProvider = flagString(parsed.flags, "assignment-provider", flow === "builder.shape" || workItem?.startsWith("local:") ? "local-file" : undefined);
|
|
87
|
+
const effectiveStateJson = flagString(parsed.flags, "effective-state-json");
|
|
88
|
+
if (flow === "builder.build" && workItem && !workItem.startsWith("local:") && !assignmentProvider) {
|
|
89
|
+
throw new Error("workflow start requires --assignment-provider <kind> for a provider-backed Work Item; provider identity is never inferred from its reference");
|
|
90
|
+
}
|
|
91
|
+
if (assignmentProvider !== "local-file" && !effectiveStateJson) {
|
|
92
|
+
throw new Error(`workflow start requires --effective-state-json <path> for assignment provider ${assignmentProvider}`);
|
|
93
|
+
}
|
|
94
|
+
if (assignmentProvider === "local-file" && effectiveStateJson) {
|
|
95
|
+
throw new Error("workflow start --effective-state-json is only valid for a non-local assignment provider");
|
|
96
|
+
}
|
|
97
|
+
if (workItem?.startsWith("local:")) {
|
|
73
98
|
const localSlug = workItem.slice("local:".length);
|
|
74
99
|
if (!taskSlug || taskSlug !== localSlug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(localSlug)) {
|
|
75
100
|
throw new Error("local Work Item retries require the exact existing --task-slug binding");
|
|
@@ -80,28 +105,57 @@ async function start(argv) {
|
|
|
80
105
|
throw new Error("local Work Item retry does not match the existing session binding");
|
|
81
106
|
}
|
|
82
107
|
}
|
|
83
|
-
else if (taskSlug) {
|
|
108
|
+
else if (taskSlug && flow !== "builder.shape") {
|
|
84
109
|
throw new Error("--task-slug is reserved for an existing local Work Item retry");
|
|
85
110
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
111
|
+
const existingSlug = taskSlug ?? (workItem ? workItemSlug(workItem) : null);
|
|
112
|
+
if (existingSlug && fs.existsSync(path.join(artifactRoot, existingSlug, "state.json"))) {
|
|
113
|
+
try {
|
|
114
|
+
const existing = await loadBuilderFlowRun({ cwd: path.dirname(path.dirname(artifactRoot)), runId: existingSlug });
|
|
115
|
+
if (existing.definitionId !== flow) {
|
|
116
|
+
throw new Error(`workflow start cannot resume ${existing.definitionId} as ${flow}; local shape sessions are not build retries. Create or claim a provider Work Item for the builder.build handoff.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.code !== "flow.run_location.not_found")
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (flow === "builder.build" && workItem && !workItem.startsWith("local:")) {
|
|
125
|
+
const slug = workItemSlug(workItem);
|
|
126
|
+
const report = path.join(artifactRoot, slug, `${slug}--pull-work.md`);
|
|
127
|
+
try {
|
|
128
|
+
const stat = fs.lstatSync(report);
|
|
129
|
+
if (stat.isSymbolicLink() || !stat.isFile() || !fs.readFileSync(report, "utf8").includes(workItem)) {
|
|
130
|
+
throw new Error("invalid");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new Error(`workflow start requires concrete pull-work selection evidence at ${report} naming ${workItem} before it can produce selected-work`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const sourceRequest = flagString(parsed.flags, "source-request", `Start ${flow} for ${workItem ?? `local:${taskSlug}`}`);
|
|
138
|
+
const summary = flagString(parsed.flags, "summary", `Deliver ${workItem ?? `local:${taskSlug}`} through ${flow}.`);
|
|
88
139
|
const forwarded = keepFlags(argv, new Set(["title", "criterion"]));
|
|
89
|
-
return
|
|
140
|
+
return mainFromPublicWorkflow([
|
|
90
141
|
"ensure-session",
|
|
91
142
|
"--artifact-root", artifactRoot,
|
|
92
143
|
"--flow-id", flow,
|
|
93
|
-
"--work-item", workItem,
|
|
144
|
+
...(workItem ? ["--work-item", workItem] : []),
|
|
94
145
|
...(taskSlug ? ["--task-slug", taskSlug] : []),
|
|
146
|
+
...(assignmentProvider ? ["--assignment-provider", assignmentProvider] : []),
|
|
147
|
+
...(effectiveStateJson ? ["--effective-state-json", path.resolve(effectiveStateJson)] : []),
|
|
95
148
|
"--source-request", sourceRequest,
|
|
96
149
|
"--summary", summary,
|
|
97
150
|
...forwarded,
|
|
98
151
|
]);
|
|
99
152
|
}
|
|
153
|
+
function workItemSlug(workItem) {
|
|
154
|
+
return workItem.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
155
|
+
}
|
|
100
156
|
async function status(sessionDir, json) {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const projectRoot = path.dirname(path.dirname(path.dirname(sessionDir)));
|
|
104
|
-
const result = await loadBuilderBuildRun({ cwd: projectRoot, runId: slug });
|
|
157
|
+
const inspected = await inspectBuilderFlowSession({ sessionDir });
|
|
158
|
+
const result = inspected.run;
|
|
105
159
|
const report = {
|
|
106
160
|
run_id: result.runId,
|
|
107
161
|
definition_id: result.definitionId,
|
|
@@ -109,7 +163,7 @@ async function status(sessionDir, json) {
|
|
|
109
163
|
status: result.state.status,
|
|
110
164
|
current_step: result.state.current_step,
|
|
111
165
|
session_dir: sessionDir,
|
|
112
|
-
next_action:
|
|
166
|
+
next_action: inspected.projection.next_action ?? null,
|
|
113
167
|
};
|
|
114
168
|
if (json)
|
|
115
169
|
console.log(JSON.stringify(report));
|
|
@@ -123,7 +177,7 @@ async function status(sessionDir, json) {
|
|
|
123
177
|
}
|
|
124
178
|
async function evidence(sessionDir, argv, json) {
|
|
125
179
|
const parsed = parseArgs(argv);
|
|
126
|
-
assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "expectation", "status", "summary", "evidence-ref-json", "accepted-gap-reason", "waived-by"]), "workflow evidence");
|
|
180
|
+
assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "expectation", "status", "summary", "route-reason", "evidence-ref-json", "criterion-json", "accepted-gap-reason", "waived-by", "command"]), "workflow evidence");
|
|
127
181
|
if (!flagString(parsed.flags, "expectation"))
|
|
128
182
|
throw new Error("workflow evidence requires --expectation <gate-expectation-id>");
|
|
129
183
|
if (!flagString(parsed.flags, "status"))
|
|
@@ -131,23 +185,111 @@ async function evidence(sessionDir, argv, json) {
|
|
|
131
185
|
if (!flagString(parsed.flags, "summary"))
|
|
132
186
|
throw new Error("workflow evidence requires --summary <text>");
|
|
133
187
|
const forwarded = stripPublicFlags(argv, new Set(["artifact-root", "session-dir", "json"]));
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
const { slug, projectRoot } = readBoundSession(sessionDir);
|
|
189
|
+
const commands = flagList(parsed.flags, "command");
|
|
190
|
+
if (Object.hasOwn(parsed.flags, "command") && commands.length === 0) {
|
|
191
|
+
throw new Error("workflow evidence --command requires a shell command value");
|
|
192
|
+
}
|
|
193
|
+
const requiresTestEvidence = flagString(parsed.flags, "expectation") === "tests-evidence" && flagString(parsed.flags, "status") === "pass";
|
|
194
|
+
// Argument and command-shape rejection must be read-only. Recovery below may
|
|
195
|
+
// repair stale projections, so it runs only after every command is accepted.
|
|
196
|
+
assertRunnableEvidenceCommands(commands, projectRoot, requiresTestEvidence);
|
|
197
|
+
const report = await withSubjectLock(path.dirname(sessionDir), slug, async () => {
|
|
198
|
+
// Validate the owner after the lock is held, then keep the lock through command
|
|
199
|
+
// execution, evidence recording, and postcondition capture so assignment and
|
|
200
|
+
// session state cannot change mid-invocation.
|
|
201
|
+
const caller = assertMatchingAssignmentActor(sessionDir, slug);
|
|
202
|
+
const repaired = await recoverBuilderFlowSession({ sessionDir });
|
|
203
|
+
const beforeEvidence = manifestEvidenceIdentity(repaired.run.manifest);
|
|
204
|
+
await mainFromPublicWorkflow([
|
|
205
|
+
"record-gate-claim",
|
|
206
|
+
sessionDir,
|
|
207
|
+
...forwarded,
|
|
208
|
+
"--actor",
|
|
209
|
+
caller.actorKey,
|
|
210
|
+
]);
|
|
211
|
+
const synchronized = await syncBuilderFlowSession({ sessionDir });
|
|
212
|
+
const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
|
|
213
|
+
const run = await loadBuilderFlowRun({ cwd: repaired.projectRoot, runId: slug });
|
|
214
|
+
const afterEvidence = manifestEvidenceIdentity(run.manifest);
|
|
215
|
+
const beforeIds = new Set(beforeEvidence.map((entry) => entry.id));
|
|
216
|
+
const newEvidence = afterEvidence.filter((entry) => !beforeIds.has(entry.id));
|
|
217
|
+
if (synchronized.attached && (newEvidence.length !== 1 || newEvidence[0]?.sha256 !== digest)) {
|
|
218
|
+
throw new Error("workflow evidence did not attach exactly this invocation's resulting trust.bundle digest");
|
|
219
|
+
}
|
|
220
|
+
if (!synchronized.attached && newEvidence.length !== 0) {
|
|
221
|
+
throw new Error("workflow evidence changed the canonical manifest while synchronization reported no attachment");
|
|
222
|
+
}
|
|
223
|
+
const updatedSidecar = readBoundSession(sessionDir).sidecar;
|
|
224
|
+
return immutableReport({
|
|
225
|
+
run_id: run.runId,
|
|
226
|
+
status: run.state.status,
|
|
227
|
+
current_step: run.state.current_step,
|
|
228
|
+
attached: synchronized.attached,
|
|
229
|
+
awaiting_evidence: !synchronized.attached,
|
|
230
|
+
next_action: updatedSidecar.next_action ?? null,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
145
233
|
if (json)
|
|
146
234
|
console.log(JSON.stringify(report));
|
|
147
235
|
else
|
|
148
|
-
console.log(
|
|
236
|
+
console.log(report.attached
|
|
237
|
+
? `Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`
|
|
238
|
+
: `Recorded evidence; canonical run is awaiting the remaining gate expectations at ${report.current_step}.`);
|
|
149
239
|
return 0;
|
|
150
240
|
}
|
|
241
|
+
async function critique(sessionDir, argv, json) {
|
|
242
|
+
const parsed = parseArgs(argv);
|
|
243
|
+
assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "id", "verdict", "summary", "artifact-ref", "finding-json", "lane-json", "timestamp"]), "workflow critique");
|
|
244
|
+
if (!flagString(parsed.flags, "summary"))
|
|
245
|
+
throw new Error("workflow critique requires --summary <text>");
|
|
246
|
+
if (Object.hasOwn(parsed.flags, "reviewer"))
|
|
247
|
+
throw new Error("workflow critique derives reviewer identity from the authenticated assignment actor; --reviewer is not accepted");
|
|
248
|
+
const { slug, projectRoot } = readBoundSession(sessionDir);
|
|
249
|
+
const forwarded = stripPublicFlags(argv, new Set(["artifact-root", "session-dir", "json"]));
|
|
250
|
+
const report = await withSubjectLock(path.dirname(sessionDir), slug, async () => {
|
|
251
|
+
const caller = assertDistinctReviewActor(sessionDir, slug);
|
|
252
|
+
const current = await loadBuilderFlowRun({ cwd: projectRoot, runId: slug });
|
|
253
|
+
if (current.definitionId !== "builder.build" || current.state.current_step !== "verify")
|
|
254
|
+
throw new Error("workflow critique is allowed only for the canonical builder.build verify step");
|
|
255
|
+
const beforeManifest = JSON.parse(JSON.stringify(current.manifest));
|
|
256
|
+
const beforeTrustBundle = optionalFileDigest(path.join(sessionDir, "trust.bundle"));
|
|
257
|
+
const legacySidecars = ["critique.json", "evidence.json"].map((name) => ({ name, digest: optionalFileDigest(path.join(sessionDir, name)) }));
|
|
258
|
+
await mainFromPublicWorkflow(["record-critique", sessionDir, ...forwarded, "--reviewer", caller.actorKey]);
|
|
259
|
+
const result = await recoverBuilderFlowSession({ sessionDir });
|
|
260
|
+
const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
|
|
261
|
+
if (!isDeepStrictEqual(result.run.manifest, beforeManifest)) {
|
|
262
|
+
throw new Error("workflow critique must not attach or otherwise mutate the Flow manifest");
|
|
263
|
+
}
|
|
264
|
+
if (beforeTrustBundle === digest) {
|
|
265
|
+
throw new Error("workflow critique did not change trust.bundle");
|
|
266
|
+
}
|
|
267
|
+
if (legacySidecars.some(({ name, digest: legacyDigest }) => optionalFileDigest(path.join(sessionDir, name)) !== legacyDigest)) {
|
|
268
|
+
throw new Error("workflow critique must persist only through trust.bundle");
|
|
269
|
+
}
|
|
270
|
+
return immutableReport({ run_id: slug, recorded: true });
|
|
271
|
+
});
|
|
272
|
+
if (json)
|
|
273
|
+
console.log(JSON.stringify(report));
|
|
274
|
+
else
|
|
275
|
+
console.log("Recorded critique in the trust bundle.");
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
function assertRunnableEvidenceCommands(commands, projectRoot, requiresTestEvidence) {
|
|
279
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../scripts/hooks/lib/runnable-command.js");
|
|
280
|
+
const { isRunnableCommandText } = REQUIRE(helperPath);
|
|
281
|
+
if (commands.length > 1 && new Set(commands).size !== commands.length) {
|
|
282
|
+
throw new Error("workflow evidence --command values must be unique because observations are matched by exact command text");
|
|
283
|
+
}
|
|
284
|
+
for (const command of commands) {
|
|
285
|
+
if (!isRunnableCommandText(command)) {
|
|
286
|
+
throw new Error(`workflow evidence --command ${JSON.stringify(command)} is not a runnable shell command — prose belongs in --summary, which is never executed.`);
|
|
287
|
+
}
|
|
288
|
+
if (requiresTestEvidence && !isMeaningfulTestCommand(command, projectRoot)) {
|
|
289
|
+
throw new Error("workflow evidence tests-evidence command must resolve through a non-vacuous package script or a known test/check/verify/eval runner or project-local test path; shell wrappers, no-ops, version/help commands, and arbitrary node -e commands are not evidence");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
151
293
|
function resolveSessionDir(flags) {
|
|
152
294
|
const explicit = flagString(flags, "session-dir");
|
|
153
295
|
if (explicit)
|
|
@@ -170,11 +312,16 @@ function doctor(argv) {
|
|
|
170
312
|
const install = readOptionalJson(installFile);
|
|
171
313
|
const state = readCurrentState(artifactRoot);
|
|
172
314
|
const packageKit = readOptionalJson(path.join(packageRoot, "kits", "builder", "kit.json"));
|
|
173
|
-
const packageFlow = readOptionalJson(path.join(packageRoot, "kits", "builder", "flows", "build.flow.json"));
|
|
174
315
|
const installedKitFile = path.join(projectRoot, "kits", "builder", "kit.json");
|
|
175
|
-
const installedFlowFile = path.join(projectRoot, "kits", "builder", "flows", "build.flow.json");
|
|
176
316
|
const installedKit = readOptionalJson(installedKitFile);
|
|
177
|
-
const
|
|
317
|
+
const definitions = [
|
|
318
|
+
{ id: "builder.build", file: "build.flow.json" },
|
|
319
|
+
{ id: "builder.shape", file: "shape.flow.json" },
|
|
320
|
+
].map(({ id, file }) => {
|
|
321
|
+
const packageFile = path.join(packageRoot, "kits", "builder", "flows", file);
|
|
322
|
+
const installedFile = path.join(projectRoot, "kits", "builder", "flows", file);
|
|
323
|
+
return { id, packageFile, installedFile, packageDefinition: readOptionalJson(packageFile), installedDefinition: readOptionalJson(installedFile) };
|
|
324
|
+
});
|
|
178
325
|
const resolvedFlowPackage = readOptionalJson(resolveDependencyPackageJson("@kontourai/flow"));
|
|
179
326
|
const installedVersion = typeof install?.version === "string" ? install.version : null;
|
|
180
327
|
const staleInstall = installedVersion !== null && installedVersion !== cliVersion;
|
|
@@ -195,19 +342,39 @@ function doctor(argv) {
|
|
|
195
342
|
warnings.push(`Repository-local Flow Agents ${String(localDependency.version)} differs from executing CLI ${cliVersion}; keep automation explicitly versioned.`);
|
|
196
343
|
if (activeKitIds.includes("builder") && !installedKit)
|
|
197
344
|
warnings.push(`Activated Builder Kit is missing at ${installedKitFile}. Run: ${remediation}`);
|
|
198
|
-
|
|
199
|
-
|
|
345
|
+
for (const definition of definitions) {
|
|
346
|
+
if (activeKitIds.includes("builder") && !definition.installedDefinition) {
|
|
347
|
+
warnings.push(`Activated ${definition.id} definition is missing at ${definition.installedFile}. Run: ${remediation}`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (!definition.installedDefinition)
|
|
351
|
+
continue;
|
|
352
|
+
const validationError = validateFlowDefinition(definition.id, definition.installedDefinition);
|
|
353
|
+
if (validationError)
|
|
354
|
+
warnings.push(`Installed ${definition.id} definition is invalid: ${validationError}. Run: ${remediation}`);
|
|
355
|
+
if (definition.installedDefinition.id !== definition.packageDefinition?.id) {
|
|
356
|
+
warnings.push(`Installed ${definition.id} definition id ${String(definition.installedDefinition.id)} differs from CLI definition ${String(definition.packageDefinition?.id)}. Run: ${remediation}`);
|
|
357
|
+
}
|
|
358
|
+
if (definition.installedDefinition.version !== definition.packageDefinition?.version) {
|
|
359
|
+
warnings.push(`Installed ${definition.id} version ${String(definition.installedDefinition.version)} differs from CLI version ${String(definition.packageDefinition?.version)}. Run: ${remediation}`);
|
|
360
|
+
}
|
|
361
|
+
else if (fileSha256(definition.installedFile) !== fileSha256(definition.packageFile)) {
|
|
362
|
+
warnings.push(`Installed ${definition.id} content differs from Flow Agents ${cliVersion}. Run: ${remediation}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
200
365
|
if (installedKit && installedKit.schema_version !== packageKit?.schema_version) {
|
|
201
366
|
warnings.push(`Installed Builder Kit schema ${String(installedKit.schema_version)} differs from CLI schema ${String(packageKit?.schema_version)}. Run: ${remediation}`);
|
|
202
367
|
}
|
|
203
368
|
if (installedKit && fileSha256(installedKitFile) !== fileSha256(path.join(packageRoot, "kits", "builder", "kit.json"))) {
|
|
204
369
|
warnings.push(`Installed Builder Kit content differs from Flow Agents ${cliVersion}. Run: ${remediation}`);
|
|
205
370
|
}
|
|
206
|
-
|
|
207
|
-
|
|
371
|
+
const currentRun = state?.flow_run && typeof state.flow_run === "object" ? state.flow_run : null;
|
|
372
|
+
const currentDefinition = definitions.find((definition) => definition.id === currentRun?.definition_id);
|
|
373
|
+
if (currentRun?.definition_id && !currentDefinition) {
|
|
374
|
+
warnings.push(`Current run uses unsupported Flow definition ${String(currentRun.definition_id)}; recover or migrate before mutation.`);
|
|
208
375
|
}
|
|
209
|
-
if (
|
|
210
|
-
warnings.push(`Current run uses
|
|
376
|
+
else if (currentRun && currentDefinition && currentRun.definition_version !== currentDefinition.packageDefinition?.version) {
|
|
377
|
+
warnings.push(`Current run uses ${currentDefinition.id}@${String(currentRun.definition_version)} while CLI resolves ${currentDefinition.id}@${String(currentDefinition.packageDefinition?.version)}; recover or migrate before mutation.`);
|
|
211
378
|
}
|
|
212
379
|
if (state && state.schema_version !== "1.0")
|
|
213
380
|
warnings.push(`Artifact schema ${String(state.schema_version)} is unsupported; recreate or migrate the session with CLI ${cliVersion}.`);
|
|
@@ -245,7 +412,13 @@ function doctor(argv) {
|
|
|
245
412
|
source: installedKit ? installedKitFile : path.join(packageRoot, "kits", "builder", "kit.json"),
|
|
246
413
|
},
|
|
247
414
|
flow_runtime: { package_version: resolvedFlowVersion, expected_range: expectedFlowRange, compatible: resolvedFlowVersion ? flowVersionCompatible(resolvedFlowVersion, expectedFlowRange) : false },
|
|
248
|
-
definition: { id:
|
|
415
|
+
definition: { id: currentRun?.definition_id ?? definitions[0].packageDefinition?.id ?? null, version: currentRun?.definition_version ?? definitions[0].packageDefinition?.version ?? null },
|
|
416
|
+
definitions: definitions.map((definition) => ({
|
|
417
|
+
id: definition.id,
|
|
418
|
+
resolved_version: definition.packageDefinition?.version ?? null,
|
|
419
|
+
installed_version: definition.installedDefinition?.version ?? null,
|
|
420
|
+
installed_valid: definition.installedDefinition ? validateFlowDefinition(definition.id, definition.installedDefinition) === null : false,
|
|
421
|
+
})),
|
|
249
422
|
artifact: {
|
|
250
423
|
state_schema_version: state?.schema_version ?? null,
|
|
251
424
|
trust_bundle_schema_version: trustBundleSchema,
|
|
@@ -309,6 +482,91 @@ function readOptionalJson(file) {
|
|
|
309
482
|
throw error;
|
|
310
483
|
}
|
|
311
484
|
}
|
|
485
|
+
function validateFlowDefinition(expectedId, definition) {
|
|
486
|
+
if (definition.id !== expectedId)
|
|
487
|
+
return `expected id ${expectedId}, received ${String(definition.id)}`;
|
|
488
|
+
try {
|
|
489
|
+
validateDefinition(definition);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
return error instanceof Error ? error.message : String(error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function isSafeSlug(value) {
|
|
497
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
498
|
+
}
|
|
499
|
+
function readBoundSession(sessionDir) {
|
|
500
|
+
const slug = path.basename(sessionDir);
|
|
501
|
+
if (!isSafeSlug(slug))
|
|
502
|
+
throw new Error("workflow session basename must be a safe task slug");
|
|
503
|
+
const sidecar = readJsonFile(path.join(sessionDir, "state.json"), "workflow state");
|
|
504
|
+
if (sidecar.task_slug !== slug)
|
|
505
|
+
throw new Error("workflow state task_slug must exactly match the safe session basename");
|
|
506
|
+
return { sidecar, slug, projectRoot: path.dirname(path.dirname(path.dirname(sessionDir))) };
|
|
507
|
+
}
|
|
508
|
+
function readAssignment(sessionDir, slug) {
|
|
509
|
+
const artifactRoot = path.dirname(sessionDir);
|
|
510
|
+
const assignmentRoot = path.join(artifactRoot, "assignment");
|
|
511
|
+
const stat = fs.lstatSync(assignmentRoot);
|
|
512
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
513
|
+
throw new Error("workflow assignment directory must be a non-symlink directory");
|
|
514
|
+
const file = path.join(assignmentRoot, `${slug}.json`);
|
|
515
|
+
const fileStat = fs.lstatSync(file);
|
|
516
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile())
|
|
517
|
+
throw new Error("workflow assignment must be a non-symlink regular file");
|
|
518
|
+
return readJsonFile(file, "workflow assignment");
|
|
519
|
+
}
|
|
520
|
+
function assertMatchingAssignmentActor(sessionDir, slug) {
|
|
521
|
+
const assignment = readActiveAssignment(sessionDir, slug);
|
|
522
|
+
const caller = resolveCurrentAssignmentActor();
|
|
523
|
+
const normalizeActor = (value) => value && typeof value === "object" ? { ...value, human: value.human ?? null } : null;
|
|
524
|
+
if (assignment.actor_key !== caller.actorKey || !isDeepStrictEqual(normalizeActor(assignment.actor), normalizeActor(caller.actor))) {
|
|
525
|
+
throw new Error("workflow mutation requires the session's active, matching assignment actor");
|
|
526
|
+
}
|
|
527
|
+
return caller;
|
|
528
|
+
}
|
|
529
|
+
function readActiveAssignment(sessionDir, slug) {
|
|
530
|
+
const assignment = readAssignment(sessionDir, slug);
|
|
531
|
+
if (assignment.status !== "claimed" || assignment.artifact_dir !== slug || typeof assignment.actor_key !== "string" || !assignment.actor_key || !assignment.actor || typeof assignment.actor !== "object" || Array.isArray(assignment.actor)) {
|
|
532
|
+
throw new Error("workflow mutation requires the session's active implementation assignment");
|
|
533
|
+
}
|
|
534
|
+
return assignment;
|
|
535
|
+
}
|
|
536
|
+
function assertDistinctReviewActor(sessionDir, slug) {
|
|
537
|
+
const assignment = readActiveAssignment(sessionDir, slug);
|
|
538
|
+
const caller = resolveCurrentAssignmentActor();
|
|
539
|
+
if (assignment.actor_key === caller.actorKey) {
|
|
540
|
+
throw new Error("workflow critique requires a reviewer identity distinct from the active implementation assignment actor");
|
|
541
|
+
}
|
|
542
|
+
return caller;
|
|
543
|
+
}
|
|
544
|
+
function immutableReport(value) {
|
|
545
|
+
if (!value || typeof value !== "object")
|
|
546
|
+
return value;
|
|
547
|
+
for (const nested of Object.values(value))
|
|
548
|
+
immutableReport(nested);
|
|
549
|
+
return Object.freeze(value);
|
|
550
|
+
}
|
|
551
|
+
function manifestEvidenceIdentity(manifest) {
|
|
552
|
+
const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
|
|
553
|
+
return evidence
|
|
554
|
+
.flatMap((entry) => entry && typeof entry === "object"
|
|
555
|
+
&& typeof entry.id === "string"
|
|
556
|
+
&& typeof entry.sha256 === "string"
|
|
557
|
+
? [{ id: String(entry.id), sha256: String(entry.sha256) }]
|
|
558
|
+
: []);
|
|
559
|
+
}
|
|
560
|
+
function optionalFileDigest(file) {
|
|
561
|
+
try {
|
|
562
|
+
return fileSha256(file);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (error.code === "ENOENT")
|
|
566
|
+
return null;
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
312
570
|
function stripPublicFlags(argv, removed) {
|
|
313
571
|
const result = [];
|
|
314
572
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -28,6 +28,19 @@ export interface KitFlowStepActionEntry {
|
|
|
28
28
|
step_id: string;
|
|
29
29
|
skills: string[];
|
|
30
30
|
operations: string[];
|
|
31
|
+
/** Gate expectations produced by this action's explicit operation(s). */
|
|
32
|
+
expectation_ids?: string[];
|
|
33
|
+
/** Durable artifacts produced by an operation-only action. */
|
|
34
|
+
artifacts: string[];
|
|
35
|
+
}
|
|
36
|
+
export type KitSkillRole = "entrypoint" | "profile" | "step" | "shared-primitive" | "extension";
|
|
37
|
+
export interface KitSkillRoleEntry {
|
|
38
|
+
skill_id: string;
|
|
39
|
+
role: KitSkillRole;
|
|
40
|
+
flow_id?: string;
|
|
41
|
+
step_ids: string[];
|
|
42
|
+
artifacts: string[];
|
|
43
|
+
expectation_ids: string[];
|
|
31
44
|
}
|
|
32
45
|
/**
|
|
33
46
|
* Parse and shape-validate a kit manifest's `dependencies` field (Flow Agents
|
|
@@ -60,6 +73,10 @@ export declare function parseKitFlowStepActions(manifest: Record<string, unknown
|
|
|
60
73
|
entries: KitFlowStepActionEntry[];
|
|
61
74
|
errors: string[];
|
|
62
75
|
};
|
|
76
|
+
export declare function parseKitSkillRoles(manifest: Record<string, unknown>, manifestPath: string): {
|
|
77
|
+
entries: KitSkillRoleEntry[];
|
|
78
|
+
errors: string[];
|
|
79
|
+
};
|
|
63
80
|
export type KitTargetConsumer = "flow" | "flow-agents" | string;
|
|
64
81
|
export interface KitConformanceLevel {
|
|
65
82
|
/** K0: valid core Flow Kit container with at least one flow (gates evaluable agentlessly). */
|