session-orchestrator 5.2.0 → 5.3.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 (295) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +5 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +5 -1
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +5 -1
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +5 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +5 -1
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +5 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +5 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +5 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +5 -1
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +5 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  83. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  84. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  85. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  86. package/.codex-plugin/skills/session/SKILL.md +1 -1
  87. package/.cursor/commands/eval.md +1 -1
  88. package/.cursor/commands/session.md +1 -1
  89. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  90. package/.cursor/rules/050-plan.mdc +1 -1
  91. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  92. package/.cursor/skills/eval/SKILL.md +1 -1
  93. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  94. package/.cursor-plugin/plugin.json +1 -1
  95. package/.orchestrator/policy/blocked-commands.json +12 -3
  96. package/AGENTS.md +3 -2
  97. package/CHANGELOG.md +136 -0
  98. package/README.md +9 -9
  99. package/SECURITY.md +12 -0
  100. package/agents/dialectic-deriver.md +13 -10
  101. package/agents/eval-judge.md +67 -45
  102. package/agents/skill-applied-judge.md +34 -19
  103. package/commands/session.md +7 -3
  104. package/docs/baseline.md +12 -6
  105. package/docs/codex-setup.md +14 -2
  106. package/docs/components.md +7 -5
  107. package/docs/events-schema.md +56 -9
  108. package/docs/rule-authoring.md +58 -6
  109. package/docs/session-config-reference.md +100 -7
  110. package/docs/session-config-template.md +31 -2
  111. package/docs/telemetry.md +2 -0
  112. package/hooks/_lib/hook-import-set.json +85 -8
  113. package/hooks/_lib/subagent-transcript.mjs +582 -31
  114. package/hooks/config-protection.mjs +11 -3
  115. package/hooks/cwd-change-restore.mjs +11 -3
  116. package/hooks/enforce-commands.mjs +70 -23
  117. package/hooks/enforce-scope.mjs +143 -33
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/loop-guard.mjs +11 -3
  121. package/hooks/on-session-end.mjs +58 -23
  122. package/hooks/on-session-start.mjs +48 -11
  123. package/hooks/on-stop.mjs +168 -22
  124. package/hooks/operator-steer.mjs +11 -3
  125. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  126. package/hooks/post-bash-write-verify.mjs +3 -2
  127. package/hooks/post-edit-import-probe.mjs +17 -9
  128. package/hooks/post-edit-validate.mjs +13 -5
  129. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  130. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  131. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  132. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  133. package/hooks/pre-auq-clarity.mjs +15 -2
  134. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  135. package/hooks/pre-bash-issue-budget.mjs +16 -11
  136. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  138. package/hooks/pre-bash-staging-fence.mjs +335 -31
  139. package/hooks/pre-bash-templates-first.mjs +19 -14
  140. package/hooks/pre-task-scope-disjoint.mjs +233 -2
  141. package/hooks/subagent-telemetry.mjs +15 -19
  142. package/hooks/wave-scope-commit-guard.mjs +197 -100
  143. package/monitors/monitors.json +1 -1
  144. package/output-styles/wave-summary.md +1 -1
  145. package/package.json +1 -1
  146. package/pi/prompts/eval.md +1 -1
  147. package/pi/prompts/session.md +1 -1
  148. package/rules/README.md +1 -1
  149. package/rules/opt-in-domain/prompt-caching.md +1 -1
  150. package/rules/opt-in-stack/backend-data.md +1 -1
  151. package/rules/opt-in-stack/backend.md +3 -3
  152. package/rules/opt-in-stack/frontend.md +1 -1
  153. package/rules/opt-in-stack/security-web.md +3 -3
  154. package/rules/opt-in-stack/swift.md +1 -1
  155. package/scripts/autopilot.mjs +23 -2
  156. package/scripts/backfill-abandoned-sessions.mjs +117 -15
  157. package/scripts/check-sessions-integrity.mjs +300 -0
  158. package/scripts/dialectic-deriver.mjs +50 -13
  159. package/scripts/emit-session.mjs +75 -29
  160. package/scripts/eval-session.mjs +65 -3
  161. package/scripts/generate-agents-skills.mjs +102 -29
  162. package/scripts/generate-cursor-adapter.mjs +61 -16
  163. package/scripts/lib/agent-status.mjs +2 -31
  164. package/scripts/lib/auq/clarity.mjs +10 -2
  165. package/scripts/lib/auq/parse.mjs +12 -31
  166. package/scripts/lib/auq/schema.mjs +56 -41
  167. package/scripts/lib/auto-dialectic.mjs +304 -15
  168. package/scripts/lib/autopilot/flags.mjs +12 -1
  169. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  170. package/scripts/lib/autopilot/loop.mjs +14 -1
  171. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  172. package/scripts/lib/ci-status-banner.mjs +376 -16
  173. package/scripts/lib/command-blocker.mjs +275 -28
  174. package/scripts/lib/config/dialectic.mjs +12 -3
  175. package/scripts/lib/config/gate.mjs +74 -0
  176. package/scripts/lib/config/reaper.mjs +162 -0
  177. package/scripts/lib/config.mjs +14 -0
  178. package/scripts/lib/convergence-monitor.mjs +74 -11
  179. package/scripts/lib/ecosystem-health.mjs +11 -0
  180. package/scripts/lib/eval/engine.mjs +421 -53
  181. package/scripts/lib/eval/judge.mjs +463 -40
  182. package/scripts/lib/eval/schema.mjs +10 -1
  183. package/scripts/lib/events-rotation.mjs +221 -25
  184. package/scripts/lib/events-schema.mjs +114 -0
  185. package/scripts/lib/events.mjs +524 -5
  186. package/scripts/lib/frontmatter-guard.mjs +21 -10
  187. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  188. package/scripts/lib/gates/gate-full.mjs +28 -3
  189. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  190. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  191. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  192. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  193. package/scripts/lib/instruction-budget-guard.mjs +146 -4
  194. package/scripts/lib/io.mjs +42 -8
  195. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  196. package/scripts/lib/js-mask.mjs +197 -0
  197. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  198. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  199. package/scripts/lib/orphan-reaper.mjs +1588 -0
  200. package/scripts/lib/peer-cards/merger.mjs +48 -10
  201. package/scripts/lib/peer-cards/reader.mjs +78 -2
  202. package/scripts/lib/process-group.mjs +899 -0
  203. package/scripts/lib/quality-gate.mjs +107 -28
  204. package/scripts/lib/reconcile/backlog.mjs +368 -0
  205. package/scripts/lib/reconcile/engine.mjs +55 -188
  206. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  207. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  208. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  209. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  210. package/scripts/lib/rule-loader.mjs +41 -12
  211. package/scripts/lib/scope-echo.mjs +39 -2
  212. package/scripts/lib/scope-gate.mjs +605 -1
  213. package/scripts/lib/session-close-backfill.mjs +33 -6
  214. package/scripts/lib/session-id.mjs +9 -20
  215. package/scripts/lib/session-invocation.mjs +20 -0
  216. package/scripts/lib/session-schema/constants.mjs +30 -2
  217. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  218. package/scripts/lib/session-schema.mjs +8 -3
  219. package/scripts/lib/session-start-probes.mjs +95 -10
  220. package/scripts/lib/sessions-canonical.mjs +23 -0
  221. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  222. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  223. package/scripts/lib/skill-evidence-window.mjs +891 -0
  224. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  225. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  226. package/scripts/lib/skill-judge.mjs +45 -3
  227. package/scripts/lib/tail-window.mjs +56 -0
  228. package/scripts/lib/telemetry/schema.mjs +30 -0
  229. package/scripts/lib/telemetry/sync.mjs +61 -6
  230. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  231. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  232. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  233. package/scripts/lib/user-invocable-skills.mjs +23 -3
  234. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  235. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  236. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  237. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  238. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  239. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  240. package/scripts/lib/validate/check-rules.mjs +7 -5
  241. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  242. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  243. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  244. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  245. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  246. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  247. package/scripts/lib/validate/confidential-names.mjs +10 -0
  248. package/scripts/lib/validate-vendored-rules.mjs +4 -3
  249. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  250. package/scripts/lib/vault-mirror/process.mjs +10 -3
  251. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  252. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  253. package/scripts/lib/vault-yaml.mjs +118 -0
  254. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  255. package/scripts/release-session-lock.mjs +305 -0
  256. package/scripts/release.mjs +30 -5
  257. package/scripts/resolve-session-invocation.mjs +59 -0
  258. package/scripts/run-quality-gate.mjs +156 -17
  259. package/scripts/sweep-expired-rules.mjs +14 -3
  260. package/scripts/validate-plugin.mjs +12 -0
  261. package/scripts/validate-wave-scope.mjs +32 -105
  262. package/scripts/vault-mirror.mjs +9 -1
  263. package/skills/_shared/platform-tools.md +23 -11
  264. package/skills/autopilot/SKILL.md +22 -7
  265. package/skills/claude-md-drift-check/SKILL.md +1 -1
  266. package/skills/convergence-monitoring/README.md +8 -1
  267. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  268. package/skills/convergence-monitoring/SKILL.md +15 -6
  269. package/skills/eval/SKILL.md +39 -24
  270. package/skills/eval/rubric-v1.md +1 -0
  271. package/skills/eval/rubric-v2.md +457 -0
  272. package/skills/evolve/SKILL.md +1 -1
  273. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  274. package/skills/gitlab-ops/SKILL.md +3 -2
  275. package/skills/npm-publish/SKILL.md +1 -1
  276. package/skills/reconcile/SKILL.md +11 -0
  277. package/skills/session-end/SKILL.md +13 -16
  278. package/skills/session-end/discovery-scan.md +1 -1
  279. package/skills/session-end/phase-3-6-tail.md +55 -9
  280. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  281. package/skills/session-end/session-metrics-write.md +10 -0
  282. package/skills/session-plan/SKILL.md +17 -5
  283. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  284. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
  285. package/skills/ux-grill/SKILL.md +1 -1
  286. package/skills/wave-executor/SKILL.md +8 -4
  287. package/skills/wave-executor/circuit-breaker.md +2 -0
  288. package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
  289. package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
  290. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
  291. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
  292. package/.cursor/commands/convergence-monitoring.md +0 -13
  293. package/.cursor/commands/npm-publish.md +0 -13
  294. package/pi/prompts/convergence-monitoring.md +0 -11
  295. package/pi/prompts/npm-publish.md +0 -11
