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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +117 -23
  2. package/dist/akm-migrate +3 -1
  3. package/dist/assets/hints/cli-hints-full.md +3 -4
  4. package/dist/assets/hints/cli-hints-short.md +5 -5
  5. package/dist/assets/workflows/workflow-template.md +4 -3
  6. package/dist/cli/invocation.js +3 -2
  7. package/dist/cli/retired-commands.js +3 -0
  8. package/dist/cli/unknown-flags.js +226 -0
  9. package/dist/cli.js +19 -35
  10. package/dist/commands/agent/contribute-cli.js +15 -3
  11. package/dist/commands/feedback-cli.js +6 -3
  12. package/dist/commands/improve/collapse-detector.js +2 -3
  13. package/dist/commands/lint/base-linter.js +4 -16
  14. package/dist/commands/lint/index.js +13 -13
  15. package/dist/commands/log.js +6 -1
  16. package/dist/commands/migration-tool.js +4 -5
  17. package/dist/commands/observability-cli.js +1 -1
  18. package/dist/commands/proposal/repository.js +5 -5
  19. package/dist/commands/read/knowledge.js +2 -0
  20. package/dist/commands/registry-cli.js +5 -3
  21. package/dist/commands/sources/add-cli.js +6 -6
  22. package/dist/commands/sources/self-update.js +30 -7
  23. package/dist/commands/sources/source-add.js +17 -2
  24. package/dist/commands/tasks/tasks.js +8 -3
  25. package/dist/commands/workflow-cli.js +142 -119
  26. package/dist/core/adapter/adapters/akm-lint.js +17 -13
  27. package/dist/core/adapter/adapters/akm-task-adapter.js +14 -11
  28. package/dist/core/asset/akm-markdown.js +41 -8
  29. package/dist/core/asset/frontmatter.js +22 -0
  30. package/dist/core/asset/resolve-ref.js +23 -3
  31. package/dist/core/common.js +45 -2
  32. package/dist/core/config/config-schema.js +8 -0
  33. package/dist/core/config/schema/experimental.js +5 -13
  34. package/dist/core/config/schema/sources-bundles.js +11 -0
  35. package/dist/core/config/schema/workflow.js +3 -1
  36. package/dist/core/errors.js +5 -0
  37. package/dist/core/logs-db.js +2 -1
  38. package/dist/core/parse.js +4 -1
  39. package/dist/core/state/migrations.js +11 -14
  40. package/dist/core/state-db.js +4 -6
  41. package/dist/core/subprocess.js +6 -4
  42. package/dist/core/type-presentation.js +1 -1
  43. package/dist/indexer/indexer.js +16 -1
  44. package/dist/indexer/search/search-source.js +1 -1
  45. package/dist/indexer/walk/matchers.js +3 -1
  46. package/dist/integrations/agent/config.js +2 -2
  47. package/dist/integrations/agent/detect.js +49 -19
  48. package/dist/integrations/agent/profiles.js +10 -0
  49. package/dist/integrations/agent/spawn.js +1 -2
  50. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +7 -3
  51. package/dist/integrations/lockfile.js +11 -56
  52. package/dist/output/shapes/passthrough.js +0 -4
  53. package/dist/output/text/command-format.js +3 -3
  54. package/dist/output/text/helpers.js +1 -1
  55. package/dist/output/text/show-directives.js +4 -6
  56. package/dist/output/text/workflow-format.js +7 -62
  57. package/dist/output/text/workflow.js +1 -5
  58. package/dist/scripts/akm-migrate-node.js +58773 -0
  59. package/dist/scripts/akm-migrate.js +33976 -11391
  60. package/dist/setup/detect.js +40 -15
  61. package/dist/setup/setup.js +1 -1
  62. package/dist/sources/providers/git-stash.js +4 -2
  63. package/dist/sources/providers/website.js +5 -0
  64. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  65. package/dist/sources/snapshot-fetchers/content-extract.js +370 -0
  66. package/dist/sources/snapshot-fetchers/fetcher-util.js +40 -0
  67. package/dist/sources/snapshot-fetchers/host-guard.js +199 -0
  68. package/dist/sources/snapshot-fetchers/registry.js +10 -1
  69. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  70. package/dist/sources/snapshot-fetchers/rss.js +279 -0
  71. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  72. package/dist/sources/snapshot-fetchers/website-ingest.js +488 -257
  73. package/dist/sources/snapshot-fetchers/x.js +193 -0
  74. package/dist/storage/engines/sqlite-migrations.js +22 -107
  75. package/dist/tasks/runner.js +19 -16
  76. package/dist/tasks/schema.js +24 -1
  77. package/dist/workflows/exec/brief.js +1 -1
  78. package/dist/workflows/exec/frozen-judge.js +28 -2
  79. package/dist/workflows/exec/native-executor.js +18 -1
  80. package/dist/workflows/exec/report.js +11 -4
  81. package/dist/workflows/exec/run-workflow.js +103 -44
  82. package/dist/workflows/exec/step-work.js +19 -18
  83. package/dist/workflows/exec/unit-dispatch.js +4 -0
  84. package/dist/workflows/exec/workflow-engine-gate.js +11 -13
  85. package/dist/workflows/ir/compile.js +2 -2
  86. package/dist/workflows/ir/freeze.js +16 -8
  87. package/dist/workflows/ir/params.js +134 -10
  88. package/dist/workflows/ir/plan-hash.js +1 -1
  89. package/dist/workflows/ir/schema.js +6 -2
  90. package/dist/workflows/renderer.js +2 -2
  91. package/dist/workflows/runtime/checkin.js +3 -3
  92. package/dist/workflows/runtime/runs.js +50 -29
  93. package/dist/workflows/validate-summary.js +30 -14
  94. package/docs/migration/release-notes/0.9.0.md +31 -4
  95. package/docs/migration/v0.8-to-v0.9.md +69 -100
  96. package/docs/reference/data-and-telemetry.md +5 -5
  97. package/package.json +6 -2
  98. package/schemas/akm-config.json +24 -0
  99. package/schemas/akm-workflow.json +1 -1
  100. package/dist/workflows/cli.js +0 -33
