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
package/STABILITY.md CHANGED
@@ -58,6 +58,7 @@ enumeration of the whole `proposal` noun group.
58
58
  | `akm curate` | Stable | |
59
59
  | `akm show` | Stable | |
60
60
  | `akm workflow status` | Stable | |
61
+ | `akm workflow plan` | Evolving | New in 0.9.2; secret-free provenance output; envelope shape may change. |
61
62
  | `akm workflow list` | Stable | |
62
63
  | `akm workflow create` | Stable | |
63
64
  | `akm workflow resume` | Stable | |
@@ -110,6 +111,7 @@ enumeration of the whole `proposal` noun group.
110
111
  | `akm task history` | Evolving | |
111
112
  | `akm task sync` | Evolving | |
112
113
  | `akm task doctor` | Evolving | |
114
+ | `akm task explain` | Evolving | New in 0.9.2; secret-shaped values in provenance output are redacted on a best-effort heuristic basis (not a guarantee). |
113
115
 
114
116
  ## Stable
115
117
 
@@ -254,11 +256,27 @@ CHANGELOG with a migration note.
254
256
  `akm improve && akm proposal drain --promote --yes`, or a `triage` block
255
257
  with `applyMode: "promote"` in your strategy.
256
258
  - **Tasks** — `akm task` subcommand surface (`add | run | sync | doctor |
257
- history`; no alias, no `list`/`remove`/`init`/`enable`/`disable`); strict
258
- version-2 YAML for scheduled tasks. Prompt tasks use named engines and task
259
- history metadata is versioned. Schema additions in patch releases; removals
260
- only at minor. Bare `akm task` is a usage error naming the subcommands
261
- (`akm task doctor` reports scheduler diagnostics).
259
+ history | explain`; no alias, no `list`/`remove`/`init`/`enable`/`disable`);
260
+ task source v4 YAML (typed `inputs:`, optional `schedule:`) is the only
261
+ accepted version task v3 and task v2 sources are converted by
262
+ `akm migrate apply`. Command tasks use named engines and task history
263
+ metadata is versioned. Schema additions in patch releases; removals only at
264
+ minor. Bare `akm task` is a usage error naming the subcommands
265
+ (`akm task doctor` reports scheduler diagnostics). `akm task explain <ref>`
266
+ (new in 0.9.2) and `akm workflow plan <ref>` are both zero-write
267
+ provenance surfaces: they show what a task or workflow would do —
268
+ resolved target, input bindings, child expansion — without starting or
269
+ publishing a run. `akm workflow plan` is secret-free **by construction**
270
+ (the excluded data never reaches the command). `akm task explain`
271
+ instead **redacts** secret-shaped input values on a best-effort
272
+ heuristic basis — a value that doesn't match the heuristic can still
273
+ print unredacted.
274
+ - **Workflow plan** — `akm workflow plan <ref>`, new in 0.9.2: zero-write
275
+ compile+freeze introspection (the canonical step graph, task/child
276
+ expansion, input bindings, and lowering notices for a workflow, without
277
+ starting or publishing a run). The envelope shape may still change; the
278
+ five long-Stable `workflow` verbs (`status`, `list`, `create`, `resume`,
279
+ `abandon`) and `run` are unaffected.
262
280
  - **Events / log** — `akm log` is the event-stream surface (0.9.0: the
263
281
  asset-scoped `akm history` surface, and `log`'s own `tail` subcommand, were
264
282
  both removed; `log` is now a leaf command — the former `list` surface).
@@ -362,16 +362,21 @@ akm task sync --rebind # Also re-pin the scheduler's akm
362
362
  akm task doctor # Scheduler binding + runtime eligibility diagnosis
363
363
  akm task history # Recent run rows (status, timing)
364
364
  akm task run <id> # Run one task immediately (works when disabled)
365
+ akm task explain <ref> # Read-only: declared inputs, target, schedule — spawns nothing
365
366
  akm search --type task # Enumerate task assets (there is no `task list`)
366
367
  ```
367
368
 
