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.
- package/CHANGELOG.md +189 -0
- package/dist/assets/hints/cli-hints-full.md +18 -8
- package/dist/assets/hints/cli-hints-short.md +6 -5
- package/dist/assets/stash-skeleton/README.md +6 -2
- package/dist/commands/feedback-cli.js +354 -31
- package/dist/commands/improve/distill.js +61 -2
- package/dist/commands/improve/loop-stages.js +16 -1
- package/dist/commands/improve/memory/memory-belief.js +1 -1
- package/dist/commands/improve/preparation.js +46 -0
- package/dist/commands/improve/reflect.js +15 -4
- package/dist/commands/proposal/repository.js +23 -6
- package/dist/core/asset/akm-markdown.js +40 -16
- package/dist/core/asset/frontmatter.js +67 -7
- package/dist/core/config/config-schema.js +1 -1
- package/dist/core/config/config.js +0 -4
- package/dist/core/config/schema/feedback.js +2 -19
- package/dist/output/shapes/helpers.js +2 -0
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/scripts/akm-migrate-node.js +8 -2
- package/dist/scripts/akm-migrate.js +8 -2
- package/dist/storage/repositories/proposals-repository.js +9 -0
- package/docs/reference/cli.md +43 -16
- package/docs/reference/configuration.md +7 -4
- package/docs/reference/data-and-telemetry.md +3 -3
- package/package.json +1 -1
- package/schemas/akm-config.json +0 -14
|
@@ -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
|
-
/**
|
|
53
|
-
|
|
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
|
-
|
|
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.
|
|
886
|
-
*
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 =
|
|
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
|
-
* (
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
206
|
-
if (
|
|
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
|
-
|
|
209
|
-
|
|
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 {
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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)
|