akm-cli 0.9.20 → 0.9.22
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 +113 -1
- package/dist/assets/hints/cli-hints-full.md +9 -4
- package/dist/assets/hints/cli-hints-short.md +7 -4
- package/dist/assets/improve-strategies/default.json +2 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -1
- package/dist/assets/stash-skeleton/README.md +5 -1
- package/dist/commands/feedback-cli.js +14 -6
- package/dist/commands/health/improve-metrics.js +1 -1
- package/dist/commands/improve/distill.js +3 -1
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve.js +3 -27
- package/dist/commands/improve/preparation.js +35 -29
- package/dist/commands/improve/proactive-maintenance.js +2 -9
- package/dist/commands/improve/reflect.js +3 -2
- package/dist/commands/improve/stage.js +36 -31
- package/dist/commands/proposal/repository.js +14 -11
- package/dist/commands/proposal/validators/proposals.js +8 -2
- package/dist/commands/remember.js +7 -1
- package/dist/core/improve-result.js +4 -1
- package/dist/core/write-source.js +28 -5
- package/dist/scripts/akm-migrate-node.js +4 -4
- package/dist/scripts/akm-migrate.js +4 -4
- package/dist/sources/providers/git-stash.js +16 -9
- package/dist/storage/repositories/improve-runs-repository.js +1 -3
- package/docs/reference/cli.md +41 -15
- package/package.json +1 -1
|
@@ -120,29 +120,31 @@ export function rejectedProposalContext(stash, ref, ctx, eventsCtx) {
|
|
|
120
120
|
}
|
|
121
121
|
// ── Mint ─────────────────────────────────────────────────────────────────────
|
|
122
122
|
/**
|
|
123
|
-
* Create a stage's proposal. `judged` stamps a `staged`
|
|
124
|
-
* judged content's hash
|
|
125
|
-
*
|
|
126
|
-
* improve ledger).
|
|
123
|
+
* Create a stage's proposal. `judged` (the passing verdict) stamps a `staged`
|
|
124
|
+
* gate decision with the judged content's hash and the judge's scores and reason
|
|
125
|
+
* (the triage drain accepts it while the content still matches); `review`
|
|
126
|
+
* leaves it `deferred` for a human (`review_needed` in the improve ledger).
|
|
127
127
|
*/
|
|
128
128
|
export function mintProposal(stash, proposalsCtx, input, verdict = {}) {
|
|
129
129
|
const proposal = createProposal(stash, input, proposalsCtx);
|
|
130
130
|
if (verdict.review) {
|
|
131
131
|
return recordGateDecision(stash, proposal.id, { outcome: "deferred", ...verdict.review }, proposalsCtx) ?? proposal;
|
|
132
132
|
}
|
|
133
|
-
return verdict.judged ? stageJudgedProposal(stash, proposal, proposalsCtx) : proposal;
|
|
133
|
+
return verdict.judged ? stageJudgedProposal(stash, proposal, verdict.judged, proposalsCtx) : proposal;
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
136
|
* Stamp a proposal the quality judge passed. Best-effort: a failed stamp only
|
|
137
137
|
* means the triage drain judges it again.
|
|
138
138
|
*/
|
|
139
|
-
export function stageJudgedProposal(stash, proposal, proposalsCtx) {
|
|
139
|
+
export function stageJudgedProposal(stash, proposal, judged, proposalsCtx) {
|
|
140
140
|
try {
|
|
141
141
|
return (recordGateDecision(stash, proposal.id, {
|
|
142
142
|
outcome: "staged",
|
|
143
143
|
reason: "quality-judge",
|
|
144
144
|
gate: "quality-gate",
|
|
145
145
|
contentHash: proposalContentHash(proposal),
|
|
146
|
+
...(judged?.criteria ? { scores: judged.criteria } : {}),
|
|
147
|
+
...(judged ? { judgeReason: judged.reason } : {}),
|
|
146
148
|
}, proposalsCtx) ?? proposal);
|
|
147
149
|
}
|
|
148
150
|
catch (error) {
|
|
@@ -196,17 +198,15 @@ function buildChangedRegion(sourceContent, candidateContent) {
|
|
|
196
198
|
const added = candidate.slice(prefix, candidate.length - suffix).join("\n");
|
|
197
199
|
return boundedDocument(`Removed or replaced:\n${removed || "(none)"}\n\nAdded or replacement:\n${added || "(none)"}`);
|
|
198
200
|
}
|
|
199
|
-
/** Judge prompt for an in-place revision
|
|
201
|
+
/** Judge prompt for an in-place revision. */
|
|
200
202
|
export function buildReflectJudgePrompt(candidateContent, sourceContent, feedback) {
|
|
201
203
|
return [
|
|
202
204
|
"You are evaluating a proposed revision to an existing akm asset.",
|
|
203
205
|
"",
|
|
204
206
|
"Score this revision on each criterion from 1 (poor) to 5 (excellent):",
|
|
205
|
-
"1.
|
|
206
|
-
"2. PRESERVATION: Does it
|
|
207
|
-
"3. QUALITY: Is
|
|
208
|
-
"",
|
|
209
|
-
"Overlap with the source is expected and must not lower the score by itself; this is an in-place revision, not a new lesson.",
|
|
207
|
+
"1. NEED: Does the revision fix a concrete problem in the source? Concrete problems are: something the feedback reports as wrong or missing; a factual error; or broken, garbled, truncated or missing text, including frontmatter fields such as description or when_to_use. Score 4-5 when it fixes one, even a small one. Score 1-2 when the source was already correct and the revision only rewords, restates, reformats, or adds headings, an introduction or a table of contents.",
|
|
208
|
+
"2. PRESERVATION: Does it keep every concrete fact, identifier, command, path, number and example from the source, without truncation?",
|
|
209
|
+
"3. QUALITY: Is it coherent and accurate, with no claims, steps or details that the source or the feedback does not support?",
|
|
210
210
|
"",
|
|
211
211
|
"Feedback:",
|
|
212
212
|
"```",
|
|
@@ -228,13 +228,13 @@ export function buildReflectJudgePrompt(candidateContent, sourceContent, feedbac
|
|
|
228
228
|
buildChangedRegion(sourceContent, candidateContent),
|
|
229
229
|
"```",
|
|
230
230
|
"",
|
|
231
|
-
'Return ONLY valid JSON, no prose: {"scores": {"
|
|
231
|
+
'Return ONLY valid JSON, no prose: {"scores": {"need": <1-5 integer>, "preservation": <1-5 integer>, "quality": <1-5 integer>}, "reason": "<one sentence>"}',
|
|
232
232
|
].join("\n");
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
235
235
|
* `grounding` is scored with the other lesson criteria but left out of their
|
|
236
236
|
* mean: a lesson about a different subject than its source reads as novel and
|
|
237
|
-
* non-redundant, so
|
|
237
|
+
* non-redundant, so they would pass it (or, in the review band, mint it as
|
|
238
238
|
* a pending proposal). The rubric reserves 1-2 for a different subject. A score
|
|
239
239
|
* of {@link UNGROUNDED_MAX_SCORE} or less is a rejection whatever the mean says
|
|
240
240
|
* (#999). A higher score up to {@link BORDERLINE_GROUNDING_MAX_SCORE} is only
|
|
@@ -250,12 +250,12 @@ const GROUNDING_CRITERION = "grounding";
|
|
|
250
250
|
const UNGROUNDED_MAX_SCORE = 1;
|
|
251
251
|
const BORDERLINE_GROUNDING_MAX_SCORE = 2;
|
|
252
252
|
const LESSON_JUDGE_CRITERIA = ["novelty", "nonRedundancy", GROUNDING_CRITERION];
|
|
253
|
-
const REFLECT_JUDGE_CRITERIA = ["
|
|
253
|
+
const REFLECT_JUDGE_CRITERIA = ["need", "preservation", "quality"];
|
|
254
254
|
/**
|
|
255
255
|
* Read a judge response: the per-criterion shape (averaged here, `grounding`
|
|
256
|
-
* aside) or the older `{"score"}`
|
|
257
|
-
* any missing or out-of-range
|
|
258
|
-
* ignored.
|
|
256
|
+
* aside; `lowest` is the lowest score in that mean) or the older `{"score"}`
|
|
257
|
+
* shape. Only the expected criteria are read; any missing or out-of-range
|
|
258
|
+
* (1..5) value is a parse failure, extra keys are ignored.
|
|
259
259
|
*/
|
|
260
260
|
function parseJudgeResponse(raw, keys) {
|
|
261
261
|
const parsed = parseEmbeddedJsonResponse(raw);
|
|
@@ -277,9 +277,14 @@ function parseJudgeResponse(raw, keys) {
|
|
|
277
277
|
const averaged = Object.entries(criteria)
|
|
278
278
|
.filter(([key]) => key !== GROUNDING_CRITERION)
|
|
279
279
|
.map(([, value]) => value);
|
|
280
|
-
return {
|
|
280
|
+
return {
|
|
281
|
+
score: averaged.reduce((a, b) => a + b, 0) / averaged.length,
|
|
282
|
+
lowest: Math.min(...averaged),
|
|
283
|
+
reason,
|
|
284
|
+
criteria,
|
|
285
|
+
};
|
|
281
286
|
}
|
|
282
|
-
return inRange(parsed.score) ? { score: parsed.score, reason } : undefined;
|
|
287
|
+
return inRange(parsed.score) ? { score: parsed.score, lowest: parsed.score, reason } : undefined;
|
|
283
288
|
}
|
|
284
289
|
function judgeResponseSchema(keys) {
|
|
285
290
|
return {
|
|
@@ -299,14 +304,14 @@ function judgeResponseSchema(keys) {
|
|
|
299
304
|
}
|
|
300
305
|
/**
|
|
301
306
|
* The quality judge. Fails closed: no runner, an unparseable verdict or a
|
|
302
|
-
* provider failure never passes content. Bands:
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* margin in mind.
|
|
307
|
+
* provider failure never passes content. Bands: every criterion in the mean
|
|
308
|
+
* >= 4 passes, otherwise a mean >= 2.5 is review and a lower one reject; a
|
|
309
|
+
* `grounding` score of {@link UNGROUNDED_MAX_SCORE} or less rejects whatever
|
|
310
|
+
* the mean is, and one of {@link BORDERLINE_GROUNDING_MAX_SCORE} routes a lesson
|
|
311
|
+
* that would pass to review (a mean that rejects stays a rejection).
|
|
312
|
+
* Temperature is set to 0, which reduces run-to-run variation but does not
|
|
313
|
+
* remove it: on some servers (llama.cpp batching, for one) the same request can
|
|
314
|
+
* score a point apart, so the routing rules are chosen with that margin in mind.
|
|
310
315
|
*/
|
|
311
316
|
async function runQualityJudge(feature, config, prompt, keys, chat, options) {
|
|
312
317
|
const resolved = !options.runnerSelectionFrozen && !options.llmRunner
|
|
@@ -338,7 +343,7 @@ async function runQualityJudge(feature, config, prompt, keys, chat, options) {
|
|
|
338
343
|
const parsed = parseJudgeResponse(outcome.raw, keys);
|
|
339
344
|
if (!parsed)
|
|
340
345
|
return { pass: false, score: -1, reason: "judge parse failed — routed to review", reviewNeeded: true };
|
|
341
|
-
const { score, reason, criteria } = parsed;
|
|
346
|
+
const { score, lowest, reason, criteria } = parsed;
|
|
342
347
|
const grounding = criteria?.[GROUNDING_CRITERION];
|
|
343
348
|
if (criteria && grounding !== undefined && grounding <= UNGROUNDED_MAX_SCORE) {
|
|
344
349
|
return {
|
|
@@ -348,8 +353,8 @@ async function runQualityJudge(feature, config, prompt, keys, chat, options) {
|
|
|
348
353
|
criteria,
|
|
349
354
|
};
|
|
350
355
|
}
|
|
351
|
-
const verdict =
|
|
352
|
-
// Borderline grounding is a person's call even when the
|
|
356
|
+
const verdict = lowest >= 4 ? { pass: true } : score >= 2.5 ? { pass: false, reviewNeeded: true } : { pass: false };
|
|
357
|
+
// Borderline grounding is a person's call even when the lesson would pass; a mean that rejects stays rejected.
|
|
353
358
|
if (criteria &&
|
|
354
359
|
grounding !== undefined &&
|
|
355
360
|
grounding <= BORDERLINE_GROUNDING_MAX_SCORE &&
|
|
@@ -28,7 +28,7 @@ import { canonicalBundleIdForTarget, resolveBundleWriteTarget } from "../../core
|
|
|
28
28
|
import { getStateDbPath, withImmediateTransaction, withStateDb } from "../../core/state-db.js";
|
|
29
29
|
import { warn } from "../../core/warn.js";
|
|
30
30
|
import { recordWrittenPath } from "../../core/write-provenance.js";
|
|
31
|
-
import { assertAkmAssetWrite, commitWriteTargetBoundary, prepareWriteTargetForMutation, resolveWriteTarget, } from "../../core/write-source.js";
|
|
31
|
+
import { assertAkmAssetWrite, commitAcceptedPaths, commitWriteTargetBoundary, prepareWriteTargetForMutation, resolveWriteTarget, } from "../../core/write-source.js";
|
|
32
32
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
33
33
|
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
34
34
|
import { deriveInstallations } from "../../indexer/installations.js";
|
|
@@ -708,6 +708,9 @@ function persistProposalDecision(stashDir, proposal, decision, ctx) {
|
|
|
708
708
|
...(decision.gateDecision
|
|
709
709
|
? {
|
|
710
710
|
gateDecision: {
|
|
711
|
+
// The drain's verdict replaces the quality judge's stamp; the judge's evidence stays on the row.
|
|
712
|
+
...(current.gateDecision?.scores ? { scores: current.gateDecision.scores } : {}),
|
|
713
|
+
...(current.gateDecision?.judgeReason ? { judgeReason: current.gateDecision.judgeReason } : {}),
|
|
711
714
|
...decision.gateDecision,
|
|
712
715
|
decidedAt: decision.gateDecision.decidedAt ?? decision.decidedAt,
|
|
713
716
|
},
|
|
@@ -1040,6 +1043,10 @@ function requireAcceptedTarget(proposal) {
|
|
|
1040
1043
|
}
|
|
1041
1044
|
return proposal.acceptedTarget;
|
|
1042
1045
|
}
|
|
1046
|
+
/** An accept's commit subject: the generator, the proposal id's first 8 characters and the asset ref. */
|
|
1047
|
+
function acceptCommitMessage(proposal) {
|
|
1048
|
+
return `akm accept: ${proposal.source} ${proposal.id.slice(0, 8)} ${proposal.ref}`;
|
|
1049
|
+
}
|
|
1043
1050
|
/**
|
|
1044
1051
|
* O1 (alpha.9): an accepted consolidate PROMOTION retires its source memory
|
|
1045
1052
|
* (and its `.derived` twin), so promotion no longer leaves a memory/
|
|
@@ -1059,7 +1066,7 @@ function requireAcceptedTarget(proposal) {
|
|
|
1059
1066
|
* existed carries no hash at all, so it is treated the same way: never
|
|
1060
1067
|
* archived, not verified against a hash that was never recorded.
|
|
1061
1068
|
*/
|
|
1062
|
-
function retirePromotionSource(mutationTarget, accepted) {
|
|
1069
|
+
function retirePromotionSource(mutationTarget, accepted, paths) {
|
|
1063
1070
|
if (!accepted.promotionSource)
|
|
1064
1071
|
return;
|
|
1065
1072
|
try {
|
|
@@ -1086,17 +1093,12 @@ function retirePromotionSource(mutationTarget, accepted) {
|
|
|
1086
1093
|
successorRefs: [accepted.ref],
|
|
1087
1094
|
};
|
|
1088
1095
|
const record = archiveCleanupCandidate(mutationTarget.source.path, candidate, sourcePath);
|
|
1089
|
-
|
|
1090
|
-
sourcePath,
|
|
1091
|
-
path.join(mutationTarget.source.path, record.archivedPath),
|
|
1092
|
-
path.join(mutationTarget.source.path, record.auditPath),
|
|
1093
|
-
];
|
|
1096
|
+
paths.push(sourcePath, path.join(mutationTarget.source.path, record.archivedPath), path.join(mutationTarget.source.path, record.auditPath));
|
|
1094
1097
|
const twin = derivedTwinPath(sourcePath, sourceRef.type);
|
|
1095
1098
|
if (twin) {
|
|
1096
1099
|
const twinRecord = archiveCleanupCandidate(mutationTarget.source.path, candidate, twin);
|
|
1097
1100
|
paths.push(twin, path.join(mutationTarget.source.path, twinRecord.archivedPath), path.join(mutationTarget.source.path, twinRecord.auditPath));
|
|
1098
1101
|
}
|
|
1099
|
-
commitWriteTargetBoundary(mutationTarget, `Retire promoted source ${accepted.promotionSource}`, { paths });
|
|
1100
1102
|
}
|
|
1101
1103
|
catch (error) {
|
|
1102
1104
|
warn(`[proposal] O1: failed to retire promotion source ${accepted.promotionSource} for ${accepted.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1133,7 +1135,6 @@ async function promoteProposalWithLease(stashDir, config, id, options, ctx) {
|
|
|
1133
1135
|
const decidedAt = nowIso(ctx);
|
|
1134
1136
|
const content = preflight.stampedContent.endsWith("\n") ? preflight.stampedContent : `${preflight.stampedContent}\n`;
|
|
1135
1137
|
writeProposalAssetFile(assetPath, content);
|
|
1136
|
-
commitWriteTargetBoundary(mutationTarget, `Update ${proposalForMutation.ref}`, { paths: [assetPath] });
|
|
1137
1138
|
const accepted = persistProposalDecision(stashDir, proposalForMutation, {
|
|
1138
1139
|
operation: "accept",
|
|
1139
1140
|
target: mutationTarget,
|
|
@@ -1146,8 +1147,10 @@ async function promoteProposalWithLease(stashDir, config, id, options, ctx) {
|
|
|
1146
1147
|
decidedAt,
|
|
1147
1148
|
}, ctx);
|
|
1148
1149
|
await indexWrittenProposalAsset(mutationTarget, assetPath);
|
|
1150
|
+
const paths = [assetPath];
|
|
1149
1151
|
if (accepted.status === "accepted" && accepted.source === "consolidate")
|
|
1150
|
-
retirePromotionSource(mutationTarget, accepted);
|
|
1152
|
+
retirePromotionSource(mutationTarget, accepted, paths);
|
|
1153
|
+
commitAcceptedPaths(mutationTarget, acceptCommitMessage(accepted), paths);
|
|
1151
1154
|
return { proposal: accepted, assetPath, ref: accepted.ref };
|
|
1152
1155
|
}
|
|
1153
1156
|
/**
|
|
@@ -1406,7 +1409,7 @@ async function retireProposalWithLease(stashDir, config, proposal, options, ctx)
|
|
|
1406
1409
|
paths.push(twinPath, path.join(mutationTarget.source.path, twinRecord.archivedPath), path.join(mutationTarget.source.path, twinRecord.auditPath));
|
|
1407
1410
|
}
|
|
1408
1411
|
if (paths.length > 0)
|
|
1409
|
-
|
|
1412
|
+
commitAcceptedPaths(mutationTarget, acceptCommitMessage(proposal), paths);
|
|
1410
1413
|
if (archiveDirs.length === 0) {
|
|
1411
1414
|
// Recorded intent, but neither file is at its original location NOR
|
|
1412
1415
|
// archived under this proposal's id: something else removed the target
|
|
@@ -25,7 +25,9 @@ export function validateProposal(proposal) {
|
|
|
25
25
|
* Normalize line endings and complete a truncated frontmatter `description`
|
|
26
26
|
* (`repairTruncatedDescription`, with the body as context). Nothing else: an
|
|
27
27
|
* earlier repair that deleted body lines gutted any asset documenting
|
|
28
|
-
* frontmatter.
|
|
28
|
+
* frontmatter. Only a single-line description is repaired: one YAML wrapped
|
|
29
|
+
* over indented lines (`yaml.stringify` does that past ~80 columns) has a
|
|
30
|
+
* first line that merely looks truncated. Callers re-validate the result.
|
|
29
31
|
*/
|
|
30
32
|
export function repairProposalContent(content) {
|
|
31
33
|
if (typeof content !== "string" || content.trim() === "")
|
|
@@ -34,5 +36,9 @@ export function repairProposalContent(content) {
|
|
|
34
36
|
const { fmText, body } = splitFrontmatter(repaired);
|
|
35
37
|
if (fmText === null)
|
|
36
38
|
return repaired;
|
|
37
|
-
|
|
39
|
+
if (/^description:.*\n[ \t]/m.test(fmText))
|
|
40
|
+
return repaired;
|
|
41
|
+
// The frontmatter block only: a body line starting with `description:` is not the description.
|
|
42
|
+
const frontmatter = repaired.slice(0, repaired.length - body.length);
|
|
43
|
+
return (frontmatter.replace(/^(description:[ \t]*)(.*?)(\r?\n)/m, (_match, prefix, rawDesc, nl) => `${prefix}${repairTruncatedDescription(rawDesc.trim(), body)}${nl}`) + body);
|
|
38
44
|
}
|
|
@@ -101,7 +101,9 @@ export function readMemoryContent(contentArg) {
|
|
|
101
101
|
* Split `text` into sentence-shaped chunks on `.`/`!`/`?`, swallowing any
|
|
102
102
|
* immediately-trailing closing quotes/brackets/repeated terminators into the
|
|
103
103
|
* same sentence (so `Alice said, "hi there."` ends the sentence at the
|
|
104
|
-
* closing quote, not the period).
|
|
104
|
+
* closing quote, not the period). A terminator ends a sentence only when
|
|
105
|
+
* whitespace or the end of the text follows, so `192.168.0.203`, `0.9.12`,
|
|
106
|
+
* `example.com` and `notes.md` stay whole.
|
|
105
107
|
*
|
|
106
108
|
* Ported from akm-eval's memory backend (`splitIntoSentences` /
|
|
107
109
|
* `firstSentencesCapped` in akm-eval/src/memory/backends/akm.ts), which
|
|
@@ -119,6 +121,10 @@ function splitIntoSentences(text) {
|
|
|
119
121
|
let end = i + 1;
|
|
120
122
|
while (end < text.length && /["'”’)\]!?.]/.test(text.charAt(end)))
|
|
121
123
|
end += 1;
|
|
124
|
+
if (end < text.length && !/\s/.test(text.charAt(end))) {
|
|
125
|
+
i = end;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
122
128
|
sentences.push(text.slice(start, end));
|
|
123
129
|
while (end < text.length && /\s/.test(text.charAt(end)))
|
|
124
130
|
end += 1;
|
|
@@ -49,6 +49,10 @@ const COMMON_FIELDS = [
|
|
|
49
49
|
"reflectCooldownActions",
|
|
50
50
|
"reflectSkippedActions",
|
|
51
51
|
"reflectGuardRejectedActions",
|
|
52
|
+
// No longer written (nothing has set it since the confidence gate was
|
|
53
|
+
// deleted), but kept in the allow-list so `decodeImproveResult` still reads
|
|
54
|
+
// the improve_runs rows an older release wrote with it, its value ignored —
|
|
55
|
+
// AGENTS.md "Reading persisted data".
|
|
52
56
|
"gateAutoAcceptedCount",
|
|
53
57
|
"gateAutoAcceptFailedCount",
|
|
54
58
|
"triage",
|
|
@@ -494,7 +498,6 @@ function validateCommon(value) {
|
|
|
494
498
|
"reflectCooldownActions",
|
|
495
499
|
"reflectSkippedActions",
|
|
496
500
|
"reflectGuardRejectedActions",
|
|
497
|
-
"gateAutoAcceptedCount",
|
|
498
501
|
"gateAutoAcceptFailedCount",
|
|
499
502
|
]) {
|
|
500
503
|
if (value[field] !== undefined && typeof value[field] !== "number")
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* Nothing commits per asset (issue #507). Callers write and delete through
|
|
21
21
|
* {@link writeAssetToSource} / {@link deleteAssetFromSource}, then fire
|
|
22
22
|
* {@link commitWriteTargetBoundary} once — or wrap a custom mutation in
|
|
23
|
-
* {@link withWriteTargetMutation}, which does both under the asset lease.
|
|
23
|
+
* {@link withWriteTargetMutation}, which does both under the asset lease. An
|
|
24
|
+
* accepted proposal commits its own paths ({@link commitAcceptedPaths}), on a
|
|
25
|
+
* filesystem stash that is a git repository too.
|
|
24
26
|
*/
|
|
25
27
|
import fs from "node:fs";
|
|
26
28
|
import path from "node:path";
|
|
@@ -394,11 +396,12 @@ export function withWriteTargetMutation(target, paths, options, mutate) {
|
|
|
394
396
|
* Commit a git target's recorded paths plus `options.paths` (absolute or
|
|
395
397
|
* repository-relative) as one commit, and push it with `--force-with-lease`
|
|
396
398
|
* when the target is writable, has an upstream, and `push !== false`. A no-op
|
|
397
|
-
* for filesystem targets. Ignored paths stay local:
|
|
398
|
-
* commit with a warning rather than failing a write
|
|
399
|
+
* for filesystem targets unless `filesystem` is set. Ignored paths stay local:
|
|
400
|
+
* they are dropped from the commit with a warning rather than failing a write
|
|
401
|
+
* that already landed.
|
|
399
402
|
*/
|
|
400
403
|
export function commitWriteTargetBoundary(target, message, options) {
|
|
401
|
-
if (target.source.kind !== "git")
|
|
404
|
+
if (target.source.kind !== "git" && !options?.filesystem)
|
|
402
405
|
return;
|
|
403
406
|
const repoDir = repoDirFor(target.source);
|
|
404
407
|
const recorded = pendingGitPaths.get(repoDir) ?? new Set();
|
|
@@ -417,11 +420,13 @@ export function commitWriteTargetBoundary(target, message, options) {
|
|
|
417
420
|
if (committable.length === 0)
|
|
418
421
|
return;
|
|
419
422
|
try {
|
|
420
|
-
saveGitStash(undefined, message, resolveWritable(target.config), {
|
|
423
|
+
const saved = saveGitStash(undefined, message, resolveWritable(target.config), {
|
|
421
424
|
repoDir,
|
|
422
425
|
paths: committable,
|
|
423
426
|
...(options?.push === undefined ? {} : { push: options.push }),
|
|
424
427
|
});
|
|
428
|
+
if (saved.reason)
|
|
429
|
+
warn(`warning: "${target.source.name}" ${saved.reason}`);
|
|
425
430
|
}
|
|
426
431
|
catch (error) {
|
|
427
432
|
if (error instanceof GitStashPushError) {
|
|
@@ -432,3 +437,21 @@ export function commitWriteTargetBoundary(target, message, options) {
|
|
|
432
437
|
throw error;
|
|
433
438
|
}
|
|
434
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Commit exactly the paths an accepted proposal wrote or removed, when the
|
|
442
|
+
* target is a git repository: a filesystem stash with a `.git` too, which the
|
|
443
|
+
* boundary commit skips, locally (the end-of-run sync pushes). A failed commit
|
|
444
|
+
* only warns, since the accept already landed.
|
|
445
|
+
*/
|
|
446
|
+
export function commitAcceptedPaths(target, message, paths) {
|
|
447
|
+
try {
|
|
448
|
+
commitWriteTargetBoundary(target, message, {
|
|
449
|
+
paths,
|
|
450
|
+
filesystem: true,
|
|
451
|
+
...(target.source.kind === "git" ? {} : { push: false }),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
warn(`warning: could not commit the accept (${message}): ${error instanceof Error ? error.message : String(error)}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -31339,14 +31339,14 @@ var consolidate_default = {
|
|
|
31339
31339
|
};
|
|
31340
31340
|
// src/assets/improve-strategies/default.json
|
|
31341
31341
|
var default_default = {
|
|
31342
|
-
description: "Standard improve pass — reflect, distill, consolidation (promotion plus the reviewed pair-pass retire/supersede proposals), and validation. Memory inference is listed below but only runs when experimental.improveAutonomy is set; improve-stage extract and proactive maintenance off.",
|
|
31342
|
+
description: "Standard improve pass — reflect (rewrites only from negative feedback), distill, consolidation (promotion plus the reviewed pair-pass retire/supersede proposals), and validation. Memory inference is listed below but only runs when experimental.improveAutonomy is set; improve-stage extract and proactive maintenance off.",
|
|
31343
31343
|
processes: {
|
|
31344
31344
|
reflect: {
|
|
31345
31345
|
enabled: true,
|
|
31346
31346
|
limit: 25,
|
|
31347
31347
|
allowedTypes: ["agent", "command", "knowledge", "lesson", "memory", "skill", "workflow"]
|
|
31348
31348
|
},
|
|
31349
|
-
distill: { enabled: true, allowedTypes: ["memory"], requirePlannedRefs:
|
|
31349
|
+
distill: { enabled: true, allowedTypes: ["memory"], requirePlannedRefs: false },
|
|
31350
31350
|
consolidate: { enabled: true, allowedTypes: ["memory"] },
|
|
31351
31351
|
memoryInference: { enabled: true },
|
|
31352
31352
|
extract: { enabled: false, triage: { enabled: true, minScore: 2 } },
|
|
@@ -31358,7 +31358,7 @@ var default_default = {
|
|
|
31358
31358
|
};
|
|
31359
31359
|
// src/assets/improve-strategies/proactive-maintenance.json
|
|
31360
31360
|
var proactive_maintenance_default = {
|
|
31361
|
-
description: "Opt-in proactive-maintenance pass — reflect, distill, proposal triage (promote, high budget), and the proactive-maintenance lane (maxPerRun 100); consolidate/memoryInference/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
|
|
31361
|
+
description: "Opt-in proactive-maintenance pass — reflect (on negative feedback), distill, proposal triage (promote, high budget), and the proactive-maintenance lane (maxPerRun 100), which picks due assets for scoring but plans no rewrite; consolidate/memoryInference/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
|
|
31362
31362
|
processes: {
|
|
31363
31363
|
reflect: {
|
|
31364
31364
|
enabled: true,
|
|
@@ -31441,7 +31441,7 @@ var thorough_default = {
|
|
|
31441
31441
|
distill: {
|
|
31442
31442
|
enabled: true,
|
|
31443
31443
|
allowedTypes: ["memory"],
|
|
31444
|
-
requirePlannedRefs:
|
|
31444
|
+
requirePlannedRefs: false
|
|
31445
31445
|
},
|
|
31446
31446
|
consolidate: {
|
|
31447
31447
|
enabled: true,
|
|
@@ -30667,14 +30667,14 @@ var consolidate_default = {
|
|
|
30667
30667
|
};
|
|
30668
30668
|
// src/assets/improve-strategies/default.json
|
|
30669
30669
|
var default_default = {
|
|
30670
|
-
description: "Standard improve pass \u2014 reflect, distill, consolidation (promotion plus the reviewed pair-pass retire/supersede proposals), and validation. Memory inference is listed below but only runs when experimental.improveAutonomy is set; improve-stage extract and proactive maintenance off.",
|
|
30670
|
+
description: "Standard improve pass \u2014 reflect (rewrites only from negative feedback), distill, consolidation (promotion plus the reviewed pair-pass retire/supersede proposals), and validation. Memory inference is listed below but only runs when experimental.improveAutonomy is set; improve-stage extract and proactive maintenance off.",
|
|
30671
30671
|
processes: {
|
|
30672
30672
|
reflect: {
|
|
30673
30673
|
enabled: true,
|
|
30674
30674
|
limit: 25,
|
|
30675
30675
|
allowedTypes: ["agent", "command", "knowledge", "lesson", "memory", "skill", "workflow"]
|
|
30676
30676
|
},
|
|
30677
|
-
distill: { enabled: true, allowedTypes: ["memory"], requirePlannedRefs:
|
|
30677
|
+
distill: { enabled: true, allowedTypes: ["memory"], requirePlannedRefs: false },
|
|
30678
30678
|
consolidate: { enabled: true, allowedTypes: ["memory"] },
|
|
30679
30679
|
memoryInference: { enabled: true },
|
|
30680
30680
|
extract: { enabled: false, triage: { enabled: true, minScore: 2 } },
|
|
@@ -30686,7 +30686,7 @@ var default_default = {
|
|
|
30686
30686
|
};
|
|
30687
30687
|
// src/assets/improve-strategies/proactive-maintenance.json
|
|
30688
30688
|
var proactive_maintenance_default = {
|
|
30689
|
-
description: "Opt-in proactive-maintenance pass \u2014 reflect, distill, proposal triage (promote, high budget), and the proactive-maintenance lane (maxPerRun 100); consolidate/memoryInference/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
|
|
30689
|
+
description: "Opt-in proactive-maintenance pass \u2014 reflect (on negative feedback), distill, proposal triage (promote, high budget), and the proactive-maintenance lane (maxPerRun 100), which picks due assets for scoring but plans no rewrite; consolidate/memoryInference/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
|
|
30690
30690
|
processes: {
|
|
30691
30691
|
reflect: {
|
|
30692
30692
|
enabled: true,
|
|
@@ -30769,7 +30769,7 @@ var thorough_default = {
|
|
|
30769
30769
|
distill: {
|
|
30770
30770
|
enabled: true,
|
|
30771
30771
|
allowedTypes: ["memory"],
|
|
30772
|
-
requirePlannedRefs:
|
|
30772
|
+
requirePlannedRefs: false
|
|
30773
30773
|
},
|
|
30774
30774
|
consolidate: {
|
|
30775
30775
|
enabled: true,
|
|
@@ -167,7 +167,9 @@ export function resolveWritableOverride(config) {
|
|
|
167
167
|
* - Not a git repo → skipped (no-op)
|
|
168
168
|
* - Git repo, no remote → commit only
|
|
169
169
|
* - Git repo, has remote, but stash is not writable → commit only
|
|
170
|
-
* - Git repo, has remote, stash is writable → commit + push
|
|
170
|
+
* - Git repo, has remote, stash is writable → commit + push, unless the
|
|
171
|
+
* branch has no upstream or is behind or diverged from it: then commit
|
|
172
|
+
* only, and `reason` says why
|
|
171
173
|
*
|
|
172
174
|
* When `name` is omitted the primary stash directory is used.
|
|
173
175
|
* When `message` is omitted a timestamp is used.
|
|
@@ -288,7 +290,9 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
288
290
|
throw new Error(`git remote failed: ${remoteResult.stderr?.trim() || "unknown error"}`);
|
|
289
291
|
}
|
|
290
292
|
const hasRemote = remoteResult.stdout.trim().length > 0;
|
|
291
|
-
|
|
293
|
+
// A branch that cannot be pushed still gets its commit: only the push is skipped, and the result says why.
|
|
294
|
+
const upstream = hasRemote && writable && allowPush ? readActualUpstream(repoDir, baseHead) : undefined;
|
|
295
|
+
const pushTarget = typeof upstream === "object" ? upstream : undefined;
|
|
292
296
|
const exactCommit = createExactPathCommit(repoDir, {
|
|
293
297
|
baseHead,
|
|
294
298
|
commitMessage,
|
|
@@ -304,6 +308,7 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
304
308
|
committed: true,
|
|
305
309
|
pushed: false,
|
|
306
310
|
skipped: false,
|
|
311
|
+
...(typeof upstream === "string" ? { reason: `not pushed: ${upstream}` } : {}),
|
|
307
312
|
output: `commit ${exactCommit}`,
|
|
308
313
|
commit: exactCommit,
|
|
309
314
|
};
|
|
@@ -387,20 +392,22 @@ function readBranchRef(repoDir) {
|
|
|
387
392
|
}
|
|
388
393
|
return result.stdout.trim();
|
|
389
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Where a commit on top of `baseHead` can be pushed: the branch's upstream, as
|
|
397
|
+
* long as it is an ancestor of `baseHead` (ahead is fine, the push fast-forwards
|
|
398
|
+
* it). Otherwise why not: no upstream, or behind or diverged from it.
|
|
399
|
+
*/
|
|
390
400
|
function readActualUpstream(repoDir, baseHead) {
|
|
391
|
-
if (!baseHead)
|
|
392
|
-
throw new UsageError(`Writable Git target at ${repoDir} has no commit to publish.`);
|
|
393
401
|
const branchRef = readBranchRef(repoDir);
|
|
394
402
|
const branch = branchRef.replace(/^refs\/heads\//, "");
|
|
395
403
|
const remote = runGit(["-C", repoDir, "config", "--get", `branch.${branch}.remote`]);
|
|
396
404
|
const merge = runGit(["-C", repoDir, "config", "--get", `branch.${branch}.merge`]);
|
|
397
405
|
const upstream = runGit(["-C", repoDir, "rev-parse", "--verify", "@{u}"]);
|
|
398
|
-
if (remote.status !== 0 || merge.status !== 0 || upstream.status !== 0)
|
|
399
|
-
|
|
400
|
-
}
|
|
406
|
+
if (remote.status !== 0 || merge.status !== 0 || upstream.status !== 0)
|
|
407
|
+
return "no upstream branch is configured";
|
|
401
408
|
const upstreamHead = upstream.stdout.trim();
|
|
402
|
-
if (!
|
|
403
|
-
|
|
409
|
+
if (!baseHead || runGit(["-C", repoDir, "merge-base", "--is-ancestor", upstreamHead, baseHead]).status !== 0) {
|
|
410
|
+
return "the branch is behind its upstream or has diverged from it";
|
|
404
411
|
}
|
|
405
412
|
return { remote: remote.stdout.trim(), mergeRef: merge.stdout.trim(), upstreamHead };
|
|
406
413
|
}
|
|
@@ -16,7 +16,6 @@ export function computeImproveRunMetrics(result) {
|
|
|
16
16
|
let acceptedCount = 0;
|
|
17
17
|
let rejectedCount = 0;
|
|
18
18
|
let skippedCount = 0;
|
|
19
|
-
let autoAcceptedCount = 0;
|
|
20
19
|
let errorCount = 0;
|
|
21
20
|
for (const action of actions) {
|
|
22
21
|
// Bucketing delegated to the shared classifyImproveAction so this aggregate
|
|
@@ -42,8 +41,7 @@ export function computeImproveRunMetrics(result) {
|
|
|
42
41
|
break;
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
|
-
|
|
46
|
-
autoAcceptedCount += result.gateAutoAcceptedCount ?? 0;
|
|
44
|
+
const autoAcceptedCount = result.triage?.promoted ?? 0;
|
|
47
45
|
// C1 (13-bus-factor): distill-skipped rows are folded into the bounded
|
|
48
46
|
// `distillSkipped` aggregate and no longer live in `actions`. Add the
|
|
49
47
|
// aggregate total to the skipped + total-actions counters so metrics_json
|