akm-cli 0.9.11 → 0.9.13
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 +227 -0
- package/STABILITY.md +6 -1
- package/dist/assets/hints/cli-hints-full.md +1 -1
- package/dist/assets/improve-strategies/consolidate.json +1 -1
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -2
- package/dist/assets/workflows/workflow-template.md +4 -0
- package/dist/cli/shared.js +16 -4
- package/dist/cli.js +15 -13
- package/dist/commands/agent/agent-dispatch.js +8 -0
- package/dist/commands/command/execution-source-loader.js +25 -22
- package/dist/commands/command/portable-template.js +4 -26
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env-binding.js +10 -3
- package/dist/commands/env/env-cli.js +7 -0
- package/dist/commands/env/secret-cli.js +15 -4
- package/dist/commands/health/checks.js +186 -71
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +2 -2
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/execution.js +1 -2
- package/dist/commands/improve/extract.js +82 -56
- package/dist/commands/improve/improve-strategies.js +26 -8
- package/dist/commands/improve/improve.js +14 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/base-linter.js +10 -0
- package/dist/commands/lint/index.js +3 -1
- package/dist/commands/migrate-cli.js +6 -4
- package/dist/commands/proposal/drain-policies.js +22 -2
- package/dist/commands/proposal/drain.js +48 -6
- package/dist/commands/proposal/proposal-cli.js +1 -0
- package/dist/commands/proposal/repository.js +4 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
- package/dist/commands/proposal/validators/proposals.js +10 -19
- package/dist/commands/read/show.js +42 -31
- package/dist/commands/registry-cli.js +4 -2
- package/dist/commands/sources/init.js +4 -8
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-clone.js +5 -7
- package/dist/commands/sources/sources-cli.js +3 -5
- package/dist/commands/tasks/tasks-cli.js +4 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/workflow-cli.js +17 -15
- package/dist/core/activation-policy.js +31 -3
- package/dist/core/adapter/execution-source.js +39 -11
- package/dist/core/asset/stash-meta.js +7 -41
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +56 -6
- package/dist/core/config/config.js +42 -17
- package/dist/core/config/legacy-source-shape-shim.js +79 -0
- package/dist/core/config/schema/embedding.js +2 -2
- package/dist/core/config/schema/engines.js +2 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +27 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +4 -3
- package/dist/core/improve-types.js +17 -0
- package/dist/core/json-schema.js +1 -11
- package/dist/core/maintenance-barrier.js +17 -2
- package/dist/core/paths.js +12 -15
- package/dist/core/state/migrations.js +28 -0
- package/dist/core/state-db.js +28 -1
- package/dist/core/write-source.js +6 -6
- package/dist/indexer/bundle-identity-guard.js +3 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/indexer.js +11 -3
- package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
- package/dist/indexer/passes/metadata.js +16 -5
- package/dist/indexer/search/search-fields.js +1 -30
- package/dist/integrations/agent/engine-resolution.js +15 -1
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +28 -8
- package/dist/llm/embedders/remote.js +3 -2
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +50 -3
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1737 -1392
- package/dist/scripts/akm-migrate.js +1736 -1391
- package/dist/setup/setup.js +14 -21
- package/dist/sources/include.js +150 -20
- package/dist/sources/providers/git-install.js +14 -12
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
- package/dist/sources/website-url.js +12 -4
- package/dist/storage/engines/sqlite-migrations.js +40 -10
- package/dist/storage/like-pattern.js +7 -0
- package/dist/storage/repositories/extract-sessions-repository.js +23 -0
- package/dist/storage/repositories/index-connection.js +27 -10
- package/dist/storage/repositories/index-entry-schema.js +19 -2
- package/dist/storage/repositories/index-schema.js +30 -9
- package/dist/storage/repositories/proposals-repository.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +14 -7
- package/dist/storage/repositories/workflow-runs-repository.js +133 -11
- package/dist/storage/sqlite-read-snapshot.js +11 -9
- package/dist/tasks/backends/cron.js +34 -5
- package/dist/tasks/backends/launchd.js +23 -26
- package/dist/tasks/backends/schtasks.js +50 -3
- package/dist/tasks/frozen-script.js +2 -0
- package/dist/tasks/prepare/prepare.js +2 -7
- package/dist/tasks/prepare/script-capture.js +38 -6
- package/dist/tasks/schedule.js +154 -13
- package/dist/tasks/source/task-source-v3-frozen.js +0 -1
- package/dist/tasks/source/task-source-v4.js +0 -1
- package/dist/workflows/exec/child-workflow.js +2 -3
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +20 -11
- package/dist/workflows/exec/step-work.js +76 -56
- package/dist/workflows/freeze/resolve-steps.js +19 -11
- package/dist/workflows/freeze/source-freeze.js +7 -0
- package/dist/workflows/freeze/targets/child-workflow.js +12 -18
- package/dist/workflows/freeze/targets/command.js +14 -2
- package/dist/workflows/ir/environment-v4.js +4 -2
- package/dist/workflows/ir/freeze-v4.js +2 -5
- package/dist/workflows/ir/plan-hash.js +0 -3
- package/dist/workflows/ir/schema-v4.js +14 -9
- package/dist/workflows/ir/schema.js +1 -3
- package/dist/workflows/parser.js +1 -1
- package/dist/workflows/resource-limits.js +35 -48
- package/dist/workflows/runtime/plan-classifier.js +89 -41
- package/dist/workflows/runtime/run-outputs.js +1 -21
- package/dist/workflows/runtime/runs.js +104 -154
- package/dist/workflows/source-files.js +28 -54
- package/dist/workflows/source-ir/program.js +2 -2
- package/dist/workflows/source-ir/semantics.js +5 -23
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +92 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
|
@@ -35,7 +35,7 @@ import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
|
35
35
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
36
36
|
import { redactSensitiveText } from "../../core/redaction.js";
|
|
37
37
|
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
38
|
-
import { warn } from "../../core/warn.js";
|
|
38
|
+
import { warn, warnOnce } from "../../core/warn.js";
|
|
39
39
|
import { lookup } from "../../indexer/indexer.js";
|
|
40
40
|
import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
|
|
41
41
|
import { fallbackAnnouncement, NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
|
|
@@ -47,7 +47,7 @@ import { collectDispatchSensitiveValues } from "../../integrations/agent/runner-
|
|
|
47
47
|
import { isJsonSchemaKnownUnsupported, LlmCallError } from "../../llm/client.js";
|
|
48
48
|
import { callStructured } from "../../llm/structured-call.js";
|
|
49
49
|
import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
|
|
50
|
-
import { isProposalSkipped, listProposalsReadOnly, proposalContent, } from "../proposal/repository.js";
|
|
50
|
+
import { isProposalSkipped, listProposalsReadOnly, proposalContent, recordGateDecision, } from "../proposal/repository.js";
|
|
51
51
|
import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
|
|
52
52
|
import { deriveLessonRef } from "./distill.js";
|
|
53
53
|
import { runReflectQualityJudge } from "./distill/quality-gate.js";
|
|
@@ -119,6 +119,10 @@ export const REFLECT_ALLOWED_TYPES = new Set([
|
|
|
119
119
|
"command",
|
|
120
120
|
"workflow",
|
|
121
121
|
]);
|
|
122
|
+
const REFLECT_REFUSED_TYPES = new Set(["secret"]);
|
|
123
|
+
function isReflectableSourceShape(content) {
|
|
124
|
+
return parseFrontmatter(content).frontmatter !== null;
|
|
125
|
+
}
|
|
122
126
|
/**
|
|
123
127
|
* Identity / structural frontmatter fields the LLM is NEVER allowed to change.
|
|
124
128
|
*
|
|
@@ -539,24 +543,15 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
|
539
543
|
// predicate lives in `core/proposal-quality-validators` so the same check
|
|
540
544
|
// also runs inside `runProposalValidators` on `proposal accept`.
|
|
541
545
|
const sizeOutcome = checkReflectSize(sourceBody, cleanedBody);
|
|
546
|
+
let sizeGuardRatio;
|
|
542
547
|
if (!sizeOutcome.ok) {
|
|
543
548
|
const pct = (sizeOutcome.ratio * 100).toFixed(0);
|
|
544
549
|
const limit = sizeOutcome.code === "EXCESSIVE_SHRINKAGE" ? "minimum 50%" : "maximum 250%";
|
|
545
550
|
const cause = sizeOutcome.code === "EXCESSIVE_SHRINKAGE"
|
|
546
551
|
? "Concrete content was likely deleted."
|
|
547
552
|
: "Speculative material was likely added.";
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
warnings,
|
|
551
|
-
reject: {
|
|
552
|
-
// Content-policy guard hit (EXCESSIVE_SHRINKAGE / EXCESSIVE_EXPANSION).
|
|
553
|
-
// This is the guard working as designed — the LLM responded fine, we
|
|
554
|
-
// blocked the output. Routed through `content_policy_reject` so the
|
|
555
|
-
// health aggregator can split guard hits out of true LLM faults.
|
|
556
|
-
reason: "content_policy_reject",
|
|
557
|
-
error: `Reflect rejected: ${sizeOutcome.code} — proposed body is ${pct}% of source (${limit}) for ref ${targetRef}. ${cause}`,
|
|
558
|
-
},
|
|
559
|
-
};
|
|
553
|
+
warnings.push(`${sizeOutcome.code} — proposed body is ${pct}% of source (${limit}) for ref ${targetRef}. ${cause} Flagged for review.`);
|
|
554
|
+
sizeGuardRatio = { code: sizeOutcome.code, ratio: sizeOutcome.ratio };
|
|
560
555
|
}
|
|
561
556
|
// Reassemble final content: merged frontmatter + cleaned body.
|
|
562
557
|
// When there is no frontmatter at all (no source fm and no LLM fm), emit body
|
|
@@ -570,6 +565,7 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
|
570
565
|
content: reassembled,
|
|
571
566
|
...(hasFrontmatter ? { frontmatter: mergedFm } : {}),
|
|
572
567
|
warnings,
|
|
568
|
+
...(sizeGuardRatio ? { sizeGuardRatio } : {}),
|
|
573
569
|
};
|
|
574
570
|
}
|
|
575
571
|
/**
|
|
@@ -636,29 +632,6 @@ function wantsJsonSchemaOutput(connection) {
|
|
|
636
632
|
}
|
|
637
633
|
/** Critique prompt injected between prior draft and refinement request (Self-Refine loop). */
|
|
638
634
|
const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above. Review it critically and provide an improved version that is more specific, actionable, and avoids any issues with the previous attempt. Return only the improved response using the output contract from the original prompt.";
|
|
639
|
-
/**
|
|
640
|
-
* OpenAI-compatible thinking models charge hidden reasoning against
|
|
641
|
-
* `max_tokens` before they emit the visible response. Reflect asks for a
|
|
642
|
-
* machine-readable payload and requests `enableThinking: false`, but local
|
|
643
|
-
* servers do not uniformly honour that flag. Keep visible-content sizing
|
|
644
|
-
* separate from the allowance that lets an uncooperative thinking model reach
|
|
645
|
-
* its JSON/frame envelope.
|
|
646
|
-
*
|
|
647
|
-
* The 2,048-token allowance exceeds the observed 1,798-token peak that
|
|
648
|
-
* previously cut direct reflect responses off mid-envelope. It applies to all
|
|
649
|
-
* bounded direct-LLM calls because a server's thinking behavior is not a
|
|
650
|
-
* reliable capability signal; the post-processor still enforces the original
|
|
651
|
-
* content-size policy.
|
|
652
|
-
*/
|
|
653
|
-
const REFLECT_REASONING_TOKEN_HEADROOM = 2_048;
|
|
654
|
-
const REFLECT_RESPONSE_ENVELOPE_CHARS = 500;
|
|
655
|
-
function reflectMaxTokensForOutput(maxOutputChars) {
|
|
656
|
-
if (maxOutputChars === undefined)
|
|
657
|
-
return undefined;
|
|
658
|
-
// Divide by 3 chars/token (conservative — most models are 3.5–4), retain
|
|
659
|
-
// space for the JSON/frame wrapper, then reserve independent reasoning room.
|
|
660
|
-
return Math.ceil((maxOutputChars + REFLECT_RESPONSE_ENVELOPE_CHARS) / 3) + REFLECT_REASONING_TOKEN_HEADROOM;
|
|
661
|
-
}
|
|
662
635
|
function reflectLlmTelemetry(result) {
|
|
663
636
|
if (!result.parsed || typeof result.parsed !== "object" || Array.isArray(result.parsed))
|
|
664
637
|
return undefined;
|
|
@@ -909,7 +882,7 @@ function failureEnvelope(result, ref, engine, fallbackReason = "non_zero_exit")
|
|
|
909
882
|
* byte-identical.
|
|
910
883
|
*/
|
|
911
884
|
async function finalizeReflectProposal(args) {
|
|
912
|
-
const { assetContent, result, options, engineName, config, qualityGateEnabled, qualityJudgeRunner, qualityJudgeLease, feedback, stash, emitReflectFailed, onNotices, } = args;
|
|
885
|
+
const { assetContent, result, options, engineName, config, qualityGateEnabled, qualityGateSkippedNoJudge, qualityJudgeRunner, qualityJudgeLease, feedback, stash, emitReflectFailed, onNotices, } = args;
|
|
913
886
|
let payload = args.payload;
|
|
914
887
|
const outputTelemetry = reflectLlmTelemetry(result);
|
|
915
888
|
// 7. Reflect content-preservation rails:
|
|
@@ -989,7 +962,7 @@ async function finalizeReflectProposal(args) {
|
|
|
989
962
|
}
|
|
990
963
|
// 7c. Judge the exact sanitized content that can be persisted. Fail closed
|
|
991
964
|
// on cancellation, transport failure, malformed output, or an invalid score.
|
|
992
|
-
if (qualityGateEnabled) {
|
|
965
|
+
if (qualityGateEnabled && !sanitizeOutcome.sizeGuardRatio) {
|
|
993
966
|
const judgeResult = await runReflectQualityJudge(config, payload.content, assetContent ?? "", feedback, options.chat, {
|
|
994
967
|
runnerSelectionFrozen: true,
|
|
995
968
|
...(qualityJudgeRunner ? { llmRunner: qualityJudgeRunner } : {}),
|
|
@@ -1029,6 +1002,8 @@ async function finalizeReflectProposal(args) {
|
|
|
1029
1002
|
durationMs: result.durationMs,
|
|
1030
1003
|
emitReflectFailed,
|
|
1031
1004
|
outputTelemetry,
|
|
1005
|
+
qualityGateSkippedNoJudge,
|
|
1006
|
+
sizeGuardRatio: sanitizeOutcome.sizeGuardRatio,
|
|
1032
1007
|
});
|
|
1033
1008
|
}
|
|
1034
1009
|
/**
|
|
@@ -1038,7 +1013,7 @@ async function finalizeReflectProposal(args) {
|
|
|
1038
1013
|
* `akmReflect`'s finalize tail.
|
|
1039
1014
|
*/
|
|
1040
1015
|
function createReflectProposal(args) {
|
|
1041
|
-
const { payload, options, stash, engineName, durationMs, emitReflectFailed, outputTelemetry } = args;
|
|
1016
|
+
const { payload, options, stash, engineName, durationMs, emitReflectFailed, outputTelemetry, qualityGateSkippedNoJudge, sizeGuardRatio, } = args;
|
|
1042
1017
|
// 8. Create the proposal. The proposal queue is the ONLY thing reflect
|
|
1043
1018
|
// writes — promotion to a real asset is gated by `akm proposal accept`.
|
|
1044
1019
|
//
|
|
@@ -1101,7 +1076,21 @@ function createReflectProposal(args) {
|
|
|
1101
1076
|
exitCode: null,
|
|
1102
1077
|
};
|
|
1103
1078
|
}
|
|
1104
|
-
|
|
1079
|
+
let proposal = proposalResult;
|
|
1080
|
+
const reviewReasons = [];
|
|
1081
|
+
if (qualityGateSkippedNoJudge)
|
|
1082
|
+
reviewReasons.push("no-judge-configured");
|
|
1083
|
+
if (sizeGuardRatio)
|
|
1084
|
+
reviewReasons.push("reflect-size-ratio");
|
|
1085
|
+
if (reviewReasons.length > 0) {
|
|
1086
|
+
proposal =
|
|
1087
|
+
recordGateDecision(stash, proposal.id, {
|
|
1088
|
+
outcome: "deferred",
|
|
1089
|
+
reason: reviewReasons.join("+"),
|
|
1090
|
+
gate: "reflect",
|
|
1091
|
+
...(sizeGuardRatio ? { measured: Math.round(sizeGuardRatio.ratio * 100) } : {}),
|
|
1092
|
+
}, options.ctx) ?? proposal;
|
|
1093
|
+
}
|
|
1105
1094
|
appendEvent({
|
|
1106
1095
|
eventType: "reflect_completed",
|
|
1107
1096
|
ref: proposal.ref,
|
|
@@ -1109,6 +1098,8 @@ function createReflectProposal(args) {
|
|
|
1109
1098
|
proposalId: proposal.id,
|
|
1110
1099
|
source: "reflect",
|
|
1111
1100
|
engine: engineName,
|
|
1101
|
+
...(qualityGateSkippedNoJudge ? { qualityGateSkippedNoJudge: true } : {}),
|
|
1102
|
+
...(sizeGuardRatio ? { sizeGuardRatio: sizeGuardRatio.code, sizeGuardRatioValue: sizeGuardRatio.ratio } : {}),
|
|
1112
1103
|
...(outputTelemetry ?? {}),
|
|
1113
1104
|
},
|
|
1114
1105
|
}, options.eventsCtx);
|
|
@@ -1299,6 +1290,19 @@ function resolveReflectRunner(options) {
|
|
|
1299
1290
|
}
|
|
1300
1291
|
return { config, activeStrategy, runnerSpec, engineName, notices };
|
|
1301
1292
|
}
|
|
1293
|
+
function unsupportedTypeFailure(ref, type, detail, emitReflectFailed) {
|
|
1294
|
+
emitReflectFailed("unsupported_type", "unsupported_type", ref, { type });
|
|
1295
|
+
return {
|
|
1296
|
+
failure: {
|
|
1297
|
+
schemaVersion: 2,
|
|
1298
|
+
ok: false,
|
|
1299
|
+
reason: "unsupported_type",
|
|
1300
|
+
error: `Reflect refused: asset type "${type}" is not supported by reflect (${detail}). Use \`akm proposal new\` or edit the file directly.`,
|
|
1301
|
+
ref,
|
|
1302
|
+
exitCode: null,
|
|
1303
|
+
},
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1302
1306
|
/**
|
|
1303
1307
|
* Resolve the reflect target's parsed ref + current on-disk content: enforce the
|
|
1304
1308
|
* REFLECT_ALLOWED_TYPES markdown-canonical type guard (returning a terminal
|
|
@@ -1311,27 +1315,10 @@ async function resolveReflectSource(options, stash, emitReflectFailed) {
|
|
|
1311
1315
|
let parsedRef;
|
|
1312
1316
|
if (options.ref) {
|
|
1313
1317
|
parsedRef = parseRefInput(options.ref);
|
|
1314
|
-
// 2a.
|
|
1315
|
-
//
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
if (!REFLECT_ALLOWED_TYPES.has(parsedRef.type)) {
|
|
1319
|
-
// Deterministic type-guard rejection — the LLM is never invoked. Emit
|
|
1320
|
-
// with reason `unsupported_type` so the improve loop can route this to
|
|
1321
|
-
// the `reflect-skipped` action bucket instead of `reflect-failed`. See
|
|
1322
|
-
// `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1a
|
|
1323
|
-
// ("Reflect refused asset type" — ~9% of reflect-failed events).
|
|
1324
|
-
emitReflectFailed("unsupported_type", "unsupported_type", options.ref, { type: parsedRef.type });
|
|
1325
|
-
return {
|
|
1326
|
-
failure: {
|
|
1327
|
-
schemaVersion: 2,
|
|
1328
|
-
ok: false,
|
|
1329
|
-
reason: "unsupported_type",
|
|
1330
|
-
error: `Reflect refused: asset type "${parsedRef.type}" is not supported by reflect (only markdown-canonical types are allowed: ${[...REFLECT_ALLOWED_TYPES].sort().join(", ")}). Use \`akm proposal new\` or edit the file directly.`,
|
|
1331
|
-
ref: options.ref,
|
|
1332
|
-
exitCode: null,
|
|
1333
|
-
},
|
|
1334
|
-
};
|
|
1318
|
+
// 2a. Refuse `secret` before any content is read — a secret's content is
|
|
1319
|
+
// never touched by reflect, regardless of what it happens to look like.
|
|
1320
|
+
if (REFLECT_REFUSED_TYPES.has(parsedRef.type)) {
|
|
1321
|
+
return unsupportedTypeFailure(options.ref, parsedRef.type, "secret material is never read or sent to an LLM", emitReflectFailed);
|
|
1335
1322
|
}
|
|
1336
1323
|
if (options.assetContent !== undefined) {
|
|
1337
1324
|
// Test seam — caller pre-loaded the source content.
|
|
@@ -1357,6 +1344,11 @@ async function resolveReflectSource(options, stash, emitReflectFailed) {
|
|
|
1357
1344
|
// Index miss is non-fatal — the agent can still propose a fresh asset.
|
|
1358
1345
|
}
|
|
1359
1346
|
}
|
|
1347
|
+
if (!REFLECT_ALLOWED_TYPES.has(parsedRef.type)) {
|
|
1348
|
+
if (assetContent === undefined || !isReflectableSourceShape(assetContent)) {
|
|
1349
|
+
return unsupportedTypeFailure(options.ref, parsedRef.type, "its content is not frontmatter + markdown", emitReflectFailed);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1360
1352
|
}
|
|
1361
1353
|
return { assetContent, parsedRef };
|
|
1362
1354
|
}
|
|
@@ -1393,7 +1385,7 @@ async function runReflectRefineIterations(args) {
|
|
|
1393
1385
|
draftPathsToCleanup.push(iterDraftPath);
|
|
1394
1386
|
lastDraftPath = iterDraftPath;
|
|
1395
1387
|
}
|
|
1396
|
-
const { prompt
|
|
1388
|
+
const { prompt } = buildReflectPrompt({
|
|
1397
1389
|
...(options.ref ? { ref: options.ref } : {}),
|
|
1398
1390
|
...(parsedRef?.type ? { type: parsedRef.type } : {}),
|
|
1399
1391
|
...(parsedRef?.name ? { name: parsedRef.name } : {}),
|
|
@@ -1414,7 +1406,6 @@ async function runReflectRefineIterations(args) {
|
|
|
1414
1406
|
...(iterDraftPath ? { draftFilePath: iterDraftPath } : {}),
|
|
1415
1407
|
...(outputMode ? { outputMode } : {}),
|
|
1416
1408
|
});
|
|
1417
|
-
const maxTokensForLlm = reflectMaxTokensForOutput(maxOutputChars);
|
|
1418
1409
|
let iterResult;
|
|
1419
1410
|
if (runnerIsLlm(runnerSpec)) {
|
|
1420
1411
|
// LLM HTTP runners cannot honor the file-write contract, so they return
|
|
@@ -1435,7 +1426,6 @@ async function runReflectRefineIterations(args) {
|
|
|
1435
1426
|
...(options.ref ? { targetRef: options.ref } : {}),
|
|
1436
1427
|
allowRepair: repairAttempts === 0,
|
|
1437
1428
|
...(options.chat ? { chat: options.chat } : {}),
|
|
1438
|
-
...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
|
|
1439
1429
|
onNotices,
|
|
1440
1430
|
});
|
|
1441
1431
|
}
|
|
@@ -1638,18 +1628,11 @@ export async function akmReflect(options = {}) {
|
|
|
1638
1628
|
const executionNotices = new Map();
|
|
1639
1629
|
collectLoweringNotices(executionNotices, resolutionNotices);
|
|
1640
1630
|
const collectExecutionNotices = (notices) => collectLoweringNotices(executionNotices, notices);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
reason: "parse_error",
|
|
1647
|
-
error: 'Reflect proposal quality gate rejected: score=-1, reason="no LLM configured — cannot judge, failing closed"',
|
|
1648
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1649
|
-
engine: engineName,
|
|
1650
|
-
exitCode: null,
|
|
1651
|
-
...reflectNoticeFields(executionNotices),
|
|
1652
|
-
};
|
|
1631
|
+
let qualityJudgeSelection = resolveReflectQualityJudgeRunner(config, runnerSpec, isReflectQualityGateEnabled(activeStrategy), collectExecutionNotices);
|
|
1632
|
+
const qualityGateSkippedNoJudge = qualityJudgeSelection.enabled && !qualityJudgeSelection.runner;
|
|
1633
|
+
if (qualityGateSkippedNoJudge) {
|
|
1634
|
+
warnOnce("reflect-quality-gate-no-judge", "Reflect proposal quality gate has no LLM configured to judge proposals (set defaults.llmEngine, or improve.strategies.<name>.processes.reflect.qualityGate.engine). Skipping the gate for this run; the proposal is queued for human review instead.");
|
|
1635
|
+
qualityJudgeSelection = Object.freeze({ enabled: false, runner: undefined });
|
|
1653
1636
|
}
|
|
1654
1637
|
const qualityJudgeRunner = qualityJudgeSelection.runner;
|
|
1655
1638
|
let generationLease;
|
|
@@ -1782,6 +1765,7 @@ export async function akmReflect(options = {}) {
|
|
|
1782
1765
|
engineName,
|
|
1783
1766
|
config,
|
|
1784
1767
|
qualityGateEnabled: qualityJudgeSelection.enabled,
|
|
1768
|
+
qualityGateSkippedNoJudge,
|
|
1785
1769
|
qualityJudgeRunner,
|
|
1786
1770
|
qualityJudgeLease,
|
|
1787
1771
|
feedback,
|
|
@@ -132,6 +132,14 @@ function fixMissingUpdated(raw, mtime) {
|
|
|
132
132
|
return spliceFrontmatterLine(raw, `updated: ${localDateStamp(mtime)}`) ?? raw;
|
|
133
133
|
}
|
|
134
134
|
// ── stale-path helpers ────────────────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* A path segment shaped like a run-time filename template rather than a
|
|
137
|
+
* literal reference: `<timestamp>`-style angle brackets, `{stamp}`/`${VAR}`
|
|
138
|
+
* braces, a `YYYYMMDD`/`HHMMSS`-style run of date-format letters, or a glob
|
|
139
|
+
* character. Such a path never exists under its literal spelling, so
|
|
140
|
+
* `stale-path` skips it instead of flagging it.
|
|
141
|
+
*/
|
|
142
|
+
const PATH_PLACEHOLDER_PATTERN = /[<{]|[*?]|[YMDHS]{4,}/;
|
|
135
143
|
function checkStalePath(body) {
|
|
136
144
|
const pathRe = /(?:\/home\/|\/tmp\/|\/var\/|\/root\/|\/opt\/)[^\s"'`)\]>,\n]+/g;
|
|
137
145
|
let match;
|
|
@@ -139,6 +147,8 @@ function checkStalePath(body) {
|
|
|
139
147
|
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
|
|
140
148
|
while ((match = pathRe.exec(body)) !== null) {
|
|
141
149
|
const candidate = match[0];
|
|
150
|
+
if (PATH_PLACEHOLDER_PATTERN.test(candidate))
|
|
151
|
+
continue;
|
|
142
152
|
if (!fs.existsSync(candidate)) {
|
|
143
153
|
stale.push(candidate);
|
|
144
154
|
}
|
|
@@ -753,7 +753,9 @@ export async function akmLint(options = {}) {
|
|
|
753
753
|
// classic singular/plural typo ("workflow" for "workflows"). Non-akm
|
|
754
754
|
// adapters keep their own type vocabularies (see lintViaAdapter).
|
|
755
755
|
if (options.typeFilter && !STASH_SUBDIRS.includes(options.typeFilter)) {
|
|
756
|
-
|
|
756
|
+
warn(`Warning: lint --type "${options.typeFilter}" is not a recognized akm stash subdirectory — ` +
|
|
757
|
+
`valid types: ${STASH_SUBDIRS.join(", ")}. The whole bundle was validated.`);
|
|
758
|
+
options = { ...options, typeFilter: undefined };
|
|
757
759
|
}
|
|
758
760
|
return lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options);
|
|
759
761
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { defineGroupCommand, defineJsonCommand, EXIT_CODES,
|
|
4
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, outputWithExitCode } from "../cli/shared.js";
|
|
5
5
|
import { runMigrationTool } from "./migration-tool.js";
|
|
6
6
|
/**
|
|
7
7
|
* `akm migrate` is a thin wrapper over the standalone `akm-migrate`
|
|
@@ -23,9 +23,11 @@ export async function runMigrateSubcommand(command, args, runTool = runMigration
|
|
|
23
23
|
catch {
|
|
24
24
|
plan = undefined;
|
|
25
25
|
}
|
|
26
|
-
if (plan)
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (plan) {
|
|
27
|
+
outputWithExitCode(command, plan, result.status);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (line)
|
|
29
31
|
console.log(line);
|
|
30
32
|
if (result.status !== EXIT_CODES.SUCCESS)
|
|
31
33
|
process.exitCode = result.status;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { UsageError } from "../../core/errors.js";
|
|
20
|
+
import { warnOnce } from "../../core/warn.js";
|
|
20
21
|
import { PROPOSAL_SOURCES } from "./repository.js";
|
|
21
22
|
// Valid `generator` values for a drain rule are exactly the canonical proposal
|
|
22
23
|
// `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/repository.ts). The
|
|
@@ -87,7 +88,7 @@ const DrainAcceptRuleSchema = z
|
|
|
87
88
|
minContentLines: z.number().int().nonnegative().optional(),
|
|
88
89
|
requireType: z.string().optional(),
|
|
89
90
|
})
|
|
90
|
-
.
|
|
91
|
+
.passthrough();
|
|
91
92
|
const DrainPolicySchema = z
|
|
92
93
|
.object({
|
|
93
94
|
name: z.string().min(1),
|
|
@@ -95,7 +96,17 @@ const DrainPolicySchema = z
|
|
|
95
96
|
rejectEmpty: z.boolean(),
|
|
96
97
|
defer: z.array(GeneratorSchema),
|
|
97
98
|
})
|
|
98
|
-
.
|
|
99
|
+
.passthrough();
|
|
100
|
+
function warnIgnoredPolicyKeys(filePath, label, raw, knownKeys) {
|
|
101
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
102
|
+
return;
|
|
103
|
+
const extra = Object.keys(raw).filter((key) => !knownKeys.includes(key));
|
|
104
|
+
if (extra.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
warnOnce(`drain-policy-ignored-keys:${filePath}:${label}:${extra.join(",")}`, `[proposal] Policy file "${filePath}" has ${label} field(s) akm does not recognize and ignores: ${extra.join(", ")}. Check for a typo, or the file may be written for a newer akm version.`);
|
|
107
|
+
}
|
|
108
|
+
const DRAIN_POLICY_KNOWN_KEYS = Object.keys(DrainPolicySchema.shape);
|
|
109
|
+
const DRAIN_ACCEPT_RULE_KNOWN_KEYS = Object.keys(DrainAcceptRuleSchema.shape);
|
|
99
110
|
/**
|
|
100
111
|
* Resolve a `--policy <preset|path>` argument into a {@link DrainPolicy}.
|
|
101
112
|
*
|
|
@@ -127,5 +138,14 @@ export function resolveDrainPolicy(arg) {
|
|
|
127
138
|
if (!validated.success) {
|
|
128
139
|
throw new UsageError(`Invalid policy file "${value}": ${validated.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ")}`, "INVALID_FLAG_VALUE");
|
|
129
140
|
}
|
|
141
|
+
warnIgnoredPolicyKeys(value, "top-level", parsed, DRAIN_POLICY_KNOWN_KEYS);
|
|
142
|
+
if (parsed &&
|
|
143
|
+
typeof parsed === "object" &&
|
|
144
|
+
!Array.isArray(parsed) &&
|
|
145
|
+
Array.isArray(parsed.accept)) {
|
|
146
|
+
parsed.accept.forEach((rule, index) => {
|
|
147
|
+
warnIgnoredPolicyKeys(value, `accept[${index}]`, rule, DRAIN_ACCEPT_RULE_KNOWN_KEYS);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
130
150
|
return validated.data;
|
|
131
151
|
}
|
|
@@ -123,6 +123,42 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
|
|
|
123
123
|
function deferReasonForSource(source) {
|
|
124
124
|
return source === "distill" ? "possible-dup" : "mid-band";
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Map a thrown error's message to one of `DrainResult.failed`'s stable reason
|
|
128
|
+
* codes, falling back to `fallback` for anything not specifically recognized.
|
|
129
|
+
* Recognizes the write-time guards a proposal can trip during promotion
|
|
130
|
+
* (see repository.ts's `promoteProposalWithLease` / `preflightProposalPromotion`).
|
|
131
|
+
*/
|
|
132
|
+
function categorizeDrainFailure(message, fallback) {
|
|
133
|
+
if (/target (?:changed after|was created after) proposal/.test(message))
|
|
134
|
+
return "stale-target";
|
|
135
|
+
if (/failed validation:/.test(message))
|
|
136
|
+
return "validation";
|
|
137
|
+
return fallback;
|
|
138
|
+
}
|
|
139
|
+
function pushDrainFailure(result, id, err, fallbackReason) {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
result.failed.push({ id, reason: categorizeDrainFailure(message, fallbackReason), detail: message });
|
|
142
|
+
return message;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Mirror repository.ts's `promoteProposalWithLease` stale-target guard so a
|
|
146
|
+
* dry-run preflight predicts the same refusal a real promote would hit,
|
|
147
|
+
* without writing anything. `assetPath` is the path `preflightProposalPromotion`
|
|
148
|
+
* already resolved for this proposal.
|
|
149
|
+
*/
|
|
150
|
+
function assertProposalTargetFresh(proposal, assetPath) {
|
|
151
|
+
const backup = fs.existsSync(assetPath) ? fs.readFileSync(assetPath) : undefined;
|
|
152
|
+
const currentHash = backup ? createHash("sha256").update(backup).digest("hex") : undefined;
|
|
153
|
+
if (proposal.beforeHash !== undefined && (!backup || currentHash !== proposal.beforeHash)) {
|
|
154
|
+
throw new Error(`Proposal target changed after proposal ${proposal.id} was created; refusing to overwrite newer content.`);
|
|
155
|
+
}
|
|
156
|
+
if (proposal.beforeHash === undefined &&
|
|
157
|
+
backup !== undefined &&
|
|
158
|
+
proposal.changes.some((change) => change.op === "create")) {
|
|
159
|
+
throw new Error(`Proposal target was created after proposal ${proposal.id} was created; refusing to overwrite newer content.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
126
162
|
// ---------------------------------------------------------------------------
|
|
127
163
|
// Judgment tier (Phase 3)
|
|
128
164
|
// ---------------------------------------------------------------------------
|
|
@@ -493,6 +529,7 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
493
529
|
deferred: classification.deferred,
|
|
494
530
|
skippedByCap: [],
|
|
495
531
|
staged: [],
|
|
532
|
+
failed: [],
|
|
496
533
|
};
|
|
497
534
|
// A configured judgment runner makes every deferred item dispatch-eligible.
|
|
498
535
|
// Validate its symbolic credentials before applying any deterministic gate,
|
|
@@ -518,7 +555,8 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
518
555
|
result.rejected.push(target.id);
|
|
519
556
|
}
|
|
520
557
|
catch (err) {
|
|
521
|
-
|
|
558
|
+
const message = pushDrainFailure(result, target.id, err, "reject-error");
|
|
559
|
+
warn(`[triage] reject failed for ${target.id}: ${message}`);
|
|
522
560
|
}
|
|
523
561
|
}
|
|
524
562
|
// --- Accept ceiling: enforced BEFORE the promote loop ---
|
|
@@ -550,13 +588,15 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
550
588
|
deterministicPromoted += 1;
|
|
551
589
|
}
|
|
552
590
|
catch (err) {
|
|
553
|
-
|
|
591
|
+
const message = pushDrainFailure(result, id, err, "promote-error");
|
|
592
|
+
warn(`[triage] promote failed for ${id}: ${message}`);
|
|
554
593
|
}
|
|
555
594
|
}
|
|
556
595
|
}
|
|
557
596
|
else if (opts.applyMode === "promote" && opts.dryRun) {
|
|
558
|
-
// Exercise the same stamped candidate and
|
|
559
|
-
//
|
|
597
|
+
// Exercise the same stamped candidate, lint, and stale-target boundary as
|
|
598
|
+
// real promotion so a dry-run's predicted promotions match what a real
|
|
599
|
+
// run would do. Tests that omit config retain the classification-only seam.
|
|
560
600
|
const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
|
|
561
601
|
for (const id of withinCap) {
|
|
562
602
|
try {
|
|
@@ -564,7 +604,7 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
564
604
|
const proposal = byId.get(id);
|
|
565
605
|
if (!proposal)
|
|
566
606
|
throw new Error(`Proposal ${id} disappeared during drain preflight.`);
|
|
567
|
-
preflightProposalPromotion(opts.config, proposal, {
|
|
607
|
+
const preflight = preflightProposalPromotion(opts.config, proposal, {
|
|
568
608
|
...(opts.target ? { target: opts.target } : {}),
|
|
569
609
|
gateDecision: {
|
|
570
610
|
outcome: "auto-accepted",
|
|
@@ -572,12 +612,14 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
572
612
|
gate: gateLabel,
|
|
573
613
|
},
|
|
574
614
|
});
|
|
615
|
+
assertProposalTargetFresh(proposal, preflight.assetPath);
|
|
575
616
|
}
|
|
576
617
|
result.promoted.push(id);
|
|
577
618
|
deterministicPromoted += 1;
|
|
578
619
|
}
|
|
579
620
|
catch (err) {
|
|
580
|
-
|
|
621
|
+
const message = pushDrainFailure(result, id, err, "promote-error");
|
|
622
|
+
warn(`[triage] preflight failed for ${id}: ${message}`);
|
|
581
623
|
}
|
|
582
624
|
}
|
|
583
625
|
}
|
|
@@ -69,7 +69,7 @@ import { formatNewAssetDiff, formatUnifiedDiff } from "./diff-format.js";
|
|
|
69
69
|
import { isAutomatedProposalSource, isValidProposalSource, PROPOSAL_SOURCES, } from "./proposal-types.js";
|
|
70
70
|
import { hasCanonicalProposalValidator } from "./validators/proposal-validators.js";
|
|
71
71
|
import { repairProposalContent, validateProposal } from "./validators/proposals.js";
|
|
72
|
-
const
|
|
72
|
+
const PROMOTION_LINT_ISSUE_TYPES = new Set(["unquoted-colon", "missing-ref", "stale-path"]);
|
|
73
73
|
// ── Proposal domain types (moved to ./proposal-types.ts, WI-9.8 KILL 1) ─────
|
|
74
74
|
//
|
|
75
75
|
// Proposal / ProposalStatus / ProposalPayload / ProposalReview /
|
|
@@ -1546,7 +1546,7 @@ function promotionLintBlockers(raw, assetPath, targetRoot, refType, config) {
|
|
|
1546
1546
|
fix: false,
|
|
1547
1547
|
stashRoot: targetRoot,
|
|
1548
1548
|
extraStashRoots,
|
|
1549
|
-
}).filter((finding) =>
|
|
1549
|
+
}).filter((finding) => PROMOTION_LINT_ISSUE_TYPES.has(finding.issue));
|
|
1550
1550
|
}
|
|
1551
1551
|
/** Build and validate the exact stamped bytes promotion would publish, without writing. */
|
|
1552
1552
|
export function preflightProposalPromotion(config, proposal, options = {}, ctx) {
|
|
@@ -1571,8 +1571,8 @@ export function preflightProposalPromotion(config, proposal, options = {}, ctx)
|
|
|
1571
1571
|
: repairedContent;
|
|
1572
1572
|
const lintBlockers = promotionLintBlockers(stampedContent, assetPath, target.source.path, ref.type, config);
|
|
1573
1573
|
if (lintBlockers.length > 0) {
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1574
|
+
const summary = lintBlockers.map((finding) => `[${finding.issue}] ${finding.detail}`).join("; ");
|
|
1575
|
+
warn(`[proposal] promotion lint for ${proposal.id} found (non-blocking): ${summary}`);
|
|
1576
1576
|
}
|
|
1577
1577
|
return { proposal: preparedProposal, repairedContent, ref, target, assetPath, stampedContent };
|
|
1578
1578
|
}
|
|
@@ -370,14 +370,35 @@ const reflectSizeGuardValidator = {
|
|
|
370
370
|
];
|
|
371
371
|
},
|
|
372
372
|
};
|
|
373
|
+
/**
|
|
374
|
+
* Report a validator's findings as advisory.
|
|
375
|
+
*
|
|
376
|
+
* These validators judge prose quality — a description that reads like a
|
|
377
|
+
* heading, an odd backtick count, a body that grew more than the reflect
|
|
378
|
+
* ratio allows. They used to BLOCK `proposal accept`, which a human types
|
|
379
|
+
* after reading the diff, and the error told that human to "fix the proposal
|
|
380
|
+
* payload and try again" — but there is no `akm proposal edit` and `accept`
|
|
381
|
+
* takes no `--force`, so the only way out was hand-editing the proposals
|
|
382
|
+
* database. A blocking check whose remedy does not exist is not a check.
|
|
383
|
+
*
|
|
384
|
+
* Structural findings stay blocking: an empty body, an unparseable ref,
|
|
385
|
+
* malformed frontmatter and a broken workflow shape genuinely cannot be
|
|
386
|
+
* written, and they live in {@link defaultProposalValidators}.
|
|
387
|
+
*/
|
|
388
|
+
function advisory(validator) {
|
|
389
|
+
return {
|
|
390
|
+
...validator,
|
|
391
|
+
validate: (proposal, ctx) => validator.validate(proposal, ctx).map((finding) => ({ ...finding, severity: "warn" })),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
373
394
|
/**
|
|
374
395
|
* Full set of quality validators in registration order. Appended onto
|
|
375
396
|
* {@link defaultProposalValidators} so they run inside `validateProposal` on
|
|
376
|
-
* `proposal accept` automatically.
|
|
397
|
+
* `proposal accept` automatically, and report without blocking.
|
|
377
398
|
*/
|
|
378
399
|
export const defaultProposalQualityValidators = [
|
|
379
400
|
descriptionQualityValidator,
|
|
380
401
|
lessonContentQualityValidator,
|
|
381
402
|
sourceNotSupersededValidator,
|
|
382
403
|
reflectSizeGuardValidator,
|
|
383
|
-
];
|
|
404
|
+
].map(advisory);
|
|
@@ -26,14 +26,18 @@ export function validateProposal(proposal) {
|
|
|
26
26
|
* structure and applies {@link repairTruncatedDescription} to a truncated
|
|
27
27
|
* description when one is detected.
|
|
28
28
|
*
|
|
29
|
-
* Repairs performed
|
|
30
|
-
* 1.
|
|
31
|
-
* (e.g. `**description**: …` or `when_to_use: …` in the body).
|
|
32
|
-
* 2. Remove stray body `---` horizontal-rule lines (leaving exactly the two
|
|
33
|
-
* frontmatter fences when the content has a valid frontmatter block).
|
|
34
|
-
* 3. Apply {@link repairTruncatedDescription} to a truncated/hanging
|
|
29
|
+
* Repairs performed:
|
|
30
|
+
* 1. Apply {@link repairTruncatedDescription} to a truncated/hanging
|
|
35
31
|
* `description` field in the frontmatter.
|
|
36
32
|
*
|
|
33
|
+
* It deliberately does NOT delete body lines. Two earlier repairs dropped
|
|
34
|
+
* every body line that restated a frontmatter key and every `---` in a body
|
|
35
|
+
* with frontmatter. Both fired inside fenced code blocks, so any asset
|
|
36
|
+
* documenting frontmatter — a note about akm, Claude Code skills, Jekyll,
|
|
37
|
+
* Hugo — was silently gutted on `proposal accept`, and the repaired bytes
|
|
38
|
+
* were written back over the original in the proposals database. A repair
|
|
39
|
+
* that can destroy content is not a repair.
|
|
40
|
+
*
|
|
37
41
|
* Returns the repaired content string. When no repairs apply the input is
|
|
38
42
|
* returned byte-identical so callers can use strict equality to detect
|
|
39
43
|
* whether a repair actually happened.
|
|
@@ -81,19 +85,6 @@ export function repairProposalContent(content) {
|
|
|
81
85
|
repairedLines.push(line);
|
|
82
86
|
continue;
|
|
83
87
|
}
|
|
84
|
-
// Repair 1: Strip pseudo-frontmatter restatements in the body.
|
|
85
|
-
// Matches lines like `**description**: …` or `when_to_use: …`.
|
|
86
|
-
if (/^\s*(\*\*|__)?\s*(description|when_to_use)\s*(\*\*|__)?\s*:/i.test(line)) {
|
|
87
|
-
// Drop the line — it is a structural defect, not user content.
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
// Repair 2: Remove stray `---` horizontal-rule lines in the body.
|
|
91
|
-
// We keep these only when the content has NO frontmatter (in that case
|
|
92
|
-
// `---` is a legitimate thematic break in plain-body content).
|
|
93
|
-
if (isFence && hasFrontmatter) {
|
|
94
|
-
// Drop: these are extra `---` fences beyond the two frontmatter delimiters.
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
88
|
repairedLines.push(line);
|
|
98
89
|
}
|
|
99
90
|
let repaired = repairedLines.join("\n");
|