akm-cli 0.9.16-alpha.1 → 0.9.16

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 (147) hide show
  1. package/CHANGELOG.md +56 -132
  2. package/dist/assets/hints/cli-hints-full.md +13 -6
  3. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  4. package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
  5. package/dist/cli/retired-commands.js +0 -4
  6. package/dist/cli/unknown-flags.js +3 -36
  7. package/dist/commands/env/env-binding.js +4 -4
  8. package/dist/commands/env/env-cli.js +3 -3
  9. package/dist/commands/improve/collapse-detector.js +2 -2
  10. package/dist/commands/improve/consolidate.js +4 -6
  11. package/dist/commands/improve/improve-cli.js +20 -15
  12. package/dist/commands/improve/reflect.js +23 -2
  13. package/dist/commands/lint/base-linter.js +9 -0
  14. package/dist/commands/lint/env-key-rules.js +2 -2
  15. package/dist/commands/proposal/propose.js +15 -1
  16. package/dist/commands/proposal/repository.js +3 -12
  17. package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
  18. package/dist/commands/proposal/validators/proposal-validators.js +5 -4
  19. package/dist/commands/read/curate.js +44 -34
  20. package/dist/commands/read/search.js +35 -54
  21. package/dist/commands/read/show.js +21 -2
  22. package/dist/commands/registry-cli.js +5 -5
  23. package/dist/commands/sources/add-cli.js +59 -16
  24. package/dist/commands/sources/bundle-cli.js +35 -11
  25. package/dist/commands/sources/bundle-config-ops.js +30 -0
  26. package/dist/commands/sources/dangerous-env-audit.js +4 -4
  27. package/dist/commands/sources/info.js +8 -8
  28. package/dist/commands/sources/installed-stashes.js +55 -61
  29. package/dist/commands/sources/source-add.js +39 -38
  30. package/dist/commands/sources/source-manage.js +34 -12
  31. package/dist/commands/sources/stash-cli.js +111 -119
  32. package/dist/commands/sources/stash-skeleton.js +6 -3
  33. package/dist/commands/tasks/explain.js +4 -1
  34. package/dist/commands/tasks/tasks-cli.js +31 -9
  35. package/dist/commands/tasks/tasks.js +239 -194
  36. package/dist/commands/tasks/validate.js +20 -32
  37. package/dist/core/activation-policy.js +4 -4
  38. package/dist/core/adapter/adapters/akm-adapter.js +8 -35
  39. package/dist/core/adapter/adapters/akm-metadata.js +1 -11
  40. package/dist/core/adapter/execution-source.js +10 -29
  41. package/dist/core/asset/asset-placement.js +0 -35
  42. package/dist/core/config/config-schema.js +64 -8
  43. package/dist/core/config/config-sources.js +96 -2
  44. package/dist/core/config/config.js +190 -24
  45. package/dist/core/config/legacy-source-shape-shim.js +9 -0
  46. package/dist/core/config/schema/embedding.js +30 -7
  47. package/dist/core/config/schema/execution.js +23 -0
  48. package/dist/core/config/schema/experimental.js +1 -1
  49. package/dist/core/config/schema/scheduler.js +20 -0
  50. package/dist/core/config/schema/search.js +10 -12
  51. package/dist/core/config/schema/sources-bundles.js +32 -1
  52. package/dist/core/content-safety.js +52 -0
  53. package/dist/core/errors.js +2 -5
  54. package/dist/core/maintenance-barrier.js +11 -13
  55. package/dist/core/paths.js +11 -0
  56. package/dist/core/run-lock.js +2 -5
  57. package/dist/core/state/migrations.js +1 -26
  58. package/dist/core/state-db.js +27 -63
  59. package/dist/core/type-presentation.js +1 -1
  60. package/dist/core/write-source.js +13 -8
  61. package/dist/indexer/bundle-identity-guard.js +45 -8
  62. package/dist/indexer/ensure-index.js +0 -5
  63. package/dist/indexer/index-db-contention.js +56 -0
  64. package/dist/indexer/index-rebuild-lock.js +73 -0
  65. package/dist/indexer/index-written-assets.js +171 -133
  66. package/dist/indexer/indexer.js +1621 -458
  67. package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
  68. package/dist/indexer/materialize-embeddings.js +785 -0
  69. package/dist/indexer/passes/dir-staleness.js +161 -0
  70. package/dist/indexer/passes/metadata.js +1 -18
  71. package/dist/indexer/scan/drain-dir.js +70 -27
  72. package/dist/indexer/search/db-search.js +89 -373
  73. package/dist/indexer/search/ranking-contributors.js +16 -21
  74. package/dist/indexer/search/ranking.js +57 -135
  75. package/dist/indexer/search/search-source.js +29 -11
  76. package/dist/integrations/agent/execution-lowering.js +3 -2
  77. package/dist/integrations/agent/execution-preparation.js +32 -1
  78. package/dist/integrations/agent/prompts.js +1 -1
  79. package/dist/integrations/agent/request-lowering.js +3 -2
  80. package/dist/llm/client.js +3 -11
  81. package/dist/llm/embedder.js +3 -10
  82. package/dist/llm/embedders/remote.js +104 -133
  83. package/dist/llm/feature-gate.js +2 -4
  84. package/dist/llm/rerank-client.js +3 -3
  85. package/dist/output/html-render.js +2 -1
  86. package/dist/output/shapes/passthrough.js +2 -1
  87. package/dist/output/stdout.js +24 -0
  88. package/dist/output/text/command-format.js +13 -19
  89. package/dist/output/text/helpers.js +1 -1
  90. package/dist/output/text/index.js +2 -5
  91. package/dist/output/text.js +4 -3
  92. package/dist/registry/resolve.js +37 -10
  93. package/dist/scripts/akm-migrate-node.js +15197 -11351
  94. package/dist/scripts/akm-migrate.js +15514 -11668
  95. package/dist/setup/semantic-assets.js +2 -2
  96. package/dist/setup/setup.js +3 -3
  97. package/dist/setup/steps/connection.js +2 -3
  98. package/dist/setup/steps/tasks.js +29 -36
  99. package/dist/sources/providers/git-install.js +17 -11
  100. package/dist/sources/providers/git-provider.js +12 -5
  101. package/dist/sources/providers/git-stash.js +38 -16
  102. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  103. package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
  104. package/dist/storage/repositories/index-connection.js +3 -1
  105. package/dist/storage/repositories/index-entries-repository.js +68 -77
  106. package/dist/storage/repositories/index-entry-schema.js +25 -16
  107. package/dist/storage/repositories/index-fts-repository.js +263 -29
  108. package/dist/storage/repositories/index-meta-repository.js +29 -0
  109. package/dist/storage/repositories/index-schema.js +122 -115
  110. package/dist/storage/repositories/index-utility-repository.js +1 -1
  111. package/dist/storage/repositories/index-vec-repository.js +435 -22
  112. package/dist/tasks/activation-config.js +90 -0
  113. package/dist/tasks/backends/cron.js +9 -0
  114. package/dist/tasks/backends/launchd.js +1 -0
  115. package/dist/tasks/backends/schtasks.js +2 -0
  116. package/dist/tasks/embedded.js +4 -5
  117. package/dist/tasks/scheduler-binding.js +2 -2
  118. package/dist/tasks/scheduler-sync-preview.js +8 -1
  119. package/dist/tasks/scheduler-sync.js +19 -10
  120. package/dist/tasks/source/parse-task-source.js +10 -113
  121. package/dist/tasks/source/project-v4.js +2 -2
  122. package/dist/tasks/source/task-source-v4.js +4 -12
  123. package/dist/tasks/source/task-to-v3.js +4 -12
  124. package/dist/tasks/source/task-to-v4.js +40 -7
  125. package/docs/migration/README.md +1 -0
  126. package/docs/migration/release-notes/0.9.15.md +36 -34
  127. package/docs/migration/release-notes/0.9.16.md +60 -98
  128. package/docs/migration/release-notes/README.md +0 -5
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  130. package/docs/reference/cli.md +124 -122
  131. package/docs/reference/configuration.md +137 -133
  132. package/docs/reference/data-and-telemetry.md +1 -2
  133. package/docs/reference/tasks.md +34 -29
  134. package/package.json +1 -1
  135. package/schemas/akm-config.json +170 -6
  136. package/schemas/akm-task.json +1 -2
  137. package/dist/commands/sources/index-status.js +0 -99
  138. package/dist/core/hash.js +0 -18
  139. package/dist/indexer/drain.js +0 -306
  140. package/dist/indexer/embedding-identity.js +0 -20
  141. package/dist/indexer/enrich.js +0 -260
  142. package/dist/indexer/reconcile.js +0 -890
  143. package/dist/indexer/scan/parse-file.js +0 -66
  144. package/dist/indexer/units/unit.js +0 -159
  145. package/dist/llm/embedders/provider-limits.js +0 -288
  146. package/dist/storage/repositories/files-repository.js +0 -181
  147. package/dist/storage/repositories/units-repository.js +0 -510
