akm-cli 0.9.10 → 0.9.12

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 (145) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/STABILITY.md +28 -15
  3. package/dist/assets/hints/cli-hints-full.md +1 -1
  4. package/dist/assets/improve-strategies/consolidate.json +1 -1
  5. package/dist/assets/improve-strategies/default.json +1 -1
  6. package/dist/assets/improve-strategies/thorough.json +1 -2
  7. package/dist/cli/shared.js +16 -4
  8. package/dist/cli.js +15 -13
  9. package/dist/commands/agent/agent-dispatch.js +8 -0
  10. package/dist/commands/command/execution-source-loader.js +25 -22
  11. package/dist/commands/command/portable-template.js +4 -26
  12. package/dist/commands/config-cli.js +10 -4
  13. package/dist/commands/env/env-binding.js +10 -3
  14. package/dist/commands/env/env-cli.js +7 -0
  15. package/dist/commands/env/secret-cli.js +15 -4
  16. package/dist/commands/health/checks.js +209 -78
  17. package/dist/commands/health/improve-metrics.js +12 -0
  18. package/dist/commands/health.js +16 -4
  19. package/dist/commands/improve/distill/quality-gate.js +15 -7
  20. package/dist/commands/improve/distill.js +28 -12
  21. package/dist/commands/improve/eval-cases.js +9 -2
  22. package/dist/commands/improve/execution.js +1 -2
  23. package/dist/commands/improve/extract.js +82 -56
  24. package/dist/commands/improve/improve-strategies.js +26 -8
  25. package/dist/commands/improve/improve.js +32 -4
  26. package/dist/commands/improve/loop-stages.js +13 -3
  27. package/dist/commands/improve/preparation.js +9 -6
  28. package/dist/commands/improve/reflect.js +61 -77
  29. package/dist/commands/lint/index.js +3 -1
  30. package/dist/commands/migrate-cli.js +6 -4
  31. package/dist/commands/proposal/drain-policies.js +22 -2
  32. package/dist/commands/proposal/repository.js +4 -4
  33. package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
  34. package/dist/commands/proposal/validators/proposals.js +10 -19
  35. package/dist/commands/read/show.js +42 -31
  36. package/dist/commands/registry-cli.js +4 -2
  37. package/dist/commands/sources/init.js +4 -8
  38. package/dist/commands/sources/self-update.js +2 -2
  39. package/dist/commands/sources/source-clone.js +5 -7
  40. package/dist/commands/sources/sources-cli.js +3 -5
  41. package/dist/commands/tasks/tasks-cli.js +36 -12
  42. package/dist/commands/tasks/tasks.js +38 -35
  43. package/dist/commands/tasks/validate.js +186 -0
  44. package/dist/commands/url-checker.js +75 -16
  45. package/dist/commands/workflow-cli.js +17 -15
  46. package/dist/core/activation-policy.js +31 -3
  47. package/dist/core/adapter/execution-source.js +39 -11
  48. package/dist/core/asset/stash-meta.js +7 -41
  49. package/dist/core/bundle-id.js +7 -1
  50. package/dist/core/common.js +8 -17
  51. package/dist/core/config/config-schema.js +3 -23
  52. package/dist/core/config/config-walker.js +50 -4
  53. package/dist/core/config/config.js +21 -5
  54. package/dist/core/config/legacy-source-shape-shim.js +79 -0
  55. package/dist/core/config/schema/embedding.js +2 -2
  56. package/dist/core/config/schema/engines.js +19 -2
  57. package/dist/core/config/schema/index-config.js +19 -21
  58. package/dist/core/config/schema/primitives.js +21 -10
  59. package/dist/core/config/schema/sources-bundles.js +1 -6
  60. package/dist/core/errors.js +2 -3
  61. package/dist/core/improve-result.js +8 -0
  62. package/dist/core/improve-types.js +17 -0
  63. package/dist/core/json-schema.js +1 -11
  64. package/dist/core/maintenance-barrier.js +17 -2
  65. package/dist/core/paths.js +124 -15
  66. package/dist/core/state/migrations.js +28 -0
  67. package/dist/core/state-db.js +28 -1
  68. package/dist/core/write-source.js +6 -6
  69. package/dist/indexer/bundle-identity-guard.js +3 -0
  70. package/dist/indexer/ensure-index.js +5 -0
  71. package/dist/indexer/indexer.js +11 -3
  72. package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
  73. package/dist/indexer/passes/metadata.js +16 -5
  74. package/dist/indexer/search/search-fields.js +1 -30
  75. package/dist/indexer/search/search-source.js +3 -2
  76. package/dist/integrations/agent/engine-resolution.js +107 -4
  77. package/dist/integrations/agent/execution-lowering.js +15 -2
  78. package/dist/integrations/agent/model-map.js +16 -10
  79. package/dist/integrations/agent/prompts.js +13 -6
  80. package/dist/integrations/agent/runner-dispatch.js +16 -3
  81. package/dist/integrations/agent/runner.js +2 -0
  82. package/dist/integrations/lockfile.js +22 -7
  83. package/dist/llm/client.js +16 -0
  84. package/dist/llm/index-passes.js +3 -2
  85. package/dist/output/shapes/passthrough.js +10 -3
  86. package/dist/output/shapes.js +5 -3
  87. package/dist/output/text/workflow-format.js +8 -1
  88. package/dist/scripts/akm-migrate-node.js +2028 -1613
  89. package/dist/scripts/akm-migrate.js +2027 -1612
  90. package/dist/setup/setup.js +14 -21
  91. package/dist/sources/include.js +150 -20
  92. package/dist/sources/providers/git-install.js +14 -12
  93. package/dist/sources/providers/git-provider.js +3 -3
  94. package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
  95. package/dist/sources/website-url.js +12 -4
  96. package/dist/storage/engines/sqlite-migrations.js +40 -10
  97. package/dist/storage/like-pattern.js +7 -0
  98. package/dist/storage/repositories/extract-sessions-repository.js +23 -0
  99. package/dist/storage/repositories/index-connection.js +27 -10
  100. package/dist/storage/repositories/index-entry-schema.js +19 -2
  101. package/dist/storage/repositories/index-schema.js +30 -9
  102. package/dist/storage/repositories/proposals-repository.js +2 -1
  103. package/dist/storage/repositories/task-history-repository.js +14 -7
  104. package/dist/storage/repositories/workflow-runs-repository.js +15 -1
  105. package/dist/storage/sqlite-read-snapshot.js +11 -9
  106. package/dist/tasks/backends/cron.js +34 -5
  107. package/dist/tasks/backends/launchd.js +23 -26
  108. package/dist/tasks/backends/schtasks.js +50 -3
  109. package/dist/tasks/frozen-script.js +2 -0
  110. package/dist/tasks/prepare/prepare.js +2 -7
  111. package/dist/tasks/prepare/script-capture.js +38 -6
  112. package/dist/tasks/schedule.js +154 -13
  113. package/dist/tasks/scheduler-sync.js +51 -25
  114. package/dist/tasks/source/task-source-v3-frozen.js +0 -1
  115. package/dist/tasks/source/task-source-v4.js +0 -1
  116. package/dist/workflows/exec/child-workflow.js +2 -3
  117. package/dist/workflows/exec/dispatch-redaction.js +21 -7
  118. package/dist/workflows/exec/exec-unit.js +3 -4
  119. package/dist/workflows/exec/run-workflow.js +19 -10
  120. package/dist/workflows/exec/step-work.js +35 -56
  121. package/dist/workflows/freeze/resolve-steps.js +19 -11
  122. package/dist/workflows/freeze/source-freeze.js +7 -0
  123. package/dist/workflows/freeze/targets/child-workflow.js +12 -18
  124. package/dist/workflows/freeze/targets/command.js +14 -2
  125. package/dist/workflows/ir/environment-v4.js +4 -2
  126. package/dist/workflows/ir/freeze-v4.js +2 -5
  127. package/dist/workflows/ir/plan-hash.js +0 -3
  128. package/dist/workflows/ir/schema-v4.js +14 -9
  129. package/dist/workflows/ir/schema.js +1 -3
  130. package/dist/workflows/resource-limits.js +35 -48
  131. package/dist/workflows/runtime/plan-classifier.js +89 -41
  132. package/dist/workflows/runtime/run-outputs.js +1 -21
  133. package/dist/workflows/runtime/runs.js +76 -150
  134. package/dist/workflows/source-files.js +28 -54
  135. package/dist/workflows/source-ir/program.js +2 -2
  136. package/dist/workflows/source-ir/semantics.js +5 -23
  137. package/docs/integration/bundling-akm.md +1 -1
  138. package/docs/migration/v0.8-to-v0.9.md +32 -0
  139. package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
  140. package/docs/reference/cli.md +105 -22
  141. package/docs/reference/configuration.md +12 -2
  142. package/docs/reference/data-and-telemetry.md +1 -1
  143. package/docs/reference/tasks.md +8 -0
  144. package/package.json +1 -1
  145. package/schemas/akm-config.json +11 -8
