codehere 0.6.1 → 0.7.0

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 (81) hide show
  1. package/README.md +5 -3
  2. package/dist/application/use-cases/domain-trust.js +110 -0
  3. package/dist/application/use-cases/generate-final-report.js +84 -5
  4. package/dist/application/use-cases/generate-narration.js +20 -3
  5. package/dist/application/use-cases/generate-thread-title.js +4 -0
  6. package/dist/application/use-cases/thread-export.js +155 -0
  7. package/dist/application/use-cases/verify-claim.js +274 -0
  8. package/dist/application/use-cases/write-pass.js +277 -0
  9. package/dist/index.js +218 -19
  10. package/dist/infrastructure/completion/bash-completion.js +2 -2
  11. package/dist/infrastructure/config/config-manager.js +1 -1
  12. package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
  13. package/dist/infrastructure/context/adapters/figma-source.js +163 -0
  14. package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
  15. package/dist/infrastructure/context/adapters/linear-source.js +190 -0
  16. package/dist/infrastructure/context/context-resolver.js +144 -0
  17. package/dist/infrastructure/context/context-source.js +114 -0
  18. package/dist/infrastructure/context/oauth-flow.js +207 -0
  19. package/dist/infrastructure/context/oauth-store.js +119 -0
  20. package/dist/infrastructure/context/registry-bootstrap.js +99 -0
  21. package/dist/infrastructure/context/write-pass-resolver.js +106 -0
  22. package/dist/infrastructure/cost/budget-config.js +1 -1
  23. package/dist/infrastructure/cost/spend-aggregator.js +5 -5
  24. package/dist/infrastructure/hooks/claude-sdk-hook.js +79 -59
  25. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  26. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  27. package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
  28. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  29. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  30. package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
  31. package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
  32. package/dist/infrastructure/orchestration/context-handoff.js +4 -3
  33. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  34. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  35. package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
  36. package/dist/infrastructure/orchestration/graphify-context.js +6 -13
  37. package/dist/infrastructure/orchestration/memory-injector.js +11 -16
  38. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
  39. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  40. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  41. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  42. package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
  43. package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
  44. package/dist/infrastructure/orchestration/session-manager.js +142 -29
  45. package/dist/infrastructure/orchestration/session-persistence.js +139 -30
  46. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  47. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  48. package/dist/infrastructure/protocols/cloud-sync.js +10 -11
  49. package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
  50. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  51. package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
  52. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  53. package/dist/infrastructure/protocols/verify-page.js +941 -0
  54. package/dist/infrastructure/security/trust-scorer.js +76 -1
  55. package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
  56. package/dist/infrastructure/storage/project-thread-migration.js +14 -9
  57. package/dist/infrastructure/storage/project-thread-reader.js +11 -1
  58. package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
  59. package/dist/monitoring.js +1 -1
  60. package/dist/presentation/cli/commands/audit-command.js +126 -0
  61. package/dist/presentation/cli/commands/budget-command.js +2 -2
  62. package/dist/presentation/cli/commands/config-command.js +2 -2
  63. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  64. package/dist/presentation/cli/commands/help-command.js +51 -1
  65. package/dist/presentation/cli/commands/init-command.js +5 -4
  66. package/dist/presentation/cli/commands/memory-command.js +29 -15
  67. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  68. package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
  69. package/dist/presentation/cli/commands/report-command.js +6 -4
  70. package/dist/presentation/cli/commands/setup-command.js +71 -18
  71. package/dist/presentation/cli/commands/status-command.js +0 -4
  72. package/dist/presentation/cli/commands/thread-command.js +288 -0
  73. package/dist/presentation/cli/commands/verify-command.js +233 -0
  74. package/dist/presentation/cli/error-display.js +1 -1
  75. package/dist/presentation/cli/repl.js +52 -2
  76. package/dist/presentation/cli/task-deliverable.js +127 -0
  77. package/dist/testing/cassette.js +125 -0
  78. package/dist/testing/replay-adapter.js +166 -0
  79. package/dist/ui.js +54 -29
  80. package/dist/utils/typo-guard.js +1 -1
  81. package/package.json +4 -2
