codehere 0.8.0 → 1.0.2

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 (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Tier 1.1 (verdict-trust punch list, 2026-06-09) · RE-RUN the tests, don't
3
+ * trust the agent's stdout.
4
+ *
5
+ * The gap this closes: `artifacts.tests` was regex-scraped from the AGENT's own
6
+ * captured stdout (`parseTestSummary(textAccum)`) and never re-executed — so
7
+ * "18/18 passed" was verified against the agent's own claim. A skeptic waves
8
+ * that off instantly. We already check FILE claims against ground truth
9
+ * (`git diff`); this extends the same "observe reality, don't trust the
10
+ * narration" rule to tests: codehere independently runs the project's test
11
+ * command on the real on-disk state and reads the REAL exit code.
12
+ *
13
+ * The authoritative signal is the EXIT CODE (a test runner exits non-zero on
14
+ * failure), not the scraped count — counts are display-only. So a report that
15
+ * claims "all tests pass" while the re-run exits non-zero becomes a caught lie
16
+ * (verify-claim flags it `broken` once `failed > 0`).
17
+ *
18
+ * Safe by construction: async (never blocks the daemon), timeout-bounded,
19
+ * opt-out via CODEHERE_RERUN_TESTS=0, and only ever runs the repo's OWN test
20
+ * script — strictly inside the trust boundary codehere already crosses by
21
+ * spawning agents that execute code in this repo.
22
+ */
23
+ import { spawn } from 'node:child_process';
24
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { parseTestSummary } from '../../infrastructure/orchestration/artifacts-extractor.js';
27
+ const TIMEOUT_SETTING = Number(process.env.CODEHERE_RERUN_TIMEOUT_MS);
28
+ /**
29
+ * Whole milliseconds that a timer can hold; anything else falls back to two minutes. Node fires a NaN, fractional
30
+ * or over-2^31 timer after 1 ms, which would time out every run.
31
+ */
32
+ const DEFAULT_TIMEOUT_MS = Number.isInteger(TIMEOUT_SETTING) && TIMEOUT_SETTING > 0 && TIMEOUT_SETTING <= 2 ** 31 - 1
33
+ ? TIMEOUT_SETTING
34
+ : 120_000;
35
+ /** 500 → "0.5s", 120000 → "120s". */
36
+ const seconds = (ms) => `${Number.isInteger(ms / 1000) ? ms / 1000 : (ms / 1000).toFixed(1)}s`;
37
+ /** True when node's test runner printed a summary and every summary says 0 tests, or when go test found no test files. `node --test`
38
+ * exits 0 for a misnamed file (`calc.tests.js`), so the exit code alone reads as a pass.
39
+ * pytest already exits 5 in that case. */
40
+ export function reportedZeroTests(out) {
41
+ const counts = [...out.matchAll(/^[ \t]*(?:ℹ|#)[ \t]+tests[ \t]+(\d+)[ \t]*$/gm)].map((m) => Number(m[1]));
42
+ if (counts.length > 0)
43
+ return counts.every((n) => n === 0);
44
+ // `go test ./...` exits 0 and prints `[no test files]` for every package when the module has no _test.go files.
45
+ return /^\?[ \t]+.*\[no test files\]/m.test(out) && !/^(ok|FAIL|---)[ \t]/m.test(out);
46
+ }
47
+ /** True when the runner's own summary says tests were skipped and none passed or failed: node:test (`pass 0`,
48
+ * `skipped N`), pytest's final line, and the jest or vitest `Tests` line. A run like that tested nothing. */
49
+ export function reportedOnlySkipped(out) {
50
+ const last = (re) => [...out.matchAll(re)].pop();
51
+ const node = (k) => {
52
+ const m = last(new RegExp(`^[ \\t]*(?:ℹ|#)[ \\t]+${k}[ \\t]+(\\d+)[ \\t]*$`, 'gm'));
53
+ return m ? Number(m[1]) : null;
54
+ };
55
+ const pass = node('pass');
56
+ const skipped = node('skipped');
57
+ if (pass !== null && skipped !== null)
58
+ return pass === 0 && (node('fail') ?? 0) === 0 && skipped > 0;
59
+ const py = last(/^=+ (.*) in [\d.]+s(?: \([^)]*\))? =+$/gm);
60
+ if (py)
61
+ return /\b\d+ skipped\b/.test(py[1]) && !/\b\d+ (passed|failed|errors?|xpassed)\b/.test(py[1]);
62
+ const js = last(/^[ \t]*Tests:?[ \t]+(.*)$/gm);
63
+ if (js)
64
+ return /\b\d+ skipped\b/.test(js[1]) && !/\b\d+ (passed|failed)\b/.test(js[1]);
65
+ return false;
66
+ }
67
+ /** True if `file` exists in `cwd` and its text contains `needle` (case-insensitive). */
68
+ function fileMentions(cwd, file, needle) {
69
+ try {
70
+ const p = join(cwd, file);
71
+ return existsSync(p) && readFileSync(p, 'utf8').toLowerCase().includes(needle.toLowerCase());
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /** True if `cwd` (top level, or a test/ or tests/ dir) holds a Node built-in
78
+ * test file — `*.test.{mjs,cjs,js}`. Lets codehere observe reality in a plain
79
+ * Node repo that uses `node --test` with no npm script or runner config (a
80
+ * common setup that previously left the verifier blind — DISCOVERED via a real
81
+ * V3 run, 2026-06-14). Bounded: top level + two conventional dirs, never a
82
+ * recursive walk. */
83
+ function hasNodeTestFiles(cwd) {
84
+ const isTest = (f) => /\.test\.(mjs|cjs|js)$/.test(f);
85
+ for (const dir of [cwd, join(cwd, 'test'), join(cwd, 'tests')]) {
86
+ try {
87
+ if (readdirSync(dir).some(isTest))
88
+ return true;
89
+ }
90
+ catch {
91
+ /* dir absent / unreadable → keep looking */
92
+ }
93
+ }
94
+ return false;
95
+ }
96
+ /**
97
+ * Detect a runnable test command in `cwd`, across ecosystems — so "codehere
98
+ * re-runs your tests and catches a lying report" holds for Python/Go/Rust repos,
99
+ * not only npm. Conservative: each branch keys off an UNAMBIGUOUS marker; we
100
+ * return null (never guess) when there's nothing known to run.
101
+ *
102
+ * Precedence: explicit override → Node (npm test) → Go → Rust → Python (pytest)
103
+ * → a JS runner config without an npm script. First match wins.
104
+ */
105
+ export function detectTestCommand(cwd) {
106
+ // 0. Explicit per-repo override — the escape hatch for anything we can't infer
107
+ // (monorepos, custom runners). e.g. CODEHERE_TEST_CMD="make test". Run via
108
+ // execFile (no shell), so args are whitespace-split — a quoted arg with
109
+ // spaces (CODEHERE_TEST_CMD='pytest -k "a b"') is NOT supported; use a
110
+ // wrapper script for that case.
111
+ const override = process.env.CODEHERE_TEST_CMD?.trim();
112
+ if (override) {
113
+ const [cmd, ...args] = override.split(/\s+/);
114
+ return { cmd, args, display: override };
115
+ }
116
+ // 1. Node — the project's declared `npm test` (dominant case + this repo).
117
+ const pj = join(cwd, 'package.json');
118
+ if (existsSync(pj)) {
119
+ try {
120
+ const json = JSON.parse(readFileSync(pj, 'utf8'));
121
+ const test = json.scripts?.test;
122
+ // npm's default placeholder script is not a real suite — skip it.
123
+ if (typeof test === 'string' && test.trim() && !/no test specified/i.test(test)) {
124
+ return { cmd: 'npm', args: ['test', '--silent'], display: 'npm test' };
125
+ }
126
+ }
127
+ catch {
128
+ /* unreadable package.json → fall through to other ecosystems */
129
+ }
130
+ }
131
+ // 2. Go — go.mod present → the standard whole-module test run.
132
+ if (existsSync(join(cwd, 'go.mod'))) {
133
+ return { cmd: 'go', args: ['test', './...'], display: 'go test ./...' };
134
+ }
135
+ // 3. Rust — Cargo.toml → `cargo test`.
136
+ if (existsSync(join(cwd, 'Cargo.toml'))) {
137
+ return { cmd: 'cargo', args: ['test'], display: 'cargo test' };
138
+ }
139
+ // 4. Python — pytest is the dominant runner. Key off pytest-specific markers
140
+ // (conftest.py / pytest.ini are unambiguous). For pyproject/setup.cfg, match
141
+ // the actual pytest CONFIG SECTION header — not a bare "pytest" substring,
142
+ // which would fire on a comment or an unrelated dependency string and run
143
+ // pytest on a project that doesn't use it.
144
+ if (existsSync(join(cwd, 'pytest.ini')) ||
145
+ existsSync(join(cwd, 'conftest.py')) ||
146
+ fileMentions(cwd, 'pyproject.toml', '[tool.pytest') ||
147
+ fileMentions(cwd, 'setup.cfg', '[tool:pytest]')) {
148
+ return { cmd: 'pytest', args: ['-q'], display: 'pytest' };
149
+ }
150
+ // 5. A JS test runner with config but no npm `test` script.
151
+ //
152
+ // SECURITY (2026-08-08) · `--no-install` is load-bearing on THIS path above all
153
+ // others. Bare `npx vitest` falls back to the public registry when the runner
154
+ // is not installed locally, so codehere would DOWNLOAD a test runner and
155
+ // execute it against the user's repo — and then report the result as
156
+ // "verifier re-ran the suite", the strongest claim this product makes. The
157
+ // same fallback let `npx graphify-ts` fetch an unrelated stranger's package
158
+ // (fixed 86c7893); here the blast radius is the evidence itself.
159
+ //
160
+ // Refusing is also the semantically right answer: a runner the project does
161
+ // not have installed is not the project's test suite, so re-running with it
162
+ // proves nothing. When this returns nothing runnable, applyTestObservation
163
+ // records an honest `testsUnobserved` reason instead — which is exactly the
164
+ // abstention the eval harness scores 2/2 on.
165
+ if (existsSync(join(cwd, 'vitest.config.ts')) || existsSync(join(cwd, 'vitest.config.js'))) {
166
+ return { cmd: 'npx', args: ['--no-install', 'vitest', 'run'], display: 'npx vitest run' };
167
+ }
168
+ if (existsSync(join(cwd, 'jest.config.js')) || existsSync(join(cwd, 'jest.config.ts'))) {
169
+ return { cmd: 'npx', args: ['--no-install', 'jest'], display: 'npx jest' };
170
+ }
171
+ // 6. Node built-in test runner — `*.test.{mjs,cjs,js}` present but no npm
172
+ // script or runner config above. `node --test` self-discovers them. This is
173
+ // the plain-Node case that left codehere blind to test reality (the verifier
174
+ // can't catch a "tests pass" lie it never observed). Lowest precedence so a
175
+ // declared script/runner always wins.
176
+ if (hasNodeTestFiles(cwd)) {
177
+ return { cmd: 'node', args: ['--test'], display: 'node --test' };
178
+ }
179
+ return null;
180
+ }
181
+ /**
182
+ * The one command that would make the suite runnable — returned as a STRING to
183
+ * SHOW the user, NEVER executed. The owner's call (2026-08-17): guide, never execute —
184
+ * auto-installing runs arbitrary postinstall code, which is exactly the posture
185
+ * a trust tool must not take (the same reason detectTestCommand keeps
186
+ * `--no-install`). Mirrors detectTestCommand's marker-probing: pick the
187
+ * ecosystem, name its install. Returns undefined when there's nothing to suggest.
188
+ */
189
+ export function detectRemedy(cwd) {
190
+ const has = (f) => existsSync(join(cwd, f));
191
+ // Node — deps missing / runner not installed → install from the lockfile.
192
+ if (has('package.json')) {
193
+ if (has('pnpm-lock.yaml'))
194
+ return 'pnpm install';
195
+ if (has('yarn.lock'))
196
+ return 'yarn install';
197
+ if (has('package-lock.json'))
198
+ return 'npm ci';
199
+ return 'npm install';
200
+ }
201
+ // Python — set up a venv and install, then re-check.
202
+ if (has('requirements.txt')) {
203
+ return 'python -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt';
204
+ }
205
+ if (has('pyproject.toml') || has('setup.py') || has('setup.cfg')) {
206
+ return 'python -m venv .venv && . .venv/bin/activate && pip install -e .';
207
+ }
208
+ if (has('pytest.ini') || has('conftest.py'))
209
+ return 'pip install pytest';
210
+ if (has('go.mod'))
211
+ return 'go mod download';
212
+ if (has('Cargo.toml'))
213
+ return 'cargo fetch';
214
+ return undefined;
215
+ }
216
+ /**
217
+ * Re-run the project's tests on the real on-disk state. Async + timeout-bounded.
218
+ * Resolves with `ran:false` + a reason when there's nothing to run or it's
219
+ * disabled — never throws.
220
+ */
221
+ export function rerunTests(cwd) {
222
+ if (process.env.CODEHERE_RERUN_TESTS === '0') {
223
+ return Promise.resolve({ ran: false, kind: 'disabled', skippedReason: 'disabled (CODEHERE_RERUN_TESTS=0)' });
224
+ }
225
+ const detected = detectTestCommand(cwd);
226
+ if (!detected) {
227
+ return Promise.resolve({ ran: false, kind: 'no-command', skippedReason: 'no known test command in this project' });
228
+ }
229
+ return new Promise((resolve) => {
230
+ const start = Date.now();
231
+ let out = '';
232
+ let settled = false;
233
+ const done = (r) => {
234
+ if (settled)
235
+ return;
236
+ settled = true;
237
+ resolve(r);
238
+ };
239
+ let child;
240
+ try {
241
+ child = spawn(detected.cmd, detected.args, {
242
+ cwd,
243
+ env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
244
+ stdio: ['ignore', 'pipe', 'pipe'],
245
+ // Own process group so a timeout can kill the whole TREE — `npm test`
246
+ // forks the real runner, and killing just the npm wrapper orphaned the
247
+ // grandchild, which kept running past the timeout (self-audit 2026-06-09).
248
+ detached: true,
249
+ });
250
+ }
251
+ catch (e) {
252
+ done({
253
+ ran: false,
254
+ kind: 'command-not-found',
255
+ command: detected.display,
256
+ skippedReason: `could not spawn test command: ${String(e).slice(0, 80)}`,
257
+ remedy: detectRemedy(cwd),
258
+ });
259
+ return;
260
+ }
261
+ const timer = setTimeout(() => {
262
+ try {
263
+ // Negative pid = kill the whole process group (the detached tree), so the
264
+ // forked test runner dies too, not just the npm wrapper.
265
+ if (typeof child.pid === 'number')
266
+ process.kill(-child.pid, 'SIGKILL');
267
+ else
268
+ child.kill('SIGKILL');
269
+ }
270
+ catch {
271
+ try {
272
+ child.kill('SIGKILL');
273
+ }
274
+ catch {
275
+ /* best-effort */
276
+ }
277
+ }
278
+ done({
279
+ ran: false,
280
+ kind: 'timed-out',
281
+ command: detected.display,
282
+ skippedReason: `test re-run timed out after ${seconds(DEFAULT_TIMEOUT_MS)}`,
283
+ });
284
+ }, DEFAULT_TIMEOUT_MS);
285
+ const cap = (chunk) => {
286
+ out += String(chunk);
287
+ // bound memory — keep only the tail the parser reads
288
+ if (out.length > 200_000)
289
+ out = out.slice(-100_000);
290
+ };
291
+ child.stdout?.on('data', cap);
292
+ child.stderr?.on('data', cap);
293
+ child.on('error', (e) => {
294
+ clearTimeout(timer);
295
+ done({
296
+ ran: false,
297
+ kind: 'command-not-found',
298
+ command: detected.display,
299
+ skippedReason: `test command errored: ${String(e.message).slice(0, 80)}`,
300
+ remedy: detectRemedy(cwd),
301
+ });
302
+ });
303
+ child.on('close', (code) => {
304
+ clearTimeout(timer);
305
+ const ec = code ?? -1;
306
+ const parsed = parseTestSummary(out);
307
+ // 126/127 = the wrapper launched but the runner/deps aren't installed —
308
+ // nothing was tested, so this is NOT a failing suite. Classify it as
309
+ // runner-missing (with the one-command remedy) so no surface renders a
310
+ // red measured-fail for an unrunnable suite.
311
+ const allSkipped = ec === 0 && reportedOnlySkipped(out);
312
+ const noTests = ec === 0 && (reportedZeroTests(out) || allSkipped);
313
+ const kind = ec === 126 || ec === 127 ? 'runner-missing' : ec === 0 && !noTests ? 'passed' : 'failed';
314
+ done({
315
+ ran: true,
316
+ kind,
317
+ command: detected.display,
318
+ exitCode: ec,
319
+ passed: parsed?.passed,
320
+ failed: parsed?.failed,
321
+ ...(noTests ? { noTests: true } : {}),
322
+ ...(allSkipped ? { allSkipped: true } : {}),
323
+ durationMs: Date.now() - start,
324
+ ...(kind === 'runner-missing' ? { remedy: detectRemedy(cwd) } : {}),
325
+ });
326
+ });
327
+ });
328
+ }
329
+ //# sourceMappingURL=rerun-tests.js.map