akm-cli 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/dist/assets/profiles/default.json +15 -0
  3. package/dist/assets/profiles/graph-refresh.json +13 -0
  4. package/dist/assets/profiles/memory-focus.json +12 -0
  5. package/dist/assets/profiles/quick.json +15 -0
  6. package/dist/assets/profiles/thorough.json +15 -0
  7. package/dist/assets/stash-skeleton/README.md +76 -0
  8. package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
  9. package/dist/cli.js +8 -3
  10. package/dist/commands/consolidate.js +36 -15
  11. package/dist/commands/extract-prompt.js +14 -1
  12. package/dist/commands/health.js +89 -8
  13. package/dist/commands/improve-cli.js +2 -2
  14. package/dist/commands/improve-profiles.js +13 -59
  15. package/dist/commands/improve-result-file.js +9 -4
  16. package/dist/commands/improve.js +86 -65
  17. package/dist/commands/info.js +23 -28
  18. package/dist/commands/init.js +6 -1
  19. package/dist/commands/{proposal-drain-policies.js → proposal/drain-policies.js} +2 -2
  20. package/dist/commands/{proposal-drain.js → proposal/drain.js} +10 -10
  21. package/dist/commands/show.js +47 -0
  22. package/dist/commands/stash-skeleton.js +78 -0
  23. package/dist/{setup/ripgrep-install.js → core/ripgrep/install.js} +2 -2
  24. package/dist/{setup/ripgrep-resolve.js → core/ripgrep/resolve.js} +2 -2
  25. package/dist/core/stash-meta.js +110 -0
  26. package/dist/indexer/indexer.js +2 -2
  27. package/dist/llm/graph-extract.js +1 -1
  28. package/dist/output/cli-hints.js +2 -2
  29. package/dist/setup/detect.js +27 -0
  30. package/dist/setup/harness-config-import.js +170 -0
  31. package/dist/setup/registry-stash-loader.js +99 -0
  32. package/dist/setup/setup.js +229 -72
  33. package/dist/tasks/backends/launchd.js +1 -1
  34. package/dist/tasks/backends/schtasks.js +1 -1
  35. package/dist/wiki/wiki-templates.js +3 -3
  36. package/dist/wiki/wiki.js +1 -1
  37. package/dist/workflows/authoring.js +1 -1
  38. package/package.json +1 -1
  39. /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
  40. /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
  41. /package/dist/{commands → assets}/help/help-accept.md +0 -0
  42. /package/dist/{commands → assets}/help/help-improve.md +0 -0
  43. /package/dist/{commands → assets}/help/help-proposals.md +0 -0
  44. /package/dist/{commands → assets}/help/help-propose.md +0 -0
  45. /package/dist/{commands → assets}/help/help-reject.md +0 -0
  46. /package/dist/{output → assets/hints}/cli-hints-full.md +0 -0
  47. /package/dist/{output → assets/hints}/cli-hints-short.md +0 -0
  48. /package/dist/{llm → assets}/prompts/extract-session.md +0 -0
  49. /package/dist/{llm → assets}/prompts/graph-extract-user-prompt.md +0 -0
  50. /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
  51. /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
  52. /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
  53. /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
  54. /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
@@ -1,6 +1,11 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import profileDefault from "../assets/profiles/default.json" with { type: "json" };
5
+ import profileGraphRefresh from "../assets/profiles/graph-refresh.json" with { type: "json" };
6
+ import profileMemoryFocus from "../assets/profiles/memory-focus.json" with { type: "json" };
7
+ import profileQuick from "../assets/profiles/quick.json" with { type: "json" };
8
+ import profileThorough from "../assets/profiles/thorough.json" with { type: "json" };
4
9
  import { parseAssetRef } from "../core/asset-ref";
5
10
  import { warn } from "../core/warn";
6
11
  /** Profile name used as the final fallback when nothing else resolves. */
@@ -11,66 +16,15 @@ export const DEFAULT_ALLOWED_TYPES = {
11
16
  distill: ["memory"],
12
17
  consolidate: ["memory"],
13
18
  };
