akm-cli 0.9.2-alpha.4 → 0.9.2

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 (143) hide show
  1. package/CHANGELOG.md +493 -0
  2. package/STABILITY.md +23 -5
  3. package/dist/assets/hints/cli-hints-full.md +12 -7
  4. package/dist/assets/tasks/core/extract.yml +3 -5
  5. package/dist/assets/tasks/core/improve.yml +3 -5
  6. package/dist/assets/tasks/core/index-refresh.yml +3 -5
  7. package/dist/assets/tasks/core/sync.yml +3 -5
  8. package/dist/assets/tasks/core/version-check.yml +3 -5
  9. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
  10. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
  12. package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
  13. package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
  14. package/dist/cli/unknown-flags.js +12 -1
  15. package/dist/cli.js +8 -1
  16. package/dist/commands/command/command-execution.js +23 -2
  17. package/dist/commands/health/improve-metrics.js +38 -0
  18. package/dist/commands/health/windows.js +8 -4
  19. package/dist/commands/health.js +8 -4
  20. package/dist/commands/lint/index.js +1 -1
  21. package/dist/commands/migrate-cli.js +130 -24
  22. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  23. package/dist/commands/tasks/explain.js +304 -0
  24. package/dist/commands/tasks/tasks-cli.js +185 -3
  25. package/dist/commands/tasks/tasks.js +265 -52
  26. package/dist/commands/workflow/plan.js +159 -0
  27. package/dist/commands/workflow-cli.js +94 -2
  28. package/dist/core/activation-policy.js +2 -12
  29. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  30. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  31. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  32. package/dist/core/errors.js +45 -0
  33. package/dist/core/json-schema.js +15 -5
  34. package/dist/core/state/migrations.js +57 -0
  35. package/dist/core/state-db.js +16 -14
  36. package/dist/core/subprocess.js +47 -13
  37. package/dist/execution/guarded-source.js +44 -0
  38. package/dist/execution/input-contract.js +250 -0
  39. package/dist/execution/target-ref.js +63 -0
  40. package/dist/indexer/usage/usage-events.js +14 -3
  41. package/dist/integrations/agent/execution-lowering.js +12 -1
  42. package/dist/output/shapes/passthrough.js +2 -0
  43. package/dist/output/text/helpers.js +1 -1
  44. package/dist/output/text/migrate.js +12 -3
  45. package/dist/output/text/workflow-format.js +192 -10
  46. package/dist/output/text/workflow.js +2 -1
  47. package/dist/runtime.js +1 -0
  48. package/dist/scripts/akm-migrate-node.js +11838 -10118
  49. package/dist/scripts/akm-migrate.js +11828 -10117
  50. package/dist/setup/steps/tasks.js +34 -17
  51. package/dist/storage/repositories/task-history-repository.js +5 -1
  52. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  53. package/dist/tasks/backends/launchd.js +31 -84
  54. package/dist/tasks/embedded.js +13 -7
  55. package/dist/tasks/model/invocation.js +4 -0
  56. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  57. package/dist/tasks/prepare/prepare-support.js +154 -0
  58. package/dist/tasks/prepare/prepare.js +117 -0
  59. package/dist/tasks/prepare/prepared-execution.js +4 -0
  60. package/dist/tasks/prepare/script-capture.js +80 -0
  61. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  62. package/dist/tasks/run/load-task.js +117 -0
  63. package/dist/tasks/run/provenance.js +20 -0
  64. package/dist/tasks/run/run-command-task.js +92 -0
  65. package/dist/tasks/run/run-native-task.js +222 -0
  66. package/dist/tasks/run/run-task.js +99 -0
  67. package/dist/tasks/run/run-workflow-task.js +222 -0
  68. package/dist/tasks/run/task-history.js +134 -0
  69. package/dist/tasks/run/task-log.js +179 -0
  70. package/dist/tasks/run/task-result.js +19 -0
  71. package/dist/tasks/scheduler-binding.js +66 -2
  72. package/dist/tasks/scheduler-invocation.js +63 -3
  73. package/dist/tasks/scheduler-sync.js +77 -14
  74. package/dist/tasks/source/bounded-document.js +455 -0
  75. package/dist/tasks/source/parse-task-source.js +59 -0
  76. package/dist/tasks/source/project-v4.js +62 -0
  77. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  78. package/dist/tasks/source/task-source-v4.js +626 -0
  79. package/dist/tasks/source-v3.js +10 -733
  80. package/dist/tasks/task-run-reserved-flags.js +79 -0
  81. package/dist/workflows/authoring/authoring.js +17 -8
  82. package/dist/workflows/exec/child-invocation.js +34 -0
  83. package/dist/workflows/exec/child-workflow.js +370 -0
  84. package/dist/workflows/exec/exec-unit.js +50 -170
  85. package/dist/workflows/exec/frozen-judge.js +19 -2
  86. package/dist/workflows/exec/native-executor.js +49 -27
  87. package/dist/workflows/exec/param-secrets.js +12 -0
  88. package/dist/workflows/exec/run-workflow.js +48 -59
  89. package/dist/workflows/exec/step-work.js +222 -80
  90. package/dist/workflows/exec/unit-dispatch.js +72 -0
  91. package/dist/workflows/freeze/child-output-references.js +94 -0
  92. package/dist/workflows/freeze/environment.js +174 -0
  93. package/dist/workflows/freeze/identity.js +22 -0
  94. package/dist/workflows/freeze/resolve-steps.js +78 -0
  95. package/dist/workflows/freeze/source-freeze.js +57 -0
  96. package/dist/workflows/freeze/step-values.js +68 -0
  97. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  98. package/dist/workflows/freeze/targets/command.js +81 -0
  99. package/dist/workflows/freeze/targets/script.js +57 -0
  100. package/dist/workflows/freeze/targets/shell.js +31 -0
  101. package/dist/workflows/freeze/targets/task.js +179 -0
  102. package/dist/workflows/freeze/task-bindings.js +180 -0
  103. package/dist/workflows/ir/compile.js +59 -11
  104. package/dist/workflows/ir/environment-v4.js +3 -3
  105. package/dist/workflows/ir/freeze-v4.js +41 -7
  106. package/dist/workflows/ir/params.js +58 -131
  107. package/dist/workflows/ir/plan-hash.js +3 -3
  108. package/dist/workflows/ir/schema-v4.js +246 -17
  109. package/dist/workflows/parser.js +74 -2
  110. package/dist/workflows/program/schema.js +5 -2
  111. package/dist/workflows/resource-limits.js +20 -0
  112. package/dist/workflows/runtime/plan-classifier.js +24 -7
  113. package/dist/workflows/runtime/run-outputs.js +103 -0
  114. package/dist/workflows/runtime/runs.js +114 -9
  115. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  116. package/dist/workflows/source-files.js +5 -5
  117. package/dist/workflows/source-ir/compare.js +17 -0
  118. package/dist/workflows/source-ir/compile.js +7 -3
  119. package/dist/workflows/source-ir/github-yaml.js +64 -17
  120. package/dist/workflows/source-ir/schema.js +69 -21
  121. package/dist/workflows/source-ir/semantics.js +7 -25
  122. package/dist/workflows/source-ir/triggers.js +79 -0
  123. package/dist/workflows/source-ir/uses.js +33 -7
  124. package/docs/migration/README.md +1 -1
  125. package/docs/migration/release-notes/0.9.2.md +87 -11
  126. package/docs/migration/release-notes/README.md +3 -2
  127. package/docs/migration/v0.8-to-v0.9.md +13 -11
  128. package/docs/migration/v0.9.0-troubleshooting.md +20 -13
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  130. package/docs/reference/README.md +1 -1
  131. package/docs/reference/cli.md +140 -46
  132. package/docs/reference/configuration.md +6 -5
  133. package/docs/reference/supported-formats.md +9 -5
  134. package/docs/reference/tasks.md +338 -75
  135. package/docs/reference/workflow-schema.md +290 -16
  136. package/docs/reference/workflows.md +57 -7
  137. package/package.json +1 -1
  138. package/schemas/akm-task.json +173 -118
  139. package/schemas/akm-workflow.json +28 -0
  140. package/dist/tasks/runner.js +0 -941
  141. package/dist/tasks/runtime-v3.js +0 -281
  142. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  143. package/dist/workflows/source-ir/ordering.js +0 -38
