@event4u/agent-config 9.16.0 → 9.18.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +80 -130
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/agent-handoff.md +100 -48
  6. package/dist/agent-src/commands/challenge-me/vision.md +5 -0
  7. package/dist/agent-src/commands/commit.md +1 -1
  8. package/dist/agent-src/commands/condense.md +19 -64
  9. package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
  10. package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
  11. package/dist/agent-src/commands/feature/plan.md +20 -0
  12. package/dist/agent-src/commands/feature/roadmap.md +19 -0
  13. package/dist/agent-src/commands/memory/promote.md +14 -1
  14. package/dist/agent-src/commands/mode.md +9 -5
  15. package/dist/agent-src/commands/optimize/deep.md +72 -11
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/roadmap/create.md +29 -0
  18. package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
  19. package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
  20. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  21. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  22. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
  23. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
  24. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
  25. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
  26. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
  27. package/dist/agent-src/profiles/agency.yml +3 -3
  28. package/dist/agent-src/profiles/content_creator.yml +2 -2
  29. package/dist/agent-src/profiles/developer.yml +2 -2
  30. package/dist/agent-src/profiles/finance.yml +1 -1
  31. package/dist/agent-src/profiles/founder.yml +2 -2
  32. package/dist/agent-src/profiles/ops.yml +2 -2
  33. package/dist/agent-src/rules/active-remediation.md +2 -0
  34. package/dist/agent-src/rules/architecture.md +4 -2
  35. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  36. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  37. package/dist/agent-src/rules/brand-consistency.md +10 -62
  38. package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
  39. package/dist/agent-src/rules/broken-access-control.md +4 -0
  40. package/dist/agent-src/rules/cli-output-handling.md +5 -0
  41. package/dist/agent-src/rules/code-comment-discipline.md +3 -0
  42. package/dist/agent-src/rules/commit-conventions.md +2 -0
  43. package/dist/agent-src/rules/context-hygiene.md +3 -2
  44. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  45. package/dist/agent-src/rules/design-fidelity.md +2 -0
  46. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  47. package/dist/agent-src/rules/docker-commands.md +4 -0
  48. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  49. package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
  50. package/dist/agent-src/rules/domain-safety-pii.md +1 -1
  51. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  52. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  55. package/dist/agent-src/rules/laravel-routing.md +4 -0
  56. package/dist/agent-src/rules/legal-safety-floor.md +3 -0
  57. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +5 -0
  59. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  60. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  61. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  62. package/dist/agent-src/rules/output-discipline.md +9 -2
  63. package/dist/agent-src/rules/php-coding.md +2 -0
  64. package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
  65. package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
  66. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  67. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  68. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  69. package/dist/agent-src/rules/scale-discipline.md +3 -0
  70. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  71. package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
  72. package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
  73. package/dist/agent-src/rules/skill-quality.md +2 -0
  74. package/dist/agent-src/rules/source-confidentiality.md +4 -0
  75. package/dist/agent-src/rules/source-discovery-gate.md +3 -0
  76. package/dist/agent-src/rules/source-of-truth.md +2 -0
  77. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  78. package/dist/agent-src/rules/think-before-action.md +5 -0
  79. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  80. package/dist/agent-src/rules/ui-audit-gate.md +2 -0
  81. package/dist/agent-src/rules/user-interaction.md +3 -4
  82. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  83. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
  84. package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
  85. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
  86. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  87. package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
  88. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
  89. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
  90. package/dist/agent-src/skills/design-review/SKILL.md +4 -141
  91. package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
  92. package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
  93. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
  94. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
  95. package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
  96. package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
  97. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
  98. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  99. package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
  100. package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
  101. package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
  102. package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
  103. package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
  104. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  105. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  106. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  107. package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
  108. package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
  109. package/dist/agent-src/templates/agent-settings.md +13 -15
  110. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
  111. package/dist/agent-src/templates/persona.md +2 -2
  112. package/dist/agent-src/templates/roadmaps.md +13 -1
  113. package/dist/agent-src/templates/rule.md +1 -1
  114. package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  115. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  116. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  117. package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  118. package/dist/agent-src/templates/skill.md +1 -1
  119. package/dist/cli/registry.js +3 -0
  120. package/dist/cli/registry.js.map +1 -1
  121. package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
  122. package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
  123. package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
  124. package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
  125. package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
  126. package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
  127. package/dist/cli-delegate/cmd_conformance.js +4 -3
  128. package/dist/cli-delegate/cmd_converge.js +3 -3
  129. package/dist/cli-delegate/cmd_doctor.js +3 -2
  130. package/dist/cli-delegate/cmd_explain.js +36 -36
  131. package/dist/cli-delegate/cmd_export.js +4 -4
  132. package/dist/cli-delegate/cmd_handoff.js +1169 -0
  133. package/dist/cli-delegate/cmd_memory_get.js +35 -3754
  134. package/dist/cli-delegate/cmd_migrate.js +2 -2
  135. package/dist/cli-delegate/cmd_prune.js +4 -4
  136. package/dist/cli-delegate/cmd_route_audit.js +249 -0
  137. package/dist/cli-delegate/cmd_route_explain.js +285 -0
  138. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  139. package/dist/cli-delegate/cmd_sync.js +4 -4
  140. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  141. package/dist/cli-delegate/cmd_update.js +3 -3
  142. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  143. package/dist/cli-delegate/cmd_validate.js +4 -4
  144. package/dist/cli-delegate/cmd_versions.js +3 -3
  145. package/dist/discovery/deprecation-report.md +1 -1
  146. package/dist/discovery/discovery-manifest.json +84 -84
  147. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  148. package/dist/discovery/discovery-manifest.summary.md +1 -1
  149. package/dist/discovery/orphan-report.md +1 -1
  150. package/dist/discovery/packs.json +2 -2
  151. package/dist/discovery/trust-report.md +1 -1
  152. package/dist/discovery/workspaces.json +2 -2
  153. package/dist/hooks/dispatch.js +602 -507
  154. package/dist/install/install.mjs +34 -0
  155. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  156. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  157. package/dist/mcp/registry-manifest.json +4 -4
  158. package/dist/mcp/server.json +2 -2
  159. package/dist/mcp/server.mjs +25 -3
  160. package/dist/router.json +1 -1
  161. package/dist/scripts/_lib/agent_src.js +39 -6
  162. package/dist/scripts/_lib/agent_src.js.map +1 -1
  163. package/dist/server/schemas/settings.js +25 -0
  164. package/dist/server/schemas/settings.js.map +1 -1
  165. package/docs/guidelines/abstraction-thresholds.md +53 -0
  166. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
  167. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
  168. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
  169. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
  170. package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
  171. package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
  172. package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
  173. package/llms.txt +1 -0
  174. package/package.json +1 -1
  175. package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  176. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  177. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  178. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  179. package/src/config/agent-settings.template.yml +68 -0
  180. package/src/config/evaluator-budgets.json +8 -11
  181. package/src/config/gate-coverage.yml +70 -0
  182. package/src/scripts/_cli/cmd_explain.ts +41 -36
  183. package/src/scripts/_cli/cmd_handoff.ts +405 -0
  184. package/src/scripts/_cli/cmd_route_audit.ts +290 -0
  185. package/src/scripts/_cli/cmd_route_explain.ts +269 -0
  186. package/src/scripts/_cli/handoff_generate.ts +453 -0
  187. package/src/scripts/_cli/handoff_sessions.ts +435 -0
  188. package/src/scripts/_dispatch.bash +32 -0
  189. package/src/scripts/_lib/agent_src.ts +36 -6
  190. package/src/scripts/_lib/git_env.ts +40 -0
  191. package/src/scripts/_lib/md_table.ts +27 -0
  192. package/src/scripts/_lib/planning_settings.ts +43 -0
  193. package/src/scripts/_lib/release_material.ts +156 -0
  194. package/src/scripts/_lib/router_match.ts +301 -0
  195. package/src/scripts/annotate_r1_outcomes.ts +258 -0
  196. package/src/scripts/check_completion_review.ts +1140 -0
  197. package/src/scripts/check_condensation.ts +2 -1
  198. package/src/scripts/check_finding_dispositions.ts +278 -0
  199. package/src/scripts/check_pack_size.ts +4 -3
  200. package/src/scripts/check_references.ts +1 -0
  201. package/src/scripts/check_release_highlights.ts +300 -0
  202. package/src/scripts/check_release_pr_shape.ts +4 -0
  203. package/src/scripts/check_release_surface_equality.ts +132 -0
  204. package/src/scripts/check_reply_consistency.ts +187 -138
  205. package/src/scripts/compile_router.ts +20 -3
  206. package/src/scripts/dispatch_r2_reviewer.ts +968 -0
  207. package/src/scripts/eval_ui_triviality.ts +156 -0
  208. package/src/scripts/handoff_context_hook.ts +148 -0
  209. package/src/scripts/hook_manifest.yaml +17 -7
  210. package/src/scripts/hooks/concern_registry.ts +2 -0
  211. package/src/scripts/lint_abstraction_thresholds.ts +327 -0
  212. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  213. package/src/scripts/lint_plan_risk_register.ts +931 -0
  214. package/src/scripts/lint_profile_personas.ts +153 -0
  215. package/src/scripts/lint_token_budget_discipline.ts +148 -0
  216. package/src/scripts/lint_trigger_collisions.ts +257 -0
  217. package/src/scripts/release.ts +156 -32
  218. package/src/scripts/release_drill.ts +491 -0
  219. package/src/scripts/router_telemetry.ts +16 -290
  220. package/src/scripts/schemas/command.schema.json +33 -0
  221. package/src/scripts/schemas/rule.schema.json +10 -0
  222. package/src/scripts/self_review_gate.ts +41 -3
  223. package/src/scripts/skill_linter.ts +3 -1
  224. package/src/scripts/smoke/router.sh +1 -1
  225. package/src/scripts/update_counts.ts +60 -12
  226. package/src/server/schemas/settings.ts +41 -0
  227. package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Release-surface equality gate (release-truth Phase 1).
