akm-cli 0.9.16-alpha.1 → 0.9.16-alpha.2

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 (144) hide show
  1. package/CHANGELOG.md +40 -132
  2. package/dist/assets/hints/cli-hints-full.md +13 -6
  3. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  4. package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
  5. package/dist/cli/retired-commands.js +0 -4
  6. package/dist/cli/unknown-flags.js +3 -36
  7. package/dist/commands/env/env-binding.js +4 -4
  8. package/dist/commands/env/env-cli.js +3 -3
  9. package/dist/commands/improve/collapse-detector.js +2 -2
  10. package/dist/commands/improve/consolidate.js +4 -6
  11. package/dist/commands/improve/improve-cli.js +20 -15
  12. package/dist/commands/improve/reflect.js +23 -2
  13. package/dist/commands/lint/base-linter.js +9 -0
  14. package/dist/commands/lint/env-key-rules.js +2 -2
  15. package/dist/commands/proposal/propose.js +15 -1
  16. package/dist/commands/proposal/repository.js +3 -12
  17. package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
  18. package/dist/commands/proposal/validators/proposal-validators.js +5 -4
  19. package/dist/commands/read/curate.js +44 -34
  20. package/dist/commands/read/search.js +35 -54
  21. package/dist/commands/read/show.js +21 -2
  22. package/dist/commands/registry-cli.js +5 -5
  23. package/dist/commands/sources/add-cli.js +59 -16
  24. package/dist/commands/sources/bundle-cli.js +35 -11
  25. package/dist/commands/sources/bundle-config-ops.js +30 -0
  26. package/dist/commands/sources/dangerous-env-audit.js +4 -4
  27. package/dist/commands/sources/info.js +8 -8
  28. package/dist/commands/sources/installed-stashes.js +55 -61
  29. package/dist/commands/sources/source-add.js +39 -38
  30. package/dist/commands/sources/source-manage.js +34 -12
  31. package/dist/commands/sources/stash-cli.js +111 -119
  32. package/dist/commands/sources/stash-skeleton.js +6 -3
  33. package/dist/commands/tasks/explain.js +4 -1
  34. package/dist/commands/tasks/tasks-cli.js +31 -9
  35. package/dist/commands/tasks/tasks.js +239 -194
  36. package/dist/commands/tasks/validate.js +20 -32
  37. package/dist/core/activation-policy.js +4 -4
  38. package/dist/core/adapter/adapters/akm-adapter.js +8 -35
  39. package/dist/core/adapter/adapters/akm-metadata.js +1 -11
  40. package/dist/core/adapter/execution-source.js +10 -29
  41. package/dist/core/asset/asset-placement.js +0 -35
  42. package/dist/core/config/config-schema.js +64 -8
  43. package/dist/core/config/config-sources.js +96 -2
  44. package/dist/core/config/config.js +190 -24
  45. package/dist/core/config/legacy-source-shape-shim.js +9 -0
  46. package/dist/core/config/schema/embedding.js +30 -7
  47. package/dist/core/config/schema/execution.js +23 -0
  48. package/dist/core/config/schema/experimental.js +1 -1
  49. package/dist/core/config/schema/scheduler.js +20 -0
  50. package/dist/core/config/schema/search.js +10 -12
  51. package/dist/core/config/schema/sources-bundles.js +32 -1
  52. package/dist/core/content-safety.js +52 -0
  53. package/dist/core/errors.js +2 -5
  54. package/dist/core/maintenance-barrier.js +11 -13
  55. package/dist/core/paths.js +11 -0
  56. package/dist/core/run-lock.js +2 -5
  57. package/dist/core/state/migrations.js +1 -26
  58. package/dist/core/state-db.js +27 -63
  59. package/dist/core/type-presentation.js +1 -1
  60. package/dist/core/write-source.js +13 -8
  61. package/dist/indexer/bundle-identity-guard.js +45 -8
  62. package/dist/indexer/ensure-index.js +0 -5
  63. package/dist/indexer/index-db-contention.js +56 -0
  64. package/dist/indexer/index-rebuild-lock.js +73 -0
  65. package/dist/indexer/index-written-assets.js +171 -133
  66. package/dist/indexer/indexer.js +1621 -458
  67. package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
  68. package/dist/indexer/materialize-embeddings.js +785 -0
  69. package/dist/indexer/passes/dir-staleness.js +161 -0
  70. package/dist/indexer/passes/metadata.js +1 -18
  71. package/dist/indexer/scan/drain-dir.js +70 -27
  72. package/dist/indexer/search/db-search.js +89 -373
  73. package/dist/indexer/search/ranking-contributors.js +16 -21
  74. package/dist/indexer/search/ranking.js +57 -135
  75. package/dist/indexer/search/search-source.js +29 -11
  76. package/dist/integrations/agent/execution-lowering.js +3 -2
  77. package/dist/integrations/agent/execution-preparation.js +32 -1
  78. package/dist/integrations/agent/prompts.js +1 -1
  79. package/dist/integrations/agent/request-lowering.js +3 -2
  80. package/dist/llm/client.js +3 -11
  81. package/dist/llm/embedder.js +3 -10
  82. package/dist/llm/embedders/remote.js +104 -133
  83. package/dist/llm/feature-gate.js +2 -4
  84. package/dist/llm/rerank-client.js +3 -3
  85. package/dist/output/shapes/passthrough.js +2 -1
  86. package/dist/output/text/command-format.js +13 -19
  87. package/dist/output/text/helpers.js +1 -1
  88. package/dist/output/text/index.js +2 -5
  89. package/dist/registry/resolve.js +37 -10
  90. package/dist/scripts/akm-migrate-node.js +15197 -11351
  91. package/dist/scripts/akm-migrate.js +15514 -11668
  92. package/dist/setup/semantic-assets.js +2 -2
  93. package/dist/setup/setup.js +3 -3
  94. package/dist/setup/steps/connection.js +2 -3
  95. package/dist/setup/steps/tasks.js +29 -36
  96. package/dist/sources/providers/git-install.js +17 -11
  97. package/dist/sources/providers/git-provider.js +12 -5
  98. package/dist/sources/providers/git-stash.js +38 -16
  99. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  100. package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
  101. package/dist/storage/repositories/index-connection.js +3 -1
  102. package/dist/storage/repositories/index-entries-repository.js +68 -77
  103. package/dist/storage/repositories/index-entry-schema.js +25 -16
  104. package/dist/storage/repositories/index-fts-repository.js +263 -29
  105. package/dist/storage/repositories/index-meta-repository.js +29 -0
  106. package/dist/storage/repositories/index-schema.js +122 -115
  107. package/dist/storage/repositories/index-utility-repository.js +1 -1
  108. package/dist/storage/repositories/index-vec-repository.js +435 -22
  109. package/dist/tasks/activation-config.js +90 -0
  110. package/dist/tasks/backends/cron.js +9 -0
  111. package/dist/tasks/backends/launchd.js +1 -0
  112. package/dist/tasks/backends/schtasks.js +2 -0
  113. package/dist/tasks/embedded.js +4 -5
  114. package/dist/tasks/scheduler-binding.js +2 -2
  115. package/dist/tasks/scheduler-sync-preview.js +8 -1
  116. package/dist/tasks/scheduler-sync.js +19 -10
  117. package/dist/tasks/source/parse-task-source.js +10 -113
  118. package/dist/tasks/source/project-v4.js +2 -2
  119. package/dist/tasks/source/task-source-v4.js +4 -12
  120. package/dist/tasks/source/task-to-v3.js +4 -12
  121. package/dist/tasks/source/task-to-v4.js +40 -7
  122. package/docs/migration/README.md +1 -0
  123. package/docs/migration/release-notes/0.9.15.md +36 -34
  124. package/docs/migration/release-notes/0.9.16.md +60 -98
  125. package/docs/migration/release-notes/README.md +0 -5
  126. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  127. package/docs/reference/cli.md +124 -122
  128. package/docs/reference/configuration.md +137 -133
  129. package/docs/reference/data-and-telemetry.md +1 -2
  130. package/docs/reference/tasks.md +34 -29
  131. package/package.json +1 -1
  132. package/schemas/akm-config.json +170 -6
  133. package/schemas/akm-task.json +1 -2
  134. package/dist/commands/sources/index-status.js +0 -99
  135. package/dist/core/hash.js +0 -18
  136. package/dist/indexer/drain.js +0 -306
  137. package/dist/indexer/embedding-identity.js +0 -20
  138. package/dist/indexer/enrich.js +0 -260
  139. package/dist/indexer/reconcile.js +0 -890
  140. package/dist/indexer/scan/parse-file.js +0 -66
  141. package/dist/indexer/units/unit.js +0 -159
  142. package/dist/llm/embedders/provider-limits.js +0 -288
  143. package/dist/storage/repositories/files-repository.js +0 -181
  144. package/dist/storage/repositories/units-repository.js +0 -510
