akm-cli 0.8.1 → 0.8.3
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 +91 -0
- package/dist/assets/profiles/quick.json +2 -1
- package/dist/assets/stash-skeleton/README.md +76 -0
- package/dist/cli.js +8 -3
- package/dist/commands/consolidate.js +4 -4
- package/dist/commands/health.js +20 -0
- package/dist/commands/improve-cli.js +1 -1
- package/dist/commands/improve-result-file.js +9 -4
- package/dist/commands/improve.js +67 -26
- package/dist/commands/init.js +6 -1
- package/dist/commands/{proposal-drain-policies.js → proposal/drain-policies.js} +2 -2
- package/dist/commands/{proposal-drain.js → proposal/drain.js} +10 -10
- package/dist/commands/show.js +47 -0
- package/dist/commands/stash-skeleton.js +78 -0
- package/dist/core/file-lock.js +22 -0
- package/dist/{setup/ripgrep-install.js → core/ripgrep/install.js} +2 -2
- package/dist/{setup/ripgrep-resolve.js → core/ripgrep/resolve.js} +2 -2
- package/dist/core/stash-meta.js +110 -0
- package/dist/setup/detect.js +27 -0
- package/dist/setup/harness-config-import.js +170 -0
- package/dist/setup/registry-stash-loader.js +99 -0
- package/dist/setup/setup.js +229 -72
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,97 @@ 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.3] - 2026-06-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`improve.lock` leaked on signal death (cron timeout).** The improve
|
|
12
|
+
SIGTERM/SIGINT/SIGHUP handler calls `process.exit()`, which skips `finally`
|
|
13
|
+
blocks — so the `finally` that releases `improve.lock` never ran, and every
|
|
14
|
+
timed-out cron run leaked the lock sentinel. (It wasn't a permanent deadlock
|
|
15
|
+
only because the next run reclaims a dead-PID lock, a path that PID reuse can
|
|
16
|
+
defeat.) The lock is now released from a `process.on("exit", …)` handler
|
|
17
|
+
registered at acquire time (exit handlers DO run on `process.exit()`), via a
|
|
18
|
+
new ownership-checked `releaseLockIfOwned(path, pid)` so a backstop release can
|
|
19
|
+
never delete a different run's lock. This generalizes to the budget watchdog
|
|
20
|
+
and any future exit path.
|
|
21
|
+
- **`quick` profile was not quick.** It was documented "Reflect-only" but did
|
|
22
|
+
not disable the session-`extract` process (which is default-ON), so a `quick`
|
|
23
|
+
run processed the entire unindexed-session backlog (~40 min) — guaranteeing a
|
|
24
|
+
5-minute cron timeout → SIGTERM → the lock leak above, every run. `quick` now
|
|
25
|
+
explicitly sets `processes.extract.enabled: false`.
|
|
26
|
+
|
|
27
|
+
## [0.8.2] - 2026-06-05
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **LM Studio auto-detection in setup wizard** — `akm setup` now probes
|
|
32
|
+
`localhost:1234/v1/models` at startup and, when the server is running, pre-fills
|
|
33
|
+
the LLM backend with the active model list, mirroring the existing Ollama detection
|
|
34
|
+
flow (#522).
|
|
35
|
+
- **Agent harness config import** — `akm setup` detects installed AI coding harnesses
|
|
36
|
+
(currently Claude Code and OpenCode) and pre-populates LLM provider, model, and
|
|
37
|
+
base-URL fields from the harness configuration. The importer registry
|
|
38
|
+
(`HARNESS_CONFIG_IMPORTERS`) makes adding future harnesses a single append (#523).
|
|
39
|
+
API key *values* are never read or stored — only the environment variable name is
|
|
40
|
+
imported.
|
|
41
|
+
- **Registry-driven stash selection** — the "Add Sources" step now fetches available
|
|
42
|
+
stashes from the official AKM registry at startup. `DEFAULT_SELECTED_STASH_IDS`
|
|
43
|
+
in `src/setup/registry-stash-loader.ts` is the single edit point for changing
|
|
44
|
+
which stashes are pre-checked. Falls back to a hardcoded list on network error (#520).
|
|
45
|
+
- **`improve.autoAccept.{promoted,validationFailed}` health metrics** — auto-accepted
|
|
46
|
+
proposals that pass the confidence threshold but fail validation (truncated
|
|
47
|
+
description, invalid frontmatter) are now counted as `gateAutoAcceptFailedCount`
|
|
48
|
+
in the improve result envelope and surfaced as `improve.autoAccept.validationFailed`
|
|
49
|
+
in `akm health` reports.
|
|
50
|
+
- **`auto-accept-validation` health advisory** — heuristic advisory that warns when
|
|
51
|
+
`validationFailed > 0` so malformed proposals are visible before they pile up in
|
|
52
|
+
the queue.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **`akm-improve` tasks recorded as failed on budget exhaustion** — the budget
|
|
57
|
+
exhaustion timer called `process.exit(1)`, causing every budget-limited run to be
|
|
58
|
+
recorded as a task failure. Changed to `process.exit(0)`; budget exhaustion is a
|
|
59
|
+
normal exit condition.
|
|
60
|
+
- **`improve_runs.started_at` always equal to `completed_at`** — `writeImproveResultFile`
|
|
61
|
+
was called at end-of-run, so `new Date()` captured the completion time and both
|
|
62
|
+
columns held the same value (649/661 real runs affected, regressed ~May 26).
|
|
63
|
+
`started_at` now uses the timestamp captured at process launch, passed in from the
|
|
64
|
+
CLI entry point. A regex-based fallback decodes the timestamp embedded in the run ID
|
|
65
|
+
for any call site that does not supply an explicit value (#524).
|
|
66
|
+
- **`akm-health-report` task fails on transient DNS errors** — the Discord webhook
|
|
67
|
+
script caught `HTTPError` but not the parent `URLError`, so DNS blips caused the
|
|
68
|
+
task runner to record the health report as failed. `URLError` is now caught and
|
|
69
|
+
logged as a warning with a clean exit.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **Stash `.meta/` convention** — a stash may carry an optional, human-authored
|
|
74
|
+
`.meta/` directory at its root for orientation: purpose, key assets, conventions,
|
|
75
|
+
and maintainer info. Surface it on demand with `akm show meta` (the working
|
|
76
|
+
stash's `.meta/index.md`), `akm show meta:<name>` (e.g. `.meta/about.md`), or
|
|
77
|
+
scope it to a specific stash with `akm show <origin>//meta[:<name>]`. Because
|
|
78
|
+
`.meta/` is a dot-directory, the indexer already skips it, so these docs never
|
|
79
|
+
pollute search results — they are direct-read on demand. Owners extend the
|
|
80
|
+
convention by dropping new files (`.meta/about.md`, `.meta/conventions.md`,
|
|
81
|
+
`.meta/license`) with no code changes. `akm init` scaffolds a `.meta/index.md`
|
|
82
|
+
template into newly created stashes.
|
|
83
|
+
- **Default stash skeleton** — `akm init` (and `akm setup`) now copies
|
|
84
|
+
`src/assets/stash-skeleton/` into every newly created stash. Currently ships
|
|
85
|
+
a `README.md` covering what the stash contains and how agents use `akm` to
|
|
86
|
+
access assets. Existing files are never overwritten. Add files to
|
|
87
|
+
`src/assets/stash-skeleton/` to extend what ships with a fresh install.
|
|
88
|
+
|
|
89
|
+
### Improved
|
|
90
|
+
|
|
91
|
+
- **Setup wizard pre-populates from existing config** — on re-run, `akm setup`
|
|
92
|
+
initialises every prompt default from the current saved configuration so users
|
|
93
|
+
only need to change what has actually changed (#519).
|
|
94
|
+
- **Config backup before every setup write** — `backupExistingConfig()` is now called
|
|
95
|
+
before each `saveConfig` in the setup wizard, ensuring the previous config is always
|
|
96
|
+
recoverable if a wizard run is interrupted (#521).
|
|
97
|
+
|
|
7
98
|
## [0.8.1] - 2026-06-05
|
|
8
99
|
|
|
9
100
|
### Added
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Reflect-only pass — no distill, consolidate, memoryInference, or graphExtraction.",
|
|
2
|
+
"description": "Reflect-only pass — no extract, distill, consolidate, memoryInference, or graphExtraction.",
|
|
3
3
|
"processes": {
|
|
4
4
|
"reflect": {
|
|
5
5
|
"enabled": true,
|
|
6
6
|
"allowedTypes": ["agent", "command", "knowledge", "lesson", "memory", "skill", "wiki", "workflow"]
|
|
7
7
|
},
|
|
8
|
+
"extract": { "enabled": false },
|
|
8
9
|
"distill": { "enabled": false },
|
|
9
10
|
"consolidate": { "enabled": false },
|
|
10
11
|
"memoryInference": { "enabled": false },
|
|
@@ -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
|
|
96
|
-
import { resolveDrainPolicy } from "./commands/proposal
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/dist/commands/health.js
CHANGED
|
@@ -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
|
|
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
|
|
87
|
+
startedAt: resolvedStartedAt,
|
|
88
|
+
completedAt,
|
|
84
89
|
stashDir,
|
|
85
90
|
dryRun: Boolean(result.dryRun),
|
|
86
91
|
profile: null,
|
package/dist/commands/improve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { daysToMs, isAssetType } from "../core/common";
|
|
|
8
8
|
import { getDefaultLlmConfig, loadConfig } from "../core/config";
|
|
9
9
|
import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../core/errors";
|
|
10
10
|
import { appendEvent, readEvents } from "../core/events";
|
|
11
|
-
import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
|
|
11
|
+
import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../core/file-lock";
|
|
12
12
|
import { parseFrontmatter } from "../core/frontmatter";
|
|
13
13
|
import { detectAndWriteContradictions } from "../core/memory-contradiction-detect";
|
|
14
14
|
import { analyzeMemoryCleanup, applyMemoryCleanup, } from "../core/memory-improve";
|
|
@@ -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
|
|
40
|
-
import { resolveDrainPolicy } from "./proposal
|
|
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";
|
|
@@ -497,6 +497,17 @@ export async function akmImprove(options = {}) {
|
|
|
497
497
|
}
|
|
498
498
|
lockAcquired = false;
|
|
499
499
|
};
|
|
500
|
+
// Signal-safe lock release (0.8.3 hotfix). The SIGTERM/SIGINT/SIGHUP handler
|
|
501
|
+
// in improve-cli.ts calls `process.exit()`, which does NOT run the `finally`
|
|
502
|
+
// below that owns lock release — so a cron-timeout SIGTERM leaked
|
|
503
|
+
// `improve.lock` every run. `process.exit()` DOES fire `'exit'` listeners,
|
|
504
|
+
// so we release the lock from one. `releaseLockIfOwned` only unlinks a lock
|
|
505
|
+
// still owned by this PID, so it is safe even if a later run re-acquired it.
|
|
506
|
+
// The listener is removed in the `finally` so the normal path stays single-release
|
|
507
|
+
// and repeated in-process `akmImprove` calls (tests) do not accumulate listeners.
|
|
508
|
+
const releaseLockOnExit = () => {
|
|
509
|
+
releaseLockIfOwned(resolvedLockPath, process.pid);
|
|
510
|
+
};
|
|
500
511
|
const preEnsureCleanupWarnings = [];
|
|
501
512
|
let plannedRefs;
|
|
502
513
|
let memorySummary;
|
|
@@ -510,6 +521,9 @@ export async function akmImprove(options = {}) {
|
|
|
510
521
|
if (!options.dryRun) {
|
|
511
522
|
acquireLock();
|
|
512
523
|
lockAcquired = true;
|
|
524
|
+
// Backstop release on process.exit() (signal handler / budget watchdog),
|
|
525
|
+
// which skips the finally below. Removed in that finally on the normal path.
|
|
526
|
+
process.on("exit", releaseLockOnExit);
|
|
513
527
|
// Phase 4 triage pre-pass (§7, §13): drain the standing pending backlog
|
|
514
528
|
// BEFORE ensureIndex so improve generates fresh proposals against a cleared
|
|
515
529
|
// queue (no `duplicate_pending` collisions) and ensureIndex absorbs triage's
|
|
@@ -678,7 +692,8 @@ export async function akmImprove(options = {}) {
|
|
|
678
692
|
budgetAbortController.abort("improve budget exhausted");
|
|
679
693
|
// Grace period: let finally run to release improve.lock, then hard-exit
|
|
680
694
|
// to prevent the process outliving the task timeout window (lock-cascade fix).
|
|
681
|
-
|
|
695
|
+
// Exit 0: budget exhaustion is a normal scheduled-task condition, not an error.
|
|
696
|
+
setTimeout(() => process.exit(0), 5_000);
|
|
682
697
|
}, budgetMs);
|
|
683
698
|
// Clear the timer when the run ends to avoid keeping the event loop alive.
|
|
684
699
|
clearBudgetTimer = () => clearTimeout(budgetTimer);
|
|
@@ -729,7 +744,7 @@ export async function akmImprove(options = {}) {
|
|
|
729
744
|
rejectedProposalsByRef.set(e.ref, e);
|
|
730
745
|
}
|
|
731
746
|
}
|
|
732
|
-
const { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount: loopGateCount, } = await runImproveLoopStage({
|
|
747
|
+
const { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount: loopGateCount, gateAutoAcceptFailedCount: loopGateFailedCount, } = await runImproveLoopStage({
|
|
733
748
|
scope,
|
|
734
749
|
options,
|
|
735
750
|
primaryStashDir,
|
|
@@ -748,7 +763,7 @@ export async function akmImprove(options = {}) {
|
|
|
748
763
|
eventsCtx,
|
|
749
764
|
improveProfile,
|
|
750
765
|
});
|
|
751
|
-
const { allWarnings, consolidation, deadUrls, memoryInference, graphExtraction, stalenessDetection, maintenanceActions, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, gateAutoAcceptedCount: postLoopGateCount, } = await runImprovePostLoopStage({
|
|
766
|
+
const { allWarnings, consolidation, deadUrls, memoryInference, graphExtraction, stalenessDetection, maintenanceActions, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, gateAutoAcceptedCount: postLoopGateCount, gateAutoAcceptFailedCount: postLoopGateFailedCount, } = await runImprovePostLoopStage({
|
|
752
767
|
scope,
|
|
753
768
|
options,
|
|
754
769
|
primaryStashDir,
|
|
@@ -833,6 +848,10 @@ export async function akmImprove(options = {}) {
|
|
|
833
848
|
const t = preparation.gateAutoAcceptedCount + loopGateCount + postLoopGateCount;
|
|
834
849
|
return t > 0 ? { gateAutoAcceptedCount: t } : {};
|
|
835
850
|
})(),
|
|
851
|
+
...(() => {
|
|
852
|
+
const f = preparation.gateAutoAcceptFailedCount + loopGateFailedCount + postLoopGateFailedCount;
|
|
853
|
+
return f > 0 ? { gateAutoAcceptFailedCount: f } : {};
|
|
854
|
+
})(),
|
|
836
855
|
};
|
|
837
856
|
if (!result.dryRun)
|
|
838
857
|
emitImproveCompletedEvent(result, {
|
|
@@ -917,6 +936,9 @@ export async function akmImprove(options = {}) {
|
|
|
917
936
|
catch {
|
|
918
937
|
// ignore
|
|
919
938
|
}
|
|
939
|
+
// The normal path released the lock above; drop the process.exit backstop so
|
|
940
|
+
// it does not fire later (or accumulate across repeated in-process calls).
|
|
941
|
+
process.removeListener("exit", releaseLockOnExit);
|
|
920
942
|
// I1: close the long-lived state.db connection opened at the top of the run.
|
|
921
943
|
try {
|
|
922
944
|
eventsDb?.close();
|
|
@@ -1066,6 +1088,7 @@ async function runImprovePreparationStage(args) {
|
|
|
1066
1088
|
// The extract envelope's own `warnings` field surfaces what went wrong.
|
|
1067
1089
|
let extractResults;
|
|
1068
1090
|
let gateAutoAcceptedCount = 0;
|
|
1091
|
+
let gateAutoAcceptFailedCount = 0;
|
|
1069
1092
|
const extractConfig = options.config ?? loadConfig();
|
|
1070
1093
|
const extractGateCfg = makeGateConfig("extract", {
|
|
1071
1094
|
globalThreshold: options.autoAccept,
|
|
@@ -1087,12 +1110,16 @@ async function runImprovePreparationStage(args) {
|
|
|
1087
1110
|
dryRun: options.dryRun ?? false,
|
|
1088
1111
|
});
|
|
1089
1112
|
extractResults.push(result);
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1113
|
+
{
|
|
1114
|
+
const gr = await runAutoAcceptGate(primaryStashDir
|
|
1115
|
+
? result.proposals.map((proposalId) => {
|
|
1116
|
+
const proposal = getProposal(primaryStashDir, proposalId);
|
|
1117
|
+
return { proposalId, confidence: resolveExtractConfidence(proposal) };
|
|
1118
|
+
})
|
|
1119
|
+
: [], extractGateCfg);
|
|
1120
|
+
gateAutoAcceptedCount += gr.promoted.length;
|
|
1121
|
+
gateAutoAcceptFailedCount += gr.failed.length;
|
|
1122
|
+
}
|
|
1096
1123
|
}
|
|
1097
1124
|
catch (err) {
|
|
1098
1125
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1118,7 +1145,9 @@ async function runImprovePreparationStage(args) {
|
|
|
1118
1145
|
proposalId: p.id,
|
|
1119
1146
|
confidence: resolveExtractConfidence(p),
|
|
1120
1147
|
}));
|
|
1121
|
-
|
|
1148
|
+
const backlogGr = await runAutoAcceptGate(backlogCandidates, extractGateCfg);
|
|
1149
|
+
gateAutoAcceptedCount += backlogGr.promoted.length;
|
|
1150
|
+
gateAutoAcceptFailedCount += backlogGr.failed.length;
|
|
1122
1151
|
}
|
|
1123
1152
|
}
|
|
1124
1153
|
// eligibleCount = raw pre-filter count (before cooldown/signal/cleanup filters).
|
|
@@ -1544,6 +1573,7 @@ async function runImprovePreparationStage(args) {
|
|
|
1544
1573
|
recentErrors,
|
|
1545
1574
|
utilityMap,
|
|
1546
1575
|
gateAutoAcceptedCount,
|
|
1576
|
+
gateAutoAcceptFailedCount,
|
|
1547
1577
|
};
|
|
1548
1578
|
}
|
|
1549
1579
|
// 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 +1657,7 @@ async function runImproveLoopStage(args) {
|
|
|
1627
1657
|
? listProposals(dedupeStashDirForProposals, { status: "pending" }).map((p) => p.ref)
|
|
1628
1658
|
: []);
|
|
1629
1659
|
let gateAutoAcceptedCount = 0;
|
|
1660
|
+
let gateAutoAcceptFailedCount = 0;
|
|
1630
1661
|
const reflectGateCfg = makeGateConfig("reflect", {
|
|
1631
1662
|
globalThreshold: options.autoAccept,
|
|
1632
1663
|
dryRun: options.dryRun ?? false,
|
|
@@ -1803,7 +1834,9 @@ async function runImproveLoopStage(args) {
|
|
|
1803
1834
|
},
|
|
1804
1835
|
}, eventsCtx);
|
|
1805
1836
|
if (reflectResult.ok) {
|
|
1806
|
-
|
|
1837
|
+
const reflectGr = await runAutoAcceptGate([{ proposalId: reflectResult.proposal.id, confidence: reflectResult.proposal.confidence }], reflectGateCfg);
|
|
1838
|
+
gateAutoAcceptedCount += reflectGr.promoted.length;
|
|
1839
|
+
gateAutoAcceptFailedCount += reflectGr.failed.length;
|
|
1807
1840
|
}
|
|
1808
1841
|
} // end else (reflect type/profile check)
|
|
1809
1842
|
}
|
|
@@ -1914,7 +1947,9 @@ async function runImproveLoopStage(args) {
|
|
|
1914
1947
|
});
|
|
1915
1948
|
actions.push({ ref: planned.ref, mode: "distill", result: distillResult });
|
|
1916
1949
|
if (distillResult.outcome === "queued" && distillResult.proposal) {
|
|
1917
|
-
|
|
1950
|
+
const distillGr = await runAutoAcceptGate([{ proposalId: distillResult.proposal.id, confidence: distillResult.proposal.confidence }], distillGateCfg);
|
|
1951
|
+
gateAutoAcceptedCount += distillGr.promoted.length;
|
|
1952
|
+
gateAutoAcceptFailedCount += distillGr.failed.length;
|
|
1918
1953
|
}
|
|
1919
1954
|
if (parsedPlannedRef.type === "memory") {
|
|
1920
1955
|
const promotedToKnowledge = distillResult.outcome === "queued" && distillResult.proposalKind === "knowledge";
|
|
@@ -1987,7 +2022,7 @@ async function runImproveLoopStage(args) {
|
|
|
1987
2022
|
completedCount++;
|
|
1988
2023
|
info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
|
|
1989
2024
|
}
|
|
1990
|
-
return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount };
|
|
2025
|
+
return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount, gateAutoAcceptFailedCount };
|
|
1991
2026
|
}
|
|
1992
2027
|
async function runImprovePostLoopStage(args) {
|
|
1993
2028
|
const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memorySummary, memoryRefsForInference, reindexFn, eventsCtx, budgetSignal, improveProfile, } = args;
|
|
@@ -2083,6 +2118,7 @@ async function runImprovePostLoopStage(args) {
|
|
|
2083
2118
|
durationMs: 0,
|
|
2084
2119
|
};
|
|
2085
2120
|
let gateAutoAcceptedCount = 0;
|
|
2121
|
+
let gateAutoAcceptFailedCount = 0;
|
|
2086
2122
|
const consolidateGateCfg = makeGateConfig("consolidate", {
|
|
2087
2123
|
globalThreshold: options.autoAccept,
|
|
2088
2124
|
dryRun: options.dryRun ?? false,
|
|
@@ -2121,17 +2157,21 @@ async function runImprovePostLoopStage(args) {
|
|
|
2121
2157
|
// still wins because the spread above runs first.
|
|
2122
2158
|
autoAccept: options.consolidateOptions?.autoAccept ?? options.autoAccept,
|
|
2123
2159
|
});
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2160
|
+
{
|
|
2161
|
+
const consolidateGr = await runAutoAcceptGate(consolidation.promoted.map((proposalId) => {
|
|
2162
|
+
try {
|
|
2163
|
+
if (!primaryStashDir)
|
|
2164
|
+
return { proposalId, confidence: undefined };
|
|
2165
|
+
const proposal = getProposal(primaryStashDir, proposalId);
|
|
2166
|
+
return { proposalId, confidence: proposal.confidence };
|
|
2167
|
+
}
|
|
2168
|
+
catch {
|
|
2127
2169
|
return { proposalId, confidence: undefined };
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
}
|
|
2134
|
-
}), consolidateGateCfg)).promoted.length;
|
|
2170
|
+
}
|
|
2171
|
+
}), consolidateGateCfg);
|
|
2172
|
+
gateAutoAcceptedCount += consolidateGr.promoted.length;
|
|
2173
|
+
gateAutoAcceptFailedCount += consolidateGr.failed.length;
|
|
2174
|
+
}
|
|
2135
2175
|
if (consolidation.processed > 0) {
|
|
2136
2176
|
appendEvent({
|
|
2137
2177
|
eventType: "consolidate_completed",
|
|
@@ -2206,6 +2246,7 @@ async function runImprovePostLoopStage(args) {
|
|
|
2206
2246
|
orphansPurged: maintenanceResult.orphansPurged,
|
|
2207
2247
|
proposalsExpired: maintenanceResult.proposalsExpired,
|
|
2208
2248
|
gateAutoAcceptedCount,
|
|
2249
|
+
gateAutoAcceptFailedCount,
|
|
2209
2250
|
};
|
|
2210
2251
|
}
|
|
2211
2252
|
// 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.
|
package/dist/commands/init.js
CHANGED
|
@@ -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 "../
|
|
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 "
|
|
20
|
-
import { PROPOSAL_SOURCES } from "
|
|
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 "
|
|
40
|
-
import { resolveAssetPathFromName, TYPE_DIRS } from "
|
|
41
|
-
import { appendEvent } from "
|
|
42
|
-
import { parseFrontmatter } from "
|
|
43
|
-
import { listProposals } from "
|
|
44
|
-
import { info, warn } from "
|
|
45
|
-
import { runAgent } from "
|
|
46
|
-
import { runOpencodeSdk } from "
|
|
47
|
-
import { chatCompletion, stripJsonFences } from "
|
|
48
|
-
import { akmProposalAccept, akmProposalReject } from "
|
|
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
|
// ---------------------------------------------------------------------------
|