akm-cli 0.9.26-alpha.1 → 0.9.26

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.
@@ -34,6 +34,7 @@ import { runSchemaRepairPass } from "../sources/schema-repair.js";
34
34
  import { isAutonomyLaneAllowed } from "./autonomy-gate.js";
35
35
  import { akmConsolidate, inspectConsolidationPool, loadExistingKnowledgeBodyHashes, makeConsolidateResult, } from "./consolidate.js";
36
36
  import { computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
37
+ import { contentHash } from "./content-hash.js";
37
38
  import { assetTypeOf, buildUtilityMap, dedupeRefs, findAssetFilePath, isDistillCandidateRef, isLessonCandidate, resolveImproveScope, withIndexDb, } from "./eligibility.js";
38
39
  import { akmExtract, countNewExtractCandidates } from "./extract.js";
39
40
  import { computeValenceScore } from "./feedback-valence.js";
@@ -545,6 +546,51 @@ function isSignalEvent(metadata) {
545
546
  const meta = metadata;
546
547
  return meta !== undefined && (typeof meta.signal === "string" || typeof meta.note === "string");
547
548
  }
549
+ /**
550
+ * Whether `candidate` was flagged wrong and not edited since. A negative
551
+ * feedback inside the signal window that recorded the hash of the body it
552
+ * judged flags it exactly when the body still has that hash, so a later write
553
+ * that leaves the text alone (an inference stamp, a frontmatter repair) does not
554
+ * lift the flag. One without a hash flags it when it is newer than the file's
555
+ * last write: its mtime, the one the retrieval scope reads for new material. A
556
+ * candidate with no readable file is not flagged.
557
+ */
558
+ export function isFlaggedSinceLastEdit(candidate, eventsCtx) {
559
+ if (!candidate.filePath)
560
+ return false;
561
+ let editedAtMs;
562
+ let bodyHash;
563
+ try {
564
+ editedAtMs = fs.statSync(candidate.filePath).mtimeMs;
565
+ bodyHash = contentHash(fs.readFileSync(candidate.filePath, "utf8"), "body");
566
+ }
567
+ catch {
568
+ return false;
569
+ }
570
+ const since = new Date(Date.now() - daysToMs(FEEDBACK_SIGNAL_WINDOW_DAYS)).toISOString();
571
+ return readEvents({ type: "feedback", ref: keyOf(candidate), since }, eventsCtx).events.some((e) => {
572
+ const meta = e.metadata;
573
+ if (meta?.signal !== "negative")
574
+ return false;
575
+ return typeof meta.contentHash === "string" ? meta.contentHash === bodyHash : Date.parse(e.ts) > editedAtMs;
576
+ });
577
+ }
578
+ /**
579
+ * Whether the only feedback `candidate` has inside the signal window is positive and says nothing: no reason, no
580
+ * note. A bare `--positive` only records that a note helped, which gives the writer nothing to distil, so it restates
581
+ * the memory: 10 of the 11 lessons distilled from a memory with nothing more were rejected (the 2026-10-05 review).
582
+ * A candidate with no feedback in the window is not matched.
583
+ */
584
+ export function hasOnlyBarePositiveFeedback(candidate, eventsCtx) {
585
+ const since = new Date(Date.now() - daysToMs(FEEDBACK_SIGNAL_WINDOW_DAYS)).toISOString();
586
+ const signals = readEvents({ type: "feedback", ref: keyOf(candidate), since }, eventsCtx).events.filter((e) => isSignalEvent(e.metadata));
587
+ const hasText = (value) => typeof value === "string" && value.trim() !== "";
588
+ return (signals.length > 0 &&
589
+ signals.every((e) => {
590
+ const meta = e.metadata;
591
+ return meta.signal === "positive" && !hasText(meta.reason) && !hasText(meta.note);
592
+ }));
593
+ }
548
594
  /** One read of the feedback events and the ledger's reflect/distill rows. */
549
595
  export function buildSnapshotManifest(args) {
550
596
  const { eventsCtx, stashDir } = args;
@@ -38,6 +38,7 @@ import { isJsonSchemaKnownUnsupported, LlmCallError } from "../../llm/client.js"
38
38
  import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
39
39
  import { isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
40
40
  import { CHARS_PER_TOKEN, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
41
+ import { contentHash } from "./content-hash.js";
41
42
  import { findAssetFilePath } from "./eligibility.js";
42
43
  import { resolveImproveExecution } from "./execution.js";
43
44
  import { recordLedgerAttempt } from "./ledger.js";
@@ -46,19 +47,29 @@ import { loadRetrievalQueries, runRetrievalRegressionGate } from "./retrieval-ga
46
47
  import { callStageOnce, errMessage, mintProposal, noticeSet, rejectedProposalContext, resolveQualityGateJudge, runReflectQualityJudge, } from "./stage.js";
47
48
  const MAX_FEEDBACK_LINES = 10;
48
49
  const MAX_GLOBAL_FEEDBACK_LINES = 20;
50
+ /** Ends a feedback line whose event judged text the asset no longer has. */
51
+ const EARLIER_TEXT_MARK = " (given on an earlier version of the text)";
49
52
  function readOnlyEventsContext(ctx) {
50
53
  return ctx?.db ? ctx : { ...(ctx ?? {}), readOnly: true };
51
54
  }
52
- /** Recent `feedback` lines for `ref` (or across all assets without one). Best-effort. */
53
- function readRecentFeedback(ref, eventsCtx) {
55
+ /**
56
+ * Recent `feedback` lines for `ref` (or across all assets without one). Given the
57
+ * asset's current content, a line whose event recorded the hash of a different
58
+ * body is marked. Best-effort.
59
+ */
60
+ function readRecentFeedback(ref, eventsCtx, assetContent) {
54
61
  try {
55
62
  const events = readEvents({ type: "feedback", ...(ref ? { ref } : {}) }, readOnlyEventsContext(eventsCtx)).events;
63
+ const bodyHash = assetContent === undefined ? undefined : contentHash(assetContent, "body");
56
64
  return events.slice(-(ref ? MAX_FEEDBACK_LINES : MAX_GLOBAL_FEEDBACK_LINES)).map((event) => {
57
65
  const md = event.metadata ?? {};
58
66
  const signal = typeof md.signal === "string" ? md.signal : "?";
59
67
  const note = typeof md.reason === "string" ? md.reason : typeof md.note === "string" ? md.note : "";
60
68
  const details = note ? `[${signal}] ${note}` : `[${signal}]`;
61
- return !ref && event.ref ? `${event.ref} ${details}` : details;
69
+ const line = !ref && event.ref ? `${event.ref} ${details}` : details;
70
+ return bodyHash !== undefined && typeof md.contentHash === "string" && md.contentHash !== bodyHash
71
+ ? `${line}${EARLIER_TEXT_MARK}`
72
+ : line;
62
73
  });
63
74
  }
64
75
  catch {
@@ -636,7 +647,7 @@ function computeReflectContentBudgetChars(promptInput, runnerSpec) {
636
647
  /** Every read-only prompt input, shared by dispatch and `--show-prompt`. */
637
648
  function gatherReflectPromptSources(options, stash, parsedRef, assetContent) {
638
649
  return {
639
- feedback: readRecentFeedback(options.ref ? (options.itemRef ?? options.ref) : undefined, options.eventsCtx),
650
+ feedback: readRecentFeedback(options.ref ? (options.itemRef ?? options.ref) : undefined, options.eventsCtx, assetContent),
640
651
  schemaHints: buildSchemaHints(parsedRef?.type ?? "", assetContent),
641
652
  rejectedProposals: rejectedProposalContext(stash, options.ref, options.ctx, options.eventsCtx),
642
653
  standardsContext: resolveStandardsContext(options.ref, stash),
@@ -13,12 +13,13 @@ import { randomUUID } from "node:crypto";
13
13
  import fs from "node:fs";
14
14
  import os from "node:os";
15
15
  import path from "node:path";
16
+ import { isDeepStrictEqual } from "node:util";
16
17
  import { parse as parseYaml } from "yaml";
17
18
  import { ensureAkmMarkdownType } from "../../core/asset/akm-markdown.js";
18
19
  import { assetPathForName, placementTypes, stashDirFor } from "../../core/asset/asset-placement.js";
19
20
  import { isBundleSlug, parseBundleRef } from "../../core/asset/asset-ref.js";
20
21
  import { assembleAsset, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
21
- import { carryForwardBookkeepingFrontmatter, parseFrontmatter } from "../../core/asset/frontmatter.js";
22
+ import { carryForwardBookkeepingFrontmatter, parseFrontmatter, replaceFrontmatterBlocks, } from "../../core/asset/frontmatter.js";
22
23
  import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
23
24
  import { loadConfig } from "../../core/config/config.js";
24
25
  import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
@@ -287,6 +288,7 @@ export function createProposal(stashDir, input, ctx) {
287
288
  ...(input.eligibilitySource !== undefined ? { eligibilitySource: input.eligibilitySource } : {}),
288
289
  ...(input.promotionSource !== undefined ? { promotionSource: input.promotionSource } : {}),
289
290
  ...(input.promotionSourceHash !== undefined ? { promotionSourceHash: input.promotionSourceHash } : {}),
291
+ ...(input.feedback !== undefined ? { feedback: input.feedback } : {}),
290
292
  };
291
293
  upsertProposal(db, proposal, stashDir);
292
294
  for (const ref of input.attemptedRefs ?? [normalizedRef]) {
@@ -882,8 +884,17 @@ function isPlainRecord(value) {
882
884
  * Stamp provenance onto a promoted asset's frontmatter: bare top-level
883
885
  * `generated` and `verified` (as OKF v0.2 spells them; `verified` accumulates),
884
886
  * and `sources` under `provenance:`, since a bare `sources:` is the wiki
885
- * citation-string convention. An existing frontmatter block keeps its raw body
886
- * bytes.
887
+ * citation-string convention.
888
+ *
889
+ * Source preservation: an existing frontmatter block is edited as text and not
890
+ * written out again through the YAML serializer, which rewraps long values,
891
+ * requotes and drops comments in lines the stamp never touched. The blocks of
892
+ * the keys the stamp sets (`generated`, `verified`, `provenance`) are removed
893
+ * wherever they were and written again, serialized, as the last lines of the
894
+ * frontmatter; every other byte, the body included, stays as written. The
895
+ * edited text is parsed back to confirm it holds exactly the intended mapping;
896
+ * when it does not (an indented `---` inside a block scalar), the frontmatter is
897
+ * written out again instead, as it always used to be.
887
898
  */
888
899
  function stampProposalProvenance(content, proposal, gateDecision, ctx, nowIsoStr) {
889
900
  const parsed = parseFrontmatter(content);
@@ -916,9 +927,15 @@ function stampProposalProvenance(content, proposal, gateDecision, ctx, nowIsoStr
916
927
  fm.provenance = provenance;
917
928
  else
918
929
  delete fm.provenance;
919
- return parsed.frontmatter !== null
920
- ? `---\n${serializeFrontmatter(fm)}\n---\n${parsed.content}`
921
- : assembleAsset(fm, parsed.content);
930
+ if (parsed.frontmatter === null)
931
+ return assembleAsset(fm, parsed.content);
932
+ const stamped = { generated: fm.generated, verified: fm.verified };
933
+ if (fm.provenance !== undefined)
934
+ stamped.provenance = fm.provenance;
935
+ const edited = replaceFrontmatterBlocks(content, ["generated", "verified", "provenance"], serializeFrontmatter(stamped).split("\n"));
936
+ if (edited !== null && isDeepStrictEqual(parseFrontmatter(edited).data, fm))
937
+ return edited;
938
+ return `---\n${serializeFrontmatter(fm)}\n---\n${parsed.content}`;
922
939
  }
923
940
  /**
924
941
  * Validate, stamp and write an accepted proposal into its bound target, then
@@ -1,11 +1,12 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { isDeepStrictEqual } from "node:util";
4
5
  import { parse as parseYaml } from "yaml";
5
6
  import { localDateStamp } from "../common.js";
6
7
  import { UsageError } from "../errors.js";
7
8
  import { serializeFrontmatter } from "./asset-serialize.js";
8
- import { parseFrontmatterBlock, spliceFrontmatterLine } from "./frontmatter.js";
9
+ import { parseFrontmatterBlock, replaceFrontmatterLine, spliceFrontmatterLine } from "./frontmatter.js";
9
10
  /**
10
11
  * Ensure an AKM-authored Markdown concept is also a conformant OKF concept.
11
12
  *
@@ -20,12 +21,18 @@ import { parseFrontmatterBlock, spliceFrontmatterLine } from "./frontmatter.js";
20
21
  * re-stamp on every write (which would churn timestamps and manufacture
21
22
  * needless diffs in git-backed bundles).
22
23
  *
23
- * Source preservation: when the type already matches and the ONLY change is
24
- * adding `updated`, the line is spliced into the original block textually —
25
- * round-tripping through the YAML serializer would drop user-authored
26
- * comments and normalize formatting just to contribute one field. Only a
27
- * document whose `type` must actually be corrected takes the re-serialize
28
- * path (as it always has).
24
+ * Source preservation: whenever the frontmatter block parses, it is edited as
25
+ * text and never round-tripped through the YAML serializer, which rewraps long
26
+ * values, reorders keys and drops comments — changes nobody made, shown to the
27
+ * reviewer of what may be a one-line correction. A missing `type` and a
28
+ * missing `updated` are each added as one line before the closing `---`, in
29
+ * that order; a wrong `type` is replaced on its own line. Every other byte —
30
+ * comments, wrapping, quoting, key order, line endings, the body — is kept as
31
+ * written. The edited text is parsed back to confirm it holds exactly the
32
+ * intended mapping; when it does not (a `type` value that spans several lines,
33
+ * a flow-style `{…}` block), the document is re-serialized instead, as it
34
+ * always used to be. A document with no frontmatter block gets a new one;
35
+ * malformed YAML throws.
29
36
  */
30
37
  export function ensureAkmMarkdownType(content, type, now = new Date()) {
31
38
  const block = parseFrontmatterBlock(content);
@@ -45,19 +52,36 @@ export function ensureAkmMarkdownType(content, type, now = new Date()) {
45
52
  throw new UsageError("AKM Markdown frontmatter must be a YAML mapping.", "INVALID_FLAG_VALUE");
46
53
  }
47
54
  const data = parsed;
55
+ const updated = localDateStamp(now);
48
56
  const needsUpdated = !("updated" in data);
49
- if (data.type === type) {
50
- if (!needsUpdated)
51
- return content;
52
- const spliced = spliceFrontmatterLine(content, `updated: ${localDateStamp(now)}`);
53
- if (spliced !== null)
54
- return spliced;
55
- // Unreachable in practice (parseFrontmatterBlock succeeded above), but a
56
- // re-serialized document beats a non-conformant one.
57
+ if (data.type === type && !needsUpdated)
58
+ return content;
59
+ let edited = content;
60
+ if (data.type !== type) {
61
+ edited =
62
+ "type" in data
63
+ ? replaceFrontmatterLine(edited, "type", `type: ${type}`)
64
+ : spliceFrontmatterLine(edited, `type: ${type}`);
57
65
  }
66
+ if (edited !== null && needsUpdated)
67
+ edited = spliceFrontmatterLine(edited, `updated: ${updated}`);
68
+ const intended = needsUpdated ? { ...data, type, updated } : { ...data, type };
69
+ if (edited !== null && parsesTo(edited, intended))
70
+ return edited;
71
+ // The text edit could not be done safely, but a re-serialized document
72
+ // beats a non-conformant one.
58
73
  const { type: _priorType, ...rest } = data;
59
74
  const next = { type, ...rest };
60
75
  if (needsUpdated)
61
- next.updated = localDateStamp(now);
76
+ next.updated = updated;
62
77
  return `---\n${serializeFrontmatter(next)}\n---\n${block.content}`;
63
78
  }
79
+ /** True when the frontmatter of `text` parses to exactly `intended`. */
80
+ function parsesTo(text, intended) {
81
+ try {
82
+ return isDeepStrictEqual(parseYaml(parseFrontmatterBlock(text)?.frontmatter ?? ""), intended);
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
@@ -196,17 +196,77 @@ function countLines(text) {
196
196
  * frontmatter: round-tripping the mapping through the YAML serializer drops
197
197
  * comments and normalizes formatting, which is unacceptable for a write that
198
198
  * only needs to contribute one line. Shared by `ensureAkmMarkdownType`
199
- * (stamping `updated:` on write) and lint's `--fix` for `missing-updated`.
199
+ * (adding `type:` and `updated:` on write) and lint's `--fix` for
200
+ * `missing-updated`. The new line takes the block's own line ending.
200
201
  */
201
202
  export function spliceFrontmatterLine(raw, line) {
202
- const lines = raw.split(/\r?\n/);
203
- if (lines[0]?.trim() !== "---")
203
+ // Alternating [line, eol, line, eol, …, line]: joining the parts back
204
+ // reproduces `raw` byte for byte, CRLF and all.
205
+ const parts = raw.split(/(\r?\n)/);
206
+ if (parts[0]?.trim() !== "---")
204
207
  return null;
205
- const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
206
- if (closeIdx === -1)
208
+ const closeAt = parts.findIndex((part, i) => i > 0 && part.trim() === "---");
209
+ if (closeAt === -1)
210
+ return null;
211
+ parts.splice(closeAt, 0, line, parts[1]);
212
+ return parts.join("");
213
+ }
214
+ /**
215
+ * Replace the top-level `key:` line of an existing frontmatter block with
216
+ * `line`, leaving every other byte untouched — the counterpart to
217
+ * {@link spliceFrontmatterLine} for correcting a field's value in place.
218
+ *
219
+ * Only that one physical line is replaced, so a value that continues onto
220
+ * further lines (a block sequence, a folded scalar) would be left dangling:
221
+ * the caller must check that the result still parses to what it meant. Returns
222
+ * null when `raw` has no well-formed block or no such line.
223
+ */
224
+ export function replaceFrontmatterLine(raw, key, line) {
225
+ const parts = raw.split(/(\r?\n)/);
226
+ if (parts[0]?.trim() !== "---")
227
+ return null;
228
+ const closeAt = parts.findIndex((part, i) => i > 0 && part.trim() === "---");
229
+ if (closeAt === -1)
207
230
  return null;
208
- lines.splice(closeIdx, 0, line);
209
- return lines.join("\n");
231
+ const at = parts.findIndex((part, i) => i > 0 && i < closeAt && part.match(/^(\w[\w-]*):(?:[ \t]|$)/)?.[1] === key);
232
+ if (at === -1)
233
+ return null;
234
+ parts[at] = line;
235
+ return parts.join("");
236
+ }
237
+ /**
238
+ * Replace the top-level blocks of `keys` in an existing frontmatter block with
239
+ * `lines`, leaving every other byte untouched — the counterpart to
240
+ * {@link spliceFrontmatterLine} for a write that owns whole keys, such as the
241
+ * provenance stamp's `generated`, `verified` and `provenance`.
242
+ *
243
+ * A key's block is its `key:` line and the lines directly under it that
244
+ * continue it: indented lines and `- ` items. Every such block is removed
245
+ * wherever it sits, and `lines` are added just before the closing `---`, in the
246
+ * block's own line ending. A block with a blank line or a comment inside it is
247
+ * not removed whole, so the caller must check that the result still parses to
248
+ * what it meant. Returns null when `raw` has no well-formed block.
249
+ */
250
+ export function replaceFrontmatterBlocks(raw, keys, lines) {
251
+ const parts = raw.split(/(\r?\n)/);
252
+ if (parts[0]?.trim() !== "---")
253
+ return null;
254
+ const closeAt = parts.findIndex((part, i) => i > 0 && part.trim() === "---");
255
+ if (closeAt === -1)
256
+ return null;
257
+ const kept = [];
258
+ for (let i = 2; i < closeAt; i += 2) {
259
+ const key = parts[i].match(/^(\w[\w-]*):(?:[ \t]|$)/)?.[1];
260
+ if (key === undefined || !keys.includes(key)) {
261
+ kept.push(parts[i], parts[i + 1]);
262
+ continue;
263
+ }
264
+ // One of the keys: drop its line and the lines that continue it.
265
+ while (i + 2 < closeAt && /^(?:[ \t]|-(?:[ \t]|$))/.test(parts[i + 2]))
266
+ i += 2;
267
+ }
268
+ const added = lines.flatMap((line) => [line, parts[1]]);
269
+ return [parts[0], parts[1], ...kept, ...added, ...parts.slice(closeAt)].join("");
210
270
  }
211
271
  /**
212
272
  * Strip one layer of matching quotes — frontmatter list items are often quoted
@@ -59,7 +59,7 @@ import { WorkflowConfigSchema } from "./schema/workflow.js";
59
59
  export { EmbeddingConnectionConfigSchema } from "./schema/embedding.js";
60
60
  export { EngineConfigSchema, EnginesSchema, LlmConnectionConfigSchema, LlmProfileConfigSchema } from "./schema/engines.js";
61
61
  export { ExperimentalConfigSchema } from "./schema/experimental.js";
62
- export { FEEDBACK_FAILURE_MODES, FeedbackConfigSchema } from "./schema/feedback.js";
62
+ export { FeedbackConfigSchema } from "./schema/feedback.js";
63
63
  export { ImproveConfigSchema } from "./schema/improve.js";
64
64
  export { ConsolidateProcessConfigSchema, DistillProcessConfigSchema, ExtractProcessConfigSchema, ImproveProcessConfigSchema, ImproveProfileConfigSchema, MemoryInferenceProcessConfigSchema, ProactiveMaintenanceProcessConfigSchema, ReflectProcessConfigSchema, TriageProcessConfigSchema, ValidationProcessConfigSchema, } from "./schema/improve-processes.js";
65
65
  export { IndexConfigSchema, IndexPassConfigSchema } from "./schema/index-config.js";
@@ -23,10 +23,6 @@ import { getConfigPath } from "../paths.js";
23
23
  import { warn, warnOnce } from "../warn.js";
24
24
  // Canonical harness-id source of truth (#565) — runtime value re-export.
25
25
  export { VALID_HARNESS_IDS } from "./config-types.js";
26
- // ── Feedback failure-mode constants (F-3 / #384) ────────────────────────────
27
- // Canonical taxonomy lives in the schema/validator layer; re-exported here so
28
- // existing `../core/config/config` import sites keep working.
29
- export { FEEDBACK_FAILURE_MODES } from "./config-schema.js";
30
26
  // ── Defaults ────────────────────────────────────────────────────────────────
31
27
  export const DEFAULT_CONFIG = {
32
28
  configVersion: "0.9.0",
@@ -2,30 +2,13 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * Feedback taxonomy + `feedback` config section. Extracted verbatim from the
6
- * former `config-schema.ts` monolith — no behavior change.
5
+ * `feedback` config section. The retired `allowedFailureModes` key (it went
6
+ * with `akm feedback --failure-mode`) is tolerated as an unknown key.
7
7
  */
8
8
  import { z } from "zod";
9
- import { nonEmptyString } from "./primitives.js";
10
- // ── Feedback failure modes (F-3 / #384) ─────────────────────────────────────
11
- /**
12
- * Curated taxonomy of failure modes for negative feedback.
13
- *
14
- * Structured failure modes enable aggregation across feedback events so the
15
- * distill pipeline can detect that "5 assets failed for the same reason" and
16
- * act on it — free-text strings about the same issue are not aggregatable.
17
- */
18
- export const FEEDBACK_FAILURE_MODES = [
19
- "incorrect", // Factually wrong or logically flawed content
20
- "outdated", // Correct at some point but now stale
21
- "dangerous", // Could cause harm if followed (security, safety)
22
- "incomplete", // Missing key steps, context, or caveats
23
- "redundant", // Duplicates another asset without adding value
24
- ];
25
9
  // ── Feedback ────────────────────────────────────────────────────────────────
26
10
  export const FeedbackConfigSchema = z
27
11
  .object({
28
12
  requireReason: z.boolean().optional(),
29
- allowedFailureModes: z.array(nonEmptyString).optional(),
30
13
  })
31
14
  .passthrough();
@@ -85,6 +85,7 @@ export function shapeProposalEntry(entry, detail) {
85
85
  "updatedAt",
86
86
  "confidence",
87
87
  "gateDecision",
88
+ "feedback",
88
89
  "review",
89
90
  "reviewHistory",
90
91
  "retirement",
@@ -101,6 +102,7 @@ export function shapeProposalEntry(entry, detail) {
101
102
  "updatedAt",
102
103
  "confidence",
103
104
  "gateDecision",
105
+ "feedback",
104
106
  "payload",
105
107
  "review",
106
108
  "reviewHistory",
@@ -183,6 +183,11 @@ export function formatProposalShowPlain(r) {
183
183
  if (gate.decidedAt)
184
184
  lines.push(`gate.decidedAt: ${String(gate.decidedAt)}`);
185
185
  }
186
+ const fix = p.feedback;
187
+ if (fix) {
188
+ lines.push(`feedback.reason: ${String(fix.reason)}`);
189
+ lines.push(`feedback.source: ${String(fix.source)}`);
190
+ }
186
191
  const review = p.review;
187
192
  if (review) {
188
193
  lines.push(`review.outcome: ${String(review.outcome ?? "?")}`);
@@ -28539,6 +28539,7 @@ function shapeProposalEntry(entry, detail) {
28539
28539
  "updatedAt",
28540
28540
  "confidence",
28541
28541
  "gateDecision",
28542
+ "feedback",
28542
28543
  "review",
28543
28544
  "reviewHistory",
28544
28545
  "retirement"
@@ -28554,6 +28555,7 @@ function shapeProposalEntry(entry, detail) {
28554
28555
  "updatedAt",
28555
28556
  "confidence",
28556
28557
  "gateDecision",
28558
+ "feedback",
28557
28559
  "payload",
28558
28560
  "review",
28559
28561
  "reviewHistory",
@@ -30491,6 +30493,11 @@ function formatProposalShowPlain(r) {
30491
30493
  if (gate.decidedAt)
30492
30494
  lines.push(`gate.decidedAt: ${String(gate.decidedAt)}`);
30493
30495
  }
30496
+ const fix = p.feedback;
30497
+ if (fix) {
30498
+ lines.push(`feedback.reason: ${String(fix.reason)}`);
30499
+ lines.push(`feedback.source: ${String(fix.source)}`);
30500
+ }
30494
30501
  const review = p.review;
30495
30502
  if (review) {
30496
30503
  lines.push(`review.outcome: ${String(review.outcome ?? "?")}`);
@@ -35838,8 +35845,7 @@ var ExperimentalConfigSchema = exports_external.object({
35838
35845
 
35839
35846
  // src/core/config/schema/feedback.ts
35840
35847
  var FeedbackConfigSchema = exports_external.object({
35841
- requireReason: exports_external.boolean().optional(),
35842
- allowedFailureModes: exports_external.array(nonEmptyString).optional()
35848
+ requireReason: exports_external.boolean().optional()
35843
35849
  }).passthrough();
35844
35850
 
35845
35851
  // src/core/config/schema/improve-processes.ts
@@ -27867,6 +27867,7 @@ function shapeProposalEntry(entry, detail) {
27867
27867
  "updatedAt",
27868
27868
  "confidence",
27869
27869
  "gateDecision",
27870
+ "feedback",
27870
27871
  "review",
27871
27872
  "reviewHistory",
27872
27873
  "retirement"
@@ -27882,6 +27883,7 @@ function shapeProposalEntry(entry, detail) {
27882
27883
  "updatedAt",
27883
27884
  "confidence",
27884
27885
  "gateDecision",
27886
+ "feedback",
27885
27887
  "payload",
27886
27888
  "review",
27887
27889
  "reviewHistory",
@@ -29819,6 +29821,11 @@ function formatProposalShowPlain(r) {
29819
29821
  if (gate.decidedAt)
29820
29822
  lines.push(`gate.decidedAt: ${String(gate.decidedAt)}`);
29821
29823
  }
29824
+ const fix = p.feedback;
29825
+ if (fix) {
29826
+ lines.push(`feedback.reason: ${String(fix.reason)}`);
29827
+ lines.push(`feedback.source: ${String(fix.source)}`);
29828
+ }
29822
29829
  const review = p.review;
29823
29830
  if (review) {
29824
29831
  lines.push(`review.outcome: ${String(review.outcome ?? "?")}`);
@@ -35166,8 +35173,7 @@ var ExperimentalConfigSchema = exports_external.object({
35166
35173
 
35167
35174
  // src/core/config/schema/feedback.ts
35168
35175
  var FeedbackConfigSchema = exports_external.object({
35169
- requireReason: exports_external.boolean().optional(),
35170
- allowedFailureModes: exports_external.array(nonEmptyString).optional()
35176
+ requireReason: exports_external.boolean().optional()
35171
35177
  }).passthrough();
35172
35178
 
35173
35179
  // src/core/config/schema/improve-processes.ts
@@ -171,6 +171,12 @@ function validatePresentMetadata(meta) {
171
171
  }
172
172
  }
173
173
  }
174
+ if (Object.hasOwn(meta, "feedback")) {
175
+ const fix = meta.feedback;
176
+ if (!isRecord(fix) || typeof fix.reason !== "string" || typeof fix.source !== "string") {
177
+ invalidPresentField("feedback");
178
+ }
179
+ }
174
180
  if (Object.hasOwn(meta, "acceptedTarget")) {
175
181
  const target = meta.acceptedTarget;
176
182
  if (typeof target !== "object" ||
@@ -293,6 +299,7 @@ export function proposalRowToProposal(row) {
293
299
  ...(meta.reviewHistory !== undefined ? { reviewHistory: meta.reviewHistory } : {}),
294
300
  ...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
295
301
  ...(meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {}),
302
+ ...(meta.feedback !== undefined ? { feedback: meta.feedback } : {}),
296
303
  ...(typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {}),
297
304
  ...(meta.acceptedTarget !== undefined ? { acceptedTarget: meta.acceptedTarget } : {}),
298
305
  ...(typeof meta.eligibilitySource === "string"
@@ -359,6 +366,8 @@ export function proposalToRowValues(proposal, stashDir) {
359
366
  metaObj.confidence = proposal.confidence;
360
367
  if (proposal.gateDecision !== undefined)
361
368
  metaObj.gateDecision = proposal.gateDecision;
369
+ if (proposal.feedback !== undefined)
370
+ metaObj.feedback = proposal.feedback;
362
371
  if (proposal.backupContent !== undefined)
363
372
  metaObj.backupContent = proposal.backupContent;
364
373
  if (proposal.acceptedTarget !== undefined)