akm-cli 0.9.0-rc.13 → 0.9.0-rc.14

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 (100) hide show
  1. package/CHANGELOG.md +117 -23
  2. package/dist/akm-migrate +3 -1
  3. package/dist/assets/hints/cli-hints-full.md +3 -4
  4. package/dist/assets/hints/cli-hints-short.md +5 -5
  5. package/dist/assets/workflows/workflow-template.md +4 -3
  6. package/dist/cli/invocation.js +3 -2
  7. package/dist/cli/retired-commands.js +3 -0
  8. package/dist/cli/unknown-flags.js +226 -0
  9. package/dist/cli.js +19 -35
  10. package/dist/commands/agent/contribute-cli.js +15 -3
  11. package/dist/commands/feedback-cli.js +6 -3
  12. package/dist/commands/improve/collapse-detector.js +2 -3
  13. package/dist/commands/lint/base-linter.js +4 -16
  14. package/dist/commands/lint/index.js +13 -13
  15. package/dist/commands/log.js +6 -1
  16. package/dist/commands/migration-tool.js +4 -5
  17. package/dist/commands/observability-cli.js +1 -1
  18. package/dist/commands/proposal/repository.js +5 -5
  19. package/dist/commands/read/knowledge.js +2 -0
  20. package/dist/commands/registry-cli.js +5 -3
  21. package/dist/commands/sources/add-cli.js +6 -6
  22. package/dist/commands/sources/self-update.js +30 -7
  23. package/dist/commands/sources/source-add.js +17 -2
  24. package/dist/commands/tasks/tasks.js +8 -3
  25. package/dist/commands/workflow-cli.js +142 -119
  26. package/dist/core/adapter/adapters/akm-lint.js +17 -13
  27. package/dist/core/adapter/adapters/akm-task-adapter.js +14 -11
  28. package/dist/core/asset/akm-markdown.js +41 -8
  29. package/dist/core/asset/frontmatter.js +22 -0
  30. package/dist/core/asset/resolve-ref.js +23 -3
  31. package/dist/core/common.js +45 -2
  32. package/dist/core/config/config-schema.js +8 -0
  33. package/dist/core/config/schema/experimental.js +5 -13
  34. package/dist/core/config/schema/sources-bundles.js +11 -0
  35. package/dist/core/config/schema/workflow.js +3 -1
  36. package/dist/core/errors.js +5 -0
  37. package/dist/core/logs-db.js +2 -1
  38. package/dist/core/parse.js +4 -1
  39. package/dist/core/state/migrations.js +11 -14
  40. package/dist/core/state-db.js +4 -6
  41. package/dist/core/subprocess.js +6 -4
  42. package/dist/core/type-presentation.js +1 -1
  43. package/dist/indexer/indexer.js +16 -1
  44. package/dist/indexer/search/search-source.js +1 -1
  45. package/dist/indexer/walk/matchers.js +3 -1
  46. package/dist/integrations/agent/config.js +2 -2
  47. package/dist/integrations/agent/detect.js +49 -19
  48. package/dist/integrations/agent/profiles.js +10 -0
  49. package/dist/integrations/agent/spawn.js +1 -2
  50. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +7 -3
  51. package/dist/integrations/lockfile.js +11 -56
  52. package/dist/output/shapes/passthrough.js +0 -4
  53. package/dist/output/text/command-format.js +3 -3
  54. package/dist/output/text/helpers.js +1 -1
  55. package/dist/output/text/show-directives.js +4 -6
  56. package/dist/output/text/workflow-format.js +7 -62
  57. package/dist/output/text/workflow.js +1 -5
  58. package/dist/scripts/akm-migrate-node.js +58773 -0
  59. package/dist/scripts/akm-migrate.js +33976 -11391
  60. package/dist/setup/detect.js +40 -15
  61. package/dist/setup/setup.js +1 -1
  62. package/dist/sources/providers/git-stash.js +4 -2
  63. package/dist/sources/providers/website.js +5 -0
  64. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  65. package/dist/sources/snapshot-fetchers/content-extract.js +370 -0
  66. package/dist/sources/snapshot-fetchers/fetcher-util.js +40 -0
  67. package/dist/sources/snapshot-fetchers/host-guard.js +199 -0
  68. package/dist/sources/snapshot-fetchers/registry.js +10 -1
  69. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  70. package/dist/sources/snapshot-fetchers/rss.js +279 -0
  71. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  72. package/dist/sources/snapshot-fetchers/website-ingest.js +488 -257
  73. package/dist/sources/snapshot-fetchers/x.js +193 -0
  74. package/dist/storage/engines/sqlite-migrations.js +22 -107
  75. package/dist/tasks/runner.js +19 -16
  76. package/dist/tasks/schema.js +24 -1
  77. package/dist/workflows/exec/brief.js +1 -1
  78. package/dist/workflows/exec/frozen-judge.js +28 -2
  79. package/dist/workflows/exec/native-executor.js +18 -1
  80. package/dist/workflows/exec/report.js +11 -4
  81. package/dist/workflows/exec/run-workflow.js +103 -44
  82. package/dist/workflows/exec/step-work.js +19 -18
  83. package/dist/workflows/exec/unit-dispatch.js +4 -0
  84. package/dist/workflows/exec/workflow-engine-gate.js +11 -13
  85. package/dist/workflows/ir/compile.js +2 -2
  86. package/dist/workflows/ir/freeze.js +16 -8
  87. package/dist/workflows/ir/params.js +134 -10
  88. package/dist/workflows/ir/plan-hash.js +1 -1
  89. package/dist/workflows/ir/schema.js +6 -2
  90. package/dist/workflows/renderer.js +2 -2
  91. package/dist/workflows/runtime/checkin.js +3 -3
  92. package/dist/workflows/runtime/runs.js +50 -29
  93. package/dist/workflows/validate-summary.js +30 -14
  94. package/docs/migration/release-notes/0.9.0.md +31 -4
  95. package/docs/migration/v0.8-to-v0.9.md +69 -100
  96. package/docs/reference/data-and-telemetry.md +5 -5
  97. package/package.json +6 -2
  98. package/schemas/akm-config.json +24 -0
  99. package/schemas/akm-workflow.json +1 -1
  100. package/dist/workflows/cli.js +0 -33
