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.
Files changed (90) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  3. package/dist/commands/env/env-binding.js +95 -0
  4. package/dist/commands/env/env-cli.js +8 -65
  5. package/dist/commands/sources/migration-help.js +7 -4
  6. package/dist/commands/workflow-cli.js +276 -12
  7. package/dist/core/asset/asset-spec.js +58 -1
  8. package/dist/core/config/config-schema.js +21 -0
  9. package/dist/core/json-schema.js +142 -0
  10. package/dist/indexer/db/db.js +2 -1
  11. package/dist/indexer/walk/matchers.js +39 -0
  12. package/dist/integrations/agent/builders.js +7 -5
  13. package/dist/integrations/agent/model-aliases.js +9 -0
  14. package/dist/integrations/agent/profiles.js +72 -5
  15. package/dist/integrations/agent/runner-dispatch.js +25 -1
  16. package/dist/integrations/agent/spawn.js +137 -14
  17. package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
  18. package/dist/integrations/harnesses/aider/index.js +58 -0
  19. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  20. package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
  21. package/dist/integrations/harnesses/amazonq/index.js +59 -0
  22. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  23. package/dist/integrations/harnesses/claude/agent-builder.js +45 -6
  24. package/dist/integrations/harnesses/claude/index.js +25 -23
  25. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  26. package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
  27. package/dist/integrations/harnesses/codex/index.js +63 -0
  28. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  29. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  30. package/dist/integrations/harnesses/copilot/index.js +60 -0
  31. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  32. package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
  33. package/dist/integrations/harnesses/gemini/index.js +60 -0
  34. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  35. package/dist/integrations/harnesses/index.js +26 -4
  36. package/dist/integrations/harnesses/opencode/index.js +15 -16
  37. package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
  38. package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
  39. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +581 -91
  40. package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
  41. package/dist/integrations/harnesses/openhands/index.js +58 -0
  42. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  43. package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
  44. package/dist/integrations/harnesses/pi/index.js +58 -0
  45. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  46. package/dist/integrations/harnesses/types.js +7 -0
  47. package/dist/integrations/session-logs/index.js +24 -11
  48. package/dist/output/renderers.js +3 -2
  49. package/dist/output/shapes/passthrough.js +4 -0
  50. package/dist/output/text/helpers.js +212 -1
  51. package/dist/output/text/workflow.js +3 -1
  52. package/dist/schemas/akm-config.json +14225 -0
  53. package/dist/schemas/akm-workflow.json +328 -0
  54. package/dist/scripts/migrate-storage.js +1034 -6973
  55. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +768 -10
  56. package/dist/storage/repositories/workflow-runs-repository.js +189 -1
  57. package/dist/text-import-hook.mjs +1 -1
  58. package/dist/workflows/authoring/authoring.js +123 -10
  59. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  60. package/dist/workflows/cli.js +4 -0
  61. package/dist/workflows/db.js +135 -0
  62. package/dist/workflows/exec/brief.js +484 -0
  63. package/dist/workflows/exec/native-executor.js +975 -0
  64. package/dist/workflows/exec/param-secrets.js +115 -0
  65. package/dist/workflows/exec/report.js +1295 -0
  66. package/dist/workflows/exec/run-workflow.js +596 -0
  67. package/dist/workflows/exec/scheduler.js +100 -0
  68. package/dist/workflows/exec/step-work.js +1156 -0
  69. package/dist/workflows/exec/unit-writer.js +23 -0
  70. package/dist/workflows/exec/watch.js +116 -0
  71. package/dist/workflows/exec/worktree.js +171 -0
  72. package/dist/workflows/ir/compile.js +388 -0
  73. package/dist/workflows/ir/params.js +54 -0
  74. package/dist/workflows/ir/plan-hash.js +33 -0
  75. package/dist/workflows/ir/schema.js +4 -0
  76. package/dist/workflows/parser.js +3 -1
  77. package/dist/workflows/program/expressions.js +369 -0
  78. package/dist/workflows/program/parser.js +760 -0
  79. package/dist/workflows/program/project.js +105 -0
  80. package/dist/workflows/program/schema.js +54 -0
  81. package/dist/workflows/renderer.js +82 -5
  82. package/dist/workflows/runtime/agent-identity.js +59 -14
  83. package/dist/workflows/runtime/runs.js +206 -36
  84. package/dist/workflows/runtime/unit-checkin.js +45 -0
  85. package/dist/workflows/runtime/workflow-asset-loader.js +64 -1
  86. package/dist/workflows/validate-summary.js +24 -3
  87. package/dist/workflows/validator.js +1 -1
  88. package/docs/data-and-telemetry.md +2 -1
  89. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  90. 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 in the working stash",
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: { type: "string", description: "Import and validate markdown from an existing file" },
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 template",
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 markdown file or ref and print any errors",
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
- if (!target.startsWith("workflow:"))
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
- ...markdownSpec,
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
+ }
@@ -46,7 +46,8 @@ export function openIndexDatabase(dbPath, options) {
46
46
  */
47
47
  function resolveConfiguredEmbeddingDim() {
48
48
  try {
49
- const { loadConfig } = require("../../core/config/config");
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;