package/CHANGELOG.md CHANGED
@@ -4,6 +4,232 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.9.12] - 2026-09-03
8
+
9
+ ### Added
10
+
11
+ - **`akm health` probes LLM engine reachability (#914).** `default-llm-engine`
12
+ and `configured-engines` now send one bounded `GET` to the endpoint's
13
+ `/models` route (3 s timeout, one probe per distinct endpoint per
14
+ invocation, no cross-run cache) instead of only checking that a credential is
15
+ present. Any HTTP response counts as reachable, so a cold local server is
16
+ never asked to load a model just to be checked. An unreachable default LLM engine is a hard `fail` naming the
17
+ connection error; an unreachable non-default engine is a `warn`. Pass
18
+ `--no-probe` on an offline or air-gapped host to keep the credential-only
19
+ verdict; the message then says reachability was not probed.
20
+ - **`akm proposal extract` reports the engine it resolved (#913).** The
21
+ envelope carries `engine` and `engineKind` (`llm`, `sdk`, or `agent`), every
22
+ `sessions[]` entry carries `engine`, and the `extract_sessions_seen` ledger
23
+ metadata records it, so "which engine did that run actually use" is one
24
+ field instead of config archaeology. `akm health`'s `active-improve-strategy`
25
+ check names the engine each improve process resolved to, which makes a
26
+ strategy-level `engine` pin that shadows `defaults.llmEngine` visible.
27
+ - **`akm workflow run <ref>` says when it resumes, and `--new` starts fresh
28
+ (#919).** Resolving a ref to an already-active run in the current scope is
29
+ unchanged, but the `workflow-run` envelope now carries `resumed: true` and
30
+ the text output leads with `resuming existing run <id> for <ref>; pass --new
31
+ to start a fresh run`. `--new` starts a second run and leaves the active one
32
+ untouched. `status`, `abandon`, `resume`, and `run <id>` all accept a unique
33
+ run-id prefix of eight or more characters.
34
+
35
+ ### Changed
36
+
37
+ - **A run that skipped every session for an infrastructure reason is visible
38
+ in the extract envelope (#912).** `warnings[]` gains one aggregate line per
39
+ infrastructure skip reason (`llm_unavailable`, `read_failed`, `exception`,
40
+ `locked_concurrent`), for example `25 of 25 sessions skipped: llm_unavailable
41
+ (engine "default")`, and a `skipReasons` count map is present whenever
42
+ `sessionsSkipped > 0`. `ok` keeps meaning "the command ran" and the exit code
43
+ does not change; that meaning is now written down on the type.
44
+ - **`akm health`'s `session-extraction` check reads the extraction ledger
45
+ (#914).** It used to read only `improve_runs`, which the hook-driven
46
+ `akm proposal extract --session-id` never writes, so a plugin-driven machine
47
+ reported "not active" as `pass` forever. It now derives its verdict from the
48
+ last seven days of `extract_sessions_seen`: `unknown` when nothing was
49
+ recorded, `warn` naming the reason and engine when every session was skipped
50
+ or failed for an infrastructure reason (`llm_unavailable`, `read_failed`,
51
+ `exception`, `locked_concurrent`), otherwise `pass` with per-outcome counts.
52
+ - **Every passthrough success envelope carries `ok: true` (#918).**
53
+ `akm config set` and `akm config unset` printed the resulting config with no
54
+ `ok` field while their failure envelope had `ok: false`, so a caller
55
+ branching on `.ok` read success as failure. The shared passthrough stamp now
56
+ adds `ok: true` when a result has no `ok` of its own; commands whose exit
57
+ code grades the outcome (`task sync`, `task sync --dry-run`, `task prune`,
58
+ `workflow run`, `upgrade`, and the `migrate` subcommands) set `ok` and the
59
+ exit code from the same value, so the two cannot disagree. `--silent` still
60
+ prints nothing.
61
+ - **The unsupported-plan error names the real situation (#919).** A frozen
62
+ workflow plan with an `irVersion` above the current one no longer reports
63
+ "pre-irVersion-5 ... after the 0.9.2 upgrade"; it says the plan was probably
64
+ written by a newer akm. The reported `irVersion 111` did not reproduce
65
+ against this tree (the column is only ever written as `5` and no path
66
+ rewrites it on a source edit); a regression test pins that editing a
67
+ workflow source leaves its in-flight run executable.
68
+
69
+ ### Fixed
70
+
71
+ - **`akm proposal accept` no longer rewrites your content.** Two "repairs" ran
72
+ before validation: one deleted every body line matching `description:` or
73
+ `when_to_use:`, the other deleted every `---` in a body that had
74
+ frontmatter. Both fired inside fenced code blocks, so any asset documenting
75
+ frontmatter — a note about akm, Claude Code skills, Jekyll, Hugo — was
76
+ silently gutted on accept, and the rewritten bytes were saved back over the
77
+ original in the proposals database. Nothing was printed. Both repairs are
78
+ gone; the truncated-description repair, which only ever rewrote a
79
+ frontmatter value, stays.
80
+ - **Prose-quality findings no longer block `proposal accept`.** A description
81
+ that read like a heading, an odd number of backticks, or a reflect revision
82
+ outside the size ratio refused the promotion and told the user to "fix the
83
+ proposal payload and try again" — but there is no `akm proposal edit` and
84
+ `accept` has no `--force`, so the only way out was hand-editing the
85
+ proposals database. These findings are now reported as warnings on a
86
+ command a human typed. Structural defects that genuinely cannot be written
87
+ (empty content, an unparseable ref, malformed frontmatter, a broken
88
+ workflow shape) still block.
89
+ - **akm no longer refuses to open a state database migrated by a newer akm.**
90
+ Two akm versions sharing one data directory is a supported deployment — a
91
+ bundled CLI beside a newer global install — and the old binary was bricked
92
+ for every command that touches `state.db`, not degraded. It protected
93
+ nothing: an older binary's entire migration registry is already applied, so
94
+ it has no pending migration to run. It now opens, warns once naming the
95
+ migrations it does not know, and reads and writes the tables it knows. A
96
+ ledger that genuinely diverges (a migration this akm has was never applied
97
+ and something else was applied in its place) is still refused.
98
+ - **`akm agent --prompt` no longer rejects prose that looks like a template.**
99
+ A prompt was validated as if it were a portable command template, so any
100
+ prompt containing `}}` from compact JSON, a `$VAR`, a `${...}`, a `$(...)`
101
+ shell snippet, an `@path`, or a `` !` `` was rejected with "unsupported
102
+ portable template construct" before the agent started. akm substitutes
103
+ nothing into a prompt, so it is now sent verbatim. The template language is
104
+ unchanged for stored command files, which are templates.
105
+
106
+ - **The 0.9.2 `claude-code` -> `claude` harness rename left the ledgers split
107
+ (#915).** State migration `027-extract-sessions-seen-harness-rename` moves
108
+ `extract_sessions_seen` and `workflow_runs.agent_harness` rows off the old
109
+ key, keeping a session already recorded under `claude` as the authoritative
110
+ row, and empties the old key space. `PERSISTED_HARNESS_IDS` in the harness
111
+ registry is now pinned by a test so the next rename cannot ship without a
112
+ migration. Scripts querying the ledger by `claude-code` will find it empty
113
+ after upgrade; see the 0.9.1 -> 0.9.2 migration guide.
114
+
115
+ ### Changed — refusals that now degrade
116
+
117
+ A repo-wide audit reviewed every defensive refusal in the codebase against
118
+ three tests: has it demonstrably helped a real user, does its failure mode cost
119
+ less than the hazard it guards, and is the hazard already gated behind a
120
+ deliberate human command. Refusals that failed those tests were removed or
121
+ downgraded to a warning, in that order of preference. Machinery that prevents
122
+ data loss or corruption — atomic writes, backups, write-path validation, path
123
+ containment — was explicitly out of scope and is unchanged.
124
+
125
+ The user-visible effect is that akm stops aborting on conditions it can
126
+ survive. Highlights:
127
+
128
+ - **Config load no longer bricks every command over one bad key.** Retired
129
+ vocabulary (`profiles`, `llm`, `agent`, `features`, `stashes`,
130
+ `modelAliases`, `bindings`, top-level `writable`) warns and passes through,
131
+ and the pre-`bundles` `stashDir`/`sources[]`/`installed[]` shape folds into
132
+ the current shape in memory. An unknown `config set` key warns and stores.
133
+ - **A newer or unfamiliar state.db migration ledger no longer refuses to
134
+ open.** A ledger carrying migrations this binary does not know warns once and
135
+ degrades; only a genuinely inconsistent ledger still aborts. This is what let
136
+ a bundled older akm keep working against a newer host's data directory.
137
+ A `plan_ir_version` of NULL (a row predating the column) decodes normally.
138
+ - **Free text is no longer validated as code.** `akm agent` accepts prompts
139
+ containing `{{`/`}}`; inline workflow `akm/command` content and portable
140
+ command templates accept `$HOME`, `@file`, `${...}` and the rest as the prose
141
+ they are. Only `$ARGUMENTS[N]`, which merely looks like the one placeholder
142
+ akm expands, still warns.
143
+ - **Version skew stops being treated as corruption.** A newer index is left
144
+ alone rather than wiped, a stale indexed workflow identity falls back, and
145
+ frozen-plan spine drift from a formatting change between releases warns
146
+ instead of marking every in-flight run corrupt.
147
+ - **Deliberate commands stop being second-guessed.** `--since` bypasses
148
+ `extract`'s per-run cap the way `--force` already did; `workflow create
149
+ --force` no longer also demands `--reset`; scheduler writes, `setup --dir`
150
+ and `bundle create --dir` under transient paths warn instead of refusing.
151
+ - **Symlinks are followed on read paths** (task sources, `models.json`, stash
152
+ meta, `akm.include` entries) with realpath containment doing the actual
153
+ safety work, rather than being refused outright.
154
+ - **Blocks that had no escape hatch got one, or got removed.** `env run` gained
155
+ `--allow-insecure` for the third-party dangerous-key block that previously
156
+ had no bypass; `registry add` accepts a credentialed URL with a warning that
157
+ redacts the credential; promotion lint findings on `proposal accept` report
158
+ instead of blocking, since there is no `proposal edit` and no `accept
159
+ --force` to work around them.
160
+ - **Limits that bounded nothing real were deleted.** Workflow plan and embedded
161
+ child-plan byte caps, step-evidence tombstoning, search-text truncation, and
162
+ the JSON-schema node budget are gone; the akm.lock acquisition budget went
163
+ from 300 ms to ~30 s with backoff and a "waiting for another akm process"
164
+ notice.
165
+ - **A cron line too long for vixie-cron now spills into a wrapper script**
166
+ instead of refusing the install. The length limit stays, because a truncated
167
+ cron line would execute a partial command.
168
+
169
+ Guards that passed the three tests were kept and documented, including the
170
+ registry-URL credential inspection limit, the third-party dangerous-key block
171
+ itself, `MAX_SCHTASKS_TRIGGERS`, and every path-containment check.
172
+
173
+ ## [0.9.11] - 2026-09-03
174
+
175
+ ### Added
176
+
177
+ - **`akm task validate <path>` reports what `akm task sync` would say about one
178
+ task file (#907).** It parses a single file by filesystem path, without a
179
+ bundle, a concept ref, or a configured engine, and reports `valid`,
180
+ `converts` (a v2/v3 source the migrator converts deterministically),
181
+ `blocked` (needs a human decision), `invalid`, or `not-a-task`, with the
182
+ reason sync would give. It runs the same two gates sync runs before
183
+ installing a schedule, the defaults-applied schedule input contract and
184
+ the cron dialect check, which are now shared with `compileTaskSources` so
185
+ the two commands cannot drift. Exit 0 for valid/converts, 1 for the rest,
186
+ 2 for a missing or unreadable path. `resolved` on success is the compiled
187
+ task shape (id, version, target, inputs, schedule), never an
188
+ execution-lowered plan.
189
+ - **`engines.<name>.apiKeyFile` supplies an LLM engine credential from a file
190
+ (#905).** A host that refuses secrets in the process environment can point
191
+ an engine at a path instead of a `$VAR` reference: `~` expands, one
192
+ trailing newline is trimmed, the file is read only at dispatch, and a
193
+ missing or empty file is a config error naming the engine and path but
194
+ never the value. Setting both `apiKey` and `apiKeyFile` is rejected;
195
+ setting neither still falls through to the implicit
196
+ `AKM_ENGINE_<NAME>_API_KEY` convention. The value is redacted from
197
+ dispatch output the same way an env-backed credential is, and `akm health`
198
+ checks the file is present and non-empty.
199
+
200
+ ### Changed
201
+
202
+ - **`akm improve`'s machine-local writers moved out of `$STASH/.akm` into
203
+ `$STATE` and `$CACHE` (#890).** `distill-rejected/`, `eval-cases/`,
204
+ `measurement/verdicts/`, the synthetic `unresolved-sources/` placeholder,
205
+ and the improve-pipeline lock files never met the "must travel with the
206
+ content" rule; they now live under `$STATE/improve/…/<stash>/`,
207
+ `$CACHE/index/unresolved-sources/<stash>/`, and `$STATE/locks/<stash>/`,
208
+ namespaced by a short hash of the resolved stash path. `akm migrate
209
+ status` and `apply` gain a relocation step that covers the default stash
210
+ and every other filesystem-backed bundle (remote sources are skipped
211
+ without a network call), moves files by rename or copy-then-delete, and
212
+ deletes an old lock only when the same staleness check `akm improve` uses
213
+ says its holder is dead; a live lock is left in place and reported. The
214
+ step is idempotent and `--dry-run` moves nothing. Scripts that read the
215
+ old paths should switch to the new locations named in
216
+ `docs/architecture/internals/storage-locations.md`.
217
+
218
+ ### Fixed
219
+
220
+ - **The dead-link check states its coverage and no longer lies about it
221
+ (#892).** `akm improve`'s post-loop check scanned at most ten knowledge
222
+ refs and fired every HEAD request at once with no timeout. It now scans
223
+ every actionable ref, checks at a bounded concurrency, bounds each request
224
+ at five seconds and reports a timeout as a dead entry, and counts a DNS or
225
+ connection failure as skipped rather than as dead or as fine. The result
226
+ carries `checked`, `total`, and `skipped`, threaded into the improve
227
+ result, the `improve_completed` event, and `akm health`'s improve summary.
228
+ The other constants the issue names (`MAX_URLS`, the per-entry slice,
229
+ `MAX_BODY_CHARS`, the duplicated `MAX_CONTENT_CHARS`) were already removed
230
+ in 0.9.8, and the curate score floors only decide whether a second search
231
+ pass runs; they never filter returned results.
232
+
7
233
  ## [0.9.10] - 2026-09-02
8
234
 
9
235
  ### Fixed
package/STABILITY.md CHANGED
@@ -112,6 +112,7 @@ enumeration of the whole `proposal` noun group.
112
112
  | `akm task sync` | Evolving | |
113
113
  | `akm task doctor` | Evolving | |
114
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). |
115
+ | `akm task validate` | Evolving | New in 0.9.11; read-only, and the only `task` subcommand that takes a bare filesystem path instead of a ref — the file need not belong to any configured bundle. |
115
116
 
116
117
  ## Stable
117
118
 
@@ -218,6 +219,10 @@ enumeration of the whole `proposal` noun group.
218
219
  | `4` | Health warning (`akm health` only) |
219
220
  | `70` | Internal / unclassified |
220
221
  | `78` | Configuration error |
222
+
223
+ **From 0.9.12**, every success envelope produced by the passthrough stamp
224
+ (`config`, `clone`, `models`, `task-*`, `workflow-*`, `registry-*`, …) also
225
+ carries `ok: true`; a command that already computes its own `ok` keeps it.
221
226
  - **Install scripts** — `install.sh` and `install.ps1` URLs; the `--prefix`
222
227
  / `AKM_INSTALL_DIR` environment override.
223
228
  - **Runtime** — the npm package requires Node.js >= 22 as its bootstrap and
@@ -256,21 +261,28 @@ CHANGELOG with a migration note.
256
261
  `akm improve && akm proposal drain --promote --yes`, or a `triage` block
257
262
  with `applyMode: "promote"` in your strategy.
258
263
  - **Tasks** — `akm task` subcommand surface (`add | run | sync | doctor |
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
264
+ history | explain | validate`; no alias, no
265
+ `list`/`remove`/`init`/`enable`/`disable`); task source v4 YAML (typed
266
+ `inputs:`, optional `schedule:`) is the only accepted version — task v3 and
267
+ task v2 sources are converted by `akm migrate apply`. Command tasks use
268
+ named engines and task history metadata is versioned. Schema additions in
269
+ patch releases; removals only at minor. Bare `akm task` is a usage error
270
+ naming the subcommands (`akm task doctor` reports scheduler diagnostics).
271
+ `akm task explain <ref>` (new in 0.9.2) and `akm workflow plan <ref>` are
272
+ both zero-write provenance surfaces: they show what a task or workflow
273
+ would do — resolved target, input bindings, child expansion — without
274
+ starting or publishing a run. `akm workflow plan` is secret-free **by
275
+ construction** (the excluded data never reaches the command). `akm task
276
+ explain` instead **redacts** secret-shaped input values on a best-effort
272
277
  heuristic basis — a value that doesn't match the heuristic can still
273
- print unredacted.
278
+ print unredacted. `akm task validate <path>` (new in 0.9.11) is the same
279
+ kind of zero-write introspection as `explain`, but takes a bare filesystem
280
+ path rather than a bundle-qualified ref — it reports whether that ONE file
281
+ would parse cleanly (`valid`), auto-convert from task v2/v3 (`converts`),
282
+ need a human decision the deterministic migrator can't make (`blocked`),
283
+ fail schema validation (`invalid`), or isn't a task source at all
284
+ (`not-a-task`) — exactly the diagnostic `akm task sync` would produce for
285
+ it, before the file is ever wired into a bundle or the scheduler.
274
286
  - **Workflow plan** — `akm workflow plan <ref>`, new in 0.9.2: zero-write
275
287
  compile+freeze introspection (the canonical step graph, task/child
276
288
  expansion, input bindings, and lowering notices for a workflow, without
@@ -431,7 +443,6 @@ on them.
431
443
  | `AKM_SQLITE_JOURNAL_MODE` | SQLite journal mode (network filesystems) |
432
444
  | `AKM_BIN` | Absolute `akm` path for scheduler registration |
433
445
  | `AKM_INSTALL_DIR` | Install-script prefix |
434
- | `AKM_FORCE_SETUP_TMP_STASH` | Documented escape hatch for intentional temp-directory bundles |
435
446
  | `AKM_UPGRADE_SKIP_CHECKSUM` | Recovery hatch for a broken upgrade checksum |
436
447
 
437
448
  **Internal** — no compatibility guarantee, may vanish without notice:
@@ -473,6 +484,8 @@ lives in this repo). **D3** shipped too, in the end: `akm mv` was removed in
473
484
  0.9.0 (see the Renames bullet above), with `scripts/rekey-asset-ref.ts` as the
474
485
  Internal replacement for the one capability nothing else covered.
475
486
 
487
+ - **0.10 — `config set`/`config unset` may drop the config dump** in favor of
488
+ a compact `{ok, shape, key}` result; `akm config list` remains the full read.
476
489
  - **0.10 — migration extraction.** The migration machinery leaves the CLI for
477
490
  a separately published `akm-migrate` package (see Internal above).
478
491
  - **0.10 — `--auto-accept` hard error.** It is currently accepted-and-warned;
@@ -414,7 +414,7 @@ Result-envelope commands accept `--format`, `--detail`, and `--shape` flags:
414
414
  - `--detail full` — includes scores, paths, timing, debug info
415
415
  - `--shape human` (default) — standard projection
416
416
  - `--shape agent` — agent-optimized output: strips non-actionable fields
417
- - `--shape summary` — metadata only (no content/template/prompt), under 200 tokens; only valid on `akm show`
417
+ - `--shape summary` — metadata only (no content/template/prompt), under 200 tokens; only `akm show` has a dedicated summary projection — elsewhere it falls back to `agent` with a warning
418
418
 
419
419
  Run `akm help <command>` or `akm <command> -h` for per-command help. Run
420
420
  `akm --help` for the sectioned command overview.
@@ -3,7 +3,7 @@
3
3
  "processes": {
4
4
  "reflect": { "enabled": false },
5
5
  "distill": { "enabled": false },
6
- "consolidate": { "enabled": true, "allowedTypes": ["memory"], "maxChunkSize": 25, "minPoolSize": 500 },
6
+ "consolidate": { "enabled": true, "allowedTypes": ["memory"], "maxChunkSize": 25 },
7
7
  "memoryInference": { "enabled": false },
8
8
  "graphExtraction": { "enabled": false },
9
9
  "extract": { "enabled": false },
@@ -7,7 +7,7 @@
7
7
  "allowedTypes": ["agent", "command", "knowledge", "lesson", "memory", "skill", "workflow"]
8
8
  },
9
9
  "distill": { "enabled": true, "allowedTypes": ["memory"], "requirePlannedRefs": true },
10
- "consolidate": { "enabled": true, "allowedTypes": ["memory"], "minPoolSize": 500 },
10
+ "consolidate": { "enabled": true, "allowedTypes": ["memory"] },
11
11
  "memoryInference": { "enabled": true },
12
12
  "graphExtraction": { "enabled": true },
13
13
  "extract": { "enabled": false, "triage": { "enabled": true, "minScore": 2 } },
@@ -13,8 +13,7 @@
13
13
  },
14
14
  "consolidate": {
15
15
  "enabled": true,
16
- "allowedTypes": ["memory"],
17
- "minPoolSize": 500
16
+ "allowedTypes": ["memory"]
18
17
  },
19
18
  "memoryInference": {
20
19
  "enabled": true
@@ -150,13 +150,14 @@ export const GLOBAL_OUTPUT_ARGS = {
150
150
  // R-050(c): single-sourced with the root command's own `--shape` help
151
151
  // (`main.args.shape` in src/cli.ts, which spreads this object) so the
152
152
  // caveat is visible from every leaf's own `--help`, not only the top-level
153
- // one. `summary` outside `show` is a hard usage error (exit 2,
154
- // INVALID_SHAPE_VALUE), enforced at startup in src/cli.ts before any
155
- // command body runs.
153
+ // one. `summary` outside `show` falls back to `agent` with a warning
154
+ // (shapeForCommand in src/output/shapes.ts) `--shape` is a global flag,
155
+ // so a script that passes it to a mixed batch of commands still works.
156
156
  shape: {
157
157
  type: "string",
158
158
  description: "Output projection: human|agent|summary (global flag). 'agent' trims to agent-essential fields; " +
159
- "'summary' is only valid on 'akm show' (a usage error, exit 2, everywhere else). Default: human.",
159
+ "'summary' only has a dedicated projection on 'akm show' elsewhere it falls back to 'agent' with a " +
160
+ "warning. Default: human.",
160
161
  },
161
162
  output: {
162
163
  type: "string",
@@ -269,6 +270,17 @@ export function defineGroupCommand(def) {
269
270
  * document is written to that file instead of stdout (jsonl excepted — it is
270
271
  * a line-streaming protocol and always goes to stdout).
271
272
  */
273
+ /**
274
+ * Emit a result whose success is graded by an exit code. `ok` on the envelope
275
+ * and `process.exitCode` are set from the same value here, so the two cannot
276
+ * disagree (#918); `exitCode` undefined or 0 means success.
277
+ */
278
+ export function outputWithExitCode(command, result, exitCode) {
279
+ const failed = exitCode !== undefined && exitCode !== EXIT_CODES.SUCCESS;
280
+ output(command, { ...result, ok: !failed });
281
+ if (failed)
282
+ process.exitCode = exitCode;
283
+ }
272
284
  export function output(command, result) {
273
285
  const mode = getOutputMode();
274
286
  const shaped = shapeForCommand(command, result, mode.detail, mode.shape);
package/dist/cli.js CHANGED
@@ -57,7 +57,7 @@ process.on("uncaughtException", (err) => {
57
57
  });
58
58
  import fs from "node:fs";
59
59
  import { defineCommand, parseArgs, renderUsage, runCommand, showUsage } from "citty";
60
- import { findCittyTopLevelCommand, findCittyTopLevelCommandIndex, getParsedInvocation, parseAllFlagValues, resolveHelpMigrateVersionArg, setParsedInvocation, } from "./cli/invocation.js";
60
+ import { findCittyTopLevelCommandIndex, getParsedInvocation, parseAllFlagValues, resolveHelpMigrateVersionArg, setParsedInvocation, } from "./cli/invocation.js";
61
61
  import { retiredCommandHint } from "./cli/retired-commands.js";
62
62
  import { defineGroupCommand, EXIT_CODES, emitJsonError, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors, } from "./cli/shared.js";
63
63
  import { assertKnownFlags, closestMatch } from "./cli/unknown-flags.js";
@@ -284,6 +284,18 @@ const healthCommand = defineCommand({
284
284
  description: "Fetch the full report dataset: per-run rows, trend deltas vs the prior window, and the pending proposal queue. Renders as the rich report under --format md/html and as complete data under any other format.",
285
285
  default: false,
286
286
  },
287
+ // #914: citty strips any `--no-X` argument and treats it as negating `X`
288
+ // (see `parseArgs` in citty's dist), regardless of whether an arg literally
289
+ // named "no-X" is declared — declaring "no-probe" directly would silently
290
+ // never populate `args["no-probe"]` and the flag would do nothing. Declare
291
+ // the positive flag instead; `--no-probe` is citty's automatic negation of
292
+ // it (rendered in `--help` via `negativeDescription`).
293
+ probe: {
294
+ type: "boolean",
295
+ default: true,
296
+ description: "Probe default-llm-engine / configured-engines reachability (on by default).",
297
+ negativeDescription: "Skip the reachability probes (for an offline or air-gapped host).",
298
+ },
287
299
  },
288
300
  async run({ args }) {
289
301
  let resultStatus;
@@ -316,11 +328,12 @@ const healthCommand = defineCommand({
316
328
  const sinceIsDuration = args.since !== undefined && parseDuration(args.since, DURATION_UNITS) !== null;
317
329
  const implicitCompare = explicitWindows ? undefined : ((sinceIsDuration ? args.since : undefined) ?? "24h");
318
330
  const windowCompare = report ? (args["window-compare"] ?? implicitCompare) : args["window-compare"];
319
- const base = akmHealth({
331
+ const base = await akmHealth({
320
332
  since: args.since,
321
333
  groupBy: report ? "run" : groupBy,
322
334
  windowCompare,
323
335
  windows,
336
+ probe: args.probe !== false,
324
337
  });
325
338
  const reportCompare = windowCompare ??
326
339
  (explicitWindows
@@ -945,18 +958,7 @@ async function runCli() {
945
958
  emitJsonError(error);
946
959
  return;
947
960
  }
948
- // `--shape summary` is only meaningful on `akm show`. Reject it up front for
949
- // every other command so a write command (e.g. `akm proposal accept …`)
950
- // fails fast BEFORE performing its mutation, rather than throwing at
951
- // output-shaping time after the side effect has already happened. The
952
- // shape-registry gate in shapeForCommand() remains as defense-in-depth (and
953
- // covers the in-process test harness, which skips this startup block).
954
961
  const commandPath = resolveCittyCommandPath(main, process.argv.slice(2));
955
- const topLevelCommand = commandPath[0] ?? findCittyTopLevelCommand(process.argv.slice(2), MAIN_TOP_LEVEL_ARGS);
956
- if (getOutputMode().shape === "summary" && topLevelCommand !== "show") {
957
- emitJsonError(new UsageError("'--shape summary' is only valid on 'akm show'.", "INVALID_SHAPE_VALUE"));
958
- return;
959
- }
960
962
  // D7 — every command that renders through output() honours all six --format
961
963
  // values. The declared exempt set (src/output/format-exempt.ts) does not
962
964
  // render an envelope at all, so warn rather than pretend: silently ignoring
@@ -32,10 +32,18 @@ function rejectInvalidAgentRef(agentRef) {
32
32
  return;
33
33
  throw new UsageError(`agent expects an agent asset ref under agents/...; received ${JSON.stringify(agentRef)}.`, "INVALID_FLAG_VALUE");
34
34
  }
35
+ /**
36
+ * Dispatch a `--prompt` / `--prompt-stdin` task through the canonical command
37
+ * path. The prompt is a person's free text, not a template: it is sent to the
38
+ * agent verbatim (`inlineContentMode: "literal"`), so prose containing `}}`
39
+ * from compact JSON, a `$VAR`, a shell snippet, or an `@path` reaches the
40
+ * agent instead of being rejected as an unsupported template construct.
41
+ */
35
42
  async function delegateCanonicalCommand(options, seams, action) {
36
43
  const execute = seams.executeCommand ?? executeCommandInvocation;
37
44
  const result = await execute({
38
45
  action,
46
+ inlineContentMode: "literal",
39
47
  config: options.agentConfig,
40
48
  current: canonicalCurrent(options),
41
49
  });
@@ -10,6 +10,7 @@ import { isWithin } from "../../core/common.js";
10
10
  import { loadConfig } from "../../core/config/config.js";
11
11
  import { bundleComponentConfig, bundlesToSourceEntries } from "../../core/config/config-sources.js";
12
12
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
13
+ import { warnOnce } from "../../core/warn.js";
13
14
  import { lookupBundleRef } from "../../indexer/indexer.js";
14
15
  import { deriveInstallations } from "../../indexer/installations.js";
15
16
  import { resolveEntryContentDir, resolveSourceEntries } from "../../indexer/search/search-source.js";
@@ -66,10 +67,10 @@ function implicitComponentForEntry(entry, config, realRoot) {
66
67
  continue;
67
68
  const canonicalRoot = realDirectory(component.root, `Canonical implicit source root for ${entry.bundleId}`);
68
69
  if (canonicalRoot !== realRoot) {
69
- throw new ConfigError(`Canonical implicit source root drift for ${JSON.stringify(entry.itemRef)}; the indexed root no longer matches the working bundle source.`, "INVALID_CONFIG_FILE", "Run `akm index --full` after changing AKM_BUNDLE_DIR or the default bundle directory.");
70
+ warnOnce(`execution-source-root-drift:${entry.itemRef}`, `[command] Indexed root for ${JSON.stringify(entry.itemRef)} no longer matches the working bundle source; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
70
71
  }
71
72
  if (component.adapter !== entry.adapterId) {
72
- throw new ConfigError(`Implicit adapter drift for ${JSON.stringify(entry.itemRef)}: the index records ${JSON.stringify(entry.adapterId)} but the canonical working source selects ${JSON.stringify(component.adapter)}.`, "INVALID_CONFIG_FILE", "Run `akm index --full` after changing the working source adapter.");
73
+ warnOnce(`execution-source-adapter-drift:${entry.itemRef}`, `[command] Indexed adapter ${JSON.stringify(entry.adapterId)} for ${JSON.stringify(entry.itemRef)} no longer matches the working source's adapter ${JSON.stringify(component.adapter)}; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
73
74
  }
74
75
  return Object.freeze({
75
76
  id: installation.id,
@@ -91,31 +92,33 @@ function componentForEntry(entry, config, realRoot) {
91
92
  const component = bundleComponentConfig(configured);
92
93
  const configuredAdapter = component?.adapter;
93
94
  if (configuredAdapter && configuredAdapter !== entry.adapterId) {
94
- throw new ConfigError(`Configured adapter drift for ${JSON.stringify(entry.itemRef)}: the index records ${JSON.stringify(entry.adapterId)} but the bundle config selects ${JSON.stringify(configuredAdapter)}.`, "INVALID_CONFIG_FILE", "Run `akm index --full` after changing a bundle adapter.");
95
+ warnOnce(`execution-source-adapter-drift:${entry.itemRef}`, `[command] Indexed adapter ${JSON.stringify(entry.adapterId)} for ${JSON.stringify(entry.itemRef)} no longer matches the bundle config's adapter ${JSON.stringify(configuredAdapter)}; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
95
96
  }
96
97
  const configuredSource = bundlesToSourceEntries(config)?.find((source) => source.name === entry.bundleId);
97
98
  const configuredContentRoot = configuredSource ? resolveEntryContentDir(configuredSource) : undefined;
98
99
  if (!configuredSource || !configuredContentRoot) {
99
- throw new ConfigError(`Configured source for ${JSON.stringify(entry.itemRef)} no longer resolves to materialized content.`, "INVALID_CONFIG_FILE", "Restore or update the bundle, then run `akm index --full` before dispatching this asset.");
100
+ warnOnce(`execution-source-unresolved:${entry.itemRef}`, `[command] Configured source for ${JSON.stringify(entry.itemRef)} no longer resolves to materialized content; dispatching from the indexed location instead. Run \`akm index --full\` after restoring or updating the bundle.`);
100
101
  }
101
- const lexicalSourceRoot = path.resolve(configuredContentRoot);
102
- const lexicalConfiguredRoot = path.resolve(configuredContentRoot, component?.root ?? ".");
103
- if (!isLexicallyWithin(lexicalConfiguredRoot, lexicalSourceRoot)) {
104
- throw new ConfigError(`Configured component root for ${JSON.stringify(entry.itemRef)} resolves outside its materialized bundle source.`, "INVALID_CONFIG_FILE", "Keep component roots inside their owning bundle source, then run `akm index --full`.");
105
- }
106
- const configuredSourceRoot = realDirectory(lexicalSourceRoot, `Configured source root for ${entry.bundleId}`);
107
- let configuredRoot;
108
- try {
109
- configuredRoot = realDirectory(lexicalConfiguredRoot, `Configured component root for ${entry.bundleId}`);
110
- }
111
- catch {
112
- throw new ConfigError(`Configured ${JSON.stringify(configuredSource.type)} source for ${JSON.stringify(entry.itemRef)} is not materialized at its current component root.`, "INVALID_CONFIG_FILE", "Restore or update the bundle, then run `akm index --full` before dispatching this asset.");
113
- }
114
- if (!isWithin(configuredRoot, configuredSourceRoot)) {
115
- throw new ConfigError(`Configured component root for ${JSON.stringify(entry.itemRef)} resolves outside its materialized bundle source.`, "INVALID_CONFIG_FILE", "Remove the escaping symlink or component path, then run `akm index --full`.");
116
- }
117
- if (configuredRoot !== realRoot) {
118
- throw new ConfigError(`Configured source or component root drift for ${JSON.stringify(entry.itemRef)}; the indexed root no longer matches the ${JSON.stringify(configuredSource.type)} bundle source.`, "INVALID_CONFIG_FILE", "Run `akm index --full` after changing a bundle source or component root.");
102
+ else {
103
+ const lexicalSourceRoot = path.resolve(configuredContentRoot);
104
+ const lexicalConfiguredRoot = path.resolve(configuredContentRoot, component?.root ?? ".");
105
+ if (!isLexicallyWithin(lexicalConfiguredRoot, lexicalSourceRoot)) {
106
+ throw new ConfigError(`Configured component root for ${JSON.stringify(entry.itemRef)} resolves outside its materialized bundle source.`, "INVALID_CONFIG_FILE", "Keep component roots inside their owning bundle source, then run `akm index --full`.");
107
+ }
108
+ const configuredSourceRoot = realDirectory(lexicalSourceRoot, `Configured source root for ${entry.bundleId}`);
109
+ let configuredRoot;
110
+ try {
111
+ configuredRoot = realDirectory(lexicalConfiguredRoot, `Configured component root for ${entry.bundleId}`);
112
+ }
113
+ catch {
114
+ throw new ConfigError(`Configured ${JSON.stringify(configuredSource.type)} source for ${JSON.stringify(entry.itemRef)} is not materialized at its current component root.`, "INVALID_CONFIG_FILE", "Restore or update the bundle, then run `akm index --full` before dispatching this asset.");
115
+ }
116
+ if (!isWithin(configuredRoot, configuredSourceRoot)) {
117
+ throw new ConfigError(`Configured component root for ${JSON.stringify(entry.itemRef)} resolves outside its materialized bundle source.`, "INVALID_CONFIG_FILE", "Remove the escaping symlink or component path, then run `akm index --full`.");
118
+ }
119
+ if (configuredRoot !== realRoot) {
120
+ warnOnce(`execution-source-root-drift:${entry.itemRef}`, `[command] Indexed root for ${JSON.stringify(entry.itemRef)} no longer matches the ${JSON.stringify(configuredSource.type)} bundle source; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
121
+ }
119
122
  }
120
123
  const writable = component?.writable ?? configured.writable ?? configured.path !== undefined;
121
124
  return Object.freeze({
@@ -1,22 +1,9 @@
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 { UsageError } from "../../core/errors.js";
4
+ import { warn } from "../../core/warn.js";
5
5
  export const PORTABLE_ARGUMENTS_PLACEHOLDER = "$ARGUMENTS";
6
- const UNSUPPORTED_TEMPLATE_CONSTRUCTS = Object.freeze([
7
- { label: "$ARGUMENTS[N]", pattern: /\$ARGUMENTS\s*\[/u },
8
- { label: "```! ... ```", pattern: /(?:^|\r?\n)[\t ]*```!/u },
9
- { label: "!`...`", pattern: /!`/u },
10
- { label: "@file", pattern: /(?<![A-Za-z0-9._%+-])@(?:\.{0,2}\/)?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*/u },
11
- { label: "$" + "{...}", pattern: /\$\{/u },
12
- { label: "$(...)", pattern: /\$\(/u },
13
- { label: "$N", pattern: /\$\d/u },
14
- { label: "$NAME", pattern: /\$[A-Za-z_][A-Za-z0-9_]*/u },
15
- { label: "{{...}}", pattern: /\{\{|\}\}/u },
16
- ]);
17
- function unsupportedTemplate(source, label) {
18
- return new UsageError(`Command ${JSON.stringify(source)} uses unsupported portable template construct ${label}.`, "INVALID_FLAG_VALUE", `AKM command execution supports only the literal ${PORTABLE_ARGUMENTS_PLACEHOLDER} placeholder. Invoke native-only templates through their owning tool instead.`);
19
- }
6
+ const INDEXED_ARGUMENTS_PATTERN = /\$ARGUMENTS\s*\[/u;
20
7
  /**
21
8
  * Validate the deliberately small portable command-template language.
22
9
  *
@@ -28,17 +15,8 @@ export function validatePortableCommandTemplate(template, source) {
28
15
  throw new TypeError("command template must be a string");
29
16
  if (typeof source !== "string" || source.length === 0)
30
17
  throw new TypeError("command source must be a string");
31
- // Check native extensions of the portable spelling before masking the one
32
- // supported token. AKM never interprets indexed native placeholders.
33
- const indexedArguments = UNSUPPORTED_TEMPLATE_CONSTRUCTS[0];
34
- if (indexedArguments?.pattern.test(template))
35
- throw unsupportedTemplate(source, indexedArguments.label);
36
- // Mask only the exact portable token. Everything else remains visible to the
37
- // unsupported-construct detectors, including `$ARGUMENTS_SUFFIX`.
38
- const portableMasked = template.replace(/\$ARGUMENTS(?![A-Za-z0-9_])/gu, "");
39
- for (const construct of UNSUPPORTED_TEMPLATE_CONSTRUCTS.slice(1)) {
40
- if (construct.pattern.test(portableMasked))
41
- throw unsupportedTemplate(source, construct.label);
18
+ if (INDEXED_ARGUMENTS_PATTERN.test(template)) {
19
+ warn(`Command ${JSON.stringify(source)} uses "$ARGUMENTS[N]", which akm does not support. Only the literal $ARGUMENTS placeholder is expanded; the indexed form is left as-is.`);
42
20
  }
43
21
  }
44
22
  /**