@@ -7,15 +7,22 @@
7
7
  * `create --print` emits Markdown; execution accepts peer `.md` and
8
8
  * GitHub-shaped `.yml` workflow sources. Validate with `akm lint --type workflows`.
9
9
  */
10
+ import { getParsedInvocation } from "../cli/invocation.js";
10
11
  import { getStringArg } from "../cli/parse-args.js";
11
12
  import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
12
13
  import { armAbortDeadline } from "../core/abort-deadline.js";
13
14
  import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
14
15
  import { NotFoundError, UsageError } from "../core/errors.js";
15
16
  import { akmIndex } from "../indexer/indexer.js";
17
+ import { getOutputMode } from "../output/context.js";
18
+ import { renderGenericText } from "../output/generic-render.js";
19
+ import { deliverRendered } from "../output/html-render.js";
20
+ import { shapeForCommand } from "../output/shapes.js";
21
+ import { formatPlain } from "../output/text.js";
16
22
  import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
17
23
  import { WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
18
24
  import { abandonWorkflowRun, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
25
+ import { akmWorkflowPlan } from "./workflow/plan.js";
19
26
  const workflowStatusCommand = defineJsonCommand({
20
27
  meta: {
21
28
  name: "status",
@@ -63,9 +70,18 @@ const workflowListCommand = defineJsonCommand({
63
70
  args: {
64
71
  ref: { type: "string", description: "Filter to one workflow ref" },
65
72
  active: { type: "boolean", description: "Only show active runs", default: false },
73
+ children: {
74
+ type: "boolean",
75
+ description: "Also include child workflow runs (hidden by default, P3b)",
76
+ default: false,
77
+ },
66
78
  },
67
79
  async run({ args }) {
68
- const result = await listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
80
+ const result = await listWorkflowRuns({
81
+ workflowRef: args.ref,
82
+ activeOnly: args.active,
83
+ includeChildren: args.children,
84
+ });
69
85
  output("workflow-list", result);
70
86
  },
71
87
  });
@@ -131,7 +147,9 @@ const workflowCreateCommand = defineJsonCommand({
131
147
  // Index the newly-written workflow so `akm workflow run` can resolve
132
148
  // a workflowEntryId without requiring an explicit `akm index` call
133
149
  // first. Uses the same incremental index path that `akm add` uses.
134
- await akmIndex({ stashDir: result.stashDir });
150
+ // `result.bundleDir` the indexer's own `IndexOptions.stashDir` field
151
+ // name is unchanged (indexer vocabulary is out of scope, P4 row B-50).
152
+ await akmIndex({ stashDir: result.bundleDir });
135
153
  output("workflow-create", { ok: true, ...result });
136
154
  },
137
155
  });
@@ -286,6 +304,79 @@ function parseWorkflowTimeout(raw) {
286
304
  }
287
305
  return timeoutMs;
288
306
  }
307
+ // P3b Lane B (spec docs/plans/specs/p3b-child-executor.md §4.6): read-only
308
+ // compile+freeze introspection — zero durable writes, zero usage/event rows
309
+ // (row B-48). `--json` is deliberately NOT a flag anywhere in this CLI
310
+ // (B-N9); the global `--format json` is spliced on by `defineJsonCommand`.
311
+ const workflowPlanCommand = defineJsonCommand({
312
+ meta: {
313
+ name: "plan",
314
+ description: "Compile and freeze a workflow WITHOUT publishing it: the canonical step graph, per-step frozen target " +
315
+ "kinds, task/child expansion, input bindings, source read set, and lowering notices. Zero durable writes.",
316
+ },
317
+ args: {
318
+ ref: { type: "positional", description: "Workflow ref (workflows/<name>)", required: true },
319
+ },
320
+ async run({ args }) {
321
+ const result = await akmWorkflowPlan(args.ref);
322
+ // B-46/B-57: `akm workflow plan` is read-only introspection whose UNMARKED
323
+ // default is a human summary — `--format json` (B-N9) is the opt-in for
324
+ // the full structure, the mirror image of every other verb's json-by-
325
+ // default (DEFAULT_CONFIG.output.format).
326
+ //
327
+ // Detecting "the caller named no format at all" MUST NOT read
328
+ // `args.format` (code-review round 4, finding 3 / Review log R3):
329
+ // citty's one-parse rule (GLOBAL_OUTPUT_ARGS's own doc comment, "no
330
+ // command body may read these args") isn't just style here — reading it
331
+ // is actively wrong. citty parses each command level against only that
332
+ // level's own remaining argv, so a GLOBAL, pre-subcommand `--format json`
333
+ // (e.g. `akm --format json workflow plan <ref>`) is consumed by the ROOT
334
+ // command's own declared `format` arg before the `workflow`/`plan`
335
+ // subcommand tokens are even resolved — this LEAF's `args.format` reads
336
+ // `undefined` in exactly that case too, indistinguishable from "no
337
+ // format was named anywhere". Reproduced live: that invocation printed
338
+ // the human TEXT summary at exit 0 even though `getOutputMode().format`
339
+ // was already `"json"` (the control, `akm --format json workflow list`,
340
+ // correctly emitted JSON — only this leaf's own arg-read was wrong).
341
+ // Detect it instead off the process-wide invocation singleton
342
+ // (`getParsedInvocation`, src/cli/invocation.ts) — the same canonical,
343
+ // position-independent argv parse `src/cli.ts` mints ONCE at startup
344
+ // (`setParsedInvocation`, immediately before `initOutputMode` builds the
345
+ // `getOutputMode()` singleton from that identical argv), so this agrees
346
+ // with `getOutputMode()` regardless of where `--format` appeared. A bare
347
+ // `process.argv` read is reserved for `src/cli.ts`/`cli/invocation.ts`
348
+ // themselves (`lint-process-argv.ts`); every other module reads through
349
+ // this singleton instead. When explicit, this defers to the normal
350
+ // `output()` path (json/yaml/text/md/html/jsonl, `--output <path>`)
351
+ // unchanged; when absent, it reproduces `output()`'s OWN "text" branch
352
+ // verbatim (same shape/detail projection, same registered-formatter-or-
353
+ // generic-fallback, same `--output <path>` handling) without touching
354
+ // the shared dispatcher other commands rely on.
355
+ //
356
+ // "No format named anywhere" also has to check the RESOLVED mode, not
357
+ // just argv: `getOutputMode().format` already folds a persisted
358
+ // `output.format` config default in ahead of the hardcoded "json"
359
+ // fallback (`resolveOutputMode`, src/output/context.ts — argv ?? config
360
+ // default ?? "json"). A user who has configured e.g. `output.format:
361
+ // "yaml"` gets yaml from every other command with no `--format` on the
362
+ // line; `workflow plan` must honor that too instead of forcing its
363
+ // human-text branch over a real persisted default. A resolved format of
364
+ // exactly "json" is deliberately left on the text branch below: it's
365
+ // indistinguishable from "nothing configured" (DEFAULT_CONFIG.output.format
366
+ // is also "json" — OutputConfigSchema's `format` carries no independent
367
+ // zod default, so the merge is the only source), and collapsing that case
368
+ // onto the JSON envelope would erase the documented unmarked-default text
369
+ // summary for the overwhelmingly common "user configured nothing" case.
370
+ const mode = getOutputMode();
371
+ if (getParsedInvocation().getFlagValue("--format") === undefined && mode.format === "json") {
372
+ const shaped = shapeForCommand("workflow-plan", result, mode.detail, mode.shape);
373
+ const plain = formatPlain("workflow-plan", shaped, mode.detail);
374
+ deliverRendered(plain ?? renderGenericText("workflow-plan", shaped), mode.outputPath);
375
+ return;
376
+ }
377
+ output("workflow-plan", result);
378
+ },
379
+ });
289
380
  const workflowAbandonCommand = defineJsonCommand({
290
381
  meta: {
291
382
  name: "abandon",
@@ -324,6 +415,7 @@ export const workflowCommand = defineGroupCommand({
324
415
  resume: workflowResumeCommand,
325
416
  abandon: workflowAbandonCommand,
326
417
  run: workflowRunCommand,
418
+ plan: workflowPlanCommand,
327
419
  },
328
420
  // No `defaultRun`: bare `akm workflow` is a usage error (exit 2), the
329
421
  // canonical bare-group behavior — owner ruling 12. Run `akm workflow list
@@ -27,22 +27,12 @@ export function decideDangerousKeyInstall(input) {
27
27
  return "allow";
28
28
  return input.allowInsecure ? "warn-allow" : "gate";
29
29
  }
30
- // ── Rule 3: task activation (tasks/runner.ts) ────────────────────────────────
31
- /**
32
- * Whether a scheduler-generated task invocation must be skipped because the
33
- * task is not activated (its `enabled:` is false). Manual (non-scheduled) runs
34
- * are always dispatched — installing a task grants nothing until enabled, but
35
- * the operator may still run it by hand for catch-up / testing. See rule 3.
36
- */
37
- export function shouldSkipUnactivatedTask(input) {
38
- return !input.enabled && input.scheduled;
39
- }
40
- // ── Rule 4: write activation (search-source.ts, installations.ts) ────────────
30
+ // ── Rule 3: write activation (search-source.ts, installations.ts) ────────────
41
31
  /**
42
32
  * Whether a resolved source is write-activated. Only the primary stash and
43
33
  * sources explicitly marked `writable: true` are writable; registry-cached
44
34
  * (installed, read-only) sources are never written in place because
45
- * `akm update` overwrites them. See rule 4 above.
35
+ * `akm update` overwrites them. See rule 3 above.
46
36
  */
47
37
  export function isSourceWriteActivated(source) {
48
38
  return source.writable === true;
@@ -52,7 +52,8 @@
52
52
  */
53
53
  import path from "node:path";
54
54
  import { isDangerousEnvKey } from "../../../commands/lint/env-key-rules.js";
55
- import { parseTaskV3Yaml, taskV3SourceErrorDetail } from "../../../tasks/source-v3.js";
55
+ import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
56
+ import { taskSourceErrorDetail } from "../../../tasks/source-v3.js";
56
57
  import { compileWorkflowPlan } from "../../../workflows/ir/compile.js";
57
58
  import { compileWorkflowSource } from "../../../workflows/source-ir/compile.js";
58
59
  import { conceptIdForStashFile } from "../../asset/resolve-ref.js";
@@ -271,13 +272,15 @@ export function factDiagnostics(relPath, data) {
271
272
  return [];
272
273
  }
273
274
  /**
274
- * Task validation has one semantic owner: the strict task-v3 source parser.
275
+ * Task validation has one semantic owner: the version-routed task source
276
+ * parser (spec docs/plans/specs/p2a-task-source-v4.md §3.6) — `version: 3`
277
+ * through the strict task-v3 grammar, `version: 4` through task source v4.
275
278
  * Keeping raw YAML at this boundary preserves duplicate-key, alias/tag,
276
279
  * source-location, descriptor, resource-bound, and migration-hint behavior.
277
280
  */
278
281
  export function taskDiagnostics(relPath, raw, workspaceRoot) {
279
282
  try {
280
- parseTaskV3Yaml({
283
+ parseTaskSource({
281
284
  filePath: relPath,
282
285
  yaml: raw,
283
286
  ...(workspaceRoot ? { workspaceRoot } : {}),
@@ -289,7 +292,7 @@ export function taskDiagnostics(relPath, raw, workspaceRoot) {
289
292
  {
290
293
  file: relPath,
291
294
  issue: "invalid-task-yaml",
292
- detail: taskV3SourceErrorDetail(cause),
295
+ detail: taskSourceErrorDetail(cause),
293
296
  fixed: false,
294
297
  },
295
298
  ];
@@ -55,7 +55,7 @@
55
55
  * distinction never triggers.
56
56
  */
57
57
  import { scanEnvKeyNames } from "../../../commands/env/env.js";
58
- import { parseTaskV3Yaml } from "../../../tasks/source-v3.js";
58
+ import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
59
59
  import { compileWorkflowSource } from "../../../workflows/source-ir/compile.js";
60
60
  import { sourceStepInstructions } from "../../../workflows/source-ir/program.js";
61
61
  import { parseFrontmatter } from "../../asset/frontmatter.js";
@@ -229,25 +229,37 @@ export function foldRecognizedMetadata(rendererName, file) {
229
229
  case "task-yaml": {
230
230
  out.tags = Array.from(new Set([...(out.tags ?? []), "task", "scheduled"]));
231
231
  try {
232
- const task = parseTaskV3Yaml({ yaml: file.content(), filePath: file.absPath, workspaceRoot: file.stashRoot });
232
+ const parsed = parseTaskSource({ yaml: file.content(), filePath: file.absPath, workspaceRoot: file.stashRoot });
233
+ const v4 = parsed.v4;
234
+ const target = v4.target;
233
235
  const hints = new Set();
234
- for (const binding of task.triggers.schedules)
236
+ for (const binding of v4.schedule)
235
237
  hints.add(`schedule:${binding.cron}`);
236
- if (task.target.kind === "uses") {
237
- if (task.target.uses.kind === "workflow")
238
- hints.add(`workflow:${task.target.uses.ref}`);
239
- else if (task.target.uses.kind === "command")
240
- hints.add(`prompt:${task.target.uses.ref}`);
241
- else if (task.target.command?.kind === "inline")
242
- hints.add(`prompt:${task.target.command.content}`);
243
- else if (task.target.command?.kind === "stored")
244
- hints.add(`prompt:${task.target.command.ref}`);
238
+ if (target.kind === "uses") {
239
+ if (target.uses.kind === "workflow")
240
+ hints.add(`workflow:${target.uses.ref}`);
241
+ else if (target.uses.kind === "command")
242
+ hints.add(`prompt:${target.uses.ref}`);
243
+ else if (target.command?.kind === "inline")
244
+ hints.add(`prompt:${target.command.content}`);
245
+ else if (target.command?.kind === "stored")
246
+ hints.add(`prompt:${target.command.ref}`);
245
247
  else
246
- hints.add(`uses:${task.target.uses.ref}`);
248
+ hints.add(`uses:${target.uses.ref}`);
247
249
  }
248
250
  else {
249
- hints.add(`run:${task.target.run}`);
251
+ hints.add(`run:${target.run}`);
250
252
  }
253
+ if (v4.description && !out.description) {
254
+ out.description = v4.description;
255
+ out.source = "task-source";
256
+ out.confidence = 0.9;
257
+ }
258
+ if (v4.tags && v4.tags.length > 0) {
259
+ out.tags = Array.from(new Set([...(out.tags ?? []), ...v4.tags]));
260
+ }
261
+ if (v4.when_to_use)
262
+ hints.add(`when_to_use:${v4.when_to_use}`);
251
263
  finalizeHints(out, hints);
252
264
  }
253
265
  catch {
@@ -2,7 +2,7 @@
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
4
  /**
5
- * The `akm-task` adapter for strict task-v3 `.yml` sources.
5
+ * The `akm-task` adapter for task `.yml` sources.
6
6
  *
7
7
  * A native akm task-YAML bundle (spec §6/§7). A `.yml` file derives
8
8
  * `type: task`; conceptId strips the `.yml` extension. Tasks are AKM-native
@@ -16,11 +16,13 @@
16
16
  *
17
17
  * ── validate (spec §6 task validation column) ──
18
18
  *
19
- * Validation enters the canonical task-v3 source parser. That parser owns the
20
- * closed key sets, executable-selector XOR, scheduling-source XOR, hostile
21
- * YAML policy, trigger classification, built-in action validation, bounds,
22
- * and physical `working-directory` containment. The adapter only translates a
23
- * parser failure into the format-family diagnostic shape.
19
+ * Validation enters the canonical task source parser (`parseTaskSource`,
20
+ * task source v4 only as of P4 — a `version: 3` or `version: 2` document now
21
+ * fails closed with `TASK_SCHEMA_VERSION_UNSUPPORTED`). That parser owns the
22
+ * closed key sets, the executable-selector XOR, hostile YAML policy, the
23
+ * `akm/command` builtin, bounds, and physical `working-directory`
24
+ * containment. The adapter only translates a parser failure into the
25
+ * format-family diagnostic shape.
24
26
  *
25
27
  * Conformance oracle (authored, DO NOT modify): fixture
26
28
  * `tests/fixtures/bundles/akm-task/` + goldens
@@ -28,7 +30,8 @@
28
30
  */
29
31
  import fs from "node:fs";
30
32
  import path from "node:path";
31
- import { parseTaskV3Yaml, TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail, taskV3SourceErrorDetail, } from "../../../tasks/source-v3.js";
33
+ import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
34
+ import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail, taskSourceErrorDetail, } from "../../../tasks/source-v3.js";
32
35
  import { hashContent } from "./shared.js";
33
36
  /** A native task bundle is single-component; its one component is `main`. */
34
37
  const COMPONENT_ID = "main";
@@ -83,13 +86,13 @@ async function validate(c, changes, ctx) {
83
86
  continue;
84
87
  }
85
88
  try {
86
- parseTaskV3Yaml({ yaml: raw, filePath: relPath, workspaceRoot: c.root });
89
+ parseTaskSource({ yaml: raw, filePath: relPath, workspaceRoot: c.root });
87
90
  }
88
91
  catch (cause) {
89
92
  diagnostics.push({
90
93
  file: relPath,
91
94
  issue: "invalid-task-yaml",
92
- detail: taskV3SourceErrorDetail(cause),
95
+ detail: taskSourceErrorDetail(cause),
93
96
  fixed: false,
94
97
  });
95
98
  }
@@ -146,7 +149,7 @@ export const akmTaskAdapter = {
146
149
  continue;
147
150
  }
148
151
  try {
149
- parseTaskV3Yaml({ yaml: raw, filePath: entry.name, workspaceRoot: root });
152
+ parseTaskSource({ yaml: raw, filePath: entry.name, workspaceRoot: root });
150
153
  return true;
151
154
  }
152
155
  catch {
@@ -19,6 +19,20 @@ const CONFIG_HINTS = {
19
19
  UNKNOWN_IMPROVE_STRATEGY: "Pass one of the listed strategy names to `--strategy`, or define it under `improve.strategies`. Names are case-sensitive.",
20
20
  EXECUTION_NOT_AUTHORIZED: "Change the selected tools or update the machine/user execution policy, then retry.",
21
21
  };
22
+ // Code-review finding: COMPOSITION_INVALID covers several unrelated causes
23
+ // (a rejected with:, a multi-job source, a composition cycle/depth/size
24
+ // violation, an invalid child-output reference). USAGE_HINTS below carries
25
+ // only the with:-rejection text — accurate for every with:-rejection throw
26
+ // site (none of which pass an explicit constructor hint), but wrong for the
27
+ // others. Those throw sites pass their OWN explicit hint (the constructor's
28
+ // 3rd argument overrides USAGE_HINTS, per errors-usage-hints.test.ts's
29
+ // "explicit constructor hint still overrides the USAGE_HINTS default").
30
+ // Multi-job rejection is thrown from 5 separate call sites across
31
+ // src/tasks/prepare/prepare-support.ts and src/workflows/**, so its hint is
32
+ // centralized here as the one shared string all 5 import, rather than
33
+ // duplicated at each site and risking drift.
34
+ export const COMPOSITION_INVALID_MULTI_JOB_HINT = "AKM workflows support exactly one job per source, with no needs: between jobs. Split the extra job(s) into " +
35
+ "their own workflow file, and compose them with uses: workflows/<ref> instead.";
22
36
  /** Default hint for each UsageError code. */
23
37
  const USAGE_HINTS = {
24
38
  INVALID_FLAG_VALUE: "Run `akm <command> --help` to see accepted values.",
@@ -32,6 +46,37 @@ const USAGE_HINTS = {
32
46
  MISSING_REQUIRED_ARGUMENT: "Refs use the form [bundle//]conceptId, e.g. `akm show knowledge/guide.md` or `akm show skills/deploy`.",
33
47
  UNKNOWN_COMMAND: "Run `akm --help` to see available commands.",
34
48
  UNKNOWN_FLAG: "Run the command with `--help` to see its accepted flags.",
49
+ // P2b (docs/plans/specs/p2b-input-bindings.md §1.7 A-N5, §7 F-A3): the
50
+ // "arrives in a later 0.9.x release" promise is gone now that task-call
51
+ // inputs are implemented. Names the two real rejection causes instead:
52
+ // (1) a task target that declares no inputs: at all, (2) commands/<ref> /
53
+ // scripts/<ref>, which are never binding surfaces. F-A3 authorizes this
54
+ // edit and the matching pinned-string update in
55
+ // tests/core/errors-usage-hints.test.ts in the same commit.
56
+ //
57
+ // Code-review finding: this default is reached ONLY by with:-rejection
58
+ // throw sites (the ones above, plus task.ts's noDeclaredInputsError and
59
+ // resolve-steps.ts's rejectNonTaskBindingWith) — every other
60
+ // COMPOSITION_INVALID throw site (multi-job source, composition
61
+ // cycle/depth/size, invalid child-output reference, an env: on a
62
+ // composing step) passes its own explicit constructor hint instead of
63
+ // falling through to this text, so this stays scoped and accurate rather
64
+ // than generalized into something vaguer.
65
+ COMPOSITION_INVALID: "Remove the with: block, or target a tasks/<ref> whose source declares inputs: — commands/<ref> and scripts/<ref> steps are not binding surfaces.",
66
+ TASK_SOURCE_INVALID: "Fix the task source at the reported path and line, then re-run.",
67
+ TARGET_REF_INVALID: "Targets are canonical asset refs: `commands/review`, `scripts/build.sh`, `tasks/nightly`, `workflows/release`.",
68
+ // P4 (docs/plans/specs/p4-deletions-closeout.md §4.1, row B-53, R-R5): the
69
+ // original hint named `akm workflow validate`, a verb that was never
70
+ // implemented. Points at the two verbs that actually inspect a workflow
71
+ // source without executing it.
72
+ WORKFLOW_SOURCE_INVALID: "Run `akm lint` to see the failing source location, or `akm workflow plan <ref>` to compile it without writing.",
73
+ INPUT_BINDING_INVALID: "Check the step's with: keys against the target's declared inputs.",
74
+ TASK_TARGET_UNSUPPORTED: "Task definitions support command, script, workflow, and shell (run:) targets; akm/command is layered by callers.",
75
+ // P3a (docs/plans/specs/p3a-plan-v5-child-freeze.md §3.2, A-N2): the
76
+ // complete-or-abandon policy for a stored pre-irVersion-5 run.
77
+ WORKFLOW_IR_VERSION_UNSUPPORTED: "Abandon the run with `akm workflow abandon <id>`, then start it again from the workflow source — pre-0.9.2 frozen plans are not re-executable.",
78
+ // P3b (docs/plans/specs/p3b-child-executor.md §4.3).
79
+ WORKFLOW_OUTPUT_INVALID: "Check each `outputs:` entry's `from:` against the step artifact it names, and its `schema:` against the value that step actually promotes.",
35
80
  };
36
81
  /** Default hint for each NotFoundError code. */
37
82
  const NOT_FOUND_HINTS = {
@@ -62,10 +62,10 @@
62
62
  const MAX_DEFINITION_DEPTH = 64;
63
63
  /** Total (schema node × value node) visits one {@link validateJsonSchemaSubset} call may make. */
64
64
  const MAX_VALIDATION_NODES = 100_000;
65
- export function validateJsonSchemaSubset(value, schema) {
65
+ export function validateJsonSchemaSubset(value, schema, options) {
66
66
  const errors = [];
67
67
  const budget = { nodes: MAX_VALIDATION_NODES };
68
- validateNode(value, schema, "$", { errors, budget, depth: 0 });
68
+ validateNode(value, schema, "$", { errors, budget, depth: 0, redactValues: options?.redactValues ?? false });
69
69
  if (budget.nodes < 0) {
70
70
  errors.push(`$: schema evaluation exceeded the limit of ${MAX_VALIDATION_NODES} checks and was stopped`);
71
71
  }
@@ -390,7 +390,13 @@ function validateNode(value, schema, path, ctx) {
390
390
  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
391
391
  const allowed = schema.enum;
392
392
  if (!allowed.some((candidate) => candidate === value)) {
393
- errors.push(`${path}: value ${JSON.stringify(value)} is not one of ${JSON.stringify(allowed)}`);
393
+ // Never echo the supplied value when redacting: enum-constrained inputs
394
+ // can carry credentials, and this detail lands in stderr envelopes that
395
+ // get pasted into CI logs. The allowed list is safe — it comes from the
396
+ // author-declared schema, not from user-supplied data.
397
+ errors.push(ctx.redactValues
398
+ ? `${path}: value is not one of ${JSON.stringify(allowed)}`
399
+ : `${path}: value ${JSON.stringify(value)} is not one of ${JSON.stringify(allowed)}`);
394
400
  return;
395
401
  }
396
402
  }
@@ -409,10 +415,14 @@ function validateNode(value, schema, path, ctx) {
409
415
  }
410
416
  if ((actual === "number" || actual === "integer") && typeof value === "number") {
411
417
  if (typeof schema.minimum === "number" && value < schema.minimum) {
412
- errors.push(`${path}: ${value} is below minimum ${schema.minimum}`);
418
+ errors.push(ctx.redactValues
419
+ ? `${path}: value is below minimum ${schema.minimum}`
420
+ : `${path}: ${value} is below minimum ${schema.minimum}`);
413
421
  }
414
422
  if (typeof schema.maximum === "number" && value > schema.maximum) {
415
- errors.push(`${path}: ${value} is above maximum ${schema.maximum}`);
423
+ errors.push(ctx.redactValues
424
+ ? `${path}: value is above maximum ${schema.maximum}`
425
+ : `${path}: ${value} is above maximum ${schema.maximum}`);
416
426
  }
417
427
  return;
418
428
  }
@@ -33,6 +33,8 @@ export const STATE_MIGRATION_SAFETY_BY_ID = Object.freeze({
33
33
  "020-three-db-cutover": "additive",
34
34
  "021-asset-state-missing-since": "additive",
35
35
  "022-workflow-unit-attempts": "additive",
36
+ "023-child-workflow-runs": "additive",
37
+ "024-workflow-run-outputs": "additive",
36
38
  });
37
39
  export const STATE_MIGRATIONS = [
38
40
  // ── Migration 001 — initial schema ──────────────────────────────────────────
@@ -1022,6 +1024,61 @@ export const STATE_MIGRATIONS = [
1022
1024
  ON workflow_run_unit_attempts(run_id, status, claim_expires_at);
1023
1025
  `,
1024
1026
  },
1027
+ // ── Migration 023 — child workflow run parentage (P3a) ────────────────────
1028
+ //
1029
+ // Adds the columns and partial unique index `publishChildWorkflowRun`
1030
+ // (src/storage/repositories/workflow-runs-repository.ts) needs to record a
1031
+ // durable child workflow run under the parent unit that spawned it, and to
1032
+ // make that publication idempotent per invocation
1033
+ // (docs/plans/specs/p3a-plan-v5-child-freeze.md §5.1).
1034
+ //
1035
+ // Disambiguation: this migration's `workflow_runs.parent_unit_id` is the
1036
+ // PARENT RUN's unit that spawned this CHILD RUN — a cross-run composition
1037
+ // edge, new in P3a. It is NOT the same concept as the pre-existing
1038
+ // `workflow_run_units.parent_unit_id` (migration 004, consolidated above
1039
+ // into migration 020's `workflow_run_units` table, migrations.ts:934),
1040
+ // which records MAP FAN-OUT TEMPLATE PARENTAGE — one expanded unit's row
1041
+ // pointing back at the map-template unit it fanned out from, WITHIN a
1042
+ // single run. Same column name, two tables, two different concepts. So the
1043
+ // two are never confused at a call site, the TypeScript repository API
1044
+ // deliberately never spells this one `parentUnitId`: it is always
1045
+ // `spawnedByUnitId` on `PublishChildWorkflowRunInput` and on every child
1046
+ // run row the repository returns.
1047
+ //
1048
+ // `invocation_key` is the deterministic identity of one spawn attempt
1049
+ // (`computeChildInvocationKey`, src/workflows/exec/child-invocation.ts).
1050
+ // The unique index is partial (`WHERE parent_run_id IS NOT NULL`) because a
1051
+ // top-level run — every run before this migration, and every run started
1052
+ // directly by `akm workflow run` — has no parent and no invocation key, and
1053
+ // must never be constrained by this uniqueness rule.
1054
+ {
1055
+ id: "023-child-workflow-runs",
1056
+ up: `
1057
+ ALTER TABLE workflow_runs ADD COLUMN parent_run_id TEXT;
1058
+ ALTER TABLE workflow_runs ADD COLUMN parent_unit_id TEXT;
1059
+ ALTER TABLE workflow_runs ADD COLUMN invocation_key TEXT;
1060
+ CREATE INDEX idx_workflow_runs_parent ON workflow_runs(parent_run_id);
1061
+ CREATE UNIQUE INDEX idx_workflow_runs_invocation_key
1062
+ ON workflow_runs(parent_run_id, invocation_key)
1063
+ WHERE parent_run_id IS NOT NULL;
1064
+ `,
1065
+ },
1066
+ // ── Migration 024 — workflow run outputs (P3b) ─────────────────────────────
1067
+ //
1068
+ // Adds workflow_runs.outputs_json — the resolved map of a plan's declared
1069
+ // outputs:, persisted once at run completion in the SAME transaction as the
1070
+ // final step's completion (docs/plans/specs/p3b-child-executor.md §4.3,
1071
+ // B-N13). NULL for every run whose plan declares no outputs: (the
1072
+ // overwhelming majority, including every run before this migration) and for
1073
+ // every run that fails or blocks before completing — never `{}`.
1074
+ // `WorkflowRunsRepository.setRunOutputs` is the only writer;
1075
+ // `src/workflows/runtime/run-outputs.ts` is the only resolver.
1076
+ {
1077
+ id: "024-workflow-run-outputs",
1078
+ up: `
1079
+ ALTER TABLE workflow_runs ADD COLUMN outputs_json TEXT;
1080
+ `,
1081
+ },
1025
1082
  ];
1026
1083
  assertMigrationRegistry(STATE_MIGRATIONS);
1027
1084
  function assertStateMigrationSafetyRegistry() {
@@ -158,15 +158,17 @@ function descriptorAlias(handle) {
158
158
  }
159
159
  return undefined;
160
160
  }
161
- function sqliteBoundFilePath(handle, label) {
162
- const alias = descriptorAlias(handle);
163
- if (alias)
164
- return alias;
165
- // SQLite's Windows VFS keeps an open database pathname from being replaced;
166
- // the caller still performs identity checks immediately around every open.
167
- if (process.platform === "win32")
168
- return handle.path;
169
- throw new Error(`${label} cannot be bound to its held inode: this platform has no descriptor-backed path.`);
161
+ function sqliteBoundFilePath(handle) {
162
+ // A descriptor-backed alias (/proc/self/fd, /dev/fd) lets SQLite open the
163
+ // exact held inode even if its path gets swapped out from under it. It is
164
+ // an optimization, not the actual protection: every caller re-verifies the
165
+ // held identity (dev/ino/uid) immediately before and after every open that
166
+ // uses this path, so a plain path is safe whenever no alias is available —
167
+ // Windows never has one, and macOS's /dev/fd is a small fixed-size devfs
168
+ // table that a process holding higher fd numbers (as a bundled standalone
169
+ // binary routinely does) can miss entirely. Either way, a swap in that
170
+ // window is still caught by the surrounding identity checks.
171
+ return descriptorAlias(handle) ?? handle.path;
170
172
  }
171
173
  function closeFileIdentity(handle) {
172
174
  try {
@@ -301,16 +303,16 @@ function createHistoricalStateSafetyCopy(source, migrationId) {
301
303
  // The migration connection already holds BEGIN IMMEDIATE. A distinct
302
304
  // read-only connection bound to the held source inode can snapshot the
303
305
  // committed WAL view without trying to VACUUM from inside that transaction.
304
- reader = openDatabase(sqliteBoundFilePath(source, "state.db snapshot source"), { readonly: true });
306
+ reader = openDatabase(sqliteBoundFilePath(source), { readonly: true });
305
307
  assertStateDatabaseSource(source);
306
- reader.prepare("VACUUM INTO ?").run(sqliteBoundFilePath(reservation, "Reserved state.db safety-copy target"));
308
+ reader.prepare("VACUUM INTO ?").run(sqliteBoundFilePath(reservation));
307
309
  assertStateDatabaseSource(source);
308
310
  reader.close();
309
311
  reader = undefined;
310
312
  assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
311
313
  fs.fsyncSync(reservation.fd);
312
314
  assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
313
- const verified = openDatabase(sqliteBoundFilePath(reservation, "Reserved state.db safety-copy target"), {
315
+ const verified = openDatabase(sqliteBoundFilePath(reservation), {
314
316
  readonly: true,
315
317
  });
316
318
  try {
@@ -403,7 +405,7 @@ export function openStateDatabase(dbPath, options) {
403
405
  if (!freshReservation) {
404
406
  existingSource = openExistingStateDatabaseSource(resolvedPath);
405
407
  assertStateDatabaseSource(existingSource);
406
- const preflight = openDatabase(sqliteBoundFilePath(existingSource, "Existing state.db preflight"), {
408
+ const preflight = openDatabase(sqliteBoundFilePath(existingSource), {
407
409
  readonly: true,
408
410
  });
409
411
  try {
@@ -425,7 +427,7 @@ export function openStateDatabase(dbPath, options) {
425
427
  if (boundSource)
426
428
  assertStateDatabaseSource(boundSource);
427
429
  openedDb = openManagedDatabase({
428
- path: boundSource ? sqliteBoundFilePath(boundSource, "Managed state.db writer") : resolvedPath,
430
+ path: boundSource ? sqliteBoundFilePath(boundSource) : resolvedPath,
429
431
  pragmas: { dataDir: path.dirname(resolvedPath) },
430
432
  init: (db) => {
431
433
  if (boundSource)