package/CHANGELOG.md CHANGED
@@ -6,6 +6,79 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added
10
+
11
+ - **RSS, Bluesky, and X sources.** `akm bundle add` now recognizes three new
12
+ kinds of URL and snapshots them as knowledge assets instead of crawling
13
+ them as ordinary web pages:
14
+
15
+ ```sh
16
+ akm bundle add https://blog.example/feed # RSS 2.0 / Atom / RDF
17
+ akm bundle add https://bsky.app/profile/<handle> # public, no auth
18
+ akm bundle add https://x.com/<user> # see token note below
19
+ ```
20
+
21
+ Any of these falling through — a `/feed` URL that actually serves HTML, an
22
+ unresolvable Bluesky handle — degrades to the normal website crawl rather
23
+ than failing the command.
24
+
25
+ X needs credentials: set `X_BEARER_TOKEN` for the X API v2, or
26
+ `X_RSS_TEMPLATE` to an RSS bridge URL containing `{username}`. To keep the
27
+ token out of your shell history, store it as an akm secret and inject it
28
+ per-invocation:
29
+
30
+ ```sh
31
+ akm secret set x-bearer-token
32
+ akm secret run secrets/x-bearer-token X_BEARER_TOKEN -- akm bundle add https://x.com/<user>
33
+ ```
34
+
35
+ With neither set, the X fetcher emits one warning and falls through.
36
+
37
+ ### Changed
38
+
39
+ - **X source tokens now resolve from the secret store during bundle update.**
40
+ The `secrets/x-bearer-token` akm secret is honored on the provider
41
+ `sync()` / bundle-update path, not just when adding or importing a URL —
42
+ closing a gap where a refresh saw only the `X_BEARER_TOKEN` environment
43
+ variable. Implemented as a `SecretResolver` capability injected from above
44
+ the source-provider import cycle; internals are documented in
45
+ `docs/architecture/reviews/env-secret-access.md`.
46
+
47
+ - **`website` crawls now have a hard time limit.** `crawlTimeoutMs` (default
48
+ 600000 — 10 minutes) bounds the entire crawl, and unlike the previous
49
+ between-page check it aborts work already in flight: a `Retry-After` sleep
50
+ could previously park `akm bundle add` for as long as a rate-limiting server
51
+ asked, well past the advertised cap. Raise it for a large site, or set
52
+ `"crawlTimeoutMs": 0` to disable the cap. Relatedly, `fetchWithRetry` now
53
+ honors its caller's `AbortSignal` during retry backoff, so any operation that
54
+ passes a signal can actually interrupt a long wait.
55
+
56
+ - **Website snapshots now extract the page's main content.** Conversion moved
57
+ from a hand-rolled regex converter to a DOM parse plus Turndown, scoped to
58
+ the page's content region (`<main>`, `<article>`, `[role=main]`, then common
59
+ content ids/classes, falling back to `<body>` minus nav/header/footer/aside).
60
+ Navigation, ads, and boilerplate no longer land in snapshots, and tables,
61
+ nested lists, and fenced code blocks with language hints now survive
62
+ conversion. **Existing website snapshots will change on their next refresh**
63
+ — expect them to get shorter and cleaner. Link discovery still scans the
64
+ whole page, so crawl coverage is unchanged.
65
+
66
+
67
+ - **`website` sources now respect `robots.txt` by default.** Before crawling
68
+ an origin, akm fetches and parses that origin's `/robots.txt` and skips
69
+ paths disallowed for the `akm`/`akm-cli` product tokens (or `*`), honoring
70
+ `Crawl-delay` (clamped to 10s) between page fetches. This is a deliberate
71
+ behavior change: **existing website sources may return fewer pages, or
72
+ fail with an error if the start URL itself is disallowed, after
73
+ upgrading.** Re-running `akm bundle update` on a website source is what
74
+ surfaces it. Opt out with `"respectRobots": false` on the website
75
+ descriptor to restore the exact pre-upgrade behavior (no `/robots.txt`
76
+ request at all):
77
+
78
+ ```json
79
+ { "bundles": { "docs": { "website": { "url": "https://docs.example.com", "respectRobots": false } } } }
80
+ ```
81
+
9
82
  ## [0.9.0-rc.13] - 2026-07-31
10
83
 
11
84
  ### Security
@@ -139,6 +212,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
139
212
 
140
213
  ### Changed
141
214
 
