akm-cli 0.10.261015-beta → 0.10.261017-beta

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 (124) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +8 -10
  3. package/SECURITY.md +7 -99
  4. package/STABILITY.md +58 -98
  5. package/dist/assets/hints/cli-hints-full.md +6 -4
  6. package/dist/assets/hints/cli-hints-short.md +4 -2
  7. package/dist/assets/opencode/akm-plugin.js +16 -8
  8. package/dist/assets/prompts/workflow-unit-preamble.md +3 -1
  9. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
  10. package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
  11. package/dist/cli.js +32 -41
  12. package/dist/commands/feedback-cli.js +5 -4
  13. package/dist/commands/health/plugin-staleness.js +86 -22
  14. package/dist/commands/health.js +15 -16
  15. package/dist/commands/proposal/proposal-cli.js +1 -1
  16. package/dist/commands/read/registry-search.js +4 -1
  17. package/dist/commands/read/search-cli.js +2 -2
  18. package/dist/commands/read/search.js +6 -6
  19. package/dist/commands/read/show.js +15 -3
  20. package/dist/commands/sources/self-update.js +9 -5
  21. package/dist/commands/tasks/tasks.js +12 -13
  22. package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
  23. package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
  24. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  25. package/dist/core/adapter/adapters/claude-adapter.js +1 -1
  26. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  27. package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
  28. package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
  29. package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
  30. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
  31. package/dist/core/asset/markdown.js +9 -0
  32. package/dist/core/bundle-rename.js +17 -7
  33. package/dist/core/config/config-walker.js +35 -32
  34. package/dist/core/config/schema/connections.js +6 -3
  35. package/dist/core/config/schema/harnesses.js +8 -1
  36. package/dist/core/config/schema/primitives.js +12 -1
  37. package/dist/core/config/schema/sources-bundles.js +2 -2
  38. package/dist/core/npm-package.js +48 -13
  39. package/dist/indexer/bundle-identity-guard.js +6 -6
  40. package/dist/indexer/index-written-assets.js +3 -3
  41. package/dist/indexer/indexer.js +12 -8
  42. package/dist/indexer/materialize-embeddings.js +12 -2
  43. package/dist/indexer/passes/metadata.js +103 -46
  44. package/dist/indexer/search/ranking.js +7 -3
  45. package/dist/indexer/search/search-fields.js +1 -1
  46. package/dist/indexer/usage/usage-events.js +30 -8
  47. package/dist/indexer/walk/walker.js +1 -1
  48. package/dist/output/renderers.js +2 -2
  49. package/dist/output/shapes/helpers.js +5 -3
  50. package/dist/output/text/command-format.js +5 -6
  51. package/dist/output/text/show-format.js +7 -0
  52. package/dist/registry/semver.js +12 -0
  53. package/dist/scripts/akm-migrate-node.js +21653 -5734
  54. package/dist/scripts/akm-migrate.js +21661 -5742
  55. package/dist/sources/providers/git-provider.js +2 -4
  56. package/dist/sources/providers/provider-utils.js +8 -13
  57. package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
  58. package/dist/storage/repositories/events-repository.js +15 -0
  59. package/dist/storage/repositories/improve-ledger-repository.js +13 -0
  60. package/dist/storage/repositories/index-entries-repository.js +43 -5
  61. package/dist/storage/repositories/outcome-repository.js +12 -0
  62. package/dist/storage/repositories/proposals-repository.js +5 -5
  63. package/dist/storage/repositories/salience-repository.js +13 -0
  64. package/dist/storage/repositories/task-history-repository.js +8 -7
  65. package/dist/storage/repositories/workflow-runs-repository.js +10 -8
  66. package/dist/tasks/backends/cron.js +19 -0
  67. package/docs/README.md +24 -60
  68. package/docs/agents/README.md +3 -8
  69. package/docs/architecture/README.md +18 -20
  70. package/docs/architecture/adapters.md +40 -43
  71. package/docs/architecture/architecture.md +171 -151
  72. package/docs/architecture/improvement.md +95 -91
  73. package/docs/architecture/internals/classification.md +101 -119
  74. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
  75. package/docs/architecture/internals/health-advisories.md +59 -46
  76. package/docs/architecture/internals/improve-workflow.md +166 -693
  77. package/docs/architecture/internals/indexing.md +65 -465
  78. package/docs/architecture/internals/storage-locations.md +301 -848
  79. package/docs/architecture/persisted-data-compat.md +6 -6
  80. package/docs/architecture/specs/okf-support.md +67 -95
  81. package/docs/architecture/specs/ref.md +67 -124
  82. package/docs/architecture/testing/testing-workflow.md +114 -175
  83. package/docs/architecture/workflow-engine.md +34 -33
  84. package/docs/guides/README.md +5 -5
  85. package/docs/guides/author-bundles.md +72 -98
  86. package/docs/guides/author-workflows.md +11 -18
  87. package/docs/guides/bundles.md +91 -123
  88. package/docs/guides/capture-knowledge.md +10 -9
  89. package/docs/guides/concepts.md +45 -30
  90. package/docs/guides/discover-and-load.md +51 -36
  91. package/docs/guides/getting-started.md +27 -26
  92. package/docs/guides/improve-the-library.md +98 -80
  93. package/docs/guides/recipes/headless-install.md +4 -4
  94. package/docs/guides/recipes/website-source.md +9 -5
  95. package/docs/guides/run-workflows.md +10 -11
  96. package/docs/guides/scheduling.md +11 -17
  97. package/docs/guides/use-with-any-agent.md +15 -11
  98. package/docs/guides/wikis.md +6 -9
  99. package/docs/integration/bundling-akm.md +21 -26
  100. package/docs/maintainers/README.md +2 -2
  101. package/docs/maintainers/local-development.md +3 -16
  102. package/docs/maintainers/release-checklist.md +15 -8
  103. package/docs/migration/README.md +2 -1
  104. package/docs/migration/release-notes/0.10.0.md +167 -22
  105. package/docs/migration/release-notes/README.md +2 -2
  106. package/docs/migration/v0.9-to-v0.10.md +47 -7
  107. package/docs/product-surface.md +10 -8
  108. package/docs/reference/README.md +26 -15
  109. package/docs/reference/asset-types.md +11 -12
  110. package/docs/reference/bundle-types.md +6 -5
  111. package/docs/reference/cli.md +74 -31
  112. package/docs/reference/configuration.md +46 -60
  113. package/docs/reference/data-and-telemetry.md +204 -421
  114. package/docs/reference/env-and-secrets.md +80 -83
  115. package/docs/reference/memory.md +1 -1
  116. package/docs/reference/refs.md +23 -36
  117. package/docs/reference/registry.md +138 -348
  118. package/docs/reference/supported-formats.md +8 -8
  119. package/docs/reference/tasks.md +13 -19
  120. package/docs/reference/website-sources.md +23 -15
  121. package/docs/reference/workflow-schema.md +28 -47
  122. package/docs/reference/workflows.md +33 -37
  123. package/package.json +4 -4
  124. package/schemas/akm-config.json +535 -302
