akm-cli 0.7.4 → 0.8.0-rc1

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 (158) hide show
  1. package/{CHANGELOG.md → .github/CHANGELOG.md} +34 -1
  2. package/.github/LICENSE +374 -0
  3. package/dist/cli/parse-args.js +43 -0
  4. package/dist/cli.js +1007 -593
  5. package/dist/commands/agent-dispatch.js +102 -0
  6. package/dist/commands/agent-support.js +62 -0
  7. package/dist/commands/config-cli.js +68 -84
  8. package/dist/commands/consolidate.js +823 -0
  9. package/dist/commands/curate.js +1 -0
  10. package/dist/commands/distill-promotion-policy.js +658 -0
  11. package/dist/commands/distill.js +250 -48
  12. package/dist/commands/eval-cases.js +40 -0
  13. package/dist/commands/events.js +12 -24
  14. package/dist/commands/graph.js +222 -0
  15. package/dist/commands/health.js +376 -0
  16. package/dist/commands/help/help-accept.md +9 -0
  17. package/dist/commands/help/help-improve.md +53 -0
  18. package/dist/commands/help/help-proposals.md +15 -0
  19. package/dist/commands/help/help-propose.md +17 -0
  20. package/dist/commands/help/help-reject.md +8 -0
  21. package/dist/commands/history.js +3 -30
  22. package/dist/commands/improve.js +1170 -0
  23. package/dist/commands/info.js +2 -2
  24. package/dist/commands/init.js +2 -2
  25. package/dist/commands/install-audit.js +5 -1
  26. package/dist/commands/installed-stashes.js +118 -138
  27. package/dist/commands/knowledge.js +133 -0
  28. package/dist/commands/lint/agent-linter.js +46 -0
  29. package/dist/commands/lint/base-linter.js +251 -0
  30. package/dist/commands/lint/command-linter.js +46 -0
  31. package/dist/commands/lint/default-linter.js +13 -0
  32. package/dist/commands/lint/index.js +107 -0
  33. package/dist/commands/lint/knowledge-linter.js +13 -0
  34. package/dist/commands/lint/memory-linter.js +58 -0
  35. package/dist/commands/lint/registry.js +33 -0
  36. package/dist/commands/lint/skill-linter.js +42 -0
  37. package/dist/commands/lint/task-linter.js +47 -0
  38. package/dist/commands/lint/types.js +1 -0
  39. package/dist/commands/lint/workflow-linter.js +53 -0
  40. package/dist/commands/lint.js +1 -0
  41. package/dist/commands/migration-help.js +2 -2
  42. package/dist/commands/proposal.js +8 -7
  43. package/dist/commands/propose.js +113 -43
  44. package/dist/commands/reflect.js +175 -41
  45. package/dist/commands/registry-search.js +2 -2
  46. package/dist/commands/remember.js +55 -1
  47. package/dist/commands/schema-repair.js +130 -0
  48. package/dist/commands/search.js +21 -5
  49. package/dist/commands/show.js +131 -52
  50. package/dist/commands/source-add.js +10 -10
  51. package/dist/commands/source-manage.js +11 -19
  52. package/dist/commands/tasks.js +385 -0
  53. package/dist/commands/url-checker.js +39 -0
  54. package/dist/commands/vault.js +7 -33
  55. package/dist/core/action-contributors.js +25 -0
  56. package/dist/core/asset-registry.js +5 -17
  57. package/dist/core/asset-spec.js +11 -1
  58. package/dist/core/common.js +94 -0
  59. package/dist/core/concurrent.js +22 -0
  60. package/dist/core/config.js +229 -122
  61. package/dist/core/events.js +87 -123
  62. package/dist/core/frontmatter.js +3 -1
  63. package/dist/core/markdown.js +17 -0
  64. package/dist/core/memory-improve.js +678 -0
  65. package/dist/core/parse.js +155 -0
  66. package/dist/core/paths.js +101 -3
  67. package/dist/core/proposal-validators.js +61 -0
  68. package/dist/core/proposals.js +49 -38
  69. package/dist/core/state-db.js +775 -0
  70. package/dist/core/time.js +51 -0
  71. package/dist/core/warn.js +59 -1
  72. package/dist/indexer/db-search.js +86 -472
  73. package/dist/indexer/db.js +392 -6
  74. package/dist/indexer/ensure-index.js +133 -0
  75. package/dist/indexer/graph-boost.js +247 -94
  76. package/dist/indexer/graph-db.js +201 -0
  77. package/dist/indexer/graph-dedup.js +99 -0
  78. package/dist/indexer/graph-extraction.js +417 -74
  79. package/dist/indexer/index-context.js +10 -0
  80. package/dist/indexer/indexer.js +466 -298
  81. package/dist/indexer/llm-cache.js +47 -0
  82. package/dist/indexer/match-contributors.js +141 -0
  83. package/dist/indexer/matchers.js +24 -190
  84. package/dist/indexer/memory-inference.js +63 -29
  85. package/dist/indexer/metadata-contributors.js +26 -0
  86. package/dist/indexer/metadata.js +188 -175
  87. package/dist/indexer/path-resolver.js +89 -0
  88. package/dist/indexer/ranking-contributors.js +204 -0
  89. package/dist/indexer/ranking.js +74 -0
  90. package/dist/indexer/search-hit-enrichers.js +22 -0
  91. package/dist/indexer/search-source.js +24 -9
  92. package/dist/indexer/semantic-status.js +2 -16
  93. package/dist/indexer/walker.js +25 -0
  94. package/dist/integrations/agent/config.js +175 -3
  95. package/dist/integrations/agent/index.js +3 -1
  96. package/dist/integrations/agent/pipeline.js +39 -0
  97. package/dist/integrations/agent/profiles.js +67 -5
  98. package/dist/integrations/agent/prompts.js +114 -29
  99. package/dist/integrations/agent/runners.js +31 -0
  100. package/dist/integrations/agent/sdk-runner.js +120 -0
  101. package/dist/integrations/agent/spawn.js +136 -28
  102. package/dist/integrations/lockfile.js +10 -18
  103. package/dist/integrations/session-logs/index.js +65 -0
  104. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  105. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  106. package/dist/integrations/session-logs/types.js +1 -0
  107. package/dist/llm/call-ai.js +74 -0
  108. package/dist/llm/client.js +63 -86
  109. package/dist/llm/feature-gate.js +27 -16
  110. package/dist/llm/graph-extract.js +297 -64
  111. package/dist/llm/memory-infer.js +52 -71
  112. package/dist/llm/metadata-enhance.js +39 -22
  113. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  114. package/dist/output/cli-hints-full.md +277 -0
  115. package/dist/output/cli-hints-short.md +65 -0
  116. package/dist/output/cli-hints.js +2 -309
  117. package/dist/output/renderers.js +196 -124
  118. package/dist/output/shapes.js +41 -3
  119. package/dist/output/text.js +257 -21
  120. package/dist/registry/providers/skills-sh.js +61 -49
  121. package/dist/registry/providers/static-index.js +44 -48
  122. package/dist/setup/setup.js +510 -11
  123. package/dist/sources/provider-factory.js +2 -1
  124. package/dist/sources/providers/git.js +44 -2
  125. package/dist/sources/website-ingest.js +4 -0
  126. package/dist/tasks/backends/cron.js +200 -0
  127. package/dist/tasks/backends/exec-utils.js +25 -0
  128. package/dist/tasks/backends/index.js +32 -0
  129. package/dist/tasks/backends/launchd-template.xml +19 -0
  130. package/dist/tasks/backends/launchd.js +184 -0
  131. package/dist/tasks/backends/schtasks-template.xml +29 -0
  132. package/dist/tasks/backends/schtasks.js +212 -0
  133. package/dist/tasks/parser.js +198 -0
  134. package/dist/tasks/resolveAkmBin.js +84 -0
  135. package/dist/tasks/runner.js +432 -0
  136. package/dist/tasks/schedule.js +208 -0
  137. package/dist/tasks/schema.js +13 -0
  138. package/dist/tasks/validator.js +59 -0
  139. package/dist/wiki/index-template.md +12 -0
  140. package/dist/wiki/ingest-workflow-template.md +54 -0
  141. package/dist/wiki/log-template.md +8 -0
  142. package/dist/wiki/schema-template.md +61 -0
  143. package/dist/wiki/wiki-templates.js +12 -0
  144. package/dist/wiki/wiki.js +10 -61
  145. package/dist/workflows/authoring.js +5 -25
  146. package/dist/workflows/db.js +9 -0
  147. package/dist/workflows/renderer.js +8 -3
  148. package/dist/workflows/runs.js +73 -88
  149. package/dist/workflows/scope-key.js +76 -0
  150. package/dist/workflows/validator.js +1 -1
  151. package/dist/workflows/workflow-template.md +24 -0
  152. package/docs/README.md +3 -0
  153. package/docs/migration/release-notes/0.7.0.md +1 -1
  154. package/docs/migration/release-notes/0.7.4.md +1 -1
  155. package/docs/migration/release-notes/0.7.5.md +20 -0
  156. package/docs/migration/release-notes/0.8.0.md +43 -0
  157. package/package.json +4 -3
  158. package/dist/templates/wiki-templates.js +0 -100
package/dist/cli.js CHANGED
@@ -1,39 +1,46 @@
1
1
  #!/usr/bin/env bun
2
+ import { spawnSync } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import * as p from "@clack/prompts";
5
6
  import { defineCommand, runMain } from "citty";
7
+ import { hasSubcommand, parsePositiveIntFlag } from "./cli/parse-args";
8
+ import { akmAgentDispatch } from "./commands/agent-dispatch";
6
9
  import { generateBashCompletions, installBashCompletions } from "./commands/completions";
7
10
  import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./commands/config-cli";
8
11
  import { akmCurate } from "./commands/curate";
9
- import { akmDistill } from "./commands/distill";
10
12
  import { akmEventsList, akmEventsTail } from "./commands/events";
13
+ import { akmGraphEntities, akmGraphExport, akmGraphRelated, akmGraphRelations, akmGraphSummary, } from "./commands/graph";
14
+ import { akmHealth } from "./commands/health";
11
15
  import { akmHistory } from "./commands/history";
16
+ import { akmImprove } from "./commands/improve";
12
17
  import { assembleInfo } from "./commands/info";
13
18
  import { akmInit } from "./commands/init";
14
19
  import { akmListSources, akmRemove, akmUpdate } from "./commands/installed-stashes";
20
+ import { readKnowledgeInput, writeMarkdownAsset } from "./commands/knowledge";
21
+ import { akmLint } from "./commands/lint";
15
22
  import { renderMigrationHelp } from "./commands/migration-help";
16
23
  import { akmProposalAccept, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalShow, } from "./commands/proposal";
17
24
  import { akmPropose } from "./commands/propose";
18
- import { akmReflect } from "./commands/reflect";
19
25
  import { searchRegistry } from "./commands/registry-search";
20
- import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich, } from "./commands/remember";
21
- import { akmSearch, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
26
+ import { buildMemoryFrontmatter, parseDuration, readMemoryContent, resolveRememberContentArg, runAutoHeuristics, runLlmEnrich, } from "./commands/remember";
27
+ import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
22
28
  import { checkForUpdate, performUpgrade } from "./commands/self-update";
23
- import { akmShowUnified } from "./commands/show";
29
+ import { akmShowUnified, normalizeShowArgv } from "./commands/show";
24
30
  import { akmAdd } from "./commands/source-add";
25
31
  import { akmClone } from "./commands/source-clone";
26
32
  import { addStash } from "./commands/source-manage";
33
+ import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksList, akmTasksRemove, akmTasksRun, akmTasksSetEnabled, akmTasksShow, akmTasksSync, parseTaskRef, } from "./commands/tasks";
27
34
  import { parseAssetRef } from "./core/asset-ref";
28
35
  import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./core/asset-spec";
29
- import { isHttpUrl, isWithin, resolveStashDir, tryReadStdinText } from "./core/common";
30
- import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./core/config";
36
+ import { isHttpUrl, resolveStashDir } from "./core/common";
37
+ import { DEFAULT_CONFIG, loadConfig, loadUserConfig, resolveConfiguredSources, saveConfig } from "./core/config";
31
38
  import { ConfigError, NotFoundError, UsageError } from "./core/errors";
32
39
  import { appendEvent } from "./core/events";
33
- import { getCacheDir, getDbPath, getDefaultStashDir } from "./core/paths";
34
- import { setQuiet, setVerbose, warn } from "./core/warn";
35
- import { resolveWriteTarget, writeAssetToSource } from "./core/write-source";
36
- import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "./indexer/db";
40
+ import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "./core/paths";
41
+ import { clearLogFile, info, setLogFile, setQuiet, setVerbose, warn } from "./core/warn";
42
+ import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, openExistingDatabase } from "./indexer/db";
43
+ import { ensureIndex } from "./indexer/ensure-index";
37
44
  import { akmIndex } from "./indexer/indexer";
38
45
  import { resolveSourceEntries } from "./indexer/search-source";
39
46
  import { insertUsageEvent } from "./indexer/usage-events";
@@ -45,16 +52,22 @@ import { buildRegistryIndex, writeRegistryIndex } from "./registry/build-index";
45
52
  import { resolveSourcesForOrigin } from "./registry/origin-resolve";
46
53
  import { saveGitStash } from "./sources/providers/git";
47
54
  import { resolveAssetPath } from "./sources/resolve";
48
- import { fetchWebsiteMarkdownSnapshot } from "./sources/website-ingest";
49
55
  import { pkgVersion } from "./version";
50
56
  import { createWorkflowAsset, formatWorkflowErrors, getWorkflowTemplate, validateWorkflowSource, } from "./workflows/authoring";
51
57
  import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflows/cli";
52
58
  import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflows/runs";
53
- const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
54
59
  const SKILLS_SH_NAME = "skills.sh";
55
60
  const SKILLS_SH_URL = "https://skills.sh";
56
61
  const SKILLS_SH_PROVIDER = "skills-sh";
57
62
  import { stringify as yamlStringify } from "yaml";
