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

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 (111) hide show
  1. package/CHANGELOG.md +162 -23
  2. package/dist/akm-migrate +3 -1
  3. package/dist/assets/hints/cli-hints-full.md +3 -4
  4. package/dist/assets/hints/cli-hints-short.md +5 -5
  5. package/dist/assets/workflows/workflow-template.md +4 -3
  6. package/dist/cli/invocation.js +3 -2
  7. package/dist/cli/retired-commands.js +5 -0
  8. package/dist/cli/unknown-flags.js +226 -0
  9. package/dist/cli.js +19 -35
  10. package/dist/commands/agent/agent-dispatch.js +7 -3
  11. package/dist/commands/agent/contribute-cli.js +15 -3
  12. package/dist/commands/feedback-cli.js +6 -3
  13. package/dist/commands/health/checks.js +6 -3
  14. package/dist/commands/improve/collapse-detector.js +2 -3
  15. package/dist/commands/improve/reflect.js +5 -3
  16. package/dist/commands/lint/base-linter.js +4 -16
  17. package/dist/commands/lint/index.js +13 -13
  18. package/dist/commands/log.js +6 -1
  19. package/dist/commands/migration-tool.js +4 -5
  20. package/dist/commands/observability-cli.js +1 -1
  21. package/dist/commands/proposal/propose.js +5 -3
  22. package/dist/commands/proposal/repository.js +5 -5
  23. package/dist/commands/read/knowledge.js +2 -0
  24. package/dist/commands/registry-cli.js +5 -3
  25. package/dist/commands/sources/add-cli.js +6 -6
  26. package/dist/commands/sources/init.js +1 -1
  27. package/dist/commands/sources/self-update.js +30 -7
  28. package/dist/commands/sources/source-add.js +17 -2
  29. package/dist/commands/tasks/tasks.js +10 -10
  30. package/dist/commands/workflow-cli.js +133 -259
  31. package/dist/core/adapter/adapters/akm-lint.js +17 -13
  32. package/dist/core/adapter/adapters/akm-task-adapter.js +14 -11
  33. package/dist/core/asset/akm-markdown.js +41 -8
  34. package/dist/core/asset/frontmatter.js +22 -0
  35. package/dist/core/asset/resolve-ref.js +23 -3
  36. package/dist/core/common.js +45 -2
  37. package/dist/core/config/config-schema.js +8 -0
  38. package/dist/core/config/schema/experimental.js +0 -17
  39. package/dist/core/config/schema/sources-bundles.js +11 -0
  40. package/dist/core/config/schema/workflow.js +3 -3
  41. package/dist/core/errors.js +5 -1
  42. package/dist/core/logs-db.js +2 -1
  43. package/dist/core/parse.js +4 -1
  44. package/dist/core/state/migrations.js +11 -14
  45. package/dist/core/state-db.js +4 -6
  46. package/dist/core/subprocess.js +6 -4
  47. package/dist/core/type-presentation.js +1 -1
  48. package/dist/indexer/indexer.js +16 -1
  49. package/dist/indexer/search/search-source.js +1 -1
  50. package/dist/indexer/walk/matchers.js +3 -1
  51. package/dist/integrations/agent/config.js +2 -2
  52. package/dist/integrations/agent/detect.js +49 -19
  53. package/dist/integrations/agent/engine-fallback.js +76 -0
  54. package/dist/integrations/agent/profiles.js +14 -0
  55. package/dist/integrations/agent/spawn.js +1 -2
  56. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  57. package/dist/integrations/lockfile.js +11 -56
  58. package/dist/output/shapes/passthrough.js +0 -6
  59. package/dist/output/text/command-format.js +3 -3
  60. package/dist/output/text/helpers.js +1 -1
  61. package/dist/output/text/show-directives.js +4 -6
  62. package/dist/output/text/workflow-format.js +15 -181
  63. package/dist/output/text/workflow.js +1 -6
  64. package/dist/scripts/akm-migrate-node.js +58666 -0
  65. package/dist/scripts/akm-migrate.js +33976 -11498
  66. package/dist/setup/detect.js +42 -15
  67. package/dist/setup/setup.js +1 -1
  68. package/dist/sources/providers/git-stash.js +4 -2
  69. package/dist/sources/providers/website.js +5 -0
  70. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  71. package/dist/sources/snapshot-fetchers/content-extract.js +370 -0
  72. package/dist/sources/snapshot-fetchers/fetcher-util.js +40 -0
  73. package/dist/sources/snapshot-fetchers/host-guard.js +199 -0
  74. package/dist/sources/snapshot-fetchers/registry.js +10 -1
  75. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  76. package/dist/sources/snapshot-fetchers/rss.js +279 -0
  77. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  78. package/dist/sources/snapshot-fetchers/website-ingest.js +488 -257
  79. package/dist/sources/snapshot-fetchers/x.js +193 -0
  80. package/dist/storage/engines/sqlite-migrations.js +22 -107
  81. package/dist/storage/repositories/workflow-runs-repository.js +9 -23
  82. package/dist/tasks/runner.js +43 -19
  83. package/dist/tasks/schema.js +24 -1
  84. package/dist/tasks/validator.js +6 -2
  85. package/dist/workflows/exec/frozen-judge.js +28 -2
  86. package/dist/workflows/exec/native-executor.js +44 -28
  87. package/dist/workflows/exec/param-secrets.js +9 -9
  88. package/dist/workflows/exec/run-workflow.js +122 -60
  89. package/dist/workflows/exec/step-work.js +64 -175
  90. package/dist/workflows/exec/unit-dispatch.js +4 -0
  91. package/dist/workflows/ir/compile.js +2 -2
  92. package/dist/workflows/ir/freeze.js +29 -9
  93. package/dist/workflows/ir/params.js +135 -11
  94. package/dist/workflows/ir/plan-hash.js +1 -1
  95. package/dist/workflows/ir/schema.js +6 -2
  96. package/dist/workflows/renderer.js +2 -2
  97. package/dist/workflows/runtime/checkin.js +3 -3
  98. package/dist/workflows/runtime/runs.js +72 -92
  99. package/dist/workflows/runtime/unit-checkin.js +1 -1
  100. package/dist/workflows/runtime/unit-phases.js +2 -2
  101. package/dist/workflows/validate-summary.js +30 -14
  102. package/docs/migration/release-notes/0.9.0.md +31 -4
  103. package/docs/migration/v0.8-to-v0.9.md +70 -100
  104. package/docs/reference/data-and-telemetry.md +5 -5
  105. package/package.json +7 -3
  106. package/schemas/akm-config.json +24 -6
  107. package/schemas/akm-workflow.json +1 -1
  108. package/dist/workflows/cli.js +0 -33
  109. package/dist/workflows/exec/brief.js +0 -450
  110. package/dist/workflows/exec/report.js +0 -1460
  111. package/dist/workflows/exec/workflow-engine-gate.js +0 -67