@@ -20,7 +20,7 @@ import { recognizeMatch } from "../../core/adapter/recognize-match.js";
20
20
  import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
21
21
  import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
22
22
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
23
- import { extractSection, markdownFragmentSlugs } from "../../core/asset/markdown.js";
23
+ import { extractSection, markdownFragmentSlugs, markdownToc } from "../../core/asset/markdown.js";
24
24
  import { buildMarkdownLeadContext, fragmentForSelector, MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS, } from "../../core/asset/markdown-fragments.js";
25
25
  import { displayRef, displayRefForConceptId, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
26
26
  import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
@@ -297,8 +297,10 @@ export async function showLocal(input) {
297
297
  const indexedFragmentContent = opaqueFragmentSelector ? indexedFragment?.content : undefined;
298
298
  const indexedRenderer = rendererForIndexedEntry(indexedEntry, fileCtx);
299
299
  let response;
300
+ let markdown = false;
300
301
  try {
301
302
  if (indexedRenderer === null) {
303
+ markdown = path.extname(assetPath).toLowerCase() === ".md";
302
304
  response = buildIndexedProjectionResponse(indexedEntry, assetPath, parsed.fragment, indexedFragmentContent);
303
305
  }
304
306
  else {
@@ -315,8 +317,9 @@ export async function showLocal(input) {
315
317
  const renderDefaultBundle = config.defaultBundle ?? (source?.isDefault === true ? renderBundle : undefined);
316
318
  const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs, renderBundle, renderDefaultBundle);
317
319
  response = renderer.buildShowResponse(renderCtx);
320
+ markdown = match.renderer.endsWith("-md");
318
321
  if (parsed.fragment !== undefined) {
319
- if (!match.renderer.endsWith("-md")) {
322
+ if (!markdown) {
320
323
  warn(`Fragment "#${parsed.fragment}" was ignored: ${makeBundleRef(parsed.bundle, parsed.conceptId)} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`);
321
324
  }
322
325
  else {
@@ -324,6 +327,13 @@ export async function showLocal(input) {
324
327
  }
325
328
  }
326
329
  }
330
+ // The outline is read from the bytes `#<slug>` resolves against, so every
331
+ // listed slug selects its section even when the index is stale.
332
+ if (markdown && parsed.fragment === undefined) {
333
+ const toc = markdownToc(fileCtx.content());
334
+ if (toc.length > 0)
335
+ response.toc = toc;
336
+ }
327
337
  }
328
338
  catch (error) {
329
339
  throwIndexedPathNotFound(error, input.ref);
@@ -598,6 +608,7 @@ function applyMarkdownResponsePayload(response, content) {
598
608
  else
599
609
  response.content = content;
600
610
  }
611
+ const SHOW_OUTLINE_HINT = "Run `akm show <ref> --detail brief` for the document's outline (`toc`, no body), then `akm show <ref>#<slug>` for one section.";
601
612
  function requireMarkdownSection(content, fragment, name) {
602
613
  const section = extractSection(content, fragment);
603
614
  if (section)
@@ -609,7 +620,7 @@ function requireMarkdownSection(content, fragment, name) {
609
620
  }
610
621
  const available = markdownFragmentSlugs(content);
611
622
  throw new NotFoundError(`Fragment "#${fragment}" not found in ${name}.` +
612
- (available.length > 0 ? ` Available fragments: ${available.map((slug) => `#${slug}`).join(", ")}.` : ""));
623
+ (available.length > 0 ? ` Available fragments: ${available.map((slug) => `#${slug}`).join(", ")}.` : ""), "ASSET_NOT_FOUND", SHOW_OUTLINE_HINT);
613
624
  }
614
625
  /**
615
626
  * Build a reduced brief response from a full ShowResponse.
@@ -625,6 +636,7 @@ function buildBriefResponse(full, assetPath) {
625
636
  ...(summary.ref ? { ref: summary.ref } : {}),
626
637
  ...(summary.description ? { description: summary.description } : {}),
627
638
  ...(summary.action ? { action: summary.action } : {}),
639
+ ...(full.toc ? { toc: full.toc } : {}),
628
640
  ...(summary.run ? { run: summary.run } : {}),
629
641
  ...(summary.origin !== undefined ? { origin: summary.origin } : {}),
630
642
  ...(full.editable !== undefined ? { editable: full.editable } : {}),
@@ -7,7 +7,7 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap, readChunkWithDeadline, } from "../../core/common.js";
9
9
  import { ConfigError } from "../../core/errors.js";
10
- import { warn } from "../../core/warn.js";
10
+ import { isQuiet, warn } from "../../core/warn.js";
11
11
  import { githubHeaders } from "../../integrations/github.js";
12
12
  import { getDirname, mainPath, semverOrder } from "../../runtime.js";
13
13
  import { resolveNpmGlobalRoot } from "../../tasks/resolve-akm-bin.js";
@@ -428,7 +428,11 @@ async function runMigrationStep(runTool) {
428
428
  return { status: "failed", error: result.stderr.trim() || line || `akm-migrate exited ${result.status}` };
429
429
  }
430
430
  /**
431
- * Rebuild the derived index after a successful upgrade.
431
+ * Rebuild the derived index after a successful upgrade. The first index after
432
+ * an upgrade can re-embed part of the corpus and take minutes, so the child's
433
+ * stderr (its progress lines) goes straight to the terminal; its stdout, never a
434
+ * terminal here, is captured. `--format json` keeps the progress on stderr
435
+ * whatever output format the user has configured, and `--quiet` is passed on.
432
436
  */
433
437
  function runPostUpgradeTasks(akmBin, opts) {
434
438
  if (opts.skip) {
@@ -439,10 +443,10 @@ function runPostUpgradeTasks(akmBin, opts) {
439
443
  };
440
444
  }
441
445
  try {
442
- const result = childProcess.spawnSync(akmBin, ["index"], {
446
+ const result = childProcess.spawnSync(akmBin, ["index", "--format", "json", ...(isQuiet() ? ["--quiet"] : [])], {
443
447
  encoding: "utf8",
444
448
  env: process.env,
445
- stdio: "pipe",
449
+ stdio: ["pipe", "pipe", "inherit"],
446
450
  });
447
451
  if (result.error) {
448
452
  return {
@@ -452,7 +456,7 @@ function runPostUpgradeTasks(akmBin, opts) {
452
456
  };
453
457
  }
454
458
  if (result.status !== 0) {
455
- const detail = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `exit code ${result.status}`;
459
+ const detail = (result.stdout ?? "").trim() || `exit code ${result.status}`;
456
460
  return {
457
461
  ok: false,
458
462
  skipped: false,
@@ -178,10 +178,10 @@ export async function akmTasksRun(id, options = {}) {
178
178
  const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
179
179
  const resolvedId = taskIdForAdapter(parsed.id, adapterId);
180
180
  const scheduled = options.scheduled === true;
181
- // Built ONCE at this invocation boundary. eventSource is "task"
182
- // whether or not --scheduled was passed — scheduled stays a
183
- // separate field carrying its own pre-existing meaning (activation policy,
184
- // scheduler env), never selecting the event source.
181
+ // Built ONCE at this invocation boundary. eventSource is "task" whether or
182
+ // not --scheduled was passed — scheduled stays a separate field carrying its
183
+ // own meaning (activation policy, scheduler env), never selecting the event
184
+ // source.
185
185
  const provenance = createExecutionProvenanceContext(scheduled);
186
186
  // The raw, exact-name input flags `tasks-cli.ts`'s Stage 1 captures ride
187
187
  // through unchanged to `runTask` -> `loadPreparedTask`'s Stage 2 materializer,
@@ -206,8 +206,8 @@ export async function akmTasksRun(id, options = {}) {
206
206
  // projectability, and resolver failures therefore create no history row.
207
207
  const result = await runTask(resolvedId, runOptions);
208
208
  // "command" means the agent/LLM arm — the native shell/script arm reports
209
- // "shell" / "script" — so a shell/script task's process exit 78 still passes
210
- // through as CLI exit 78 (documented behavior, src/assets/hints/cli-hints-short.md).
209
+ // "shell" / "script". A shell/script task's process exit 78 passes through
210
+ // as CLI exit 78 (documented behavior, src/assets/hints/cli-hints-short.md).
211
211
  const exitCode = result.status === "failed" &&
212
212
  (result.target.kind === "shell" || result.target.kind === "script") &&
213
213
  result.detail?.exitCode === 78
@@ -597,10 +597,9 @@ export async function akmTasksDoctor(deps = {}) {
597
597
  const config = loadConfig();
598
598
  const defaultEngine = (defaultHarness(config) ?? defaultConnection(config))?.name;
599
599
  const engines = [...Object.keys(config.connections ?? {}), ...Object.keys(config.harnesses ?? {})];
600
- // §6.1: surface the effective triage settings for the default improve
601
- // strategy. The struct is a fixed shape, so this is a deliberate addition.
600
+ // Surface the effective triage settings for the default improve strategy.
602
601
  const improveStrategyName = config.improve?.strategy ?? "default";
603
- // D8 — report the EFFECTIVE strategy, not the raw one. Resolving the raw
602
+ // Report the EFFECTIVE strategy, not the raw one. Resolving the raw
604
603
  // strategy here would report `applyMode: "promote"` for a promote strategy
605
604
  // under a review-first config, while the run actually uses "queue" — a doctor
606
605
  // command lying about the thing it exists to diagnose.
@@ -753,7 +752,7 @@ function errorMessage(cause) {
753
752
  }
754
753
  /**
755
754
  * Infer a JSON-Schema-subset `type` keyword from one `--params` value's
756
- * runtime shape. `null` is not one of the five runtime types
755
+ * runtime shape. `null` is not one of the five usual runtime types
757
756
  * (string/number/boolean/object/array) but is a value JSON.parse can still
758
757
  * produce for a param; `src/core/json-schema.ts`'s subset validator accepts
759
758
  * `"null"` as a `type`, so it is handled rather than mis-typed.
@@ -768,9 +767,9 @@ function jsonSchemaTypeOf(value) {
768
767
  /**
769
768
  * Render `--params` as typed `inputs:` declarations, one per key, each
770
769
  * carrying a `default:` equal to the authored value and a `type:` inferred
771
- * from its own JSON runtime shape (row B-20). Never emitted as `with:` —
772
- * task source v4 accepts `with:` only on `uses: akm/command` (§3.2.6's
773
- * `parseTarget`/`checkTopLevelKeys`), and a workflow target's declared
770
+ * from its own JSON runtime shape. Never emitted as `with:` —
771
+ * task source v4 accepts `with:` only on `uses: akm/command`
772
+ * (`parseTarget`/`checkTopLevelKeys`), and a workflow target's declared
774
773
  * inputs are what `load-task.ts`'s existing v4 delivery override binds into
775
774
  * the child run's params.
776
775
  */
@@ -2,7 +2,7 @@
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
  /**
5
- * The `agent-skills` adapter — akm 0.9.0 format-family work item (#46).
5
+ * The `agent-skills` adapter.
6
6
  *
7
7
  * Recognizes a COLLECTION of standalone SKILL.md packages (like
8
8
  * github.com/anthropics/skills): each `<name>/SKILL.md` is one `skill` item
@@ -38,7 +38,7 @@
38
38
  *
39
39
  * Conformance oracle (authored, DO NOT modify): fixture
40
40
  * `tests/fixtures/bundles/agent-skills/` + goldens
41
- * `tests/fixtures/format-family-goldens/agent-skills/{recognition,placement,lint,renderer}.json`.
41
+ * `tests/fixtures/format-family-goldens/agent-skills/{recognition,lint,renderer}.json`.
42
42
  */
43
43
  import fs from "node:fs";
44
44
  import path from "node:path";
@@ -52,7 +52,7 @@ const SKILL_MANIFEST = "SKILL.md";
52
52
  /** Agent Skills hard limits. */
53
53
  const NAME_MAX = 64;
54
54
  const DESCRIPTION_MAX = 1024;
55
- /** Agent Skills reserved words a name must not contain (real-world source). */
55
+ /** Agent Skills reserved words a name must not contain. */
56
56
  const RESERVED_NAME_WORDS = ["anthropic", "claude"];
57
57
  /** name charset/shape rule. */
58
58
  const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
@@ -98,7 +98,7 @@ function recognize(c, file) {
98
98
  doc.tags = tags;
99
99
  return doc;
100
100
  }
101
- /** The Agent Skills §4.5 hard-rule checks for one SKILL.md (name + description). */
101
+ /** The Agent Skills hard-rule checks for one SKILL.md (name + description). */
102
102
  function skillFieldDiagnostics(relPath, dirName, data) {
103
103
  const diagnostics = [];
104
104
  const name = typeof data.name === "string" ? data.name.normalize("NFKC") : "";
@@ -176,8 +176,8 @@ async function scanPackageCandidate(dir, entries, ctx, depth) {
176
176
  *
177
177
  * `validate` walks CHANGES, and a change is always a file — so a package
178
178
  * directory carrying resources but no manifest contributes nothing the
179
- * change-loop can see, and the check the spec (§4.5) and the lint golden's
180
- * `missingSkillMd` edge case both name was unreachable. This scans the
179
+ * change-loop can see, and the check the lint golden's `missingSkillMd` edge
180
+ * case names would be unreachable. This scans the
181
181
  * component root through {@link ValidateContext.list} instead, so the case is
182
182
  * actually reported.
183
183
  *
@@ -230,7 +230,7 @@ export const agentSkillsAdapter = {
230
230
  readCandidates(c, conceptId) {
231
231
  return [{ path: path.join(c.root, conceptId, SKILL_MANIFEST), conceptId }];
232
232
  },
233
- /** Install-time probe (§1.2): a root that directly contains one or more `<name>/SKILL.md` skill packages. */
233
+ /** Install-time probe: a root that directly contains one or more `<name>/SKILL.md` skill packages. */
234
234
  looksLikeRoot(root) {
235
235
  let entries;
236
236
  try {
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * Conformance oracle (authored, DO NOT modify): fixture
28
28
  * `tests/fixtures/bundles/akm-task/` + goldens
29
- * `tests/fixtures/format-family-goldens/akm-task/{recognition,placement,lint,renderer}.json`.
29
+ * `tests/fixtures/format-family-goldens/akm-task/{recognition,lint,renderer}.json`.
30
30
  */
31
31
  import fs from "node:fs";
32
32
  import path from "node:path";
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * Conformance oracle (authored, DO NOT modify): fixture
17
17
  * `tests/fixtures/bundles/akm-workflow/` + goldens
18
- * `tests/fixtures/format-family-goldens/akm-workflow/{recognition,placement,lint,renderer}.json`.
18
+ * `tests/fixtures/format-family-goldens/akm-workflow/{recognition,lint,renderer}.json`.
19
19
  */
20
20
  import fs from "node:fs";
21
21
  import path from "node:path";
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Conformance oracle (authored, DO NOT modify): fixture
18
18
  * `tests/fixtures/bundles/claude/` + goldens
19
- * `tests/fixtures/format-family-goldens/claude/{recognition,placement,lint,renderer}.json`.
19
+ * `tests/fixtures/format-family-goldens/claude/{recognition,lint,renderer}.json`.
20
20
  */
21
21
  import fs from "node:fs";
22
22
  import path from "node:path";
@@ -25,7 +25,7 @@
25
25
  *
26
26
  * Conformance oracle (authored, DO NOT modify): fixture
27
27
  * `tests/fixtures/bundles/dotenv/` + goldens
28
- * `tests/fixtures/format-family-goldens/dotenv/{recognition,placement,lint,renderer}.json`.
28
+ * `tests/fixtures/format-family-goldens/dotenv/{recognition,lint,renderer}.json`.
29
29
  */
30
30
  import fs from "node:fs";
31
31
  import path from "node:path";
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * Conformance oracle (authored, DO NOT modify): fixture
21
21
  * `tests/fixtures/bundles/generic-files/` + goldens
22
- * `tests/fixtures/format-family-goldens/generic-files/{recognition,placement,lint,renderer}.json`.
22
+ * `tests/fixtures/format-family-goldens/generic-files/{recognition,lint,renderer}.json`.
23
23
  */
24
24
  import path from "node:path";
25
25
  import { parseFrontmatter } from "../../asset/frontmatter.js";
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Conformance oracle (authored, DO NOT modify): fixture
13
13
  * `tests/fixtures/bundles/llm-wiki/` + goldens
14
- * `tests/fixtures/format-family-goldens/llm-wiki/{recognition,placement,renderer,lint}.json`.
14
+ * `tests/fixtures/format-family-goldens/llm-wiki/{recognition,renderer,lint}.json`.
15
15
  * The adapter is built to MATCH those goldens (driven by
16
16
  * `tests/core/adapter/llm-wiki-adapter.test.ts`).
17
17
  *
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Conformance oracle (authored, DO NOT modify): fixture
18
18
  * `tests/fixtures/bundles/opencode/` + goldens
19
- * `tests/fixtures/format-family-goldens/opencode/{recognition,placement,lint,renderer}.json`.
19
+ * `tests/fixtures/format-family-goldens/opencode/{recognition,lint,renderer}.json`.
20
20
  */
21
21
  import fs from "node:fs";
22
22
  import path from "node:path";
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Conformance oracle (authored, DO NOT modify): fixture
24
24
  * `tests/fixtures/bundles/website-snapshot/` + goldens
25
- * `tests/fixtures/format-family-goldens/website-snapshot/{recognition,placement,lint,renderer}.json`.
25
+ * `tests/fixtures/format-family-goldens/website-snapshot/{recognition,lint,renderer}.json`.
26
26
  */
27
27
  import fs from "node:fs";
28
28
  import path from "node:path";
@@ -16,6 +16,15 @@ export function markdownHeadingSlug(heading) {
16
16
  export function markdownFragmentSlugs(content) {
17
17
  return uniqueHeadingSlugs(parseMarkdownToc(content).headings).filter(Boolean);
18
18
  }
19
+ /**
20
+ * The headings `extractSection` can select, with the slug it resolves each by.
21
+ * A heading with no sluggable text has no selector, so it is left out.
22
+ */
23
+ export function markdownToc(content) {
24
+ const headings = parseMarkdownToc(content).headings;
25
+ const slugs = uniqueHeadingSlugs(headings);
26
+ return headings.flatMap((heading, index) => slugs[index] ? [{ level: heading.level, text: heading.text, slug: slugs[index], startLine: heading.line }] : []);
27
+ }
19
28
  /** One slug per heading, in order; a heading with no sluggable text gets "". */
20
29
  export function uniqueHeadingSlugs(headings) {
21
30
  const used = new Set();
@@ -6,7 +6,9 @@
6
6
  * bundle's key. A bundle id is a mass identity prefix: every durable ref this
7
7
  * tool minted (`entries.item_ref`/`bundle_id`, `proposals.ref`, a pending
8
8
  * proposal's `proposedTarget.source`, a workflow task_history row's
9
- * `target_ref`, a `workflow_runs.workflow_ref`, `scheduler.enabled[].ref`) carries it, and a hand-edited
9
+ * `target_ref`, a `workflow_runs.workflow_ref`, a `usage_events.entry_ref`, an
10
+ * `events.ref`, an `asset_salience`/`asset_outcome` `asset_ref`, an
11
+ * `improve_ledger.ref`, `scheduler.enabled[].ref`) carries it, and a hand-edited
10
12
  * `bundles` key strands all of it (see `src/indexer/bundle-identity-guard.ts`
11
13
  * §11.5). This module is the rekey transaction that guard's docstring points
12
14
  * to.
@@ -20,17 +22,17 @@
20
22
  * `akm mv`/editing territory, not a config-identity rename. `--dry-run`
21
23
  * (and every real run) lists the indexed files that still spell the old
22
24
  * `<old>//` prefix so the operator can follow up.
23
- *
24
- * Left behind on purpose: `usage_events`/`asset_salience`/`asset_outcome`
25
- * keyed by the old bundle prefix (learned utility/outcome history) — outside
26
- * this item's scope; `warnOnBundleRenameDrift` already documents this class
27
- * of gap and `akm index --full` re-mints under the new id.
28
25
  */
29
26
  import fs from "node:fs";
27
+ import { renameUsageEventRefs } from "../indexer/usage/usage-events.js";
30
28
  import { readLockfile, renameLockEntry } from "../integrations/lockfile.js";
29
+ import { renameEventRefs } from "../storage/repositories/events-repository.js";
30
+ import { renameImproveLedgerRefs } from "../storage/repositories/improve-ledger-repository.js";
31
31
  import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../storage/repositories/index-connection.js";
32
32
  import { getFilePathsByBundle, renameEntriesBundleId } from "../storage/repositories/index-entries-repository.js";
33
+ import { renameAssetOutcomeRefs } from "../storage/repositories/outcome-repository.js";
33
34
  import { countProposalsForBundleRename, renameProposalsBundleRef } from "../storage/repositories/proposals-repository.js";
35
+ import { renameAssetSalienceRefs } from "../storage/repositories/salience-repository.js";
34
36
  import { countTaskHistoryTargetRefs, renameTaskHistoryTargetRefs, } from "../storage/repositories/task-history-repository.js";
35
37
  import { countWorkflowRunRefsForRename, renameWorkflowRunRefs } from "../storage/repositories/workflow-runs-repository.js";
36
38
  import { selectBackend } from "../tasks/backends/index.js";
@@ -262,12 +264,20 @@ export async function renameBundle(oldId, newId, options = {}, deps) {
262
264
  }
263
265
  }
264
266
  // State: proposals.ref, a pending proposal's proposedTarget.source,
265
- // workflow task_history.target_ref, and workflow_runs.workflow_ref.
267
+ // workflow task_history.target_ref, workflow_runs.workflow_ref, and the
268
+ // learned history keyed by item ref: usage_events.entry_ref (search ranking
269
+ // counts feedback by it), events.ref and improve_ledger.ref (improve reads
270
+ // feedback and its own attempts by them), and asset_salience/asset_outcome.asset_ref.
266
271
  if (fs.existsSync(getStateDbPath())) {
267
272
  withStateDb((db) => {
268
273
  renameProposalsBundleRef(db, oldId, newId);
269
274
  renameTaskHistoryTargetRefs(db, oldId, newId);
270
275
  renameWorkflowRunRefs(db, oldId, newId);
276
+ renameUsageEventRefs(db, oldId, newId);
277
+ renameEventRefs(db, oldId, newId);
278
+ renameImproveLedgerRefs(db, oldId, newId);
279
+ renameAssetSalienceRefs(db, oldId, newId);
280
+ renameAssetOutcomeRefs(db, oldId, newId);
271
281
  });
272
282
  }
273
283
  // Scheduler: remove native rows still naming `<old>//` (see
@@ -27,7 +27,7 @@ import { hasRegistryUrlCredentials, REGISTRY_CREDENTIALS_UNSUPPORTED } from "../
27
27
  import { warnOnce } from "../warn.js";
28
28
  import { AkmConfigBaseSchema, listTopLevelConfigKeys } from "./config-schema.js";
29
29
  import { deepMergeConfig } from "./deep-merge.js";
30
- import { isEnvReference } from "./schema/primitives.js";
30
+ import { isEnvReference, OpaqueFieldSchema } from "./schema/primitives.js";
31
31
  /**
32
32
  * Parse a dotted path into segments. Empty segments are rejected. Bracket
33
33
  * notation (e.g. `sources[0]`) is NOT supported — arrays are set as JSON.
@@ -46,33 +46,39 @@ function parsePath(dotted) {
46
46
  function unwrap(schema) {
47
47
  let current = schema;
48
48
  while (true) {
49
- if (current instanceof z.ZodOptional) {
50
- current = current._def.innerType;
51
- }
52
- else if (current instanceof z.ZodDefault) {
53
- current = current._def.innerType;
54
- }
55
- else if (current instanceof z.ZodNullable) {
56
- current = current._def.innerType;
57
- }
58
- else if (current instanceof z.ZodCatch) {
49
+ if (current instanceof z.ZodOptional ||
50
+ current instanceof z.ZodDefault ||
51
+ current instanceof z.ZodNullable ||
59
52
  // `.catch(...)` wraps an inner schema with a fallback value.
60
- current = current._def.innerType;
53
+ current instanceof z.ZodCatch ||
54
+ current instanceof z.ZodReadonly) {
55
+ current = current.def.innerType;
61
56
  }
62
- else if (current instanceof z.ZodEffects) {
63
- // `.refine()` / `.superRefine()` / `.transform()` / `.preprocess()` —
64
- // descend into the inner schema (`schema` for refine/transform, `out`
65
- // for preprocess — both are exposed at `_def.schema`).
66
- current = current._def.schema;
67
- }
68
- else if (current instanceof z.ZodReadonly) {
69
- current = current._def.innerType;
57
+ else if (current instanceof z.ZodPipe) {
58
+ // `.transform()` pipes the schema into a transform and `z.preprocess()` pipes a transform into it:
59
+ // descend into the side that is a schema. (`.refine()` and `.superRefine()` wrap nothing.)
60
+ const { in: input, out } = current.def;
61
+ current = (input instanceof z.ZodTransform ? out : input);
70
62
  }
71
63
  else {
72
64
  return current;
73
65
  }
74
66
  }
75
67
  }
68
+ /**
69
+ * The schema of the keys an object takes without naming them (`index.<passName>`,
70
+ * a connection's request fields), or `undefined` when its extra keys are not
71
+ * ones akm defines. `.passthrough()` gives an object an `unknown` catchall, so
72
+ * only `OpaqueFieldSchema` itself says "any key, contents left to the endpoint".
73
+ */
74
+ function namedCatchall(object) {
75
+ const catchall = object.def.catchall;
76
+ if (catchall === undefined || catchall instanceof z.ZodNever)
77
+ return undefined;
78
+ if (catchall instanceof z.ZodUnknown && catchall !== OpaqueFieldSchema)
79
+ return undefined;
80
+ return catchall;
81
+ }
76
82
  /** True for a schema that accepts any value (`z.unknown()`): its contents are not keys akm defines. */
77
83
  export function isOpaqueSchema(schema) {
78
84
  return unwrap(schema) instanceof z.ZodUnknown;
@@ -91,13 +97,10 @@ export function resolveSchemaAt(path, config, raw) {
91
97
  const next = schema.shape[segment];
92
98
  if (!next) {
93
99
  // Catchall (e.g. index.<passName>) — descend into the catchall schema.
94
- const catchall = schema._def.catchall;
95
- if (catchall && !(catchall instanceof z.ZodNever)) {
96
- schema = catchall;
97
- }
98
- else {
100
+ const catchall = namedCatchall(schema);
101
+ if (!catchall)
99
102
  return undefined;
100
- }
103
+ schema = catchall;
101
104
  }
102
105
  else {
103
106
  schema = next;
@@ -106,13 +109,13 @@ export function resolveSchemaAt(path, config, raw) {
106
109
  else if (schema instanceof z.ZodRecord) {
107
110
  // Named records (connections, harnesses, improve.strategies, sources, etc.)
108
111
  // accept any string key — descend into the value schema.
109
- schema = schema._def.valueType;
112
+ schema = schema.def.valueType;
110
113
  }
111
114
  else if (schema instanceof z.ZodUnion) {
112
- const option = selectUnionObjectOption(schema._def.options, segment, existing, index === path.length - 1 ? raw : undefined);
115
+ const option = selectUnionObjectOption(schema.options, segment, existing, index === path.length - 1 ? raw : undefined);
113
116
  if (!option)
114
117
  return undefined;
115
- schema = (option.shape[segment] ?? option._def.catchall);
118
+ schema = (option.shape[segment] ?? namedCatchall(option));
116
119
  }
117
120
  else {
118
121
  // Cannot descend into a non-object schema.
@@ -145,7 +148,7 @@ function selectUnionObjectOption(candidates, segment, existing, raw) {
145
148
  }
146
149
  return (options.find((option) => segment in option.shape) ??
147
150
  // A model entry such as `{ model, reasoning_effort }` holds request fields the object does not name.
148
- options.find((option) => !(option._def.catchall instanceof z.ZodNever)));
151
+ options.find((option) => namedCatchall(option) !== undefined));
149
152
  }
150
153
  /**
151
154
  * Get the value at the dotted path from a config-shaped object. Returns
@@ -350,7 +353,7 @@ function coerceForSchema(schema, raw, key) {
350
353
  return raw;
351
354
  }
352
355
  if (target instanceof z.ZodLiteral) {
353
- return typeof target._def.value === "string" ? raw : target._def.value;
356
+ return typeof target.value === "string" ? raw : target.value;
354
357
  }
355
358
  if (target instanceof z.ZodArray || target instanceof z.ZodObject || target instanceof z.ZodRecord) {
356
359
  if (raw === "" || raw === "null")
@@ -366,7 +369,7 @@ function coerceForSchema(schema, raw, key) {
366
369
  if (target instanceof z.ZodUnion) {
367
370
  // Try each option in order. Use the first that the schema accepts after
368
371
  // coercion. Falls through to JSON parsing if all fail.
369
- const options = target._def.options;
372
+ const options = target.options;
370
373
  let lastErr;
371
374
  for (const opt of options) {
372
375
  try {
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { z } from "zod";
14
14
  import { WORKFLOW_MAX_TIMEOUT_MS } from "../../../workflows/resource-limits.js";
15
- import { connectionEndpoint, engineName, envReferenceApiKey, nonEmptyString, positiveInt, retiredApiKeyFile, } from "./primitives.js";
15
+ import { connectionEndpoint, engineName, envReferenceApiKey, nonEmptyString, OpaqueFieldSchema, positiveInt, retiredApiKeyFile, } from "./primitives.js";
16
16
  /**
17
17
  * Timeouts share the workflow ceiling: a larger value would pass config
18
18
  * validation and then fail every run with an unlocated "invocation is invalid".
@@ -24,7 +24,10 @@ export const timeoutMsField = z.union([positiveInt.max(WORKFLOW_MAX_TIMEOUT_MS),
24
24
  * request fields of its own. The object is listed first so a JSON value given to
25
25
  * `akm config set` is read as one.
26
26
  */
27
- export const ModelEntrySchema = z.union([z.object({ model: nonEmptyString }).catchall(z.unknown()), nonEmptyString]);
27
+ export const ModelEntrySchema = z.union([
28
+ z.object({ model: nonEmptyString }).catchall(OpaqueFieldSchema),
29
+ nonEmptyString,
30
+ ]);
28
31
  /** Alias (`fast`, `balanced`, `reasoning`, or any the user adds) to the model it names. */
29
32
  export const ModelAliasesSchema = z.record(nonEmptyString, ModelEntrySchema);
30
33
  export const ConnectionSchema = z
@@ -37,5 +40,5 @@ export const ConnectionSchema = z
37
40
  timeoutMs: timeoutMsField,
38
41
  concurrency: positiveInt.optional(),
39
42
  })
40
- .catchall(z.unknown());
43
+ .catchall(OpaqueFieldSchema);
41
44
  export const ConnectionsSchema = z.record(engineName, ConnectionSchema);
@@ -13,7 +13,14 @@ import { ModelAliasesSchema, timeoutMsField } from "./connections.js";
13
13
  import { engineName, nonEmptyString } from "./primitives.js";
14
14
  export const HarnessSchema = z
15
15
  .object({
16
- platform: z.enum(HARNESS_CONFIG_PLATFORMS).optional(),
16
+ // Zod 4's message for a value outside an enum no longer quotes the value; the warning for a dropped harness should.
17
+ platform: z
18
+ .enum(HARNESS_CONFIG_PLATFORMS, {
19
+ error: (issue) => issue.code === "invalid_value"
20
+ ? `${JSON.stringify(issue.input)} is not one of ${HARNESS_CONFIG_PLATFORMS.join(", ")}`
21
+ : undefined,
22
+ })
23
+ .optional(),
17
24
  bin: nonEmptyString.optional(),
18
25
  args: z.array(z.string()).optional(),
19
26
  workspace: nonEmptyString.optional(),
@@ -12,11 +12,22 @@ import { redactUrlCredentials } from "../../redaction.js";
12
12
  import { isSecretRefToken } from "../../ref-token.js";
13
13
  import { warnOnce } from "../../warn.js";
14
14
  import { ENGINE_NAME_PATTERN_SOURCE } from "../engine-semantics.js";
15
+ // Zod 4 reports a map key its key schema rejects as "Invalid key in record" and keeps that schema's own message
16
+ // (the naming rule, the slug rule) inside the issue; config errors and warnings print only the outer message.
17
+ z.config({
18
+ customError: (issue) => issue.code === "invalid_key" ? issue.issues.map((inner) => inner.message).join("; ") : undefined,
19
+ });
15
20
  /** Persisted config schema version. Package prerelease/patch versions do not change this value. */
16
21
  export const CURRENT_CONFIG_VERSION = "0.9.0";
17
22
  // ── Reusable atomic schemas ─────────────────────────────────────────────────
18
23
  /** Positive integer (used for tokens, timeouts, batch sizes). */
19
24
  export const positiveInt = z.number().int().positive();
25
+ /**
26
+ * The catchall of an object whose extra keys are the endpoint's to define (a connection's request fields):
27
+ * any key, contents left alone. The config walker knows it by identity: a `.passthrough()` object also has an
28
+ * `unknown` catchall, and its extra keys are ones akm does not define.
29
+ */
30
+ export const OpaqueFieldSchema = z.unknown();
20
31
  /** Non-negative finite number (used for scores, weights, days). */
21
32
  export const nonNegativeNumber = z.number().finite().min(0);
22
33
  /** Non-empty string (rejects "" and whitespace-only). */
@@ -92,7 +103,7 @@ export const connectionEndpoint = z.string().superRefine((value, ctx) => {
92
103
  warnOnce(`connectionEndpoint:userinfo:${shown}`, `Config endpoint "${shown}" embeds a username/password; move the credential out of the URL (apiKey: "$VAR", or credentials.json).`);
93
104
  }
94
105
  });
95
- export const ExtraParamsSchema = z.record(z.unknown()).superRefine((value, ctx) => {
106
+ export const ExtraParamsSchema = z.record(z.string(), z.unknown()).superRefine((value, ctx) => {
96
107
  for (const issue of validateExtraParams(value)) {
97
108
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: issue.path, message: issue.message });
98
109
  }
@@ -16,7 +16,7 @@ import { isBundleSlug } from "../../asset/asset-ref.js";
16
16
  import { httpUrl, isApiKeyReference, nonEmptyString, positiveInt } from "./primitives.js";
17
17
  const VALID_ADAPTER_IDS_SET = new Set(VALID_ADAPTER_IDS);
18
18
  // ── Sources / registries / installed ────────────────────────────────────────
19
- const SourceConfigEntryOptionsSchema = z.record(z.unknown());
19
+ const SourceConfigEntryOptionsSchema = z.record(z.string(), z.unknown());
20
20
  export const SourceConfigEntrySchema = z
21
21
  .object({
22
22
  type: nonEmptyString,
@@ -74,7 +74,7 @@ export const RegistryConfigEntrySchema = z
74
74
  name: z.string().min(1).optional(),
75
75
  enabled: z.boolean().optional(),
76
76
  provider: z.string().min(1).optional(),
77
- options: z.record(z.unknown()).optional(),
77
+ options: z.record(z.string(), z.unknown()).optional(),
78
78
  })
79
79
  .passthrough();
80
80
  // ── Bundles ─────────────────────────────────────────────────────────────────