akm-cli 0.9.0-beta.26 → 0.9.0-beta.27
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 +46 -0
- package/dist/commands/improve/improve.js +281 -146
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +1 -0
- package/dist/commands/lint/registry.js +2 -0
- package/dist/commands/read/curate.js +50 -1
- package/dist/commands/read/show.js +67 -2
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/config/config-schema.js +14 -2
- package/dist/indexer/db/db.js +17 -0
- package/dist/indexer/db/graph-db.js +48 -0
- package/dist/indexer/graph/graph-extraction.js +203 -3
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/prompts.js +1 -0
- package/dist/output/renderers.js +73 -1
- package/dist/scripts/migrate-storage.js +88 -4
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +30 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,52 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0-beta.27] — 2026-06-20
|
|
10
|
+
|
|
11
|
+
All new behavior is **opt-in / default-preserving** — default runs are byte-identical.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **#624 P2 — priority-ranked graph extraction.** `processes.graphExtraction.topN`:
|
|
16
|
+
when set, the graph-extraction pass ranks eligible files by asset utility
|
|
17
|
+
(`utility_scores`, read-only join) and processes only the top-N per run, so
|
|
18
|
+
high-value assets get graphed first instead of a ~55h full-corpus sweep. Unset
|
|
19
|
+
(default) = no ranking, byte-identical.
|
|
20
|
+
- **#624 P3 — lazy on-demand graph extraction.** New `graph_extraction_queue` table
|
|
21
|
+
+ `enqueueGraphExtraction`/`drainExtractionQueue`/`extractGraphForSingleFile`.
|
|
22
|
+
`akm curate` enqueues an ungraphed hit (non-blocking); `akm show` can extract a
|
|
23
|
+
missing graph inline — gated on `index.graph.lazyGraphExtraction: true`
|
|
24
|
+
(**default off**: `show` makes no LLM call by default), model-guarded, and bounded
|
|
25
|
+
by a 30s timeout so it never hangs. The pass drains the queue before the ranked
|
|
26
|
+
sweep. This **closes #624** (all three layers shipped).
|
|
27
|
+
- **#616 — bounded multi-cycle phasing.** `profiles.improve.<name>.maxCycles`
|
|
28
|
+
(default 1): when > 1, the improve passes run in an N-cycle loop so gate-accepted
|
|
29
|
+
output of cycle N feeds cycle N+1 within the same run (re-running ensureIndex +
|
|
30
|
+
ref selection each cycle), stopping at a fixed point and respecting the run budget.
|
|
31
|
+
`maxCycles: 1` = byte-identical to today.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Release CI unblocked.** `runCliCapture` (test harness) restored `process.exitCode`
|
|
36
|
+
to a captured `undefined`, which under `bun test` does not clear a previously-set
|
|
37
|
+
non-zero exit code — so the unit suite exited 1 with 0 failures at `TEST_PARALLEL=1`
|
|
38
|
+
(exactly how `release.yml` runs), silently blocking every npm publish since beta.11.
|
|
39
|
+
Fixed to restore to `0`. (This is why beta.26 was the first successful workflow publish.)
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **CI/release tests sharded across runner jobs (~15 min → ~2 min).** Bun 1.3.x
|
|
44
|
+
in-process test parallelism (`--parallel=N`, N>1) hits an intermittent
|
|
45
|
+
`epoll_ctl EEXIST` race / busy-spin hang on the `--isolate` workers, which had
|
|
46
|
+
forced fully-sequential (`TEST_PARALLEL=1`) runs. Tests now shard across separate
|
|
47
|
+
runner jobs (each a separate process tree, so no cross-shard fd/epoll collisions)
|
|
48
|
+
with `--parallel=1` within each shard; the matrix runs shards concurrently. The
|
|
49
|
+
release gate runs the identical set of tests. Local `bun run check` defaults to
|
|
50
|
+
sequential too (the only safe mode on this Bun version). Coverage unchanged.
|
|
51
|
+
Each shard runs through `scripts/run-test-shard.sh`, which retries **only on a
|
|
52
|
+
hang/timeout** (the busy-spin can rarely fire even at `--parallel=1`) and never
|
|
53
|
+
on a real test failure, so genuine red tests still fail fast and are never masked.
|
|
54
|
+
|
|
9
55
|
## [0.9.0-beta.26] — 2026-06-20
|
|
10
56
|
|
|
11
57
|
### Added
|
|
@@ -683,6 +683,11 @@ export async function akmImprove(options = {}) {
|
|
|
683
683
|
const ensureIndexFn = options.ensureIndexFn ?? ensureIndex;
|
|
684
684
|
const reindexFn = options.reindexFn ?? akmIndex;
|
|
685
685
|
const drainProposalsFn = options.drainProposalsFn ?? drainProposals;
|
|
686
|
+
// #616 multi-cycle test seams. Default to the real module-local fns.
|
|
687
|
+
const collectEligibleRefsImpl = options.collectEligibleRefsFn ?? collectEligibleRefs;
|
|
688
|
+
const runImprovePreparationStageImpl = options.runImprovePreparationStageFn ?? runImprovePreparationStage;
|
|
689
|
+
const runImproveLoopStageImpl = options.runImproveLoopStageFn ?? runImproveLoopStage;
|
|
690
|
+
const runImprovePostLoopStageImpl = options.runImprovePostLoopStageFn ?? runImprovePostLoopStage;
|
|
686
691
|
// Resolve the improve profile for this run. Profile drives type filtering,
|
|
687
692
|
// process gating, and default autoAccept/limit values.
|
|
688
693
|
const _earlyConfig = options.config ?? loadConfig();
|
|
@@ -697,6 +702,9 @@ export async function akmImprove(options = {}) {
|
|
|
697
702
|
// CLI --limit takes precedence over both.
|
|
698
703
|
limit: options.limit ?? improveProfile?.processes?.reflect?.limit ?? improveProfile.limit,
|
|
699
704
|
};
|
|
705
|
+
// #616 — bounded multi-cycle phasing. CLI/programmatic override wins over
|
|
706
|
+
// profile.maxCycles; default 1 => single pass (byte-identical to pre-#616).
|
|
707
|
+
const maxCycles = Math.max(1, Math.trunc(options.maxCycles ?? improveProfile.maxCycles ?? 1));
|
|
700
708
|
let primaryStashDir;
|
|
701
709
|
try {
|
|
702
710
|
primaryStashDir = resolveSourceEntries(options.stashDir)[0]?.path;
|
|
@@ -741,70 +749,23 @@ export async function akmImprove(options = {}) {
|
|
|
741
749
|
// Lock base directory — same `.akm/` under the primary stash dir.
|
|
742
750
|
const lockBaseDir = primaryStashDir ? path.join(primaryStashDir, ".akm") : path.join(options.stashDir ?? ".", ".akm");
|
|
743
751
|
const preEnsureCleanupWarnings = [];
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
752
|
+
// #616: assigned by runIndexAndCollect() (closure) so TS cannot prove definite
|
|
753
|
+
// assignment — seed with empty values; the first runIndexAndCollect() call
|
|
754
|
+
// (cycle 1, in the first try) always overwrites them before any read.
|
|
755
|
+
let plannedRefs = [];
|
|
756
|
+
let memorySummary = { eligible: 0, derived: 0 };
|
|
757
|
+
let profileFilteredRefs = [];
|
|
747
758
|
let memoryCleanupPlan;
|
|
748
759
|
let guidance;
|
|
749
760
|
let triageDrain;
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
for (const p of heldProcessLocks) {
|
|
759
|
-
releaseLockIfOwned(p, process.pid);
|
|
760
|
-
}
|
|
761
|
-
};
|
|
762
|
-
process.on("exit", releaseAllOnExit);
|
|
763
|
-
// #607 triage pre-pass: acquire triage.lock, drain the standing pending
|
|
764
|
-
// backlog BEFORE ensureIndex so improve generates fresh proposals against
|
|
765
|
-
// a cleared queue (no `duplicate_pending` collisions) and ensureIndex
|
|
766
|
-
// absorbs triage's promotions for free. Release immediately after —
|
|
767
|
-
// triage.lock is not needed again until the next improve run.
|
|
768
|
-
if (primaryStashDir && resolveProcessEnabled("triage", improveProfile)) {
|
|
769
|
-
if (scope.mode === "ref") {
|
|
770
|
-
warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
const triageLPath = processLockPath(lockBaseDir, "triage");
|
|
774
|
-
const triageResult = tryAcquireProcessLock(triageLPath, PROCESS_LOCK_DEFS.triage.staleAfterMs, options.skipIfLocked, "triage");
|
|
775
|
-
if (triageResult === "skipped") {
|
|
776
|
-
triageDrain = undefined;
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
try {
|
|
780
|
-
const triageConfig = improveProfile.processes?.triage;
|
|
781
|
-
const policy = resolveDrainPolicy(triageConfig?.policy);
|
|
782
|
-
const applyMode = triageConfig?.applyMode ?? "queue";
|
|
783
|
-
const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
|
|
784
|
-
const judgment = triageConfig?.judgment
|
|
785
|
-
? resolveTriageJudgmentRunner(triageConfig.judgment, _earlyConfig)
|
|
786
|
-
: null;
|
|
787
|
-
triageDrain = await drainProposalsFn({
|
|
788
|
-
stashDir: primaryStashDir,
|
|
789
|
-
policy,
|
|
790
|
-
applyMode,
|
|
791
|
-
maxAccepts,
|
|
792
|
-
dryRun: false,
|
|
793
|
-
excludeIds: new Set(),
|
|
794
|
-
...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
|
|
795
|
-
judgment,
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
catch (err) {
|
|
799
|
-
warn(`[improve] triage pre-pass failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
800
|
-
}
|
|
801
|
-
finally {
|
|
802
|
-
releaseProcessLock(triageLPath);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
761
|
+
// #616 — ensureIndex + collectEligibleRefs + memory-cleanup recompute, lifted
|
|
762
|
+
// into a helper so the SAME sequence runs once for cycle 1 (below, in the
|
|
763
|
+
// first try) and is re-run at the top of each subsequent multi-cycle cycle.
|
|
764
|
+
// Re-running ensureIndex between cycles makes cycle N's gate-promoted
|
|
765
|
+
// proposals visible to cycle N+1's collectEligibleRefs. Mutates the
|
|
766
|
+
// outer-scope plannedRefs/memorySummary/profileFilteredRefs/memoryCleanupPlan/
|
|
767
|
+
// guidance so for maxCycles:1 the body is byte-identical to pre-#616.
|
|
768
|
+
const runIndexAndCollect = async () => {
|
|
808
769
|
// #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs. The eligible-ref
|
|
809
770
|
// query reads the `entries` table; if a DB version upgrade just dropped that
|
|
810
771
|
// table (or the index is otherwise empty), the prior run order silently
|
|
@@ -862,7 +823,7 @@ export async function akmImprove(options = {}) {
|
|
|
862
823
|
}
|
|
863
824
|
}
|
|
864
825
|
}
|
|
865
|
-
({ plannedRefs, memorySummary, profileFilteredRefs } = await
|
|
826
|
+
({ plannedRefs, memorySummary, profileFilteredRefs } = await collectEligibleRefsImpl(scope, options.stashDir, improveProfile));
|
|
866
827
|
const cleanupParentRef = memoryCleanupParentRef(scope, options.stashDir);
|
|
867
828
|
// M-1 (#367): Run contradiction-detection BEFORE analyzeMemoryCleanup so
|
|
868
829
|
// the SCC resolver in resolveFamilyContradictions has edges to work on.
|
|
@@ -884,6 +845,69 @@ export async function akmImprove(options = {}) {
|
|
|
884
845
|
memorySummary.eligible > 0
|
|
885
846
|
? "Improve folds memory cleanup into the same proposal queue: speculative promotions still go through reflect/distill proposals, while high-confidence redundant derived memories are moved into a recoverable cleanup archive instead of being left active in the stash."
|
|
886
847
|
: undefined;
|
|
848
|
+
};
|
|
849
|
+
try {
|
|
850
|
+
// #607: Per-process lock acquisition. Each process acquires only the lock(s)
|
|
851
|
+
// it needs. The dry-run branch produces plannedRefs/memorySummary WITHOUT any
|
|
852
|
+
// locks (decision: dry-run never mutates the queue).
|
|
853
|
+
if (!options.dryRun) {
|
|
854
|
+
// Backstop release on process.exit() (signal handler / budget watchdog),
|
|
855
|
+
// which skips the finally below. Removed in that finally on the normal path.
|
|
856
|
+
const releaseAllOnExit = () => {
|
|
857
|
+
for (const p of heldProcessLocks) {
|
|
858
|
+
releaseLockIfOwned(p, process.pid);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
process.on("exit", releaseAllOnExit);
|
|
862
|
+
// #607 triage pre-pass: acquire triage.lock, drain the standing pending
|
|
863
|
+
// backlog BEFORE ensureIndex so improve generates fresh proposals against
|
|
864
|
+
// a cleared queue (no `duplicate_pending` collisions) and ensureIndex
|
|
865
|
+
// absorbs triage's promotions for free. Release immediately after —
|
|
866
|
+
// triage.lock is not needed again until the next improve run.
|
|
867
|
+
if (primaryStashDir && resolveProcessEnabled("triage", improveProfile)) {
|
|
868
|
+
if (scope.mode === "ref") {
|
|
869
|
+
warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
const triageLPath = processLockPath(lockBaseDir, "triage");
|
|
873
|
+
const triageResult = tryAcquireProcessLock(triageLPath, PROCESS_LOCK_DEFS.triage.staleAfterMs, options.skipIfLocked, "triage");
|
|
874
|
+
if (triageResult === "skipped") {
|
|
875
|
+
triageDrain = undefined;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
try {
|
|
879
|
+
const triageConfig = improveProfile.processes?.triage;
|
|
880
|
+
const policy = resolveDrainPolicy(triageConfig?.policy);
|
|
881
|
+
const applyMode = triageConfig?.applyMode ?? "queue";
|
|
882
|
+
const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
|
|
883
|
+
const judgment = triageConfig?.judgment
|
|
884
|
+
? resolveTriageJudgmentRunner(triageConfig.judgment, _earlyConfig)
|
|
885
|
+
: null;
|
|
886
|
+
triageDrain = await drainProposalsFn({
|
|
887
|
+
stashDir: primaryStashDir,
|
|
888
|
+
policy,
|
|
889
|
+
applyMode,
|
|
890
|
+
maxAccepts,
|
|
891
|
+
dryRun: false,
|
|
892
|
+
excludeIds: new Set(),
|
|
893
|
+
...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
|
|
894
|
+
judgment,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
warn(`[improve] triage pre-pass failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
899
|
+
}
|
|
900
|
+
finally {
|
|
901
|
+
releaseProcessLock(triageLPath);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs (now inside the
|
|
908
|
+
// helper). Cycle 1 runs it here; subsequent multi-cycle cycles re-run it via
|
|
909
|
+
// the same helper at the top of each cycle below.
|
|
910
|
+
await runIndexAndCollect();
|
|
887
911
|
if (options.dryRun) {
|
|
888
912
|
const result = {
|
|
889
913
|
schemaVersion: 1,
|
|
@@ -981,89 +1005,194 @@ export async function akmImprove(options = {}) {
|
|
|
981
1005
|
},
|
|
982
1006
|
}, eventsCtx);
|
|
983
1007
|
}
|
|
984
|
-
// #
|
|
985
|
-
//
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
//
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1008
|
+
// #616 — bounded multi-cycle phasing. The prep->loop->post-loop sequence is
|
|
1009
|
+
// wrapped in an N-cycle loop. Each cycle re-runs ensureIndex +
|
|
1010
|
+
// collectEligibleRefs (via runIndexAndCollect) so gate-accepted output of
|
|
1011
|
+
// cycle N becomes selectable input to cycle N+1. The per-stage process locks
|
|
1012
|
+
// (consolidate / reflect-distill) are acquired+released INSIDE each cycle,
|
|
1013
|
+
// exactly as the single-pass path did. For maxCycles:1 the loop runs once and
|
|
1014
|
+
// every accumulator below collapses to the single-cycle value (sum-of-one,
|
|
1015
|
+
// concat-of-one, last==only) => BYTE-IDENTICAL to pre-#616.
|
|
1016
|
+
//
|
|
1017
|
+
// Accumulators (see CONSTRAINTS / aggregation plan in #616): SUM the count
|
|
1018
|
+
// fields and durations; CONCAT the array fields; LAST-WINS for point-in-time
|
|
1019
|
+
// objects (the final cycle's value reflects the converged state).
|
|
1020
|
+
let cyclesRun = 0;
|
|
1021
|
+
// Last-wins point-in-time values (assigned every cycle; the final cycle wins).
|
|
1022
|
+
let preparation;
|
|
1023
|
+
let memoryRefsForInference = new Set();
|
|
1024
|
+
let consolidation;
|
|
1025
|
+
let memoryInference;
|
|
1026
|
+
let graphExtraction;
|
|
1027
|
+
let stalenessDetection;
|
|
1028
|
+
let recombination;
|
|
1029
|
+
let proceduralCompilation;
|
|
1030
|
+
// Summed counters/durations.
|
|
1031
|
+
let prepGateCount = 0;
|
|
1032
|
+
let prepGateFailedCount = 0;
|
|
1033
|
+
let reflectsWithErrorContext = 0;
|
|
1034
|
+
let loopGateCount = 0;
|
|
1035
|
+
let loopGateFailedCount = 0;
|
|
1036
|
+
let postLoopGateCount = 0;
|
|
1037
|
+
let postLoopGateFailedCount = 0;
|
|
1038
|
+
let memoryInferenceDurationMs = 0;
|
|
1039
|
+
let graphExtractionDurationMs = 0;
|
|
1040
|
+
let orphansPurged;
|
|
1041
|
+
let proposalsExpired;
|
|
1042
|
+
// Concatenated arrays.
|
|
1043
|
+
const allWarnings = [];
|
|
1044
|
+
let deadUrls;
|
|
1045
|
+
const finalActions = [];
|
|
1046
|
+
for (let cycleIndex = 0; cycleIndex < maxCycles; cycleIndex++) {
|
|
1047
|
+
// #616 budget gate: never start a NEW cycle once the run's wall-clock
|
|
1048
|
+
// budget is exhausted (or the run was aborted). Cycle 0 ALWAYS runs so
|
|
1049
|
+
// maxCycles:1 is byte-identical regardless of budget.
|
|
1050
|
+
if (cycleIndex > 0) {
|
|
1051
|
+
const remaining = budgetAbortController.signal.remainingBudgetMs;
|
|
1052
|
+
if (budgetAbortController.signal.aborted || (remaining !== undefined && remaining <= 0)) {
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
// Re-run ensureIndex + collectEligibleRefs + memory-cleanup recompute for
|
|
1057
|
+
// cycles 2+ (cycle 1 already ran them in the first try above). This makes
|
|
1058
|
+
// cycle N's gate-promoted proposals visible to this cycle's ref selection.
|
|
1059
|
+
if (cycleIndex > 0) {
|
|
1060
|
+
await runIndexAndCollect();
|
|
1061
|
+
// Re-emit the profile-filtered audit summary for this cycle's selection.
|
|
1062
|
+
if (profileFilteredRefs.length > 0) {
|
|
1063
|
+
appendEvent({
|
|
1064
|
+
eventType: "improve_skipped",
|
|
1065
|
+
ref: undefined,
|
|
1066
|
+
metadata: { reason: "profile_filtered_all_passes", count: profileFilteredRefs.length },
|
|
1067
|
+
}, eventsCtx);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// #607: acquire consolidate.lock for the preparation stage (consolidate,
|
|
1071
|
+
// ensureIndex, extract all write index.db). Released immediately after.
|
|
1072
|
+
const consolidateLPath = processLockPath(lockBaseDir, "consolidate");
|
|
1073
|
+
const consolidatePrepAcquired = tryAcquireProcessLock(consolidateLPath, PROCESS_LOCK_DEFS.consolidate.staleAfterMs, options.skipIfLocked, "consolidate") === "acquired";
|
|
1074
|
+
preparation = await runImprovePreparationStageImpl({
|
|
1075
|
+
scope,
|
|
1076
|
+
options,
|
|
1077
|
+
plannedRefs,
|
|
1078
|
+
memoryCleanupPlan,
|
|
1079
|
+
primaryStashDir,
|
|
1080
|
+
memorySummary,
|
|
1081
|
+
reindexFn,
|
|
1082
|
+
startMs,
|
|
1083
|
+
budgetMs,
|
|
1084
|
+
eventsCtx,
|
|
1085
|
+
initialCleanupWarnings: preEnsureCleanupWarnings,
|
|
1086
|
+
improveProfile,
|
|
1087
|
+
budgetSignal: budgetAbortController.signal,
|
|
1088
|
+
});
|
|
1089
|
+
if (consolidatePrepAcquired)
|
|
1090
|
+
releaseProcessLock(consolidateLPath);
|
|
1091
|
+
prepGateCount += preparation.gateAutoAcceptedCount;
|
|
1092
|
+
prepGateFailedCount += preparation.gateAutoAcceptFailedCount;
|
|
1093
|
+
// D6: pre-load all proposal_rejected events from the last 30 days once,
|
|
1094
|
+
// so the per-asset loop can use a Map lookup instead of N DB round trips.
|
|
1095
|
+
const REJECTED_PROPOSAL_WINDOW_MS = daysToMs(30);
|
|
1096
|
+
const rejectedProposalSince = new Date(Date.now() - REJECTED_PROPOSAL_WINDOW_MS).toISOString();
|
|
1097
|
+
const allRejectedProposalEvents = readEvents({ type: "proposal_rejected", since: rejectedProposalSince }).events;
|
|
1098
|
+
const rejectedProposalsByRef = new Map();
|
|
1099
|
+
for (const e of allRejectedProposalEvents) {
|
|
1100
|
+
if (e.ref && (!rejectedProposalsByRef.has(e.ref) || e.ts > (rejectedProposalsByRef.get(e.ref)?.ts ?? ""))) {
|
|
1101
|
+
rejectedProposalsByRef.set(e.ref, e);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// #607: acquire reflect-distill.lock for the loop stage (reflect + distill
|
|
1105
|
+
// both write proposals to state.db). Released immediately after.
|
|
1106
|
+
const reflectDistillLPath = processLockPath(lockBaseDir, "reflectDistill");
|
|
1107
|
+
const reflectDistillAcquired = tryAcquireProcessLock(reflectDistillLPath, PROCESS_LOCK_DEFS.reflectDistill.staleAfterMs, options.skipIfLocked, "reflect-distill") === "acquired";
|
|
1108
|
+
const loopResult = await runImproveLoopStageImpl({
|
|
1109
|
+
scope,
|
|
1110
|
+
options,
|
|
1111
|
+
primaryStashDir,
|
|
1112
|
+
reflectFn,
|
|
1113
|
+
distillFn,
|
|
1114
|
+
loopRefs: preparation.loopRefs,
|
|
1115
|
+
actions: preparation.actions,
|
|
1116
|
+
signalBearingSet: preparation.signalBearingSet,
|
|
1117
|
+
distillCooledRefs: preparation.distillCooledRefs,
|
|
1118
|
+
distillOnlyRefs: preparation.distillOnlyRefs,
|
|
1119
|
+
recentErrors: preparation.recentErrors,
|
|
1120
|
+
rejectedProposalsByRef,
|
|
1121
|
+
utilityMap: preparation.utilityMap,
|
|
1122
|
+
startMs,
|
|
1123
|
+
budgetMs,
|
|
1124
|
+
eventsCtx,
|
|
1125
|
+
improveProfile,
|
|
1126
|
+
budgetSignal: budgetAbortController.signal,
|
|
1127
|
+
});
|
|
1128
|
+
if (reflectDistillAcquired)
|
|
1129
|
+
releaseProcessLock(reflectDistillLPath);
|
|
1130
|
+
const loopGateCountThisCycle = loopResult.gateAutoAcceptedCount;
|
|
1131
|
+
reflectsWithErrorContext += loopResult.reflectsWithErrorContext;
|
|
1132
|
+
loopGateCount += loopResult.gateAutoAcceptedCount;
|
|
1133
|
+
loopGateFailedCount += loopResult.gateAutoAcceptFailedCount;
|
|
1134
|
+
memoryRefsForInference = loopResult.memoryRefsForInference;
|
|
1135
|
+
// #551: consolidation now runs in the preparation stage (before extract);
|
|
1136
|
+
// its result and run-flag are read from `preparation`, not the post-loop.
|
|
1137
|
+
consolidation = preparation.consolidation;
|
|
1138
|
+
// #607: acquire consolidate.lock for the post-loop stage (memoryInference +
|
|
1139
|
+
// graphExtraction both write index.db). Released immediately after.
|
|
1140
|
+
const consolidatePostLPath = processLockPath(lockBaseDir, "consolidate");
|
|
1141
|
+
const consolidatePostAcquired = tryAcquireProcessLock(consolidatePostLPath, PROCESS_LOCK_DEFS.consolidate.staleAfterMs, options.skipIfLocked, "consolidate") === "acquired";
|
|
1142
|
+
const postLoopResult = await runImprovePostLoopStageImpl({
|
|
1143
|
+
scope,
|
|
1144
|
+
options,
|
|
1145
|
+
primaryStashDir,
|
|
1146
|
+
actionableRefs: preparation.actionableRefs,
|
|
1147
|
+
appliedCleanup: preparation.appliedCleanup,
|
|
1148
|
+
cleanupWarnings: preparation.cleanupWarnings,
|
|
1149
|
+
memoryRefsForInference,
|
|
1150
|
+
reindexFn,
|
|
1151
|
+
eventsCtx,
|
|
1152
|
+
budgetSignal: budgetAbortController.signal,
|
|
1153
|
+
improveProfile,
|
|
1154
|
+
consolidationRan: preparation.consolidationRan,
|
|
1155
|
+
});
|
|
1156
|
+
if (consolidatePostAcquired)
|
|
1157
|
+
releaseProcessLock(consolidatePostLPath);
|
|
1158
|
+
const postLoopGateCountThisCycle = postLoopResult.gateAutoAcceptedCount;
|
|
1159
|
+
// Last-wins point-in-time objects.
|
|
1160
|
+
memoryInference = postLoopResult.memoryInference;
|
|
1161
|
+
graphExtraction = postLoopResult.graphExtraction;
|
|
1162
|
+
stalenessDetection = postLoopResult.stalenessDetection;
|
|
1163
|
+
recombination = postLoopResult.recombination;
|
|
1164
|
+
proceduralCompilation = postLoopResult.proceduralCompilation;
|
|
1165
|
+
// Summed counters/durations.
|
|
1166
|
+
postLoopGateCount += postLoopResult.gateAutoAcceptedCount;
|
|
1167
|
+
postLoopGateFailedCount += postLoopResult.gateAutoAcceptFailedCount;
|
|
1168
|
+
memoryInferenceDurationMs += postLoopResult.memoryInferenceDurationMs;
|
|
1169
|
+
graphExtractionDurationMs += postLoopResult.graphExtractionDurationMs;
|
|
1170
|
+
if (postLoopResult.orphansPurged !== undefined) {
|
|
1171
|
+
orphansPurged = (orphansPurged ?? 0) + postLoopResult.orphansPurged;
|
|
1172
|
+
}
|
|
1173
|
+
if (postLoopResult.proposalsExpired !== undefined) {
|
|
1174
|
+
proposalsExpired = (proposalsExpired ?? 0) + postLoopResult.proposalsExpired;
|
|
1014
1175
|
}
|
|
1176
|
+
// Concatenated arrays.
|
|
1177
|
+
allWarnings.push(...postLoopResult.allWarnings);
|
|
1178
|
+
if (postLoopResult.deadUrls !== undefined) {
|
|
1179
|
+
deadUrls = [...(deadUrls ?? []), ...postLoopResult.deadUrls];
|
|
1180
|
+
}
|
|
1181
|
+
const maintenanceActions = postLoopResult.maintenanceActions;
|
|
1182
|
+
if (maintenanceActions && maintenanceActions.length > 0) {
|
|
1183
|
+
finalActions.push(...preparation.actions, ...maintenanceActions);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
finalActions.push(...preparation.actions);
|
|
1187
|
+
}
|
|
1188
|
+
cyclesRun++;
|
|
1189
|
+
// #616 fixed-point stop: a cycle that produced ZERO gate-accepted proposals
|
|
1190
|
+
// (summed across prep + loop + post-loop) would feed cycle N+1 an identical
|
|
1191
|
+
// ref set, so end the loop here rather than spin a pointless next cycle.
|
|
1192
|
+
const gateAcceptedThisCycle = preparation.gateAutoAcceptedCount + loopGateCountThisCycle + postLoopGateCountThisCycle;
|
|
1193
|
+
if (gateAcceptedThisCycle === 0)
|
|
1194
|
+
break;
|
|
1015
1195
|
}
|
|
1016
|
-
// #607: acquire reflect-distill.lock for the loop stage (reflect + distill
|
|
1017
|
-
// both write proposals to state.db). Released immediately after.
|
|
1018
|
-
const reflectDistillLPath = processLockPath(lockBaseDir, "reflectDistill");
|
|
1019
|
-
const reflectDistillAcquired = tryAcquireProcessLock(reflectDistillLPath, PROCESS_LOCK_DEFS.reflectDistill.staleAfterMs, options.skipIfLocked, "reflect-distill") === "acquired";
|
|
1020
|
-
const { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount: loopGateCount, gateAutoAcceptFailedCount: loopGateFailedCount, } = await runImproveLoopStage({
|
|
1021
|
-
scope,
|
|
1022
|
-
options,
|
|
1023
|
-
primaryStashDir,
|
|
1024
|
-
reflectFn,
|
|
1025
|
-
distillFn,
|
|
1026
|
-
loopRefs: preparation.loopRefs,
|
|
1027
|
-
actions: preparation.actions,
|
|
1028
|
-
signalBearingSet: preparation.signalBearingSet,
|
|
1029
|
-
distillCooledRefs: preparation.distillCooledRefs,
|
|
1030
|
-
distillOnlyRefs: preparation.distillOnlyRefs,
|
|
1031
|
-
recentErrors: preparation.recentErrors,
|
|
1032
|
-
rejectedProposalsByRef,
|
|
1033
|
-
utilityMap: preparation.utilityMap,
|
|
1034
|
-
startMs,
|
|
1035
|
-
budgetMs,
|
|
1036
|
-
eventsCtx,
|
|
1037
|
-
improveProfile,
|
|
1038
|
-
});
|
|
1039
|
-
if (reflectDistillAcquired)
|
|
1040
|
-
releaseProcessLock(reflectDistillLPath);
|
|
1041
|
-
// #551: consolidation now runs in the preparation stage (before extract);
|
|
1042
|
-
// its result and run-flag are read from `preparation`, not the post-loop.
|
|
1043
|
-
const consolidation = preparation.consolidation;
|
|
1044
|
-
// #607: acquire consolidate.lock for the post-loop stage (memoryInference +
|
|
1045
|
-
// graphExtraction both write index.db). Released immediately after.
|
|
1046
|
-
const consolidatePostLPath = processLockPath(lockBaseDir, "consolidate");
|
|
1047
|
-
const consolidatePostAcquired = tryAcquireProcessLock(consolidatePostLPath, PROCESS_LOCK_DEFS.consolidate.staleAfterMs, options.skipIfLocked, "consolidate") === "acquired";
|
|
1048
|
-
const { allWarnings, deadUrls, memoryInference, graphExtraction, stalenessDetection, maintenanceActions, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, gateAutoAcceptedCount: postLoopGateCount, gateAutoAcceptFailedCount: postLoopGateFailedCount, recombination, proceduralCompilation, } = await runImprovePostLoopStage({
|
|
1049
|
-
scope,
|
|
1050
|
-
options,
|
|
1051
|
-
primaryStashDir,
|
|
1052
|
-
actionableRefs: preparation.actionableRefs,
|
|
1053
|
-
appliedCleanup: preparation.appliedCleanup,
|
|
1054
|
-
cleanupWarnings: preparation.cleanupWarnings,
|
|
1055
|
-
memoryRefsForInference,
|
|
1056
|
-
reindexFn,
|
|
1057
|
-
eventsCtx,
|
|
1058
|
-
budgetSignal: budgetAbortController.signal,
|
|
1059
|
-
improveProfile,
|
|
1060
|
-
consolidationRan: preparation.consolidationRan,
|
|
1061
|
-
});
|
|
1062
|
-
if (consolidatePostAcquired)
|
|
1063
|
-
releaseProcessLock(consolidatePostLPath);
|
|
1064
|
-
const finalActions = maintenanceActions && maintenanceActions.length > 0
|
|
1065
|
-
? [...preparation.actions, ...maintenanceActions]
|
|
1066
|
-
: preparation.actions;
|
|
1067
1196
|
const result = {
|
|
1068
1197
|
schemaVersion: 1,
|
|
1069
1198
|
ok: true,
|
|
@@ -1130,11 +1259,11 @@ export async function akmImprove(options = {}) {
|
|
|
1130
1259
|
reflectSkippedActions: finalActions.filter((a) => a.mode === "reflect-skipped").length,
|
|
1131
1260
|
reflectGuardRejectedActions: finalActions.filter((a) => a.mode === "reflect-guard-rejected").length,
|
|
1132
1261
|
...(() => {
|
|
1133
|
-
const t =
|
|
1262
|
+
const t = prepGateCount + loopGateCount + postLoopGateCount;
|
|
1134
1263
|
return t > 0 ? { gateAutoAcceptedCount: t } : {};
|
|
1135
1264
|
})(),
|
|
1136
1265
|
...(() => {
|
|
1137
|
-
const f =
|
|
1266
|
+
const f = prepGateFailedCount + loopGateFailedCount + postLoopGateFailedCount;
|
|
1138
1267
|
return f > 0 ? { gateAutoAcceptFailedCount: f } : {};
|
|
1139
1268
|
})(),
|
|
1140
1269
|
...(triageDrain
|
|
@@ -1148,6 +1277,9 @@ export async function akmImprove(options = {}) {
|
|
|
1148
1277
|
}
|
|
1149
1278
|
: {}),
|
|
1150
1279
|
...(preparation.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
|
|
1280
|
+
// #616 — report cycles run only when >1 so the default single-pass
|
|
1281
|
+
// serialized envelope stays byte-identical to pre-#616 (AC1).
|
|
1282
|
+
...(cyclesRun > 1 ? { cyclesRun } : {}),
|
|
1151
1283
|
...(options.runId !== undefined ? { runId: options.runId } : {}),
|
|
1152
1284
|
};
|
|
1153
1285
|
if (!result.dryRun)
|
|
@@ -3900,6 +4032,9 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
3900
4032
|
const graphEnabled = isProcessEnabled("index", "graph_extraction", config);
|
|
3901
4033
|
const graphExtractionDisabledByProfile = improveProfile?.processes?.graphExtraction?.enabled === false;
|
|
3902
4034
|
const graphExtractionFullScan = improveProfile?.processes?.graphExtraction?.fullScan === true;
|
|
4035
|
+
// #624 P2: optional incremental high-signal-first cap. Unset = process all
|
|
4036
|
+
// eligible (byte-identical to today; no ranking/slice).
|
|
4037
|
+
const graphExtractionTopN = improveProfile?.processes?.graphExtraction?.topN;
|
|
3903
4038
|
// Build the set of refs actually touched this run.
|
|
3904
4039
|
const touchedRefs = new Set();
|
|
3905
4040
|
for (const r of args.actionableRefs)
|
|
@@ -3961,7 +4096,7 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
3961
4096
|
db,
|
|
3962
4097
|
reEnrich: false,
|
|
3963
4098
|
onProgress: progressHandler,
|
|
3964
|
-
options: { candidatePaths },
|
|
4099
|
+
options: { candidatePaths, ...(graphExtractionTopN != null ? { topN: graphExtractionTopN } : {}) },
|
|
3965
4100
|
}));
|
|
3966
4101
|
graphExtractionDurationMs = Date.now() - extractionStart;
|
|
3967
4102
|
actions.push({ ref: "graph:_artifact", mode: "graph-extraction", result: graphExtraction });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { BaseLinter } from "./base-linter.js";
|
|
5
|
+
/** Recommended `category` values for facts (see docs/design/fact-asset-type.md). */
|
|
6
|
+
const KNOWN_CATEGORIES = new Set(["personal", "team", "project", "convention", "meta"]);
|
|
7
|
+
/**
|
|
8
|
+
* Linter for `facts/` assets.
|
|
9
|
+
*
|
|
10
|
+
* Extra check beyond base:
|
|
11
|
+
* - `missing-category`: a fact without a non-empty `category` frontmatter
|
|
12
|
+
* key. Category scopes the fact (personal/team/project/convention/meta)
|
|
13
|
+
* and drives how it is surfaced/injected, so it is expected. Reported as a
|
|
14
|
+
* non-fixable warning — we cannot infer the right scope automatically.
|
|
15
|
+
*/
|
|
16
|
+
export class FactLinter extends BaseLinter {
|
|
17
|
+
types = ["facts"];
|
|
18
|
+
lint(ctx) {
|
|
19
|
+
const issues = this.runBaseChecks(ctx);
|
|
20
|
+
const category = typeof ctx.data.category === "string" ? ctx.data.category.trim() : "";
|
|
21
|
+
if (!category) {
|
|
22
|
+
issues.push({
|
|
23
|
+
file: ctx.relPath,
|
|
24
|
+
issue: "missing-category",
|
|
25
|
+
detail: "fact is missing a `category` (personal|team|project|convention|meta)",
|
|
26
|
+
fixed: false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (!KNOWN_CATEGORIES.has(category)) {
|
|
30
|
+
issues.push({
|
|
31
|
+
file: ctx.relPath,
|
|
32
|
+
issue: "missing-category",
|
|
33
|
+
detail: `unrecognized category "${category}" (expected one of: ${[...KNOWN_CATEGORIES].join(", ")})`,
|
|
34
|
+
fixed: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { AgentLinter } from "./agent-linter.js";
|
|
5
5
|
import { CommandLinter } from "./command-linter.js";
|
|
6
6
|
import { DefaultLinter } from "./default-linter.js";
|
|
7
|
+
import { FactLinter } from "./fact-linter.js";
|
|
7
8
|
import { KnowledgeLinter } from "./knowledge-linter.js";
|
|
8
9
|
import { MemoryLinter } from "./memory-linter.js";
|
|
9
10
|
import { SkillLinter } from "./skill-linter.js";
|
|
@@ -18,6 +19,7 @@ const LINTERS = [
|
|
|
18
19
|
new KnowledgeLinter(),
|
|
19
20
|
new SkillLinter(),
|
|
20
21
|
new TaskLinter(),
|
|
22
|
+
new FactLinter(),
|
|
21
23
|
];
|
|
22
24
|
// Single shared DefaultLinter instance — used both as the explicit "lessons"
|
|
23
25
|
// handler and as the fallback for any unrecognised asset type.
|