codehere 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +5 -3
  2. package/dist/application/use-cases/domain-trust.js +110 -0
  3. package/dist/application/use-cases/generate-final-report.js +84 -5
  4. package/dist/application/use-cases/generate-narration.js +20 -3
  5. package/dist/application/use-cases/generate-thread-title.js +4 -0
  6. package/dist/application/use-cases/thread-export.js +155 -0
  7. package/dist/application/use-cases/verify-claim.js +274 -0
  8. package/dist/application/use-cases/write-pass.js +277 -0
  9. package/dist/index.js +218 -19
  10. package/dist/infrastructure/completion/bash-completion.js +2 -2
  11. package/dist/infrastructure/config/config-manager.js +1 -1
  12. package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
  13. package/dist/infrastructure/context/adapters/figma-source.js +163 -0
  14. package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
  15. package/dist/infrastructure/context/adapters/linear-source.js +190 -0
  16. package/dist/infrastructure/context/context-resolver.js +144 -0
  17. package/dist/infrastructure/context/context-source.js +114 -0
  18. package/dist/infrastructure/context/oauth-flow.js +207 -0
  19. package/dist/infrastructure/context/oauth-store.js +119 -0
  20. package/dist/infrastructure/context/registry-bootstrap.js +99 -0
  21. package/dist/infrastructure/context/write-pass-resolver.js +106 -0
  22. package/dist/infrastructure/cost/budget-config.js +1 -1
  23. package/dist/infrastructure/cost/spend-aggregator.js +5 -5
  24. package/dist/infrastructure/hooks/claude-sdk-hook.js +79 -59
  25. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  26. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  27. package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
  28. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  29. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  30. package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
  31. package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
  32. package/dist/infrastructure/orchestration/context-handoff.js +4 -3
  33. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  34. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  35. package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
  36. package/dist/infrastructure/orchestration/graphify-context.js +6 -13
  37. package/dist/infrastructure/orchestration/memory-injector.js +11 -16
  38. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
  39. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  40. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  41. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  42. package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
  43. package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
  44. package/dist/infrastructure/orchestration/session-manager.js +142 -29
  45. package/dist/infrastructure/orchestration/session-persistence.js +139 -30
  46. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  47. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  48. package/dist/infrastructure/protocols/cloud-sync.js +10 -11
  49. package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
  50. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  51. package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
  52. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  53. package/dist/infrastructure/protocols/verify-page.js +941 -0
  54. package/dist/infrastructure/security/trust-scorer.js +76 -1
  55. package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
  56. package/dist/infrastructure/storage/project-thread-migration.js +14 -9
  57. package/dist/infrastructure/storage/project-thread-reader.js +11 -1
  58. package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
  59. package/dist/monitoring.js +1 -1
  60. package/dist/presentation/cli/commands/audit-command.js +126 -0
  61. package/dist/presentation/cli/commands/budget-command.js +2 -2
  62. package/dist/presentation/cli/commands/config-command.js +2 -2
  63. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  64. package/dist/presentation/cli/commands/help-command.js +51 -1
  65. package/dist/presentation/cli/commands/init-command.js +5 -4
  66. package/dist/presentation/cli/commands/memory-command.js +29 -15
  67. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  68. package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
  69. package/dist/presentation/cli/commands/report-command.js +6 -4
  70. package/dist/presentation/cli/commands/setup-command.js +71 -18
  71. package/dist/presentation/cli/commands/status-command.js +0 -4
  72. package/dist/presentation/cli/commands/thread-command.js +288 -0
  73. package/dist/presentation/cli/commands/verify-command.js +233 -0
  74. package/dist/presentation/cli/error-display.js +1 -1
  75. package/dist/presentation/cli/repl.js +52 -2
  76. package/dist/presentation/cli/task-deliverable.js +127 -0
  77. package/dist/testing/cassette.js +125 -0
  78. package/dist/testing/replay-adapter.js +166 -0
  79. package/dist/ui.js +54 -29
  80. package/dist/utils/typo-guard.js +1 -1
  81. package/package.json +4 -2