package/CHANGELOG.md CHANGED
@@ -6,147 +6,71 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.9.16-alpha.1] - 2026-09-11
9
+ ## [0.9.16] - 2026-09-22
10
10
 
11
- ### Added
11
+ ### Fixed
12
12
 
13
- - **`akm index status`.** A cheap, read-only snapshot of `index.db`'s
14
- current state — files tracked, entries, distinct units, how many have a
15
- vector for the active embedding identity (and therefore the embedding
16
- queue's remaining depth), the active identity itself, and the last
17
- reconcile/build times — with no writes. Mirrors `akm info`'s
18
- absent/inaccessible handling: a missing index reads as the ordinary
19
- first-run state, an unreadable one is reported, never silently presented
20
- as empty.
21
- - **A credential diagnostic on the embedding queue.** Before the first
22
- provider request `akm index`'s drain makes, one default-level line names
23
- the embedding endpoint, model, and the credential's SOURCE (never the
24
- resolved value) whenever a remote endpoint is configured — so a field run
25
- can compare it against what the gateway actually saw (#953).
13
+ - **Result documents larger than 64 KiB are no longer truncated on a piped
14
+ stdout.** `akm show`/`search`/`config get … | python3 -c …` (or `| head`, or
15
+ any other pipe) returned exactly 65,536 bytes — the Linux pipe-buffer size —
16
+ producing unparseable JSON, while the same command with `--output <file>`
17
+ wrote the complete document. The two stdout writers (`deliverRendered` for
18
+ json/yaml/text/md/html, `outputJsonl` for jsonl) used `console.log`, and on
19
+ Bun `console.log` issues a single `write(2)` against a non-blocking fd 1 and
20
+ silently discards whatever the kernel did not accept; a pipe accepts at most
21
+ one buffer's worth. Both now go through `writeStdout`
22
+ (`src/output/stdout.ts`), which uses `process.stdout.write` — that handles
23
+ the short write correctly, and the queued remainder keeps the process alive
24
+ until it drains. Byte-for-byte output is unchanged on every format; only the
25
+ transport moved.
26
26
 
27
- ### Changed
27
+ ### Added
28
28
 
29
- - **`akm index` is redesigned end to end
30
- (`docs/plans/index-redesign.md`).** The old walk/clean/embed/finalize
31
- phase pipeline, per-directory fingerprint cache, and duplicated storage
32
- (entry text held three times, every vector held twice plus a salvage
33
- copy) are replaced by two steps: reconcile, then drain. Reconcile
34
- (`src/indexer/reconcile.ts`) stat-walks every configured root against a
35
- `files` cache, hashing and re-parsing only what actually changed, and
36
- derives content-addressed units — one "card" unit per entry
37
- (name/description/tags/hints) and one "fragment" unit per Markdown
38
- section — into a single text table (`unit_texts`/`units_fts`). Drain
39
- (`src/indexer/drain.ts`) treats embedding as a queue, not a phase: the
40
- pending set is exactly the unit hashes with no vector under the active
41
- embedding identity, packed against the provider's own probed context
42
- window and slot count, written into a single vector table
43
- (`units`/`units_vec`) keyed by `(unit_hash, identity)`. An unchanged file
44
- or unit is never re-derived or re-embedded again — not on a rename, not
45
- on `akm index --full`, not on a future generation bump — because
46
- everything is keyed by content hash and observed provider identity, never
47
- by a row id or a config string.
48
- - **Every write path indexes what it just wrote, inline.** `remember`,
49
- `import`, extract's session-asset capture, `source clone`, and proposal
50
- accept each reconcile and drain exactly the paths/units they touched, in
51
- the same call as the write, with no lock probe and no background reindex
52
- spawn.
53
- - **Search is one query over `units`**, scoring lexical evidence by BM25
54
- magnitude through the calibrated transform the repository already had and
55
- combining it with semantic distance on the proven 0.7/0.3 split. Reciprocal
56
- rank fusion was tried first and measured worse than the path it replaced
57
- (0.918 against 0.933 on the `curate-golden` fixture, unchanged by splitting
58
- or weighting the lists), because a rank cannot tell a strong match from a
59
- weak one; the shipped scoring measures 0.936 with no banned-above-required
60
- hits. The semantic-only `minScore` floor is gone, type filters now apply in
61
- SQL before the candidate cap, and the exact/prefix/relaxed ladder tops up to
62
- the candidate budget instead of stopping at the first non-empty tier. The
63
- tier a hit came from is also ranking evidence: a unit matching every query
64
- token outranks one matching a subset, because the calibrated BM25 transform
65
- compresses even a sixfold magnitude difference into a few thousandths — far
66
- less than any single ranking contributor — so tier decides across tiers and
67
- magnitude decides within one.
68
- - **`akm index --full`** no longer drops anything first: it forces every
69
- walked file to be re-parsed (skipping the unchanged-file shortcut) but
70
- updates each file's existing row in place, keeping its id, vectors, and
71
- learned utility scores. **`akm index --reembed`** now means "drop the
72
- active embedding identity's vectors, then re-embed every unit from
73
- scratch."
74
- - **`akm index --skip-if-locked` is deprecated and does nothing.** Index
75
- runs no longer take a rebuild lock — every write is a short, idempotent,
76
- content-addressed transaction, so two concurrent runs converge instead of
77
- contending. Passing the flag prints one deprecation warning; kept only so
78
- an existing script does not fail on an unknown flag.
79
- - **The derived `index.db` generation changes from v23 to v24.** The first
80
- read (or explicit `akm index`) after upgrade re-derives `entries` and
81
- every unit from files, and embeds the full corpus once against the
82
- configured provider, since the new unit vector store does not carry
83
- forward the superseded per-entry vector tables. See the [0.9.16 migration
84
- note](docs/migration/release-notes/0.9.16.md) for the cost, stated
85
- plainly.
29
+ - **Scheduled execution is now granted by host-local config and bound to the
30
+ approved source installation.** `scheduler.enabled` records
31
+ `{kind, ref, sourceId}` entries written by `akm task enable`; authored task
32
+ frontmatter cannot enable itself. `akm migrate apply` performs the explicit,
33
+ backed-up conversion of older grants, and runtime config points unmigrated
34
+ installations to that command instead of silently inventing authority.
35
+ - **Executable assets now run beneath a host-owned tool ceiling.** Local
36
+ `execution.allowedTools` config caps asset tool requests. Workspace,
37
+ environment, and opaque runtime selection are no longer accepted from asset
38
+ frontmatter, and a transport that cannot enforce a non-empty resolved tool
39
+ policy fails before dispatch.
86
40
 
87
- ### Removed
41
+ ### Changed
88
42
 
89
- - **The index phase pipeline, directory-fingerprint staleness cache, the
90
- index rebuild lock, and the index-path use of the maintenance barrier** —
91
- roughly 4,800 lines across the files that implemented the old index core,
92
- replaced by the reconcile/drain design above (`docs/plans/index-redesign.md`).
93
- - **`entries_fts`, `entry_fragments_fts`, the legacy per-entry `embeddings`
94
- materializer (`materialize-embeddings.ts`), and `embedding_salvage`** —
95
- superseded by the single `unit_texts`/`units_fts` and `units`/`units_vec`
96
- tables, which never need a salvage-before-discard step because they are
97
- content-addressed and never wholesale-discarded.
98
- - **`akm index --enrich`, `--re-enrich`, `--clean`, and `--dry-run`.**
99
- Plain `akm index` now always performs metadata enrichment when an engine
100
- is configured, and every run already removes stale entries as part of
101
- reconcile — the work these flags used to separately opt into. All four
102
- now fail with a `UsageError` naming the replacement, instead of silently
103
- doing nothing or being silently accepted.
104
- - **Config keys `embedding.maxInputTokens`, `embedding.maxTokens`,
105
- `embedding.batchSize`, `embedding.contextLength`, and
106
- `search.minScore`.** Embedding request packing is sourced from the
107
- provider's own probed limits (unchanged from 0.9.15 packing, applied to
108
- units instead of whole entries); the fused score has no comparable 0–1
109
- threshold to tune. A config that still sets any of them loads without
110
- error and is simply ignored.
111
- - **The `"ready-js"` semantic-search status.** Named a pure-JS
112
- cosine-similarity fallback for when `sqlite-vec` was unavailable; the new
113
- unit vector store has no BLOB-table fallback to fall back to, so nothing
114
- produces that value any more.
43
+ - **Unscoped `akm task sync` reconciles every enabled bundle.** It refreshes
44
+ the native scheduler from the latest locally enabled task/workflow sources
45
+ and removes attributable bindings for bundles that have been disabled.
46
+ Removing a bundle also revokes its scheduler grants.
47
+ - **Bundle activation and identity are consistent across the CLI.** A disabled
48
+ bundle is inert for reads, writes, indexing, execution, and scheduling;
49
+ explicit lifecycle updates remain available. Physical-root identity rejects
50
+ duplicate or symlink-aliased bundle registrations and prevents an
51
+ `AKM_BUNDLE_DIR` alias from reactivating disabled content.
52
+ - **Shared config inheritance now separates portable policy from host
53
+ authority.** Source/default ownership, registries, scheduler and execution
54
+ grants, credentials, executable arguments/workspace, setup/experimental
55
+ state, reranker connections, and publication hooks stay local. Every
56
+ bundle-relative `extends` hop is checked by real path containment.
57
+ - **Unsafe overrides now name one risk each.** Use
58
+ `--allow-insecure-transport` for reviewed plain HTTP and
59
+ `--allow-dangerous-env-keys` for reviewed process-hijacking environment
60
+ keys; the former combined `--allow-insecure` switch is removed.
61
+ - **Bundle/source resolution carries explicit default and priority state.**
62
+ Search, show, write targeting, registry installation, and configuration
63
+ mutation no longer infer ownership or trust from array position.
115
64
 
