akm-cli 0.9.3 → 0.9.4

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 (43) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.md +1 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/cli.js +5 -5
  8. package/dist/commands/health/improve-metrics.js +17 -0
  9. package/dist/commands/health/windows.js +2 -2
  10. package/dist/commands/health.js +2 -2
  11. package/dist/commands/improve/preparation.js +8 -1
  12. package/dist/commands/lint/index.js +3 -7
  13. package/dist/commands/tasks/tasks-cli.js +28 -3
  14. package/dist/commands/tasks/tasks.js +29 -1
  15. package/dist/core/adapter/adapters/akm-adapter.js +21 -14
  16. package/dist/core/adapter/adapters/akm-lint.js +3 -2
  17. package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
  18. package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
  19. package/dist/core/adapter/recognize-match.js +1 -20
  20. package/dist/core/asset/asset-placement.js +21 -2
  21. package/dist/core/common.js +21 -1
  22. package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
  23. package/dist/indexer/search/db-search.js +6 -0
  24. package/dist/indexer/walk/matchers.js +0 -22
  25. package/dist/output/shapes/helpers.js +19 -1
  26. package/dist/output/shapes/passthrough.js +1 -0
  27. package/dist/output/text/command-format.js +4 -0
  28. package/dist/registry/pinned-request-helper.js +2 -2
  29. package/dist/registry/pinned-transport.js +6 -6
  30. package/dist/scripts/akm-migrate-node.js +12624 -12555
  31. package/dist/scripts/akm-migrate.js +12624 -12555
  32. package/dist/storage/repositories/proposals-repository.js +33 -1
  33. package/dist/storage/repositories/task-history-repository.js +22 -10
  34. package/dist/tasks/run/task-history.js +23 -3
  35. package/dist/tasks/scheduler-sync-preview.js +43 -0
  36. package/dist/tasks/scheduler-sync.js +1 -0
  37. package/dist/tasks/source/bounded-document.js +1 -1
  38. package/dist/tasks/source/parse-task-source.js +77 -11
  39. package/dist/tasks/source/task-source-v3-frozen.js +428 -0
  40. package/dist/tasks/source/task-to-v3.js +500 -0
  41. package/dist/tasks/source/task-to-v4.js +467 -0
  42. package/docs/reference/cli.md +8 -3
  43. package/package.json +4 -4
@@ -151,10 +151,10 @@ const BUILTIN_PLACEMENT_SPECS = {
151
151
  isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".yml",
152
152
  toCanonicalName: (typeRoot, filePath) => {
153
153
  const rel = toPosix(path.relative(typeRoot, filePath));
154
- return rel.endsWith(".yml") ? rel.slice(0, -4) : rel;
154
+ return rel.toLowerCase().endsWith(".yml") ? rel.slice(0, -4) : rel;
155
155
  },
156
156
  toAssetPath: (typeRoot, name) => {
157
- const withExt = name.endsWith(".yml") ? name : `${name}.yml`;
157
+ const withExt = name.toLowerCase().endsWith(".yml") ? name : `${name}.yml`;
158
158
  return path.join(typeRoot, withExt);
159
159
  },
160
160
  },
@@ -241,3 +241,22 @@ export function assetPathForName(assetType, typeRoot, name) {
241
241
  throw new Error(`Unknown asset type: "${assetType}"`);
242
242
  return spec.toAssetPath(typeRoot, name);
243
243
  }
244
+ /**
245
+ * Every physical path spelling that could own `name`, closed-form (#857).
246
+ * `toAssetPath` is a function, so it can only pick ONE spelling — but `env`'s
247
+ * "default" alias is genuinely dual-owned: both `<dir>/.env` and
248
+ * `<dir>/default.env` derive the same canonical name (`toCanonicalName`
249
+ * above), so a physical-owner lookup must consider both without reading
250
+ * either file. Every other placement type has exactly one inverse spelling.
251
+ */
252
+ export function assetPathCandidatesForName(assetType, typeRoot, name) {
253
+ const primary = assetPathForName(assetType, typeRoot, name);
254
+ if (assetType !== "env")
255
+ return [primary];
256
+ const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
257
+ if (base === undefined)
258
+ return [primary];
259
+ const dotForm = path.join(typeRoot, base, ".env");
260
+ const namedForm = path.join(typeRoot, base, "default.env");
261
+ return [...new Set([primary, dotForm, namedForm])];
262
+ }
@@ -5,7 +5,7 @@ import crypto from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { ConfigError } from "./errors.js";
8
- import { getConfigPath, getDefaultStashDir } from "./paths.js";
8
+ import { getConfigPath, getDefaultStashDir, getRegistryCacheDir, getRegistryIndexCacheDir } from "./paths.js";
9
9
  // ── Constants ───────────────────────────────────────────────────────────────
