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
@@ -1,183 +1,7 @@
1
1
  /**
2
- * ArtifactsExtractor — PM UX Layer 3 (principles #2 "info density", #7 "agent-provenance").
3
- *
4
- * When a task finishes, a product manager reports: what changed, was anything
5
- * risky, did tests pass. We derive that from ground truth:
6
- * - git diff (working tree + untracked) vs the HEAD snapshot taken at spawn
7
- * - trust score per changed file via the existing SAST scorer (F0)
8
- * - test pass/fail counts parsed out of collected stdout (best-effort)
9
- *
10
- * Agent-agnostic on purpose. Both Claude and Codex mutate the filesystem;
11
- * git tells the truth regardless of which tool did the write.
12
- *
13
- * v0.6.x: dirty-baseline subtraction. Without it, files the user had
14
- * uncommitted at spawn time would falsely appear in artifacts even though
15
- * the agent never touched them — git diff vs spawn-HEAD includes EVERYTHING
16
- * dirty in the working tree, not just what changed during the session.
17
- * Now snapshotCwd captures `git status --porcelain` + per-file mtime/size at
18
- * spawn time. extractArtifacts filters out files whose stat is unchanged
19
- * since spawn — they were dirty before, not by this session. Files that were
20
- * dirty at spawn AND further modified during the session still appear.
21
- *
22
- * Limitation: cannot distinguish concurrent human writes from agent writes
23
- * during the session window. True PID-level attribution would require
24
- * fanotify/audit subsystem. The dirty-baseline filter eliminates the most
25
- * common false-positive (pre-existing uncommitted work) without that.
2
+ * Test summary parsing: "N passed", "M failed" and the runner-specific variants, out of collected stdout.
3
+ * Used by `codehere check` to read what a test run reported.
26
4
  */
