zelari-code 2.50.1 → 2.51.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 (40) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/acp/command.js +127 -0
  3. package/dist/cli/acp/command.js.map +1 -0
  4. package/dist/cli/acp/eventMap.js +84 -0
  5. package/dist/cli/acp/eventMap.js.map +1 -0
  6. package/dist/cli/acp/framing.js +118 -0
  7. package/dist/cli/acp/framing.js.map +1 -0
  8. package/dist/cli/acp/protocol.js +192 -0
  9. package/dist/cli/acp/protocol.js.map +1 -0
  10. package/dist/cli/acp/server.js +229 -0
  11. package/dist/cli/acp/server.js.map +1 -0
  12. package/dist/cli/acp/turnAdapter.js +144 -0
  13. package/dist/cli/acp/turnAdapter.js.map +1 -0
  14. package/dist/cli/commands/report.js +273 -0
  15. package/dist/cli/commands/report.js.map +1 -0
  16. package/dist/cli/components/statusChips.js +18 -1
  17. package/dist/cli/components/statusChips.js.map +1 -1
  18. package/dist/cli/main.bundled.js +2392 -763
  19. package/dist/cli/main.bundled.js.map +4 -4
  20. package/dist/cli/main.js +32 -0
  21. package/dist/cli/main.js.map +1 -1
  22. package/dist/cli/safety/jails/baseEnv.js +132 -0
  23. package/dist/cli/safety/jails/baseEnv.js.map +1 -0
  24. package/dist/cli/safety/jails/execPath.js +116 -0
  25. package/dist/cli/safety/jails/execPath.js.map +1 -0
  26. package/dist/cli/safety/jails/linux.js +19 -21
  27. package/dist/cli/safety/jails/linux.js.map +1 -1
  28. package/dist/cli/safety/osJail.js +29 -5
  29. package/dist/cli/safety/osJail.js.map +1 -1
  30. package/dist/cli/slashCommands.js +21 -1
  31. package/dist/cli/slashCommands.js.map +1 -1
  32. package/dist/cli/slashHandlers/statusline.js +126 -0
  33. package/dist/cli/slashHandlers/statusline.js.map +1 -0
  34. package/dist/cli/statusline/statuslineConfig.js +169 -0
  35. package/dist/cli/statusline/statuslineConfig.js.map +1 -0
  36. package/dist/cli/statusline/statuslineCustom.js +154 -0
  37. package/dist/cli/statusline/statuslineCustom.js.map +1 -0
  38. package/dist/cli/statusline/statuslineItems.js +93 -0
  39. package/dist/cli/statusline/statuslineItems.js.map +1 -0
  40. package/package.json +2 -2