package/dist/cli.js CHANGED
@@ -60,6 +60,7 @@ import { defineCommand, parseArgs, renderUsage, runCommand, showUsage } from "ci
60
60
  import { findCittyTopLevelCommand, findCittyTopLevelCommandIndex, getParsedInvocation, parseAllFlagValues, resolveHelpMigrateVersionArg, setParsedInvocation, } from "./cli/invocation.js";
61
61
  import { retiredCommandHint } from "./cli/retired-commands.js";
62
62
  import { defineGroupCommand, EXIT_CODES, emitJsonError, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors, } from "./cli/shared.js";
63
+ import { assertKnownFlags, closestMatch } from "./cli/unknown-flags.js";
63
64
  import { agentCommand, lintCommand } from "./commands/agent/contribute-cli.js";
64
65
  import { generateBashCompletions, installBashCompletions } from "./commands/completions.js";
65
66
  import { configCommand } from "./commands/config-cli.js";
@@ -236,7 +237,13 @@ const setupCommand = defineCommand({
236
237
  printSetupTtyHint(result);
237
238
  }
238
239
  else {
239
- // Interactive wizard
240
+ // Interactive wizard. Guard the TTY first: the wizard's prompts read
241
+ // from stdin, so a piped/redirected/CI invocation would render the
242
+ // first prompt and then block forever instead of failing — and `akm
243
+ // setup` is the first command users automate.
244
+ if (process.stdin.isTTY !== true) {
245
+ throw new UsageError("Interactive setup requires a TTY. Pass --yes to accept defaults, or --config <json> / --from <file> to configure non-interactively.", "NON_INTERACTIVE_REQUIRES_YES");
246
+ }
240
247
  const { runSetupWizard } = await import("./setup/setup.js");
241
248
  await runSetupWizard({ dir: args.dir, noInit });
242
249
  }
@@ -818,40 +825,9 @@ function findUnknownCommandAttempt(rawArgs) {
818
825
  args = args.slice(idx + 1);
819
826
  }
820
827
  }
821
- /**
822
- * Standard edit-distance DP, single-row-at-a-time (sizes here are always
823
- * short command-name strings). Builds each row left-to-right, appending as
824
- * it goes, so every index read below is already-populated — the `?? 0`
825
- * fallbacks only satisfy `noUncheckedIndexedAccess`, they never fire.
826
- */
827
- function levenshteinDistance(a, b) {
828
- let previousRow = Array.from({ length: b.length + 1 }, (_, j) => j);
829
- for (let i = 1; i <= a.length; i++) {
830
- const currentRow = [i];
831
- for (let j = 1; j <= b.length; j++) {
832
- const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
833
- const deletion = (previousRow[j] ?? 0) + 1;
834
- const insertion = (currentRow[j - 1] ?? 0) + 1;
835
- const substitution = (previousRow[j - 1] ?? 0) + substitutionCost;
836
- currentRow.push(Math.min(deletion, insertion, substitution));
837
- }
838
- previousRow = currentRow;
839
- }
840
- return previousRow[b.length] ?? 0;
841
- }
842
- /** Closest candidate within a length-scaled distance threshold, or undefined when nothing is close enough to be worth suggesting. */
828
+ /** Closest command within a length-scaled distance threshold (shared DP in cli/unknown-flags.ts). */
843
829
  function closestCommandMatch(attempted, candidates) {
844
- let best;
845
- let bestDistance = Number.POSITIVE_INFINITY;
846
- for (const candidate of candidates) {
847
- const distance = levenshteinDistance(attempted, candidate);
848
- if (distance < bestDistance) {
849
- bestDistance = distance;
850
- best = candidate;
851
- }
852
- }
853
- const threshold = Math.max(2, Math.ceil(attempted.length / 2));
854
- return best !== undefined && bestDistance <= threshold ? best : undefined;
830
+ return closestMatch(attempted, candidates, Math.max(2, Math.ceil(attempted.length / 2)));
855
831
  }