@@ -9,23 +9,18 @@
9
9
  * a bundle/adapter/concept id at all — it reads exactly the path it was
10
10
  * given and classifies it.
11
11
  *
12
- * Reuses the exact version-routing shim `parseTaskSource`
13
- * (`src/tasks/source/parse-task-source.ts`) already applies for every other
14
- * task-source reader (`akm task sync`'s `compileTaskSources` included) —
15
- * this module never forks a second parser or a second v2/v3 migration
16
- * planner. `readBoundedTaskSourceYaml` / `peekTaskSourceVersion` / `own` are
17
- * the SAME front-end helpers that shim itself calls first; they are used
18
- * here only to recover the file's ORIGINALLY DECLARED schema version for
19
- * the report, because `parseTaskSource`'s own `ParsedTaskSource.version` is
20
- * always `4` post-shim — it cannot answer "was this a v2/v3/v4 file?" on
21
- * its own once a v2/v3 source has been converted in memory.
12
+ * Reuses the exact current-schema router `parseTaskSource`
13
+ * (`src/tasks/source/parse-task-source.ts`) used by every other task-source
14
+ * reader. Historical v2/v3 documents are deliberately rejected here and
15
+ * point to `akm migrate apply`; validation does not embed a second legacy
16
+ * parser or perform an in-memory migration.
22
17
  *
23
18
  * Beyond parsing, this module also runs the SAME two per-source gates
24
19
  * `akm task sync`'s `compileTaskSources` runs before it ever installs a
25
20
  * schedule — `assertTaskScheduleInputsSatisfyContract` and
26
21
  * `assertTaskScheduleCronValid` (both extracted from `scheduler-sync.ts` for
27
22
  * exactly this reuse) — so a file `sync` would reject can never
28
- * be reported `valid`/`converts` here. Cron dialect is checked against
23
+ * be reported `valid` here. Cron dialect is checked against
29
24
  * `backendNameForPlatform()`, the same platform default `sync` falls back
30
25
  * to whenever it has no injected/native-inspected backend to hand (see
31
26
  * `akmTasksAdd`, `src/commands/tasks/tasks.ts`); a bare file was never
@@ -48,19 +43,12 @@
48
43
  * table in its header, extended for the two gates above):