3
+ *
4
+ * Asserts `normalized(release-PR body) == normalized(CHANGELOG entry)` for
5
+ * the version a release PR carries — whitespace/anchor normalization only,
6
+ * never "similar". The recorded failure this closes: at 9.14.0 the PR body
7
+ * and the merged changelog disagreed on scope and test count because the
8
+ * surfaces were rendered at different times; with `release.ts` deriving every
9
+ * surface from the changelog section this gate proves the derivation held at
10
+ * the head CI actually sees.
11
+ *
12
+ * Usage:
13
+ * check_release_surface_equality --pr <number> # fetch body via gh
14
+ * check_release_surface_equality --body-file <path> # fixture / local
15
+ * [--changelog <path>] [--version <X.Y.Z>]
16
+ *
17
+ * `--version` defaults to package.json's version (the release PR has already
18
+ * bumped it). Exit codes: 0 equal · 1 divergence · 2 usage/extraction error.
19
+ */
20
+ import { spawnSync } from 'node:child_process';
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+ import process from 'node:process';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ import {
27
+ extract_changelog_section,
28
+ strip_pr_wrapper,
29
+ surface_divergence,
30
+ } from './_lib/release_material.js';
31
+
32
+ const _HERE = fileURLToPath(import.meta.url);
33
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
34
+
35
+ export interface EqualityInput {
36
+ prBody: string;
37
+ changelogText: string;
38
+ version: string;
39
+ }
40
+
41
+ export interface EqualityResult {
42
+ ok: boolean;
43
+ reason: string | null;
44
+ }
45
+
46
+ /** Pure core — the CLI and the release-validation job both run exactly this. */
47
+ export function check_surface_equality(input: EqualityInput): EqualityResult {
48
+ const section = extract_changelog_section(input.changelogText, input.version);
49
+ if (!section) {
50
+ return {
51
+ ok: false,
52
+ reason: `CHANGELOG carries no section for ${input.version}`,
53
+ };
54
+ }
55
+ const middle = strip_pr_wrapper(input.prBody, input.version);
56
+ if (middle === null) {
57
+ return {
58
+ ok: false,
59
+ reason:
60
+ `PR body does not carry the canonical "Release ${input.version}." prefix — ` +
61
+ 'it was not generated from the changelog section',
62
+ };
63
+ }
64
+ const divergence = surface_divergence(middle, section.body);
65
+ return divergence === null ? { ok: true, reason: null } : { ok: false, reason: divergence };
66
+ }
67
+
68
+ function _pr_body(prNumber: string): string {
69
+ const r = spawnSync('gh', ['pr', 'view', prNumber, '--json', 'body', '-q', '.body'], {
70
+ encoding: 'utf-8',
71
+ });
72
+ if (r.status !== 0) {
73
+ process.stderr.write(`gh pr view ${prNumber} failed: ${r.stderr}\n`);
74
+ process.exit(2);
75
+ }
76
+ return r.stdout;
77
+ }
78
+
79
+ function main(argv: readonly string[]): number {
80
+ let pr: string | null = null;
81
+ let bodyFile: string | null = null;
82
+ let changelogPath = path.join(REPO_ROOT, 'CHANGELOG.md');
83
+ let version: string | null = null;
84
+ for (let i = 0; i < argv.length; i++) {
85
+ const a = argv[i];
86
+ if (a === '--pr') pr = argv[++i] ?? null;
87
+ else if (a === '--body-file') bodyFile = argv[++i] ?? null;
88
+ else if (a === '--changelog') changelogPath = argv[++i] ?? changelogPath;
89
+ else if (a === '--version') version = argv[++i] ?? null;
90
+ else {
91
+ process.stderr.write(`unknown argument: ${a}\n`);
92
+ return 2;
93
+ }
94
+ }
95
+ if ((pr === null) === (bodyFile === null)) {
96
+ process.stderr.write('exactly one of --pr / --body-file is required\n');
97
+ return 2;
98
+ }
99
+ if (version === null) {
100
+ const pkg = JSON.parse(
101
+ fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8'),
102
+ ) as Record<string, unknown>;
103
+ version = String(pkg['version']);
104
+ }
105
+ const prBody = pr !== null ? _pr_body(pr) : fs.readFileSync(bodyFile!, 'utf-8');
106
+ const changelogText = fs.readFileSync(changelogPath, 'utf-8');
107
+ const result = check_surface_equality({ prBody, changelogText, version });
108
+ if (result.ok) {
109
+ process.stdout.write(`✅ release surfaces equal for ${version}\n`);
110
+ return 0;
111
+ }
112
+ process.stderr.write(
113
+ `❌ release-surface divergence for ${version}: ${result.reason}\n` +
114
+ ' The PR body must equal the CHANGELOG entry (whitespace-normalized). ' +
115
+ 'Regenerate it: task release -- --resume --yes (or gh pr edit with the section body).\n',
116
+ );
117
+ return 1;
118
+ }
119
+
120
+ const _isMain = (() => {
121
+ const entry = process.argv[1];
122
+ if (!entry) return false;
123
+ try {
124
+ return fs.realpathSync(entry) === fs.realpathSync(_HERE);
125
+ } catch {
126
+ return false;
127
+ }
128
+ })();
129
+
130
+ if (_isMain) {
131
+ process.exit(main(process.argv.slice(2)));
132
+ }
@@ -2,17 +2,42 @@
2
2
  /**
3
3
  * check_reply_consistency.ts — enforce user-interaction.md Iron Laws.
4
4
  *
5
- * Ported from the retired Python `src/scripts/check_reply_consistency.py` (ADR-200,
6
- * Phase 4 / Wave 4c). CLI contract mirrored EXACTLY — `--stdin` /
7
- * `--file` / `--scan-dir` mutually-exclusive required group, `--strict`,
8
- * `-v`/`--verbose`, `--quiet`; exit codes (0 ok · 2 inline tag · 3 multi-rec ·
9
- * 4 rec-not-in-options · 5 options-without-rec strict · 6 scan-dir found ·
10
- * 9 usage error); byte-identical messages; stdout/stderr split.
5
+ * Full rewrite (council 2026-08, "fix/extraction-threshold-consistency"):
6
+ * the previous version was a 1:1 port of a retired Python prototype whose
7
+ * lax-by-default, whole-reply semantics contradicted the spec it cites
8
+ * (`src/rules/user-interaction.md` Iron Law 1 +
9
+ * `src/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md`
10
+ * § Pre-send self-check).
11
11
  *
12
- * Single-Source Recommendation Line: a reply with numbered options must
13
- * have ONE bolded `Recommendation: N` / `Empfehlung: N` line, no inline
14
- * `(recommended)` / `(rec)` / `(empfohlen)` tag next to options, and the
15
- * recommended number must appear in the option block.
12
+ * Semantics (strict is the DEFAULT — the documented invocation
13
+ * `./scripts-run src/scripts/check_reply_consistency --stdin < draft.md`
14
+ * IS the strict behavior; there is no --strict/--lax flag):
15
+ *
16
+ * 1. Per-block validation. EVERY numbered-options block (>= 2 consecutive
17
+ * numbered lines, optionally blockquoted) must carry exactly ONE
18
+ * `Recommendation:`/`Empfehlung:` line DIRECTLY under it — within the
19
+ * trailing 2 non-blank lines after the block, before any new heading or
20
+ * new options block.
21
+ * 2. Multi-block replies: each block gets its own recommendation line. Two
22
+ * blocks with two (different) recommendation numbers is VALID (mechanics
23
+ * § self-check rule 5); a block without one is a finding.
24
+ * 3. The recommendation number must be within its block's option range.
25
+ * 4. No inline `(recommended)` / `(rec)` / `(empfohlen)` tag on an option
26
+ * line (Iron Law 1 — the option block stays neutral).
27
+ *
28
+ * Known limit — wrong-language label: Iron Law 1 says a wrong-language label
29
+ * (`Recommendation:` for a German user, or vice versa) counts as NO
30
+ * recommendation, but this script cannot know the user's language from the
31
+ * draft alone. Both `Recommendation:` and `Empfehlung:` are therefore
32
+ * accepted in either context; the language match stays a model-side check.
33
+ *
34
+ * `--scan-dir` keeps the legacy-pattern sweep over `.md` trees (inline
35
+ * `(recommended)` tags only — docs legitimately contain numbered lists
36
+ * without recommendation lines, so per-block validation does not apply
37
+ * there).
38
+ *
39
+ * Exit: 0 clean · 1 usage/IO error · 2 findings (consistent with sibling
40
+ * lints, e.g. lint_abstraction_thresholds).
16
41
  */
