codehere 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/application/use-cases/domain-trust.js +110 -0
- package/dist/application/use-cases/generate-final-report.js +87 -168
- package/dist/application/use-cases/generate-narration.js +11 -0
- package/dist/application/use-cases/generate-thread-title.js +4 -0
- package/dist/application/use-cases/thread-export.js +155 -0
- package/dist/application/use-cases/verify-claim.js +274 -0
- package/dist/application/use-cases/write-pass.js +277 -0
- package/dist/index.js +211 -16
- package/dist/infrastructure/completion/bash-completion.js +2 -2
- package/dist/infrastructure/config/config-manager.js +1 -1
- package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
- package/dist/infrastructure/context/adapters/figma-source.js +163 -0
- package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
- package/dist/infrastructure/context/adapters/linear-source.js +190 -0
- package/dist/infrastructure/context/context-resolver.js +144 -0
- package/dist/infrastructure/context/context-source.js +114 -0
- package/dist/infrastructure/context/oauth-flow.js +207 -0
- package/dist/infrastructure/context/oauth-store.js +119 -0
- package/dist/infrastructure/context/registry-bootstrap.js +99 -0
- package/dist/infrastructure/context/write-pass-resolver.js +106 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +73 -57
- package/dist/infrastructure/hooks/session-analyzer.js +65 -8
- package/dist/infrastructure/observability/execution-tracer.js +0 -5
- package/dist/infrastructure/orchestration/aider-adapter.js +49 -18
- package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
- package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
- package/dist/infrastructure/orchestration/claude-code-adapter.js +38 -5
- package/dist/infrastructure/orchestration/codex-adapter.js +150 -22
- package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
- package/dist/infrastructure/orchestration/context-handoff.js +3 -2
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
- package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
- package/dist/infrastructure/orchestration/gemini-adapter.js +12 -2
- package/dist/infrastructure/orchestration/graphify-context.js +18 -13
- package/dist/infrastructure/orchestration/memory-injector.js +46 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +230 -0
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +22 -2
- package/dist/infrastructure/orchestration/progress-aggregator.js +177 -3
- package/dist/infrastructure/orchestration/session-manager.js +112 -6
- package/dist/infrastructure/orchestration/session-persistence.js +140 -31
- package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
- package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
- package/dist/infrastructure/protocols/cloud-sync.js +1 -6
- package/dist/infrastructure/protocols/mcp-server-impl.js +1283 -13
- package/dist/infrastructure/protocols/thread-phase.js +105 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +3913 -88
- package/dist/infrastructure/protocols/ui-page.js +12 -3299
- package/dist/infrastructure/protocols/verify-page.js +941 -0
- package/dist/infrastructure/security/trust-scorer.js +76 -1
- package/dist/infrastructure/storage/cross-tool-agents.js +1 -1
- package/dist/infrastructure/storage/project-thread-migration.js +2 -2
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +1 -1
- package/dist/presentation/cli/commands/audit-command.js +126 -0
- package/dist/presentation/cli/commands/config-command.js +2 -2
- package/dist/presentation/cli/commands/context-source-command.js +198 -0
- package/dist/presentation/cli/commands/help-command.js +51 -1
- package/dist/presentation/cli/commands/report-command.js +6 -4
- package/dist/presentation/cli/commands/setup-command.js +71 -18
- package/dist/presentation/cli/commands/status-command.js +0 -4
- package/dist/presentation/cli/commands/thread-command.js +288 -0
- package/dist/presentation/cli/commands/verify-command.js +233 -0
- package/dist/presentation/cli/error-display.js +1 -1
- package/dist/presentation/cli/repl.js +32 -1
- package/dist/presentation/cli/task-deliverable.js +127 -0
- package/dist/testing/cassette.js +125 -0
- package/dist/testing/replay-adapter.js +166 -0
- package/dist/ui.js +18 -1
- package/package.json +5 -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
|
|
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
|
-
|
|
154
|
+
const { ready, all } = summarizeAgents();
|
|
155
|
+
console.log(colors.bold('Setup'));
|
|
117
156
|
console.log();
|
|
118
|
-
|
|
157
|
+
if (all.length)
|
|
158
|
+
printAgentLines(all);
|
|
119
159
|
console.log();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
151
|
-
sectionHeader('Step 1:
|
|
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 (
|
|
154
|
-
console.log(success(`
|
|
155
|
-
|
|
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('
|
|
213
|
+
console.log(warning('No coding agent is set up yet.'));
|
|
160
214
|
newline();
|
|
161
|
-
console.log(colors.bold('
|
|
162
|
-
console.log(colors.cyan('
|
|
163
|
-
console.log(colors.
|
|
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,
|
|
269
|
+
function getErrorSuggestions(errorType, errorMessage, _context) {
|
|
270
270
|
const suggestions = [];
|
|
271
271
|
const lowerMessage = errorMessage.toLowerCase();
|
|
272
272
|
switch (errorType) {
|