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.
Files changed (139) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/narrator-prompt-template.js +1 -1
  11. package/dist/application/use-cases/plan-auto-review.js +57 -0
  12. package/dist/application/use-cases/reconcile-audit.js +100 -0
  13. package/dist/application/use-cases/rerun-tests.js +226 -0
  14. package/dist/application/use-cases/search-sessions.js +70 -0
  15. package/dist/application/use-cases/should-auto-review.js +71 -0
  16. package/dist/application/use-cases/thread-export.js +90 -0
  17. package/dist/application/use-cases/verify-claim.js +187 -5
  18. package/dist/domain/audit.js +11 -0
  19. package/dist/domain/services/codebase-detector.js +0 -6
  20. package/dist/domain/task.js +91 -0
  21. package/dist/error-handler.js +3 -115
  22. package/dist/index.js +279 -31
  23. package/dist/infrastructure/completion/bash-completion.js +7 -3
  24. package/dist/infrastructure/completion/fish-completion.js +7 -12
  25. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  26. package/dist/infrastructure/config/config-manager.js +4 -3
  27. package/dist/infrastructure/config/product-context.js +1 -19
  28. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  29. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  30. package/dist/infrastructure/context/github-api.js +110 -0
  31. package/dist/infrastructure/context/github-device-flow.js +65 -0
  32. package/dist/infrastructure/context/oauth-store.js +5 -2
  33. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  34. package/dist/infrastructure/cost/budget-config.js +4 -3
  35. package/dist/infrastructure/cost/cost-router.js +16 -23
  36. package/dist/infrastructure/cost/spend-aggregator.js +91 -16
  37. package/dist/infrastructure/cost/team-ledger.js +355 -0
  38. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  39. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  40. package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
  41. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  42. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  43. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  44. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  45. package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
  46. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  47. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  48. package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
  49. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  50. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  51. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  52. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  53. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  54. package/dist/infrastructure/orchestration/failover-config.js +1 -11
  55. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  56. package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
  57. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  58. package/dist/infrastructure/orchestration/memory-injector.js +4 -1
  59. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
  60. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  61. package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
  62. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  63. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  64. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  65. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  66. package/dist/infrastructure/protocols/cloud-sync.js +2 -2
  67. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  68. package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
  69. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  70. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  71. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  72. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  73. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  74. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  75. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  76. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  77. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  78. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  79. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  80. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  81. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  82. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  83. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  84. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  85. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  86. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  87. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  88. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  89. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  90. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  91. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  92. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
  93. package/dist/infrastructure/protocols/verify-page.js +884 -148
  94. package/dist/infrastructure/security/fix-code.js +144 -0
  95. package/dist/infrastructure/security/fix-secret.js +89 -0
  96. package/dist/infrastructure/security/permissions-config.js +3 -2
  97. package/dist/infrastructure/security/scan-types.js +10 -0
  98. package/dist/infrastructure/security/ship-scan.js +195 -0
  99. package/dist/infrastructure/security/trust-scorer.js +70 -3
  100. package/dist/infrastructure/security/vibe-checks.js +99 -0
  101. package/dist/infrastructure/storage/atomic-write.js +86 -0
  102. package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
  103. package/dist/infrastructure/storage/project-thread-reader.js +262 -25
  104. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  105. package/dist/monitoring.js +2 -2
  106. package/dist/presentation/cli/commands/audit-command.js +79 -4
  107. package/dist/presentation/cli/commands/budget-command.js +2 -2
  108. package/dist/presentation/cli/commands/check-command.js +96 -0
  109. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  110. package/dist/presentation/cli/commands/help-command.js +5 -5
  111. package/dist/presentation/cli/commands/hook-command.js +5 -5
  112. package/dist/presentation/cli/commands/init-command.js +89 -66
  113. package/dist/presentation/cli/commands/memory-command.js +28 -28
  114. package/dist/presentation/cli/commands/permission-command.js +1 -12
  115. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  116. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  117. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  118. package/dist/presentation/cli/commands/report-command.js +48 -32
  119. package/dist/presentation/cli/commands/scan-command.js +52 -0
  120. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  121. package/dist/presentation/cli/commands/search-command.js +51 -0
  122. package/dist/presentation/cli/commands/setup-command.js +31 -8
  123. package/dist/presentation/cli/commands/status-command.js +39 -86
  124. package/dist/presentation/cli/commands/team-command.js +256 -0
  125. package/dist/presentation/cli/commands/thread-command.js +87 -5
  126. package/dist/presentation/cli/commands/trust-command.js +53 -31
  127. package/dist/presentation/cli/commands/verify-command.js +74 -9
  128. package/dist/presentation/cli/error-display.js +0 -8
  129. package/dist/presentation/cli/repl.js +15 -7
  130. package/dist/presentation/cli/task-deliverable.js +36 -10
  131. package/dist/testing/replay-adapter.js +0 -19
  132. package/dist/ui.js +51 -239
  133. package/dist/utils/version.js +0 -6
  134. package/package.json +30 -10
  135. package/dist/domain/entities/task.js +0 -61
  136. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  137. package/dist/infrastructure/security/input-validator.js +0 -152
  138. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  139. 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) {