akm-cli 0.9.11 → 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.
Files changed (127) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/STABILITY.md +6 -1
  3. package/dist/assets/hints/cli-hints-full.md +1 -1
  4. package/dist/assets/improve-strategies/consolidate.json +1 -1
  5. package/dist/assets/improve-strategies/default.json +1 -1
  6. package/dist/assets/improve-strategies/thorough.json +1 -2
  7. package/dist/cli/shared.js +16 -4
  8. package/dist/cli.js +15 -13
  9. package/dist/commands/agent/agent-dispatch.js +8 -0
  10. package/dist/commands/command/execution-source-loader.js +25 -22
  11. package/dist/commands/command/portable-template.js +4 -26
  12. package/dist/commands/config-cli.js +10 -4
  13. package/dist/commands/env/env-binding.js +10 -3
  14. package/dist/commands/env/env-cli.js +7 -0
  15. package/dist/commands/env/secret-cli.js +15 -4
  16. package/dist/commands/health/checks.js +186 -71
  17. package/dist/commands/health.js +16 -4
  18. package/dist/commands/improve/distill/quality-gate.js +2 -2
  19. package/dist/commands/improve/distill.js +28 -12
  20. package/dist/commands/improve/execution.js +1 -2
  21. package/dist/commands/improve/extract.js +82 -56
  22. package/dist/commands/improve/improve-strategies.js +26 -8
  23. package/dist/commands/improve/improve.js +13 -0
  24. package/dist/commands/improve/preparation.js +9 -6
  25. package/dist/commands/improve/reflect.js +61 -77
  26. package/dist/commands/lint/index.js +3 -1
  27. package/dist/commands/migrate-cli.js +6 -4
  28. package/dist/commands/proposal/drain-policies.js +22 -2
  29. package/dist/commands/proposal/repository.js +4 -4
  30. package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
  31. package/dist/commands/proposal/validators/proposals.js +10 -19
  32. package/dist/commands/read/show.js +42 -31
  33. package/dist/commands/registry-cli.js +4 -2
  34. package/dist/commands/sources/init.js +4 -8
  35. package/dist/commands/sources/self-update.js +2 -2
  36. package/dist/commands/sources/source-clone.js +5 -7
  37. package/dist/commands/sources/sources-cli.js +3 -5
  38. package/dist/commands/tasks/tasks-cli.js +4 -12
  39. package/dist/commands/tasks/tasks.js +38 -35
  40. package/dist/commands/workflow-cli.js +17 -15
  41. package/dist/core/activation-policy.js +31 -3
  42. package/dist/core/adapter/execution-source.js +39 -11
  43. package/dist/core/asset/stash-meta.js +7 -41
  44. package/dist/core/common.js +8 -17
  45. package/dist/core/config/config-schema.js +3 -23
  46. package/dist/core/config/config-walker.js +50 -4
  47. package/dist/core/config/config.js +21 -5
  48. package/dist/core/config/legacy-source-shape-shim.js +79 -0
  49. package/dist/core/config/schema/embedding.js +2 -2
  50. package/dist/core/config/schema/engines.js +2 -2
  51. package/dist/core/config/schema/index-config.js +19 -21
  52. package/dist/core/config/schema/primitives.js +21 -10
  53. package/dist/core/config/schema/sources-bundles.js +1 -6
  54. package/dist/core/errors.js +2 -3
  55. package/dist/core/improve-types.js +17 -0
  56. package/dist/core/json-schema.js +1 -11
  57. package/dist/core/maintenance-barrier.js +17 -2
  58. package/dist/core/paths.js +12 -15
  59. package/dist/core/state/migrations.js +28 -0
  60. package/dist/core/state-db.js +28 -1
  61. package/dist/core/write-source.js +6 -6
  62. package/dist/indexer/bundle-identity-guard.js +3 -0
  63. package/dist/indexer/ensure-index.js +5 -0
  64. package/dist/indexer/indexer.js +11 -3
  65. package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
  66. package/dist/indexer/passes/metadata.js +16 -5
  67. package/dist/indexer/search/search-fields.js +1 -30
  68. package/dist/integrations/agent/engine-resolution.js +15 -1
  69. package/dist/integrations/agent/model-map.js +16 -10
  70. package/dist/integrations/agent/prompts.js +13 -6
  71. package/dist/integrations/lockfile.js +22 -7
  72. package/dist/llm/client.js +16 -0
  73. package/dist/llm/index-passes.js +3 -2
  74. package/dist/output/shapes/passthrough.js +9 -3
  75. package/dist/output/shapes.js +5 -3
  76. package/dist/output/text/workflow-format.js +8 -1
  77. package/dist/scripts/akm-migrate-node.js +1461 -1267
  78. package/dist/scripts/akm-migrate.js +1460 -1266
  79. package/dist/setup/setup.js +14 -21
  80. package/dist/sources/include.js +150 -20
  81. package/dist/sources/providers/git-install.js +14 -12
  82. package/dist/sources/providers/git-provider.js +3 -3
  83. package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
  84. package/dist/sources/website-url.js +12 -4
  85. package/dist/storage/engines/sqlite-migrations.js +40 -10
  86. package/dist/storage/like-pattern.js +7 -0
  87. package/dist/storage/repositories/extract-sessions-repository.js +23 -0
  88. package/dist/storage/repositories/index-connection.js +27 -10
  89. package/dist/storage/repositories/index-entry-schema.js +19 -2
  90. package/dist/storage/repositories/index-schema.js +30 -9
  91. package/dist/storage/repositories/proposals-repository.js +2 -1
  92. package/dist/storage/repositories/task-history-repository.js +14 -7
  93. package/dist/storage/repositories/workflow-runs-repository.js +15 -1
  94. package/dist/storage/sqlite-read-snapshot.js +11 -9
  95. package/dist/tasks/backends/cron.js +34 -5
  96. package/dist/tasks/backends/launchd.js +23 -26
  97. package/dist/tasks/backends/schtasks.js +50 -3
  98. package/dist/tasks/frozen-script.js +2 -0
  99. package/dist/tasks/prepare/prepare.js +2 -7
  100. package/dist/tasks/prepare/script-capture.js +38 -6
  101. package/dist/tasks/schedule.js +154 -13
  102. package/dist/tasks/source/task-source-v3-frozen.js +0 -1
  103. package/dist/tasks/source/task-source-v4.js +0 -1
  104. package/dist/workflows/exec/child-workflow.js +2 -3
  105. package/dist/workflows/exec/exec-unit.js +3 -4
  106. package/dist/workflows/exec/run-workflow.js +19 -10
  107. package/dist/workflows/exec/step-work.js +35 -56
  108. package/dist/workflows/freeze/resolve-steps.js +19 -11
  109. package/dist/workflows/freeze/source-freeze.js +7 -0
  110. package/dist/workflows/freeze/targets/child-workflow.js +12 -18
  111. package/dist/workflows/freeze/targets/command.js +14 -2
  112. package/dist/workflows/ir/environment-v4.js +4 -2
  113. package/dist/workflows/ir/freeze-v4.js +2 -5
  114. package/dist/workflows/ir/plan-hash.js +0 -3
  115. package/dist/workflows/ir/schema-v4.js +14 -9
  116. package/dist/workflows/ir/schema.js +1 -3
  117. package/dist/workflows/resource-limits.js +35 -48
  118. package/dist/workflows/runtime/plan-classifier.js +89 -41
  119. package/dist/workflows/runtime/run-outputs.js +1 -21
  120. package/dist/workflows/runtime/runs.js +76 -150
  121. package/dist/workflows/source-files.js +28 -54
  122. package/dist/workflows/source-ir/program.js +2 -2
  123. package/dist/workflows/source-ir/semantics.js +5 -23
  124. package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
  125. package/docs/reference/cli.md +74 -17
  126. package/package.json +1 -1
  127. 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
