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
@@ -22,10 +22,17 @@
22
22
  * triageDecision(finding, candidates)
23
23
  * → {action: 'create'|'update'|'ignore', target?, reason: string, confidence: number}
24
24
  *
25
+ * buildIssueBody(finding, fp) → string
26
+ *
27
+ * neutralizeFingerprintSentinel(text) → string
28
+ * fingerprintSentinelLine(fp) → string
29
+ * The one shared sentinel writer + neutraliser, also used by
30
+ * ux-grill/reconcile.mjs (#1339 P6).
31
+ *
25
32
  * ReconcileError
26
33
  *
27
34
  * Security:
28
- * - #388 (SEC-IR-MED-1): sentinel injection hardening — sanitizeRecommendation()
35
+ * - #388 (SEC-IR-MED-1): sentinel injection hardening — neutralizeFingerprintSentinel()
29
36
  * strips **Fingerprint:** literals from free-text fields before they are
30
37
  * embedded in the body (before the authoritative sentinel line is appended).
31
38
  * - #389 (SEC-IR-LOW-1): maxBuffer set to 4 MB on every execFile call;
@@ -126,39 +133,64 @@ function validateFinding(finding) {
126
133
  // ---------------------------------------------------------------------------
127
134
 
128
135
  /**
129
- * Sanitize free-text recommendation fields before embedding in the issue body.
130
- * Replaces any literal `**Fingerprint:**` with `__Fingerprint__` to prevent
131
- * a crafted recommendation from spoofing the authoritative fingerprint sentinel
132
- * line that is appended by buildIssueBody().
136
+ * Neutralise every `**Fingerprint:**` literal in untrusted text by rewriting it
137
+ * to `__Fingerprint__`. The ONE copy of this neutralisation: this module's
138
+ * {@link buildIssueBody} and `ux-grill/reconcile.mjs` both route free text
139
+ * through it (#1339 P6), so a hardening added here reaches both producers.
133
140
  *
134
- * Context: glab parses issue bodies as Markdown. If `recommendation` contained
141
+ * Context: glab parses issue bodies as Markdown. If a free-text field contained
135
142
  * `**Fingerprint:** <attacker-hash>`, a grep for the fingerprint sentinel in
136
143
  * listExistingFindings() would match the spoofed value instead of the real one,
137
144
  * bypassing dedup. Replacing the literal prevents the attack without altering
138
- * the semantic meaning of the recommendation text.
145
+ * the semantic meaning of the text.
146
+ *
147
+ * @param {unknown} text - coerced with `String(text ?? '')`
148
+ * @returns {string}
149
+ */
150
+ export function neutralizeFingerprintSentinel(text) {
151
+ // gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
152
+ // are also neutralized. Sanitizer is intentionally broader than the case-sensitive
153
+ // extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
154
+ return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
155
+ }
156
+
157
+ /**
158
+ * The authoritative dedup sentinel line, in the exact shape
159
+ * {@link extractFingerprintFromBody} reads. The caller passes an
160
+ * already-trusted value; this function does not sanitise it.
161
+ *
162
+ * @param {string} fp - 16-char hex fingerprint
163
+ * @returns {string}
164
+ */
165
+ export function fingerprintSentinelLine(fp) {
166
+ return `**Fingerprint:** \`${fp}\``;
167
+ }
168
+
169
+ /**
170
+ * Sanitize free-text recommendation fields before embedding in the issue body
171
+ * (#388 SEC-IR-MED-1) — {@link neutralizeFingerprintSentinel} for a non-empty
172
+ * string, anything else returned unchanged (a `null` description is dropped by
173
+ * buildIssueBody's line filter rather than rendered as an empty line).
139
174
  *
140
175
  * @param {string|undefined|null} text
141
176
  * @returns {string|undefined|null}
142
177
  */
143
178
  function sanitizeRecommendation(text) {
144
179
  if (!text || typeof text !== 'string') return text;
145
- // gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
146
- // are also neutralized. Sanitizer is intentionally broader than the case-sensitive
147
- // extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
148
- return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
180
+ return neutralizeFingerprintSentinel(text);
149
181
  }
150
182
 
151
183
  /**
152
184
  * Render untrusted text as the content of ONE inline Markdown code span:
153
- * sentinel literals neutralised (sanitizeRecommendation), line breaks and NUL
154
- * folded to a space, backticks turned into `'` — so a page-controlled locator
155
- * can neither close its code span nor start a new body line.
185
+ * sentinel literals neutralised, line breaks and NUL folded to a space,
186
+ * backticks turned into `'` — so a page-controlled locator can neither close
187
+ * its code span nor start a new body line.
156
188
  *
157
189
  * @param {unknown} text
158
190
  * @returns {string}
159
191
  */
160
192
  function codeSpanContent(text) {
161
- return sanitizeRecommendation(String(text ?? ''))
193
+ return neutralizeFingerprintSentinel(text)
162
194
  .replace(/[\r\n\0]+/g, ' ')
163
195
  .replace(/`/g, "'");
164
196
  }
@@ -218,7 +250,7 @@ export function buildIssueBody(finding, fp) {
218
250
  '',
219
251
  safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
220
252
  '',
221
- `**Fingerprint:** \`${fp}\``,
253
+ fingerprintSentinelLine(fp),
222
254
  `**Severity:** ${finding.severity}`,
223
255
  `**Check:** ${finding.checkId}`,
224
256
  `**Locator:** \`${codeSpanContent(finding.locator)}\``,
@@ -2,7 +2,8 @@
2
2
  * scripts/lib/tmux-layout/telemetry-stats.mjs
3
3
  *
4
4
  * Aggregator stub for /tmux-layout telemetry promotion gate (#563).
5
- * Reads .orchestrator/metrics/events.jsonl and computes:
5
+ * Reads the events ledger — active file AND every rotated archive (#1407) —
6
+ * and computes:
6
7
  * - invocation count (total + per layout)
7
8
  * - completion rate (completed / invoked) — promotion gate threshold 80%
8
9
  * - top-K degradation reasons
@@ -13,26 +14,68 @@
13
14
  * - CLI: `node scripts/lib/tmux-layout/telemetry-stats.mjs` (emits JSON to stdout)
14
15
  */
15
16
 
16
- import { readFileSync, existsSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { readEventsWithRotations } from '../events.mjs';
17
19
  import { isMainModule } from '../is-main-module.mjs';
18
20
 
19
21
  const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
20
22
 
23
+ /** Every event this module reports on carries this prefix. */
24
+ const TMUX_EVENT_PREFIX = 'tmux-layout.';
25
+
26
+ /**
27
+ * Read the tmux-layout events ACROSS rotation boundaries, together with the
28
+ * honesty verdict of that read.
29
+ *
30
+ * WHY THE ENVELOPE (#1407 acceptance criterion 3): the stats below are an
31
+ * ALL-TIME rate. Reading only the active file silently redefines "all time" as
32
+ * "since the last rotation", and a genuinely missing archive would then look
33
+ * identical to a quiet week. `complete === false` says which of the two it is;
34
+ * the CLI prints it and never computes over a partial set in silence.
35
+ *
36
+ * THREE states (#1423): `complete === null` is a THIRD reading — no source
37
+ * existed at all, so `invocations: 0` is UNMEASURED, not a measured zero, and
38
+ * `meetsPromotionGate: false` below rests on nothing. The CLI names that case
39
+ * separately; folding it into `false` would report a gap nobody found, folding
40
+ * it into `true` would report a measurement nobody took.
41
+ *
42
+ * CEILING (BV-004): `readEventsWithRotations` loads the active file and every
43
+ * archive fully into memory — up to ~60 MB transient at the default
44
+ * `max-size-mb: 10` / `max-backups: 5`. Acceptable here because this is a COLD
45
+ * ops path: a hand-run CLI / promotion-gate check, never a hook and never on
46
+ * the session path. Revisit if this module gains a hot-path caller or if
47
+ * `max-size-mb` is raised past ~100.
48
+ *
49
+ * @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
50
+ * @returns {{events: Array<object>, complete: boolean|null, gaps: Array<object>,
51
+ * notices: Array<object>}}
52
+ */
53
+ export function readTmuxEventsEnvelope(eventsPath = EVENTS_PATH) {
54
+ const { events, gaps, complete, notices } = readEventsWithRotations(undefined, {
55
+ filePath: eventsPath,
56
+ });
57
+ return {
58
+ events: events.filter(
59
+ (rec) => rec && typeof rec.event === 'string' && rec.event.startsWith(TMUX_EVENT_PREFIX),
60
+ ),
61
+ complete,
62
+ gaps,
63
+ notices: notices ?? [],
64
+ };
65
+ }
66
+
21
67
  /**
22
- * Read events.jsonl and return tmux-layout-related events.
68
+ * Read the events ledger and return tmux-layout-related events.
69
+ *
70
+ * Thin array-returning view of {@link readTmuxEventsEnvelope} — the shape every
71
+ * existing caller expects. Use the envelope when the completeness of the read
72
+ * matters to the answer.
23
73
  *
24
74
  * @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
25
75
  * @returns {Array<object>} parsed event records (filtered to tmux-layout.* events)
26
76
  */
27
77
  export function readTmuxEvents(eventsPath = EVENTS_PATH) {
28
- if (!existsSync(eventsPath)) return [];
29
- const raw = readFileSync(eventsPath, 'utf-8');
30
- const lines = raw.split('\n').filter((l) => l.trim().length > 0);
31
- return lines
32
- .map((l) => {
33
- try { return JSON.parse(l); } catch { return null; }
34
- })
35
- .filter((rec) => rec && typeof rec.event === 'string' && rec.event.startsWith('tmux-layout.'));
78
+ return readTmuxEventsEnvelope(eventsPath).events;
36
79
  }
37
80
 
38
81
  /**
@@ -95,7 +138,23 @@ export function computeStats(events) {
95
138
 
96
139
  // CLI entry-point — emit stats as JSON to stdout when run directly
97
140
  if (isMainModule(import.meta.url)) {
98
- const events = readTmuxEvents();
141
+ const { events, complete, gaps } = readTmuxEventsEnvelope();
99
142
  const stats = computeStats(events);
100
- console.log(JSON.stringify(stats, null, 2));
143
+ if (complete === null) {
144
+ // NOT MEASURED — a different sentence from "incomplete" (#1423): no active
145
+ // file and no archive means the zeros below were never observed.
146
+ console.error(
147
+ 'WARN: no events source was readable (no active file, no archive) — the stats below are UNMEASURED, not zero',
148
+ );
149
+ } else if (!complete) {
150
+ // A gap, never an empty window: diagnostics on stderr (cli-design.md),
151
+ // the machine-readable verdict in the JSON below.
152
+ const detail = gaps
153
+ .map((g) => `${g.kind}:${path.basename(String(g.archived_as ?? 'unknown'))}`)
154
+ .join(', ');
155
+ console.error(
156
+ `WARN: events ledger incomplete — the all-time rate below is computed over a PARTIAL set (${gaps.length} gap(s): ${detail})`,
157
+ );
158
+ }
159
+ console.log(JSON.stringify({ ...stats, ledgerComplete: complete, ledgerGaps: gaps }, null, 2));
101
160
  }
@@ -102,11 +102,31 @@ function normaliseScalar(text) {
102
102
  * than inventing a fifth dialect. Because the demotion is the surprising half,
103
103
  * each one emits exactly one stderr WARN naming the file.
104
104
  *
105
+ * The same normalisation governs the SIBLING boolean marker
106
+ * `disable-model-invocation` (the Cursor and Codex adapters read it with this
107
+ * predicate). The WARN therefore names the key it was asked about rather than a
108
+ * hard-coded `user-invocable`: measured 2026-09-18, a `disable-model-invocation:
109
+ * yes` demotion reported `user-invocable: "yes"`, sending the operator to a
110
+ * line that does not exist in that file.
111
+ *
112
+ * The NOUN follows the key too (#1388 P10). Naming the key while hard-coding
113
+ * "is NOT a slash-command marker" told a `disable-model-invocation: yes` author
114
+ * that his value failed to be something it never was — a correct key with a
115
+ * wrong subject reads as a broken diagnostic. The default phrase is unchanged
116
+ * for `user-invocable`, so the four existing call sites see identical text.
117
+ *
105
118
  * @param {unknown} value the raw frontmatter value
106
119
  * @param {string} [file] path named in the WARN when a truthy-looking value is demoted
120
+ * @param {string} [key] frontmatter key named in the WARN (default `user-invocable`)
107
121
  * @returns {boolean}
108
122
  */
109
- export function isUserInvocableValue(value, file) {
123
+ const MARKER_NOUN = Object.freeze({
124
+ 'user-invocable': 'a slash-command marker',
125
+ 'disable-model-invocation': 'a model-invocation opt-out marker',
126
+ });
127
+ const DEFAULT_MARKER_NOUN = 'a boolean marker';
128
+
129
+ export function isUserInvocableValue(value, file, key = 'user-invocable') {
110
130
  if (value === true) return true;
111
131
  if (typeof value !== 'string') return false;
112
132
  const token = normaliseScalar(value);
@@ -114,8 +134,8 @@ export function isUserInvocableValue(value, file) {
114
134
  if (TRUTHY_LOOKALIKE.test(token)) {
115
135
  const where = file ? `${file}: ` : '';
116
136
  process.stderr.write(
117
- `WARN ${where}user-invocable: ${JSON.stringify(value)} is NOT a slash-command marker — `
118
- + 'only `true` is (YAML 1.2 reads yes/on/1 as strings). Write `user-invocable: true`.\n',
137
+ `WARN ${where}${key}: ${JSON.stringify(value)} is NOT ${MARKER_NOUN[key] ?? DEFAULT_MARKER_NOUN} — `
138
+ + `only \`true\` is (YAML 1.2 reads yes/on/1 as strings). Write \`${key}: true\`.\n`,
119
139
  );
120
140
  }
121
141
  return false;
@@ -37,12 +37,16 @@
37
37
  * mistaken import away from filing issues nobody asked for.
38
38
  *
39
39
  * Exports:
40
- * UX_GRILL_LABELS, buildIssueTitle, buildIssueBody, reconcileFindings
40
+ * UX_GRILL_LABELS, buildIssueTitle, buildUxGrillIssueBody, reconcileFindings
41
+ * buildIssueBody — @deprecated alias of buildUxGrillIssueBody (5.2.0 name),
42
+ * removed in 6.0.0
41
43
  */
42
44
 
43
45
  import {
44
46
  createFinding as defaultCreateFinding,
47
+ fingerprintSentinelLine,
45
48
  listExistingFindings as defaultListExisting,
49
+ neutralizeFingerprintSentinel,
46
50
  triageDecision,
47
51
  updateFinding as defaultUpdateFinding,
48
52
  } from '../test-runner/issue-reconcile.mjs';
@@ -69,31 +73,22 @@ const TITLE_MAX_LENGTH = 100;
69
73
  const AUTO_SEVERITIES = Object.freeze(['critical', 'high']);
70
74
 
71
75
  /**
72
- * Neutralise a fingerprint-sentinel literal in free text (#388 SEC-IR-MED-1).
76
+ * Neutralise fingerprint-sentinel literals (#388 SEC-IR-MED-1) and strip the
77
+ * characters `createFinding` rejects at the argv boundary, so a multi-line page
78
+ * title in a finding message cannot turn into a VALIDATION failure for the whole
79
+ * reconcile pass.
73
80
  *
74
- * `issue-reconcile.mjs` applies this to `recommendation` only, inside
75
- * `reconcileFinding` — a body built HERE and handed to `createFinding` is not
76
- * sanitised by anything, so a finding `message` echoing page text could forge
77
- * the authoritative `**Fingerprint:**` line and make the next run's
81
+ * The neutralisation is the SHARED one from `issue-reconcile.mjs` (#1339 P6): a
82
+ * body built HERE and handed to `createFinding` is not sanitised by anything
83
+ * else, so a finding `message` echoing page text could otherwise forge the
84
+ * authoritative `**Fingerprint:**` line and make the next run's
78
85
  * `triageDecision` dedup against the wrong issue.
79
86
  *
80
87
  * @param {unknown} text
81
88
  * @returns {string}
82
89
  */
83
- function sanitizeSentinel(text) {
84
- return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
85
- }
86
-
87
- /**
88
- * Strip the characters `createFinding` rejects at the argv boundary, so a
89
- * multi-line page title in a finding message cannot turn into a VALIDATION
90
- * failure for the whole reconcile pass.
91
- *
92
- * @param {unknown} text
93
- * @returns {string}
94
- */
95
90
  function oneLine(text) {
96
- return sanitizeSentinel(text).replace(/[\n\r\0]+/g, ' ').trim();
91
+ return neutralizeFingerprintSentinel(text).replace(/[\n\r\0]+/g, ' ').trim();
97
92
  }
98
93
 
99
94
  /**
@@ -144,7 +139,7 @@ export function buildIssueTitle(finding) {
144
139
  * @param {string} [context.rubricHash]
145
140
  * @returns {string}
146
141
  */
147
- export function buildIssueBody(finding, { runId, rubricHash } = {}) {
142
+ export function buildUxGrillIssueBody(finding, { runId, rubricHash } = {}) {
148
143
  const evidence = finding?.evidence ?? {};
149
144
  const evidenceLines = Object.entries(evidence)
150
145
  .filter(([, value]) => typeof value === 'string' || Array.isArray(value))
@@ -153,7 +148,7 @@ export function buildIssueBody(finding, { runId, rubricHash } = {}) {
153
148
  return [
154
149
  oneLine(finding?.message) || `${oneLine(finding?.checkId)} at ${oneLine(finding?.locator)}`,
155
150
  '',
156
- `**Fingerprint:** \`${oneLine(finding?.fingerprint)}\``,
151
+ fingerprintSentinelLine(oneLine(finding?.fingerprint)),
157
152
  `**Severity:** ${oneLine(finding?.severity)}`,
158
153
  `**Check:** ${oneLine(finding?.checkId)}`,
159
154
  `**Locator:** \`${oneLine(finding?.locator)}\``,
@@ -168,6 +163,37 @@ export function buildIssueBody(finding, { runId, rubricHash } = {}) {
168
163
  .join('\n');
169
164
  }
170
165
 
166
+ /** One stderr line per process for the deprecated alias below, not one per call. */
167
+ let buildIssueBodyDeprecationWarned = false;
168
+
169
+ /**
170
+ * Deprecated alias of {@link buildUxGrillIssueBody}, shipped under this name in
171
+ * 5.2.0.
172
+ *
173
+ * @deprecated since 5.3.0 — use buildUxGrillIssueBody; removed in 6.0.0.
174
+ * Kept one minor cycle (`.claude/rules/development.md` § Package Lifecycle &
175
+ * Versioning) because `package.json` carries no `exports` map: every packed
176
+ * `scripts/lib/` export is a public deep-import, and a consumer calling this
177
+ * name would otherwise get a runtime TypeError, not a compile error. It
178
+ * delegates to the LOCAL ux-grill body builder — never to the unrelated
179
+ * `buildIssueBody(finding, fp)` of `test-runner/issue-reconcile.mjs`, whose
180
+ * finding shape and second argument differ.
181
+ *
182
+ * @param {object} finding - a `makeFinding` record
183
+ * @param {object} [context] - same as {@link buildUxGrillIssueBody}
184
+ * @returns {string}
185
+ */
186
+ export function buildIssueBody(finding, context) {
187
+ if (!buildIssueBodyDeprecationWarned) {
188
+ buildIssueBodyDeprecationWarned = true;
189
+ console.warn(
190
+ '[deprecated] buildIssueBody from scripts/lib/ux-grill/reconcile.mjs is deprecated since 5.3.0 ' +
191
+ 'and will be removed in 6.0.0 — use buildUxGrillIssueBody instead.',
192
+ );
193
+ }
194
+ return buildUxGrillIssueBody(finding, context);
195
+ }
196
+
171
197
  /**
172
198
  * Collect every string in a finding's `evidence` that looks like an absolute
173
199
  * host path (POSIX `/…` or Windows `C:\…`).
@@ -296,7 +322,7 @@ export async function reconcileFindings({
296
322
  }
297
323
 
298
324
  const title = buildIssueTitle(finding);
299
- const body = buildIssueBody(finding, { runId, rubricHash });
325
+ const body = buildUxGrillIssueBody(finding, { runId, rubricHash });
300
326
  const decision = triageDecision({ fingerprint: finding?.fingerprint, title }, candidates);
301
327
 
302
328
  if (decision.action === 'ignore') {
@@ -11,16 +11,16 @@
11
11
  * inode, or byte-identical).
12
12
  * B. root `plugin.json` — the agent-plugins.org 1.0.0 manifest; its `version`
13
13
  * must equal `package.json`'s and the two existing plugin manifests'.
14
- * C. `.agents/skills/<name>/SKILL.md` — one spec-legal mirror per source
15
- * skill, no orphans, agentskills.io field list only, description ≤ 1024.
14
+ * C. `.agents/skills/<name>/SKILL.md` — one spec-legal mirror per skill or
15
+ * command, no orphans, agentskills.io field list only, description ≤ 1024.
16
16
  *
17
17
  * INDEPENDENCE (load-bearing). This checker deliberately imports NOTHING from
18
18
  * `scripts/generate-agents-skills.mjs` and re-derives the spec field list, the
19
19
  * 1024 cap and the alias rule from the spec, not from the generator. A
20
20
  * generator-vs-generator check only proves the generator is self-consistent —
21
21
  * that is how the Cursor `argument-hint` defect survived. The oracles here are
22
- * the repository's own filesystem (`skills/`, `CLAUDE.md`, `package.json`) and
23
- * the published field list.
22
+ * the repository's own filesystem (`skills/`, `commands/`, `CLAUDE.md`,
23
+ * `package.json`) and the published field list.
24
24
  *
25
25
  * Usage: node scripts/lib/validate/check-agents-skills.mjs <plugin-root>
26
26
  * Exit codes: 0 — all checks passed · 1 — one or more failures.
@@ -195,6 +195,7 @@ console.log('');
195
195
  console.log('--- Check C: .agents/skills/ portable mirror ---');
196
196
  {
197
197
  const skillsDir = join(pluginRoot, 'skills');
198
+ const commandsDir = join(pluginRoot, 'commands');
198
199
  const mirrorDir = join(pluginRoot, '.agents', 'skills');
199
200
 
200
201
  const sourceSkills = existsSync(skillsDir) && statSync(skillsDir).isDirectory()
@@ -204,12 +205,21 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
204
205
  .filter((n) => existsSync(join(skillsDir, n, 'SKILL.md')))
205
206
  .sort()
206
207
  : [];
208
+ const sources = new Map(sourceSkills.map((name) => [name, `skills/${name}/SKILL.md`]));
209
+ if (existsSync(commandsDir) && statSync(commandsDir).isDirectory()) {
210
+ for (const entry of readdirSync(commandsDir, { withFileTypes: true })) {
211
+ if (!entry.isFile() || !entry.name.endsWith('.md') || /^[._]/.test(entry.name)) continue;
212
+ // Commands own public names when an internal skill has the same name.
213
+ sources.set(entry.name.slice(0, -3), `commands/${entry.name}`);
214
+ }
215
+ }
216
+ const sourceNames = [...sources.keys()].sort();
207
217
 
208
218
  if (!existsSync(mirrorDir)) {
209
- if (sourceSkills.length === 0) {
210
- pass('no skills/ and no .agents/skills/ — nothing to mirror');
219
+ if (sourceNames.length === 0) {
220
+ pass('no skill or command sources and no .agents/skills/ — nothing to mirror');
211
221
  } else {
212
- fail(`.agents/skills/ is missing but ${sourceSkills.length} source skill(s) exist `
222
+ fail(`.agents/skills/ is missing but ${sourceNames.length} source entry(s) exist `
213
223
  + '(remedy: node scripts/generate-agents-skills.mjs)');
214
224
  }
215
225
  } else {
@@ -218,12 +228,12 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
218
228
  .map((e) => e.name)
219
229
  .sort();
220
230
 
221
- const missing = sourceSkills.filter((n) => !mirrored.includes(n));
222
- const orphans = mirrored.filter((n) => !sourceSkills.includes(n));
223
- if (missing.length === 0) pass(`every source skill has a mirror (${sourceSkills.length})`);
224
- else fail(`${missing.length} source skill(s) have no mirror: ${missing.join(', ')}`);
231
+ const missing = sourceNames.filter((n) => !mirrored.includes(n));
232
+ const orphans = mirrored.filter((n) => !sources.has(n));
233
+ if (missing.length === 0) pass(`every source entry has a mirror (${sourceNames.length})`);
234
+ else fail(`${missing.length} source entry(s) have no mirror: ${missing.join(', ')}`);
225
235
  if (orphans.length === 0) pass('no orphan mirrors');
226
- else fail(`${orphans.length} orphan mirror(s) with no source skill: ${orphans.join(', ')}`);
236
+ else fail(`${orphans.length} orphan mirror(s) with no skill or command source: ${orphans.join(', ')}`);
227
237
 
228
238
  let fmFail = 0;
229
239
  let keyFail = 0;
@@ -303,8 +313,9 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
303
313
  }
304
314
  }
305
315
 
306
- if (!body.includes(`skills/${name}/SKILL.md`)) {
307
- fail(`.agents/skills/${name}/SKILL.md body does not cite the canonical skills/${name}/SKILL.md `
316
+ const canonical = sources.get(name);
317
+ if (canonical && !body.includes(canonical)) {
318
+ fail(`.agents/skills/${name}/SKILL.md body does not cite the canonical ${canonical} `
308
319
  + '— the mirror is a pointer, and a pointer with no target is dead weight');
309
320
  pointerFail += 1;
310
321
  }
@@ -314,7 +325,7 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
314
325
  if (keyFail === 0 && mirrored.length > 0) pass('all mirrors use only agentskills.io spec fields');
315
326
  if (descFail === 0 && mirrored.length > 0) pass(`all mirror descriptions are non-empty and ≤ ${DESCRIPTION_MAX} chars`);
316
327
  if (sizeFail === 0 && mirrored.length > 0) pass(`all mirrors ≤ ${MIRROR_MAX_BYTES} bytes (total ${totalBytes} bytes)`);
317
- if (pointerFail === 0 && mirrored.length > 0) pass('all mirrors cite their canonical SKILL.md');
328
+ if (pointerFail === 0 && mirrored.length > 0) pass('all mirrors cite their canonical source');
318
329
  }
319
330
  }
320
331
 
@@ -177,6 +177,7 @@ function runCli(pluginRoot) {
177
177
  } else {
178
178
  const detail = ((result.stdout ?? '') + (result.stderr ?? '')).trim();
179
179
  fail(`Cursor adapter wrappers are stale${detail ? `: ${detail}` : ''}`);
180
+ console.log(' Remedy: node scripts/generate-cursor-adapter.mjs');
180
181
  }
181
182
  }
182
183
 
@@ -76,6 +76,7 @@ import { readFileSync } from 'node:fs';
76
76
  import path from 'node:path';
77
77
 
78
78
  import { isMainModule } from '../is-main-module.mjs';
79
+ import { maskSource } from '../js-mask.mjs';
79
80
 
80
81
  /**
81
82
  * Files whose `process.argv[1]` comparison the oracle flags but which are NOT
@@ -102,50 +103,20 @@ const SELF_EXEMPT = 'scripts/lib/is-main-module.mjs';
102
103
  /**
103
104
  * Remove comments while preserving character offsets (so line numbers survive).
104
105
  *
106
+ * A thin wrapper over the shared lexer `maskSource` in `keepLiterals` mode:
107
+ * string, template and regex literals are skipped INTACT. That is load-bearing
108
+ * twice over — the V1 idiom \`file://${process.argv[1]}\` carries a `//` that a
109
+ * naive scanner reads as a line comment, and a regex literal must be recognised
110
+ * as one (#1383: a `/` the lexer read as a division let the regex body's `/*`
111
+ * open a block comment that blanked the guard below it — a fail-open miss, for
112
+ * a quote-bearing regex, a `/[/*]/` character class, and a regex after a keyword
113
+ * alike).
114
+ *
105
115
  * @param {string} src module source
106
116
  * @returns {string} same length, comment bytes replaced by spaces
107
117
  */
108
118
  export function stripComments(src) {
109
- const out = src.split('');
110
- let i = 0;
111
- let mode = 'code';
112
- while (i < src.length) {
113
- const two = src.slice(i, i + 2);
114
- if (mode === 'code') {
115
- if (two === '//') mode = 'line';
116
- else if (two === '/*') mode = 'block';
117
- else if (src[i] === '`' || src[i] === "'" || src[i] === '"') {
118
- // Skip OVER the literal to its closing delimiter, leaving the contents
119
- // intact. This is load-bearing, not hygiene: the V1 idiom is
120
- // \`file://${process.argv[1]}\` and the `//` in `file://` reads as a line
121
- // comment to a naive scanner, which blanks the rest of the line and
122
- // makes the most common variant of the defect class invisible.
123
- const quote = src[i];
124
- i++;
125
- while (i < src.length && src[i] !== quote) {
126
- if (src[i] === '\\') i++;
127
- i++;
128
- }
129
- i++;
130
- continue;
131
- }
132
- }
133
- if (mode === 'line') {
134
- if (src[i] === '\n') mode = 'code';
135
- else out[i] = ' ';
136
- } else if (mode === 'block') {
137
- if (src[i] !== '\n') out[i] = ' ';
138
- if (two === '*/') {
139
- out[i] = ' ';
140
- out[i + 1] = ' ';
141
- i += 2;
142
- mode = 'code';
143
- continue;
144
- }
145
- }
146
- i++;
147
- }
148
- return out.join('');
119
+ return maskSource(src, { keepLiterals: true });
149
120
  }
150
121
 
151
122
  /**
@@ -153,16 +124,8 @@ export function stripComments(src) {
153
124
  * Scanned rather than matched in one shot so the RECEIVER can be judged from the
154
125
  * text preceding each hit — a `.endsWith()` on anything other than
155
126
  * `process.argv[1]` is not this defect class.
156
- *
157
- * The quote characters are spelled `\x27` / `\x22` / `\x60` rather than
158
- * literally, and that is load-bearing for THIS file specifically: `stripComments`
159
- * below has no notion of a regex literal, so a bare `'` inside one reads as the
160
- * start of a string and desynchronises the scanner for the rest of the module —
161
- * measured 2026-09-17, a literal-quote version of this regex made the census
162
- * report a line-comment 40 lines further down as a finding. Same rule applies to
163
- * every regex added to this module.
164
127
  */
165
- const ENDS_WITH_LITERAL = /endsWith\(\s*([\x27\x22\x60])([^\x27\x22\x60]*)\1\s*\)/g;
128
+ const ENDS_WITH_LITERAL = /endsWith\(\s*(['"`])([^'"`]*)\1\s*\)/g;
166
129
 
167
130
  /**
168
131
  * The receiver chain, whitespace-stripped, that makes an `.endsWith()` call a
@@ -170,7 +133,7 @@ const ENDS_WITH_LITERAL = /endsWith\(\s*([\x27\x22\x60])([^\x27\x22\x60]*)\1\s*\
170
133
  * wrapper (`String(...)`, `path.basename(...)`) still matches; anchored at the
171
134
  * END so only the call immediately downstream of `process.argv[1]` counts.
172
135
  */
173
- const ARGV_RECEIVER_TAIL = /process\.argv\[1\](?:\|\|\x27\x27|\|\|\x22\x22)?\)*\??\.$/;
136
+ const ARGV_RECEIVER_TAIL = /process\.argv\[1\](?:\|\|''|\|\|"")?\)*\??\.$/;
174
137
 
175
138
  /**
176
139
  * Findings for one module body.