akm-cli 0.9.14 → 0.9.15-beta.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 +559 -0
- package/STABILITY.md +6 -3
- package/dist/akm +54 -1
- package/dist/akm-migrate +34 -1
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
- package/dist/assets/tasks/core/improve.yml +1 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
- package/dist/cli/retired-commands.js +0 -1
- package/dist/cli/shared.js +9 -0
- package/dist/cli/unknown-flags.js +1 -0
- package/dist/cli.js +40 -3
- package/dist/commands/config-cli.js +85 -3
- package/dist/commands/env/env-cli.js +1 -42
- package/dist/commands/env/env.js +1 -1
- package/dist/commands/env/secret-cli.js +1 -2
- package/dist/commands/health/checks.js +357 -63
- package/dist/commands/health/engine-usage.js +45 -0
- package/dist/commands/health/improve-metrics.js +18 -0
- package/dist/commands/health/llm-usage.js +41 -1
- package/dist/commands/health/plugin-staleness.js +7 -3
- package/dist/commands/health/version-drift.js +93 -0
- package/dist/commands/health/windows.js +3 -1
- package/dist/commands/health.js +44 -9
- package/dist/commands/improve/consolidate/chunking.js +4 -2
- package/dist/commands/improve/improve-cli.js +99 -5
- package/dist/commands/improve/improve-report.js +154 -0
- package/dist/commands/improve/improve-result-file.js +45 -33
- package/dist/commands/improve/improve-strategies.js +133 -3
- package/dist/commands/improve/improve-usage-report.js +182 -0
- package/dist/commands/improve/improve.js +40 -3
- package/dist/commands/improve/locks.js +28 -78
- package/dist/commands/improve/planner.js +1 -0
- package/dist/commands/improve/preparation.js +9 -1
- package/dist/commands/improve/reflect.js +44 -4
- package/dist/commands/models-cli.js +50 -1
- package/dist/commands/proposal/repository.js +8 -3
- package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
- package/dist/commands/proposal/validators/proposal-validators.js +24 -0
- package/dist/commands/read/search-cli.js +38 -2
- package/dist/commands/read/show.js +103 -4
- package/dist/commands/sources/info.js +5 -1
- package/dist/commands/sources/installed-stashes.js +58 -16
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +48 -0
- package/dist/commands/tasks/tasks-cli.js +49 -2
- package/dist/commands/workflow-cli.js +86 -12
- package/dist/core/asset/markdown-fragments.js +35 -0
- package/dist/core/config/config-schema.js +14 -0
- package/dist/core/config/config.js +302 -24
- package/dist/core/config/schema/embedding.js +41 -0
- package/dist/core/env-secret-ref.js +58 -5
- package/dist/core/errors.js +30 -0
- package/dist/core/file-lock.js +49 -15
- package/dist/core/improve-result.js +51 -0
- package/dist/core/loopback.js +17 -0
- package/dist/core/parent-watchdog.js +64 -0
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +107 -0
- package/dist/core/sensitive-marker-path.js +19 -0
- package/dist/core/state-db.js +74 -14
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-writer-lock.js +40 -1
- package/dist/indexer/index-written-assets.js +29 -1
- package/dist/indexer/indexer.js +93 -29
- package/dist/indexer/materialize-embeddings.js +564 -48
- package/dist/indexer/search/db-search.js +49 -2
- package/dist/indexer/search/search-source.js +23 -1
- package/dist/integrations/agent/engine-resolution.js +96 -6
- package/dist/integrations/agent/execution-definitions.js +6 -15
- package/dist/integrations/agent/execution-lowering.js +6 -1
- package/dist/integrations/agent/execution-preparation.js +1 -1
- package/dist/integrations/agent/model-map.js +123 -20
- package/dist/integrations/agent/prompts.js +40 -8
- package/dist/integrations/agent/runner-dispatch.js +9 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +8 -3
- package/dist/llm/embedder.js +20 -8
- package/dist/llm/embedders/local.js +10 -2
- package/dist/llm/embedders/remote.js +497 -32
- package/dist/output/shapes/helpers.js +38 -2
- package/dist/output/shapes/models-list.js +16 -0
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/shapes.js +4 -0
- package/dist/output/text/command-format.js +29 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/improve-report.js +27 -0
- package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
- package/dist/output/text/show-format.js +4 -0
- package/dist/output/text.js +4 -0
- package/dist/scripts/akm-migrate-node.js +25146 -21759
- package/dist/scripts/akm-migrate.js +24271 -20885
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/improve-runs-repository.js +34 -0
- package/dist/storage/repositories/index-fts-repository.js +49 -6
- package/dist/storage/repositories/index-schema.js +16 -0
- package/dist/storage/repositories/index-vec-repository.js +30 -0
- package/dist/storage/repositories/workflow-runs-repository.js +55 -18
- package/dist/tasks/backends/cron.js +14 -7
- package/dist/tasks/run/run-native-task.js +23 -1
- package/dist/tasks/run/run-workflow-task.js +16 -0
- package/dist/workflows/exec/child-workflow.js +2 -2
- package/dist/workflows/exec/dispatch-redaction.js +21 -9
- package/dist/workflows/exec/run-workflow.js +6 -5
- package/dist/workflows/runtime/runs.js +33 -5
- package/docs/migration/release-notes/0.9.15.md +133 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +271 -30
- package/docs/reference/configuration.md +234 -21
- package/docs/reference/data-and-telemetry.md +8 -0
- package/docs/reference/tasks.md +16 -1
- package/docs/reference/workflow-schema.md +5 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +47 -0
|
@@ -33,10 +33,51 @@ export const EmbeddingConnectionConfigSchema = z
|
|
|
33
33
|
// `akm index` when ensureSchema rejects it (§24.2 "Semantic" gate).
|
|
34
34
|
dimension: positiveInt.max(4096).optional(),
|
|
35
35
|
localModel: z.string().min(1).optional(),
|
|
36
|
+
/**
|
|
37
|
+
* Per-document token cap applied BEFORE batching (default 512,
|
|
38
|
+
* `DEFAULT_MAX_INPUT_TOKENS` in `src/llm/embedders/remote.ts`, #956).
|
|
39
|
+
* The materializer truncates a document's embedded text to
|
|
40
|
+
* this cap (head only, unicode-safe) instead of skipping it outright, so
|
|
41
|
+
* one oversized entry can no longer fail a whole batch. Distinct from
|
|
42
|
+
* `maxTokens` below, which bounds a whole HTTP REQUEST (many documents);
|
|
43
|
+
* this bounds one DOCUMENT.
|
|
44
|
+
*/
|
|
45
|
+
maxInputTokens: positiveInt.optional(),
|
|
46
|
+
/**
|
|
47
|
+
* Client-side per-request token budget — how many documents' estimated
|
|
48
|
+
* tokens fit in one HTTP request (default `DEFAULT_TOKEN_BUDGET` = 8000
|
|
49
|
+
* in `src/llm/embedders/remote.ts`). With the 512-token `maxInputTokens`
|
|
50
|
+
* cap above, a request carries about 16 documents by default.
|
|
51
|
+
*/
|
|
36
52
|
maxTokens: positiveInt.optional(),
|
|
37
53
|
batchSize: positiveInt.optional(),
|
|
38
54
|
chunkSize: positiveInt.optional(),
|
|
55
|
+
/**
|
|
56
|
+
* Ollama's `num_ctx` ONLY (#956) — sent verbatim as
|
|
57
|
+
* `options.num_ctx` on the native `/api/embed` request. It no longer also
|
|
58
|
+
* feeds the client-side request token budget (`maxTokens` above): the two
|
|
59
|
+
* used to share this one field, so setting it for the server's context
|
|
60
|
+
* window silently changed request batching too.
|
|
61
|
+
*/
|
|
39
62
|
contextLength: positiveInt.optional(),
|
|
40
63
|
ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
|
|
64
|
+
/**
|
|
65
|
+
* Per-request timeout in milliseconds for a remote embedding request
|
|
66
|
+
* (default 120_000, `DEFAULT_EMBEDDING_TIMEOUT_MS` in
|
|
67
|
+
* `src/llm/embedders/remote.ts`). The prior fixed 30s cut off a slow
|
|
68
|
+
* local model server on a large token-bounded batch mid-response, with
|
|
69
|
+
* no retry — every batch that hit it was silently dropped (#954).
|
|
70
|
+
*/
|
|
71
|
+
timeoutMs: positiveInt.optional(),
|
|
72
|
+
/**
|
|
73
|
+
* Overrides the fixed in-flight request window (#954, added after field
|
|
74
|
+
* evidence from multi-slot local servers). Bounded 1-16. Unset keeps
|
|
75
|
+
* today's default: 1 for a loopback endpoint, 2 for a remote one
|
|
76
|
+
* (`resolveEmbeddingConcurrency`, `src/llm/embedders/remote.ts`). Set it
|
|
77
|
+
* only for an endpoint that genuinely serves parallel requests (llama.cpp
|
|
78
|
+
* `--parallel N`, vLLM) — request SIZE (`batchSize`, `maxTokens`/
|
|
79
|
+
* `contextLength`) remains the first throughput lever.
|
|
80
|
+
*/
|
|
81
|
+
concurrency: positiveInt.max(16).optional(),
|
|
41
82
|
})
|
|
42
83
|
.passthrough();
|
|
@@ -16,13 +16,60 @@ import path from "node:path";
|
|
|
16
16
|
import { resolveSourceEntries } from "../indexer/search/search-source.js";
|
|
17
17
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
18
18
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "./asset/asset-create.js";
|
|
19
|
-
import { assetPathForName } from "./asset/asset-placement.js";
|
|
19
|
+
import { assetPathForName, deriveCanonicalAssetName } from "./asset/asset-placement.js";
|
|
20
20
|
import { displayRef, isFullRefInput, parseRefInput } from "./asset/resolve-ref.js";
|
|
21
21
|
import { isWithin } from "./common.js";
|
|
22
22
|
import { loadConfig } from "./config/config.js";
|
|
23
23
|
import { NotFoundError, UsageError } from "./errors.js";
|
|
24
24
|
import { resolveMutationTarget } from "./mutation-target.js";
|
|
25
|
+
import { sensitiveMarkerPath } from "./sensitive-marker-path.js";
|
|
25
26
|
import { formatRefForMessage, withWriteTargetMutation } from "./write-source.js";
|
|
27
|
+
export { sensitiveMarkerPath } from "./sensitive-marker-path.js";
|
|
28
|
+
/**
|
|
29
|
+
* Walk each stash's env files and return one entry per `.env` file, using the
|
|
30
|
+
* env asset spec's canonical-name logic (e.g. `env/team/prod.env` →
|
|
31
|
+
* `env/team/prod`, `env/team/.env` → `env/team/default`). Moved here from
|
|
32
|
+
* `commands/env/env-cli.ts` (#950) so `commands/health` can reuse it to name
|
|
33
|
+
* which env asset supplies a credential's variable — pure move, `akm env
|
|
34
|
+
* list`'s behaviour is unchanged. `listKeysFn` stays injected (rather than a
|
|
35
|
+
* static import of `commands/env/env.ts`) so this core module never depends
|
|
36
|
+
* upward on the commands layer. `config` defaults to the real `loadConfig()`
|
|
37
|
+
* (unchanged default for `akm env list`); `commands/health` passes its own
|
|
38
|
+
* injected/resolved config so a test-supplied `loadConfig` seam is honoured
|
|
39
|
+
* instead of this always re-reading the real config/sources.
|
|
40
|
+
*/
|
|
41
|
+
export function listEnvsRecursive(listKeysFn, config = loadConfig()) {
|
|
42
|
+
const result = [];
|
|
43
|
+
for (const source of resolveSourceEntries(undefined, config)) {
|
|
44
|
+
const root = path.join(source.path, "env");
|
|
45
|
+
if (!fs.existsSync(root))
|
|
46
|
+
continue;
|
|
47
|
+
const walk = (dir) => {
|
|
48
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const full = path.join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
walk(full);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!entry.isFile())
|
|
55
|
+
continue;
|
|
56
|
+
if (entry.name !== ".env" && !entry.name.endsWith(".env"))
|
|
57
|
+
continue;
|
|
58
|
+
const canonical = deriveCanonicalAssetName("env", root, full);
|
|
59
|
+
if (!canonical)
|
|
60
|
+
continue;
|
|
61
|
+
// Skip sensitive envs: a sibling .sensitive marker file suppresses listing.
|
|
62
|
+
const markerPath = sensitiveMarkerPath(full, "env");
|
|
63
|
+
if (fs.existsSync(markerPath))
|
|
64
|
+
continue;
|
|
65
|
+
const { keys } = listKeysFn(full);
|
|
66
|
+
result.push({ ref: makeEnvRef(canonical, source, config), path: full, keys });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
walk(root);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
26
73
|
/**
|
|
27
74
|
* The `vault` asset type was removed in 0.9.0. The env/secret input path no
|
|
28
75
|
* longer routes through the legacy stored-ref parser (which carries the removal
|
|
@@ -91,10 +138,17 @@ export function findEnvSource(origin, type, name) {
|
|
|
91
138
|
}
|
|
92
139
|
return named;
|
|
93
140
|
}
|
|
94
|
-
|
|
141
|
+
/**
|
|
142
|
+
* `config` defaults to the real `loadConfig()`, same as {@link displayDefaultBundle}.
|
|
143
|
+
* `listEnvsRecursive` threads its own (possibly injected) config through here so a
|
|
144
|
+
* caller-supplied config governs ref display the same way it governs which stashes
|
|
145
|
+
* get walked — otherwise this would silently fall back to reading the real config a
|
|
146
|
+
* second time even when the walk itself honoured an injected one.
|
|
147
|
+
*/
|
|
148
|
+
export function makeEnvRef(name, source, config = loadConfig()) {
|
|
95
149
|
// F4b output-spelling flip: `env/name` in the primary stash, `bundle//env/name`
|
|
96
150
|
// for a slug-clean named source.
|
|
97
|
-
return displayRef({ type: "env", name, bundleId: source?.registryId }, displayDefaultBundle(source));
|
|
151
|
+
return displayRef({ type: "env", name, bundleId: source?.registryId }, displayDefaultBundle(source, config));
|
|
98
152
|
}
|
|
99
153
|
/**
|
|
100
154
|
* Resolve an env ref to an absolute `.env` path. Accepts the `env/<name>`
|
|
@@ -132,8 +186,7 @@ export function makeSecretRef(name, source) {
|
|
|
132
186
|
// `bundle//secrets/name` for a slug-clean named source.
|
|
133
187
|
return displayRef({ type: "secret", name, bundleId: source?.registryId }, displayDefaultBundle(source));
|
|
134
188
|
}
|
|
135
|
-
function displayDefaultBundle(source) {
|
|
136
|
-
const config = loadConfig();
|
|
189
|
+
function displayDefaultBundle(source, config = loadConfig()) {
|
|
137
190
|
if (config.defaultBundle || !source)
|
|
138
191
|
return config.defaultBundle;
|
|
139
192
|
const primary = resolveSourceEntries(undefined, config)[0];
|
package/dist/core/errors.js
CHANGED
|
@@ -77,7 +77,11 @@ const USAGE_HINTS = {
|
|
|
77
77
|
WORKFLOW_IR_VERSION_UNSUPPORTED: "Abandon the run with `akm workflow abandon <id>`, then start it again from the workflow source — a frozen plan this akm cannot execute is not re-executable in place.",
|
|
78
78
|
// P3b (docs/plans/specs/p3b-child-executor.md §4.3).
|
|
79
79
|
WORKFLOW_OUTPUT_INVALID: "Check each `outputs:` entry's `from:` against the step artifact it names, and its `schema:` against the value that step actually promotes.",
|
|
80
|
+
};
|
|
81
|
+
/** Default hint for each TransientErrorCode. */
|
|
82
|
+
const TRANSIENT_HINTS = {
|
|
80
83
|
RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
|
|
84
|
+
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
|
|
81
85
|
};
|
|
82
86
|
/** Default hint for each NotFoundError code. */
|
|
83
87
|
const NOT_FOUND_HINTS = {
|
|
@@ -89,6 +93,7 @@ const NOT_FOUND_HINTS = {
|
|
|
89
93
|
// for a mistyped id, which points at the wrong thing entirely.
|
|
90
94
|
PROPOSAL_NOT_FOUND: "Run `akm proposal list` to see pending proposals and their ids.",
|
|
91
95
|
FILE_NOT_FOUND: "Check the path exists and is readable.",
|
|
96
|
+
IMPROVE_RUN_NOT_FOUND: "Run `akm improve` first, or `akm improve report --since 30d` to see recent run ids in `runIds`.",
|
|
92
97
|
};
|
|
93
98
|
/**
|
|
94
99
|
* Base class for all akm-thrown, classified errors. Carries the `kind`
|
|
@@ -131,6 +136,31 @@ export class UsageError extends AkmError {
|
|
|
131
136
|
return this._hint ?? USAGE_HINTS[this.code];
|
|
132
137
|
}
|
|
133
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Raised when a condition is ordinary, retryable contention rather than a
|
|
141
|
+
* bad command line or a genuine failure — another akm process holds a lock
|
|
142
|
+
* or is writing state.db right now. Distinct from `UsageError` (#948
|
|
143
|
+
* addendum, dev-team field review 2026-09-09): schedulers classify exit 2 as
|
|
144
|
+
* "fix the command line", so contention needs its own exit code (75,
|
|
145
|
+
* sysexits EX_TEMPFAIL) a cron wrapper can branch on to retry instead of
|
|
146
|
+
* alerting.
|
|
147
|
+
*/
|
|
148
|
+
export class TransientError extends AkmError {
|
|
149
|
+
kind = "transient";
|
|
150
|
+
code;
|
|
151
|
+
_hint;
|
|
152
|
+
constructor(msg, code, hint) {
|
|
153
|
+
super(msg);
|
|
154
|
+
this.name = "TransientError";
|
|
155
|
+
this.code = code;
|
|
156
|
+
this._hint = hint;
|
|
157
|
+
// Fixes `instanceof` checks under ES5 transpilation targets.
|
|
158
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
159
|
+
}
|
|
160
|
+
hint() {
|
|
161
|
+
return this._hint ?? TRANSIENT_HINTS[this.code];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
134
164
|
/** Raised when a requested resource (asset, entry, file) is not found. */
|
|
135
165
|
export class NotFoundError extends AkmError {
|
|
136
166
|
kind = "not-found";
|
package/dist/core/file-lock.js
CHANGED
|
@@ -120,9 +120,36 @@ function releaseLockRaw(lockPath) {
|
|
|
120
120
|
export function tryAcquireLockSync(lockPath, payload) {
|
|
121
121
|
return withLockOperationMutex(lockPath, () => tryAcquireLockRaw(lockPath, payload));
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Best-effort launcher pid from `AKM_LAUNCHER_PID` (set by
|
|
125
|
+
* `scripts/node-runtime/akm`/`akm-migrate`, #956) — undefined when unset,
|
|
126
|
+
* empty, or not a positive integer (never trust an ambient env var blindly
|
|
127
|
+
* into a lock message). Also the gate the parent-death watchdog
|
|
128
|
+
* (`core/parent-watchdog.ts`) uses to stay inert outside a launcher-managed
|
|
129
|
+
* run.
|
|
130
|
+
*/
|
|
131
|
+
export function launcherPidFromEnv() {
|
|
132
|
+
const raw = process.env.AKM_LAUNCHER_PID;
|
|
133
|
+
if (!raw)
|
|
134
|
+
return undefined;
|
|
135
|
+
const pid = Number.parseInt(raw, 10);
|
|
136
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build a PID-bearing payload with a unique token for one acquisition attempt.
|
|
140
|
+
* Adds `launcherPid` (#956) whenever this process is running under the
|
|
141
|
+
* published launcher, so a lock's holder can be identified by the pid every
|
|
142
|
+
* process listing and task log actually shows (the launcher's) as well as
|
|
143
|
+
* the pid that holds the lock (the bun/node child).
|
|
144
|
+
*/
|
|
124
145
|
export function createLockPayload(metadata = {}) {
|
|
125
|
-
|
|
146
|
+
const launcherPid = launcherPidFromEnv();
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
...metadata,
|
|
149
|
+
pid: process.pid,
|
|
150
|
+
...(launcherPid !== undefined ? { launcherPid } : {}),
|
|
151
|
+
lockId: randomUUID(),
|
|
152
|
+
});
|
|
126
153
|
}
|
|
127
154
|
/**
|
|
128
155
|
* Inspect an existing sentinel at `lockPath` without modifying it.
|
|
@@ -153,17 +180,17 @@ export function probeLock(lockPath, opts) {
|
|
|
153
180
|
return { state: "absent" };
|
|
154
181
|
const { rawContent, identity } = snapshot;
|
|
155
182
|
const ageMs = Date.now() - identity.mtimeMs;
|
|
156
|
-
const holderPid =
|
|
183
|
+
const { holderPid, launcherPid } = extractLockIdentity(rawContent);
|
|
157
184
|
if (holderPid === undefined) {
|
|
158
185
|
return { state: "stale", reason: "invalid_pid", ageMs, rawContent, identity };
|
|
159
186
|
}
|
|
160
187
|
if (!isProcessAlive(holderPid)) {
|
|
161
|
-
return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent, identity };
|
|
188
|
+
return { state: "stale", reason: "pid_dead", holderPid, launcherPid, ageMs, rawContent, identity };
|
|
162
189
|
}
|
|
163
190
|
if (opts?.staleAfterMs !== undefined && ageMs > opts.staleAfterMs) {
|
|
164
|
-
return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent, identity };
|
|
191
|
+
return { state: "stale", reason: "age_exceeded", holderPid, launcherPid, ageMs, rawContent, identity };
|
|
165
192
|
}
|
|
166
|
-
return { state: "held", holderPid, ageMs, rawContent, identity };
|
|
193
|
+
return { state: "held", holderPid, launcherPid, ageMs, rawContent, identity };
|
|
167
194
|
}
|
|
168
195
|
/**
|
|
169
196
|
* Revalidate and quarantine the probed sentinel while holding the same operation
|
|
@@ -254,25 +281,32 @@ export function releaseLock(ownership) {
|
|
|
254
281
|
});
|
|
255
282
|
}
|
|
256
283
|
/**
|
|
257
|
-
* Extract a
|
|
258
|
-
*
|
|
259
|
-
* a
|
|
260
|
-
*
|
|
284
|
+
* Extract a holder pid, and (#956) a launcher pid when the payload recorded
|
|
285
|
+
* one, from a sentinel body. Accepts the two shapes used across the
|
|
286
|
+
* codebase: a bare numeric string (config-io, vault, lockfile — never
|
|
287
|
+
* carries a `launcherPid`) and a JSON object with `pid`/`launcherPid` fields
|
|
288
|
+
* (`createLockPayload`). `holderPid` is undefined when the body is
|
|
289
|
+
* unparseable or yields a non-positive integer; `launcherPid` is undefined
|
|
290
|
+
* whenever the payload has none, independent of whether `holderPid` parsed.
|
|
261
291
|
*/
|
|
262
|
-
function
|
|
292
|
+
function extractLockIdentity(content) {
|
|
263
293
|
const trimmed = content.trim();
|
|
264
294
|
if (!trimmed)
|
|
265
|
-
return
|
|
295
|
+
return {};
|
|
266
296
|
if (trimmed.startsWith("{")) {
|
|
267
297
|
try {
|
|
268
298
|
const parsed = JSON.parse(trimmed);
|
|
269
299
|
const pid = typeof parsed.pid === "number" ? parsed.pid : Number.NaN;
|
|
270
|
-
|
|
300
|
+
const rawLauncherPid = typeof parsed.launcherPid === "number" ? parsed.launcherPid : Number.NaN;
|
|
301
|
+
return {
|
|
302
|
+
holderPid: Number.isInteger(pid) && pid > 0 ? pid : undefined,
|
|
303
|
+
launcherPid: Number.isInteger(rawLauncherPid) && rawLauncherPid > 0 ? rawLauncherPid : undefined,
|
|
304
|
+
};
|
|
271
305
|
}
|
|
272
306
|
catch {
|
|
273
|
-
return
|
|
307
|
+
return {};
|
|
274
308
|
}
|
|
275
309
|
}
|
|
276
310
|
const pid = Number.parseInt(trimmed, 10);
|
|
277
|
-
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
311
|
+
return { holderPid: Number.isInteger(pid) && pid > 0 ? pid : undefined };
|
|
278
312
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { cloneExecutionJsonObject } from "../execution/json.js";
|
|
5
5
|
import { isRecord } from "./common.js";
|
|
6
|
+
import { IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./config/engine-semantics.js";
|
|
6
7
|
const COMMON_FIELDS = [
|
|
7
8
|
"schemaVersion",
|
|
8
9
|
"ok",
|
|
@@ -17,6 +18,7 @@ const COMMON_FIELDS = [
|
|
|
17
18
|
"plannedRefs",
|
|
18
19
|
"plan",
|
|
19
20
|
"actions",
|
|
21
|
+
"skippedProcesses",
|
|
20
22
|
"distillSkipped",
|
|
21
23
|
"validationFailures",
|
|
22
24
|
"schemaRepairs",
|
|
@@ -47,6 +49,7 @@ const COMMON_FIELDS = [
|
|
|
47
49
|
"sync",
|
|
48
50
|
"writtenPaths",
|
|
49
51
|
"terminated",
|
|
52
|
+
"usageReport",
|
|
50
53
|
];
|
|
51
54
|
const V2_FIELDS = new Set([...COMMON_FIELDS, "strategy", "strategyFilteredRefs"]);
|
|
52
55
|
function fail(message) {
|
|
@@ -179,6 +182,50 @@ function validateProactivePlan(value) {
|
|
|
179
182
|
fail("plan.proactive.selected must equal plan.proactive.selectedRefs.length");
|
|
180
183
|
}
|
|
181
184
|
}
|
|
185
|
+
/** #947 — plan.processes: one row per IMPROVE_PROCESS_ENGINE_CAPABILITIES name, plus an optional "triage.judgment" row. */
|
|
186
|
+
function validateProcessRoutingRows(value) {
|
|
187
|
+
if (!Array.isArray(value))
|
|
188
|
+
fail("plan.processes must be an array");
|
|
189
|
+
const canonicalNames = Object.keys(IMPROVE_PROCESS_ENGINE_CAPABILITIES);
|
|
190
|
+
const engineKinds = new Set(["llm", "agent", "sdk"]);
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
for (const row of value) {
|
|
193
|
+
if (!isRecord(row))
|
|
194
|
+
fail("plan.processes entries must be objects");
|
|
195
|
+
requireExactFields(row, new Set(["process", "enabled", "engine", "model", "engineKind", "notices", "unavailable", "eligibleRefs"]));
|
|
196
|
+
if (typeof row.process !== "string" ||
|
|
197
|
+
!(canonicalNames.includes(row.process) || row.process === "triage.judgment")) {
|
|
198
|
+
fail("plan.processes.process is invalid");
|
|
199
|
+
}
|
|
200
|
+
if (seen.has(row.process))
|
|
201
|
+
fail(`plan.processes must not repeat "${row.process}"`);
|
|
202
|
+
seen.add(row.process);
|
|
203
|
+
if (typeof row.enabled !== "boolean")
|
|
204
|
+
fail("plan.processes.enabled must be a boolean");
|
|
205
|
+
if (row.engine !== undefined && typeof row.engine !== "string")
|
|
206
|
+
fail("plan.processes.engine must be a string");
|
|
207
|
+
if (row.model !== undefined && typeof row.model !== "string")
|
|
208
|
+
fail("plan.processes.model must be a string");
|
|
209
|
+
if (row.engineKind !== undefined && (typeof row.engineKind !== "string" || !engineKinds.has(row.engineKind))) {
|
|
210
|
+
fail("plan.processes.engineKind is invalid");
|
|
211
|
+
}
|
|
212
|
+
validateLoweringNotices(row.notices);
|
|
213
|
+
if (row.unavailable !== undefined) {
|
|
214
|
+
if (!isRecord(row.unavailable))
|
|
215
|
+
fail("plan.processes.unavailable must be an object");
|
|
216
|
+
requireExactFields(row.unavailable, new Set(["configKey", "reason"]));
|
|
217
|
+
if (typeof row.unavailable.configKey !== "string" || typeof row.unavailable.reason !== "string") {
|
|
218
|
+
fail("plan.processes.unavailable must contain string configKey and reason");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (row.eligibleRefs !== undefined)
|
|
222
|
+
requireCount(row, "eligibleRefs", "plan.processes entry");
|
|
223
|
+
}
|
|
224
|
+
for (const name of canonicalNames) {
|
|
225
|
+
if (!seen.has(name))
|
|
226
|
+
fail(`plan.processes must contain exactly one "${name}" row`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
182
229
|
function validateImprovePlan(value, dryRun, plannedRefNames) {
|
|
183
230
|
if (!isRecord(value))
|
|
184
231
|
fail("plan must be an object");
|
|
@@ -190,6 +237,7 @@ function validateImprovePlan(value, dryRun, plannedRefNames) {
|
|
|
190
237
|
"limits",
|
|
191
238
|
"gates",
|
|
192
239
|
"effectiveRefs",
|
|
240
|
+
"processes",
|
|
193
241
|
"proactive",
|
|
194
242
|
"consolidation",
|
|
195
243
|
"stages",
|
|
@@ -328,6 +376,7 @@ function validateImprovePlan(value, dryRun, plannedRefNames) {
|
|
|
328
376
|
if (value.limits.totalCeiling !== undefined && value.effectiveRefs.length > value.limits.totalCeiling) {
|
|
329
377
|
fail("plan.effectiveRefs cannot exceed plan.limits.totalCeiling");
|
|
330
378
|
}
|
|
379
|
+
validateProcessRoutingRows(value.processes);
|
|
331
380
|
if (value.proactive !== undefined)
|
|
332
381
|
validateProactivePlan(value.proactive);
|
|
333
382
|
validateConsolidationPlan(value.consolidation);
|
|
@@ -392,6 +441,7 @@ function validateCommon(value) {
|
|
|
392
441
|
}
|
|
393
442
|
for (const field of [
|
|
394
443
|
"actions",
|
|
444
|
+
"skippedProcesses",
|
|
395
445
|
"validationFailures",
|
|
396
446
|
"schemaRepairs",
|
|
397
447
|
"extract",
|
|
@@ -442,6 +492,7 @@ function validateCommon(value) {
|
|
|
442
492
|
"terminated",
|
|
443
493
|
"plan",
|
|
444
494
|
"deadUrlCoverage",
|
|
495
|
+
"usageReport",
|
|
445
496
|
]) {
|
|
446
497
|
if (value[field] !== undefined && !isRecord(value[field]))
|
|
447
498
|
fail(`${field} must be an object`);
|
package/dist/core/loopback.js
CHANGED
|
@@ -87,3 +87,20 @@ export function isLoopbackEndpoint(endpoint) {
|
|
|
87
87
|
return true;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The lowest-common-denominator concurrency default for an LLM/embedding
|
|
92
|
+
* endpoint: 1 for a loopback endpoint (a local model server serves one
|
|
93
|
+
* inference at a time; concurrent requests thrash it — reload thrash, HTTP
|
|
94
|
+
* 500 "Model reloaded"), 2 for a remote one (enough to overlap request
|
|
95
|
+
* latency without hammering a rate-limited API). A leaf helper so both
|
|
96
|
+
* callers — the indexer's LLM enrichment pool (`getDefaultLlmConcurrency`,
|
|
97
|
+
* `src/indexer/indexer.ts`) and the embedding pool
|
|
98
|
+
* (`resolveEmbeddingConcurrency`, `src/llm/embedders/remote.ts`) — share one
|
|
99
|
+
* definition instead of mirroring it; `src/llm/embedders/remote.ts` cannot
|
|
100
|
+
* import `getDefaultLlmConcurrency` directly (`src/indexer/indexer.ts`
|
|
101
|
+
* already depends on this module transitively through
|
|
102
|
+
* materialize-embeddings.ts).
|
|
103
|
+
*/
|
|
104
|
+
export function defaultConcurrencyForEndpoint(endpoint) {
|
|
105
|
+
return isLoopbackEndpoint(endpoint) ? 1 : 2;
|
|
106
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Parent-death watchdog (#956).
|
|
6
|
+
*
|
|
7
|
+
* The published launcher (`scripts/node-runtime/akm`) now forwards SIGTERM to
|
|
8
|
+
* its child, so a `kill <launcher-pid>` no longer orphans it — but a launcher
|
|
9
|
+
* that dies WITHOUT delivering a signal (SIGKILL, an out-of-memory kill, a
|
|
10
|
+
* supervisor that force-removes the process) still reparents the child to
|
|
11
|
+
* init with nothing to catch. Field evidence: 40 orphaned
|
|
12
|
+
* `bun …/dist/cli.js` processes over one day, up to 10h old, some created by
|
|
13
|
+
* a curate hook killing its own launcher on timeout — every akm invocation
|
|
14
|
+
* under the launcher is exposed to this, not only `akm index`, so this
|
|
15
|
+
* watchdog runs for every command (wired at the CLI entry point in
|
|
16
|
+
* `src/cli.ts`), not just index.
|
|
17
|
+
*
|
|
18
|
+
* `process.ppid` on POSIX changes the instant the parent exits (the child is
|
|
19
|
+
* reparented, usually to pid 1) — polling it is the standard orphan-detection
|
|
20
|
+
* trick when no direct death notification exists. This module owns only the
|
|
21
|
+
* poll/compare mechanics; the caller's `onOrphaned` decides what "stop"
|
|
22
|
+
* means. `src/cli.ts` wires it to `process.kill(process.pid, "SIGTERM")`,
|
|
23
|
+
* reusing the same self-signal every command already has rather than a
|
|
24
|
+
* second, parallel abort path. Only `akm index`/`improve` register their own
|
|
25
|
+
* SIGTERM listener (the index command's AbortController in
|
|
26
|
+
* `commands/sources/stash-cli.ts`) and get a graceful, in-process shutdown
|
|
27
|
+
* from it. Every other command has no listener of its own, so the runtime's
|
|
28
|
+
* default disposition terminates it directly — `exit` handlers (lock
|
|
29
|
+
* release included) do NOT run — and any lock it held is cleared later by
|
|
30
|
+
* the next acquirer's dead-pid stale-reclaim (`file-lock.ts`), not by
|
|
31
|
+
* in-process release. Either way the orphaned process stops, which is this
|
|
32
|
+
* watchdog's actual job.
|
|
33
|
+
*/
|
|
34
|
+
/** Pure comparison seam: true once the observed ppid differs from the one seen at startup. */
|
|
35
|
+
export function isReparented(initialPpid, currentPpid) {
|
|
36
|
+
return currentPpid !== initialPpid;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Start polling `getPpid()` every `intervalMs` and invoke `onOrphaned` once,
|
|
40
|
+
* the first time the observed ppid no longer matches `initialPpid`. The timer
|
|
41
|
+
* is unref'ed so it never keeps the process alive on its own — a command that
|
|
42
|
+
* finishes normally still exits promptly.
|
|
43
|
+
*/
|
|
44
|
+
export function startParentDeathWatchdog(options) {
|
|
45
|
+
const { initialPpid, onOrphaned } = options;
|
|
46
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
47
|
+
const getPpid = options.getPpid ?? (() => process.ppid);
|
|
48
|
+
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
49
|
+
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
50
|
+
let fired = false;
|
|
51
|
+
const timer = setIntervalFn(() => {
|
|
52
|
+
if (fired)
|
|
53
|
+
return;
|
|
54
|
+
if (isReparented(initialPpid, getPpid())) {
|
|
55
|
+
fired = true;
|
|
56
|
+
onOrphaned();
|
|
57
|
+
}
|
|
58
|
+
}, intervalMs);
|
|
59
|
+
if (typeof timer !== "number")
|
|
60
|
+
timer.unref?.();
|
|
61
|
+
return {
|
|
62
|
+
stop: () => clearIntervalFn(timer),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/core/paths.js
CHANGED
|
@@ -234,6 +234,17 @@ export function getDbPath(env = process.env) {
|
|
|
234
234
|
export function getIndexWriterLockPath() {
|
|
235
235
|
return path.join(getDataDir(), "index.db.write.lock");
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Path to the opt-in, PID-liveness-only rebuild lock an explicit `akm index`
|
|
239
|
+
* run acquires (#956). Distinct from {@link getIndexWriterLockPath}'s
|
|
240
|
+
* `index.db.write.lock`, which is the asset-mutation lease and unrelated to
|
|
241
|
+
* indexing since #872 — this lock never blocks and is never required, it
|
|
242
|
+
* only lets a scheduled/opportunistic `akm index --skip-if-locked` step
|
|
243
|
+
* aside instead of contending with a run already in progress.
|
|
244
|
+
*/
|
|
245
|
+
export function getIndexRebuildLockPath() {
|
|
246
|
+
return path.join(getDataDir(), "index.rebuild.lock");
|
|
247
|
+
}
|
|
237
248
|
export function getMaintenanceBarrierPath() {
|
|
238
249
|
return path.join(getDataDir(), "maintenance.barrier.lock");
|
|
239
250
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* PID-liveness-only run lock — the shared mechanics behind `akm improve`'s
|
|
6
|
+
* whole-run lock (`commands/improve/locks.ts`) and the index rebuild lock
|
|
7
|
+
* (`indexer/index-rebuild-lock.ts`, #956).
|
|
8
|
+
*
|
|
9
|
+
* No `staleAfterMs`: only a verifiably dead holder is ever reclaimed. This is
|
|
10
|
+
* the #872 lesson encoded at the mechanics layer so every future caller gets
|
|
11
|
+
* it for free — an age-based stale window let a live-but-wedged holder's
|
|
12
|
+
* lease survive forever from the holder's own point of view while stranding
|
|
13
|
+
* every OTHER invocation once the clock passed, which cost one real install
|
|
14
|
+
* a half-day indexing outage. SQLite's own WAL + busy_timeout + BEGIN
|
|
15
|
+
* IMMEDIATE already serialize concurrent writers at the correctness layer —
|
|
16
|
+
* this lock only ever avoids duplicate LOGICAL work or lets a caller choose
|
|
17
|
+
* to skip instead of contend.
|
|
18
|
+
*
|
|
19
|
+
* This module performs exactly one create-or-probe attempt (with the same
|
|
20
|
+
* absent-race and stale-reclaim retries `improve` established) and reports
|
|
21
|
+
* whether the caller now owns the lock or, if not, who currently holds it.
|
|
22
|
+
* It does not decide what "held" means — skip, throw, or warn-and-proceed is
|
|
23
|
+
* entirely up to the caller — and it does not serialize the attempt itself:
|
|
24
|
+
* every caller must wrap the call in
|
|
25
|
+
* `withMaintenanceStartBarrier`/`tryWithMaintenanceStartBarrier`
|
|
26
|
+
* (`core/maintenance-barrier.ts`) so two racing processes never both create
|
|
27
|
+
* the sentinel in the same window.
|
|
28
|
+
*/
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { ConfigError } from "./errors.js";
|
|
32
|
+
import { createLockPayload, probeLock, reclaimStaleLock, tryAcquireLockSync } from "./file-lock.js";
|
|
33
|
+
import { describeInaccessiblePath } from "./path-access.js";
|
|
34
|
+
/**
|
|
35
|
+
* Render a lock holder's pid for a message: `"4242"`, or `"4242 (launcher
|
|
36
|
+
* 4240)"` when the holder's launcher pid is known (#956) — every process
|
|
37
|
+
* listing and task log shows the launcher pid, not the bun/node child's, so
|
|
38
|
+
* naming only the holder pid left an operator unable to connect the two.
|
|
39
|
+
*/
|
|
40
|
+
export function formatLockHolderPid(holder) {
|
|
41
|
+
if (holder.pid === null)
|
|
42
|
+
return "unknown";
|
|
43
|
+
return holder.launcherPid !== null ? `${holder.pid} (launcher ${holder.launcherPid})` : String(holder.pid);
|
|
44
|
+
}
|
|
45
|
+
function parseLockPayload(rawContent) {
|
|
46
|
+
if (!rawContent)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(rawContent);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function holderOf(lock) {
|
|
56
|
+
return { pid: lock?.pid ?? null, startedAt: lock?.startedAt ?? null, launcherPid: lock?.launcherPid ?? null };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Attempt to acquire `lockPath`. Returns `{ state: "acquired" }` with an
|
|
60
|
+
* ownership handle for {@link releaseLock}, or `{ state: "held", holder }`
|
|
61
|
+
* naming the current holder (best-effort; `pid`/`startedAt` are `null` when
|
|
62
|
+
* the holder identity could not be determined, e.g. a release-then-reacquire
|
|
63
|
+
* race). Throws {@link ConfigError} if the sentinel exists but cannot be read
|
|
64
|
+
* (#791: a lock we cannot see may be genuinely held — never a reclaim
|
|
65
|
+
* candidate).
|
|
66
|
+
*/
|
|
67
|
+
export function tryAcquireRunLock(lockPath, options) {
|
|
68
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
69
|
+
const lockPayload = () => createLockPayload({ startedAt: new Date().toISOString(), ...options.payloadMetadata });
|
|
70
|
+
let ownership = tryAcquireLockSync(lockPath, lockPayload());
|
|
71
|
+
if (ownership)
|
|
72
|
+
return { state: "acquired", ownership };
|
|
73
|
+
const probe = probeLock(lockPath);
|
|
74
|
+
// Race: the holder released between the failed acquire above and this
|
|
75
|
+
// probe. Retry once rather than falling through with a null-PID "held"
|
|
76
|
+
// report for a lock nobody actually holds.
|
|
77
|
+
if (probe.state === "absent") {
|
|
78
|
+
ownership = tryAcquireLockSync(lockPath, lockPayload());
|
|
79
|
+
if (ownership)
|
|
80
|
+
return { state: "acquired", ownership };
|
|
81
|
+
// Re-grabbed by another racer in this exact window — no holder detail
|
|
82
|
+
// available without re-probing (which could itself race again).
|
|
83
|
+
return { state: "held", holder: { pid: null, startedAt: null, launcherPid: null } };
|
|
84
|
+
}
|
|
85
|
+
if (probe.state === "inaccessible") {
|
|
86
|
+
throw new ConfigError(`${options.label} lock exists but is not readable: ${describeInaccessiblePath(lockPath, probe.code)}.`, "DATA_DIR_UNREADABLE");
|
|
87
|
+
}
|
|
88
|
+
const lock = parseLockPayload(probe.rawContent);
|
|
89
|
+
if (probe.state === "stale") {
|
|
90
|
+
if (!reclaimStaleLock(lockPath, probe)) {
|
|
91
|
+
return { state: "held", holder: holderOf(lock) };
|
|
92
|
+
}
|
|
93
|
+
options.onReclaimed?.({
|
|
94
|
+
holderPid: lock?.pid ?? probe.holderPid ?? null,
|
|
95
|
+
lockedAt: lock?.startedAt ?? null,
|
|
96
|
+
ageMs: probe.ageMs ?? null,
|
|
97
|
+
reason: probe.reason === "pid_dead" ? "pid_not_alive" : probe.reason,
|
|
98
|
+
});
|
|
99
|
+
ownership = tryAcquireLockSync(lockPath, lockPayload());
|
|
100
|
+
if (ownership)
|
|
101
|
+
return { state: "acquired", ownership };
|
|
102
|
+
// Acquired by another racer during stale recovery.
|
|
103
|
+
return { state: "held", holder: holderOf(lock) };
|
|
104
|
+
}
|
|
105
|
+
// probe.state === "held"
|
|
106
|
+
return { state: "held", holder: holderOf(lock) };
|
|
107
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Path to the sibling marker file that suppresses listing for a sensitive
|
|
6
|
+
* env/secret asset. Deliberately its own leaf module (no imports) rather than
|
|
7
|
+
* living on `core/env-secret-ref.ts` (#950 originally put it there): that
|
|
8
|
+
* module transitively imports the source providers via
|
|
9
|
+
* `indexer/search/search-source`, and `commands/env/env.ts` is imported back
|
|
10
|
+
* from `core/adapter/adapters/akm-metadata.ts` (for `scanEnvKeyNames`) — so an
|
|
11
|
+
* `env.ts` import of the heavier module closes a real cycle through the
|
|
12
|
+
* adapter registry (`core/adapter/adapters/index.ts`'s `BUILTIN_ADAPTERS`
|
|
13
|
+
* construction sees `akmAdapter` still TDZ). Keeping this helper leaf-only
|
|
14
|
+
* lets `commands/env/env.ts` and `core/env-secret-ref.ts` both depend on it
|
|
15
|
+
* without either pulling the other's heavier graph.
|
|
16
|
+
*/
|
|
17
|
+
export function sensitiveMarkerPath(assetPath, type) {
|
|
18
|
+
return type === "env" ? assetPath.replace(/\.env$/, ".sensitive") : `${assetPath}.sensitive`;
|
|
19
|
+
}
|