codehere 0.6.2 → 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 (70) 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 +28 -4
  4. package/dist/application/use-cases/generate-narration.js +11 -0
  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 +211 -16
  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/hooks/claude-sdk-hook.js +73 -57
  23. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  24. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  25. package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
  26. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  27. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  28. package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
  29. package/dist/infrastructure/orchestration/codex-adapter.js +146 -13
  30. package/dist/infrastructure/orchestration/context-handoff.js +3 -2
  31. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  32. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  33. package/dist/infrastructure/orchestration/gemini-adapter.js +12 -2
  34. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
  35. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  36. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  37. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  38. package/dist/infrastructure/orchestration/opencode-adapter.js +22 -2
  39. package/dist/infrastructure/orchestration/progress-aggregator.js +177 -3
  40. package/dist/infrastructure/orchestration/session-manager.js +94 -6
  41. package/dist/infrastructure/orchestration/session-persistence.js +139 -30
  42. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  43. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  44. package/dist/infrastructure/protocols/cloud-sync.js +1 -6
  45. package/dist/infrastructure/protocols/mcp-server-impl.js +1280 -12
  46. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  47. package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
  48. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  49. package/dist/infrastructure/protocols/verify-page.js +941 -0
  50. package/dist/infrastructure/security/trust-scorer.js +76 -1
  51. package/dist/infrastructure/storage/cross-tool-agents.js +1 -1
  52. package/dist/infrastructure/storage/project-thread-migration.js +2 -2
  53. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  54. package/dist/monitoring.js +1 -1
  55. package/dist/presentation/cli/commands/audit-command.js +126 -0
  56. package/dist/presentation/cli/commands/config-command.js +2 -2
  57. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  58. package/dist/presentation/cli/commands/help-command.js +51 -1
  59. package/dist/presentation/cli/commands/report-command.js +6 -4
  60. package/dist/presentation/cli/commands/setup-command.js +71 -18
  61. package/dist/presentation/cli/commands/status-command.js +0 -4
  62. package/dist/presentation/cli/commands/thread-command.js +288 -0
  63. package/dist/presentation/cli/commands/verify-command.js +233 -0
  64. package/dist/presentation/cli/error-display.js +1 -1
  65. package/dist/presentation/cli/repl.js +32 -1
  66. package/dist/presentation/cli/task-deliverable.js +127 -0
  67. package/dist/testing/cassette.js +125 -0
  68. package/dist/testing/replay-adapter.js +166 -0
  69. package/dist/ui.js +18 -1
  70. package/package.json +4 -2
@@ -6,9 +6,47 @@
6
6
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { getConfigManager } from '../../../infrastructure/config/config-manager.js';
9
- import { colors, separator, newline, sectionHeader, success, error, warning, info, } from '../../../ui.js';
9
+ import { colors, separator, newline, sectionHeader, success, error, warning, info } from '../../../ui.js';
10
10
  import { executionTracer } from '../../../infrastructure/observability/execution-tracer.js';
11
11
  import { findCodebaseRoot } from '../../../domain/services/codebase-detector.js';
12
+ import { readinessForAllAgents } from '../../../infrastructure/orchestration/session-manager.js';
13
+ /**
14
+ * What codehere actually needs to run a task: at least one coding-agent CLI
15
+ * that's installed AND signed in. Most engineers have `claude` or `codex`
16
+ * logged in by subscription with no API key set, so framing setup around
17
+ * `ANTHROPIC_API_KEY` told them they were blocked when they weren't. This
18
+ * reuses the same readiness check that powers the web dashboard's squad bar.
19
+ */
20
+ function summarizeAgents() {
21
+ let all;
22
+ try {
23
+ all = readinessForAllAgents();
24
+ }
25
+ catch {
26
+ all = [];
27
+ }
28
+ return { ready: all.filter((a) => a.state === 'ready'), all };
29
+ }
30
+ /** One status line per agent, plain English. Used by both the TTY wizard and
31
+ * the non-TTY summary so they say the same thing. */
32
+ function printAgentLines(all) {
33
+ for (const a of all) {
34
+ if (a.state === 'ready') {
35
+ console.log(success(`${a.label}: ready`));
36
+ }
37
+ else if (a.state === 'needs_auth') {
38
+ const hint = a.authHint?.command ? colors.dim(` (sign in: ${a.authHint.command})`) : '';
39
+ console.log(warning(`${a.label}: installed, needs sign-in`) + hint);
40
+ }
41
+ else if (a.state === 'billed_key_required') {
42
+ console.log(colors.dim(`○ ${a.label}: needs a billed API key (paid)`));
43
+ }
44
+ else {
45
+ const hint = a.installHint?.command ? colors.dim(` (install: ${a.installHint.command})`) : '';
46
+ console.log(colors.dim(`○ ${a.label}: not installed`) + hint);
47
+ }
48
+ }
49
+ }
12
50
  /**
13
51
  * Check if any API key is configured
14
52
  */