856
832
  const CLI_HELP_POINTER = "Run `akm --help` for usage.";
857
833
  /**
@@ -898,6 +874,10 @@ function toUsageErrorFromCliError(error, rawArgs) {
898
874
  const message = error.message.replace(/\x1b\[[0-9;]*m/g, "");
899
875
  return new UsageError(message, code, hint);
900
876
  }
877
+ /** Normalize citty's private CLIError for the real entrypoint and in-process test harness. */
878
+ export function normalizeCittyCliError(error, rawArgs) {
879
+ return isCittyCliError(error) ? toUsageErrorFromCliError(error, rawArgs) : error;
880
+ }
901
881
  /**
902
882
  * The CLI's real startup sequence, extracted into a function so error paths
903
883
  * can `return` early — top-level `return` is a syntax error in an ES module,
@@ -1016,6 +996,10 @@ async function runCli() {
1016
996
  console.log(pkgVersion);
1017
997
  return;
1018
998
  }
999
+ // citty's parseArgs (strict: false) silently ignores undeclared flags, so
1000
+ // a typo used to run the command with defaults and exit 0. Checked here, after --help and
1001
+ // --version, so those keep working on any command.
1002
+ assertKnownFlags(main, rawArgs);
1019
1003
  await runCommand(main, { rawArgs });
1020
1004
  }
1021
1005
  catch (error) {
@@ -1027,7 +1011,7 @@ async function runCli() {
1027
1011
  // + raw `console.error(message)` — a one-line diagnosis plus a short
1028
1012
  // hint (with a did-you-mean suggestion when applicable), not a ~46KB
1029
1013
  // usage dump.
1030
- emitJsonError(toUsageErrorFromCliError(error, rawArgs));
1014
+ emitJsonError(normalizeCittyCliError(error, rawArgs));
1031
1015
  return;
1032
1016
  }
1033
1017
  // Anything else escaping here is a genuinely unexpected failure outside
@@ -16,6 +16,7 @@
16
16
  import fs from "node:fs";
17
17
  import { parseRefInput } from "../../core/asset/resolve-ref.js";
18
18
  import { NotFoundError, UsageError } from "../../core/errors.js";
19
+ import { NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
19
20
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
20
21
  import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
21
22
  /**
@@ -60,10 +61,13 @@ async function resolveAssetBody(ref) {
60
61
  export async function akmAgentDispatch(options) {
61
62
  if (!options.agentConfig)
62
63
  throw new UsageError("agent requires a valid config with an agent engine.", "MISSING_REQUIRED_ARGUMENT");
63
- const engineName = options.engine ?? options.agentConfig.defaults?.engine;
64
+ // Same implicit opencode-sdk fallback the workflow and task surfaces apply,
65
+ // so an engine-less install is usable everywhere or nowhere — not a mix.
66
+ const { config: agentConfig } = withEngineFallback(options.agentConfig);
67
+ const engineName = options.engine ?? agentConfig.defaults?.engine;
64
68
  if (!engineName)
65
- throw new UsageError("agent requires --engine or defaults.engine.", "MISSING_REQUIRED_ARGUMENT");
66
- const runner = resolveEngine(engineName, options.agentConfig);
69
+ throw new UsageError(`agent ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "MISSING_REQUIRED_ARGUMENT");
70
+ const runner = resolveEngine(engineName, agentConfig);
67
71
  if (runner.kind === "llm") {
68
72
  throw new UsageError(`Engine "${engineName}" is an LLM engine; akm agent requires an agent engine.`, "INVALID_FLAG_VALUE");
69
73
  }
@@ -25,6 +25,7 @@ import { getStringArg, parsePositiveIntFlag } from "../../cli/parse-args.js";
25
25
  import { EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
26
26
  import { loadConfig } from "../../core/config/config.js";
27
27
  import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
28
+ import { getHyphenatedBoolean } from "../../output/context.js";
28
29
  import { akmLint } from "../lint/index.js";
29
30
  import { akmAgentDispatch } from "./agent-dispatch.js";
30
31
  const EXIT_GENERAL = EXIT_CODES.GENERAL;
@@ -132,8 +133,19 @@ export const lintCommand = defineCommand({
132
133
  ...GLOBAL_OUTPUT_ARGS,
133
134
  fix: {
134
135
  type: "boolean",
135
- alias: "auto-fix",
136
- description: "Apply auto-fixes in place (alias: --auto-fix)",
136
+ description: "Apply auto-fixes in place",
137
+ default: false,
138
+ },
139
+ // Declared as its own arg rather than `alias: "auto-fix"`: citty's alias
140
+ // handling (node:util parseArgs `short` options) only supports
141
+ // SINGLE-character aliases. A
142
+ // multi-char alias rendered in help as `-auto-fix` (one dash) and parsed
143
+ // as a pile of junk single-char flags, so BOTH advertised spellings —
144
+ // `-auto-fix` and `--auto-fix` — silently ran a plain lint while claiming
145
+ // to fix. Two real boolean args, OR'd at the call site, work and render.
146
+ "auto-fix": {
147
+ type: "boolean",
148
+ description: "Apply auto-fixes in place (same as --fix)",
137
149
  default: false,
138
150
  },
139
151
  dir: { type: "string", description: "Override bundle root directory (default: from config)" },
@@ -151,7 +163,7 @@ export const lintCommand = defineCommand({
151
163
  async run({ args }) {
152
164
  await runWithJsonErrors(async () => {
153
165
  const result = akmLint({
154
- fix: args.fix ?? false,
166
+ fix: args.fix === true || getHyphenatedBoolean(args, "auto-fix"),
155
167
  dir: getStringArg(args, "dir"),
156
168
  typeFilter: getStringArg(args, "type"),
157
169
  });
@@ -9,7 +9,7 @@ import { parseFrontmatter, parseFrontmatterBlock } from "../core/asset/frontmatt
9
9
  import { conceptIdFromTypeName, parseRefInput } from "../core/asset/resolve-ref.js";
10
10
  import { isWithin, writeFileAtomic } from "../core/common.js";
11
11
  import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config/config.js";
12
- import { UsageError } from "../core/errors.js";
12
+ import { NotFoundError, UsageError } from "../core/errors.js";
13
13
  import { appendEvent } from "../core/events.js";
14
14
  import { resolveMutationTarget } from "../core/mutation-target.js";
15
15
  import { getDbPath } from "../core/paths.js";
@@ -311,8 +311,11 @@ export const feedbackCommand = defineJsonCommand({
311
311
  const lookupRef = makeBundleRef(parsedRef.bundle, parsedRef.conceptId);
312
312
  const entryId = findEntryIdByRef(db, lookupRef, requestedSource?.path);
313
313
  if (entryId === undefined) {
314
- throw new UsageError(`Ref "${ref}" is not in the index. ` +
315
- "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
314
+ // NotFoundError (exit 1), not UsageError (exit 2): the flags parsed
315
+ // fine, the asset just isn't there. The documented exit-code table
316
+ // reserves 1 for "requested resource missing", and scripts branch on it.
317
+ throw new NotFoundError(`Ref "${ref}" is not in the index. ` +
318
+ "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.", "ASSET_NOT_FOUND");
316
319
  }
317
320
  // Persist the feedback signal into usage_events. Both positive and
318
321
  // negative signals apply the EMA utility update immediately — no
@@ -2,8 +2,8 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { spawnSync } from "node:child_process";
5
- import { createRequire } from "node:module";
6
5
  import { loadConfig } from "../../core/config/config.js";
6
+ import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
7
7
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
8
8
  import { resolveModel } from "../../integrations/agent/model-aliases.js";
9
9
  import { resolveImprovePlan } from "../improve/improve-strategies.js";
@@ -28,7 +28,7 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
28
28
  if (configuredEngine?.kind === "agent" && configuredEngine.platform === "opencode-sdk") {
29
29
  let packageAvailable = false;
30
30
  try {
31
- const resolvePackage = deps.resolvePackage ?? ((name) => createRequire(import.meta.url).resolve(name));
31
+ const resolvePackage = deps.resolvePackage ?? ((name) => import.meta.resolve(name));
32
32
  resolvePackage("@opencode-ai/sdk");
33
33
  packageAvailable = true;
34
34
  }
@@ -170,7 +170,10 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
170
170
  }
171
171
  }
172
172
  export function runDefaultEngineProbe(deps = {}) {
173
- const config = deps.loadConfig?.() ?? loadConfig();
173
+ // Probe the effective view: an install with no `defaults.engine` but a usable
174
+ // opencode binary DOES have a working default, and reporting otherwise would
175
+ // contradict what `workflow run` / `task run` actually do.
176
+ const { config } = withEngineFallback(deps.loadConfig?.() ?? loadConfig(), deps.which);
174
177
  return runConfiguredEngineProbe("default-engine", config.defaults?.engine, config, deps);
175
178
  }
176
179
  export function runDefaultLlmEngineProbe(deps = {}) {
@@ -15,9 +15,8 @@
15
15
  * hardcoded 0 from the 0.9.0 confidence-gate deletion onward), so the alert
16
16
  * could never fire. See the design doc's status note for the removal
17
17
  * rationale. `improve_cycle_metrics.accepted_actions` stays in the schema and
18
- * is written as 0: its migration body is released, and 0.8 ships `state.db`,
19
- * so a deployed ledger may already have sealed that body's checksum. Editing
20
- * it would fail those installs closed.
18
+ * is written as 0 because deployed `state.db` files already contain it and
19
+ * removing it would be a destructive schema change.
21
20
  *
22
21
  * Hard invariants: deterministic only (FTS BM25 + hashing — never an LLM,
23
22
  * never an embedding model); bounded storage (< 2 KB per qualifying cycle,
@@ -37,6 +37,7 @@ import { redactSensitiveText } from "../../core/redaction.js";
37
37
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
38
38
  import { lookup } from "../../indexer/indexer.js";
39
39
  import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
40
+ import { NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
40
41
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
41
42
  import { buildReflectOutputRepairPrompt, buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
42
43
  import { materializeLlmRunnerConnection, resolveImproveProcessRunner, runnerIsLlm, runnerSupportsFileWrite, } from "../../integrations/agent/runner.js";
@@ -1185,11 +1186,12 @@ function resolveReflectRunner(options) {
1185
1186
  runnerSpec = processRunner;
1186
1187
  }
1187
1188
  else {
1188
- const defaultEngine = config.defaults?.engine;
1189
+ const { config: engineConfig } = withEngineFallback(config);
1190
+ const defaultEngine = engineConfig.defaults?.engine;
1189
1191
  if (!defaultEngine) {
1190
- throw new ConfigError("reflect requires --engine or defaults.engine.", "INVALID_CONFIG_FILE");
1192
+ throw new ConfigError(`reflect ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "INVALID_CONFIG_FILE");
1191
1193
  }
1192
- runnerSpec = resolveEngine(defaultEngine, config);
1194
+ runnerSpec = resolveEngine(defaultEngine, engineConfig);
1193
1195
  }
1194
1196
  if (options.eventSource === "improve" && !runnerIsLlm(runnerSpec)) {
1195
1197
  throw new ConfigError(`Unattended improve requires an LLM engine for reflect; engine "${runnerSpec.engine ?? options.engine ?? "unknown"}" is tool-capable.`, "INVALID_CONFIG_FILE", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine to an LLM engine.");
@@ -35,15 +35,11 @@ import fs from "node:fs";
35
35
  import path from "node:path";
36
36
  import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
37
37
  import { BUNDLE_REF_RE } from "../../core/asset/asset-ref.js";
38
+ import { spliceFrontmatterLine } from "../../core/asset/frontmatter.js";
38
39
  import { typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
40
+ import { localDateStamp } from "../../core/common.js";
39
41
  import { findFenceRegions } from "./markdown-insertion.js";
40
42
  // ── Helpers ───────────────────────────────────────────────────────────────────
41
- function formatDate(d) {
42
- const y = d.getFullYear();
43
- const m = String(d.getMonth() + 1).padStart(2, "0");
44
- const day = String(d.getDate()).padStart(2, "0");
45
- return `${y}-${m}-${day}`;
46
- }
47
43
  function checkUnquotedColon(frontmatterText) {
48
44
  if (!frontmatterText)
49
45
  return null;
@@ -86,15 +82,7 @@ function checkMissingUpdated(data, frontmatterText) {
86
82
  return frontmatterText !== null && !("updated" in data);
87
83
  }
88
84
  function fixMissingUpdated(raw, mtime) {
89
- const dateStr = formatDate(mtime);
90
- const lines = raw.split(/\r?\n/);
91
- if (lines[0]?.trim() !== "---")
92
- return raw;
93
- const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
94
- if (closeIdx === -1)
95
- return raw;
96
- lines.splice(closeIdx, 0, `updated: ${dateStr}`);
97
- return lines.join("\n");
85
+ return spliceFrontmatterLine(raw, `updated: ${localDateStamp(mtime)}`) ?? raw;
98
86
  }
99
87
  // ── stale-path helpers ────────────────────────────────────────────────────────
100
88
  function checkStalePath(body) {
@@ -529,7 +517,7 @@ export function runBaseChecks(ctx) {
529
517
  issues.push({
530
518
  file: ctx.relPath,
531
519
  issue: "missing-updated",
532
- detail: `stamped updated: ${formatDate(mtime)}`,
520
+ detail: `stamped updated: ${localDateStamp(mtime)}`,
533
521
  fixed: true,
534
522
  });
535
523
  }
@@ -6,7 +6,9 @@ import path from "node:path";
6
6
  import { parse as parseYaml } from "yaml";
7
7
  import { factDiagnostics, matchWorkflowPlaceholder, memoryOrphanStubApplies, nameOrTypeDiagnostics, ORPHANED_STUB_DETAIL, taskDiagnostics, workflowStructureDiagnostics, } from "../../core/adapter/adapters/akm-lint.js";
8
8
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
9
+ import { stashDirFor } from "../../core/asset/asset-placement.js";
9
10
  import { parseFrontmatter, parseFrontmatterBlock } from "../../core/asset/frontmatter.js";
11
+ import { conceptIdForStashFile, displayRefForConceptId } from "../../core/asset/resolve-ref.js";
10
12
  import { resolveStashDir } from "../../core/common.js";
11
13
  import { loadConfig, primaryBundlePath } from "../../core/config/config.js";
12
14
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
@@ -321,23 +323,21 @@ export function akmLint(options = {}) {
321
323
  // keys that are known to enable process-execution hijacking. Warn-only —
322
324
  // findings go into `flagged`, never `fixed`.
323
325
  const envRoots = [stashRoot, ...extraStashRoots];
326
+ const bundleIdByRoot = new Map(sources.map((source) => [path.resolve(source.path), source.registryId]));
324
327
  for (const root of envRoots) {
325
- // The `env` assets live under `env/` (ref prefix `env:`); whole-file
326
- // `secret` assets live under `secrets/` (canonical ref prefix `secret:`,
327
- // singular). Map the scan directory to its canonical ref prefix so the
328
- // finding's `Ref:` field matches what `akm show`/`akm secret` accept.
329
- for (const { scanSubdir, refPrefix } of [
330
- { scanSubdir: "env", refPrefix: "env" },
331
- { scanSubdir: "secrets", refPrefix: "secret" },
332
- ]) {
333
- const dir = path.join(root, scanSubdir);
328
+ const bundleId = bundleIdByRoot.get(path.resolve(root));
329
+ // `env` assets live under `env/`, whole-file `secret` assets under
330
+ // `secrets/`. `displayRefForConceptId` owns the short-default /
331
+ // qualified-secondary `Ref:` spelling `akm show` emits — the old
332
+ // hand-built `env:<base>` colon grammar is rejected by the 0.9.0 ref
333
+ // parser, which dead-ended a user copying the ref off a security finding.
334
+ for (const assetType of ["env", "secret"]) {
335
+ const dir = path.join(root, stashDirFor(assetType));
334
336
  if (!fs.existsSync(dir))
335
337
  continue;
336
338
  for (const envPath of collectEnvFiles(dir)) {
337
- const baseName = path.basename(envPath, ".env");
338
- // A dotfile literally named `.env` has an empty baseName — use the full
339
- // basename so it doesn't collide with `default.env` → refPrefix:default.
340
- const ref = baseName === "" ? `${refPrefix}:.env` : `${refPrefix}:${baseName}`;
339
+ const conceptId = conceptIdForStashFile(assetType, root, envPath);
340
+ const ref = displayRefForConceptId(conceptId, bundleId, cfg.defaultBundle);
341
341
  const relPath = path.relative(root, envPath);
342
342
  for (const issue of checkEnvForDangerousKeys(envPath, relPath, ref)) {
343
343
  flagged.push(issue);
@@ -16,6 +16,7 @@
16
16
  * other caller and was removed with it.
17
17
  */