@@ -8,7 +8,8 @@
8
8
  * Follows .claude/rules/cli-design.md:
9
9
  * - `--json` for machine output; human-readable by default.
10
10
  * - Data → stdout, diagnostics → stderr.
11
- * - Exit codes: 0 success/match · 1 user-error/drift · 2 system error.
11
+ * - Exit codes: 0 success/match · 1 user-error/drift · 2 system error ·
12
+ * 3 verify version-mismatch (non-standard, documented in --help).
12
13
  *
13
14
  * ## Append failures are NOT exit 0 (GitLab #969)
14
15
  *
@@ -40,6 +41,17 @@
40
41
  * Session-end Phase 3.7d remains advisory and MUST NOT gate on this exit code —
41
42
  * it catches a non-zero exit and logs a WARN (skills/session-end/SKILL.md).
42
43
  *
44
+ * ## A rubric change is not scoring drift (GitLab #1400)
45
+ *
46
+ * `--verify` re-scores a stored record with the CURRENT engine. When the stored
47
+ * `rubric_version` differs from `RUBRIC_VERSION`, the two sides answer different
48
+ * pre-registered check sets, so neither MATCH nor DRIFT is true: the run exits
49
+ * `3` with `verdict: "version-mismatch"` and re-scores nothing. Exit `0` would
50
+ * sell a cross-rubric comparison as a reproducibility proof; exit `1` would put
51
+ * a planned rubric bump in the same bucket as a forged record. Measured
52
+ * 2026-09-19: all 40 records in this repo's journal are `rubric-v1` and every
53
+ * one of them exited 1 as DRIFT under the rubric-v2 engine.
54
+ *
43
55
  * Usage:
44
56
  * eval-session.mjs [--session <id>] [--json] [--no-write]
45
57
  * [--metrics-dir <path>] [--rubric <path>]
@@ -68,6 +80,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
68
80
  const EXIT_OK = 0;
69
81
  const EXIT_USER = 1;
70
82
  const EXIT_SYSTEM = 2;
83
+ /** Non-standard, `--verify` only: the stored record predates the current rubric (#1400). */
84
+ const EXIT_VERSION_MISMATCH = 3;
71
85
 
72
86
  function readPkgVersion() {
73
87
  try {
@@ -103,6 +117,9 @@ EXIT CODES
103
117
  1 user error (session not found, unknown run-id, record failed validation)
104
118
  / verify drift
105
119
  2 system error (could not append the record to the eval journal)
120
+ 3 verify version-mismatch: the stored record was scored under a different
121
+ rubric than ${RUBRIC_VERSION}, so it is not re-verifiable with this engine
122
+ (non-standard code — neither a match nor drift)
106
123
  `;
107
124
 
108
125
  function fail(exitCode, message) {
@@ -211,6 +228,49 @@ function runVerify(runId, values) {
211
228
  fail(EXIT_USER, `--verify: run-id not found in eval journal: ${runId}`);
212
229
  }
213
230
 
231
+ // #1400 — check the rubric BEFORE re-scoring. A record scored under another
232
+ // rubric answers a different pre-registered check set, so re-running the
233
+ // current engine over it produces a comparison nobody asked for. A record
234
+ // with no `rubric_version` at all is unattributable and counts as a mismatch.
235
+ const storedRubric = typeof stored.rubric_version === 'string' && stored.rubric_version
236
+ ? stored.rubric_version
237
+ : null;
238
+ if (storedRubric !== RUBRIC_VERSION) {
239
+ // Deliberately NO diff of the dimensions the two rubrics share. Such a diff
240
+ // is technically possible but would carry almost no signal here. Measured
241
+ // 2026-09-19 over this repo's 40-record journal, replaying every record
242
+ // through `evaluateSession` + `diffDimensions`: all 40 differ on
243
+ // `process-safety` (its formula IS the v2 change) and all 40 gain a
244
+ // `present-in-fresh-only` `guard-friction`; restricted to the
245
+ // formula-unchanged dimensions, only 4 of 40 are byte-identical and the
246
+ // other 36 differ on `verification-evidence` / `gate-health` because the
247
+ // events they were scored from predate this journal's `events.jsonl`. So a
248
+ // "common dimensions" diff would re-report 90% of the journal as drift under
249
+ // a new name — the same misdirection in another shape (HR-101: a class that
250
+ // fires on ~90% of samples is a broken instrument, not a signal).
251
+ // Ceiling: holds while a rubric bump coincides with truncated evidence.
252
+ // Revisit-Trigger: the next rubric bump where every stored record's source
253
+ // events are still present — then a common-dimension diff carries signal.
254
+ if (values.json) {
255
+ writeStdoutLineSync(
256
+ JSON.stringify({
257
+ run_id: runId,
258
+ match: false,
259
+ verdict: 'version-mismatch',
260
+ stored_rubric_version: storedRubric,
261
+ engine_rubric_version: RUBRIC_VERSION,
262
+ }),
263
+ );
264
+ } else {
265
+ writeStdoutLineSync(
266
+ `VERSION-MISMATCH: ${runId} was scored under ${storedRubric ?? '(no rubric_version)'}; ` +
267
+ `this engine scores ${RUBRIC_VERSION} — not re-verifiable with this engine ` +
268
+ '(not drift, not a match).',
269
+ );
270
+ }
271
+ process.exit(EXIT_VERSION_MISMATCH);
272
+ }
273
+
214
274
  let fresh;
215
275
  try {
216
276
  fresh = evaluateSession({
@@ -236,7 +296,9 @@ function runVerify(runId, values) {
236
296
 
237
297
  if (diffs.length === 0) {
238
298
  if (values.json) {
239
- writeStdoutLineSync(JSON.stringify({ run_id: runId, match: true, dimensions: fresh.dimensions.length }));
299
+ writeStdoutLineSync(
300
+ JSON.stringify({ run_id: runId, match: true, verdict: 'match', dimensions: fresh.dimensions.length }),
301
+ );
240
302
  } else {
241
303
  writeStdoutLineSync(`MATCH: ${runId} re-evaluates identically across ${fresh.dimensions.length} dimension(s).`);
242
304
  }
@@ -244,7 +306,7 @@ function runVerify(runId, values) {
244
306
  }
245
307
 
246
308
  if (values.json) {
247
- writeStdoutLineSync(JSON.stringify({ run_id: runId, match: false, diffs }));
309
+ writeStdoutLineSync(JSON.stringify({ run_id: runId, match: false, verdict: 'drift', diffs }));
248
310
  } else {
249
311
  const out = [`DRIFT: ${runId} re-evaluation differs from the stored record:`];
250
312
  for (const d of diffs) {
@@ -12,10 +12,11 @@
12
12
  * This generator writes two artefacts, both DERIVED and never hand-edited:
13
13
  *
14
14
  * 1. `AGENTS.md` — a BYTE-IDENTICAL copy of `CLAUDE.md`.
15
- * 2. `.agents/skills/<name>/SKILL.md` — a portable mirror of each
16
- * `skills/<name>/SKILL.md`, carrying ONLY agentskills.io-spec-legal
15
+ * 2. `.agents/skills/<name>/SKILL.md` — a portable mirror of each skill and
16
+ * command (commands own overlapping names), carrying ONLY spec-legal
17
17
  * frontmatter plus a pointer body (progressive disclosure — the mirror
18
- * never duplicates the canonical instructions).
18
+ * never duplicates the canonical instructions). Native Codex invocation
19
+ * policy travels in an adjacent `agents/openai.yaml` when applicable.
19
20
  *
20
21
  * ## Why a generated copy and NOT a symlink (measured, not preferred)
21
22
  *
@@ -53,6 +54,9 @@ import path from 'node:path';
53
54
  import { fileURLToPath } from 'node:url';
54
55
  import { execFileSync } from 'node:child_process';
55
56
  import yaml from 'js-yaml';
57
+ import { isUserInvocableValue } from './lib/user-invocable-skills.mjs';
58
+
59
+ const POLICY_MARKER = '# Generated by scripts/generate-agents-skills.mjs; do not edit.\n';
56
60
 
57
61
  /**
58
62
  * The complete set of SKILL.md frontmatter keys agentskills.io permits outside
@@ -159,29 +163,40 @@ export function toPortableFrontmatter(source, skillName) {
159
163
  *
160
164
  * @param {string} skillName
161
165
  * @param {Record<string, unknown>} portableFm
166
+ * @param {string} [canonical] plugin-relative source path; defaults to the skill
162
167
  * @returns {string}
163
168
  */
164
- export function renderMirror(skillName, portableFm) {
169
+ export function renderMirror(skillName, portableFm, canonical = `skills/${skillName}/SKILL.md`) {
165
170
  const front = yaml.dump(portableFm, { lineWidth: -1, noRefs: true, quotingType: '"' });
166
- const canonical = `skills/${skillName}/SKILL.md`;
167
- return [
171
+ const command = canonical.startsWith('commands/');
172
+ const lines = [
168
173
  '---',
169
174
  front.trimEnd(),
170
175
  '---',
171
176
  '',
172
177
  `# ${skillName}`,
173
178
  '',
174
- `> **Portable mirror — generated, do not edit.** The canonical skill body lives at`,
179
+ `> **Portable mirror — generated, do not edit.** The canonical workflow lives at`,
175
180
  `> [\`${canonical}\`](../../../${canonical}); read that file for the full instructions.`,
176
181
  `> This mirror carries only agentskills.io-spec-legal frontmatter so harnesses that`,
177
182
  `> discover skills under \`.agents/skills/\` can find and route to the skill.`,
178
183
  '>',
179
184
  '> Regenerate with `node scripts/generate-agents-skills.mjs`.',
180
185
  '',
181
- ].join('\n');
186
+ 'Read the linked document in full and follow its complete workflow, including prechecks and stop conditions. Resolve its link relative to this SKILL.md, not the project working directory. The plugin root is three directories above this file. Resolve package paths such as `skills/` and `scripts/` from that root and relative links in the canonical document from its own directory. Keep the user’s project as the target of project operations.',
187
+ ];
188
+ if (command || isUserInvocableValue(portableFm.metadata?.['user-invocable'])) lines.push(
189
+ '',
190
+ '`$ARGUMENTS` means the trailing user input after the selected command skill, or an empty string when absent. Preserve flags, quoted text, and Unicode as data. Do not perform global substitution in the command document, shell expansion on the argument string, or execution of that string as shell code. Pass command arguments through structured tool parameters or safely quoted individual arguments.',
191
+ );
192
+ if (command) lines.push(
193
+ '',
194
+ 'Read the full command before invoking an internal skill. An instruction to invoke a skill (including `session-orchestrator:<name>` or the `Skill` tool) means read and follow the canonical `skills/<name>/SKILL.md` beneath the plugin root. Do not redispatch the public command adapter: a same-named command and internal skill are distinct documents, and redispatch would recurse.',
195
+ );
196
+ return [...lines, ''].join('\n');
182
197
  }