19
+ // Built-in profiles are loaded from embedded JSON files in src/assets/profiles/.
20
+ // To add a new profile: create a new .json file there, import it above, and add
21
+ // it to this map. No code change needed beyond those two steps.
14
22
  const BUILTIN_PROFILES = {
15
- default: {
16
- description: "Standard improve pass — all sub-processes, markdown asset types.",
17
- processes: {
18
- reflect: { enabled: true, allowedTypes: DEFAULT_ALLOWED_TYPES.reflect },
19
- distill: { enabled: true, allowedTypes: DEFAULT_ALLOWED_TYPES.distill },
20
- consolidate: { enabled: true, allowedTypes: DEFAULT_ALLOWED_TYPES.consolidate },
21
- memoryInference: { enabled: true },
22
- graphExtraction: { enabled: true },
23
- // validation: deliberately undefined — third-tier classifier is opt-in.
24
- triage: { enabled: false, applyMode: "queue", policy: "personal-stash" },
25
- },
26
- sync: { enabled: true, push: true },
27
- },
28
- quick: {
29
- description: "Reflect-only pass — no distill, consolidate, memoryInference, or graphExtraction.",
30
- processes: {
31
- reflect: { enabled: true, allowedTypes: DEFAULT_ALLOWED_TYPES.reflect },
32
- distill: { enabled: false },
33
- consolidate: { enabled: false },
34
- memoryInference: { enabled: false },
35
- graphExtraction: { enabled: false },
36
- triage: { enabled: false },
37
- },
38
- // Lightweight passes opt out of end-of-run sync: a reflect-only `quick`
39
- // run should not auto-commit/push the git-backed stash to its remote.
40
- // (The auto-sync gate in improve.ts treats an absent sync block as
41
- // ENABLED + push, so we set this explicitly to avoid a surprise push.)
42
- sync: { enabled: false },
43
- },
44
- thorough: {
45
- // Reserved for future divergence; for now behaviorally identical to
46
- // `default`. Documented here so callers picking `--profile thorough` do
47
- // not expect a different code path until we wire stricter limits in.
48
- description: "All sub-processes enabled (currently identical to default; reserved for future divergence).",
49
- processes: {
50
- reflect: { enabled: true, allowedTypes: DEFAULT_ALLOWED_TYPES.reflect },
51
- distill: { enabled: true, allowedTypes: DEFAULT_ALLOWED_TYPES.distill },
52
- consolidate: { enabled: true, allowedTypes: DEFAULT_ALLOWED_TYPES.consolidate },
53
- memoryInference: { enabled: true },
54
- graphExtraction: { enabled: true },
55
- triage: { enabled: true, applyMode: "queue" },
56
- },
57
- sync: { enabled: true, push: true },
58
- },
59
- "memory-focus": {
60
- description: "Memory and lesson improvement only — no distill or consolidate.",
61
- processes: {
62
- reflect: { enabled: true, allowedTypes: ["memory", "lesson"] },
63
- distill: { enabled: false },
64
- consolidate: { enabled: false },
65
- memoryInference: { enabled: true },
66
- graphExtraction: { enabled: false },
67
- triage: { enabled: false },
68
- },
69
- // Limited pass opts out of end-of-run sync for the same reason as `quick`:
70
- // a memory/lesson-only run should not auto-commit/push the stash. Explicit
71
- // here because improve.ts treats an absent sync block as ENABLED + push.
72
- sync: { enabled: false },
73
- },
23
+ default: profileDefault,
24
+ quick: profileQuick,
25
+ thorough: profileThorough,
26
+ "memory-focus": profileMemoryFocus,
27
+ "graph-refresh": profileGraphRefresh,
74
28
  };
75
29
  /**
76
30
  * Default enabled-state for known improve processes when neither the user
@@ -73,14 +73,19 @@ export function relativeImproveResultPath(runId) {
73
73
  * (closes the dry-run/real-run artifact-trap recorded in MEMORY.md
74
74
  * `feedback_akm_dryrun_artifact_trap`).
75
75
  */
