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.
- package/CHANGELOG.md +664 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/plugin-staleness.js +219 -0
- package/dist/commands/health/type-directory-check.js +167 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +71 -9
- package/dist/commands/improve/extract.js +22 -1
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/read/remember-cli.js +6 -1
- package/dist/commands/remember.js +70 -0
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +233 -45
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/integrations/harnesses/claude/session-log.js +85 -25
- package/dist/integrations/session-logs/pre-filter.js +152 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/registry/semver.js +4 -0
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +55 -12
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +19 -5
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +2 -2
- package/docs/migration/v0.8-to-v0.9.md +9 -7
- package/docs/migration/v0.9.0-troubleshooting.md +14 -7
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +3 -3
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +281 -8
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- 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`);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
`
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
`akm
|
|
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:
|
|
1
|
+
version: 4
|
|
2
2
|
run: akm proposal extract --auto
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
1
|
+
version: 4
|
|
2
2
|
run: akm improve --strategy graph-refresh --skip-if-locked
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
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
|
-
|
|
9
|
-
|
|
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:
|
|
1
|
+
version: 4
|
|
2
2
|
run: akm improve --strategy consolidate --skip-if-locked
|
|
3
|
-
|
|
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:
|
|
1
|
+
version: 4
|
|
2
2
|
run: akm improve --strategy frequent --skip-if-locked
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
1
|
+
version: 4
|
|
2
2
|
run: akm improve --strategy thorough --skip-if-locked
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|