akm-cli 0.9.2-alpha.3 → 0.9.2-alpha.5

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 (151) hide show
  1. package/CHANGELOG.md +664 -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/plugin-staleness.js +219 -0
  19. package/dist/commands/health/type-directory-check.js +167 -0
  20. package/dist/commands/health/windows.js +8 -4
  21. package/dist/commands/health.js +71 -9
  22. package/dist/commands/improve/extract.js +22 -1
  23. package/dist/commands/lint/index.js +1 -1
  24. package/dist/commands/migrate-cli.js +130 -24
  25. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  26. package/dist/commands/read/remember-cli.js +6 -1
  27. package/dist/commands/remember.js +70 -0
  28. package/dist/commands/tasks/explain.js +304 -0
  29. package/dist/commands/tasks/tasks-cli.js +185 -3
  30. package/dist/commands/tasks/tasks.js +233 -45
  31. package/dist/commands/workflow/plan.js +159 -0
  32. package/dist/commands/workflow-cli.js +94 -2
  33. package/dist/core/activation-policy.js +2 -12
  34. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  35. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  36. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  37. package/dist/core/errors.js +45 -0
  38. package/dist/core/json-schema.js +15 -5
  39. package/dist/core/state/migrations.js +57 -0
  40. package/dist/core/state-db.js +16 -14
  41. package/dist/core/subprocess.js +47 -13
  42. package/dist/execution/guarded-source.js +44 -0
  43. package/dist/execution/input-contract.js +250 -0
  44. package/dist/execution/target-ref.js +63 -0
  45. package/dist/indexer/usage/usage-events.js +14 -3
  46. package/dist/integrations/agent/execution-lowering.js +12 -1
  47. package/dist/integrations/harnesses/claude/session-log.js +85 -25
  48. package/dist/integrations/session-logs/pre-filter.js +152 -2
  49. package/dist/output/shapes/passthrough.js +2 -0
  50. package/dist/output/text/helpers.js +1 -1
  51. package/dist/output/text/migrate.js +12 -3
  52. package/dist/output/text/workflow-format.js +192 -10
  53. package/dist/output/text/workflow.js +2 -1
  54. package/dist/registry/semver.js +4 -0
  55. package/dist/runtime.js +1 -0
  56. package/dist/scripts/akm-migrate-node.js +11838 -10118
  57. package/dist/scripts/akm-migrate.js +11828 -10117
  58. package/dist/setup/steps/tasks.js +34 -17
  59. package/dist/storage/repositories/task-history-repository.js +5 -1
  60. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  61. package/dist/tasks/backends/launchd.js +31 -84
  62. package/dist/tasks/embedded.js +13 -7
  63. package/dist/tasks/model/invocation.js +4 -0
  64. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  65. package/dist/tasks/prepare/prepare-support.js +154 -0
  66. package/dist/tasks/prepare/prepare.js +117 -0
  67. package/dist/tasks/prepare/prepared-execution.js +4 -0
  68. package/dist/tasks/prepare/script-capture.js +80 -0
  69. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  70. package/dist/tasks/run/load-task.js +117 -0
  71. package/dist/tasks/run/provenance.js +20 -0
  72. package/dist/tasks/run/run-command-task.js +92 -0
  73. package/dist/tasks/run/run-native-task.js +222 -0
  74. package/dist/tasks/run/run-task.js +99 -0
  75. package/dist/tasks/run/run-workflow-task.js +222 -0
  76. package/dist/tasks/run/task-history.js +134 -0
  77. package/dist/tasks/run/task-log.js +179 -0
  78. package/dist/tasks/run/task-result.js +19 -0
  79. package/dist/tasks/scheduler-binding.js +66 -2
  80. package/dist/tasks/scheduler-invocation.js +63 -3
  81. package/dist/tasks/scheduler-sync.js +55 -12
  82. package/dist/tasks/source/bounded-document.js +455 -0
  83. package/dist/tasks/source/parse-task-source.js +59 -0
  84. package/dist/tasks/source/project-v4.js +62 -0
  85. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  86. package/dist/tasks/source/task-source-v4.js +626 -0
  87. package/dist/tasks/source-v3.js +10 -733
  88. package/dist/tasks/task-run-reserved-flags.js +79 -0
  89. package/dist/workflows/authoring/authoring.js +17 -8
  90. package/dist/workflows/exec/child-invocation.js +34 -0
  91. package/dist/workflows/exec/child-workflow.js +370 -0
  92. package/dist/workflows/exec/exec-unit.js +50 -170
  93. package/dist/workflows/exec/frozen-judge.js +19 -2
  94. package/dist/workflows/exec/native-executor.js +49 -27
  95. package/dist/workflows/exec/param-secrets.js +12 -0
  96. package/dist/workflows/exec/run-workflow.js +48 -59
  97. package/dist/workflows/exec/step-work.js +222 -80
  98. package/dist/workflows/exec/unit-dispatch.js +72 -0
  99. package/dist/workflows/freeze/child-output-references.js +94 -0
  100. package/dist/workflows/freeze/environment.js +174 -0
  101. package/dist/workflows/freeze/identity.js +22 -0
  102. package/dist/workflows/freeze/resolve-steps.js +78 -0
  103. package/dist/workflows/freeze/source-freeze.js +57 -0
  104. package/dist/workflows/freeze/step-values.js +68 -0
  105. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  106. package/dist/workflows/freeze/targets/command.js +81 -0
  107. package/dist/workflows/freeze/targets/script.js +57 -0
  108. package/dist/workflows/freeze/targets/shell.js +31 -0
  109. package/dist/workflows/freeze/targets/task.js +179 -0
  110. package/dist/workflows/freeze/task-bindings.js +180 -0
  111. package/dist/workflows/ir/compile.js +59 -11
  112. package/dist/workflows/ir/environment-v4.js +3 -3
  113. package/dist/workflows/ir/freeze-v4.js +41 -7
  114. package/dist/workflows/ir/params.js +58 -131
  115. package/dist/workflows/ir/plan-hash.js +3 -3
  116. package/dist/workflows/ir/schema-v4.js +246 -17
  117. package/dist/workflows/parser.js +74 -2
  118. package/dist/workflows/program/schema.js +5 -2
  119. package/dist/workflows/resource-limits.js +20 -0
  120. package/dist/workflows/runtime/plan-classifier.js +19 -5
  121. package/dist/workflows/runtime/run-outputs.js +103 -0
  122. package/dist/workflows/runtime/runs.js +114 -9
  123. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  124. package/dist/workflows/source-files.js +5 -5
  125. package/dist/workflows/source-ir/compare.js +17 -0
  126. package/dist/workflows/source-ir/compile.js +7 -3
  127. package/dist/workflows/source-ir/github-yaml.js +64 -17
  128. package/dist/workflows/source-ir/schema.js +69 -21
  129. package/dist/workflows/source-ir/semantics.js +7 -25
  130. package/dist/workflows/source-ir/triggers.js +79 -0
  131. package/dist/workflows/source-ir/uses.js +33 -7
  132. package/docs/migration/README.md +1 -1
  133. package/docs/migration/release-notes/0.9.2.md +87 -11
  134. package/docs/migration/release-notes/README.md +2 -2
  135. package/docs/migration/v0.8-to-v0.9.md +9 -7
  136. package/docs/migration/v0.9.0-troubleshooting.md +14 -7
  137. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  138. package/docs/reference/README.md +1 -1
  139. package/docs/reference/cli.md +140 -46
  140. package/docs/reference/configuration.md +3 -3
  141. package/docs/reference/supported-formats.md +9 -5
  142. package/docs/reference/tasks.md +338 -75
  143. package/docs/reference/workflow-schema.md +281 -8
  144. package/docs/reference/workflows.md +57 -7
  145. package/package.json +1 -1
  146. package/schemas/akm-task.json +173 -118
  147. package/schemas/akm-workflow.json +28 -0
  148. package/dist/tasks/runner.js +0 -941
  149. package/dist/tasks/runtime-v3.js +0 -281
  150. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  151. package/dist/workflows/source-ir/ordering.js +0 -38