@@ -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
@@ -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,
@@ -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",
@@ -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) {
@@ -5,7 +5,7 @@ import * as childProcess from "node:child_process";
5
5
  import { createHash } from "node:crypto";
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
- import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap } from "../../core/common.js";
8
+ import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap, readChunkWithDeadline, } from "../../core/common.js";
9
9
  import { ConfigError } from "../../core/errors.js";
10
10
  import { warn } from "../../core/warn.js";
11
11
  import { githubHeaders } from "../../integrations/github.js";
@@ -29,7 +29,19 @@ export function defaultMigrationCommand(preparedConfigPath) {
29
29
  apply: (akmBin) => runRequiredCommand(akmBin, ["migrate", "apply", ...configArgs], "Migration apply"),
30
30
  };
31
31
  }
32
- async function streamResponseToFile(response, destination, maxBytes) {
32
+ /**
33
+ * Bounds on the binary body read. `fetchWithTimeout`'s timer only covers
34
+ * time-to-HEADERS — it is cleared the moment `fetch` resolves — so without
35
+ * these a compromised or misconfigured endpoint could dribble bytes forever
36
+ * and hang `akm upgrade` with no output and no way out but Ctrl-C. Two
37
+ * bounds, because either alone is evadable: a per-chunk STALL deadline (a
38
+ * connection making no progress dies quickly, while a slow-but-progressing
39
+ * one is left alone) and an OVERALL deadline (a trickle that sends one byte
40
+ * per stall-window can't stretch the download indefinitely).
41
+ */
42
+ const BINARY_STALL_TIMEOUT_MS = 60_000;
43
+ const BINARY_TOTAL_TIMEOUT_MS = 30 * 60_000;
44
+ export async function streamResponseToFile(response, destination, maxBytes, limits) {
33
45
  const declaredText = response.headers.get("content-length");
34
46
  if (declaredText) {
35
47
  const declared = Number(declaredText);
@@ -40,22 +52,30 @@ async function streamResponseToFile(response, destination, maxBytes) {
40
52
  }
41
53
  if (!response.body)
42
54
  throw new Error("Binary download response did not provide a streaming body.");
55
+ const url = response.url || "binary download";
56
+ const stallTimeoutMs = limits?.stallTimeoutMs ?? BINARY_STALL_TIMEOUT_MS;
57
+ const totalTimeoutMs = limits?.totalTimeoutMs ?? BINARY_TOTAL_TIMEOUT_MS;
58
+ const overallDeadlineAt = Date.now() + totalTimeoutMs;
43
59
  const fd = fs.openSync(destination, "w", 0o600);
44
60
  const reader = response.body.getReader();
45
61
  const hash = createHash("sha256");
46
62
  let byteSize = 0;
47
63
  try {
48
64
  while (true) {
49
- const { done, value } = await reader.read();
65
+ // Whichever bound bites first wins, and the error reports THAT bound —
66
+ // "no data for 60s" and "download exceeded 30m" are different diagnoses.
67
+ const stallDeadlineAt = Date.now() + stallTimeoutMs;
68
+ const deadlineAt = Math.min(stallDeadlineAt, overallDeadlineAt);
69
+ const reportedTimeoutMs = deadlineAt === overallDeadlineAt ? totalTimeoutMs : stallTimeoutMs;
70
+ const { done, value } = await readChunkWithDeadline(reader, deadlineAt, undefined, url, reportedTimeoutMs);
50
71
  if (done)
51
72
  break;
52
73
  if (!value)
53
74
  continue;
54
75
  byteSize += value.byteLength;
55
- if (byteSize > maxBytes) {
56
- await reader.cancel().catch(() => undefined);
57
- throw new ResponseTooLargeError(response.url || "binary download", maxBytes, byteSize);
58
- }
76
+ // The catch block cancels the reader for every throw path, this included.
77
+ if (byteSize > maxBytes)
78
+ throw new ResponseTooLargeError(url, maxBytes, byteSize);
59
79
  hash.update(value);
60
80
  let written = 0;
61
81
  while (written < value.byteLength)
@@ -65,6 +85,9 @@ async function streamResponseToFile(response, destination, maxBytes) {
65
85
  return { byteSize, sha256: hash.digest("hex") };
66
86
  }
67
87
  catch (error) {
88
+ // Cancel so a timed-out (still-open) connection releases its socket rather
89
+ // than lingering until process exit. Already-cancelled readers no-op.
90
+ await reader.cancel().catch(() => undefined);
68
91
  removeFileBestEffort(destination);
69
92
  throw error;
70
93
  }
@@ -14,6 +14,7 @@ import { readLockfile, removeLockEntry, upsertLockEntry } from "../../integratio
14
14
  import { parseRegistryRef } from "../../registry/resolve.js";
15
15
  import { detectStashRoot } from "../../sources/providers/provider-utils.js";
16
16
  import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
17
+ import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
17
18
  import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests, validateWebsiteInputUrl, } from "../../sources/snapshot-fetchers/website-ingest.js";
18
19
  import { bundleKeyForPath, bundleKeyForUrl, nextBundleKey } from "./bundle-config-ops.js";
19
20
  export async function akmAdd(input) {
@@ -95,13 +96,26 @@ async function addLocalSource(ref, sourcePath, stashDir, explicitName) {
95
96
  async function addWebsiteSource(ref, stashDir, name, options) {
96
97
  const allowPrivateHosts = shouldAllowPrivateWebsiteUrlForTests(ref);
97
98
  const normalizedUrl = validateWebsiteInputUrl(ref, { allowPrivateHosts });
98
- const maxPages = typeof options?.maxPages === "number" ? options.maxPages : undefined;
99
+ const numberOption = (value) => (typeof value === "number" ? value : undefined);
100
+ const maxPages = numberOption(options?.maxPages);
101
+ const maxDepth = numberOption(options?.maxDepth);
99
102
  let entry;
100
103
  mutateConfig((config) => {
101
104
  const bundles = { ...(config.bundles ?? {}) };
102
105
  const existingKey = bundleKeyForUrl(config, normalizedUrl);
103
106
  const key = existingKey ?? nextBundleKey(bundles, name ?? toWebsiteName(normalizedUrl), normalizedUrl);
104
- const website = { url: normalizedUrl, ...(maxPages !== undefined ? { maxPages } : {}) };
107
+ // Merge onto the existing descriptor rather than replacing it: re-running
108
+ // `bundle add` for a URL that already has a bundle would otherwise drop
109
+ // respectRobots / refresh, silently restoring default robots enforcement
110
+ // and changing what the next update fetches. Explicitly-passed maxPages /
111
+ // maxDepth still win over the stored values.
112
+ const existingWebsite = existingKey ? (bundles[key]?.website ?? {}) : {};
113
+ const website = {
114
+ ...existingWebsite,
115
+ url: normalizedUrl,
116
+ ...(maxPages !== undefined ? { maxPages } : {}),
117
+ ...(maxDepth !== undefined ? { maxDepth } : {}),
118
+ };
105
119
  const nextBundle = {
106
120
  ...(existingKey ? bundles[key] : {}),
107
121
  website,
@@ -117,6 +131,7 @@ async function addWebsiteSource(ref, stashDir, name, options) {
117
131
  });
118
132
  const cachePaths = await ensureWebsiteMirror(entry, {
119
133
  requireStashDir: true,
134
+ resolveSecret: storeSecretResolver.resolveSecret,
120
135
  ...(allowPrivateHosts ? { allowPrivateHosts: true } : {}),
121
136
  });
122
137
  const index = await akmIndex({ stashDir });
@@ -60,7 +60,12 @@ export async function akmTasksAdd(input, deps = {}) {
60
60
  if (!isWithin(assetPath, typeRoot)) {
61
61
  throw new UsageError(`Resolved task path escapes the stash: "${id}".`, "PATH_ESCAPE_VIOLATION");
62
62
  }
63
- if (fs.existsSync(assetPath) && !input.force) {
63
+ // Pre-0.8.0 tasks were markdown; the 0.8.0 cutover moved them to pure YAML
64
+ // (see the tasks dir rule in src/indexer/walk/matchers.ts). A leftover
65
+ // `<id>.md` still names the same task, so creating `<id>.yml` beside it
66
+ // must collide loudly rather than silently minting a duplicate.
67
+ const legacyAssetPath = path.join(typeRoot, `${id}.md`);
68
+ if ((fs.existsSync(assetPath) || fs.existsSync(legacyAssetPath)) && !input.force) {
64
69
  throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or delete its file and run \`akm task sync\` first.`, "RESOURCE_ALREADY_EXISTS");
65
70
  }
66
71
  const yaml = renderTaskYaml({
@@ -408,8 +413,8 @@ export async function akmTasksDoctor(deps = {}) {
408
413
  configKey: IMPROVE_AUTONOMY_CONFIG_KEY,
409
414
  gatedLanes: allGated.map((entry) => ({ lane: entry.lane, reason: entry.reason })),
410
415
  };
411
- // Q-05 — report the workflow engine gate's state alongside the autonomy
412
- // gate: both are `experimental.*` opt-ins doctor exists to surface.
416
+ // Q-05 — report the external-driver gate alongside the autonomy gate: both
417
+ // are `experimental.*` opt-ins doctor exists to surface.
413
418
  const workflowEngine = {
414
419
  enabled: isWorkflowEngineEnabled(config),
415
420
  configKey: WORKFLOW_ENGINE_CONFIG_KEY,