@@ -113,19 +151,26 @@ export async function executeSetupCommand(force = false) {
113
151
  // Check for interactive terminal
114
152
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
115
153
  if (!isTTY) {
116
- console.log(colors.bold('Setup Wizard'));
154
+ const { ready, all } = summarizeAgents();
155
+ console.log(colors.bold('Setup'));
117
156
  console.log();
118
- console.log(warning('Interactive terminal required.'));
157
+ if (all.length)
158
+ printAgentLines(all);
119
159
  console.log();
120
- console.log('Set an API key in your environment:');
121
- console.log(colors.cyan(' export ANTHROPIC_API_KEY=sk-ant-...') + colors.dim(' (recommended)'));
122
- console.log(colors.cyan(' export OPENAI_API_KEY=sk-...'));
123
- console.log(colors.cyan(' export COHERE_API_KEY=...'));
160
+ if (ready.length) {
161
+ console.log(success(`You're ready. ${ready[0].label} is set up.`));
162
+ console.log(colors.dim(' Next: codehere start (open the dashboard)'));
163
+ }
164
+ else {
165
+ console.log(warning('No coding agent is set up yet. Install or sign in to one:'));
166
+ console.log(colors.cyan(' npm i -g @anthropic-ai/claude-code') + colors.dim(' then run: claude (sign in)'));
167
+ console.log(colors.dim(' Already use one by API key? export ANTHROPIC_API_KEY=sk-ant-...'));
168
+ }
124
169
  return;
125
170
  }
126
171
  // Start execution tracing
127
172
  const codebaseInfo = findCodebaseRoot(process.cwd());
128
- const runId = executionTracer.startRun('setup', {
173
+ executionTracer.startRun('setup', {
129
174
  codebaseId: codebaseInfo?.rootPath || process.cwd(),
130
175
  });
131
176
  try {
@@ -147,21 +192,29 @@ export async function executeSetupCommand(force = false) {
147
192
  codebaseDetected: false,
148
193
  configPath: join(process.cwd(), 'data'),
149
194
  };
150
- // Step 1: API Key
151
- sectionHeader('Step 1: API Key Configuration');
195
+ // Step 1: Coding agents — codehere drives agent CLIs, not raw API keys.
196
+ sectionHeader('Step 1: Your coding agents');
197
+ const { ready, all } = summarizeAgents();
198
+ if (all.length)
199
+ printAgentLines(all);
200
+ newline();
152
201
  const apiKeyCheck = checkAPIKey();
153
- if (apiKeyCheck.configured) {
154
- console.log(success(`API Key: ${apiKeyCheck.provider} key found`));
155
- console.log(colors.dim(` Key preview: ${apiKeyCheck.key?.substring(0, 8)}...${apiKeyCheck.key?.substring(apiKeyCheck.key.length - 4)}`));
202
+ if (ready.length) {
203
+ console.log(success(`You're ready. ${ready[0].label} is set up.`));
204
+ result.apiKeyConfigured = true; // at least one agent can spawn right now
205
+ }
206
+ else if (apiKeyCheck.configured) {
207
+ // No agent CLI ready, but a key is present — some adapters (e.g. Aider) run off it.
208
+ console.log(success(`${apiKeyCheck.provider} API key found.`));
209
+ console.log(colors.dim(' No agent CLI detected yet — install one for the full experience.'));
156
210
  result.apiKeyConfigured = true;
157
211
  }
158
212
  else {
159
- console.log(warning('API Key: Not configured'));
213
+ console.log(warning('No coding agent is set up yet.'));
160
214
  newline();
161
- console.log(colors.bold('Set one of these environment variables:'));
162
- console.log(colors.cyan(' export ANTHROPIC_API_KEY=sk-ant-...') + colors.dim(' (recommended)'));
163
- console.log(colors.cyan(' export OPENAI_API_KEY=sk-...'));
164
- console.log(colors.cyan(' export COHERE_API_KEY=...'));
215
+ console.log(colors.bold('Install or sign in to at least one:'));
216
+ console.log(colors.cyan(' npm i -g @anthropic-ai/claude-code') + colors.dim(' then run: claude (sign in)'));
217
+ console.log(colors.dim(' Already use one by API key? export ANTHROPIC_API_KEY=sk-ant-...'));
165
218
  newline();
166
219
  }
167
220
  newline();
@@ -185,10 +185,6 @@ function extractFilePath(entry) {
185
185
  }
186
186
  return null;
187
187
  }
188
- function shortReason(entry, maxLen = 60) {
189
- const r = extractBlockReason(entry);
190
- return r.length <= maxLen ? r : r.substring(0, maxLen - 1) + '…';
191
- }
192
188
  const BLOCK_CATEGORY_LABEL = {
193
189
  'self-test': "codehere's own canary tests",
194
190
  'cache-cleanup': 'routine cache/tmp deletion',
@@ -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) {