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.
- package/CHANGELOG.md +40 -132
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -4
- package/dist/cli/unknown-flags.js +3 -36
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +4 -6
- package/dist/commands/improve/improve-cli.js +20 -15
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/repository.js +3 -12
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/curate.js +44 -34
- package/dist/commands/read/search.js +35 -54
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +55 -61
- package/dist/commands/sources/source-add.js +39 -38
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-cli.js +111 -119
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +8 -35
- package/dist/core/adapter/adapters/akm-metadata.js +1 -11
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/asset/asset-placement.js +0 -35
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/embedding.js +30 -7
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +10 -12
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/errors.js +2 -5
- package/dist/core/maintenance-barrier.js +11 -13
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +2 -5
- package/dist/core/state/migrations.js +1 -26
- package/dist/core/state-db.js +27 -63
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/ensure-index.js +0 -5
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-written-assets.js +171 -133
- package/dist/indexer/indexer.js +1621 -458
- package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
- package/dist/indexer/materialize-embeddings.js +785 -0
- package/dist/indexer/passes/dir-staleness.js +161 -0
- package/dist/indexer/passes/metadata.js +1 -18
- package/dist/indexer/scan/drain-dir.js +70 -27
- package/dist/indexer/search/db-search.js +89 -373
- package/dist/indexer/search/ranking-contributors.js +16 -21
- package/dist/indexer/search/ranking.js +57 -135
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +3 -11
- package/dist/llm/embedder.js +3 -10
- package/dist/llm/embedders/remote.js +104 -133
- package/dist/llm/feature-gate.js +2 -4
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/command-format.js +13 -19
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +2 -5
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +15197 -11351
- package/dist/scripts/akm-migrate.js +15514 -11668
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/connection.js +2 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/index-connection.js +3 -1
- package/dist/storage/repositories/index-entries-repository.js +68 -77
- package/dist/storage/repositories/index-entry-schema.js +25 -16
- package/dist/storage/repositories/index-fts-repository.js +263 -29
- package/dist/storage/repositories/index-meta-repository.js +29 -0
- package/dist/storage/repositories/index-schema.js +122 -115
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +435 -22
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.15.md +36 -34
- package/docs/migration/release-notes/0.9.16.md +60 -98
- package/docs/migration/release-notes/README.md +0 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +124 -122
- package/docs/reference/configuration.md +137 -133
- package/docs/reference/data-and-telemetry.md +1 -2
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +170 -6
- package/schemas/akm-task.json +1 -2
- package/dist/commands/sources/index-status.js +0 -99
- package/dist/core/hash.js +0 -18
- package/dist/indexer/drain.js +0 -306
- package/dist/indexer/embedding-identity.js +0 -20
- package/dist/indexer/enrich.js +0 -260
- package/dist/indexer/reconcile.js +0 -890
- package/dist/indexer/scan/parse-file.js +0 -66
- package/dist/indexer/units/unit.js +0 -159
- package/dist/llm/embedders/provider-limits.js +0 -288
- package/dist/storage/repositories/files-repository.js +0 -181
- 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
|
|
13
|
-
* (`src/tasks/source/parse-task-source.ts`)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
-
* - `
|
|
52
|
-
*
|
|
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
|
|
62
|
-
*
|
|
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 {
|
|
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
|
|
165
|
-
//
|
|
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:
|
|
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-
|
|
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
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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:
|
|
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
|
-
|
|
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)
|