17
42
 
18
43
  import * as fs from 'node:fs';
@@ -26,17 +51,36 @@ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
26
51
 
27
52
  const QUIET = process.argv.slice(2).includes('--quiet');
28
53
 
29
- const OPTION_LINE_RE = /^\s*>?\s*(\d+)\.\s+\S/;
30
- // Python re.IGNORECASE on (?:Recommendation|Empfehlung)\s*:\s*(\d+)\b
31
- const REC_LINE_RE = /(?:Recommendation|Empfehlung)\s*:\s*(\d+)\b/gi;
32
- const TAG_RE = /\((?:recommended|rec|empfohlen)\)/i;
54
+ export const OPTION_LINE_RE = /^\s*>?\s*(\d+)\.\s+\S/;
55
+ export const REC_LINE_RE = /(?:Recommendation|Empfehlung)\s*:\s*(\d+)\b/i;
56
+ export const TAG_RE = /\((?:recommended|rec|empfohlen)\)/i;
33
57
  const CODESPAN_RE = /`[^`\n]*`/g;
58
+ const HEADING_RE = /^#{1,6}\s/;
59
+ const FENCE_RE = /^\s*(```|~~~)/;
60
+
61
+ /** How many non-blank lines after a block may separate it from its recommendation line. */
62
+ export const REC_ADJACENCY_WINDOW = 2;
63
+
64
+ export interface OptionBlock {
65
+ /** 1-based line of the first option line. */
66
+ startLine: number;
67
+ /** 1-based line of the last option line. */
68
+ endLine: number;
69
+ /** Option numbers in order of appearance. */
70
+ numbers: number[];
71
+ }
72
+
73
+ export interface Finding {
74
+ /** 1-based line the finding anchors to. */
75
+ line: number;
76
+ message: string;
77
+ }
34
78
 