27
- import { execFileSync } from 'child_process';
28
- import { existsSync, readFileSync, statSync } from 'fs';
29
- import { join, isAbsolute } from 'path';
30
- import { scoreFile } from '../security/trust-scorer.js';
31
- /** Capture mtime + size for a single path. Returns null if stat fails. */
32
- function statBaseline(fullPath) {
33
- try {
34
- const st = statSync(fullPath);
35
- return { mtimeMs: st.mtimeMs, size: st.size };
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
41
- /** Read `git status --porcelain` and stat every dirty path. Best-effort.
42
- * Paths are git-relative (i.e. relative to repoRoot, not cwd). */
43
- function captureDirtyBaseline(cwd, repoRoot) {
44
- const baseline = new Map();
45
- try {
46
- const out = execFileSync('git', ['status', '--porcelain', '-z'], {
47
- cwd,
48
- encoding: 'utf-8',
49
- timeout: 5000,
50
- stdio: ['ignore', 'pipe', 'ignore'],
51
- });
52
- // Porcelain -z separates entries by NUL. First two chars are status, then space, then path.
53
- // Renames have an extra NUL-separated old path which we don't need here.
54
- for (const entry of out.split('\0')) {
55
- if (!entry)
56
- continue;
57
- // Strip the 2-char status prefix + 1 separator (3 chars total)
58
- const path = entry.slice(3);
59
- if (!path)
60
- continue;
61
- const full = isAbsolute(path) ? path : join(repoRoot, path);
62
- const b = statBaseline(full);
63
- if (b)
64
- baseline.set(path, b);
65
- }
66
- }
67
- catch {
68
- /* best-effort — empty baseline means no filtering, status-quo behavior */
69
- }
70
- return baseline;
71
- }
72
- /** Get repo root via `git rev-parse --show-toplevel`. Returns null if not in a repo. */
73
- function getRepoRoot(cwd) {
74
- try {
75
- const out = execFileSync('git', ['rev-parse', '--show-toplevel'], {
76
- cwd,
77
- encoding: 'utf-8',
78
- timeout: 2000,
79
- stdio: ['ignore', 'pipe', 'ignore'],
80
- }).trim();
81
- return out.length > 0 ? out : null;
82
- }
83
- catch {
84
- return null;
85
- }
86
- }
87
- /** Take a baseline so Layer 3 can diff against it on exit. Cheap; ~5ms. */
88
- export function snapshotCwd(cwd) {
89
- const spawnedAtMs = Date.now();
90
- try {
91
- const out = execFileSync('git', ['rev-parse', 'HEAD'], {
92
- cwd,
93
- encoding: 'utf-8',
94
- timeout: 2000,
95
- stdio: ['ignore', 'pipe', 'ignore'],
96
- }).trim();
97
- const isGitRepo = out.length > 0;
98
- const repoRoot = isGitRepo ? (getRepoRoot(cwd) ?? cwd) : cwd;
99
- return {
100
- cwd,
101
- head: out,
102
- isGitRepo,
103
- spawnedAtMs,
104
- repoRoot,
105
- dirtyBaseline: isGitRepo ? captureDirtyBaseline(cwd, repoRoot) : new Map(),
106
- };
107
- }
108
- catch {
109
- return {
110
- cwd,
111
- head: null,
112
- isGitRepo: false,
113
- spawnedAtMs,
114
- repoRoot: cwd,
115
- dirtyBaseline: new Map(),
116
- };
117
- }
118
- }
119
- /** Collect {path, +, -} from git diff (numstat) + untracked files (wc -l). */
120
- function changedFiles(snap) {
121
- if (!snap.isGitRepo)
122
- return [];
123
- const out = [];
124
- try {
125
- // Staged + unstaged vs HEAD at spawn
126
- const diff = execFileSync('git', ['diff', '--numstat', snap.head ?? 'HEAD'], {
127
- cwd: snap.cwd,
128
- encoding: 'utf-8',
129
- timeout: 5000,
130
- stdio: ['ignore', 'pipe', 'ignore'],
131
- });
132
- for (const line of diff.split('\n')) {
133
- const m = line.match(/^(-|\d+)\t(-|\d+)\t(.+)$/);
134
- if (!m)
135
- continue;
136
- const [, aStr, rStr, path] = m;
137
- out.push({
138
- path,
139
- added: aStr === '-' ? 0 : parseInt(aStr, 10) || 0,
140
- removed: rStr === '-' ? 0 : parseInt(rStr, 10) || 0,
141
- });
142
- }
143
- }
144
- catch {
145
- /* diff failed — skip */
146
- }
147
- try {
148
- // Untracked files — not in diff output
149
- const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
150
- cwd: snap.cwd,
151
- encoding: 'utf-8',
152
- timeout: 5000,
153
- stdio: ['ignore', 'pipe', 'ignore'],
154
- });
155
- for (const rel of untracked.split('\n')) {
156
- if (!rel.trim())
157
- continue;
158
- // Count lines in the new file. Untracked paths come from git relative
159
- // to repoRoot — must join with repoRoot, not cwd, when cwd is a subdir.
160
- try {
161
- const full = isAbsolute(rel) ? rel : join(snap.repoRoot, rel);
162
- if (!existsSync(full))
163
- continue;
164
- const size = statSync(full).size;
165
- if (size > 2 * 1024 * 1024)
166
- continue; // skip files >2MB, don't need their trust score
167
- const content = readFileSync(full, 'utf-8');
168
- const lines = content ? content.split('\n').length : 0;
169
- out.push({ path: rel, added: lines, removed: 0 });
170
- }
171
- catch {
172
- /* skip */
173
- }
174
- }
175
- }
176
- catch {
177
- /* ls-files failed — skip */
178
- }
179
- return out;
180
- }
181
5
  /** Parse "N passed", "M failed", "K tests" out of accumulated stdout. Best-effort.
182
6
  * Vitest + jest output both have TWO pass/fail lines ("Test Files 11 passed" then
183
7
  * "Tests 47 passed"). We want the second/last. Take the last match in the buffer,
@@ -239,91 +63,4 @@ export function parseTestSummary(stdout) {
239
63
  result.durationMs = Math.round(parseFloat(dur[1]) * 1000);
240
64
  return result;
241
65
  }
242
- /** Score each changed file with the existing SAST trust scorer. Safe on binaries (skipped). */
243
- function annotateTrust(snap, files) {
244
- return files.map((f) => {
245
- try {
246
- // Git-relative path: join with repoRoot, not cwd (matters when cwd is
247
- // a subdirectory of the repo).
248
- const full = isAbsolute(f.path) ? f.path : join(snap.repoRoot, f.path);
249
- if (!existsSync(full))
250
- return f;
251
- const size = statSync(full).size;
252
- if (size > 2 * 1024 * 1024)
253
- return f;
254
- // Only score text-like files — trust scorer regexes operate on UTF-8
255
- if (!/\.(ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|kt|cs|php|sh|bash|zsh|yml|yaml|json|md|sql)$/i.test(f.path))
256
- return f;
257
- const content = readFileSync(full, 'utf-8');
258
- const scored = scoreFile(f.path, content);
259
- return { ...f, trust: scored.score };
260
- }
261
- catch {
262
- return f;
263
- }
264
- });
265
- }
266
- /**
267
- * Keep only files whose mtime is at or after spawn time — the signal that this
268
- * session touched them. Prevents Session B in the same cwd from reporting files
269
- * Session A already edited before B started. Not bulletproof (both sessions
270
- * hitting the same file both report it) but fixes the common concurrent-sessions
271
- * pollution observed in beta.3.
272
- */
273
- function filterByMtime(snap, files) {
274
- const grace = 1000; // ms; allow clock skew between git's touch and our Date.now()
275
- return files.filter((f) => {
276
- try {
277
- // Git-relative path: join with repoRoot, not cwd.
278
- const full = isAbsolute(f.path) ? f.path : join(snap.repoRoot, f.path);
279
- if (!existsSync(full))
280
- return true; // file was deleted — keep it
281
- const { mtimeMs } = statSync(full);
282
- return mtimeMs >= snap.spawnedAtMs - grace;
283
- }
284
- catch {
285
- return true; // stat failed — be permissive
286
- }
287
- });
288
- }
289
- /**
290
- * Drop files that were dirty at spawn time and have NOT been further modified
291
- * since. The git-diff-vs-spawn-HEAD path picks up the user's pre-existing
292
- * uncommitted changes; without this filter, those files get mis-attributed to
293
- * the agent's task. A file is considered "further modified" if its mtime OR
294
- * size changed since the spawn snapshot — content-level tampering with same
295
- * mtime/size is rare enough to ignore.
296
- */
297
- function filterBySpawnBaseline(snap, files) {
298
- if (snap.dirtyBaseline.size === 0)
299
- return files;
300
- return files.filter((f) => {
301
- const baseline = snap.dirtyBaseline.get(f.path);
302
- if (!baseline)
303
- return true; // wasn't dirty at spawn — definitely a session change
304
- try {
305
- // Git-relative path: join with repoRoot, not cwd.
306
- const full = isAbsolute(f.path) ? f.path : join(snap.repoRoot, f.path);
307
- if (!existsSync(full))
308
- return true; // deleted during session — that's a change
309
- const { mtimeMs, size } = statSync(full);
310
- // Further-modified if either mtime advanced or size differs from baseline.
311
- return mtimeMs > baseline.mtimeMs || size !== baseline.size;
312
- }
313
- catch {
314
- return true; // stat failed — be permissive (don't drop files we can't verify)
315
- }
316
- });
317
- }
318
- export function extractArtifacts(snap, collectedStdout) {
319
- const raw = changedFiles(snap);
320
- const sessionScoped = filterByMtime(snap, raw);
321
- const agentLikely = filterBySpawnBaseline(snap, sessionScoped);
322
- const files = annotateTrust(snap, agentLikely);
323
- const tests = parseTestSummary(collectedStdout);
324
- // Non-git cwd: files is empty because we can't diff, not because nothing
325
- // changed. Flag it so the UI + Final Report say "can't track here" instead
326
- // of silently implying "no files changed".
327
- return { files, tests, ...(snap.isGitRepo ? {} : { noGitTracking: true }) };
328
- }
329
66
  //# sourceMappingURL=artifacts-extractor.js.map
