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
@@ -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;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Task deliverable · the CLI counterpart to the web UI session card.
3
+ *
4
+ * After a task finishes, the non-technical user needs the SAME thing they get
5
+ * in the dashboard, just in the terminal: what the agent did (Final Report),
6
+ * which files changed + the trust score, whether the report's claims check out
7
+ * (verifier), and one obvious next step. No raw event types, no jargon.
8
+ *
9
+ * Shared by `task --watch` (print on completion) and the daemon-fetch path so
10
+ * both surfaces say the same honest thing. Per feedback_no_screen_lies: every
11
+ * line here traces to recorded state — we never print a success we can't prove.
12
+ */
13
+ import http from 'http';
14
+ import { colors } from '../../ui.js';
15
+ /** Pull one session's full result from a running daemon's /api/tasks. Matches
16
+ * by exact id or short prefix. Returns null if the daemon is unreachable or
17
+ * the id isn't found. */
18
+ export function fetchSessionResult(base, sessionId) {
19
+ return new Promise((resolve) => {
20
+ const r = http.get(`${base}/api/tasks`, (res) => {
21
+ let buf = '';
22
+ res.on('data', (c) => {
23
+ buf += c.toString();
24
+ });
25
+ res.on('end', () => {
26
+ try {
27
+ const j = JSON.parse(buf);
28
+ const all = [...(j.spawned ?? []), ...(j.persisted ?? [])];
29
+ const exact = all.find((s) => s.sessionId === sessionId);
30
+ resolve(exact ?? all.find((s) => s.sessionId.startsWith(sessionId)) ?? null);
31
+ }
32
+ catch {
33
+ resolve(null);
34
+ }
35
+ });
36
+ });
37
+ r.on('error', () => resolve(null));
38
+ });
39
+ }
40
+ /** Poll the daemon until the Final Report lands (or we give up). The report is
41
+ * generated asynchronously after the agent exits, so a single fetch right at
42
+ * 'exit' often misses it. */
43
+ export async function waitForDeliverable(base, sessionId, opts = {}) {
44
+ // Real agents take ~30-60s to synthesize the Final Report after the agent
45
+ // exits (mock is instant). 90s covers a real claude run without giving up early.
46
+ const timeoutMs = opts.timeoutMs ?? 90_000;
47
+ const intervalMs = opts.intervalMs ?? 500;
48
+ const deadline = Date.now() + timeoutMs;
49
+ let last = null;
50
+ while (Date.now() < deadline) {
51
+ last = await fetchSessionResult(base, sessionId);
52
+ if (last && (last.finalReport || last.failedReport || last.status === 'error' || last.status === 'killed')) {
53
+ return last;
54
+ }
55
+ await new Promise((r) => setTimeout(r, intervalMs));
56
+ }
57
+ return last;
58
+ }
59
+ /** Plain-English deliverable block. The terminal twin of the dashboard card. */
60
+ export function formatDeliverable(r) {
61
+ const lines = [];
62
+ const bar = colors.dim('─'.repeat(60));
63
+ const verb = r.status === 'completed'
64
+ ? 'Done'
65
+ : r.status === 'error'
66
+ ? 'Failed'
67
+ : r.status === 'killed'
68
+ ? 'Stopped'
69
+ : 'Finished';
70
+ lines.push('');
71
+ lines.push(bar);
72
+ lines.push(`${colors.bold(verb)} · ${r.agent} · ${colors.dim(r.sessionId.slice(0, 8))}`);
73
+ lines.push(bar);
74
+ if (r.stalledReason) {
75
+ lines.push(colors.warning(`The agent stalled: ${r.stalledReason}`));
76
+ }
77
+ // 1 · What the agent did (the Final Report narrative).
78
+ if (r.finalReport?.summary) {
79
+ lines.push(r.finalReport.summary.trim());
80
+ }
81
+ else if (r.failedReport) {
82
+ lines.push(colors.dim("codehere couldn't write a report for this run."));
83
+ }
84
+ else if (r.status === 'error') {
85
+ lines.push(colors.dim('The agent ran into an error before finishing.'));
86
+ }
87
+ else {
88
+ lines.push(colors.dim('No report yet.'));
89
+ }
90
+ // 2 · Files changed + trust score (or the honest can't-track note).
91
+ const files = r.artifacts?.files ?? [];
92
+ if (r.artifacts?.noGitTracking) {
93
+ lines.push('');
94
+ lines.push(colors.dim("Files: codehere couldn't track changes here (this folder isn't a git repository)."));
95
+ }
96
+ else if (files.length > 0) {
97
+ lines.push('');
98
+ lines.push(colors.bold('Files changed:'));
99
+ for (const f of files) {
100
+ const trust = typeof f.trust === 'number' ? ` · trust ${f.trust}/100` : '';
101
+ lines.push(` • ${f.path} ${colors.success('+' + f.added)}/${colors.error('-' + f.removed)}${colors.dim(trust)}`);
102
+ }
103
+ }
104
+ else {
105
+ lines.push('');
106
+ lines.push(colors.dim('Files: none changed.'));
107
+ }
108
+ // 3 · Did the report's claims check out? (verifier cross-check)
109
+ if (r.verifyResult) {
110
+ const v = r.verifyResult;
111
+ const label = v.status === 'verified'
112
+ ? colors.success('verified')
113
+ : v.status === 'broken'
114
+ ? colors.error('does not match reality')
115
+ : v.status === 'partial'
116
+ ? colors.warning('partly unverifiable')
117
+ : colors.dim('nothing to check');
118
+ lines.push('');
119
+ lines.push(`${colors.bold('Check:')} ${label} — ${v.summary}`);
120
+ for (const f of v.findings.filter((x) => x.severity !== 'info')) {
121
+ lines.push(colors.dim(` • ${f.message}`));
122
+ }
123
+ }
124
+ lines.push(bar);
125
+ return lines.join('\n');
126
+ }
127
+ //# sourceMappingURL=task-deliverable.js.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Cassette schema (LOCKED — do not modify; see docs/TEST_PLAN_v0.7.md §3.L3b).
3
+ *
4
+ * JSONL at `agent/test/cassettes/<adapter>/<scenario>-YYYY-MM-DD.jsonl`. One line per chunk.
5
+ *
6
+ * Line 1 (header):
7
+ * {"t":0,"meta":{"adapter":"<id>","scenario":"<id>","captured_at":"<ISO>","binary_version":"<x.y.z|synthetic>","origin":"real"|"synthetic","prompt":"<original>","cwd_hint":"<relpath>","schema":1}}
8
+ * Line 2..N (events):
9
+ * {"t":<ms-from-start>,"stream":"stdout"|"stderr","data":"<raw bytes, newlines preserved>"}
10
+ * Line N+1 (term):
11
+ * {"t":<ms-from-start>,"exit_code":<int>,"signal":<string|null>}
12
+ *
13
+ * Loaders validate shape and reject malformed cassettes loudly — a silently-
14
+ * misparsed cassette would produce a false-green test, defeating L3b's point.
15
+ */
16
+ import { readFileSync } from 'fs';
17
+ function isObject(x) {
18
+ return typeof x === 'object' && x !== null && !Array.isArray(x);
19
+ }
20
+ function parseLine(line, lineNo, path) {
21
+ try {
22
+ return JSON.parse(line);
23
+ }
24
+ catch (err) {
25
+ const msg = err instanceof Error ? err.message : String(err);
26
+ throw new Error(`Cassette ${path} line ${lineNo}: invalid JSON (${msg})`, { cause: err });
27
+ }
28
+ }
29
+ /** Read a JSONL cassette from disk, validate shape, return structured form. */
30
+ export function loadCassette(path) {
31
+ const raw = readFileSync(path, 'utf-8');
32
+ const rawLines = raw.split('\n');
33
+ const lines = [];
34
+ for (let i = 0; i < rawLines.length; i++) {
35
+ const line = rawLines[i].trim();
36
+ if (line)
37
+ lines.push({ line, no: i + 1 });
38
+ }
39
+ if (lines.length < 2) {
40
+ throw new Error(`Cassette ${path}: needs at least header + terminal lines (got ${lines.length})`);
41
+ }
42
+ // Header
43
+ const headerRaw = parseLine(lines[0].line, lines[0].no, path);
44
+ if (!isObject(headerRaw) || headerRaw.t !== 0 || !isObject(headerRaw.meta)) {
45
+ throw new Error(`Cassette ${path} line ${lines[0].no}: header must be {"t":0,"meta":{...}}`);
46
+ }
47
+ const meta = headerRaw.meta;
48
+ const requiredMeta = [
49
+ 'adapter',
50
+ 'scenario',
51
+ 'captured_at',
52
+ 'binary_version',
53
+ 'origin',
54
+ 'prompt',
55
+ 'cwd_hint',
56
+ 'schema',
57
+ ];
58
+ for (const k of requiredMeta) {
59
+ if (!(k in meta)) {
60
+ throw new Error(`Cassette ${path}: header meta missing required field "${k}"`);
61
+ }
62
+ }
63
+ if (meta.schema !== 1) {
64
+ throw new Error(`Cassette ${path}: schema must be 1, got ${String(meta.schema)}`);
65
+ }
66
+ if (meta.origin !== 'real' && meta.origin !== 'synthetic') {
67
+ throw new Error(`Cassette ${path}: meta.origin must be "real" or "synthetic"`);
68
+ }
69
+ // Terminal (last line)
70
+ const termEntry = lines[lines.length - 1];
71
+ const termRaw = parseLine(termEntry.line, termEntry.no, path);
72
+ if (!isObject(termRaw) || typeof termRaw.t !== 'number' || !('exit_code' in termRaw) || !('signal' in termRaw)) {
73
+ throw new Error(`Cassette ${path} line ${termEntry.no}: terminal must be {"t":...,"exit_code":...,"signal":...}`);
74
+ }
75
+ if (typeof termRaw.exit_code !== 'number') {
76
+ throw new Error(`Cassette ${path} line ${termEntry.no}: terminal.exit_code must be a number`);
77
+ }
78
+ if (termRaw.signal !== null && typeof termRaw.signal !== 'string') {
79
+ throw new Error(`Cassette ${path} line ${termEntry.no}: terminal.signal must be string or null`);
80
+ }
81
+ // Middle: events
82
+ const events = [];
83
+ for (let i = 1; i < lines.length - 1; i++) {
84
+ const { line, no } = lines[i];
85
+ const ev = parseLine(line, no, path);
86
+ if (!isObject(ev) || typeof ev.t !== 'number') {
87
+ throw new Error(`Cassette ${path} line ${no}: event must have numeric "t"`);
88
+ }
89
+ if (ev.stream !== 'stdout' && ev.stream !== 'stderr') {
90
+ throw new Error(`Cassette ${path} line ${no}: event.stream must be "stdout" or "stderr"`);
91
+ }
92
+ if (typeof ev.data !== 'string') {
93
+ throw new Error(`Cassette ${path} line ${no}: event.data must be a string`);
94
+ }
95
+ events.push({ t: ev.t, stream: ev.stream, data: ev.data });
96
+ }
97
+ const cassette = {
98
+ header: { t: 0, meta: meta },
99
+ events,
100
+ terminal: {
101
+ t: termRaw.t,
102
+ exit_code: termRaw.exit_code,
103
+ signal: termRaw.signal,
104
+ },
105
+ };
106
+ validateCassette(cassette);
107
+ return cassette;
108
+ }
109
+ /** Sanity-check ordering invariants. Throws on violation. */
110
+ export function validateCassette(c) {
111
+ if (c.header.t !== 0)
112
+ throw new Error('Cassette: header.t must be 0');
113
+ let prev = 0;
114
+ for (let i = 0; i < c.events.length; i++) {
115
+ const t = c.events[i].t;
116
+ if (t < prev) {
117
+ throw new Error(`Cassette: events out of order at index ${i} (t=${t} < prev=${prev})`);
118
+ }
119
+ prev = t;
120
+ }
121
+ if (c.terminal.t < prev) {
122
+ throw new Error(`Cassette: terminal.t (${c.terminal.t}) must be >= last event.t (${prev})`);
123
+ }
124
+ }
125
+ //# sourceMappingURL=cassette.js.map
@@ -0,0 +1,166 @@
1
+ /**
2
+ * L3b cassette byte-replay shim — see docs/TEST_PLAN_v0.7.md §3.L3b.
3
+ *
4
+ * This module swaps `child_process.spawn` so a real adapter's stdout/stderr/
5
+ * exit handlers receive bytes from a cassette instead of a live subprocess.
6
+ * The adapter under test is the production code path — only its boundary with
7
+ * the OS process layer is substituted. That keeps L3b honest about the parser
8
+ * + state-machine behavior the adapter exhibits on real captured bytes.
9
+ *
10
+ * Why a module-level "active cassette" instead of dependency injection:
11
+ * `vi.mock('child_process', factory)` hoists to module load and runs once.
12
+ * The factory has no way to receive a per-test cassette argument. The factory
13
+ * therefore reads `getActiveCassette()` lazily at every `spawn()` call. Tests
14
+ * call `setActiveCassette(loadCassette(path))` in `beforeEach`, then call
15
+ * `SessionManager.getInstance().spawn(...)` to drive the adapter.
16
+ *
17
+ * Honest call-out: this approach mocks the entire `child_process` import in
18
+ * the test file. The same factory wraps `execFileSync` so adapter `which X`
19
+ * probes succeed even when the binary is absent on the test host. Anything
20
+ * else (git, ps) passes through to the real implementation.
21
+ *
22
+ * The synthetic `ChildProcess` honors what the per-adapter spawn boundary
23
+ * actually touches per `/tmp/l3b-spawn-boundary-map.json`:
24
+ * - `stdout` / `stderr` Readable that emit `data` events at the cassette `t`s
25
+ * - `stdin` writable that swallows writes (codex's prompt write is recorded
26
+ * by the capture-cassette script on the spawn side; stdin replay deferred)
27
+ * - `exit` then `close` at the terminal `t`
28
+ * - `kill(signal)` cancels pending timers and emits exit with that signal
29
+ * - `pid` set to a fake non-zero integer
30
+ * - `unref()` / `ref()` no-ops (gemini detached path)
31
+ */
32
+ import { EventEmitter } from 'events';
33
+ import { Readable, Writable } from 'stream';
34
+ import { loadCassette } from './cassette.js';
35
+ // ── Active cassette holder (single-cassette-per-test pattern) ───────────────
36
+ let _activeCassette;
37
+ export function setActiveCassette(c) {
38
+ _activeCassette = c;
39
+ }
40
+ export function getActiveCassette() {
41
+ return _activeCassette;
42
+ }
43
+ let _fakePidCounter = 1_000_000;
44
+ /** Drop-in replacement for `child_process.spawn`. Plays the cassette through
45
+ * a synthetic ChildProcess; ignores cmd/args/options because the cassette
46
+ * describes the stream end-to-end. */
47
+ export function createReplaySpawn(cassette) {
48
+ const replaySpawn = ((_cmd, _args, _opts) => {
49
+ return makeReplayChild(cassette);
50
+ });
51
+ return replaySpawn;
52
+ }
53
+ function makeReplayChild(cassette) {
54
+ const child = new EventEmitter();
55
+ const timers = [];
56
+ let killed = false;
57
+ // Readable streams. We push() bytes manually as timers fire; no _read backpressure
58
+ // because the adapter parsers consume eagerly via 'data' events.
59
+ const stdout = new Readable({ read() { } });
60
+ const stderr = new Readable({ read() { } });
61
+ const stdin = new Writable({
62
+ write(_chunk, _enc, cb) {
63
+ cb();
64
+ },
65
+ final(cb) {
66
+ cb();
67
+ },
68
+ });
69
+ Object.defineProperty(child, 'stdout', { value: stdout, configurable: true });
70
+ Object.defineProperty(child, 'stderr', { value: stderr, configurable: true });
71
+ Object.defineProperty(child, 'stdin', { value: stdin, configurable: true });
72
+ Object.defineProperty(child, 'pid', { value: ++_fakePidCounter, configurable: true });
73
+ Object.defineProperty(child, 'killed', {
74
+ get: () => killed,
75
+ configurable: true,
76
+ });
77
+ // child_process API surface adapters touch (per /tmp/l3b-spawn-boundary-map.json).
78
+ child.kill = ((signal) => {
79
+ if (killed)
80
+ return false;
81
+ killed = true;
82
+ for (const t of timers)
83
+ clearTimeout(t);
84
+ timers.length = 0;
85
+ const sig = typeof signal === 'string' ? signal : 'SIGTERM';
86
+ queueMicrotask(() => {
87
+ try {
88
+ stdout.push(null);
89
+ }
90
+ catch {
91
+ /* readable already ended */
92
+ }
93
+ try {
94
+ stderr.push(null);
95
+ }
96
+ catch {
97
+ /* readable already ended */
98
+ }
99
+ child.emit('exit', null, sig);
100
+ child.emit('close', null, sig);
101
+ });
102
+ return true;
103
+ });
104
+ child.unref = (() => child);
105
+ child.ref = (() => child);
106
+ child.disconnect = (() => {
107
+ /* noop for replay */
108
+ });
109
+ // Schedule cassette events. setTimeout is fine for replay — Vitest doesn't
110
+ // need fake timers; the test waits on the session reaching an exited status.
111
+ for (const ev of cassette.events) {
112
+ const handle = setTimeout(() => {
113
+ if (killed)
114
+ return;
115
+ const stream = ev.stream === 'stdout' ? stdout : stderr;
116
+ try {
117
+ stream.push(Buffer.from(ev.data, 'utf-8'));
118
+ }
119
+ catch {
120
+ /* stream ended early — ignore */
121
+ }
122
+ }, ev.t);
123
+ timers.push(handle);
124
+ }
125
+ // Terminal.
126
+ const termHandle = setTimeout(() => {
127
+ if (killed)
128
+ return;
129
+ try {
130
+ stdout.push(null);
131
+ }
132
+ catch {
133
+ /* ignore */
134
+ }
135
+ try {
136
+ stderr.push(null);
137
+ }
138
+ catch {
139
+ /* ignore */
140
+ }
141
+ const code = cassette.terminal.signal ? null : cassette.terminal.exit_code;
142
+ child.emit('exit', code, cassette.terminal.signal);
143
+ child.emit('close', code, cassette.terminal.signal);
144
+ }, cassette.terminal.t);
145
+ timers.push(termHandle);
146
+ return child;
147
+ }
148
+ /** Test convenience: load cassette, set it active, spawn via SessionManager.
149
+ *
150
+ * Callers MUST install `vi.mock('child_process', factory)` at the top of
151
+ * their test file with a factory that delegates `spawn` to
152
+ * `createReplaySpawn(getActiveCassette())`. See cassette-replay.test.ts
153
+ * for the canonical setup.
154
+ *
155
+ * Returns the spawned TaskSession (typed loosely to avoid pulling the
156
+ * whole adapter type tree into src/testing/).
157
+ */
158
+ export async function replayThroughAdapter(cassettePath, sessionManager) {
159
+ const cassette = loadCassette(cassettePath);
160
+ setActiveCassette(cassette);
161
+ const session = sessionManager.spawn(cassette.header.meta.prompt, cassette.header.meta.adapter, {
162
+ cwd: cassette.header.meta.cwd_hint,
163
+ });
164
+ return session;
165
+ }
166
+ //# sourceMappingURL=replay-adapter.js.map
package/dist/ui.js CHANGED
@@ -285,7 +285,10 @@ import { getVersionWithPrefix } from './utils/version.js';
285
285
  export function createWelcomeMessage() {
286
286
  const logo = createCodehereLogo();
287
287
  const version = getVersionWithPrefix();
288
- const tagline = 'AI coding agent with built-in safety';
288
+ // Matches the positioning in `codehere --help` and `codehere help`. codehere
289
+ // is the LAYER over your coding agents, not itself an agent — the old
290
+ // "AI coding agent with built-in safety" line implied the opposite.
291
+ const tagline = 'A PM for your AI coding agents';
289
292
  return `
290
293
  ${colors.bold(logo)} ${colors.dim(version)}
291
294
  ${colors.dim(tagline)}
@@ -422,6 +425,20 @@ export const SLASH_COMMANDS = [
422
425
  { name: 'trust', args: '[path]', description: 'Scan for vulnerabilities', noKey: true },
423
426
  { name: 'report', args: '[path]', description: 'Shareable security audit', noKey: true },
424
427
  { name: 'status', args: '', description: 'Security dashboard — hook activity', noKey: true },
428
+ {
429
+ name: 'verify',
430
+ args: '[session-id]',
431
+ description: "Verify a session's Final Report against recorded reality",
432
+ noKey: true,
433
+ },
434
+ { name: 'audit', args: '[verify|show]', description: 'Inspect the hash-chained audit log', noKey: true },
435
+ {
436
+ name: 'context-source',
437
+ args: '[list|connect|disconnect]',
438
+ description: 'Connect Linear/Figma via OAuth',
439
+ noKey: true,
440
+ },
441
+ { name: 'thread', args: '<sub> [arg]', description: 'Share or import a signed session bundle', noKey: true },
425
442
  { name: 'health', args: '', description: 'System health + metrics', noKey: true },
426
443
  // Orchestration
427
444
  { name: 'start', args: '', description: 'Open the dashboard at localhost:3773' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehere",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -31,7 +31,8 @@
31
31
  "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
32
32
  "format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts'",
33
33
  "sbom": "npm sbom --sbom-format=spdx --omit=dev",
34
- "pretag:check": "../scripts/pretag-npm-check.sh"
34
+ "pretag:check": "../scripts/pretag-npm-check.sh",
35
+ "cassette:record": "../scripts/capture-cassette.sh"
35
36
  },
36
37
  "keywords": [
37
38
  "ai-coding-agents",
@@ -76,6 +77,7 @@
76
77
  "@types/cli-progress": "^3.11.5",
77
78
  "@types/node": "^20.19.25",
78
79
  "eslint": "^10.2.0",
80
+ "playwright": "^1.60.0",
79
81
  "prettier": "^3.8.3",
80
82
  "tsx": "^4.21.0",
81
83
  "typescript": "^5.9.3",