@@ -0,0 +1,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)}
@@ -397,37 +400,59 @@ export function randomFarewell() {
397
400
  export function randomCleanScan() {
398
401
  return CLEAN_SCAN_CELEBRATIONS[Math.floor(Math.random() * CLEAN_SCAN_CELEBRATIONS.length)];
399
402
  }
403
+ /**
404
+ * v0.6.2 sync — every non-replOnly entry MUST correspond to a real
405
+ * `program.command('<name>')` registration in src/index.ts. Drift is
406
+ * blocked by test/cli/slash-commands-sync.test.ts (UAT-001 CI guard).
407
+ *
408
+ * Pre-fix (v0.6.1 and earlier): this list carried 16 ghost commands
409
+ * (ask/explain/fix/review/plan/suggest/test/docs/refactor/orchestrate/run/
410
+ * git-review/index/cost/models/undo) removed in v0.3 — and was missing
411
+ * 8 real commands (init/memory/permission/narrator/budget/start/task/kill).
412
+ * Catastrophic first-impression bug found by UAT 2026-05-19.
413
+ */
400
414
  export const SLASH_COMMANDS = [
401
- // AI-powered
402
- { name: 'ask', args: '<question>', description: 'Ask anything about your codebase' },
403
- { name: 'explain', args: '<file>', description: 'Explain a file' },
404
- { name: 'fix', args: '<file> <hint>', description: 'Apply a targeted fix' },
405
- { name: 'review', args: '<file>', description: 'Code review with findings' },
406
- { name: 'plan', args: '<goal>', description: 'Break a goal into steps' },
407
- { name: 'suggest', args: '<query>', description: 'Read-only assistant — no changes' },
408
- { name: 'test', args: '<file>', description: 'Generate tests for a file' },
409
- { name: 'docs', args: '<type>', description: 'Generate documentation' },
410
- { name: 'refactor', args: '<pattern> <instr>', description: 'Refactor across files' },
411
- { name: 'orchestrate', args: '<goal>', description: 'Multi-agent workflow' },
412
- { name: 'run', args: '<goal>', description: 'Execute a task or goal' },
413
- { name: 'git-review', args: '', description: 'Review git changes' },
414
- // Safety & config (no key)
415
- { name: 'trust', args: '[path]', description: 'Scan for vulnerabilities', noKey: true },
416
- { name: 'report', args: '[path]', description: 'Security audit report', noKey: true },
417
- { name: 'hook', args: '[install|status]', description: 'Safety hook management', noKey: true },
418
- { name: 'status', args: '', description: 'Dashboard — hook activity', noKey: true },
419
- { name: 'health', args: '', description: 'System health check', noKey: true },
420
- { name: 'index', args: '', description: 'Build the search index' },
421
- { name: 'serve', args: '', description: 'Start MCP server', noKey: true },
422
- { name: 'setup', args: '', description: 'Configure API provider', noKey: true },
415
+ // Setup + config (no key)
416
+ { name: 'setup', args: '', description: 'Interactive first-run wizard', noKey: true },
417
+ { name: 'init', args: '', description: 'Scaffold .codehere/ in this repo', noKey: true },
418
+ { name: 'memory', args: '<sub>', description: 'Project memory · init / repo-map / show', noKey: true },
423
419
  { name: 'config', args: '[key] [value]', description: 'Manage preferences', noKey: true },
424
- { name: 'cost', args: '', description: 'View API cost tracking', noKey: true },
425
- { name: 'models', args: '', description: 'List available AI models', noKey: true },
426
- { name: 'undo', args: '<file>', description: 'Undo file changes', noKey: true },
427
- // Utility
420
+ { name: 'permission', args: '<sub>', description: 'Manage .codehere/permissions.yaml allow/deny', noKey: true },
421
+ { name: 'narrator', args: '', description: 'Customize the PM narrator prompt', noKey: true },
422
+ { name: 'budget', args: '[set <amount>]', description: 'Daily spend budget', noKey: true },
423
+ // Safety + observability (no key)
424
+ { name: 'hook', args: '[install|status]', description: 'Safety hook management', noKey: true },
425
+ { name: 'trust', args: '[path]', description: 'Scan for vulnerabilities', noKey: true },
426
+ { name: 'report', args: '[path]', description: 'Shareable security audit', noKey: true },
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 },
442
+ { name: 'health', args: '', description: 'System health + metrics', noKey: true },
443
+ // Orchestration
444
+ { name: 'start', args: '', description: 'Open the dashboard at localhost:3773' },
445
+ { name: 'serve', args: '', description: 'Run headless MCP server (no UI)', noKey: true },
446
+ { name: 'task', args: '<prompt>', description: 'Delegate a task to a coding agent' },
447
+ { name: 'kill', args: '<sessionId>', description: 'Kill a running task' },
448
+ // Utility (no key)
428
449
  { name: 'help', args: '', description: 'Command reference', noKey: true },
429
- { name: 'clear', args: '', description: 'Clear the screen', noKey: true },
430
- { name: 'exit', args: '', description: 'Leave the session', noKey: true },
450
+ // REPL-only meta-commands (handled in repl.ts, not in Commander).
451
+ // Marked replOnly so the CI sync guard skips them.
452
+ { name: 'agents', args: '', description: 'List installed specialists', noKey: true, replOnly: true },
453
+ { name: 'history', args: '', description: 'Recent REPL session history', noKey: true, replOnly: true },
454
+ { name: 'clear', args: '', description: 'Clear the screen', noKey: true, replOnly: true },
455
+ { name: 'exit', args: '', description: 'Leave the session', noKey: true, replOnly: true },
431
456
  ];
432
457
  export function showSlashCommandMenu() {
433
458
  newline();
@@ -64,7 +64,7 @@ function checkTypoSingle(token, knownCommands) {
64
64
  .map((n) => ({ n, d: levenshtein(lower, n) }))
65
65
  .filter((s) => s.d > 0 && s.d <= 2 && s.d < lower.length)
66
66
  // Deterministic tie-breaker (alphabetical) so ranking is stable across runs.
67
- .sort((a, b) => (a.d - b.d) || a.n.localeCompare(b.n))
67
+ .sort((a, b) => a.d - b.d || a.n.localeCompare(b.n))
68
68
  .slice(0, 3);
69
69
  return scored.length > 0 ? scored.map((s) => s.n) : null;
70
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehere",
3
- "version": "0.6.1",
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",