@@ -0,0 +1,229 @@
1
+ /**
2
+ * acp/server — the stdio JSON-RPC 2.0 loop behind `zelari-code acp`.
3
+ *
4
+ * Responsibilities (and nothing else): decode frames, route methods, keep the
5
+ * loop NON-BLOCKING while a turn runs, translate failures into JSON-RPC
6
+ * errors, shut down cleanly on EOF. The agent runtime itself lives behind the
7
+ * injected `AcpTurnDispatcher` (turnAdapter.ts) — this module has no opinion
8
+ * about how a turn is executed, which keeps its tests model-free.
9
+ *
10
+ * Fail-soft invariants (P2), each covered by a test:
11
+ * - malformed frame -> reported to `log` (stderr), loop continues;
12
+ * - unknown method -> -32601, loop continues;
13
+ * - bad params -> -32602, loop continues;
14
+ * - internal throw -> -32603, loop continues (process stays alive);
15
+ * - stdin EOF/error -> in-flight prompts settle as 'cancelled', reader
16
+ * detaches, `onShutdown` fires once (exit 0 upstream).
17
+ *
18
+ * Protocol subset + explicit non-goals: see the header of protocol.ts.
19
+ */
20
+ import { randomUUID } from 'node:crypto';
21
+ import { attachFrameReader, createFrameWriter } from './framing.js';
22
+ import { AcpError, JSON_RPC_ERRORS, parseJsonRpcMessage, readInitializeParams, readPromptParams, readSessionIdParams, readSessionNewParams, sessionUpdateNotification, } from './protocol.js';
23
+ /** Agent capabilities advertised on `initialize` (no reverse requests). */
24
+ export const ACP_AGENT_CAPABILITIES = {
25
+ loadSession: false,
26
+ promptCapabilities: { image: false, audio: false, embeddedContext: false },
27
+ };
28
+ function resultResponse(id, result) {
29
+ return { jsonrpc: '2.0', id, result };
30
+ }
31
+ function errorResponse(id, code, message) {
32
+ return { jsonrpc: '2.0', id, error: { code, message } };
33
+ }
34
+ function errorCodeOf(err) {
35
+ return err instanceof AcpError ? err.code : JSON_RPC_ERRORS.INTERNAL_ERROR;
36
+ }
37
+ function errorMessageOf(err) {
38
+ return err instanceof Error ? err.message : String(err);
39
+ }
40
+ /** Start the ACP transport on the given (or process) stdio pair. */
41
+ export function startAcpServer(deps) {
42
+ const input = deps.input ?? process.stdin;
43
+ const log = deps.log ?? ((line) => process.stderr.write(`${line}\n`));
44
+ const sessions = new Map();
45
+ let closed = false;
46
+ // The frame writer binds sink.write NOW (framing.ts): a turn's stdout
47
+ // capture (turnAdapter.ts) can never swallow a JSON-RPC frame.
48
+ const writer = createFrameWriter(deps.output ?? process.stdout, (message) => log(`[zelari-code acp] stdout write failed: ${message}`));
49
+ const shutdownWaiters = [];
50
+ const reader = attachFrameReader(input, {
51
+ onMessage: (raw) => {
52
+ void handleMessage(raw);
53
+ },
54
+ onEof: () => shutdown(),
55
+ onMalformedFrame: (detail) => log(`[zelari-code acp] frame dropped (fail-soft): ${detail}`),
56
+ });
57
+ function shutdown() {
58
+ if (closed)
59
+ return;
60
+ closed = true;
61
+ for (const session of sessions.values())
62
+ cancelPrompt(session);
63
+ reader.detach();
64
+ for (const waiter of shutdownWaiters.splice(0))
65
+ waiter();
66
+ try {
67
+ deps.onShutdown?.();
68
+ }
69
+ catch {
70
+ /* shutdown is best-effort */
71
+ }
72
+ }
73
+ function cancelPrompt(session) {
74
+ const inflight = session.inflight;
75
+ if (!inflight)
76
+ return;
77
+ inflight.cancelled = true;
78
+ try {
79
+ inflight.abort.abort();
80
+ }
81
+ catch {
82
+ /* ignore */
83
+ }
84
+ inflight.settle({ stopReason: 'cancelled' });
85
+ }
86
+ async function handleMessage(raw) {
87
+ const parsed = parseJsonRpcMessage(raw);
88
+ if (parsed.kind === 'invalid') {
89
+ writer.write(errorResponse(parsed.id, JSON_RPC_ERRORS.INVALID_REQUEST, parsed.reason));
90
+ return;
91
+ }
92
+ if (parsed.kind === 'notification') {
93
+ handleNotification(parsed.notification.method, parsed.notification.params);
94
+ return;
95
+ }
96
+ const { id, method, params } = parsed.request;
97
+ let outcome;
98
+ try {
99
+ outcome = handleRequest(method, params);
100
+ }
101
+ catch (err) {
102
+ writer.write(errorResponse(id, errorCodeOf(err), errorMessageOf(err)));
103
+ return;
104
+ }
105
+ if (outcome.kind === 'result') {
106
+ writer.write(resultResponse(id, outcome.value));
107
+ return;
108
+ }
109
+ // Pending (a turn is running): the loop keeps reading stdin while the
110
+ // response is written when the dispatcher settles.
111
+ outcome.promise.then((value) => writer.write(resultResponse(id, value)), (err) => writer.write(errorResponse(id, errorCodeOf(err), errorMessageOf(err))));
112
+ }
113
+ /** Inbound notifications are never answered (JSON-RPC 2.0). */
114
+ function handleNotification(method, params) {
115
+ try {
116
+ if (method === 'initialized')
117
+ return; // handshake tail: nothing to do
118
+ if (method === 'session/cancel') {
119
+ const { sessionId } = readSessionIdParams(params, 'session/cancel');
120
+ const session = sessions.get(sessionId);
121
+ if (!session) {
122
+ log(`[zelari-code acp] session/cancel for unknown session '${sessionId}'`);
123
+ return;
124
+ }
125
+ cancelPrompt(session);
126
+ return;
127
+ }
128
+ if (method === '$/cancelRequest')
129
+ return; // acknowledged, no-op
130
+ log(`[zelari-code acp] ignoring unknown notification '${method}'`);
131
+ }
132
+ catch (err) {
133
+ log(`[zelari-code acp] notification '${method}' rejected: ${errorMessageOf(err)}`);
134
+ }
135
+ }
136
+ function handleRequest(method, params) {
137
+ switch (method) {
138
+ case 'initialize': {
139
+ const { protocolVersion } = readInitializeParams(params);
140
+ return {
141
+ kind: 'result',
142
+ value: {
143
+ protocolVersion,
144
+ agentCapabilities: ACP_AGENT_CAPABILITIES,
145
+ authMethods: [],
146
+ },
147
+ };
148
+ }
149
+ case 'session/new': {
150
+ const { cwd } = readSessionNewParams(params, deps.fallbackCwd);
151
+ const sessionId = randomUUID();
152
+ sessions.set(sessionId, { id: sessionId, cwd });
153
+ return { kind: 'result', value: { sessionId } };
154
+ }
155
+ case 'session/prompt': {
156
+ const { sessionId, text } = readPromptParams(params);
157
+ const session = sessions.get(sessionId);
158
+ if (!session) {
159
+ throw new AcpError(JSON_RPC_ERRORS.INVALID_PARAMS, `unknown session '${sessionId}'`);
160
+ }
161
+ if (session.inflight) {
162
+ throw new AcpError(JSON_RPC_ERRORS.INVALID_PARAMS, `session '${sessionId}' already has a prompt in flight`);
163
+ }
164
+ return { kind: 'pending', promise: startPrompt(session, text) };
165
+ }
166
+ case 'session/cancel': {
167
+ const { sessionId } = readSessionIdParams(params, 'session/cancel');
168
+ const session = sessions.get(sessionId);
169
+ if (!session) {
170
+ throw new AcpError(JSON_RPC_ERRORS.INVALID_PARAMS, `unknown session '${sessionId}'`);
171
+ }
172
+ cancelPrompt(session);
173
+ return { kind: 'result', value: null };
174
+ }
175
+ default:
176
+ throw new AcpError(JSON_RPC_ERRORS.METHOD_NOT_FOUND, `method not found: ${method}`);
177
+ }
178
+ }
179
+ function startPrompt(session, text) {
180
+ const record = {
181
+ cancelled: false,
182
+ settled: false,
183
+ abort: new AbortController(),
184
+ settle: () => { },
185
+ };
186
+ const promise = new Promise((resolve) => {
187
+ record.settle = (result) => {
188
+ if (record.settled)
189
+ return;
190
+ record.settled = true;
191
+ resolve(result);
192
+ };
193
+ });
194
+ session.inflight = record;
195
+ const onUpdate = (update) => {
196
+ // A cancelled turn stops streaming: the client was already told so.
197
+ if (record.cancelled)
198
+ return;
199
+ writer.write(sessionUpdateNotification(session.id, update));
200
+ };
201
+ void deps
202
+ .dispatcher({
203
+ sessionId: session.id,
204
+ cwd: session.cwd,
205
+ prompt: text,
206
+ onUpdate,
207
+ signal: record.abort.signal,
208
+ })
209
+ .then((result) => record.settle({ stopReason: record.cancelled ? 'cancelled' : result.stopReason }), (err) => {
210
+ log(`[zelari-code acp] turn failed: ${errorMessageOf(err)}`);
211
+ record.settle({ stopReason: record.cancelled ? 'cancelled' : 'refusal' });
212
+ })
213
+ .finally(() => {
214
+ if (session.inflight === record)
215
+ session.inflight = undefined;
216
+ });
217
+ return promise;
218
+ }
219
+ return {
220
+ sessionIds: () => [...sessions.keys()],
221
+ close: () => shutdown(),
222
+ whenClosed: () => closed
223
+ ? Promise.resolve()
224
+ : new Promise((resolve) => {
225
+ shutdownWaiters.push(resolve);
226
+ }),
227
+ };
228
+ }
229
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/cli/acp/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ import { drainLines, mapCapturedLine, } from './eventMap.js';
2
+ import { agentMessageChunk } from './protocol.js';
3
+ /** Swap `process.stdout.write` for the duration of a turn. Returns restore. */
4
+ export function captureStdout(onChunk) {
5
+ const original = process.stdout.write;
6
+ const patched = ((chunk, ...rest) => {
7
+ try {
8
+ const text = typeof chunk === 'string'
9
+ ? chunk
10
+ : Buffer.isBuffer(chunk)
11
+ ? chunk.toString('utf8')
12
+ : String(chunk);
13
+ onChunk(text);
14
+ }
15
+ catch {
16
+ /* a capture bug must never kill the turn */
17
+ }
18
+ // Callers (e.g. content builders) may pass a completion callback; honour it
19
+ // so nothing can hang waiting on a write that "succeeded".
20
+ const last = rest[rest.length - 1];
21
+ if (typeof last === 'function') {
22
+ try {
23
+ last();
24
+ }
25
+ catch {
26
+ /* ignore */
27
+ }
28
+ }
29
+ return true;
30
+ });
31
+ process.stdout.write = patched;
32
+ return () => {
33
+ process.stdout.write = original;
34
+ };
35
+ }
36
+ /**
37
+ * Serialize turn execution: the stdout capture is process-global, so exactly
38
+ * one turn may be in flight. Later prompts queue behind earlier ones.
39
+ */
40
+ function createSingleFlight() {
41
+ let chain = Promise.resolve();
42
+ return (fn) => {
43
+ const run = chain.then(fn, fn);
44
+ chain = run.then(() => undefined, () => undefined);
45
+ return run;
46
+ };
47
+ }
48
+ /** Real dispatcher: lazy provider resolution + stdout capture, serialized. */
49
+ export function createHeadlessTurnDispatcher(deps = {}) {
50
+ const enqueue = createSingleFlight();
51
+ const mode = deps.mode ?? 'kraken';
52
+ let streamPromise = null;
53
+ const ensureStream = () => {
54
+ if (!streamPromise) {
55
+ streamPromise = (async () => {
56
+ if (deps.resolveStream)
57
+ return deps.resolveStream();
58
+ // Dynamic imports keep the TUI/provider graph out of the module load
59
+ // path until a turn actually needs it (same discipline as runHeadless).
60
+ const { resolveHeadlessKey, resolveHeadlessProvider } = await import('../headless.js');
61
+ const { provider, model } = resolveHeadlessProvider({
62
+ ...(deps.provider ? { provider: deps.provider } : {}),
63
+ ...(deps.model ? { model: deps.model } : {}),
64
+ });
65
+ const key = await resolveHeadlessKey(provider);
66
+ if ('error' in key)
67
+ throw new Error(key.error);
68
+ const { buildProviderStream } = await import('../provider/resolveStream.js');
69
+ const stream = buildProviderStream({
70
+ providerId: provider,
71
+ apiKey: key.apiKey,
72
+ baseUrl: key.baseUrl,
73
+ model,
74
+ });
75
+ return { provider, model, stream };
76
+ })();
77
+ streamPromise.catch(() => {
78
+ streamPromise = null; // allow a retry once the key materializes
79
+ });
80
+ }
81
+ return streamPromise;
82
+ };
83
+ const runTurn = deps.dispatch ?? defaultDispatch;
84
+ return (request) => enqueue(async () => {
85
+ if (request.signal.aborted)
86
+ return { stopReason: 'cancelled', exitCode: 0 };
87
+ const { provider, model, stream } = await ensureStream();
88
+ const opts = {
89
+ task: request.prompt,
90
+ output: 'json',
91
+ mode,
92
+ phase: 'build',
93
+ useCouncil: mode === 'council',
94
+ cwd: request.cwd,
95
+ };
96
+ const buffer = { text: '' };
97
+ const emit = (line) => {
98
+ for (const update of mapCapturedLine(line))
99
+ request.onUpdate(update);
100
+ };
101
+ const restore = captureStdout((chunk) => {
102
+ buffer.text += chunk;
103
+ drainLines(buffer, false, emit);
104
+ });
105
+ const priorHostMarker = process.env['ZELARI_SERVE_HARNESS'];
106
+ process.env['ZELARI_SERVE_HARNESS'] = '1';
107
+ let exitCode = 2;
108
+ try {
109
+ exitCode = await runTurn(opts, provider, model, stream);
110
+ }
111
+ catch (err) {
112
+ request.onUpdate(agentMessageChunk(`\n[zelari-code acp] turn failed: ${err instanceof Error ? err.message : String(err)}\n`));
113
+ exitCode = 2;
114
+ }
115
+ finally {
116
+ if (priorHostMarker === undefined)
117
+ delete process.env['ZELARI_SERVE_HARNESS'];
118
+ else
119
+ process.env['ZELARI_SERVE_HARNESS'] = priorHostMarker;
120
+ restore();
121
+ drainLines(buffer, true, emit);
122
+ }
123
+ return {
124
+ stopReason: request.signal.aborted ? 'cancelled' : stopReasonForExit(exitCode),
125
+ exitCode,
126
+ };
127
+ });
128
+ }
129
+ /**
130
+ * Headless exit codes (headless.ts header): 0 completed, 1 user error,
131
+ * 2 runtime error, 3 agent errored, 5/6 strict evidence gates. ACP only has
132
+ * `end_turn | max_tokens | refusal | cancelled`; every non-zero outcome is
133
+ * reported as `refusal` (the turn did not deliver) — the exit code travels
134
+ * with AcpTurnResult for hosts that log it.
135
+ */
136
+ function stopReasonForExit(exitCode) {
137
+ return exitCode === 0 ? 'end_turn' : 'refusal';
138
+ }
139
+ /** Default turn runner — the real headless dispatch (imported lazily). */
140
+ async function defaultDispatch(opts, provider, model, stream) {
141
+ const { dispatchHeadlessTurn } = await import('../runHeadless.js');
142
+ return dispatchHeadlessTurn(opts, provider, model, stream);
143
+ }
144
+ //# sourceMappingURL=turnAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turnAdapter.js","sourceRoot":"","sources":["../../../src/cli/acp/turnAdapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,273 @@
1
+ /**
2
+ * commands/report — `/report`: one aggregate view of the ACTIVE session spine.
3
+ *
4
+ * WHAT IT READS: the session spine (`<sessionsDir>/<sessionId>/events.jsonl`,
5
+ * ADR-0016 — `resolveSessionsDir` honors ZELARI_SESSIONS_DIR + workspaceRoot),
6
+ * parsed with the CORE reader (`parseSessionLogText` → `buildProjection`,
7
+ * @zelari/core/session). Nothing is re-parsed by hand: the tolerant reader
8
+ * already reports corrupt lines as `ReplayIssue`s and skips retired kinds.
9
+ *
10
+ * WHAT IT PRINTS (markdown-ish text, deterministic order):
11
+ * - header: session id, event count, issues (corrupt/unknown lines);
12
+ * - files touched: unique paths + per-kind counters (`file.read`,
13
+ * `file.applied`, `file.rejected` — the ADR-0033 spine SSOT). Spines that
14
+ * predate ADR-0033 fall back to the `tool.call` args of the file tools,
15
+ * and the header SAYS SO (never a silent different source);
16
+ * - tool calls per tool (`tool.call`), with the result count;
17
+ * - verify outcomes: the LAST `verification.run` per criterion + totals;
18
+ * - tokens/cost: ONLY when those numbers are already on the spine
19
+ * (compaction projections + any event carrying usage/cost fields).
20
+ *
21
+ * FAIL-SOFT: a missing sessions dir, an unknown session id, an unreadable
22
+ * file, or a spine with no events all produce a clear message and exit 0 —
23
+ * this command is diagnostics, never a gate.
24
+ */
25
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
26
+ import path from 'node:path';
27
+ import { buildProjection, parseSessionLogText, resolveSessionsDir } from '@zelari/core/session';
28
+ import { getCurrentSessionId } from '../sessionManager.js';
29
+ const FILE_TOOLS = new Set(['read_file', 'write_file', 'edit']);
30
+ /** Newest spine session dir that actually holds an events.jsonl (null when none). */
31
+ function newestSessionId(sessionsDir) {
32
+ try {
33
+ const names = readdirSync(sessionsDir).filter((n) => existsSync(path.join(sessionsDir, n, 'events.jsonl')));
34
+ if (names.length === 0)
35
+ return null;
36
+ const scored = names.map((n) => {
37
+ try {
38
+ const text = readFileSync(path.join(sessionsDir, n, 'events.jsonl'), 'utf-8');
39
+ const last = parseSessionLogText('events.jsonl', text).events;
40
+ return { n, ts: last[last.length - 1]?.ts ?? 0 };
41
+ }
42
+ catch {
43
+ return { n, ts: 0 };
44
+ }
45
+ });
46
+ scored.sort((a, b) => b.ts - a.ts);
47
+ return scored[0]?.n ?? null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /** Human path (cwd-relative when possible) so the report stays readable. */
54
+ function shortPath(p, cwd) {
55
+ const rel = path.relative(cwd, p);
56
+ return rel && !rel.startsWith('..') ? rel : p;
57
+ }
58
+ function render(report, cwd, sessionId) {
59
+ const projection = buildProjection(report.events, report.issues);
60
+ const lines = [];
61
+ lines.push(`# session report — ${sessionId}`);
62
+ lines.push('');
63
+ lines.push(`events: ${projection.eventCount} tool calls: ${projection.toolCalls} results: ${projection.toolResults}`);
64
+ if (report.issues.length > 0) {
65
+ const kinds = new Map();
66
+ for (const issue of report.issues)
67
+ kinds.set(issue.type, (kinds.get(issue.type) ?? 0) + 1);
68
+ lines.push(`spine issues: ${report.issues.length} (` +
69
+ [...kinds.entries()].map(([t, n]) => `${t}×${n}`).join(', ') +
70
+ ') — corrupt/unknown lines were skipped by the tolerant reader');
71
+ }
72
+ if (projection.startedAt !== undefined) {
73
+ lines.push(`started: ${new Date(projection.startedAt).toISOString()}`);
74
+ }
75
+ if (projection.endedAt !== undefined) {
76
+ lines.push(`ended: ${new Date(projection.endedAt).toISOString()}`);
77
+ }
78
+ // ── files ───────────────────────────────────────────────────────────────
79
+ const files = new Map();
80
+ const touch = (p) => {
81
+ const key = p;
82
+ const found = files.get(key);
83
+ if (found)
84
+ return found;
85
+ const created = { path: p, read: 0, applied: 0, rejected: 0 };
86
+ files.set(key, created);
87
+ return created;
88
+ };
89
+ let fileEvents = 0;
90
+ for (const e of report.events) {
91
+ const p = typeof e.data.path === 'string' ? e.data.path : null;
92
+ if (!p)
93
+ continue;
94
+ if (e.kind === 'file.read') {
95
+ touch(p).read += 1;
96
+ fileEvents += 1;
97
+ }
98
+ else if (e.kind === 'file.applied') {
99
+ touch(p).applied += 1;
100
+ fileEvents += 1;
101
+ }
102
+ else if (e.kind === 'file.rejected') {
103
+ touch(p).rejected += 1;
104
+ fileEvents += 1;
105
+ }
106
+ }
107
+ let filesSource = 'file.* spine events (ADR-0033)';
108
+ if (fileEvents === 0) {
109
+ // Pre-ADR-0033 spine: derive from the tool.call args instead, and say so.
110
+ filesSource = 'tool.call args (legacy spine: no file.* events)';
111
+ for (const e of report.events) {
112
+ if (e.kind !== 'tool.call')
113
+ continue;
114
+ const tool = typeof e.data.tool === 'string' ? e.data.tool : '';
115
+ if (!FILE_TOOLS.has(tool))
116
+ continue;
117
+ const args = e.data.args;
118
+ const p = typeof args?.path === 'string' ? args.path : null;
119
+ if (p)
120
+ touch(p).applied += 1;
121
+ }
122
+ }
123
+ lines.push('');
124
+ lines.push(`## files touched — ${files.size} unique (${filesSource})`);
125
+ if (files.size === 0) {
126
+ lines.push(' (none)');
127
+ }
128
+ else {
129
+ const sorted = [...files.values()].sort((a, b) => a.path.localeCompare(b.path));
130
+ for (const f of sorted) {
131
+ const marks = [
132
+ f.read > 0 ? `read×${f.read}` : '',
133
+ f.applied > 0 ? `write×${f.applied}` : '',
134
+ f.rejected > 0 ? `rejected×${f.rejected}` : '',
135
+ ]
136
+ .filter(Boolean)
137
+ .join(' ');
138
+ lines.push(` ${shortPath(f.path, cwd)} ${marks}`);
139
+ }
140
+ }
141
+ // ── tools ───────────────────────────────────────────────────────────────
142
+ const byTool = new Map();
143
+ for (const e of report.events) {
144
+ if (e.kind !== 'tool.call')
145
+ continue;
146
+ const tool = typeof e.data.tool === 'string' && e.data.tool.length > 0 ? e.data.tool : '(unknown)';
147
+ byTool.set(tool, (byTool.get(tool) ?? 0) + 1);
148
+ }
149
+ lines.push('');
150
+ lines.push('## tool calls');
151
+ if (byTool.size === 0) {
152
+ lines.push(' (none)');
153
+ }
154
+ else {
155
+ const sorted = [...byTool.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
156
+ for (const [tool, count] of sorted)
157
+ lines.push(` ${String(count).padStart(4)} ${tool}`);
158
+ }
159
+ // ── verify ──────────────────────────────────────────────────────────────
160
+ lines.push('');
161
+ lines.push('## verify');
162
+ if (projection.verifications.length === 0) {
163
+ lines.push(' (no verification.run events on this spine)');
164
+ }
165
+ else {
166
+ const last = projection.verifications[projection.verifications.length - 1];
167
+ const pass = last.results.filter((r) => r.status === 'pass').length;
168
+ const fail = last.results.filter((r) => r.status !== 'pass').length;
169
+ lines.push(` runs: ${projection.verifications.length} last: ${new Date(last.at).toISOString()} — ` +
170
+ `${pass} pass / ${fail} fail${last.complete === undefined ? '' : last.complete ? ' (complete)' : ' (incomplete)'}`);
171
+ for (const r of last.results) {
172
+ lines.push(` ${r.status.padEnd(8)} ${r.criterionId} (evidence ${r.evidenceCount})`);
173
+ }
174
+ }
175
+ // ── tokens / cost (only what the spine already carries) ─────────────────
176
+ let inputTokens = 0;
177
+ let outputTokens = 0;
178
+ let savedTokens = 0;
179
+ let costUsd = 0;
180
+ let sawTokens = false;
181
+ let sawCost = false;
182
+ for (const e of report.events) {
183
+ const d = e.data;
184
+ if (e.kind === 'session.compacted') {
185
+ if (typeof d.inputTokens === 'number') {
186
+ inputTokens += d.inputTokens;
187
+ sawTokens = true;
188
+ }
189
+ if (typeof d.outputTokens === 'number') {
190
+ outputTokens += d.outputTokens;
191
+ sawTokens = true;
192
+ }
193
+ if (typeof d.savedTokens === 'number') {
194
+ savedTokens += d.savedTokens;
195
+ sawTokens = true;
196
+ }
197
+ }
198
+ if (typeof d.costUsd === 'number') {
199
+ costUsd += d.costUsd;
200
+ sawCost = true;
201
+ }
202
+ const usage = d.usage;
203
+ if (usage && typeof usage === 'object') {
204
+ const u = usage;
205
+ if (typeof u.inputTokens === 'number') {
206
+ inputTokens += u.inputTokens;
207
+ sawTokens = true;
208
+ }
209
+ if (typeof u.outputTokens === 'number') {
210
+ outputTokens += u.outputTokens;
211
+ sawTokens = true;
212
+ }
213
+ if (typeof u.costUsd === 'number') {
214
+ costUsd += u.costUsd;
215
+ sawCost = true;
216
+ }
217
+ }
218
+ }
219
+ if (sawTokens || sawCost) {
220
+ lines.push('');
221
+ lines.push('## tokens / cost (as recorded on the spine)');
222
+ if (sawTokens) {
223
+ lines.push(` compaction tokens: in ${inputTokens} / out ${outputTokens} / saved ${savedTokens}`);
224
+ }
225
+ if (sawCost)
226
+ lines.push(` cost: $${costUsd.toFixed(4)}`);
227
+ }
228
+ return lines.join('\n');
229
+ }
230
+ /**
231
+ * Build the report. Never throws: every failure mode returns
232
+ * `{ ok: false, markdown: <clear message> }` (exit 0 by design).
233
+ */
234
+ export function buildSessionReport(opts = {}) {
235
+ const cwd = opts.cwd ?? process.cwd();
236
+ const sessionsDir = resolveSessionsDir({ workspaceRoot: cwd, env: opts.env });
237
+ const marker = opts.sessionId ?? getCurrentSessionId() ?? undefined;
238
+ const marked = marker ? path.join(sessionsDir, marker, 'events.jsonl') : null;
239
+ const sessionId = marker && marked && existsSync(marked) ? marker : newestSessionId(sessionsDir);
240
+ if (!sessionId) {
241
+ return {
242
+ ok: false,
243
+ sessionId: '',
244
+ markdown: `no session spine found under ${sessionsDir} — nothing to report (this is not an error).`,
245
+ };
246
+ }
247
+ const eventsPath = path.join(sessionsDir, sessionId, 'events.jsonl');
248
+ let text;
249
+ try {
250
+ text = readFileSync(eventsPath, 'utf-8');
251
+ }
252
+ catch (err) {
253
+ const msg = err instanceof Error ? err.message : String(err);
254
+ return { ok: false, sessionId, markdown: `cannot read the session spine at ${eventsPath}: ${msg}` };
255
+ }
256
+ const parsed = parseSessionLogText(eventsPath, text);
257
+ if (parsed.events.length === 0) {
258
+ return {
259
+ ok: false,
260
+ sessionId,
261
+ markdown: `session ${sessionId}: the spine holds no readable events (${parsed.issues.length} issue(s)) — ${eventsPath}`,
262
+ };
263
+ }
264
+ return { ok: true, sessionId, markdown: render(parsed, cwd, sessionId) };
265
+ }
266
+ /** CLI entry: print the report to stdout; ALWAYS exit 0 (fail-soft). */
267
+ export function runReport(opts = {}) {
268
+ const report = buildSessionReport(opts);
269
+ // eslint-disable-next-line no-console
270
+ console.log(report.markdown);
271
+ return 0;
272
+ }
273
+ //# sourceMappingURL=report.js.map