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.
Files changed (120) hide show
  1. package/CHANGELOG.md +559 -0
  2. package/STABILITY.md +6 -3
  3. package/dist/akm +54 -1
  4. package/dist/akm-migrate +34 -1
  5. package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
  6. package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
  7. package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
  8. package/dist/assets/tasks/core/improve.yml +1 -1
  9. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  10. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
  11. package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
  12. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
  13. package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
  14. package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
  15. package/dist/cli/retired-commands.js +0 -1
  16. package/dist/cli/shared.js +9 -0
  17. package/dist/cli/unknown-flags.js +1 -0
  18. package/dist/cli.js +40 -3
  19. package/dist/commands/config-cli.js +85 -3
  20. package/dist/commands/env/env-cli.js +1 -42
  21. package/dist/commands/env/env.js +1 -1
  22. package/dist/commands/env/secret-cli.js +1 -2
  23. package/dist/commands/health/checks.js +357 -63
  24. package/dist/commands/health/engine-usage.js +45 -0
  25. package/dist/commands/health/improve-metrics.js +18 -0
  26. package/dist/commands/health/llm-usage.js +41 -1
  27. package/dist/commands/health/plugin-staleness.js +7 -3
  28. package/dist/commands/health/version-drift.js +93 -0
  29. package/dist/commands/health/windows.js +3 -1
  30. package/dist/commands/health.js +44 -9
  31. package/dist/commands/improve/consolidate/chunking.js +4 -2
  32. package/dist/commands/improve/improve-cli.js +99 -5
  33. package/dist/commands/improve/improve-report.js +154 -0
  34. package/dist/commands/improve/improve-result-file.js +45 -33
  35. package/dist/commands/improve/improve-strategies.js +133 -3
  36. package/dist/commands/improve/improve-usage-report.js +182 -0
  37. package/dist/commands/improve/improve.js +40 -3
  38. package/dist/commands/improve/locks.js +28 -78
  39. package/dist/commands/improve/planner.js +1 -0
  40. package/dist/commands/improve/preparation.js +9 -1
  41. package/dist/commands/improve/reflect.js +44 -4
  42. package/dist/commands/models-cli.js +50 -1
  43. package/dist/commands/proposal/repository.js +8 -3
  44. package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
  45. package/dist/commands/proposal/validators/proposal-validators.js +24 -0
  46. package/dist/commands/read/search-cli.js +38 -2
  47. package/dist/commands/read/show.js +103 -4
  48. package/dist/commands/sources/info.js +5 -1
  49. package/dist/commands/sources/installed-stashes.js +58 -16
  50. package/dist/commands/sources/self-update.js +2 -2
  51. package/dist/commands/sources/stash-cli.js +48 -0
  52. package/dist/commands/tasks/tasks-cli.js +49 -2
  53. package/dist/commands/workflow-cli.js +86 -12
  54. package/dist/core/asset/markdown-fragments.js +35 -0
  55. package/dist/core/config/config-schema.js +14 -0
  56. package/dist/core/config/config.js +302 -24
  57. package/dist/core/config/schema/embedding.js +41 -0
  58. package/dist/core/env-secret-ref.js +58 -5
  59. package/dist/core/errors.js +30 -0
  60. package/dist/core/file-lock.js +49 -15
  61. package/dist/core/improve-result.js +51 -0
  62. package/dist/core/loopback.js +17 -0
  63. package/dist/core/parent-watchdog.js +64 -0
  64. package/dist/core/paths.js +11 -0
  65. package/dist/core/run-lock.js +107 -0
  66. package/dist/core/sensitive-marker-path.js +19 -0
  67. package/dist/core/state-db.js +74 -14
  68. package/dist/indexer/index-rebuild-lock.js +73 -0
  69. package/dist/indexer/index-writer-lock.js +40 -1
  70. package/dist/indexer/index-written-assets.js +29 -1
  71. package/dist/indexer/indexer.js +93 -29
  72. package/dist/indexer/materialize-embeddings.js +564 -48
  73. package/dist/indexer/search/db-search.js +49 -2
  74. package/dist/indexer/search/search-source.js +23 -1
  75. package/dist/integrations/agent/engine-resolution.js +96 -6
  76. package/dist/integrations/agent/execution-definitions.js +6 -15
  77. package/dist/integrations/agent/execution-lowering.js +6 -1
  78. package/dist/integrations/agent/execution-preparation.js +1 -1
  79. package/dist/integrations/agent/model-map.js +123 -20
  80. package/dist/integrations/agent/prompts.js +40 -8
  81. package/dist/integrations/agent/runner-dispatch.js +9 -3
  82. package/dist/integrations/agent/runner.js +2 -0
  83. package/dist/llm/client.js +8 -3
  84. package/dist/llm/embedder.js +20 -8
  85. package/dist/llm/embedders/local.js +10 -2
  86. package/dist/llm/embedders/remote.js +497 -32
  87. package/dist/output/shapes/helpers.js +38 -2
  88. package/dist/output/shapes/models-list.js +16 -0
  89. package/dist/output/shapes/passthrough.js +2 -0
  90. package/dist/output/shapes.js +4 -0
  91. package/dist/output/text/command-format.js +29 -0
  92. package/dist/output/text/helpers.js +1 -1
  93. package/dist/output/text/improve-report.js +27 -0
  94. package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
  95. package/dist/output/text/show-format.js +4 -0
  96. package/dist/output/text.js +4 -0
  97. package/dist/scripts/akm-migrate-node.js +25146 -21759
  98. package/dist/scripts/akm-migrate.js +24271 -20885
  99. package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
  100. package/dist/storage/repositories/improve-runs-repository.js +34 -0
  101. package/dist/storage/repositories/index-fts-repository.js +49 -6
  102. package/dist/storage/repositories/index-schema.js +16 -0
  103. package/dist/storage/repositories/index-vec-repository.js +30 -0
  104. package/dist/storage/repositories/workflow-runs-repository.js +55 -18
  105. package/dist/tasks/backends/cron.js +14 -7
  106. package/dist/tasks/run/run-native-task.js +23 -1
  107. package/dist/tasks/run/run-workflow-task.js +16 -0
  108. package/dist/workflows/exec/child-workflow.js +2 -2
  109. package/dist/workflows/exec/dispatch-redaction.js +21 -9
  110. package/dist/workflows/exec/run-workflow.js +6 -5
  111. package/dist/workflows/runtime/runs.js +33 -5
  112. package/docs/migration/release-notes/0.9.15.md +133 -0
  113. package/docs/migration/release-notes/README.md +5 -0
  114. package/docs/reference/cli.md +271 -30
  115. package/docs/reference/configuration.md +234 -21
  116. package/docs/reference/data-and-telemetry.md +8 -0
  117. package/docs/reference/tasks.md +16 -1
  118. package/docs/reference/workflow-schema.md +5 -1
  119. package/package.json +1 -1
  120. package/schemas/akm-config.json +47 -0
