codehere 1.0.1 → 1.0.3

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 (163) hide show
  1. package/README.md +35 -193
  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 +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,199 +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
- // -uall: porcelain normally collapses an untracked DIRECTORY into one
47
- // "?? dir/" entry, while changedFiles() lists its contents per-file via
48
- // ls-files --others — so the baseline could never match those paths and a
49
- // whole pre-existing untracked dir got attributed to the agent (2026-06-07
50
- // v1 walk: memory-init's .codehere/ scaffolding showed as 8 agent edits).
51
- const out = execFileSync('git', ['status', '--porcelain', '-uall', '-z'], {
52
- cwd,
53
- encoding: 'utf-8',
54
- timeout: 5000,
55
- stdio: ['ignore', 'pipe', 'ignore'],
56
- });
57
- // Porcelain -z separates entries by NUL. First two chars are status, then space, then path.
58
- // Renames have an extra NUL-separated old path which we don't need here.
59
- for (const entry of out.split('\0')) {
60
- if (!entry)
61
- continue;
62
- // Strip the 2-char status prefix + 1 separator (3 chars total)
63
- const path = entry.slice(3);
64
- if (!path)
65
- continue;
66
- const full = isAbsolute(path) ? path : join(repoRoot, path);
67
- const b = statBaseline(full);
68
- if (b)
69
- baseline.set(path, b);
70
- }
71
- }
72
- catch {
73
- /* best-effort — empty baseline means no filtering, status-quo behavior */
74
- }
75
- return baseline;
76
- }
77
- /** Get repo root via `git rev-parse --show-toplevel`. Returns null if not in a repo. */
78
- function getRepoRoot(cwd) {
79
- try {
80
- const out = execFileSync('git', ['rev-parse', '--show-toplevel'], {
81
- cwd,
82
- encoding: 'utf-8',
83
- timeout: 2000,
84
- stdio: ['ignore', 'pipe', 'ignore'],
85
- }).trim();
86
- return out.length > 0 ? out : null;
87
- }
88
- catch {
89
- return null;
90
- }
91
- }
92
- /** Take a baseline so Layer 3 can diff against it on exit. Cheap; ~5ms. */
93
- export function snapshotCwd(cwd) {
94
- const spawnedAtMs = Date.now();
95
- try {
96
- const out = execFileSync('git', ['rev-parse', 'HEAD'], {
97
- cwd,
98
- encoding: 'utf-8',
99
- timeout: 2000,
100
- stdio: ['ignore', 'pipe', 'ignore'],
101
- }).trim();
102
- const isGitRepo = out.length > 0;
103
- const repoRoot = isGitRepo ? (getRepoRoot(cwd) ?? cwd) : cwd;
104
- return {
105
- cwd,
106
- head: out,
107
- isGitRepo,
108
- spawnedAtMs,
109
- repoRoot,
110
- dirtyBaseline: isGitRepo ? captureDirtyBaseline(cwd, repoRoot) : new Map(),
111
- };
112
- }
113
- catch {
114
- return {
115
- cwd,
116
- head: null,
117
- isGitRepo: false,
118
- spawnedAtMs,
119
- repoRoot: cwd,
120
- dirtyBaseline: new Map(),
121
- };
122
- }
123
- }
124
- /** Collect {path, +, -} from git diff (numstat) + untracked files (wc -l). */
125
- // codehere's own per-project state dir. Its contents (memory, protocols,
126
- // skills, context packs) are product plumbing, not the user's code — they
127
- // never belong in the room's "what the agent changed" list, even when the
128
- // memory write-back touches them mid-session.
129
- function isInternalPath(path) {
130
- return path === '.codehere' || path.startsWith('.codehere/') || path.includes('/.codehere/');
131
- }
132
- function changedFiles(snap) {
133
- if (!snap.isGitRepo)
134
- return [];
135
- const out = [];
136
- try {
137
- // Staged + unstaged vs HEAD at spawn
138
- const diff = execFileSync('git', ['diff', '--numstat', snap.head ?? 'HEAD'], {
139
- cwd: snap.cwd,
140
- encoding: 'utf-8',
141
- timeout: 5000,
142
- stdio: ['ignore', 'pipe', 'ignore'],
143
- });
144
- for (const line of diff.split('\n')) {
145
- const m = line.match(/^(-|\d+)\t(-|\d+)\t(.+)$/);
146
- if (!m)
147
- continue;
148
- const [, aStr, rStr, path] = m;
149
- if (isInternalPath(path))
150
- continue;
151
- out.push({
152
- path,
153
- added: aStr === '-' ? 0 : parseInt(aStr, 10) || 0,
154
- removed: rStr === '-' ? 0 : parseInt(rStr, 10) || 0,
155
- });
156
- }
157
- }
158
- catch {
159
- /* diff failed — skip */
160
- }
161
- try {
162
- // Untracked files — not in diff output
163
- const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
164
- cwd: snap.cwd,
165
- encoding: 'utf-8',
166
- timeout: 5000,
167
- stdio: ['ignore', 'pipe', 'ignore'],
168
- });
169
- for (const rel of untracked.split('\n')) {
170
- if (!rel.trim())
171
- continue;
172
- if (isInternalPath(rel))
173
- continue;
174
- // Count lines in the new file. Untracked paths come from git relative
175
- // to repoRoot — must join with repoRoot, not cwd, when cwd is a subdir.
176
- try {
177
- const full = isAbsolute(rel) ? rel : join(snap.repoRoot, rel);
178
- if (!existsSync(full))
179
- continue;
180
- const size = statSync(full).size;
181
- if (size > 2 * 1024 * 1024)
182
- continue; // skip files >2MB, don't need their trust score
183
- const content = readFileSync(full, 'utf-8');
184
- const lines = content ? content.split('\n').length : 0;
185
- out.push({ path: rel, added: lines, removed: 0 });
186
- }
187
- catch {
188
- /* skip */
189
- }
190
- }
191
- }
192
- catch {
193
- /* ls-files failed — skip */
194
- }
195
- return out;
196
- }
197
5
  /** Parse "N passed", "M failed", "K tests" out of accumulated stdout. Best-effort.
198
6
  * Vitest + jest output both have TWO pass/fail lines ("Test Files 11 passed" then
199
7
  * "Tests 47 passed"). We want the second/last. Take the last match in the buffer,
@@ -255,133 +63,4 @@ export function parseTestSummary(stdout) {
255
63
  result.durationMs = Math.round(parseFloat(dur[1]) * 1000);
256
64
  return result;
257
65
  }
258
- /** Score each changed file with the existing SAST trust scorer. Safe on binaries (skipped). */
259
- function annotateTrust(snap, files) {
260
- return files.map((f) => {
261
- try {
262
- // Git-relative path: join with repoRoot, not cwd (matters when cwd is
263
- // a subdirectory of the repo).
264
- const full = isAbsolute(f.path) ? f.path : join(snap.repoRoot, f.path);
265
- if (!existsSync(full))
266
- return f;
267
- const size = statSync(full).size;
268
- if (size > 2 * 1024 * 1024)
269
- return f;
270
- // Only score text-like files — trust scorer regexes operate on UTF-8
271
- 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))
272
- return f;
273
- const content = readFileSync(full, 'utf-8');
274
- const scored = scoreFile(f.path, content);
275
- return { ...f, trust: scored.score };
276
- }
277
- catch {
278
- return f;
279
- }
280
- });
281
- }
282
- /**
283
- * Keep only files whose mtime is at or after spawn time — the signal that this
284
- * session touched them. Prevents Session B in the same cwd from reporting files
285
- * Session A already edited before B started. Not bulletproof (both sessions
286
- * hitting the same file both report it) but fixes the common concurrent-sessions
287
- * pollution observed in beta.3.
288
- */
289
- function filterByMtime(snap, files) {
290
- const grace = 1000; // ms; allow clock skew between git's touch and our Date.now()
291
- return files.filter((f) => {
292
- try {
293
- // Git-relative path: join with repoRoot, not cwd.
294
- const full = isAbsolute(f.path) ? f.path : join(snap.repoRoot, f.path);
295
- if (!existsSync(full))
296
- return true; // file was deleted — keep it
297
- const { mtimeMs } = statSync(full);
298
- return mtimeMs >= snap.spawnedAtMs - grace;
299
- }
300
- catch {
301
- return true; // stat failed — be permissive
302
- }
303
- });
304
- }
305
- /**
306
- * Drop files that were dirty at spawn time and have NOT been further modified
307
- * since. The git-diff-vs-spawn-HEAD path picks up the user's pre-existing
308
- * uncommitted changes; without this filter, those files get mis-attributed to
309
- * the agent's task. A file is considered "further modified" if its mtime OR
310
- * size changed since the spawn snapshot — content-level tampering with same
311
- * mtime/size is rare enough to ignore.
312
- */
313
- function filterBySpawnBaseline(snap, files) {
314
- if (snap.dirtyBaseline.size === 0)
315
- return files;
316
- return files.filter((f) => {
317
- const baseline = snap.dirtyBaseline.get(f.path);
318
- if (!baseline)
319
- return true; // wasn't dirty at spawn — definitely a session change
320
- try {
321
- // Git-relative path: join with repoRoot, not cwd.
322
- const full = isAbsolute(f.path) ? f.path : join(snap.repoRoot, f.path);
323
- if (!existsSync(full))
324
- return true; // deleted during session — that's a change
325
- const { mtimeMs, size } = statSync(full);
326
- // Further-modified if either mtime advanced or size differs from baseline.
327
- return mtimeMs > baseline.mtimeMs || size !== baseline.size;
328
- }
329
- catch {
330
- return true; // stat failed — be permissive (don't drop files we can't verify)
331
- }
332
- });
333
- }
334
- /** The spawned tool's OWN housekeeping — never the task's work product. Live
335
- * 2026-07-02: aider wrote .aider* + a .gitignore line on a run that did
336
- * nothing else, the audit confirmed those writes on disk, and a no-op run
337
- * graded proven/green off its tool's litter. Chat histories, tool caches,
338
- * and the .gitignore edits tools make to hide them don't count as work.
339
- * .gitignore is dropped ONLY when every added line is tool litter — a task
340
- * whose real job is editing .gitignore still counts. */
341
- const TOOL_HOUSEKEEPING = /(^|\/)\.(aider|claude|codehere|gemini|codex|opencode)[^/]*($|\/)/i;
342
- const LITTER_LINE = /^\s*($|#|\.?(aider|claude|codehere|gemini|codex|opencode))/i;
343
- function gitignoreOnlyLitter(snap, path) {
344
- try {
345
- const d = execFileSync('git', ['diff', snap.head ?? 'HEAD', '--', path], {
346
- cwd: snap.cwd,
347
- encoding: 'utf-8',
348
- timeout: 3000,
349
- stdio: ['ignore', 'pipe', 'ignore'],
350
- });
351
- let added = d
352
- .split('\n')
353
- .filter((l) => l.startsWith('+') && !l.startsWith('+++'))
354
- .map((l) => l.slice(1));
355
- if (added.length === 0) {
356
- // Untracked (brand-new) .gitignore: git diff HEAD shows nothing — the
357
- // whole file is the addition.
358
- added = readFileSync(join(snap.cwd, path), 'utf-8').split('\n');
359
- }
360
- const lines = added.filter((l) => l.trim().length > 0);
361
- return lines.length > 0 && lines.every((l) => LITTER_LINE.test(l));
362
- }
363
- catch {
364
- return false; // can't inspect — keep it counted (honest over-count beats hiding work)
365
- }
366
- }
367
- function dropToolHousekeeping(snap, files) {
368
- return files.filter((f) => {
369
- if (TOOL_HOUSEKEEPING.test(f.path))
370
- return false;
371
- if (/(^|\/)\.gitignore$/.test(f.path))
372
- return !gitignoreOnlyLitter(snap, f.path);
373
- return true;
374
- });
375
- }
376
- export function extractArtifacts(snap, collectedStdout) {
377
- const raw = dropToolHousekeeping(snap, changedFiles(snap));
378
- const sessionScoped = filterByMtime(snap, raw);
379
- const agentLikely = filterBySpawnBaseline(snap, sessionScoped);
380
- const files = annotateTrust(snap, agentLikely);
381
- const tests = parseTestSummary(collectedStdout);
382
- // Non-git cwd: files is empty because we can't diff, not because nothing
383
- // changed. Flag it so the UI + Final Report say "can't track here" instead
384
- // of silently implying "no files changed".
385
- return { files, tests, ...(snap.isGitRepo ? {} : { noGitTracking: true }) };
386
- }
387
66
  //# sourceMappingURL=artifacts-extractor.js.map
@@ -1,12 +1,63 @@
1
1
  /**
2
2
  * CLI: check — the simplest proof. codehere independently RE-RUNS your project's
3
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 1 on failure (CI-usable).
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.
5
6
  */
6
7
  import { execFileSync } from 'node:child_process';
8
+ import { statSync } from 'node:fs';
7
9
  import { resolve } from 'node:path';
8
10
  import { colors, symbols } from '../../../ui.js';
9
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
+ }
10
61
  /** Short commit SHA of `cwd`'s repo, or null when it isn't a git checkout. */