63
+ function applyEarlyStderrFlags(argv) {
64
+ if (argv.includes("--quiet") || argv.includes("-q")) {
65
+ setQuiet(true);
66
+ }
67
+ if (argv.includes("--verbose")) {
68
+ setVerbose(true);
69
+ }
70
+ }
58
71
  /**
59
72
  * Collect all occurrences of a repeatable flag from process.argv.
60
73
  * Citty's StringArgDef only exposes the last value when a flag is repeated,
@@ -78,6 +91,43 @@ function parseAllFlagValues(flag) {
78
91
  }
79
92
  return values;
80
93
  }
94
+ function resolveHelpMigrateVersionArg(version) {
95
+ if (version === undefined)
96
+ return undefined;
97
+ const parsedFormat = parseFlagValue(process.argv, "--format");
98
+ if (parsedFormat !== undefined &&
99
+ version === parsedFormat &&
100
+ wasHelpMigrateFlagValueConsumedAsVersion(version, parsedFormat, "--format")) {
101
+ return undefined;
102
+ }
103
+ const parsedDetail = parseFlagValue(process.argv, "--detail");
104
+ if (parsedDetail !== undefined &&
105
+ version === parsedDetail &&
106
+ wasHelpMigrateFlagValueConsumedAsVersion(version, parsedDetail, "--detail")) {
107
+ return undefined;
108
+ }
109
+ return version;
110
+ }
111
+ function wasHelpMigrateFlagValueConsumedAsVersion(version, flagValue, flagName) {
112
+ const argv = process.argv.slice(2);
113
+ const helpIndex = argv.indexOf("help");
114
+ const tokens = helpIndex >= 0 ? argv.slice(helpIndex + 1) : argv;
115
+ const migrateIndex = tokens.indexOf("migrate");
116
+ const relevant = migrateIndex >= 0 ? tokens.slice(migrateIndex + 1) : tokens;
117
+ let flagIndex = -1;
118
+ for (let i = 0; i < relevant.length; i += 1) {
119
+ const token = relevant[i];
120
+ if (token === flagName || token === `${flagName}=${flagValue}`) {
121
+ flagIndex = i;
122
+ break;
123
+ }
124
+ }
125
+ if (flagIndex === -1)
126
+ return false;
127
+ if (relevant.slice(0, flagIndex).includes(version))
128
+ return false;
129
+ return relevant[flagIndex] === flagName ? relevant[flagIndex + 1] === version : true;
130
+ }
81
131
  function output(command, result) {
82
132
  const mode = getOutputMode();
83
133
  const shaped = shapeForCommand(command, result, mode.detail, mode.forAgent);
@@ -109,12 +159,57 @@ function output(command, result) {
109
159
  const setupCommand = defineCommand({
110
160
  meta: {
111
161
  name: "setup",
112
- description: "Interactive configuration wizard: detects services and walks you through embeddings, LLM, registries, sources, and agent profiles. Writes config once at the end.",
162
+ description: "Interactive configuration wizard. Configures embeddings/LLM connections (for indexing/enrichment), agent profiles (CLI agent, embedded SDK, or none), sources, and registries. Shows which features are enabled at the end. Use --config <json> or --yes for non-interactive/scripting mode.",
113
163
  },
114
- async run() {
164
+ args: {
165
+ config: {
166
+ type: "string",
167
+ description: 'Config JSON to apply non-interactively, e.g. \'{"llm":{"endpoint":"...","model":"..."}}\'',
168
+ },
169
+ yes: {
170
+ type: "boolean",
171
+ default: false,
172
+ description: "Accept all defaults, skip all prompts. Idempotent — safe to run in CI.",
173
+ },
174
+ dir: {
175
+ type: "string",
176
+ description: "Stash directory path (overrides stashDir in config or --config JSON)",
177
+ },
178
+ probe: {
179
+ type: "boolean",
180
+ default: false,
181
+ description: "Probe LLM/embedding endpoints after writing config to verify connectivity",
182
+ },
183
+ },
184
+ async run({ args }) {
115
185
  await runWithJsonErrors(async () => {
116
- const { runSetupWizard } = await import("./setup/setup");
117
- await runSetupWizard();
186
+ const noInit = getHyphenatedBoolean(args, "no-init");
187
+ if (args.config) {
188
+ // Non-interactive config mode
189
+ const { runSetupFromConfig } = await import("./setup/setup");
190
+ const result = await runSetupFromConfig({
191
+ configJson: args.config,
192
+ dir: args.dir,
193
+ noInit,
194
+ probe: args.probe,
195
+ });
196
+ output("setup", result);
197
+ }
198
+ else if (args.yes) {
199
+ // Defaults mode — no prompts
200
+ const { runSetupWithDefaults } = await import("./setup/setup");
201
+ const result = await runSetupWithDefaults({
202
+ dir: args.dir,
203
+ noInit,
204
+ probe: args.probe,
205
+ });
206
+ output("setup", result);
207
+ }
208
+ else {
209
+ // Interactive wizard
210
+ const { runSetupWizard } = await import("./setup/setup");
211
+ await runSetupWizard({ dir: args.dir, noInit });
212
+ }
118
213
  });
119
214
  },
120
215
  });
@@ -140,16 +235,23 @@ const indexCommand = defineCommand({
140
235
  meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
141
236
  args: {
142
237
  full: { type: "boolean", description: "Force full reindex", default: false },
143
- enrich: { type: "boolean", description: "Enable LLM inference and enrichment passes", default: false },
144
238
  verbose: { type: "boolean", description: "Print phase-by-phase indexing progress to stderr", default: false },
145
239
  },
146
240
  async run({ args }) {
147
241
  await runWithJsonErrors(async () => {
242
+ if (getHyphenatedBoolean(args, "enrich") || parseFlagValue(process.argv, "--enrich") !== undefined) {
243
+ throw new UsageError("`akm index --enrich` has been removed. Plain `akm index` now performs metadata enrichment by default.");
244
+ }
245
+ if (getHyphenatedBoolean(args, "re-enrich") || parseFlagValue(process.argv, "--re-enrich") !== undefined) {
246
+ throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
247
+ }
148
248
  const outputMode = getOutputMode();
149
249
  const controller = new AbortController();
150
250
  const abort = () => controller.abort(new Error("index interrupted"));
151
251
  process.once("SIGINT", abort);
152
252
  process.once("SIGTERM", abort);
253
+ const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
254
+ setLogFile(indexLogFile);
153
255
  const spin = !args.verbose && outputMode.format === "text" ? p.spinner() : null;
154
256
  if (spin) {
155
257
  spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
@@ -158,12 +260,11 @@ const indexCommand = defineCommand({
158
260
  try {
159
261
  const result = await akmIndex({
160
262
  full: args.full,
161
- enrich: args.enrich,
162
- onProgress: ({ message, processed, total }) => {
263
+ onProgress: ({ phase, message, processed, total }) => {
163
264
  latestMessage = message;
164
265
  const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
165
266
  if (args.verbose) {
166
- console.error(`[index] ${progressPrefix}${message}`);
267
+ info(`[index:${phase}] ${progressPrefix}${message}`);
167
268
  }
168
269
  else if (spin) {
169
270
  spin.stop(`${progressPrefix}${message}`);
@@ -184,6 +285,7 @@ const indexCommand = defineCommand({
184
285
  throw error;
185
286
  }
186
287
  finally {
288
+ clearLogFile();
187
289
  process.off("SIGINT", abort);
188
290
  process.off("SIGTERM", abort);
189
291
  }
@@ -199,6 +301,102 @@ const infoCommand = defineCommand({
199
301
  });
200
302
  },
201
303
  });
304
+ const healthCommand = defineCommand({
305
+ meta: { name: "health", description: "Check akm runtime health, artifacts, and improve metrics" },
306
+ args: {
307
+ since: {
308
+ type: "string",
309
+ description: "Rolling window start (ISO timestamp, date, epoch ms, or shorthand like 24h / 7d)",
310
+ },
311
+ },
312
+ run({ args }) {
313
+ return runWithJsonErrors(() => {
314
+ const result = akmHealth({ since: args.since });
315
+ output("health", result);
316
+ });
317
+ },
318
+ });
319
+ const graphCommand = defineCommand({
320
+ meta: { name: "graph", description: "Inspect the indexed entity graph stored in SQLite" },
321
+ subCommands: {
322
+ summary: defineCommand({
323
+ meta: { name: "summary", description: "Show entity-graph counts and quality telemetry" },
324
+ args: {
325
+ source: { type: "string", description: "Source name/path (default: primary stash source)" },
326
+ },
327
+ run({ args }) {
328
+ return runWithJsonErrors(() => {
329
+ output("graph-summary", akmGraphSummary({ source: args.source }));
330
+ });
331
+ },
332
+ }),
333
+ entities: defineCommand({
334
+ meta: { name: "entities", description: "List entities with per-file occurrence counts" },
335
+ args: {
336
+ source: { type: "string", description: "Source name/path (default: primary stash source)" },
337
+ limit: { type: "string", description: "Maximum entities to return" },
338
+ },
339
+ run({ args }) {
340
+ return runWithJsonErrors(() => {
341
+ output("graph-entities", akmGraphEntities({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
342
+ });
343
+ },
344
+ }),
345
+ relations: defineCommand({
346
+ meta: { name: "relations", description: "List relations with occurrence counts" },
347
+ args: {
348
+ source: { type: "string", description: "Source name/path (default: primary stash source)" },
349
+ limit: { type: "string", description: "Maximum relations to return" },
350
+ },
351
+ run({ args }) {
352
+ return runWithJsonErrors(() => {
353
+ output("graph-relations", akmGraphRelations({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
354
+ });
355
+ },
356
+ }),
357
+ related: defineCommand({
358
+ meta: { name: "related", description: "Show graph-related neighboring assets for a ref" },
359
+ args: {
360
+ ref: { type: "positional", description: "Asset ref", required: true },
361
+ source: { type: "string", description: "Source name/path (default: primary stash source)" },
362
+ limit: { type: "string", description: "Maximum related assets to return" },
363
+ },
364
+ async run({ args }) {
365
+ return runWithJsonErrors(async () => {
366
+ output("graph-related", await akmGraphRelated({
367
+ ref: args.ref ?? "",
368
+ source: args.source,
369
+ limit: parsePositiveIntFlag(args.limit ?? undefined),
370
+ }));
371
+ });
372
+ },
373
+ }),
374
+ export: defineCommand({
375
+ meta: { name: "export", description: "Export graph artifact as JSON or JSONL" },
376
+ args: {
377
+ source: { type: "string", description: "Source name/path (default: primary stash source)" },
378
+ out: { type: "string", description: "Output path" },
379
+ format: { type: "string", description: "Export format (json|jsonl)", default: "json" },
380
+ },
381
+ run({ args }) {
382
+ return runWithJsonErrors(() => {
383
+ output("graph-export", akmGraphExport({
384
+ source: args.source,
385
+ out: args.out ?? "",
386
+ format: args.format,
387
+ }));
388
+ });
389
+ },
390
+ }),
391
+ },
392
+ run({ args }) {
393
+ return runWithJsonErrors(() => {
394
+ if (hasSubcommand(args, GRAPH_SUBCOMMAND_SET))
395
+ return;
396
+ output("graph-summary", akmGraphSummary());
397
+ });
398
+ },
399
+ });
202
400
  const searchCommand = defineCommand({
203
401
  meta: { name: "search", description: "Search the stash" },
204
402
  args: {
@@ -218,29 +416,30 @@ const searchCommand = defineCommand({
218
416
  description: 'Include entries with quality:"proposed" in the result set. Excluded by default (v1 spec §4.2).',
219
417
  default: false,
220
418
  },
419
+ belief: {
420
+ type: "string",
421
+ description: "Memory belief filter: all|current|historical. current keeps active memory beliefs; historical keeps contradicted/superseded/archived memory beliefs.",
422
+ default: "all",
423
+ },
221
424
  format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
222
425
  detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
223
426
  },
224
427
  async run({ args }) {
225
428
  await runWithJsonErrors(async () => {
226
- // An empty query enumerates all indexed assets (list mode).
227
- // The guard that rejected empty queries was removed; akmSearch handles
228
- // empty strings end-to-end via getAllEntries (DB path) and the
229
- // substring-search fallback's query-less branch.
230
429
  const query = (args.query ?? "").trim();
231
- const type = args.type;
232
- const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
233
- if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
234
- throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
430
+ if (!query) {
431
+ throw new UsageError('A search query is required. Usage: akm search "<query>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT", 'Pass a query like `akm search "docker"` or `akm search "code review" --type skill`.');
235
432
  }
236
- const limit = limitRaw;
433
+ const type = args.type;
434
+ const limit = parsePositiveIntFlag(args.limit ?? undefined);
237
435
  const source = parseSearchSource(args.source);
238
436
  // Repeatable; citty exposes only the last `--filter` value, so read all
239
437
  // occurrences directly from argv (same pattern as `--tag`).
240
438
  const filterTokens = parseAllFlagValues("--filter");
241
439
  const filters = parseScopeFilterFlags(filterTokens, "--filter");
242
440
  const includeProposed = args["include-proposed"] === true;
243
- const result = await akmSearch({ query, type, limit, source, filters, includeProposed });
441
+ const belief = parseBeliefFilterMode(typeof args.belief === "string" ? args.belief : undefined);
442
+ const result = await akmSearch({ query, type, limit, source, filters, includeProposed, belief });
244
443
  output("search", result);
245
444
  });
246
445
  },
@@ -265,11 +464,8 @@ const curateCommand = defineCommand({
265
464
  throw new UsageError('A curate query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT", 'Describe the task you want assets for, e.g. `akm curate "deploy to prod"`.');
266
465
  }
267
466
  const type = args.type;
268
- const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
269
- if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
270
- throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
271
- }
272
- const limit = limitRaw && limitRaw > 0 ? limitRaw : 4;
467
+ const limitParsed = parsePositiveIntFlag(args.limit ?? undefined);
468
+ const limit = limitParsed && limitParsed > 0 ? limitParsed : 4;
273
469
  const source = parseSearchSource(args.source ?? "stash");
274
470
  const curated = await akmCurate({ query: args.query, type, limit, source });
275
471
  output("curate", curated);
@@ -543,15 +739,17 @@ const showCommand = defineCommand({
543
739
  },
544
740
  async run({ args }) {
545
741
  await runWithJsonErrors(async () => {
546
- try {
547
- parseAssetRef(args.ref);
548
- }
549
- catch (error) {
550
- if (error instanceof UsageError && error.code === "MISSING_REQUIRED_ARGUMENT") {
551
- throw new UsageError(error.message, "INVALID_FLAG_VALUE", error.hint());
742
+ const subcommand = Array.isArray(args._) ? args._[0] : undefined;
743
+ if (subcommand === "proposal") {
744
+ const proposalId = Array.isArray(args._) ? args._[1] : undefined;
745
+ if (typeof proposalId !== "string" || !proposalId.trim()) {
746
+ throw new UsageError("Usage: akm show proposal <id>", "MISSING_REQUIRED_ARGUMENT");
552
747
  }
553
- throw error;
748
+ const result = akmProposalShow({ id: proposalId.trim() });
749
+ output("proposal-show", result);
750
+ return;
554
751
  }
752
+ parseAssetRef(args.ref);
555
753
  // The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
556
754
  // is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
557
755
  // by `normalizeShowArgv` before citty parses argv. We read those values
@@ -640,6 +838,14 @@ const configCommand = defineCommand({
640
838
  });
641
839
  },
642
840
  }),
841
+ show: defineCommand({
842
+ meta: { name: "show", description: "Alias for `akm config list` — list current configuration" },
843
+ run() {
844
+ return runWithJsonErrors(() => {
845
+ output("config", listConfig(loadConfig()));
846
+ });
847
+ },
848
+ }),
643
849
  get: defineCommand({
644
850
  meta: { name: "get", description: "Get a configuration value by key" },
645
851
  args: {
@@ -681,7 +887,7 @@ const configCommand = defineCommand({
681
887
  },
682
888
  run({ args }) {
683
889
  return runWithJsonErrors(() => {
684
- if (hasConfigSubcommand(args))
890
+ if (hasSubcommand(args, CONFIG_SUBCOMMAND_SET))
685
891
  return;
686
892
  if (args.list) {
687
893
  output("config", listConfig(loadConfig()));
@@ -725,7 +931,7 @@ const saveCommand = defineCommand({
725
931
  ? undefined
726
932
  : args.name;
727
933
  let writable;
728
- if (!effectiveName) {
934
+ if (effectiveName === undefined) {
729
935
  // Primary stash — honour the root-level writable flag from config.
730
936
  const cfg = loadConfig();
731
937
  writable = cfg.writable === true ? true : undefined;
@@ -901,10 +1107,7 @@ const registryCommand = defineCommand({
901
1107
  },
902
1108
  async run({ args }) {
903
1109
  await runWithJsonErrors(async () => {
904
- const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
905
- if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
906
- throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
907
- }
1110
+ const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
908
1111
  const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
909
1112
  output("registry-search", result);
910
1113
  });
@@ -939,13 +1142,37 @@ const registryCommand = defineCommand({
939
1142
  }),
940
1143
  },
941
1144
  });
1145
+ const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
1146
+ const MAX_FEEDBACK_TAGS = 10;
1147
+ function validateFeedbackTags(raw) {
1148
+ const seen = new Set();
1149
+ const out = [];
1150
+ for (const tag of raw) {
1151
+ const parts = tag.split(":");
1152
+ if (parts.length < 2 || parts[0] === "" || parts.slice(1).join("") === "") {
1153
+ throw new UsageError(`Invalid tag "${tag}". Tags must be in key:value format where key matches [a-z_][a-z0-9_]* and value is non-empty.`, "INVALID_FLAG_VALUE");
1154
+ }
1155
+ const key = parts[0];
1156
+ if (!TAG_KEY_RE.test(key)) {
1157
+ throw new UsageError(`Invalid tag key "${key}" in "${tag}". Key must match [a-z_][a-z0-9_]*.`, "INVALID_FLAG_VALUE");
1158
+ }
1159
+ if (seen.has(tag))
1160
+ continue;
1161
+ seen.add(tag);
1162
+ out.push(tag);
1163
+ }
1164
+ if (out.length > MAX_FEEDBACK_TAGS) {
1165
+ throw new UsageError(`Too many tags: ${out.length}. Maximum is ${MAX_FEEDBACK_TAGS}.`, "INVALID_FLAG_VALUE");
1166
+ }
1167
+ return out;
1168
+ }
942
1169
  const feedbackCommand = defineCommand({
943
1170
  meta: {
944
1171
  name: "feedback",
945
1172
  description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
946
1173
  "Positive feedback boosts an asset's EMA utility score, making it rank higher\n" +
947
1174
  "in future searches without requiring a full reindex.\n\n" +
948
- "Negative feedback records a negative signal in usage_events and events.jsonl.\n" +
1175
+ "Negative feedback records a negative signal in usage_events and state.db events.\n" +
949
1176
  "It does NOT immediately lower the asset's ranking — the EMA utility score is\n" +
950
1177
  "updated the next time `akm index` runs (incremental or full). Run `akm index`\n" +
951
1178
  "after recording negative feedback to have it reflected in search results.",
@@ -962,10 +1189,18 @@ const feedbackCommand = defineCommand({
962
1189
  "Reindexing is required for the signal to affect search results.",
963
1190
  default: false,
964
1191
  },
965
- note: { type: "string", description: "Optional note to attach to the feedback" },
1192
+ reason: {
1193
+ type: "string",
1194
+ description: "Reason for the feedback (recommended for negative feedback, used by distillation)",
1195
+ },
1196
+ note: { type: "string", description: "Alias for --reason (backward-compatible, prefer --reason)" },
1197
+ tag: {
1198
+ type: "string",
1199
+ description: "Tag to attach to the feedback (repeatable, e.g. --tag slice:train --tag team:platform)",
1200
+ },
966
1201
  },
967
1202
  run({ args }) {
968
- return runWithJsonErrors(() => {
1203
+ return runWithJsonErrors(async () => {
969
1204
  const ref = (args.ref ?? "").trim();
970
1205
  if (!ref) {
971
1206
  throw new UsageError("Asset ref is required. Usage: akm feedback <ref> --positive|--negative", "MISSING_REQUIRED_ARGUMENT", "Pass a ref like `skill:deploy` and either --positive or --negative.");
@@ -978,12 +1213,35 @@ const feedbackCommand = defineCommand({
978
1213
  throw new UsageError("Specify --positive or --negative.");
979
1214
  }
980
1215
  const signal = args.positive ? "positive" : "negative";
981
- const metadata = args.note ? JSON.stringify({ note: args.note }) : undefined;
1216
+ const reason = args.reason ?? args.note;
1217
+ if (args.negative === true && !reason?.trim()) {
1218
+ const cfg = loadConfig();
1219
+ if (cfg.feedback?.requireReason === true) {
1220
+ throw new UsageError("Negative feedback requires --reason (feedback.requireReason is enabled).", "MISSING_REQUIRED_ARGUMENT");
1221
+ }
1222
+ else {
1223
+ warn("Warning: negative feedback without --reason provides less distillation signal.");
1224
+ }
1225
+ }
1226
+ const rawTags = parseAllFlagValues("--tag");
1227
+ const validatedTags = validateFeedbackTags(rawTags);
1228
+ const metadataObj = {
1229
+ signal,
1230
+ ...(reason?.trim() ? { reason: reason.trim() } : {}),
1231
+ ...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
1232
+ };
1233
+ const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
1234
+ // Auto-index when stale so the index is current before recording feedback.
1235
+ const sources = resolveSourceEntries();
1236
+ if (sources.length > 0) {
1237
+ await ensureIndex(sources[0].path);
1238
+ }
982
1239
  const db = openExistingDatabase();
983
1240
  try {
984
1241
  const entryId = findEntryIdByRef(db, ref);
985
1242
  if (entryId === undefined) {
986
- throw new UsageError(`Ref "${ref}" is not in the current index. Run "akm index" and try again.`);
1243
+ throw new UsageError(`Ref "${ref}" is not in the index. ` +
1244
+ "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
987
1245
  }
988
1246
  // Persist the feedback signal into usage_events. For positive signals,
989
1247
  // the EMA utility score is updated immediately on the next read path.
@@ -995,8 +1253,27 @@ const feedbackCommand = defineCommand({
995
1253
  entry_ref: ref,
996
1254
  entry_id: entryId,
997
1255
  signal,
998
- metadata,
1256
+ metadata: metadataStr,
999
1257
  });
1258
+ // Apply feedback-derived utility score adjustment immediately so that
1259
+ // positive/negative signals influence search ranking without requiring
1260
+ // a full reindex. We query the total accumulated feedback counts from
1261
+ // usage_events so the delta reflects the entire signal history.
1262
+ try {
1263
+ const counts = db
1264
+ .prepare(`SELECT
1265
+ SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
1266
+ SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg
1267
+ FROM usage_events
1268
+ WHERE event_type = 'feedback' AND entry_id = ?`)
1269
+ .get(entryId);
1270
+ const pos = counts?.pos ?? 0;
1271
+ const neg = counts?.neg ?? 0;
1272
+ applyFeedbackToUtilityScore(db, entryId, pos, neg);
1273
+ }
1274
+ catch {
1275
+ // best-effort — feedback recording succeeds even if utility update fails
1276
+ }
1000
1277
  }
1001
1278
  finally {
1002
1279
  closeDatabase(db);
@@ -1004,9 +1281,9 @@ const feedbackCommand = defineCommand({
1004
1281
  appendEvent({
1005
1282
  eventType: "feedback",
1006
1283
  ref,
1007
- metadata: { signal, ...(args.note ? { note: args.note } : {}) },
1284
+ metadata: metadataObj,
1008
1285
  });
1009
- output("feedback", { ok: true, ref, signal, note: args.note ?? null });
1286
+ output("feedback", { ok: true, ref, signal, reason: reason?.trim() ?? null, tags: validatedTags });
1010
1287
  });
1011
1288
  },
1012
1289
  });
@@ -1016,8 +1293,8 @@ const historyCommand = defineCommand({
1016
1293
  description: "Show mutation/usage history for a single asset (--ref) or stash-wide.\n\n" +
1017
1294
  "Event sources:\n" +
1018
1295
  " usage_events (default): search, show, and feedback events from the local index.\n" +
1019
- " events.jsonl (--include-proposals): proposal lifecycle events (promoted, rejected)\n" +
1020
- " emitted by `akm proposal accept` / `akm proposal reject`.\n\n" +
1296
+ " state.db events (--include-proposals): proposal lifecycle events (promoted, rejected)\n" +
1297
+ " emitted by `akm accept` / `akm reject`.\n\n" +
1021
1298
  "Results from all active sources are merged and sorted chronologically.",
1022
1299
  },
1023
1300
  args: {
@@ -1025,7 +1302,7 @@ const historyCommand = defineCommand({
1025
1302
  since: { type: "string", description: "ISO timestamp or epoch ms — only events on/after this time" },
1026
1303
  "include-proposals": {
1027
1304
  type: "boolean",
1028
- description: "Also include proposal lifecycle events (promoted, rejected) from events.jsonl. " +
1305
+ description: "Also include proposal lifecycle events (promoted, rejected) from state.db events. " +
1029
1306
  "Default: false (usage_events only).",
1030
1307
  default: false,
1031
1308
  },
@@ -1042,97 +1319,10 @@ const historyCommand = defineCommand({
1042
1319
  });
1043
1320
  },
1044
1321
  });
1045
- function normalizeMarkdownAssetName(name, fallback) {
1046
- const trimmed = (name ?? fallback)
1047
- .trim()
1048
- .replace(/\\/g, "/")
1049
- .replace(/^\/+|\/+$/g, "")
1050
- .replace(/\.md$/i, "");
1051
- if (!trimmed)
1052
- throw new UsageError("Asset name cannot be empty.");
1053
- const segments = trimmed.split("/");
1054
- if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
1055
- throw new UsageError("Asset name must be a relative path without '.' or '..' segments.");
1056
- }
1057
- return trimmed;
1058
- }
1059
- function slugifyAssetName(value, fallbackPrefix) {
1060
- const slug = value
1061
- .toLowerCase()
1062
- .replace(/^[#>\-\s]+/, "")
1063
- .replace(/[^a-z0-9]+/g, "-")
1064
- .replace(/^-+|-+$/g, "")
1065
- .slice(0, MAX_CAPTURED_ASSET_SLUG_LENGTH);
1066
- return slug || `${fallbackPrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
1067
- }
1068
- function inferAssetName(content, fallbackPrefix, preferred) {
1069
- const firstNonEmptyLine = content
1070
- .split(/\r?\n/)
1071
- .map((line) => line.trim())
1072
- .find((line) => line.length > 0);
1073
- const basis = preferred?.trim() || firstNonEmptyLine || fallbackPrefix;
1074
- return slugifyAssetName(basis, fallbackPrefix);
1075
- }
1076
- function readKnowledgeContent(source) {
1077
- if (source === "-") {
1078
- const content = tryReadStdinText();
1079
- if (!content?.trim()) {
1080
- throw new UsageError("No stdin content received. Pipe a document into stdin or pass a file path.");
1081
- }
1082
- return { content };
1083
- }
1084
- const resolvedSource = path.resolve(source);
1085
- let stat;
1086
- try {
1087
- stat = fs.statSync(resolvedSource);
1088
- }
1089
- catch {
1090
- throw new UsageError(`Knowledge source not found: "${source}". Pass a readable file path or "-" for stdin.`);
1091
- }
1092
- if (!stat.isFile()) {
1093
- throw new UsageError(`Knowledge source must be a file: "${source}".`);
1094
- }
1095
- return {
1096
- content: fs.readFileSync(resolvedSource, "utf8"),
1097
- preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
1098
- };
1099
- }
1100
- async function readKnowledgeInput(source) {
1101
- if (!isHttpUrl(source))
1102
- return readKnowledgeContent(source);
1103
- const snapshot = await fetchWebsiteMarkdownSnapshot(source);
1104
- return { content: snapshot.content, preferredName: snapshot.preferredName };
1105
- }
1106
- async function writeMarkdownAsset(options) {
1107
- // Resolve write target via the v1 precedence chain (`--target` →
1108
- // `defaultWriteTarget` → working stash). Per spec §10 step 5, this is the
1109
- // single dispatch point — `core/write-source.ts` owns all kind-branching.
1110
- const cfg = loadConfig();
1111
- const { source, config } = resolveWriteTarget(cfg, options.target);
1112
- const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
1113
- const normalizedName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
1114
- // Pre-flight: existence + force semantics. The helper itself overwrites
1115
- // unconditionally; the CLI surfaces a friendlier UsageError before any
1116
- // disk activity when --force is absent.
1117
- const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
1118
- if (!isWithin(assetPath, typeRoot)) {
1119
- throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
1120
- }
1121
- if (fs.existsSync(assetPath) && !options.force) {
1122
- throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
1123
- }
1124
- // Delegate the actual write (and optional git commit/push) to the helper.
1125
- const result = await writeAssetToSource(source, config, { type: options.type, name: normalizedName }, options.content);
1126
- return {
1127
- ref: result.ref,
1128
- path: result.path,
1129
- stashDir: source.path,
1130
- };
1131
- }
1132
1322
  const workflowStartCommand = defineCommand({
1133
1323
  meta: {
1134
1324
  name: "start",
1135
- description: "Start a new workflow run",
1325
+ description: "Start a new workflow run in the current working scope",
1136
1326
  },
1137
1327
  args: {
1138
1328
  ref: { type: "positional", description: "Workflow ref (workflow:<name>)", required: true },
@@ -1148,22 +1338,25 @@ const workflowStartCommand = defineCommand({
1148
1338
  const workflowNextCommand = defineCommand({
1149
1339
  meta: {
1150
1340
  name: "next",
1151
- description: "Show the next actionable workflow step, auto-starting a run when passed a workflow ref",
1341
+ description: "Show the next actionable workflow step in the current scope, auto-starting a run when passed a workflow ref",
1152
1342
  },
1153
1343
  args: {
1154
1344
  target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
1155
1345
  params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
1346
+ "dry-run": { type: "boolean", description: "Not supported — rejected with an error", default: false },
1156
1347
  },
1157
1348
  async run({ args }) {
1158
1349
  await runWithJsonErrors(async () => {
1350
+ if (getHyphenatedBoolean(args, "dry-run")) {
1351
+ throw new UsageError("`akm workflow next` does not support --dry-run. Remove the flag to start or resume a run.", "INVALID_FLAG_VALUE");
1352
+ }
1159
1353
  const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
1160
1354
  // If the target looks like a UUID-style run id (no `:` and matches the
1161
1355
  // run-id shape), short-circuit with a structured WORKFLOW_NOT_FOUND
1162
1356
  // error before parseAssetRef gets to throw an unhelpful ref-parse error.
1163
1357
  if (looksLikeWorkflowRunId(args.target)) {
1164
- const { listWorkflowRuns: listRuns } = await import("./workflows/runs.js");
1165
- const { runs: existingRuns } = listRuns({});
1166
- if (!existingRuns.some((r) => r.id === args.target)) {
1358
+ const { hasWorkflowRun } = await import("./workflows/runs.js");
1359
+ if (!(await hasWorkflowRun(args.target))) {
1167
1360
  throw new NotFoundError(`Workflow run "${args.target}" not found.`, "WORKFLOW_NOT_FOUND", "Run `akm workflow list --active` to see runs.");
1168
1361
  }
1169
1362
  }
@@ -1208,7 +1401,7 @@ const workflowCompleteCommand = defineCommand({
1208
1401
  },
1209
1402
  async run({ args }) {
1210
1403
  await runWithJsonErrors(async () => {
1211
- const result = completeWorkflowStep({
1404
+ const result = await completeWorkflowStep({
1212
1405
  runId: args.runId,
1213
1406
  stepId: args.step,
1214
1407
  status: parseWorkflowStepState(args.state),
@@ -1222,13 +1415,13 @@ const workflowCompleteCommand = defineCommand({
1222
1415
  const workflowStatusCommand = defineCommand({
1223
1416
  meta: {
1224
1417
  name: "status",
1225
- description: "Show full workflow run state for review or resume",
1418
+ description: "Show full workflow run state for review or resume; workflow refs resolve within the current scope",
1226
1419
  },
1227
1420
  args: {
1228
1421
  target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
1229
1422
  },
1230
1423
  run({ args }) {
1231
- return runWithJsonErrors(() => {
1424
+ return runWithJsonErrors(async () => {
1232
1425
  const target = args.target;
1233
1426
  // Check if target looks like a workflow ref
1234
1427
  const parsed = (() => {
@@ -1241,18 +1434,18 @@ const workflowStatusCommand = defineCommand({
1241
1434
  })();
1242
1435
  if (parsed?.type === "workflow") {
1243
1436
  const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
1244
- const { runs } = listWorkflowRuns({ workflowRef: ref });
1437
+ const { runs } = await listWorkflowRuns({ workflowRef: ref });
1245
1438
  if (runs.length === 0) {
1246
1439
  throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
1247
1440
  }
1248
1441
  const mostRecent = runs[0];
1249
1442
  if (!mostRecent)
1250
1443
  throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
1251
- const result = getWorkflowStatus(mostRecent.id);
1444
+ const result = await getWorkflowStatus(mostRecent.id);
1252
1445
  output("workflow-status", result);
1253
1446
  }
1254
1447
  else {
1255
- const result = getWorkflowStatus(target);
1448
+ const result = await getWorkflowStatus(target);
1256
1449
  output("workflow-status", result);
1257
1450
  }
1258
1451
  });
@@ -1261,15 +1454,15 @@ const workflowStatusCommand = defineCommand({
1261
1454
  const workflowListCommand = defineCommand({
1262
1455
  meta: {
1263
1456
  name: "list",
1264
- description: "List workflow runs",
1457
+ description: "List workflow runs in the current working scope",
1265
1458
  },
1266
1459
  args: {
1267
1460
  ref: { type: "string", description: "Filter to one workflow ref" },
1268
1461
  active: { type: "boolean", description: "Only show active runs", default: false },
1269
1462
  },
1270
1463
  run({ args }) {
1271
- return runWithJsonErrors(() => {
1272
- const result = listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
1464
+ return runWithJsonErrors(async () => {
1465
+ const result = await listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
1273
1466
  output("workflow-list", result);
1274
1467
  });
1275
1468
  },
@@ -1382,8 +1575,8 @@ const workflowResumeCommand = defineCommand({
1382
1575
  runId: { type: "positional", description: "Workflow run id", required: true },
1383
1576
  },
1384
1577
  run({ args }) {
1385
- return runWithJsonErrors(() => {
1386
- const result = resumeWorkflowRun(args.runId);
1578
+ return runWithJsonErrors(async () => {
1579
+ const result = await resumeWorkflowRun(args.runId);
1387
1580
  output("workflow-resume", result);
1388
1581
  });
1389
1582
  },
@@ -1405,10 +1598,10 @@ const workflowCommand = defineCommand({
1405
1598
  validate: workflowValidateCommand,
1406
1599
  },
1407
1600
  run({ args }) {
1408
- return runWithJsonErrors(() => {
1601
+ return runWithJsonErrors(async () => {
1409
1602
  if (hasWorkflowSubcommand(args))
1410
1603
  return;
1411
- output("workflow-list", listWorkflowRuns({ activeOnly: true }));
1604
+ output("workflow-list", await listWorkflowRuns({ activeOnly: true }));
1412
1605
  });
1413
1606
  },
1414
1607
  });
@@ -1478,6 +1671,10 @@ const rememberCommand = defineCommand({
1478
1671
  type: "string",
1479
1672
  description: "Scope this memory to a channel name (persisted as `scope_channel` frontmatter)",
1480
1673
  },
1674
+ showSimilar: {
1675
+ type: "boolean",
1676
+ description: "Return top-3 similar existing memories in output (opt-in)",
1677
+ },
1481
1678
  },
1482
1679
  async run({ args }) {
1483
1680
  return runWithJsonErrors(async () => {
@@ -1499,7 +1696,7 @@ const rememberCommand = defineCommand({
1499
1696
  if (typeof args.channel === "string" && args.channel.trim())
1500
1697
  scopeFields.channel = args.channel.trim();
1501
1698
  const hasScope = Object.keys(scopeFields).length > 0;
1502
- const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description || args.enrich;
1699
+ const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description;
1503
1700
  const hasStructuredArgs = hasTagRequiringArgs || hasScope || args.auto;
1504
1701
  if (!hasStructuredArgs) {
1505
1702
  const result = await writeMarkdownAsset({
@@ -1515,7 +1712,13 @@ const rememberCommand = defineCommand({
1515
1712
  ref: result.ref,
1516
1713
  metadata: { path: result.path, force: args.force === true },
1517
1714
  });
1518
- output("remember", { ok: true, ...result });
1715
+ if (args.showSimilar) {
1716
+ const similar = await fetchSimilarMemories(body.slice(0, 500), result.ref);
1717
+ output("remember", { ok: true, ...result, similar });
1718
+ }
1719
+ else {
1720
+ output("remember", { ok: true, ...result });
1721
+ }
1519
1722
  return;
1520
1723
  }
1521
1724
  // ── Accumulate metadata from all three modes ──────────────────────────
@@ -1604,53 +1807,31 @@ const rememberCommand = defineCommand({
1604
1807
  ...(hasScope ? { scope: scopeFields } : {}),
1605
1808
  },
1606
1809
  });
1607
- output("remember", { ok: true, ...result });
1810
+ if (args.showSimilar) {
1811
+ const similar = await fetchSimilarMemories((body ?? args.content ?? "").slice(0, 500), result.ref);
1812
+ output("remember", { ok: true, ...result, similar });
1813
+ }
1814
+ else {
1815
+ output("remember", { ok: true, ...result });
1816
+ }
1608
1817
  });
1609
1818
  },
1610
1819
  });
1611
- function resolveRememberContentArg(content) {
1612
- if (content === undefined)
1613
- return undefined;
1614
- const parsedFormat = parseFlagValue(process.argv, "--format");
1615
- if (parsedFormat !== undefined &&
1616
- content === parsedFormat &&
1617
- wasRememberFlagValueConsumedAsContent(content, parsedFormat, "--format")) {
1618
- return undefined;
1619
- }
1620
- const parsedDetail = parseFlagValue(process.argv, "--detail");
1621
- if (parsedDetail !== undefined &&
1622
- content === parsedDetail &&
1623
- wasRememberFlagValueConsumedAsContent(content, parsedDetail, "--detail")) {
1624
- return undefined;
1820
+ /**
1821
+ * Best-effort top-3 similar memory search for `--show-similar`.
1822
+ * Scoped to memory: type; excludes the just-written ref.
1823
+ */
1824
+ async function fetchSimilarMemories(query, excludeRef) {
1825
+ try {
1826
+ const result = await akmSearch({ query, type: "memory", limit: 4 });
1827
+ return (result.hits ?? [])
1828
+ .filter((h) => "ref" in h && h.ref !== excludeRef)
1829
+ .slice(0, 3)
1830
+ .map((h) => ({ ref: h.ref, ...(h.name ? { title: h.name } : {}) }));
1625
1831
  }
1626
- return content;
1627
- }
1628
- function wasRememberFlagValueConsumedAsContent(content, flagValue, flagName) {
1629
- const argv = process.argv.slice(2);
1630
- const rememberIndex = argv.indexOf("remember");
1631
- const tokens = rememberIndex >= 0 ? argv.slice(rememberIndex + 1) : argv;
1632
- let flagIndex = -1;
1633
- let flagConsumesNextToken = false;
1634
- for (let i = 0; i < tokens.length; i += 1) {
1635
- const token = tokens[i];
1636
- if (token === flagName) {
1637
- flagIndex = i;
1638
- flagConsumesNextToken = true;
1639
- break;
1640
- }
1641
- if (token === `${flagName}=${flagValue}`) {
1642
- flagIndex = i;
1643
- break;
1644
- }
1832
+ catch {
1833
+ return [];
1645
1834
  }
1646
- if (flagIndex === -1)
1647
- return false;
1648
- if (tokens.slice(0, flagIndex).includes(content))
1649
- return false;
1650
- const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? 2 : 1);
1651
- if (tokens.slice(firstTokenAfterFlag).includes(content))
1652
- return false;
1653
- return true;
1654
1835
  }