@@ -0,0 +1,251 @@
1
+ /**
2
+ * CLI: check — the simplest proof. codehere independently RE-RUNS your project's
3
+ * tests and reports the real result, so you trust what the AI actually did, not
4
+ * what it claimed. Zero setup, no daemon, no API. Exit codes for CI: 0 a verified pass, 1 a finding (tests
5
+ * failed, no test ran, a test weakened, a claimed test file missing), 2 nothing could be verified.
6
+ */
7
+ import { execFileSync } from 'node:child_process';
8
+ import { statSync } from 'node:fs';
9
+ import { resolve } from 'node:path';
10
+ import { colors, symbols } from '../../../ui.js';
11
+ import { rerunTests } from '../../../application/use-cases/rerun-tests.js';
12
+ import { scanTestWeakening, } from '../../../application/use-cases/detect-test-weakening.js';
13
+ import { findMissingClaimedTests } from '../../../application/use-cases/check-claimed-tests.js';
14
+ const WHY_TEXT = {
15
+ 'trivial-assertion': 'assertion that checks nothing',
16
+ skipped: 'test skipped',
17
+ 'commented-out': 'assertion commented out',
18
+ 'unreachable-assertion': 'assertion after an early return',
19
+ };
20
+ // File names, diff lines and branch names come from whoever wrote the change, so they are treated as untrusted:
21
+ // control characters (terminal escapes among them) are replaced, and in the markdown receipt the text sits inside
22
+ // an inline code span, where no link, @mention or emphasis renders.
23
+ // Control characters, plus direction overrides, zero-width, soft-hyphen, line-separator and tag characters that can
24
+ // disguise a file name.
25
+ const plain = (s) => s.replace(/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff\u{e0000}-\u{e007f}]|\u034f/gu, '?');
26
+ const code = (s) => '`' + plain(s).replace(/`/g, "'") + '`';
27
+ /** Characters a branch name usually has; anything else is not repeated in the reproduce command. */
28
+ const SAFE_REF = /^[A-Za-z0-9._/@+-]+$/;
29
+ const weakenedLines = (w) => w.map((f) => `${plain(f.file)}:${f.line} · ${WHY_TEXT[f.why]} · ${plain(f.text)}`);
30
+ const weakenedMarkdown = (w) => w.map((f) => `${code(f.file)}:${f.line} · ${WHY_TEXT[f.why]} · ${code(f.text)}`);
31
+ export const RECEIPT_MARKER = '<!-- codehere-receipt -->';
32
+ /**
33
+ * One word per result, for CI. Green (`pass`) only when codehere re-ran the suite, it exited 0, no test
34
+ * was weakened, no named test file was missing and the weakened-test scan finished. Tampering is `fail` even if
35
+ * no suite could run; every other case where nothing was verified is `unverified`, never a pass.
36
+ */
37
+ export function checkVerdict(r, weakened, missingClaimed, scanError) {
38
+ if (weakened.length || missingClaimed.length)
39
+ return 'fail';
40
+ if (r.kind === 'disabled')
41
+ return 'skipped';
42
+ if (r.kind === 'failed')
43
+ return 'fail';
44
+ if (scanError)
45
+ return 'unverified';
46
+ if (r.kind === 'passed' && r.ran && (r.exitCode ?? 1) === 0)
47
+ return 'pass';
48
+ return 'unverified';
49
+ }
50
+ /** The check command that gives the same verdict as this run: the options that changed it are repeated. */
51
+ function reproduceCommand({ base, claim }) {
52
+ let cmd = 'codehere check';
53
+ if (base)
54
+ cmd += SAFE_REF.test(base) ? ` --base ${base}` : ' --base <the same base>';
55
+ if (claim) {
56
+ const inlineable = claim.length <= 160 && !/[`'\n\r]/.test(claim);
57
+ cmd += inlineable ? ` --claim '${plain(claim)}'` : ' --claim <the same summary>';
58
+ }
59
+ return cmd;
60
+ }
61
+ /** Short commit SHA of `cwd`'s repo, or null when it isn't a git checkout. */
62
+ function gitShortSha(cwd) {
63
+ try {
64
+ return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
65
+ .toString()
66
+ .trim();
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * A paste-ready "verified outcome receipt": the one thing a reviewer can trust
74
+ * without re-running anything — codehere independently re-ran the suite, here's the
75
+ * observed result, pinned to a commit + timestamp, WITH the exact command to
76
+ * reproduce it independently. Plain markdown so it drops into a PR description or
77
+ * review comment as-is.
78
+ *
79
+ * Why reproducibility is the trust mechanism (not a digest): a self-computed hash
80
+ * over self-reported facts proves nothing — a sender could edit the facts AND the
81
+ * hash. What a third party can actually trust is REPRODUCING it: check out the
82
+ * pinned commit, run `codehere check`, get the same exit code. So the receipt leads
83
+ * with the reproduce command and is honest about SCOPE (a test re-run, not a
84
+ * security/logic audit) — it never says more than it verified.
85
+ *
86
+ * ponytail: this is the standalone (no-daemon) receipt. Upgrade path to true
87
+ * tamper-EVIDENCE (authenticity, not just reproducibility): when a ~/.codehere audit
88
+ * chain covers this commit, fold in its verified hash (audit-chain.ts) so the receipt
89
+ * also carries a digest a third party can check against an append-only log.
90
+ */
91
+ function buildProofBlock(cwd, r, weakened = [], missingClaimed = [], repro = {}, scan = { findings: [] }) {
92
+ const sha = gitShortSha(cwd);
93
+ const when = new Date().toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
94
+ const at = sha ? `commit \`${sha}\`` : 'the working tree';
95
+ const weakenedNote = weakened.length
96
+ ? `> Tests changed in this diff, so a pass may check nothing: ${weakenedMarkdown(weakened).join(' · ')}\n`
97
+ : '';
98
+ const missingNote = missingClaimed.length
99
+ ? `> The AI named test files that are not in the repo: ${missingClaimed.map(code).join(' · ')}\n`
100
+ : '';
101
+ const scanNote = scan.error
102
+ ? `> Weakened-test scan could not run: ${scan.error}\n`
103
+ : scan.skipped
104
+ ? `> Weakened-test scan skipped: ${scan.skipped}\n`
105
+ : '';
106
+ // A finding is reported even when the suite could not run.
107
+ const notes = weakenedNote + missingNote + scanNote;
108
+ if (!r.ran) {
109
+ if (r.kind === 'command-not-found') {
110
+ const fix = r.remedy ? ` · fix: \`${r.remedy}\`` : '';
111
+ return `> **codehere · verified outcome receipt** — couldn't run ${code(r.command || 'the tests')} (runner/deps not installed) · unverified${fix} · ${when}\n${notes}`;
112
+ }
113
+ return `> **codehere · verified outcome receipt** — no suite re-run (${code(r.skippedReason || 'none detected')}) · nothing verified · ${when}\n${notes}`;
114
+ }
115
+ if (r.kind === 'runner-missing') {
116
+ const fix = r.remedy ? ` · fix: \`${r.remedy}\`` : '';
117
+ return `> **codehere · verified outcome receipt** — couldn't run ${code(r.command ?? '')} on ${at} (runner not installed, exit ${r.exitCode}) · unverified${fix} · ${when}\n${notes}`;
118
+ }
119
+ const passed = (r.exitCode ?? 1) === 0 && !r.noTests;
120
+ const counts = typeof r.passed === 'number' || typeof r.failed === 'number'
121
+ ? ` (${r.passed ?? '?'} passed · ${r.failed ?? 0} failed)`
122
+ : '';
123
+ const verdict = r.noTests
124
+ ? '✕ no tests ran'
125
+ : !passed
126
+ ? '✕ tests FAIL'
127
+ : weakened.length
128
+ ? `✕ tests pass · ${weakened.length} weakened`
129
+ : missingClaimed.length
130
+ ? `✕ tests pass · ${missingClaimed.length} claimed test file not found`
131
+ : scan.error
132
+ ? '✕ tests pass · weakened-test scan could not run'
133
+ : '✓ tests pass';
134
+ // Reproduce line: the real, verifiable trust — anyone can re-run and confirm.
135
+ const reproduce = sha ? `> Reproduce independently: \`git checkout ${sha} && ${reproduceCommand(repro)}\`\n` : '';
136
+ return (`> **codehere · verified outcome receipt** · ${verdict}${counts}\n` +
137
+ `> codehere independently re-ran ${code(r.command ?? '')} on ${at} — exit ${r.exitCode}. Observed, not the AI's claim. · ${when}\n` +
138
+ notes +
139
+ reproduce +
140
+ `> Scope: the project's test suite — not a security or logic audit.\n`);
141
+ }
142
+ export async function executeCheckCommand(target, options = {}) {
143
+ const cwd = resolve(target || '.');
144
+ let isFolder = false;
145
+ try {
146
+ isFolder = statSync(cwd).isDirectory();
147
+ }
148
+ catch {
149
+ /* missing path: handled below */
150
+ }
151
+ if (!isFolder)
152
+ throw new Error(`No such folder: ${target}`);
153
+ const r = await rerunTests(cwd);
154
+ const scan = scanTestWeakening(cwd, options.base);
155
+ const weakened = scan.findings;
156
+ const missingClaimed = options.claim ? findMissingClaimedTests(cwd, options.claim) : [];
157
+ const verdict = checkVerdict(r, weakened, missingClaimed, scan.error);
158
+ const weakeningScan = scan.error ? `could not run: ${scan.error}` : scan.skipped ? `skipped: ${scan.skipped}` : 'ran';
159
+ const exitCode = verdict === 'pass' ? 0 : verdict === 'fail' ? 1 : 2;
160
+ const weakenedText = () => weakened.length
161
+ ? weakenedLines(weakened)
162
+ .map((l) => colors.dim(` ${l}\n`))
163
+ .join('') + colors.dim(` › ${options.base ? `git diff ${plain(options.base)}...HEAD` : 'git diff HEAD'}\n`)
164
+ : '';
165
+ const scanText = () => scan.error
166
+ ? colors.dim(` weakened-test scan could not run: ${scan.error}\n`)
167
+ : scan.skipped
168
+ ? colors.dim(` weakened-test scan skipped: ${scan.skipped}\n`)
169
+ : '';
170
+ const missingLines = () => missingClaimed.length
171
+ ? missingClaimed.map((f) => colors.dim(` ${plain(f)} · named by the AI, not in the repo\n`)).join('')
172
+ : '';
173
+ if (options.json) {
174
+ const receipt = RECEIPT_MARKER +
175
+ '\n' +
176
+ buildProofBlock(cwd, r, weakened, missingClaimed, { base: options.base, claim: options.claim }, scan);
177
+ process.stdout.write(JSON.stringify({ cwd, ...r, weakened, missingClaimed, weakeningScan, verdict, receipt }, null, 2) + '\n');
178
+ return exitCode;
179
+ }
180
+ if (options.proof) {
181
+ process.stdout.write(buildProofBlock(cwd, r, weakened, missingClaimed, { base: options.base, claim: options.claim }, scan));
182
+ return exitCode;
183
+ }
184
+ if (!r.ran) {
185
+ // The runner/deps aren't installed (e.g. `spawn pytest ENOENT`) — that's a
186
+ // fixable environment gap, not "no tests". Name the one command that fixes it.
187
+ if (r.kind === 'command-not-found') {
188
+ process.stdout.write('\n' +
189
+ colors.error(`${symbols.error} Couldn't run the tests`) +
190
+ colors.dim(` — the runner for '${r.command || 'your tests'}' isn't installed here.\n`) +
191
+ (r.remedy
192
+ ? colors.dim(' Fix it, then re-check: ') + colors.success(`${r.remedy}\n\n`)
193
+ : colors.dim(' Install the test runner, then run check again.\n\n')));
194
+ process.stdout.write(weakenedText() + missingLines() + scanText());
195
+ return exitCode; // couldn't verify (2); a weakened test or a missing named test file still counts (1)
196
+ }
197
+ if (r.kind === 'timed-out' || r.kind === 'disabled') {
198
+ process.stdout.write(colors.error(`${symbols.error} Nothing verified`) +
199
+ colors.dim(` — ${r.kind === 'timed-out' ? `'${r.command}' ${r.skippedReason?.replace(/^test re-run /, '')}` : r.skippedReason}.\n`));
200
+ process.stdout.write(weakenedText() + missingLines() + scanText());
201
+ return exitCode;
202
+ }
203
+ process.stdout.write(colors.dim(`No test suite codehere can run here — ${r.skippedReason || 'none detected'}.\n`) +
204
+ colors.dim('codehere proves the AI by re-running your tests; add one (npm test / node --test / pytest / go test / cargo test) and run check again.\n'));
205
+ process.stdout.write(weakenedText() + missingLines() + scanText());
206
+ return exitCode; // nothing was verified (2); a weakened test or a missing named test file still counts (1)
207
+ }
208
+ // Honesty: runner-missing (exit 126/127) means the test command couldn't be RUN
209
+ // (deps not installed), not that the tests failed. Never report "FAIL" for that —
210
+ // name the one command that makes it runnable.
211
+ if (r.kind === 'runner-missing') {
212
+ process.stdout.write('\n' +
213
+ colors.error(`${symbols.error} Couldn't run the tests`) +
214
+ colors.dim(` — '${r.command}' exited ${r.exitCode}: the deps/runner aren't installed here.\n`) +
215
+ (r.remedy
216
+ ? colors.dim(' Fix it, then re-check: ') + colors.success(`${r.remedy}\n\n`)
217
+ : colors.dim(' Install deps, then run check again.\n\n')));
218
+ process.stdout.write(weakenedText() + missingLines() + scanText());
219
+ return exitCode; // non-zero for CI (unverified), but NOT a false "tests failed"
220
+ }
221
+ const passed = (r.exitCode ?? 1) === 0 && !r.noTests;
222
+ const counts = typeof r.passed === 'number' || typeof r.failed === 'number'
223
+ ? ` ${r.passed ?? '?'} passed · ${r.failed ?? 0} failed`
224
+ : '';
225
+ const headline = r.noTests
226
+ ? colors.error(`${symbols.error} No tests ran`)
227
+ : !passed
228
+ ? colors.error(`${symbols.error} Tests FAIL`)
229
+ : weakened.length
230
+ ? colors.error(`${symbols.error} Tests pass · ${weakened.length} weakened in this change`)
231
+ : missingClaimed.length
232
+ ? colors.error(`${symbols.error} Tests pass · ${missingClaimed.length} claimed test file not found`)
233
+ : scan.error
234
+ ? colors.error(`${symbols.error} Tests pass · weakened-test scan could not run`)
235
+ : colors.success(`${symbols.success} Tests pass`);
236
+ process.stdout.write('\n' +
237
+ headline +
238
+ colors.dim(` — codehere re-ran them itself (${r.command}, exit ${r.exitCode})${counts}\n`) +
239
+ colors.dim("This is observed reality, not the agent's claim.\n") +
240
+ (r.noTests
241
+ ? colors.dim(r.allSkipped
242
+ ? ' The runner says every test was skipped, so nothing was tested.\n'
243
+ : ' The runner reported 0 tests, so nothing was tested. Check the test file names.\n')
244
+ : '') +
245
+ weakenedText() +
246
+ missingLines() +
247
+ scanText() +
248
+ '\n');
249
+ return exitCode;
250
+ }
251
+ //# sourceMappingURL=check-command.js.map