@event4u/agent-config 12.1.0 → 14.0.0

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 (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +164 -186
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/cost/report.md +23 -0
  5. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  6. package/dist/agent-src/commands/fix/ci.md +39 -1
  7. package/dist/agent-src/commands/pr/create.md +65 -4
  8. package/dist/agent-src/commands/roadmap/next.md +28 -5
  9. package/dist/agent-src/commands/work.md +16 -0
  10. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  11. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  12. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  13. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  14. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  15. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  16. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  17. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  18. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  19. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  20. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  21. package/dist/agent-src/rules/design-fidelity.md +1 -1
  22. package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
  23. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  24. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  25. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  26. package/dist/agent-src/rules/ui-audit-gate.md +34 -12
  27. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  28. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  29. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  30. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  31. package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
  32. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  33. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  34. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  35. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  36. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  37. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  38. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  39. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  40. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  41. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  42. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  43. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  44. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  45. package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
  46. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  47. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  48. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  49. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  50. package/dist/agent-src/templates/roadmaps.md +43 -3
  51. package/dist/cli/registry.js +1 -0
  52. package/dist/cli/registry.js.map +1 -1
  53. package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
  54. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  55. package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
  57. package/dist/cli-delegate/cmd_conformance.js +224 -104
  58. package/dist/cli-delegate/cmd_doctor.js +2 -2
  59. package/dist/cli-delegate/cmd_export.js +2 -2
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +2 -2
  62. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  63. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +2 -2
  65. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  66. package/dist/cli-delegate/cmd_validate.js +2 -2
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +73 -30
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +3 -3
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +7 -5
  74. package/dist/discovery/trust-report.md +3 -3
  75. package/dist/discovery/workspaces.json +6 -4
  76. package/dist/hooks/dispatch.js +2653 -1365
  77. package/dist/install/install.mjs +12 -4
  78. package/dist/mcp/registry-manifest.json +3 -3
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +2679 -887
  81. package/dist/router.json +1 -1
  82. package/dist/scripts/condense.js +1 -1
  83. package/dist/scripts/condense.js.map +1 -1
  84. package/dist/server/schemas/settings.js +2 -2
  85. package/dist/server/schemas/settings.js.map +1 -1
  86. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  87. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  88. package/dist/ui/index.html +1 -1
  89. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  90. package/docs/guidelines/design-handover-extraction.md +87 -0
  91. package/docs/guidelines/prompt-templates.md +24 -1
  92. package/package.json +1 -1
  93. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  94. package/src/agent-src/scripts/gate_execute.ts +395 -0
  95. package/src/agent-src/scripts/resume_probe.ts +422 -0
  96. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  97. package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
  98. package/src/config/ci-local-parity.yml +13 -0
  99. package/src/config/evaluator-budgets.json +9 -8
  100. package/src/config/gate-coverage.yml +140 -12
  101. package/src/config/gate-violation-baselines.json +9 -3
  102. package/src/config/hook-token-budget.json +8 -1
  103. package/src/config/rule-activation-census.json +41 -0
  104. package/src/config/rule-stub-ceilings.json +52 -0
  105. package/src/scripts/_dispatch.bash +4 -0
  106. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  107. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  108. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  109. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  110. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  111. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  112. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  113. package/src/scripts/_lib/gh_transient.ts +80 -0
  114. package/src/scripts/_lib/release_material.ts +16 -5
  115. package/src/scripts/_lib/review_independence.ts +111 -0
  116. package/src/scripts/_lib/session_register.ts +14 -0
  117. package/src/scripts/_lib/skill_catalogue.ts +278 -5
  118. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  119. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  120. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  121. package/src/scripts/ai_council/_py_json.ts +96 -0
  122. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  123. package/src/scripts/ai_council/clients.ts +101 -164
  124. package/src/scripts/ai_council/config.ts +25 -11
  125. package/src/scripts/ai_council/probe_store.ts +196 -0
  126. package/src/scripts/ai_council/qualification.ts +425 -0
  127. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  128. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  129. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  130. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  131. package/src/scripts/bench_ab_v2_run.ts +3 -42
  132. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  133. package/src/scripts/bench_ab_v2_search.ts +343 -0
  134. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  135. package/src/scripts/build_archive_index.ts +383 -0
  136. package/src/scripts/capture_skill_catalogue.ts +369 -4
  137. package/src/scripts/check_evaluator_schema.ts +249 -0
  138. package/src/scripts/check_pr_ci_current.ts +409 -0
  139. package/src/scripts/check_review_schema.ts +195 -0
  140. package/src/scripts/check_rule_activation_census.ts +451 -0
  141. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  142. package/src/scripts/check_source_size_budget.ts +428 -0
  143. package/src/scripts/check_suppression_hygiene.ts +5 -1
  144. package/src/scripts/condense.ts +1 -1
  145. package/src/scripts/cost/backfill_rates.mjs +298 -0
  146. package/src/scripts/cost/budget.mjs +12 -32
  147. package/src/scripts/cost_summary.ts +25 -0
  148. package/src/scripts/council_cli.ts +141 -191
  149. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  150. package/src/scripts/external_sources_denylist.json +1 -0
  151. package/src/scripts/hook_manifest.yaml +99 -6
  152. package/src/scripts/hooks/concern_registry.ts +6 -0
  153. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  154. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  155. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  156. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  157. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  158. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  159. package/src/scripts/hooks_doctor.ts +112 -0
  160. package/src/scripts/interruption_report.ts +443 -0
  161. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  162. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  163. package/src/scripts/lint_harvest_provenance.ts +35 -1
  164. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  165. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  166. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  167. package/src/scripts/lint_skill_router_head.ts +21 -0
  168. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  169. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  170. package/src/scripts/mcp_server/tools.ts +87 -0
  171. package/src/scripts/release.ts +43 -58
  172. package/src/scripts/routing_doctor.ts +50 -13
  173. package/src/scripts/rule_activation_census.ts +292 -0
  174. package/src/scripts/schemas/command.schema.json +1 -1
  175. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  176. package/src/scripts/schemas/review-findings.schema.json +31 -0
  177. package/src/scripts/schemas/skill.schema.json +18 -1
  178. package/src/scripts/self_review_gate.ts +6 -0
  179. package/src/scripts/session_register_hook.ts +30 -1
  180. package/src/scripts/sync_pr_branch.ts +271 -0
  181. package/src/scripts/test_council_qualification.ts +213 -0
  182. package/src/scripts/trigger_coverage.ts +224 -3
  183. package/src/server/schemas/settings.ts +2 -2
  184. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
  185. package/src/config/budget-routing.json +0 -7
@@ -66,6 +66,7 @@ import { fileURLToPath } from 'node:url';
66
66
 
67
67
  import { parse as parseYaml } from 'yaml';
68
68
 
69
+ import { GateLedger } from './_lib/gate_ledger.js';
69
70
  import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
70
71
 
71
72
  const _HERE = fileURLToPath(import.meta.url);
@@ -404,6 +405,48 @@ function main(argv: readonly string[] = process.argv.slice(2)): number {
404
405
  const errors = findings.filter((f) => f.kind === 'unreachable-route');
405
406
  const advisories = findings.filter((f) => f.kind === 'unrouted-skill');
406
407
 
408
+ // Per-target completeness accounting over BOTH populations this gate
409
+ // reasons about, id-prefixed so they stay distinguishable in one tally.
410
+ // The skips are the point: check 1 walks past every unscoped rule and
411
+ // check 2 past every unscoped skill, because "ships everywhere" makes the
412
+ // reachability question inapplicable rather than satisfied. Without the
413
+ // ledger those walk-pasts were indistinguishable from checks that passed —
414
+ // the exact "checked 20 of 300, printed one green line" shape.
415
+ //
416
+ // This tally is deliberately NOT the `scanned:` line below. That line is
417
+ // the machine-read coverage contract and stays the rule count; this is the
418
+ // human-auditable breakdown beside it, per `_lib/gate_ledger` § report.
419
+ const ledger = new GateLedger('lint_rule_skill_pack_reach');
420
+ const routedSkills = [...new Set(rules.flatMap((r) => r.routesToSkills))]
421
+ .filter((sk) => skillPacks.has(sk))
422
+ .sort();
423
+ ledger.plan([
424
+ ...rules.map((r) => `rule:${r.name}`),
425
+ ...routedSkills.map((sk) => `skill:${sk}`),
426
+ ]);
427
+ const failedSubjects = new Set(findings.map((f) => f.subject));
428
+ for (const rule of rules) {
429
+ const id = `rule:${rule.name}`;
430
+ if (rule.packs.length === 0) {
431
+ ledger.skip(id, 'not_applicable_kind');
432
+ } else if ([...failedSubjects].some((sub) => sub.startsWith(`${rule.name} → skill:`))) {
433
+ ledger.fail(id, 'unreachable-route');
434
+ } else {
435
+ ledger.complete(id);
436
+ }
437
+ }
438
+ for (const sk of routedSkills) {
439
+ const id = `skill:${sk}`;
440
+ if ((skillPacks.get(sk) ?? []).length === 0) {
441
+ ledger.skip(id, 'not_applicable_kind');
442
+ } else if (failedSubjects.has(id)) {
443
+ ledger.fail(id, 'unrouted-skill');
444
+ } else {
445
+ ledger.complete(id);
446
+ }
447
+ }
448
+ ledger.report();
449
+
407
450
  // --quiet suppresses the per-finding lines but never the count line: this
408
451
  // gate is expected to stay non-empty for a while, and a quiet run that
409
452
  // printed nothing at all would read as clean.
@@ -38,6 +38,7 @@ import * as fs from 'node:fs';
38
38
  import * as path from 'node:path';
39
39
  import { fileURLToPath, pathToFileURL } from 'node:url';
40
40
 
41
+ import { GateLedger } from './_lib/gate_ledger.js';
41
42
  import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
42
43
 
43
44
  const _FILE = fileURLToPath(import.meta.url);
@@ -204,6 +205,26 @@ export function main(argv: string[] = process.argv.slice(2)): number {
204
205
  const heads = collectSkillHeads(path.join(REPO, SKILLS_REL));
205
206
  const { findings, staleAllowlist } = evaluate(heads);
206
207
 
208
+ // Per-target completeness accounting. The interesting outcome here is the
209
+ // SKIP: a grandfathered head is over the cap and deliberately unchecked, so
210
+ // without a ledger it was indistinguishable from a head that passed. The
211
+ // allowlist size is already printed, but only the ledger ties each skipped
212
+ // name to the reason it went unchecked.
213
+ const ledger = new GateLedger('lint_skill_router_head');
214
+ ledger.plan(heads.map((h) => h.name));
215
+ const failed = new Set(findings.map((f) => f.skill));
216
+ const allow = new Set(GRANDFATHERED);
217
+ for (const h of heads) {
218
+ if (failed.has(h.name)) {
219
+ ledger.fail(h.name, `${String(h.lines)} lines over the ${String(MAX_HEAD_LINES)}-line cap with no mode bodies`);
220
+ } else if (h.lines > MAX_HEAD_LINES && allow.has(h.name)) {
221
+ ledger.skip(h.name, 'declared_exemption');
222
+ } else {
223
+ ledger.complete(h.name);
224
+ }
225
+ }
226
+ ledger.report();
227
+
207
228
  for (const s of staleAllowlist) {
208
229
  process.stderr.write(
209
230
  `⚠️ lint_skill_router_head: '${s}' is grandfathered but no longer over the cap — remove it from GRANDFATHERED so the ratchet keeps its teeth\n`,
@@ -544,10 +544,33 @@
544
544
  },
545
545
  {
546
546
  "name": "suggest_skill_for_task",
547
- "description": "[stub — implemented on demand] Match a free-form task description to the most relevant skills, ranked by their frontmatter triggers. Use to pick a skill for a described task before starting work. Read-only. Returns a ranked list of skills with name, description, and match reason.",
547
+ "description": "Match a free-form task description to the most relevant skills, ranked by a deterministic keyword scorer over SKILL.md frontmatter. Use when a skill you need is not in the catalogue the host delivered — a measured host dropped 402 entries from its model-visible list — so asking by name is impossible while asking by task is not. Read-only: no shell, no writes, and no skill bodies are returned, only names, scores and declared personas.",
548
548
  "side_effect": "ro",
549
- "implemented_on": [],
550
- "input_schema": {}
549
+ "implemented_on": [
550
+ "stdio"
551
+ ],
552
+ "input_schema": {
553
+ "type": "object",
554
+ "required": [
555
+ "task"
556
+ ],
557
+ "properties": {
558
+ "task": {
559
+ "type": "string",
560
+ "description": "Free-form description of the task to match skills against."
561
+ },
562
+ "limit": {
563
+ "type": "integer",
564
+ "minimum": 1,
565
+ "default": 5,
566
+ "description": "Maximum number of skills to return. Defaults to 5."
567
+ }
568
+ },
569
+ "additionalProperties": false
570
+ },
571
+ "annotations": {
572
+ "readOnlyHint": true
573
+ }
551
574
  },
552
575
  {
553
576
  "name": "sync_agent_settings",
@@ -96,30 +96,12 @@ export const STUB_TOOLS: readonly StubToolSource[] = [
96
96
  additionalProperties: false,
97
97
  },
98
98
  },
99
- {
100
- name: 'suggest_skill_for_task',
101
- description:
102
- 'Match a free-form task description to the most relevant ' +
103
- 'skills, ranked by their frontmatter triggers. Use to pick a ' +
104
- 'skill for a described task before starting work. Read-only. ' +
105
- 'Returns a ranked list of skills with name, description, and ' +
106
- 'match reason.',
107
- side_effect: 'ro',
108
- input_schema: {
109
- type: 'object',
110
- properties: {
111
- task: { type: 'string', description: 'Free-form description of the task to find a skill for.' },
112
- limit: {
113
- type: 'integer',
114
- minimum: 1,
115
- default: 5,
116
- description: 'Maximum number of skills to return. Defaults to 5.',
117
- },
118
- },
119
- required: ['task'],
120
- additionalProperties: false,
121
- },
122
- },
99
+ // `suggest_skill_for_task` moved to ALLOWLIST in mcp_server/tools.ts
100
+ // (road-to-inbox-harvest-2026-08-d-runtime-skill-routing 4.1) — it is
101
+ // implemented now, and `build_mcp_catalog` refuses a name registered in
102
+ // both places. Its published input schema (`task` + `limit`) was carried
103
+ // over verbatim rather than re-designed: the stub is a contract consumers
104
+ // may already have read.
123
105
  {
124
106
  name: 'mine_session',
125
107
  description:
@@ -1210,7 +1210,94 @@ async function _readResourceBodyHandler(
1210
1210
  // unlisted tool. Boot-time stderr log enumerates the registered set.
1211
1211
  // ---------------------------------------------------------------------
1212
1212
 
1213
+ /**
1214
+ * `suggest_skill_for_task` — the recovery path for a truncated catalogue.
1215
+ *
1216
+ * Promoted from a discovery stub because the defect it answers is measured, not
1217
+ * hypothetical: on the one host that publishes its own truncation, a default
1218
+ * install had 402 entries dropped from the model-visible skills list — the
1219
+ * host's own count against its own denominator, not a subtraction over ours
1220
+ * (`agents/evidence/analysis/scoped-projection-host-delivery.md`). An agent
1221
+ * that cannot see a skill also cannot ask for it by name, so the only reachable
1222
+ * question is "what fits this task" — which is what the deterministic ranker
1223
+ * answers, over the TREE rather than over the host's catalogue.
1224
+ *
1225
+ * Read-only and shell-free by construction: it reads SKILL.md frontmatter and
1226
+ * returns names, scores and declared personas. No skill BODY is returned — the
1227
+ * caller opens what it picked, so a wrong rank costs one read rather than a
1228
+ * context payload.
1229
+ */
1230
+ async function _suggestSkillForTaskHandler(
1231
+ args: Record<string, unknown>,
1232
+ consumerRoot: string,
1233
+ ): Promise<Record<string, unknown>> {
1234
+ const task = typeof args['task'] === 'string' ? args['task'] : '';
1235
+ if (task.trim() === '') {
1236
+ return { status: 'error', error: 'task must be a non-empty string', suggestions: [] };
1237
+ }
1238
+ // `limit`, not `top` — the stub published `limit` and a consumer may have
1239
+ // read that contract already. Renaming it on promotion would break a
1240
+ // caller for cosmetic reasons.
1241
+ const limitRaw = args['limit'];
1242
+ const limit =
1243
+ typeof limitRaw === 'number' && Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 5;
1244
+
1245
+ const catalogue = await import('../_lib/skill_catalogue.js');
1246
+ const skillsDir = catalogue.resolveSkillsRoot(consumerRoot);
1247
+ if (skillsDir === null) {
1248
+ // A missing catalogue is UNKNOWN, never "no skill fits this task". The
1249
+ // second reads as a ranked answer and would be a confident empty list —
1250
+ // the same zero-from-silence failure `capture_skill_catalogue` refuses.
1251
+ return {
1252
+ status: 'no_catalogue',
1253
+ searched: [...catalogue.DEFAULT_CATALOGUE_ROOTS],
1254
+ consumer_root: consumerRoot,
1255
+ suggestions: [],
1256
+ };
1257
+ }
1258
+
1259
+ const { rank } = await import('../skill_tools/score_skill_relevance.js');
1260
+ const rows = rank(task, skillsDir).slice(0, Math.max(1, limit));
1261
+ return {
1262
+ status: 'ok',
1263
+ skills_root: skillsDir,
1264
+ suggestions: rows.map(([name, score, personas]) => ({ skill: name, score, personas })),
1265
+ };
1266
+ }
1267
+
1213
1268
  export const ALLOWLIST: Record<string, BuiltinTool> = {
1269
+ suggest_skill_for_task: {
1270
+ name: 'suggest_skill_for_task',
1271
+ side_effect: 'ro',
1272
+ description:
1273
+ 'Match a free-form task description to the most relevant skills, ' +
1274
+ 'ranked by a deterministic keyword scorer over SKILL.md ' +
1275
+ 'frontmatter. Use when a skill you need is not in the catalogue ' +
1276
+ 'the host delivered — a measured host dropped 402 entries from ' +
1277
+ 'its model-visible list — so asking by name is impossible while ' +
1278
+ 'asking by task is not. ' +
1279
+ 'Read-only: no shell, no writes, and no skill bodies are returned, ' +
1280
+ 'only names, scores and declared personas.',
1281
+ input_schema: {
1282
+ type: 'object',
1283
+ required: ['task'],
1284
+ properties: {
1285
+ task: {
1286
+ type: 'string',
1287
+ description:
1288
+ 'Free-form description of the task to match skills against.',
1289
+ },
1290
+ limit: {
1291
+ type: 'integer',
1292
+ minimum: 1,
1293
+ default: 5,
1294
+ description: 'Maximum number of skills to return. Defaults to 5.',
1295
+ },
1296
+ },
1297
+ additionalProperties: false,
1298
+ },
1299
+ handler: _suggestSkillForTaskHandler,
1300
+ },
1214
1301
  lint_skills: {
1215
1302
  name: 'lint_skills',
1216
1303
  side_effect: 'ro',
@@ -124,7 +124,9 @@ import {
124
124
  derive_category_hits,
125
125
  render_derived_head_values,
126
126
  } from './_lib/release_highlights.js';
127
+ import { gh_argv_label, gh_retry } from './_lib/gh_transient.js';
127
128
  import {
129
+ NEXT_SECTION_RE,
128
130
  extract_changelog_section,
129
131
  pr_body_from_section,
130
132
  release_notes_from_section,
@@ -616,6 +618,31 @@ function git(args: readonly string[], opts: { capture?: boolean } = {}): string
616
618
  return capture ? r.stdout.trim() : '';
617
619
  }
618
620
 
621
+ function _sleep_ms(ms: number): void {
622
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
623
+ }
624
+
625
+ /**
626
+ * `gh` with transient-failure retry and a readable death. Always captures, so
627
+ * the classifier can see stderr; `check: false` keeps the caller's fallback.
628
+ */
629
+ function gh(args: readonly string[], opts: { check?: boolean } = {}): RunResult {
630
+ const check = opts.check ?? true;
631
+ const r = gh_retry(args, (argv) => run(argv, { check: false, capture: true }), {
632
+ sleep: (ms) => (_exec_override ? undefined : _sleep_ms(ms)),
633
+ notify: (m) => process.stdout.write(`${m}\n`),
634
+ });
635
+ if (check && r.returncode !== 0) {
636
+ die(`${gh_argv_label(args)} failed (${r.returncode}):\n${(r.stderr || r.stdout).trim()}`);
637
+ }
638
+ // Mutating calls used to inherit stdio — echo, or the PR / release URL
639
+ // vanishes. Probes (`check: false`) stay quiet; their stdout is JSON.
640
+ if (check && r.stdout.trim()) {
641
+ process.stdout.write(`${r.stdout.trim()}\n`);
642
+ }
643
+ return r;
644
+ }
645
+
619
646
  /**
620
647
  * One name per failing check, from `gh pr checks --json name,bucket` output.
621
648
  *
@@ -879,14 +906,14 @@ function watch_pr_checks(branch: string): void {
879
906
  *
880
907
  * git's wording for this is stable across versions: the hint is `fetch first`
881
908
  * / `Updates were rejected`, and the ref line carries `[rejected]` with
882
- * `non-fast-forward` or `fetch first`.
909
+ * `non-fast-forward` or `fetch first`. A stale lease is a SECOND wording —
910
+ * `[remote rejected]` + `cannot lock ref … but expected`; pinned in the tests.
883
911
  */
884
912
  export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
885
913
  const text = `${stderr}\n${stdout}`;
886
- return (
887
- /\[rejected\]/i.test(text) &&
888
- /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text)
889
- );
914
+ if (!/\[(?:remote )?rejected\]/i.test(text)) return false;
915
+ if (/cannot lock ref[^\n]*but expected/i.test(text)) return true;
916
+ return /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text);
890
917
  }
891
918
 
892
919
  function push_release_branch(branch: string): void {
@@ -923,13 +950,9 @@ function push_release_branch(branch: string): void {
923
950
 
924
951
  /** mergeStateStatus of the branch's PR ('' when the probe fails). */
925
952
  function _pr_merge_state(branch: string): string {
926
- const r = run(
927
- ['gh', 'pr', 'view', branch, '--json', 'mergeStateStatus', '--jq', '.mergeStateStatus'],
928
- {
929
- check: false,
930
- capture: true,
931
- },
932
- );
953
+ const r = gh(['pr', 'view', branch, '--json', 'mergeStateStatus', '--jq', '.mergeStateStatus'], {
954
+ check: false,
955
+ });
933
956
  return r.returncode === 0 ? r.stdout.trim() : '';
934
957
  }
935
958
 
@@ -973,14 +996,11 @@ function _refresh_pr_body_from_head(branch: string, target: string): void {
973
996
  }
974
997
  const capped = _cap_body(section!.body, GH_PR_BODY_LIMIT - 200, '`CHANGELOG.md` in this PR');
975
998
  const body = pr_body_from_section(capped, target);
976
- const live = run(['gh', 'pr', 'view', branch, '--json', 'body', '-q', '.body'], {
977
- check: false,
978
- capture: true,
979
- });
999
+ const live = gh(['pr', 'view', branch, '--json', 'body', '-q', '.body'], { check: false });
980
1000
  if (live.returncode === 0 && live.stdout.replace(/\r\n/gu, '\n').trim() === body.trim()) {
981
1001
  return;
982
1002
  }
983
- run(['gh', 'pr', 'edit', branch, '--body', body]);
1003
+ gh(['pr', 'edit', branch, '--body', body]);
984
1004
  }
985
1005
 
986
1006
  function merge_release_pr(branch: string, wait_for_checks: boolean): void {
@@ -1147,22 +1167,8 @@ function _push_tag(tag: string): void {
1147
1167
 
1148
1168
  /** Most recent PR (any state) with `release/X.Y.Z` as head, or null. */
1149
1169
  function _pr_for_branch(branch: string): Record<string, unknown> | null {
1150
- const r = run(
1151
- [
1152
- 'gh',
1153
- 'pr',
1154
- 'list',
1155
- '--head',
1156
- branch,
1157
- '--state',
1158
- 'all',
1159
- '--json',
1160
- 'number,state,url',
1161
- '--limit',
1162
- '1',
1163
- ],
1164
- { check: false, capture: true },
1165
- );
1170
+ const argv = ['pr', 'list', '--head', branch, '--state', 'all', '--json', 'number,state,url'];
1171
+ const r = gh([...argv, '--limit', '1'], { check: false });
1166
1172
  if (r.returncode !== 0) {
1167
1173
  return null;
1168
1174
  }
@@ -1476,8 +1482,7 @@ function _previous_test_count_from_changelog(prev_tag: string | null): number |
1476
1482
  }
1477
1483
  const headEnd = m.index + m[0].length;
1478
1484
  const rest = text.slice(headEnd);
1479
- const next_re = /^##\s+\[?\d+\.\d+\.\d+/m;
1480
- const next_heading = next_re.exec(rest);
1485
+ const next_heading = NEXT_SECTION_RE.exec(rest);
1481
1486
  const sectionEnd = headEnd + (next_heading ? next_heading.index : rest.length);
1482
1487
  const section = text.slice(headEnd, sectionEnd);
1483
1488
  const count_match = _TEST_COUNT_LINE_RE.exec(section);
@@ -2141,19 +2146,8 @@ function execute(
2141
2146
  '`CHANGELOG.md` in this PR',
2142
2147
  );
2143
2148
  const pr_body = pr_body_from_section(pr_changelog, plan.target);
2144
- run([
2145
- 'gh',
2146
- 'pr',
2147
- 'create',
2148
- '--base',
2149
- MAIN_BRANCH,
2150
- '--head',
2151
- branch,
2152
- '--title',
2153
- `release: ${plan.target}`,
2154
- '--body',
2155
- pr_body,
2156
- ]);
2149
+ gh(['pr', 'create', '--base', MAIN_BRANCH, '--head', branch,
2150
+ '--title', `release: ${plan.target}`, '--body', pr_body]);
2157
2151
  }
2158
2152
 
2159
2153
  // ─── 6. wait for checks ─────────────────────────────────────────────────
@@ -2236,16 +2230,7 @@ function execute(
2236
2230
  GH_RELEASE_NOTES_LIMIT,
2237
2231
  '`CHANGELOG.md`',
2238
2232
  );
2239
- run([
2240
- 'gh',
2241
- 'release',
2242
- 'create',
2243
- plan.target,
2244
- '--title',
2245
- plan.target,
2246
- '--notes',
2247
- notes,
2248
- ]);
2233
+ gh(['release', 'create', plan.target, '--title', plan.target, '--notes', notes]);
2249
2234
 
2250
2235
  // ─── 9b. dispatch-chain the tag-triggered workflows (--ci only) ──────
2251
2236
  // release-guard.yml, publish-npm.yml, and cloud-release.yml all
@@ -63,7 +63,7 @@ import {
63
63
  classifyTask,
64
64
  type ActivationInputs,
65
65
  } from "./_lib/auto_dispatch.js";
66
- import { TIER_ORDER, readCooldowns } from "./_lib/tier_budget_routing.js";
66
+ import { COOLDOWN_FILE, TIER_ORDER, readCooldowns } from "./_lib/tier_budget_routing.js";
67
67
  import {
68
68
  instructionsLoadedRecord,
69
69
  measureStandingDelivery,
@@ -111,6 +111,13 @@ export interface OrchestrationReport {
111
111
  downshift: boolean;
112
112
  /** cost.budgets.per_tier ceilings + live cool-down state per tier. */
113
113
  tier_budgets: Record<string, { ceiling_usd: number | null; cooldown_until_ms: number }>;
114
+ /**
115
+ * Whether the cool-down store exists on disk. `readCooldowns` returns all
116
+ * zeros both when no tier is cooling AND when nothing has ever written the
117
+ * file, so a bare zero cannot distinguish "measured: live" from "no reading
118
+ * exists". This flag is what separates them.
119
+ */
120
+ cooldown_store_present: boolean;
114
121
  host_manifest: HostCapabilityManifest;
115
122
  /**
116
123
  * The host identifier the capability registry was keyed on, and whether it
@@ -158,14 +165,23 @@ export interface OrchestrationReport {
158
165
  }
159
166
 
160
167
  /**
161
- * Adherence ≠ delivery: budget-aware tier routing has no on/off setting any
162
- * more (always-on orchestration removed `subagents.budget_routing`), so the
163
- * doctor always checks the DELIVERY evidence — orchestration audit lines
164
- * that carry a tier decision. Dispatches recorded with ZERO tier-carrying
165
- * lines → WARN with the exact reason. Read-only over
166
- * agents/runtime/state/audit/*.jsonl; no new state. This closes the same
167
- * blind-spot class the session-canary incident exposed (policy present,
168
- * delivery silently absent).
168
+ * Tier-decision coverage in the orchestration record.
169
+ *
170
+ * REPOINTED 2026-08-16. This used to check "delivery evidence" for budget-aware
171
+ * tier routing, on the reasoning that policy can be present while delivery is
172
+ * silently absent — the session-canary blind-spot class. That layer is now
173
+ * ARCHIVED (`docs/contracts/budget-routing.md`), so the old rationale no longer
174
+ * holds and the old warning would have been permanently true and un-actionable:
175
+ * nothing dispatches through a tier decision by design, so telling the operator
176
+ * "the relation may not be running" is a dead advisory of exactly the shape this
177
+ * repository keeps removing elsewhere.
178
+ *
179
+ * What it measures now is narrower and still worth having: how many recorded
180
+ * dispatches carry a `tier` at all. That is the SAME field the archived layer's
181
+ * revisit-if depends on — a saving can only be computed once both the chosen and
182
+ * the realized tier are recorded — so this is the instrument that would show the
183
+ * reopen condition arriving. It reports coverage rather than warning about it.
184
+ * Read-only over agents/runtime/state/audit/*.jsonl; no new state.
169
185
  */
170
186
  export function check_budget_delivery(
171
187
  workspace_root: string,
@@ -202,9 +218,10 @@ export function check_budget_delivery(
202
218
  }
203
219
  if (out.eligible_dispatches > 0 && out.budget_evidence_lines === 0) {
204
220
  out.warning =
205
- `budget-aware tier routing shows no delivery evidence: ` +
206
- `${out.eligible_dispatches} orchestration dispatch(es) recorded, 0 carry a tier decision — ` +
207
- `the relation may not be running (adherence != delivery)`;
221
+ `tier-decision coverage is 0 of ${out.eligible_dispatches} recorded dispatch(es) — ` +
222
+ `expected while budget routing stays archived (docs/contracts/budget-routing.md). ` +
223
+ `This is the counter its revisit-if reads: a non-zero value here is the signal ` +
224
+ `that per-tier dispatch has reappeared and the decision can be reopened`;
208
225
  }
209
226
  return out;
210
227
  }
@@ -356,6 +373,7 @@ export function collect_orchestration(
356
373
  const ledger_present = fs.existsSync(path.join(tracking_dir, "sessions.jsonl"));
357
374
  const per_tier = (cost_budgets["per_tier"] ?? {}) as Record<string, unknown>;
358
375
  const cooldowns = readCooldowns(tracking_dir);
376
+ const cooldown_store_present = fs.existsSync(path.join(tracking_dir, COOLDOWN_FILE));
359
377
  const tier_budgets: OrchestrationReport["tier_budgets"] = {};
360
378
  for (const t of TIER_ORDER) {
361
379
  const v = per_tier[t];
@@ -382,6 +400,7 @@ export function collect_orchestration(
382
400
  halted,
383
401
  downshift,
384
402
  tier_budgets,
403
+ cooldown_store_present,
385
404
  host_manifest,
386
405
  host_platform: platform,
387
406
  host_platform_assumed: platform_assumed,
@@ -636,10 +655,28 @@ function _render(report: DoctorReport): string {
636
655
  );
637
656
  const tierBits = Object.entries(o.tier_budgets).map(([t, s]) => {
638
657
  const cap = s.ceiling_usd === null ? "no cap" : `$${s.ceiling_usd}`;
639
- const cool = s.cooldown_until_ms > 0 ? " COOLING" : "";
658
+ // `> Date.now()`, not `> 0`. The purger went with the archived permit
659
+ // lifecycle (`tripCooldown` rewrote the map on every write), so a stale
660
+ // entry now has nothing to clear it and `> 0` would render COOLING for
661
+ // ever. Latent rather than live — there is no writer either — but the
662
+ // comparison should be the one that stays correct if a writer returns.
663
+ const cool = s.cooldown_until_ms > Date.now() ? " COOLING" : "";
640
664
  return `${t}=${cap}${cool}`;
641
665
  });
642
666
  lines.push(` tier budgets (rolling-24h): ${tierBits.join(" · ")}`);
667
+ // `readCooldowns` returns all zeros both when no tier is cooling and when
668
+ // nothing has ever written the store, so a missing COOLING marker alone
669
+ // cannot be read as a measured "this tier is live". Report which of the two
670
+ // it is — the same unavailable-vs-false distinction the capability-
671
+ // provenance line above makes two lines earlier. Derived from the store's
672
+ // existence rather than from a caller count, so it stays true if the
673
+ // cool-down producer is ever wired.
674
+ if (!o.cooldown_store_present) {
675
+ lines.push(
676
+ ` cool-down state: no reading on disk (${COOLDOWN_FILE} absent) — ` +
677
+ `absence of COOLING above is not evidence of a live tier`,
678
+ );
679
+ }
643
680
  if (o.delivery.warning) {
644
681
  lines.push(` ⚠️ ${o.delivery.warning}`);
645
682
  } else if (o.delivery.eligible_dispatches > 0) {