1655
1836
  const importKnowledgeCommand = defineCommand({
1656
1837
  meta: {
@@ -1740,10 +1921,11 @@ const helpCommand = defineCommand({
1740
1921
  },
1741
1922
  run({ args }) {
1742
1923
  return runWithJsonErrors(() => {
1743
- if (!args.version || !String(args.version).trim()) {
1924
+ const version = resolveHelpMigrateVersionArg(typeof args.version === "string" ? args.version : undefined);
1925
+ if (!version?.trim()) {
1744
1926
  throw new UsageError("Usage: akm help migrate <version>.", "MISSING_REQUIRED_ARGUMENT", "Pass a version like `0.6.0`, `v0.6.0`, `0.6.0-rc1`, or `latest`.");
1745
1927
  }
1746
- process.stdout.write(renderMigrationHelp(args.version));
1928
+ process.stdout.write(renderMigrationHelp(version));
1747
1929
  });
1748
1930
  },
1749
1931
  }),
@@ -1773,8 +1955,8 @@ const completionsCommand = defineCommand({
1773
1955
  const script = generateBashCompletions(main);
1774
1956
  if (args.install) {
1775
1957
  const dest = installBashCompletions(script);
1776
- console.error(`Completions installed to ${dest}`);
1777
- console.error(`Restart your shell or run: source ${dest}`);
1958
+ info(`Completions installed to ${dest}`);
1959
+ info(`Restart your shell or run: source ${dest}`);
1778
1960
  }
1779
1961
  else {
1780
1962
  process.stdout.write(script);
@@ -1841,21 +2023,36 @@ const disableCommand = defineCommand({
1841
2023
  });
1842
2024
  // ── vault ───────────────────────────────────────────────────────────────────
1843
2025
  //
1844
- // `akm vault` manages secrets stored in `.env` files under the vaults/
1845
- // asset directory. Values are NEVER written to stdout. `vault load` is
1846
- // the only value-emitting path: it parses the vault with dotenv, writes
1847
- // a safely-escaped shell script to a mode-0600 temp file, and emits only
1848
- // `. <temp>; rm -f <temp>` on stdout for `eval`. The shell reads values
1849
- // from the temp file — they never transit through akm's stdout.
2026
+ // `akm vault` manages secrets stored in `.env` files under each stash's
2027
+ // vaults/ directory. Values are NEVER written to stdout or structured output.
2028
+ function parseVaultRef(ref) {
2029
+ return parseAssetRef(ref.includes(":") ? ref : `vault:${ref}`);
2030
+ }
2031
+ function findVaultSource(origin) {
2032
+ const sources = resolveSourceEntries(undefined, loadConfig());
2033
+ if (sources.length === 0) {
2034
+ throw new UsageError("No stashes configured. Run `akm init` to create your working stash.");
2035
+ }
2036
+ if (!origin || origin === "local")
2037
+ return sources[0];
2038
+ const named = sources.find((source) => source.registryId === origin);
2039
+ if (!named) {
2040
+ throw new NotFoundError(`Source not found for origin: ${origin}`);
2041
+ }
2042
+ return named;
2043
+ }
2044
+ function makeVaultRef(name, source) {
2045
+ return source?.registryId ? `${source.registryId}//vault:${name}` : `vault:${name}`;
2046
+ }
1850
2047
  function resolveVaultPath(ref) {
1851
- const stashDir = resolveStashDir({ readOnly: true });
1852
- const parsed = parseAssetRef(ref.includes(":") ? ref : `vault:${ref}`);
2048
+ const parsed = parseVaultRef(ref);
1853
2049
  if (parsed.type !== "vault") {
1854
2050
  throw new UsageError(`Expected a vault ref (vault:<name>); got "${ref}".`);
1855
2051
  }
1856
- const typeRoot = path.join(stashDir, "vaults");
2052
+ const source = findVaultSource(parsed.origin);
2053
+ const typeRoot = path.join(source.path, "vaults");
1857
2054
  const absPath = resolveAssetPathFromName("vault", typeRoot, parsed.name);
1858
- return { name: parsed.name, absPath };
2055
+ return { name: parsed.name, absPath, source, parsedRef: parsed };
1859
2056
  }
1860
2057
  /**
1861
2058
  * Walk `vaults/` recursively and return one entry per `.env` file, using the
@@ -1864,97 +2061,58 @@ function resolveVaultPath(ref) {
1864
2061
  * `vault:team/prod`, `vaults/team/.env` → `vault:team/default`).
1865
2062
  */
1866
2063
  function listVaultsRecursive(listKeysFn) {
1867
- const stashDir = resolveStashDir({ readOnly: true });
1868
- const vaultsDir = path.join(stashDir, "vaults");
1869
2064
  const result = [];
1870
- if (!fs.existsSync(vaultsDir))
1871
- return result;
1872
- const walk = (dir) => {
1873
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1874
- const full = path.join(dir, entry.name);
1875
- if (entry.isDirectory()) {
1876
- walk(full);
1877
- continue;
1878
- }
1879
- if (!entry.isFile())
1880
- continue;
1881
- if (entry.name !== ".env" && !entry.name.endsWith(".env"))
1882
- continue;
1883
- const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
1884
- if (!canonical)
1885
- continue;
1886
- const { keys } = listKeysFn(full);
1887
- result.push({ ref: `vault:${canonical}`, path: full, keyCount: keys.length });
1888
- }
1889
- };
1890
- walk(vaultsDir);
2065
+ for (const source of resolveSourceEntries(undefined, loadConfig())) {
2066
+ const vaultsDir = path.join(source.path, "vaults");
2067
+ if (!fs.existsSync(vaultsDir))
2068
+ continue;
2069
+ const walk = (dir) => {
2070
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2071
+ const full = path.join(dir, entry.name);
2072
+ if (entry.isDirectory()) {
2073
+ walk(full);
2074
+ continue;
2075
+ }
2076
+ if (!entry.isFile())
2077
+ continue;
2078
+ if (entry.name !== ".env" && !entry.name.endsWith(".env"))
2079
+ continue;
2080
+ const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
2081
+ if (!canonical)
2082
+ continue;
2083
+ const { keys } = listKeysFn(full);
2084
+ result.push({ ref: makeVaultRef(canonical, source), path: full, keys });
2085
+ }
2086
+ };
2087
+ walk(vaultsDir);
2088
+ }
1891
2089
  return result;
1892
2090
  }
1893
- function wasRefMisparsedAsFlagValue(ref, flag, flagValue) {
1894
- const argv = process.argv.slice(2);
1895
- const vaultIndex = argv.indexOf("vault");
1896
- const listIndex = vaultIndex >= 0 ? argv.indexOf("list", vaultIndex + 1) : -1;
1897
- const tokens = listIndex >= 0 ? argv.slice(listIndex + 1) : argv;
1898
- let flagIndex = -1;
1899
- let flagConsumesNextToken = false;
1900
- for (let i = 0; i < tokens.length; i += 1) {
1901
- const token = tokens[i];
1902
- if (token === flag) {
1903
- flagIndex = i;
1904
- flagConsumesNextToken = true;
1905
- break;
1906
- }
1907
- if (token === `${flag}=${flagValue}`) {
1908
- flagIndex = i;
1909
- break;
1910
- }
2091
+ function splitVaultRunTarget(target) {
2092
+ const full = resolveVaultPath(target);
2093
+ if (fs.existsSync(full.absPath)) {
2094
+ return { ref: makeVaultRef(full.name, full.source) };
1911
2095
  }
1912
- if (flagIndex === -1)
1913
- return false;
1914
- // If the same token appeared before the flag, the user explicitly passed it
1915
- // as the positional ref and it was not consumed by the output flag.
1916
- if (tokens.slice(0, flagIndex).includes(ref))
1917
- return false;
1918
- // Skip past either `--flag value` (2 tokens) or `--flag=value` (1 token)
1919
- // before checking whether the ref appears elsewhere as a real positional.
1920
- const TOKENS_AFTER_SPACE_FLAG = 2;
1921
- const TOKENS_AFTER_EQUALS_FLAG = 1;
1922
- const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? TOKENS_AFTER_SPACE_FLAG : TOKENS_AFTER_EQUALS_FLAG);
1923
- if (tokens.slice(firstTokenAfterFlag).includes(ref))
1924
- return false;
1925
- return true;
1926
- }
1927
- function resolveVaultListRef(ref) {
1928
- if (ref === undefined)
1929
- return undefined;
1930
- const parsedFormat = parseFlagValue(process.argv, "--format");
1931
- if (parsedFormat !== undefined && ref === parsedFormat && wasRefMisparsedAsFlagValue(ref, "--format", parsedFormat)) {
1932
- return undefined;
2096
+ const slashIndex = target.lastIndexOf("/");
2097
+ if (slashIndex <= 0) {
2098
+ throw new NotFoundError(`Vault not found: ${target.includes(":") ? target : `vault:${target}`}`);
1933
2099
  }
1934
- const parsedDetail = parseFlagValue(process.argv, "--detail");
1935
- if (parsedDetail !== undefined && ref === parsedDetail && wasRefMisparsedAsFlagValue(ref, "--detail", parsedDetail)) {
1936
- return undefined;
2100
+ const refPart = target.slice(0, slashIndex);
2101
+ const key = target.slice(slashIndex + 1).trim();
2102
+ if (!key) {
2103
+ throw new UsageError("Expected vault run target in the form <ref> or <ref/KEY>.");
1937
2104
  }
1938
- return ref;
2105
+ const resolved = resolveVaultPath(refPart);
2106
+ if (!fs.existsSync(resolved.absPath)) {
2107
+ throw new NotFoundError(`Vault not found: ${makeVaultRef(resolved.name, resolved.source)}`);
2108
+ }
2109
+ return { ref: makeVaultRef(resolved.name, resolved.source), key };
1939
2110
  }
1940
2111
  const vaultListCommand = defineCommand({
1941
- meta: { name: "list", description: "List vaults, or list keys (no values) inside one vault" },
1942
- args: {
1943
- ref: { type: "positional", description: "Optional vault ref (e.g. vault:prod or just prod)", required: false },
1944
- },
1945
- run({ args }) {
2112
+ meta: { name: "list", description: "List all vaults across all stashes with their available key names (no values)" },
2113
+ run() {
1946
2114
  return runWithJsonErrors(async () => {
1947
- const { listKeys, listEntries } = await import("./commands/vault.js");
1948
- const effectiveRef = resolveVaultListRef(args.ref);
1949
- if (effectiveRef) {
1950
- const { name, absPath } = resolveVaultPath(effectiveRef);
1951
- if (!fs.existsSync(absPath)) {
1952
- throw new NotFoundError(`Vault not found: vault:${name}`);
1953
- }
1954
- const entries = listEntries(absPath);
1955
- output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
1956
- return;
1957
- }
2115
+ const { listKeys } = await import("./commands/vault.js");
1958
2116
  const vaults = listVaultsRecursive(listKeys);
1959
2117
  output("vault-list", { vaults });
1960
2118
  });
@@ -1968,9 +2126,9 @@ const vaultCreateCommand = defineCommand({
1968
2126
  run({ args }) {
1969
2127
  return runWithJsonErrors(async () => {
1970
2128
  const { createVault } = await import("./commands/vault.js");
1971
- const { name, absPath } = resolveVaultPath(args.name);
2129
+ const { name, absPath, source } = resolveVaultPath(args.name);
1972
2130
  createVault(absPath);
1973
- output("vault-create", { ref: `vault:${name}`, path: absPath });
2131
+ output("vault-create", { ref: makeVaultRef(name, source), path: absPath });
1974
2132
  });
1975
2133
  },
1976
2134
  });
@@ -1992,7 +2150,7 @@ const vaultSetCommand = defineCommand({
1992
2150
  run({ args }) {
1993
2151
  return runWithJsonErrors(async () => {
1994
2152
  const { setKey } = await import("./commands/vault.js");
1995
- const { name, absPath } = resolveVaultPath(args.ref);
2153
+ const { name, absPath, source } = resolveVaultPath(args.ref);
1996
2154
  let realKey;
1997
2155
  let realValue;
1998
2156
  if ((args.value === undefined || args.value === "") && args.key.includes("=")) {
@@ -2005,7 +2163,7 @@ const vaultSetCommand = defineCommand({
2005
2163
  realValue = args.value ?? "";
2006
2164
  }
2007
2165
  setKey(absPath, realKey, realValue, args.comment);
2008
- output("vault-set", { ref: `vault:${name}`, key: realKey, path: absPath });
2166
+ output("vault-set", { ref: makeVaultRef(name, source), key: realKey, path: absPath });
2009
2167
  });
2010
2168
  },
2011
2169
  });
@@ -2018,104 +2176,93 @@ const vaultUnsetCommand = defineCommand({
2018
2176
  run({ args }) {
2019
2177
  return runWithJsonErrors(async () => {
2020
2178
  const { unsetKey } = await import("./commands/vault.js");
2021
- const { name, absPath } = resolveVaultPath(args.ref);
2179
+ const { name, absPath, source } = resolveVaultPath(args.ref);
2022
2180
  if (!fs.existsSync(absPath)) {
2023
- throw new NotFoundError(`Vault not found: vault:${name}`);
2181
+ throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
2024
2182
  }
2025
2183
  const removed = unsetKey(absPath, args.key);
2026
- output("vault-unset", { ref: `vault:${name}`, key: args.key, removed, path: absPath });
2184
+ output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed, path: absPath });
2027
2185
  });
2028
2186
  },
2029
2187
  });
2030
- const vaultLoadCommand = defineCommand({
2188
+ const vaultPathCommand = defineCommand({
2031
2189
  meta: {
2032
- name: "load",
2033
- description: 'Emit a shell snippet that loads vault values into the current shell. Use: eval "$(akm vault load vault:<name>)". Values are parsed by dotenv, written to a mode-0600 temp file with safe single-quote escaping, then sourced and removed. No values appear on akm\'s stdout, and no shell expansion happens on raw vault content.',
2190
+ name: "path",
2191
+ description: 'Print the absolute vault file path so you can load it directly, e.g. `source "$(akm vault path vault:prod)"`.',
2034
2192
  },
2035
2193
  args: {
2036
2194
  ref: { type: "positional", description: "Vault ref", required: true },
2037
2195
  },
2038
- async run({ args }) {
2196
+ run({ args }) {
2039
2197
  return runWithJsonErrors(async () => {
2040
- // This command deliberately bypasses output()/JSON shaping. Its stdout
2041
- // is a shell snippet intended for `eval`, not structured output.
2042
- const { name, absPath } = resolveVaultPath(args.ref);
2198
+ const { name, absPath, source } = resolveVaultPath(args.ref);
2043
2199
  if (!fs.existsSync(absPath)) {
2044
- throw new NotFoundError(`Vault not found: vault:${name}`);
2045
- }
2046
- const { buildShellExportScript } = await import("./commands/vault.js");
2047
- const crypto = await import("node:crypto");
2048
- const os = await import("node:os");
2049
- // Parse via dotenv (no expansion, no code execution) and build a
2050
- // script of literal `export KEY='value'` lines with `'\''` escaping.
2051
- // Sourcing this is safe even if the raw vault file contained shell
2052
- // metacharacters like $, backticks, or $(...).
2053
- const script = buildShellExportScript(absPath);
2054
- // Write to a mode-0600 temp file the shell can source.
2055
- //
2056
- // INTENTIONAL: this site uses `os.tmpdir()` (i.e. `/tmp` on Unix)
2057
- // rather than `${getCacheDir()}/vault/`. The temp file is written
2058
- // mode-0600, sourced by the parent shell via `eval`, and immediately
2059
- // `rm -f`'d on the same line of the emitted snippet. `/tmp` is the
2060
- // conventional location for short-lived shell-eval scratch files and
2061
- // benefits from tmp-cleanup-on-reboot semantics, which operators
2062
- // expect for ephemeral secret material. Moving to `~/.cache/akm/`
2063
- // would surprise those operators and also persist the file across
2064
- // reboots if the eval is interrupted before the inline `rm -f` runs.
2065
- // The bench/registry-build rationale (#276/#284) — orphan dirs
2066
- // accumulating under `/tmp` from long-running builds — does not
2067
- // apply here: the file is single-shot, a few hundred bytes, and
2068
- // removed by the same shell command that sources it.
2069
- // Regression test: tests/vault-load-error.test.ts verifies the
2070
- // emitted snippet contains both `. <path>` and `rm -f <path>`.
2071
- const tmpPath = path.join(os.tmpdir(), `akm-vault-${crypto.randomBytes(12).toString("hex")}.sh`);
2072
- fs.writeFileSync(tmpPath, script, { mode: 0o600, encoding: "utf8" });
2073
- try {
2074
- fs.chmodSync(tmpPath, 0o600);
2200
+ throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
2075
2201
  }
2076
- catch {
2077
- /* best-effort on platforms without chmod */
2078
- }
2079
- const quotedTmp = `'${tmpPath.replace(/'/g, "'\\''")}'`;
2080
- // Emit: source the temp file, then remove it — values reach bash only
2081
- // via the temp file (mode 0600), never via akm's stdout.
2082
- process.stdout.write(`. ${quotedTmp}; rm -f ${quotedTmp}\n`);
2202
+ process.stdout.write(`${absPath}\n`);
2083
2203
  });
2084
2204
  },
2085
2205
  });
2086
- const vaultShowCommand = defineCommand({
2087
- meta: { name: "show", description: "Show keys (no values) inside a vault — alias for `vault list <ref>`" },
2206
+ const vaultRunCommand = defineCommand({
2207
+ meta: {
2208
+ name: "run",
2209
+ description: "Run a command with env injected from a vault or a single vault key: `akm vault run <ref[/KEY]> -- <command>`",
2210
+ },
2088
2211
  args: {
2089
- ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
2212
+ target: { type: "positional", description: "Vault ref or ref/key target", required: true },
2090
2213
  },
2091
2214
  run({ args }) {
2092
2215
  return runWithJsonErrors(async () => {
2093
- const { listEntries } = await import("./commands/vault.js");
2094
- const { name, absPath } = resolveVaultPath(args.ref);
2216
+ const dashIndex = process.argv.indexOf("--");
2217
+ if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
2218
+ throw new UsageError("Missing command. Usage: akm vault run <ref[/KEY]> -- <command>");
2219
+ }
2220
+ const command = process.argv.slice(dashIndex + 1);
2221
+ const { loadEnv } = await import("./commands/vault.js");
2222
+ const { ref, key } = splitVaultRunTarget(args.target);
2223
+ const { name, absPath, source } = resolveVaultPath(ref);
2095
2224
  if (!fs.existsSync(absPath)) {
2096
- throw new NotFoundError(`Vault not found: vault:${name}`);
2225
+ throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
2226
+ }
2227
+ const envValues = loadEnv(absPath);
2228
+ const mergedEnv = { ...process.env };
2229
+ if (key) {
2230
+ if (!(key in envValues)) {
2231
+ throw new NotFoundError(`Key not found in ${makeVaultRef(name, source)}: ${key}`);
2232
+ }
2233
+ mergedEnv[key] = envValues[key];
2097
2234
  }
2098
- const entries = listEntries(absPath);
2099
- output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
2235
+ else {
2236
+ for (const [envKey, envValue] of Object.entries(envValues)) {
2237
+ mergedEnv[envKey] = envValue;
2238
+ }
2239
+ }
2240
+ const result = spawnSync(command[0], command.slice(1), {
2241
+ stdio: "inherit",
2242
+ env: mergedEnv,
2243
+ });
2244
+ if (result.error)
2245
+ throw result.error;
2246
+ process.exit(result.status ?? 0);
2100
2247
  });
2101
2248
  },
2102
2249
  });
2103
2250
  const vaultCommand = defineCommand({
2104
2251
  meta: {
2105
2252
  name: "vault",
2106
- description: "Manage secret vaults (.env files). Lists keys + comments only values never returned in structured output.",
2253
+ description: "Manage secret vaults (.env files). Keys are visible, values stay on disk and never appear in structured output.",
2107
2254
  },
2108
2255
  subCommands: {
2109
2256
  list: vaultListCommand,
2110
- show: vaultShowCommand,
2257
+ path: vaultPathCommand,
2258
+ run: vaultRunCommand,
2111
2259
  create: vaultCreateCommand,
2112
2260
  set: vaultSetCommand,
2113
2261
  unset: vaultUnsetCommand,
2114
- load: vaultLoadCommand,
2115
2262
  },
2116
2263
  run({ args }) {
2117
2264
  return runWithJsonErrors(async () => {
2118
- if (hasVaultSubcommand(args))
2265
+ if (hasSubcommand(args, VAULT_SUBCOMMAND_SET))
2119
2266
  return;
2120
2267
  // Default action: list all vaults
2121
2268
  const { listKeys } = await import("./commands/vault.js");
@@ -2279,12 +2426,39 @@ const wikiStashCommand = defineCommand({
2279
2426
  name: { type: "positional", description: "Wiki name", required: true },
2280
2427
  source: { type: "positional", description: "Source file path, URL, or '-' to read from stdin", required: true },
2281
2428
  as: { type: "string", description: "Preferred slug base (defaults to source filename or first-line slug)" },
2429
+ target: {
2430
+ type: "string",
2431
+ description: "Name of a writable stash source to write into instead of the default stash. Must match a configured source name (run `akm list` to see sources).",
2432
+ },
2282
2433
  },
2283
2434
  run({ args }) {
2284
2435
  return runWithJsonErrors(async () => {
2285
2436
  const { stashRaw } = await import("./wiki/wiki.js");
2286
- const { content, preferredName } = await readKnowledgeInput(args.source);
2287
- const stashDir = resolveStashDir();
2437
+ const { content, preferredName } = await (async () => {
2438
+ if (!isHttpUrl(args.source))
2439
+ return readKnowledgeInput(args.source);
2440
+ const { fetchWebsiteMarkdownSnapshot } = await import("./sources/website-ingest");
2441
+ const snapshot = await fetchWebsiteMarkdownSnapshot(args.source);
2442
+ return { content: snapshot.content, preferredName: args.as ?? snapshot.preferredName };
2443
+ })();
2444
+ let stashDir;
2445
+ if (args.target) {
2446
+ // Resolve the named source to its filesystem path.
2447
+ const cfg = loadConfig();
2448
+ const sources = resolveConfiguredSources(cfg);
2449
+ const match = sources.find((s) => s.name === args.target);
2450
+ if (!match) {
2451
+ throw new UsageError(`--target must reference a configured source name. No source named "${args.target}" found. Run \`akm list\` to see available sources.`, "INVALID_FLAG_VALUE");
2452
+ }
2453
+ const spec = match.source;
2454
+ if (spec.type !== "filesystem" && spec.type !== "local") {
2455
+ throw new ConfigError(`Source "${args.target}" is not a filesystem source and cannot be used as a wiki stash target.`, "INVALID_CONFIG_FILE", `Use a source with type "filesystem" or "local", or omit --target to use the default stash.`);
2456
+ }
2457
+ stashDir = spec.path;
2458
+ }
2459
+ else {
2460
+ stashDir = resolveStashDir();
2461
+ }
2288
2462
  const result = stashRaw({
2289
2463
  stashDir,
2290
2464
  wikiName: args.name,
@@ -2353,7 +2527,7 @@ const wikiCommand = defineCommand({
2353
2527
  },
2354
2528
  run({ args }) {
2355
2529
  return runWithJsonErrors(async () => {
2356
- if (hasWikiSubcommand(args))
2530
+ if (hasSubcommand(args, WIKI_SUBCOMMAND_SET))
2357
2531
  return;
2358
2532
  // Default action: list wikis
2359
2533
  const { listWikis } = await import("./wiki/wiki.js");
@@ -2362,38 +2536,62 @@ const wikiCommand = defineCommand({
2362
2536
  },
2363
2537
  });
2364
2538
  // ── `akm events` ────────────────────────────────────────────────────────────
2365
- // Append-only events stream surface (#204). `list` reads `events.jsonl`
2366
- // with optional --since/--type/--ref filters; `tail` follows the file via
2539
+ // Append-only events stream surface (#204). `list` reads state.db events
2540
+ // with optional --since/--type/--ref filters; `tail` follows the table via
2367
2541
  // a polling loop and prints each event as a single JSONL line.
2368
2542
  const eventsListCommand = defineCommand({
2369
- meta: { name: "list", description: "List events from the append-only events.jsonl stream" },
2543
+ meta: { name: "list", description: "List events from the append-only state.db events stream" },
2370
2544
  args: {
2371
2545
  since: {
2372
2546
  type: "string",
2373
- description: "ISO timestamp / epoch ms, OR `@offset:<bytes>` for a durable byte-cursor (resume across processes)",
2547
+ description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
2374
2548
  },
2375
2549
  type: { type: "string", description: "Filter by event type (add, remove, remember, feedback, ...)" },
2376
2550
  ref: { type: "string", description: "Filter by asset ref (type:name)" },
2551
+ "exclude-tags": {
2552
+ type: "string",
2553
+ description: "Exclude events matching these tags (repeatable)",
2554
+ },
2555
+ "include-tags": {
2556
+ type: "string",
2557
+ description: "Only include events with ALL these tags (repeatable)",
2558
+ },
2377
2559
  },
2378
2560
  run({ args }) {
2379
2561
  return runWithJsonErrors(() => {
2380
- const result = akmEventsList({ since: args.since, type: args.type, ref: args.ref });
2562
+ const excludeTags = parseAllFlagValues("--exclude-tags");
2563
+ const includeTags = parseAllFlagValues("--include-tags");
2564
+ const result = akmEventsList({
2565
+ since: args.since,
2566
+ type: args.type,
2567
+ ref: args.ref,
2568
+ ...(excludeTags.length > 0 ? { excludeTags } : {}),
2569
+ ...(includeTags.length > 0 ? { includeTags } : {}),
2570
+ });
2381
2571
  output("events-list", result);
2382
2572
  });
2383
2573
  },
2384
2574
  });
2385
2575
  const eventsTailCommand = defineCommand({
2386
- meta: { name: "tail", description: "Follow the append-only events.jsonl stream (polling)" },
2576
+ meta: { name: "tail", description: "Follow the append-only state.db events stream (polling)" },
2387
2577
  args: {
2388
2578
  since: {
2389
2579
  type: "string",
2390
- description: "ISO timestamp / epoch ms, OR `@offset:<bytes>` for a durable byte-cursor (resume across processes)",
2580
+ description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
2391
2581
  },
2392
2582
  type: { type: "string", description: "Filter by event type" },
2393
2583
  ref: { type: "string", description: "Filter by asset ref (type:name)" },
2394
2584
  "interval-ms": { type: "string", description: "Polling interval in ms (default: 75)" },
2395
2585
  "max-duration-ms": { type: "string", description: "Stop after this many ms (default: never)" },
2396
2586
  "max-events": { type: "string", description: "Stop after observing this many events" },
2587
+ "exclude-tags": {
2588
+ type: "string",
2589
+ description: "Exclude events matching these tags (repeatable)",
2590
+ },
2591
+ "include-tags": {
2592
+ type: "string",
2593
+ description: "Only include events with ALL these tags (repeatable)",
2594
+ },
2397
2595
  },
2398
2596
  async run({ args }) {
2399
2597
  await runWithJsonErrors(async () => {
@@ -2406,6 +2604,8 @@ const eventsTailCommand = defineCommand({
2406
2604
  // also rendered through the standard output() pipeline so JSON
2407
2605
  // consumers always get the canonical envelope.
2408
2606
  const stream = mode.format === "text" || mode.format === "jsonl";
2607
+ const excludeTags = parseAllFlagValues("--exclude-tags");
2608
+ const includeTags = parseAllFlagValues("--include-tags");
2409
2609
  const result = await akmEventsTail({
2410
2610
  since: args.since,
2411
2611
  type: args.type,
@@ -2413,6 +2613,8 @@ const eventsTailCommand = defineCommand({
2413
2613
  intervalMs,
2414
2614
  maxDurationMs,
2415
2615
  maxEvents,
2616
+ ...(excludeTags.length > 0 ? { excludeTags } : {}),
2617
+ ...(includeTags.length > 0 ? { includeTags } : {}),
2416
2618
  onEvent: stream
2417
2619
  ? (event) => {
2418
2620
  if (mode.format === "jsonl") {
@@ -2464,7 +2666,7 @@ function parsePositiveInt(raw, flag) {
2464
2666
  const eventsCommand = defineCommand({
2465
2667
  meta: {
2466
2668
  name: "events",
2467
- description: "Read or follow the append-only events.jsonl stream (mutations, feedback, indexing)",
2669
+ description: "Read or follow the append-only state.db events stream (mutations, feedback, indexing)",
2468
2670
  },
2469
2671
  subCommands: {
2470
2672
  list: eventsListCommand,
@@ -2472,16 +2674,12 @@ const eventsCommand = defineCommand({
2472
2674
  },
2473
2675
  });
2474
2676
  // ── proposal substrate (#225) ────────────────────────────────────────────────
2475
- const proposalListCommand = defineCommand({
2476
- meta: { name: "list", description: "List pending proposals (use --include-archive to see decided ones)" },
2677
+ const proposalsCommand = defineCommand({
2678
+ meta: { name: "proposals", description: "List proposal queue entries" },
2477
2679
  args: {
2478
2680
  status: { type: "string", description: "Filter by status (pending|accepted|rejected)" },
2479
2681
  ref: { type: "string", description: "Filter by asset ref (type:name)" },
2480
- "include-archive": {
2481
- type: "boolean",
2482
- description: "Include accepted/rejected proposals from the archive",
2483
- default: false,
2484
- },
2682
+ type: { type: "string", description: "Filter by asset type" },
2485
2683
  },
2486
2684
  run({ args }) {
2487
2685
  return runWithJsonErrors(() => {
@@ -2489,28 +2687,20 @@ const proposalListCommand = defineCommand({
2489
2687
  const result = akmProposalList({
2490
2688
  status,
2491
2689
  ref: args.ref,
2492
- includeArchive: getHyphenatedBoolean(args, "include-archive"),
2690
+ includeArchive: status === "accepted" || status === "rejected",
2493
2691
  });
2494
2692
  output("proposal-list", result);
2495
2693
  });
2496
2694
  },
2497
2695
  });
2498
- const proposalShowCommand = defineCommand({
2499
- meta: { name: "show", description: "Show a proposal's metadata, payload, and validation report" },
2500
- args: {
2501
- id: { type: "positional", description: "Proposal id (uuid)", required: true },
2502
- },
2503
- run({ args }) {
2504
- return runWithJsonErrors(() => {
2505
- const result = akmProposalShow({ id: args.id });
2506
- output("proposal-show", result);
2507
- });
2508
- },
2509
- });
2510
- const proposalAcceptCommand = defineCommand({
2511
- meta: { name: "accept", description: "Validate and promote a proposal to a real asset" },
2696
+ const acceptCommand = defineCommand({
2697
+ meta: { name: "accept", description: "Accept a proposal and promote it into the stash" },
2512
2698
  args: {
2513
- id: { type: "positional", description: "Proposal id (uuid)", required: true },
2699
+ id: {
2700
+ type: "positional",
2701
+ description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
2702
+ required: true,
2703
+ },
2514
2704
  target: { type: "string", description: "Override the write target by source name" },
2515
2705
  },
2516
2706
  async run({ args }) {
@@ -2520,23 +2710,34 @@ const proposalAcceptCommand = defineCommand({
2520
2710
  });
2521
2711
  },
2522
2712
  });
2523
- const proposalRejectCommand = defineCommand({
2524
- meta: { name: "reject", description: "Archive a pending proposal with an optional reason" },
2713
+ const rejectCommand = defineCommand({
2714
+ meta: { name: "reject", description: "Reject a proposal and record the reason" },
2525
2715
  args: {
2526
- id: { type: "positional", description: "Proposal id (uuid)", required: true },
2527
- reason: { type: "string", description: "Reason for rejection (recorded in the archived proposal)" },
2716
+ id: {
2717
+ type: "positional",
2718
+ description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
2719
+ required: true,
2720
+ },
2721
+ reason: { type: "string", description: "Reason for rejection (required)" },
2528
2722
  },
2529
2723
  run({ args }) {
2530
2724
  return runWithJsonErrors(() => {
2725
+ if (!args.reason || !String(args.reason).trim()) {
2726
+ throw new UsageError("Usage: akm reject <id> --reason '<reason>'", "MISSING_REQUIRED_ARGUMENT");
2727
+ }
2531
2728
  const result = akmProposalReject({ id: args.id, reason: args.reason });
2532
2729
  output("proposal-reject", result);
2533
2730
  });
2534
2731
  },
2535
2732
  });
2536
- const proposalDiffCommand = defineCommand({
2537
- meta: { name: "diff", description: "Show the diff between an existing asset and a pending proposal" },
2733
+ const diffCommand = defineCommand({
2734
+ meta: { name: "diff", description: "Show the diff for a proposal (accepts full UUID, UUID prefix, or asset ref)" },
2538
2735
  args: {
2539
- id: { type: "positional", description: "Proposal id (uuid)", required: true },
2736
+ id: {
2737
+ type: "positional",
2738
+ description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
2739
+ required: true,
2740
+ },
2540
2741
  target: { type: "string", description: "Override the write target by source name" },
2541
2742
  },
2542
2743
  run({ args }) {
@@ -2546,76 +2747,7 @@ const proposalDiffCommand = defineCommand({
2546
2747
  });
2547
2748
  },
2548
2749
  });
2549
- const proposalCommand = defineCommand({
2550
- meta: {
2551
- name: "proposal",
2552
- description: "Review and promote queued asset proposals (durable storage under .akm/proposals/)",
2553
- },
2554
- subCommands: {
2555
- list: proposalListCommand,
2556
- show: proposalShowCommand,
2557
- accept: proposalAcceptCommand,
2558
- reject: proposalRejectCommand,
2559
- diff: proposalDiffCommand,
2560
- },
2561
- });
2562
2750
  // ── distill (#228) ──────────────────────────────────────────────────────────
2563
- const distillCommand = defineCommand({
2564
- meta: {
2565
- name: "distill",
2566
- description: "Distil feedback for an asset into a queued lesson proposal (gated on llm.features.feedback_distillation)",
2567
- },
2568
- args: {
2569
- ref: { type: "positional", description: "Asset ref (type:name) to distil from", required: true },
2570
- "source-run": {
2571
- type: "string",
2572
- description: "Optional run id propagated onto the queued proposal for traceability",
2573
- },
2574
- "exclude-feedback-from": {
2575
- type: "string",
2576
- description: "Comma-separated asset refs whose feedback events MUST be filtered out before the LLM input is built. Falls back to AKM_DISTILL_EXCLUDE_FEEDBACK_FROM when omitted.",
2577
- },
2578
- },
2579
- async run({ args }) {
2580
- await runWithJsonErrors(async () => {
2581
- const excludeFlag = getHyphenatedArg(args, "exclude-feedback-from");
2582
- const excludeEnv = process.env.AKM_DISTILL_EXCLUDE_FEEDBACK_FROM;
2583
- // CLI flag takes precedence over the env var when both are present.
2584
- const excludeRaw = excludeFlag ?? excludeEnv;
2585
- const excludeFeedbackFromRefs = parseExcludeFeedbackFromRefs(excludeRaw);
2586
- const result = await akmDistill({
2587
- ref: args.ref,
2588
- sourceRun: getHyphenatedArg(args, "source-run"),
2589
- ...(excludeFeedbackFromRefs.length > 0 ? { excludeFeedbackFromRefs } : {}),
2590
- });
2591
- output("distill", result);
2592
- });
2593
- },
2594
- });
2595
- /**
2596
- * Parse a comma-separated list of asset refs (#267 — `--exclude-feedback-from`
2597
- * and `AKM_DISTILL_EXCLUDE_FEEDBACK_FROM`). Each entry is validated against
2598
- * the canonical `[origin//]type:name` grammar via `parseAssetRef`; an
2599
- * invalid entry surfaces as a UsageError → exit 2.
2600
- */
2601
- function parseExcludeFeedbackFromRefs(raw) {
2602
- if (raw === undefined || raw.trim() === "")
2603
- return [];
2604
- const refs = raw
2605
- .split(",")
2606
- .map((part) => part.trim())
2607
- .filter((part) => part.length > 0);
2608
- for (const ref of refs) {
2609
- try {
2610
- parseAssetRef(ref);
2611
- }
2612
- catch (err) {
2613
- const message = err instanceof Error ? err.message : String(err);
2614
- throw new UsageError(`Invalid --exclude-feedback-from ref "${ref}": ${message}`, "INVALID_FLAG_VALUE", "Each ref must match `[origin//]type:name`, e.g. skill:deploy or team//memory:auth-tips.");
2615
- }
2616
- }
2617
- return refs;
2618
- }
2619
2751
  function parseProposalStatus(raw) {
2620
2752
  if (raw === undefined)
2621
2753
  return undefined;
@@ -2626,39 +2758,211 @@ function parseProposalStatus(raw) {
2626
2758
  return trimmed;
2627
2759
  throw new UsageError(`Invalid --status value: "${raw}". Expected one of: pending, accepted, rejected.`, "INVALID_FLAG_VALUE");
2628
2760
  }
2629
- // ── reflect / propose (agent proposal-producers, #226) ──────────────────────
2630
- const reflectCommand = defineCommand({
2761
+ const agentCommand = defineCommand({
2631
2762
  meta: {
2632
- name: "reflect",
2633
- description: "Ask the configured agent CLI to review an asset (or recent feedback) and queue a revised proposal",
2763
+ name: "agent",
2764
+ description: "Dispatch an agent by named profile, optionally injecting a prompt from inline text, a stash command: asset, or a stash workflow: asset",
2634
2765
  },
2635
2766
  args: {
2636
- ref: {
2767
+ profile: {
2637
2768
  type: "positional",
2638
- description: "Asset ref (type:name) to reflect on. Optional omit to reflect across recent feedback.",
2769
+ description: "Agent profile name (from config.agent.profiles or a built-in)",
2639
2770
  required: false,
2640
2771
  },
2641
- task: { type: "string", description: "Optional task hint passed into the reflection prompt" },
2642
- profile: { type: "string", description: "Override the agent profile (defaults to agent.default)" },
2772
+ prompt: { type: "string", description: "Inline prompt text to pass to the agent" },
2773
+ command: { type: "string", description: "Load the body of a command: asset from the index and use as the prompt" },
2774
+ workflow: {
2775
+ type: "string",
2776
+ description: "Load the body of a workflow: asset from the index and use as the prompt",
2777
+ },
2643
2778
  "timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
2644
2779
  },
2645
2780
  async run({ args }) {
2646
2781
  await runWithJsonErrors(async () => {
2782
+ if (!args.profile) {
2783
+ throw new UsageError("Usage: akm agent <profile> [--prompt <text>] [--command <ref>] [--workflow <ref>] [args...]", "MISSING_REQUIRED_ARGUMENT", "Provide the agent profile name. Available profiles are listed in config.agent.profiles.");
2784
+ }
2785
+ // Collect extra positional args (forwarded to the agent and used as
2786
+ // template placeholders when a command/workflow ref is specified).
2787
+ const extraArgs = Array.isArray(args._) ? args._.filter((a) => a !== args.profile) : [];
2647
2788
  const timeoutRaw = args["timeout-ms"];
2648
2789
  const timeoutMs = typeof timeoutRaw === "string" && timeoutRaw.trim() ? Number.parseInt(timeoutRaw, 10) : undefined;
2649
- const result = await akmReflect({
2650
- ref: typeof args.ref === "string" && args.ref.trim() ? args.ref : undefined,
2651
- task: typeof args.task === "string" && args.task.trim() ? args.task : undefined,
2652
- profile: typeof args.profile === "string" && args.profile.trim() ? args.profile : undefined,
2790
+ const config = loadConfig();
2791
+ const { parseAgentConfig } = await import("./integrations/agent/config.js");
2792
+ const agentConfig = parseAgentConfig(config.agent);
2793
+ const result = await akmAgentDispatch({
2794
+ profileName: String(args.profile),
2795
+ prompt: typeof args.prompt === "string" ? args.prompt : undefined,
2796
+ commandRef: typeof args.command === "string" && args.command.trim() ? args.command.trim() : undefined,
2797
+ workflowRef: typeof args.workflow === "string" && args.workflow.trim() ? args.workflow.trim() : undefined,
2798
+ args: extraArgs.length > 0 ? extraArgs : undefined,
2799
+ agentConfig,
2800
+ llmConfig: config.llm,
2653
2801
  ...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
2654
2802
  });
2655
- output("reflect", result);
2656
- if (result.ok === false) {
2803
+ output("agent-result", result);
2804
+ if (!result.ok) {
2657
2805
  process.exit(EXIT_GENERAL);
2658
2806
  }
2659
2807
  });
2660
2808
  },
2661
2809
  });
2810
+ const lintCommand = defineCommand({
2811
+ meta: {
2812
+ name: "lint",
2813
+ description: "Scan stash .md files for structural issues (unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths). Use --fix to auto-fix Tier 1 issues.",
2814
+ },
2815
+ args: {
2816
+ fix: { type: "boolean", description: "Apply auto-fixes in place", default: false },
2817
+ dir: { type: "string", description: "Override stash root directory (default: from config)" },
2818
+ },
2819
+ async run({ args }) {
2820
+ await runWithJsonErrors(async () => {
2821
+ const result = akmLint({
2822
+ fix: args.fix ?? false,
2823
+ dir: typeof args.dir === "string" && args.dir.trim() ? args.dir.trim() : undefined,
2824
+ });
2825
+ output("lint", result);
2826
+ if (!result.ok)
2827
+ process.exit(EXIT_GENERAL);
2828
+ });
2829
+ },
2830
+ });
2831
+ const improveCommand = defineCommand({
2832
+ meta: {
2833
+ name: "improve",
2834
+ description: "Analyze existing AKM assets and generate improvement proposals; also consolidates memories when llm.features.memory_consolidation is enabled",
2835
+ },
2836
+ args: {
2837
+ scope: {
2838
+ type: "positional",
2839
+ description: "Optional asset type or asset ref to improve",
2840
+ required: false,
2841
+ },
2842
+ task: { type: "string", description: "Add extra guidance for this improvement pass" },
2843
+ "dry-run": { type: "boolean", description: "Show planned actions without writing", default: false },
2844
+ target: { type: "string", description: "Override the write target for accepted proposals" },
2845
+ "auto-accept": {
2846
+ type: "string",
2847
+ description: "Automatically accept low-risk proposals (only 'safe' is supported)",
2848
+ },
2849
+ limit: { type: "string", description: "Maximum number of assets to process (highest utility first)" },
2850
+ "timeout-ms": {
2851
+ type: "string",
2852
+ description: "Wall-clock budget for the entire run in milliseconds (default: 7200000 = 2 hours)",
2853
+ },
2854
+ "ignore-cooldown": {
2855
+ type: "boolean",
2856
+ description: "Ignore all cooldown periods (equivalent to --reflect-cooldown-days 0 --distill-cooldown-days 0 --consolidate-cooldown-days 0)",
2857
+ default: false,
2858
+ },
2859
+ "reflect-cooldown-days": {
2860
+ type: "string",
2861
+ description: "Override reflect cooldown for this run only (default: 7, 0 to disable)",
2862
+ },
2863
+ "distill-cooldown-days": {
2864
+ type: "string",
2865
+ description: "Override distill cooldown for this run only (default: 30, 0 to disable)",
2866
+ },
2867
+ "consolidate-cooldown-days": {
2868
+ type: "string",
2869
+ description: "Override consolidate cooldown for this run only (default: 14, 0 to disable)",
2870
+ },
2871
+ "consolidate-recovery": {
2872
+ type: "string",
2873
+ description: "How to handle stale/incomplete consolidation journals: abort (default) or clean (remove stale journal artifacts)",
2874
+ },
2875
+ "require-feedback-signal": {
2876
+ type: "boolean",
2877
+ description: "Only process assets with recent feedback signals (disables retrieval fallback)",
2878
+ default: false,
2879
+ },
2880
+ "min-retrieval-count": {
2881
+ type: "string",
2882
+ description: "Minimum retrieval count for zero-feedback fallback eligibility (default: 5)",
2883
+ },
2884
+ },
2885
+ async run({ args }) {
2886
+ await runWithJsonErrors(async () => {
2887
+ const autoAcceptRaw = getHyphenatedArg(args, "auto-accept");
2888
+ if (autoAcceptRaw !== undefined && autoAcceptRaw !== "safe") {
2889
+ throw new UsageError("--auto-accept only supports the value 'safe'.", "INVALID_FLAG_VALUE");
2890
+ }
2891
+ const targetArg = typeof args.target === "string" && args.target.trim() ? args.target.trim() : undefined;
2892
+ const taskArg = typeof args.task === "string" && args.task.trim() ? args.task : undefined;
2893
+ const dryRun = getHyphenatedBoolean(args, "dry-run");
2894
+ const autoAccept = autoAcceptRaw === "safe" ? "safe" : undefined;
2895
+ const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
2896
+ const timeoutRaw = getHyphenatedArg(args, "timeout-ms");
2897
+ const timeoutMs = timeoutRaw !== undefined ? parseInt(timeoutRaw, 10) : undefined;
2898
+ if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) {
2899
+ throw new UsageError(`Invalid --timeout-ms value: "${timeoutRaw}". Must be a positive integer.`);
2900
+ }
2901
+ const parseNonNegativeCooldownDays = (raw, flagName) => {
2902
+ if (raw === undefined)
2903
+ return undefined;
2904
+ if (!/^\d+$/.test(raw.trim())) {
2905
+ throw new UsageError(`Invalid ${flagName} value: "${raw}". Must be a non-negative integer.`);
2906
+ }
2907
+ return parseInt(raw, 10);
2908
+ };
2909
+ const ignoreCooldown = getHyphenatedBoolean(args, "ignore-cooldown");
2910
+ const reflectCooldownRaw = getHyphenatedArg(args, "reflect-cooldown-days");
2911
+ const reflectCooldownDays = ignoreCooldown
2912
+ ? 0
2913
+ : parseNonNegativeCooldownDays(reflectCooldownRaw, "--reflect-cooldown-days");
2914
+ const distillCooldownRaw = getHyphenatedArg(args, "distill-cooldown-days");
2915
+ const distillCooldownDays = ignoreCooldown
2916
+ ? 0
2917
+ : parseNonNegativeCooldownDays(distillCooldownRaw, "--distill-cooldown-days");
2918
+ const consolidateCooldownRaw = getHyphenatedArg(args, "consolidate-cooldown-days");
2919
+ const consolidateCooldownDays = ignoreCooldown
2920
+ ? 0
2921
+ : parseNonNegativeCooldownDays(consolidateCooldownRaw, "--consolidate-cooldown-days");
2922
+ const consolidateRecoveryRaw = getHyphenatedArg(args, "consolidate-recovery");
2923
+ const consolidateRecovery = consolidateRecoveryRaw === undefined
2924
+ ? undefined
2925
+ : consolidateRecoveryRaw.trim().toLowerCase();
2926
+ if (consolidateRecovery !== undefined && consolidateRecovery !== "abort" && consolidateRecovery !== "clean") {
2927
+ throw new UsageError(`Invalid --consolidate-recovery value: "${consolidateRecoveryRaw}". Must be one of: abort, clean.`, "INVALID_FLAG_VALUE");
2928
+ }
2929
+ const minRetrievalCountRaw = getHyphenatedArg(args, "min-retrieval-count");
2930
+ const minRetrievalCount = parseNonNegativeCooldownDays(minRetrievalCountRaw, "--min-retrieval-count");
2931
+ const requireFeedbackSignal = getHyphenatedBoolean(args, "require-feedback-signal");
2932
+ const improveLogFile = path.join(getCacheDir(), "logs", "improve", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
2933
+ setLogFile(improveLogFile);
2934
+ let improveResult;
2935
+ try {
2936
+ improveResult = await akmImprove({
2937
+ scope: typeof args.scope === "string" && args.scope.trim() ? args.scope : undefined,
2938
+ task: taskArg,
2939
+ dryRun,
2940
+ target: targetArg,
2941
+ autoAccept,
2942
+ ...(limitRaw !== undefined ? { limit: limitRaw } : {}),
2943
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
2944
+ ...(reflectCooldownDays !== undefined ? { reflectCooldownDays } : {}),
2945
+ ...(distillCooldownDays !== undefined ? { distillCooldownDays } : {}),
2946
+ ...(consolidateCooldownDays !== undefined ? { consolidateCooldownDays } : {}),
2947
+ ...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
2948
+ ...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
2949
+ consolidateOptions: {
2950
+ target: targetArg,
2951
+ dryRun,
2952
+ autoAccept,
2953
+ task: taskArg,
2954
+ ...(consolidateRecovery !== undefined ? { recoveryMode: consolidateRecovery } : {}),
2955
+ },
2956
+ });
2957
+ }
2958
+ finally {
2959
+ clearLogFile();
2960
+ }
2961
+ output("improve", improveResult);
2962
+ process.exit(0);
2963
+ });
2964
+ },
2965
+ });
2662
2966
  const proposeCommand = defineCommand({
2663
2967
  meta: {
2664
2968
  name: "propose",
@@ -2671,6 +2975,7 @@ const proposeCommand = defineCommand({
2671
2975
  type: { type: "positional", description: "Asset type (skill, command, knowledge, lesson, ...)", required: false },
2672
2976
  name: { type: "positional", description: "Asset name (slug or path under the type dir)", required: false },
2673
2977
  task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
2978
+ file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
2674
2979
  profile: { type: "string", description: "Override the agent profile (defaults to agent.default)" },
2675
2980
  "timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
2676
2981
  },
@@ -2679,15 +2984,21 @@ const proposeCommand = defineCommand({
2679
2984
  // citty silently shows help and exits 0 when required positionals are
2680
2985
  // omitted. Re-validate explicitly so the exit code is 2 (USAGE) and a
2681
2986
  // structured JSON error reaches scripted callers.
2682
- if (!args.type || !args.name || !args.task) {
2683
- throw new UsageError("Usage: akm propose <type> <name> --task '<task>'.", "MISSING_REQUIRED_ARGUMENT", "Provide the asset type, name, and a --task description, e.g. `akm propose skill deploy --task 'Deploy a service'`.");
2987
+ const taskFromFlag = typeof args.task === "string" ? args.task : undefined;
2988
+ const fileFromFlag = typeof args.file === "string" ? args.file : undefined;
2989
+ if (!args.type || !args.name || (!taskFromFlag && !fileFromFlag)) {
2990
+ throw new UsageError("Usage: akm propose <type> <name> (--task '<task>' | --file <path>).", "MISSING_REQUIRED_ARGUMENT", "Provide the asset type, name, and exactly one of --task or --file.");
2991
+ }
2992
+ if (taskFromFlag && fileFromFlag) {
2993
+ throw new UsageError("Pass exactly one of --task or --file.", "INVALID_FLAG_VALUE");
2684
2994
  }
2995
+ const taskText = fileFromFlag ? fs.readFileSync(path.resolve(fileFromFlag), "utf8") : (taskFromFlag ?? "");
2685
2996
  const timeoutRaw = args["timeout-ms"];
2686
2997
  const timeoutMs = typeof timeoutRaw === "string" && timeoutRaw.trim() ? Number.parseInt(timeoutRaw, 10) : undefined;
2687
2998
  const result = await akmPropose({
2688
2999
  type: String(args.type),
2689
3000
  name: String(args.name),
2690
- task: String(args.task ?? ""),
3001
+ task: taskText,
2691
3002
  profile: typeof args.profile === "string" && args.profile.trim() ? args.profile : undefined,
2692
3003
  ...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
2693
3004
  });
@@ -2698,6 +3009,192 @@ const proposeCommand = defineCommand({
2698
3009
  });
2699
3010
  },
2700
3011
  });
3012
+ const TASKS_SUBCOMMAND_SET = new Set([
3013
+ "add",
3014
+ "list",
3015
+ "show",
3016
+ "remove",
3017
+ "enable",
3018
+ "disable",
3019
+ "run",
3020
+ "history",
3021
+ "sync",
3022
+ "doctor",
3023
+ ]);
3024
+ const GRAPH_SUBCOMMAND_SET = new Set(["summary", "entities", "relations", "related", "export"]);
3025
+ const tasksAddCommand = defineCommand({
3026
+ meta: { name: "add", description: "Register a new scheduled task and install it in the OS scheduler" },
3027
+ args: {
3028
+ id: { type: "positional", description: "Task id (used as filename and scheduler entry)", required: true },
3029
+ schedule: { type: "string", description: 'Cron-style schedule, e.g. "0 9 * * *" or "@daily"', required: true },
3030
+ workflow: { type: "string", description: "Workflow ref to invoke (e.g. workflow:my-flow)" },
3031
+ prompt: {
3032
+ type: "string",
3033
+ description: "Prompt for the configured agent harness — inline text, an asset ref like agent:foo, or ./path.md",
3034
+ },
3035
+ profile: { type: "string", description: "Agent profile to use for prompt targets (default: config.agent.default)" },
3036
+ params: { type: "string", description: "Workflow params as a JSON object" },
3037
+ description: { type: "string", description: "Human-readable description" },
3038
+ tags: { type: "string", description: "Comma-separated tags" },
3039
+ disabled: { type: "boolean", description: "Register but leave disabled in the OS scheduler", default: false },
3040
+ force: { type: "boolean", description: "Overwrite an existing task with the same id", default: false },
3041
+ },
3042
+ async run({ args }) {
3043
+ await runWithJsonErrors(async () => {
3044
+ const result = await akmTasksAdd({
3045
+ id: args.id,
3046
+ schedule: args.schedule,
3047
+ workflow: args.workflow,
3048
+ prompt: args.prompt,
3049
+ profile: args.profile,
3050
+ params: args.params,
3051
+ description: args.description,
3052
+ tags: args.tags
3053
+ ? args.tags
3054
+ .split(/[\s,]+/)
3055
+ .map((s) => s.trim())
3056
+ .filter(Boolean)
3057
+ : undefined,
3058
+ disabled: args.disabled === true,
3059
+ force: args.force === true,
3060
+ });
3061
+ output("tasks-add", result);
3062
+ });
3063
+ },
3064
+ });
3065
+ const tasksListCommand = defineCommand({
3066
+ meta: { name: "list", description: "List scheduled tasks in the stash" },
3067
+ async run() {
3068
+ await runWithJsonErrors(async () => {
3069
+ const result = await akmTasksList();
3070
+ output("tasks-list", result);
3071
+ });
3072
+ },
3073
+ });
3074
+ const tasksShowCommand = defineCommand({
3075
+ meta: { name: "show", description: "Show a parsed task definition" },
3076
+ args: { id: { type: "positional", description: "Task id or task:<id>", required: true } },
3077
+ async run({ args }) {
3078
+ await runWithJsonErrors(async () => {
3079
+ const { id } = parseTaskRef(args.id);
3080
+ const result = await akmTasksShow(id);
3081
+ output("tasks-show", result);
3082
+ });
3083
+ },
3084
+ });
3085
+ const tasksRemoveCommand = defineCommand({
3086
+ meta: { name: "remove", description: "Delete a task file and uninstall it from the OS scheduler" },
3087
+ args: { id: { type: "positional", description: "Task id", required: true } },
3088
+ async run({ args }) {
3089
+ await runWithJsonErrors(async () => {
3090
+ const { id } = parseTaskRef(args.id);
3091
+ const result = await akmTasksRemove(id);
3092
+ output("tasks-remove", result);
3093
+ });
3094
+ },
3095
+ });
3096
+ const tasksEnableCommand = defineCommand({
3097
+ meta: { name: "enable", description: "Enable a previously-disabled task" },
3098
+ args: { id: { type: "positional", description: "Task id", required: true } },
3099
+ async run({ args }) {
3100
+ await runWithJsonErrors(async () => {
3101
+ const { id } = parseTaskRef(args.id);
3102
+ const result = await akmTasksSetEnabled(id, true);
3103
+ output("tasks-enable", result);
3104
+ });
3105
+ },
3106
+ });
3107
+ const tasksDisableCommand = defineCommand({
3108
+ meta: { name: "disable", description: "Disable a task in the OS scheduler without removing the file" },
3109
+ args: { id: { type: "positional", description: "Task id", required: true } },
3110
+ async run({ args }) {
3111
+ await runWithJsonErrors(async () => {
3112
+ const { id } = parseTaskRef(args.id);
3113
+ const result = await akmTasksSetEnabled(id, false);
3114
+ output("tasks-disable", result);
3115
+ });
3116
+ },
3117
+ });
3118
+ const tasksRunCommand = defineCommand({
3119
+ meta: {
3120
+ name: "run",
3121
+ description: "Execute a task now (this is what cron / launchd / schtasks invoke at the scheduled time)",
3122
+ },
3123
+ args: { id: { type: "positional", description: "Task id", required: true } },
3124
+ async run({ args }) {
3125
+ await runWithJsonErrors(async () => {
3126
+ const { id } = parseTaskRef(args.id);
3127
+ const envelope = await akmTasksRun(id);
3128
+ output("tasks-run", envelope);
3129
+ if (envelope.exitCode !== 0)
3130
+ process.exit(envelope.exitCode);
3131
+ });
3132
+ },
3133
+ });
3134
+ const tasksHistoryCommand = defineCommand({
3135
+ meta: { name: "history", description: "Show recent task run history" },
3136
+ args: {
3137
+ id: { type: "string", description: "Filter to one task id" },
3138
+ limit: { type: "string", description: "Maximum rows to return (default 50)" },
3139
+ },
3140
+ async run({ args }) {
3141
+ await runWithJsonErrors(async () => {
3142
+ const limit = parsePositiveIntFlag(args.limit ?? undefined);
3143
+ const result = await akmTasksHistory({ id: args.id, limit });
3144
+ output("tasks-history", result);
3145
+ });
3146
+ },
3147
+ });
3148
+ const tasksSyncCommand = defineCommand({
3149
+ meta: {
3150
+ name: "sync",
3151
+ description: "Reconcile the on-disk task files with the OS scheduler",
3152
+ },
3153
+ async run() {
3154
+ await runWithJsonErrors(async () => {
3155
+ const result = await akmTasksSync();
3156
+ output("tasks-sync", result);
3157
+ });
3158
+ },
3159
+ });
3160
+ const tasksDoctorCommand = defineCommand({
3161
+ meta: {
3162
+ name: "doctor",
3163
+ description: "Report the active scheduler backend, akm bin path, log dir, and supported schedule subset",
3164
+ },
3165
+ async run() {
3166
+ await runWithJsonErrors(async () => {
3167
+ const result = await akmTasksDoctor();
3168
+ output("tasks-doctor", result);
3169
+ });
3170
+ },
3171
+ });
3172
+ const tasksCommand = defineCommand({
3173
+ meta: {
3174
+ name: "tasks",
3175
+ description: "Schedule workflows or prompts via the OS-native scheduler (cron / launchd / schtasks)",
3176
+ },
3177
+ subCommands: {
3178
+ add: tasksAddCommand,
3179
+ list: tasksListCommand,
3180
+ show: tasksShowCommand,
3181
+ remove: tasksRemoveCommand,
3182
+ enable: tasksEnableCommand,
3183
+ disable: tasksDisableCommand,
3184
+ run: tasksRunCommand,
3185
+ history: tasksHistoryCommand,
3186
+ sync: tasksSyncCommand,
3187
+ doctor: tasksDoctorCommand,
3188
+ },
3189
+ run({ args }) {
3190
+ return runWithJsonErrors(async () => {
3191
+ if (hasSubcommand(args, TASKS_SUBCOMMAND_SET))
3192
+ return;
3193
+ const result = await akmTasksList();
3194
+ output("tasks-list", result);
3195
+ });
3196
+ },
3197
+ });
2701
3198
  const main = defineCommand({
2702
3199
  meta: {
2703
3200
  name: "akm",
@@ -2718,7 +3215,9 @@ const main = defineCommand({
2718
3215
  setup: setupCommand,
2719
3216
  init: initCommand,
2720
3217
  index: indexCommand,
3218
+ health: healthCommand,
2721
3219
  info: infoCommand,
3220
+ graph: graphCommand,
2722
3221
  add: addCommand,
2723
3222
  list: listCommand,
2724
3223
  remove: removeCommand,
@@ -2739,19 +3238,24 @@ const main = defineCommand({
2739
3238
  feedback: feedbackCommand,
2740
3239
  history: historyCommand,
2741
3240
  events: eventsCommand,
2742
- proposal: proposalCommand,
2743
- reflect: reflectCommand,
3241
+ agent: agentCommand,
3242
+ lint: lintCommand,
3243
+ improve: improveCommand,
2744
3244
  propose: proposeCommand,
2745
- distill: distillCommand,
3245
+ proposals: proposalsCommand,
3246
+ accept: acceptCommand,
3247
+ reject: rejectCommand,
3248
+ diff: diffCommand,
2746
3249
  help: helpCommand,
2747
3250
  hints: hintsCommand,
2748
3251
  completions: completionsCommand,
2749
3252
  vault: vaultCommand,
2750
3253
  wiki: wikiCommand,
3254
+ tasks: tasksCommand,
2751
3255
  },
2752
3256
  });
2753
- const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
2754
- const VAULT_SUBCOMMAND_SET = new Set(["list", "show", "create", "set", "unset", "load"]);
3257
+ const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "show", "get", "set", "unset"]);
3258
+ const VAULT_SUBCOMMAND_SET = new Set(["list", "path", "run", "create", "set", "unset"]);
2755
3259
  const WIKI_SUBCOMMAND_SET = new Set([
2756
3260
  "create",
2757
3261
  "register",
@@ -2764,7 +3268,6 @@ const WIKI_SUBCOMMAND_SET = new Set([
2764
3268
  "lint",
2765
3269
  "ingest",
2766
3270
  ]);
2767
- const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
2768
3271
  // ── Exit codes ──────────────────────────────────────────────────────────────
2769
3272
  const EXIT_GENERAL = 1;
2770
3273
  const EXIT_USAGE = 2;
@@ -2778,6 +3281,7 @@ process.argv = normalizeShowArgv(process.argv);
2778
3281
  // invalid; surface it through the same JSON-error path the rest of the CLI uses
2779
3282
  // rather than letting the raw exception escape with a stack trace.
2780
3283
  try {
3284
+ applyEarlyStderrFlags(process.argv);
2781
3285
  initOutputMode(process.argv, loadConfig().output ?? {});
2782
3286
  }
2783
3287
  catch (error) {
@@ -2802,16 +3306,7 @@ function classifyExitCode(error) {
2802
3306
  }
2803
3307
  async function runWithJsonErrors(fn) {
2804
3308
  try {
2805
- // Apply --quiet flag early so warnings inside the command are suppressed
2806
- if (process.argv.includes("--quiet") || process.argv.includes("-q")) {
2807
- setQuiet(true);
2808
- }
2809
- // Apply --verbose flag early so per-spec diagnostics (gated behind
2810
- // `isVerbose()` in src/core/warn.ts) are restored. The `AKM_VERBOSE`
2811
- // env var still wins regardless — see warn.ts for the precedence rule.
2812
- if (process.argv.includes("--verbose")) {
2813
- setVerbose(true);
2814
- }
3309
+ applyEarlyStderrFlags(process.argv);
2815
3310
  await fn();
2816
3311
  }
2817
3312
  catch (error) {
@@ -2838,87 +3333,6 @@ function extractHint(error) {
2838
3333
  }
2839
3334
  return undefined;
2840
3335
  }
2841
- function hasConfigSubcommand(args) {
2842
- const command = Array.isArray(args._) ? args._[0] : undefined;
2843
- return typeof command === "string" && CONFIG_SUBCOMMAND_SET.has(command);
2844
- }
2845
- function hasVaultSubcommand(args) {
2846
- const command = Array.isArray(args._) ? args._[0] : undefined;
2847
- return typeof command === "string" && VAULT_SUBCOMMAND_SET.has(command);
2848
- }
2849
- function hasWikiSubcommand(args) {
2850
- const command = Array.isArray(args._) ? args._[0] : undefined;
2851
- return typeof command === "string" && WIKI_SUBCOMMAND_SET.has(command);
2852
- }
2853
- /**
2854
- * Normalize argv so positional view-mode arguments after the asset ref
2855
- * are rewritten into internal flags that citty can parse.
2856
- *
2857
- * Converts:
2858
- * akm show knowledge:guide.md toc → akm show knowledge:guide.md --akmView toc
2859
- * akm show knowledge:guide.md section Auth → akm show knowledge:guide.md --akmView section --akmHeading Auth
2860
- * akm show knowledge:guide.md lines 1 50 → akm show knowledge:guide.md --akmView lines --akmStart 1 --akmEnd 50
2861
- *
2862
- * Legacy `--view` is intentionally unsupported.
2863
- * Returns a new array; the input is never modified.
2864
- */
2865
- function normalizeShowArgv(argv) {
2866
- // argv[0]=bun argv[1]=script argv[2]=subcommand argv[3]=ref argv[4..]=rest
2867
- if (argv[2] !== "show")
2868
- return argv;
2869
- if (argv.includes("--view") || argv.includes("--heading") || argv.includes("--start") || argv.includes("--end")) {
2870
- throw new UsageError('Legacy show flags are no longer supported. Use positional syntax like `akm show knowledge:guide toc` or `akm show knowledge:guide section "Auth"`.');
2871
- }
2872
- // Separate global flags from positional/show-specific args
2873
- const prefix = argv.slice(0, 3); // [bun, script, show]
2874
- const rest = argv.slice(3);
2875
- const globalFlags = [];
2876
- const showArgs = [];
2877
- for (let i = 0; i < rest.length; i++) {
2878
- const arg = rest[i];
2879
- if (arg === "--quiet" || arg === "-q" || arg === "--for-agent" || arg === "--for-agent=true") {
2880
- globalFlags.push(arg);
2881
- continue;
2882
- }
2883
- if (arg.startsWith("--format=") || arg.startsWith("--detail=")) {
2884
- globalFlags.push(arg);
2885
- continue;
2886
- }
2887
- if (arg === "--format" || arg === "--detail") {
2888
- globalFlags.push(arg);
2889
- if (rest[i + 1] !== undefined) {
2890
- globalFlags.push(rest[i + 1]);
2891
- i++;
2892
- }
2893
- continue;
2894
- }
2895
- showArgs.push(arg);
2896
- }
2897
- // showArgs[0] = ref, showArgs[1] = potential view mode, showArgs[2..] = view params
2898
- const ref = showArgs[0];
2899
- const viewMode = showArgs[1];
2900
- if (!ref || !viewMode || !SHOW_VIEW_MODES.has(viewMode)) {
2901
- return argv;
2902
- }
2903
- const result = [...prefix, ref, "--akmView", viewMode];
2904
- if (viewMode === "section") {
2905
- // Next arg is the heading name; pass empty string when missing so the
2906
- // show handler can produce a clear "section not found" error.
2907
- const heading = showArgs[2] ?? "";
2908
- result.push("--akmHeading", heading);
2909
- }
2910
- else if (viewMode === "lines") {
2911
- // Next two args are start and end
2912
- const start = showArgs[2];
2913
- const end = showArgs[3];
2914
- if (start)
2915
- result.push("--akmStart", start);
2916
- if (end)
2917
- result.push("--akmEnd", end);
2918
- }
2919
- result.push(...globalFlags);
2920
- return result;
2921
- }
2922
3336
  // ── Hints (embedded AGENTS.md) ──────────────────────────────────────────────
2923
3337
  function loadHints(detail = "normal") {
2924
3338
  const filename = detail === "full" ? "AGENTS.full.md" : "AGENTS.md";