codehere 0.7.1 → 1.0.1
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 +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 1.1 (verdict-trust punch list, 2026-06-09) · RE-RUN the tests, don't
|
|
3
|
+
* trust the agent's stdout.
|
|
4
|
+
*
|
|
5
|
+
* The gap this closes: `artifacts.tests` was regex-scraped from the AGENT's own
|
|
6
|
+
* captured stdout (`parseTestSummary(textAccum)`) and never re-executed — so
|
|
7
|
+
* "18/18 passed" was verified against the agent's own claim. A skeptic waves
|
|
8
|
+
* that off instantly. We already check FILE claims against ground truth
|
|
9
|
+
* (`git diff`); this extends the same "observe reality, don't trust the
|
|
10
|
+
* narration" rule to tests: codehere independently runs the project's test
|
|
11
|
+
* command on the real on-disk state and reads the REAL exit code.
|
|
12
|
+
*
|
|
13
|
+
* The authoritative signal is the EXIT CODE (a test runner exits non-zero on
|
|
14
|
+
* failure), not the scraped count — counts are display-only. So a report that
|
|
15
|
+
* claims "all tests pass" while the re-run exits non-zero becomes a caught lie
|
|
16
|
+
* (verify-claim flags it `broken` once `failed > 0`).
|
|
17
|
+
*
|
|
18
|
+
* Safe by construction: async (never blocks the daemon), timeout-bounded,
|
|
19
|
+
* opt-out via CODEHERE_RERUN_TESTS=0, and only ever runs the repo's OWN test
|
|
20
|
+
* script — strictly inside the trust boundary codehere already crosses by
|
|
21
|
+
* spawning agents that execute code in this repo.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { parseTestSummary } from '../../infrastructure/orchestration/artifacts-extractor.js';
|
|
27
|
+
const DEFAULT_TIMEOUT_MS = Number(process.env.CODEHERE_RERUN_TIMEOUT_MS ?? 120_000);
|
|
28
|
+
/** True if `file` exists in `cwd` and its text contains `needle` (case-insensitive). */
|
|
29
|
+
function fileMentions(cwd, file, needle) {
|
|
30
|
+
try {
|
|
31
|
+
const p = join(cwd, file);
|
|
32
|
+
return existsSync(p) && readFileSync(p, 'utf8').toLowerCase().includes(needle.toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** True if `cwd` (top level, or a test/ or tests/ dir) holds a Node built-in
|
|
39
|
+
* test file — `*.test.{mjs,cjs,js}`. Lets codehere observe reality in a plain
|
|
40
|
+
* Node repo that uses `node --test` with no npm script or runner config (a
|
|
41
|
+
* common setup that previously left the verifier blind — DISCOVERED via a real
|
|
42
|
+
* V3 run, 2026-06-14). Bounded: top level + two conventional dirs, never a
|
|
43
|
+
* recursive walk. */
|
|
44
|
+
function hasNodeTestFiles(cwd) {
|
|
45
|
+
const isTest = (f) => /\.test\.(mjs|cjs|js)$/.test(f);
|
|
46
|
+
for (const dir of [cwd, join(cwd, 'test'), join(cwd, 'tests')]) {
|
|
47
|
+
try {
|
|
48
|
+
if (readdirSync(dir).some(isTest))
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* dir absent / unreadable → keep looking */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Detect a runnable test command in `cwd`, across ecosystems — so "codehere
|
|
59
|
+
* re-runs your tests and catches a lying report" holds for Python/Go/Rust repos,
|
|
60
|
+
* not only npm. Conservative: each branch keys off an UNAMBIGUOUS marker; we
|
|
61
|
+
* return null (never guess) when there's nothing known to run.
|
|
62
|
+
*
|
|
63
|
+
* Precedence: explicit override → Node (npm test) → Go → Rust → Python (pytest)
|
|
64
|
+
* → a JS runner config without an npm script. First match wins.
|
|
65
|
+
*/
|
|
66
|
+
export function detectTestCommand(cwd) {
|
|
67
|
+
// 0. Explicit per-repo override — the escape hatch for anything we can't infer
|
|
68
|
+
// (monorepos, custom runners). e.g. CODEHERE_TEST_CMD="make test". Run via
|
|
69
|
+
// execFile (no shell), so args are whitespace-split — a quoted arg with
|
|
70
|
+
// spaces (CODEHERE_TEST_CMD='pytest -k "a b"') is NOT supported; use a
|
|
71
|
+
// wrapper script for that case.
|
|
72
|
+
const override = process.env.CODEHERE_TEST_CMD?.trim();
|
|
73
|
+
if (override) {
|
|
74
|
+
const [cmd, ...args] = override.split(/\s+/);
|
|
75
|
+
return { cmd, args, display: override };
|
|
76
|
+
}
|
|
77
|
+
// 1. Node — the project's declared `npm test` (dominant case + this repo).
|
|
78
|
+
const pj = join(cwd, 'package.json');
|
|
79
|
+
if (existsSync(pj)) {
|
|
80
|
+
try {
|
|
81
|
+
const json = JSON.parse(readFileSync(pj, 'utf8'));
|
|
82
|
+
const test = json.scripts?.test;
|
|
83
|
+
// npm's default placeholder script is not a real suite — skip it.
|
|
84
|
+
if (typeof test === 'string' && test.trim() && !/no test specified/i.test(test)) {
|
|
85
|
+
return { cmd: 'npm', args: ['test', '--silent'], display: 'npm test' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* unreadable package.json → fall through to other ecosystems */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 2. Go — go.mod present → the standard whole-module test run.
|
|
93
|
+
if (existsSync(join(cwd, 'go.mod'))) {
|
|
94
|
+
return { cmd: 'go', args: ['test', './...'], display: 'go test ./...' };
|
|
95
|
+
}
|
|
96
|
+
// 3. Rust — Cargo.toml → `cargo test`.
|
|
97
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) {
|
|
98
|
+
return { cmd: 'cargo', args: ['test'], display: 'cargo test' };
|
|
99
|
+
}
|
|
100
|
+
// 4. Python — pytest is the dominant runner. Key off pytest-specific markers
|
|
101
|
+
// (conftest.py / pytest.ini are unambiguous). For pyproject/setup.cfg, match
|
|
102
|
+
// the actual pytest CONFIG SECTION header — not a bare "pytest" substring,
|
|
103
|
+
// which would fire on a comment or an unrelated dependency string and run
|
|
104
|
+
// pytest on a project that doesn't use it.
|
|
105
|
+
if (existsSync(join(cwd, 'pytest.ini')) ||
|
|
106
|
+
existsSync(join(cwd, 'conftest.py')) ||
|
|
107
|
+
fileMentions(cwd, 'pyproject.toml', '[tool.pytest') ||
|
|
108
|
+
fileMentions(cwd, 'setup.cfg', '[tool:pytest]')) {
|
|
109
|
+
return { cmd: 'pytest', args: ['-q'], display: 'pytest' };
|
|
110
|
+
}
|
|
111
|
+
// 5. A JS test runner with config but no npm `test` script.
|
|
112
|
+
//
|
|
113
|
+
// SECURITY (2026-08-08) · `--no-install` is load-bearing on THIS path above all
|
|
114
|
+
// others. Bare `npx vitest` falls back to the public registry when the runner
|
|
115
|
+
// is not installed locally, so codehere would DOWNLOAD a test runner and
|
|
116
|
+
// execute it against the user's repo — and then report the result as
|
|
117
|
+
// "verifier re-ran the suite", the strongest claim this product makes. The
|
|
118
|
+
// same fallback let `npx graphify-ts` fetch an unrelated stranger's package
|
|
119
|
+
// (fixed 86c7893); here the blast radius is the evidence itself.
|
|
120
|
+
//
|
|
121
|
+
// Refusing is also the semantically right answer: a runner the project does
|
|
122
|
+
// not have installed is not the project's test suite, so re-running with it
|
|
123
|
+
// proves nothing. When this returns nothing runnable, applyTestObservation
|
|
124
|
+
// records an honest `testsUnobserved` reason instead — which is exactly the
|
|
125
|
+
// abstention the eval harness scores 2/2 on.
|
|
126
|
+
if (existsSync(join(cwd, 'vitest.config.ts')) || existsSync(join(cwd, 'vitest.config.js'))) {
|
|
127
|
+
return { cmd: 'npx', args: ['--no-install', 'vitest', 'run'], display: 'npx vitest run' };
|
|
128
|
+
}
|
|
129
|
+
if (existsSync(join(cwd, 'jest.config.js')) || existsSync(join(cwd, 'jest.config.ts'))) {
|
|
130
|
+
return { cmd: 'npx', args: ['--no-install', 'jest'], display: 'npx jest' };
|
|
131
|
+
}
|
|
132
|
+
// 6. Node built-in test runner — `*.test.{mjs,cjs,js}` present but no npm
|
|
133
|
+
// script or runner config above. `node --test` self-discovers them. This is
|
|
134
|
+
// the plain-Node case that left codehere blind to test reality (the verifier
|
|
135
|
+
// can't catch a "tests pass" lie it never observed). Lowest precedence so a
|
|
136
|
+
// declared script/runner always wins.
|
|
137
|
+
if (hasNodeTestFiles(cwd)) {
|
|
138
|
+
return { cmd: 'node', args: ['--test'], display: 'node --test' };
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Re-run the project's tests on the real on-disk state. Async + timeout-bounded.
|
|
144
|
+
* Resolves with `ran:false` + a reason when there's nothing to run or it's
|
|
145
|
+
* disabled — never throws.
|
|
146
|
+
*/
|
|
147
|
+
export function rerunTests(cwd) {
|
|
148
|
+
if (process.env.CODEHERE_RERUN_TESTS === '0') {
|
|
149
|
+
return Promise.resolve({ ran: false, skippedReason: 'disabled (CODEHERE_RERUN_TESTS=0)' });
|
|
150
|
+
}
|
|
151
|
+
const detected = detectTestCommand(cwd);
|
|
152
|
+
if (!detected) {
|
|
153
|
+
return Promise.resolve({ ran: false, skippedReason: 'no known test command in this project' });
|
|
154
|
+
}
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const start = Date.now();
|
|
157
|
+
let out = '';
|
|
158
|
+
let settled = false;
|
|
159
|
+
const done = (r) => {
|
|
160
|
+
if (settled)
|
|
161
|
+
return;
|
|
162
|
+
settled = true;
|
|
163
|
+
resolve(r);
|
|
164
|
+
};
|
|
165
|
+
let child;
|
|
166
|
+
try {
|
|
167
|
+
child = spawn(detected.cmd, detected.args, {
|
|
168
|
+
cwd,
|
|
169
|
+
env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
|
|
170
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
171
|
+
// Own process group so a timeout can kill the whole TREE — `npm test`
|
|
172
|
+
// forks the real runner, and killing just the npm wrapper orphaned the
|
|
173
|
+
// grandchild, which kept running past the timeout (self-audit 2026-06-09).
|
|
174
|
+
detached: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
done({ ran: false, skippedReason: `could not spawn test command: ${String(e).slice(0, 80)}` });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const timer = setTimeout(() => {
|
|
182
|
+
try {
|
|
183
|
+
// Negative pid = kill the whole process group (the detached tree), so the
|
|
184
|
+
// forked test runner dies too, not just the npm wrapper.
|
|
185
|
+
if (typeof child.pid === 'number')
|
|
186
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
187
|
+
else
|
|
188
|
+
child.kill('SIGKILL');
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
try {
|
|
192
|
+
child.kill('SIGKILL');
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* best-effort */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
done({ ran: false, skippedReason: `test re-run timed out after ${Math.round(DEFAULT_TIMEOUT_MS / 1000)}s` });
|
|
199
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
200
|
+
const cap = (chunk) => {
|
|
201
|
+
out += String(chunk);
|
|
202
|
+
// bound memory — keep only the tail the parser reads
|
|
203
|
+
if (out.length > 200_000)
|
|
204
|
+
out = out.slice(-100_000);
|
|
205
|
+
};
|
|
206
|
+
child.stdout?.on('data', cap);
|
|
207
|
+
child.stderr?.on('data', cap);
|
|
208
|
+
child.on('error', (e) => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
done({ ran: false, skippedReason: `test command errored: ${String(e.message).slice(0, 80)}` });
|
|
211
|
+
});
|
|
212
|
+
child.on('close', (code) => {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
const parsed = parseTestSummary(out);
|
|
215
|
+
done({
|
|
216
|
+
ran: true,
|
|
217
|
+
command: detected.display,
|
|
218
|
+
exitCode: code ?? -1,
|
|
219
|
+
passed: parsed?.passed,
|
|
220
|
+
failed: parsed?.failed,
|
|
221
|
+
durationMs: Date.now() - start,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=rerun-tests.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provable session search — REQUEST_METHOD stage 7 (Learn), and the Luca-seeded
|
|
3
|
+
* differentiator (#19): codehere already keeps a signed, cost-attributed record
|
|
4
|
+
* of every past session. This makes it SEARCHABLE — so a request can recover a
|
|
5
|
+
* prior decision, failed attempt, or command instead of starting from zero.
|
|
6
|
+
*
|
|
7
|
+
* The wedge vs a plain history search (e.g. ctx): every hit carries PROVENANCE —
|
|
8
|
+
* which agent did it and whether the run succeeded — because codehere's record
|
|
9
|
+
* has it and a raw-transcript search doesn't. "Search history you can trust."
|
|
10
|
+
*
|
|
11
|
+
* Local-first: reads the on-disk project thread records; no server, no network.
|
|
12
|
+
*/
|
|
13
|
+
import { listProjects, readProjectThread, } from '../../infrastructure/storage/project-thread-reader.js';
|
|
14
|
+
/** Pure ranking — score a task against query terms (prompt weighted 2x report). */
|
|
15
|
+
export function scoreTask(task, terms) {
|
|
16
|
+
const prompt = String(task.prompt || '').toLowerCase();
|
|
17
|
+
const report = String(task.finalReport?.summary || '').toLowerCase();
|
|
18
|
+
let score = 0;
|
|
19
|
+
for (const t of terms) {
|
|
20
|
+
if (prompt.includes(t))
|
|
21
|
+
score += 2;
|
|
22
|
+
if (report.includes(t))
|
|
23
|
+
score += 1;
|
|
24
|
+
}
|
|
25
|
+
let snippet = '';
|
|
26
|
+
const hay = report || prompt;
|
|
27
|
+
for (const t of terms) {
|
|
28
|
+
const i = hay.indexOf(t);
|
|
29
|
+
if (i >= 0) {
|
|
30
|
+
const start = Math.max(0, i - 30);
|
|
31
|
+
snippet = (start > 0 ? '…' : '') + hay.slice(start, start + 100).trim() + '…';
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { score, snippet: snippet || String(task.prompt || '').slice(0, 90) };
|
|
36
|
+
}
|
|
37
|
+
/** Search past sessions across all projects. Ranked, top-N, with provenance. */
|
|
38
|
+
export function searchSessions(query, opts) {
|
|
39
|
+
const terms = String(query || '')
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.split(/\s+/)
|
|
42
|
+
.filter((w) => w.length >= 2);
|
|
43
|
+
if (!terms.length)
|
|
44
|
+
return [];
|
|
45
|
+
const limit = opts?.limit ?? 10;
|
|
46
|
+
const hits = [];
|
|
47
|
+
for (const p of listProjects()) {
|
|
48
|
+
const thread = readProjectThread(p.slug);
|
|
49
|
+
if (!thread)
|
|
50
|
+
continue;
|
|
51
|
+
for (const task of thread.tasks) {
|
|
52
|
+
if (task.archived)
|
|
53
|
+
continue;
|
|
54
|
+
const { score, snippet } = scoreTask(task, terms);
|
|
55
|
+
if (score > 0) {
|
|
56
|
+
hits.push({
|
|
57
|
+
sessionId: task.sessionId,
|
|
58
|
+
agent: task.agent || '?',
|
|
59
|
+
status: String(task.status || 'unknown'),
|
|
60
|
+
project: p.remote || p.slug,
|
|
61
|
+
prompt: task.prompt || '',
|
|
62
|
+
snippet,
|
|
63
|
+
score,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return hits.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=search-sessions.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2.1 · slice 1 — the pure decision: should a just-completed task get an
|
|
3
|
+
* AUTOMATIC cross-vendor adversarial review, and by which vendor?
|
|
4
|
+
*
|
|
5
|
+
* Today cross-vendor proof is opportunistic: it only fires if a human happened
|
|
6
|
+
* to run a second, different-vendor task that reviewed the first. 2.1 makes the
|
|
7
|
+
* differentiator automatic — on completion, codehere itself spawns a DIFFERENT
|
|
8
|
+
* vendor in refute-mode and folds its verdict into the composite (`SessionVerdict`).
|
|
9
|
+
*
|
|
10
|
+
* This module is ONLY the eligibility gate, kept pure so the policy is testable
|
|
11
|
+
* without a daemon, a spawn, or real spend (the spawn + verdict wiring are later
|
|
12
|
+
* slices). The honesty bar from assess-cross-vendor applies downstream: a real
|
|
13
|
+
* review with a real reviewer, never a laundered green.
|
|
14
|
+
*
|
|
15
|
+
* The three gates, in order:
|
|
16
|
+
* 1. cross-vendor possible — a DIFFERENT installed vendor exists to review with
|
|
17
|
+
* (single-vendor incumbents structurally can't do this; it's the whole wedge).
|
|
18
|
+
* 2. worth reviewing — the task actually changed something (no-op tasks have
|
|
19
|
+
* nothing to refute; reviewing them burns spend for a guaranteed-empty proof).
|
|
20
|
+
* 3. within budget — the review is a real spawn with real cost; never blow the
|
|
21
|
+
* cost-bounded promise to produce a receipt.
|
|
22
|
+
*/
|
|
23
|
+
/** Reviewer preference order — strongest-at-review first, mirrors ROUTING_RULES'
|
|
24
|
+
* review rule (claude-code leads, then codex). Any installed different-vendor
|
|
25
|
+
* agent is acceptable; this only decides WHICH when several are available. */
|
|
26
|
+
// Reviewer preference favors reliably-authed, cost-bounded vendors first: Claude +
|
|
27
|
+
// Cursor (flat subscriptions, ~free marginal) before the metered/rate-limit-prone
|
|
28
|
+
// ones (aider/opencode on the OpenAI key, codex on a ChatGPT plan, gemini needs a
|
|
29
|
+
// key). A runnable reviewer on a subscription you've paid for beats a "better" one
|
|
30
|
+
// that's out of quota.
|
|
31
|
+
const REVIEWER_PREFERENCE = ['claude-code', 'cursor', 'aider', 'opencode', 'codex', 'gemini'];
|
|
32
|
+
/** Pick the strongest available reviewer that is NOT the writer's vendor. */
|
|
33
|
+
function pickReviewer(writerAgent, installed) {
|
|
34
|
+
const candidates = installed.filter((a) => a !== writerAgent && a !== 'conductor');
|
|
35
|
+
if (candidates.length === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
for (const pref of REVIEWER_PREFERENCE) {
|
|
38
|
+
if (candidates.includes(pref))
|
|
39
|
+
return pref;
|
|
40
|
+
}
|
|
41
|
+
return candidates[0];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Decide whether to auto-spawn a cross-vendor review. Pure; no I/O, no spend.
|
|
45
|
+
*/
|
|
46
|
+
export function shouldAutoReview(input, thresholds = {}) {
|
|
47
|
+
const minFiles = thresholds.minFilesChanged ?? 1;
|
|
48
|
+
const minBudget = thresholds.minBudgetUsd ?? 0.1;
|
|
49
|
+
const reviewerVendor = pickReviewer(input.writerAgent, input.installedAgents);
|
|
50
|
+
if (!reviewerVendor) {
|
|
51
|
+
return { eligible: false, reason: `No different vendor installed to review ${input.writerAgent}'s work.` };
|
|
52
|
+
}
|
|
53
|
+
if (input.filesChanged < minFiles) {
|
|
54
|
+
return {
|
|
55
|
+
eligible: false,
|
|
56
|
+
reason: `Nothing to review — the task changed ${input.filesChanged} file(s); a cross-vendor review needs real work to refute.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (input.budgetRemainingUsd < minBudget) {
|
|
60
|
+
return {
|
|
61
|
+
eligible: false,
|
|
62
|
+
reason: `Skipped to stay cost-bounded — $${input.budgetRemainingUsd.toFixed(2)} left under the cap, below the $${minBudget.toFixed(2)} a review needs.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
eligible: true,
|
|
67
|
+
reviewerVendor,
|
|
68
|
+
reason: `${reviewerVendor} will independently review ${input.writerAgent}'s work in refute-mode.`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=should-auto-review.js.map
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* Pure logic. The CLI + HTTP routes wrap this and do I/O.
|
|
21
21
|
*/
|
|
22
22
|
import { sign as cryptoSign, verify as cryptoVerify, generateKeyPairSync, createPrivateKey, createPublicKey, } from 'crypto';
|
|
23
|
+
import { existsSync, readFileSync } from 'fs';
|
|
24
|
+
import { computeEntryHash, AUDIT_FILE } from '../../infrastructure/hooks/claude-sdk-hook.js';
|
|
23
25
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
24
26
|
/* Canonicalization · deterministic JSON form so signatures match */
|
|
25
27
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
@@ -127,9 +129,97 @@ function isBundle(x) {
|
|
|
127
129
|
return false;
|
|
128
130
|
if (typeof b.session !== 'object' || b.session === null)
|
|
129
131
|
return false;
|
|
132
|
+
// auditChain is optional, but if present it must be an array (the signature
|
|
133
|
+
// already protects its contents; this just guards the shape).
|
|
134
|
+
if (b.auditChain !== undefined && !Array.isArray(b.auditChain))
|
|
135
|
+
return false;
|
|
130
136
|
return true;
|
|
131
137
|
}
|
|
132
138
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
139
|
+
/* Audit chain · embed (export side) + re-verify (import side) */
|
|
140
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
141
|
+
/**
|
|
142
|
+
* Collect the audit entries for one session from the on-disk chain, preserving
|
|
143
|
+
* each entry's original chain fields (seq/prevHash/entryHash). The export use-case
|
|
144
|
+
* puts these on ThreadBundleBody.auditChain so the recipient can re-verify the
|
|
145
|
+
* agent's actions. Best-effort: a missing/unreadable log yields [].
|
|
146
|
+
*/
|
|
147
|
+
export function collectAuditEntriesForSession(sessionId, filepath = AUDIT_FILE, alsoSessionIds = []) {
|
|
148
|
+
if (!existsSync(filepath))
|
|
149
|
+
return [];
|
|
150
|
+
let content;
|
|
151
|
+
try {
|
|
152
|
+
content = readFileSync(filepath, 'utf-8');
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
// 1.3 (extended to audit reconciliation) · a conductor-routed task's tool
|
|
158
|
+
// calls are keyed by the SPECIALIST's runtime sid, not the conductor task id.
|
|
159
|
+
// Matching the delegate id too lets reconciliation see the real writes —
|
|
160
|
+
// without it Auto tasks reconcile 'no-writes' and never earn the observed
|
|
161
|
+
// signal, so the 2.2 composite grade stays dark for the default path.
|
|
162
|
+
const ids = new Set([sessionId, ...alsoSessionIds]);
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const line of content.split('\n')) {
|
|
165
|
+
if (!line.trim())
|
|
166
|
+
continue;
|
|
167
|
+
try {
|
|
168
|
+
const e = JSON.parse(line);
|
|
169
|
+
if (ids.has(e.sessionId))
|
|
170
|
+
out.push(e);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* skip unparseable line */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Re-verify an embedded audit chain's PER-ENTRY integrity: each chained entry's
|
|
180
|
+
* stored entryHash must equal its recomputed hash (computeEntryHash, the exact
|
|
181
|
+
* function the writer used). A mismatch means the entry's content was altered
|
|
182
|
+
* after it was written → the record of what the agent did is not trustworthy.
|
|
183
|
+
*
|
|
184
|
+
* This is deliberately a self-hash check, not a cross-link walk: a single-session
|
|
185
|
+
* export is a SUBSET of the global interleaved chain, so its entries' prevHash
|
|
186
|
+
* values point at other sessions' entries that aren't present. Per-entry self-hash
|
|
187
|
+
* + the bundle signature (signer vouches for the exact set) is the honest,
|
|
188
|
+
* verifiable team-proof; global completeness is not claimable from a subset. See
|
|
189
|
+
* ThreadBundleBody.auditChain for the FULL trust model (signer attestation, not
|
|
190
|
+
* independent provenance — a malicious signer can fabricate a valid chain).
|
|
191
|
+
*
|
|
192
|
+
* CALLER CONTRACT: `valid` means "no tampered entry was found" — it is `true`
|
|
193
|
+
* for an EMPTY or all-legacy set where NOTHING was cryptographically checked.
|
|
194
|
+
* Any "✓ verified" surface MUST gate on `verified > 0`, not bare `valid`, or it
|
|
195
|
+
* will claim verification that didn't happen.
|
|
196
|
+
*/
|
|
197
|
+
export function verifyEmbeddedAuditChain(entries) {
|
|
198
|
+
const check = { valid: true, total: 0, verified: 0, legacy: 0, tampered: [] };
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
check.total++;
|
|
201
|
+
const hasChain = typeof entry.seq === 'number' && typeof entry.prevHash === 'string' && typeof entry.entryHash === 'string';
|
|
202
|
+
if (!hasChain) {
|
|
203
|
+
check.legacy++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const recomputed = computeEntryHash(entry);
|
|
207
|
+
if (recomputed !== entry.entryHash) {
|
|
208
|
+
check.valid = false;
|
|
209
|
+
check.tampered.push({
|
|
210
|
+
seq: entry.seq,
|
|
211
|
+
sessionId: entry.sessionId,
|
|
212
|
+
toolName: entry.toolName,
|
|
213
|
+
reason: 'self_hash_mismatch',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
check.verified++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return check;
|
|
221
|
+
}
|
|
222
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
133
223
|
/* Key derivation helpers · PEM encoding for storage */
|
|
134
224
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
135
225
|
export function exportPrivatePem(key) {
|