76
- export function writeImproveResultFile(stashDir, runId, result) {
76
+ export function writeImproveResultFile(stashDir, runId, result, startedAt) {
77
77
  const db = openStateDatabase();
78
78
  try {
79
- const startedAt = new Date().toISOString();
79
+ const completedAt = new Date().toISOString();
80
+ // startedAt is the ISO timestamp captured at process launch (passed from the
81
+ // CLI entry point). If omitted, fall back to the run-id's embedded timestamp
82
+ // so started_at != completed_at even on older call sites.
83
+ const resolvedStartedAt = startedAt ??
84
+ runId.slice(0, 24).replace(/^(\d{4}-\d{2}-\d{2}T)(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "$1$2:$3:$4.$5Z");
80
85
  recordImproveRun(db, {
81
86
  id: runId,
82
- startedAt,
83
- completedAt: startedAt,
87
+ startedAt: resolvedStartedAt,
88
+ completedAt,
84
89
  stashDir,
85
90
  dryRun: Boolean(result.dryRun),
86
91
  profile: null,
@@ -25,7 +25,7 @@ import { resolveAssetPath } from "../indexer/path-resolver";
25
25
  import { getWritableStashDirs, resolveSourceEntries } from "../indexer/search-source";
26
26
  import { runStalenessDetectionPass } from "../indexer/staleness-detect";
27
27
  import { resolveImproveProcessRunnerFromProfile, resolveTriageJudgmentRunner } from "../integrations/agent/runner";
28
- import { getAvailableHarnesses, getExecutionLogCandidates } from "../integrations/session-logs";
28
+ import { getAvailableHarnesses } from "../integrations/session-logs";
29
29
  import { isLlmFeatureEnabled, isProcessEnabled } from "../llm/feature-gate";
30
30
  import { isGitBackedStash, resolveWritableOverride, saveGitStash } from "../sources/providers/git";
31
31
  import { akmConsolidate } from "./consolidate";
@@ -36,8 +36,8 @@ import { akmExtract } from "./extract";
36
36
  import { makeGateConfig, resolveExtractConfidence, runAutoAcceptGate } from "./improve-auto-accept";
37
37
  import { isProfileFilteredForAllPasses, resolveImproveProfile, resolveProcessEnabled, shouldSkipRef, } from "./improve-profiles";
38
38
  import { akmLint } from "./lint/index";
39
- import { drainProposals } from "./proposal-drain";
40
- import { resolveDrainPolicy } from "./proposal-drain-policies";
39
+ import { drainProposals } from "./proposal/drain";
40
+ import { resolveDrainPolicy } from "./proposal/drain-policies";
41
41
  import { akmReflect } from "./reflect";
42
42
  import { runSchemaRepairPass } from "./schema-repair";
43
43
  import { checkDeadUrls } from "./url-checker";
@@ -674,7 +674,13 @@ export async function akmImprove(options = {}) {
674
674
  // lives in the outer scope. It is always assigned at the top of the try.
675
675
  let eventsCtx = {};
676
676
  try {
677
- const budgetTimer = setTimeout(() => budgetAbortController.abort("improve budget exhausted"), budgetMs);
677
+ const budgetTimer = setTimeout(() => {
678
+ budgetAbortController.abort("improve budget exhausted");
679
+ // Grace period: let finally run to release improve.lock, then hard-exit
680
+ // to prevent the process outliving the task timeout window (lock-cascade fix).
681
+ // Exit 0: budget exhaustion is a normal scheduled-task condition, not an error.
682
+ setTimeout(() => process.exit(0), 5_000);
683
+ }, budgetMs);
678
684
  // Clear the timer when the run ends to avoid keeping the event loop alive.
679
685
  clearBudgetTimer = () => clearTimeout(budgetTimer);
680
686
  try {
@@ -724,7 +730,7 @@ export async function akmImprove(options = {}) {
724
730
  rejectedProposalsByRef.set(e.ref, e);
725
731
  }
726
732
  }
727
- const { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount: loopGateCount, } = await runImproveLoopStage({
733
+ const { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount: loopGateCount, gateAutoAcceptFailedCount: loopGateFailedCount, } = await runImproveLoopStage({
728
734
  scope,
729
735
  options,
730
736
  primaryStashDir,
@@ -743,7 +749,7 @@ export async function akmImprove(options = {}) {
743
749
  eventsCtx,
744
750
  improveProfile,
745
751
  });
746
- const { allWarnings, consolidation, deadUrls, memoryInference, graphExtraction, stalenessDetection, maintenanceActions, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, gateAutoAcceptedCount: postLoopGateCount, } = await runImprovePostLoopStage({
752
+ const { allWarnings, consolidation, deadUrls, memoryInference, graphExtraction, stalenessDetection, maintenanceActions, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, gateAutoAcceptedCount: postLoopGateCount, gateAutoAcceptFailedCount: postLoopGateFailedCount, } = await runImprovePostLoopStage({
747
753
  scope,
748
754
  options,
749
755
  primaryStashDir,
@@ -798,9 +804,6 @@ export async function akmImprove(options = {}) {
798
804
  ...(preparation.lintSummary !== undefined ? { lintSummary: preparation.lintSummary } : {}),
799
805
  ...(preparation.memoryIndexHealth !== undefined ? { memoryIndexHealth: preparation.memoryIndexHealth } : {}),
800
806
  ...(preparation.coverageGaps.length > 0 ? { coverageGaps: preparation.coverageGaps } : {}),
801
- ...(preparation.executionLogCandidates.length > 0
802
- ? { executionLogCandidates: preparation.executionLogCandidates }
803
- : {}),
804
807
  ...(preparation.extract && preparation.extract.length > 0 ? { extract: preparation.extract } : {}),
805
808
  ...(primaryStashDir !== undefined ? { evalCasesWritten: countEvalCases(primaryStashDir) } : {}),
806
809
  ...(deadUrls !== undefined && deadUrls.length > 0 ? { deadUrls } : {}),
@@ -831,6 +834,10 @@ export async function akmImprove(options = {}) {
831
834
  const t = preparation.gateAutoAcceptedCount + loopGateCount + postLoopGateCount;
832
835
  return t > 0 ? { gateAutoAcceptedCount: t } : {};
833
836
  })(),
837
+ ...(() => {
838
+ const f = preparation.gateAutoAcceptFailedCount + loopGateFailedCount + postLoopGateFailedCount;
839
+ return f > 0 ? { gateAutoAcceptFailedCount: f } : {};
840
+ })(),
834
841
  };
835
842
  if (!result.dryRun)
836
843
  emitImproveCompletedEvent(result, {
@@ -990,7 +997,6 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
990
997
  reflectSkippedActions: actionCounts.reflectSkipped,
991
998
  reflectsWithErrorContext: result.reflectsWithErrorContext ?? 0,
992
999
  coverageGapCount: result.coverageGaps?.length ?? 0,
993
- executionLogCandidateCount: result.executionLogCandidates?.length ?? 0,
994
1000
  evalCasesWritten: result.evalCasesWritten ?? 0,
995
1001
  deadUrlCount: result.deadUrls?.length ?? 0,
996
1002
  memoryEligible: result.memorySummary.eligible,
@@ -1047,15 +1053,6 @@ async function runImprovePreparationStage(args) {
1047
1053
  }
1048
1054
  }
1049
1055
  }
1050
- // Phase 0 — execution log synthesis
1051
- let executionLogCandidates = [];
1052
- try {
1053
- const logEntries = getExecutionLogCandidates(7);
1054
- executionLogCandidates = logEntries.filter((e) => e.isFailurePattern).map((e) => e.topic);
1055
- }
1056
- catch {
1057
- // best-effort
1058
- }
1059
1056
  // Phase 0.4 — session-extract pass.
1060
1057
  //
1061
1058
  // Reads native session files (claude-code JSONL, opencode storage tree)
@@ -1074,6 +1071,7 @@ async function runImprovePreparationStage(args) {
1074
1071
  // The extract envelope's own `warnings` field surfaces what went wrong.
1075
1072
  let extractResults;
1076
1073
  let gateAutoAcceptedCount = 0;
1074
+ let gateAutoAcceptFailedCount = 0;
1077
1075
  const extractConfig = options.config ?? loadConfig();
1078
1076
  const extractGateCfg = makeGateConfig("extract", {
1079
1077
  globalThreshold: options.autoAccept,
@@ -1095,12 +1093,16 @@ async function runImprovePreparationStage(args) {
1095
1093
  dryRun: options.dryRun ?? false,
1096
1094
  });
1097
1095
  extractResults.push(result);
1098
- gateAutoAcceptedCount += (await runAutoAcceptGate(primaryStashDir
1099
- ? result.proposals.map((proposalId) => {
1100
- const proposal = getProposal(primaryStashDir, proposalId);
1101
- return { proposalId, confidence: resolveExtractConfidence(proposal) };
1102
- })
1103
- : [], extractGateCfg)).promoted.length;
1096
+ {
1097
+ const gr = await runAutoAcceptGate(primaryStashDir
1098
+ ? result.proposals.map((proposalId) => {
1099
+ const proposal = getProposal(primaryStashDir, proposalId);
1100
+ return { proposalId, confidence: resolveExtractConfidence(proposal) };
1101
+ })
1102
+ : [], extractGateCfg);
1103
+ gateAutoAcceptedCount += gr.promoted.length;
1104
+ gateAutoAcceptFailedCount += gr.failed.length;
1105
+ }
1104
1106
  }
1105
1107
  catch (err) {
1106
1108
  const msg = err instanceof Error ? err.message : String(err);
@@ -1126,7 +1128,9 @@ async function runImprovePreparationStage(args) {
1126
1128
  proposalId: p.id,
1127
1129
  confidence: resolveExtractConfidence(p),
1128
1130
  }));
1129
- gateAutoAcceptedCount += (await runAutoAcceptGate(backlogCandidates, extractGateCfg)).promoted.length;
1131
+ const backlogGr = await runAutoAcceptGate(backlogCandidates, extractGateCfg);
1132
+ gateAutoAcceptedCount += backlogGr.promoted.length;
1133
+ gateAutoAcceptFailedCount += backlogGr.failed.length;
1130
1134
  }
1131
1135
  }
1132
1136
  // eligibleCount = raw pre-filter count (before cooldown/signal/cleanup filters).
@@ -1539,7 +1543,6 @@ async function runImprovePreparationStage(args) {
1539
1543
  cleanupWarnings,
1540
1544
  appliedCleanup,
1541
1545
  memoryIndexHealth,
1542
- executionLogCandidates,
1543
1546
  extract: extractResults,
1544
1547
  actionableRefs,
1545
1548
  signalBearingSet,
@@ -1553,6 +1556,7 @@ async function runImprovePreparationStage(args) {
1553
1556
  recentErrors,
1554
1557
  utilityMap,
1555
1558
  gateAutoAcceptedCount,
1559
+ gateAutoAcceptFailedCount,
1556
1560
  };
1557
1561
  }
1558
1562
  // TODO(refactor): 13 args including `actions`/`recentErrors` mutation channels. Restructure into immutable plan + mutable context objects — deferred to dedicated refactor with isolated testing.
@@ -1636,6 +1640,7 @@ async function runImproveLoopStage(args) {
1636
1640
  ? listProposals(dedupeStashDirForProposals, { status: "pending" }).map((p) => p.ref)
1637
1641
  : []);
1638
1642
  let gateAutoAcceptedCount = 0;
1643
+ let gateAutoAcceptFailedCount = 0;
1639
1644
  const reflectGateCfg = makeGateConfig("reflect", {
1640
1645
  globalThreshold: options.autoAccept,
1641
1646
  dryRun: options.dryRun ?? false,
@@ -1812,7 +1817,9 @@ async function runImproveLoopStage(args) {
1812
1817
  },
1813
1818
  }, eventsCtx);
1814
1819
  if (reflectResult.ok) {
1815
- gateAutoAcceptedCount += (await runAutoAcceptGate([{ proposalId: reflectResult.proposal.id, confidence: reflectResult.proposal.confidence }], reflectGateCfg)).promoted.length;
1820
+ const reflectGr = await runAutoAcceptGate([{ proposalId: reflectResult.proposal.id, confidence: reflectResult.proposal.confidence }], reflectGateCfg);
1821
+ gateAutoAcceptedCount += reflectGr.promoted.length;
1822
+ gateAutoAcceptFailedCount += reflectGr.failed.length;
1816
1823
  }
1817
1824
  } // end else (reflect type/profile check)
1818
1825
  }
@@ -1923,7 +1930,9 @@ async function runImproveLoopStage(args) {
1923
1930
  });
1924
1931
  actions.push({ ref: planned.ref, mode: "distill", result: distillResult });
1925
1932
  if (distillResult.outcome === "queued" && distillResult.proposal) {
1926
- gateAutoAcceptedCount += (await runAutoAcceptGate([{ proposalId: distillResult.proposal.id, confidence: distillResult.proposal.confidence }], distillGateCfg)).promoted.length;
1933
+ const distillGr = await runAutoAcceptGate([{ proposalId: distillResult.proposal.id, confidence: distillResult.proposal.confidence }], distillGateCfg);
1934
+ gateAutoAcceptedCount += distillGr.promoted.length;
1935
+ gateAutoAcceptFailedCount += distillGr.failed.length;
1927
1936
  }
1928
1937
  if (parsedPlannedRef.type === "memory") {
1929
1938
  const promotedToKnowledge = distillResult.outcome === "queued" && distillResult.proposalKind === "knowledge";
@@ -1996,7 +2005,7 @@ async function runImproveLoopStage(args) {
1996
2005
  completedCount++;
1997
2006
  info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
1998
2007
  }
1999
- return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount };
2008
+ return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount, gateAutoAcceptFailedCount };
2000
2009
  }
2001
2010
  async function runImprovePostLoopStage(args) {
2002
2011
  const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memorySummary, memoryRefsForInference, reindexFn, eventsCtx, budgetSignal, improveProfile, } = args;
@@ -2092,6 +2101,7 @@ async function runImprovePostLoopStage(args) {
2092
2101
  durationMs: 0,
2093
2102
  };
2094
2103
  let gateAutoAcceptedCount = 0;
2104
+ let gateAutoAcceptFailedCount = 0;
2095
2105
  const consolidateGateCfg = makeGateConfig("consolidate", {
2096
2106
  globalThreshold: options.autoAccept,
2097
2107
  dryRun: options.dryRun ?? false,
@@ -2111,13 +2121,16 @@ async function runImprovePostLoopStage(args) {
2111
2121
  // Tie consolidate proposals back to this improve invocation so
2112
2122
  // accept-rate-per-run aggregation works. Mirrors reflect/propose/extract.
2113
2123
  sourceRun: `consolidate-${Date.now()}`,
2114
- // Incremental consolidation: in steady state (not bootstrap, not volume-
2115
- // triggered) pass the last-consolidation timestamp so akmConsolidate skips
2116
- // chunks with no memory changed since then. Converts consolidation cost
2117
- // from O(pool) to O(changed clusters) the fix for the rising p95 tail
2118
- // where full-pool re-judging produced 5–10 min runs that promoted ~0.
2119
- // undefined full pass (bootstrap, or volume-triggered large-pool sweep).
2120
- incrementalSince: volumeTriggered ? undefined : lastConsolidateTs,
2124
+ // Incremental consolidation: pass the last-consolidation timestamp so
2125
+ // akmConsolidate skips chunks with no memory changed since then. Converts
2126
+ // consolidation cost from O(pool) to O(changed clusters) the fix for
2127
+ // the rising p95 tail where full-pool re-judging produced 5–10 min runs
2128
+ // that promoted ~0. undefined full pass on first-ever run (bootstrap).
2129
+ // volumeTriggered correctly forces the run past cooldown but must NOT
2130
+ // override incrementalSince the stash has ~1400 eligible memories so
2131
+ // volumeTriggered=true on every run, permanently forcing full 12-chunk
2132
+ // scans (~264s) instead of the intended 1-2 chunk incremental path (~44s).
2133
+ incrementalSince: lastConsolidateTs,
2121
2134
  maxChunkSize: improveProfile?.processes?.consolidate?.maxChunkSize,
2122
2135
  // Honor profile.autoAccept (already merged into options.autoAccept at the
2123
2136
  // top of akmImprove). The CLI parser always supplies 90 when --auto-accept
@@ -2127,17 +2140,21 @@ async function runImprovePostLoopStage(args) {
2127
2140
  // still wins because the spread above runs first.
2128
2141
  autoAccept: options.consolidateOptions?.autoAccept ?? options.autoAccept,
2129
2142
  });
2130
- gateAutoAcceptedCount += (await runAutoAcceptGate(consolidation.promoted.map((proposalId) => {
2131
- try {
2132
- if (!primaryStashDir)
2143
+ {
2144
+ const consolidateGr = await runAutoAcceptGate(consolidation.promoted.map((proposalId) => {
2145
+ try {
2146
+ if (!primaryStashDir)
2147
+ return { proposalId, confidence: undefined };
2148
+ const proposal = getProposal(primaryStashDir, proposalId);
2149
+ return { proposalId, confidence: proposal.confidence };
2150
+ }
2151
+ catch {
2133
2152
  return { proposalId, confidence: undefined };
2134
- const proposal = getProposal(primaryStashDir, proposalId);
2135
- return { proposalId, confidence: proposal.confidence };
2136
- }
2137
- catch {
2138
- return { proposalId, confidence: undefined };
2139
- }
2140
- }), consolidateGateCfg)).promoted.length;
2153
+ }
2154
+ }), consolidateGateCfg);
2155
+ gateAutoAcceptedCount += consolidateGr.promoted.length;
2156
+ gateAutoAcceptFailedCount += consolidateGr.failed.length;
2157
+ }
2141
2158
  if (consolidation.processed > 0) {
2142
2159
  appendEvent({
2143
2160
  eventType: "consolidate_completed",
@@ -2212,6 +2229,7 @@ async function runImprovePostLoopStage(args) {
2212
2229
  orphansPurged: maintenanceResult.orphansPurged,
2213
2230
  proposalsExpired: maintenanceResult.proposalsExpired,
2214
2231
  gateAutoAcceptedCount,
2232
+ gateAutoAcceptFailedCount,
2215
2233
  };
2216
2234
  }
2217
2235
  // TODO(refactor): mutates the passed-in `allWarnings` array as a hidden side channel. Return warnings in ImproveMaintenanceResult and merge in caller — invasive signature change deferred to next refactor pass.
@@ -2287,25 +2305,25 @@ async function runImproveMaintenancePasses(args) {
2287
2305
  }
2288
2306
  const graphEnabled = isProcessEnabled("index", "graph_extraction", config);
2289
2307
  const graphExtractionDisabledByProfile = improveProfile?.processes?.graphExtraction?.enabled === false;
2308
+ const graphExtractionFullScan = improveProfile?.processes?.graphExtraction?.fullScan === true;
2290
2309
  // Build the set of refs actually touched this run.
2291
2310
  const touchedRefs = new Set();
2292
2311
  for (const r of args.actionableRefs)
2293
2312
  touchedRefs.add(r.ref);
2294
2313
  for (const r of memoryRefsForInference)
2295
2314
  touchedRefs.add(r);
2296
- // INVARIANT: graph extraction must never run on the full corpus from the
2297
- // improve post-loop. Full-corpus scans belong in `akm index`. We enforce
2298
- // this by ALWAYS passing `candidatePaths` (possibly an empty Set) to the
2299
- // extractor never `undefined`. With an empty Set, the extractor's
2300
- // filter (graph-extraction.ts ~L452) rejects every file and returns the
2301
- // empty result without scanning. The pass is still invoked so that the
2302
- // action is recorded, the D9 post-consolidation reindex still fires, and
2303
- // mock injection (graphExtractionFn) used by tests stays exercised.
2315
+ // INVARIANT: graph extraction normally runs only on files touched by
2316
+ // actionable refs (candidatePaths). Full-corpus scans are opt-in via
2317
+ // profile.processes.graphExtraction.fullScan = true (used by the
2318
+ // `graph-refresh` built-in profile and its weekly scheduled task).
2319
+ // The empty-Set fallback is intentional when no refs were touched —
2320
+ // the extractor's filter rejects every file and returns empty, keeping
2321
+ // the pass invoked so the action is recorded and tests stay exercised.
2304
2322
  if (graphExtractionDisabledByProfile) {
2305
2323
  info("[improve] graph extraction skipped (disabled by improve profile)");
2306
2324
  }
2307
2325
  else if (sources.length > 0 && graphEnabled) {
2308
- info("[improve] graph extraction starting");
2326
+ info(`[improve] graph extraction starting${graphExtractionFullScan ? " (full-corpus scan)" : ""}`);
2309
2327
  const extractionStart = Date.now();
2310
2328
  try {
2311
2329
  // D9: if consolidation ran but memory inference did not reindex, force a reindex
@@ -2325,15 +2343,18 @@ async function runImproveMaintenancePasses(args) {
2325
2343
  closeDatabase(db);
2326
2344
  db = openDatabase(getDbPath(), config.embedding?.dimension ? { embeddingDim: config.embedding.dimension } : undefined);
2327
2345
  }
2328
- // Resolve touched refs to absolute file paths. Empty Set is intentional
2329
- // when no refs were touched see INVARIANT above.
2330
- const candidatePaths = new Set();
2331
- if (primaryStashDir && touchedRefs.size > 0) {
2332
- const writableDirSet = new Set(getWritableStashDirs(primaryStashDir).map((d) => path.resolve(d)));
2333
- const resolved = await Promise.all([...touchedRefs].map((ref) => findAssetFilePath(ref, primaryStashDir, writableDirSet).catch(() => null)));
2334
- for (const p of resolved) {
2335
- if (typeof p === "string" && p.length > 0)
2336
- candidatePaths.add(p);
2346
+ // Resolve touched refs to absolute file paths. Skipped for fullScan
2347
+ // (candidatePaths stays undefined extractor processes all files).
2348
+ let candidatePaths;
2349
+ if (!graphExtractionFullScan) {
2350
+ candidatePaths = new Set();
2351
+ if (primaryStashDir && touchedRefs.size > 0) {
2352
+ const writableDirSet = new Set(getWritableStashDirs(primaryStashDir).map((d) => path.resolve(d)));
2353
+ const resolved = await Promise.all([...touchedRefs].map((ref) => findAssetFilePath(ref, primaryStashDir, writableDirSet).catch(() => null)));
2354
+ for (const p of resolved) {
2355
+ if (typeof p === "string" && p.length > 0)
2356
+ candidatePaths.add(p);
2357
+ }
2337
2358
  }
2338
2359
  }
2339
2360
  const progressHandler = (event) => {
@@ -46,8 +46,11 @@ export function assembleInfo(options) {
46
46
  ...(s.url ? { url: s.url } : {}),
47
47
  ...(s.enabled !== undefined ? { enabled: s.enabled } : {}),
48
48
  }));
49
- // Index stats
50
- const indexStats = readIndexStats(options?.dbPath);
49
+ // Index stats — resolve the DB path from config so info reads the same
50
+ // database that health and search use, rather than a bare getDbPath() call
51
+ // that ignores XDG_DATA_HOME or per-config overrides.
52
+ const resolvedDbPath = options?.dbPath ?? getDbPath();
53
+ const indexStats = readIndexStats(resolvedDbPath);
51
54
  return {
52
55
  schemaVersion: 1,
53
56
  version: pkgVersion,
@@ -64,38 +67,30 @@ export function assembleInfo(options) {
64
67
  indexStats,
65
68
  };
66
69
  }
67
- function readIndexStats(dbPath) {
68
- const resolvedPath = dbPath ?? getDbPath();
69
- // If no index file exists, return zeros
70
- if (!fs.existsSync(resolvedPath)) {
71
- return {
72
- entryCount: 0,
73
- lastBuiltAt: null,
74
- hasEmbeddings: false,
75
- vecAvailable: false,
76
- };
77
- }
70
+ function readIndexStats(resolvedPath) {
71
+ const EMPTY = {
72
+ entryCount: 0,
73
+ lastBuiltAt: null,
74
+ hasEmbeddings: false,
75
+ vecAvailable: false,
76
+ };
77
+ if (!fs.existsSync(resolvedPath))
78
+ return EMPTY;
78
79
  let db;
79
80
  try {
80
81
  db = openExistingDatabase(resolvedPath);
81
- const entryCount = getEntryCount(db);
82
- const lastBuiltAt = getMeta(db, "builtAt") ?? null;
83
- const vecAvailable = isVecAvailable(db);
84
- const hasEmbeddings = getMeta(db, "hasEmbeddings") === "1";
85
82
  return {
86
- entryCount,
87
- lastBuiltAt,
88
- hasEmbeddings,
89
- vecAvailable,
83
+ entryCount: getEntryCount(db),
84
+ lastBuiltAt: getMeta(db, "builtAt") ?? null,
85
+ hasEmbeddings: getMeta(db, "hasEmbeddings") === "1",
86
+ vecAvailable: isVecAvailable(db),
90
87
  };
91
88
  }
92
- catch {
93
- return {
94
- entryCount: 0,
95
- lastBuiltAt: null,
96
- hasEmbeddings: false,
97
- vecAvailable: false,
98
- };
89
+ catch (err) {
90
+ // Surface the error so operators can diagnose mismatches between
91
+ // `akm info` and `akm health` rather than silently returning zeros.
92
+ process.stderr.write(`[akm info] failed to read index stats from ${resolvedPath}: ${String(err)}\n`);
93
+ return EMPTY;
99
94
  }
100
95
  finally {
101
96
  if (db) {
@@ -14,7 +14,8 @@ import { TYPE_DIRS } from "../core/asset-spec";
14
14
  import { loadUserConfig, saveConfig } from "../core/config";
15
15
  import { ConfigError } from "../core/errors";
16
16
  import { assertSafeStashDir, getBinDir, getConfigPath, getDefaultStashDir } from "../core/paths";
17
- import { ensureRg } from "../setup/ripgrep-install";
17
+ import { ensureRg } from "../core/ripgrep/install";
18
+ import { copyStashSkeleton, scaffoldStashMeta } from "./stash-skeleton";
18
19
  /**
19
20
  * Refuse to persist a temporary-directory stashDir to the user's config when
20
21
  * running under a test runner AND `--dir <tempdir>` was passed explicitly.
@@ -74,6 +75,10 @@ export async function akmInit(options) {
74
75
  }
75
76
  // Ensure the default stash is a local git repo (no remote required)
76
77
  ensureGitRepo(stashDir);
78
+ if (created) {
79
+ copyStashSkeleton(stashDir);
80
+ scaffoldStashMeta(stashDir);
81
+ }
77
82
  // Persist stashDir in config.json
78
83
  const configPath = getConfigPath();
79
84
  const existing = loadUserConfig();
@@ -16,8 +16,8 @@
16
16
  */
17
17
  import fs from "node:fs";
18
18
  import { z } from "zod";
19
- import { UsageError } from "../core/errors";
20
- import { PROPOSAL_SOURCES } from "../core/proposals";
19
+ import { UsageError } from "../../core/errors";
20
+ import { PROPOSAL_SOURCES } from "../../core/proposals";
21
21
  // Valid `generator` values for a drain rule are exactly the canonical proposal
22
22
  // `source` values (see {@link PROPOSAL_SOURCES} in src/core/proposals.ts). The
23
23
  // engine matches rules via `policy.accept.find(r => r.generator === proposal.source)`,
@@ -36,16 +36,16 @@
36
36
  */
37
37
  import fs from "node:fs";
38
38
  import path from "node:path";
39
- import { parseAssetRef } from "../core/asset-ref";
40
- import { resolveAssetPathFromName, TYPE_DIRS } from "../core/asset-spec";
41
- import { appendEvent } from "../core/events";
42
- import { parseFrontmatter } from "../core/frontmatter";
43
- import { listProposals } from "../core/proposals";
44
- import { info, warn } from "../core/warn";
45
- import { runAgent } from "../integrations/agent";
46
- import { runOpencodeSdk } from "../integrations/agent/sdk-runner";
47
- import { chatCompletion, stripJsonFences } from "../llm/client";
48
- import { akmProposalAccept, akmProposalReject } from "./proposal";
39
+ import { parseAssetRef } from "../../core/asset-ref";
40
+ import { resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset-spec";
41
+ import { appendEvent } from "../../core/events";
42
+ import { parseFrontmatter } from "../../core/frontmatter";
43
+ import { listProposals } from "../../core/proposals";
44
+ import { info, warn } from "../../core/warn";
45
+ import { runAgent } from "../../integrations/agent";
46
+ import { runOpencodeSdk } from "../../integrations/agent/sdk-runner";
47
+ import { chatCompletion, stripJsonFences } from "../../llm/client";
48
+ import { akmProposalAccept, akmProposalReject } from "../proposal";
49
49
  // ---------------------------------------------------------------------------
50
50
  // Content helpers
51
51
  // ---------------------------------------------------------------------------
@@ -24,6 +24,7 @@ import { loadConfig } from "../core/config";
24
24
  import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../core/errors";
25
25
  import { appendEvent, readEvents } from "../core/events";
26
26
  import { parseFrontmatter } from "../core/frontmatter";
27
+ import { META_DIR, parseMetaRef, resolveMetaFilePath } from "../core/stash-meta";
27
28
  import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "../indexer/db";
28
29
  import { ensureIndex } from "../indexer/ensure-index";
29
30
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
@@ -105,6 +106,16 @@ function resolveRegisteredWikiAssetPath(wikiRoot, wikiName, assetName) {
105
106
  */
106
107
  export async function akmShowUnified(input) {
107
108
  const ref = input.ref.trim();
109
+ // 0a. Stash `.meta/` convention: `[origin//]meta[:name]` direct-reads a
110
+ // human-authored orientation doc from the stash's `.meta/` directory.
111
+ // These files are not indexed (the walker skips dot-dirs), so they are
112
+ // resolved here before the index lookup and the `type:name` parser,
113
+ // which would otherwise reject the non-asset-type `meta`.
114
+ {
115
+ const metaRef = parseMetaRef(ref);
116
+ if (metaRef)
117
+ return showStashMeta(metaRef);
118
+ }
108
119
  // 0. Wiki-root shortcut: `wiki:<name>` with no page path routes to the
109
120
  // wiki summary (same payload as `akm wiki show <name>`). Honour
110
121
  // `parsed.origin` by resolving against the matching stash source(s),
@@ -152,6 +163,42 @@ export async function akmShowUnified(input) {
152
163
  }
153
164
  return result;
154
165
  }
166
+ /**
167
+ * Resolve a stash `.meta/` doc and return it as a lightweight ShowResponse.
168
+ *
169
+ * With no origin the working stash (and other configured sources, in order)
170
+ * is searched and the first hit wins. With an origin the lookup is narrowed
171
+ * to that stash; an uninstalled origin yields an actionable "not installed"
172
+ * error. The file is read directly from disk — `.meta/` is never indexed.
173
+ */
174
+ async function showStashMeta(metaRef) {
175
+ const allSources = resolveSourceEntries();
176
+ const sources = resolveSourcesForOrigin(metaRef.origin, allSources);
177
+ if (metaRef.origin && sources.length === 0) {
178
+ throw new NotFoundError(`Stash "${metaRef.origin}" is not installed, so its ${META_DIR}/ docs are unavailable. ` +
179
+ `Run: akm add ${metaRef.origin}`);
180
+ }
181
+ const config = loadConfig();
182
+ for (const source of sources) {
183
+ const filePath = resolveMetaFilePath(source.path, metaRef.name);
184
+ if (!filePath)
185
+ continue;
186
+ const content = fs.readFileSync(filePath, "utf8");
187
+ const editable = isEditable(filePath, config);
188
+ appendEvent({ eventType: "show", ref: `meta:${metaRef.name}`, metadata: { type: "meta", name: metaRef.name } });
189
+ return {
190
+ type: "meta",
191
+ name: metaRef.name,
192
+ path: filePath,
193
+ content,
194
+ origin: source.registryId ?? null,
195
+ editable,
196
+ };
197
+ }
198
+ throw new NotFoundError(`No ${META_DIR}/${metaRef.name} doc found${metaRef.origin ? ` in "${metaRef.origin}"` : ""}. ` +
199
+ `Stash maintainers can create ${META_DIR}/${metaRef.name}.md to describe this stash ` +
200
+ `(purpose, key assets, conventions, maintainer).`);
201
+ }
155
202
  function hasAnyScopeKey(scope) {
156
203
  return Boolean(scope.user || scope.agent || scope.run || scope.channel);
157
204
  }