18
18
  import { makeBundleRef, parseBundleRef } from "../core/asset/asset-ref.js";
19
+ import { loadConfig } from "../core/config/config.js";
19
20
  import { UsageError } from "../core/errors.js";
20
21
  import { readEvents } from "../core/events.js";
21
22
  import { parseSinceToIso } from "../core/time.js";
@@ -49,7 +50,11 @@ function validateRef(ref) {
49
50
  throw new UsageError("--ref cannot be empty.", "INVALID_FLAG_VALUE");
50
51
  }
51
52
  const parsed = parseBundleRef(trimmed);
52
- return makeBundleRef(parsed.bundle, parsed.conceptId);
53
+ // Deliberately shallow qualification: a bare conceptId is assumed to mean
54
+ // the default bundle, without the full priority-walk `resolveRef` performs —
55
+ // event refs are recorded fully qualified, so the default is the only
56
+ // spelling a short --ref can usefully mean here.
57
+ return makeBundleRef(parsed.bundle ?? loadConfig().defaultBundle, parsed.conceptId);
53
58
  }
54
59
  function validateRunId(run) {
55
60
  if (run === undefined)
@@ -7,8 +7,8 @@ import { fileURLToPath } from "node:url";
7
7
  import { NotFoundError } from "../core/errors.js";
8
8
  function migrationEntryPoint() {
9
9
  const candidates = [
10
- fileURLToPath(new URL("../../scripts/akm-migrate.ts", import.meta.url)),
11
- fileURLToPath(new URL("../scripts/akm-migrate.js", import.meta.url)),
10
+ fileURLToPath(new URL(process.versions.bun ? "../scripts/akm-migrate.js" : "../scripts/akm-migrate-node.js", import.meta.url)),
11
+ ...(process.versions.bun ? [fileURLToPath(new URL("../../scripts/akm-migrate.ts", import.meta.url))] : []),
12
12
  ];
13
13
  return candidates.find((candidate) => fs.existsSync(candidate));
14
14
  }
@@ -27,14 +27,13 @@ export async function runMigrationTool(args) {
27
27
  // `scripts/akm-standalone.ts`, which embeds the migrator and dispatches to
28
28
  // it when `AKM_MIGRATE_ENTRY=1` — so re-exec ourselves with the marker.
29
29
  // (src must not import scripts/: the dist build's tsc has `rootDir: src`.)
30
- const runtime = process.versions.bun ? process.execPath : "bun";
31
- const result = spawnSync(runtime, entry ? [entry, ...args] : [...args], {
30
+ const result = spawnSync(process.execPath, entry ? [entry, ...args] : [...args], {
32
31
  encoding: "utf8",
33
32
  env: entry ? process.env : { ...process.env, AKM_MIGRATE_ENTRY: "1" },
34
33
  maxBuffer: 16 * 1024 * 1024,
35
34
  });
36
35
  if (result.error) {
37
- throw new NotFoundError(`Cannot start the standalone akm-migrate tool: ${result.error.message}`, "FILE_NOT_FOUND", "Install Bun, then run akm-migrate directly.");
36
+ throw new NotFoundError(`Cannot start the standalone akm-migrate tool: ${result.error.message}`, "FILE_NOT_FOUND", "Reinstall akm-cli, or use a runtime-free standalone release binary.");
38
37
  }
39
38
  if (result.stdout)
40
39
  process.stdout.write(result.stdout);
@@ -72,7 +72,7 @@ export const logCommand = defineJsonCommand({
72
72
  ref: { type: "string", description: "Filter by asset ref ([bundle//]conceptId)" },
73
73
  run: {
74
74
  type: "string",
75
- description: "Filter to a workflow run's events (metadata.runId), e.g. the id from `akm workflow start`",
75
+ description: "Filter to a workflow run's events (metadata.runId), e.g. the id from `akm workflow run`",
76
76
  },
77
77
  "exclude-tags": {
78
78
  type: "string",
@@ -21,6 +21,7 @@ import { appendEvent } from "../../core/events.js";
21
21
  import { redactSensitiveText } from "../../core/redaction.js";
22
22
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
23
23
  import { deriveEntryProvenance } from "../../indexer/installations.js";
24
+ import { NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
24
25
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
25
26
  import { buildProposePrompt, parseAgentProposalPayload } from "../../integrations/agent/prompts.js";
26
27
  import { collectDispatchSensitiveValues, executeRunner } from "../../integrations/agent/runner-dispatch.js";
@@ -83,10 +84,11 @@ export async function akmPropose(options) {
83
84
  const config = options.agentConfig ?? (await import("../../core/config/config.js")).loadConfig();
84
85
  const target = resolveProposalQueueTarget(stash, config);
85
86
  emitProposeInvoked(target.source, options);
86
- const engineName = options.engine ?? config.defaults?.engine;
87
+ const { config: engineConfig } = withEngineFallback(config);
88
+ const engineName = options.engine ?? engineConfig.defaults?.engine;
87
89
  if (!engineName)
88
- throw new ConfigError("propose requires --engine or defaults.engine.", "INVALID_CONFIG_FILE");
89
- const runner = resolveEngine(engineName, config);
90
+ throw new ConfigError(`propose ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "INVALID_CONFIG_FILE");
91
+ const runner = resolveEngine(engineName, engineConfig);
90
92
  const profile = runner.kind === "llm" ? undefined : runner.profile;
91
93
  // 3. Build prompt.
92
94
  // Synthesize a temp draft path so opencode can write the asset content
@@ -168,7 +168,7 @@ function persistProposalUpdate(db, proposal, stashDir) {
168
168
  .prepare("SELECT metadata_json FROM proposals WHERE id = ? AND stash_dir = ?")
169
169
  .get(proposal.id, stashDir);
170
170
  if (!row)
171
- throw new NotFoundError(`Proposal "${proposal.id}" not found.`, "FILE_NOT_FOUND");
171
+ throw new NotFoundError(`Proposal "${proposal.id}" not found.`, "PROPOSAL_NOT_FOUND");
172
172
  const metadata = JSON.parse(row.metadata_json);
173
173
  for (const field of [
174
174
  "sourceRun",
@@ -572,7 +572,7 @@ export function getProposal(stashDir, id, ctx) {
572
572
  function requireProposal(db, stashDir, id) {
573
573
  const proposal = getStateProposal(db, id, stashDir);
574
574
  if (!proposal) {
575
- throw new NotFoundError(`Proposal "${id}" not found.`, "FILE_NOT_FOUND");
575
+ throw new NotFoundError(`Proposal "${id}" not found.`, "PROPOSAL_NOT_FOUND");
576
576
  }
577
577
  return proposal;
578
578
  }
@@ -588,7 +588,7 @@ function requireProposal(db, stashDir, id) {
588
588
  function requireProposalLenient(db, stashDir, id) {
589
589
  const proposal = getStateProposalLenient(db, id, stashDir);
590
590
  if (!proposal) {
591
- throw new NotFoundError(`Proposal "${id}" not found.`, "FILE_NOT_FOUND");
591
+ throw new NotFoundError(`Proposal "${id}" not found.`, "PROPOSAL_NOT_FOUND");
592
592
  }
593
593
  return proposal;
594
594
  }
@@ -623,7 +623,7 @@ export function resolveProposalId(stashDir, idOrRef, ctx) {
623
623
  const archived = byRecency(forConcept());
624
624
  if (archived)
625
625
  return archived;
626
- throw new NotFoundError(`No proposal found for ref "${idOrRef}".`, "FILE_NOT_FOUND");
626
+ throw new NotFoundError(`No proposal found for ref "${idOrRef}".`, "PROPOSAL_NOT_FOUND");
627
627
  }
628
628
  // 3. UUID prefix (pending queue only).
629
629
  const prefixMatches = listStateProposalIdsByPrefix(db, stashDir, idOrRef);
@@ -632,7 +632,7 @@ export function resolveProposalId(stashDir, idOrRef, ctx) {
632
632
  if (prefixMatches.length > 1) {
633
633
  throw new UsageError(`Ambiguous prefix "${idOrRef}" — matches: ${prefixMatches.join(", ")}`, "INVALID_FLAG_VALUE");
634
634
  }
635
- throw new NotFoundError(`Proposal "${idOrRef}" not found.`, "FILE_NOT_FOUND");
635
+ throw new NotFoundError(`Proposal "${idOrRef}" not found.`, "PROPOSAL_NOT_FOUND");
636
636
  });
637
637
  }
638
638
  /**
@@ -27,6 +27,7 @@ import { commitWriteTargetBoundary, formatRefForMessage, recordWriteTargetPath,
27
27
  import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
28
28
  import { deriveInstallations, slugForPath } from "../../indexer/installations.js";
29
29
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
30
+ import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
30
31
  import { fetchWebsiteMarkdownSnapshot, shouldAllowPrivateWebsiteUrlForTests, } from "../../sources/snapshot-fetchers/website-ingest.js";
31
32
  import { writeSupersededEdge } from "../improve/memory/memory-belief.js";
32
33
  import { refToRelPath, resolveRefPathInStash } from "../lint/base-linter.js";
@@ -119,6 +120,7 @@ export async function readKnowledgeInput(source, options) {
119
120
  return readKnowledgeContent(source);
120
121
  const snapshot = await fetchWebsiteMarkdownSnapshot(source, {
121
122
  stashDir: options?.stashDir,
123
+ resolveSecret: storeSecretResolver.resolveSecret,
122
124
  allowPrivateHosts: options?.allowPrivateHosts ?? shouldAllowPrivateWebsiteUrlForTests(source),
123
125
  });
124
126
  return { content: snapshot.content, preferredName: snapshot.preferredName };
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
5
5
  import { getEffectiveRegistries, loadUserConfig, mutateConfig } from "../core/config/config.js";
6
- import { UsageError } from "../core/errors.js";
6
+ import { NotFoundError, UsageError } from "../core/errors.js";
7
7
  import { warn } from "../core/warn.js";
8
8
  export const registryCommand = defineGroupCommand({
9
9
  meta: { name: "registry", description: "Manage bundle registries" },
@@ -82,8 +82,10 @@ export const registryCommand = defineGroupCommand({
82
82
  const registries = [...(config.registries ?? [])];
83
83
  const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
84
84
  if (idx === -1) {
85
- output("registry-remove", { registries, removed: false, message: "No matching registry found" });
86
- return;
85
+ // Was a success envelope with `removed: false` and exit 0, so
86
+ // `akm registry remove typo && ...` proceeded as if it had removed
87
+ // something. A missing target is exit 1, like every other not-found.
88
+ throw new NotFoundError(`No registry matching "${args.target}" is configured.`, "SOURCE_NOT_FOUND", "Run `akm registry list` to see configured registries.");
87
89
  }
88
90
  const { confirmDestructive } = await import("../cli/confirm.js");
89
91
  const confirmed = await confirmDestructive(`Remove registry "${args.target}"? This cannot be undone.`, {
@@ -4,6 +4,7 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import * as p from "../../cli/clack.js";
7
+ import { getStringArg, parsePositiveIntFlag } from "../../cli/parse-args.js";
7
8
  import { defineJsonCommand, output } from "../../cli/shared.js";
8
9
  import { decideDangerousKeyInstall } from "../../core/activation-policy.js";
9
10
  import { UsageError } from "../../core/errors.js";
@@ -14,12 +15,11 @@ import { akmAdd } from "./source-add.js";
14
15
  import { addStash } from "./source-manage.js";
15
16
  // ── Shared website-options helper ──────────
16
17
  export function buildWebsiteOptions(args) {
17
- const websiteOptions = {};
18
- if (typeof args["max-pages"] === "string" && args["max-pages"].length > 0)
19
- websiteOptions.maxPages = args["max-pages"];
20
- if (typeof args["max-depth"] === "string" && args["max-depth"].length > 0)
21
- websiteOptions.maxDepth = args["max-depth"];
22
- return websiteOptions;
18
+ // getStringArg maps absent/blank to undefined; parsePositiveIntFlag maps
19
+ // undefined to undefined — so an unsupplied flag simply omits the key.
20
+ const maxPages = parsePositiveIntFlag(getStringArg(args, "max-pages"), "--max-pages");
21
+ const maxDepth = parsePositiveIntFlag(getStringArg(args, "max-depth"), "--max-depth");
22
+ return { ...(maxPages !== undefined ? { maxPages } : {}), ...(maxDepth !== undefined ? { maxDepth } : {}) };
23
23
  }
24
24
  // ── HTTP safety check ─────────────────────────────────────────────────────────
25
25
  export function shouldWarnOnPlainHttp(ref) {
@@ -87,7 +87,7 @@ async function akmInitReal(options) {
87
87
  for (const sub of stashDirNames()) {
88
88
  const subDir = path.join(stashDir, sub);
89
89
  if (!fs.existsSync(subDir)) {
90
- fs.mkdirSync(subDir, { recursive: true });
90
+ fs.mkdirSync(subDir, { recursive: true, mode: sub === "env" || sub === "secrets" ? 0o700 : undefined });
91
91
  }
92
92
  }
93
93
  // Ensure the default stash is a local git repo (no remote required)