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.
Files changed (134) 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/plan-auto-review.js +57 -0
  11. package/dist/application/use-cases/reconcile-audit.js +100 -0
  12. package/dist/application/use-cases/rerun-tests.js +226 -0
  13. package/dist/application/use-cases/search-sessions.js +70 -0
  14. package/dist/application/use-cases/should-auto-review.js +71 -0
  15. package/dist/application/use-cases/thread-export.js +90 -0
  16. package/dist/application/use-cases/verify-claim.js +187 -5
  17. package/dist/domain/audit.js +11 -0
  18. package/dist/domain/services/codebase-detector.js +0 -6
  19. package/dist/domain/task.js +91 -0
  20. package/dist/error-handler.js +3 -115
  21. package/dist/index.js +269 -25
  22. package/dist/infrastructure/completion/bash-completion.js +7 -3
  23. package/dist/infrastructure/completion/fish-completion.js +7 -12
  24. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  25. package/dist/infrastructure/config/config-manager.js +4 -3
  26. package/dist/infrastructure/config/product-context.js +1 -19
  27. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  28. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  29. package/dist/infrastructure/context/github-api.js +110 -0
  30. package/dist/infrastructure/context/github-device-flow.js +65 -0
  31. package/dist/infrastructure/context/oauth-store.js +5 -2
  32. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  33. package/dist/infrastructure/cost/budget-config.js +3 -2
  34. package/dist/infrastructure/cost/cost-router.js +16 -23
  35. package/dist/infrastructure/cost/spend-aggregator.js +90 -15
  36. package/dist/infrastructure/cost/team-ledger.js +355 -0
  37. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  38. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  39. package/dist/infrastructure/hooks/claude-sdk-hook.js +133 -3
  40. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  41. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  42. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  43. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  44. package/dist/infrastructure/orchestration/agents-config-seed.js +22 -10
  45. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  46. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  47. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  48. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  49. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  50. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  51. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  52. package/dist/infrastructure/orchestration/failover-config.js +0 -10
  53. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  54. package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
  55. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  56. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
  57. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  58. package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
  59. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  60. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  61. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  62. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  63. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  64. package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
  65. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  66. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  67. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  68. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  69. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  70. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  71. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  72. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  73. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  74. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  75. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  76. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  77. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  78. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  79. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  80. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  81. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  82. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  83. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  84. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  85. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  86. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  87. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  88. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4214
  89. package/dist/infrastructure/protocols/verify-page.js +849 -155
  90. package/dist/infrastructure/security/fix-code.js +144 -0
  91. package/dist/infrastructure/security/fix-secret.js +89 -0
  92. package/dist/infrastructure/security/permissions-config.js +3 -2
  93. package/dist/infrastructure/security/scan-types.js +10 -0
  94. package/dist/infrastructure/security/ship-scan.js +195 -0
  95. package/dist/infrastructure/security/trust-scorer.js +70 -3
  96. package/dist/infrastructure/security/vibe-checks.js +99 -0
  97. package/dist/infrastructure/storage/atomic-write.js +86 -0
  98. package/dist/infrastructure/storage/project-thread-mirror.js +150 -3
  99. package/dist/infrastructure/storage/project-thread-reader.js +261 -24
  100. package/dist/monitoring.js +2 -2
  101. package/dist/presentation/cli/commands/audit-command.js +79 -4
  102. package/dist/presentation/cli/commands/budget-command.js +2 -2
  103. package/dist/presentation/cli/commands/check-command.js +96 -0
  104. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  105. package/dist/presentation/cli/commands/help-command.js +5 -5
  106. package/dist/presentation/cli/commands/hook-command.js +5 -5
  107. package/dist/presentation/cli/commands/init-command.js +26 -14
  108. package/dist/presentation/cli/commands/memory-command.js +28 -28
  109. package/dist/presentation/cli/commands/permission-command.js +1 -12
  110. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  111. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  112. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  113. package/dist/presentation/cli/commands/report-command.js +48 -32
  114. package/dist/presentation/cli/commands/scan-command.js +52 -0
  115. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  116. package/dist/presentation/cli/commands/search-command.js +51 -0
  117. package/dist/presentation/cli/commands/setup-command.js +31 -8
  118. package/dist/presentation/cli/commands/status-command.js +39 -86
  119. package/dist/presentation/cli/commands/team-command.js +256 -0
  120. package/dist/presentation/cli/commands/thread-command.js +87 -5
  121. package/dist/presentation/cli/commands/trust-command.js +53 -31
  122. package/dist/presentation/cli/commands/verify-command.js +74 -9
  123. package/dist/presentation/cli/error-display.js +0 -8
  124. package/dist/presentation/cli/repl.js +15 -7
  125. package/dist/presentation/cli/task-deliverable.js +36 -10
  126. package/dist/testing/replay-adapter.js +0 -19
  127. package/dist/ui.js +51 -239
  128. package/dist/utils/version.js +0 -6
  129. package/package.json +30 -10
  130. package/dist/domain/entities/task.js +0 -61
  131. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  132. package/dist/infrastructure/security/input-validator.js +0 -152
  133. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  134. 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, extra, options = {}) {
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
- colors.dim(` view it: `) +
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 — can't remove)\n`));
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
- const totalFindings = results.reduce((s, r) => s + r.findings.length, 0);
178
- const criticalCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'critical').length, 0);
179
- const highCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'high').length, 0);
180
- const mediumCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'medium').length, 0);
181
- const lowCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'low').length, 0);
182
- const overallScore = results.length > 0 ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length) : 100;
183
- const grade = overallScore >= 90 ? 'A' : overallScore >= 75 ? 'B' : overallScore >= 60 ? 'C' : overallScore >= 40 ? 'D' : 'F';
184
- const baseRecommendation = criticalCount > 0
185
- ? `${criticalCount} critical ${criticalCount === 1 ? 'issue' : 'issues'} — worth sorting before this merges.`
186
- : highCount > 0
187
- ? `Nothing critical, but ${highCount} high-severity ${highCount === 1 ? 'pattern' : 'patterns'} worth a look.`
188
- : mediumCount + lowCount > 0
189
- ? `${mediumCount + lowCount} low-priority ${mediumCount + lowCount === 1 ? 'finding' : 'findings'} — nothing blocking, but worth a pass.`
190
- : 'Clean scan. Nothing blocking a merge.';
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
- console.log(` Score ${scoreBar(overallScore)} ${colors.bold(String(overallScore))}/100 ${gradeColor(grade)}`);
224
- newline();
225
- // Findings summary (inline, compact)
226
- const parts = [
227
- criticalCount > 0 ? colors.red(`${criticalCount} critical`) : null,
228
- highCount > 0 ? colors.yellow(`${highCount} high`) : null,
229
- mediumCount > 0 ? colors.dim(`${mediumCount} medium`) : null,
230
- lowCount > 0 ? colors.dim(`${lowCount} low`) : null,
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(` ${parts.join(colors.dim(' · '))}`);
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 { verifyClaim } from '../../../application/use-cases/verify-claim.js';
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 = verifyClaim({
48
- summary: session.finalReport?.summary,
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('→') + ' ' + colors.cyan('/hook install') + colors.dim(' to activate safety scanning'));
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(' │ ✗ ') + data.message);
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.verifyResult) {
110
- const v = r.verifyResult;
111
- const label = v.status === 'verified'
112
- ? colors.success('verified')
113
- : v.status === 'broken'
114
- ? colors.error('does not match reality')
115
- : v.status === 'partial'
116
- ? colors.warning('partly unverifiable')
117
- : colors.dim('nothing to check');
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:')} ${label} — ${v.summary}`);
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