session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -22,9 +22,14 @@
22
22
  * Here `src` is defined by NEGATION, so a new top-level directory joins the
23
23
  * denominator the moment it is committed, with no rule edit and no re-derivation.
24
24
  *
25
- * universe `git ls-files`, filtered to CODE_EXTENSIONS
26
- * numerator tracked code under `tests/`
25
+ * universe `git ls-files`, filtered to CODE_EXTENSIONS, minus declaration
26
+ * files (`*.d.ts`, `*.d.mts`, `*.d.cts` — types, not executable code)
27
+ * numerator tracked code under a top-level `tests/`, plus co-located tests
28
+ * anywhere: basename `*.test.<ext>` / `*.spec.<ext>`, or any file
29
+ * under a `__tests__/` directory
27
30
  * denominator every OTHER tracked code file
31
+ * reported tracked shell (EXCLUDED_SHELL_EXTENSIONS) — in NEITHER bucket,
32
+ * its volume shipped as `excludedScope.shell` (#1488, variant B)
28
33
  *
29
34
  * ## The four questions, answered in code rather than left open
30
35
  *
@@ -37,8 +42,8 @@
37
42
  * Only git-tracked? YES — reproducible at a SHA, and it is
38
43
  * what keeps node_modules/, coverage/ and
39
44
  * untracked scratch out without an ignore list.
40
- * Do `tests/fixtures/` count as test LOC? YES when they are code (`.mjs`/`.js`/
41
- * `.cjs`). A code fixture is maintained
45
+ * Do `tests/fixtures/` count as test LOC? YES when they are code (any of
46
+ * CODE_EXTENSIONS). A code fixture is maintained
42
47
  * code that exists only to serve the suite —
43
48
  * that is test-corpus cost. Non-code
44
49
  * fixtures (`.json`, `.jsonl`, `.md`) are
@@ -62,7 +67,9 @@
62
67
  * <repo-root> defaults to process.cwd()
63
68
  * --json emit a single JSON object on stdout, nothing else
64
69
  * --check exit 1 when the ratio exceeds the ceiling (consolidation
65
- * wave required); exit 0 when inside the corridor
70
+ * wave required); exit 3 when the ratio is not measurable
71
+ * (`reason: 'no-tests-found'`, #1451); exit 0 when inside
72
+ * the corridor
66
73
  * --ceiling <n> override the TV-003 ceiling (default 1.60)
67
74
  * --stdin take newline-separated paths from stdin instead of
68
75
  * enumerating via `git ls-files` (test seam / staged-only mode)
@@ -71,6 +78,12 @@
71
78
  * 0 — measurement completed (and, under --check, ratio is within the corridor)
72
79
  * 1 — --check only: ratio exceeds the ceiling
73
80
  * 2 — tool error (missing/unreadable root, bad argv)
81
+ * 3 — --check only: ratio not measurable — 0 test files counted while test-like
82
+ * files ARE tracked (`reason: 'no-tests-found'`, e.g. a Python repo whose
83
+ * `tests/test_*.py` sit outside CODE_EXTENSIONS). Fail-closed since #1451:
84
+ * before, such a repo read `ratio 0, withinCorridor true` — "not found"
85
+ * reported as "in corridor". TypeScript tests count since the 2026-10-01
86
+ * owner decision on #1451; other stacks still get "not measurable".
74
87
  */
75
88
 
76
89
  import { readFileSync, existsSync, statSync } from 'node:fs';
@@ -90,23 +103,85 @@ import { isMainModule } from './is-main-module.mjs';
90
103
  * Applied symmetrically to numerator and denominator by construction — a file
91
104
  * type can never inflate one side while being invisible to the other.
92
105
  */
93
- export const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
106
+ export const CODE_EXTENSIONS = Object.freeze([
107
+ '.mjs',
108
+ '.js',
109
+ '.cjs',
110
+ '.jsx',
111
+ '.ts',
112
+ '.tsx',
113
+ '.mts',
114
+ '.cts',
115
+ ]);
94
116
 
95
- /** Path prefix that makes a tracked code file part of the TEST corpus. */
117
+ /**
118
+ * TypeScript declaration files end in a CODE_EXTENSION but carry only types —
119
+ * no executable line a test could exercise. Excluded from BOTH sides, so a
120
+ * generated `.d.ts` can never move the ratio.
121
+ */
122
+ export const DECLARATION_SUFFIXES = Object.freeze(['.d.ts', '.d.mts', '.d.cts']);
123
+
124
+ /** Top-level path prefix that makes a tracked code file part of the TEST corpus. */
96
125
  export const TEST_PREFIX = 'tests/';
97
126
 
127
+ /**
128
+ * Shell extensions the ratio deliberately does NOT count (#1488, owner decision
129
+ * 2026-10-02, variant B): they stay out of numerator and denominator, but their
130
+ * volume is MEASURED and reported under `excludedScope.shell`, so "this repo has
131
+ * no shell" and "this repo has 2.5k unmeasured shell lines" stop looking alike.
132
+ * Detection is by extension only — an extensionless shebang script (`.husky/*`)
133
+ * is not seen; sniffing shebangs would mean reading every extensionless tracked
134
+ * file inside a synchronous session-start probe.
135
+ */
136
+ export const EXCLUDED_SHELL_EXTENSIONS = Object.freeze(['.sh', '.bash', '.bats']);
137
+
138
+ /**
139
+ * Co-located test files, wherever they sit: a `*.test.<ext>` / `*.spec.<ext>`
140
+ * basename (the vitest/jest default include shape) or a `__tests__/` segment.
141
+ */
142
+ const COLOCATED_TEST_RE = /\.(test|spec)\.[^./]+$|(^|\/)__tests__\//;
143
+
144
+ /**
145
+ * The one test-corpus rule, shared by the counted code buckets and the reported
146
+ * shell scope so the two can never split a path differently.
147
+ * @param {string} p normalised repo-relative path
148
+ */
149
+ function isTestCorpusPath(p) {
150
+ return p.startsWith(TEST_PREFIX) || COLOCATED_TEST_RE.test(p);
151
+ }
152
+
153
+ /**
154
+ * A tracked path that LOOKS like a test, counted or not: a `*.test.*` / `*.spec.*`
155
+ * JS/TS file, anything under a `__tests__/` segment, or any file under `tests/`
156
+ * regardless of extension. Used only to tell "this repo has no tests" apart from
157
+ * "this repo has tests the recipe cannot see" (#1451) — e.g. `tests/test_x.py`.
158
+ */
159
+ const TEST_LIKE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
160
+
161
+ /**
162
+ * @param {string} relPath repo-relative path
163
+ * @returns {boolean}
164
+ */
165
+ export function isTestLikePath(relPath) {
166
+ const p = String(relPath).replace(/\\/g, '/').replace(/^\.\//, '');
167
+ return TEST_LIKE_RE.test(p) || p.startsWith(TEST_PREFIX) || /(^|\/)__tests__\//.test(p);
168
+ }
169
+
98
170
  /** The TV-003 ceiling. Exceeding it switches the consolidation rule on. */
99
171
  export const DEFAULT_CEILING = 1.6;
100
172
 
101
173
  /** Machine-readable schema tag for the --json envelope. */
102
- export const SCHEMA = 'tests-src-ratio/1';
174
+ export const SCHEMA = 'tests-src-ratio/2';
103
175
 
104
176
  /**
105
177
  * Which bucket a repo-relative path belongs to.
106
178
  *
107
- * `src` is defined by NEGATION — every tracked code file that is not under
108
- * `tests/`. That is what makes the two buckets a partition rather than two
109
- * globs that can overlap or leave a gap.
179
+ * `src` is defined by NEGATION — every tracked code file that is not a test
180
+ * (under `tests/`, or co-located per COLOCATED_TEST_RE). That is what makes the
181
+ * two buckets a partition rather than two globs that can overlap or leave a gap.
182
+ *
183
+ * Known blind spot: a nested `pkg/tests/helper.ts` that is not itself named
184
+ * `*.test.*` counts as src — only the TOP-LEVEL `tests/` is a test directory.
110
185
  *
111
186
  * @param {string} relPath repo-relative path, `/`-separated
112
187
  * @returns {'test'|'src'|null} null = outside the metric entirely
@@ -114,7 +189,22 @@ export const SCHEMA = 'tests-src-ratio/1';
114
189
  export function classifyPath(relPath) {
115
190
  const p = String(relPath).replace(/\\/g, '/').replace(/^\.\//, '');
116
191
  if (!CODE_EXTENSIONS.some((ext) => p.endsWith(ext))) return null;
117
- return p === TEST_PREFIX.slice(0, -1) || p.startsWith(TEST_PREFIX) ? 'test' : 'src';
192
+ if (DECLARATION_SUFFIXES.some((ext) => p.endsWith(ext))) return null;
193
+ return isTestCorpusPath(p) ? 'test' : 'src';
194
+ }
195
+
196
+ /**
197
+ * Which half of the EXCLUDED shell scope a path falls into — reported, never
198
+ * counted in the ratio. `.bats` is a test framework's own extension, so it is
199
+ * test-shell anywhere; any other shell file follows the code rule above.
200
+ *
201
+ * @param {string} relPath repo-relative path
202
+ * @returns {'test'|'src'|null} null = not shell
203
+ */
204
+ function classifyShellPath(relPath) {
205
+ const p = String(relPath).replace(/\\/g, '/').replace(/^\.\//, '');
206
+ if (!EXCLUDED_SHELL_EXTENSIONS.some((ext) => p.endsWith(ext))) return null;
207
+ return p.endsWith('.bats') || isTestCorpusPath(p) ? 'test' : 'src';
118
208
  }
119
209
 
120
210
  /**
@@ -154,7 +244,10 @@ export function countPhysicalLines(content) {
154
244
  * @param {(relPath: string) => string|null} opts.readFile returns content, or null when unreadable
155
245
  * @param {number} [opts.ceiling]
156
246
  * @returns {{testFiles:number,testLoc:number,srcFiles:number,srcLoc:number,ratio:number|null,
157
- * ceiling:number,withinCorridor:boolean,consolidationWaveRequired:boolean,skipped:number}}
247
+ * ceiling:number,withinCorridor:boolean|null,consolidationWaveRequired:boolean|null,
248
+ * skipped:number,testLikeFilesSeen:number,reason:'no-tests-found'|null,
249
+ * excludedScope:{shell:{files:number,loc:number,testFiles:number,testLoc:number,
250
+ * srcFiles:number,srcLoc:number,skipped:number}}}}
158
251
  */
159
252
  export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
160
253
  let testFiles = 0;
@@ -162,8 +255,32 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
162
255
  let srcFiles = 0;
163
256
  let srcLoc = 0;
164
257
  let skipped = 0;
258
+ let testLikeFilesSeen = 0;
259
+ // Reported only — never added to testLoc/srcLoc (#1488 variant B). Always
260
+ // present, zeros included: a measured zero, not an absent measurement.
261
+ const shell = { files: 0, loc: 0, testFiles: 0, testLoc: 0, srcFiles: 0, srcLoc: 0, skipped: 0 };
165
262
 
166
263
  for (const rel of files) {
264
+ if (isTestLikePath(rel)) testLikeFilesSeen++;
265
+ const shellBucket = classifyShellPath(rel);
266
+ if (shellBucket !== null) {
267
+ const content = readFile(rel);
268
+ if (content === null || content === undefined) {
269
+ shell.skipped++;
270
+ continue;
271
+ }
272
+ const lines = countPhysicalLines(content);
273
+ shell.files++;
274
+ shell.loc += lines;
275
+ if (shellBucket === 'test') {
276
+ shell.testFiles++;
277
+ shell.testLoc += lines;
278
+ } else {
279
+ shell.srcFiles++;
280
+ shell.srcLoc += lines;
281
+ }
282
+ continue;
283
+ }
167
284
  const bucket = classifyPath(rel);
168
285
  if (bucket === null) continue;
169
286
  const content = readFile(rel);
@@ -183,6 +300,27 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
183
300
 
184
301
  // A repo with no src code has an undefined ratio, not an infinite one. Saying
185
302
  // `null` keeps the consumer from reading Infinity as a corridor breach.
303
+ // Fail closed (#1451): a zero numerator next to tracked test-like files means
304
+ // the recipe cannot SEE this repo's tests (e.g. `tests/test_x.py`), not that
305
+ // there are none. Reporting ratio 0 / withinCorridor true there turned "not found"
306
+ // into "green". The verdict is unknown, so every verdict field is null.
307
+ if (testFiles === 0 && testLikeFilesSeen > 0) {
308
+ return {
309
+ testFiles,
310
+ testLoc,
311
+ srcFiles,
312
+ srcLoc,
313
+ ratio: null,
314
+ ceiling,
315
+ withinCorridor: null,
316
+ consolidationWaveRequired: null,
317
+ skipped,
318
+ testLikeFilesSeen,
319
+ reason: 'no-tests-found',
320
+ excludedScope: { shell },
321
+ };
322
+ }
323
+
186
324
  const ratio = srcLoc === 0 ? null : Number((testLoc / srcLoc).toFixed(4));
187
325
  const withinCorridor = ratio === null ? true : ratio <= ceiling;
188
326
 
@@ -196,6 +334,9 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
196
334
  withinCorridor,
197
335
  consolidationWaveRequired: !withinCorridor,
198
336
  skipped,
337
+ testLikeFilesSeen,
338
+ reason: null,
339
+ excludedScope: { shell },
199
340
  };
200
341
  }
201
342
 
@@ -208,11 +349,21 @@ export function definitionOf(source) {
208
349
  return {
209
350
  source: source === 'stdin' ? 'stdin path list' : 'git ls-files (tracked files only)',
210
351
  codeExtensions: [...CODE_EXTENSIONS],
211
- numerator: `tracked code files under ${TEST_PREFIX}`,
352
+ declarationsExcluded: [...DECLARATION_SUFFIXES],
353
+ numerator:
354
+ `tracked code files under a top-level ${TEST_PREFIX}, plus co-located tests anywhere: ` +
355
+ 'basename *.test.<ext> / *.spec.<ext>, or any file under a __tests__/ directory',
212
356
  denominator: `every OTHER tracked code file (src defined by negation, not by a directory list)`,
213
357
  lineRule: 'physical lines; blank + comment lines counted; EOF-newline-insensitive',
214
358
  excluded:
215
- 'non-code extensions (.md, .json, .jsonl, .yml) on BOTH sides; untracked files (node_modules/, coverage/, scratch)',
359
+ `declaration files (${DECLARATION_SUFFIXES.map((x) => `*${x}`).join(', ')}: types, not ` +
360
+ 'executable code) and non-code extensions (.md, .json, .jsonl, .yml) on BOTH sides; ' +
361
+ 'untracked files (node_modules/, coverage/, scratch)',
362
+ excludedScope:
363
+ `shell (${EXCLUDED_SHELL_EXTENSIONS.join(' ')}, by extension only — extensionless shebang ` +
364
+ 'scripts such as .husky/ hooks are not seen): counted in NEITHER bucket, its volume ' +
365
+ 'reported under excludedScope.shell (#1488). test-shell = .bats anywhere, or the ' +
366
+ 'numerator path rule above; every other shell file is src-shell. Same line rule.',
216
367
  };
217
368
  }
218
369
 
@@ -322,7 +473,11 @@ const USAGE =
322
473
  * because a measurement problem must not block a session start.
323
474
  *
324
475
  * @param {{ repoRoot?: string, ceiling?: number }} [opts]
325
- * @returns {{ severity: 'warn', message: string, ratio: number, ceiling: number } | null}
476
+ * The one non-null path besides a breach is `reason: 'no-tests-found'` (#1451):
477
+ * a repo whose tests the recipe cannot see gets a WARN, never silence — silence
478
+ * is what made the unmeasurable ratio read as green fleet-wide.
479
+ *
480
+ * @returns {{ severity: 'warn', message: string, ratio: number|null, ceiling: number, reason?: string } | null}
326
481
  */
327
482
  export function checkTestsSrcRatio({ repoRoot, ceiling = DEFAULT_CEILING } = {}) {
328
483
  try {
@@ -342,6 +497,18 @@ export function checkTestsSrcRatio({ repoRoot, ceiling = DEFAULT_CEILING } = {})
342
497
  };
343
498
 
344
499
  const result = measure({ files, readFile, ceiling });
500
+ if (result.reason === 'no-tests-found') {
501
+ return {
502
+ severity: 'warn',
503
+ ratio: null,
504
+ ceiling: result.ceiling,
505
+ reason: result.reason,
506
+ message:
507
+ `⚠ tests:src ratio nicht messbar — 0 Testdateien gezählt, ` +
508
+ `${result.testLikeFilesSeen} test-artige Dateien getrackt (#1451). ` +
509
+ `Detail: node scripts/lib/tests-src-ratio.mjs --json`,
510
+ };
511
+ }
345
512
  if (result.ratio === null || result.withinCorridor) return null;
346
513
 
347
514
  const dirty = isDirty(root);
@@ -402,17 +569,23 @@ function main() {
402
569
  console.log(USAGE);
403
570
  console.log('');
404
571
  console.log('The canonical tests:src LOC measurement for TV-003 (test-value.md).');
405
- console.log(' numerator tracked code under tests/');
572
+ console.log(' numerator tracked code under tests/, plus *.test.* / *.spec.* / __tests__/ anywhere');
406
573
  console.log(' denominator every other tracked code file (src by negation)');
407
574
  console.log(` code exts ${CODE_EXTENSIONS.join(' ')} (.md / .json never counted)`);
575
+ console.log(` excluded declaration files ${DECLARATION_SUFFIXES.join(' ')} on both sides`);
576
+ console.log(
577
+ ` shell ${EXCLUDED_SHELL_EXTENSIONS.join(' ')} counted nowhere, volume reported (excludedScope.shell)`,
578
+ );
408
579
  console.log(' lines physical; blanks + comments counted; EOF-newline-insensitive');
409
580
  console.log('');
410
581
  console.log(' --json machine-readable envelope on stdout');
411
- console.log(' --check exit 1 when the ratio exceeds the ceiling');
582
+ console.log(' --check exit 1 when the ratio exceeds the ceiling, 3 when not measurable');
412
583
  console.log(` --ceiling <n> override the TV-003 ceiling (default ${DEFAULT_CEILING})`);
413
584
  console.log(' --stdin read newline-separated paths instead of git ls-files');
414
585
  console.log('');
415
- console.log('Exit: 0 ok / within corridor · 1 (--check) ceiling exceeded · 2 tool error');
586
+ console.log(
587
+ 'Exit: 0 ok / within corridor · 1 (--check) ceiling exceeded · 2 tool error · 3 (--check) not measurable',
588
+ );
416
589
  process.exit(0);
417
590
  }
418
591
 
@@ -460,18 +633,37 @@ function main() {
460
633
  // fail-open class is not worth re-litigating per payload size.
461
634
  writeStdoutLineSync(JSON.stringify(envelope, null, 2));
462
635
  } else {
463
- const r = result.ratio === null ? 'n/a (no src code)' : result.ratio.toFixed(4);
464
- const verdict = result.withinCorridor
465
- ? 'within corridor'
466
- : `ABOVE ceiling ${result.ceiling} — TV-003 consolidation wave required`;
636
+ const notMeasurable = result.reason === 'no-tests-found';
637
+ const r = notMeasurable
638
+ ? 'n/a (no tests found)'
639
+ : result.ratio === null
640
+ ? 'n/a (no src code)'
641
+ : result.ratio.toFixed(4);
642
+ const verdict = notMeasurable
643
+ ? `NOT MEASURABLE — ${result.testLikeFilesSeen} test-like files tracked but 0 counted (#1451)`
644
+ : result.withinCorridor
645
+ ? 'within corridor'
646
+ : `ABOVE ceiling ${result.ceiling} — TV-003 consolidation wave required`;
467
647
  const at = envelope.ref ? ` @ ${envelope.ref}${dirty ? '+dirty' : ''}` : '';
648
+ const sh = result.excludedScope.shell;
468
649
  writeStdoutLineSync(
469
650
  `tests:src = ${r} (${result.testLoc} test LOC / ${result.srcLoc} src LOC` +
470
651
  `; ${result.testFiles} test + ${result.srcFiles} src files${at}) — ${verdict}` +
652
+ (sh.files > 0
653
+ ? `\n excluded: shell ${sh.files} files / ${sh.loc} lines ` +
654
+ `(${sh.testFiles} test + ${sh.srcFiles} src files) — in neither bucket (#1488)`
655
+ : '') +
471
656
  (dirty ? '\n NOTE: working tree is dirty — this number is NOT reproducible at that SHA' : ''),
472
657
  );
473
658
  }
474
659
 
660
+ if (args.check && result.reason === 'no-tests-found') {
661
+ console.error(
662
+ `tests:src ratio not measurable — 0 test files counted but ${result.testLikeFilesSeen} ` +
663
+ `test-like files tracked (no-tests-found); see #1451`,
664
+ );
665
+ process.exit(3);
666
+ }
475
667
  process.exit(args.check && !result.withinCorridor ? 1 : 0);
476
668
  }
477
669
 
@@ -25,6 +25,7 @@
25
25
 
26
26
  import path from 'node:path';
27
27
  import { resolveStateDir } from '../platform.mjs';
28
+ import { shellQuote } from '../sh-quote.mjs';
28
29
  import { detectVcsCommand } from './vcs-detector.mjs';
29
30
  import { isSessionCollision } from './tmux-shell.mjs';
30
31
 
@@ -48,17 +49,6 @@ try {
48
49
  // Internal helpers
49
50
  // ---------------------------------------------------------------------------
50
51
 
51
- /**
52
- * Single-quote a shell command string for use as a tmux send-keys argument.
53
- * Escapes embedded single-quotes using the POSIX `'...' '\'' '...'` pattern.
54
- *
55
- * @param {string} s
56
- * @returns {string} e.g. 'tail -F /path/to/STATE.md'
57
- */
58
- function shellQuote(s) {
59
- return "'" + String(s).replace(/'/g, "'\\''") + "'";
60
- }
61
-
62
52
  /** Poll interval (seconds) of the agent-status pane loop. */
63
53
  const STATUS_PANE_POLL_SECONDS = 2;
64
54
 
@@ -23,6 +23,18 @@ const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
23
23
  /** Every event this module reports on carries this prefix. */
24
24
  const TMUX_EVENT_PREFIX = 'tmux-layout.';
25
25
 
26
+ /**
27
+ * Events window this reader needs retained (#1401 part 3): UNBOUNDED, hence
28
+ * `null`. The stats are an ALL-TIME rate, so every record ever written is in
29
+ * scope and no finite `events-rotation` retention satisfies it — a number here
30
+ * could only be invented. Truncation is reported by the envelope's own
31
+ * `complete: false` (pruned archives leave rotation tombstones), not by a
32
+ * retention floor. Read by `scripts/lib/events-retention-banner.mjs`.
33
+ *
34
+ * @type {number|null}
35
+ */
36
+ export const REQUIRED_EVENTS_WINDOW_DAYS = null;
37
+
26
38
  /**
27
39
  * Read the tmux-layout events ACROSS rotation boundaries, together with the
28
40
  * honesty verdict of that read.
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { execSync } from 'node:child_process';
8
8
 
9
+ import { shellQuote } from '../sh-quote.mjs';
10
+
9
11
  const MIN_MAJOR = 3;
10
12
  const MIN_MINOR = 0;
11
13
 
@@ -69,14 +71,3 @@ export function isSessionCollision(sessionName) {
69
71
  return { exists: false, error: `tmux has-session failed: ${err.message}` };
70
72
  }
71
73
  }
72
-
73
- /**
74
- * Shell-quote a string for safe interpolation into a shell command.
75
- * Uses single-quote wrapping with embedded single-quote escaping.
76
- *
77
- * @param {string} s
78
- * @returns {string}
79
- */
80
- function shellQuote(s) {
81
- return "'" + String(s).replace(/'/g, "'\\''") + "'";
82
- }
@@ -187,7 +187,7 @@ export function buildIssueBody(finding, context) {
187
187
  if (!buildIssueBodyDeprecationWarned) {
188
188
  buildIssueBodyDeprecationWarned = true;
189
189
  console.warn(
190
- '[deprecated] buildIssueBody from scripts/lib/ux-grill/reconcile.mjs is deprecated since 5.3.0 ' +
190
+ '[deprecated] buildIssueBody from scripts/lib/ux-grill/reconcile.mjs is deprecated since the 5.3 line ' +
191
191
  'and will be removed in 6.0.0 — use buildUxGrillIssueBody instead.',
192
192
  );
193
193
  }
@@ -327,7 +327,7 @@ if (mdFiles.length === 0) {
327
327
  if (modelVal === null || (!ALLOWED_MODEL_ALIASES.has(modelVal) && !MODEL_ID_RE.test(modelVal))) {
328
328
  const got = modelVal ?? '';
329
329
  const aliasList = [...ALLOWED_MODEL_ALIASES].join('|');
330
- fail(`${agentName}: model must be ${aliasList} or a full model ID like 'claude-opus-4-7' or 'claude-sonnet-5' (got: '${got}')`);
330
+ fail(`${agentName}: model must be ${aliasList} or a full model ID like 'claude-opus-5-5' or 'claude-sonnet-5' (got: '${got}')`);
331
331
  }
332
332
  }
333
333
 
@@ -113,6 +113,11 @@ const DOCUMENTED_ASYMMETRIES = {
113
113
  'skill-invocation-telemetry.mjs',
114
114
  'enforce-scope.mjs',
115
115
  'config-protection.mjs',
116
+ // pre-edit-sessions-ledger-guard (#1443): same Edit/Write/MultiEdit tool-name
117
+ // gate as enforce-scope/config-protection above — Codex writes files through
118
+ // `apply_patch`, a name this hook never matches. Wired, it would run and allow
119
+ // everything: a silent no-op, not enforcement. Gap registered, not faked.
120
+ 'pre-edit-sessions-ledger-guard.mjs',
116
121
  'pre-bash-destructive-guard.mjs',
117
122
  'pre-bash-staging-fence.mjs',
118
123
  'pre-bash-memory-propose-audit.mjs',