116
65
  ### Fixed
117
66
 
118
- - **Two akm processes starting at the same moment against a database neither
119
- has created yet no longer fail.** `state.db` and `index.db` each had a
120
- first-open race. `index.db` created `entries` about twenty statements
121
- before it stamped the generation, so a second opener read
122
- entries-without-a-generation as a stale index and dropped the table out
123
- from under the first process, which then exited 70 with `no such table:
124
- entries` — measured at 8 of 440 racing child processes. `state.db` read its
125
- migration ledger and its "does this file have any other tables" check as
126
- two separate statements, so a sibling's bootstrap committing between them
127
- looked exactly like a legacy unversioned database and was refused outright
128
- — 2 of 1200 racing trials. Both initializations are now single atomic
129
- units, measured at zero failures in 840 and 1200 trials respectively, idle
130
- and under load. Only a genuinely contended run still fails, as
131
- `INDEX_DB_CONTENDED`/`STATE_DB_CONTENDED` at exit 75, the documented
132
- retry-shortly contract. An already-initialized database takes the same
133
- unlocked path it always did.
134
- - **`akm show <memory>` no longer fails when that memory has an inferred
135
- `.derived` twin.** It exited 2 with `RESOURCE_ALREADY_EXISTS` ("multiple
136
- physical owners"), so once `akm improve` derived a memory — its ordinary
137
- output — the base ref stopped being usable, and the read path that did not
138
- fail served the twin's content instead of the memory's. `.derived` is a
139
- provenance marker on the same identity and the placement layer always
140
- declared that the plain file wins; the physical-owner lookup now honours
141
- that instead of discarding it. Genuinely ambiguous cases, including two
142
- case-only spellings of one name and `env`'s co-equal `.env`/`default.env`
143
- pair, still fail loudly and unchanged. Present since before 0.9.15.
144
- - **A derived memory no longer outranks the memory it was derived from.** A
145
- twin's own filename contributed a `derived` tag that minted a synthetic
146
- alias, and the machine-written `source:` provenance backref was folded into
147
- search hints, together handing the twin a flat 0.42 of ranking credit for
148
- bookkeeping no author wrote — enough to beat a memory whose description
149
- matched the query verbatim. Neither field earns ranking credit any more.
67
+ - Protected generated improve content from credential echoes, redacted bodies,
68
+ and run-only scaffolding, while exercising the real bounded engine probe.
69
+ - Repaired degraded sqlite-vec mirrors, preserved scheduler intent across
70
+ synchronization, kept explicit setup choices and rollback serialization
71
+ stable, and tolerated valid sharded startup contention.
72
+ - Made git, website, npm, and filesystem bundle add/update/remove workflows
73
+ converge on the same lifecycle and dangerous-environment audit behavior.
150
74
 
151
75
  ## [0.9.15] - 2026-09-10
152
76
 
@@ -264,6 +264,7 @@ akm bundle add @scope/pkg # From npm (managed)
264
264
  akm bundle add owner/repo # From GitHub (managed)
265
265
  akm bundle add ./path/to/local/bundle # Local directory
266
266
  akm bundle add git@github.com:org/repo.git --provider git --name my-skills --writable
267
+ akm bundle add https://github.com/org/private.git --provider git --credential '$GIT_READ_TOKEN'
267
268
  akm registry add https://skills.sh --name skills.sh --provider skills-sh # Add the skills.sh registry
268
269
  akm registry remove skills.sh # Remove the skills.sh registry
269
270
  akm bundle list # List all sources
@@ -271,9 +272,13 @@ akm bundle list --kind git # Filter by provider (files
271
272
  akm bundle remove <target> # Remove by id, ref, path, or name
272
273
  akm bundle update --all # Update all managed sources
273
274
  akm bundle update <target> --force # Force re-download
274
- akm bundle update <target> --allow-insecure # Approve reviewed dangerous env keys
275
+ akm bundle update <target> --allow-dangerous-env-keys # Approve reviewed dangerous env keys
276
+ akm bundle update --all --skip-if-locked # Scheduled refresh; exit 0 on DB contention
275
277
  ```
276
278
 
279
+ Git bundles refresh only when `akm bundle update` runs. Schedule that command
280
+ when automatic refresh is desired.
281
+
277
282
  ## Registries
278
283
 
279
284
  ```sh
@@ -357,7 +362,9 @@ file plus one `sync` is a complete workflow.
357
362
  ```sh
358
363
  akm task add nightly-improve --schedule "@daily" --command "akm improve --strategy default"
359
364
  akm task add briefing --schedule "0 9 * * *" --prompt "Draft the morning briefing" # Inline command task
360
- akm task sync # Reconcile task files with the OS scheduler
365
+ akm task enable <bundle>//tasks/<id> # Enable locally and sync that bundle
366
+ akm task disable <bundle>//tasks/<id> # Disable locally and unschedule it
367
+ akm task sync # Reconcile activated refs from every enabled configured bundle
361
368
  akm task sync --rebind # Also re-pin the scheduler's akm binary/spelling
362
369
  akm task doctor # Scheduler binding + runtime eligibility diagnosis
363
370
  akm task history # Recent run rows (status, timing)
@@ -370,10 +377,10 @@ Task files use task source v4 (`version: 4`). There is no `akm:` options bag
370
377
  or `on:` block — every control (`schedule`, `timeout`, `engine`, `model`,
371
378
  `redact`, `maxSteps`, `maxRetries`, …) is a top-level key now. Typed
372
379
  `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
380
+ workflow's (`output:` replaces v3's `akm.outputSchema`). Task source never
381
+ controls activation: use `akm task enable` / `disable`, which update the
382
+ host-local config allow-list and sync. To remove one, delete the YAML and run
383
+ `akm task sync` — the scheduler entry is unbound. Top-level
377
384
  `timeout:` may be `null` (disable the invocation timer) or a duration/number
