akm-cli 0.9.0-beta.35 → 0.9.0-beta.37
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 +35 -0
- package/dist/assets/prompts/distill-lesson-system.md +1 -1
- package/dist/assets/prompts/extract-session.md +1 -1
- package/dist/commands/improve/consolidate.js +12 -2
- package/dist/commands/improve/distill.js +18 -0
- package/dist/commands/improve/extract-prompt.js +6 -0
- package/dist/commands/improve/extract.js +17 -3
- package/dist/commands/improve/improve.js +45 -2
- package/dist/commands/improve/procedural.js +12 -3
- package/dist/commands/improve/recombine.js +11 -2
- package/dist/commands/improve/reflect.js +5 -0
- package/dist/commands/improve/salience.js +59 -7
- package/dist/commands/proposal/drain.js +7 -0
- package/dist/commands/proposal/propose.js +5 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +116 -3
- package/dist/commands/sources/schema-repair.js +13 -1
- package/dist/core/authoring-rules.js +83 -0
- package/dist/core/standards/resolve-standards-context.js +56 -0
- package/dist/core/standards/resolve-stash-standards.js +87 -0
- package/dist/core/state-db.js +25 -0
- package/dist/integrations/agent/prompts.js +32 -0
- package/dist/scripts/migrate-storage.js +6 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +6 -0
- package/dist/wiki/wiki.js +37 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0-beta.36] — 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Stash standards + wiki schemas are surfaced to authoring agents at write
|
|
14
|
+
time.** When an agent edits a page under `wikis/<name>/`, that wiki's
|
|
15
|
+
`schema.md` body is injected into the prompt; when it creates/edits a non-wiki
|
|
16
|
+
asset, the bodies of `category: convention`/`meta` `fact` assets are injected.
|
|
17
|
+
Two mutually-exclusive features selected by target type, sharing one
|
|
18
|
+
`standardsContext` prompt seam. Wired into reflect, propose, and every
|
|
19
|
+
improve authoring pass (distill, consolidate, recombine, procedural, extract,
|
|
20
|
+
schema-repair). (#642)
|
|
21
|
+
- **Unified, validator-sourced authoring-rules seam.** A new
|
|
22
|
+
`authoringRulesForType(type)` injects the hard authoring rules (no
|
|
23
|
+
pseudo-frontmatter in body, exactly two `---` fences, description/`when_to_use`
|
|
24
|
+
length + shape) into every authoring prompt. The numeric bounds live in one
|
|
25
|
+
module that the validators import, so the prompt can no longer drift from what
|
|
26
|
+
the gate enforces. (#645)
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **High-salience reflect lane now reflects each asset at most once.** The
|
|
31
|
+
`#608` admission gate lacked the cooldown its sibling high-retrieval gate has,
|
|
32
|
+
so zero-feedback assets were re-selected on every run (auto-accept emits
|
|
33
|
+
`promoted`, not `feedback`), burning LLM calls and churning assets. (#643)
|
|
34
|
+
- **Stuck validation-failing proposals no longer dead-end.** The triage drain no
|
|
35
|
+
longer overwrites an `auto-rejected` gate stamp with a misleading
|
|
36
|
+
`auto-accepted` (the failure stays truthful and visible). A bounded,
|
|
37
|
+
content-preserving auto-repair (strip pseudo-frontmatter / stray `---`, repair
|
|
38
|
+
truncated descriptions) runs at the promote boundary and re-validates — fixable
|
|
39
|
+
proposals promote; genuinely unrepairable ones stay `pending` for manual
|
|
40
|
+
review, with nothing fabricated and validation never bypassed. (#645)
|
|
41
|
+
- Corrected a prompt/validator drift where the distill system prompt asked for an
|
|
42
|
+
80–200 char description while the gate enforced 20–400. (#645)
|
|
43
|
+
|
|
9
44
|
## [0.9.0-beta.35] — 2026-06-21
|
|
10
45
|
|
|
11
46
|
### Fixed
|
|
@@ -15,7 +15,7 @@ when_to_use: <one complete sentence describing the concrete trigger condition>
|
|
|
15
15
|
<lesson body — plain markdown, 1–3 short paragraphs of practical guidance>
|
|
16
16
|
|
|
17
17
|
## description field (MANDATORY)
|
|
18
|
-
- A single complete sentence in present tense,
|
|
18
|
+
- A single complete sentence in present tense, 20–400 chars, NO markdown.
|
|
19
19
|
- Self-contained: a reviewer must understand the lesson from this field alone.
|
|
20
20
|
- DO NOT start with "When ", "If ", or a connector word — that belongs in when_to_use.
|
|
21
21
|
- DO NOT copy a section heading ("Key takeaways", "For example", "Key pitfalls").
|
|
@@ -28,7 +28,7 @@ Things NOT to extract:
|
|
|
28
28
|
- Started: {{STARTED_AT}}
|
|
29
29
|
- Ended: {{ENDED_AT}}
|
|
30
30
|
- Project hint: {{PROJECT_HINT}}
|
|
31
|
-
|
|
31
|
+
{{STANDARDS}}
|
|
32
32
|
## Filtered session transcript
|
|
33
33
|
|
|
34
34
|
The transcript below has already had read-only `akm` meta-ops and platform boilerplate stripped. Only content that might carry signal remains.
|
|
@@ -15,6 +15,7 @@ import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
|
15
15
|
import { ConfigError } from "../../core/errors.js";
|
|
16
16
|
// Note: appendEvent import removed (WS-3a: archive TTL machinery retired)
|
|
17
17
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
18
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
18
19
|
import { detectTruncatedDescription } from "../../core/text-truncation.js";
|
|
19
20
|
import { hasHotCaptureMode, hasSupersededStatus, MERGE_ABSOLUTE_FLOOR_CHARS, MERGE_SHRINK_RATIO_MIN, validateProposalFrontmatter, } from "../proposal/validators/proposal-quality-validators.js";
|
|
20
21
|
import { createProposal, isProposalSkipped, listProposals } from "../proposal/validators/proposals.js";
|
|
@@ -377,7 +378,7 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb) {
|
|
|
377
378
|
* is precomputed once per run by `loadPendingConsolidateProposalHashes`
|
|
378
379
|
* so the cost stays O(memories) inside the chunk loop.
|
|
379
380
|
*/
|
|
380
|
-
export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks, bodyTruncation, pendingProposalBodyHashes = new Set()) {
|
|
381
|
+
export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks, bodyTruncation, pendingProposalBodyHashes = new Set(), standardsContext = "") {
|
|
381
382
|
const start = memories[0] ? `memory:${memories[0].name}` : "";
|
|
382
383
|
const end = memories[memories.length - 1] ? `memory:${memories[memories.length - 1].name}` : "";
|
|
383
384
|
const annotationsByIndex = [];
|
|
@@ -405,6 +406,11 @@ export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks,
|
|
|
405
406
|
`Chunk ${chunkIndex + 1} of ${totalChunks}, memories ${start}–${end}:`,
|
|
406
407
|
"",
|
|
407
408
|
];
|
|
409
|
+
if (standardsContext.trim()) {
|
|
410
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
411
|
+
lines.push(standardsContext.trim());
|
|
412
|
+
lines.push("");
|
|
413
|
+
}
|
|
408
414
|
// Top-of-prompt protection block for hot refs. Neutral phrasing — avoid
|
|
409
415
|
// op-words like "promote", "merge", "contradict" so the model doesn't
|
|
410
416
|
// accidentally treat the warning as a hint to use that op elsewhere
|
|
@@ -1246,6 +1252,10 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
|
|
|
1246
1252
|
}
|
|
1247
1253
|
warn(`[consolidate] ${memories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
|
|
1248
1254
|
` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
|
|
1255
|
+
// Consolidate output merges memories (non-wiki) → stash authoring standards.
|
|
1256
|
+
// Resolved ONCE per run and passed to each chunk prompt (facts not re-read
|
|
1257
|
+
// per chunk).
|
|
1258
|
+
const standardsContext = resolveStashStandards(stashDir);
|
|
1249
1259
|
const chunkOpsArrays = [];
|
|
1250
1260
|
// Structured skip-reason histogram (2026-05-26): every deterministic
|
|
1251
1261
|
// post-LLM op rejection site below also calls `pushSkipReason` so the
|
|
@@ -1363,7 +1373,7 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
|
|
|
1363
1373
|
continue;
|
|
1364
1374
|
}
|
|
1365
1375
|
warn(`[consolidate] chunk ${chunkIdx + 1}/${chunks.length} (${chunk.length} memories) …`);
|
|
1366
|
-
const userPrompt = buildChunkPrompt(sourceName, chunk, chunkIdx, chunks.length, bodyTruncation, pendingProposalBodyHashes);
|
|
1376
|
+
const userPrompt = buildChunkPrompt(sourceName, chunk, chunkIdx, chunks.length, bodyTruncation, pendingProposalBodyHashes, standardsContext);
|
|
1367
1377
|
let raw = await tryLlmFeature("memory_consolidation", config, async () => {
|
|
1368
1378
|
if (!llmConfig)
|
|
1369
1379
|
return { ok: false, error: "No LLM configured for consolidation" };
|
|
@@ -58,12 +58,14 @@ import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
|
58
58
|
import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
|
|
59
59
|
import { parseFrontmatter, writeSalienceToFrontmatter } from "../../core/asset/frontmatter.js";
|
|
60
60
|
import { stripMarkdownFences } from "../../core/asset/markdown.js";
|
|
61
|
+
import { authoringRulesForType } from "../../core/authoring-rules.js";
|
|
61
62
|
import { resolveStashDir, timestampForFilename } from "../../core/common.js";
|
|
62
63
|
import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
63
64
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
64
65
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
65
66
|
import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
66
67
|
import { getDbPath } from "../../core/paths.js";
|
|
68
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
67
69
|
import { openStateDatabase } from "../../core/state-db.js";
|
|
68
70
|
import { warnVerbose } from "../../core/warn.js";
|
|
69
71
|
import { closeDatabase, getAllEntries, openDatabase } from "../../indexer/db/db.js";
|
|
@@ -307,6 +309,18 @@ export function buildDistillPrompt(input) {
|
|
|
307
309
|
const lines = [];
|
|
308
310
|
lines.push(`Asset ref: ${input.inputRef}`);
|
|
309
311
|
lines.push("");
|
|
312
|
+
if (input.standardsContext?.trim()) {
|
|
313
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
314
|
+
lines.push(input.standardsContext.trim());
|
|
315
|
+
lines.push("");
|
|
316
|
+
}
|
|
317
|
+
{
|
|
318
|
+
const authoringRules = authoringRulesForType(input.proposalKind ?? "lesson");
|
|
319
|
+
if (authoringRules) {
|
|
320
|
+
lines.push(authoringRules);
|
|
321
|
+
lines.push("");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
310
324
|
lines.push("Asset content:");
|
|
311
325
|
if (input.assetContent) {
|
|
312
326
|
const body = input.assetContent.trim().slice(0, 3000);
|
|
@@ -939,12 +953,16 @@ export async function akmDistill(options) {
|
|
|
939
953
|
// Fail open — CLS is supplemental, never required.
|
|
940
954
|
}
|
|
941
955
|
}
|
|
956
|
+
// Distill output is a lesson/knowledge (non-wiki) → stash authoring
|
|
957
|
+
// standards. Resolved once for this single call.
|
|
958
|
+
const standardsContext = resolveStashStandards(stash);
|
|
942
959
|
const baseUserPrompt = buildDistillPrompt({
|
|
943
960
|
inputRef,
|
|
944
961
|
assetContent,
|
|
945
962
|
feedback,
|
|
946
963
|
proposalKind: effectiveProposalKind,
|
|
947
964
|
...(rejectedForRef.length > 0 ? { rejectedProposals: rejectedForRef } : {}),
|
|
965
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
948
966
|
});
|
|
949
967
|
const userPrompt = clsContext ? `${baseUserPrompt}${clsContext}` : baseUserPrompt;
|
|
950
968
|
const messages = [
|
|
@@ -149,6 +149,11 @@ export function buildExtractPrompt(input) {
|
|
|
149
149
|
const ref = input.data.ref;
|
|
150
150
|
const startedAt = ref.startedAt ? new Date(ref.startedAt).toISOString() : "unknown";
|
|
151
151
|
const endedAt = ref.endedAt ? new Date(ref.endedAt).toISOString() : "unknown";
|
|
152
|
+
// Optional standards block — rendered to the lead-in + body when present,
|
|
153
|
+
// or an empty string (no section) when absent. Gated on non-empty.
|
|
154
|
+
const standards = input.standardsContext?.trim()
|
|
155
|
+
? `\n## Standards to follow (the rulebook for this target)\n\n${input.standardsContext.trim()}\n`
|
|
156
|
+
: "";
|
|
152
157
|
return promptTemplate
|
|
153
158
|
.replace("{{HARNESS}}", ref.harness)
|
|
154
159
|
.replace("{{TITLE}}", ref.title ?? "(no title)")
|
|
@@ -156,6 +161,7 @@ export function buildExtractPrompt(input) {
|
|
|
156
161
|
.replace("{{ENDED_AT}}", endedAt)
|
|
157
162
|
.replace("{{PROJECT_HINT}}", ref.projectHint ?? "(no project hint)")
|
|
158
163
|
.replace("{{ALREADY_PRESERVED}}", formatAlreadyPreserved(input.inlineRefs))
|
|
164
|
+
.replace("{{STANDARDS}}", standards)
|
|
159
165
|
.replace("{{TRANSCRIPT}}", formatTranscript(input.events));
|
|
160
166
|
}
|
|
161
167
|
/**
|
|
@@ -31,6 +31,7 @@ import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
|
31
31
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
32
32
|
import { appendEvent } from "../../core/events.js";
|
|
33
33
|
import { probeLock, releaseLock, tryAcquireLockSync } from "../../core/file-lock.js";
|
|
34
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
34
35
|
import { getExtractedSessionsMap, getLastExtractRunAt, getStateDbPath, openStateDatabase, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../core/state-db.js";
|
|
35
36
|
import { repairTruncatedDescription } from "../../core/text-truncation.js";
|
|
36
37
|
import { warn } from "../../core/warn.js";
|
|
@@ -265,7 +266,11 @@ triage, sessionIndexing, schemaSimilarityCtx,
|
|
|
265
266
|
// prior row + bypass flags are threaded in from the caller. Skipping here still
|
|
266
267
|
// costs ZERO LLM calls (the expensive resource #602 protects); only the cheap
|
|
267
268
|
// file read is incurred.
|
|
268
|
-
prior, force
|
|
269
|
+
prior, force,
|
|
270
|
+
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
271
|
+
// output. Resolved ONCE per run by the caller and threaded in so facts are
|
|
272
|
+
// not re-read per session. Empty string when none exist.
|
|
273
|
+
standardsContext) {
|
|
269
274
|
const warnings = [];
|
|
270
275
|
let data;
|
|
271
276
|
try {
|
|
@@ -360,7 +365,12 @@ prior, force) {
|
|
|
360
365
|
};
|
|
361
366
|
}
|
|
362
367
|
}
|
|
363
|
-
const prompt = buildExtractPrompt({
|
|
368
|
+
const prompt = buildExtractPrompt({
|
|
369
|
+
data,
|
|
370
|
+
events: filtered.events,
|
|
371
|
+
inlineRefs: data.inlineRefs,
|
|
372
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
373
|
+
});
|
|
364
374
|
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
365
375
|
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
|
|
366
376
|
// warning; it NEVER changes the proposal/skip outcome of extract. Returns the
|
|
@@ -771,6 +781,10 @@ export async function akmExtract(options) {
|
|
|
771
781
|
embedFn: options.schemaSimilarityEmbedFn,
|
|
772
782
|
};
|
|
773
783
|
}
|
|
784
|
+
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
785
|
+
// extract output. Resolved ONCE per run and threaded into each session's
|
|
786
|
+
// prompt so facts are not re-read per session.
|
|
787
|
+
const extractStandardsContext = resolveStashStandards(stashDir);
|
|
774
788
|
for (const summary of candidates) {
|
|
775
789
|
// #602 — the already-extracted skip moved INTO processSession (the content
|
|
776
790
|
// hash needs the session body, only available after readSession). The prior
|
|
@@ -810,7 +824,7 @@ export async function akmExtract(options) {
|
|
|
810
824
|
}
|
|
811
825
|
}
|
|
812
826
|
try {
|
|
813
|
-
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, prior, options.force === true);
|
|
827
|
+
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, prior, options.force === true, extractStandardsContext);
|
|
814
828
|
sessions.push(result);
|
|
815
829
|
// #626 — triage aggregation. A session reached the triage gate only when it
|
|
816
830
|
// was NOT already preempted by an earlier skip (read_failed / too_short /
|
|
@@ -54,7 +54,7 @@ import { DEFAULT_DUE_DAYS, DEFAULT_MAX_PER_RUN, filterProactiveDue, selectProact
|
|
|
54
54
|
import { akmProcedural } from "./procedural.js";
|
|
55
55
|
import { akmRecombine } from "./recombine.js";
|
|
56
56
|
import { akmReflect } from "./reflect.js";
|
|
57
|
-
import { buildRankChangeReport, computeSalience, getAllRankScores, getAssetSalience, getConsecutiveNoOps, getLastUseMsByRef, recordNoOp, resetConsecutiveNoOps, SALIENCE_NO_OP_DAMPEN_FACTOR, SALIENCE_NO_OP_DAMPEN_THRESHOLD, upsertAssetSalience, } from "./salience.js";
|
|
57
|
+
import { buildRankChangeReport, computeSalience, getAllRankScores, getAssetSalience, getConsecutiveNoOps, getLastUseMsByRef, isContentEncodingRow, recordNoOp, resetConsecutiveNoOps, SALIENCE_NO_OP_DAMPEN_FACTOR, SALIENCE_NO_OP_DAMPEN_THRESHOLD, upsertAssetSalience, } from "./salience.js";
|
|
58
58
|
// #607 Lock Decomposition: fine-grained per-process locks replace the single
|
|
59
59
|
// `improve.lock`. Three independent locks allow concurrent improve runs when
|
|
60
60
|
// they touch different subsystems (e.g. quick-shredder consolidate can run
|
|
@@ -2463,6 +2463,14 @@ async function runImprovePreparationStage(args) {
|
|
|
2463
2463
|
// Cap: at most 10% of the effective run limit so the lane cannot crowd out
|
|
2464
2464
|
// reactive feedback. Requires state.db to have an asset_salience row — refs
|
|
2465
2465
|
// without a row (pre-#608 assets still on the type-weight stub) are skipped.
|
|
2466
|
+
//
|
|
2467
|
+
// Cooldown: a ref qualifies at most once — when no prior reflect proposal
|
|
2468
|
+
// exists for it (`!lastReflectProposalTs.has`). Without this guard the lane
|
|
2469
|
+
// re-selects the same high-salience refs on EVERY run (auto-accept emits a
|
|
2470
|
+
// `promoted` event, not `feedback`, so the ref never leaves
|
|
2471
|
+
// noFeedbackCandidates), burning LLM calls and churning the asset. This
|
|
2472
|
+
// mirrors the P0-A high-retrieval gate's `!lastReflectProposalTs.has(r.ref)`
|
|
2473
|
+
// guard above so all "rescue" lanes share the same once-per-asset semantics.
|
|
2466
2474
|
const highSalienceRefs = [];
|
|
2467
2475
|
const salienceCfg = (options.config ?? loadConfig()).improve?.salience;
|
|
2468
2476
|
const salienceThreshold = salienceCfg?.salienceThreshold ?? 0.75;
|
|
@@ -2478,7 +2486,7 @@ async function runImprovePreparationStage(args) {
|
|
|
2478
2486
|
if (highSalienceRefs.length >= highSalienceCap)
|
|
2479
2487
|
break;
|
|
2480
2488
|
const row = getAssetSalience(dbForHighSalience, r.ref);
|
|
2481
|
-
if (row && row.encoding_salience >= salienceThreshold) {
|
|
2489
|
+
if (row && row.encoding_salience >= salienceThreshold && !lastReflectProposalTs.has(r.ref)) {
|
|
2482
2490
|
highSalienceRefs.push(r);
|
|
2483
2491
|
}
|
|
2484
2492
|
}
|
|
@@ -2725,6 +2733,37 @@ async function runImprovePreparationStage(args) {
|
|
|
2725
2733
|
const outcomeWeightEnabled = salienceConfig?.outcomeWeightEnabled === true;
|
|
2726
2734
|
const salienceMap = new Map();
|
|
2727
2735
|
const nowForSalience = Date.now();
|
|
2736
|
+
// #644 — preserve content-derived encoding scores across runs.
|
|
2737
|
+
//
|
|
2738
|
+
// Before computing the salience vector, load each ref's stored encoding score
|
|
2739
|
+
// and its provenance. When the stored row carries a genuine content-derived
|
|
2740
|
+
// score (written by the distill path via `scoreEncodingSalience`), pass that
|
|
2741
|
+
// value back in as `inputs.encodingSalience` so `computeSalience` does NOT fall
|
|
2742
|
+
// back to the type-weight stub — keeping both the persisted `encoding_salience`
|
|
2743
|
+
// AND the derived `rank_score` keyed on real novelty/magnitude/prediction-error.
|
|
2744
|
+
// Refs that have never been content-scored keep the type-weight stub fallback.
|
|
2745
|
+
const storedEncodingByRef = new Map();
|
|
2746
|
+
{
|
|
2747
|
+
let dbForStoredEncoding;
|
|
2748
|
+
try {
|
|
2749
|
+
dbForStoredEncoding = openStateDatabase(eventsCtx?.dbPath);
|
|
2750
|
+
for (const r of mergedRefs) {
|
|
2751
|
+
const type = r.ref.includes(":") ? r.ref.slice(0, r.ref.indexOf(":")) : "";
|
|
2752
|
+
const row = getAssetSalience(dbForStoredEncoding, r.ref);
|
|
2753
|
+
if (row && isContentEncodingRow(row, type)) {
|
|
2754
|
+
storedEncodingByRef.set(r.ref, row.encoding_salience);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
catch (err) {
|
|
2759
|
+
rethrowIfTestIsolationError(err);
|
|
2760
|
+
// best-effort: if DB unavailable, fall back to type-weight stub (prior behaviour)
|
|
2761
|
+
}
|
|
2762
|
+
finally {
|
|
2763
|
+
if (dbForStoredEncoding)
|
|
2764
|
+
dbForStoredEncoding.close();
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2728
2767
|
for (const r of mergedRefs) {
|
|
2729
2768
|
const type = r.ref.includes(":") ? r.ref.slice(0, r.ref.indexOf(":")) : "";
|
|
2730
2769
|
const sizeBytes = (() => {
|
|
@@ -2738,9 +2777,13 @@ async function runImprovePreparationStage(args) {
|
|
|
2738
2777
|
return undefined;
|
|
2739
2778
|
}
|
|
2740
2779
|
})();
|
|
2780
|
+
const storedEncoding = storedEncodingByRef.get(r.ref);
|
|
2741
2781
|
const vector = computeSalience({
|
|
2742
2782
|
ref: r.ref,
|
|
2743
2783
|
type,
|
|
2784
|
+
// #644: pass the stored content-derived score (if any) so the type-weight
|
|
2785
|
+
// stub is NOT re-asserted over a real distill-written encoding score.
|
|
2786
|
+
...(storedEncoding !== undefined ? { encodingSalience: storedEncoding } : {}),
|
|
2744
2787
|
retrievalFreq: retrievalCounts.get(r.ref) ?? 0,
|
|
2745
2788
|
lastUseMs: lastUseMsByRef.get(r.ref),
|
|
2746
2789
|
utilityScore: utilityMap.get(r.ref),
|
|
@@ -27,6 +27,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
27
27
|
import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
28
28
|
import { appendEvent } from "../../core/events.js";
|
|
29
29
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
30
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
30
31
|
import { warn } from "../../core/warn.js";
|
|
31
32
|
import { closeDatabase, getAllEntries, openExistingDatabase } from "../../indexer/db/db.js";
|
|
32
33
|
import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
|
|
@@ -144,12 +145,17 @@ export function deriveProceduralWorkflowRef(cluster) {
|
|
|
144
145
|
}
|
|
145
146
|
// ── Prompt + parse ──────────────────────────────────────────────────────────────
|
|
146
147
|
/** Assemble the per-sequence user prompt fed to the procedural LLM. */
|
|
147
|
-
export function buildProceduralPrompt(cluster) {
|
|
148
|
+
export function buildProceduralPrompt(cluster, standardsContext = "") {
|
|
148
149
|
const lines = [
|
|
149
150
|
`A recurring successful action sequence observed across ${cluster.members.length} sessions.`,
|
|
150
151
|
"",
|
|
151
|
-
"Ordered actions (turn EACH into exactly one step, in this order):",
|
|
152
152
|
];
|
|
153
|
+
if (standardsContext.trim()) {
|
|
154
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
155
|
+
lines.push(standardsContext.trim());
|
|
156
|
+
lines.push("");
|
|
157
|
+
}
|
|
158
|
+
lines.push("Ordered actions (turn EACH into exactly one step, in this order):");
|
|
153
159
|
cluster.normalized.forEach((step, i) => {
|
|
154
160
|
lines.push(`${i + 1}. ${step}`);
|
|
155
161
|
});
|
|
@@ -312,6 +318,9 @@ export async function akmProcedural(opts) {
|
|
|
312
318
|
warnings.push("procedural: no LLM configured — skipping");
|
|
313
319
|
return finish({ sequencesScanned, clustersFormed: 0 });
|
|
314
320
|
}
|
|
321
|
+
// Procedural output is a workflow (non-wiki) → stash authoring standards.
|
|
322
|
+
// Resolved ONCE per run and passed to each sequence prompt.
|
|
323
|
+
const standardsContext = resolveStashStandards(stashDir);
|
|
315
324
|
for (const cluster of clusters) {
|
|
316
325
|
if (opts.signal?.aborted) {
|
|
317
326
|
warnings.push("aborted-mid-run");
|
|
@@ -319,7 +328,7 @@ export async function akmProcedural(opts) {
|
|
|
319
328
|
}
|
|
320
329
|
clustersFormed += 1;
|
|
321
330
|
const workflowRef = deriveProceduralWorkflowRef(cluster);
|
|
322
|
-
const prompt = buildProceduralPrompt(cluster);
|
|
331
|
+
const prompt = buildProceduralPrompt(cluster, standardsContext);
|
|
323
332
|
const raw = await llmFn(prompt);
|
|
324
333
|
const doc = parseProceduralWorkflow(raw, cluster.normalized.length);
|
|
325
334
|
if (!doc) {
|
|
@@ -45,6 +45,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
45
45
|
import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
46
46
|
import { appendEvent } from "../../core/events.js";
|
|
47
47
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
48
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
48
49
|
import { decayUnseenRecombineHypotheses, findMatchingRecombineHypothesis, getRecombineHypothesis, getStateDbPath, markRecombineHypothesisPromoted, openStateDatabase, recordRecombineInduction, } from "../../core/state-db.js";
|
|
49
50
|
import { warn } from "../../core/warn.js";
|
|
50
51
|
import { closeDatabase, getAllEntries, getEntitiesByEntryIds, openExistingDatabase, } from "../../indexer/db/db.js";
|
|
@@ -228,12 +229,17 @@ function readBody(entry) {
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
/** Assemble the per-cluster user prompt fed to the recombine LLM. */
|
|
231
|
-
export function buildClusterPrompt(cluster) {
|
|
232
|
+
export function buildClusterPrompt(cluster, standardsContext = "") {
|
|
232
233
|
const lines = [
|
|
233
234
|
`Shared signal: ${cluster.signature}`,
|
|
234
235
|
`Cluster of ${cluster.members.length} related memories:`,
|
|
235
236
|
"",
|
|
236
237
|
];
|
|
238
|
+
if (standardsContext.trim()) {
|
|
239
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
240
|
+
lines.push(standardsContext.trim());
|
|
241
|
+
lines.push("");
|
|
242
|
+
}
|
|
237
243
|
for (const m of cluster.members) {
|
|
238
244
|
lines.push(`[memory:${m.entry.name}]`);
|
|
239
245
|
if (m.entry.description)
|
|
@@ -399,6 +405,9 @@ export async function akmRecombine(opts) {
|
|
|
399
405
|
// Refs re-induced (defensible generalization passed the quality gate) THIS
|
|
400
406
|
// run — everything else is decayed after the loop.
|
|
401
407
|
const seenThisRun = new Set();
|
|
408
|
+
// Recombine output is knowledge/lesson (non-wiki) → stash authoring
|
|
409
|
+
// standards. Resolved ONCE per run and passed to each cluster prompt.
|
|
410
|
+
const standardsContext = resolveStashStandards(stashDir);
|
|
402
411
|
try {
|
|
403
412
|
for (const cluster of clusters) {
|
|
404
413
|
if (opts.signal?.aborted) {
|
|
@@ -406,7 +415,7 @@ export async function akmRecombine(opts) {
|
|
|
406
415
|
break;
|
|
407
416
|
}
|
|
408
417
|
clustersFormed += 1;
|
|
409
|
-
const prompt = buildClusterPrompt(cluster);
|
|
418
|
+
const prompt = buildClusterPrompt(cluster, standardsContext);
|
|
410
419
|
const raw = await llmFn(prompt);
|
|
411
420
|
const generalization = parseGeneralization(raw);
|
|
412
421
|
if (!generalization) {
|
|
@@ -34,6 +34,7 @@ import { loadConfig } from "../../core/config/config.js";
|
|
|
34
34
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
35
35
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
36
36
|
import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
37
|
+
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
37
38
|
import { lookup } from "../../indexer/indexer.js";
|
|
38
39
|
import { runAgent, } from "../../integrations/agent/index.js";
|
|
39
40
|
import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
|
|
@@ -745,6 +746,9 @@ export async function akmReflect(options = {}) {
|
|
|
745
746
|
// Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
|
|
746
747
|
// reproducing proposals that have already been reviewed and refused.
|
|
747
748
|
const rejectedProposals = readRejectedProposals(stash, options.ref);
|
|
749
|
+
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
750
|
+
// convention/meta facts (non-wiki asset); empty when neither fires.
|
|
751
|
+
const standardsContext = resolveStandardsContext(options.ref, stash);
|
|
748
752
|
// 5. Spawn the agent — with optional Self-Refine loop (R-1 / #372).
|
|
749
753
|
//
|
|
750
754
|
// maxRefineIters controls how many agent invocations are made:
|
|
@@ -813,6 +817,7 @@ export async function akmReflect(options = {}) {
|
|
|
813
817
|
...(schemaHints.length > 0 ? { schemaHints } : {}),
|
|
814
818
|
...(relatedLessons.length > 0 ? { relatedLessons } : {}),
|
|
815
819
|
...(options.task ? { task: options.task } : {}),
|
|
820
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
816
821
|
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
817
822
|
...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
|
|
818
823
|
// R-1: inject prior draft as self-critique target on iterations > 0
|
|
@@ -126,6 +126,7 @@ export function computeSalience(inputs) {
|
|
|
126
126
|
// Fall back to the type-importance stub only when the caller has not yet
|
|
127
127
|
// computed a content-based score (e.g. on older assets before the first
|
|
128
128
|
// staleness refresh runs).
|
|
129
|
+
const encodingSource = inputs.encodingSalience !== undefined ? "content" : "type-stub";
|
|
129
130
|
const encoding = inputs.encodingSalience !== undefined
|
|
130
131
|
? Math.min(1, Math.max(0, inputs.encodingSalience))
|
|
131
132
|
: (DEFAULT_TYPE_ENCODING_WEIGHTS[inputs.type] ?? DEFAULT_ENCODING_SALIENCE);
|
|
@@ -213,24 +214,75 @@ export function computeSalience(inputs) {
|
|
|
213
214
|
}
|
|
214
215
|
const rawRankScore = (we * encoding + wo * outcome + wr * retrieval) * sizePenalty;
|
|
215
216
|
const rankScore = Math.min(1, Math.max(0, rawRankScore));
|
|
216
|
-
return { encoding, outcome, retrieval, rankScore };
|
|
217
|
+
return { encoding, outcome, retrieval, rankScore, encodingSource };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Does this row carry a genuine content-derived `encoding_salience` (#644)?
|
|
221
|
+
*
|
|
222
|
+
* Returns true when the provenance flag is `"content"`. For legacy rows
|
|
223
|
+
* (`encoding_source === null`, written before migration 015) we apply a
|
|
224
|
+
* conservative heuristic: treat the stored value as content-derived only when it
|
|
225
|
+
* does NOT equal the pure type-weight stub for the asset's type — because before
|
|
226
|
+
* the #644 fix every run overwrote real scores with the stub, a value that still
|
|
227
|
+
* differs from the stub must have been content-written and never re-clobbered.
|
|
228
|
+
* When the type cannot be determined (no `type` given) a null-provenance row is
|
|
229
|
+
* treated as a stub (the safe default).
|
|
230
|
+
*/
|
|
231
|
+
export function isContentEncodingRow(row, type) {
|
|
232
|
+
if (row.encoding_source === "content")
|
|
233
|
+
return true;
|
|
234
|
+
if (row.encoding_source === "type-stub")
|
|
235
|
+
return false;
|
|
236
|
+
// Legacy NULL provenance: differ-from-stub heuristic.
|
|
237
|
+
if (!type)
|
|
238
|
+
return false;
|
|
239
|
+
const stub = DEFAULT_TYPE_ENCODING_WEIGHTS[type] ?? DEFAULT_ENCODING_SALIENCE;
|
|
240
|
+
return Math.abs(row.encoding_salience - stub) > 1e-9;
|
|
217
241
|
}
|
|
218
242
|
/**
|
|
219
243
|
* Upsert salience scores for one asset into state.db.
|
|
220
244
|
*
|
|
221
|
-
* Idempotent: safe to call every run; updates
|
|
245
|
+
* Idempotent: safe to call every run; updates the outcome / retrieval / rank
|
|
246
|
+
* columns on conflict.
|
|
247
|
+
*
|
|
248
|
+
* #644 — encoding provenance guard: the `encoding_salience` + `encoding_source`
|
|
249
|
+
* columns are NOT lowered from a real content-derived score to a type-weight
|
|
250
|
+
* stub. When the stored row is `encoding_source = 'content'` and the incoming
|
|
251
|
+
* vector is a `type-stub` fallback, the stored encoding score and its provenance
|
|
252
|
+
* are preserved (only the other sub-scores and `rank_score` advance). A `content`
|
|
253
|
+
* write always wins; a `type-stub` write only seeds a row that has no content
|
|
254
|
+
* score yet. This stops the improve loop's type-weight fallback re-asserting the
|
|
255
|
+
* stub over a distill-written score on every run.
|
|
256
|
+
*
|
|
257
|
+
* NOTE: when the guard preserves the stored encoding score, the incoming
|
|
258
|
+
* `vector.rankScore` (computed from the stub encoding) is still written. Callers
|
|
259
|
+
* that want the rank_score to reflect the preserved content score should pass the
|
|
260
|
+
* stored content score back in as `inputs.encodingSalience` to `computeSalience`
|
|
261
|
+
* — which the improve loop does. The guard here is the defensive backstop.
|
|
222
262
|
*/
|
|
223
263
|
export function upsertAssetSalience(db, ref, vector, now) {
|
|
224
264
|
const ts = now ?? Date.now();
|
|
225
265
|
db.prepare(`INSERT INTO asset_salience
|
|
226
|
-
(asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at)
|
|
227
|
-
VALUES (?, ?, ?, ?, ?, 0, ?)
|
|
266
|
+
(asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at, encoding_source)
|
|
267
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ?)
|
|
228
268
|
ON CONFLICT(asset_ref) DO UPDATE SET
|
|
229
|
-
|
|
269
|
+
-- #644: never lower a real content-derived score to a type-weight stub.
|
|
270
|
+
-- Keep the stored encoding score + provenance when the stored row is
|
|
271
|
+
-- 'content' and the incoming write is a 'type-stub' fallback.
|
|
272
|
+
encoding_salience = CASE
|
|
273
|
+
WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
|
|
274
|
+
THEN asset_salience.encoding_salience
|
|
275
|
+
ELSE excluded.encoding_salience
|
|
276
|
+
END,
|
|
277
|
+
encoding_source = CASE
|
|
278
|
+
WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
|
|
279
|
+
THEN asset_salience.encoding_source
|
|
280
|
+
ELSE excluded.encoding_source
|
|
281
|
+
END,
|
|
230
282
|
outcome_salience = excluded.outcome_salience,
|
|
231
283
|
retrieval_salience = excluded.retrieval_salience,
|
|
232
284
|
rank_score = excluded.rank_score,
|
|
233
|
-
updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts);
|
|
285
|
+
updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts, vector.encodingSource ?? "type-stub");
|
|
234
286
|
}
|
|
235
287
|
/**
|
|
236
288
|
* Load the salience row for one asset, or undefined if not yet computed.
|
|
@@ -238,7 +290,7 @@ export function upsertAssetSalience(db, ref, vector, now) {
|
|
|
238
290
|
export function getAssetSalience(db, ref) {
|
|
239
291
|
const row = db
|
|
240
292
|
.prepare(`SELECT asset_ref, encoding_salience, outcome_salience, retrieval_salience,
|
|
241
|
-
rank_score, consecutive_no_ops, updated_at
|
|
293
|
+
rank_score, consecutive_no_ops, updated_at, encoding_source
|
|
242
294
|
FROM asset_salience WHERE asset_ref = ?`)
|
|
243
295
|
.get(ref);
|
|
244
296
|
// Bun SQLite returns null (not undefined) when no row found.
|
|
@@ -370,6 +370,13 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
370
370
|
// — these are re-stamped `no-judge-configured` when no runner resolves them.
|
|
371
371
|
const needsJudge = new Set();
|
|
372
372
|
for (const proposal of pending) {
|
|
373
|
+
// Do NOT reclassify a proposal that was already conclusively stamped
|
|
374
|
+
// `auto-rejected` by a prior gate run (e.g. the improve confidence gate).
|
|
375
|
+
// Overwriting an authoritative rejection with `auto-accepted` would corrupt
|
|
376
|
+
// the audit trail and silently promote content the gate explicitly rejected.
|
|
377
|
+
// Such proposals remain pending for manual review (or TTL expiry).
|
|
378
|
+
if (proposal.gateDecision?.outcome === "auto-rejected")
|
|
379
|
+
continue;
|
|
373
380
|
const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
|
|
374
381
|
if (decision === null)
|
|
375
382
|
continue;
|
|
@@ -18,6 +18,7 @@ import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
|
18
18
|
import { resolveStashDir } from "../../core/common.js";
|
|
19
19
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
20
20
|
import { appendEvent } from "../../core/events.js";
|
|
21
|
+
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
21
22
|
import { runAgent, } from "../../integrations/agent/index.js";
|
|
22
23
|
import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
|
|
23
24
|
import { buildProposePrompt, parseAgentProposalPayload } from "../../integrations/agent/prompts.js";
|
|
@@ -93,10 +94,14 @@ export async function akmPropose(options) {
|
|
|
93
94
|
// directly using its file tools rather than returning JSON via stdout.
|
|
94
95
|
const draftFilePath = import("node:os").then((os) => import("node:path").then((path) => path.join(os.tmpdir(), `akm-propose-${options.type}-${options.name.replace(/[^a-z0-9_-]/gi, "_")}-${Date.now()}.md`)));
|
|
95
96
|
const resolvedDraftPath = await draftFilePath;
|
|
97
|
+
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
98
|
+
// convention/meta facts (non-wiki asset); empty when neither fires.
|
|
99
|
+
const standardsContext = resolveStandardsContext(`${options.type}:${options.name}`, stash);
|
|
96
100
|
const prompt = buildProposePrompt({
|
|
97
101
|
type: options.type,
|
|
98
102
|
name: options.name,
|
|
99
103
|
task: options.task,
|
|
104
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
100
105
|
draftFilePath: resolvedDraftPath,
|
|
101
106
|
});
|
|
102
107
|
// 4. Spawn the agent.
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
*/
|
|
60
60
|
// ── Reflect-size guard ───────────────────────────────────────────────────────
|
|
61
61
|
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
62
|
+
import { DESCRIPTION_MAX_CHARS, DESCRIPTION_MIN_CHARS, WHEN_TO_USE_MAX_CHARS, WHEN_TO_USE_MIN_CHARS, } from "../../../core/authoring-rules.js";
|
|
62
63
|
import { detectTruncatedDescription, TRUNCATION_TRAILING_WORDS } from "../../../core/text-truncation.js";
|
|
63
64
|
// ── Description / when_to_use shape ─────────────────────────────────────────
|
|
64
65
|
export const HEADING_FRAGMENT_PATTERNS = [
|
|
@@ -80,10 +81,10 @@ export function isValidDescription(value, inputRef, options = {}) {
|
|
|
80
81
|
const v = value.trim();
|
|
81
82
|
if (!v)
|
|
82
83
|
return { ok: false, reason: "description is empty" };
|
|
83
|
-
if (v.length <
|
|
84
|
-
return { ok: false, reason: `description is too short (${v.length} chars; need
|
|
85
|
-
if (v.length >
|
|
86
|
-
return { ok: false, reason: `description is too long (${v.length} chars; max
|
|
84
|
+
if (v.length < DESCRIPTION_MIN_CHARS)
|
|
85
|
+
return { ok: false, reason: `description is too short (${v.length} chars; need ≥${DESCRIPTION_MIN_CHARS})` };
|
|
86
|
+
if (v.length > DESCRIPTION_MAX_CHARS)
|
|
87
|
+
return { ok: false, reason: `description is too long (${v.length} chars; max ${DESCRIPTION_MAX_CHARS})` };
|
|
87
88
|
if (/^\s*[\d#*\->`]/.test(v))
|
|
88
89
|
return { ok: false, reason: "description starts with a digit or markdown marker" };
|
|
89
90
|
const last = v.slice(-1);
|
|
@@ -133,10 +134,10 @@ export function isValidWhenToUse(value, inputRef) {
|
|
|
133
134
|
const v = value.trim();
|
|
134
135
|
if (!v)
|
|
135
136
|
return { ok: false, reason: "when_to_use is empty" };
|
|
136
|
-
if (v.length <
|
|
137
|
-
return { ok: false, reason: `when_to_use is too short (${v.length} chars; need
|
|
138
|
-
if (v.length >
|
|
139
|
-
return { ok: false, reason: `when_to_use is too long (${v.length} chars; max
|
|
137
|
+
if (v.length < WHEN_TO_USE_MIN_CHARS)
|
|
138
|
+
return { ok: false, reason: `when_to_use is too short (${v.length} chars; need ≥${WHEN_TO_USE_MIN_CHARS})` };
|
|
139
|
+
if (v.length > WHEN_TO_USE_MAX_CHARS)
|
|
140
|
+
return { ok: false, reason: `when_to_use is too long (${v.length} chars; max ${WHEN_TO_USE_MAX_CHARS})` };
|
|
140
141
|
if (/^when working with\b/i.test(v))
|
|
141
142
|
return { ok: false, reason: "when_to_use is the circular 'When working with ...' fallback" };
|
|
142
143
|
const refTail = inputRef.split(":").pop()?.toLowerCase() ?? "";
|
|
@@ -51,6 +51,7 @@ import { resolveAssetPathFromName, TYPE_DIRS } from "../../../core/asset/asset-s
|
|
|
51
51
|
import { NotFoundError, UsageError } from "../../../core/errors.js";
|
|
52
52
|
import { appendEvent } from "../../../core/events.js";
|
|
53
53
|
import { getStateDbPath, getStateProposal, hasImportedFsProposals, insertProposalIfAbsent, listStateProposalIdsByPrefix, listStateProposals, openStateDatabase, recordFsProposalsImport, upsertProposal, withImmediateTransaction, } from "../../../core/state-db.js";
|
|
54
|
+
import { repairTruncatedDescription } from "../../../core/text-truncation.js";
|
|
54
55
|
import { warn } from "../../../core/warn.js";
|
|
55
56
|
import { commitWriteTargetBoundary, formatRefForMessage, resolveWriteTarget, writeAssetToSource, } from "../../../core/write-source.js";
|
|
56
57
|
import { runProposalValidators } from "./proposal-validators.js";
|
|
@@ -716,6 +717,99 @@ export function expireStaleProposals(stashDir, config, ctx) {
|
|
|
716
717
|
export function validateProposal(proposal) {
|
|
717
718
|
return runProposalValidators(proposal);
|
|
718
719
|
}
|
|
720
|
+
// ── Content repair ──────────────────────────────────────────────────────────
|
|
721
|
+
/**
|
|
722
|
+
* Attempt bounded, deterministic repair of mechanically-fixable defects in a
|
|
723
|
+
* proposal's markdown content. NEVER fabricates text — only strips known-bad
|
|
724
|
+
* structure and applies {@link repairTruncatedDescription} to a truncated
|
|
725
|
+
* description when one is detected.
|
|
726
|
+
*
|
|
727
|
+
* Repairs performed (in order):
|
|
728
|
+
* 1. Strip body lines that restate frontmatter fields as pseudo-frontmatter
|
|
729
|
+
* (e.g. `**description**: …` or `when_to_use: …` in the body).
|
|
730
|
+
* 2. Remove stray body `---` horizontal-rule lines (leaving exactly the two
|
|
731
|
+
* frontmatter fences when the content has a valid frontmatter block).
|
|
732
|
+
* 3. Apply {@link repairTruncatedDescription} to a truncated/hanging
|
|
733
|
+
* `description` field in the frontmatter.
|
|
734
|
+
*
|
|
735
|
+
* Returns the repaired content string. When no repairs apply the input is
|
|
736
|
+
* returned byte-identical so callers can use strict equality to detect
|
|
737
|
+
* whether a repair actually happened.
|
|
738
|
+
*
|
|
739
|
+
* CRITICAL: This function is CONTENT-PRESERVING. Callers MUST re-validate the
|
|
740
|
+
* repaired output via {@link validateProposal} / {@link runProposalValidators}
|
|
741
|
+
* before promotion — a repair that makes things *worse* (or is simply
|
|
742
|
+
* insufficient) must be caught by the existing gate.
|
|
743
|
+
*/
|
|
744
|
+
export function repairProposalContent(content) {
|
|
745
|
+
if (typeof content !== "string" || content.trim() === "")
|
|
746
|
+
return content;
|
|
747
|
+
// Determine whether the content has a frontmatter block so we know how
|
|
748
|
+
// many `---` fence lines are expected.
|
|
749
|
+
const hasFrontmatter = /^---\r?\n[\s\S]*?\r?\n---/.test(content);
|
|
750
|
+
// Split into lines for structural repairs.
|
|
751
|
+
const lines = content.split(/\r?\n/);
|
|
752
|
+
// Track whether we are inside the opening frontmatter block so we can
|
|
753
|
+
// leave it untouched and only repair the body.
|
|
754
|
+
let inFrontmatter = false;
|
|
755
|
+
// Frontmatter fence index tracking: first fence opens FM, second closes it.
|
|
756
|
+
let fmOpenSeen = false;
|
|
757
|
+
let fmCloseSeen = false;
|
|
758
|
+
const repairedLines = [];
|
|
759
|
+
for (const line of lines) {
|
|
760
|
+
const isFence = /^---\s*$/.test(line);
|
|
761
|
+
// Track frontmatter fences (first two `---` fences delimit the FM block).
|
|
762
|
+
if (isFence && !fmCloseSeen) {
|
|
763
|
+
if (!fmOpenSeen) {
|
|
764
|
+
fmOpenSeen = true;
|
|
765
|
+
inFrontmatter = true;
|
|
766
|
+
repairedLines.push(line);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (inFrontmatter) {
|
|
770
|
+
fmCloseSeen = true;
|
|
771
|
+
inFrontmatter = false;
|
|
772
|
+
repairedLines.push(line);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// We are now in the body (past the frontmatter or no frontmatter).
|
|
777
|
+
if (inFrontmatter) {
|
|
778
|
+
// Still inside the frontmatter — keep as-is.
|
|
779
|
+
repairedLines.push(line);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
// Repair 1: Strip pseudo-frontmatter restatements in the body.
|
|
783
|
+
// Matches lines like `**description**: …` or `when_to_use: …`.
|
|
784
|
+
if (/^\s*(\*\*|__)?\s*(description|when_to_use)\s*(\*\*|__)?\s*:/i.test(line)) {
|
|
785
|
+
// Drop the line — it is a structural defect, not user content.
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
// Repair 2: Remove stray `---` horizontal-rule lines in the body.
|
|
789
|
+
// We keep these only when the content has NO frontmatter (in that case
|
|
790
|
+
// `---` is a legitimate thematic break in plain-body content).
|
|
791
|
+
if (isFence && hasFrontmatter) {
|
|
792
|
+
// Drop: these are extra `---` fences beyond the two frontmatter delimiters.
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
repairedLines.push(line);
|
|
796
|
+
}
|
|
797
|
+
let repaired = repairedLines.join("\n");
|
|
798
|
+
// Repair 3: Apply repairTruncatedDescription to the description field.
|
|
799
|
+
// We operate on the raw text rather than re-parsing YAML to avoid
|
|
800
|
+
// reformatting unrelated frontmatter keys.
|
|
801
|
+
if (hasFrontmatter) {
|
|
802
|
+
// Extract the body text (after the second `---`) so we can pass it to
|
|
803
|
+
// repairTruncatedDescription as context for the swap-in heuristic.
|
|
804
|
+
const bodyMatch = repaired.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
|
|
805
|
+
const bodyText = bodyMatch?.[1] ?? "";
|
|
806
|
+
repaired = repaired.replace(/^(description:\s*)(.*?)(\r?\n)/m, (_match, prefix, rawDesc, nl) => {
|
|
807
|
+
const fixed = repairTruncatedDescription(rawDesc.trim(), bodyText);
|
|
808
|
+
return `${prefix}${fixed}${nl}`;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return repaired;
|
|
812
|
+
}
|
|
719
813
|
/**
|
|
720
814
|
* Validate a proposal, then promote it through the canonical
|
|
721
815
|
* {@link writeAssetToSource} dispatch (the single place that branches on
|
|
@@ -733,12 +827,31 @@ export async function promoteProposal(stashDir, config, id, options = {}, ctx) {
|
|
|
733
827
|
if (proposal.status !== "pending") {
|
|
734
828
|
throw new UsageError(`Proposal ${id} is not pending (current status: ${proposal.status}). Only pending proposals can be accepted.`, "INVALID_FLAG_VALUE");
|
|
735
829
|
}
|
|
736
|
-
|
|
830
|
+
// Attempt bounded auto-repair of mechanically-fixable structural defects
|
|
831
|
+
// (pseudo-frontmatter-in-body, stray `---` fences, truncated description)
|
|
832
|
+
// BEFORE running validation. If the repair produces valid content, we
|
|
833
|
+
// promote the repaired version; if validation still fails, the original
|
|
834
|
+
// error path throws as before. The repair is content-preserving and
|
|
835
|
+
// deterministic — it never invents text.
|
|
836
|
+
const repairedContent = repairProposalContent(proposal.payload.content);
|
|
837
|
+
const proposalToValidate = repairedContent !== proposal.payload.content
|
|
838
|
+
? { ...proposal, payload: { ...proposal.payload, content: repairedContent } }
|
|
839
|
+
: proposal;
|
|
840
|
+
const report = validateProposal(proposalToValidate);
|
|
737
841
|
if (!report.ok) {
|
|
738
842
|
const message = report.findings.map((f) => `[${f.kind}] ${f.message}`).join("\n");
|
|
739
843
|
throw new UsageError(`Proposal ${id} failed validation:\n${message}`, "MISSING_REQUIRED_ARGUMENT", "Fix the proposal payload (frontmatter / content) and try again, or reject the proposal with a reason.");
|
|
740
844
|
}
|
|
741
|
-
|
|
845
|
+
// Use the (possibly repaired) payload for the promotion write. Persist the
|
|
846
|
+
// repaired content back onto the DB row so the audit trail reflects the
|
|
847
|
+
// final promoted payload (not the defective original).
|
|
848
|
+
if (repairedContent !== proposal.payload.content) {
|
|
849
|
+
withProposalsDb(stashDir, ctx, (db) => {
|
|
850
|
+
const updated = { ...proposal, payload: { ...proposal.payload, content: repairedContent } };
|
|
851
|
+
upsertProposal(db, updated, stashDir);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const ref = parseAssetRef(proposalToValidate.ref);
|
|
742
855
|
if (!TYPE_DIRS[ref.type]) {
|
|
743
856
|
throw new UsageError(`Proposal ${id} targets unknown asset type "${ref.type}".`, "INVALID_FLAG_VALUE");
|
|
744
857
|
}
|
|
@@ -760,7 +873,7 @@ export async function promoteProposal(stashDir, config, id, options = {}, ctx) {
|
|
|
760
873
|
// missing-revert path is visible.
|
|
761
874
|
warn(`[proposals] promoteProposal: failed to capture backup for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
762
875
|
}
|
|
763
|
-
const written = await writeAssetToSource(target.source, target.config, ref,
|
|
876
|
+
const written = await writeAssetToSource(target.source, target.config, ref, repairedContent);
|
|
764
877
|
// 0.9.0 (issue #507): single batch commit at the write boundary for git
|
|
765
878
|
// targets. No-op for filesystem/primary-stash targets.
|
|
766
879
|
commitWriteTargetBoundary(target, `Update ${formatRefForMessage(ref)}`);
|
|
@@ -17,7 +17,9 @@ import path from "node:path";
|
|
|
17
17
|
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
18
18
|
import { assembleAsset } from "../../core/asset/asset-serialize.js";
|
|
19
19
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
20
|
+
import { authoringRulesForType } from "../../core/authoring-rules.js";
|
|
20
21
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
22
|
+
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
21
23
|
import { info, warn } from "../../core/warn.js";
|
|
22
24
|
import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
|
|
23
25
|
import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
|
|
@@ -94,6 +96,16 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
94
96
|
const fieldList = missingFields.join(" and ");
|
|
95
97
|
info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
|
|
96
98
|
const bodyPreview = (fm.content ?? raw).slice(0, 2000);
|
|
99
|
+
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
100
|
+
// convention/meta facts (non-wiki asset); empty when neither fires or no
|
|
101
|
+
// stash dir is available. `resolveStandardsContext` dispatches on the ref.
|
|
102
|
+
const standardsContext = stashDir ? resolveStandardsContext(failure.ref, stashDir) : "";
|
|
103
|
+
const standardsSection = standardsContext.trim()
|
|
104
|
+
? `\n\nStandards to follow (the rulebook for this target):\n${standardsContext.trim()}`
|
|
105
|
+
: "";
|
|
106
|
+
const assetType = parseAssetRef(failure.ref).type;
|
|
107
|
+
const authoringRules = authoringRulesForType(assetType);
|
|
108
|
+
const authoringRulesSection = authoringRules ? `\n\n${authoringRules}` : "";
|
|
97
109
|
const llmResponse = await chatFn(llmConfig, [
|
|
98
110
|
{
|
|
99
111
|
role: "system",
|
|
@@ -101,7 +113,7 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
101
113
|
},
|
|
102
114
|
{
|
|
103
115
|
role: "user",
|
|
104
|
-
content: `Generate the missing frontmatter fields (${fieldList}) for this ${
|
|
116
|
+
content: `Generate the missing frontmatter fields (${fieldList}) for this ${assetType} asset. Return ONLY valid JSON like {"description": "...", "when_to_use": "..."}${standardsSection}${authoringRulesSection}\n\n${bodyPreview}`,
|
|
105
117
|
},
|
|
106
118
|
]);
|
|
107
119
|
const parsed = parseEmbeddedJsonResponse(llmResponse.trim());
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Canonical HARD authoring rules — the single source of truth shared by the
|
|
6
|
+
* proposal validators (which REJECT violations) and the improve/authoring
|
|
7
|
+
* prompts (which must TELL the agent the same rules, in the same words).
|
|
8
|
+
*
|
|
9
|
+
* Why this module exists: authoring rules were duplicated and drifted across
|
|
10
|
+
* prompt templates. `distill-lesson-system.md` told the model "80–200 chars"
|
|
11
|
+
* while the validator enforced 20–400; reflect's prompt omitted the
|
|
12
|
+
* no-pseudo-frontmatter / single-fence rules entirely, so reflect generated
|
|
13
|
+
* proposals that the gate then rejected and that got stuck in the queue.
|
|
14
|
+
*
|
|
15
|
+
* The fix: the numeric bounds live HERE and are imported by both the validators
|
|
16
|
+
* (`isValidDescription` / `isValidWhenToUse` in proposal-quality-validators.ts)
|
|
17
|
+
* and the prompt text (`authoringRulesForType`). The agent-facing rule prose
|
|
18
|
+
* sits next to the bounds it describes, so a developer changing a validator
|
|
19
|
+
* sees the prompt copy that must change with it. `tests/authoring-rules-*`
|
|
20
|
+
* asserts the two representations stay consistent.
|
|
21
|
+
*
|
|
22
|
+
* SCOPE: only HARD rules (a validator rejects the proposal if violated) belong
|
|
23
|
+
* here. Soft/style conventions (voice, paragraph count, "include a # Title")
|
|
24
|
+
* are user-editable and flow through the separate `standardsContext` seam
|
|
25
|
+
* (stash `category: convention` facts) — NOT this module.
|
|
26
|
+
*/
|
|
27
|
+
// ── Canonical numeric bounds (imported by the validators — do not duplicate) ──
|
|
28
|
+
/** `description` length bounds (chars). Enforced by `isValidDescription`. */
|
|
29
|
+
export const DESCRIPTION_MIN_CHARS = 20;
|
|
30
|
+
export const DESCRIPTION_MAX_CHARS = 400;
|
|
31
|
+
/** `when_to_use` length bounds (chars). Enforced by `isValidWhenToUse`. */
|
|
32
|
+
export const WHEN_TO_USE_MIN_CHARS = 15;
|
|
33
|
+
export const WHEN_TO_USE_MAX_CHARS = 400;
|
|
34
|
+
// ── Agent-facing rule prose (mirrors the validator checks one-for-one) ────────
|
|
35
|
+
/**
|
|
36
|
+
* Rules that apply to any markdown asset authored with YAML frontmatter + a
|
|
37
|
+
* body. Enforced by `detectDoubleFrontmatter` (currently fires for lesson
|
|
38
|
+
* proposals, but the rules are universally correct, so we state them for every
|
|
39
|
+
* type to prevent the same defect class elsewhere).
|
|
40
|
+
*/
|
|
41
|
+
const FRONTMATTER_BODY_RULES = [
|
|
42
|
+
"Emit EXACTLY TWO `---` fence lines — the opening and closing of the YAML frontmatter. Do NOT use `---` as a horizontal rule anywhere in the body.",
|
|
43
|
+
"Do NOT restate `description:` or `when_to_use:` inside the body (no `**description:** …` or `**when_to_use:** …` lines). Those keys belong in the frontmatter ONLY.",
|
|
44
|
+
];
|
|
45
|
+
/** Rules for the `description` frontmatter field. Enforced by `isValidDescription`. */
|
|
46
|
+
const DESCRIPTION_RULES = [
|
|
47
|
+
`\`description\` must be ${DESCRIPTION_MIN_CHARS}–${DESCRIPTION_MAX_CHARS} characters of plain-prose sentence — no leading digit or markdown marker, balanced backticks, and it must NOT end with \`:\`, \`;\`, or \`,\` (those read as truncation).`,
|
|
48
|
+
'`description` must NOT be a section-heading fragment (e.g. "Overview", "Key points", "Summary"), a code fragment (must not start with `def`/`function`/`class`/`const`/…), or end on a hanging connector word ("a", "the", "and", "to", …).',
|
|
49
|
+
"`description` must NOT merely restate the asset's ref/name; write what the asset actually does.",
|
|
50
|
+
'`description` should NOT start with "When" — that phrasing belongs in `when_to_use`.',
|
|
51
|
+
];
|
|
52
|
+
/** Rules for the `when_to_use` frontmatter field. Enforced by `isValidWhenToUse`. */
|
|
53
|
+
const WHEN_TO_USE_RULES = [
|
|
54
|
+
`\`when_to_use\` is REQUIRED and must be ${WHEN_TO_USE_MIN_CHARS}–${WHEN_TO_USE_MAX_CHARS} characters describing a concrete trigger. Never write the circular fallback "When working with <name>".`,
|
|
55
|
+
"`description` and `when_to_use` must be different from each other.",
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Asset types that carry a `description` and a body where the
|
|
59
|
+
* frontmatter/body rules apply. (Types without those — if any are added later —
|
|
60
|
+
* simply fall through to the cross-cutting block.)
|
|
61
|
+
*/
|
|
62
|
+
const DESCRIPTION_TYPES = new Set(["lesson", "knowledge", "memory", "skill", "command", "agent", "workflow", "fact"]);
|
|
63
|
+
/** Types where `when_to_use` is a HARD requirement (validator rejects if absent). */
|
|
64
|
+
const WHEN_TO_USE_TYPES = new Set(["lesson"]);
|
|
65
|
+
/**
|
|
66
|
+
* Build the hard-rules block for a given asset type, ready to inject as a prompt
|
|
67
|
+
* section. Returns `""` for an unknown type (no over-claiming). The block is
|
|
68
|
+
* deterministic so prompt snapshots stay stable.
|
|
69
|
+
*
|
|
70
|
+
* Inject this VERBATIM into every improve/authoring prompt that creates or edits
|
|
71
|
+
* an asset of `type`, so the agent is told exactly what the gate will reject.
|
|
72
|
+
*/
|
|
73
|
+
export function authoringRulesForType(type) {
|
|
74
|
+
const rules = [...FRONTMATTER_BODY_RULES];
|
|
75
|
+
if (DESCRIPTION_TYPES.has(type))
|
|
76
|
+
rules.push(...DESCRIPTION_RULES);
|
|
77
|
+
if (WHEN_TO_USE_TYPES.has(type))
|
|
78
|
+
rules.push(...WHEN_TO_USE_RULES);
|
|
79
|
+
if (rules.length === 0)
|
|
80
|
+
return "";
|
|
81
|
+
const heading = `Hard authoring rules for ${type} assets (the validator REJECTS proposals that violate these):`;
|
|
82
|
+
return [heading, ...rules.map((r) => `- ${r}`)].join("\n");
|
|
83
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Dispatch resolver for the standards prompt seam — selects which of the two
|
|
6
|
+
* standards features fires for a given write target, mutually exclusively:
|
|
7
|
+
*
|
|
8
|
+
* - **Feature A — wiki schema**: the target is a wiki page (a ref/path under
|
|
9
|
+
* `wikis/<name>/`, NOT a `raw/` file and NOT a wiki infra file
|
|
10
|
+
* `schema.md`/`index.md`/`log.md`). Returns that wiki's `schema.md` body.
|
|
11
|
+
* - **Feature B — stash standards**: the target is any non-wiki asset.
|
|
12
|
+
* Returns the concatenated `category: convention`/`meta` fact bodies.
|
|
13
|
+
* - **Neither fires**: a wiki `raw/` file or a wiki infra file. Returns `""`.
|
|
14
|
+
*
|
|
15
|
+
* The two NEVER both fire. Both underlying readers degrade to `""` on
|
|
16
|
+
* missing/malformed input and never throw, so this resolver never throws.
|
|
17
|
+
*/
|
|
18
|
+
import { extractWikiNameFromRef, INDEX_MD, LOG_MD, loadWikiSchema, SCHEMA_MD } from "../../wiki/wiki.js";
|
|
19
|
+
import { resolveStashStandards } from "./resolve-stash-standards.js";
|
|
20
|
+
/** Wiki infra files that are not authored pages (relative to the wiki root). */
|
|
21
|
+
const WIKI_INFRA_BASENAMES = new Set([SCHEMA_MD, INDEX_MD, LOG_MD]);
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the standards context for a write target identified by its asset ref.
|
|
24
|
+
*
|
|
25
|
+
* @param ref Canonical asset ref of the write target (e.g. `skill:foo`,
|
|
26
|
+
* `wiki:research/topics/x`). When undefined, the target is a
|
|
27
|
+
* non-wiki authoring flow → stash standards.
|
|
28
|
+
* @param stashRoot Stash root directory.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveStandardsContext(ref, stashRoot) {
|
|
31
|
+
const wikiName = ref ? extractWikiNameFromRef(ref) : undefined;
|
|
32
|
+
if (!wikiName) {
|
|
33
|
+
// Non-wiki asset target → Feature B (stash authoring standards).
|
|
34
|
+
return resolveStashStandards(stashRoot);
|
|
35
|
+
}
|
|
36
|
+
// Wiki target. Extract the page path after `wiki:<name>/`.
|
|
37
|
+
const prefix = `wiki:${wikiName}/`;
|
|
38
|
+
const pagePath = ref?.startsWith(prefix) ? ref.slice(prefix.length) : "";
|
|
39
|
+
// `wiki:<name>` with no page, a `raw/` file, or a wiki infra file → neither
|
|
40
|
+
// feature fires.
|
|
41
|
+
if (!pagePath)
|
|
42
|
+
return "";
|
|
43
|
+
if (pagePath === "raw" || pagePath.startsWith("raw/"))
|
|
44
|
+
return "";
|
|
45
|
+
// Infra files (`schema`/`index`/`log`) are only special at the WIKI ROOT.
|
|
46
|
+
// A nested page like `wiki:research/analysis/schema` is a genuine page and
|
|
47
|
+
// must NOT be suppressed, so only check when the page is at root depth.
|
|
48
|
+
if (!pagePath.includes("/")) {
|
|
49
|
+
// Refs drop the `.md` extension; compare against both forms defensively.
|
|
50
|
+
if (WIKI_INFRA_BASENAMES.has(pagePath) || WIKI_INFRA_BASENAMES.has(`${pagePath}.md`)) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// A genuine wiki page → Feature A (that wiki's schema body).
|
|
55
|
+
return loadWikiSchema(stashRoot, wikiName).body;
|
|
56
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the stash-authoring "standards" context (Feature B of the standards
|
|
6
|
+
* plan): gather the bodies of `fact` assets whose `category` frontmatter is
|
|
7
|
+
* `convention` or `meta` so naming / tag / frontmatter conventions are surfaced
|
|
8
|
+
* to the agent when it creates or edits a non-wiki asset.
|
|
9
|
+
*
|
|
10
|
+
* Selection is by **frontmatter `category`**, never by path — flat
|
|
11
|
+
* (`facts/x.md`) and nested (`facts/conventions/x.md`) layouts resolve
|
|
12
|
+
* identically. The MVP does no parsing of fenced blocks, no rule objects, and
|
|
13
|
+
* no warnings: it concatenates the selected facts' bodies in stable enumeration
|
|
14
|
+
* order, each preceded by a one-line `# <ref>` provenance header. Returns `""`
|
|
15
|
+
* when no matching facts exist.
|
|
16
|
+
*/
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { parseFrontmatter } from "../asset/frontmatter.js";
|
|
20
|
+
/** `category` values that mark a fact as an authoring standard. */
|
|
21
|
+
const STANDARD_CATEGORIES = new Set(["convention", "meta"]);
|
|
22
|
+
/** Directory (under the stash root) where `fact` assets live. */
|
|
23
|
+
const FACTS_SUBDIR = "facts";
|
|
24
|
+
/**
|
|
25
|
+
* Recursively collect `.md` files under `dir` in stable (sorted) enumeration
|
|
26
|
+
* order. Returns absolute paths. Missing dir → `[]`.
|
|
27
|
+
*/
|
|
28
|
+
function collectMarkdownFiles(dir) {
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const results = [];
|
|
37
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
38
|
+
const full = path.join(dir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
results.push(...collectMarkdownFiles(full));
|
|
41
|
+
}
|
|
42
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
43
|
+
results.push(full);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Derive a fact ref (`fact:conventions/naming`) from an absolute markdown path
|
|
50
|
+
* relative to the facts root. Mirrors the canonical-name derivation in
|
|
51
|
+
* `asset-spec.ts` (POSIX separators, `.md` stripped).
|
|
52
|
+
*/
|
|
53
|
+
function toFactRef(factsRoot, absPath) {
|
|
54
|
+
const rel = path.relative(factsRoot, absPath).split(path.sep).join("/");
|
|
55
|
+
const name = rel.endsWith(".md") ? rel.slice(0, -3) : rel;
|
|
56
|
+
return `fact:${name}`;
|
|
57
|
+
}
|
|
58
|
+
export function resolveStashStandards(stashRoot) {
|
|
59
|
+
const factsRoot = path.join(stashRoot, FACTS_SUBDIR);
|
|
60
|
+
const sections = [];
|
|
61
|
+
for (const absPath of collectMarkdownFiles(factsRoot)) {
|
|
62
|
+
let raw;
|
|
63
|
+
try {
|
|
64
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
let category = "";
|
|
70
|
+
let body = "";
|
|
71
|
+
try {
|
|
72
|
+
const parsed = parseFrontmatter(raw);
|
|
73
|
+
category = typeof parsed.data.category === "string" ? parsed.data.category.trim() : "";
|
|
74
|
+
body = parsed.content;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!STANDARD_CATEGORIES.has(category))
|
|
80
|
+
continue;
|
|
81
|
+
const trimmed = body.trim();
|
|
82
|
+
if (!trimmed)
|
|
83
|
+
continue; // skip stub facts with frontmatter but no body
|
|
84
|
+
sections.push(`# ${toFactRef(factsRoot, absPath)}\n${trimmed}`);
|
|
85
|
+
}
|
|
86
|
+
return sections.join("\n\n");
|
|
87
|
+
}
|
package/dist/core/state-db.js
CHANGED
|
@@ -791,6 +791,31 @@ const MIGRATIONS = [
|
|
|
791
791
|
ON recombine_hypotheses(last_seen_at);
|
|
792
792
|
`,
|
|
793
793
|
},
|
|
794
|
+
// ── Migration 015 — asset_salience: encoding_source provenance column (#644) ──
|
|
795
|
+
//
|
|
796
|
+
// Records HOW the stored `encoding_salience` was derived so an improve run's
|
|
797
|
+
// type-weight fallback can no longer clobber a real content-derived score:
|
|
798
|
+
//
|
|
799
|
+
// "content" — written by the distill path from `scoreEncodingSalience`
|
|
800
|
+
// (novelty·0.40 + magnitude·0.35 + predictionError·0.25).
|
|
801
|
+
// "type-stub" — written by `computeSalience`'s `DEFAULT_TYPE_ENCODING_WEIGHTS`
|
|
802
|
+
// fallback (no content-based score available for this ref yet).
|
|
803
|
+
// NULL — legacy row written before this migration; provenance unknown.
|
|
804
|
+
// Treated as a stub by readers UNLESS its stored value differs
|
|
805
|
+
// from the pure type-weight (best-effort heuristic for old data).
|
|
806
|
+
//
|
|
807
|
+
// Why a column rather than inference: before #644, every improve run overwrote
|
|
808
|
+
// the distill-written content score with the type-weight stub, so the stored
|
|
809
|
+
// value alone cannot distinguish a real score from a stub. The provenance flag
|
|
810
|
+
// is the single source of truth going forward; `upsertAssetSalience` refuses to
|
|
811
|
+
// lower a "content" row to a "type-stub" so the high-salience gate (#608) keys
|
|
812
|
+
// on genuine novelty/magnitude/prediction-error, not the asset type.
|
|
813
|
+
{
|
|
814
|
+
id: "015-asset-salience-encoding-source",
|
|
815
|
+
up: `
|
|
816
|
+
ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
|
|
817
|
+
`,
|
|
818
|
+
},
|
|
794
819
|
];
|
|
795
820
|
/**
|
|
796
821
|
* Apply every pending migration in a single transaction per migration.
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* during validation. We carry it through if the agent supplies it.
|
|
27
27
|
*/
|
|
28
28
|
import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
29
|
+
import { authoringRulesForType } from "../../core/authoring-rules.js";
|
|
29
30
|
import { parseEmbeddedJsonResponse, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
|
|
30
31
|
/**
|
|
31
32
|
* Per-asset-type frontmatter / authoring hints surfaced in the prompt so
|
|
@@ -156,6 +157,17 @@ export function buildReflectPrompt(input) {
|
|
|
156
157
|
// ref is set but no feedback — explicitly constrain scope to schema compliance
|
|
157
158
|
sections.push("No usage feedback recorded. Limit your proposal to schema and structural improvements only: missing required frontmatter fields, unclear `when_to_use`, ambiguous description, or broken formatting. Do not speculate about runtime weaknesses you have not observed.");
|
|
158
159
|
}
|
|
160
|
+
if (input.standardsContext?.trim()) {
|
|
161
|
+
sections.push("Standards to follow (the rulebook for this target):");
|
|
162
|
+
sections.push(input.standardsContext.trim());
|
|
163
|
+
}
|
|
164
|
+
{
|
|
165
|
+
const resolvedType = input.type ?? (input.ref?.includes(":") ? input.ref.split(":")[0] : "");
|
|
166
|
+
const authoringRules = resolvedType ? authoringRulesForType(resolvedType) : "";
|
|
167
|
+
if (authoringRules) {
|
|
168
|
+
sections.push(authoringRules);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
159
171
|
if (input.assetContent?.trim()) {
|
|
160
172
|
// Cap at 12 000 chars to stay well under OS ARG_MAX when the prompt is
|
|
161
173
|
// passed as a CLI argument to opencode/claude. Large assets (wiki snapshots,
|
|
@@ -289,6 +301,16 @@ export function buildProposePrompt(input) {
|
|
|
289
301
|
for (const line of input.schemaHints)
|
|
290
302
|
sections.push(`- ${line}`);
|
|
291
303
|
}
|
|
304
|
+
if (input.standardsContext?.trim()) {
|
|
305
|
+
sections.push("Standards to follow (the rulebook for this target):");
|
|
306
|
+
sections.push(input.standardsContext.trim());
|
|
307
|
+
}
|
|
308
|
+
{
|
|
309
|
+
const authoringRules = authoringRulesForType(input.type);
|
|
310
|
+
if (authoringRules) {
|
|
311
|
+
sections.push(authoringRules);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
292
314
|
sections.push("Produce a single proposal that, if accepted, would land as the asset described above.");
|
|
293
315
|
sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
|
|
294
316
|
return sections.join("\n\n");
|
|
@@ -305,6 +327,16 @@ export function buildSchemaRepairPrompt(input) {
|
|
|
305
327
|
`while preserving all existing content.`);
|
|
306
328
|
sections.push(`Target ref: ${input.ref}`);
|
|
307
329
|
sections.push(`Schema requirements for ${input.type} assets: ${hintForType(input.type)}`);
|
|
330
|
+
if (input.standardsContext?.trim()) {
|
|
331
|
+
sections.push("Standards to follow (the rulebook for this target):");
|
|
332
|
+
sections.push(input.standardsContext.trim());
|
|
333
|
+
}
|
|
334
|
+
{
|
|
335
|
+
const authoringRules = authoringRulesForType(input.type);
|
|
336
|
+
if (authoringRules) {
|
|
337
|
+
sections.push(authoringRules);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
308
340
|
const CONTENT_CAP = 3000;
|
|
309
341
|
const body = input.assetContent.trimEnd();
|
|
310
342
|
const truncated = body.length > CONTENT_CAP;
|
|
@@ -9978,6 +9978,12 @@ var init_state_db = __esm(() => {
|
|
|
9978
9978
|
CREATE INDEX IF NOT EXISTS idx_recombine_hypotheses_last_seen
|
|
9979
9979
|
ON recombine_hypotheses(last_seen_at);
|
|
9980
9980
|
`
|
|
9981
|
+
},
|
|
9982
|
+
{
|
|
9983
|
+
id: "015-asset-salience-encoding-source",
|
|
9984
|
+
up: `
|
|
9985
|
+
ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
|
|
9986
|
+
`
|
|
9981
9987
|
}
|
|
9982
9988
|
];
|
|
9983
9989
|
});
|
|
@@ -9108,6 +9108,12 @@ var MIGRATIONS = [
|
|
|
9108
9108
|
CREATE INDEX IF NOT EXISTS idx_recombine_hypotheses_last_seen
|
|
9109
9109
|
ON recombine_hypotheses(last_seen_at);
|
|
9110
9110
|
`
|
|
9111
|
+
},
|
|
9112
|
+
{
|
|
9113
|
+
id: "015-asset-salience-encoding-source",
|
|
9114
|
+
up: `
|
|
9115
|
+
ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
|
|
9116
|
+
`
|
|
9111
9117
|
}
|
|
9112
9118
|
];
|
|
9113
9119
|
function runMigrations2(db) {
|
package/dist/wiki/wiki.js
CHANGED
|
@@ -224,6 +224,43 @@ function readSchemaDescription(wikiDir) {
|
|
|
224
224
|
return undefined;
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Load a wiki's `schema.md` body and frontmatter.
|
|
229
|
+
*
|
|
230
|
+
* Unlike {@link readSchemaDescription} (which returns only the frontmatter
|
|
231
|
+
* `description` for `listWikis` summaries), this returns the markdown **body**
|
|
232
|
+
* — everything after the closing `---` of the frontmatter — which is where the
|
|
233
|
+
* page contract, operations, and hard rules live. The body is the rulebook
|
|
234
|
+
* injected into the write-time prompt for wiki-page edits.
|
|
235
|
+
*
|
|
236
|
+
* Swallow-and-degrade like the existing reader: a missing file, an unresolvable
|
|
237
|
+
* wiki dir, or malformed content yields `{ body: "", frontmatter: {} }`. Never
|
|
238
|
+
* throws.
|
|
239
|
+
*/
|
|
240
|
+
export function loadWikiSchema(stashRoot, name) {
|
|
241
|
+
const empty = { body: "", frontmatter: {} };
|
|
242
|
+
let wikiDir;
|
|
243
|
+
try {
|
|
244
|
+
wikiDir = resolveWikiDir(stashRoot, name);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return empty;
|
|
248
|
+
}
|
|
249
|
+
let raw;
|
|
250
|
+
try {
|
|
251
|
+
raw = fs.readFileSync(path.join(wikiDir, SCHEMA_MD), "utf8");
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return empty;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const parsed = parseFrontmatter(raw);
|
|
258
|
+
return { body: parsed.content, frontmatter: parsed.data };
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return empty;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
227
264
|
function toIsoDate(ms) {
|
|
228
265
|
return new Date(ms).toISOString();
|
|
229
266
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|