215
+ - **Workflow execution is consolidated on stable `akm workflow run`.** The
216
+ public `workflow start`, `next`, and `complete` commands are removed with
217
+ explicit `UNKNOWN_COMMAND` migration hints; `run <ref|run-id>` now owns
218
+ creation, active-run continuation, native dispatch, completion, and durable
219
+ replay. It is no longer gated by `experimental.workflowEngine`; only the
220
+ experimental `brief`/`report` external-driver protocol retains that opt-in.
221
+ Workflow parameters move from the opaque `--params '<json>'` bag to exact
222
+ declared flags (`--version 1.2.3`, repeated array flags, JSON object/array
223
+ values) coerced through the frozen parameter schemas. New invocation controls
224
+ add bounded failed-step retries (`--max-retries`) and a whole-run timeout
225
+ (`--timeout N|Nms|Ns|Nm`); failures, gate rejection, timeout, and signals now
226
+ produce non-zero process statuses while leaving interrupted work resumable.
227
+
228
+ Criteria-bearing gates now require `workflow.judgeEngine`, which may name a
229
+ configured LLM or agent engine and is frozen into the run. Verification is
230
+ fail-closed: a missing/failing verifier or malformed verdict rejects instead
231
+ of silently advancing. Scheduled workflow tasks now execute through the same
232
+ native orchestrator rather than stopping after run creation. Migration:
233
+ replace `workflow start/next/complete` loops with `workflow run`, replace
234
+ `--params` with exact declared flags, and configure `workflow.judgeEngine`
235
+ before running a workflow with a non-empty `### gate` rubric.
236
+
142
237
  - **The two workflow authoring formats — markdown documents and YAML
143
238
  orchestration programs — are unified into one format**, per
144
239
  `docs/architecture/specs/workflow-format-unification.md`. A workflow is
@@ -169,15 +264,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
169
264
  bullet section — they live under a step's `### gate` sub-heading, the
170
265
  format's one reserved marker, as full prose a judge receives byte-exact.
171
266
  Frontmatter `gate:` now carries only optional `max_loops` configuration.
172
- Omitted or empty rubric text skips validation; a non-empty rubric enables
173
- fail-open validation, and unavailable or malformed judges are skipped.
267
+ Omitted or empty rubric text skips validation; a non-empty rubric requires
268
+ the frozen `workflow.judgeEngine`, and unavailable or malformed judges reject
269
+ the gate.
174
270
 
175
- This is a **pre-1.0 change to an unshipped, opt-in feature** —
176
- `experimental.workflowEngine` has never been enabled by default, and no
177
- workflow asset has shipped outside the ten example workflows under
178
- `scripts/akm-eval/example-stash/workflows/`, all rewritten to the
179
- unified format in this change. There is nothing on disk to migrate and
180
- no users to break.
271
+ This is a **pre-1.0 format change**. The ten example workflows under
272
+ `scripts/akm-eval/example-stash/workflows/` are rewritten to the unified
273
+ format in this change; existing user-authored workflow assets must be updated
274
+ manually before execution.
181
275
 
182
276
  - **akm is described as a knowledge toolkit, not a package manager** (R-048).
183
277
  The npm one-liner, the README lede, and the `concepts.md` opener all led with
@@ -766,9 +860,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
766
860
  - **The CHURN alert class is removed from the collapse detector.** Its input was
767
861
  a hard-coded `0` from the 0.9.0 confidence-gate deletion onward, so the alert
768
862
  could never fire. The other three alert classes are unaffected. The
769
- `improve_cycle_metrics.accepted_actions` column stays and is written as `0`:
770
- it lives in a released migration body, and 0.8 ships `state.db`, so a deployed
771
- ledger can already have sealed that body's checksum.
863
+ `improve_cycle_metrics.accepted_actions` column stays and is written as `0`
864
+ because deployed 0.8 `state.db` files already contain it.
772
865
 
773
866
  - **`IndexResponse.graphQuality` is removed** from the `akm index` envelope — it
774
867
  was declared but never assigned in any code path, so it was always absent.
@@ -839,15 +932,16 @@ earlier `0.9.0-rc.1` / `0.9.0-beta.*` development entries below.
839
932
  order). The pre-0.9.0 `[origin//]type:name` grammar is removed — there is no
840
933
  compatibility parser; the frozen migrator in `scripts/akm-migrate/migrate/`
841
934
  is the only place it survives.
842
- - **Explicit, journaled, crash-resumable cutover (`akm migrate apply`).** The
935
+ - **Explicit, crash-resumable cutover (`akm migrate apply`).** The
843
936
  migrator re-keys all durable state to the new spelling, folds the former
844
937
  `workflow.db` into `state.db` (four databases down to three: `state.db` /
845
938
  `index.db` / a separate `logs.db`), and migrates config from the flat
846
939
  `stashDir` / `sources` / `installed` / `wikiName` keys to `bundles` /
847
- `defaultBundle`. A verified, installation-scoped **backup manifest v3**
848
- (covering the pre-rescue `index.db`) is taken before any ledger is sealed;
849
- expected orphans are quarantined, integrity failures fail closed, and the
850
- whole cutover resumes idempotently after a crash. Normal commands refuse an
940
+ `defaultBundle`. A semantically verified, installation-scoped **backup manifest v4**
941
+ (covering the pre-rescue `index.db`) is taken before mutation. One phase-free
942
+ incomplete sentinel retains that backup and target; expected orphans are
943
+ quarantined, integrity failures fail closed, and the whole cutover reruns
944
+ idempotently after a crash. Normal commands refuse an
851
945
  un-migrated or divergent durable schema rather than migrating as a side effect.