368
- Task files use strict task v3 (`version: 3`). To disable one, set
369
- `akm.enabled: false` and run `akm task sync`
370
- (the cron line stays, commented). To remove one, delete the YAML and run
371
- `akm task sync` the scheduler entry is unbound. Per-task `akm.timeout` may
372
- be `null` (disable the invocation timer) or a duration/number overriding the
373
- selected engine invocation timeout. Preview old task-v2 conversion with
374
- `akm migrate apply --dry-run`.
369
+ Task files use task source v4 (`version: 4`). There is no `akm:` options bag
370
+ or `on:` block every control (`schedule`, `timeout`, `engine`, `model`,
371
+ `redact`, `maxSteps`, `maxRetries`, ) is a top-level key now. Typed
372
+ `inputs:` declarations and a bounded `output:` schema work like a
373
+ workflow's (`output:` replaces v3's `akm.outputSchema`). To disable one
374
+ schedule entry, set that entry's `enabled: false` under `schedule:` and run
375
+ `akm task sync` (the cron line stays, commented); to remove one, delete the
376
+ YAML and run `akm task sync` — the scheduler entry is unbound. Top-level
377
+ `timeout:` may be `null` (disable the invocation timer) or a duration/number
378
+ overriding the selected engine invocation timeout. Preview old task-v2/v3
379
+ conversion with `akm migrate apply --dry-run`.
375
380
 
376
381
  ## Agent Dispatch
377
382
 
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm proposal extract --auto
3
- akm:
4
- schedule: "*/30 * * * *"
5
- enabled: true
6
- description: Opt-in all-harness session extraction every 30 min
3
+ description: Opt-in all-harness session extraction every 30 min
4
+ schedule: "*/30 * * * *"
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm improve
3
- akm:
4
- schedule: "0 2 * * *"
5
- enabled: true
6
- description: Run improve pipeline nightly
3
+ description: Run improve pipeline nightly
4
+ schedule: "0 2 * * *"
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm index
3
- akm:
4
- schedule: "0 4 * * *"
5
- enabled: true
6
- description: Nightly incremental index refresh
3
+ description: Nightly incremental index refresh
4
+ schedule: "0 4 * * *"
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm sync
3
- akm:
4
- schedule: "*/15 * * * *"
5
- enabled: true
6
- description: Sync stash changes to git remote every 15 min
3
+ description: Sync stash changes to git remote every 15 min
4
+ schedule: "*/15 * * * *"
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm upgrade --check
3
- akm:
4
- schedule: "0 9 * * 1"
5
- enabled: true
6
- description: Weekly check for new akm releases
3
+ description: Weekly check for new akm releases
4
+ schedule: "0 9 * * 1"
@@ -1,6 +1,4 @@
1
- version: 3
1
+ version: 4
2
2
  run: akm improve --strategy graph-refresh --skip-if-locked
