akm-cli 0.10.261008-beta → 0.10.261010-beta

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 (82) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/STABILITY.md +25 -8
  3. package/dist/api.d.ts +6 -0
  4. package/dist/api.js +12 -1
  5. package/dist/assets/prompts/consolidate-pair-check.md +2 -0
  6. package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
  7. package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
  8. package/dist/assets/prompts/consolidate-pair.md +4 -12
  9. package/dist/assets/prompts/memory-infer-system.md +1 -1
  10. package/dist/assets/prompts/memory-infer-user.md +3 -2
  11. package/dist/assets/ui/app.js +59 -2
  12. package/dist/assets/ui/asset.js +71 -6
  13. package/dist/assets/ui/bundles.js +80 -3
  14. package/dist/assets/ui/diff-view.js +1 -1
  15. package/dist/assets/ui/editor.js +103 -0
  16. package/dist/assets/ui/index.html +2 -0
  17. package/dist/assets/ui/lib.js +51 -1
  18. package/dist/assets/ui/markdown.js +86 -19
  19. package/dist/assets/ui/search.js +25 -5
  20. package/dist/assets/ui/styles.css +70 -0
  21. package/dist/commands/health/improve-metrics.js +11 -1
  22. package/dist/commands/health/md-report.js +3 -3
  23. package/dist/commands/health.js +1 -2
  24. package/dist/commands/improve/consolidate/pair-pass.js +59 -31
  25. package/dist/commands/improve/extract.js +18 -12
  26. package/dist/commands/improve/loop-stages.js +2 -2
  27. package/dist/commands/improve/memory/memory-belief.js +2 -2
  28. package/dist/commands/proposal/repository.js +44 -13
  29. package/dist/commands/read/curate.js +78 -19
  30. package/dist/commands/read/knowledge.js +109 -77
  31. package/dist/commands/read/search-cli.js +25 -11
  32. package/dist/commands/read/search.js +3 -3
  33. package/dist/commands/read/show.js +47 -1
  34. package/dist/commands/sources/bundle-cli.js +4 -6
  35. package/dist/commands/sources/installed-stashes.js +10 -19
  36. package/dist/commands/ui/api.js +95 -5
  37. package/dist/commands/ui/document-source.js +51 -0
  38. package/dist/commands/ui/git-read.js +18 -2
  39. package/dist/commands/ui/proposal-actions.js +5 -3
  40. package/dist/commands/ui/server.js +25 -13
  41. package/dist/commands/ui/ui-cli.js +3 -3
  42. package/dist/commands/ui/workspace-actions.js +264 -0
  43. package/dist/commands/workflow-cli.js +3 -1
  44. package/dist/core/adapter/adapters/akm-adapter.js +1 -0
  45. package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
  46. package/dist/core/adapter/adapters/okf-adapter.js +3 -16
  47. package/dist/core/asset/akm-markdown.js +19 -3
  48. package/dist/core/asset/markdown-link-targets.js +43 -0
  49. package/dist/core/asset/markdown.js +14 -6
  50. package/dist/core/config/config.js +6 -4
  51. package/dist/core/non-task-input.js +67 -3
  52. package/dist/core/write-source.js +2 -2
  53. package/dist/indexer/index-writer-lock.js +5 -1
  54. package/dist/indexer/passes/memory-inference.js +28 -6
  55. package/dist/indexer/passes/metadata.js +17 -0
  56. package/dist/indexer/scan/doc-to-entry.js +1 -0
  57. package/dist/indexer/search/db-search.js +16 -7
  58. package/dist/indexer/search/search-hit-enrichers.js +3 -3
  59. package/dist/llm/memory-infer.js +23 -14
  60. package/dist/output/shapes/curate.js +3 -1
  61. package/dist/output/shapes/helpers.js +6 -0
  62. package/dist/output/text/show-format.js +7 -0
  63. package/dist/scripts/akm-migrate-node.js +1820 -329
  64. package/dist/scripts/akm-migrate.js +1820 -329
  65. package/dist/storage/repositories/index-entries-repository.js +16 -0
  66. package/dist/storage/repositories/index-fts-repository.js +28 -6
  67. package/dist/storage/repositories/index-vec-repository.js +19 -3
  68. package/dist/workflows/exec/environment.js +4 -8
  69. package/dist/workflows/parser.js +13 -4
  70. package/docs/architecture/0.10-process-rules.md +9 -7
  71. package/docs/architecture/architecture.md +4 -0
  72. package/docs/architecture/internals/storage-locations.md +1 -1
  73. package/docs/architecture/persisted-data-compat.md +1 -1
  74. package/docs/guides/author-workflows.md +78 -28
  75. package/docs/guides/bundles.md +16 -2
  76. package/docs/reference/api.md +19 -1
  77. package/docs/reference/cli.md +90 -18
  78. package/docs/reference/env-and-secrets.md +11 -0
  79. package/docs/reference/memory.md +7 -6
  80. package/docs/reference/ui-api.md +84 -8
  81. package/docs/reference/workflow-schema.md +63 -32
  82. package/package.json +2 -1
