akm-cli 0.9.0-rc.0 → 0.9.0-rc.1
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 +123 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/config/config-schema.js +21 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/indexer/db/db.js +2 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builders.js +7 -5
- package/dist/integrations/agent/model-aliases.js +9 -0
- package/dist/integrations/agent/profiles.js +72 -5
- package/dist/integrations/agent/runner-dispatch.js +25 -1
- package/dist/integrations/agent/spawn.js +137 -14
- package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
- package/dist/integrations/harnesses/aider/index.js +58 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
- package/dist/integrations/harnesses/amazonq/index.js +59 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +45 -6
- package/dist/integrations/harnesses/claude/index.js +25 -23
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
- package/dist/integrations/harnesses/codex/index.js +63 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +60 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
- package/dist/integrations/harnesses/gemini/index.js +60 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +26 -4
- package/dist/integrations/harnesses/opencode/index.js +15 -16
- package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +581 -91
- package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
- package/dist/integrations/harnesses/openhands/index.js +58 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
- package/dist/integrations/harnesses/pi/index.js +58 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +7 -0
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/passthrough.js +4 -0
- package/dist/output/text/helpers.js +212 -1
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +14225 -0
- package/dist/schemas/akm-workflow.json +328 -0
- package/dist/scripts/migrate-storage.js +1034 -6973
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +768 -10
- package/dist/storage/repositories/workflow-runs-repository.js +189 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/db.js +135 -0
- package/dist/workflows/exec/brief.js +484 -0
- package/dist/workflows/exec/native-executor.js +975 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1295 -0
- package/dist/workflows/exec/run-workflow.js +596 -0
- package/dist/workflows/exec/scheduler.js +100 -0
- package/dist/workflows/exec/step-work.js +1156 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +388 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +33 -0
- package/dist/workflows/ir/schema.js +4 -0
- package/dist/workflows/parser.js +3 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +760 -0
- package/dist/workflows/program/project.js +105 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/runs.js +206 -36
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +64 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +1 -1
- package/docs/data-and-telemetry.md +2 -1
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `looksLikeWorkflowRunId` and `resolveWorkflowFilePath` move with the family.
|
|
13
13
|
*/
|
|
14
14
|
import { defineCommand } from "citty";
|
|
15
|
+
import { getStringArg } from "../cli/parse-args.js";
|
|
15
16
|
import { defineJsonCommand, output, runWithJsonErrors } from "../cli/shared.js";
|
|
16
17
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
|
|
17
18
|
import { parseAssetRef } from "../core/asset/asset-ref.js";
|
|
@@ -22,8 +23,9 @@ import { resolveSourceEntries } from "../indexer/search/search-source.js";
|
|
|
22
23
|
import { hasBooleanFlag } from "../output/context.js";
|
|
23
24
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
24
25
|
import { resolveAssetPath } from "../sources/resolve.js";
|
|
25
|
-
import { createWorkflowAsset, formatWorkflowErrors, getWorkflowTemplate, validateWorkflowSource, } from "../workflows/authoring/authoring.js";
|
|
26
|
+
import { createWorkflowAsset, formatWorkflowErrors, getWorkflowProgramTemplate, getWorkflowTemplate, validateWorkflowProgramSource, validateWorkflowSource, } from "../workflows/authoring/authoring.js";
|
|
26
27
|
import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "../workflows/cli.js";
|
|
28
|
+
import { isWorkflowProgramPath } from "../workflows/program/project.js";
|
|
27
29
|
import { abandonWorkflowRun, completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
28
30
|
const workflowStartCommand = defineJsonCommand({
|
|
29
31
|
meta: {
|
|
@@ -140,9 +142,16 @@ const workflowStatusCommand = defineJsonCommand({
|
|
|
140
142
|
},
|
|
141
143
|
args: {
|
|
142
144
|
target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
|
|
145
|
+
units: {
|
|
146
|
+
type: "boolean",
|
|
147
|
+
description: "Also list per-unit rows from the run journal (unit id, status, failure_reason, and any result/error " +
|
|
148
|
+
"diagnostic text). Diagnostics only — step evidence stays deterministic and is unaffected (#22).",
|
|
149
|
+
default: false,
|
|
150
|
+
},
|
|
143
151
|
},
|
|
144
152
|
async run({ args }) {
|
|
145
153
|
const target = args.target;
|
|
154
|
+
const includeUnits = args.units === true;
|
|
146
155
|
// Check if target looks like a workflow ref
|
|
147
156
|
const parsed = (() => {
|
|
148
157
|
try {
|
|
@@ -161,11 +170,11 @@ const workflowStatusCommand = defineJsonCommand({
|
|
|
161
170
|
const mostRecent = runs[0];
|
|
162
171
|
if (!mostRecent)
|
|
163
172
|
throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
|
|
164
|
-
const result = await getWorkflowStatus(mostRecent.id);
|
|
173
|
+
const result = await getWorkflowStatus(mostRecent.id, { includeUnits });
|
|
165
174
|
output("workflow-status", result);
|
|
166
175
|
}
|
|
167
176
|
else {
|
|
168
|
-
const result = await getWorkflowStatus(target);
|
|
177
|
+
const result = await getWorkflowStatus(target, { includeUnits });
|
|
169
178
|
output("workflow-status", result);
|
|
170
179
|
}
|
|
171
180
|
},
|
|
@@ -187,19 +196,22 @@ const workflowListCommand = defineJsonCommand({
|
|
|
187
196
|
const workflowCreateCommand = defineJsonCommand({
|
|
188
197
|
meta: {
|
|
189
198
|
name: "create",
|
|
190
|
-
description: "Create a workflow markdown document
|
|
199
|
+
description: "Create a workflow in the working stash (markdown document by default; a .yaml/.yml name writes a YAML program)",
|
|
191
200
|
},
|
|
192
201
|
args: {
|
|
193
202
|
name: {
|
|
194
203
|
type: "positional",
|
|
195
|
-
description: "Workflow name (flat, no '/'; use --path for a subdirectory)",
|
|
204
|
+
description: "Workflow name (flat, no '/'; use --path for a subdirectory). A .yaml/.yml suffix creates a YAML program.",
|
|
196
205
|
required: true,
|
|
197
206
|
},
|
|
198
207
|
path: {
|
|
199
208
|
type: "string",
|
|
200
209
|
description: "Relative subdirectory under workflows/ to place the workflow in (e.g. 'release'). The filename comes from the name.",
|
|
201
210
|
},
|
|
202
|
-
from: {
|
|
211
|
+
from: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "Import and validate content from an existing file (parsed per the destination extension)",
|
|
214
|
+
},
|
|
203
215
|
force: {
|
|
204
216
|
type: "boolean",
|
|
205
217
|
description: "Overwrite an existing workflow (requires --from or --reset)",
|
|
@@ -237,26 +249,54 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
237
249
|
const workflowTemplateCommand = defineCommand({
|
|
238
250
|
meta: {
|
|
239
251
|
name: "template",
|
|
240
|
-
description: "Print a valid workflow markdown
|
|
252
|
+
description: "Print a valid workflow template (markdown by default, --yaml for a YAML program)",
|
|
253
|
+
},
|
|
254
|
+
args: {
|
|
255
|
+
yaml: {
|
|
256
|
+
type: "boolean",
|
|
257
|
+
description: "Print a minimal valid YAML workflow program instead of the markdown template",
|
|
258
|
+
default: false,
|
|
259
|
+
},
|
|
241
260
|
},
|
|
242
|
-
run() {
|
|
243
|
-
process.stdout.write(getWorkflowTemplate());
|
|
261
|
+
run({ args }) {
|
|
262
|
+
process.stdout.write(args.yaml ? getWorkflowProgramTemplate() : getWorkflowTemplate());
|
|
244
263
|
},
|
|
245
264
|
});
|
|
246
265
|
const workflowValidateCommand = defineJsonCommand({
|
|
247
266
|
meta: {
|
|
248
267
|
name: "validate",
|
|
249
|
-
description: "Validate a workflow
|
|
268
|
+
description: "Validate a workflow file or ref (markdown document or YAML program) and print any errors",
|
|
250
269
|
},
|
|
251
270
|
args: {
|
|
252
271
|
target: {
|
|
253
272
|
type: "positional",
|
|
254
|
-
description: "Workflow ref (workflow:<name>) or filesystem path to a workflow .md",
|
|
273
|
+
description: "Workflow ref (workflow:<name>) or filesystem path to a workflow .md/.yaml",
|
|
255
274
|
required: true,
|
|
256
275
|
},
|
|
257
276
|
},
|
|
258
277
|
async run({ args }) {
|
|
259
278
|
const filePath = await resolveWorkflowFilePath(args.target);
|
|
279
|
+
// YAML programs (redesign addendum, R1) validate through the program
|
|
280
|
+
// parser AND compiler so expression/reference errors surface at lint
|
|
281
|
+
// time; both error lists carry line numbers. Markdown is unchanged.
|
|
282
|
+
if (isWorkflowProgramPath(filePath)) {
|
|
283
|
+
const { result } = validateWorkflowProgramSource(filePath);
|
|
284
|
+
if (!result.ok) {
|
|
285
|
+
throw new UsageError(formatWorkflowErrors(filePath, result.errors));
|
|
286
|
+
}
|
|
287
|
+
// Non-fatal WARNINGS ride the envelope additively — `ok` stays true. The
|
|
288
|
+
// text formatter renders them clearly marked for humans; the JSON key is
|
|
289
|
+
// the machine channel. Empty array when the program is fully typed/declared.
|
|
290
|
+
output("workflow-validate", {
|
|
291
|
+
ok: true,
|
|
292
|
+
path: filePath,
|
|
293
|
+
format: "program",
|
|
294
|
+
title: result.program.name,
|
|
295
|
+
stepCount: result.program.steps.length,
|
|
296
|
+
warnings: result.warnings.map((w) => ({ line: w.line, message: w.message })),
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
260
300
|
const { parse } = validateWorkflowSource(filePath);
|
|
261
301
|
if (parse.ok) {
|
|
262
302
|
output("workflow-validate", {
|
|
@@ -271,7 +311,15 @@ const workflowValidateCommand = defineJsonCommand({
|
|
|
271
311
|
},
|
|
272
312
|
});
|
|
273
313
|
async function resolveWorkflowFilePath(target) {
|
|
274
|
-
|
|
314
|
+
// A bare (`workflow:<name>`) OR origin-qualified (`<origin>//workflow:<name>`)
|
|
315
|
+
// ref resolves through the source search, exactly like `workflow start` /
|
|
316
|
+
// `status` / `next`. Anything else is treated as a filesystem path. Detecting
|
|
317
|
+
// the origin-qualified form here (not just the bare prefix) keeps `validate`'s
|
|
318
|
+
// ref contract in lockstep with the rest of the workflow command family — an
|
|
319
|
+
// `extra//workflow:foo` ref validates the file that `extra//workflow:foo`
|
|
320
|
+
// starts, rather than being mistaken for a relative path that does not exist.
|
|
321
|
+
const looksLikeWorkflowRef = target.startsWith("workflow:") || target.includes("//workflow:");
|
|
322
|
+
if (!looksLikeWorkflowRef)
|
|
275
323
|
return target;
|
|
276
324
|
const parsed = parseAssetRef(target);
|
|
277
325
|
if (parsed.type !== "workflow") {
|
|
@@ -290,6 +338,218 @@ async function resolveWorkflowFilePath(target) {
|
|
|
290
338
|
}
|
|
291
339
|
throw new UsageError(`Workflow not found for ref: workflow:${parsed.name}`);
|
|
292
340
|
}
|
|
341
|
+
const workflowRunCommand = defineJsonCommand({
|
|
342
|
+
meta: {
|
|
343
|
+
name: "run",
|
|
344
|
+
description: "EXPERIMENTAL: execute a workflow's steps with the native engine — akm dispatches each step's units " +
|
|
345
|
+
"(fan-out, schema output) to the configured runner and advances the run through the normal completion gates",
|
|
346
|
+
},
|
|
347
|
+
args: {
|
|
348
|
+
target: { type: "positional", description: "Workflow run id or workflow ref (auto-starts a run)", required: true },
|
|
349
|
+
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
350
|
+
"max-steps": { type: "string", description: "Stop after executing this many steps" },
|
|
351
|
+
"require-gates": {
|
|
352
|
+
type: "boolean",
|
|
353
|
+
description: "Treat every criteria-bearing completion gate as required: if no LLM judge is available, BLOCK the step " +
|
|
354
|
+
"(for a human to resolve via `akm workflow resume`) instead of failing open. A per-step `gate.required: true` " +
|
|
355
|
+
"in the workflow does the same on every surface; this is the run-wide override (#18).",
|
|
356
|
+
default: false,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
async run({ args }) {
|
|
360
|
+
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
361
|
+
const rawMaxSteps = getStringArg(args, "max-steps");
|
|
362
|
+
let maxSteps;
|
|
363
|
+
if (rawMaxSteps !== undefined) {
|
|
364
|
+
maxSteps = Number.parseInt(rawMaxSteps, 10);
|
|
365
|
+
if (!/^\d+$/.test(rawMaxSteps) || maxSteps <= 0) {
|
|
366
|
+
throw new UsageError(`--max-steps must be a positive integer, got "${rawMaxSteps}".`, "INVALID_FLAG_VALUE");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const result = await runWorkflowSteps({
|
|
370
|
+
target: args.target,
|
|
371
|
+
...(args.params ? { params: parseWorkflowJsonObject(args.params, "--params") } : {}),
|
|
372
|
+
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
373
|
+
...(args["require-gates"] === true ? { requireGates: true } : {}),
|
|
374
|
+
});
|
|
375
|
+
output("workflow-run", result);
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
const workflowBriefCommand = defineJsonCommand({
|
|
379
|
+
meta: {
|
|
380
|
+
name: "brief",
|
|
381
|
+
description: "EXPERIMENTAL: describe a run's active step as an executable work-list for ANY agent session (the " +
|
|
382
|
+
"harness-neutral driver protocol) — read-only, takes no engine lease, mutates nothing; prints per-unit " +
|
|
383
|
+
"instructions, output schema, env binding names, and the exact `akm workflow report` command lines",
|
|
384
|
+
},
|
|
385
|
+
args: {
|
|
386
|
+
target: {
|
|
387
|
+
type: "positional",
|
|
388
|
+
description: "Workflow run id (or a workflow ref with an active run)",
|
|
389
|
+
required: true,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
async run({ args }) {
|
|
393
|
+
const { buildWorkflowBrief } = await import("../workflows/exec/brief.js");
|
|
394
|
+
const result = await buildWorkflowBrief(args.target);
|
|
395
|
+
output("workflow-brief", result);
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
const WORKFLOW_REPORT_STATES = ["completed", "failed", "running"];
|
|
399
|
+
const workflowReportCommand = defineJsonCommand({
|
|
400
|
+
meta: {
|
|
401
|
+
name: "report",
|
|
402
|
+
description: "EXPERIMENTAL: report a unit's result back into a run (the mutating half of the harness-neutral driver " +
|
|
403
|
+
"protocol) — ingested through the SAME shared step semantics the engine uses. --status running claims/" +
|
|
404
|
+
"heartbeats a unit; completed/failed records it and, when the step's work-list is fully terminal, runs the " +
|
|
405
|
+
"engine's completion path (reducer, artifact + schema validation, gate). --settle (no --unit) advances a run " +
|
|
406
|
+
"parked on a route-only/empty step. Refused while a live engine lease exists",
|
|
407
|
+
},
|
|
408
|
+
args: {
|
|
409
|
+
target: {
|
|
410
|
+
type: "positional",
|
|
411
|
+
description: "Workflow run id (or a workflow ref with an active run)",
|
|
412
|
+
required: true,
|
|
413
|
+
},
|
|
414
|
+
unit: {
|
|
415
|
+
type: "string",
|
|
416
|
+
description: "Content-derived unit id from `akm workflow brief` (copy it verbatim). Omit with --settle.",
|
|
417
|
+
},
|
|
418
|
+
settle: {
|
|
419
|
+
type: "boolean",
|
|
420
|
+
description: "Advance/finalize a run whose active step has NO unit left to report: a non-dispatching step (params-based route, empty fan-out, all-unresolvable) OR a fully-terminal step still needing finalization (every unit ran but the gate never judged — e.g. after resuming a required-gate block). Runs the deterministic completion path. Mutually exclusive with --unit; refused when the step still has genuinely pending units",
|
|
421
|
+
default: false,
|
|
422
|
+
},
|
|
423
|
+
"expect-step": {
|
|
424
|
+
type: "string",
|
|
425
|
+
description: "Guard: the step id you briefed against. Refuses the report if the run's active step has since moved (from the `brief` report/settle command line)",
|
|
426
|
+
},
|
|
427
|
+
status: { type: "string", description: `Unit status: ${WORKFLOW_REPORT_STATES.join(", ")}` },
|
|
428
|
+
result: { type: "string", description: "Result payload (JSON for a schema unit, else text). completed only." },
|
|
429
|
+
"result-file": { type: "string", description: "Read the result payload from this file instead of --result/stdin" },
|
|
430
|
+
tokens: { type: "string", description: "Tokens spent on this unit (counts against a declared budget)" },
|
|
431
|
+
"session-id": { type: "string", description: "Harness-native session id revealed while executing the unit" },
|
|
432
|
+
"failure-reason": { type: "string", description: "Structured failure vocabulary for a --status failed report" },
|
|
433
|
+
note: { type: "string", description: "Short progress note for a --status running heartbeat (not persisted)" },
|
|
434
|
+
rerun: {
|
|
435
|
+
type: "boolean",
|
|
436
|
+
description: "Re-run an already-FAILED unit: record a NEW attempt (re-applies budget) instead of refusing a differing re-report",
|
|
437
|
+
default: false,
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
async run({ args }) {
|
|
441
|
+
// --settle: the unit-less verb that advances a run parked on a
|
|
442
|
+
// non-dispatching step. Mutually exclusive with the per-unit report flags.
|
|
443
|
+
if (args.settle === true) {
|
|
444
|
+
if (getStringArg(args, "unit") !== undefined || getStringArg(args, "status") !== undefined) {
|
|
445
|
+
throw new UsageError("--settle advances a route-only/empty step and takes no --unit or --status. Drop them, or report a " +
|
|
446
|
+
"specific unit with `--unit <id> --status <state>` instead.", "INVALID_FLAG_VALUE");
|
|
447
|
+
}
|
|
448
|
+
const { settleWorkflowSpine } = await import("../workflows/exec/report.js");
|
|
449
|
+
const result = await settleWorkflowSpine({
|
|
450
|
+
target: args.target,
|
|
451
|
+
...(getStringArg(args, "expect-step") !== undefined ? { expectStep: getStringArg(args, "expect-step") } : {}),
|
|
452
|
+
});
|
|
453
|
+
output("workflow-report", result);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const status = args.status;
|
|
457
|
+
if (!status) {
|
|
458
|
+
throw new UsageError("--status is required (completed | failed | running), or pass --settle to advance a non-dispatching step.", "MISSING_REQUIRED_ARGUMENT");
|
|
459
|
+
}
|
|
460
|
+
if (!WORKFLOW_REPORT_STATES.includes(status)) {
|
|
461
|
+
throw new UsageError(`Invalid --status "${status}". Expected one of: ${WORKFLOW_REPORT_STATES.join(", ")}.`, "INVALID_FLAG_VALUE");
|
|
462
|
+
}
|
|
463
|
+
const unitId = getStringArg(args, "unit");
|
|
464
|
+
if (!unitId) {
|
|
465
|
+
throw new UsageError("--unit is required (the content-derived unit id from `akm workflow brief`), or pass --settle for a route-only/empty step.", "MISSING_REQUIRED_ARGUMENT");
|
|
466
|
+
}
|
|
467
|
+
let tokens;
|
|
468
|
+
const rawTokens = getStringArg(args, "tokens");
|
|
469
|
+
if (rawTokens !== undefined) {
|
|
470
|
+
tokens = Number.parseInt(rawTokens, 10);
|
|
471
|
+
if (!/^\d+$/.test(rawTokens)) {
|
|
472
|
+
throw new UsageError(`--tokens must be a non-negative integer, got "${rawTokens}".`, "INVALID_FLAG_VALUE");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Result payload precedence: --result, then --result-file, then stdin
|
|
476
|
+
// (completed/failed only; a running heartbeat carries no result).
|
|
477
|
+
let resultRaw;
|
|
478
|
+
if (status !== "running") {
|
|
479
|
+
const resultFile = getStringArg(args, "result-file");
|
|
480
|
+
if (args.result !== undefined && resultFile !== undefined) {
|
|
481
|
+
throw new UsageError("Pass at most one of --result or --result-file.", "INVALID_FLAG_VALUE");
|
|
482
|
+
}
|
|
483
|
+
if (args.result !== undefined) {
|
|
484
|
+
resultRaw = String(args.result);
|
|
485
|
+
}
|
|
486
|
+
else if (resultFile !== undefined) {
|
|
487
|
+
const fs = await import("node:fs");
|
|
488
|
+
resultRaw = fs.readFileSync(resultFile, "utf8");
|
|
489
|
+
}
|
|
490
|
+
else if (!process.stdin.isTTY) {
|
|
491
|
+
resultRaw = await readStdin();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const { reportWorkflowUnit } = await import("../workflows/exec/report.js");
|
|
495
|
+
const result = await reportWorkflowUnit({
|
|
496
|
+
target: args.target,
|
|
497
|
+
unitId,
|
|
498
|
+
status: status,
|
|
499
|
+
...(getStringArg(args, "expect-step") !== undefined ? { expectStep: getStringArg(args, "expect-step") } : {}),
|
|
500
|
+
...(resultRaw !== undefined ? { resultRaw } : {}),
|
|
501
|
+
...(tokens !== undefined ? { tokens } : {}),
|
|
502
|
+
...(args.rerun === true ? { rerun: true } : {}),
|
|
503
|
+
...(getStringArg(args, "session-id") !== undefined ? { sessionId: getStringArg(args, "session-id") } : {}),
|
|
504
|
+
...(getStringArg(args, "failure-reason") !== undefined
|
|
505
|
+
? { failureReason: getStringArg(args, "failure-reason") }
|
|
506
|
+
: {}),
|
|
507
|
+
...(getStringArg(args, "note") !== undefined ? { note: getStringArg(args, "note") } : {}),
|
|
508
|
+
});
|
|
509
|
+
output("workflow-report", result);
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
async function readStdin() {
|
|
513
|
+
const chunks = [];
|
|
514
|
+
for await (const chunk of process.stdin)
|
|
515
|
+
chunks.push(chunk);
|
|
516
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
517
|
+
}
|
|
518
|
+
const workflowWatchCommand = defineJsonCommand({
|
|
519
|
+
meta: {
|
|
520
|
+
name: "watch",
|
|
521
|
+
description: "Print a run's workflow_* events (state.db events table) as NDJSON and exit; --stream polls in the " +
|
|
522
|
+
"foreground until the run reaches a terminal status (no daemon)",
|
|
523
|
+
},
|
|
524
|
+
args: {
|
|
525
|
+
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
526
|
+
stream: {
|
|
527
|
+
type: "boolean",
|
|
528
|
+
description: "Keep polling for new events until the run leaves 'active' (completed/failed/blocked)",
|
|
529
|
+
default: false,
|
|
530
|
+
},
|
|
531
|
+
"interval-ms": { type: "string", description: "Poll interval in milliseconds for --stream (default: 1000)" },
|
|
532
|
+
},
|
|
533
|
+
async run({ args }) {
|
|
534
|
+
const rawInterval = getStringArg(args, "interval-ms");
|
|
535
|
+
let intervalMs;
|
|
536
|
+
if (rawInterval !== undefined) {
|
|
537
|
+
intervalMs = Number.parseInt(rawInterval, 10);
|
|
538
|
+
if (!/^\d+$/.test(rawInterval) || intervalMs <= 0) {
|
|
539
|
+
throw new UsageError(`--interval-ms must be a positive integer, got "${rawInterval}".`, "INVALID_FLAG_VALUE");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const { watchWorkflowRun } = await import("../workflows/exec/watch.js");
|
|
543
|
+
const result = await watchWorkflowRun({
|
|
544
|
+
runId: args.runId,
|
|
545
|
+
stream: args.stream === true,
|
|
546
|
+
...(intervalMs !== undefined ? { intervalMs } : {}),
|
|
547
|
+
});
|
|
548
|
+
// The event lines above are raw NDJSON on stdout; this trailing envelope
|
|
549
|
+
// is the machine-readable command result (counts + terminal status).
|
|
550
|
+
output("workflow-watch", { ok: true, ...result });
|
|
551
|
+
},
|
|
552
|
+
});
|
|
293
553
|
const workflowAbandonCommand = defineJsonCommand({
|
|
294
554
|
meta: {
|
|
295
555
|
name: "abandon",
|
|
@@ -332,6 +592,10 @@ export const workflowCommand = defineCommand({
|
|
|
332
592
|
resume: workflowResumeCommand,
|
|
333
593
|
abandon: workflowAbandonCommand,
|
|
334
594
|
validate: workflowValidateCommand,
|
|
595
|
+
run: workflowRunCommand,
|
|
596
|
+
brief: workflowBriefCommand,
|
|
597
|
+
report: workflowReportCommand,
|
|
598
|
+
watch: workflowWatchCommand,
|
|
335
599
|
},
|
|
336
600
|
run({ args }) {
|
|
337
601
|
return runWithJsonErrors(async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { buildWorkflowAction } from "../../output/renderers.js";
|
|
6
7
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry.js";
|
|
@@ -8,6 +9,59 @@ function toPosix(input) {
|
|
|
8
9
|
return input.replace(/\\/g, "/");
|
|
9
10
|
}
|
|
10
11
|
const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
|
|
12
|
+
/**
|
|
13
|
+
* Recognized workflow asset extensions, in resolution-priority order.
|
|
14
|
+
* `.md` (classic linear markdown workflows — the stable contract) stays
|
|
15
|
+
* FIRST for back-compat; `.yaml`/`.yml` hold YAML workflow *programs*
|
|
16
|
+
* (redesign addendum, R1). `workflow:<name>` refs resolve against this list.
|
|
17
|
+
*/
|
|
18
|
+
export const WORKFLOW_EXTENSIONS = [".md", ".yaml", ".yml"];
|
|
19
|
+
/**
|
|
20
|
+
* Strip a recognized workflow extension (`.md`/`.yaml`/`.yml`) from a workflow
|
|
21
|
+
* asset *name* so `foo`, `foo.yaml`, `foo.yml`, and `foo.md` collapse to one
|
|
22
|
+
* canonical identity — the same collapse `workflowSpec.toCanonicalName`
|
|
23
|
+
* performs on a resolved file path. Callers that turn a `workflow:<name>` ref
|
|
24
|
+
* into run identity (the active-run guard, list/status filters) MUST route the
|
|
25
|
+
* name through this so an aliased spelling (`workflow:foo.yaml`) and the
|
|
26
|
+
* canonical `workflow:foo` cannot start or hide parallel runs of the same
|
|
27
|
+
* workflow. Names without a recognized workflow extension pass through
|
|
28
|
+
* unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export function canonicalizeWorkflowName(name) {
|
|
31
|
+
const lower = name.toLowerCase();
|
|
32
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
33
|
+
if (lower.endsWith(ext))
|
|
34
|
+
return name.slice(0, -ext.length);
|
|
35
|
+
}
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
const workflowSpec = {
|
|
39
|
+
isRelevantFile: (fileName) => WORKFLOW_EXTENSIONS.includes(path.extname(fileName).toLowerCase()),
|
|
40
|
+
toCanonicalName: (typeRoot, filePath) => {
|
|
41
|
+
const rel = toPosix(path.relative(typeRoot, filePath));
|
|
42
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
43
|
+
if (rel.toLowerCase().endsWith(ext))
|
|
44
|
+
return rel.slice(0, -ext.length);
|
|
45
|
+
}
|
|
46
|
+
return rel;
|
|
47
|
+
},
|
|
48
|
+
toAssetPath: (typeRoot, name) => {
|
|
49
|
+
// Explicit extension wins (accepts refs like "release/ship.yaml").
|
|
50
|
+
const lower = name.toLowerCase();
|
|
51
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
52
|
+
if (lower.endsWith(ext))
|
|
53
|
+
return path.join(typeRoot, name);
|
|
54
|
+
}
|
|
55
|
+
// Probe in priority order — `.md` first for back-compat — and fall back
|
|
56
|
+
// to the markdown path so error messages keep naming the canonical file.
|
|
57
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
58
|
+
const candidate = path.join(typeRoot, `${name}${ext}`);
|
|
59
|
+
if (fs.existsSync(candidate))
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
return path.join(typeRoot, `${name}.md`);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
11
65
|
const markdownSpec = {
|
|
12
66
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
13
67
|
toCanonicalName: (typeRoot, filePath) => {
|
|
@@ -62,7 +116,10 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
62
116
|
knowledge: { stashDir: "knowledge", ...markdownSpec },
|
|
63
117
|
workflow: {
|
|
64
118
|
stashDir: "workflows",
|
|
65
|
-
...
|
|
119
|
+
...workflowSpec,
|
|
120
|
+
// Type-level renderer for markdown workflows; YAML programs are claimed by
|
|
121
|
+
// the dedicated `workflowProgramMatcher`, which names the
|
|
122
|
+
// "workflow-program-yaml" renderer directly on its MatchResult.
|
|
66
123
|
rendererName: "workflow-md",
|
|
67
124
|
actionBuilder: (ref) => buildWorkflowAction(ref),
|
|
68
125
|
},
|
|
@@ -832,6 +832,26 @@ export const IndexConfigSchema = z.preprocess((raw, ctx) => {
|
|
|
832
832
|
stalenessDetection: StalenessDetectionSchema.optional(),
|
|
833
833
|
})
|
|
834
834
|
.catchall(IndexPassConfigSchema));
|
|
835
|
+
// ── Workflow engine ─────────────────────────────────────────────────────────
|
|
836
|
+
/**
|
|
837
|
+
* Workflow-engine settings (`workflow`).
|
|
838
|
+
*
|
|
839
|
+
* `maxConcurrency` is the engine-wide ceiling on concurrent units for native
|
|
840
|
+
* fan-out (`akm workflow run`). It replaces the hard-coded `min(16, cores−2)`
|
|
841
|
+
* cap (which matched Claude Code) with a user knob:
|
|
842
|
+
* - UNSET → the CPU-derived default `min(16, max(1, cores−2))`.
|
|
843
|
+
* - SET → the explicit positive integer, CLAMPED at read time to
|
|
844
|
+
* `[1, WORKFLOW_MAX_CONCURRENCY_CEILING]` (64). Values above the ceiling
|
|
845
|
+
* are clamped, not rejected, so a config shared across machines with wildly
|
|
846
|
+
* different core counts never hard-fails validation.
|
|
847
|
+
* The R3 brief/report driver surface does NOT consult this — drivers own their
|
|
848
|
+
* own parallelism (the engine only caps native dispatch).
|
|
849
|
+
*/
|
|
850
|
+
export const WorkflowConfigSchema = z
|
|
851
|
+
.object({
|
|
852
|
+
maxConcurrency: positiveInt.optional(),
|
|
853
|
+
})
|
|
854
|
+
.passthrough();
|
|
835
855
|
// ── Setup-derived recommendations ──────────────────────────────────────────
|
|
836
856
|
/**
|
|
837
857
|
* Cron-style schedule hints derived by `akm setup --reset-recommended`.
|
|
@@ -895,6 +915,7 @@ export const AkmConfigShape = {
|
|
|
895
915
|
feedback: FeedbackConfigSchema.optional(),
|
|
896
916
|
archiveRetentionDays: nonNegativeNumber.optional(),
|
|
897
917
|
improve: ImproveConfigSchema.optional(),
|
|
918
|
+
workflow: WorkflowConfigSchema.optional(),
|
|
898
919
|
setup: SetupConfigSchema.optional(),
|
|
899
920
|
};
|
|
900
921
|
export const AkmConfigBaseSchema = z.object(AkmConfigShape).passthrough();
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Structural JSON-Schema-subset validator (orchestration plan P1).
|
|
6
|
+
*
|
|
7
|
+
* The workflow engine's structured-output normalization needs to validate
|
|
8
|
+
* unit results against the author-declared unit `output` schema on any harness —
|
|
9
|
+
* including ones with no native schema support. Pulling in a full
|
|
10
|
+
* draft-2020-12 validator is deliberately avoided (dependency surface); this
|
|
11
|
+
* module implements the bounded subset that covers the schemas workflow
|
|
12
|
+
* authors actually write:
|
|
13
|
+
*
|
|
14
|
+
* Supported: `type` (string | string[] — string, number, integer, boolean,
|
|
15
|
+
* object, array, null), `properties`, `required`, `items`,
|
|
16
|
+
* `additionalProperties: false`, `enum` (primitives), `minItems`,
|
|
17
|
+
* `maxItems`, `minLength`, `maxLength`, `minimum`, `maximum`.
|
|
18
|
+
*
|
|
19
|
+
* Ignored (permissive): `$ref`, `allOf`/`anyOf`/`oneOf`/`not`, `pattern`,
|
|
20
|
+
* `format`, and every other keyword. Unknown keywords never throw — a
|
|
21
|
+
* schema using them simply constrains less. Callers needing full JSON
|
|
22
|
+
* Schema semantics should validate downstream.
|
|
23
|
+
*
|
|
24
|
+
* Returns a flat list of human-readable error strings (empty = valid), each
|
|
25
|
+
* prefixed with a JSON-pointer-ish path — the shape `runStructured`'s
|
|
26
|
+
* corrective-feedback builder wants.
|
|
27
|
+
*/
|
|
28
|
+
export function validateJsonSchemaSubset(value, schema) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
validateNode(value, schema, "$", errors);
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
33
|
+
function typeOf(value) {
|
|
34
|
+
if (value === null)
|
|
35
|
+
return "null";
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return "array";
|
|
38
|
+
switch (typeof value) {
|
|
39
|
+
case "string":
|
|
40
|
+
return "string";
|
|
41
|
+
case "boolean":
|
|
42
|
+
return "boolean";
|
|
43
|
+
case "number":
|
|
44
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
45
|
+
default:
|
|
46
|
+
return "object";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function matchesType(actual, expected) {
|
|
50
|
+
if (expected === actual)
|
|
51
|
+
return true;
|
|
52
|
+
// JSON Schema: every integer is also a number.
|
|
53
|
+
return expected === "number" && actual === "integer";
|
|
54
|
+
}
|
|
55
|
+
function validateNode(value, schema, path, errors) {
|
|
56
|
+
const actual = typeOf(value);
|
|
57
|
+
const declared = schema.type;
|
|
58
|
+
if (typeof declared === "string" || Array.isArray(declared)) {
|
|
59
|
+
const expected = (Array.isArray(declared) ? declared : [declared]).filter((t) => typeof t === "string");
|
|
60
|
+
if (expected.length > 0 && !expected.some((t) => matchesType(actual, t))) {
|
|
61
|
+
errors.push(`${path}: expected type ${expected.join(" | ")}, got ${actual}`);
|
|
62
|
+
return; // type mismatch makes the remaining constraints meaningless
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
66
|
+
const allowed = schema.enum;
|
|
67
|
+
if (!allowed.some((candidate) => candidate === value)) {
|
|
68
|
+
errors.push(`${path}: value ${JSON.stringify(value)} is not one of ${JSON.stringify(allowed)}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (actual === "string" && typeof value === "string") {
|
|
73
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
74
|
+
errors.push(`${path}: string shorter than minLength ${schema.minLength}`);
|
|
75
|
+
}
|
|
76
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
77
|
+
errors.push(`${path}: string longer than maxLength ${schema.maxLength}`);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if ((actual === "number" || actual === "integer") && typeof value === "number") {
|
|
82
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) {
|
|
83
|
+
errors.push(`${path}: ${value} is below minimum ${schema.minimum}`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) {
|
|
86
|
+
errors.push(`${path}: ${value} is above maximum ${schema.maximum}`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (actual === "array" && Array.isArray(value)) {
|
|
91
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
92
|
+
errors.push(`${path}: array has fewer than minItems ${schema.minItems}`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
95
|
+
errors.push(`${path}: array has more than maxItems ${schema.maxItems}`);
|
|
96
|
+
}
|
|
97
|
+
const items = schema.items;
|
|
98
|
+
if (items && typeof items === "object" && !Array.isArray(items)) {
|
|
99
|
+
value.forEach((element, index) => {
|
|
100
|
+
validateNode(element, items, `${path}[${index}]`, errors);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (actual === "object" && typeof value === "object" && value !== null) {
|
|
106
|
+
const record = value;
|
|
107
|
+
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)
|
|
108
|
+
? schema.properties
|
|
109
|
+
: undefined;
|
|
110
|
+
if (Array.isArray(schema.required)) {
|
|
111
|
+
for (const key of schema.required) {
|
|
112
|
+
// `Object.hasOwn`, not `key in record`: a required key satisfied only by
|
|
113
|
+
// an inherited prototype member (e.g. "toString", "constructor") is NOT
|
|
114
|
+
// present on the value itself, so `{}` must fail `required: ["toString"]`.
|
|
115
|
+
if (typeof key === "string" && !Object.hasOwn(record, key)) {
|
|
116
|
+
errors.push(`${path}: missing required property "${key}"`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (properties) {
|
|
121
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
122
|
+
if (!Object.hasOwn(record, key))
|
|
123
|
+
continue;
|
|
124
|
+
if (propSchema && typeof propSchema === "object" && !Array.isArray(propSchema)) {
|
|
125
|
+
validateNode(record[key], propSchema, `${path}.${key}`, errors);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// `additionalProperties: false` closes the object to exactly its declared
|
|
130
|
+
// `properties`. This MUST run even when no `properties` object is present:
|
|
131
|
+
// `{ type: "object", additionalProperties: false }` admits only `{}`. Use
|
|
132
|
+
// `Object.hasOwn` so an inherited key name (e.g. "toString") on the empty
|
|
133
|
+
// property set is not mistaken for a declared property.
|
|
134
|
+
if (schema.additionalProperties === false) {
|
|
135
|
+
for (const key of Object.keys(record)) {
|
|
136
|
+
if (!properties || !Object.hasOwn(properties, key)) {
|
|
137
|
+
errors.push(`${path}: unexpected property "${key}" (additionalProperties: false)`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/indexer/db/db.js
CHANGED
|
@@ -46,7 +46,8 @@ export function openIndexDatabase(dbPath, options) {
|
|
|
46
46
|
*/
|
|
47
47
|
function resolveConfiguredEmbeddingDim() {
|
|
48
48
|
try {
|
|
49
|
-
const
|
|
49
|
+
const esmRequire = createRequire(import.meta.url);
|
|
50
|
+
const { loadConfig } = esmRequire("../../core/config/config");
|
|
50
51
|
const dim = loadConfig().embedding?.dimension;
|
|
51
52
|
if (typeof dim === "number" && Number.isInteger(dim) && dim > 0 && dim <= 4096) {
|
|
52
53
|
return dim;
|