3
- akm:
4
- schedule: "10 3 * * 0"
5
- enabled: true
6
- description: Full-corpus graph rebuild (weekly Sunday 3:10am)
3
+ description: Full-corpus graph rebuild (weekly Sunday 3:10am)
4
+ schedule: "10 3 * * 0"
@@ -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,
@@ -0,0 +1,219 @@
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
+ * `plugin-version` advisory for `akm health` (itlackey/akm#832).
6
+ *
7
+ * #828 was filed as a CLI bug — session extraction failing on 232/234 runs —
8
+ * and took a full investigation to resolve. The actual cause: the harness
9
+ * plugin (`akm@akm-plugins`, installed in Claude Code's plugin cache) was
10
+ * three days stale relative to the fix, and its own `AKM_VERSION_RANGE` gate
11
+ * had nothing to do with it — the plugin was simply running old code. Every
12
+ * fact needed to reach that conclusion in one step was already on disk:
13
+ *
14
+ * - installed plugin version: `~/.claude/plugins/cache/<marketplace>/akm/<version>/.claude-plugin/plugin.json`
15
+ * - the plugin's own akm-cli compatibility contract: `<pluginDir>/shared/akm-version.ts`'s `AKM_VERSION_RANGE`
16
+ * - the running CLI's version: `../../version.ts`'s `pkgVersion`
17
+ *
18
+ * Nothing correlated them, so a stale plugin was indistinguishable from a
19
+ * broken CLI. This module closes that gap with three checks, one per
20
+ * detected plugin:
21
+ *
22
+ * 1. report the installed version;
23
+ * 2. compare it against the newest tag published to the plugin's git
24
+ * remote, and warn (naming the update command) when behind;
25
+ * 3. the sharp one — check whether the *installed plugin's* declared
26
+ * `AKM_VERSION_RANGE` admits the *running* CLI version. When it does
27
+ * not, the plugin has silently disabled itself (both surfaces log
28
+ * `version_out_of_range` / `akm_version_mismatch` and degrade quietly)
29
+ * and there was previously no way to know that from the CLI side.
30
+ *
31
+ * Read-only: this never fetches, writes, or mutates the plugin cache or
32
+ * marketplace clone. Check 2 is the one deliberate exception to "`akm
33
+ * health` makes no network call" (see `./health-advisories.md`): a plugin's
34
+ * local marketplace clone is not proof of what is newest upstream — the
35
+ * incident above involved a clone that hadn't seen the fix's tag at all — so
36
+ * the only way to ever detect drift is to ask the remote what tags exist.
37
+ * That query is a `git ls-remote --tags` (lists refs; fetches nothing,
38
+ * writes nothing) with a short timeout, and any failure (offline, no
39
+ * remote, timeout) degrades to "installed version reported, no staleness
40
+ * claim" rather than a false positive or a hang.
41
+ *
42
+ * Every collector here is best-effort and silent on missing/unreadable
43
+ * input: no Claude plugin installed, no marketplace clone, an unreadable
44
+ * manifest, or a malformed version range must never crash `akm health` and
45
+ * must never produce a false "stale" or "inactive" warning.
46
+ */
47
+ import { spawnSync } from "node:child_process";
48
+ import fs from "node:fs";
49
+ import os from "node:os";
50
+ import path from "node:path";
51
+ import { isExactSemver, isSemverRange, maxSatisfying, satisfiesRange } from "../../registry/semver.js";
52
+ /**
53
+ * Root directory holding Claude Code's plugin cache + marketplace clones.
54
+ * Resolved per call (not memoized) so `AKM_CLAUDE_PLUGINS_DIR` can be set
55
+ * after import — the override exists so tests point this at an empty
56
+ * fixture directory instead of the real `~/.claude/plugins`, matching
57
+ * `AKM_CLAUDE_PROJECTS_DIR` in `../../integrations/harnesses/claude/session-log.ts`.
58
+ */
59
+ function claudePluginsDir() {
60
+ return process.env.AKM_CLAUDE_PLUGINS_DIR ?? path.join(os.homedir(), ".claude", "plugins");
61
+ }
62
+ /**
63
+ * Scan `<pluginsRoot>/cache/<marketplace>/akm/<version>/` for every
64
+ * `akm` plugin cache entry, picking the highest cached version per
65
+ * marketplace when more than one is present. Returns `[]` (never throws)
66
+ * when the cache directory is absent, empty, or unreadable — that is the
67
+ * ordinary "no Claude plugin installed" case, not an error.
68
+ */
69
+ function detectInstalledPlugins(pluginsRoot) {
70
+ const cacheDir = path.join(pluginsRoot, "cache");
71
+ let marketplaces;
72
+ try {
73
+ marketplaces = fs.readdirSync(cacheDir);
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ const detected = [];
79
+ for (const marketplace of marketplaces) {
80
+ const pluginDir = path.join(cacheDir, marketplace, "akm");
81
+ let versions;
82
+ try {
83
+ versions = fs.readdirSync(pluginDir).filter(isExactSemver);
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ if (versions.length === 0)
89
+ continue;
90
+ const latest = maxSatisfying(versions, "*") ?? versions.sort().at(-1);
91
+ if (!latest)
92
+ continue;
93
+ const versionDir = path.join(pluginDir, latest);
94
+ const manifestPath = path.join(versionDir, ".claude-plugin", "plugin.json");
95
+ try {
96
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
97
+ if (typeof manifest.version !== "string")
98
+ continue;
99
+ detected.push({
100
+ harness: "claude",
101
+ marketplace,
102
+ pluginName: "akm",
103
+ version: manifest.version,
104
+ pluginDir: versionDir,
105
+ });
106
+ }
107
+ catch {
108
+ // Unreadable/malformed manifest — skip this entry rather than crash.
109
+ }
110
+ }
111
+ return detected;
112
+ }
113
+ /**
114
+ * Extract `AKM_VERSION_RANGE` from the installed plugin's vendored
115
+ * `shared/akm-version.ts`. Returns `undefined` when the file is missing,
116
+ * unreadable, or does not contain the expected declaration — callers must
117
+ * treat that as "compatibility unknown", never as a mismatch.
118
+ */
119
+ function readVersionRange(pluginDir) {
120
+ const versionFilePath = path.join(pluginDir, "shared", "akm-version.ts");
121
+ let text;
122
+ try {
123
+ text = fs.readFileSync(versionFilePath, "utf8");
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ const match = text.match(/export\s+const\s+AKM_VERSION_RANGE\s*=\s*["']([^"']+)["']/);
129
+ return match?.[1];
130
+ }
131
+ const LS_REMOTE_TIMEOUT_MS = 5_000;
132
+ /**
133
+ * `git ls-remote --tags origin` against the marketplace clone's configured
134
+ * remote — lists refs only, fetches no objects, writes no local refs.
135
+ * Returns `undefined` (never throws) when the directory is not a git
136
+ * checkout, has no `origin` remote, or the command fails/times out (e.g.
137
+ * offline) — all "cannot determine availability", not "up to date".
138
+ */
139
+ const realListRemoteTags = (marketplaceDir) => {
140
+ let result;
141
+ try {
142
+ result = spawnSync("git", ["-C", marketplaceDir, "ls-remote", "--tags", "origin"], {
143
+ encoding: "utf8",
144
+ timeout: LS_REMOTE_TIMEOUT_MS,
145
+ });
146
+ }
147
+ catch {
148
+ return undefined;
149
+ }
150
+ if (result.status !== 0 || !result.stdout)
151
+ return undefined;
152
+ const tags = result.stdout
153
+ .split("\n")
154
+ .map((line) => line.trim())
155
+ .filter(Boolean)
156
+ .map((line) => line.split("\t")[1])
157
+ .filter((ref) => typeof ref === "string" && ref.startsWith("refs/tags/"))
158
+ .map((ref) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, ""))
159
+ .map((tag) => tag.replace(/^v/, ""))
160
+ .filter(isExactSemver);
161
+ return [...new Set(tags)];
162
+ };
163
+ /**
164
+ * Build one `plugin-version` advisory per detected `akm` harness plugin.
165
+ * Returns `[]` when no plugin is installed — the benign, common case.
166
+ */
167
+ export function collectPluginStalenessAdvisories(options) {
168
+ const pluginsRoot = options.pluginsRoot ?? claudePluginsDir();
169
+ const listRemoteTags = options.listRemoteTags ?? realListRemoteTags;
170
+ const plugins = detectInstalledPlugins(pluginsRoot);
171
+ return plugins.map((plugin) => buildAdvisory(plugin, options.cliVersion, pluginsRoot, listRemoteTags));
172
+ }
173
+ function buildAdvisory(plugin, cliVersion, pluginsRoot, listRemoteTags) {
174
+ const pluginRef = `${plugin.pluginName}@${plugin.marketplace}`;
175
+ // Point 2: newest available vs. installed, via the marketplace clone's remote.
176
+ const marketplaceDir = path.join(pluginsRoot, "marketplaces", plugin.marketplace);
177
+ let availableVersion;
178
+ try {
179
+ const tags = fs.existsSync(marketplaceDir) ? listRemoteTags(marketplaceDir) : undefined;
180
+ availableVersion = tags && tags.length > 0 ? maxSatisfying(tags, "*") : undefined;
181
+ }
182
+ catch {
183
+ availableVersion = undefined;
184
+ }
185
+ const stale = availableVersion !== undefined &&
186
+ plugin.version !== availableVersion &&
187
+ maxSatisfying([plugin.version, availableVersion], "*") === availableVersion;
188
+ // Point 3: does the plugin's own declared range admit the running CLI?
189
+ const versionRange = readVersionRange(plugin.pluginDir);
190
+ const rangeKnown = versionRange !== undefined && isSemverRange(versionRange);
191
+ const admitted = rangeKnown ? satisfiesRange(cliVersion, versionRange) : undefined;
192
+ const messageParts = [`${pluginRef}: installed ${plugin.version}`];
193
+ if (availableVersion !== undefined) {
194
+ messageParts.push(stale ? `available ${availableVersion} (STALE)` : `available ${availableVersion} (up to date)`);
195
+ }
196
+ if (stale)
197
+ messageParts.push(`-> claude plugin update ${pluginRef}`);
198
+ if (rangeKnown && admitted === false) {
199
+ messageParts.push(`installed plugin requires akm-cli ${versionRange}; running ${cliVersion} -> NOT ADMITTED (plugin is inactive)`);
200
+ }
201
+ return {
202
+ name: "plugin-version",
203
+ kind: "deterministic",
204
+ status: stale || admitted === false ? "warn" : "pass",
205
+ confidence: "high",
206
+ message: messageParts.join(" — "),
207
+ evidence: {
208
+ harness: plugin.harness,
209
+ marketplace: plugin.marketplace,
210
+ plugin: plugin.pluginName,
211
+ installedVersion: plugin.version,
212
+ availableVersion: availableVersion ?? null,
213
+ stale,
214
+ versionRange: versionRange ?? null,
215
+ cliVersion,
216
+ admitted: admitted ?? null,
217
+ },
218
+ };
219
+ }