@@ -0,0 +1,288 @@
1
+ /**
2
+ * CLI: `codehere thread` · v0.8 S4 · signed-payload share + import
3
+ *
4
+ * Subcommands:
5
+ * export <session-id> [--out <file>] [--json]
6
+ * Sign the session bundle and write it to <file> (or stdout when no --out).
7
+ * import <file> [--trust] [--json]
8
+ * Verify the signature. If the signer's key isn't trusted, refuse unless
9
+ * --trust is set OR the user is on a TTY and confirms. On valid+trusted,
10
+ * merge the session into ~/.codehere/sessions.jsonl so it appears in the
11
+ * dashboard.
12
+ * keys · print the local public key
13
+ * trust <pubkey> [label] · add a public key to the trusted set
14
+ * untrust <pubkey> · remove
15
+ *
16
+ * No network. Both export and import are local file operations.
17
+ */
18
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
19
+ import { homedir, hostname } from 'os';
20
+ import { dirname, join, isAbsolute, resolve } from 'path';
21
+ import { colors, symbols } from '../../../ui.js';
22
+ import { signBundle, verifyBundle, shortFingerprint, } from '../../../application/use-cases/thread-export.js';
23
+ import { loadOrCreateKeypair, localPublicKeyBase64, trustKey, untrustKey, trustedKeySet, loadTrustedKeys, isKeyTrusted, } from '../../../infrastructure/orchestration/thread-key-store.js';
24
+ import { loadSessionById } from '../../../infrastructure/orchestration/session-persistence.js';
25
+ import { getVersion } from '../../../utils/version.js';
26
+ export function executeThreadCommand(sub, arg, extra, options = {}) {
27
+ switch (sub) {
28
+ case 'export':
29
+ return exportThread(arg, options);
30
+ case 'import':
31
+ return importThread(arg, options);
32
+ case 'keys':
33
+ return showKeys(options.json ?? false);
34
+ case 'trust':
35
+ return trustCmd(arg, options.label, options.json ?? false);
36
+ case 'untrust':
37
+ return untrustCmd(arg, options.json ?? false);
38
+ default:
39
+ process.stderr.write(colors.error(`${symbols.error} Unknown subcommand: ${sub ?? '(none)'}\n`) +
40
+ colors.dim('Try: codehere thread export <id> | import <file> | keys | trust <key> | untrust <key>\n'));
41
+ return 2;
42
+ }
43
+ }
44
+ /* ──────────────────────────────────────────────────────────────────────── */
45
+ /* export */
46
+ /* ──────────────────────────────────────────────────────────────────────── */
47
+ function exportThread(sessionId, options) {
48
+ if (!sessionId) {
49
+ process.stderr.write(colors.error(`${symbols.error} Need a session id\n`));
50
+ return 2;
51
+ }
52
+ const session = loadSessionById(sessionId);
53
+ if (!session) {
54
+ process.stderr.write(colors.error(`${symbols.error} No session found matching "${sessionId}"\n`));
55
+ return 2;
56
+ }
57
+ const keys = loadOrCreateKeypair();
58
+ const body = {
59
+ schema: 1,
60
+ kind: 'thread',
61
+ sessionId: session.sessionId,
62
+ session,
63
+ exporter: {
64
+ hostname: safeHostname(),
65
+ codehereVersion: getVersion(),
66
+ },
67
+ exportedAt: new Date().toISOString(),
68
+ };
69
+ const bundle = signBundle(body, keys);
70
+ const text = JSON.stringify(bundle, null, 2);
71
+ if (options.out) {
72
+ const outPath = isAbsolute(options.out) ? options.out : resolve(process.cwd(), options.out);
73
+ mkdirSync(dirname(outPath), { recursive: true });
74
+ writeFileSync(outPath, text, 'utf-8');
75
+ if (options.json) {
76
+ process.stdout.write(JSON.stringify({ ok: true, path: outPath, fingerprint: shortFingerprint(bundle.publicKey) }) + '\n');
77
+ }
78
+ else {
79
+ process.stdout.write(colors.green(`${symbols.success} Exported`) +
80
+ colors.dim(` ${outPath}\n`) +
81
+ colors.dim(` signed by ${shortFingerprint(bundle.publicKey)}\n`) +
82
+ colors.dim(` share via Slack/email/PR · recipient runs: `) +
83
+ colors.bold(`codehere thread import ${outPath}\n`));
84
+ }
85
+ return 0;
86
+ }
87
+ // No --out · print to stdout (machine-pipe-friendly)
88
+ process.stdout.write(text + '\n');
89
+ return 0;
90
+ }
91
+ /* ──────────────────────────────────────────────────────────────────────── */
92
+ /* import */
93
+ /* ──────────────────────────────────────────────────────────────────────── */
94
+ function importThread(filePath, options) {
95
+ if (!filePath) {
96
+ process.stderr.write(colors.error(`${symbols.error} Need a file path\n`));
97
+ return 2;
98
+ }
99
+ const fp = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
100
+ if (!existsSync(fp)) {
101
+ process.stderr.write(colors.error(`${symbols.error} Not found: ${fp}\n`));
102
+ return 2;
103
+ }
104
+ let raw;
105
+ try {
106
+ raw = JSON.parse(readFileSync(fp, 'utf-8'));
107
+ }
108
+ catch (err) {
109
+ process.stderr.write(colors.error(`${symbols.error} Invalid JSON: ${err instanceof Error ? err.message : String(err)}\n`));
110
+ return 2;
111
+ }
112
+ const verdict = verifyBundle(raw, trustedKeySet());
113
+ if (verdict.status === 'malformed') {
114
+ if (options.json) {
115
+ process.stdout.write(JSON.stringify({ ok: false, error: 'malformed', detail: verdict.reason }) + '\n');
116
+ }
117
+ else {
118
+ process.stderr.write(colors.error(`${symbols.error} Malformed bundle: ${verdict.reason}\n`));
119
+ }
120
+ return 2;
121
+ }
122
+ if (verdict.status === 'tampered') {
123
+ if (options.json) {
124
+ process.stdout.write(JSON.stringify({ ok: false, error: 'tampered', detail: verdict.reason }) + '\n');
125
+ }
126
+ else {
127
+ process.stderr.write(colors.error(`${symbols.error} Signature INVALID · refusing to import\n`) +
128
+ colors.dim(` ${verdict.reason}\n`));
129
+ }
130
+ return 1;
131
+ }
132
+ // status: 'valid' or 'unknown_key'
133
+ const bundle = verdict.status === 'valid' ? verdict.bundle : verdict.bundle;
134
+ const fp_key = shortFingerprint(bundle.publicKey);
135
+ if (verdict.status === 'unknown_key') {
136
+ if (!options.trust) {
137
+ if (options.json) {
138
+ process.stdout.write(JSON.stringify({
139
+ ok: false,
140
+ error: 'unknown_key',
141
+ publicKey: bundle.publicKey,
142
+ fingerprint: fp_key,
143
+ hint: 'Re-run with --trust to add this key to your trust store and import.',
144
+ }) + '\n');
145
+ }
146
+ else {
147
+ process.stderr.write(colors.warning(`${symbols.warning} Signer's key is not in your trust store\n`) +
148
+ colors.dim(` fingerprint: ${fp_key}\n`) +
149
+ colors.dim(` The signature is cryptographically valid but you've never trusted this key.\n`) +
150
+ colors.dim(` To trust + import: `) +
151
+ colors.bold(`codehere thread import ${fp} --trust\n`) +
152
+ colors.dim(` To inspect first: `) +
153
+ colors.bold(`codehere thread trust ${bundle.publicKey} <label>`) +
154
+ '\n');
155
+ }
156
+ return 2;
157
+ }
158
+ // --trust set · add key to store and continue
159
+ trustKey(bundle.publicKey, options.label);
160
+ }
161
+ // Persist into ~/.codehere/sessions.jsonl as 3 records (spawned, completed, finalReport)
162
+ const ses = bundle.body.session;
163
+ const home = process.env.CODEHERE_HOME || homedir();
164
+ const sessionsPath = join(home, '.codehere', 'sessions.jsonl');
165
+ mkdirSync(dirname(sessionsPath), { recursive: true });
166
+ const lines = [];
167
+ lines.push(JSON.stringify({
168
+ t: 'spawned',
169
+ id: ses.sessionId,
170
+ agent: ses.agent,
171
+ prompt: ses.prompt,
172
+ cwd: ses.cwd,
173
+ startedAt: ses.startedAt,
174
+ }));
175
+ lines.push(JSON.stringify({
176
+ t: 'completed',
177
+ id: ses.sessionId,
178
+ status: ses.status,
179
+ durationMs: ses.durationMs,
180
+ completedAt: ses.completedAt,
181
+ steps: ses.steps,
182
+ artifacts: ses.artifacts,
183
+ }));
184
+ if (ses.finalReport) {
185
+ lines.push(JSON.stringify({
186
+ t: 'finalReport',
187
+ id: ses.sessionId,
188
+ summary: ses.finalReport.summary,
189
+ generatedAt: ses.finalReport.generatedAt,
190
+ durationMs: ses.finalReport.durationMs,
191
+ }));
192
+ }
193
+ appendFileSync(sessionsPath, lines.join('\n') + '\n', 'utf-8');
194
+ if (options.json) {
195
+ process.stdout.write(JSON.stringify({
196
+ ok: true,
197
+ sessionId: ses.sessionId,
198
+ trusted: verdict.status === 'valid' ? true : 'just-added',
199
+ fingerprint: fp_key,
200
+ }) + '\n');
201
+ }
202
+ else {
203
+ 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: `) +
206
+ colors.bold(`codehere start`) +
207
+ colors.dim(` then click the task in the dashboard\n`));
208
+ }
209
+ return 0;
210
+ }
211
+ /* ──────────────────────────────────────────────────────────────────────── */
212
+ /* keys / trust / untrust */
213
+ /* ──────────────────────────────────────────────────────────────────────── */
214
+ function showKeys(json) {
215
+ loadOrCreateKeypair();
216
+ const localB64 = localPublicKeyBase64() ?? '';
217
+ const trusted = loadTrustedKeys();
218
+ if (json) {
219
+ process.stdout.write(JSON.stringify({
220
+ local: { publicKey: localB64, fingerprint: shortFingerprint(localB64) },
221
+ trusted: Object.entries(trusted.keys).map(([k, v]) => ({
222
+ publicKey: k,
223
+ fingerprint: shortFingerprint(k),
224
+ label: v.label,
225
+ addedAt: v.addedAt,
226
+ })),
227
+ }, null, 2) + '\n');
228
+ return 0;
229
+ }
230
+ process.stdout.write('\n' + colors.bold('This machine') + '\n');
231
+ process.stdout.write(' ' + colors.cyan(shortFingerprint(localB64)) + colors.dim(' signs your exports\n\n'));
232
+ process.stdout.write(colors.bold('Trusted keys') + colors.dim(' (imports from these accepted silently)\n'));
233
+ const entries = Object.entries(trusted.keys);
234
+ if (entries.length === 0) {
235
+ process.stdout.write(' ' + colors.dim('(empty)\n'));
236
+ }
237
+ else {
238
+ for (const [k, v] of entries) {
239
+ const label = v.label || '(no label)';
240
+ process.stdout.write(' ' + colors.cyan(shortFingerprint(k)) + ' ' + colors.dim(label) + '\n');
241
+ }
242
+ }
243
+ process.stdout.write('\n' + colors.dim('Add a key: ') + colors.bold('codehere thread trust <pubkey> <label>') + '\n');
244
+ process.stdout.write(colors.dim('Remove a key: ') + colors.bold('codehere thread untrust <pubkey>') + '\n\n');
245
+ return 0;
246
+ }
247
+ function trustCmd(pubkey, label, json) {
248
+ if (!pubkey) {
249
+ process.stderr.write(colors.error(`${symbols.error} Need a public key (base64)\n`));
250
+ return 2;
251
+ }
252
+ trustKey(pubkey, label);
253
+ if (json) {
254
+ process.stdout.write(JSON.stringify({ ok: true, fingerprint: shortFingerprint(pubkey) }) + '\n');
255
+ }
256
+ else {
257
+ process.stdout.write(colors.green(`${symbols.success} trusted ${shortFingerprint(pubkey)}\n`));
258
+ }
259
+ return 0;
260
+ }
261
+ function untrustCmd(pubkey, json) {
262
+ if (!pubkey) {
263
+ process.stderr.write(colors.error(`${symbols.error} Need a public key (base64)\n`));
264
+ return 2;
265
+ }
266
+ const ok = untrustKey(pubkey);
267
+ if (json) {
268
+ process.stdout.write(JSON.stringify({ ok }) + '\n');
269
+ }
270
+ else if (ok) {
271
+ process.stdout.write(colors.green(`${symbols.success} untrusted ${shortFingerprint(pubkey)}\n`));
272
+ }
273
+ 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`));
275
+ }
276
+ return 0;
277
+ }
278
+ function safeHostname() {
279
+ try {
280
+ return hostname();
281
+ }
282
+ catch {
283
+ return undefined;
284
+ }
285
+ }
286
+ // Public alias for the connection-status check used in the daemon HTTP routes.
287
+ export { isKeyTrusted };
288
+ //# sourceMappingURL=thread-command.js.map
@@ -0,0 +1,233 @@
1
+ /**
2
+ * CLI command: verify
3
+ *
4
+ * v0.7 verifier-agent · Slice 2 (CLI surface for the pure verify-claim use case).
5
+ *
6
+ * Compares what the agent's Final Report TEXT claims against the structured
7
+ * reality (artifacts + steps) captured during the session. Catches the
8
+ * easy lies before they reach a PR description.
9
+ *
10
+ * Usage:
11
+ * codehere verify <session-id> # run verifier on a completed session
12
+ * codehere verify <session-id> --json # machine-readable output for CI
13
+ * codehere verify # list recent sessions to pick from
14
+ *
15
+ * Exit codes:
16
+ * 0 = verified OR partial (warnings only · unverifiable claims)
17
+ * 0 = no-report (nothing to verify; not a failure)
18
+ * 1 = broken (at least one claim contradicts recorded reality)
19
+ * 2 = session not found / invalid input
20
+ *
21
+ * The verifier is intentionally conservative: only claims that map to
22
+ * structured data are checked. Prose claims pass through silently.
23
+ */
24
+ import { colors, symbols } from '../../../ui.js';
25
+ import { loadSessionById, loadRecent, } from '../../../infrastructure/orchestration/session-persistence.js';
26
+ import { verifyClaim } from '../../../application/use-cases/verify-claim.js';
27
+ export async function executeVerifyCommand(sessionId, options = {}) {
28
+ // No session id: show a picker list and exit gracefully.
29
+ if (!sessionId) {
30
+ return showSessionPicker(options.json ?? false);
31
+ }
32
+ const session = loadSessionById(sessionId);
33
+ if (!session) {
34
+ if (options.json) {
35
+ process.stdout.write(JSON.stringify({
36
+ error: 'session-not-found',
37
+ sessionId,
38
+ hint: 'Run `codehere verify` (no args) to list recent sessions.',
39
+ }, null, 2) + '\n');
40
+ }
41
+ else {
42
+ process.stderr.write(colors.error(`${symbols.error} No completed session found matching "${sessionId}".\n`) +
43
+ colors.dim(' Tip: run `codehere verify` (no args) to see recent sessions.\n'));
44
+ }
45
+ return 2;
46
+ }
47
+ const result = verifyClaim({
48
+ summary: session.finalReport?.summary,
49
+ artifacts: session.artifacts,
50
+ steps: session.steps,
51
+ });
52
+ if (options.json) {
53
+ process.stdout.write(JSON.stringify(serializeResult(session, result), null, 2) + '\n');
54
+ }
55
+ else {
56
+ printHumanReadable(session, result);
57
+ }
58
+ // Exit code: only `broken` is a hard failure (CI-friendly).
59
+ return result.status === 'broken' ? 1 : 0;
60
+ }
61
+ /* ──────────────────────────────────────────────────────────────────────── */
62
+ /* Picker mode · no session id given */
63
+ /* ──────────────────────────────────────────────────────────────────────── */
64
+ function showSessionPicker(json) {
65
+ const recent = loadRecent(15);
66
+ if (recent.length === 0) {
67
+ if (json) {
68
+ process.stdout.write(JSON.stringify({ sessions: [], note: 'No completed sessions found.' }, null, 2) + '\n');
69
+ }
70
+ else {
71
+ process.stdout.write(colors.dim('No completed sessions found. Run a task first, then verify it:\n') +
72
+ colors.dim(' codehere task "describe what you want done"\n'));
73
+ }
74
+ return 0;
75
+ }
76
+ if (json) {
77
+ const items = recent.map((s) => ({
78
+ sessionId: s.sessionId,
79
+ agent: s.agent,
80
+ prompt: s.prompt,
81
+ completedAt: s.completedAt,
82
+ hasFinalReport: Boolean(s.finalReport),
83
+ }));
84
+ process.stdout.write(JSON.stringify({ sessions: items }, null, 2) + '\n');
85
+ return 0;
86
+ }
87
+ process.stdout.write('\n' + colors.bold('Recent sessions') + colors.dim(' (pass --json for machine output)\n\n'));
88
+ for (const s of recent) {
89
+ const shortId = s.sessionId.slice(0, 8);
90
+ const when = formatRelativeTime(s.completedAt);
91
+ const reportTag = s.finalReport ? colors.gray('· report ✓') : colors.gray('· no report');
92
+ const promptLine = truncate(s.prompt.replace(/\s+/g, ' ').trim(), 70);
93
+ process.stdout.write(` ${colors.cyan(shortId)} ${colors.gray(s.agent.padEnd(11))}${colors.dim(when.padEnd(14))} ${reportTag}\n` +
94
+ ` ${colors.dim(promptLine)}\n\n`);
95
+ }
96
+ process.stdout.write(colors.dim('To verify one of these: ') + colors.bold('codehere verify <id>') + '\n\n');
97
+ return 0;
98
+ }
99
+ /* ──────────────────────────────────────────────────────────────────────── */
100
+ /* Human-readable output */
101
+ /* ──────────────────────────────────────────────────────────────────────── */
102
+ function printHumanReadable(session, result) {
103
+ const shortId = session.sessionId.slice(0, 8);
104
+ const when = formatRelativeTime(session.completedAt);
105
+ // Header
106
+ process.stdout.write('\n' +
107
+ colors.dim('session ') +
108
+ colors.cyan(shortId) +
109
+ colors.dim(' · ') +
110
+ colors.gray(session.agent) +
111
+ colors.dim(' · ') +
112
+ colors.gray(when) +
113
+ '\n' +
114
+ colors.dim('task: ') +
115
+ truncate(session.prompt.replace(/\s+/g, ' ').trim(), 80) +
116
+ '\n\n');
117
+ // What the agent did — lead with the deliverable, not the audit. A
118
+ // non-technical user opening this wants "what happened" first, then "does
119
+ // it check out". Both come straight from recorded state (no screen lies).
120
+ if (session.finalReport?.summary) {
121
+ process.stdout.write(session.finalReport.summary.trim() + '\n\n');
122
+ }
123
+ // Files changed + trust, or the honest can't-track note.
124
+ const files = session.artifacts?.files ?? [];
125
+ if (session.artifacts?.noGitTracking) {
126
+ process.stdout.write(colors.dim("Files: codehere couldn't track changes here (this folder isn't a git repository).\n\n"));
127
+ }
128
+ else if (files.length > 0) {
129
+ process.stdout.write(colors.bold('Files changed:\n'));
130
+ for (const f of files) {
131
+ const trust = typeof f.trust === 'number' ? colors.dim(` · trust ${f.trust}/100`) : '';
132
+ process.stdout.write(` • ${f.path} ${colors.success('+' + f.added)}/${colors.error('-' + f.removed)}${trust}\n`);
133
+ }
134
+ process.stdout.write('\n');
135
+ }
136
+ // Verdict line · color-coded by status
137
+ const verdict = verdictLine(result);
138
+ process.stdout.write(colors.bold('Fact-check: ') + verdict + '\n\n');
139
+ // Findings detail
140
+ if (result.findings.length > 0 && result.status !== 'no-report') {
141
+ process.stdout.write(colors.bold('Findings:\n\n'));
142
+ for (let i = 0; i < result.findings.length; i++) {
143
+ const f = result.findings[i];
144
+ const num = colors.dim(` ${(i + 1).toString().padStart(2)}. `);
145
+ const sev = severityTag(f.severity);
146
+ const kind = colors.gray(`[${f.kind}]`);
147
+ process.stdout.write(`${num}${sev} ${kind} ${f.message}\n`);
148
+ process.stdout.write(` ${colors.dim('claim:')} ${colors.gray(f.claim)}\n`);
149
+ process.stdout.write(` ${colors.dim('reality:')} ${colors.gray(f.reality)}\n\n`);
150
+ }
151
+ }
152
+ // What was checked (so users know the verifier's scope)
153
+ process.stdout.write(colors.dim('What was checked:\n'));
154
+ process.stdout.write(colors.dim(' · numeric test claims (X tests passed, X of Y, etc) vs artifacts.tests\n'));
155
+ process.stdout.write(colors.dim(' · universal claims (all tests pass, no failures) vs artifacts.tests.failed\n'));
156
+ process.stdout.write(colors.dim(' · file claims (X files changed, no files changed) vs artifacts.files\n'));
157
+ process.stdout.write(colors.dim('Not checked: prose claims, intent, semantic correctness of changes.\n\n'));
158
+ }
159
+ function verdictLine(result) {
160
+ switch (result.status) {
161
+ case 'verified':
162
+ return colors.green(`${symbols.success} Verified`) + colors.dim(' ' + result.summary);
163
+ case 'partial':
164
+ return colors.warning(`${symbols.warning} Partial`) + colors.dim(' ' + result.summary);
165
+ case 'broken':
166
+ return colors.error(`${symbols.error} Broken`) + colors.dim(' ' + result.summary);
167
+ case 'no-report':
168
+ return colors.gray(`${symbols.dot} No report`) + colors.dim(' ' + result.summary);
169
+ }
170
+ }
171
+ function severityTag(sev) {
172
+ switch (sev) {
173
+ case 'fail':
174
+ return colors.error('fail');
175
+ case 'warn':
176
+ return colors.warning('warn');
177
+ case 'info':
178
+ return colors.info('info');
179
+ }
180
+ }
181
+ /* ──────────────────────────────────────────────────────────────────────── */
182
+ /* JSON serialization */
183
+ /* ──────────────────────────────────────────────────────────────────────── */
184
+ function serializeResult(session, result) {
185
+ return {
186
+ sessionId: session.sessionId,
187
+ agent: session.agent,
188
+ prompt: session.prompt,
189
+ completedAt: session.completedAt,
190
+ durationMs: session.durationMs,
191
+ hasFinalReport: Boolean(session.finalReport),
192
+ verdict: {
193
+ status: result.status,
194
+ summary: result.summary,
195
+ findings: result.findings.map((f) => ({
196
+ severity: f.severity,
197
+ kind: f.kind,
198
+ claim: f.claim,
199
+ reality: f.reality,
200
+ message: f.message,
201
+ })),
202
+ },
203
+ };
204
+ }
205
+ /* ──────────────────────────────────────────────────────────────────────── */
206
+ /* Small utilities */
207
+ /* ──────────────────────────────────────────────────────────────────────── */
208
+ function formatRelativeTime(ts) {
209
+ if (!ts || ts <= 0)
210
+ return 'unknown';
211
+ const deltaMs = Date.now() - ts;
212
+ if (deltaMs < 0)
213
+ return 'just now';
214
+ const seconds = Math.floor(deltaMs / 1000);
215
+ if (seconds < 60)
216
+ return `${seconds}s ago`;
217
+ const minutes = Math.floor(seconds / 60);
218
+ if (minutes < 60)
219
+ return `${minutes}m ago`;
220
+ const hours = Math.floor(minutes / 60);
221
+ if (hours < 24)
222
+ return `${hours}h ago`;
223
+ const days = Math.floor(hours / 24);
224
+ if (days < 30)
225
+ return `${days}d ago`;
226
+ return new Date(ts).toISOString().split('T')[0];
227
+ }
228
+ function truncate(s, max) {
229
+ if (s.length <= max)
230
+ return s;
231
+ return s.slice(0, max - 1).trimEnd() + '…';
232
+ }
233
+ //# sourceMappingURL=verify-command.js.map
@@ -266,7 +266,7 @@ export function displayError(error, options = {}) {
266
266
  /**
267
267
  * Get recovery suggestions based on error type
268
268
  */
269
- function getErrorSuggestions(errorType, errorMessage, context) {
269
+ function getErrorSuggestions(errorType, errorMessage, _context) {
270
270
  const suggestions = [];
271
271
  const lowerMessage = errorMessage.toLowerCase();
272
272
  switch (errorType) {
@@ -119,6 +119,38 @@ const SLASH_COMMANDS = [
119
119
  noKey: true,
120
120
  subcommand: 'status',
121
121
  },
122
+ {
123
+ name: 'verify',
124
+ args: '[session-id]',
125
+ description: "Verify a session's Final Report against recorded reality",
126
+ kind: 'sub',
127
+ noKey: true,
128
+ subcommand: 'verify',
129
+ },
130
+ {
131
+ name: 'audit',
132
+ args: '[verify|show]',
133
+ description: 'Inspect the hash-chained audit log',
134
+ kind: 'sub',
135
+ noKey: true,
136
+ subcommand: 'audit',
137
+ },
138
+ {
139
+ name: 'context-source',
140
+ args: '[list|connect|disconnect]',
141
+ description: 'Connect Linear/Figma via OAuth',
142
+ kind: 'sub',
143
+ noKey: true,
144
+ subcommand: 'context-source',
145
+ },
146
+ {
147
+ name: 'thread',
148
+ args: '<sub> [arg]',
149
+ description: 'Share or import a signed session bundle',
150
+ kind: 'sub',
151
+ noKey: true,
152
+ subcommand: 'thread',
153
+ },
122
154
  { name: 'health', args: '', description: 'System health check', kind: 'sub', noKey: true, subcommand: 'health' },
123
155
  {
124
156
  name: 'setup',
@@ -278,7 +310,6 @@ export async function runInteractiveRepl() {
278
310
  let escTimer = null;
279
311
  const cols = () => stdout.columns || 80;
280
312
  function truncate(s, maxWidth) {
281
- // eslint-disable-next-line no-control-regex -- ANSI escape stripping requires the literal ESC byte
282
313
  const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
283
314
  if (visible.length <= maxWidth)
284
315
  return s;
@@ -648,7 +679,24 @@ async function runSubcommand(name, extraArgs) {
648
679
  });
649
680
  });
650
681
  }
682
+ /**
683
+ * UAT-002 — exit-intent words typed at the REPL prompt without a leading
684
+ * slash should leave the session, not get routed to a Conductor spawn.
685
+ *
686
+ * Pre-fix: typing bare `exit` (no `/`) fell through to spawnViaConductor,
687
+ * which booted Conductor, picked claude-code under default routing, made
688
+ * a real API call (network or quota), and emitted a confusing Final Report
689
+ * echoing the provider error verbatim. First-impression UX bug found by
690
+ * UAT 2026-05-19.
691
+ */
692
+ const EXIT_INTENT_WORDS = new Set(['exit', 'quit', 'q', 'bye', 'goodbye', 'logout', ':q']);
651
693
  async function spawnViaConductor(prompt, state) {
694
+ // UAT-002 guard — exit-intent before any spawn / API call.
695
+ const trimmed = prompt.trim().toLowerCase();
696
+ if (EXIT_INTENT_WORDS.has(trimmed)) {
697
+ console.log(colors.dim('Goodbye! 👋'));
698
+ process.exit(0);
699
+ }
652
700
  const installed = availableAgents().filter((a) => a.installed);
653
701
  if (installed.length === 0) {
654
702
  console.log(warning('No specialist installed — install one and try again.'));
@@ -711,7 +759,9 @@ function renderEvent(event) {
711
759
  console.log(colors.dim(' │ ') + icon + ' ' + label);
712
760
  // P22 / Track B #8 — render last 3 lines of step output for done/failed
713
761
  if (lastStep.output && String(lastStep.output).trim()) {
714
- const outLines = String(lastStep.output).split(String.fromCharCode(10)).filter(l => l.trim());
762
+ const outLines = String(lastStep.output)
763
+ .split(String.fromCharCode(10))
764
+ .filter((l) => l.trim());
715
765
  const preview = outLines.slice(-3);
716
766
  for (const ln of preview) {
717
767
  console.log(colors.dim(' │ ') + colors.gray(ln));