49
44
  * - `valid` — parses as task source v4 directly (declared `version: 4`)
50
45
  * and passes both sync gates.
51
- * - `converts` — declared `version: 2` or `3`; the deterministic
52
- * in-memory migrator produced a valid v4 document that
53
- * passes both sync gates.
54
- * - `blocked` — declared `version: 2` or `3`; the migrator itself
55
- * could not convert it (an ambiguous/unmigratable
56
- * shape) — the ONLY way `parseTaskSource` ever throws
57
- * for those two version numbers, so no message-text
58
- * sniffing is needed to tell this apart from `invalid`.
46
+ * - `blocked` — declared `version: 2` or `3`; runtime does not accept
47
+ * it and the explicit migrator must rewrite it first.
59
48
  * - `invalid` — the document declares SOME version (`4`, or anything
60
49
  * other than 2/3/4) but fails to parse/validate, OR it
61
- * parsed (directly or via a SUCCESSFUL v2/v3
62
- * conversion) but fails one of the two sync gates
63
- * above, OR the YAML itself does not parse at all
50
+ * parsed but fails one of the two sync gates above, OR
51
+ * the YAML itself does not parse at all
64
52
  * (a genuine syntax error, not merely a non-task
65
53
  * shape) — reported with the parser's own reason.
66
54
  * - `not-a-task` — the document parses as YAML but never declares a
