akm-cli 0.9.14 → 0.9.15-beta.1

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 (110) hide show
  1. package/CHANGELOG.md +397 -0
  2. package/STABILITY.md +6 -3
  3. package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
  4. package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
  5. package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
  6. package/dist/assets/tasks/core/improve.yml +1 -1
  7. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  8. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
  9. package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
  10. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
  11. package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
  12. package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
  13. package/dist/cli/retired-commands.js +0 -1
  14. package/dist/cli/shared.js +9 -0
  15. package/dist/cli/unknown-flags.js +1 -0
  16. package/dist/cli.js +3 -2
  17. package/dist/commands/config-cli.js +85 -3
  18. package/dist/commands/env/env-cli.js +1 -42
  19. package/dist/commands/env/env.js +1 -1
  20. package/dist/commands/env/secret-cli.js +1 -2
  21. package/dist/commands/health/checks.js +357 -63
  22. package/dist/commands/health/engine-usage.js +45 -0
  23. package/dist/commands/health/improve-metrics.js +18 -0
  24. package/dist/commands/health/llm-usage.js +41 -1
  25. package/dist/commands/health/plugin-staleness.js +7 -3
  26. package/dist/commands/health/version-drift.js +93 -0
  27. package/dist/commands/health/windows.js +3 -1
  28. package/dist/commands/health.js +44 -9
  29. package/dist/commands/improve/consolidate/chunking.js +4 -2
  30. package/dist/commands/improve/improve-cli.js +99 -5
  31. package/dist/commands/improve/improve-report.js +154 -0
  32. package/dist/commands/improve/improve-result-file.js +45 -33
  33. package/dist/commands/improve/improve-strategies.js +133 -3
  34. package/dist/commands/improve/improve-usage-report.js +182 -0
  35. package/dist/commands/improve/improve.js +40 -3
  36. package/dist/commands/improve/locks.js +27 -78
  37. package/dist/commands/improve/planner.js +1 -0
  38. package/dist/commands/improve/preparation.js +9 -1
  39. package/dist/commands/improve/reflect.js +44 -4
  40. package/dist/commands/models-cli.js +50 -1
  41. package/dist/commands/proposal/repository.js +8 -3
  42. package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
  43. package/dist/commands/proposal/validators/proposal-validators.js +24 -0
  44. package/dist/commands/read/search-cli.js +38 -2
  45. package/dist/commands/read/show.js +103 -4
  46. package/dist/commands/sources/info.js +5 -1
  47. package/dist/commands/sources/self-update.js +2 -2
  48. package/dist/commands/sources/stash-cli.js +31 -0
  49. package/dist/commands/tasks/tasks-cli.js +49 -2
  50. package/dist/commands/workflow-cli.js +86 -12
  51. package/dist/core/asset/markdown-fragments.js +35 -0
  52. package/dist/core/config/config-schema.js +14 -0
  53. package/dist/core/config/config.js +302 -24
  54. package/dist/core/env-secret-ref.js +58 -5
  55. package/dist/core/errors.js +30 -0
  56. package/dist/core/improve-result.js +51 -0
  57. package/dist/core/loopback.js +17 -0
  58. package/dist/core/paths.js +11 -0
  59. package/dist/core/run-lock.js +96 -0
  60. package/dist/core/sensitive-marker-path.js +19 -0
  61. package/dist/core/state-db.js +74 -14
  62. package/dist/indexer/index-rebuild-lock.js +73 -0
  63. package/dist/indexer/index-writer-lock.js +40 -1
  64. package/dist/indexer/index-written-assets.js +21 -1
  65. package/dist/indexer/indexer.js +18 -17
  66. package/dist/indexer/materialize-embeddings.js +282 -32
  67. package/dist/indexer/search/db-search.js +49 -2
  68. package/dist/integrations/agent/engine-resolution.js +96 -6
  69. package/dist/integrations/agent/execution-definitions.js +6 -15
  70. package/dist/integrations/agent/execution-lowering.js +6 -1
  71. package/dist/integrations/agent/execution-preparation.js +1 -1
  72. package/dist/integrations/agent/model-map.js +123 -20
  73. package/dist/integrations/agent/prompts.js +40 -8
  74. package/dist/integrations/agent/runner-dispatch.js +9 -3
  75. package/dist/integrations/agent/runner.js +2 -0
  76. package/dist/llm/client.js +8 -3
  77. package/dist/llm/embedder.js +20 -8
  78. package/dist/llm/embedders/local.js +10 -2
  79. package/dist/llm/embedders/remote.js +188 -21
  80. package/dist/output/shapes/helpers.js +38 -2
  81. package/dist/output/shapes/models-list.js +16 -0
  82. package/dist/output/shapes/passthrough.js +2 -0
  83. package/dist/output/shapes.js +4 -0
  84. package/dist/output/text/command-format.js +29 -0
  85. package/dist/output/text/helpers.js +1 -1
  86. package/dist/output/text/improve-report.js +27 -0
  87. package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
  88. package/dist/output/text/show-format.js +4 -0
  89. package/dist/output/text.js +4 -0
  90. package/dist/scripts/akm-migrate-node.js +24798 -21732
  91. package/dist/scripts/akm-migrate.js +23408 -20343
  92. package/dist/storage/repositories/improve-runs-repository.js +34 -0
  93. package/dist/storage/repositories/index-fts-repository.js +49 -6
  94. package/dist/storage/repositories/index-vec-repository.js +30 -0
  95. package/dist/storage/repositories/workflow-runs-repository.js +55 -18
  96. package/dist/tasks/backends/cron.js +14 -7
  97. package/dist/tasks/run/run-workflow-task.js +16 -0
  98. package/dist/workflows/exec/child-workflow.js +2 -2
  99. package/dist/workflows/exec/dispatch-redaction.js +21 -9
  100. package/dist/workflows/exec/run-workflow.js +6 -5
  101. package/dist/workflows/runtime/runs.js +33 -5
  102. package/docs/migration/release-notes/0.9.15.md +52 -0
  103. package/docs/migration/release-notes/README.md +4 -0
  104. package/docs/reference/cli.md +245 -29
  105. package/docs/reference/configuration.md +180 -19
  106. package/docs/reference/data-and-telemetry.md +8 -0
  107. package/docs/reference/tasks.md +16 -1
  108. package/docs/reference/workflow-schema.md +5 -1
  109. package/package.json +1 -1
  110. package/schemas/akm-config.json +8 -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: {
@@ -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,6 +42,7 @@ 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";
@@ -66,6 +67,16 @@ export const indexCommand = defineCommand({
66
67
  description: "When combined with --clean, report stale entries without deleting them.",
67
68
  default: false,
68
69
  },
70
+ reembed: {
71
+ type: "boolean",
72
+ description: "Force re-embedding of every entry, bypassing the embedding-model-rename compatibility check.",
73
+ default: false,
74
+ },
75
+ "skip-if-locked": {
76
+ type: "boolean",
77
+ 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.",
78
+ default: false,
79
+ },
69
80
  },
70
81
  async run({ args }) {
71
82
  await runWithJsonErrors(async () => {
@@ -75,6 +86,23 @@ export const indexCommand = defineCommand({
75
86
  if (getHyphenatedBoolean(args, "re-enrich") || getParsedInvocation().getFlagValue("--re-enrich") !== undefined) {
76
87
  throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
77
88
  }
89
+ // #956: opt-in, non-blocking rebuild lock — never gates a human-typed
90
+ // `akm index` (it only warns and contends), but a scheduled/opportunistic
91
+ // caller can pass --skip-if-locked to step aside instead of piling up
92
+ // behind a run already in progress. Acquired before any other side
93
+ // effect (log file, spinner) so a skip does neither.
94
+ const lockAcquisition = tryAcquireIndexRebuildLock(args["skip-if-locked"]);
95
+ if (lockAcquisition.state === "skipped") {
96
+ output("index", {
97
+ ok: true,
98
+ skipped: {
99
+ reason: "lock-held",
100
+ pid: lockAcquisition.holder.pid,
101
+ startedAt: lockAcquisition.holder.startedAt,
102
+ },
103
+ });
104
+ return;
105
+ }
78
106
  const outputMode = getOutputMode();
79
107
  const controller = new AbortController();
80
108
  const abort = () => controller.abort(new Error("index interrupted"));
@@ -98,6 +126,7 @@ export const indexCommand = defineCommand({
98
126
  full: args.full,
99
127
  clean: args.clean,
100
128
  dryRun: args["dry-run"],
129
+ reembed: args.reembed,
101
130
  onProgress: ({ phase, message, processed, total }) => {
102
131
  latestMessage = message;
103
132
  const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
@@ -126,6 +155,8 @@ export const indexCommand = defineCommand({
126
155
  clearLogFile();
127
156
  process.off("SIGINT", abort);
128
157
  process.off("SIGTERM", abort);
158
+ if (lockAcquisition.state === "acquired")
159
+ releaseIndexRebuildLock(lockAcquisition.ownership);
129
160
  }
130
161
  });
131
162
  },
@@ -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.
@@ -11,7 +11,8 @@ import { getStringArg } from "../cli/parse-args.js";
11
11
  import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output, outputWithExitCode } from "../cli/shared.js";
12
12
  import { armAbortDeadline } from "../core/abort-deadline.js";
13
13
  import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
14
- import { NotFoundError, UsageError } from "../core/errors.js";
14
+ import { NotFoundError, TransientError, UsageError } from "../core/errors.js";
15
+ import { warn } from "../core/warn.js";
15
16
  import { akmIndex } from "../indexer/indexer.js";
16
17
  import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
17
18
  import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
@@ -30,10 +31,16 @@ const workflowStatusCommand = defineJsonCommand({
30
31
  "diagnostic text). Diagnostics only — step evidence stays deterministic and is unaffected.",
31
32
  default: false,
32
33
  },
34
+ "all-scopes": {
35
+ type: "boolean",
36
+ description: "When resolving a workflow ref (not a run id), search every scope instead of only the current one (#942).",
37
+ default: false,
38
+ },
33
39
  },
34
40
  async run({ args }) {
35
41
  const target = args.target;
36
42
  const includeUnits = args.units === true;
43
+ const allScopes = args["all-scopes"] === true;
37
44
  const resolvedRunId = await resolveWorkflowRunTarget(target);
38
45
  if (resolvedRunId !== undefined) {
39
46
  const result = await getWorkflowStatus(resolvedRunId, { includeUnits });
@@ -41,8 +48,9 @@ const workflowStatusCommand = defineJsonCommand({
41
48
  return;
42
49
  }
43
50
  let runs;
51
+ let scopeKey;
44
52
  try {
45
- ({ runs } = await listWorkflowRuns({ workflowRef: target }));
53
+ ({ runs, scopeKey } = await listWorkflowRuns({ workflowRef: target, allScopes }));
46
54
  }
47
55
  catch (error) {
48
56
  if (!target.includes(":") && !target.includes("/")) {
@@ -51,8 +59,17 @@ const workflowStatusCommand = defineJsonCommand({
51
59
  throw error;
52
60
  }
53
61
  const mostRecent = runs[0];
54
- if (!mostRecent)
62
+ if (!mostRecent) {
63
+ // #942: name the scope actually searched and point at `--all-scopes`
64
+ // rather than a bare "not found" — the ref-fallthrough lookup is
65
+ // scope-local by default, so "no runs" here means "none in THIS
66
+ // scope", not "none anywhere". Already searching every scope (or no
67
+ // real scope was filtered on) has nothing more specific to suggest.
68
+ if (!allScopes && scopeKey !== null) {
69
+ throw new NotFoundError(`No workflow runs found for ${target} in scope ${scopeKey}.`, "WORKFLOW_NOT_FOUND", `Run 'akm workflow status ${target} --all-scopes' to search every scope.`);
70
+ }
55
71
  throw new NotFoundError(`No workflow runs found for ${target}`, "WORKFLOW_NOT_FOUND");
72
+ }
56
73
  const result = await getWorkflowStatus(mostRecent.id, { includeUnits });
57
74
  output("workflow-status", result);
58
75
  },
@@ -70,12 +87,20 @@ const workflowListCommand = defineJsonCommand({
70
87
  description: "Also include child workflow runs (hidden by default, P3b)",
71
88
  default: false,
72
89
  },
90
+ "all-scopes": {
91
+ type: "boolean",
92
+ description: "Search every scope instead of only the current one (#942). The envelope's top-level `scopeKey` is " +
93
+ "`null` with this flag, otherwise the scope that was searched — so an empty `runs: []` is never " +
94
+ 'indistinguishable from "nothing anywhere".',
95
+ default: false,
96
+ },
73
97
  },
74
98
  async run({ args }) {
75
99
  const result = await listWorkflowRuns({
76
100
  workflowRef: args.ref,
77
101
  activeOnly: args.active,
78
102
  includeChildren: args.children,
103
+ allScopes: args["all-scopes"],
79
104
  });
80
105
  output("workflow-list", result);
81
106
  },
@@ -145,6 +170,20 @@ const workflowCreateCommand = defineJsonCommand({
145
170
  output("workflow-create", { ok: true, ...result });
146
171
  },
147
172
  });
173
+ /**
174
+ * `--skip-if-locked` (#948) eligibility: only these two named, retryable
175
+ * `TransientError` codes (#948 addendum — moved off UsageError, exit 75) turn
176
+ * a `workflow run` failure into a graceful skip — `RUN_LEASE_HELD` (another
177
+ * engine invocation is driving THIS run, `workflow-runs-repository.ts`'s
178
+ * single-driver lease) and `STATE_DB_CONTENDED` (an unrelated akm process is
179
+ * writing state.db right now, `core/state-db.ts`'s BEGIN IMMEDIATE retry
180
+ * exhaustion). Every other error — a bad flag, an unresolvable target —
181
+ * still fails loudly.
182
+ */
183
+ const WORKFLOW_RUN_SKIP_REASONS = {
184
+ RUN_LEASE_HELD: "lock-held",
185
+ STATE_DB_CONTENDED: "state-db-contended",
186
+ };
148
187
  const workflowRunCommand = defineJsonCommand({
149
188
  meta: {
150
189
  name: "run",
@@ -161,6 +200,13 @@ const workflowRunCommand = defineJsonCommand({
161
200
  "(never abandons it). A workflow ref only — passing a run id with --new is a usage error.",
162
201
  default: false,
163
202
  },
203
+ "skip-if-locked": {
204
+ type: "boolean",
205
+ description: "If another akm process already holds this run's engine lease, or state.db is busy with another " +
206
+ "writer, skip gracefully (exit 0) instead of failing (exit 75). Use for high-frequency scheduled runs " +
207
+ "so they don't pile up failures while a longer-running invocation is in progress.",
208
+ default: false,
209
+ },
164
210
  },
165
211
  async run({ args, rawArgs }) {
166
212
  const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
@@ -168,6 +214,7 @@ const workflowRunCommand = defineJsonCommand({
168
214
  const maxSteps = parseIntegerFlag(getStringArg(args, "max-steps"), "--max-steps", 1);
169
215
  const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0);
170
216
  const timeoutMs = parseWorkflowTimeout(getStringArg(args, "timeout"));
217
+ const skipIfLocked = args["skip-if-locked"];
171
218
  const controller = new AbortController();
172
219
  let signalExitCode;
173
220
  const interrupt = (signal) => {
@@ -185,14 +232,31 @@ const workflowRunCommand = defineJsonCommand({
185
232
  reason: `Workflow run timed out after ${timeoutMs}ms.`,
186
233
  });
187
234
  try {
188
- const result = await runWorkflowSteps({
189
- target: args.target,
190
- parameterFlags,
191
- ...(maxSteps !== undefined ? { maxSteps } : {}),
192
- ...(maxRetries !== undefined ? { maxRetries } : {}),
193
- newRun: args.new,
194
- signal: controller.signal,
195
- });
235
+ let result;
236
+ try {
237
+ result = await runWorkflowSteps({
238
+ target: args.target,
239
+ parameterFlags,
240
+ ...(maxSteps !== undefined ? { maxSteps } : {}),
241
+ ...(maxRetries !== undefined ? { maxRetries } : {}),
242
+ newRun: args.new,
243
+ signal: controller.signal,
244
+ });
245
+ }
246
+ catch (err) {
247
+ // #948: `--skip-if-locked` extends improve's "another run already
248
+ // holds this" skip semantics to `workflow run`. Only these two named,
249
+ // retryable TransientError codes are eligible (#948 addendum — moved
250
+ // off UsageError) — a bad flag or malformed input still fails loudly
251
+ // even with the flag set.
252
+ if (skipIfLocked && err instanceof TransientError && WORKFLOW_RUN_SKIP_REASONS[err.code]) {
253
+ const reason = WORKFLOW_RUN_SKIP_REASONS[err.code];
254
+ warn(`[workflow] ${err.message} skipping (--skip-if-locked)`);
255
+ output("workflow-run", { ok: true, target: args.target, skipped: { reason, message: err.message } });
256
+ return;
257
+ }
258
+ throw err;
259
+ }
196
260
  // The abort is observed between steps, so a deadline landing in the run's
197
261
  // final bookkeeping fires on a run that then finishes. Reporting that as
198
262
  // timed out would send an operator to resume a run with nothing left to
@@ -222,7 +286,17 @@ const WORKFLOW_RUN_VALUE_FLAGS = new Set([
222
286
  "shape",
223
287
  "output",
224
288
  ]);
225
- const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose", "new", "no-new"]);
289
+ const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set([
290
+ "quiet",
291
+ "verbose",
292
+ "help",
293
+ "no-quiet",
294
+ "no-verbose",
295
+ "new",
296
+ "no-new",
297
+ "skip-if-locked",
298
+ "no-skip-if-locked",
299
+ ]);
226
300
  export function parseWorkflowParameterFlags(rawArgs, target) {
227
301
  const flags = [];
228
302
  let targetSeen = false;
@@ -12,6 +12,8 @@ import { createHash } from "node:crypto";
12
12
  import { markdownHeadingSlug, parseMarkdownToc } from "./markdown.js";
13
13
  export const MARKDOWN_FRAGMENT_MAX_CHARS = 1600;
14
14
  export const MARKDOWN_FRAGMENT_PREFIX = "akm-fragment-";
15
+ export const MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS = 3200;
16
+ export const MARKDOWN_FRAGMENT_SELECTED_LABEL = "[Selected matching fragment]";
15
17
  function hash(text) {
16
18
  return createHash("sha256").update(text).digest("hex");
17
19
  }
@@ -144,3 +146,36 @@ export function splitMarkdownFragments(body, maxChars = MARKDOWN_FRAGMENT_MAX_CH
144
146
  export function fragmentForSelector(body, selector) {
145
147
  return splitMarkdownFragments(body).find((fragment) => fragment.fragmentId === selector || fragment.headingSlug === selector);
146
148
  }
149
+ /**
150
+ * Assemble the document lead and selected fragment under one hard character
151
+ * budget. The selected match is always labelled and last. When both pieces do
152
+ * not fit, lead bytes are discarded before any selected-fragment bytes so the
153
+ * evidence that caused retrieval remains intact whenever the caller's budget
154
+ * can hold it.
155
+ */
156
+ export function buildMarkdownLeadContext(fragments, selectedOrdinal, maxChars = MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS) {
157
+ if (!Number.isSafeInteger(maxChars) || maxChars <= 0) {
158
+ throw new RangeError("Markdown fragment context maxChars must be a positive safe integer");
159
+ }
160
+ const selected = fragments[selectedOrdinal];
161
+ if (!selected)
162
+ throw new RangeError(`Markdown fragment ordinal ${selectedOrdinal} is out of range`);
163
+ const selectedBlock = `${MARKDOWN_FRAGMENT_SELECTED_LABEL}\n${selected.text}`;
164
+ if (selectedBlock.length > maxChars) {
165
+ return { content: selectedBlock.slice(0, maxChars), truncated: true };
166
+ }
167
+ const lead = fragments[0];
168
+ if (!lead || lead.ordinal === selected.ordinal)
169
+ return { content: selectedBlock, truncated: false };
170
+ const separator = "\n\n";
171
+ const availableLeadChars = maxChars - selectedBlock.length - separator.length;
172
+ if (availableLeadChars <= 0)
173
+ return { content: selectedBlock, truncated: true };
174
+ const leadText = lead.text.slice(0, availableLeadChars).trimEnd();
175
+ if (!leadText)
176
+ return { content: selectedBlock, truncated: true };
177
+ return {
178
+ content: `${leadText}${separator}${selectedBlock}`,
179
+ truncated: leadText.length < lead.text.length,
180
+ };
181
+ }