@@ -0,0 +1,167 @@
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
+ * `type-directory-disagreement` advisory for `akm health` (#831).
6
+ *
7
+ * The invariant "a file's directory declares its type" is load-bearing for
8
+ * refs, namespace listings, and `akm show` paths (see #824: three files
9
+ * written to `memories/` were indexed as `type: command`, moved their refs
10
+ * under `commands/memories/<slug>`, and silently vanished from the
11
+ * `memories/` namespace — nothing on any normal surface said so). This
12
+ * advisory re-checks that invariant against every currently indexed entry.
13
+ *
14
+ * Legitimate disagreements exist by design — a `knowledge/` file containing
15
+ * `$ARGUMENTS` is deliberately a `command`, and an `agents/` file with an
16
+ * `agent:` frontmatter key is deliberately a `command` (both asserted in
17
+ * `tests/integration/commands/show.test.ts`). So this is never a hard
18
+ * failure: every disagreement is reported with a `winner` naming which
19
+ * classifier signal produced the resolved type, so a deliberate override
20
+ * reads differently from an unexplained one.
21
+ */
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
25
+ /**
26
+ * Directory → declared-type map, mirroring `DIR_TYPE_MAP` in
27
+ * `src/indexer/walk/matchers.ts` minus its per-directory extension test —
28
+ * this check only needs "which type does this directory declare", not which
29
+ * extensions it accepts. Keep in sync if `DIR_TYPE_MAP` gains, renames, or
30
+ * removes a directory.
31
+ */
32
+ const DECLARED_DIR_TYPES = {
33
+ memories: "memory",
34
+ knowledge: "knowledge",
35
+ commands: "command",
36
+ agents: "agent",
37
+ workflows: "workflow",
38
+ facts: "fact",
39
+ lessons: "lesson",
40
+ sessions: "session",
41
+ instructions: "instruction",
42
+ scripts: "script",
43
+ env: "env",
44
+ secrets: "secret",
45
+ tasks: "task",
46
+ };
47
+ const realReadFile = (absPath) => {
48
+ try {
49
+ return fs.readFileSync(absPath, "utf8");
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ };
55
+ /**
56
+ * The type a file's directory declares, mirroring the `classifyByDirectory` /
57
+ * `classifyByParentDirHint` precedence in matchers.ts: the immediate parent
58
+ * directory wins when it is itself typed (parentDirHint, specificity 15);
59
+ * otherwise the outermost typed ancestor wins (directoryMatcher, specificity
60
+ * 10, which walks root-to-leaf and returns on the first hit).
61
+ */
62
+ function declaredTypeForPath(absPath) {
63
+ const segments = path
64
+ .dirname(absPath)
65
+ .split(path.sep)
66
+ .filter((seg) => seg.length > 0);
67
+ const immediateParent = segments.at(-1);
68
+ if (immediateParent) {
69
+ const parentType = DECLARED_DIR_TYPES[immediateParent];
70
+ if (parentType)
71
+ return { dir: immediateParent, type: parentType };
72
+ }
73
+ for (const seg of segments) {
74
+ const type = DECLARED_DIR_TYPES[seg];
75
+ if (type)
76
+ return { dir: seg, type };
77
+ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * Best-effort explanation for why `classifyBySmartMd` (matchers.ts) would
82
+ * have produced `resolvedType` for this content, in the SAME precedence
83
+ * order the real function checks them. Returns `undefined` when no known
84
+ * override signal is found — that absence is itself the accident signal:
85
+ * nothing in the file explains why its type disagrees with its directory.
86
+ *
87
+ * The numeric-placeholder branch is flagged `knownGoodOverride: false` on
88
+ * purpose: since #826, that heuristic is guarded to never fire when the file
89
+ * sits under a declared-type directory, so seeing it win here would mean the
90
+ * guard regressed, not that this is a sanctioned override.
91
+ */
92
+ function explainOverride(resolvedType, content) {
93
+ const fm = parseFrontmatter(content).data;
94
+ if (fm.type === "workflow" && resolvedType === "workflow") {
95
+ return { winner: "smart-md:workflow-frontmatter", knownGoodOverride: true };
96
+ }
97
+ if ("tools" in fm && resolvedType === "agent") {
98
+ return { winner: "smart-md:tools-frontmatter", knownGoodOverride: true };
99
+ }
100
+ if ("agent" in fm && resolvedType === "command") {
101
+ return { winner: "smart-md:agent-frontmatter", knownGoodOverride: true };
102
+ }
103
+ if (resolvedType === "command" && content.includes("$ARGUMENTS")) {
104
+ return { winner: "smart-md:$ARGUMENTS", knownGoodOverride: true };
105
+ }
106
+ if (resolvedType === "command" && /\$[123](?!\d|[.,]\d)/.test(content)) {
107
+ return { winner: "smart-md:numeric-placeholder", knownGoodOverride: false };
108
+ }
109
+ if ("model" in fm && resolvedType === "agent") {
110
+ return { winner: "smart-md:model-frontmatter", knownGoodOverride: true };
111
+ }
112
+ return undefined;
113
+ }
114
+ /**
115
+ * Compare every indexed entry's resolved type against the type its
116
+ * directory declares (see {@link DECLARED_DIR_TYPES}), and return one
117
+ * {@link TypeDirectoryDisagreement} per mismatch, sorted by path. Entries
118
+ * outside any declared-type directory are not checked — this is only the
119
+ * "directory declares type" invariant.
120
+ */
121
+ export function collectTypeDirectoryDisagreements(entries, readFile = realReadFile) {
122
+ const disagreements = [];
123
+ for (const entry of entries) {
124
+ const declared = declaredTypeForPath(entry.filePath);
125
+ if (!declared || declared.type === entry.type)
126
+ continue;
127
+ const content = readFile(entry.filePath);
128
+ const explanation = content === undefined ? undefined : explainOverride(entry.type, content);
129
+ disagreements.push({
130
+ path: entry.filePath,
131
+ resolved: entry.type,
132
+ expected: declared.type,
133
+ winner: explanation?.winner ?? "unknown",
134
+ knownGoodOverride: explanation?.knownGoodOverride ?? false,
135
+ });
136
+ }
137
+ return disagreements.sort((a, b) => a.path.localeCompare(b.path));
138
+ }
139
+ const MAX_DETAIL_LINES = 10;
140
+ /**
141
+ * Build the `type-directory-disagreement` advisory, or `undefined` when
142
+ * every indexed entry agrees with its directory. Always `status: "warn"`
143
+ * (never `"fail"`) — a deliberate override is still a disagreement worth
144
+ * seeing, just not a gate.
145
+ */
146
+ export function buildTypeDirectoryAdvisory(entries, readFile = realReadFile, displayPath = (p) => p) {
147
+ const disagreements = collectTypeDirectoryDisagreements(entries, readFile);
148
+ if (disagreements.length === 0)
149
+ return undefined;
150
+ const lines = disagreements.slice(0, MAX_DETAIL_LINES).map((d) => {
151
+ const note = d.knownGoodOverride ? " (known-good override)" : "";
152
+ return `${displayPath(d.path)} resolved=${d.resolved} expected=${d.expected} winner=${d.winner}${note}`;
153
+ });
154
+ if (disagreements.length > MAX_DETAIL_LINES) {
155
+ lines.push(`+${disagreements.length - MAX_DETAIL_LINES} more`);
156
+ }
157
+ return {
158
+ name: "type-directory-disagreement",
159
+ kind: "deterministic",
160
+ status: "warn",
161
+ confidence: "high",
162
+ message: `${disagreements.length} indexed asset(s) have a resolved type that disagrees with the type their directory declares: ${lines.join("; ")}`,
163
+ evidence: {
164
+ disagreements: disagreements.map((d) => ({ ...d, path: displayPath(d.path) })),
165
+ },
166
+ };
167
+ }
@@ -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());
@@ -15,15 +15,19 @@ import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
15
15
  import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
16
16
  import { readSemanticStatus } from "../indexer/search/semantic-status.js";
17
17
  import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
18
+ import { getAllEntries } from "../storage/repositories/index-entries-repository.js";
18
19
  import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
20
+ import { pkgVersion } from "../version.js";
19
21
  import { collectImproveAdvisories } from "./health/advisories.js";
20
22
  import { HEALTH_CHECKS, runHealthEngineProbes } from "./health/checks.js";
21
- 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";
22
24
  import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
23
25
  import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
26
+ import { collectPluginStalenessAdvisories } from "./health/plugin-staleness.js";
24
27
  import { collectStashExposureAdvisory } from "./health/stash-exposure.js";
25
28
  import { collectSurfacesAdvisories } from "./health/surfaces.js";
26
29
  import { buildPerRunSummaries } from "./health/task-runs.js";
30
+ import { buildTypeDirectoryAdvisory } from "./health/type-directory-check.js";
27
31
  import { ACTIVE_RUN_WARN_MS, IMPROVE_COMPLETED_EVENT, MIN_ROWS_FOR_WORST_TASK_FAIL_RATE, } from "./health/types.js";
28
32
  import { buildWindowMetrics, computeDeltas, partitionLogBackedRows, resolveWindowCompare } from "./health/windows.js";
29
33
  const DEFAULT_SINCE_MS = 24 * 60 * 60 * 1000;
@@ -120,14 +124,18 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
120
124
  const failedTaskRows = taskRows.filter((row) => row.status === "failed");
121
125
  const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
122
126
  const stuckActiveRows = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS);
123
- const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
124
- 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) => {
125
133
  const detail = parseTaskMetadata(row).detail;
126
134
  return typeof detail?.reason === "string" && detail.reason.length > 0;
127
135
  });
