akm-cli 0.9.0-beta.1 → 0.9.0-beta.10
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 +469 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +732 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +47 -5
- package/dist/commands/config-cli.js +0 -10
- package/dist/commands/feedback-cli.js +42 -37
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +666 -0
- package/dist/commands/health.js +186 -13
- package/dist/commands/improve/consolidate.js +39 -6
- package/dist/commands/improve/distill.js +26 -5
- package/dist/commands/improve/extract-prompt.js +1 -1
- package/dist/commands/improve/extract.js +52 -8
- package/dist/commands/improve/improve-auto-accept.js +33 -1
- package/dist/commands/improve/improve-cli.js +7 -0
- package/dist/commands/improve/improve-profiles.js +4 -0
- package/dist/commands/improve/improve.js +874 -447
- package/dist/commands/improve/proactive-maintenance.js +113 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +31 -0
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +17 -1
- package/dist/commands/proposal/validators/proposals.js +365 -329
- package/dist/commands/read/curate.js +17 -0
- package/dist/commands/remember.js +6 -2
- package/dist/commands/sources/stash-cli.js +10 -2
- package/dist/commands/tasks/tasks.js +32 -8
- package/dist/core/config/config-schema.js +30 -0
- package/dist/core/logs-db.js +304 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/state-db.js +152 -14
- package/dist/indexer/db/db.js +99 -13
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +61 -22
- package/dist/integrations/harnesses/claude/session-log.js +17 -5
- package/dist/llm/client.js +38 -4
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +69 -1
- package/dist/scripts/migrate-storage.js +153 -25
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +21 -2
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/tasks/backends/cron.js +46 -9
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +4 -0
- package/package.json +3 -2
- package/dist/commands/config-edit.js +0 -344
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
/** One day in milliseconds. */
|
|
5
|
+
const DAY_MS = 86_400_000;
|
|
6
|
+
/**
|
|
7
|
+
* Importance multipliers by asset type. Higher = more worth maintaining. These
|
|
8
|
+
* are the design defaults; callers may override any subset via config.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_IMPORTANCE_WEIGHTS = Object.freeze({
|
|
11
|
+
skill: 1.5,
|
|
12
|
+
agent: 1.5,
|
|
13
|
+
command: 1.3,
|
|
14
|
+
workflow: 1.3,
|
|
15
|
+
lesson: 1.2,
|
|
16
|
+
knowledge: 1.0,
|
|
17
|
+
script: 0.9,
|
|
18
|
+
memory: 0.7,
|
|
19
|
+
});
|
|
20
|
+
/** Default staleness gate: an asset is due when last reflected > this many days ago (or never). */
|
|
21
|
+
export const DEFAULT_DUE_DAYS = 30;
|
|
22
|
+
/** Default bound on how many assets the selector surfaces per run. */
|
|
23
|
+
export const DEFAULT_MAX_PER_RUN = 25;
|
|
24
|
+
/**
|
|
25
|
+
* Half-life (days) for the recency-of-use decay term. An asset used today
|
|
26
|
+
* contributes a full recency multiplier; one unused for one half-life
|
|
27
|
+
* contributes half. Mirrors the validated prototype (21 days).
|
|
28
|
+
*/
|
|
29
|
+
const RECENCY_HALFLIFE_DAYS = 21;
|
|
30
|
+
/** Lower bound on size used in the cost denominator so tiny files don't divide by ~0. */
|
|
31
|
+
const SIZE_FLOOR_BYTES = 200;
|
|
32
|
+
/** Parse the bare asset type out of a `type:name` ref. Returns "" when unparseable. */
|
|
33
|
+
function refType(ref) {
|
|
34
|
+
const i = ref.indexOf(":");
|
|
35
|
+
return i > 0 ? ref.slice(0, i) : "";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Score and select due assets for proactive maintenance.
|
|
39
|
+
*
|
|
40
|
+
* Priority formula (mirrors the validated prototype):
|
|
41
|
+
*
|
|
42
|
+
* priority = (importance × log(1 + retrievalFreq) × (0.1 + 0.5^(useAgeDays/21)))
|
|
43
|
+
* / log10(max(size, 200))
|
|
44
|
+
*
|
|
45
|
+
* DUE gate: an asset is eligible only if it was never reflected OR last
|
|
46
|
+
* reflected/distilled more than `dueDays` ago. The same gate doubles as the
|
|
47
|
+
* ROTATION cooldown — a freshly-reflected asset is excluded until it ages back
|
|
48
|
+
* past `dueDays`, so successive runs rotate through the due pool rather than
|
|
49
|
+
* re-selecting the same heads. Non-due assets never enter the selection.
|
|
50
|
+
*/
|
|
51
|
+
export function selectProactiveMaintenanceRefs(params) {
|
|
52
|
+
const now = params.now ?? Date.now();
|
|
53
|
+
const dueDays = params.dueDays ?? DEFAULT_DUE_DAYS;
|
|
54
|
+
const maxPerRun = params.maxPerRun ?? DEFAULT_MAX_PER_RUN;
|
|
55
|
+
const weights = { ...DEFAULT_IMPORTANCE_WEIGHTS, ...(params.importanceWeights ?? {}) };
|
|
56
|
+
const scored = [];
|
|
57
|
+
for (const candidate of params.candidates) {
|
|
58
|
+
const ref = candidate.ref;
|
|
59
|
+
const type = refType(ref);
|
|
60
|
+
// Staleness from the most recent of reflect/distill — either one touching
|
|
61
|
+
// the asset resets its maintenance clock.
|
|
62
|
+
const reflectIso = params.lastReflectTs.get(ref);
|
|
63
|
+
const distillIso = params.lastDistillTs.get(ref);
|
|
64
|
+
let lastTouchMs = 0;
|
|
65
|
+
if (reflectIso)
|
|
66
|
+
lastTouchMs = Math.max(lastTouchMs, Date.parse(reflectIso) || 0);
|
|
67
|
+
if (distillIso)
|
|
68
|
+
lastTouchMs = Math.max(lastTouchMs, Date.parse(distillIso) || 0);
|
|
69
|
+
const neverReflected = lastTouchMs === 0;
|
|
70
|
+
const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
|
|
71
|
+
// DUE / rotation gate.
|
|
72
|
+
const due = neverReflected || staleDays > dueDays;
|
|
73
|
+
// Retrieval frequency + recency decay.
|
|
74
|
+
const retrievalFreq = params.retrievalCounts.get(ref) ?? 0;
|
|
75
|
+
const lastUse = params.lastUseMs?.get(ref) ?? 0;
|
|
76
|
+
const useAgeDays = lastUse > 0 ? (now - lastUse) / DAY_MS : 9999;
|
|
77
|
+
const recencyDecay = 0.1 + 0.5 ** (useAgeDays / RECENCY_HALFLIFE_DAYS);
|
|
78
|
+
// Size proxy (cost): larger assets are slightly deprioritized, but only by
|
|
79
|
+
// log10 so a big-but-hot asset is never starved.
|
|
80
|
+
let sizeBytes = params.sizeBytesOf?.(candidate) ?? 0;
|
|
81
|
+
if (!sizeBytes || sizeBytes < 0)
|
|
82
|
+
sizeBytes = SIZE_FLOOR_BYTES;
|
|
83
|
+
const sizeProxy = Math.max(SIZE_FLOOR_BYTES, sizeBytes);
|
|
84
|
+
const importance = weights[type] ?? 1.0;
|
|
85
|
+
const priority = (importance * Math.log(1 + retrievalFreq) * recencyDecay) / Math.log10(sizeProxy);
|
|
86
|
+
scored.push({
|
|
87
|
+
ref: candidate,
|
|
88
|
+
type,
|
|
89
|
+
staleDays,
|
|
90
|
+
neverReflected,
|
|
91
|
+
retrievalFreq,
|
|
92
|
+
recencyDecay,
|
|
93
|
+
sizeBytes,
|
|
94
|
+
importance,
|
|
95
|
+
priority,
|
|
96
|
+
due,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const dueScored = scored.filter((s) => s.due);
|
|
100
|
+
const dueTotal = dueScored.length;
|
|
101
|
+
const neverReflected = dueScored.filter((s) => s.neverReflected).length;
|
|
102
|
+
// Rank due assets by composite priority (desc). Ties broken by staleness
|
|
103
|
+
// (older first) then ref string for deterministic ordering.
|
|
104
|
+
const ranked = dueScored.slice().sort((a, b) => {
|
|
105
|
+
if (b.priority !== a.priority)
|
|
106
|
+
return b.priority - a.priority;
|
|
107
|
+
if (b.staleDays !== a.staleDays)
|
|
108
|
+
return b.staleDays - a.staleDays;
|
|
109
|
+
return a.ref.ref < b.ref.ref ? -1 : a.ref.ref > b.ref.ref ? 1 : 0;
|
|
110
|
+
});
|
|
111
|
+
const selected = ranked.slice(0, Math.max(0, maxPerRun)).map((s) => s.ref);
|
|
112
|
+
return { selected, dueTotal, neverReflected, scored };
|
|
113
|
+
}
|
|
Binary file
|
|
@@ -46,6 +46,7 @@ import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigF
|
|
|
46
46
|
import { checkReflectSize } from "../proposal/validators/proposal-quality-validators.js";
|
|
47
47
|
import { createProposal, isProposalSkipped, listProposals, } from "../proposal/validators/proposals.js";
|
|
48
48
|
import { deriveLessonRef, runLessonQualityJudge } from "./distill.js";
|
|
49
|
+
import { classifyReflectChange } from "./reflect-noise.js";
|
|
49
50
|
const MAX_FEEDBACK_LINES = 10;
|
|
50
51
|
const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
51
52
|
/**
|
|
@@ -584,6 +585,9 @@ export async function akmReflect(options = {}) {
|
|
|
584
585
|
metadata: {
|
|
585
586
|
...(options.task ? { task: options.task } : {}),
|
|
586
587
|
...(options.profile ? { profile: options.profile } : {}),
|
|
588
|
+
// Attribution tagging: stamp the eligibility lane so reflect_invoked can be
|
|
589
|
+
// sliced by lane downstream. See EligibilitySource.
|
|
590
|
+
...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
|
|
587
591
|
},
|
|
588
592
|
});
|
|
589
593
|
// Fix #3 (observability 0.8.0): every failure path below MUST emit
|
|
@@ -1138,6 +1142,30 @@ export async function akmReflect(options = {}) {
|
|
|
1138
1142
|
content: sanitizeOutcome.content,
|
|
1139
1143
|
...(sanitizeOutcome.frontmatter ? { frontmatter: sanitizeOutcome.frontmatter } : {}),
|
|
1140
1144
|
};
|
|
1145
|
+
// 7c. Noise gate (#580): never queue a proposal whose sanitized content is
|
|
1146
|
+
// identical to the current asset (empty diff) or differs only cosmetically
|
|
1147
|
+
// (whitespace reflow, code-fence language hints, YAML scalar re-folding).
|
|
1148
|
+
// Pure deterministic text comparison — see `reflect-noise.ts`. Runs before
|
|
1149
|
+
// the draftMode branch so self-consistency sampling never votes a no-op
|
|
1150
|
+
// candidate into the queue either. Skipped when there is no source asset
|
|
1151
|
+
// (new-asset proposals have nothing to diff against).
|
|
1152
|
+
if (assetContent !== undefined) {
|
|
1153
|
+
const changeKind = classifyReflectChange(assetContent, payload.content);
|
|
1154
|
+
if (changeKind !== "substantive") {
|
|
1155
|
+
const subreason = changeKind === "noop" ? "reflect_skipped_noop" : "reflect_skipped_cosmetic";
|
|
1156
|
+
emitReflectFailed("no_change", subreason, options.ref, { changeKind });
|
|
1157
|
+
return {
|
|
1158
|
+
schemaVersion: 1,
|
|
1159
|
+
ok: false,
|
|
1160
|
+
reason: "no_change",
|
|
1161
|
+
error: changeKind === "noop"
|
|
1162
|
+
? `Reflect skipped: proposed content for ${payload.ref} is identical to the current asset (empty diff); no proposal created.`
|
|
1163
|
+
: `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
|
|
1164
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1165
|
+
exitCode: result.exitCode,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1141
1169
|
// 8. Create the proposal. The proposal queue is the ONLY thing reflect
|
|
1142
1170
|
// writes — promotion to a real asset is gated by `akm proposal accept`.
|
|
1143
1171
|
//
|
|
@@ -1203,6 +1231,9 @@ export async function akmReflect(options = {}) {
|
|
|
1203
1231
|
// `parseAgentProposalPayload` already clamps to [0, 1] and drops non-
|
|
1204
1232
|
// finite values; `createProposal` runs its own sanitizer as a safety net.
|
|
1205
1233
|
...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
|
|
1234
|
+
// Attribution tagging: persist the eligibility lane on the proposal so it
|
|
1235
|
+
// survives to accept/reject/revert time even across runs. See EligibilitySource.
|
|
1236
|
+
...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
|
|
1206
1237
|
};
|
|
1207
1238
|
const proposalResult = createProposal(stash, createInput, options.ctx);
|
|
1208
1239
|
if (isProposalSkipped(proposalResult)) {
|
|
@@ -46,7 +46,7 @@ import { runAgent } from "../../integrations/agent/index.js";
|
|
|
46
46
|
import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
|
|
47
47
|
import { chatCompletion, stripJsonFences } from "../../llm/client.js";
|
|
48
48
|
import { akmProposalAccept, akmProposalReject } from "./proposal.js";
|
|
49
|
-
import { listProposals } from "./validators/proposals.js";
|
|
49
|
+
import { listProposals, recordGateDecision } from "./validators/proposals.js";
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
// Content helpers
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
@@ -78,7 +78,7 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
|
|
|
78
78
|
const content = proposal.payload.content ?? "";
|
|
79
79
|
// Empty / near-empty diffs reject first (the reject-empty floor).
|
|
80
80
|
if (policy.rejectEmpty && isEmptyDiff(proposal)) {
|
|
81
|
-
return { verdict: "reject", reason: "empty diff" };
|
|
81
|
+
return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
|
|
82
82
|
}
|
|
83
83
|
const rule = policy.accept.find((r) => r.generator === proposal.source);
|
|
84
84
|
if (rule) {
|
|
@@ -87,16 +87,25 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
|
|
|
87
87
|
// Per-rule and global diff bounds defer large accepts (no silent rewrites).
|
|
88
88
|
const effectiveMax = Math.min(rule.maxDiffLines ?? Number.POSITIVE_INFINITY, maxDiffLines ?? Number.POSITIVE_INFINITY);
|
|
89
89
|
if (lines > effectiveMax) {
|
|
90
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
verdict: "defer",
|
|
92
|
+
reason: "mid-band",
|
|
93
|
+
gate: { reason: "max-diff-lines", measured: lines, thresholds: { maxDiffLines: effectiveMax } },
|
|
94
|
+
};
|
|
91
95
|
}
|
|
92
96
|
if (rule.minContentLines !== undefined && body < rule.minContentLines) {
|
|
93
97
|
// Too little content to confidently auto-accept — leave for judgment.
|
|
94
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
verdict: "defer",
|
|
100
|
+
reason: "mid-band",
|
|
101
|
+
gate: { reason: "min-content-lines", measured: body, thresholds: { minContentLines: rule.minContentLines } },
|
|
102
|
+
};
|
|
95
103
|
}
|
|
96
|
-
return { verdict: "accept" };
|
|
104
|
+
return { verdict: "accept", gate: { reason: "policy-accept" } };
|
|
97
105
|
}
|
|
98
106
|
if (policy.defer.includes(proposal.source)) {
|
|
99
|
-
|
|
107
|
+
const reason = deferReasonForSource(proposal.source);
|
|
108
|
+
return { verdict: "defer", reason, gate: { reason } };
|
|
100
109
|
}
|
|
101
110
|
// No matching rule — leave pending, untouched.
|
|
102
111
|
return null;
|
|
@@ -347,10 +356,31 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
347
356
|
// First, classify every proposal deterministically.
|
|
348
357
|
const acceptIds = [];
|
|
349
358
|
const rejectTargets = [];
|
|
359
|
+
const gateLabel = `triage:${opts.policy.name}`;
|
|
360
|
+
// Items deferred purely because they need a judge (no threshold-based reason)
|
|
361
|
+
// — these are re-stamped `no-judge-configured` when no runner resolves them.
|
|
362
|
+
const needsJudge = new Set();
|
|
350
363
|
for (const proposal of pending) {
|
|
351
364
|
const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
|
|
352
365
|
if (decision === null)
|
|
353
366
|
continue;
|
|
367
|
+
// #577: stamp the gate's verdict onto the proposal so `akm proposal show`
|
|
368
|
+
// can explain WHY it landed here. A dry-run performs zero writes, so it
|
|
369
|
+
// records nothing.
|
|
370
|
+
const outcome = decision.verdict === "accept" ? "auto-accepted" : decision.verdict === "reject" ? "auto-rejected" : "deferred";
|
|
371
|
+
stampGateDecision(opts, proposal.id, {
|
|
372
|
+
outcome,
|
|
373
|
+
reason: decision.gate.reason,
|
|
374
|
+
...(decision.gate.measured !== undefined ? { measured: decision.gate.measured } : {}),
|
|
375
|
+
...(decision.gate.thresholds ? { thresholds: decision.gate.thresholds } : {}),
|
|
376
|
+
gate: gateLabel,
|
|
377
|
+
});
|
|
378
|
+
// A defer with no threshold (mid-band / possible-dup from the defer list) is
|
|
379
|
+
// pending only because it needs adjudication — re-stampable to
|
|
380
|
+
// `no-judge-configured`. A band-based defer keeps its specific reason.
|
|
381
|
+
if (decision.verdict === "defer" && !decision.gate.thresholds) {
|
|
382
|
+
needsJudge.add(proposal.id);
|
|
383
|
+
}
|
|
354
384
|
if (decision.verdict === "accept") {
|
|
355
385
|
acceptIds.push(proposal.id);
|
|
356
386
|
}
|
|
@@ -434,14 +464,51 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
434
464
|
if (tier.skippedByCap.length > 0) {
|
|
435
465
|
info(`[triage] accept ceiling reached in judgment tier: ${tier.skippedByCap.length} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
|
|
436
466
|
}
|
|
467
|
+
// #577: re-stamp the gate decision for items the judgment tier resolved so
|
|
468
|
+
// `akm proposal show` reflects the judge's verdict, not the earlier
|
|
469
|
+
// deterministic defer.
|
|
470
|
+
for (const id of tier.promoted) {
|
|
471
|
+
stampGateDecision(opts, id, { outcome: "auto-accepted", reason: "judgment-accept", gate: gateLabel });
|
|
472
|
+
}
|
|
473
|
+
for (const id of tier.rejected) {
|
|
474
|
+
stampGateDecision(opts, id, { outcome: "auto-rejected", reason: "judgment-reject", gate: gateLabel });
|
|
475
|
+
}
|
|
437
476
|
// Replace the deferred list with only the items the judgment tier could NOT
|
|
438
477
|
// resolve (verdict "defer", parse failure, or runner error). Staged
|
|
439
478
|
// queue-mode accepts are RESOLVED and tracked in result.staged instead.
|
|
440
479
|
result.deferred = tier.stillDeferred;
|
|
441
480
|
}
|
|
481
|
+
else if (result.deferred.length > 0) {
|
|
482
|
+
// #577: no judgment runner configured — items deferred *because they need a
|
|
483
|
+
// judge* (mid-band / possible-dup, no threshold reason) stay pending solely
|
|
484
|
+
// for lack of one. Re-stamp those as `no-judge-configured` so the operator
|
|
485
|
+
// sees a per-proposal reason instead of inferring it from the run-level
|
|
486
|
+
// triage_deferred aggregate. Band-deferred items keep their specific reason
|
|
487
|
+
// (e.g. `max-diff-lines`), which is more actionable than "no judge".
|
|
488
|
+
for (const item of result.deferred) {
|
|
489
|
+
if (needsJudge.has(item.id)) {
|
|
490
|
+
stampGateDecision(opts, item.id, { outcome: "deferred", reason: "no-judge-configured", gate: gateLabel });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
442
494
|
emitDrainEvents(opts, result);
|
|
443
495
|
return result;
|
|
444
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Persist a gate decision onto a proposal, honouring the dry-run contract
|
|
499
|
+
* (a dry run performs zero writes, so it records nothing) and never letting a
|
|
500
|
+
* persistence failure abort the drain (#577). Best-effort by design.
|
|
501
|
+
*/
|
|
502
|
+
function stampGateDecision(opts, id, decision) {
|
|
503
|
+
if (opts.dryRun)
|
|
504
|
+
return;
|
|
505
|
+
try {
|
|
506
|
+
recordGateDecision(opts.stashDir, id, decision);
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
warn(`[triage] failed to record gate decision for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
445
512
|
// ---------------------------------------------------------------------------
|
|
446
513
|
// Events
|
|
447
514
|
// ---------------------------------------------------------------------------
|
|
@@ -16,6 +16,8 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
16
16
|
import { loadConfig } from "../../core/config/config.js";
|
|
17
17
|
import { UsageError } from "../../core/errors.js";
|
|
18
18
|
import { resolveTriageJudgmentRunner } from "../../integrations/agent/runner.js";
|
|
19
|
+
import { installLlmUsagePersistenceIfAbsent } from "../../llm/usage-persist.js";
|
|
20
|
+
import { withLlmStage } from "../../llm/usage-telemetry.js";
|
|
19
21
|
import { resolveImproveProfile } from "../improve/improve-profiles.js";
|
|
20
22
|
import { drainProposals } from "./drain.js";
|
|
21
23
|
import { resolveDrainPolicy } from "./drain-policies.js";
|
|
@@ -407,16 +409,26 @@ const proposalDrainCommand = defineJsonCommand({
|
|
|
407
409
|
// nothing is configured → the engine leaves deferred items unresolved and
|
|
408
410
|
// emits triage_deferred.
|
|
409
411
|
const judgment = args.judgment === true ? resolveTriageJudgmentRunner(triageConfig?.judgment, cfg) : null;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
412
|
+
// #576: persist + attribute per-call LLM usage for the standalone drain
|
|
413
|
+
// path. `IfAbsent` keeps an enclosing `akm improve` sink in charge when
|
|
414
|
+
// drain runs as a sub-step; the disposer clears only a sink we installed.
|
|
415
|
+
const disposeDrainUsageSink = installLlmUsagePersistenceIfAbsent();
|
|
416
|
+
let result;
|
|
417
|
+
try {
|
|
418
|
+
result = await withLlmStage("drain", () => drainProposals({
|
|
419
|
+
stashDir,
|
|
420
|
+
policy,
|
|
421
|
+
applyMode,
|
|
422
|
+
maxAccepts,
|
|
423
|
+
dryRun,
|
|
424
|
+
...(maxDiffLines !== undefined ? { maxDiffLines } : {}),
|
|
425
|
+
...(excludeIds ? { excludeIds } : {}),
|
|
426
|
+
judgment,
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
disposeDrainUsageSink();
|
|
431
|
+
}
|
|
420
432
|
output("proposal-drain", {
|
|
421
433
|
schemaVersion: 1,
|
|
422
434
|
ok: true,
|
|
@@ -22,6 +22,17 @@ function resolveStash(stashDir) {
|
|
|
22
22
|
return stashDir;
|
|
23
23
|
return resolveStashDir();
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Thin in-process read of the pending proposal queue, used by the health HTML
|
|
27
|
+
* report builder (#582) so it never shells out to `akm proposal list`.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately narrow (one optional arg, returns the storage-layer rows) so
|
|
30
|
+
* the parallel proposal-storage-to-SQLite consolidation only has to swap this
|
|
31
|
+
* one function's body.
|
|
32
|
+
*/
|
|
33
|
+
export function listPendingProposals(stashDir) {
|
|
34
|
+
return listProposals(resolveStash(stashDir), { status: "pending" });
|
|
35
|
+
}
|
|
25
36
|
export function akmProposalList(options = {}) {
|
|
26
37
|
const stash = resolveStash(options.stashDir);
|
|
27
38
|
// `--status accepted|rejected|reverted` implies archive-inclusion since the
|
|
@@ -64,6 +75,11 @@ export async function akmProposalAccept(options) {
|
|
|
64
75
|
source: result.proposal.source,
|
|
65
76
|
...(result.proposal.sourceRun !== undefined ? { sourceRun: result.proposal.sourceRun } : {}),
|
|
66
77
|
assetPath: result.assetPath,
|
|
78
|
+
// Attribution tagging: carry the eligibility lane from the proposal record
|
|
79
|
+
// onto the promoted event so accept outcomes can be sliced by lane.
|
|
80
|
+
...(result.proposal.eligibilitySource !== undefined
|
|
81
|
+
? { eligibilitySource: result.proposal.eligibilitySource }
|
|
82
|
+
: {}),
|
|
67
83
|
},
|
|
68
84
|
});
|
|
69
85
|
return {
|
|
@@ -141,7 +157,7 @@ export function akmProposalCreate(options) {
|
|
|
141
157
|
* (raised by `resolveProposalId` / `getProposal`).
|
|
142
158
|
* - Proposal is not `status === "accepted"` → `UsageError("INVALID_FLAG_VALUE")`
|
|
143
159
|
* with message `"only accepted proposals can be reverted ..."`.
|
|
144
|
-
* - No
|
|
160
|
+
* - No backup content on the record (new-asset proposals capture none) →
|
|
145
161
|
* `UsageError` with message `"no backup available for this proposal ..."`.
|
|
146
162
|
*
|
|
147
163
|
* On success, emits a `proposal_reverted` event for observability, mirroring
|