10
10
  // Moved to the platform leaf so paths.ts can use it without a common↔paths
11
11
  // cycle (chunk-8 WI-8.6, DoD 11); re-exported here for the existing surface.
@@ -382,6 +382,26 @@ export function isContainedRelativePath(value) {
382
382
  export function isWithin(candidate, root) {
383
383
  return isContainedResolvedPath(safeRealpath(candidate), safeRealpath(root));
384
384
  }
385
+ /**
386
+ * True when `filePath` sits inside akm's OWN resolved registry-cache
387
+ * directories (`<cache>/registry`, `<cache>/registry-index` —
388
+ * {@link getRegistryCacheDir}/{@link getRegistryIndexCacheDir}), the
389
+ * read-only installed-source/registry-index copies that lint (and `--fix`)
390
+ * must never touch.
391
+ *
392
+ * This is the single source of truth for that exclusion — it replaces what
393
+ * used to be three independent unanchored substring checks
394
+ * (`posixPath.includes("/.cache/") || posixPath.includes("/registry/")`).
395
+ * That check matched ANY path merely containing the literal text `.cache` or
396
+ * `registry` anywhere in it — a normal XDG `~/.cache/...` user bundle, or a
397
+ * CI workspace checked out under a `.cache`-named directory, tripped it and
398
+ * silently got zero lint findings. Using {@link isWithin} (realpath +
399
+ * containment, not a string search) fixes that while still excluding the
400
+ * real cache content the check was meant to skip.
401
+ */
402
+ export function isAkmRegistryCachePath(filePath) {
403
+ return isWithin(filePath, getRegistryCacheDir()) || isWithin(filePath, getRegistryIndexCacheDir());
404
+ }
385
405
  /**
386
406
  * {@link isWithin} for callers that must not block the event loop (e.g. the
387
407
  * workflow exec dispatch path, which runs once per fan-out unit). Same
@@ -10,10 +10,6 @@ import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
10
10
  import { resolveUniqueWorkflowSource, workflowNameForConceptId, } from "../../workflows/source-files.js";
11
11
  import { buildFileContext } from "../walk/file-context.js";
12
12
  const CONTENT_READ_REQUIRED = Symbol("adapter ownership probe requires content");
13
- const OWNER_SCAN_MAX_DIRECTORIES = 4_096;
14
- const OWNER_SCAN_MAX_FILES = 16_384;
15
- const OWNER_SCAN_SKIP_DIRECTORIES = new Set([".git", "node_modules", "bin", ".cache"]);
16
- const OWNER_SCAN_SKIP_FILES = new Set([".stash.json", ".gitignore", ".gitattributes"]);
17
13
  export class AdapterConceptOwnershipError extends UsageError {
18
14
  }
19
15
  export class AdapterConceptCollisionError extends AdapterConceptOwnershipError {
@@ -24,13 +20,6 @@ export class AdapterConceptCollisionError extends AdapterConceptOwnershipError {
24
20
  Object.setPrototypeOf(this, new.target.prototype);
25
21
  }
26
22
  }
27
- export class AdapterConceptScanError extends AdapterConceptOwnershipError {
28
- constructor(adapterId, sourcePath, detail) {
29
- super(`Adapter "${adapterId}" could not safely enumerate ${sourcePath}: ${detail}.`, "INVALID_FLAG_VALUE");
30
- this.name = "AdapterConceptScanError";
31
- Object.setPrototypeOf(this, new.target.prototype);
32
- }
33
- }
34
23
  function normalizedConceptId(conceptId) {
35
24
  const normalized = conceptId.replaceAll("\\", "/");
36
25
  if (!normalized ||
@@ -128,79 +117,6 @@ function claimsWithoutContent(adapter, component, owner) {
128
117
  throw error;
129
118
  }
130
119
  }
131
- function adapterPathContext(root, authoredPath) {
132
- const file = buildFileContext(root, authoredPath);
133
- return {
134
- absPath: file.absPath,
135
- relPath: file.relPath,
136
- ext: file.ext,
137
- fileName: file.fileName,
138
- parentDir: file.parentDir,
139
- parentDirAbs: file.parentDirAbs,
140
- ancestorDirs: file.ancestorDirs,
141
- stashRoot: file.stashRoot,
142
- };
143
- }
144
- function scanRegularAuthoredPaths(sourcePath, realRoot, adapterId) {
145
- const paths = [];
146
- const stack = [path.resolve(sourcePath)];
147
- let directories = 0;
148
- let files = 0;
149
- while (stack.length > 0) {
150
- const current = stack.pop();
151
- if (!current)
152
- continue;
153
- directories++;
154
- if (directories > OWNER_SCAN_MAX_DIRECTORIES) {
155
- throw new AdapterConceptScanError(adapterId, sourcePath, `directory limit ${OWNER_SCAN_MAX_DIRECTORIES} exceeded`);
156
- }
157
- let entries;
158
- try {
159
- entries = fs
160
- .readdirSync(current, { withFileTypes: true })
161
- .sort((left, right) => comparePaths(left.name, right.name));
162
- }
163
- catch (error) {
164
- throw new AdapterConceptScanError(adapterId, sourcePath, `cannot read ${current}: ${String(error)}`);
165
- }
166
- for (let index = entries.length - 1; index >= 0; index--) {
167
- const entry = entries[index];
168
- if (!entry)
169
- continue;
170
- const authoredPath = path.join(current, entry.name);
171
- if (entry.isSymbolicLink())
172
- continue;
173
- if (entry.isDirectory()) {
174
- if (OWNER_SCAN_SKIP_DIRECTORIES.has(entry.name) || entry.name.startsWith("."))
175
- continue;
176
- stack.push(authoredPath);
177
- continue;
178
- }
179
- if (!entry.isFile() || OWNER_SCAN_SKIP_FILES.has(entry.name))
180
- continue;
181
- files++;
182
- if (files > OWNER_SCAN_MAX_FILES) {
183
- throw new AdapterConceptScanError(adapterId, sourcePath, `file limit ${OWNER_SCAN_MAX_FILES} exceeded`);
184
- }
185
- let realPath;
186
- try {
187
- realPath = fs.realpathSync(authoredPath);
188
- }
189
- catch {
190
- continue;
191
- }
192
- if (isWithin(realPath, realRoot))
193
- paths.push(authoredPath);
194
- }
195
- }
196
- return paths.sort(comparePaths);
197
- }
198
- function scannedReadCandidates(adapter, component, sourcePath, realRoot, conceptId) {
199
- const recognizePathCandidates = adapter.recognizePathCandidates;
200
- if (!recognizePathCandidates)
201
- return [];
202
- return scanRegularAuthoredPaths(sourcePath, realRoot, adapter.id).flatMap((authoredPath) => recognizePathCandidates(component, adapterPathContext(sourcePath, authoredPath)).flatMap((candidateConceptId) => candidateConceptId === conceptId ? [{ path: authoredPath, conceptId: candidateConceptId }] : []));
203
- }
204
120
  /**
205
121
  * Resolve one adapter/component's exact physical concept owner without reading
206
122
  * authored bytes. Native workflow arbitration is reused verbatim; every other
@@ -240,11 +156,12 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId) {
240
156
  ownersByIdentity.set(`${path.resolve(workflowOwner.path)}\0${resolutionConceptId}`, workflowOwner);
241
157
  }
242
158
  }
243
- const directSpellings = (adapter.readCandidates?.(component, resolutionConceptId) ?? []).flatMap((candidate) => candidateSpellings(candidate, sourcePath, realRoot));
244
- const spellings = [
245
- ...directSpellings,
246
- ...scannedReadCandidates(adapter, component, sourcePath, realRoot, resolutionConceptId),
247
- ];
159
+ // Closed-form candidate set (#857): `readCandidates` inverts the conceptId
160
+ // directly into every physical spelling that could own it (no bundle walk
161
+ // — see each adapter's own doc comment for its candidate classes), and
162
+ // `candidateSpellings` adds case-only siblings via one `readdirSync` of
163
+ // each candidate's own parent directory.
164
+ const spellings = (adapter.readCandidates?.(component, resolutionConceptId) ?? []).flatMap((candidate) => candidateSpellings(candidate, sourcePath, realRoot));
248
165
  const inspected = [
249
166
  ...new Map(spellings.map((candidate) => [`${path.resolve(candidate.path)}\0${candidate.conceptId}`, candidate])).values(),
250
167
  ]
@@ -785,6 +785,10 @@ export async function buildDbHit(input) {
785
785
  ...(input.entry.beliefState ? { beliefState: input.entry.beliefState } : {}),
786
786
  ...(input.entry.currentBeliefRefs ? { currentBeliefRefs: input.entry.currentBeliefRefs } : {}),
787
787
  ...(graphHit ? { graph: { entities: graphHit.entities, relations: graphHit.relations } } : {}),
788
+ // Which stage of the progressive AND->OR lexical ladder produced this
789
+ // hit. Omitted when the hit has no FTS component (pure-semantic hybrid
790
+ // contribution).
791
+ ...(input.lexicalMatch ? { matchStage: input.lexicalMatch } : {}),
788
792
  };
789
793
  attachDbHitAttribution(hit, input);
790
794
  if (input.entry.derivedFrom) {
@@ -825,6 +829,8 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted, graphBoost, lexicalM
825
829
  ];
826
830
  if (lexicalMatch === "relaxed")
827
831
  reasons.push("lexical recovery after strict query returned no hits");
832
+ if (lexicalMatch === "prefix")
833
+ reasons.push("prefix match after strict query returned no hits");
828
834
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
829
835
  const queryLower = query.toLowerCase().trim();
830
836
  const name = entry.name.toLowerCase();
@@ -270,28 +270,6 @@ function matchResultForFact(fact) {
270
270
  ...(fact.meta ? { meta: fact.meta } : {}),
271
271
  };
272
272
  }
273
- /**
274
- * Every smart-Markdown result possible from path fields alone. The actual
275
- * classifier above returns only facts from this shared table, so owner
276
- * discovery can conservatively model a byte request without reading bytes.
277
- */
278
- export function smartMdPathCandidates(ctx) {
279
- if (ctx.ext !== ".md" || ctx.ancestorDirs.includes("secrets"))
280
- return [];
281
- const facts = isTypedDirDocFile(ctx.fileName)
282
- ? [SMART_MD_FACTS.knowledge]
283
- : [
284
- SMART_MD_FACTS.workflow,
285
- SMART_MD_FACTS.toolsAgent,
286
- SMART_MD_FACTS.command,
287
- SMART_MD_FACTS.modelAgent,
288
- SMART_MD_FACTS.knowledge,
289
- ];
290
- return facts.flatMap((fact) => {
291
- const result = matchResultForFact(fact);
292
- return result ? [result] : [];
293
- });
294
- }
295
273
  // ---------------------------------------------------------------------------
296
274
  // Public matchers (API unchanged)
297
275
  // ---------------------------------------------------------------------------
@@ -275,7 +275,20 @@ export function shapeSearchHit(hit, detail) {
275
275
  // visible without forcing callers up to `--detail full`. Optional
276
276
  // `quality` (v1 spec §4.2) is also surfaced when present so callers
277
277
  // can see why a `proposed` entry showed up under `--include-proposed`.
278
- const shaped = capDescription(pickFields(hit, ["type", "name", "description", "action", "score", "estimatedTokens", "warnings", "quality"]), NORMAL_DESCRIPTION_LIMIT);
278
+ // `matchStage` (issue #856) surfaces which stage of the progressive
279
+ // AND->OR lexical ladder produced the hit; cheap compact enum, worth
280
+ // showing without requiring `--detail full`.
281
+ const shaped = capDescription(pickFields(hit, [
282
+ "type",
283
+ "name",
284
+ "description",
285
+ "action",
286
+ "score",
287
+ "estimatedTokens",
288
+ "warnings",
289
+ "quality",
290
+ "matchStage",
291
+ ]), NORMAL_DESCRIPTION_LIMIT);
279
292
  if (Array.isArray(hit.keys) && hit.keys.length > 0)
280
293
  shaped.keys = hit.keys;
281
294
  return shaped;
@@ -296,6 +309,11 @@ export function shapeSearchHitForAgent(hit) {
296
309
  "score",
297
310
  "estimatedTokens",
298
311
  "keys",
312
+ // Issue #856: which stage of the progressive AND->OR lexical ladder
313
+ // produced this hit. Agents need this to gauge how much to trust a
314
+ // hit (a strict-AND match is stronger signal than an OR-fallback
315
+ // recovery match) without going to `--detail full`.
316
+ "matchStage",
299
317
  ]);
300
318
  if (picked.editable !== false)
301
319
  delete picked.editHint;
@@ -57,6 +57,7 @@ const PASSTHROUGH_COMMANDS = [
57
57
  "task-history",
58
58
  "task-run",
59
59
  "task-sync",
60
+ "task-sync-dry-run",
60
61
  "update",
61
62
  "upgrade",
62
63
  "workflow-abandon",
@@ -270,6 +270,10 @@ export function formatSearchPlain(r, detail) {
270
270
  // Optional v1 spec §4.2 quality marker (e.g. "curated" / "proposed").
271
271
  if (typeof hit.quality === "string" && hit.quality)
272
272
  lines.push(` quality: ${hit.quality}`);
273
+ // Issue #856: which stage of the progressive AND->OR lexical ladder
274
+ // produced this hit ("exact" | "prefix" | "relaxed").
275
+ if (typeof hit.matchStage === "string" && hit.matchStage)
276
+ lines.push(` matchStage: ${hit.matchStage}`);
273
277
  // Surface optional hit-level warnings (v1 spec §4.2).
274
278
  if (Array.isArray(hit.warnings) && hit.warnings.length > 0) {
275
279
  lines.push(` warnings: ${hit.warnings.join("; ")}`);
@@ -117,8 +117,8 @@ export async function nodePinnedRequestHelperMain(readRequest, http, https, isIP
117
117
  };
118
118
  try {
119
119
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
120
- if (!Number.isInteger(nodeMajor) || nodeMajor < 24) {
121
- throw new Error(`The pinned registry helper requires Node.js >= 24; found ${process.versions.node}`);
120
+ if (!Number.isInteger(nodeMajor) || nodeMajor < 22) {
121
+ throw new Error(`The pinned registry helper requires Node.js >= 22; found ${process.versions.node}`);
122
122
  }
123
123
  const { addressFamily, bodyPresent, headers, initialBody, iterator, method, pinnedAddress, timeoutMs, url } = await readRequest(isIP);
124
124
  const hostname = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname;
@@ -233,7 +233,7 @@ async function requestWithNodeHelper(url, address, prepared, init, timeoutMs, ex
233
233
  }
234
234
  function resolveNodeExecutable(override) {
235
235
  if (override === null) {
236
- throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 24 on PATH; no Node executable is available");
236
+ throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 22 on PATH; no Node executable is available");
237
237
  }
238
238
  const candidates = override === undefined
239
239
  ? (process.env.PATH ?? "")
@@ -254,10 +254,10 @@ function resolveNodeExecutable(override) {
254
254
  return resolved;
255
255
  }
256
256
  catch {
257
- // Try the next PATH entry. The helper itself enforces Node >= 24.
257
+ // Try the next PATH entry. The helper itself enforces Node >= 22.
258
258
  }
259
259
  }
260
- throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 24 on PATH. Install Node.js or run akm with Node.");
260
+ throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 22 on PATH. Install Node.js or run akm with Node.");
261
261
  }
262
262
  function isSupportedNodeExecutable(executable) {
263
263
  const result = spawnSync(executable, ["--version"], {
@@ -272,14 +272,14 @@ function isSupportedNodeExecutable(executable) {
272
272
  return false;
273
273
  const match = /^v(\d+)\./.exec(result.stdout.trim());
274
274
  const major = match?.[1];
275
- return major !== undefined && Number.parseInt(major, 10) >= 24;
275
+ return major !== undefined && Number.parseInt(major, 10) >= 22;
276
276
  }
277
277
  function assertNodeRuntimeVersion() {
278
278
  if (process.versions.bun)
279
279
  return;
280
280
  const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
281
- if (!Number.isInteger(major) || major < 24) {
282
- throw new RegistryPinnedTransportError(`Pinned registry networking requires Node.js >= 24; found ${process.versions.node}`);
281
+ if (!Number.isInteger(major) || major < 22) {
282
+ throw new RegistryPinnedTransportError(`Pinned registry networking requires Node.js >= 22; found ${process.versions.node}`);
283
283
  }
284
284
  }
285
285
  function createHelperArtifacts(ca) {