852
946
  The retired `stashDir` / `sources` / `installed` keys are **hard-rejected** by
853
947
  the 0.9.0 config schema whenever present (the error names `akm migrate apply`);
@@ -888,13 +982,13 @@ See `docs/migration/v0.8-to-v0.9.md` and
888
982
  migration, dashboard, or health schema was added.
889
983
  - **Explicit, crash-resumable 0.9 migration coordination.** `akm migrate
890
984
  status` classifies config, `state.db`, and `workflow.db` independently;
891
- `akm migrate apply [--config <prepared>]` creates a verified,
892
- installation-scoped backup before sealing ledgers or applying pending
893
- migrations. Apply and restore use authenticated phase journals, exact
894
- artifact fingerprints, bounded streaming I/O, SQLite integrity checks,
895
- active-writer barriers, WAL/SHM-safe publication, and idempotent recovery.
896
- Routine reads and current database opens no longer depend on a historical
897
- cutover bundle. See `docs/migration/release-notes/0.9.0.md`.
985
+ `akm migrate apply [--config <prepared>]` creates a semantically verified,
986
+ installation-scoped config/database backup before applying pending migrations.
987
+ Apply and restore use one phase-free incomplete sentinel, bounded control-file
988
+ reads, SQLite integrity and ordered-ledger checks, active-writer barriers,
989
+ WAL/SHM-safe publication, and idempotent replay. Legacy checksum columns are
990
+ inert. Routine reads and current database opens no longer depend on a
991
+ historical cutover bundle. See `docs/migration/release-notes/0.9.0.md`.
898
992
  - **Workflow orchestration engine (experimental).** akm can now execute
899
993
  multi-step workflows through a native engine or any agent session. Workflow
900
994
  assets use the unified markdown format described above; the stable manual
package/dist/akm-migrate CHANGED
@@ -24,11 +24,13 @@ const bunProbe = process.versions.bun
24
24
  const [bunMajor = 0] = (bunProbe.status === 0 ? bunProbe.stdout.trim() : "").split(".").map(Number);
25
25
  const useBun = bunMajor >= 1;
26
26
  const bunEntry = fileURLToPath(new URL("./scripts/akm-migrate.js", import.meta.url));
27
+ const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.meta.url));
27
28
 
