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
@@ -1,9 +1,9 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm improve --strategy catchup --skip-if-locked
3
- akm:
4
- schedule: "0 4 * * *"
3
+ description: Manual recovery — consolidation + triage drain (run on demand via `akm task run akm-improve-catchup`)
5
4
  # Manual-recovery task: ships disabled (the retired registerDefaultTasks
6
5
  # marked it enableMode: "manual"). `akm task run` works while disabled;
7
- # opting into the schedule is `enabled: true` + `akm task sync`.
8
- enabled: false
9
- description: Manual recovery consolidation + triage drain (run on demand via `akm task run akm-improve-catchup`)
6
+ # opting into the schedule is `schedule[].enabled: true` + `akm task sync`.
7
+ schedule:
8
+ - cron: "0 4 * * *"
9
+ enabled: false
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm improve --strategy consolidate --skip-if-locked
3
- akm:
4
- schedule: "20 */4 * * *"
5
- enabled: true
6
- description: Consolidation-only pass (every 4h at :20)
3
+ description: Consolidation-only pass (every 4h at :20)
4
+ schedule: "20 */4 * * *"
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm improve --strategy frequent --skip-if-locked
3
- akm:
4
- schedule: "40 * * * *"
5
- enabled: true
6
- description: Frequent inference pass (hourly at :40; improve-stage extract off)
3
+ description: Frequent inference pass (hourly at :40; improve-stage extract off)
4
+ schedule: "40 * * * *"
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm improve --strategy thorough --skip-if-locked
3
- akm:
4
- schedule: "15 2 * * *"
5
- enabled: true
6
- description: Full nightly quality sweep (daily 2:15am; suggested for server installs)
3
+ description: Full nightly quality sweep (daily 2:15am; suggested for server installs)
4
+ schedule: "15 2 * * *"
@@ -181,7 +181,18 @@ export function assertKnownFlags(root, rawArgs) {
181
181
  const known = collectKnownArgs(root, rawArgs);
182
182
  if (!known.resolved)
183
183
  return;
184
- const dynamicWorkflowParams = known.path.join(" ") === "workflow run";
184
+ // `workflow run`, `task run`, and `task explain` each own one deliberately
185
+ // dynamic namespace: long options become exact-name parameter/input flags
186
+ // and are checked against the frozen plan / task source's own contract
187
+ // before this gate would otherwise reject them (spec
188
+ // docs/plans/specs/p2a-task-source-v4.md §5.1 — task run's
189
+ // UNKNOWN_FLAG/INPUT_BINDING_INVALID must be raised from INSIDE the command
190
+ // body, where runWithJsonErrors renders the JSON envelope, not from this
191
+ // generic pre-dispatch gate; docs/plans/specs/p2b-input-bindings.md §4.5,
192
+ // B-55 — `task explain` reuses the identical `parseTaskInputFlags`
193
+ // scanner, so it needs the identical exemption).
194
+ const dynamicNamedFlagCommands = new Set(["workflow run", "task run", "task explain"]);
195
+ const dynamicWorkflowParams = dynamicNamedFlagCommands.has(known.path.join(" "));
185
196
  const selfDiagnosed = SELF_DIAGNOSED_FLAGS.get(known.path.join(" "));
186
197
  for (let i = 0; i < ownArgs.length; i += 1) {
187
198
  const token = ownArgs[i];
package/dist/cli.js CHANGED
@@ -557,7 +557,14 @@ export const main = defineCommand({
557
557
  },
558
558
  });
559
559
  const MAIN_TOP_LEVEL_ARGS = main.args;
560
- function isTaskRunWithId(argv) {
560
+ /**
561
+ * F-5 (spec docs/plans/specs/p2a-task-source-v4.md §6): exported so
562
+ * `akm task run`'s input-flags coverage can pin that an undeclared,
563
+ * per-task dynamic flag (e.g. `--scope all`) does not change this
564
+ * classification — it parses only `args.id` off the declared args and
565
+ * tolerates extra tokens (citty's non-strict `parseArgs`).
566
+ */
567
+ export function isTaskRunWithId(argv) {
561
568
  const args = argv.slice(2);
562
569
  const commandIndex = findCittyTopLevelCommandIndex(args, MAIN_TOP_LEVEL_ARGS);
563
570
  const command = commandIndex >= 0 ? args[commandIndex] : undefined;
@@ -14,6 +14,23 @@ import { prepareResolvedExecution } from "../../integrations/agent/execution-pre
14
14
  import { parseBuiltinCommandAction } from "./builtin-action.js";
15
15
  import { loadAdapterExecutionSource, } from "./execution-source-loader.js";
16
16
  import { applyPortableCommandArguments } from "./portable-template.js";
17
+ /**
18
+ * F-1 (spec §5.2 point 3): resolve the ambient-first provenance stamp
19
+ * (matching the native arm's own `process.env.AKM_EVENT_SOURCE ?? …`) and
20
+ * hand it to `dispatchLoweredExecutionRequest`'s dedicated, single-purpose
21
+ * `eventSource` field — never through `runOptions`/`agentOptions`, which
22
+ * stays exactly as untrusted for overriding resolved content as it was
23
+ * before P1b (a caller-supplied `runOptions.env` still cannot replace frozen
24
+ * request data, including a scheduler-restored directory value — see
25
+ * `tests/integration/tasks-runner.test.ts`'s "forwards scheduled AKM
26
+ * directory context … without trusting task or caller overrides").
27
+ */
28
+ function loweredDispatchOptions(options) {
29
+ const { eventSource, ...rest } = options;
30
+ if (eventSource === undefined)
31
+ return rest;
32
+ return { ...rest, eventSource: process.env.AKM_EVENT_SOURCE ?? eventSource };
33
+ }
17
34
  function own(value, key) {
18
35
  return Object.hasOwn(value, key);
19
36
  }
@@ -291,13 +308,17 @@ export async function dispatchPreparedCommandInvocation(prepared, options = {})
291
308
  if (!selectedEngine) {
292
309
  throw new ConfigError(`command ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "INVALID_CONFIG_FILE");
293
310
  }
294
- const result = await dispatchLoweredExecutionRequest(lowered, options);
311
+ const result = await dispatchLoweredExecutionRequest(lowered, loweredDispatchOptions(options));
295
312
  const consumedRefs = new Set();
296
313
  if (request.command.source)
297
314
  consumedRefs.add(request.command.source.ref);
298
315
  if (request.persona)
299
316
  consumedRefs.add(request.persona.source.ref);
300
- const eventSource = resolveUsageEventSource();
317
+ // F-1 (spec §5.2 point 3): options.eventSource is only a FALLBACK — an
318
+ // ambient AKM_EVENT_SOURCE still wins (D5 clause d). Absent, this is
319
+ // byte-identical to the pre-P1b bare resolveUsageEventSource() call (P-07's
320
+ // own default is "user").
321
+ const eventSource = resolveUsageEventSource(process.env, options.eventSource ?? "user");
301
322
  for (const ref of consumedRefs)
302
323
  recordIndexedShowUsage(ref, eventSource);
303
324
  const announcement = fallbackAnnouncement(prepared.fallbackEngineName, selectedEngine);
@@ -15,6 +15,44 @@ export function parseTaskMetadata(row) {
15
15
  ...(metadata.engine !== undefined ? { engine: metadata.engine } : {}),
16
16
  };
17
17
  }
18
+ /**
19
+ * D8 read-boundary predicate (spec docs/plans/specs/p1b-model-extraction.md
20
+ * §5.3) for `akm health`'s `agentFailureRate`: true for a `task_history` row
21
+ * that represents a prepared command (agent/LLM) result, across both
22
+ * vocabularies. Mirrors src/tasks/run/task-history.ts's
23
+ * `taskHistoryRowToResult` read mapping:
24
+ * - NEW rows mark themselves with metadata `targetVocab: 2` and store
25
+ * `target_kind: "command"` for the agent/LLM arm.
26
+ * - LEGACY rows (no marker, written before P1b's F-2 re-code) stored
27
+ * `target_kind: "prompt"` for the same arm — and `"command"` for the
28
+ * UNRELATED native shell/script arm, which must NOT be counted here.
29
+ * So an unmarked `"command"` row is a legacy shell/script run, not an
30
+ * agent/LLM one; a marked `"command"` row (or an unmarked `"prompt"` row) is.
31
+ */
32
+ export function isAgentTaskHistoryRow(row) {
33
+ // Check target_kind BEFORE decoding metadata: some rows in the wild
34
+ // (e.g. improve-pipeline task_history rows, target_kind "improve") carry
35
+ // metadata_json that predates the metadataVersion:2 shape entirely, and
36
+ // decodeTaskHistoryMetadata throws on that — exactly like the pre-fix
37
+ // `target_kind === "prompt"` filter, which never called it for a row this
38
+ // function isn't going to count anyway. Only decode for the two target
39
+ // kinds this predicate can return true for.
40
+ if (row.target_kind !== "command" && row.target_kind !== "prompt")
41
+ return false;
42
+ // An undecodable metadata_json is by definition unmarked: pre-P1b rows can
43
+ // carry shapes decodeTaskHistoryMetadata rejects, and `akm health` must
44
+ // classify them as legacy rather than throw (round-3 review advisory).
45
+ let marked = false;
46
+ try {
47
+ marked = decodeTaskHistoryMetadata(row.metadata_json).targetVocab === 2;
48
+ }
49
+ catch {
50
+ marked = false;
51
+ }
52
+ if (row.target_kind === "command")
53
+ return marked;
54
+ return !marked;
55
+ }
18
56
  function createUnknownImproveMetrics() {
19
57
  return {
20
58
  invoked: 0,
@@ -11,7 +11,7 @@ import { readEvents } from "../../core/events.js";
11
11
  import { buildTaskRunId, getLoggedRunIds } from "../../core/logs-db.js";
12
12
  import { DURATION_UNITS, parseDuration } from "../../core/time.js";
13
13
  import { queryTaskHistory } from "../../storage/repositories/task-history-repository.js";
14
- import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./improve-metrics.js";
14
+ import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./improve-metrics.js";
15
15
  import { readLlmUsageAggregate } from "./llm-usage.js";
16
16
  import { computeDegradationMetrics, computeDenominatorFixedCoverage } from "./metrics.js";
17
17
  import { buildPerRunSummaries } from "./task-runs.js";
@@ -148,14 +148,18 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
148
148
  const failedTaskRows = taskRows.filter((row) => row.status === "failed");
149
149
  const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
150
150
  const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
151
- const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
152
- const promptFailures = promptRows.filter((row) => {
151
+ // D8 (spec §5.3): a marked "command" row or a legacy (unmarked) "prompt"
152
+ // row is the agent/LLM arm; an unmarked "command" row is the legacy
153
+ // native shell/script arm and must not be counted here (see
154
+ // isAgentTaskHistoryRow's header comment for the full mapping).
155
+ const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
156
+ const agentFailures = agentRows.filter((row) => {
153
157
  const detail = parseTaskMetadata(row).detail;
154
158
  return typeof detail?.reason === "string" && detail.reason.length > 0;
155
159
  });
156
160
  const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
157
161
  const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
158
- const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
162
+ const agentFailureRate = agentRows.length === 0 ? 0 : agentFailures.length / agentRows.length;
159
163
  const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime()).length;
160
164
  const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
161
165
  const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
@@ -20,7 +20,7 @@ import { queryTaskHistory } from "../storage/repositories/task-history-repositor
20
20
  import { pkgVersion } from "../version.js";
21
21
  import { collectImproveAdvisories } from "./health/advisories.js";
22
22
  import { HEALTH_CHECKS, runHealthEngineProbes } from "./health/checks.js";
23
- import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./health/improve-metrics.js";
23
+ import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./health/improve-metrics.js";
24
24
  import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
25
25
  import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
26
26
  import { collectPluginStalenessAdvisories } from "./health/plugin-staleness.js";
@@ -124,14 +124,18 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
124
124
  const failedTaskRows = taskRows.filter((row) => row.status === "failed");
125
125
  const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
126
126
  const stuckActiveRows = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS);
127
- const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
128
- const promptFailures = promptRows.filter((row) => {
127
+ // D8 (spec §5.3): a marked "command" row or a legacy (unmarked) "prompt"
128
+ // row is the agent/LLM arm; an unmarked "command" row is the legacy
129
+ // native shell/script arm and must not be counted here (see
130
+ // isAgentTaskHistoryRow's header comment for the full mapping).
131
+ const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
132
+ const agentFailures = agentRows.filter((row) => {
129
133
  const detail = parseTaskMetadata(row).detail;
130
134
  return typeof detail?.reason === "string" && detail.reason.length > 0;
131
135
  });
132
136
  const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
133
137
  const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
134
- const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
138
+ const agentFailureRate = agentRows.length === 0 ? 0 : agentFailures.length / agentRows.length;
135
139
  return {
136
140
  tableNames,
137
141
  missingTables,
@@ -18,7 +18,7 @@ import { warn } from "../../core/warn.js";
18
18
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
19
19
  import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail } from "../../tasks/source-v3.js";
20
20
  import { resolveWorkflowSourceDomains } from "../../workflows/source-files.js";
21
- import { compareWorkflowSourceCodePoints } from "../../workflows/source-ir/ordering.js";
21
+ import { compareWorkflowSourceCodePoints } from "../../workflows/source-ir/compare.js";
22
22
  import { runBaseChecks } from "./base-linter.js";
23
23
  import { checkEnvForDangerousKeys } from "./env-key-rules.js";
24
24
  import { isAdvisoryLintIssue } from "./types.js";
@@ -1,44 +1,149 @@
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 { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
4
+ import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
5
5
  import { runMigrationTool } from "./migration-tool.js";
6
- /**
7
- * Run the task-only migrator and render its one JSON plan through the normal
8
- * output pipeline.
9
- */
10
- async function runMigrateSubcommand(command, args) {
11
- const result = await runMigrationTool(args);
6
+ async function callMigrateTool(args, runTool) {
7
+ const result = await runTool(args);
12
8
  if (result.stderr)
13
9
  process.stderr.write(result.stderr);
14
10
  const resultLine = result.stdout.trim();
15
- if (resultLine) {
16
- try {
17
- output(command, JSON.parse(resultLine));
18
- }
19
- catch {
20
- console.log(resultLine);
21
- }
11
+ if (!resultLine)
12
+ return { status: result.status };
13
+ try {
14
+ return { status: result.status, plan: JSON.parse(resultLine) };
15
+ }
16
+ catch {
17
+ console.log(resultLine);
18
+ return { status: result.status };
19
+ }
20
+ }
21
+ function worstStatus(left, right) {
22
+ if (left === "blocked" || right === "blocked")
23
+ return "blocked";
24
+ if (left === "ready" || right === "ready")
25
+ return "ready";
26
+ return "current";
27
+ }
28
+ /**
29
+ * Resolve one generation's contribution to the combined status — fail
30
+ * CLOSED, never open (code-review finding: this tool advertises itself as
31
+ * "blocked-not-guessed").
32
+ *
33
+ * A generation that exited SUCCESS with no plan on stdout legitimately means
34
+ * "nothing to report" and defaults to `"current"`. A generation that exited
35
+ * NON-SUCCESS (by the caller's own guard, this can only be `EXIT_CODES.
36
+ * GENERAL` — the "blocked" code) with a parsed `plan.status` reports that
37
+ * status verbatim, same as before.
38
+ *
39
+ * The gap this closes: NON-SUCCESS with NO parseable plan at all —
40
+ * `runMigrationTool` coerces a `spawnSync` `status` of `null` (the child was
41
+ * killed by a signal — OOM, a timeout, a manual kill — never scheduled to
42
+ * exit) to `1`, indistinguishable from the migrator's own legitimate
43
+ * "blocked" exit code, and truncated/malformed stdout hits the same
44
+ * `JSON.parse` catch in `callMigrateTool`. Previously `?? "current"` silently
45
+ * read a crashed generation as "nothing to migrate"; this reports it as
46
+ * `"blocked"` with an explanatory blocker instead, so the combined exit code
47
+ * (`EXIT_CODES.GENERAL` below) actually reflects that the generation's real
48
+ * state is unknown, rather than reporting success at exit 0.
49
+ */
50
+ export function resolveGenerationStatus(call, label) {
51
+ const planStatus = call.plan?.status;
52
+ if (planStatus !== undefined)
53
+ return { status: planStatus };
54
+ if (call.status !== EXIT_CODES.SUCCESS) {
55
+ return {
56
+ status: "blocked",
57
+ error: `${label}: the child process exited without printing a plan (exit status ${call.status}) — its real migration state is unknown.`,
58
+ };
22
59
  }
23
- // R-067: `process.exitCode = …; return;` (not `process.exit()`) so the
24
- // command's normal cleanup (`disposeDispatchResources()` in `runCommand`,
25
- // src/cli.ts) still runs before the process exits with the child's status.
26
- if (result.status !== 0) {
27
- process.exitCode = result.status;
60
+ return { status: "current" };
61
+ }
62
+ /**
63
+ * Run BOTH migration generations — task-v2-to-v3, then task-v3-to-task-
64
+ * source-v4 — and print one combined plan (spec
65
+ * docs/plans/specs/p4-deletions-closeout.md §3.2.5, rows B-31/B-32).
66
+ *
67
+ * Each generation is its OWN subprocess call into the standalone migrator
68
+ * (`scripts/akm-migrate.ts`'s `status`/`apply` and `task-v4-status`/
69
+ * `task-v4-apply` verbs, UNCHANGED — row B-33), so each keeps its own
70
+ * `withConfigLock` + `O_EXCL` backup root + prevalidate + TOCTOU recheck +
71
+ * atomic replace + reverse rollback + convergence check, and the two are
72
+ * NEVER interleaved. The two calls are unconditional and independent of
73
+ * each other's outcome: a blocked (or otherwise incomplete) generation-1
74
+ * result does not stop generation 2 from running against whatever is
75
+ * already task source v4 — exactly `akm-migrate status`/`task-v4-status`
76
+ * (or `apply`/`task-v4-apply`) run back to back by hand. Only a genuine
77
+ * hard failure (a status neither SUCCESS nor the "blocked" GENERAL code —
78
+ * a config error, a crash) aborts the second call, since generation 1 never
79
+ * got to look at a stable tree in that case.
80
+ */
81
+ export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runTool = runMigrationTool) {
82
+ const first = await callMigrateTool(genOneArgs, runTool);
83
+ if (first.status !== EXIT_CODES.SUCCESS && first.status !== EXIT_CODES.GENERAL) {
84
+ process.exitCode = first.status;
85
+ return;
86
+ }
87
+ const second = await callMigrateTool(genTwoArgs, runTool);
88
+ if (second.status !== EXIT_CODES.SUCCESS && second.status !== EXIT_CODES.GENERAL) {
89
+ process.exitCode = second.status;
90
+ return;
91
+ }
92
+ if (!first.plan && !second.plan) {
93
+ if (first.status !== EXIT_CODES.SUCCESS)
94
+ process.exitCode = first.status;
28
95
  return;
29
96
  }
97
+ const combined = combineMigrationPlans(first, second);
98
+ output(command, combined);
99
+ if (combined.status === "blocked")
100
+ process.exitCode = EXIT_CODES.GENERAL;
101
+ }
102
+ /**
103
+ * Merge both generations' plans into the one combined envelope the command
104
+ * prints. Deliberately PURE — every rule the combined plan encodes (the
105
+ * {@link worstStatus} rollup, the fail-closed
106
+ * {@link resolveGenerationStatus} contribution, and the blockers merge, which
107
+ * orders generation 1's own blockers after its resolution error and before
108
+ * generation 2's) is decided here from two plain values, so it is provable
109
+ * without a subprocess, a CLI dispatch, or an output-mode singleton. The
110
+ * caller keeps the only two effectful decisions: whether generation 2 runs at
111
+ * all, and the process exit code.
112
+ */
113
+ export function combineMigrationPlans(first, second) {
114
+ const firstResolved = resolveGenerationStatus(first, "task-v2-to-v3");
115
+ const secondResolved = resolveGenerationStatus(second, "task-v3-to-task-source-v4");
116
+ return {
117
+ schemaVersion: 1,
118
+ status: worstStatus(firstResolved.status, secondResolved.status),
119
+ blockers: [
120
+ ...(firstResolved.error ? [firstResolved.error] : []),
121
+ ...(first.plan?.blockers ?? []),
122
+ ...(secondResolved.error ? [secondResolved.error] : []),
123
+ ...(second.plan?.blockers ?? []),
124
+ ],
125
+ taskV3Migration: first.plan?.taskV3Migration,
126
+ taskV4Migration: second.plan?.taskV4Migration,
127
+ ...(first.plan?.backupPath !== undefined ? { backupPath: first.plan.backupPath } : {}),
128
+ ...(first.plan?.applied !== undefined ? { applied: first.plan.applied } : {}),
129
+ ...(second.plan?.backupPath !== undefined ? { taskV4BackupPath: second.plan.backupPath } : {}),
130
+ ...(second.plan?.applied !== undefined ? { taskV4Applied: second.plan.applied } : {}),
131
+ };
30
132
  }
31
133
  export const migrateCommand = defineGroupCommand({
32
- meta: { name: "migrate", description: "Inspect or apply task-v2 to task-v3 migrations" },
134
+ meta: { name: "migrate", description: "Inspect or apply task-v2 and task-v3 sources to task source v4" },
33
135
  subCommands: {
34
136
  status: defineJsonCommand({
35
- meta: { name: "status", description: "Read-only task-v2 migration check" },
137
+ meta: { name: "status", description: "Read-only task-v2 and task-v3 migration check" },
36
138
  run() {
37
- return runMigrateSubcommand("migrate-status", ["status"]);
139
+ return runMigrateSubcommand("migrate-status", ["status"], ["task-v4-status"]);
38
140
  },
39
141
  }),
40
142
  apply: defineJsonCommand({
41
- meta: { name: "apply", description: "Back up and atomically convert task-v2 files to task v3" },
143
+ meta: {
144
+ name: "apply",
145
+ description: "Back up and atomically convert task-v2 and task-v3 files to task source v4",
146
+ },
42
147
  args: {
43
148
  "dry-run": {
44
149
  type: "boolean",
@@ -47,7 +152,8 @@ export const migrateCommand = defineGroupCommand({
47
152
  },
48
153
  },
49
154
  run({ args }) {
50
- return runMigrateSubcommand("migrate-apply", ["apply", ...(args.dryRun ? ["--dry-run"] : [])]);
155
+ const dryRunFlag = args.dryRun ? ["--dry-run"] : [];
156
+ return runMigrateSubcommand("migrate-apply", ["apply", ...dryRunFlag], ["task-v4-apply", ...dryRunFlag]);
51
157
  },
52
158
  }),
53
159
  },
@@ -5,7 +5,7 @@ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
5
5
  import { parseRefInput } from "../../../core/asset/resolve-ref.js";
6
6
  import { proposalContent } from "../../../core/file-change.js";
7
7
  import { lintLessonContent } from "../../../core/lesson-lint.js";
8
- import { parseTaskV3Yaml } from "../../../tasks/source-v3.js";
8
+ import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
9
9
  import { compileWorkflowSource } from "../../../workflows/source-ir/compile.js";
10
10
  import { defaultProposalQualityValidators } from "./proposal-quality-validators.js";
11
11
  const genericProposalValidator = {
@@ -52,7 +52,12 @@ const canonicalProposalValidators = {
52
52
  const name = ctx.parsedRef?.name;
53
53
  if (!name)
54
54
  return [];
55
- parseTaskV3Yaml({
55
+ // Version-routing seam (spec docs/plans/specs/p2a-task-source-v4.md
56
+ // §3.6): a proposal body is validated by parsing alone — neither arm's
57
+ // parsed document is inspected further, so routing through the union is
58
+ // a pure swap; any parse failure (either version) is turned into an
59
+ // `invalid-task-structure` finding by the try/catch this call sits in.
60
+ parseTaskSource({
56
61
  yaml: proposalContent(proposal),
57
62
  filePath: proposal.changes[0]?.path || proposal.ref,
58
63
  });