akm-cli 0.8.1 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,77 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.8.2] - 2026-06-05
8
+
9
+ ### Added
10
+
11
+ - **LM Studio auto-detection in setup wizard** — `akm setup` now probes
12
+ `localhost:1234/v1/models` at startup and, when the server is running, pre-fills
13
+ the LLM backend with the active model list, mirroring the existing Ollama detection
14
+ flow (#522).
15
+ - **Agent harness config import** — `akm setup` detects installed AI coding harnesses
16
+ (currently Claude Code and OpenCode) and pre-populates LLM provider, model, and
17
+ base-URL fields from the harness configuration. The importer registry
18
+ (`HARNESS_CONFIG_IMPORTERS`) makes adding future harnesses a single append (#523).
19
+ API key *values* are never read or stored — only the environment variable name is
20
+ imported.
21
+ - **Registry-driven stash selection** — the "Add Sources" step now fetches available
22
+ stashes from the official AKM registry at startup. `DEFAULT_SELECTED_STASH_IDS`
23
+ in `src/setup/registry-stash-loader.ts` is the single edit point for changing
24
+ which stashes are pre-checked. Falls back to a hardcoded list on network error (#520).
25
+ - **`improve.autoAccept.{promoted,validationFailed}` health metrics** — auto-accepted
26
+ proposals that pass the confidence threshold but fail validation (truncated
27
+ description, invalid frontmatter) are now counted as `gateAutoAcceptFailedCount`
28
+ in the improve result envelope and surfaced as `improve.autoAccept.validationFailed`
29
+ in `akm health` reports.
30
+ - **`auto-accept-validation` health advisory** — heuristic advisory that warns when
31
+ `validationFailed > 0` so malformed proposals are visible before they pile up in
32
+ the queue.
33
+
34
+ ### Fixed
35
+
36
+ - **`akm-improve` tasks recorded as failed on budget exhaustion** — the budget
37
+ exhaustion timer called `process.exit(1)`, causing every budget-limited run to be
38
+ recorded as a task failure. Changed to `process.exit(0)`; budget exhaustion is a
39
+ normal exit condition.
40
+ - **`improve_runs.started_at` always equal to `completed_at`** — `writeImproveResultFile`
41
+ was called at end-of-run, so `new Date()` captured the completion time and both
42
+ columns held the same value (649/661 real runs affected, regressed ~May 26).
43
+ `started_at` now uses the timestamp captured at process launch, passed in from the
44
+ CLI entry point. A regex-based fallback decodes the timestamp embedded in the run ID
45
+ for any call site that does not supply an explicit value (#524).
46
+ - **`akm-health-report` task fails on transient DNS errors** — the Discord webhook
47
+ script caught `HTTPError` but not the parent `URLError`, so DNS blips caused the
48
+ task runner to record the health report as failed. `URLError` is now caught and
49
+ logged as a warning with a clean exit.
50
+
51
+ ### Added
52
+
53
+ - **Stash `.meta/` convention** — a stash may carry an optional, human-authored
54
+ `.meta/` directory at its root for orientation: purpose, key assets, conventions,
55
+ and maintainer info. Surface it on demand with `akm show meta` (the working
56
+ stash's `.meta/index.md`), `akm show meta:<name>` (e.g. `.meta/about.md`), or
57
+ scope it to a specific stash with `akm show <origin>//meta[:<name>]`. Because
58
+ `.meta/` is a dot-directory, the indexer already skips it, so these docs never
59
+ pollute search results — they are direct-read on demand. Owners extend the
60
+ convention by dropping new files (`.meta/about.md`, `.meta/conventions.md`,
61
+ `.meta/license`) with no code changes. `akm init` scaffolds a `.meta/index.md`
62
+ template into newly created stashes.
63
+ - **Default stash skeleton** — `akm init` (and `akm setup`) now copies
64
+ `src/assets/stash-skeleton/` into every newly created stash. Currently ships
65
+ a `README.md` covering what the stash contains and how agents use `akm` to
66
+ access assets. Existing files are never overwritten. Add files to
67
+ `src/assets/stash-skeleton/` to extend what ships with a fresh install.
68
+
69
+ ### Improved
70
+
71
+ - **Setup wizard pre-populates from existing config** — on re-run, `akm setup`
72
+ initialises every prompt default from the current saved configuration so users
73
+ only need to change what has actually changed (#519).
74
+ - **Config backup before every setup write** — `backupExistingConfig()` is now called
75
+ before each `saveConfig` in the setup wizard, ensuring the previous config is always
76
+ recoverable if a wizard run is interrupted (#521).
77
+
7
78
  ## [0.8.1] - 2026-06-05
8
79
 
9
80
  ### Added
@@ -0,0 +1,76 @@
1
+ # AKM Stash
2
+
3
+ This is an **AKM stash** — a structured knowledge repository that stores reusable
4
+ assets for you and your AI agents. AKM (Agent Knowledge Management) indexes, ranks,
5
+ and surfaces these assets at the right moment during coding sessions, improving
6
+ consistency and reducing repeated context-setting.
7
+
8
+ ## What this stash contains
9
+
10
+ | Directory | Asset type | Purpose |
11
+ |-----------|-----------|---------|
12
+ | `skills/` | Skills | Step-by-step instructions agents follow for specific tasks |
13
+ | `knowledge/` | Knowledge | Reference documents, guides, architecture notes |
14
+ | `memories/` | Memories | Persistent facts and preferences learned over time |
15
+ | `commands/` | Commands | Parameterised prompt templates for common workflows |
16
+ | `agents/` | Agents | Agent definitions with system prompts and tool policies |
17
+ | `workflows/` | Workflows | Multi-step orchestration sequences |
18
+ | `tasks/` | Tasks | Scheduled or on-demand automation tasks |
19
+ | `lessons/` | Lessons | Durable lessons extracted from past sessions |
20
+
21
+ Add your own assets to any of these directories. AKM will index them automatically
22
+ on the next `akm index` run (or when the background improve pipeline picks them up).
23
+
24
+ ## For agents: how to access this stash
25
+
26
+ All assets in this stash are searchable via the `akm` CLI. Use these commands to
27
+ find and read assets during a session:
28
+
29
+ ```sh
30
+ # Find assets relevant to your current task (recommended first step)
31
+ akm curate "<task description including project name>"
32
+
33
+ # Full-text + semantic search
34
+ akm search "<query>"
35
+ akm search "<query>" --type skill
36
+ akm search "<query>" --type knowledge
37
+
38
+ # Show a specific asset by ref
39
+ akm show skill:<name>
40
+ akm show knowledge:<name>
41
+ akm show memory:<name>
42
+ akm show command:<name>
43
+
44
+ # List available assets by type
45
+ akm list --type skill
46
+ akm list --type knowledge
47
+ ```
48
+
49
+ ### Recording feedback and new knowledge
50
+
51
+ ```sh
52
+ # Mark an asset as helpful (improves future rankings)
53
+ akm feedback <ref> --positive
54
+
55
+ # Capture a durable lesson or memory from the current session
56
+ akm remember "<fact or lesson>"
57
+ ```
58
+
59
+ ### Improving and maintaining the stash
60
+
61
+ ```sh
62
+ # Run the self-improvement pipeline (extract, reflect, consolidate)
63
+ akm improve
64
+
65
+ # Check stash health and pipeline metrics
66
+ akm health
67
+
68
+ # Review pending improvement proposals
69
+ akm proposal list
70
+ akm proposal show <id>
71
+ akm proposal accept <id>
72
+ ```
73
+
74
+ ---
75
+
76
+ *Created by `akm init`. See `akm --help` for full command reference.*
package/dist/cli.js CHANGED
@@ -92,8 +92,8 @@ function resolveEventSource() {
92
92
  }
93
93
  import { resolveImproveProfile } from "./commands/improve-profiles";
94
94
  import { akmProposalAccept, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalRevert, akmProposalShow, } from "./commands/proposal";
95
- import { drainProposals } from "./commands/proposal-drain";
96
- import { resolveDrainPolicy } from "./commands/proposal-drain-policies";
95
+ import { drainProposals } from "./commands/proposal/drain";
96
+ import { resolveDrainPolicy } from "./commands/proposal/drain-policies";
97
97
  import { akmPropose } from "./commands/propose";
98
98
  import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
99
99
  import { checkForUpdate, performUpgrade } from "./commands/self-update";
@@ -107,6 +107,7 @@ import { DEFAULT_CONFIG, loadConfig, loadUserConfig, resolveConfiguredSources, s
107
107
  import { ConfigError, NotFoundError, UsageError } from "./core/errors";
108
108
  import { appendEvent } from "./core/events";
109
109
  import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "./core/paths";
110
+ import { parseMetaRef } from "./core/stash-meta";
110
111
  import { plainize } from "./core/tty";
111
112
  import { clearLogFile, info, isQuiet, isVerbose, setLogFile, setQuiet, setVerbose, warn } from "./core/warn";
112
113
  import { closeDatabase, openExistingDatabase } from "./indexer/db";
@@ -872,7 +873,11 @@ const showCommand = defineCommand({
872
873
  output("proposal-show", result);
873
874
  return;
874
875
  }
875
- parseAssetRef(args.ref);
876
+ // `[origin//]meta[:name]` targets the stash `.meta/` convention, which is
877
+ // not a typed asset ref — skip ref validation and let akmShowUnified
878
+ // direct-read it. (`parseAssetRef` would reject the non-type `meta`.)
879
+ if (!parseMetaRef(args.ref))
880
+ parseAssetRef(args.ref);
876
881
  // The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
877
882
  // is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
878
883
  // by `normalizeShowArgv` before citty parses argv. We read those values
@@ -170,7 +170,7 @@ export function isHotCapturedMemory(filePath) {
170
170
  return false;
171
171
  }
172
172
  }
173
- export function consolidateGuardStatus(filePath) {
173
+ function consolidateGuardStatus(filePath) {
174
174
  if (!fs.existsSync(filePath))
175
175
  return "missing";
176
176
  let content;
@@ -395,7 +395,7 @@ export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks,
395
395
  * trimmed). Empty set on any read/parse error — fail-safe to "annotate
396
396
  * nothing" so the LLM still proposes, just slightly more wastefully.
397
397
  */
398
- export function loadPendingConsolidateProposalHashes(stashDir) {
398
+ function loadPendingConsolidateProposalHashes(stashDir) {
399
399
  const hashes = new Set();
400
400
  try {
401
401
  const pending = listProposals(stashDir, { status: "pending" }).filter((p) => p.source === "consolidate");
@@ -1924,7 +1924,7 @@ export function normalizeUpdatedField(fm) {
1924
1924
  * Two slugs that normalise to the same string are considered the same asset
1925
1925
  * for dedup purposes even if they don't share an exact ref.
1926
1926
  */
1927
- export function normalizeSlugForDedup(ref) {
1927
+ function normalizeSlugForDedup(ref) {
1928
1928
  const slug = ref.replace(/^[^:]+:/, "");
1929
1929
  const monthRe = /(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i;
1930
1930
  const tokens = slug
@@ -1962,7 +1962,7 @@ export function normalizeSlugForDedup(ref) {
1962
1962
  * improve invocation — a different concern from the cross-run content-hash
1963
1963
  * dedup, and cheap (no embeddings, no DB query).
1964
1964
  */
1965
- export async function checkPreEmitDedup(opts) {
1965
+ async function checkPreEmitDedup(opts) {
1966
1966
  const normCandidate = normalizeSlugForDedup(opts.candidateRef);
1967
1967
  // Pending consolidate proposals (slug match) — within the same improve run.
1968
1968
  const pendingConsolidate = listProposals(opts.stashDir, { status: "pending" }).filter((p) => p.source === "consolidate");
@@ -71,6 +71,7 @@ function createUnknownImproveMetrics() {
71
71
  graphExtraction: 0,
72
72
  error: 0,
73
73
  },
74
+ autoAccept: { promoted: 0, validationFailed: 0 },
74
75
  reflectsWithErrorContext: 0,
75
76
  coverageGapCount: 0,
76
77
  evalCasesWritten: 0,
@@ -293,6 +294,8 @@ function projectRunMetrics(result) {
293
294
  }
294
295
  }
295
296
  }
297
+ metrics.autoAccept.promoted += toFiniteNumber(result.gateAutoAcceptedCount);
298
+ metrics.autoAccept.validationFailed += toFiniteNumber(result.gateAutoAcceptFailedCount);
296
299
  metrics.reflectsWithErrorContext += toFiniteNumber(result.reflectsWithErrorContext);
297
300
  if (Array.isArray(result.coverageGaps))
298
301
  metrics.coverageGapCount += result.coverageGaps.length;
@@ -481,6 +484,8 @@ function mergeImproveMetrics(dst, src) {
481
484
  dst.actions.memoryInference += src.actions.memoryInference;
482
485
  dst.actions.graphExtraction += src.actions.graphExtraction;
483
486
  dst.actions.error += src.actions.error;
487
+ dst.autoAccept.promoted += src.autoAccept.promoted;
488
+ dst.autoAccept.validationFailed += src.autoAccept.validationFailed;
484
489
  dst.reflectsWithErrorContext += src.reflectsWithErrorContext;
485
490
  dst.coverageGapCount += src.coverageGapCount;
486
491
  dst.evalCasesWritten += src.evalCasesWritten;
@@ -923,6 +928,8 @@ const INTERESTING_DELTA_PATHS = [
923
928
  "improve.graphExtraction.failures",
924
929
  "improve.sessionExtraction.sessionsScanned",
925
930
  "improve.sessionExtraction.proposalsCreated",
931
+ "improve.autoAccept.promoted",
932
+ "improve.autoAccept.validationFailed",
926
933
  "improve.wallTime.medianMs",
927
934
  "improve.wallTime.p95Ms",
928
935
  ];
@@ -1183,6 +1190,19 @@ export function akmHealth(options = {}) {
1183
1190
  durationMs: sx.durationMs,
1184
1191
  },
1185
1192
  });
1193
+ const aa = improveSummary.autoAccept;
1194
+ advisories.push({
1195
+ name: "auto-accept-validation",
1196
+ kind: "heuristic",
1197
+ status: aa.validationFailed > 0 ? "warn" : "pass",
1198
+ confidence: aa.promoted + aa.validationFailed > 0 ? "high" : "low",
1199
+ message: aa.validationFailed > 0
1200
+ ? `${aa.validationFailed} proposal(s) passed confidence threshold but failed auto-accept validation (truncated description, invalid frontmatter, etc.) — they remain in the queue for manual review.`
1201
+ : aa.promoted > 0
1202
+ ? `Auto-accept healthy: ${aa.promoted} proposal(s) promoted, 0 validation failures.`
1203
+ : "Auto-accept gate did not run (disabled or no proposals above threshold).",
1204
+ evidence: { promoted: aa.promoted, validationFailed: aa.validationFailed },
1205
+ });
1186
1206
  const metrics = {
1187
1207
  taskFailRate: roundRate(taskFailRate),
1188
1208
  agentFailureRate: roundRate(agentFailureRate),
@@ -217,7 +217,7 @@ export const improveCommand = defineCommand({
217
217
  runRecorded = true; // Suppress any late signal-handler write — the success path owns the row now.
218
218
  if (primaryStashDir) {
219
219
  try {
220
- writeImproveResultFile(primaryStashDir, runId, improveResult);
220
+ writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso);
221
221
  }
222
222
  catch (err) {
223
223
  // Stderr warning on the failure path is preferable to crashing
@@ -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,
@@ -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";
@@ -678,7 +678,8 @@ export async function akmImprove(options = {}) {
678
678
  budgetAbortController.abort("improve budget exhausted");
679
679
  // Grace period: let finally run to release improve.lock, then hard-exit
680
680
  // to prevent the process outliving the task timeout window (lock-cascade fix).
681
- setTimeout(() => process.exit(1), 5_000);
681
+ // Exit 0: budget exhaustion is a normal scheduled-task condition, not an error.
682
+ setTimeout(() => process.exit(0), 5_000);
682
683
  }, budgetMs);
683
684
  // Clear the timer when the run ends to avoid keeping the event loop alive.
684
685
  clearBudgetTimer = () => clearTimeout(budgetTimer);
@@ -729,7 +730,7 @@ export async function akmImprove(options = {}) {
729
730
  rejectedProposalsByRef.set(e.ref, e);
730
731
  }
731
732
  }
732
- const { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount: loopGateCount, } = await runImproveLoopStage({
733
+ const { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount: loopGateCount, gateAutoAcceptFailedCount: loopGateFailedCount, } = await runImproveLoopStage({
733
734
  scope,
734
735
  options,
735
736
  primaryStashDir,
@@ -748,7 +749,7 @@ export async function akmImprove(options = {}) {
748
749
  eventsCtx,
749
750
  improveProfile,
750
751
  });
751
- 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({
752
753
  scope,
753
754
  options,
754
755
  primaryStashDir,
@@ -833,6 +834,10 @@ export async function akmImprove(options = {}) {
833
834
  const t = preparation.gateAutoAcceptedCount + loopGateCount + postLoopGateCount;
834
835
  return t > 0 ? { gateAutoAcceptedCount: t } : {};
835
836
  })(),
837
+ ...(() => {
838
+ const f = preparation.gateAutoAcceptFailedCount + loopGateFailedCount + postLoopGateFailedCount;
839
+ return f > 0 ? { gateAutoAcceptFailedCount: f } : {};
840
+ })(),
836
841
  };
837
842
  if (!result.dryRun)
838
843
  emitImproveCompletedEvent(result, {
@@ -1066,6 +1071,7 @@ async function runImprovePreparationStage(args) {
1066
1071
  // The extract envelope's own `warnings` field surfaces what went wrong.
1067
1072
  let extractResults;
1068
1073
  let gateAutoAcceptedCount = 0;
1074
+ let gateAutoAcceptFailedCount = 0;
1069
1075
  const extractConfig = options.config ?? loadConfig();
1070
1076
  const extractGateCfg = makeGateConfig("extract", {
1071
1077
  globalThreshold: options.autoAccept,
@@ -1087,12 +1093,16 @@ async function runImprovePreparationStage(args) {
1087
1093
  dryRun: options.dryRun ?? false,
1088
1094
  });
1089
1095
  extractResults.push(result);
1090
- gateAutoAcceptedCount += (await runAutoAcceptGate(primaryStashDir
1091
- ? result.proposals.map((proposalId) => {
1092
- const proposal = getProposal(primaryStashDir, proposalId);
1093
- return { proposalId, confidence: resolveExtractConfidence(proposal) };
1094
- })
1095
- : [], 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
+ }
1096
1106
  }
1097
1107
  catch (err) {
1098
1108
  const msg = err instanceof Error ? err.message : String(err);
@@ -1118,7 +1128,9 @@ async function runImprovePreparationStage(args) {
1118
1128
  proposalId: p.id,
1119
1129
  confidence: resolveExtractConfidence(p),
1120
1130
  }));
1121
- gateAutoAcceptedCount += (await runAutoAcceptGate(backlogCandidates, extractGateCfg)).promoted.length;
1131
+ const backlogGr = await runAutoAcceptGate(backlogCandidates, extractGateCfg);
1132
+ gateAutoAcceptedCount += backlogGr.promoted.length;
1133
+ gateAutoAcceptFailedCount += backlogGr.failed.length;
1122
1134
  }
1123
1135
  }
1124
1136
  // eligibleCount = raw pre-filter count (before cooldown/signal/cleanup filters).
@@ -1544,6 +1556,7 @@ async function runImprovePreparationStage(args) {
1544
1556
  recentErrors,
1545
1557
  utilityMap,
1546
1558
  gateAutoAcceptedCount,
1559
+ gateAutoAcceptFailedCount,
1547
1560
  };
1548
1561
  }
1549
1562
  // TODO(refactor): 13 args including `actions`/`recentErrors` mutation channels. Restructure into immutable plan + mutable context objects — deferred to dedicated refactor with isolated testing.
@@ -1627,6 +1640,7 @@ async function runImproveLoopStage(args) {
1627
1640
  ? listProposals(dedupeStashDirForProposals, { status: "pending" }).map((p) => p.ref)
1628
1641
  : []);
1629
1642
  let gateAutoAcceptedCount = 0;
1643
+ let gateAutoAcceptFailedCount = 0;
1630
1644
  const reflectGateCfg = makeGateConfig("reflect", {
1631
1645
  globalThreshold: options.autoAccept,
1632
1646
  dryRun: options.dryRun ?? false,
@@ -1803,7 +1817,9 @@ async function runImproveLoopStage(args) {
1803
1817
  },
1804
1818
  }, eventsCtx);
1805
1819
  if (reflectResult.ok) {
1806
- 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;
1807
1823
  }
1808
1824
  } // end else (reflect type/profile check)
1809
1825
  }
@@ -1914,7 +1930,9 @@ async function runImproveLoopStage(args) {
1914
1930
  });
1915
1931
  actions.push({ ref: planned.ref, mode: "distill", result: distillResult });
1916
1932
  if (distillResult.outcome === "queued" && distillResult.proposal) {
1917
- 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;
1918
1936
  }
1919
1937
  if (parsedPlannedRef.type === "memory") {
1920
1938
  const promotedToKnowledge = distillResult.outcome === "queued" && distillResult.proposalKind === "knowledge";
@@ -1987,7 +2005,7 @@ async function runImproveLoopStage(args) {
1987
2005
  completedCount++;
1988
2006
  info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
1989
2007
  }
1990
- return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount };
2008
+ return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount, gateAutoAcceptFailedCount };
1991
2009
  }
1992
2010
  async function runImprovePostLoopStage(args) {
1993
2011
  const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memorySummary, memoryRefsForInference, reindexFn, eventsCtx, budgetSignal, improveProfile, } = args;
@@ -2083,6 +2101,7 @@ async function runImprovePostLoopStage(args) {
2083
2101
  durationMs: 0,
2084
2102
  };
2085
2103
  let gateAutoAcceptedCount = 0;
2104
+ let gateAutoAcceptFailedCount = 0;
2086
2105
  const consolidateGateCfg = makeGateConfig("consolidate", {
2087
2106
  globalThreshold: options.autoAccept,
2088
2107
  dryRun: options.dryRun ?? false,
@@ -2121,17 +2140,21 @@ async function runImprovePostLoopStage(args) {
2121
2140
  // still wins because the spread above runs first.
2122
2141
  autoAccept: options.consolidateOptions?.autoAccept ?? options.autoAccept,
2123
2142
  });
2124
- gateAutoAcceptedCount += (await runAutoAcceptGate(consolidation.promoted.map((proposalId) => {
2125
- try {
2126
- 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 {
2127
2152
  return { proposalId, confidence: undefined };
2128
- const proposal = getProposal(primaryStashDir, proposalId);
2129
- return { proposalId, confidence: proposal.confidence };
2130
- }
2131
- catch {
2132
- return { proposalId, confidence: undefined };
2133
- }
2134
- }), consolidateGateCfg)).promoted.length;
2153
+ }
2154
+ }), consolidateGateCfg);
2155
+ gateAutoAcceptedCount += consolidateGr.promoted.length;
2156
+ gateAutoAcceptFailedCount += consolidateGr.failed.length;
2157
+ }
2135
2158
  if (consolidation.processed > 0) {
2136
2159
  appendEvent({
2137
2160
  eventType: "consolidate_completed",
@@ -2206,6 +2229,7 @@ async function runImprovePostLoopStage(args) {
2206
2229
  orphansPurged: maintenanceResult.orphansPurged,
2207
2230
  proposalsExpired: maintenanceResult.proposalsExpired,
2208
2231
  gateAutoAcceptedCount,
2232
+ gateAutoAcceptFailedCount,
2209
2233
  };
2210
2234
  }
2211
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.
@@ -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
  }