128
136
  const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
129
137
  const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
130
- const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
138
+ const agentFailureRate = agentRows.length === 0 ? 0 : agentFailures.length / agentRows.length;
131
139
  return {
132
140
  tableNames,
133
141
  missingTables,
@@ -227,11 +235,12 @@ function gatherImproveSummaryPhase(db, stateDbPath, since, now) {
227
235
  return { improveSummary, perRunSummaries };
228
236
  }
229
237
  /**
230
- * The three best-effort advisory groups beyond the health-check registry:
231
- * improve advisories, the `stash-git-exposure` probe, and the 08 surfaces
232
- * group (binary-config-skew, egress-endpoints). Order matches emission order in
233
- * the returned array. A probe/filesystem failure in either try/catch must not
234
- * abort the health report — each group degrades to "no advisory" independently.
238
+ * The four best-effort advisory groups beyond the health-check registry:
239
+ * improve advisories, the `stash-git-exposure` probe, the 08 surfaces group
240
+ * (binary-config-skew, egress-endpoints), and `plugin-version` (itlackey/akm#832).
241
+ * Order matches emission order in the returned array. A probe/filesystem
242
+ * failure in any try/catch must not abort the health report — each group
243
+ * degrades to "no advisory" independently.
235
244
  */
236
245
  function gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView) {
237
246
  const advisories = [...collectImproveAdvisories(db, stateDbPath, since, improveSummary)];
@@ -269,8 +278,61 @@ function gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, optio
269
278
  catch {
270
279
  // Non-fatal.
271
280
  }
281
+ // #831: flag indexed assets whose resolved type disagrees with the type
282
+ // their containing directory declares (see health/type-directory-check.ts).
283
+ // Best-effort — an unreadable index must not abort the health report.
284
+ try {
285
+ const typeDirMismatch = detectTypeDirectoryDisagreements(options.stashDir ?? resolveStashDir());
286
+ if (typeDirMismatch)
287
+ advisories.push(typeDirMismatch);
288
+ }
289
+ catch {
290
+ // Non-fatal.
291
+ }
292
+ // itlackey/akm#832: report installed Claude Code harness plugin version(s)
293
+ // and warn when stale or when the plugin's own akm-cli version range no
294
+ // longer admits this CLI. Best-effort — no plugin installed, an unreadable
295
+ // manifest, or a network failure while checking the newest tag must not
296
+ // abort the health report.
297
+ try {
298
+ advisories.push(...collectPluginStalenessAdvisories({ cliVersion: pkgVersion }));
299
+ }
300
+ catch {
301
+ // Non-fatal.
302
+ }
272
303
  return advisories;
273
304
  }
