codehere 1.0.1 → 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.
- package/README.md +36 -192
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,199 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
-
|
|
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.
|
|
48
|
-
|
|
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 =
|
|
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} &&
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
79
|
-
// deps not installed), not that the tests failed. Never report "FAIL" for that
|
|
80
|
-
|
|
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}
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
94
|
-
|
|
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
|