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.
- package/CHANGELOG.md +97 -0
- package/STABILITY.md +25 -8
- package/dist/api.d.ts +6 -0
- package/dist/api.js +12 -1
- package/dist/assets/prompts/consolidate-pair-check.md +2 -0
- package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
- package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
- package/dist/assets/prompts/consolidate-pair.md +4 -12
- package/dist/assets/prompts/memory-infer-system.md +1 -1
- package/dist/assets/prompts/memory-infer-user.md +3 -2
- package/dist/assets/ui/app.js +59 -2
- package/dist/assets/ui/asset.js +71 -6
- package/dist/assets/ui/bundles.js +80 -3
- package/dist/assets/ui/diff-view.js +1 -1
- package/dist/assets/ui/editor.js +103 -0
- package/dist/assets/ui/index.html +2 -0
- package/dist/assets/ui/lib.js +51 -1
- package/dist/assets/ui/markdown.js +86 -19
- package/dist/assets/ui/search.js +25 -5
- package/dist/assets/ui/styles.css +70 -0
- package/dist/commands/health/improve-metrics.js +11 -1
- package/dist/commands/health/md-report.js +3 -3
- package/dist/commands/health.js +1 -2
- package/dist/commands/improve/consolidate/pair-pass.js +59 -31
- package/dist/commands/improve/extract.js +18 -12
- package/dist/commands/improve/loop-stages.js +2 -2
- package/dist/commands/improve/memory/memory-belief.js +2 -2
- package/dist/commands/proposal/repository.js +44 -13
- package/dist/commands/read/curate.js +78 -19
- package/dist/commands/read/knowledge.js +109 -77
- package/dist/commands/read/search-cli.js +25 -11
- package/dist/commands/read/search.js +3 -3
- package/dist/commands/read/show.js +47 -1
- package/dist/commands/sources/bundle-cli.js +4 -6
- package/dist/commands/sources/installed-stashes.js +10 -19
- package/dist/commands/ui/api.js +95 -5
- package/dist/commands/ui/document-source.js +51 -0
- package/dist/commands/ui/git-read.js +18 -2
- package/dist/commands/ui/proposal-actions.js +5 -3
- package/dist/commands/ui/server.js +25 -13
- package/dist/commands/ui/ui-cli.js +3 -3
- package/dist/commands/ui/workspace-actions.js +264 -0
- package/dist/commands/workflow-cli.js +3 -1
- package/dist/core/adapter/adapters/akm-adapter.js +1 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
- package/dist/core/adapter/adapters/okf-adapter.js +3 -16
- package/dist/core/asset/akm-markdown.js +19 -3
- package/dist/core/asset/markdown-link-targets.js +43 -0
- package/dist/core/asset/markdown.js +14 -6
- package/dist/core/config/config.js +6 -4
- package/dist/core/non-task-input.js +67 -3
- package/dist/core/write-source.js +2 -2
- package/dist/indexer/index-writer-lock.js +5 -1
- package/dist/indexer/passes/memory-inference.js +28 -6
- package/dist/indexer/passes/metadata.js +17 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/search/db-search.js +16 -7
- package/dist/indexer/search/search-hit-enrichers.js +3 -3
- package/dist/llm/memory-infer.js +23 -14
- package/dist/output/shapes/curate.js +3 -1
- package/dist/output/shapes/helpers.js +6 -0
- package/dist/output/text/show-format.js +7 -0
- package/dist/scripts/akm-migrate-node.js +1820 -329
- package/dist/scripts/akm-migrate.js +1820 -329
- package/dist/storage/repositories/index-entries-repository.js +16 -0
- package/dist/storage/repositories/index-fts-repository.js +28 -6
- package/dist/storage/repositories/index-vec-repository.js +19 -3
- package/dist/workflows/exec/environment.js +4 -8
- package/dist/workflows/parser.js +13 -4
- package/docs/architecture/0.10-process-rules.md +9 -7
- package/docs/architecture/architecture.md +4 -0
- package/docs/architecture/internals/storage-locations.md +1 -1
- package/docs/architecture/persisted-data-compat.md +1 -1
- package/docs/guides/author-workflows.md +78 -28
- package/docs/guides/bundles.md +16 -2
- package/docs/reference/api.md +19 -1
- package/docs/reference/cli.md +90 -18
- package/docs/reference/env-and-secrets.md +11 -0
- package/docs/reference/memory.md +7 -6
- package/docs/reference/ui-api.md +84 -8
- package/docs/reference/workflow-schema.md +63 -32
- 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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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({
|
|
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
|
|
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 ?? "
|
|
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
|
|
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
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
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: "
|
|
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
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
836
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, plain), force, stashDir, allowDangerousEnvKeys);
|
|
846
837
|
processed.push(updated.item);
|
|
847
838
|
latestIndex = updated.index;
|
|
848
839
|
}
|