- return {
549
- content: payload.content,
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
- const proposal = proposalResult;
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. Type guard reflect only operates on asset types whose canonical
1315
- // shape is `frontmatter + markdown body`. Refuse non-markdown types
1316
- // (script / env / task) up-front so reflect never prepends YAML to a
1317
- // `.ts` file or rewrites a `.env` blob as prose. See REFLECT_ALLOWED_TYPES.
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, maxOutputChars } = buildReflectPrompt({
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
- const qualityJudgeSelection = resolveReflectQualityJudgeRunner(config, runnerSpec, isReflectQualityGateEnabled(activeStrategy), collectExecutionNotices);
1642
- if (qualityJudgeSelection.enabled && !qualityJudgeSelection.runner) {
1643
- return {
1644
- schemaVersion: 2,
1645
- ok: false,
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
- throw new UsageError(`lint: unknown --type "${options.typeFilter}". Valid types: ${STASH_SUBDIRS.join(", ")}.`, "INVALID_FLAG_VALUE");
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, output } from "../cli/shared.js";
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
- output(command, plan);
28
- else if (line)
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
- .strict();
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
- .strict();
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 PROMOTION_LINT_BLOCKERS = new Set(["unquoted-colon", "missing-ref", "stale-path"]);
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) => PROMOTION_LINT_BLOCKERS.has(finding.issue));
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 message = lintBlockers.map((finding) => `[${finding.issue}] ${finding.detail}`).join("\n");
1575
- throw new UsageError(`Proposal ${proposal.id} failed lint:\n${message}`, "INVALID_PROPOSAL", "Fix or explicitly suppress the reported lint findings, then retry.");
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 (in order):
30
- * 1. Strip body lines that restate frontmatter fields as pseudo-frontmatter
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");
@@ -31,7 +31,7 @@ import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError,
31
31
  import { appendEvent } from "../../core/events.js";
32
32
  import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
33
33
  import { presentationFor } from "../../core/type-presentation.js";
34
- import { warn } from "../../core/warn.js";
34
+ import { warn, warnOnce } from "../../core/warn.js";
35
35
  import { hasGraphData } from "../../indexer/db/graph-db.js";
36
36
  import { listRelatedPathsForFile } from "../../indexer/graph/graph-boost.js";
37
37
  import { extractGraphForSingleFile } from "../../indexer/graph/graph-extraction.js";
@@ -71,12 +71,11 @@ export async function akmShowUnified(input) {
71
71
  if (metaRef)
72
72
  return showStashMeta(metaRef);
73
73
  }
74
- // Env/secret bodies have no safe fragment surface. Reject from the canonical
75
- // ref namespace before auto-index or lookup can touch authored bytes. This is
76
- // deliberately independent of on-disk suffix probing: secret filenames keep
77
- // their natural extension, and a misspelled extensionless ref must not move
78
- // the sensitive-fragment policy behind a not-found result.
79
- assertSensitiveFragmentUnsupported(parseBundleRef(ref));
74
+ // Env/secret bodies have no safe fragment surface, and a fragment cannot
75
+ // widen what the env/secret renderers expose: both always omit the body
76
+ // (env key names only; secret never rendered), fragment or not. Warn
77
+ // and ignore the fragment rather than refusing the whole show.
78
+ warnSensitiveFragmentUnsupported(parseBundleRef(ref));
80
79
  // Auto-index when stale so the index is current before lookup.
81
80
  const { primarySource } = resolveReadSources();
82
81
  await ensurePrimaryIndexForRead(primarySource);
@@ -177,7 +176,7 @@ function enforceScopeOrThrow(filePath, ref, scope) {
177
176
  /** @internal Use akmShowUnified() for all external callers. */
178
177
  export async function showLocal(input) {
179
178
  const parsed = parseBundleRef(input.ref);
180
- assertSensitiveFragmentUnsupported(parsed);
179
+ warnSensitiveFragmentUnsupported(parsed);
181
180
  const assetParts = typeNameFromConceptId(parsed.conceptId);
182
181
  const config = loadConfig();
183
182
  const allSources = resolveSourceEntries(input.stashDir);
@@ -194,12 +193,19 @@ export async function showLocal(input) {
194
193
  `Stash "${parsed.bundle}" is not installed. Run: ${installCmd}`);
195
194
  }
196
195
  if (!indexedEntry && !resolution.owner) {
197
- const unsupportedExtension = existingUnsupportedScriptExtension(assetParts, searchSources);
198
- if (unsupportedExtension !== undefined) {
199
- const displayExtension = unsupportedExtension || "no extension";
200
- throw new NotFoundError(`Script ref "${makeBundleRef(parsed.bundle, parsed.conceptId)}" resolves to an existing file with ` +
201
- `unsupported extension "${displayExtension}". Script refs must use a supported script extension: ` +
202
- `${[...SCRIPT_EXTENSIONS].join(", ")}.`, "ASSET_NOT_FOUND");
196
+ const unrecognized = findUnrecognizedScriptSource(assetParts, searchSources);
197
+ if (unrecognized) {
198
+ const displayExtension = unrecognized.extension || "no extension";
199
+ warn(`Script ref "${makeBundleRef(parsed.bundle, parsed.conceptId)}" has extension "${displayExtension}", which is outside the recognized set used for indexing (${[...SCRIPT_EXTENSIONS].join(", ")}); showing it as plain text.`);
200
+ const fileCtx = buildFileContext(unrecognized.sourceRoot, unrecognized.path);
201
+ const renderer = await getRenderer("script-source");
202
+ if (renderer) {
203
+ const match = { type: "script", specificity: 0, renderer: "script-source" };
204
+ const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs);
205
+ const response = renderer.buildShowResponse(renderCtx);
206
+ response.name = assetParts?.name ?? response.name;
207
+ return response;
208
+ }
203
209
  }
204
210
  }
205
211
  if (!indexedEntry || !assetPath) {
@@ -242,9 +248,11 @@ export async function showLocal(input) {
242
248
  response = renderer.buildShowResponse(renderCtx);
243
249
  if (parsed.fragment !== undefined) {
244
250
  if (!match.renderer.endsWith("-md")) {
245
- throw new UsageError(`Fragments are not supported for ${makeBundleRef(parsed.bundle, parsed.conceptId)}. Only Markdown documents support heading fragments.`, "INVALID_FLAG_VALUE");
251
+ warn(`Fragment "#${parsed.fragment}" was ignored: ${makeBundleRef(parsed.bundle, parsed.conceptId)} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`);
252
+ }
253
+ else {
254
+ applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName);
246
255
  }
247
- applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName);
248
256
  }
249
257
  }
250
258
  }