11
62
  function gitShortSha(cwd) {
12
63
  try {
@@ -37,60 +88,164 @@ function gitShortSha(cwd) {
37
88
  * chain covers this commit, fold in its verified hash (audit-chain.ts) so the receipt
38
89
  * also carries a digest a third party can check against an append-only log.
39
90
  */
40
- function buildProofBlock(cwd, r) {
91
+ function buildProofBlock(cwd, r, weakened = [], missingClaimed = [], repro = {}, scan = { findings: [] }) {
41
92
  const sha = gitShortSha(cwd);
42
93
  const when = new Date().toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
43
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;
44
108
  if (!r.ran) {
45
- return `> **codehere · verified outcome receipt** — no suite re-run (${r.skippedReason || 'none detected'}) · nothing verified · ${when}\n`;
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}`;
46
114
  }
47
- if (r.exitCode === 126 || r.exitCode === 127) {
48
- return `> **codehere · verified outcome receipt** — couldn't run \`${r.command}\` on ${at} (runner not installed, exit ${r.exitCode}) · unverified · ${when}\n`;
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}`;
49
118
  }
50
- const passed = (r.exitCode ?? 1) === 0;
119
+ const passed = (r.exitCode ?? 1) === 0 && !r.noTests;
51
120
  const counts = typeof r.passed === 'number' || typeof r.failed === 'number'
52
121
  ? ` (${r.passed ?? '?'} passed · ${r.failed ?? 0} failed)`
53
122
  : '';
54
- const verdict = passed ? '✓ tests pass' : '✕ tests FAIL';
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';
55
134
  // Reproduce line: the real, verifiable trust — anyone can re-run and confirm.
56
- const reproduce = sha ? `> Reproduce independently: \`git checkout ${sha} && codehere check\`\n` : '';
135
+ const reproduce = sha ? `> Reproduce independently: \`git checkout ${sha} && ${reproduceCommand(repro)}\`\n` : '';
57
136
  return (`> **codehere · verified outcome receipt** · ${verdict}${counts}\n` +
58
- `> codehere independently re-ran \`${r.command}\` on ${at} — exit ${r.exitCode}. Observed, not the AI's claim. · ${when}\n` +
137
+ `> codehere independently re-ran ${code(r.command ?? '')} on ${at} — exit ${r.exitCode}. Observed, not the AI's claim. · ${when}\n` +
138
+ notes +
59
139
  reproduce +
60
140
  `> Scope: the project's test suite — not a security or logic audit.\n`);
61
141
  }
62
142
  export async function executeCheckCommand(target, options = {}) {
63
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}`);
64
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
+ : '';
65
173
  if (options.json) {
66
- process.stdout.write(JSON.stringify({ cwd, ...r }, null, 2) + '\n');
67
- return r.ran && (r.exitCode ?? 1) !== 0 ? 1 : 0;
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;
68
179
  }
69
180
  if (options.proof) {
70
- process.stdout.write(buildProofBlock(cwd, r));
71
- return r.ran && (r.exitCode ?? 1) !== 0 ? 1 : 0;
181
+ process.stdout.write(buildProofBlock(cwd, r, weakened, missingClaimed, { base: options.base, claim: options.claim }, scan));
182
+ return exitCode;
72
183
  }
73
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
+ }
74
203
  process.stdout.write(colors.dim(`No test suite codehere can run here — ${r.skippedReason || 'none detected'}.\n`) +
75
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'));
76
- return 0; // nothing to verify is not a failure
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)
77
207
  }
78
- // Honesty: exit 126/127 means the test command couldn't be RUN (binary missing /
79
- // deps not installed), not that the tests failed. Never report "FAIL" for that.
80
- if (r.exitCode === 126 || r.exitCode === 127) {
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') {
81
212
  process.stdout.write('\n' +
82
213
  colors.error(`${symbols.error} Couldn't run the tests`) +
83
- colors.dim(` — '${r.command}' exited ${r.exitCode} (the runner isn't installed; try installing deps, then re-run).\n\n`));
84
- return 1; // non-zero for CI (unverified), but NOT a false "tests failed"
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"
85
220
  }
86
- const passed = (r.exitCode ?? 1) === 0;
221
+ const passed = (r.exitCode ?? 1) === 0 && !r.noTests;
87
222
  const counts = typeof r.passed === 'number' || typeof r.failed === 'number'
88
223
  ? ` ${r.passed ?? '?'} passed · ${r.failed ?? 0} failed`
89
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`);
90
236
  process.stdout.write('\n' +
91
- (passed ? colors.success(`${symbols.success} Tests pass`) : colors.error(`${symbols.error} Tests FAIL`)) +
237
+ headline +
92
238
  colors.dim(` — codehere re-ran them itself (${r.command}, exit ${r.exitCode})${counts}\n`) +
93
- colors.dim("This is observed reality, not the agent's claim.\n\n"));
94
- return passed ? 0 : 1;
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;
95
250
  }
96
251
  //# sourceMappingURL=check-command.js.map