codehere 0.8.0 → 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/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 +269 -25
- 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 +3 -2
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +90 -15
- 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 +133 -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 +22 -10
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- 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 +0 -10
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
- 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/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
- 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 -4214
- package/dist/infrastructure/protocols/verify-page.js +849 -155
- 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 +150 -3
- package/dist/infrastructure/storage/project-thread-reader.js +261 -24
- 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 +26 -14
- 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
|
@@ -19,11 +19,11 @@ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } fr
|
|
|
19
19
|
import { homedir, hostname } from 'os';
|
|
20
20
|
import { dirname, join, isAbsolute, resolve } from 'path';
|
|
21
21
|
import { colors, symbols } from '../../../ui.js';
|
|
22
|
-
import { signBundle, verifyBundle, shortFingerprint, } from '../../../application/use-cases/thread-export.js';
|
|
22
|
+
import { signBundle, verifyBundle, shortFingerprint, collectAuditEntriesForSession, verifyEmbeddedAuditChain, } from '../../../application/use-cases/thread-export.js';
|
|
23
23
|
import { loadOrCreateKeypair, localPublicKeyBase64, trustKey, untrustKey, trustedKeySet, loadTrustedKeys, isKeyTrusted, } from '../../../infrastructure/orchestration/thread-key-store.js';
|
|
24
24
|
import { loadSessionById } from '../../../infrastructure/orchestration/session-persistence.js';
|
|
25
25
|
import { getVersion } from '../../../utils/version.js';
|
|
26
|
-
export function executeThreadCommand(sub, arg,
|
|
26
|
+
export function executeThreadCommand(sub, arg, _extra, options = {}) {
|
|
27
27
|
switch (sub) {
|
|
28
28
|
case 'export':
|
|
29
29
|
return exportThread(arg, options);
|
|
@@ -60,6 +60,9 @@ function exportThread(sessionId, options) {
|
|
|
60
60
|
kind: 'thread',
|
|
61
61
|
sessionId: session.sessionId,
|
|
62
62
|
session,
|
|
63
|
+
// Embed the session's tool-call audit so a teammate can inspect what the
|
|
64
|
+
// agent did (signer-attested; see ThreadBundleBody.auditChain trust model).
|
|
65
|
+
auditChain: collectAuditEntriesForSession(session.sessionId),
|
|
63
66
|
exporter: {
|
|
64
67
|
hostname: safeHostname(),
|
|
65
68
|
codehereVersion: getVersion(),
|
|
@@ -132,6 +135,15 @@ function importThread(filePath, options) {
|
|
|
132
135
|
// status: 'valid' or 'unknown_key'
|
|
133
136
|
const bundle = verdict.status === 'valid' ? verdict.bundle : verdict.bundle;
|
|
134
137
|
const fp_key = shortFingerprint(bundle.publicKey);
|
|
138
|
+
// Re-verify the embedded tool-call audit so the recipient SEES what the agent
|
|
139
|
+
// did — not just the narration. Honest semantics: "verified" is gated on
|
|
140
|
+
// verified>0 (a valid AuditChainCheck is true even for an empty set), and a
|
|
141
|
+
// tampered entry is surfaced loudly. The bundle SIGNATURE already proved the
|
|
142
|
+
// chain is signer-attested + unaltered-in-transit; this confirms each entry's
|
|
143
|
+
// self-hash on top.
|
|
144
|
+
const auditCheck = bundle.body.auditChain
|
|
145
|
+
? verifyEmbeddedAuditChain(bundle.body.auditChain)
|
|
146
|
+
: undefined;
|
|
135
147
|
if (verdict.status === 'unknown_key') {
|
|
136
148
|
if (!options.trust) {
|
|
137
149
|
if (options.json) {
|
|
@@ -160,6 +172,18 @@ function importThread(filePath, options) {
|
|
|
160
172
|
}
|
|
161
173
|
// Persist into ~/.codehere/sessions.jsonl as 3 records (spawned, completed, finalReport)
|
|
162
174
|
const ses = bundle.body.session;
|
|
175
|
+
// No-clobber / idempotent import (Phase 2.5 multi-human): re-importing the same
|
|
176
|
+
// session must not duplicate it in a shared room. Append-only never LOSES work,
|
|
177
|
+
// but a second import would double the records (and any count-based view).
|
|
178
|
+
if (loadSessionById(ses.sessionId)) {
|
|
179
|
+
if (options.json) {
|
|
180
|
+
process.stdout.write(JSON.stringify({ ok: true, sessionId: ses.sessionId, alreadyImported: true, fingerprint: fp_key }) + '\n');
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
process.stdout.write(colors.dim(`${symbols.info} session ${ses.sessionId.slice(0, 8)} is already imported · nothing to do\n`));
|
|
184
|
+
}
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
163
187
|
const home = process.env.CODEHERE_HOME || homedir();
|
|
164
188
|
const sessionsPath = join(home, '.codehere', 'sessions.jsonl');
|
|
165
189
|
mkdirSync(dirname(sessionsPath), { recursive: true });
|
|
@@ -171,6 +195,28 @@ function importThread(filePath, options) {
|
|
|
171
195
|
prompt: ses.prompt,
|
|
172
196
|
cwd: ses.cwd,
|
|
173
197
|
startedAt: ses.startedAt,
|
|
198
|
+
// S18 · practice classification is a property of the RUN on the exporter's
|
|
199
|
+
// machine, carried by the bundle — never the importing daemon's env.
|
|
200
|
+
// Absent in a legacy bundle = the work was real and audit-attested; an
|
|
201
|
+
// import performed on a mock daemon must never demote a teammate's real
|
|
202
|
+
// work to practice. Stamped explicitly so no spawned-record writer leaves
|
|
203
|
+
// the field env-classified.
|
|
204
|
+
mockRun: ses.mockRun === true,
|
|
205
|
+
// Provenance: attribute this session to the signer it was imported from,
|
|
206
|
+
// so a shared room shows "from teammate <fingerprint>", not as if local.
|
|
207
|
+
importedFrom: fp_key,
|
|
208
|
+
// Persist the audit attestation — the bundle isn't stored, so the room
|
|
209
|
+
// needs this to show the proof badge. attested gated on verified>0 && no tamper.
|
|
210
|
+
...(auditCheck
|
|
211
|
+
? {
|
|
212
|
+
importAudit: {
|
|
213
|
+
total: auditCheck.total,
|
|
214
|
+
verified: auditCheck.verified,
|
|
215
|
+
tampered: auditCheck.tampered.length,
|
|
216
|
+
attested: auditCheck.verified > 0 && auditCheck.tampered.length === 0,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
: {}),
|
|
174
220
|
}));
|
|
175
221
|
lines.push(JSON.stringify({
|
|
176
222
|
t: 'completed',
|
|
@@ -197,17 +243,53 @@ function importThread(filePath, options) {
|
|
|
197
243
|
sessionId: ses.sessionId,
|
|
198
244
|
trusted: verdict.status === 'valid' ? true : 'just-added',
|
|
199
245
|
fingerprint: fp_key,
|
|
246
|
+
audit: auditCheck
|
|
247
|
+
? {
|
|
248
|
+
total: auditCheck.total,
|
|
249
|
+
verified: auditCheck.verified,
|
|
250
|
+
tampered: auditCheck.tampered.length,
|
|
251
|
+
legacy: auditCheck.legacy,
|
|
252
|
+
// "verified" is true ONLY when entries were actually checked +
|
|
253
|
+
// none tampered — never on a bare/empty chain.
|
|
254
|
+
attested: auditCheck.verified > 0 && auditCheck.tampered.length === 0,
|
|
255
|
+
}
|
|
256
|
+
: null,
|
|
200
257
|
}) + '\n');
|
|
201
258
|
}
|
|
202
259
|
else {
|
|
203
260
|
process.stdout.write(colors.green(`${symbols.success} Imported`) +
|
|
204
|
-
colors.dim(` session ${ses.sessionId.slice(0, 8)} · signed by ${fp_key}\n`)
|
|
205
|
-
|
|
261
|
+
colors.dim(` session ${ses.sessionId.slice(0, 8)} · signed by ${fp_key}\n`));
|
|
262
|
+
process.stdout.write(formatAuditLine(auditCheck));
|
|
263
|
+
process.stdout.write(colors.dim(` view it: `) +
|
|
206
264
|
colors.bold(`codehere start`) +
|
|
207
265
|
colors.dim(` then click the task in the dashboard\n`));
|
|
208
266
|
}
|
|
209
267
|
return 0;
|
|
210
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Honest one-line summary of the embedded audit chain for the import surface.
|
|
271
|
+
* The "✓ verified" claim is gated on verified>0 (NOT bare check.valid, which is
|
|
272
|
+
* true for an empty/all-legacy set where nothing was checked); a tampered entry
|
|
273
|
+
* is surfaced as a loud warning. Returns '' when there's no chain to report.
|
|
274
|
+
*/
|
|
275
|
+
function formatAuditLine(check) {
|
|
276
|
+
if (!check || check.total === 0) {
|
|
277
|
+
return colors.dim(` (no tool-call audit was included in this bundle)\n`);
|
|
278
|
+
}
|
|
279
|
+
if (check.tampered.length > 0) {
|
|
280
|
+
return colors.error(` ${symbols.error} audit chain INCONSISTENT · ${check.tampered.length} of ${check.total} entr${check.tampered.length === 1 ? 'y was' : 'ies were'} altered after writing · the signer's tool-call record is not internally consistent\n`);
|
|
281
|
+
}
|
|
282
|
+
if (check.verified > 0) {
|
|
283
|
+
// Honest framing: this is signer ATTESTATION, not independent provenance —
|
|
284
|
+
// the hashes match and the bundle is signed, but trust reduces to the signer's
|
|
285
|
+
// key (a malicious signer could fabricate a consistent chain). Say exactly that
|
|
286
|
+
// rather than a bare "verified" that implies more than self-hash + signature.
|
|
287
|
+
return colors.green(` ${symbols.success} ${check.verified} agent tool call${check.verified === 1 ? '' : 's'} recorded` +
|
|
288
|
+
colors.dim(` · hashes intact, as attested by this signer (your trust = the key above)\n`));
|
|
289
|
+
}
|
|
290
|
+
// total>0 but nothing verifiable (all legacy/no chain fields).
|
|
291
|
+
return colors.dim(` ${check.legacy} audit entr${check.legacy === 1 ? 'y' : 'ies'} present but unverifiable (no chain fields)\n`);
|
|
292
|
+
}
|
|
211
293
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
212
294
|
/* keys / trust / untrust */
|
|
213
295
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
@@ -271,7 +353,7 @@ function untrustCmd(pubkey, json) {
|
|
|
271
353
|
process.stdout.write(colors.green(`${symbols.success} untrusted ${shortFingerprint(pubkey)}\n`));
|
|
272
354
|
}
|
|
273
355
|
else {
|
|
274
|
-
process.stdout.write(colors.gray(`${symbols.dot} key not in trust store (or it's this machine's own key
|
|
356
|
+
process.stdout.write(colors.gray(`${symbols.dot} key not in trust store (or it's this machine's own key · can't remove)\n`));
|
|
275
357
|
}
|
|
276
358
|
return 0;
|
|
277
359
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { existsSync } from 'fs';
|
|
22
22
|
import { join, relative, isAbsolute } from 'path';
|
|
23
23
|
import { execFileSync } from 'child_process';
|
|
24
|
-
import { scoreFile } from '../../../infrastructure/security/trust-scorer.js';
|
|
24
|
+
import { scoreFile, aggregateTrust } from '../../../infrastructure/security/trust-scorer.js';
|
|
25
25
|
import { collectCodeFiles } from '../../../infrastructure/security/file-collection.js';
|
|
26
26
|
import { colors, sectionHeader, error, newline, info, randomCleanScan } from '../../../ui.js';
|
|
27
27
|
import { loadProductContext, adjustRecommendation } from '../../../infrastructure/config/product-context.js';
|
|
@@ -174,24 +174,35 @@ export async function executeTrustCommand(target, options = {}) {
|
|
|
174
174
|
}
|
|
175
175
|
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
176
176
|
const results = filesToScan.map((f) => scoreFile(f.filepath, f.code));
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
177
|
+
// S8 · score + grade come from the ONE shared aggregation (trust-scorer's
|
|
178
|
+
// aggregateTrust) — `trust`, `--badge`, and `report` can no longer drift.
|
|
179
|
+
const { overallScore, grade, criticalCount, highCount, mediumCount, lowCount, totalFindings } = aggregateTrust(results);
|
|
180
|
+
// Nothing scanned is not a clean scan.
|
|
181
|
+
//
|
|
182
|
+
// aggregateTrust over zero files returns 100/A, and the recommendation below then read
|
|
183
|
+
// "Clean scan. Nothing blocking a merge." — an explicit merge recommendation earned by
|
|
184
|
+
// looking at nothing. `trust --ci` advertises itself as a gate ("Exit 1 on critical
|
|
185
|
+
// findings"), so a CI job pointed at the wrong path, or one whose diff resolution returned
|
|
186
|
+
// no files, went green and told the reader the code was fine. Same defect as `audit verify
|
|
187
|
+
// --json` reporting valid:true over an empty chain (f030769): a walk over nothing trivially
|
|
188
|
+
// succeeds, and success over nothing is not success.
|
|
189
|
+
const scanned = filesToScan.length > 0;
|
|
190
|
+
const baseRecommendation = !scanned
|
|
191
|
+
? 'No files scanned — nothing was graded. Point trust at a path with source in it, or check your diff range.'
|
|
192
|
+
: criticalCount > 0
|
|
193
|
+
? `${criticalCount} critical ${criticalCount === 1 ? 'issue' : 'issues'} · worth sorting before this merges.`
|
|
194
|
+
: highCount > 0
|
|
195
|
+
? `Nothing critical, but ${highCount} high-severity ${highCount === 1 ? 'pattern' : 'patterns'} worth a look.`
|
|
196
|
+
: mediumCount + lowCount > 0
|
|
197
|
+
? `${mediumCount + lowCount} low-priority ${mediumCount + lowCount === 1 ? 'finding' : 'findings'} · nothing blocking, but worth a pass.`
|
|
198
|
+
: 'Clean scan. Nothing blocking a merge.';
|
|
191
199
|
const productCtx = loadProductContext(process.cwd());
|
|
192
200
|
const recommendation = adjustRecommendation(baseRecommendation, criticalCount, highCount, overallScore, productCtx);
|
|
193
201
|
const report = {
|
|
194
202
|
files: results,
|
|
203
|
+
// `scanned` is the field a machine should branch on. Without it, files:[] plus grade:'A'
|
|
204
|
+
// reads as a pass to anything that only looks at the grade.
|
|
205
|
+
scanned,
|
|
195
206
|
overallScore,
|
|
196
207
|
grade,
|
|
197
208
|
totalFindings,
|
|
@@ -203,11 +214,11 @@ export async function executeTrustCommand(target, options = {}) {
|
|
|
203
214
|
scannedAt: new Date().toISOString(),
|
|
204
215
|
};
|
|
205
216
|
// Track CI failure — will throw after display so the CLI action handler catches it
|
|
206
|
-
const ciFailed = options.ci && (overallScore < 60 || criticalCount > 0);
|
|
217
|
+
const ciFailed = options.ci && (!scanned || overallScore < 60 || criticalCount > 0);
|
|
207
218
|
// ── Output ────────────────────────────────────────────────────────────────
|
|
208
219
|
if (options.json) {
|
|
209
220
|
console.log(JSON.stringify(report, null, 2));
|
|
210
|
-
if (options.ci && (overallScore < 60 || criticalCount > 0))
|
|
221
|
+
if (options.ci && (!scanned || overallScore < 60 || criticalCount > 0))
|
|
211
222
|
process.exit(1);
|
|
212
223
|
return;
|
|
213
224
|
}
|
|
@@ -219,21 +230,32 @@ export async function executeTrustCommand(target, options = {}) {
|
|
|
219
230
|
newline();
|
|
220
231
|
sectionHeader('codehere Trust Report');
|
|
221
232
|
newline();
|
|
222
|
-
// Score
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
].filter(Boolean);
|
|
232
|
-
if (parts.length === 0) {
|
|
233
|
-
console.log(` ${colors.green('✓')} ${randomCleanScan()}`);
|
|
233
|
+
// Score — but only when something was scored.
|
|
234
|
+
//
|
|
235
|
+
// A full green bar reading 100/100 A over zero files is the headline lie, and it survived
|
|
236
|
+
// fixing the JSON and the exit code: those matter to a pipeline, this is what the human
|
|
237
|
+
// reads. DESIGN.md §1 reserves green for a measurement, and an aggregation over nothing is
|
|
238
|
+
// not one. No score, no grade, no green tick — just what happened.
|
|
239
|
+
if (!scanned) {
|
|
240
|
+
console.log(` ${colors.dim('—')} no files scanned, so there is no score`);
|
|
241
|
+
newline();
|
|
234
242
|
}
|
|
235
243
|
else {
|
|
236
|
-
console.log(` ${
|
|
244
|
+
console.log(` Score ${scoreBar(overallScore)} ${colors.bold(String(overallScore))}/100 ${gradeColor(grade)}`);
|
|
245
|
+
newline();
|
|
246
|
+
// Findings summary (inline, compact)
|
|
247
|
+
const parts = [
|
|
248
|
+
criticalCount > 0 ? colors.red(`${criticalCount} critical`) : null,
|
|
249
|
+
highCount > 0 ? colors.yellow(`${highCount} high`) : null,
|
|
250
|
+
mediumCount > 0 ? colors.dim(`${mediumCount} medium`) : null,
|
|
251
|
+
lowCount > 0 ? colors.dim(`${lowCount} low`) : null,
|
|
252
|
+
].filter(Boolean);
|
|
253
|
+
if (parts.length === 0) {
|
|
254
|
+
console.log(` ${colors.green('✓')} ${randomCleanScan()}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(` ${parts.join(colors.dim(' · '))}`);
|
|
258
|
+
}
|
|
237
259
|
}
|
|
238
260
|
newline();
|
|
239
261
|
// Per-file results (show worst first)
|
|
@@ -241,7 +263,7 @@ export async function executeTrustCommand(target, options = {}) {
|
|
|
241
263
|
const sorted = [...results].sort((a, b) => a.score - b.score);
|
|
242
264
|
console.log(colors.dim(' FILES'));
|
|
243
265
|
for (const r of sorted.slice(0, 10)) {
|
|
244
|
-
const icon = r.score >= 75 ? colors.green('✓') : r.score >= 60 ? colors.yellow('!') : colors.red('
|
|
266
|
+
const icon = r.score >= 75 ? colors.green('✓') : r.score >= 60 ? colors.yellow('!') : colors.red('✕');
|
|
245
267
|
console.log(` ${icon} ${gradeColor(r.grade)} ${String(r.score).padStart(3)}/100 ${r.filepath}`);
|
|
246
268
|
}
|
|
247
269
|
if (sorted.length > 10) {
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { colors, symbols } from '../../../ui.js';
|
|
25
25
|
import { loadSessionById, loadRecent, } from '../../../infrastructure/orchestration/session-persistence.js';
|
|
26
|
-
import {
|
|
26
|
+
import { assessSession } from '../../../application/services/verdict-service.js';
|
|
27
|
+
import { describeCompleted } from '../../../domain/task.js';
|
|
28
|
+
import { buildCrossVendorVerdict } from '../../../application/use-cases/build-cross-vendor-verdict.js';
|
|
27
29
|
export async function executeVerifyCommand(sessionId, options = {}) {
|
|
28
30
|
// No session id: show a picker list and exit gracefully.
|
|
29
31
|
if (!sessionId) {
|
|
@@ -44,16 +46,13 @@ export async function executeVerifyCommand(sessionId, options = {}) {
|
|
|
44
46
|
}
|
|
45
47
|
return 2;
|
|
46
48
|
}
|
|
47
|
-
const result =
|
|
48
|
-
|
|
49
|
-
artifacts: session.artifacts,
|
|
50
|
-
steps: session.steps,
|
|
51
|
-
});
|
|
49
|
+
const result = assessSession(session);
|
|
50
|
+
const crossVendor = buildCrossVendorVerdict(session);
|
|
52
51
|
if (options.json) {
|
|
53
|
-
process.stdout.write(JSON.stringify(serializeResult(session, result), null, 2) + '\n');
|
|
52
|
+
process.stdout.write(JSON.stringify({ ...serializeResult(session, result), crossVendor: crossVendor ?? null }, null, 2) + '\n');
|
|
54
53
|
}
|
|
55
54
|
else {
|
|
56
|
-
printHumanReadable(session, result);
|
|
55
|
+
printHumanReadable(session, result, crossVendor);
|
|
57
56
|
}
|
|
58
57
|
// Exit code: only `broken` is a hard failure (CI-friendly).
|
|
59
58
|
return result.status === 'broken' ? 1 : 0;
|
|
@@ -97,9 +96,24 @@ function showSessionPicker(json) {
|
|
|
97
96
|
return 0;
|
|
98
97
|
}
|
|
99
98
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
99
|
+
/* Cross-vendor review rendering */
|
|
100
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
101
|
+
function printCrossVendor(cv) {
|
|
102
|
+
process.stdout.write(colors.bold('Cross-vendor review\n'));
|
|
103
|
+
const lead = cv.proven ? colors.success(`${symbols.success} `) : ' ';
|
|
104
|
+
process.stdout.write(`${lead}${cv.summary}\n`);
|
|
105
|
+
for (const c of cv.caveats)
|
|
106
|
+
process.stdout.write(colors.dim(` · ${c}\n`));
|
|
107
|
+
if (cv.reviewText) {
|
|
108
|
+
process.stdout.write('\n' + colors.dim(`${cv.reviewer.agent}'s findings (verbatim · codehere does not judge them):\n`));
|
|
109
|
+
process.stdout.write(colors.gray(truncate(cv.reviewText.replace(/\s+/g, ' ').trim(), 400)) + '\n');
|
|
110
|
+
}
|
|
111
|
+
process.stdout.write('\n');
|
|
112
|
+
}
|
|
113
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
100
114
|
/* Human-readable output */
|
|
101
115
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
102
|
-
function printHumanReadable(session, result) {
|
|
116
|
+
function printHumanReadable(session, result, crossVendor) {
|
|
103
117
|
const shortId = session.sessionId.slice(0, 8);
|
|
104
118
|
const when = formatRelativeTime(session.completedAt);
|
|
105
119
|
// Header
|
|
@@ -136,6 +150,10 @@ function printHumanReadable(session, result) {
|
|
|
136
150
|
// Verdict line · color-coded by status
|
|
137
151
|
const verdict = verdictLine(result);
|
|
138
152
|
process.stdout.write(colors.bold('Fact-check: ') + verdict + '\n\n');
|
|
153
|
+
// Cross-vendor review — when a DIFFERENT vendor reviewed an earlier writer in
|
|
154
|
+
// this room, this is the receipt no single-vendor tool can produce.
|
|
155
|
+
if (crossVendor)
|
|
156
|
+
printCrossVendor(crossVendor);
|
|
139
157
|
// Findings detail
|
|
140
158
|
if (result.findings.length > 0 && result.status !== 'no-report') {
|
|
141
159
|
process.stdout.write(colors.bold('Findings:\n\n'));
|
|
@@ -155,6 +173,50 @@ function printHumanReadable(session, result) {
|
|
|
155
173
|
process.stdout.write(colors.dim(' · universal claims (all tests pass, no failures) vs artifacts.tests.failed\n'));
|
|
156
174
|
process.stdout.write(colors.dim(' · file claims (X files changed, no files changed) vs artifacts.files\n'));
|
|
157
175
|
process.stdout.write(colors.dim('Not checked: prose claims, intent, semantic correctness of changes.\n\n'));
|
|
176
|
+
// Tier 1.1/0.2 · be explicit about which kind of test ✓ this is. A re-run is
|
|
177
|
+
// ground truth (exit-code observed); a scrape is the agent's own claim. The
|
|
178
|
+
// skeptic deserves to know which one they're looking at.
|
|
179
|
+
const tests = session.artifacts?.tests;
|
|
180
|
+
if (tests) {
|
|
181
|
+
if (tests.observed) {
|
|
182
|
+
process.stdout.write(colors.green(`${symbols.success} tests RE-RUN by codehere`) +
|
|
183
|
+
colors.dim(` (${tests.command || 'test suite'}, exit ${tests.exitCode}) · observed, not just reported\n\n`));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
process.stdout.write(colors.dim('Note: test counts above are AS REPORTED by the agent · codehere did not re-run the suite, so the exit code is not independently confirmed.\n\n'));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// R13/M3 · the typed completeness invariant — every verdict input is either
|
|
190
|
+
// present or carries the explicit honest reason it isn't. This block is what
|
|
191
|
+
// makes "the verdict said X" auditable: the reader sees exactly what fed it
|
|
192
|
+
// and what couldn't (e.g. "couldn't observe — no test runner detected"),
|
|
193
|
+
// never a silent blank.
|
|
194
|
+
const completeness = describeCompleted({
|
|
195
|
+
sessionId: session.sessionId,
|
|
196
|
+
agent: session.agent,
|
|
197
|
+
status: session.status,
|
|
198
|
+
completedAt: session.completedAt,
|
|
199
|
+
durationMs: session.durationMs,
|
|
200
|
+
steps: session.steps,
|
|
201
|
+
artifacts: session.artifacts,
|
|
202
|
+
finalReport: session.finalReport,
|
|
203
|
+
});
|
|
204
|
+
process.stdout.write(colors.bold('What fed this verdict\n'));
|
|
205
|
+
const inputLine = (label, input) => 'present' in input
|
|
206
|
+
? ` ${colors.green(symbols.success)} ${label}\n`
|
|
207
|
+
: ` ${colors.dim(symbols.dot)} ${label} ${colors.dim('· ' + input.absent)}\n`;
|
|
208
|
+
process.stdout.write(inputLine('file changes', completeness.files));
|
|
209
|
+
// A trust surface must say WHICH kind of test ✓ this is: a codehere re-run
|
|
210
|
+
// (exit-code observed) or the agent's own scrape (a claim, not ground truth).
|
|
211
|
+
const testsLabel = 'present' in completeness.tests
|
|
212
|
+
? completeness.tests.present.observed
|
|
213
|
+
? 'test result (re-run by codehere)'
|
|
214
|
+
: 'test result (agent-reported, not re-run)'
|
|
215
|
+
: 'test result';
|
|
216
|
+
process.stdout.write(inputLine(testsLabel, completeness.tests));
|
|
217
|
+
process.stdout.write(inputLine('written report', completeness.report));
|
|
218
|
+
process.stdout.write(inputLine('audit reconciliation', completeness.audit));
|
|
219
|
+
process.stdout.write('\n');
|
|
158
220
|
}
|
|
159
221
|
function verdictLine(result) {
|
|
160
222
|
switch (result.status) {
|
|
@@ -166,6 +228,9 @@ function verdictLine(result) {
|
|
|
166
228
|
return colors.error(`${symbols.error} Broken`) + colors.dim(' ' + result.summary);
|
|
167
229
|
case 'no-report':
|
|
168
230
|
return colors.gray(`${symbols.dot} No report`) + colors.dim(' ' + result.summary);
|
|
231
|
+
case 'nothing-to-verify':
|
|
232
|
+
// Honest neutral, not a green pass — the report made no checkable claim.
|
|
233
|
+
return colors.gray(`${symbols.dot} Nothing to verify`) + colors.dim(' ' + result.summary);
|
|
169
234
|
}
|
|
170
235
|
}
|
|
171
236
|
function severityTag(sev) {
|
|
@@ -325,12 +325,4 @@ function getErrorSuggestions(errorType, errorMessage, _context) {
|
|
|
325
325
|
}
|
|
326
326
|
return suggestions;
|
|
327
327
|
}
|
|
328
|
-
/**
|
|
329
|
-
* Display error and exit with appropriate code
|
|
330
|
-
*/
|
|
331
|
-
export function displayErrorAndExit(error, options = {}) {
|
|
332
|
-
const { exitCode = 1, ...displayOptions } = options;
|
|
333
|
-
displayError(error, displayOptions);
|
|
334
|
-
process.exit(exitCode);
|
|
335
|
-
}
|
|
336
328
|
//# sourceMappingURL=error-display.js.map
|
|
@@ -135,6 +135,14 @@ const SLASH_COMMANDS = [
|
|
|
135
135
|
noKey: true,
|
|
136
136
|
subcommand: 'audit',
|
|
137
137
|
},
|
|
138
|
+
{
|
|
139
|
+
name: 'search',
|
|
140
|
+
args: '<query>',
|
|
141
|
+
description: 'Search past sessions · every hit cites its evidence',
|
|
142
|
+
kind: 'sub',
|
|
143
|
+
noKey: true,
|
|
144
|
+
subcommand: 'search',
|
|
145
|
+
},
|
|
138
146
|
{
|
|
139
147
|
name: 'context-source',
|
|
140
148
|
args: '[list|connect|disconnect]',
|
|
@@ -271,11 +279,11 @@ export function buildWelcomeLines(state, version) {
|
|
|
271
279
|
'',
|
|
272
280
|
];
|
|
273
281
|
if (!state.hasHooks) {
|
|
274
|
-
lines.push(' ' + colors.yellow('
|
|
282
|
+
lines.push(' ' + colors.yellow('›') + ' ' + colors.cyan('/hook install') + colors.dim(' to activate safety scanning'));
|
|
275
283
|
}
|
|
276
284
|
else if (!state.hasAgents) {
|
|
277
285
|
lines.push(' ' +
|
|
278
|
-
colors.yellow('
|
|
286
|
+
colors.yellow('›') +
|
|
279
287
|
' Install ' +
|
|
280
288
|
colors.cyan('claude-code') +
|
|
281
289
|
colors.dim(' / ') +
|
|
@@ -596,7 +604,7 @@ async function handleSlash(cmd, args, state) {
|
|
|
596
604
|
return;
|
|
597
605
|
}
|
|
598
606
|
if (cmd === 'exit' || cmd === 'quit' || cmd === 'q') {
|
|
599
|
-
console.log(colors.dim('Goodbye!
|
|
607
|
+
console.log(colors.dim('Goodbye!'));
|
|
600
608
|
process.exit(0);
|
|
601
609
|
}
|
|
602
610
|
if (cmd === 'agents') {
|
|
@@ -694,7 +702,7 @@ async function spawnViaConductor(prompt, state) {
|
|
|
694
702
|
// UAT-002 guard — exit-intent before any spawn / API call.
|
|
695
703
|
const trimmed = prompt.trim().toLowerCase();
|
|
696
704
|
if (EXIT_INTENT_WORDS.has(trimmed)) {
|
|
697
|
-
console.log(colors.dim('Goodbye!
|
|
705
|
+
console.log(colors.dim('Goodbye!'));
|
|
698
706
|
process.exit(0);
|
|
699
707
|
}
|
|
700
708
|
const installed = availableAgents().filter((a) => a.installed);
|
|
@@ -735,7 +743,7 @@ function renderEvent(event) {
|
|
|
735
743
|
case 'codehere:conductor-routing': {
|
|
736
744
|
const data = event.data;
|
|
737
745
|
console.log(colors.dim(' │ ') +
|
|
738
|
-
colors.bold('routing
|
|
746
|
+
colors.bold('routing › ') +
|
|
739
747
|
colors.cyan(data.delegatedTo ?? '?') +
|
|
740
748
|
colors.dim(' · ' + (data.reason ?? '')));
|
|
741
749
|
return null;
|
|
@@ -755,7 +763,7 @@ function renderEvent(event) {
|
|
|
755
763
|
return label;
|
|
756
764
|
}
|
|
757
765
|
if (state === 'done' || state === 'failed') {
|
|
758
|
-
const icon = state === 'done' ? colors.green('✓') : colors.red('
|
|
766
|
+
const icon = state === 'done' ? colors.green('✓') : colors.red('✕');
|
|
759
767
|
console.log(colors.dim(' │ ') + icon + ' ' + label);
|
|
760
768
|
// P22 / Track B #8 — render last 3 lines of step output for done/failed
|
|
761
769
|
if (lastStep.output && String(lastStep.output).trim()) {
|
|
@@ -787,7 +795,7 @@ function renderEvent(event) {
|
|
|
787
795
|
case 'error': {
|
|
788
796
|
const data = event.data;
|
|
789
797
|
if (data?.message)
|
|
790
|
-
console.log(colors.red(' │
|
|
798
|
+
console.log(colors.red(' │ ✕ ') + data.message);
|
|
791
799
|
return null;
|
|
792
800
|
}
|
|
793
801
|
default:
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import http from 'http';
|
|
14
14
|
import { colors } from '../../ui.js';
|
|
15
|
+
import { VERDICT_WORDS, corroborationClause } from '../../application/services/verdict-service.js';
|
|
15
16
|
/** Pull one session's full result from a running daemon's /api/tasks. Matches
|
|
16
17
|
* by exact id or short prefix. Returns null if the daemon is unreachable or
|
|
17
18
|
* the id isn't found. */
|
|
@@ -106,17 +107,42 @@ export function formatDeliverable(r) {
|
|
|
106
107
|
lines.push(colors.dim('Files: none changed.'));
|
|
107
108
|
}
|
|
108
109
|
// 3 · Did the report's claims check out? (verifier cross-check)
|
|
109
|
-
if (r.
|
|
110
|
-
const v = r.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
if (r.verdict?.claim) {
|
|
111
|
+
const v = r.verdict.claim;
|
|
112
|
+
// Read the one reconciled verdict (the composite grade owns it), falling back
|
|
113
|
+
// to the raw claim status only for legacy/seeded tasks with no grade. The
|
|
114
|
+
// words come from the shared VERDICT_WORDS so the CLI says exactly what the
|
|
115
|
+
// web app says for the same state — a green composite grade reads "Verified"
|
|
116
|
+
// (a single cross-vendor review CORROBORATES; the CLI never says "Proven"
|
|
117
|
+
// for one reviewer, matching the web seal's "Corroborated by <vendor>").
|
|
118
|
+
const byHeadline = {
|
|
119
|
+
proven: 'verified',
|
|
120
|
+
broken: 'caught',
|
|
121
|
+
reported: 'reported',
|
|
122
|
+
'nothing-to-verify': 'unverified',
|
|
123
|
+
'no-report': 'unverified',
|
|
124
|
+
};
|
|
125
|
+
const byStatus = {
|
|
126
|
+
verified: 'verified',
|
|
127
|
+
broken: 'caught',
|
|
128
|
+
partial: 'partial',
|
|
129
|
+
};
|
|
130
|
+
const grade = r.verdict.grade;
|
|
131
|
+
const key = (grade && byHeadline[grade.headline]) || byStatus[v.status] || 'unverified';
|
|
132
|
+
const tone = key === 'proven' || key === 'verified'
|
|
133
|
+
? colors.success
|
|
134
|
+
: key === 'caught'
|
|
135
|
+
? colors.error
|
|
136
|
+
: key === 'reported' || key === 'partial'
|
|
137
|
+
? colors.warning
|
|
138
|
+
: colors.dim;
|
|
139
|
+
// For a green composite grade the sub-clause names the ACTUAL independent
|
|
140
|
+
// signal that earned it (matching the web seal) — not the claim summary,
|
|
141
|
+
// which for a nothing-to-verify claim would contradict the green ("Verified
|
|
142
|
+
// · the report makes no checkable claim").
|
|
143
|
+
const sub = grade && grade.green ? corroborationClause(grade) : v.summary;
|
|
118
144
|
lines.push('');
|
|
119
|
-
lines.push(`${colors.bold('Check:')} ${
|
|
145
|
+
lines.push(`${colors.bold('Check:')} ${tone(VERDICT_WORDS[key])} · ${sub}`);
|
|
120
146
|
for (const f of v.findings.filter((x) => x.severity !== 'info')) {
|
|
121
147
|
lines.push(colors.dim(` • ${f.message}`));
|
|
122
148
|
}
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import { EventEmitter } from 'events';
|
|
33
33
|
import { Readable, Writable } from 'stream';
|
|
34
|
-
import { loadCassette } from './cassette.js';
|
|
35
34
|
// ── Active cassette holder (single-cassette-per-test pattern) ───────────────
|
|
36
35
|
let _activeCassette;
|
|
37
36
|
export function setActiveCassette(c) {
|
|
@@ -145,22 +144,4 @@ function makeReplayChild(cassette) {
|
|
|
145
144
|
timers.push(termHandle);
|
|
146
145
|
return child;
|
|
147
146
|
}
|
|
148
|
-
/** Test convenience: load cassette, set it active, spawn via SessionManager.
|
|
149
|
-
*
|
|
150
|
-
* Callers MUST install `vi.mock('child_process', factory)` at the top of
|
|
151
|
-
* their test file with a factory that delegates `spawn` to
|
|
152
|
-
* `createReplaySpawn(getActiveCassette())`. See cassette-replay.test.ts
|
|
153
|
-
* for the canonical setup.
|
|
154
|
-
*
|
|
155
|
-
* Returns the spawned TaskSession (typed loosely to avoid pulling the
|
|
156
|
-
* whole adapter type tree into src/testing/).
|
|
157
|
-
*/
|
|
158
|
-
export async function replayThroughAdapter(cassettePath, sessionManager) {
|
|
159
|
-
const cassette = loadCassette(cassettePath);
|
|
160
|
-
setActiveCassette(cassette);
|
|
161
|
-
const session = sessionManager.spawn(cassette.header.meta.prompt, cassette.header.meta.adapter, {
|
|
162
|
-
cwd: cassette.header.meta.cwd_hint,
|
|
163
|
-
});
|
|
164
|
-
return session;
|
|
165
|
-
}
|
|
166
147
|
//# sourceMappingURL=replay-adapter.js.map
|