183
198
 
184
- /** Parse a SKILL.md's YAML frontmatter block; returns `{}` when absent/unparseable. */
199
+ /** Parse a source's YAML frontmatter block; returns null when absent/unparseable. */
185
200
  function readFrontmatter(absPath) {
186
201
  const content = readFileSync(absPath, 'utf8');
187
202
  const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
@@ -205,6 +220,44 @@ export function listSourceSkills(pluginRoot) {
205
220
  .sort();
206
221
  }
207
222
 
223
+ /** Union of canonical skills and commands, preserving command precedence. */
224
+ function listSourceEntries(pluginRoot) {
225
+ const sources = new Map(listSourceSkills(pluginRoot).map((name) => [name, `skills/${name}/SKILL.md`]));
226
+ const commands = path.join(pluginRoot, 'commands');
227
+ if (existsSync(commands) && statSync(commands).isDirectory()) {
228
+ for (const entry of readdirSync(commands, { withFileTypes: true })) {
229
+ if (!entry.isFile() || !entry.name.endsWith('.md') || /^[._]/.test(entry.name)) continue;
230
+ sources.set(entry.name.slice(0, -3), `commands/${entry.name}`);
231
+ }
232
+ }
233
+ return sources;
234
+ }
235
+
236
+ /** Match Codex policy precedence: command, native skill override, public skill. */
237
+ function invocationPolicy(pluginRoot, name, canonical, fm) {
238
+ if (!canonical.startsWith('commands/')) {
239
+ const nativePath = path.join(pluginRoot, 'skills', name, 'agents', 'openai.yaml');
240
+ if (existsSync(nativePath)) {
241
+ const native = yaml.load(readFileSync(nativePath, 'utf8'), { schema: yaml.CORE_SCHEMA });
242
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
243
+ if (!isRecord(native) || (Object.hasOwn(native, 'policy') && !isRecord(native.policy))) {
244
+ throw new Error(`${nativePath}: native skill metadata and policy must be mappings`);
245
+ }
246
+ if (native.policy && Object.hasOwn(native.policy, 'allow_implicit_invocation')) {
247
+ if (typeof native.policy.allow_implicit_invocation !== 'boolean') {
248
+ throw new Error(`${nativePath}: policy.allow_implicit_invocation must be a boolean`);
249
+ }
250
+ return native.policy.allow_implicit_invocation;
251
+ }
252
+ }
253
+ if (!isUserInvocableValue(fm['user-invocable'], canonical)) return undefined;
254
+ }
255
+ if (Object.hasOwn(fm, 'disable-model-invocation') && typeof fm['disable-model-invocation'] !== 'boolean') {
256
+ throw new Error(`${canonical}: disable-model-invocation must be a boolean`);
257
+ }
258
+ return fm['disable-model-invocation'] !== true;
259
+ }
260
+
208
261
  /**
209
262
  * Is `AGENTS.md` already an alias of `CLAUDE.md` by construction?
210
263
  *
@@ -268,34 +321,53 @@ export function generateAgentsSurface({ pluginRoot, check = false }) {
268
321
 
269
322
  // --- Artefact 2: .agents/skills/<name>/SKILL.md ---------------------------
270
323
  const mirrorRoot = path.join(pluginRoot, '.agents', 'skills');
271
- const skills = listSourceSkills(pluginRoot);
324
+ const sources = listSourceEntries(pluginRoot);
325
+ const skills = [...sources.keys()].sort();
272
326
  let files = 0;
273
327
  let bytes = 0;
274
328
 
275
329
  for (const name of skills) {
276
- const src = path.join(pluginRoot, 'skills', name, 'SKILL.md');
330
+ const canonical = sources.get(name);
331
+ const src = path.join(pluginRoot, canonical);
277
332
  const fm = readFrontmatter(src);
278
333
  if (fm === null) {
279
- warnings.push(`${name}: SKILL.md has no parseable frontmatter — skipped`);
334
+ warnings.push(`${canonical}: has no parseable frontmatter — skipped`);
280
335
  continue;
281
336
  }
282
337
  const { fm: portable, warnings: w } = toPortableFrontmatter(fm, name);
283
338
  warnings.push(...w);
284
- const rendered = renderMirror(name, portable);
285
- const dest = path.join(mirrorRoot, name, 'SKILL.md');
286
- files += 1;
287
- bytes += Buffer.byteLength(rendered, 'utf8');
288
-
289
- const current = existsSync(dest) ? readFileSync(dest, 'utf8') : null;
290
- if (current === rendered) continue;
291
- if (check) {
292
- drift.push(current === null
293
- ? `.agents/skills/${name}/SKILL.md is missing`
294
- : `.agents/skills/${name}/SKILL.md is stale`);
295
- } else {
296
- mkdirSync(path.dirname(dest), { recursive: true });
297
- writeFileSync(dest, rendered, 'utf8');
298
- written.push(`.agents/skills/${name}/SKILL.md`);
339
+ const artifacts = new Map([['SKILL.md', renderMirror(name, portable, canonical)]]);
340
+ const policy = invocationPolicy(pluginRoot, name, canonical, fm);
341
+ const policyPath = path.join(mirrorRoot, name, 'agents', 'openai.yaml');
342
+ if (policy !== undefined) {
343
+ artifacts.set('agents/openai.yaml', POLICY_MARKER + yaml.dump({
344
+ interface: { display_name: name },
345
+ policy: { allow_implicit_invocation: policy },
346
+ }, { lineWidth: -1, noRefs: true }));
347
+ } else if (existsSync(policyPath) && readFileSync(policyPath, 'utf8').startsWith(POLICY_MARKER)) {
348
+ if (check) drift.push(`.agents/skills/${name}/agents/openai.yaml is obsolete`);
349
+ else {
350
+ rmSync(policyPath);
351
+ written.push(`removed .agents/skills/${name}/agents/openai.yaml`);
352
+ }
353
+ }
354
+ for (const [file, rendered] of artifacts) {
355
+ const relative = `.agents/skills/${name}/${file}`;
356
+ const dest = path.join(mirrorRoot, name, file);
357
+ files += 1;
358
+ bytes += Buffer.byteLength(rendered, 'utf8');
359
+ const current = existsSync(dest) ? readFileSync(dest, 'utf8') : null;
360
+ if (file === 'agents/openai.yaml' && current !== null && !current.startsWith(POLICY_MARKER)) {
361
+ drift.push(`${relative} conflicts with an unrelated native policy — retained`);
362
+ continue;
363
+ }
364
+ if (current === rendered) continue;
365
+ if (check) drift.push(`${relative} is ${current === null ? 'missing' : 'stale'}`);
366
+ else {
367
+ mkdirSync(path.dirname(dest), { recursive: true });
368
+ writeFileSync(dest, rendered, 'utf8');
369
+ written.push(relative);
370
+ }
299
371
  }
300
372
  }
301
373
 
@@ -306,7 +378,7 @@ export function generateAgentsSurface({ pluginRoot, check = false }) {
306
378
  for (const entry of readdirSync(mirrorRoot, { withFileTypes: true })) {
307
379
  if (!entry.isDirectory() || known.has(entry.name)) continue;
308
380
  if (check) {
309
- drift.push(`.agents/skills/${entry.name}/ is an orphan (no skills/${entry.name}/SKILL.md)`);
381
+ drift.push(`.agents/skills/${entry.name}/ is an orphan (no skill or command source)`);
310
382
  } else {
311
383
  rmSync(path.join(mirrorRoot, entry.name), { recursive: true, force: true });
312
384
  written.push(`removed .agents/skills/${entry.name}/`);
@@ -356,8 +428,9 @@ function main(argv) {
356
428
  }
357
429
  } else {
358
430
  for (const w of result.warnings) process.stderr.write(` WARN: agents-surface: ${w}\n`);
431
+ for (const d of result.drift) process.stderr.write(`✗ agents-surface: ${d}\n`);
359
432
  process.stdout.write(
360
- `agents-surface: ${result.written.length} file(s) written, ${result.files} skill mirror(s), ${result.bytes} bytes\n`,
433
+ `agents-surface: ${result.written.length} file(s) written, ${result.files} portable artefact(s), ${result.bytes} bytes\n`,
361
434
  );
362
435
  }
363
436
  return result.ok ? 0 : 1;
@@ -39,6 +39,9 @@ const CURSOR_SKILLS_DIR = path.join(ROOT, '.cursor', 'skills');
39
39
  const CHECK_ONLY = process.argv.includes('--check');
40
40
  const DESCRIPTION_MAX = 1024;
41
41
 
42
+ /** Memo for {@link skillFlags} — skill name → parsed frontmatter + both markers. */
43
+ const SKILL_FLAGS = new Map();
44
+
42
45
  function isDir(p) {
43
46
  try { return statSync(p).isDirectory(); } catch { return false; }
44
47
  }
@@ -201,11 +204,41 @@ Cursor has no Skill tool. When the command says to invoke a skill, Read \`skills
201
204
  * @returns {string[]} skill names, sorted
202
205
  */
203
206
  function userInvocableSkills() {
204
- return skillDirs().filter((name) => {
205
- const file = path.join(SKILLS_DIR, name, 'SKILL.md');
206
- const fields = parseFrontmatter(readFileSync(file, 'utf8'));
207
- return isUserInvocableValue(fields['user-invocable'], file);
208
- });
207
+ return skillDirs().filter((name) => skillFlags(name).userInvocable);
208
+ }
209
+
210
+ /**
211
+ * Both boolean markers of ONE skill, parsed and judged exactly once.
212
+ *
213
+ * Memoised because the predicate is not side-effect free: a truthy-lookalike
214
+ * value (`yes`, `on`, `1`) emits a stderr WARN, so reading the same key from
215
+ * three call sites emitted the same warning three times — measured 2026-09-18
216
+ * on a two-skill fixture: 3 WARN lines for 2 defects. One skill, one reading,
217
+ * one diagnostic.
218
+ *
219
+ * @param {string} skillName
220
+ * @returns {{file: string, fields: Record<string, string>, userInvocable: boolean,
221
+ * sourceDisablesModel: boolean, disablesModel: boolean}}
222
+ */
223
+ function skillFlags(skillName) {
224
+ const cached = SKILL_FLAGS.get(skillName);
225
+ if (cached) return cached;
226
+ const file = path.join(SKILLS_DIR, skillName, 'SKILL.md');
227
+ const fields = parseFrontmatter(readFileSync(file, 'utf8'));
228
+ const userInvocable = isUserInvocableValue(fields['user-invocable'], file, 'user-invocable');
229
+ const sourceDisablesModel = isUserInvocableValue(
230
+ fields['disable-model-invocation'], file, 'disable-model-invocation',
231
+ );
232
+ const flags = {
233
+ file,
234
+ fields,
235
+ userInvocable,
236
+ sourceDisablesModel,
237
+ // See {@link disablesModelInvocation} for the two OR-ed grounds.
238
+ disablesModel: sourceDisablesModel || !userInvocable,
239
+ };
240
+ SKILL_FLAGS.set(skillName, flags);
241
+ return flags;
209
242
  }
210
243
 
211
244
  /**
@@ -217,8 +250,7 @@ function userInvocableSkills() {
217
250
  * @returns {string}
218
251
  */
219
252
  function renderSkillCommand(skillName) {
220
- const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
221
- const fields = parseFrontmatter(readFileSync(skillPath, 'utf8'));
253
+ const { fields, sourceDisablesModel } = skillFlags(skillName);
222
254
  const description = clampDescription(fields.description || `Session Orchestrator skill: ${skillName}`);
223
255
  // Same GH#54 rule as renderCommand: `argument-hint` ALWAYS goes through
224
256
  // yamlQuote(), because its canonical authored form (`[mode] [--flag]`) is a
@@ -233,7 +265,7 @@ function renderSkillCommand(skillName) {
233
265
  '---',
234
266
  frontmatterLine('description', yamlQuote(description)),
235
267
  frontmatterLine('argument-hint', yamlQuote(fields['argument-hint'])),
236
- isUserInvocableValue(fields['disable-model-invocation'], skillPath) ? 'disable-model-invocation: true' : null,
268
+ sourceDisablesModel ? 'disable-model-invocation: true' : null,
237
269
  '---',
238
270
  ].filter(Boolean).join('\n');
239
271
 
@@ -269,6 +301,18 @@ Cursor has no Skill tool. When the skill says to invoke another skill, Read \`sk
269
301
  * (measured 2026-09-17) on no Cursor-side evidence, which is the one
270
302
  * direction a guard fix must never move.
271
303
  *
304
+ * NAMED CEILING on ground 2 (BV-004). Whether Cursor reads
305
+ * `disable-model-invocation` at all is UNVERIFIED as of 2026-09-18: the key is
306
+ * documented here for Claude Code and Codex only, and `rg
307
+ * "disable-model-invocation" docs .claude .cursor/rules` returns zero Cursor
308
+ * citations. Ground 2 therefore stamps 24 wrappers on the ASSUMPTION that
309
+ * Cursor honours it — kept because stamping a restriction a reader ignores
310
+ * costs nothing, while dropping it would loosen 24 wrappers if the reader does
311
+ * honour it. REVISIT TRIGGER: Cursor documents the key (or a measured Cursor
312
+ * run shows it ignored) — then decide ground 2 on that evidence instead.
313
+ * Pinned by `tests/scripts/generate-cursor-adapter.test.mjs` ("stamps
314
+ * disable-model-invocation on ground 2 alone").
315
+ *
272
316
  * Truth table — both readings normalised by `isUserInvocableValue`, so every
273
317
  * form a YAML parser reads as `true` (`True`, `"true"`, `true # note`) counts
274
318
  * as `true` on BOTH axes. Ground 2 made that load-bearing rather than cosmetic:
@@ -283,18 +327,19 @@ Cursor has no Skill tool. When the skill says to invoke another skill, Read \`sk
283
327
  * | false/absent| true | no |
284
328
  * | false/absent| false/absent | yes (2) |
285
329
  *
286
- * @param {Record<string, string>} fields source skill frontmatter
287
- * @param {string} [file] SKILL.md path, named in a demotion WARN
330
+ * Both readings come from {@link skillFlags} (`sourceDisablesModel ||
331
+ * !userInvocable`), which parses each SKILL.md once so a truthy-lookalike
332
+ * value warns once per key per skill instead of once per call site.
333
+ *
334
+ * @param {string} skillName
288
335
  * @returns {boolean}
289
336
  */
290
- function disablesModelInvocation(fields, file) {
291
- return isUserInvocableValue(fields['disable-model-invocation'], file)
292
- || !isUserInvocableValue(fields['user-invocable'], file);
337
+ function disablesModelInvocation(skillName) {
338
+ return skillFlags(skillName).disablesModel;
293
339
  }
294
340
 
295
341
  function renderSkill(skillName) {
296
- const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
297
- const fields = parseFrontmatter(readFileSync(skillPath, 'utf8'));
342
+ const { fields } = skillFlags(skillName);
298
343
  const description = clampDescription(fields.description || `Session Orchestrator skill: ${skillName}`);
299
344
  // Same rule as renderCommand: no frontmatter value is emitted raw. `name` is
300
345
  // a directory basename today, so it is plain-safe in practice — routing it
@@ -304,7 +349,7 @@ function renderSkill(skillName) {
304
349
  `name: ${yamlQuote(skillName)}`,
305
350
  `description: ${yamlQuote(description)}`,
306
351
  ];
307
- if (disablesModelInvocation(fields, skillPath)) {
352
+ if (disablesModelInvocation(skillName)) {
308
353
  lines.push('disable-model-invocation: true');
309
354
  }
310
355
  lines.push('---');
@@ -58,6 +58,7 @@ import { isDeepStrictEqual } from 'node:util';
58
58
  import { appendJsonl } from './common.mjs';
59
59
  import { writeJsonAtomicSync } from './io.mjs';
60
60
  import { tryAcquireFileLock, releaseFileLock } from './file-lock.mjs';
61
+ import { readTailWindow } from './tail-window.mjs';
61
62
 
62
63
  // ---------------------------------------------------------------------------
63
64
  // Constants
@@ -303,36 +304,6 @@ function newestTsMs(entries) {
303
304
  return newest;
304
305
  }
305
306
 
306
- /**
307
- * Read the last `maxBytes` of a file. Returns the raw text plus whether the
308
- * window cut into the file (i.e. the first line in `text` may be partial).
309
- * @param {string} file
310
- * @param {number} maxBytes
311
- * @returns {{ text: string, size: number, cut: boolean }}
312
- */
313
- function readTail(file, maxBytes) {
314
- const fd = fs.openSync(file, 'r');
315
- try {
316
- const size = fs.fstatSync(fd).size;
317
- const want = Math.min(size, maxBytes);
318
- const start = size - want;
319
- const buf = Buffer.allocUnsafe(want);
320
- let read = 0;
321
- while (read < want) {
322
- const n = fs.readSync(fd, buf, read, want - read, start + read);
323
- if (n <= 0) break;
324
- read += n;
325
- }
326
- return { text: buf.subarray(0, read).toString('utf8'), size, cut: start > 0 };
327
- } finally {
328
- try {
329
- fs.closeSync(fd);
330
- } catch {
331
- /* best-effort */
332
- }
333
- }
334
- }
335
-
336
307
  /**
337
308
  * Shared write path for both setters: append the JSONL record, then perform a
338
309
  * lock-serialised read-modify-write of the LWW map. No-throw.
@@ -481,7 +452,7 @@ export function rebuildCurrentFromLedger(opts = {}) {
481
452
 
482
453
  let tail;
483
454
  try {
484
- tail = readTail(jsonlFile, maxBytes);
455
+ tail = readTailWindow(jsonlFile, maxBytes);
485
456
  } catch (err) {
486
457
  const reason = err?.code === 'ENOENT' ? 'ledger-missing' : 'ledger-unreadable';
487
458
  return {
@@ -801,9 +801,17 @@ function checkK5(q, at) {
801
801
  severity: 'fail',
802
802
  target: 'header',
803
803
  ...at,
804
+ // Der Text sagte bis hierher "Das Tool zeigt nur 12 — den Rest sieht der
805
+ // Operator nie". Das ist widerlegt: die 12 steht allein in der Prosa des
806
+ // Schemas (kein `.max(12)`, kein kuerzender Render-Pfad in Bundle
807
+ // 2.1.268), und 125 laengere Kopfzeilen wurden angenommen und beantwortet
808
+ // (ask-via-tool.md AUQ-006). H1 blockt deshalb nicht (BLOCKING_HURDLES =
809
+ // ['H2']) — die Meldung ist ein Hinweis, keine Abweisung, und darf keine
810
+ // Kuerzung begruenden, die es nicht braucht.
804
811
  message:
805
- `Die Kopfzeile "${header}" hat ${literal} Zeichen. Das Tool zeigt nur ` +
806
- `${THRESHOLDS.K5.headerCharsFail} den Rest sieht der Operator nie.`,
812
+ `Die Kopfzeile "${header}" hat ${literal} Zeichen; Ziel sind ` +
813
+ `${THRESHOLDS.K5.headerCharsFail} Codepoints. Das Tool schneidet nicht ab — ` +
814
+ `Kuerzen ist optional.`,
807
815
  measured: literal,
808
816
  threshold: THRESHOLDS.K5.headerCharsFail,
809
817
  hurdle: HURDLES.H1.id,
@@ -83,6 +83,7 @@ import {
83
83
  isRecommendedOption,
84
84
  } from './schema.mjs';
85
85
  import { scanFenceBlocks } from '../validate/markdown-fences.mjs';
86
+ import { maskSource } from '../js-mask.mjs';
86
87
 
87
88
  // ---------------------------------------------------------------------------
88
89
  // Korpus-Abgrenzung
@@ -142,42 +143,22 @@ export function corpusKindOf(file) {
142
143
  * Anführungspaar, das jeder „nimm das letzte Literal der Zeile"-Ansatz als
143
144
  * Label extrahiert.
144
145
  *
146
+ * Seit #1388 delegiert das an den geteilten Lexer `maskSource`
147
+ * (`../js-mask.mjs`, Modus `keepLiterals`): gleiche Zusicherung (Länge und
148
+ * Zeilenumbrüche erhalten, Literal-Inhalte unangetastet), aber MIT
149
+ * Regex-Literal-Zweig. Die frühere handgeschriebene Fassung hatte keinen —
150
+ * gemessen 2026-09-18 verschluckte `const re = /\/*$/;` als vermeintlicher
151
+ * Block-Kommentar-Anfang alles bis zum nächsten `*` + `/`, sodass ein
152
+ * darunter stehendes `label: "X (Recommended)"` spurlos verschwand: ein
153
+ * STILLER FALSCH-NEGATIVER Befund in der Klarheitsmessung, längentreu und
154
+ * daher an keiner Zeilennummer erkennbar.
155
+ *
145
156
  * @param {string} text
146
157
  * @returns {string} gleiche Länge, Kommentarinhalt durch Leerzeichen ersetzt
147
158
  */
148
159
  export function stripComments(text) {
149
160
  if (typeof text !== 'string' || text === '') return '';
150
- const out = [...text];
151
- let i = 0;
152
- while (i < out.length) {
153
- const c = text[i];
154
- if (c === '"' || c === "'" || c === '`') {
155
- const lit = readStringLiteral(text, i);
156
- i = lit ? lit.end : i + 1;
157
- continue;
158
- }
159
- if (c === '/' && text[i + 1] === '/') {
160
- while (i < out.length && text[i] !== '\n') out[i++] = ' ';
161
- continue;
162
- }
163
- if (c === '/' && text[i + 1] === '*') {
164
- out[i] = ' ';
165
- out[i + 1] = ' ';
166
- i += 2;
167
- while (i < out.length && !(text[i] === '*' && text[i + 1] === '/')) {
168
- if (text[i] !== '\n') out[i] = ' ';
169
- i++;
170
- }
171
- if (i < out.length) {
172
- out[i] = ' ';
173
- out[i + 1] = ' ';
174
- i += 2;
175
- }
176
- continue;
177
- }
178
- i++;
179
- }
180
- return out.join('');
161
+ return maskSource(text, { keepLiterals: true });
181
162
  }
182
163
 
183
164
  /**