35
79
  function _strip_codespans(line: string): string {
36
80
  return line.replace(CODESPAN_RE, '``');
37
81
  }
38
82
 
39
- /** Mirror Python `str.splitlines()` — splits on \n / \r\n / \r, drops trailing. */
83
+ /** Split on \n / \r\n / \r, dropping a single trailing empty element. */
40
84
  function _splitlines(text: string): string[] {
41
85
  if (text === '') return [];
42
86
  const parts = text.split(/\r\n|\r|\n/);
@@ -47,87 +91,122 @@ function _splitlines(text: string): string[] {
47
91
  }
48
92
 
49
93
  /**
50
- * Return [line_no, raw_line] of the first numbered-option line carrying an
51
- * inline (recommended)-class tag outside code spans, or null.
94
+ * Lines with fenced code blocks blanked out — an example options block quoted
95
+ * inside ``` / ~~~ is illustration, not a live options block.
52
96
  */
53
- function find_inline_tag(text: string): [number, string] | null {
54
- const lines = _splitlines(text);
55
- for (let idx = 0; idx < lines.length; idx++) {
56
- const raw = lines[idx]!;
57
- const stripped = _strip_codespans(raw);
58
- if (!OPTION_LINE_RE.test(stripped)) {
97
+ export function mask_fences(lines: readonly string[]): string[] {
98
+ const out: string[] = [];
99
+ let fence: string | null = null;
100
+ for (const raw of lines) {
101
+ const m = FENCE_RE.exec(raw);
102
+ if (m) {
103
+ const marker = m[1]!;
104
+ if (fence === null) {
105
+ fence = marker;
106
+ } else if (fence === marker) {
107
+ fence = null;
108
+ }
109
+ out.push('');
59
110
  continue;
60
111
  }
61
- if (TAG_RE.test(stripped)) {
62
- return [idx + 1, raw.trim()];
63
- }
112
+ out.push(fence === null ? raw : '');
64
113
  }
65
- return null;
114
+ return out;
66
115
  }
67
116
 
68
- /**
69
- * Group consecutive numbered-option lines into blocks; return list of blocks,
70
- * each a list of the numbers found in that block.
71
- */
72
- function find_option_blocks(text: string): number[][] {
73
- const blocks: number[][] = [];
74
- let current: number[] = [];
75
- for (const raw of _splitlines(text)) {
76
- const m = OPTION_LINE_RE.exec(raw);
117
+ /** Consecutive numbered-option lines (>= 2) grouped into blocks. */
118
+ export function find_option_blocks(text: string): OptionBlock[] {
119
+ const lines = mask_fences(_splitlines(text));
120
+ const blocks: OptionBlock[] = [];
121
+ let current: OptionBlock | null = null;
122
+ for (let idx = 0; idx < lines.length; idx++) {
123
+ const m = OPTION_LINE_RE.exec(lines[idx]!);
77
124
  if (m) {
78
- current.push(parseInt(m[1]!, 10));
125
+ const num = parseInt(m[1]!, 10);
126
+ if (current === null) {
127
+ current = { startLine: idx + 1, endLine: idx + 1, numbers: [num] };
128
+ } else {
129
+ current.endLine = idx + 1;
130
+ current.numbers.push(num);
131
+ }
79
132
  } else {
80
- if (current.length >= 2) {
133
+ if (current !== null && current.numbers.length >= 2) {
81
134
  blocks.push(current);
82
135
  }
83
- current = [];
136
+ current = null;
84
137
  }
85
138
  }
86
- if (current.length >= 2) {
139
+ if (current !== null && current.numbers.length >= 2) {
87
140
  blocks.push(current);
88
141
  }
89
142
  return blocks;
90
143
  }
91
144
 
92
- /** Run rules. Returns [exit_code, human_message]. */
93
- function validate(text: string, strict = false): [number, string] {
94
- const tag = find_inline_tag(text);
95
- if (tag) {
96
- const [line_no, snippet] = tag;
97
- return [2, `line ${line_no}: inline tag on numbered option — ${_pyRepr(snippet)}`];
98
- }
99
-
100
- const blocks = find_option_blocks(text);
101
- const rec_numbers: number[] = [];
102
- REC_LINE_RE.lastIndex = 0;
103
- let m: RegExpExecArray | null;
104
- while ((m = REC_LINE_RE.exec(text)) !== null) {
105
- rec_numbers.push(parseInt(m[1]!, 10));
106
- }
107
-
108
- if (blocks.length === 0) {
109
- return [0, 'ok (no numbered options block)'];
110
- }
145
+ /**
146
+ * Core check — the exported function tests drive. Returns all findings for
147
+ * one reply draft; empty array = the draft is consistent.
148
+ */
149
+ export function check_reply(text: string): Finding[] {
150
+ const rawLines = _splitlines(text);
151
+ const lines = mask_fences(rawLines);
152
+ const findings: Finding[] = [];
111
153
 
112
- if (rec_numbers.length === 0) {
113
- if (strict) {
114
- return [5, 'numbered options without Recommendation:/Empfehlung: line'];
154
+ // Iron Law 1 — no inline tag on an option line (codespans exempt).
155
+ for (let idx = 0; idx < lines.length; idx++) {
156
+ const stripped = _strip_codespans(lines[idx]!);
157
+ if (OPTION_LINE_RE.test(stripped) && TAG_RE.test(stripped)) {
158
+ findings.push({
159
+ line: idx + 1,
160
+ message: `inline tag on numbered option — ${rawLines[idx]!.trim()}`,
161
+ });
115
162
  }
116
- return [0, 'ok (options without recommendation; non-strict)'];
117
163
  }
118
164
 
119
- const distinct = [...new Set(rec_numbers)].sort((a, b) => a - b);
120
- if (distinct.length > 1) {
121
- return [3, `multiple distinct recommendation numbers: [${distinct.join(', ')}]`];
122
- }
123
-
124
- const rec_num = distinct[0]!;
165
+ // Per-block: exactly one adjacent recommendation line, number in range.
166
+ const blocks = find_option_blocks(text);
125
167
  for (const block of blocks) {
126
- if (block.includes(rec_num)) {
127
- return [0, `ok (recommendation ${rec_num} matches option block)`];
168
+ const recs: Array<{ line: number; num: number }> = [];
169
+ let inspected = 0;
170
+ for (let idx = block.endLine; idx < lines.length && inspected < REC_ADJACENCY_WINDOW; idx++) {
171
+ const line = lines[idx]!;
172
+ if (line.trim() === '') {
173
+ continue; // blank lines do not consume the adjacency window
174
+ }
175
+ if (HEADING_RE.test(line) || OPTION_LINE_RE.test(line)) {
176
+ break; // a new heading or a new options block ends this block's window
177
+ }
178
+ inspected += 1;
179
+ const m = REC_LINE_RE.exec(line);
180
+ if (m) {
181
+ recs.push({ line: idx + 1, num: parseInt(m[1]!, 10) });
182
+ }
183
+ }
184
+ if (recs.length === 0) {
185
+ findings.push({
186
+ line: block.startLine,
187
+ message:
188
+ 'numbered options block without a Recommendation:/Empfehlung: line directly under it',
189
+ });
190
+ continue;
191
+ }
192
+ if (recs.length > 1) {
193
+ findings.push({
194
+ line: recs[1]!.line,
195
+ message: `multiple recommendation lines under one options block: [${recs.map((r) => r.num).join(', ')}]`,
196
+ });
197
+ continue;
198
+ }
199
+ const rec = recs[0]!;
200
+ if (!block.numbers.includes(rec.num)) {
201
+ findings.push({
202
+ line: rec.line,
203
+ message: `recommendation ${String(rec.num)} not present in the options block above it (options: [${block.numbers.join(', ')}])`,
204
+ });
128
205
  }
129
206
  }
130
- return [4, `recommendation ${rec_num} not present in any option block`];
207
+
208
+ findings.sort((a, b) => a.line - b.line);
209
+ return findings;
131
210
  }
132
211
 
133
212
  function _isDir(p: string): boolean {
@@ -169,7 +248,8 @@ function _relToPosixOrAbs(child: string, root: string): string {
169
248
  return rel.split(path.sep).join('/');
170
249
  }
171
250
 
172
- function cmd_scan_dir(root: string): number {
251
+ /** Legacy-pattern sweep: inline (recommended)-class tags in `.md` trees. */
252
+ export function cmd_scan_dir(root: string): number {
173
253
  // If the requested root is the legacy ".agent-src.uncondensed" and it no
174
254
  // longer exists (post-monorepo-move), fall back to artefact_roots().
175
255
  let roots: string[];
@@ -179,11 +259,11 @@ function cmd_scan_dir(root: string): number {
179
259
  roots = artefact_roots();
180
260
  if (roots.length === 0) {
181
261
  process.stderr.write('error: no artefact roots found (legacy or packages/*)\n');
182
- return 9;
262
+ return 1;
183
263
  }
184
264
  } else {
185
265
  process.stderr.write(`error: not a directory: ${root}\n`);
186
- return 9;
266
+ return 1;
187
267
  }
188
268
  } else {
189
269
  roots = [root];
@@ -204,10 +284,10 @@ function cmd_scan_dir(root: string): number {
204
284
  }
205
285
  if (violations.length > 0) {
206
286
  for (const [p, line, snippet] of violations) {
207
- process.stderr.write(` 🔴 ${p}:${line} — inline-tag — ${snippet}\n`);
287
+ process.stderr.write(` 🔴 ${p}:${String(line)} — inline-tag — ${snippet}\n`);
208
288
  }
209
- process.stderr.write(`\n❌ ${violations.length} legacy-pattern violation(s)\n`);
210
- return 6;
289
+ process.stderr.write(`\n❌ ${String(violations.length)} legacy-pattern violation(s)\n`);
290
+ return 2;
211
291
  }
212
292
  if (!QUIET) {
213
293
  const scanned = roots.map((r) => _relToPosixOrAbs(r, ROOT)).join(', ');
@@ -216,39 +296,21 @@ function cmd_scan_dir(root: string): number {
216
296
  return 0;
217
297
  }
218
298
 
219
- /** Python repr() of a single string. */
220
- function _pyRepr(s: string): string {
221
- const hasSingle = s.includes("'");
222
- const hasDouble = s.includes('"');
223
- const quote = hasSingle && !hasDouble ? '"' : "'";
224
- let out = quote;
225
- for (const ch of s) {
226
- if (ch === '\\') out += '\\\\';
227
- else if (ch === quote) out += '\\' + ch;
228
- else if (ch === '\n') out += '\\n';
229
- else if (ch === '\r') out += '\\r';
230
- else if (ch === '\t') out += '\\t';
231
- else out += ch;
232
- }
233
- return out + quote;
234
- }
235
-
236
299
  interface ParsedArgs {
237
300
  stdin: boolean;
238
301
  file: string | null;
239
302
  scan_dir: string | null;
240
- strict: boolean;
241
303
  verbose: boolean;
242
304
  }
243
305
 
306
+ const USAGE =
307
+ 'usage: check_reply_consistency [-h] (--stdin | --file FILE | --scan-dir SCAN_DIR)\n' +
308
+ ' [-v] [--quiet]\n';
309
+
244
310
  function _usage_error(message: string): never {
245
- // argparse prints usage to stderr then "<prog>: error: <message>" and exits 2.
246
- process.stderr.write(
247
- 'usage: check_reply_consistency [-h] (--stdin | --file FILE | --scan-dir SCAN_DIR)\n' +
248
- ' [--strict] [-v] [--quiet]\n',
249
- );
311
+ process.stderr.write(USAGE);
250
312
  process.stderr.write(`check_reply_consistency: error: ${message}\n`);
251
- process.exit(2);
313
+ process.exit(1);
252
314
  }
253
315
 
254
316
  function _readStdin(): string {
@@ -259,11 +321,10 @@ function _readStdin(): string {
259
321
  }
260
322
  }
261
323
 
262
- function parse_args(argv: readonly string[]): ParsedArgs {
324
+ export function parse_args(argv: readonly string[]): ParsedArgs {
263
325
  let stdin = false;
264
326
  let file: string | null = null;
265
327
  let scan_dir: string | null = null;
266
- let strict = false;
267
328
  let verbose = false;
268
329
  const groupSeen: string[] = [];
269
330
  const need = (i: number, name: string): string => {
@@ -290,17 +351,12 @@ function parse_args(argv: readonly string[]): ParsedArgs {
290
351
  } else if (arg.startsWith('--scan-dir=')) {
291
352
  scan_dir = arg.slice('--scan-dir='.length);
292
353
  groupSeen.push('--scan-dir');
293
- } else if (arg === '--strict') {
294
- strict = true;
295
354
  } else if (arg === '-v' || arg === '--verbose') {
296
355
  verbose = true;
297
356
  } else if (arg === '--quiet') {
298
357
  // handled via module-level QUIET; still a valid flag
299
358
  } else if (arg === '-h' || arg === '--help') {
300
- process.stdout.write(
301
- 'usage: check_reply_consistency [-h] (--stdin | --file FILE | --scan-dir SCAN_DIR)\n' +
302
- ' [--strict] [-v] [--quiet]\n',
303
- );
359
+ process.stdout.write(USAGE);
304
360
  process.exit(0);
305
361
  } else {
306
362
  _usage_error(`unrecognized arguments: ${arg}`);
@@ -310,32 +366,37 @@ function parse_args(argv: readonly string[]): ParsedArgs {
310
366
  _usage_error('one of the arguments --stdin --file --scan-dir is required');
311
367
  }
312
368
  if (groupSeen.length > 1) {
313
- const first = groupSeen[0]!;
314
- const second = groupSeen[1]!;
315
- _usage_error(`argument ${second}: not allowed with argument ${first}`);
369
+ _usage_error(`argument ${groupSeen[1]!}: not allowed with argument ${groupSeen[0]!}`);
316
370
  }
317
- return { stdin, file, scan_dir, strict, verbose };
371
+ return { stdin, file, scan_dir, verbose };
318
372
  }
319
373
 
320
- function main(argv: readonly string[] = process.argv.slice(2)): number {
374
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
321
375
  const args = parse_args(argv);
322
376
 
323
377
  if (args.scan_dir !== null) {
324
378
  return cmd_scan_dir(args.scan_dir);
325
379
  }
326
380
 
327
- const text = args.stdin ? _readStdin() : fs.readFileSync(args.file!, 'utf-8');
328
- const [code, msg] = validate(text, args.strict);
329
- if (code === 0) {
330
- if (args.verbose) {
331
- if (!QUIET) {
332
- process.stdout.write(`✅ ${msg}\n`);
333
- }
381
+ let text: string;
382
+ try {
383
+ text = args.stdin ? _readStdin() : fs.readFileSync(args.file!, 'utf-8');
384
+ } catch (e) {
385
+ process.stderr.write(`error: ${String(e)}\n`);
386
+ return 1;
387
+ }
388
+
389
+ const findings = check_reply(text);
390
+ if (findings.length === 0) {
391
+ if (args.verbose && !QUIET) {
392
+ process.stdout.write('✅ reply consistent (every options block carries one adjacent recommendation)\n');
334
393
  }
335
394
  return 0;
336
395
  }
337
- process.stderr.write(`❌ [exit ${code}] ${msg}\n`);
338
- return code;
396
+ for (const f of findings) {
397
+ process.stderr.write(`❌ line ${String(f.line)}: ${f.message}\n`);
398
+ }
399
+ return 2;
339
400
  }
340
401
 
341
402
  function _isCliEntry(): boolean {
@@ -349,8 +410,7 @@ function _isCliEntry(): boolean {
349
410
  // A symlinked invocation (e.g. via an installed `.augment/` projection,
350
411
  // or macOS /var → /private/var temp dirs) makes the raw URLs differ:
351
412
  // import.meta.url is the resolved real path while argv[1] keeps the
352
- // symlink path. Compare realpaths so the entry guard still fires
353
- // (without this the CLI silently no-ops when run through a symlink).
413
+ // symlink path. Compare realpaths so the entry guard still fires.
354
414
  try {
355
415
  const here = fs.realpathSync(fileURLToPath(import.meta.url));
356
416
  const argv = fs.realpathSync(path.resolve(process.argv[1]));
@@ -364,15 +424,4 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
364
424
  process.exit(main());
365
425
  }
366
426
 
367
- export {
368
- ROOT,
369
- OPTION_LINE_RE,
370
- REC_LINE_RE,
371
- TAG_RE,
372
- find_inline_tag,
373
- find_option_blocks,
374
- validate,
375
- cmd_scan_dir,
376
- parse_args,
377
- main,
378
- };
427
+ export { ROOT };
@@ -61,10 +61,17 @@ export { COMPILE_TIME_TOGGLES };
61
61
 
62
62
  // Maps legacy tier values to the router-canonical names. See
63
63
  // docs/contracts/rule-router.md § Backward compatibility.
64
+ // `2b` → tier-2 is a recorded decision (2026-08-04): the 21 rules tagged 2b
65
+ // are tier-2 deliberately — before this entry they reached tier-2 only via a
66
+ // silent fallthrough that also swallowed typos. `safety-floor` is deliberately
67
+ // NOT mapped: it is documentation-only on the always-typed trio (which
68
+ // short-circuits to kernel before this map is consulted); on any other rule it
69
+ // is a contract violation and must fail compilation.
64
70
  const LEGACY_TIER_MAP: Record<string, string> = {
65
71
  '1': 'tier-1',
66
72
  '2': 'tier-2',
67
73
  '2a': 'tier-2',
74
+ '2b': 'tier-2',
68
75
  '3': 'tier-1',
69
76
  'mechanical-already': 'tier-1',
70
77
  kernel: 'kernel',
@@ -93,11 +100,21 @@ function _parse_frontmatter(text: string): JsonObject {
93
100
  : {};
94
101
  }
95
102
 
96
- function _resolve_tier(rule_type: string, raw_tier: string): string {
103
+ export function _resolve_tier(rule_type: string, raw_tier: string, rule_id: string): string {
97
104
  if (rule_type === 'always') {
98
105
  return 'kernel';
99
106
  }
100
- return LEGACY_TIER_MAP[String(raw_tier)] ?? 'tier-2';
107
+ const mapped = LEGACY_TIER_MAP[String(raw_tier)];
108
+ if (mapped === undefined) {
109
+ // A typo'd (or unmapped) tier used to downgrade silently to tier-2 —
110
+ // a zero-injection failure nobody sees. Fail the compile instead.
111
+ throw new Error(
112
+ `compile_router: rule '${rule_id}' has unknown tier '${raw_tier}' — `
113
+ + `allowed values: ${Object.keys(LEGACY_TIER_MAP).sort().join(', ')} `
114
+ + `(or type: always, where tier is inert)`,
115
+ );
116
+ }
117
+ return mapped;
101
118
  }
102
119
 
103
120
  function _normalize_trigger(item: Json): JsonObject | null {
@@ -205,7 +222,7 @@ function _collect(): JsonObject {
205
222
  if (rule_type === 'manual') {
206
223
  continue;
207
224
  }
208
- const tier = _resolve_tier(rule_type, String(fm['tier'] ?? ''));
225
+ const tier = _resolve_tier(rule_type, String(fm['tier'] ?? ''), rule_id);
209
226
  if (!ALLOWED_TIERS.has(tier)) {
210
227
  continue;
211
228
  }