@@ -318,22 +326,22 @@ export async function showLocal(input) {
318
326
  }
319
327
  return fullResponse;
320
328
  }
321
- /** Reject body fragments for namespaces whose authored bytes are sensitive. */
322
- function assertSensitiveFragmentUnsupported(ref) {
329
+ /**
330
+ * Warn and ignore body fragments for namespaces whose authored bytes are
331
+ * sensitive. `warnOnce`-keyed on the exact ref: `akmShowUnified` calls this
332
+ * before delegating to `showLocal`, which calls it again as its own
333
+ * defense-in-depth for callers that use `showLocal` directly — a single
334
+ * request must not print the same warning twice.
335
+ */
336
+ function warnSensitiveFragmentUnsupported(ref) {
323
337
  if (ref.fragment === undefined)
324
338
  return;
325
339
  const type = typeNameFromConceptId(ref.conceptId)?.type;
326
340
  if (type !== "env" && type !== "secret")
327
341
  return;
328
- throw new UsageError(`Fragments are not supported for ${makeBundleRef(ref.bundle, ref.conceptId)}. Sensitive ${type} assets do not expose body fragments.`, "INVALID_FLAG_VALUE");
342
+ warnOnce(`sensitive-fragment:${makeBundleRef(ref.bundle, ref.conceptId)}#${ref.fragment}`, `Fragment "#${ref.fragment}" was ignored: sensitive ${type} assets do not expose body fragments. Showing ${makeBundleRef(ref.bundle, ref.conceptId)} in full.`);
329
343
  }
330
- /**
331
- * Return the unsupported extension only when the exact canonical AKM script
332
- * path exists as a contained regular file. Physical owner arbitration runs
333
- * first; this is a diagnostic for its miss, never an alternate owner or
334
- * runnable-file classifier. No authored bytes are read.
335
- */
336
- function existingUnsupportedScriptExtension(assetParts, sources) {
344
+ function findUnrecognizedScriptSource(assetParts, sources) {
337
345
  if (assetParts?.type !== "script")
338
346
  return undefined;
339
347
  const extension = path.extname(assetParts.name);
@@ -357,7 +365,7 @@ function existingUnsupportedScriptExtension(assetParts, sources) {
357
365
  const realCandidate = fs.realpathSync(candidate);
358
366
  if (!isWithin(realCandidate, realRoot) || !fs.statSync(realCandidate).isFile())
359
367
  continue;
360
- return extension;
368
+ return { extension, path: realCandidate, sourceRoot: realRoot };
361
369
  }
362
370
  catch {
363
371
  // Missing, unreadable, dangling, or otherwise unsafe paths remain normal
@@ -447,7 +455,7 @@ async function maybeExtractGraphInline(config, sourceStashDir, assetPath) {
447
455
  export async function showByRef(ref) {
448
456
  const parsed = parseBundleRef(ref);
449
457
  if (parsed.fragment !== undefined) {
450
- throw new UsageError(`Fragments are not accepted by raw show: ${ref}`, "INVALID_FLAG_VALUE");
458
+ warn(`Fragment "#${parsed.fragment}" was ignored by raw show: ${ref}. Returning the whole file.`);
451
459
  }
452
460
  const entry = await lookupBundleRef(parsed);
453
461
  if (!entry) {
@@ -488,12 +496,15 @@ function indexedMatch(entry, renderer) {
488
496
  return { type: entry.type, specificity: Number.MAX_SAFE_INTEGER, renderer, meta: { name: entry.name } };
489
497
  }
490
498
  function buildIndexedProjectionResponse(entry, assetPath, fragment) {
491
- if (fragment !== undefined && path.extname(assetPath).toLowerCase() !== ".md") {
492
- throw new UsageError(`Fragments are not supported for ${entry.conceptId}. Only Markdown documents support heading fragments.`, "INVALID_FLAG_VALUE");
499
+ const isMarkdown = path.extname(assetPath).toLowerCase() === ".md";
500
+ if (fragment !== undefined && !isMarkdown) {
501
+ warn(`Fragment "#${fragment}" was ignored: ${entry.conceptId} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`);
493
502
  }
494
503
  const raw = fs.readFileSync(assetPath, "utf8");
495
504
  const parsed = parseFrontmatter(raw);
496
- const content = fragment ? requireMarkdownSection(parsed.content, fragment, entry.name).content : parsed.content;
505
+ const content = fragment !== undefined && isMarkdown
506
+ ? requireMarkdownSection(parsed.content, fragment, entry.name).content
507
+ : parsed.content;
497
508
  const description = entry.document?.description ?? asNonEmptyString(parsed.data.description);
498
509
  const tags = entry.document?.tags ??
499
510
  (Array.isArray(parsed.data.tags)
@@ -4,7 +4,7 @@
4
4
  import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
5
5
  import { getEffectiveRegistries, loadUserConfig, mutateConfig } from "../core/config/config.js";
6
6
  import { NotFoundError, UsageError } from "../core/errors.js";
7
- import { formatRegistryUrl, hasRegistryUrlCredentials, REGISTRY_CREDENTIALS_UNSUPPORTED, registryEntryForOutput, } from "../core/registry-url.js";
7
+ import { formatRegistryLabel, formatRegistryUrl, hasRegistryUrlCredentials, registryEntryForOutput, } from "../core/registry-url.js";
8
8
  import { warn } from "../core/warn.js";
9
9
  export const registryCommand = defineGroupCommand({
10
10
  meta: { name: "registry", description: "Manage bundle registries" },
@@ -33,7 +33,9 @@ export const registryCommand = defineGroupCommand({
33
33
  },
34
34
  async run({ args }) {
35
35
  if (hasRegistryUrlCredentials(args.url)) {
36
- throw new UsageError(REGISTRY_CREDENTIALS_UNSUPPORTED);
36
+ warn(`Registry ${formatRegistryLabel({ url: args.url, name: args.name })} was added, but its URL's ` +
37
+ "username/password is ignored: authenticated registries are not supported by the built-in " +
38
+ "static-index or skills-sh providers.");
37
39
  }
38
40
  if (!args.url.startsWith("http")) {
39
41
  throw new UsageError("Registry URL must start with http:// or https://");