305
+ /**
306
+ * Open index.db read-only, project every entry to `{ filePath, type }`, and
307
+ * build the `type-directory-disagreement` advisory. `stashRoot` is used only
308
+ * to shorten displayed paths (relative to the stash) when it's an ancestor of
309
+ * the entry's path; falls back to the absolute path otherwise. Returns
310
+ * `undefined` when the index is absent/unreadable or nothing disagrees —
311
+ * mirrors {@link detectIndexStateGenerationMismatch}'s best-effort shape.
312
+ */
313
+ function detectTypeDirectoryDisagreements(stashRoot) {
314
+ let indexDb;
315
+ try {
316
+ indexDb = openReadonlyExistingDatabase(getDbPath());
317
+ if (!indexDb)
318
+ return undefined;
319
+ const entries = getAllEntries(indexDb).map((entry) => ({ filePath: entry.filePath, type: entry.type }));
320
+ return buildTypeDirectoryAdvisory(entries, undefined, (absPath) => absPath.startsWith(stashRoot) ? path.relative(stashRoot, absPath) : absPath);
321
+ }
322
+ catch {
323
+ return undefined;
324
+ }
325
+ finally {
326
+ if (indexDb) {
327
+ try {
328
+ closeDatabase(indexDb);
329
+ }
330
+ catch {
331
+ // Best-effort advisory: a close failure must not abort health.
332
+ }
333
+ }
334
+ }
335
+ }
274
336
  /**
275
337
  * Detect the durable signature of an interrupted cross-database update.
276
338
  *
@@ -377,7 +377,20 @@ function runPreLlmSessionGates(args) {
377
377
  if (!force && shouldSkipAlreadyExtractedSession(prior, contentHash)) {
378
378
  return { skip: alreadyExtractedResult(harness.name, sessionRef.sessionId, prior, contentHash) };
379
379
  }
380
- const filtered = preFilterSession(data, {
380
+ // #840 harvest-without-prompting hybrid: the LLM prompt is built only from
381
+ // parent-origin events (folding stays as infrastructure for hashing above
382
+ // and inline-ref harvesting on `data.inlineRefs`, both of which still see
383
+ // the FULL folded stream). Subagent-origin events never reach
384
+ // `preFilterSession`, so #839's `dedupeTaskNotifications` naturally becomes
385
+ // a no-op on this path — a subagent's own event can no longer be in the
386
+ // kept set for a notification to be deduped against, leaving the parent's
387
+ // `<task-notification>` (the only surviving trace of that delegated work)
388
+ // untouched. See docs/plans/subagent-extraction-design.md §6.
389
+ const parentOriginData = {
390
+ ...data,
391
+ events: data.events.filter((e) => e.filePath === data.ref.filePath),
392
+ };
393
+ const filtered = preFilterSession(parentOriginData, {
381
394
  ...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
382
395
  });
383
396
  // #595/#596 — minContentChars gate: skip the LLM call for sessions whose RAW
@@ -388,6 +401,14 @@ function runPreLlmSessionGates(args) {
388
401
  // fix gated on `filtered.stats.inputCount`, which is an EVENT count, not a
389
402
  // char count — this port measures actual raw chars so the threshold matches
390
403
  // the config key's documented unit.
404
+ // #840 — deliberately measured on the FULL folded `data.events` (parent +
405
+ // subagents), not the parent-origin view above: narrowing this to
406
+ // parent-origin chars would newly skip delegation-heavy sessions with a
407
+ // thin parent transcript before extraction runs at all, even though their
408
+ // subagent work is still fully harvested via `data.inlineRefs` above. The
409
+ // full-stream measurement is today's unchanged behavior, so the worst case
410
+ // this preserves is an LLM call over a small parent-only prompt, not a
411
+ // missed extraction.
391
412
  const rawContentChars = data.events.reduce((sum, event) => sum + event.text.length, 0);
392
413
  if (minContentChars > 0 && rawContentChars < minContentChars) {
393
414
  return {
@@ -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
  });
@@ -7,7 +7,7 @@ import { defineJsonCommand, output, parseAllFlagValues } from "../../cli/shared.
7
7
  import { UsageError } from "../../core/errors.js";
8
8
  import { appendEvent } from "../../core/events.js";
9
9
  import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
10
- import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich } from "../remember.js";
10
+ import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich, synthesizeMemoryDescription, } from "../remember.js";
11
11
  import { assertFlatAssetName, inferAssetName, resolveSupersedesForWrite, resolveSupersedesWriteTarget, resolveXrefsForWrite, writeMarkdownAsset, } from "./knowledge.js";
12
12
  import { akmSearch } from "./search.js";
13
13
  // ── Helper: similar memory search ────────────────────────────────────────────
@@ -189,7 +189,9 @@ export const rememberCommand = defineJsonCommand({
189
189
  // Phase 1B / Rec 7: even the zero-flag hot-path emits
190
190
  // `captureMode: hot` + `beliefState: asserted` so user-supplied
191
191
  // memories outrank background-derived ones during ranking.
192
+ // #834: `description` is synthesized deterministically (see synthesizeMemoryDescription) so the memory is indexable.
192
193
  const frontmatterBlock = buildMemoryFrontmatter({
194
+ description: synthesizeMemoryDescription(body),
193
195
  captureMode: "hot",
194
196
  beliefState: "asserted",
195
197
  });
@@ -264,6 +266,9 @@ export const rememberCommand = defineJsonCommand({
264
266
  observed_at = enriched.observed_at;
265
267
  executionNotices = enriched.notices;
266
268
  }
269
+ // #834: no --description and no --enrich-derived one — synthesize deterministically (see zero-flag path above).
270
+ if (!description)
271
+ description = synthesizeMemoryDescription(body);
267
272
  // ── Required-field check (before any write) ───────────────────────────
268
273
  // Tags remain required when the user explicitly asked for tag-bearing
269
274
  // metadata (--tag / --enrich / --description / --source / --expires).