@@ -138,11 +126,14 @@ export async function akmTaskValidate(filePath) {
138
126
  if (!(cause instanceof UsageError))
139
127
  throw cause;
140
128
  const reason = cause.message;
141
- // `parseTaskSource` only ever throws for a declared version 2/3 via the
142
- // unmigratable-conversion branch (see this file's header) — no separate
143
- // message check needed to recognize "blocked" here.
144
129
  if (declaredVersion === 2 || declaredVersion === 3) {
145
- return { ok: false, path: resolvedPath, sourceVersion: declaredVersion, outcome: "blocked", reason };
130
+ return {
131
+ ok: false,
132
+ path: resolvedPath,
133
+ sourceVersion: declaredVersion,
134
+ outcome: "blocked",
135
+ reason: `${reason} Run \`akm migrate apply --dry-run\`, review the plan, then run \`akm migrate apply\`.`,
136
+ };
146
137
  }
147
138
  if (peekFailed) {
148
139
  return { ok: false, path: resolvedPath, outcome: "invalid", reason };
@@ -161,11 +152,8 @@ export async function akmTaskValidate(filePath) {
161
152
  // Success is unreachable from any path that leaves `declaredVersion`
162
153
  // undefined — the router requires a numeric 2/3/4 version to reach here.
163
154
  const sourceVersion = declaredVersion ?? 4;
164
- // The document itself parsed (directly, or via a successful v2/v3
165
- // conversion) — now the two gates `compileTaskSources` runs before
166
- // accepting it. A violation here is `invalid`, never `blocked`: the
167
- // migrator already succeeded, so this is the same kind of defect a
168
- // native v4 document with the identical schedule would have.
155
+ // The current document parsed; now run the two gates `compileTaskSources`
156
+ // applies before accepting it.
169
157
  try {
170
158
  assertTaskScheduleInputsSatisfyContract(parsed.v4, resolvedPath);
171
159
  assertTaskScheduleCronValid(parsed.v4, backend);
@@ -180,7 +168,7 @@ export async function akmTaskValidate(filePath) {
180
168
  ok: true,
181
169
  path: resolvedPath,
182
170
  sourceVersion,
183
- outcome: sourceVersion === 2 || sourceVersion === 3 ? "converts" : "valid",
171
+ outcome: "valid",
184
172
  resolved: buildResolved(id, parsed.v4),
185
173
  };
186
174
  }
@@ -22,7 +22,7 @@ const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
22
22
  * genuine RCE-class key; first-party stashes warn. The interactive-tool
23
23
  * group (see {@link INTERACTIVE_TOOL_ENV_KEYS}) only ever warns, since akm's
24
24
  * own env-injection path never invokes those keys as a command. An explicit
25
- * `--allow-insecure` (threaded through by the caller, same override
25
+ * `--allow-dangerous-env-keys` (threaded through by the caller, same override
26
26
  * `decideDangerousKeyInstall`'s `"warn-allow"` already honors for rule 2)
27
27
  * downgrades a remaining block to a warning too — the operator is not racing
28
28
  * themselves. See rule 1 above.
@@ -31,7 +31,7 @@ const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
31
31
  * (already filtered by the caller via `isDangerousEnvKey`).
32
32
  * @param thirdParty `true` when the env's source is a third-party stash — i.e.
33
33
  * its origin carries a `registryId`.
34
- * @param allowInsecure `true` when the operator passed `--allow-insecure` (or
34
+ * @param allowDangerousEnvKeys `true` when the operator passed `--allow-dangerous-env-keys` (or
35
35
  * its equivalent) for this injection. Defaults to `false`.
36
36
  */
37
37
  export function decideDangerousEnvInjection(input) {
@@ -39,7 +39,7 @@ export function decideDangerousEnvInjection(input) {
39
39
  return "allow";
40
40
  if (!input.thirdParty)
41
41
  return "warn";
42
- if (input.allowInsecure)
42
+ if (input.allowDangerousEnvKeys)
43
43
  return "warn";
44
44
  const onlyInteractiveTool = input.dangerousKeys.every((key) => INTERACTIVE_TOOL_ENV_KEYS.has(key));
45
45
  return onlyInteractiveTool ? "warn" : "block";
@@ -53,7 +53,7 @@ export function decideDangerousEnvInjection(input) {
53
53
  export function decideDangerousKeyInstall(input) {
54
54
  if (!input.findingsPresent)
55
55
  return "allow";
56
- return input.allowInsecure ? "warn-allow" : "gate";
56
+ return input.allowDangerousEnvKeys ? "warn-allow" : "gate";
57
57
  }
58
58
  // ── Rule 3: write activation (search-source.ts, installations.ts) ────────────
59
59
  /**
@@ -82,7 +82,7 @@
82
82
  import fs from "node:fs";
83
83
  import path from "node:path";
84
84
  import { applyPostContributorFields, applyPreContributorFields, extractPackageMetadata, getMarkdownFragmentContent, hasMarkdownFragmentContent, setMarkdownFragmentContent, } from "../../../indexer/passes/metadata.js";
85
- import { assetPathCandidatesAreOrderedByPreference, assetPathCandidatesForName, assetPathForName, deriveCanonicalAssetNameFromStashRoot, placementTypes, stashDirFor, stashDirNames, } from "../../asset/asset-placement.js";
85
+ import { assetPathCandidatesForName, assetPathForName, deriveCanonicalAssetNameFromStashRoot, placementTypes, stashDirFor, stashDirNames, } from "../../asset/asset-placement.js";
86
86
  import { parseFrontmatter } from "../../asset/frontmatter.js";
87
87
  import { executionDefaultsFromFrontmatter, renderMarkdownExecutionSource } from "../execution-source.js";
88
88
  import { recognizeMatch } from "../recognize-match.js";
@@ -130,6 +130,11 @@ function akmStashAbstains(root, absPath) {
130
130
  const segments = relPath.split(/[\\/]+/).filter(Boolean);
131
131
  if (segments.length === 0)
132
132
  return false;
133
+ // A bundle's root README describes the bundle itself; it is not an AKM
134
+ // knowledge asset. Nested README files remain adapter-owned documentation
135
+ // and are handled by the normal typed-directory matcher policy.
136
+ if (segments.length === 1 && segments[0]?.toLowerCase() === "readme.md")
137
+ return true;
133
138
  // Skip env `.env` files that have a sibling `.sensitive` marker file.
134
139
  if (segments[0] === "env" && (absPath.endsWith(".env") || path.basename(absPath) === ".env")) {
135
140
  if (fs.existsSync(absPath.replace(/\.env$/, ".sensitive")))
@@ -489,25 +494,7 @@ export const akmAdapter = {
489
494
  * type's own stash subdir) and the LOOSE FALLBACK (authored anywhere else
490
495
  * in the bundle, so the canonical name is the file's full path relative to
491
496
  * the bundle root instead of the stash subdir). `assetPathCandidatesForName`
492
- * additionally expands `env`'s `.env`/`<name>.env` duality, and memory's
493
- * `<name>`/`<name>.derived` twin duality, on each.
494
- *
495
- * `priority` (#882 fix) carries each candidate's rank WITHIN its own root's
496
- * list, but ONLY for a type whose duality is a declared, ORDERED
497
- * preference per `assetPathCandidatesAreOrderedByPreference` (only
498
- * `memory`, today) — `assetPathCandidatesForName` returns primary before
499
- * derived-twin for that type, so index 0 is the declared winner when both
500
- * exist. CANONICAL and LOOSE are separate lists whose ranks both start
501
- * back at 0: two candidates that tie on rank (e.g. the canonical and loose
502
- * spellings both being the primary, rank-0, form) are a genuine collision
503
- * between two independently-authored files, not a declared preference —
504
- * only a rank difference WITHIN one root's own list resolves silently.
505
- * Every other type's candidates (including `env`'s `.env`/`default.env`
506
- * pair — co-equal spellings, not an ordered preference, per that same
507
- * predicate's doc comment) carry NO `priority`, unchanged from before
508
- * #882: `resolveAdapterConceptOwner` treats priority-less candidates as
509
- * tied, so more than one existing together still collides. See
510
- * `AdapterReadCandidate.priority`'s doc comment.
497
+ * additionally expands `env`'s `.env`/`<name>.env` duality on each.
511
498
  */
512
499
  readCandidates(c, conceptId) {
513
500
  const posix = conceptId.replace(/\\/g, "/");
@@ -521,23 +508,9 @@ export const akmAdapter = {
521
508
  return [];
522
509
  const canonical = assetPathCandidatesForName(type, path.join(c.root, head), rest);
523
510
  const loose = assetPathCandidatesForName(type, c.root, rest);
524
- if (!assetPathCandidatesAreOrderedByPreference(type)) {
525
- return [...new Set([...canonical, ...loose])].map((candidatePath) => ({
526
- path: candidatePath,
527
- conceptId: posix,
528
- }));
529
- }
530
- const priorityByPath = new Map();
531
- for (const list of [canonical, loose]) {
532
- list.forEach((candidatePath, rank) => {
533
- if (!priorityByPath.has(candidatePath))
534
- priorityByPath.set(candidatePath, rank);
535
- });
536
- }
537
- return [...priorityByPath.keys()].map((candidatePath) => ({
511
+ return [...new Set([...canonical, ...loose])].map((candidatePath) => ({
538
512
  path: candidatePath,
539
513
  conceptId: posix,
540
- priority: priorityByPath.get(candidatePath),
541
514
  }));
542
515
  },
543
516
  /**
@@ -172,17 +172,7 @@ export function foldRecognizedMetadata(rendererName, file) {
172
172
  const fm = parseFrontmatter(file.content()).data;
173
173
  applyFrontmatterDescriptionAndTags(fm, out);
174
174
  const hints = new Set();
175
- // fix-ranking-derived-outranks-primary: `source:` on an ordinary
176
- // memory is an author-written citation worth indexing as a hint, but
177
- // on an inferred (`.derived`) twin it is ALWAYS the machine-written
178
- // provenance backref `memory-inference.ts` writes (`memories/<parent>`
179
- // — see its `FM_SOURCE`), already captured properly as
180
- // `entry.derivedFrom` (metadata.ts). Folding that backref into
181
- // searchHints too means the base memory's own name — almost always a
182
- // query token whenever the base is relevant — auto-credits the twin
183
- // via `search-hint-ranking`'s substring match, independent of whether
184
- // the twin's own content actually matches the query.
185
- const source = fm.inferred === true ? undefined : nonEmptyString(fm.source);
175
+ const source = nonEmptyString(fm.source);
186
176
  if (source)
187
177
  hints.add(source);
188
178
  const fmObservedAt = nonEmptyString(fm.observed_at);
@@ -127,15 +127,6 @@ function nullableString(value, path) {
127
127
  function nullableObject(value, path) {
128
128
  return value === null ? null : cloneExecutionJsonObject(value, path);
129
129
  }
130
- function nullableEnvironment(value, path) {
131
- if (value === null)
132
- return null;
133
- const environment = cloneExecutionJsonObject(value, path);
134
- if (Object.values(environment).some((entry) => typeof entry !== "string")) {
135
- throw new TypeError(`${path} values must be strings`);
136
- }
137
- return environment;
138
- }
139
130
  function nullableTimeout(value, path) {
140
131
  const timeout = cloneExecutionJson(value, path);
141
132
  if (timeout !== null && typeof timeout !== "string" && typeof timeout !== "number") {
@@ -167,6 +158,16 @@ export function executionDefaultsFromFrontmatter(data, options) {
167
158
  const namespace = own(frontmatter, "akm")
168
159
  ? requireMetadataMapping(frontmatter.akm, "frontmatter.akm")
169
160
  : snapshotStrictRecord({}, "frontmatter.akm");
161
+ for (const key of ["workspace", "environment", "runtime"]) {
162
+ const location = own(frontmatter, key)
163
+ ? `frontmatter.${key}`
164
+ : own(namespace, key)
165
+ ? `frontmatter.akm.${key}`
166
+ : undefined;
167
+ if (location) {
168
+ throw new TypeError(`${location} is host-controlled and cannot be declared by an executable asset; configure the selected engine or workflow execution environment instead`);
169
+ }
170
+ }
170
171
  const out = Object.create(null);
171
172
  if (kind === "command" && own(frontmatter, "agent")) {
172
173
  out.agent = nullableString(frontmatter.agent, "frontmatter.agent");
@@ -269,26 +270,6 @@ export function executionDefaultsFromFrontmatter(data, options) {
269
270
  ? topLevelTimeouts.get(selectedTimeoutKey)
270
271
  : namespacedTimeouts.get(selectedTimeoutKey);
271
272
  }
272
- for (const key of ["workspace", "environment", "runtime"]) {
273
- const namespaced = own(namespace, key)
274
- ? key === "workspace"
275
- ? nullableString(namespace[key], `frontmatter.akm.${key}`)
276
- : key === "environment"
277
- ? nullableEnvironment(namespace[key], `frontmatter.akm.${key}`)
278
- : nullableObject(namespace[key], `frontmatter.akm.${key}`)
279
- : undefined;
280
- const topLevel = allowTopLevelEngine && own(frontmatter, key)
281
- ? key === "workspace"
282
- ? nullableString(frontmatter[key], `frontmatter.${key}`)
283
- : key === "environment"
284
- ? nullableEnvironment(frontmatter[key], `frontmatter.${key}`)
285
- : nullableObject(frontmatter[key], `frontmatter.${key}`)
286
- : undefined;
287
- if (topLevel !== undefined)
288
- out[key] = topLevel;
289
- else if (namespaced !== undefined)
290
- out[key] = namespaced;
291
- }
292
273
  return out;
293
274
  }
294
275
  function snapshotRendererInput(input) {
@@ -269,38 +269,3 @@ export function assetPathCandidatesForName(assetType, typeRoot, name) {
269
269
  const namedForm = path.join(typeRoot, base, "default.env");
270
270
  return [...new Set([primary, dotForm, namedForm])];
271
271
  }
272
- /**
273
- * Whether {@link assetPathCandidatesForName}'s returned list for `assetType`
274
- * is an ORDERED preference — earlier candidates are a declared winner over
275
- * later ones, so a physical-owner resolver may pick the earliest that exists
276
- * with no throw when more than one does — or a set of CO-EQUAL spellings,
277
- * where more than one existing together is a genuine authoring collision to
278
- * report, not a preference to resolve silently. #882's fix (the memory
279
- * `.derived`-twin bug) needs this distinction: attaching a declared rank to
280
- * every multi-candidate type's list turned `env`'s co-equal collision into a
281
- * silently-resolved false negative (#882 follow-up) — the two dualities this
282
- * module documents are NOT the same kind of thing.
283
- *
284
- * Only `memory` is ordered. Its own doc comment above states a winner in so
285
- * many words: "The plain `.md` file wins when both exist, so it stays
286
- * `primary`" — `.derived` is a provenance marker on the SAME identity, not a
287
- * second, independently-authored file.
288
- *
289
- * `env`'s `.env`/`default.env` duality is explicitly NOT ordered — the doc
290
- * comment above says only that both spellings "derive the same canonical
291
- * name" and a lookup "must consider both", never that one wins over the
292
- * other. They are two independently authored files that happen to collide
293
- * on one ref; both existing together is exactly the ambiguity
294
- * `AdapterConceptCollisionError` exists to report.
295
- *
296
- * Callers that attach a preference rank to distinguish a declared duality
297
- * from a genuine collision (`AdapterReadCandidate.priority`,
298
- * `resolveAdapterConceptOwner` in `indexer/lookup/adapter-concept-owner.ts`)
299
- * must consult this predicate per asset type rather than assuming every
300
- * multi-candidate type behaves like `memory` — and must NOT special-case the
301
- * literal `.derived` suffix or `assetType === "memory"` themselves; this is
302
- * the one place that knowledge lives.
303
- */
304
- export function assetPathCandidatesAreOrderedByPreference(assetType) {
305
- return assetType === "memory";
306
- }
@@ -29,28 +29,29 @@
29
29
  * typo in an optional section.
30
30
  * - Unsupported top-level source shapes and provider kinds are hard-rejected;
31
31
  * silently dropping them would mask user data loss.
32
- * - UNKNOWN-KEY POLICY: object schemas use passthrough (unknown keys are
33
- * preserved and ignored, NOT rejected). akm runs across multiple installed
34
- * versions sharing one config.json; a newer version writes keys an older
35
- * version's schema doesn't know yet, so hard-rejecting unknown keys turned
36
- * benign version skew into `INVALID_CONFIG_FILE` failures. Known keys are
37
- * still type-checked; passthrough preserves unknown keys across a
38
- * load→save round trip so an older reader never strips a newer writer's
39
- * settings. (Replaced the prior strict-mode object walls.)
32
+ * - UNKNOWN-KEY POLICY: portable descriptive sections generally use
33
+ * passthrough so version skew round-trips newer settings. Small finite
34
+ * authority/toggle sections (`scheduler`, `execution`, `experimental`, and
35
+ * reranker policy) are strict: a misspelling there must not silently turn a
36
+ * safety decision off. The top level remains passthrough and warns on
37
+ * unknown keys.
40
38
  * - `defaultWriteTarget` resolution and similar cross-field invariants are
41
39
  * enforced at save time via `superRefine` on the top-level schema.
42
40
  */
43
41
  import { z } from "zod";
42
+ import { bundleRefToString, parseBundleRef } from "../asset/asset-ref.js";
44
43
  import { warnOnce } from "../warn.js";
45
44
  import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./engine-semantics.js";
46
45
  import { EmbeddingConnectionConfigSchema } from "./schema/embedding.js";
47
46
  import { EnginesSchema } from "./schema/engines.js";
47
+ import { ExecutionPolicyConfigSchema } from "./schema/execution.js";
48
48
  import { ExperimentalConfigSchema } from "./schema/experimental.js";
49
49
  import { FeedbackConfigSchema } from "./schema/feedback.js";
50
50
  import { ImproveConfigSchema } from "./schema/improve.js";
51
51
  import { IndexConfigSchema } from "./schema/index-config.js";
52
52
  import { OutputConfigSchema } from "./schema/output.js";
53
53
  import { CURRENT_CONFIG_VERSION, engineName, nonEmptyString, nonNegativeNumber } from "./schema/primitives.js";
54
+ import { SchedulerConfigSchema } from "./schema/scheduler.js";
54
55
  import { SearchConfigSchema } from "./schema/search.js";
55
56
  import { SetupConfigSchema } from "./schema/setup.js";
56
57
  import { BundlesConfigSchema, RegistryConfigEntrySchema } from "./schema/sources-bundles.js";
@@ -65,6 +66,7 @@ export { ConsolidateProcessConfigSchema, DistillProcessConfigSchema, ExtractProc
65
66
  export { IndexConfigSchema, IndexPassConfigSchema } from "./schema/index-config.js";
66
67
  export { OutputConfigSchema } from "./schema/output.js";
67
68
  export { CURRENT_CONFIG_VERSION, LlmInvocationOverridesSchema } from "./schema/primitives.js";
69
+ export { SchedulerActivationSchema, SchedulerConfigSchema } from "./schema/scheduler.js";
68
70
  export { SearchConfigSchema } from "./schema/search.js";
69
71
  export { SetupConfigSchema } from "./schema/setup.js";
70
72
  export { BundleConfigEntrySchema, BundlesConfigSchema, RegistryConfigEntrySchema, SourceConfigEntrySchema, } from "./schema/sources-bundles.js";
@@ -107,6 +109,8 @@ export const AkmConfigShape = {
107
109
  defaults: DefaultsSchema.optional(),
108
110
  semanticSearchMode: z.enum(["off", "auto"]).default("off"),
109
111
  embedding: EmbeddingConnectionConfigSchema.optional(),
112
+ // Host-local execution authority. Inherited layers are stripped.
113
+ execution: ExecutionPolicyConfigSchema.optional(),
110
114
  index: IndexConfigSchema.optional(),
111
115
  registries: z.array(RegistryConfigEntrySchema).optional(),
112
116
  // `bundles` + `defaultBundle` are the only source configuration shape. The
@@ -122,6 +126,9 @@ export const AkmConfigShape = {
122
126
  archiveRetentionDays: nonNegativeNumber.optional(),
123
127
  improve: ImproveConfigSchema.optional(),
124
128
  workflow: WorkflowConfigSchema.optional(),
129
+ // Host-local scheduler grants. Inherited layers are stripped by
130
+ // `resolveExtendsChain`; bundle-provided config never activates code.
131
+ scheduler: SchedulerConfigSchema.optional(),
125
132
  setup: SetupConfigSchema.optional(),
126
133
  // D8 — explicit opt-ins for behaviour outside the stability contract. Every
127
134
  // key defaults to OFF; see `src/core/config/experimental.ts` for the readers.
@@ -168,6 +175,55 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
168
175
  message: `defaultBundle "${config.defaultBundle}" does not name a configured bundle`,
169
176
  });
170
177
  }
178
+ else if (config.bundles[config.defaultBundle]?.enabled === false) {
179
+ ctx.addIssue({
180
+ code: z.ZodIssueCode.custom,
181
+ path: ["defaultBundle"],
182
+ message: `defaultBundle "${config.defaultBundle}" is disabled`,
183
+ });
184
+ }
185
+ }
186
+ if (config.defaultWriteTarget !== undefined) {
187
+ const target = config.bundles?.[config.defaultWriteTarget];
188
+ if (!target) {
189
+ ctx.addIssue({
190
+ code: z.ZodIssueCode.custom,
191
+ path: ["defaultWriteTarget"],
192
+ message: `defaultWriteTarget "${config.defaultWriteTarget}" does not name a configured bundle`,
193
+ });
194
+ }
195
+ else if (target.enabled === false) {
196
+ ctx.addIssue({
197
+ code: z.ZodIssueCode.custom,
198
+ path: ["defaultWriteTarget"],
199
+ message: `defaultWriteTarget "${config.defaultWriteTarget}" is disabled`,
200
+ });
201
+ }
202
+ }
203
+ const activationKeys = new Set();
204
+ for (const [index, activation] of (config.scheduler?.enabled ?? []).entries()) {
205
+ try {
206
+ const parsed = parseBundleRef(activation.ref);
207
+ if (!parsed.bundle || parsed.fragment !== undefined || bundleRefToString(parsed) !== activation.ref) {
208
+ throw new Error("not canonical");
209
+ }
210
+ const key = `${activation.kind}\0${activation.ref}`;
211
+ if (activationKeys.has(key)) {
212
+ ctx.addIssue({
213
+ code: z.ZodIssueCode.custom,
214
+ path: ["scheduler", "enabled", index],
215
+ message: "duplicates an earlier scheduler activation",
216
+ });
217
+ }
218
+ activationKeys.add(key);
219
+ }
220
+ catch {
221
+ ctx.addIssue({
222
+ code: z.ZodIssueCode.custom,
223
+ path: ["scheduler", "enabled", index, "ref"],
224
+ message: "must be one canonical fully-qualified ref without a fragment",
225
+ });
226
+ }
171
227
  }
172
228
  for (const key of ["llm", "agent", "improve"]) {
173
229
  if (config.defaults && key in config.defaults) {
@@ -6,6 +6,7 @@
6
6
  * values from the current bundle map in an {@link AkmConfig}.
7
7
  */
8
8
  import { createHash } from "node:crypto";
9
+ import fs from "node:fs";
9
10
  import path from "node:path";
10
11
  import { ConfigError } from "../errors.js";
11
12
  /** The current one-component-per-bundle configuration entry, if present. */
@@ -33,6 +34,87 @@ export function bundleComponentConfig(bundle) {
33
34
  export function bundleContentRoot(entryPath, componentRoot) {
34
35
  return path.resolve(entryPath, componentRoot ?? ".");
35
36
  }
37
+ /**
38
+ * Physical identity for an already materialized filesystem bundle.
39
+ *
40
+ * Configuration may spell the same directory through relative paths or
41
+ * symbolic links. Those spellings are not distinct sources: treating them as
42
+ * distinct would make ownership, default-source trust, and scheduler grants
43
+ * depend on syntax instead of the directory the OS will actually read.
44
+ */
45
+ export function bundlePhysicalContentRoot(entryPath, componentRoot) {
46
+ const resolved = bundleContentRoot(entryPath, componentRoot);
47
+ try {
48
+ return fs.realpathSync.native(resolved);
49
+ }
50
+ catch (cause) {
51
+ // Cache-backed sources can be configured before they are materialized.
52
+ // Their source descriptor remains the identity until a real path exists.
53
+ if (cause.code === "ENOENT")
54
+ return resolved;
55
+ throw new ConfigError(`Unable to resolve physical bundle root ${JSON.stringify(resolved)}: ${cause instanceof Error ? cause.message : String(cause)}`, "INVALID_CONFIG_FILE");
56
+ }
57
+ }
58
+ /** Whether a configured bundle participates in reads, writes, and execution. */
59
+ export function isBundleEnabled(config, bundleId) {
60
+ const bundle = config.bundles?.[bundleId];
61
+ return bundle !== undefined && bundle.enabled !== false;
62
+ }
63
+ /**
64
+ * Stable identity of the origin currently installed under a bundle id.
65
+ *
66
+ * This deliberately excludes mutable policy (`enabled`, `writable`,
67
+ * credentials, adapter selection) and bundle contents. Ordinary updates and
68
+ * adapter auto-detection for the same origin keep their grant; changing the
69
+ * locator or component root does not.
70
+ */
71
+ export function bundleSourceId(config, bundleId) {
72
+ const bundle = config.bundles?.[bundleId];
73
+ if (!bundle) {
74
+ throw new ConfigError(`Bundle ${JSON.stringify(bundleId)} is not configured.`, "INVALID_CONFIG_FILE");
75
+ }
76
+ const component = bundleComponentConfig(bundle);
77
+ let source;
78
+ if (bundle.path !== undefined) {
79
+ source = { kind: "filesystem", locator: bundlePhysicalContentRoot(bundle.path, component?.root) };
80
+ }
81
+ else if (bundle.git !== undefined) {
82
+ source = { kind: "git", locator: normalizeInstalledGitRef("git", bundle.git) };
83
+ }
84
+ else if (bundle.website !== undefined) {
85
+ // Crawl/refresh settings are mutable fetch policy, not source identity.
86
+ // Changing them must not silently revoke a user's scheduling decision for
87
+ // the same website origin.
88
+ source = { kind: "website", url: bundle.website.url };
89
+ }
90
+ else if (bundle.npm !== undefined) {
91
+ source = { kind: "npm", locator: bundle.npm };
92
+ }
93
+ else {
94
+ throw new ConfigError(`Bundle ${JSON.stringify(bundleId)} has no source descriptor.`, "INVALID_CONFIG_FILE");
95
+ }
96
+ return hashBundleSourceIdentity({
97
+ version: 1,
98
+ source,
99
+ registryId: bundle.registryId ?? null,
100
+ componentRoot: component?.root ?? ".",
101
+ });
102
+ }
103
+ /** Identity for an environment-only filesystem bundle not persisted in config. */
104
+ export function filesystemBundleSourceId(contentRoot) {
105
+ return hashBundleSourceIdentity({
106
+ version: 1,
107
+ source: { kind: "filesystem", locator: bundlePhysicalContentRoot(contentRoot) },
108
+ registryId: null,
109
+ componentRoot: ".",
110
+ });
111
+ }
112
+ function hashBundleSourceIdentity(identity) {
113
+ return `sha256:${createHash("sha256")
114
+ .update("akm.bundle-source\0v1\0")
115
+ .update(JSON.stringify(identity))
116
+ .digest("hex")}`;
117
+ }
36
118
  /**
37
119
  * The resolved primary stash path — the `defaultBundle`'s filesystem `path`
38
120
  * (spec §10.1) — or `undefined` when no filesystem primary is configured.
@@ -68,13 +150,20 @@ export function bundleContentRoots(config) {
68
150
  for (const [id, entry] of Object.entries(bundles)) {
69
151
  if (typeof entry.path !== "string" || entry.path.length === 0)
70
152
  continue;
71
- out.push({ id, contentRoot: bundleContentRoot(entry.path, bundleComponentConfig(entry)?.root) });
153
+ out.push({ id, contentRoot: bundlePhysicalContentRoot(entry.path, bundleComponentConfig(entry)?.root) });
72
154
  }
73
155
  return out;
74
156
  }
75
157
  /** The bundle id whose resolved content root already matches `resolvedContentRoot`, if any. */
76
158
  export function bundleKeyForContentRoot(config, resolvedContentRoot) {
77
- return bundleContentRoots(config).find((entry) => entry.contentRoot === resolvedContentRoot)?.id;
159
+ let physical = path.resolve(resolvedContentRoot);
160
+ try {
161
+ physical = fs.realpathSync.native(physical);
162
+ }
163
+ catch {
164
+ // Compare the unresolved absolute locator when the candidate is not yet materialized.
165
+ }
166
+ return bundleContentRoots(config).find((entry) => entry.contentRoot === physical)?.id;
78
167
  }
79
168
  export function bundlesToSourceEntries(config) {
80
169
  const bundles = config.bundles;
@@ -96,6 +185,7 @@ export function bundleEntryToSourceEntry(key, bundle, isPrimary = false) {
96
185
  const base = {
97
186
  name: key,
98
187
  ...(bundle.writable !== undefined ? { writable: bundle.writable } : {}),
188
+ ...(bundle.credential !== undefined ? { credential: bundle.credential } : {}),
99
189
  ...(bundle.enabled !== undefined ? { enabled: bundle.enabled } : {}),
100
190
  ...(isPrimary ? { primary: true } : {}),
101
191
  };
@@ -235,6 +325,10 @@ export function resolveConfiguredSources(config) {
235
325
  }
236
326
  return out;
237
327
  }
328
+ /** Active sources in the same deterministic order as `resolveConfiguredSources`. */
329
+ export function resolveActiveConfiguredSources(config) {
330
+ return resolveConfiguredSources(config).filter((source) => source.enabled !== false);
331
+ }
238
332
  function toConfiguredSource(persisted, isPrimary) {
239
333
  const source = parseSourceSpec(persisted);
240
334
  if (!source)