@@ -8,10 +8,12 @@
8
8
  * (knowledge and memory) from the CLI. Extracted to keep the CLI entry point
9
9
  * focused on command definitions and routing.
10
10
  */
11
+ import { createHash } from "node:crypto";
11
12
  import fs from "node:fs";
12
13
  import path from "node:path";
13
14
  import { parse as yamlParse } from "yaml";
14
15
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
16
+ import { normalizeAkmMarkdown } from "../../core/asset/akm-markdown.js";
15
17
  import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../../core/asset/asset-create.js";
16
18
  import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
17
19
  import { assembleAsset } from "../../core/asset/asset-serialize.js";
@@ -24,6 +26,7 @@ import { resolveBundleWriteTarget, resolveMutationTarget } from "../../core/muta
24
26
  import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
25
27
  import { warn } from "../../core/warn.js";
26
28
  import { commitWriteTargetBoundary, formatRefForMessage, recordWriteTargetPath, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
29
+ import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
27
30
  import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
28
31
  import { deriveInstallations, slugForPath } from "../../indexer/installations.js";
29
32
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
@@ -502,16 +505,10 @@ export function resolveSupersedesForWrite(rawRefs, target, targetFlag) {
502
505
  }
503
506
  return plan;
504
507
  }
505
- // ── Asset writing ────────────────────────────────────────────────────────────
506
- /**
507
- * Write a markdown asset (knowledge or memory) to the resolved write target.
508
- *
509
- * Resolves the write target via the v1 precedence chain (`--bundle` →
510
- * `defaultWriteTarget` → working stash), validates the path is within the
511
- * type root, enforces `--force` semantics, and delegates the actual write
512
- * to `writeAssetToSource`.
513
- */
514
508
  export async function writeMarkdownAsset(options) {
509
+ if (options.expectedContentHash !== undefined && !/^[0-9a-f]{64}$/.test(options.expectedContentHash)) {
510
+ throw new UsageError("The expected content hash must be a lowercase hex SHA-256 digest (64 characters).", "INVALID_FLAG_VALUE");
511
+ }
515
512
  const cfg = loadConfig();
516
513
  // `--name` is the flat asset name; `--path` is the subdirectory under the
517
514
  // type root. Combine them into the nested name the path resolver expects.
@@ -524,16 +521,10 @@ export async function writeMarkdownAsset(options) {
524
521
  const { target } = resolved;
525
522
  const { source, config } = target;
526
523
  const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
527
- // Pre-flight: existence + force semantics. The helper itself overwrites
528
- // unconditionally; the CLI surfaces a friendlier UsageError before any
529
- // disk activity when --force is absent.
530
524
  const assetPath = assetPathForName(options.type, typeRoot, normalizedName);
531
525
  if (!isWithin(assetPath, typeRoot)) {
532
526
  throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
533
527
  }
534
- if (fs.existsSync(assetPath) && !options.force) {
535
- throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
536
- }
537
528
  // A correction cannot supersede ITSELF. Under `--force` the ref resolves to
538
529
  // the very file this command is about to overwrite, and the demotion would
539
530
  // immediately mark the fresh correction superseded (plus a self-xref) —
@@ -544,72 +535,113 @@ export async function writeMarkdownAsset(options) {
544
535
  throw new UsageError(`--supersedes ${item.ref} resolves to the asset being written ("${resolved.displayRef}") — a correction cannot supersede itself.`, "INVALID_FLAG_VALUE", "Write the correction under a different --name, or drop --supersedes when overwriting an asset in place with --force.");
545
536
  }
546
537
  }
547
- const result = await writeAssetToSource(source, config, resolved.ref, options.content);
548
- const durableRef = result.ref;
549
- result.ref = resolved.displayRef;
550
- // SPEC-5 (--supersedes): demote each superseded asset by mutating its
551
- // frontmatter (`beliefState: superseded` + sorted-set-append `supersededBy`;
552
- // every other key and the body are preserved). Ordered BEFORE
553
- // commitWriteTargetBoundary so a git target batches the correction and the
554
- // demoted incumbent into the single boundary commit instead of leaving the
555
- // metadata edit as dirty residue after it.
556
- const superseded = [];
557
- const demotedByRoot = new Map();
558
- for (const item of options.supersedes ?? []) {
559
- if (!item.writable) {
560
- const reason = item.reason ?? "target is not writable";
561
- warn(`Warning: superseded asset ${item.ref} was NOT demoted (${reason}). ` +
562
- "The correction was written and cites it in xrefs; demote the old asset where it is writable.");
563
- superseded.push({ ref: item.ref, applied: false, reason });
564
- continue;
565
- }
566
- // The demotion round-trips the old file's frontmatter through the YAML
567
- // parser. A malformed block would silently fall back to the lossy lenient
568
- // scanner and re-serializing that result destroys list/nested values —
569
- // skip instead of rewriting, mirroring mergeXrefsIntoContent's
570
- // abort-on-malformed policy (the correction itself still writes).
571
- let oldFrontmatter = null;
572
- try {
573
- oldFrontmatter = parseFrontmatter(fs.readFileSync(item.filePath, "utf8")).frontmatter;
574
- }
575
- catch {
576
- // Unreadable file — let writeSupersededEdge surface the real fs error.
577
- }
578
- if (oldFrontmatter?.trim() && !isParseableYamlMapping(oldFrontmatter)) {
579
- const reason = "its existing frontmatter is not a parseable YAML mapping — rewriting it would drop the values the parser could not read";
580
- warn(`Warning: superseded asset ${item.ref} was NOT demoted (${reason}). ` +
581
- "The correction was written and cites it in xrefs; fix the old asset's frontmatter and re-run the correction with --force.");
582
- superseded.push({ ref: item.ref, applied: false, reason });
583
- continue;
538
+ // The lease covers only what reads or changes authored files: the existence
539
+ // and revision checks, the write, the demotions and the boundary commit. The
540
+ // commit pushes inside the lease on purpose: a push from a writer that has
541
+ // released it would race the next writer's commit on the same branch (the
542
+ // conflicting-commit hazard the lease exists for), and the sync write path
543
+ // (`withWriteTargetMutation`) draws the same boundary. Indexing is a
544
+ // regenerable cache and runs after release.
545
+ const written = await withAssetMutationLease("markdown-write", async () => {
546
+ // Pre-flight: existence + force semantics. The helper itself overwrites
547
+ // unconditionally; the CLI surfaces a friendlier UsageError before any
548
+ // disk activity when --force is absent.
549
+ if (fs.existsSync(assetPath) && !options.force) {
550
+ throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
584
551
  }
585
- // A demotion failure (fs error, concurrent delete, malformed YAML the
586
- // pre-check missed) must NOT abort the correction: the new asset is
587
- // already on disk, and bailing out here would skip the boundary commit and
588
- // the write-path indexing below — leaving the correction unindexed and,
589
- // on a git target, uncommitted (and a re-run hits RESOURCE_ALREADY_EXISTS).
590
- // Degrade to the same applied:false report the non-writable path uses.
591
- try {
592
- const sameSource = path.resolve(item.stashRoot) === path.resolve(source.path);
593
- writeSupersededEdge(item.filePath, durableRef);
594
- if (sameSource) {
595
- recordWriteTargetPath(source, item.filePath);
552
+ if (options.expectedContentHash !== undefined) {
553
+ let existing;
554
+ try {
555
+ existing = fs.readFileSync(assetPath);
556
+ }
557
+ catch (error) {
558
+ if (error.code !== "ENOENT")
559
+ throw error;
560
+ }
561
+ if (!existing || createHash("sha256").update(existing).digest("hex") !== options.expectedContentHash) {
562
+ throw new UsageError("The document changed or was removed after it was loaded. Reload its source before saving.", "ASSET_CONTENT_CHANGED");
563
+ }
564
+ // Unchanged when the live bytes are the submission itself (an untouched
565
+ // document keeps its bytes, missing metadata included) or exactly what
566
+ // the writer would write for it (e.g. the submission only lacks the
567
+ // final line break the writer adds).
568
+ const unchanged = existing.equals(Buffer.from(options.content, "utf8")) ||
569
+ existing.equals(Buffer.from(normalizeAkmMarkdown(options.content, resolved.ref.type), "utf8"));
570
+ if (unchanged && !options.supersedes?.length) {
571
+ return { ref: resolved.displayRef, path: assetPath, stashDir: source.path, unchanged: true };
596
572
  }
597
573
  }
598
- catch (error) {
599
- const reason = `demotion failed: ${error instanceof Error ? error.message : String(error)}`;
600
- warn(`Warning: superseded asset ${item.ref} was NOT demoted (${reason}). ` +
601
- "The correction was written and cites it in xrefs; demote the old asset manually or re-run the correction with --force.");
602
- superseded.push({ ref: item.ref, applied: false, reason });
603
- continue;
574
+ const result = await writeAssetToSource(source, config, resolved.ref, options.content);
575
+ const durableRef = result.ref;
576
+ result.ref = resolved.displayRef;
577
+ // SPEC-5 (--supersedes): demote each superseded asset by mutating its
578
+ // frontmatter (`beliefState: superseded` + sorted-set-append `supersededBy`;
579
+ // every other key and the body are preserved). Ordered BEFORE
580
+ // commitWriteTargetBoundary so a git target batches the correction and the
581
+ // demoted incumbent into the single boundary commit instead of leaving the
582
+ // metadata edit as dirty residue after it.
583
+ const superseded = [];
584
+ const demotedByRoot = new Map();
585
+ for (const item of options.supersedes ?? []) {
586
+ if (!item.writable) {
587
+ const reason = item.reason ?? "target is not writable";
588
+ warn(`Warning: superseded asset ${item.ref} was NOT demoted (${reason}). ` +
589
+ "The correction was written and cites it in xrefs; demote the old asset where it is writable.");
590
+ superseded.push({ ref: item.ref, applied: false, reason });
591
+ continue;
592
+ }
593
+ // The demotion round-trips the old file's frontmatter through the YAML
594
+ // parser. A malformed block would silently fall back to the lossy lenient
595
+ // scanner and re-serializing that result destroys list/nested values —
596
+ // skip instead of rewriting, mirroring mergeXrefsIntoContent's
597
+ // abort-on-malformed policy (the correction itself still writes).
598
+ let oldFrontmatter = null;
599
+ try {
600
+ oldFrontmatter = parseFrontmatter(fs.readFileSync(item.filePath, "utf8")).frontmatter;
601
+ }
602
+ catch {
603
+ // Unreadable file — let writeSupersededEdge surface the real fs error.
604
+ }
605
+ if (oldFrontmatter?.trim() && !isParseableYamlMapping(oldFrontmatter)) {
606
+ const reason = "its existing frontmatter is not a parseable YAML mapping — rewriting it would drop the values the parser could not read";
607
+ warn(`Warning: superseded asset ${item.ref} was NOT demoted (${reason}). ` +
608
+ "The correction was written and cites it in xrefs; fix the old asset's frontmatter and re-run the correction with --force.");
609
+ superseded.push({ ref: item.ref, applied: false, reason });
610
+ continue;
611
+ }
612
+ // A demotion failure (fs error, concurrent delete, malformed YAML the
613
+ // pre-check missed) must NOT abort the correction: the new asset is
614
+ // already on disk, and bailing out here would skip the boundary commit and
615
+ // the write-path indexing below — leaving the correction unindexed and,
616
+ // on a git target, uncommitted (and a re-run hits RESOURCE_ALREADY_EXISTS).
617
+ // Degrade to the same applied:false report the non-writable path uses.
618
+ try {
619
+ const sameSource = path.resolve(item.stashRoot) === path.resolve(source.path);
620
+ writeSupersededEdge(item.filePath, durableRef);
621
+ if (sameSource) {
622
+ recordWriteTargetPath(source, item.filePath);
623
+ }
624
+ }
625
+ catch (error) {
626
+ const reason = `demotion failed: ${error instanceof Error ? error.message : String(error)}`;
627
+ warn(`Warning: superseded asset ${item.ref} was NOT demoted (${reason}). ` +
628
+ "The correction was written and cites it in xrefs; demote the old asset manually or re-run the correction with --force.");
629
+ superseded.push({ ref: item.ref, applied: false, reason });
630
+ continue;
631
+ }
632
+ superseded.push({ ref: item.ref, applied: true });
633
+ const files = demotedByRoot.get(item.stashRoot) ?? [];
634
+ files.push(item.filePath);
635
+ demotedByRoot.set(item.stashRoot, files);
604
636
  }
605
- superseded.push({ ref: item.ref, applied: true });
606
- const files = demotedByRoot.get(item.stashRoot) ?? [];
607
- files.push(item.filePath);
608
- demotedByRoot.set(item.stashRoot, files);
609
- }
610
- // 0.9.0 (issue #507): single batch commit at the write boundary for git
611
- // targets. No-op for filesystem/primary-stash targets.
612
- commitWriteTargetBoundary(target, `Update ${formatRefForMessage(resolved.ref)}`);
637
+ // 0.9.0 (issue #507): single batch commit at the write boundary for git
638
+ // targets. No-op for filesystem/primary-stash targets.
639
+ commitWriteTargetBoundary(target, `Update ${formatRefForMessage(resolved.ref)}`);
640
+ return { result, superseded, demotedByRoot };
641
+ });
642
+ if (written.unchanged)
643
+ return written;
644
+ const { result, superseded, demotedByRoot } = written;
613
645
  // Write-path indexing: the asset is searchable immediately. Fail-open; reads
614
646
  // no longer trigger reindexes, so keeping the index current is the writer's
615
647
  // job. Demoted files reindex under their own containing root (usually the
@@ -24,6 +24,14 @@ import { deliverRendered } from "../../output/html-render.js";
24
24
  import { akmCurate, packCuratedHits } from "./curate.js";
25
25
  import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./search.js";
26
26
  import { akmShowUnified } from "./show.js";
27
+ const scopeFilterArg = {
28
+ type: "string",
29
+ description: "Scope filter (repeatable): --filter user=<id> --filter agent=<id> --filter run=<id> --filter channel=<name>. Narrows results without changing ranking.",
30
+ };
31
+ const beliefArg = {
32
+ type: "string",
33
+ description: "Belief filter on any asset: all|current|historical. current keeps active/asserted and unflagged assets; historical keeps deprecated/contradicted/superseded/archived assets. Labels do not establish factual truth.",
34
+ };
27
35
  export const searchCommand = defineJsonCommand({
28
36
  meta: { name: "search", description: "Search the bundle" },
29
37
  args: {
@@ -44,20 +52,13 @@ export const searchCommand = defineJsonCommand({
44
52
  description: "Include asset-level search results (only meaningful with --from registry|all)",
45
53
  default: false,
46
54
  },
47
- filter: {
48
- type: "string",
49
- description: "Scope filter (repeatable): --filter user=<id> --filter agent=<id> --filter run=<id> --filter channel=<name>. Narrows results without changing ranking.",
50
- },
55
+ filter: scopeFilterArg,
51
56
  "include-proposed": {
52
57
  type: "boolean",
53
58
  description: 'Include entries with quality:"proposed" in the result set. Excluded by default.',
54
59
  default: false,
55
60
  },
56
- belief: {
57
- type: "string",
58
- description: "Memory belief filter: all|current|historical. current keeps active memory beliefs; historical keeps contradicted/superseded/archived memory beliefs.",
59
- default: "all",
60
- },
61
+ belief: { ...beliefArg, default: "current" },
61
62
  "include-sessions": {
62
63
  type: "boolean",
63
64
  description: "Include session assets (excluded from default search results via config.search.defaultExcludeTypes).",
@@ -110,6 +111,8 @@ export const curateCommand = defineJsonCommand({
110
111
  },
111
112
  limit: { type: "string", description: "Maximum number of curated results", default: "4" },
112
113
  from: { type: "string", description: "Search source (local|registry|all)", default: "local" },
114
+ filter: scopeFilterArg,
115
+ belief: { ...beliefArg, default: "current" },
113
116
  pack: {
114
117
  type: "string",
115
118
  description: "Pack the ranked stash hits' full content into a single token-budgeted blob instead of returning refs " +
@@ -123,7 +126,14 @@ export const curateCommand = defineJsonCommand({
123
126
  async run({ args }) {
124
127
  const packBudget = parsePositiveIntFlag(args.pack ?? undefined, "--pack");
125
128
  const outputMode = getOutputMode();
126
- const curated = await runCurate({ query: args.query, type: args.type, limit: args.limit, from: args.from }, outputMode);
129
+ const curated = await runCurate({
130
+ query: args.query,
131
+ type: args.type,
132
+ limit: args.limit,
133
+ from: args.from,
134
+ filters: parseAllFlagValues("--filter"),
135
+ belief: args.belief,
136
+ }, outputMode);
127
137
  if (packBudget !== undefined) {
128
138
  const packed = await packCuratedHits(curated, packBudget);
129
139
  deliverRendered(outputMode.format === "text" ? formatCuratePackText(packed) : JSON.stringify(packed.items, null, 2), outputMode.outputPath);
@@ -149,6 +159,8 @@ export async function runCurate(args, outputMode) {
149
159
  type: args.type,
150
160
  limit: limitParsed && limitParsed > 0 ? limitParsed : 4,
151
161
  source: parseSearchSource(args.from ?? "local"),
162
+ filters: parseScopeFilterFlags(args.filters ?? [], "--filter"),
163
+ belief: parseBeliefFilterMode(args.belief ?? "current"),
152
164
  eventSource: resolveUsageEventSource(),
153
165
  attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
154
166
  });
@@ -158,7 +170,9 @@ function formatCuratePackText(packed) {
158
170
  if (packed.items.length === 0) {
159
171
  return `No packed content for "${packed.query}" (budget ${packed.budget} tokens).`;
160
172
  }
161
- return packed.items.map((item) => `## ${item.ref}\n\n${item.content}`).join("\n\n");
173
+ return packed.items
174
+ .map((item) => `## ${item.ref}\n\n${item.reason ? `${item.reason}\n\n` : ""}${item.content}`)
175
+ .join("\n\n");
162
176
  }
163
177
  /**
164
178
  * Reject any positional after the ref: `akm show` takes one ref, and a stray
@@ -103,7 +103,7 @@ export async function akmSearch(input) {
103
103
  const sources = allSources;
104
104
  const filters = normalizeScopeFilters(input.filters);
105
105
  const includeProposed = input.includeProposed === true;
106
- const belief = input.belief ?? "all";
106
+ const belief = parseBeliefFilterMode(input.belief ?? "current");
107
107
  const localResult = source === "registry"
108
108
  ? undefined
109
109
  : await searchLocal({
@@ -118,7 +118,7 @@ export async function akmSearch(input) {
118
118
  beliefFilter: belief,
119
119
  // When `--from <name>` narrowed the source list above, propagate
120
120
  // that intent down to the database layer so FTS/vector hits from
121
- // sources outside the narrowed set are filtered out post-ranking.
121
+ // sources outside the narrowed set cannot consume candidate budgets.
122
122
  // Without this, the index (which spans every configured source)
123
123
  // would leak hits from sources the caller did not request.
124
124
  restrictToSources: namedSourceName !== undefined,
@@ -384,7 +384,7 @@ export function parseBeliefFilterMode(value) {
384
384
  * `undefined` when nothing meaningful remains, so callers don't pay for an
385
385
  * empty-filter post-walk in `searchLocal`.
386
386
  */
387
- function normalizeScopeFilters(raw) {
387
+ export function normalizeScopeFilters(raw) {
388
388
  if (!raw)
389
389
  return undefined;
390
390
  const out = {};
@@ -34,13 +34,15 @@ import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
34
34
  import { presentationFor } from "../../core/type-presentation.js";
35
35
  import { warn, warnOnce } from "../../core/warn.js";
36
36
  import { lookupBundleRef, lookupBundleRefWithResolution, lookupBundleRefWithResolutionReadonly, } from "../../indexer/indexer.js";
37
- import { projectMarkdownFragmentContent } from "../../indexer/passes/metadata.js";
37
+ import { applyCuratedFrontmatter, applyProvenanceFrontmatter, projectMarkdownFragmentContent, } from "../../indexer/passes/metadata.js";
38
38
  import { ensurePrimaryIndexForRead, resolveReadSources } from "../../indexer/read-preflight.js";
39
+ import { applyEntryFilters } from "../../indexer/search/db-search.js";
39
40
  import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries, } from "../../indexer/search/search-source.js";
40
41
  import { recentShowCount, recordShowUsage } from "../../indexer/usage/show-usage.js";
41
42
  import { buildFileContext, buildRenderContext, getRenderer, } from "../../indexer/walk/file-context.js";
42
43
  import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
43
44
  import { withIndexDb } from "../../storage/repositories/index-db.js";
45
+ import { getEntryById, getEntryIdByFilePath } from "../../storage/repositories/index-entries-repository.js";
44
46
  import { getIndexedMarkdownFragment } from "../../storage/repositories/index-fts-repository.js";
45
47
  import { readEntryLinks } from "../../storage/repositories/index-links-repository.js";
46
48
  import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
@@ -258,6 +260,34 @@ export async function showLocal(input) {
258
260
  "Run `akm bundle create` to create the stash directory, or check `akm bundle list` for configured paths.");
259
261
  }
260
262
  const fileCtx = buildFileContext(sourceStashDir, assetPath);
263
+ let beliefState;
264
+ if (input.recall) {
265
+ const policy = input.recall;
266
+ const sources = policy.source && !["local", "all"].includes(policy.source)
267
+ ? resolveSourcesForOrigin(policy.source, allSources)
268
+ : allSources;
269
+ const eligible = withIndexDb((db) => {
270
+ const id = getEntryIdByFilePath(db, assetPath);
271
+ const row = id === undefined ? undefined : getEntryById(db, id);
272
+ if (!row || id === undefined)
273
+ return false;
274
+ // Source-live show must not restore a belief/scope changed after indexing.
275
+ const entry = { ...row.entry, scope: undefined };
276
+ applyCuratedFrontmatter(entry, parseFrontmatter(fileCtx.content()).data);
277
+ const allowed = applyEntryFilters([{ ...row, id, entry }], {
278
+ db,
279
+ sources,
280
+ restrictToSources: true,
281
+ filters: policy.filters,
282
+ includeProposed: false,
283
+ beliefFilter: policy.belief ?? "current",
284
+ });
285
+ beliefState = entry.beliefState;
286
+ return allowed.length > 0 && (!policy.type || policy.type === "any" || entry.type === policy.type);
287
+ }, { readOnly: input.readOnly === true });
288
+ if (!eligible)
289
+ throw new NotFoundError(`Asset not found for recall filters: ${input.ref}`);
290
+ }
261
291
  const presentedName = indexedEntry.name;
262
292
  const opaqueFragmentSelector = parsed.fragment?.startsWith("akm-fragment-") === true;
263
293
  // Friendly heading selectors retain the source-live exact response contract.
@@ -302,6 +332,8 @@ export async function showLocal(input) {
302
332
  catch (error) {
303
333
  throwIndexedPathNotFound(error, input.ref);
304
334
  }
335
+ if (beliefState)
336
+ response.beliefState = beliefState;
305
337
  response.type = indexedEntry.type;
306
338
  response.name = indexedEntry.name;
307
339
  const isPrimaryStash = source?.isDefault === true;
@@ -364,8 +396,22 @@ export async function showLocal(input) {
364
396
  delete response.toolPolicy;
365
397
  }
366
398
  const editable = isEditable(assetPath, config, allSources);
399
+ // Evidence must describe the same revision as the rendered claim: ordinary
400
+ // Markdown show is source-live, while indexed fragment show is a snapshot.
401
+ let evidenceMetadata;
402
+ if (["memory", "lesson", "knowledge"].includes(response.type)) {
403
+ evidenceMetadata = indexedFragment ? indexedEntry.document : { name: presentedName, type: response.type };
404
+ if (!indexedFragment && evidenceMetadata) {
405
+ const frontmatter = parseFrontmatter(fileCtx.content()).data;
406
+ applyCuratedFrontmatter(evidenceMetadata, frontmatter);
407
+ applyProvenanceFrontmatter(evidenceMetadata, frontmatter);
408
+ }
409
+ }
367
410
  const fullResponse = {
368
411
  ...response,
412
+ ...(evidenceMetadata?.evidence ? { evidence: evidenceMetadata.evidence } : {}),
413
+ ...(evidenceMetadata?.evidenceSources ? { evidenceSources: evidenceMetadata.evidenceSources } : {}),
414
+ ...(evidenceMetadata?.evidence && evidenceMetadata.provenance ? { provenance: evidenceMetadata.provenance } : {}),
369
415
  ref: canonicalRef,
370
416
  origin: source?.registryId ?? null,
371
417
  editable,
@@ -139,14 +139,13 @@ const updateCommand = defineJsonCommand({
139
139
  description: "Allow an update containing dangerous env keys (e.g. LD_PRELOAD, PATH). Use only after explicitly reviewing the staged bundle.",
140
140
  default: false,
141
141
  },
142
- // F1/R-058: gates ONLY the rare branch where the resolved content
143
- // directory moves and the previous `localRoot` is deleted — a normal
144
- // refresh (the overwhelming majority of updates) deletes nothing and
145
- // never consults this flag. Mirrors `remove`'s `-y/--yes`.
142
+ // Deprecated no-op, accepted through 0.10 so scripts that pass it do not
143
+ // fail with UNKNOWN_FLAG. Deleting the previous directory of a read-only
144
+ // install whose content moved no longer asks: it is a re-fetchable cache.
146
145
  yes: {
147
146
  type: "boolean",
148
147
  alias: "y",
149
- description: "Skip the confirmation prompt when an update needs to delete a previous install directory (because the resolved content location moved). No effect on a normal refresh, which deletes nothing.",
148
+ description: "Deprecated, no effect: update no longer asks before deleting a moved read-only install directory. Accepted through 0.10 for existing scripts.",
150
149
  default: false,
151
150
  },
152
151
  },
@@ -157,7 +156,6 @@ const updateCommand = defineJsonCommand({
157
156
  target: args.target,
158
157
  all: args.all,
159
158
  force: args.force,
160
- yes: args.yes,
161
159
  allowDangerousEnvKeys: args["allow-dangerous-env-keys"],
162
160
  });
163
161
  }
@@ -611,15 +611,13 @@ function managedInstallViewOfPlainNpm(config, npmSource) {
611
611
  * Sync a single registry-managed install and return the processed record:
612
612
  * fetch into staging, audit, publish, write its lock entry, reindex.
613
613
  *
614
- * `yes` gates ONLY the destructive branch below (deleting a previous
615
- * `localRoot` whose content moved) — it has no effect on the sync itself. A
616
- * normal refresh that resolves the SAME content directory (the overwhelming
617
- * majority of updates) never reaches that branch, so it never prompts and
618
- * never requires `--yes` (F1/R-058: `update` must stay usable bare in
619
- * scripts; only the branch that can `rm -rf` needs a gate, not the whole
620
- * command).
614
+ * Read-only installs are keyed by resolved revision, so every upstream change
615
+ * resolves a new content directory, and the previous one is deleted once the
616
+ * new one is published. That directory is a re-fetchable cache: local and
617
+ * writable installs never take this path, and a directory another bundle or
618
+ * lock entry still references is kept. So deleting it never asks for confirmation.
621
619
  */
622
- async function updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys) {
620
+ async function updateManagedInstall(managed, force, stashDir, allowDangerousEnvKeys) {
623
621
  const { synced, staged } = await stageManagedUpdate(managed, force);
624
622
  try {
625
623
  await auditStagedUpdate(staged.auditRoot, managed.ref, allowDangerousEnvKeys);
@@ -642,12 +640,6 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowDangerou
642
640
  path.resolve(managed.localRoot) !== path.resolve(synced.contentDir) &&
643
641
  managed.source !== "local" &&
644
642
  !managed.writable;
645
- if (movedRoot) {
646
- const { confirmDestructive } = await import("../../cli/confirm.js");
647
- const confirmed = await confirmDestructive(`Update resolved a new content directory for "${managed.installId}" (${synced.contentDir}) and would delete the previous install directory at ${managed.localRoot}. This cannot be undone.`, { yes });
648
- if (!confirmed)
649
- throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
650
- }
651
643
  const index = await publishAndIndex(stashDir, async () => {
652
644
  staged.publish();
653
645
  await upsertLockEntry({
@@ -724,7 +716,6 @@ export async function akmUpdate(input) {
724
716
  const target = input?.target?.trim();
725
717
  const all = input?.all === true;
726
718
  const force = input?.force === true;
727
- const yes = input?.yes === true;
728
719
  const allowDangerousEnvKeys = input?.allowDangerousEnvKeys === true;
729
720
  const config = loadConfig();
730
721
  const managedInstalls = listManagedInstalls(config);
@@ -732,7 +723,7 @@ export async function akmUpdate(input) {
732
723
  // Registry-managed install (lock-backed) — re-download from its locator.
733
724
  const managed = resolveManagedTarget(config, target);
734
725
  if (managed) {
735
- const updated = await updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys);
726
+ const updated = await updateManagedInstall(managed, force, stashDir, allowDangerousEnvKeys);
736
727
  return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
737
728
  }
738
729
  // Plain git / website source (bundles without a lock) — provider re-sync.
@@ -789,7 +780,7 @@ export async function akmUpdate(input) {
789
780
  return false;
790
781
  });
791
782
  if (npmMatch) {
792
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, npmMatch), force, yes, stashDir, allowDangerousEnvKeys);
783
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, npmMatch), force, stashDir, allowDangerousEnvKeys);
793
784
  return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
794
785
  }
795
786
  const filesystemMatch = stashes.find((source) => {
@@ -812,7 +803,7 @@ export async function akmUpdate(input) {
812
803
  let latestIndex;
813
804
  for (const managed of selected) {
814
805
  try {
815
- const updated = await updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys);
806
+ const updated = await updateManagedInstall(managed, force, stashDir, allowDangerousEnvKeys);
816
807
  processed.push(updated.item);
817
808
  latestIndex = updated.index;
818
809
  }
@@ -842,7 +833,7 @@ export async function akmUpdate(input) {
842
833
  latestIndex = updated.index;
843
834
  }
844
835
  else if (plain.type === "npm") {
845
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, plain), force, yes, stashDir, allowDangerousEnvKeys);
836
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, plain), force, stashDir, allowDangerousEnvKeys);
846
837
  processed.push(updated.item);
847
838
  latestIndex = updated.index;
848
839
  }