28
29
  {
29
30
  const command = process.versions.bun ? process.execPath : useBun ? "bun" : process.execPath;
31
+ const entry = process.versions.bun || useBun ? bunEntry : nodeEntry;
30
32
  const result = await new Promise((resolve) => {
31
- const child = spawn(command, [bunEntry, ...process.argv.slice(2)], { stdio: "inherit" });
33
+ const child = spawn(command, [entry, ...process.argv.slice(2)], { stdio: "inherit" });
32
34
  child.once("error", (error) => resolve({ error }));
33
35
  child.once("exit", (code, signal) => resolve({ code, signal }));
34
36
  });
@@ -93,7 +93,7 @@ akm import https://example.com/docs/auth # Fetch one URL and import it as
93
93
  akm import ./doc.md --target my-other-bundle # Route import to a named writable bundle source
94
94
  akm workflow create ship-release # Create a workflow asset in the bundle
95
95
  akm lint --type workflows # Parse and compile every unified markdown workflow; list every error
96
- akm workflow next workflows/ship-release # Start or resume the next workflow step
96
+ akm workflow run workflows/ship-release # Start or resume and execute the workflow
97
97
  akm feedback skills/code-review --positive # Record that an asset helped
98
98
  akm feedback agents/reviewer --negative --reason "wrong framework" # Record why an asset missed the mark
99
99
  akm feedback memories/deployment-notes --positive # Works for memories too
@@ -169,9 +169,8 @@ workflow. Direct run-id commands still target the exact run.
169
169
  ```sh
170
170
  akm workflow create ship-release --print # Print a starter workflow template, without writing
171
171
  akm workflow create ship-release # Scaffold a new workflow asset
172
- akm workflow start workflows/ship-release # Start a new run in the current scope
173
- akm workflow next workflows/ship-release # Advance to the next step (or auto-start) in the current scope
174
- akm workflow complete <run-id> --step <id> --summary "..." # Mark a step complete and advance
172
+ akm workflow run workflows/ship-release --version=1.2.3 # Start and execute with exact-name parameter flags
173
+ akm workflow run <run-id> --max-retries 2 --timeout 10m # Resume with invocation-wide controls
175
174
  akm workflow status <run-id> # Show the exact run by id
176
175
  akm workflow resume <run-id> # Resume a blocked or failed run
177
176
  akm workflow list # List workflow runs in the current scope
@@ -11,12 +11,12 @@ For any task, follow this loop:
11
11
  4. `akm feedback <ref> --positive` — record success; use `--negative --reason "..."` when it fails
12
12
 
13
13
  For workflow tasks:
14
- 1. `akm workflow next workflows/<name>` — get current step instructions
15
- 2. Do the step work in your workspace
16
- 3. `akm workflow complete <run-id> --step <step-id> --summary "what was done"` — mark done, get next step
14
+ 1. `akm show workflows/<name>` — inspect the procedure before executing it
15
+ 2. `akm workflow run workflows/<name> --param_name=value` — execute every step and verification gate
16
+ 3. `akm workflow status <run-id> --units` — inspect a stopped or failed run
17
17
 
18
18
  Workflow runs are scoped to your current project/worktree/directory. Ref-based
19
- commands like `workflow next workflows/<name>`, `workflow status workflows/<name>`,
19
+ commands like `workflow run workflows/<name>`, `workflow status workflows/<name>`,
20
20
  and `workflow list` operate within the current scope only.
21
21
 
22
22
  ## Quick Reference
@@ -29,7 +29,7 @@ akm search "memories/projectA/" # List a subtree (conceptId prefix
29
29
  akm search "<query>" --from all # Also search registries
30
30
  akm show <ref> # View asset details
31
31
  akm help <command> # Show a command's options and subcommands
32
- akm workflow next <ref> # Start or resume a workflow
32
+ akm workflow run <ref> # Start or resume and execute a workflow
33
33
  akm remember "Deployment needs VPN access" # Record a memory in your bundle
34
34
  akm remember "note" --bundle my-bundle # Route write to a named writable bundle source
35
35
  akm remember "note" --xref knowledge/auth-flow # Cite provenance in frontmatter xrefs (repeatable)
@@ -32,9 +32,10 @@ first step's attached artifact."
32
32
  ### gate
33
33
 
34
34
  A `### gate` sub-heading is the step's completion rubric: the judge
35
- receives this whole section byte-exact when validation is available. Omit
36
- the heading or leave its text empty to skip validation. An unavailable or
37
- malformed judge also skips validation.
35
+ receives this whole section byte-exact. Omit the heading or leave its text
36
+ empty to skip validation. A non-empty rubric requires
37
+ `workflow.judgeEngine`; verifier failures and malformed verdicts reject the
38
+ gate rather than silently skipping it.
38
39
 
39
40
  - Confirm the step accomplished what it set out to do.
40
41
  - Confirm nothing required was silently skipped.
@@ -181,10 +181,11 @@ export function _resetParsedInvocationForTests() {
181
181
  export function parseAllFlagValues(flag) {
182
182
  return getParsedInvocation().getAllFlagValues(flag);
183
183
  }
184
- function cittyComparableName(name) {
184
+ /** citty compares `foo-bar` and `fooBar` as the same arg name. */
185
+ export function cittyComparableName(name) {
185
186
  return name.replace(/[-_]+([a-zA-Z0-9])/g, (_match, char) => char.toUpperCase());
186
187
  }
187
- function toAliasArray(alias) {
188
+ export function toAliasArray(alias) {
188
189
  if (Array.isArray(alias))
189
190
  return alias;
190
191
  return typeof alias === "string" ? [alias] : [];
@@ -54,6 +54,9 @@ const RETIRED_COMMAND_HINTS = {
54
54
  "workflow template": "`akm workflow template` was folded into `akm workflow create --print` in 0.9.",
55
55
  "workflow validate": "`akm workflow validate` was folded into `akm lint --type workflows` in 0.9 (add `--fail-on-flagged` for CI gates).",
56
56
  "workflow watch": "`akm workflow watch` was removed in 0.9 — use `akm log --run <run-id>`.",
57
+ "workflow start": "`akm workflow start` was removed in 0.9 — use `akm workflow run <ref>`.",
58
+ "workflow next": "`akm workflow next` was removed in 0.9 — use `akm workflow status <target>` to inspect or `akm workflow run <target>` to execute.",
59
+ "workflow complete": "`akm workflow complete` was removed in 0.9 — native runs complete steps automatically; external drivers use `akm workflow brief` and `akm workflow report`.",
57
60
  "config show": "`akm config show` was removed in 0.9 — use `akm config list`.",
58
61
  "config validate": "`akm config validate` was removed in 0.9 — the config file is validated on every load.",
59
62
  "task enable": "`akm task enable` was removed in 0.9 — set `enabled: true` in the task YAML, then `akm task sync`.",
@@ -0,0 +1,226 @@
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
+ * Reject flags the resolved command does not declare.
6
+ *
7
+ * citty (0.2.x) parses argv with `node:util`'s parseArgs in non-strict mode
8
+ * (`strict: false`), so an undeclared flag is collected into the parsed object
9
+ * and silently ignored by the handler. Nothing
10
+ * downstream noticed, so `akm lint --fail-on-flaged` (one transposed letter in
11
+ * the flag STABILITY.md documents as a CI contract) parsed fine, exited 0, and
12
+ * the gate it was meant to enforce never fired. Same for `--limt 3`, `--jsn`,
13
+ * and every other typo — the command ran with the default instead, and the user
14
+ * had no signal.
15
+ *
16
+ * This walks the same subcommand path citty resolves (via the shared scan
17
+ * helpers in `./invocation`), unions the arg definitions declared along it,
18
+ * and fails a flag that matches nothing. The union (rather than the leaf's
19
+ * args alone) is deliberate: parent-level flags may legally appear before the
20
+ * subcommand token, and a false positive here would reject a VALID invocation
21
+ * — much worse than the silence it replaces. In the same spirit, everything
22
+ * after a literal `--` is passthrough, a declared value flag's value is never
23
+ * scanned as a flag, `--no-<name>` resolves against `<name>`, and a bare `-`
24
+ * (stdin convention) and negative numbers are left alone. A subcommand invoked
25
+ * by a `meta.name` alias rather than its key takes the stand-down path (no
26
+ * validation) — conservative by design.
27
+ */
28
+ import { UsageError } from "../core/errors.js";
29
+ import { cittyComparableName, findCittyTopLevelCommandIndex, toAliasArray, } from "./invocation.js";
30
+ /** Flags citty implements itself, which no command declares. */
31
+ const IMPLICIT_FLAGS = ["help", "h", "version", "v"];
32
+ /**
33
+ * Retired flags whose commands still diagnose them THEMSELVES, with a message
34
+ * that names the replacement ("`--scope` was removed, use `--filter`",
35
+ * "`--source` was renamed to `--generator`"). A generic "unknown flag" would
36
+ * preempt the better diagnosis, so these are passed through — but ONLY on the
37
+ * command path that owns the diagnostic, keyed by the resolved path. On every
38
+ * other command the same spelling is a genuine typo and still fails fast
39
+ * A retired flag is rejected everywhere else, where silently dropping it could
40
+ * run a real mutation.
41
+ *
42
+ * Shrink-only: when a command drops its bespoke diagnostic, drop the entry and
43
+ * the generic error takes over.
44
+ */
45
+ const SELF_DIAGNOSED_FLAGS = new Map(Object.entries({
46
+ show: ["akmView", "scope"], // removed view grammar; --scope points at --filter
47
+ index: ["enrich", "re-enrich"], // removed index-time enrichment flags
48
+ "proposal accept": ["source"], // renamed to --generator
49
+ "proposal reject": ["source"], // renamed to --generator
50
+ "proposal drain": ["profile"], // retired, points at --strategy
51
+ search: ["source"], // renamed to --from
52
+ curate: ["source"], // renamed to --from
53
+ remember: ["target"], // renamed to --bundle
54
+ clone: ["target"], // renamed to --bundle
55
+ improve: ["auto-accept", "target"], // retired in 0.9.0 / renamed to --bundle
56
+ "task add": ["target"], // renamed to --bundle
57
+ "task run": ["target"], // renamed to --bundle
58
+ "task history": ["target"], // renamed to --bundle
59
+ "task sync": ["target"], // renamed to --bundle
60
+ }).map(([path, flags]) => [path, new Set(flags.map(cittyComparableName))]));
61
+ /**
62
+ * Union the arg definitions declared along the resolved command path. The
63
+ * subcommand walk delegates to {@link findCittyTopLevelCommandIndex} — the
64
+ * same scan `src/cli.ts` resolves commands with — so both agree on which
65
+ * token is the subcommand name.
66
+ */
67
+ function collectKnownArgs(root, rawArgs) {
68
+ const names = new Set(IMPLICIT_FLAGS.map(cittyComparableName));
69
+ const valueFlags = new Set();
70
+ const booleanFlags = new Set();
71
+ const displayNames = [];
72
+ const path = [];
73
+ let cmd = root;
74
+ let args = rawArgs;
75
+ for (;;) {
76
+ for (const [key, def] of Object.entries(cmd.args ?? {})) {
77
+ // Positionals are not flags; including them would accept `--<positional>`.
78
+ if (def.type === "positional")
79
+ continue;
80
+ const comparable = cittyComparableName(key);
81
+ // Shared args (GLOBAL_OUTPUT_ARGS) recur at every level; suggest each once.
82
+ if (!names.has(comparable)) {
83
+ names.add(comparable);
84
+ displayNames.push(`--${key}`);
85
+ }
86
+ if (def.type === "string" || def.type === "enum")
87
+ valueFlags.add(comparable);
88
+ if (def.type === "boolean")
89
+ booleanFlags.add(comparable);
90
+ for (const alias of toAliasArray(def.alias)) {
91
+ names.add(cittyComparableName(alias));
92
+ if (def.type === "string" || def.type === "enum")
93
+ valueFlags.add(cittyComparableName(alias));
94
+ if (def.type === "boolean")
95
+ booleanFlags.add(cittyComparableName(alias));
96
+ }
97
+ }
98
+ const subCommands = cmd.subCommands;
99
+ if (!subCommands || Object.keys(subCommands).length === 0)
100
+ break;
101
+ const idx = findCittyTopLevelCommandIndex(args, (cmd.args ?? {}));
102
+ const token = idx >= 0 ? args[idx] : undefined;
103
+ // A group with no subcommand token: citty reports "no command specified".
104
+ if (token === undefined)
105
+ return { names, valueFlags, booleanFlags, displayNames, path, resolved: false };
106
+ const sub = subCommands[token];
107
+ // An unrecognized token: citty reports the unknown command, which is the
108
+ // real problem — its flags are beside the point.
109
+ if (!sub)
110
+ return { names, valueFlags, booleanFlags, displayNames, path, resolved: false };
111
+ path.push(token);
112
+ cmd = sub;
113
+ args = args.slice(idx + 1);
114
+ }
115
+ return { names, valueFlags, booleanFlags, displayNames, path, resolved: true };
116
+ }
117
+ /** Single-row-at-a-time edit-distance DP (inputs are short flag/command names). */
118
+ function editDistance(a, b) {
119
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
120
+ for (let i = 1; i <= a.length; i += 1) {
121
+ const current = [i];
122
+ for (let j = 1; j <= b.length; j += 1) {
123
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
124
+ current.push(Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, (previous[j - 1] ?? 0) + cost));
125
+ }
126
+ previous = current;
127
+ }
128
+ return previous[b.length] ?? 0;
129
+ }
130
+ /**
131
+ * Closest candidate within `threshold` edit distance, or undefined when
132
+ * nothing is close enough to be worth suggesting. Shared by the unknown-flag
133
+ * and unknown-command (src/cli.ts) did-you-mean paths, which pick different
134
+ * thresholds.
135
+ */
136
+ export function closestMatch(attempted, candidates, threshold) {
137
+ let best;
138
+ let bestDistance = Number.POSITIVE_INFINITY;
139
+ for (const candidate of candidates) {
140
+ const distance = editDistance(attempted, candidate);
141
+ if (distance < bestDistance) {
142
+ bestDistance = distance;
143
+ best = candidate;
144
+ }
145
+ }
146
+ return best !== undefined && bestDistance <= threshold ? best : undefined;
147
+ }
148
+ /**
149
+ * The one unknown-flag rejection, shared by the short- and long-flag scans so
150
+ * the user-facing error contract cannot drift between them. No explicit hint
151
+ * when there is no suggestion — UNKNOWN_FLAG's canned hint (core/errors.ts)
152
+ * already says to run the command with --help.
153
+ *
154
+ * @param shown The flag as the user typed it (dashes, no `=value`).
155
+ * @param attempted The spelling to edit-distance against `--long` candidates.
156
+ */
157
+ function throwUnknownFlag(shown, attempted, known) {
158
+ const threshold = Math.max(2, Math.ceil(attempted.length / 3));
159
+ const suggestion = closestMatch(attempted, known.displayNames, threshold);
160
+ throw new UsageError(`Unknown flag "${shown}".`, "UNKNOWN_FLAG", suggestion
161
+ ? `Did you mean \`${suggestion}\`? Run the command with \`--help\` to see its accepted flags.`
162
+ : undefined);
163
+ }
164
+ /**
165
+ * Throw a {@link UsageError} naming the first flag the resolved command does
166
+ * not declare. Returns silently when every flag is known.
167
+ */
168
+ export function assertKnownFlags(root, rawArgs) {
169
+ const passthroughAt = rawArgs.indexOf("--");
170
+ const ownArgs = passthroughAt === -1 ? rawArgs : rawArgs.slice(0, passthroughAt);
171
+ const known = collectKnownArgs(root, rawArgs);
172
+ if (!known.resolved)
173
+ return;
174
+ const dynamicWorkflowParams = known.path.join(" ") === "workflow run";
175
+ const selfDiagnosed = SELF_DIAGNOSED_FLAGS.get(known.path.join(" "));
176
+ for (let i = 0; i < ownArgs.length; i += 1) {
177
+ const token = ownArgs[i];
178
+ // Not a flag: positional, a bare `-` (stdin), or a negative number.
179
+ if (!token.startsWith("-") || token === "-" || /^-\d/.test(token))
180
+ continue;
181
+ // Node's util.parseArgs, which citty delegates to, treats one-dash tokens
182
+ // as bundled short flags. Boolean aliases may be combined (`-qy`), while a
183
+ // string alias consumes the remainder (`-mhello`) or the following token.
184
+ // It never treats `-auto-fix` as the long `auto-fix` option.
185
+ if (!token.startsWith("--")) {
186
+ const shortFlags = token.slice(1);
187
+ for (let offset = 0; offset < shortFlags.length; offset += 1) {
188
+ const rawName = shortFlags[offset];
189
+ const candidate = cittyComparableName(rawName);
190
+ if (!known.names.has(candidate))
191
+ throwUnknownFlag(token, `-${rawName}`, known);
192
+ if (known.valueFlags.has(candidate)) {
193
+ if (offset === shortFlags.length - 1)
194
+ i += 1;
195
+ break;
196
+ }
197
+ }
198
+ continue;
199
+ }
200
+ const withoutDashes = token.replace(/^-{1,2}/, "");
201
+ const [rawName = ""] = withoutDashes.split("=", 1);
202
+ const hasInlineValue = withoutDashes.includes("=");
203
+ // `--no-foo` is citty's negation of the BOOLEAN `--foo`. Resolving it
204
+ // against a value flag would accept `--no-limit`, which citty's own `--no-`
205
+ // preprocessing force-sets to `limit: false` (no type check, before
206
+ // parseArgs runs) — a boolean reaching a string parser, i.e. an internal
207
+ // error (exit 70) instead of the usage error (exit 2) this is here to give.
208
+ const negated = rawName.startsWith("no-") && known.booleanFlags.has(cittyComparableName(rawName.slice(3)))
209
+ ? rawName.slice(3)
210
+ : undefined;
211
+ const candidates = [cittyComparableName(rawName), ...(negated ? [cittyComparableName(negated)] : [])];
212
+ if (selfDiagnosed !== undefined && candidates.some((name) => selfDiagnosed.has(name)))
213
+ continue;
214
+ if (!candidates.some((name) => known.names.has(name))) {
215
+ // `workflow run` owns one deliberately dynamic namespace: long options
216
+ // become exact-name workflow parameters and are checked against the
217
+ // frozen plan before a run is inserted. Short flags remain strict.
218
+ if (dynamicWorkflowParams)
219
+ continue;
220
+ throwUnknownFlag(token.split("=")[0], `--${rawName}`, known);
221
+ }
222
+ // Skip a declared value flag's value so `--reason "--x"` is not scanned.
223
+ if (!hasInlineValue && candidates.some((name) => known.valueFlags.has(name)))
224
+ i += 1;
225
+ }
226
+ }
package/dist/cli.js CHANGED
@@ -60,6 +60,7 @@ import { defineCommand, parseArgs, renderUsage, runCommand, showUsage } from "ci
60
60
  import { findCittyTopLevelCommand, 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
+ import { assertKnownFlags, closestMatch } from "./cli/unknown-flags.js";
63
64
  import { agentCommand, lintCommand } from "./commands/agent/contribute-cli.js";
64
65
  import { generateBashCompletions, installBashCompletions } from "./commands/completions.js";
65
66
  import { configCommand } from "./commands/config-cli.js";
@@ -236,7 +237,13 @@ const setupCommand = defineCommand({
236
237
  printSetupTtyHint(result);
237
238
  }
238
239
  else {
239
- // Interactive wizard
240
+ // Interactive wizard. Guard the TTY first: the wizard's prompts read
241
+ // from stdin, so a piped/redirected/CI invocation would render the
242
+ // first prompt and then block forever instead of failing — and `akm
243
+ // setup` is the first command users automate.
244
+ if (process.stdin.isTTY !== true) {
245
+ throw new UsageError("Interactive setup requires a TTY. Pass --yes to accept defaults, or --config <json> / --from <file> to configure non-interactively.", "NON_INTERACTIVE_REQUIRES_YES");
246
+ }
240
247
  const { runSetupWizard } = await import("./setup/setup.js");
241
248
  await runSetupWizard({ dir: args.dir, noInit });
242
249
  }
@@ -818,40 +825,9 @@ function findUnknownCommandAttempt(rawArgs) {
818
825
  args = args.slice(idx + 1);
819
826
  }
820
827
  }
821
- /**
822
- * Standard edit-distance DP, single-row-at-a-time (sizes here are always
823
- * short command-name strings). Builds each row left-to-right, appending as
824
- * it goes, so every index read below is already-populated — the `?? 0`
825
- * fallbacks only satisfy `noUncheckedIndexedAccess`, they never fire.
826
- */
827
- function levenshteinDistance(a, b) {
828
- let previousRow = Array.from({ length: b.length + 1 }, (_, j) => j);
829
- for (let i = 1; i <= a.length; i++) {
830
- const currentRow = [i];
831
- for (let j = 1; j <= b.length; j++) {
832
- const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
833
- const deletion = (previousRow[j] ?? 0) + 1;
834
- const insertion = (currentRow[j - 1] ?? 0) + 1;
835
- const substitution = (previousRow[j - 1] ?? 0) + substitutionCost;
836
- currentRow.push(Math.min(deletion, insertion, substitution));
837
- }
838
- previousRow = currentRow;
839
- }
840
- return previousRow[b.length] ?? 0;
841
- }
842
- /** Closest candidate within a length-scaled distance threshold, or undefined when nothing is close enough to be worth suggesting. */
828
+ /** Closest command within a length-scaled distance threshold (shared DP in cli/unknown-flags.ts). */
843
829
  function closestCommandMatch(attempted, candidates) {
844
- let best;
845
- let bestDistance = Number.POSITIVE_INFINITY;
846
- for (const candidate of candidates) {
847
- const distance = levenshteinDistance(attempted, candidate);
848
- if (distance < bestDistance) {
849
- bestDistance = distance;
850
- best = candidate;
851
- }
852
- }
853
- const threshold = Math.max(2, Math.ceil(attempted.length / 2));
854
- return best !== undefined && bestDistance <= threshold ? best : undefined;
830
+ return closestMatch(attempted, candidates, Math.max(2, Math.ceil(attempted.length / 2)));
855
831
  }
856
832
  const CLI_HELP_POINTER = "Run `akm --help` for usage.";
857
833
  /**
@@ -898,6 +874,10 @@ function toUsageErrorFromCliError(error, rawArgs) {
898
874
  const message = error.message.replace(/\x1b\[[0-9;]*m/g, "");
899
875
  return new UsageError(message, code, hint);
900
876
  }
877
+ /** Normalize citty's private CLIError for the real entrypoint and in-process test harness. */
878
+ export function normalizeCittyCliError(error, rawArgs) {
879
+ return isCittyCliError(error) ? toUsageErrorFromCliError(error, rawArgs) : error;
880
+ }
901
881
  /**
902
882
  * The CLI's real startup sequence, extracted into a function so error paths
903
883
  * can `return` early — top-level `return` is a syntax error in an ES module,
@@ -1016,6 +996,10 @@ async function runCli() {
1016
996
  console.log(pkgVersion);
1017
997
  return;
1018
998
  }
999
+ // citty's parseArgs (strict: false) silently ignores undeclared flags, so
1000
+ // a typo used to run the command with defaults and exit 0. Checked here, after --help and
1001
+ // --version, so those keep working on any command.
1002
+ assertKnownFlags(main, rawArgs);
1019
1003
  await runCommand(main, { rawArgs });
1020
1004
  }
1021
1005
  catch (error) {
@@ -1027,7 +1011,7 @@ async function runCli() {
1027
1011
  // + raw `console.error(message)` — a one-line diagnosis plus a short
1028
1012
  // hint (with a did-you-mean suggestion when applicable), not a ~46KB
1029
1013
  // usage dump.
1030
- emitJsonError(toUsageErrorFromCliError(error, rawArgs));
1014
+ emitJsonError(normalizeCittyCliError(error, rawArgs));
1031
1015
  return;
1032
1016
  }
1033
1017
  // Anything else escaping here is a genuinely unexpected failure outside