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.
@@ -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` gate decision with the
124
- * judged content's hash (the triage drain accepts it while the content still
125
- * matches); `review` leaves it `deferred` for a human (`review_needed` in the
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 (overlap with the source is expected). */
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. FEEDBACK ALIGNMENT: Does the revision address the supplied feedback or improve retrieval and clarity?",
206
- "2. PRESERVATION: Does it retain the source's concrete facts, code, commands, examples, and structure without truncation?",
207
- "3. QUALITY: Is the revision coherent, actionable, complete, and free of unsupported claims?",
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": {"feedbackAlignment": <1-5 integer>, "preservation": <1-5 integer>, "quality": <1-5 integer>}, "reason": "<one sentence>"}',
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 the mean would pass it (or, in the review band, mint it as
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 = ["feedbackAlignment", "preservation", "quality"];
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"}` shape. Only the expected criteria are read;
257
- * any missing or out-of-range (1..5) value is a parse failure, extra keys are
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 { score: averaged.reduce((a, b) => a + b, 0) / averaged.length, reason, criteria };
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: >= 3.5 pass, 2.5-3.5 review,
303
- * < 2.5 reject; a `grounding` score of {@link UNGROUNDED_MAX_SCORE} or less
304
- * rejects whatever the mean is, and one of {@link BORDERLINE_GROUNDING_MAX_SCORE}
305
- * routes a lesson the mean would pass to review (a mean that rejects stays a
306
- * rejection). Temperature is set to 0, which reduces run-to-run variation but
307
- * does not remove it: on some servers (llama.cpp batching, for one) the same
308
- * request can score a point apart, so the routing rules are chosen with that
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 = score >= 3.5 ? { pass: true } : score >= 2.5 ? { pass: false, reviewNeeded: true } : { pass: false };
352
- // Borderline grounding is a person's call even when the mean would pass; a mean that rejects stays rejected.
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
- const paths = [
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
- commitWriteTargetBoundary(mutationTarget, `Retire ${proposal.ref}`, { paths });
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. Callers re-validate the result.
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
- return repaired.replace(/^(description:\s*)(.*?)(\r?\n)/m, (_match, prefix, rawDesc, nl) => `${prefix}${repairTruncatedDescription(rawDesc.trim(), body)}${nl}`);
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: they are dropped from the
398
- * commit with a warning rather than failing a write that already landed.
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: true },
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: true
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: true },
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: true
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
- const pushTarget = hasRemote && writable && allowPush ? readActualUpstream(repoDir, baseHead) : undefined;
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
- throw new UsageError(`Writable Git target at ${repoDir} has no configured upstream branch.`);
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 (!upstreamHead || upstreamHead !== baseHead) {
403
- throw new UsageError(`Writable Git target at ${repoDir} is not synchronized with its actual upstream.`);
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
- // Add gate-promoted count from the unified PostPhaseAutoAcceptGate (all phases).
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