378
385
  overriding the selected engine invocation timeout. Preview old task-v2/v3
379
386
  conversion with `akm migrate apply --dry-run`.
@@ -1,4 +1,4 @@
1
1
  version: 4
2
- run: akm index
2
+ run: akm index --skip-if-locked
3
3
  description: Nightly incremental index refresh
4
4
  schedule: "0 4 * * *"
@@ -1,9 +1,6 @@
1
1
  version: 4
2
2
  run: akm improve --strategy catchup --skip-if-locked --require-engines
3
3
  description: Manual recovery — consolidation + triage drain (run on demand via `akm task run akm-improve-catchup`)
4
- # Manual-recovery task: ships disabled (the retired registerDefaultTasks
5
- # marked it enableMode: "manual"). `akm task run` works while disabled;
6
- # opting into the schedule is `schedule[].enabled: true` + `akm task sync`.
7
- schedule:
8
- - cron: "0 4 * * *"
9
- enabled: false
4
+ # Manual-recovery task: setup offers it disabled by default. `akm task run`
5
+ # still works manually; `akm task enable` opts this host into the schedule.
6
+ schedule: "0 4 * * *"
@@ -62,8 +62,6 @@ const RETIRED_COMMAND_HINTS = {
62
62
  "workflow report": "`akm workflow report` was removed — the external-driver protocol is gone; `akm workflow run <target>` dispatches and records units itself.",
63
63
  "config show": "`akm config show` was removed in 0.9 — use `akm config list`.",
64
64
  "config validate": "`akm config validate` was removed in 0.9 — the config file is validated on every load.",
65
- "task enable": "`akm task enable` was removed in 0.9 — set `enabled: true` in the task YAML, then `akm task sync`.",
66
- "task disable": "`akm task disable` was removed in 0.9 — set `enabled: false` in the task YAML, then `akm task sync`.",
67
65
  "task init": "`akm task init` was removed in 0.9 — `akm setup` seeds the default schedules.",
68
66
  "task show": "there is no `task show` — task files are indexed assets; use `akm show <ref>`.",
69
67
  "task remove": "there is no `task remove` — delete the task YAML, then run `akm task sync` to unbind it.",
@@ -104,8 +102,6 @@ export function retiredCommandHint(parentPath, attempted) {
104
102
  */
105
103
  const RETIRED_FLAG_HINTS = {
106
104
  "index --background": "`--background` was removed in 0.9 — the flag never actually backgrounded; use `--quiet`.",
107
- "index --clean": "`--clean` was removed in the index redesign — every `akm index` run now removes stale entries as part of reconcile, the same work `--clean` used to opt into.",
108
- "index --dry-run": "`--dry-run` was removed in the index redesign along with `--clean`, the only flag it ever modified.",
109
105
  "setup --detect-only": "`--detect-only` was removed in 0.9 — environment detection runs inside `akm setup`; `akm info` reports the configured capabilities.",
110
106
  "setup --reset-recommended": "`--reset-recommended` was removed in 0.9 — `akm setup` now offers to apply recommended defaults interactively.",
111
107
  "proposal extract --watch": "`--watch` was removed in 0.9 — schedule `akm proposal extract --auto` as a task instead.",
@@ -30,28 +30,6 @@ import { cittyComparableName, findCittyTopLevelCommandIndex, toAliasArray, } fro
30
30
  import { retiredFlagHint } from "./retired-commands.js";
31
31
  /** Flags citty implements itself, which no command declares. */
32
32
  const IMPLICIT_FLAGS = ["help", "h", "version", "v"];
33
- /**
34
- * Keys `GLOBAL_OUTPUT_ARGS` (cli/shared.ts) contributes. Every leaf AND every
35
- * group re-declares these so citty's parser consumes their values (see that
36
- * constant's own doc) — so their mere presence in a group's own `args` never
37
- * means the group has a real business use for its own flags; only a key
38
- * beyond this set does. Duplicated here (not imported) to avoid coupling this
39
- * shared scanner to `cli/shared.ts`'s own dependency surface; keep in sync if
40
- * `GLOBAL_OUTPUT_ARGS` gains or loses a key.
41
- */
42
- const GLOBAL_OUTPUT_ARG_KEYS = new Set(["format", "detail", "shape", "output", "quiet", "verbose"]);
43
- /**
44
- * Whether a group declares any flag of its own beyond the global output
45
- * scaffold — i.e. whether its bare invocation (no subcommand token) runs a
46
- * REAL default body that reads those flags (`akm index`'s `full`/`reembed`),
47
- * as opposed to the canonical bare-group usage error every other group falls
48
- * through to (`defineGroupCommand`, cli/shared.ts) — a `UsageError` either
49
- * way, whose own declared args (if any) exist only so `--help` documents them,
50
- * never so a body reads them.
51
- */
52
- function hasOwnBusinessArgs(cmd) {
53
- return Object.keys(cmd.args ?? {}).some((key) => !GLOBAL_OUTPUT_ARG_KEYS.has(key));
54
- }
55
33
  /**
56
34
  * Retired flags whose commands still diagnose them THEMSELVES, with a message
57
35
  * that names the replacement ("`--scope` was removed, use `--filter`",
@@ -124,20 +102,9 @@ function collectKnownArgs(root, rawArgs) {
124
102
  break;
125
103
  const idx = findCittyTopLevelCommandIndex(args, (cmd.args ?? {}));
126
104
  const token = idx >= 0 ? args[idx] : undefined;
127
- // A group with no subcommand token: citty always calls the group's own
128
- // `run` regardless (a `defineGroupCommand` group's `run` is never
129
- // undefined — see its doc in cli/shared.ts). For most groups that `run`
130
- // is the canonical bare-group usage error, and its own declared args (if
131
- // any) exist only for `--help`, so a flag meant for the subcommand the
132
- // caller forgot to type must not be misreported as "unknown" — stand
133
- // down, exactly as before. `akm index` is the one group with a REAL
134
- // default body that reads its own flags (`full`/`reembed`), so `akm index
135
- // --background` must still be rejected even though `index` also carries a
136
- // real subcommand (`status`) — `hasOwnBusinessArgs` is what tells the two
137
- // apart.
138
- if (token === undefined) {
139
- return { names, valueFlags, booleanFlags, displayNames, path, resolved: hasOwnBusinessArgs(cmd) };
140
- }
105
+ // A group with no subcommand token: citty reports "no command specified".
106
+ if (token === undefined)
107
+ return { names, valueFlags, booleanFlags, displayNames, path, resolved: false };
141
108
  const sub = subCommands[token];
142
109
  // An unrecognized token: citty reports the unknown command, which is the
143
110
  // real problem — its flags are beside the point.
@@ -87,16 +87,16 @@ export function resolveEnvBinding(target, options = {}) {
87
87
  const decision = decideDangerousEnvInjection({
88
88
  dangerousKeys: dangerous,
89
89
  thirdParty: Boolean(source.registryId),
90
- allowInsecure: options.allowInsecure,
90
+ allowDangerousEnvKeys: options.allowDangerousEnvKeys,
91
91
  });
92
92
  if (decision === "block") {
93
93
  throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
94
94
  ` Review the file, then copy the values into a first-party env if you trust them, ` +
95
- `or pass --allow-insecure once you have.`, "INVALID_FLAG_VALUE");
95
+ `or pass --allow-dangerous-env-keys once you have.`, "INVALID_FLAG_VALUE");
96
96
  }
97
97
  if (decision === "warn") {
98
- warn(options.allowInsecure && source.registryId
99
- ? `${detail} Injecting anyway (--allow-insecure).`
98
+ warn(options.allowDangerousEnvKeys && source.registryId
99
+ ? `${detail} Injecting anyway (--allow-dangerous-env-keys).`
100
100
  : `${detail} Injecting anyway (first-party stash).`);
101
101
  }
102
102
  }
@@ -214,7 +214,7 @@ async function runEnvInjected(target, opts) {
214
214
  const { values: envValues } = resolveEnvBinding(target, {
215
215
  only: opts.only,
216
216
  except: opts.except,
217
- allowInsecure: opts.allowInsecure,
217
+ allowDangerousEnvKeys: opts.allowDangerousEnvKeys,
218
218
  });
219
219
  const mergedEnv = buildChildEnv(process.env, {
220
220
  clean: opts.clean === true,
@@ -283,7 +283,7 @@ const envRunCommand = defineJsonCommand({
283
283
  type: "string",
284
284
  description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
285
285
  },
286
- "allow-insecure": {
286
+ "allow-dangerous-env-keys": {
287
287
  type: "boolean",
288
288
  description: "Allow injecting a process-hijacking variable (e.g. LD_PRELOAD, GIT_SSH_COMMAND) from a third-party stash, which otherwise blocks. Use only after explicitly reviewing the env file.",
289
289
  default: false,
@@ -295,7 +295,7 @@ const envRunCommand = defineJsonCommand({
295
295
  except: parseKeyListFlag(args.except),
296
296
  clean: args.clean === true,
297
297
  inherit: parseKeyListFlag(args.inherit) ?? [],
298
- allowInsecure: args["allow-insecure"] === true,
298
+ allowDangerousEnvKeys: args["allow-dangerous-env-keys"] === true,
299
299
  });
300
300
  },
301
301
  });
@@ -36,10 +36,10 @@ import { getImproveProcessConfig } from "../../core/config/config.js";
36
36
  import { appendEvent } from "../../core/events.js";
37
37
  import { withStateDb } from "../../core/state-db.js";
38
38
  import { warn } from "../../core/warn.js";
39
- import { searchEntriesLexical } from "../../indexer/search/db-search.js";
40
39
  import { deactivateCanarySet, getActiveCanaries, getCanariesBySetId, insertCanaries, insertCycleMetrics, listActiveCanarySetIds, queryRecentCycleMetrics, } from "../../storage/repositories/canaries-repository.js";
41
40
  import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
42
41
  import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
42
+ import { searchFts } from "../../storage/repositories/index-fts-repository.js";
43
43
  import { computeBigramDiversity, DEFAULT_MAX_GENERATION } from "./anti-collapse.js";
44
44
  import { getAllRankScores } from "./salience.js";
45
45
  // ── Defaults (mirrored in config-schema.ts ImproveCollapseDetectorSchema) ────
@@ -189,7 +189,7 @@ export function normHash(text) {
189
189
  * Returns the 0-based rank of the first hit, or -1.
190
190
  */
191
191
  function scoreCanary(indexDb, canary, k) {
192
- const results = searchEntriesLexical(indexDb, canary.query, k);
192
+ const results = searchFts(indexDb, canary.query, k);
193
193
  const anchorConceptId = canaryConceptId(canary.anchor_ref);
194
194
  for (let i = 0; i < Math.min(results.length, k); i++) {
195
195
  const r = results[i];
@@ -24,7 +24,7 @@ import { callStructured, preflightStructuredLlmRunner } from "../../llm/structur
24
24
  import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositories/embeddings-repository.js";
25
25
  import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
26
26
  import { findEntryIdByRef, getAllEntries, getEntryById } from "../../storage/repositories/index-entries-repository.js";
27
- import { getNeighborsByEntryId } from "../../storage/repositories/units-repository.js";
27
+ import { getNeighborsByEntryId } from "../../storage/repositories/index-vec-repository.js";
28
28
  import { isProposalSkipped, listProposals, listProposalsReadOnly, proposalContent, } from "../proposal/repository.js";
29
29
  import { hasSupersededStatus, validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
30
30
  import { DEFAULT_RANDOM_CLUSTER_FRACTION } from "./anti-collapse.js";
@@ -1394,11 +1394,9 @@ export function narrowToIncrementalCandidates(memories, since, warnings, neighbo
1394
1394
  const id = findEntryIdByRef(db, conceptIdFromTypeName("memory", m.name));
1395
1395
  if (id === undefined)
1396
1396
  continue;
1397
- // index-redesign-contract.md B5f item 4 — `getNeighborsByEntryId`
1398
- // (units-repository.ts) already excludes the querying entry itself, so
1399
- // `neighborsPerChanged` genuinely OTHER neighbours are requested
1400
- // directly (no `+ 1` for self, no self-filter here).
1401
- for (const hit of getNeighborsByEntryId(db, id, neighborsPerChanged)) {
1397
+ for (const hit of getNeighborsByEntryId(db, id, neighborsPerChanged + 1)) {
1398
+ if (hit.id === id)
1399
+ continue;
1402
1400
  const entry = getEntryById(db, hit.id);
1403
1401
  if (!entry)
1404
1402
  continue;
@@ -15,7 +15,7 @@ import { redactSensitiveText } from "../../core/redaction.js";
15
15
  import { clearLogFile, setLogFile, warn } from "../../core/warn.js";
16
16
  import { resolveWriteTarget } from "../../core/write-source.js";
17
17
  import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
18
- import { probeEndpointOnce, probeLlmEndpoint } from "../../llm/client.js";
18
+ import { probeLlmReachable } from "../../llm/client.js";
19
19
  import { getOutputMode } from "../../output/context.js";
20
20
  import { deliverRendered } from "../../output/html-render.js";
21
21
  import { akmImprove, resolveImproveReadSource } from "./improve.js";
@@ -122,27 +122,32 @@ function collectRequiredEngineTargets(plan) {
122
122
  * `--require-engines` field re-test (#957): the static check above only
123
123
  * proves an engine is configured and credentialed — it cannot see a dead
124
124
  * endpoint. A field run against an unreachable engine sat silent for
125
- * minutes instead of hitting the documented exit-78 path. Reuse the SAME
126
- * bounded reachability probe `akm health`'s `default-llm-engine` /
127
- * `configured-engines` checks already run (`probeLlmEndpoint`, a single
128
- * `/models` GET bounded by its own default timeout) once per distinct
129
- * endpoint (via the shared `probeEndpointOnce` memoization health/checks.ts
130
- * also uses), so a dead engine is caught here instead of during dispatch.
125
+ * minutes instead of hitting the documented exit-78 path. Exercise the real
126
+ * model completion path with a tiny response and a three-second bound. The
127
+ * `/models` endpoint used by the lightweight health check is deliberately
128
+ * insufficient here: a gateway can list a model while its upstream completion
129
+ * route is dead (#980). Deduplicate by endpoint + model, not endpoint alone,
130
+ * because model backends behind one gateway can fail independently.
131
131
  */
132
- async function assertRequiredEnginesReachable(plan, probeReachable = probeLlmEndpoint) {
132
+ async function assertRequiredEnginesReachable(plan, probeReachable = (connection) => probeLlmReachable(connection, 3_000)) {
133
133
  const targets = collectRequiredEngineTargets(plan);
134
134
  if (targets.length === 0)
135
135
  return;
136
- const probesByEndpoint = new Map();
137
- const probed = await Promise.all(targets.map(async (target) => ({
138
- ...target,
139
- reach: await probeEndpointOnce(target.connection, probesByEndpoint, probeReachable),
140
- })));
136
+ const probesByConnection = new Map();
137
+ const probed = await Promise.all(targets.map(async (target) => {
138
+ const key = `${target.connection.endpoint.replace(/\/+$/, "")}|${target.connection.model}`;
139
+ let pending = probesByConnection.get(key);
140
+ if (!pending) {
141
+ pending = probeReachable(target.connection);
142
+ probesByConnection.set(key, pending);
143
+ }
144
+ return { ...target, reach: await pending };
145
+ }));
141
146
  const unreachable = probed.filter((item) => !item.reach.reachable);
142
147
  if (unreachable.length === 0)
143
148
  return;
144
149
  const lines = unreachable.map((item) => ` - ${item.process} (engine "${item.engine}", ${item.connection.endpoint}): ${item.reach.error ?? "did not respond"}`);
145
- throw new ConfigError(`--require-engines: ${unreachable.length} improve process${unreachable.length === 1 ? "" : "es"} cannot run because ${unreachable.length === 1 ? "its" : "their"} engine endpoint is not reachable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
150
+ throw new ConfigError(`--require-engines: ${unreachable.length} improve process${unreachable.length === 1 ? "" : "es"} cannot run because ${unreachable.length === 1 ? "its" : "their"} engine completion path is not reachable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
146
151
  }
147
152
  /**
148
153
  * `--show-prompt` (#952): render the composed reflect prompt for one asset ref
@@ -249,7 +254,7 @@ export const improveCommand = defineCommand({
249
254
  },
250
255
  "skip-if-locked": {
251
256
  type: "boolean",
252
- description: "If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with 'already running' (exit 75). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress.",
257
+ description: "If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with 'already running' (exit 78). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress.",
253
258
  default: false,
254
259
  },
255
260
  "require-engines": {
@@ -29,6 +29,7 @@ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
29
29
  import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
30
30
  import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
31
31
  import { loadConfig } from "../../core/config/config.js";
32
+ import { generatedContentRejection, stripReflectPromptScaffolding } from "../../core/content-safety.js";
32
33
  import { ConfigError, UsageError } from "../../core/errors.js";
33
34
  import { appendEvent, readEvents } from "../../core/events.js";
34
35
  import { lintLessonContent } from "../../core/lesson-lint.js";
@@ -511,7 +512,12 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
511
512
  mergedFm[field] = sourceFm[field];
512
513
  }
513
514
  }
514
- const cleanedBody = stripAppendedFrontmatter(rawLlmBody.replace(/^\s+/, ""));
515
+ const withoutAppendedFrontmatter = stripAppendedFrontmatter(rawLlmBody.replace(/^\s+/, ""));
516
+ const promptScaffolding = stripReflectPromptScaffolding(withoutAppendedFrontmatter);
517
+ const cleanedBody = promptScaffolding.content;
518
+ if (promptScaffolding.stripped) {
519
+ warnings.push('Removed echoed run-only "Avoid These Patterns" guidance from the proposed asset body (#963).');
520
+ }
515
521
  // #636 — deterministic description fallback (reflect-side belt-and-suspenders).
516
522
  // If the type requires a `description` and the merged frontmatter is still
517
523
  // MISSING one (source had none AND the model didn't author one), derive a
@@ -1881,7 +1887,22 @@ export async function akmReflect(options = {}) {
1881
1887
  // draft paths.
1882
1888
  cleanupReflectDrafts(draftPathsToCleanup);
1883
1889
  }
1884
- payload = { ...payload, content: redactSensitiveText(payload.content, sensitiveValues) };
1890
+ const unsafeContent = generatedContentRejection(payload.content, redactSensitiveText(payload.content, sensitiveValues));
1891
+ if (unsafeContent) {
1892
+ emitReflectFailed("parse_error", "parse_error", options.ref, {
1893
+ ...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
1894
+ });
1895
+ return {
1896
+ schemaVersion: 2,
1897
+ ok: false,
1898
+ reason: "parse_error",
1899
+ error: unsafeContent,
1900
+ ...(options.ref ? { ref: options.ref } : {}),
1901
+ engine: engineName,
1902
+ exitCode: result.exitCode,
1903
+ ...reflectNoticeFields(executionNotices),
1904
+ };
1905
+ }
1885
1906
  const refFailure = validateReflectPayloadRef({
1886
1907
  payload,
1887
1908
  result,
@@ -41,6 +41,7 @@ import { checkUnquotedDescriptionColon } from "../../core/asset/frontmatter-lint
41
41
  import { isArchivedRelPath } from "../../core/asset/memory-archive.js";
42
42
  import { conceptIdFromTypeName, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
43
43
  import { localDateStamp } from "../../core/common.js";
44
+ import { containsRedactedContent, REDACTED_CONTENT_MARKER } from "../../core/content-safety.js";
44
45
  import { findFenceRegions } from "./markdown-insertion.js";
45
46
  // ── Helpers ───────────────────────────────────────────────────────────────────
46
47
  /** Fold physically wrapped prose the same way a YAML plain scalar does. */
@@ -603,6 +604,14 @@ export function runBaseChecks(ctx) {
603
604
  .map((v) => String(v).trim())
604
605
  .filter(Boolean);
605
606
  const shouldRun = (issueType) => !lintSkip.includes(issueType);
607
+ if (shouldRun("redacted-content") && containsRedactedContent(currentRaw)) {
608
+ issues.push({
609
+ file: ctx.relPath,
610
+ issue: "redacted-content",
611
+ detail: `asset contains ${REDACTED_CONTENT_MARKER}; restore the original non-secret prose from version history`,
612
+ fixed: false,
613
+ });
614
+ }
606
615
  // ── 1. unquoted-colon ──────────────────────────────────────────────────
607
616
  if (shouldRun("unquoted-colon")) {
608
617
  const unquotedColonDetail = checkUnquotedDescriptionColon(ctx.frontmatter);
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Enforcement scope:
13
13
  * - `akm lint` reports findings as `dangerous-env-key` (non-blocking warn).
14
- * - `akm bundle add` BLOCKS install unless `--allow-insecure` is set (or, on TTY,
14
+ * - `akm bundle add` BLOCKS install unless `--allow-dangerous-env-keys` is set (or, on TTY,
15
15
  * the user explicitly confirms at the prompt).
16
16
  * - Local env writes do NOT consult this list — by design, the operator may
17
17
  * legitimately store any key locally. The gate exists only for third-party
@@ -22,7 +22,7 @@
22
22
  * invoked by many interactive tools and are a documented RCE vector when
23
23
  * sourced from untrusted environments. They will also flag on benign files
24
24
  * where the operator legitimately wants to set their editor — accept the
25
- * FP and bypass with `--allow-insecure` after review.
25
+ * FP and bypass with `--allow-dangerous-env-keys` after review.
26
26
  */
27
27
  import fs from "node:fs";
28
28
  import { listKeys } from "../env/env.js";