akm-cli 0.9.10 → 0.9.12
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 +226 -0
- package/STABILITY.md +28 -15
- 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/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 +209 -78
- package/dist/commands/health/improve-metrics.js +12 -0
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +15 -7
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/eval-cases.js +9 -2
- 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 +32 -4
- package/dist/commands/improve/loop-stages.js +13 -3
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- 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/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 +36 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/tasks/validate.js +186 -0
- package/dist/commands/url-checker.js +75 -16
- 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/bundle-id.js +7 -1
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +50 -4
- package/dist/core/config/config.js +21 -5
- 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 +19 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +21 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +2 -3
- package/dist/core/improve-result.js +8 -0
- 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 +124 -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/indexer/search/search-source.js +3 -2
- package/dist/integrations/agent/engine-resolution.js +107 -4
- package/dist/integrations/agent/execution-lowering.js +15 -2
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/agent/runner-dispatch.js +16 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +16 -0
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +10 -3
- package/dist/output/shapes.js +5 -3
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +2028 -1613
- package/dist/scripts/akm-migrate.js +2027 -1612
- 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 +15 -1
- 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/scheduler-sync.js +51 -25
- 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/dispatch-redaction.js +21 -7
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +19 -10
- package/dist/workflows/exec/step-work.js +35 -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/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 +76 -150
- 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/integration/bundling-akm.md +1 -1
- package/docs/migration/v0.8-to-v0.9.md +32 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +105 -22
- package/docs/reference/configuration.md +12 -2
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/tasks.md +8 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +11 -8
|
@@ -11,7 +11,7 @@ import { ConfigError, rethrowIfTestIsolationError, UsageError } from "../../core
|
|
|
11
11
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
12
12
|
import { classifyImproveAction, foldDistillSkipped } from "../../core/improve-types.js";
|
|
13
13
|
import { resolveMutationTarget } from "../../core/mutation-target.js";
|
|
14
|
-
import { getDbPath, getStateDbPathInDataDir } from "../../core/paths.js";
|
|
14
|
+
import { getDbPath, getStashLocksDir, getStateDbPathInDataDir } from "../../core/paths.js";
|
|
15
15
|
import { redactSensitiveText } from "../../core/redaction.js";
|
|
16
16
|
import { openStateDatabase } from "../../core/state-db.js";
|
|
17
17
|
import { info, warn, warnVerbose } from "../../core/warn.js";
|
|
@@ -481,8 +481,11 @@ function resolveImproveRunSetup(options) {
|
|
|
481
481
|
// calling test's own at this point; we capture it before yielding the loop.
|
|
482
482
|
const resolvedStateDbPath = getStateDbPathInDataDir();
|
|
483
483
|
// One conservative run lock protects the complete mutation window, including
|
|
484
|
-
// triage, indexing, proposal work, maintenance, and final stash sync.
|
|
485
|
-
|
|
484
|
+
// triage, indexing, proposal work, maintenance, and final stash sync. Moved
|
|
485
|
+
// out of `$STASH/.akm` to `$STATE/locks/<stash>/` (itlackey/akm#890): a lock
|
|
486
|
+
// file is machine-local coordination state, not content that must travel
|
|
487
|
+
// with the bundle.
|
|
488
|
+
const lockBaseDir = getStashLocksDir(primaryStashDir ?? options.stashDir ?? ".");
|
|
486
489
|
const resolvedLockPath = improveLockPath(lockBaseDir);
|
|
487
490
|
const effectiveSync = { ...improveProfile.sync, ...options.sync };
|
|
488
491
|
return {
|
|
@@ -1129,6 +1132,19 @@ async function runImproveStageSequence(args) {
|
|
|
1129
1132
|
},
|
|
1130
1133
|
}, eventsCtx);
|
|
1131
1134
|
}
|
|
1135
|
+
for (const item of resolvedPlan.engineUnavailable) {
|
|
1136
|
+
warn(`[improve] ${item.process} skipped — it ${item.reason}.`);
|
|
1137
|
+
appendEvent({
|
|
1138
|
+
eventType: "improve_skipped",
|
|
1139
|
+
ref: undefined,
|
|
1140
|
+
metadata: {
|
|
1141
|
+
strategy: selectedStrategy.name,
|
|
1142
|
+
reason: "engine_unavailable",
|
|
1143
|
+
process: item.process,
|
|
1144
|
+
configKey: item.configKey,
|
|
1145
|
+
},
|
|
1146
|
+
}, eventsCtx);
|
|
1147
|
+
}
|
|
1132
1148
|
// Single prep->loop->post-loop pass, run under the invocation's lock.
|
|
1133
1149
|
// Accumulators are direct assignments from the single pass's results.
|
|
1134
1150
|
let preparation;
|
|
@@ -1146,6 +1162,7 @@ async function runImproveStageSequence(args) {
|
|
|
1146
1162
|
// Concatenated arrays.
|
|
1147
1163
|
const allWarnings = [];
|
|
1148
1164
|
let deadUrls;
|
|
1165
|
+
let deadUrlCoverage;
|
|
1149
1166
|
const finalActions = [];
|
|
1150
1167
|
{
|
|
1151
1168
|
const runPreparation = () => runImprovePreparationStageImpl({
|
|
@@ -1228,6 +1245,9 @@ async function runImproveStageSequence(args) {
|
|
|
1228
1245
|
if (postLoopResult.deadUrls !== undefined) {
|
|
1229
1246
|
deadUrls = [...(deadUrls ?? []), ...postLoopResult.deadUrls];
|
|
1230
1247
|
}
|
|
1248
|
+
if (postLoopResult.deadUrlCoverage !== undefined) {
|
|
1249
|
+
deadUrlCoverage = postLoopResult.deadUrlCoverage;
|
|
1250
|
+
}
|
|
1231
1251
|
const maintenanceActions = postLoopResult.maintenanceActions;
|
|
1232
1252
|
if (maintenanceActions && maintenanceActions.length > 0) {
|
|
1233
1253
|
finalActions.push(...preparation.actions, ...maintenanceActions);
|
|
@@ -1250,6 +1270,7 @@ async function runImproveStageSequence(args) {
|
|
|
1250
1270
|
proposalsExpired,
|
|
1251
1271
|
allWarnings,
|
|
1252
1272
|
deadUrls,
|
|
1273
|
+
deadUrlCoverage,
|
|
1253
1274
|
finalActions,
|
|
1254
1275
|
};
|
|
1255
1276
|
}
|
|
@@ -1261,7 +1282,7 @@ async function runImproveStageSequence(args) {
|
|
|
1261
1282
|
function finalizeImproveResult(args) {
|
|
1262
1283
|
const { guidance, memorySummary, memoryCleanupPlan, strategyFilteredRefs, rawPlannedRefs, indexSnapshot, triageDrain, eventsCtx, } = args;
|
|
1263
1284
|
const { selectedStrategy, scope, options, primaryStashDir, startMs } = args.run;
|
|
1264
|
-
const { preparation, consolidation, memoryInference, graphExtraction, cycleMetrics, reflectsWithErrorContext, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, allWarnings, deadUrls, finalActions, } = args.seq;
|
|
1285
|
+
const { preparation, consolidation, memoryInference, graphExtraction, cycleMetrics, reflectsWithErrorContext, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, allWarnings, deadUrls, deadUrlCoverage, finalActions, } = args.seq;
|
|
1265
1286
|
// C1 (13-bus-factor): fold the per-ref `distill-skipped` rows (~13k/run,
|
|
1266
1287
|
// ~91% of result_json bytes) into a bounded aggregate BEFORE persistence.
|
|
1267
1288
|
// The metric total + per-reason breakdown are preserved on `distillSkipped`;
|
|
@@ -1326,6 +1347,10 @@ function finalizeImproveResult(args) {
|
|
|
1326
1347
|
...(preparation.extract && preparation.extract.length > 0 ? { extract: preparation.extract } : {}),
|
|
1327
1348
|
...(primaryStashDir !== undefined ? { evalCasesWritten: countEvalCases(primaryStashDir) } : {}),
|
|
1328
1349
|
...(deadUrls !== undefined && deadUrls.length > 0 ? { deadUrls } : {}),
|
|
1350
|
+
// Present whenever the check ran, unlike `deadUrls` above — a clean run
|
|
1351
|
+
// (zero dead links) still needs to tell the health report how much of
|
|
1352
|
+
// the bundle it actually covered (#892).
|
|
1353
|
+
...(deadUrlCoverage !== undefined ? { deadUrlCoverage } : {}),
|
|
1329
1354
|
...(reflectsWithErrorContext > 0 ? { reflectsWithErrorContext } : {}),
|
|
1330
1355
|
...(memoryInference ? { memoryInference } : {}),
|
|
1331
1356
|
...(graphExtraction ? { graphExtraction } : {}),
|
|
@@ -1501,6 +1526,9 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
|
|
|
1501
1526
|
coverageGapCount: result.coverageGaps?.length ?? 0,
|
|
1502
1527
|
evalCasesWritten: result.evalCasesWritten ?? 0,
|
|
1503
1528
|
deadUrlCount: result.deadUrls?.length ?? 0,
|
|
1529
|
+
deadUrlsChecked: result.deadUrlCoverage?.checked ?? 0,
|
|
1530
|
+
deadUrlsTotal: result.deadUrlCoverage?.total ?? 0,
|
|
1531
|
+
deadUrlsSkipped: result.deadUrlCoverage?.skipped ?? 0,
|
|
1504
1532
|
memoryEligible: result.memorySummary.eligible,
|
|
1505
1533
|
memoryDerived: result.memorySummary.derived,
|
|
1506
1534
|
memoryCleanupPruneCandidates: result.memoryCleanup?.pruneCandidates.length ?? 0,
|
|
@@ -569,8 +569,16 @@ export async function runImprovePostLoopStage(args) {
|
|
|
569
569
|
resolvedPlan,
|
|
570
570
|
});
|
|
571
571
|
let deadUrls;
|
|
572
|
+
let deadUrlCoverage;
|
|
572
573
|
if (scope.mode === "all" && primaryStashDir && actionableRefs.length > 0) {
|
|
573
574
|
try {
|
|
575
|
+
// Every actionable knowledge ref is scanned for URLs — there used to be
|
|
576
|
+
// a `.slice(0, 10)` here, capping the scan to the first ten refs while
|
|
577
|
+
// `deadUrlCoverage.total` counted only those, so a real bundle reported
|
|
578
|
+
// checked === total while most refs were never looked at (#892). URL
|
|
579
|
+
// extraction (a regex over already-loaded text) is cheap; it is the
|
|
580
|
+
// network requests that are expensive, and those are bounded by
|
|
581
|
+
// `checkDeadUrls`'s concurrency limit, not by trimming what gets scanned.
|
|
574
582
|
const knowledgeEntries = actionableRefs
|
|
575
583
|
.filter((r) => {
|
|
576
584
|
try {
|
|
@@ -580,7 +588,6 @@ export async function runImprovePostLoopStage(args) {
|
|
|
580
588
|
return false;
|
|
581
589
|
}
|
|
582
590
|
})
|
|
583
|
-
.slice(0, 10)
|
|
584
591
|
.map((r) => {
|
|
585
592
|
// The URL scan needs the document body; filePath is pre-resolved on
|
|
586
593
|
// eligible refs at planning time (#591). Best-effort — an unreadable
|
|
@@ -598,8 +605,10 @@ export async function runImprovePostLoopStage(args) {
|
|
|
598
605
|
});
|
|
599
606
|
if (knowledgeEntries.length > 0) {
|
|
600
607
|
info(`[improve] checking URLs in ${knowledgeEntries.length} knowledge refs`);
|
|
601
|
-
|
|
602
|
-
|
|
608
|
+
const urlCheck = await checkDeadUrls(primaryStashDir, knowledgeEntries);
|
|
609
|
+
deadUrls = urlCheck.deadUrls;
|
|
610
|
+
deadUrlCoverage = urlCheck.coverage;
|
|
611
|
+
info(`[improve] URL check complete (${deadUrls.length} dead/timeout URLs; checked ${urlCheck.coverage.checked} of ${urlCheck.coverage.total})`);
|
|
603
612
|
}
|
|
604
613
|
}
|
|
605
614
|
catch {
|
|
@@ -625,6 +634,7 @@ export async function runImprovePostLoopStage(args) {
|
|
|
625
634
|
return {
|
|
626
635
|
allWarnings,
|
|
627
636
|
deadUrls,
|
|
637
|
+
...(deadUrlCoverage ? { deadUrlCoverage } : {}),
|
|
628
638
|
...(cycleMetrics ? { cycleMetrics } : {}),
|
|
629
639
|
...(maintenanceResult.memoryInference ? { memoryInference: maintenanceResult.memoryInference } : {}),
|
|
630
640
|
...(maintenanceResult.graphExtraction ? { graphExtraction: maintenanceResult.graphExtraction } : {}),
|
|
@@ -25,7 +25,7 @@ import { isAutonomyLaneAllowed } from "./autonomy-gate.js";
|
|
|
25
25
|
import { akmConsolidate, inspectConsolidationPool } from "./consolidate.js";
|
|
26
26
|
import { computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
|
|
27
27
|
// Eligibility / candidate-selection predicates live in ./eligibility.
|
|
28
|
-
import { buildLatestFeedbackTsMap, buildLatestProposalTsMap, buildUtilityMap, dedupeRefs, findAssetFilePath, isDistillCandidateRef, isLessonCandidate, isSignalDeltaEligible, } from "./eligibility.js";
|
|
28
|
+
import { buildLatestFeedbackTsMap, buildLatestProposalTsMap, buildUtilityMap, dedupeRefs, findAssetFilePath, isDistillCandidateRef, isLessonCandidate, isSignalDeltaEligible, resolveImproveScope, } from "./eligibility.js";
|
|
29
29
|
import { akmExtract, countNewExtractCandidates } from "./extract.js";
|
|
30
30
|
import { computeValenceScore, FEEDBACK_WEIGHT, UTILITY_WEIGHT } from "./feedback-valence.js";
|
|
31
31
|
import { applyMemoryCleanup } from "./memory/memory-improve.js";
|
|
@@ -178,17 +178,20 @@ function evaluateConsolidationEligibility(args) {
|
|
|
178
178
|
// #553 minPoolSize guard: skip consolidation when the eligible memory pool is
|
|
179
179
|
// below a minimum size, rather than spending an LLM pass on a handful of
|
|
180
180
|
// memories. This is an INDEPENDENT skip condition from #551's mtime pool-delta
|
|
181
|
-
// gate — either can skip. Default
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
181
|
+
// gate — either can skip. Default 0 (disabled) — every built-in strategy
|
|
182
|
+
// used to ship 500, which meant `akm improve --strategy consolidate`, typed
|
|
183
|
+
// by a human, silently did nothing on almost every real install. Evaluated
|
|
184
|
+
// against the eligible-pool count BEFORE entering the LLM loop so a skip
|
|
185
|
+
// costs ZERO LLM calls when an operator opts back into a floor.
|
|
186
|
+
const CONSOLIDATE_DEFAULT_MIN_POOL_SIZE = 0;
|
|
185
187
|
const configuredMinPoolSize = improveProfile?.processes?.consolidate?.minPoolSize;
|
|
186
188
|
const minPoolSize = typeof configuredMinPoolSize === "number" ? configuredMinPoolSize : CONSOLIDATE_DEFAULT_MIN_POOL_SIZE;
|
|
187
189
|
const eligiblePoolSize = typeof memorySummary.eligible === "number" ? memorySummary.eligible : 0;
|
|
190
|
+
const userNamedStrategyOrScope = options.strategy !== undefined || resolveImproveScope(options.scope).mode === "ref";
|
|
188
191
|
// volumeTriggered means the pool already exceeds the volume threshold (100),
|
|
189
192
|
// so a force-triggered run never trips the pool-size guard. The guard only
|
|
190
193
|
// engages when minPoolSize > 0 and the eligible pool is strictly below it.
|
|
191
|
-
const poolBelowMinSize = !volumeTriggered && minPoolSize > 0 && eligiblePoolSize < minPoolSize;
|
|
194
|
+
const poolBelowMinSize = !volumeTriggered && !userNamedStrategyOrScope && minPoolSize > 0 && eligiblePoolSize < minPoolSize;
|
|
192
195
|
return {
|
|
193
196
|
volumeTriggered,
|
|
194
197
|
consolidationOnCooldown,
|
|
@@ -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,
|
|
@@ -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
|
}
|
|
@@ -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);
|