@@ -30,8 +30,14 @@ import { akmShowUnified } from "./show.js";
30
30
  * rejected — the command then runs against the DEFAULT `--from` value
31
31
  * (local) instead of the source the caller named, with exit 0 and no error.
32
32
  * Reject it explicitly instead.
33
+ *
34
+ * Exported so `akm task list` (`../tasks/tasks-cli.ts`, #951) — a pure
35
+ * delegating alias for `akm search --type task` — applies the identical
36
+ * guard rather than a second copy of it; a task-list-scoped `--source` must
37
+ * fail exactly like `search`'s does, not fall through to citty's silent
38
+ * unknown-flag absorption.
33
39
  */
34
- function rejectRetiredSourceFlag() {
40
+ export function rejectRetiredSourceFlag() {
35
41
  if (!getParsedInvocation().hasFlag("--source"))
36
42
  return;
37
43
  throw new UsageError("`--source` was renamed to `--from` in 0.9. Use `--from local|registry|all` instead.", "INVALID_FLAG_VALUE");
@@ -141,7 +147,7 @@ export const searchCommand = defineJsonCommand({
141
147
  export const curateCommand = defineJsonCommand({
142
148
  meta: {
143
149
  name: "curate",
144
- description: "Pick the assets worth loading for a task. Unlike `akm search`, this reranks by intent, attaches a preview and run details per hit, adds related support refs, and summarizes the set — the usual starting point for an agent.",
150
+ description: "Pick the assets worth loading for a task. Unlike `akm search`, this attaches a preview and run details per hit, adds related support refs, and summarizes the set — the usual starting point for an agent.",
145
151
  },
146
152
  args: {
147
153
  // Optional in citty so run() is invoked when omitted; we re-validate
@@ -283,6 +289,18 @@ export const showCommand = defineJsonCommand({
283
289
  type: "string",
284
290
  description: "Scope filter (repeatable): --filter user=<id> --filter agent=<id> --filter run=<id> --filter channel=<name>. Narrows resolution to assets whose frontmatter scope matches. Same axis as `akm search --filter`.",
285
291
  },
292
+ context: {
293
+ type: "string",
294
+ description: "Fragment presentation: exact (default) returns only the selected section; lead returns bounded indexed-safe document lead plus the explicitly labelled selected match.",
295
+ },
296
+ "max-tokens": {
297
+ type: "string",
298
+ description: "Approximate context budget in tokens (four characters per token). Requires --context lead; mutually exclusive with --max-chars.",
299
+ },
300
+ "max-chars": {
301
+ type: "string",
302
+ description: "Exact context budget in characters. Requires --context lead; mutually exclusive with --max-tokens.",
303
+ },
286
304
  // Declared as the POSITIVE name with `default: true` — see the
287
305
  // `project-context` comment on `searchCommand` above for why a flag NAME
288
306
  // must never start with `no-`.
@@ -331,10 +349,22 @@ export const showCommand = defineJsonCommand({
331
349
  // commands share one spelling for the scope-narrowing axis).
332
350
  const scopeTokens = parseAllFlagValues("--filter");
333
351
  const scope = parseScopeFilterFlags(scopeTokens, "--filter");
352
+ const contextMode = parseFragmentContextMode(typeof args.context === "string" ? args.context : undefined);
353
+ const maxTokens = parsePositiveIntFlag(args["max-tokens"] ?? undefined, "--max-tokens");
354
+ const maxChars = parsePositiveIntFlag(args["max-chars"] ?? undefined, "--max-chars");
355
+ if (maxTokens !== undefined && maxChars !== undefined) {
356
+ throw new UsageError("--max-tokens and --max-chars are mutually exclusive.", "INVALID_FLAG_VALUE");
357
+ }
358
+ const maxContextChars = maxChars ?? (maxTokens !== undefined ? maxTokens * 4 : undefined);
359
+ if (maxContextChars !== undefined && !Number.isSafeInteger(maxContextChars)) {
360
+ throw new UsageError("Fragment context budget is too large.", "INVALID_FLAG_VALUE");
361
+ }
334
362
  const skipLogging = args["track-usage"] === false;
335
363
  const result = await akmShowUnified({
336
364
  ref: args.ref,
337
365
  detail: showDetail,
366
+ contextMode,
367
+ maxContextChars,
338
368
  scope,
339
369
  skipLogging,
340
370
  eventSource: resolveUsageEventSource(),
@@ -342,3 +372,9 @@ export const showCommand = defineJsonCommand({
342
372
  output("show", result);
343
373
  },
344
374
  });
375
+ export function parseFragmentContextMode(raw) {
376
+ const normalized = raw?.trim().toLowerCase() || "exact";
377
+ if (normalized === "exact" || normalized === "lead")
378
+ return normalized;
379
+ throw new UsageError(`Invalid --context value: "${raw}". Expected exact or lead.`, "INVALID_FLAG_VALUE");
380
+ }
@@ -23,7 +23,7 @@ import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.
23
23
  import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
24
24
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
25
25
  import { extractSection, markdownFragmentSlugs } from "../../core/asset/markdown.js";
26
- import { fragmentForSelector } from "../../core/asset/markdown-fragments.js";
26
+ import { buildMarkdownLeadContext, fragmentForSelector, MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS, } from "../../core/asset/markdown-fragments.js";
27
27
  import { displayRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
28
28
  import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
29
29
  import { asNonEmptyString, isWithin } from "../../core/common.js";
@@ -65,6 +65,7 @@ import { getActiveWorkflowRun } from "../../workflows/runtime/runs.js";
65
65
  */
66
66
  export async function akmShowUnified(input) {
67
67
  const ref = input.ref.trim();
68
+ validateFragmentContextRequest(input);
68
69
  // 0a. Stash `.meta/` convention: `[origin//]meta[:name]` direct-reads a
69
70
  // human-authored orientation doc from the stash's `.meta/` directory.
70
71
  // These files are not indexed (the walker skips dot-dirs), so they are
@@ -148,6 +149,32 @@ async function showStashMeta(metaRef) {
148
149
  function hasAnyScopeKey(scope) {
149
150
  return Boolean(scope.user || scope.agent || scope.run || scope.channel);
150
151
  }
152
+ function validateFragmentContextRequest(input) {
153
+ const mode = input.contextMode ?? "exact";
154
+ if (mode !== "exact" && mode !== "lead") {
155
+ throw new UsageError(`Invalid --context value: "${String(mode)}". Expected exact or lead.`, "INVALID_FLAG_VALUE");
156
+ }
157
+ if (input.maxContextChars !== undefined &&
158
+ (!Number.isSafeInteger(input.maxContextChars) || input.maxContextChars <= 0)) {
159
+ throw new UsageError("Fragment context budget must be a positive safe integer.", "INVALID_FLAG_VALUE");
160
+ }
161
+ if (mode !== "lead" && input.maxContextChars !== undefined) {
162
+ throw new UsageError("--max-chars and --max-tokens require --context lead.", "INVALID_FLAG_VALUE");
163
+ }
164
+ if (mode !== "lead")
165
+ return;
166
+ if (parseMetaRef(input.ref)) {
167
+ throw new UsageError("--context lead requires an indexed Markdown asset fragment.", "INVALID_FLAG_VALUE");
168
+ }
169
+ const parsed = parseBundleRef(input.ref);
170
+ if (!parsed.fragment) {
171
+ throw new UsageError("--context lead requires a fragment-qualified ref.", "INVALID_FLAG_VALUE");
172
+ }
173
+ const type = typeNameFromConceptId(parsed.conceptId)?.type;
174
+ if (type === "env" || type === "secret") {
175
+ throw new UsageError(`--context lead is unavailable for sensitive ${type} assets.`, "INVALID_FLAG_VALUE");
176
+ }
177
+ }
151
178
  /**
152
179
  * Read the asset file's frontmatter and verify its `scope_*` keys satisfy
153
180
  * every supplied filter. Throws a {@link NotFoundError} on mismatch so the
@@ -184,6 +211,7 @@ function enforceScopeOrThrow(filePath, ref, scope) {
184
211
  }
185
212
  /** @internal Use akmShowUnified() for all external callers. */
186
213
  export async function showLocal(input) {
214
+ validateFragmentContextRequest(input);
187
215
  const parsed = parseBundleRef(input.ref);
188
216
  warnSensitiveFragmentUnsupported(parsed);
189
217
  const assetParts = typeNameFromConceptId(parsed.conceptId);
@@ -235,14 +263,19 @@ export async function showLocal(input) {
235
263
  }
236
264
  const fileCtx = buildFileContext(sourceStashDir, assetPath);
237
265
  const presentedName = indexedEntry.name;
238
- const indexedFragment = parsed.fragment?.startsWith("akm-fragment-")
266
+ const opaqueFragmentSelector = parsed.fragment?.startsWith("akm-fragment-") === true;
267
+ // Friendly heading selectors retain the source-live exact response contract.
268
+ // Only resolve the indexed-safe revision when the selector itself is opaque,
269
+ // or when the caller explicitly opts into indexed-safe contextual output.
270
+ const indexedFragment = parsed.fragment && (opaqueFragmentSelector || input.contextMode === "lead")
239
271
  ? withIndexDb((db) => getIndexedMarkdownFragment(db, indexedEntry.itemRef, parsed.fragment))
240
272
  : undefined;
273
+ const indexedFragmentContent = opaqueFragmentSelector ? indexedFragment?.content : undefined;
241
274
  const indexedRenderer = rendererForIndexedEntry(indexedEntry, fileCtx);
242
275
  let response;
243
276
  try {
244
277
  if (indexedRenderer === null) {
245
- response = buildIndexedProjectionResponse(indexedEntry, assetPath, parsed.fragment, indexedFragment?.content);
278
+ response = buildIndexedProjectionResponse(indexedEntry, assetPath, parsed.fragment, indexedFragmentContent);
246
279
  }
247
280
  else {
248
281
  const match = typeof indexedRenderer === "string" ? indexedMatch(indexedEntry, indexedRenderer) : recognizeMatch(fileCtx);
@@ -263,7 +296,7 @@ export async function showLocal(input) {
263
296
  warn(`Fragment "#${parsed.fragment}" was ignored: ${makeBundleRef(parsed.bundle, parsed.conceptId)} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`);
264
297
  }
265
298
  else {
266
- applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName, indexedFragment?.content);
299
+ applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName, indexedFragmentContent);
267
300
  }
268
301
  }
269
302
  }
@@ -280,6 +313,41 @@ export async function showLocal(input) {
280
313
  conceptId: indexedEntry.conceptId,
281
314
  bundleId: indexedEntry.bundleId,
282
315
  }, config.defaultBundle ?? (isPrimaryStash ? indexedEntry.bundleId : undefined));
316
+ if (parsed.fragment && indexedFragment) {
317
+ const selectedFragmentId = indexedFragment.fragments[indexedFragment.ordinal].fragmentId;
318
+ const selectedRef = `${canonicalRef}#${selectedFragmentId}`;
319
+ const parentEstimatedTokens = typeof indexedEntry.document?.fileSize === "number"
320
+ ? Math.round(indexedEntry.document.fileSize / 4)
321
+ : Math.round(indexedFragment.parentChars / 4);
322
+ Object.assign(response, {
323
+ selectedRef,
324
+ parentRef: canonicalRef,
325
+ fragmentOrdinal: indexedFragment.ordinal + 1,
326
+ fragmentCount: indexedFragment.count,
327
+ startLine: indexedFragment.startLine,
328
+ endLine: indexedFragment.endLine,
329
+ ...(indexedFragment.previousFragmentId
330
+ ? { previousRef: `${canonicalRef}#${indexedFragment.previousFragmentId}` }
331
+ : {}),
332
+ ...(indexedFragment.nextFragmentId ? { nextRef: `${canonicalRef}#${indexedFragment.nextFragmentId}` } : {}),
333
+ fragmentChars: indexedFragment.fragmentChars,
334
+ fragmentEstimatedTokens: Math.round(indexedFragment.fragmentChars / 4),
335
+ parentChars: indexedFragment.parentChars,
336
+ parentEstimatedTokens,
337
+ contextMode: input.contextMode ?? "exact",
338
+ contextTruncated: false,
339
+ });
340
+ if (input.contextMode === "lead") {
341
+ const contextMaxChars = input.maxContextChars ?? MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS;
342
+ const contextual = buildMarkdownLeadContext(indexedFragment.fragments, indexedFragment.ordinal, contextMaxChars);
343
+ applyMarkdownResponsePayload(response, contextual.content);
344
+ response.contextMaxChars = contextMaxChars;
345
+ response.contextTruncated = contextual.truncated;
346
+ }
347
+ }
348
+ else if (input.contextMode === "lead") {
349
+ throw new NotFoundError(`Indexed-safe fragment context is unavailable for ${input.ref}.`);
350
+ }
283
351
  if (response.type === "workflow")
284
352
  response.action = buildWorkflowAction(canonicalRef);
285
353
  // 07 P1-D: provenance-aware toolPolicy CEILING. An agent's self-declared
@@ -541,6 +609,14 @@ function applyMarkdownFragment(response, raw, fragment, name, indexedFragmentCon
541
609
  else
542
610
  response.content = section;
543
611
  }
612
+ function applyMarkdownResponsePayload(response, content) {
613
+ if (response.template !== undefined)
614
+ response.template = content;
615
+ else if (response.prompt !== undefined)
616
+ response.prompt = content;
617
+ else
618
+ response.content = content;
619
+ }
544
620
  function requireMarkdownSection(content, fragment, name) {
545
621
  const section = extractSection(content, fragment);
546
622
  if (section)
@@ -572,6 +648,7 @@ function buildBriefResponse(full, assetPath) {
572
648
  ...(summary.origin !== undefined ? { origin: summary.origin } : {}),
573
649
  ...(full.editable !== undefined ? { editable: full.editable } : {}),
574
650
  ...(full.editHint ? { editHint: full.editHint } : {}),
651
+ ...fragmentResponseProjection(full),
575
652
  };
576
653
  }
577
654
  /**
@@ -607,6 +684,28 @@ function buildSummaryResponse(full, assetPath) {
607
684
  ...(full.origin !== undefined ? { origin: full.origin } : {}),
608
685
  ...(full.editable !== undefined ? { editable: full.editable } : {}),
609
686
  ...(full.editHint ? { editHint: full.editHint } : {}),
687
+ ...fragmentResponseProjection(full),
610
688
  };
611
689
  return summary;
612
690
  }
691
+ function fragmentResponseProjection(full) {
692
+ if (!full.selectedRef)
693
+ return {};
694
+ return {
695
+ selectedRef: full.selectedRef,
696
+ parentRef: full.parentRef,
697
+ fragmentOrdinal: full.fragmentOrdinal,
698
+ fragmentCount: full.fragmentCount,
699
+ startLine: full.startLine,
700
+ endLine: full.endLine,
701
+ previousRef: full.previousRef,
702
+ nextRef: full.nextRef,
703
+ fragmentChars: full.fragmentChars,
704
+ fragmentEstimatedTokens: full.fragmentEstimatedTokens,
705
+ parentChars: full.parentChars,
706
+ parentEstimatedTokens: full.parentEstimatedTokens,
707
+ contextMode: full.contextMode,
708
+ contextMaxChars: full.contextMaxChars,
709
+ contextTruncated: full.contextTruncated,
710
+ };
711
+ }
@@ -5,7 +5,7 @@ import { placementTypes } from "../../core/asset/asset-placement.js";
5
5
  import { resolveStashDir } from "../../core/common.js";
6
6
  import { getSources, loadConfig } from "../../core/config/config.js";
7
7
  import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-access.js";
8
- import { getDbPath } from "../../core/paths.js";
8
+ import { getCacheDir, getConfigDir, getDataDir, getDbPath, getStateDir } from "../../core/paths.js";
9
9
  import { formatRegistryUrl } from "../../core/registry-url.js";
10
10
  import { error } from "../../core/warn.js";
11
11
  import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
@@ -69,6 +69,10 @@ export function assembleInfo(options) {
69
69
  version: pkgVersion,
70
70
  bundleDir: stashDir,
71
71
  defaultBundle,
72
+ dataDir: getDataDir(),
73
+ configDir: getConfigDir(),
74
+ cacheDir: getCacheDir(),
75
+ stateDir: getStateDir(),
72
76
  assetTypes,
73
77
  searchModes,
74
78
  semanticSearch: {
@@ -26,7 +26,7 @@ import { beginImmediateTransaction, getStateDbPath, openStateDatabase } from "..
26
26
  import { warn } from "../../core/warn.js";
27
27
  import { resolveGitContentRoot } from "../../core/write-source.js";
28
28
  import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
29
- import { akmIndex } from "../../indexer/indexer.js";
29
+ import { akmIndex, runEmbeddingPass } from "../../indexer/indexer.js";
30
30
  import { compareAndSwapLockfileSnapshot, publishLockfileUpdate, readLockfile, readLockfileForUpdate, } from "../../integrations/lockfile.js";
31
31
  import { parseRegistryRef } from "../../registry/resolve.js";
32
32
  import { sha256Hex } from "../../runtime.js";
@@ -350,11 +350,19 @@ export async function akmRemove(input) {
350
350
  },
351
351
  };
352
352
  }
353
- /** Read the current index generation without creating or hydrating anything. */
353
+ /**
354
+ * Read the current index generation without creating or hydrating anything.
355
+ * This path never ran an embedding pass (#954, field-report follow-up), so it has no
356
+ * `verification` to report — {@link buildUpdateResponse} falls back to the
357
+ * two facts it can actually know (whether semantic search is configured on
358
+ * at all) rather than fabricating verification numbers (`entryCount: 0`,
359
+ * `ok: true`) for a run that never verified anything.
360
+ */
354
361
  function readCurrentIndexSummary() {
355
362
  const db = openReadonlyExistingDatabase(getDbPath());
356
- if (!db)
363
+ if (!db) {
357
364
  return { mode: "incremental", totalEntries: 0, directoriesScanned: 0, directoriesSkipped: 0 };
365
+ }
358
366
  try {
359
367
  return {
360
368
  mode: "incremental",
@@ -388,6 +396,12 @@ function buildUpdateResponse(stashDir, target, all, processed, opts) {
388
396
  directoriesScanned: index.directoriesScanned,
389
397
  directoriesSkipped: index.directoriesSkipped,
390
398
  ...(index.scanComplete !== undefined ? { scanComplete: index.scanComplete } : {}),
399
+ // A real embedding pass (`akmIndex`/`runEmbeddingPass`) reports its own
400
+ // verified `semanticStatus`. When no pass ran this update (the
401
+ // no-op/nothing-configured fallback) the only two facts known without
402
+ // fabricating a verification are whether semantic search is off at all
403
+ // or, if not, that its state is simply unverified this run.
404
+ semanticStatus: index.verification?.semanticStatus ?? (finalConfig.semanticSearchMode === "off" ? "disabled" : "pending"),
391
405
  },
392
406
  };
393
407
  }
@@ -499,6 +513,45 @@ function closeUnifiedUpdateTransaction(transaction, committed) {
499
513
  : `[akm bundle update] rolled back, but closing its database handles failed: ${String(closeError)}`);
500
514
  }
501
515
  }
516
+ /**
517
+ * Run the embedding phase AFTER the coordinator's atomic commit, on its own
518
+ * fresh connection (#954). Before this, `akmUpdate` called
519
+ * `akmIndex()` for its embedding phase too, INSIDE this same unified
520
+ * `BEGIN IMMEDIATE` — so every per-batch commit the materializer opened
521
+ * nested as an unobservable SAVEPOINT, and a SIGKILL mid-run lost every
522
+ * embedding of the run rather than just the one in flight. The
523
+ * ambient-transaction drift guard (#954) now rejects that outright, so
524
+ * `akmIndex`'s own embedding phase is skipped for a deferred update
525
+ * transaction and this runs instead, once content/lock/index/state are
526
+ * already durably committed.
527
+ *
528
+ * A failing pass (provider down, timeout) does NOT fail the update: the
529
+ * bundle content and index are already committed successfully, exactly like
530
+ * a plain `akm index` whose embedding phase fails — only the reported
531
+ * `verification` reflects the shortfall (`semanticStatus: "blocked"`).
532
+ */
533
+ async function runPostCommitEmbeddingPass(index) {
534
+ const config = loadConfig();
535
+ let db;
536
+ try {
537
+ const embeddingDim = config.embedding?.dimension;
538
+ db = openIndexDatabase(getDbPath(), embeddingDim ? { embeddingDim } : undefined);
539
+ const { verification } = await runEmbeddingPass({ db, config, onProgress: () => { } });
540
+ return { ...index, verification };
541
+ }
542
+ catch (error) {
543
+ const message = error instanceof Error ? error.message : String(error);
544
+ warn(`[akm bundle update] post-commit embedding pass failed: ${message}`);
545
+ return {
546
+ ...index,
547
+ verification: { ...index.verification, ok: false, semanticStatus: "blocked", message },
548
+ };
549
+ }
550
+ finally {
551
+ if (db)
552
+ closeDatabase(db);
553
+ }
554
+ }
502
555
  function pathAtOrBelow(candidate, root) {
503
556
  const relative = path.relative(path.resolve(root), path.resolve(candidate));
504
557
  return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
@@ -779,14 +832,8 @@ async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInse
779
832
  updateTransactionHook("before-commit", id, { db: transaction.db });
780
833
  commitUnifiedUpdateTransaction(transaction);
781
834
  committed = true;
782
- try {
783
- transaction.deferred.afterCommit?.();
784
- }
785
- catch (error) {
786
- warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
787
- }
788
835
  prepared.publication?.commit();
789
- return index;
836
+ return await runPostCommitEmbeddingPass(index);
790
837
  }
791
838
  catch (error) {
792
839
  let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
@@ -1093,12 +1140,6 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure
1093
1140
  updateTransactionHook("before-commit", managed.installId, { db: transaction.db });
1094
1141
  commitUnifiedUpdateTransaction(transaction);
1095
1142
  committed = true;
1096
- try {
1097
- transaction.deferred.afterCommit?.();
1098
- }
1099
- catch (error) {
1100
- warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
1101
- }
1102
1143
  }
1103
1144
  catch (error) {
1104
1145
  let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
@@ -1146,6 +1187,7 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure
1146
1187
  }
1147
1188
  }
1148
1189
  prepared.publication?.commit();
1190
+ index = await runPostCommitEmbeddingPass(index);
1149
1191
  if (movedRoot) {
1150
1192
  const currentConfig = loadConfig();
1151
1193
  const currentLocks = readLockfile();
@@ -174,10 +174,10 @@ export function getAkmBinaryName() {
174
174
  return "akm-windows-x64.exe";
175
175
  throw new ConfigError(`Unsupported platform for binary upgrade: ${platform}/${arch}`, "UNSUPPORTED_PLATFORM");
176
176
  }
177
- export async function checkForUpdate(currentVersion) {
177
+ export async function checkForUpdate(currentVersion, fetchOptions) {
178
178
  const installMethod = detectInstallMethod();
179
179
  const url = `https://api.github.com/repos/${REPO}/releases/latest`;
180
- const response = await fetchWithRetry(url, { headers: githubHeaders() });
180
+ const response = await fetchWithRetry(url, { headers: githubHeaders() }, fetchOptions);
181
181
  if (!response.ok) {
182
182
  throw new Error(`Failed to check for updates: ${response.status} ${response.statusText}`);
183
183
  }
@@ -42,10 +42,13 @@ import { resolveBundleWriteTarget } from "../../core/mutation-target.js";
42
42
  import { getCacheDir } from "../../core/paths.js";
43
43
  import { clearLogFile, info, isVerbose, setLogFile } from "../../core/warn.js";
44
44
  import { resolveWriteTarget } from "../../core/write-source.js";
45
+ import { releaseIndexRebuildLock, tryAcquireIndexRebuildLock } from "../../indexer/index-rebuild-lock.js";
45
46
  import { akmIndex } from "../../indexer/indexer.js";
46
47
  import { getHyphenatedBoolean, getOutputMode } from "../../output/context.js";
47
48
  import { inferAssetName, mergeXrefsIntoContent, readKnowledgeInput, resolveSupersedesForWrite, resolveSupersedesWriteTarget, resolveXrefsForWrite, writeMarkdownAsset, } from "../read/knowledge.js";
48
49
  import { assembleInfo } from "./info.js";
50
+ /** Matches the high-frequency per-committed-batch progress line (#954), excluded from non-verbose JSON-mode stderr. */
51
+ const EMBEDDED_BATCH_PROGRESS_PATTERN = /^Embedded \d+\/\d+ entries\.$/;
49
52
  export const indexCommand = defineCommand({
50
53
  meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
51
54
  args: {
@@ -66,6 +69,16 @@ export const indexCommand = defineCommand({
66
69
  description: "When combined with --clean, report stale entries without deleting them.",
67
70
  default: false,
68
71
  },
72
+ reembed: {
73
+ type: "boolean",
74
+ description: "Force re-embedding of every entry, bypassing the embedding-model-rename compatibility check.",
75
+ default: false,
76
+ },
77
+ "skip-if-locked": {
78
+ type: "boolean",
79
+ description: "If another `akm index` run already holds the rebuild lock, skip gracefully (exit 0) instead of contending with it. Use for scheduled/opportunistic index runs so they don't pile up against a longer run in progress.",
80
+ default: false,
81
+ },
69
82
  },
70
83
  async run({ args }) {
71
84
  await runWithJsonErrors(async () => {
@@ -75,6 +88,27 @@ export const indexCommand = defineCommand({
75
88
  if (getHyphenatedBoolean(args, "re-enrich") || getParsedInvocation().getFlagValue("--re-enrich") !== undefined) {
76
89
  throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
77
90
  }
91
+ // #956: opt-in, non-blocking rebuild lock — never gates a human-typed
92
+ // `akm index` (it only warns and contends), but a scheduled/opportunistic
93
+ // caller can pass --skip-if-locked to step aside instead of piling up
94
+ // behind a run already in progress. Acquired before any other side
95
+ // effect (log file, spinner) so a skip does neither.
96
+ const lockAcquisition = tryAcquireIndexRebuildLock(args["skip-if-locked"]);
97
+ if (lockAcquisition.state === "skipped") {
98
+ output("index", {
99
+ ok: true,
100
+ skipped: {
101
+ reason: "lock-held",
102
+ pid: lockAcquisition.holder.pid,
103
+ // #956: the launcher pid (when known) alongside the pid that
104
+ // actually holds the lock — every process listing and task log
105
+ // shows the launcher pid, not the bun/node child's.
106
+ launcherPid: lockAcquisition.holder.launcherPid,
107
+ startedAt: lockAcquisition.holder.startedAt,
108
+ },
109
+ });
110
+ return;
111
+ }
78
112
  const outputMode = getOutputMode();
79
113
  const controller = new AbortController();
80
114
  const abort = () => controller.abort(new Error("index interrupted"));
@@ -98,6 +132,7 @@ export const indexCommand = defineCommand({
98
132
  full: args.full,
99
133
  clean: args.clean,
100
134
  dryRun: args["dry-run"],
135
+ reembed: args.reembed,
101
136
  onProgress: ({ phase, message, processed, total }) => {
102
137
  latestMessage = message;
103
138
  const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
@@ -108,6 +143,17 @@ export const indexCommand = defineCommand({
108
143
  spin.stop(`${progressPrefix}${message}`);
109
144
  spin.start(`${progressPrefix}${message}`);
110
145
  }
146
+ else if (!EMBEDDED_BATCH_PROGRESS_PATTERN.test(message)) {
147
+ // Non-verbose, non-text (JSON/yaml/etc) mode: silence used to be
148
+ // total until the run finished (#954) — a stalled
149
+ // run looked identical to "nothing written". Phase-start
150
+ // messages and the embedding heartbeat now reach stderr here
151
+ // too; the high-frequency per-batch `Embedded N/M entries.`
152
+ // line (emitted after every committed batch)
153
+ // is deliberately excluded — that would be spam, not a
154
+ // heartbeat.
155
+ info(`[index:${phase}] ${progressPrefix}${message}`);
156
+ }
111
157
  },
112
158
  signal: controller.signal,
113
159
  });
@@ -126,6 +172,8 @@ export const indexCommand = defineCommand({
126
172
  clearLogFile();
127
173
  process.off("SIGINT", abort);
128
174
  process.off("SIGTERM", abort);
175
+ if (lockAcquisition.state === "acquired")
176
+ releaseIndexRebuildLock(lockAcquisition.ownership);
129
177
  }
130
178
  });
131
179
  },
@@ -29,7 +29,11 @@ import { getParsedInvocation } from "../../cli/invocation.js";
29
29
  import { parsePositiveIntFlag } from "../../cli/parse-args.js";
30
30
  import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
31
31
  import { UsageError } from "../../core/errors.js";
32
+ import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
33
+ import { getOutputMode } from "../../output/context.js";
32
34
  import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
35
+ import { akmSearch, parseSearchSource } from "../read/search.js";
36
+ import { rejectRetiredSourceFlag } from "../read/search-cli.js";
33
37
  import { akmTaskExplain } from "./explain.js";
34
38
  import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksPrune, akmTasksRun, akmTasksSync, akmTasksSyncPlan, } from "./tasks.js";
35
39
  import { akmTaskValidate } from "./validate.js";
@@ -480,6 +484,47 @@ const tasksPruneCommand = defineJsonCommand({
480
484
  outputWithExitCode("task-prune", result, taskPruneExitCode(result));
481
485
  },
482
486
  });
487
+ /**
488
+ * #951: `akm task list` — a pure, zero-logic delegating alias for
489
+ * `akm search --type task`. 0.9.0 removed `task list` because it was a
490
+ * second, redundant IMPLEMENTATION of task listing, not because the
491
+ * spelling itself was off-limits; this reuses `search`'s exact envelope
492
+ * (its `results` alias comes along for free) instead of adding a second one.
493
+ * `query`/`--limit`/`--from` are handled identically to `searchCommand.run()`
494
+ * for the same inputs, including the retired `--source` guard (`../read/
495
+ * search-cli.ts`'s `rejectRetiredSourceFlag`, reused rather than copied) so
496
+ * `akm task list --source x` fails with the same actionable rename message
497
+ * as `akm search --type task --source x` instead of citty's silent absorb.
498
+ */
499
+ const tasksListCommand = defineJsonCommand({
500
+ meta: { name: "list", description: "List task assets (alias for `akm search --type task`)" },
501
+ args: {
502
+ query: {
503
+ type: "positional",
504
+ description: "Search query (omit to list all tasks)",
505
+ required: false,
506
+ default: "",
507
+ },
508
+ limit: { type: "string", description: "Maximum number of results" },
509
+ from: { type: "string", description: "Search source (local|registry|all)", default: "local" },
510
+ },
511
+ async run({ args }) {
512
+ rejectRetiredSourceFlag();
513
+ const query = (args.query ?? "").trim();
514
+ const limit = parsePositiveIntFlag(args.limit ?? undefined);
515
+ const source = parseSearchSource(args.from);
516
+ const outputMode = getOutputMode();
517
+ const result = await akmSearch({
518
+ query,
519
+ type: "task",
520
+ limit,
521
+ source,
522
+ eventSource: resolveUsageEventSource(),
523
+ attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
524
+ });
525
+ output("search", result);
526
+ },
527
+ });
483
528
  export const taskCommand = defineGroupCommand({
484
529
  meta: {
485
530
  name: "task",
@@ -491,12 +536,14 @@ export const taskCommand = defineGroupCommand({
491
536
  explain: tasksExplainCommand,
492
537
  validate: tasksValidateCommand,
493
538
  history: tasksHistoryCommand,
539
+ list: tasksListCommand,
494
540
  sync: tasksSyncCommand,
495
541
  prune: tasksPruneCommand,
496
542
  doctor: tasksDoctorCommand,
497
543
  },
498
- // Bare `akm task` reports scheduler diagnostics. Inspection of individual
499
- // tasks moved to the generic `akm search` / `akm show <bundle//tasks/id>`.
544
+ // Bare `akm task` reports scheduler diagnostics. Deeper inspection of
545
+ // individual tasks is the generic `akm show <bundle//tasks/id>`; `list`
546
+ // above is a delegating alias for `akm search --type task` (#951).
500
547
  // No `defaultRun`: bare `akm task` is a usage error (exit 2), the canonical
501
548
  // bare-group behavior — owner ruling 12. Run `akm task doctor` for what the
502
549
  // bare form used to run.