@basou/core 0.10.0 → 0.11.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.
package/dist/index.js CHANGED
@@ -4025,6 +4025,17 @@ function parseDuration(input) {
4025
4025
  return ms;
4026
4026
  }
4027
4027
 
4028
+ // src/lib/format-duration.ts
4029
+ function formatDurationMs(ms) {
4030
+ const totalSeconds = Math.round(ms / 1e3);
4031
+ const hours = Math.floor(totalSeconds / 3600);
4032
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
4033
+ const seconds = totalSeconds % 60;
4034
+ if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
4035
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
4036
+ return `${seconds}s`;
4037
+ }
4038
+
4028
4039
  // src/lib/id-resolver.ts
4029
4040
  async function resolveSessionId(paths, input) {
4030
4041
  return resolveIdInternal(paths, input, "session");
@@ -4080,319 +4091,314 @@ async function resolveIdInternal(paths, input, kind, options = {}) {
4080
4091
  return matches[0];
4081
4092
  }
4082
4093
 
4083
- // src/runtime/child-process-runner.ts
4084
- import { spawn as spawn2 } from "child_process";
4085
- var DEFAULT_KILL_GRACE_MS = 5e3;
4086
- var ChildProcessRunner = class {
4087
- async run(command, args, options) {
4088
- validateOptions(options);
4089
- if (options.signal?.aborted) {
4090
- throw new Error("Process aborted before spawn", {
4091
- cause: options.signal.reason
4092
- });
4093
- }
4094
- const snapshotCommand = command;
4095
- const snapshotArgs = [...args];
4096
- const snapshotCwd = options.cwd;
4097
- const captureMode = options.capture ?? "buffer";
4098
- const started_at = /* @__PURE__ */ new Date();
4099
- let child;
4094
+ // src/report/report-renderer.ts
4095
+ import { join as join14 } from "path";
4096
+
4097
+ // src/stats/work-stats.ts
4098
+ import { join as join13 } from "path";
4099
+ function resolveTimeZone(timeZone) {
4100
+ if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
4101
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
4102
+ }
4103
+ var STATUS_ORDER = [
4104
+ "completed",
4105
+ "failed",
4106
+ "running",
4107
+ "interrupted",
4108
+ "waiting_approval",
4109
+ "initialized",
4110
+ "imported",
4111
+ "archived"
4112
+ ];
4113
+ async function computeWorkStats(input) {
4114
+ const { now } = input;
4115
+ const timeZone = resolveTimeZone(input.timeZone);
4116
+ const unreadableEmitted = /* @__PURE__ */ new Set();
4117
+ const wrappedSkip = (sid, reason) => {
4118
+ if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
4119
+ input.onSessionSkip?.(sid, reason);
4120
+ };
4121
+ const loadOpts = { now, onSkip: wrappedSkip };
4122
+ if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
4123
+ const entries = await loadSessionEntries(input.paths, loadOpts);
4124
+ const sessions = [];
4125
+ for (const entry of entries) {
4126
+ const events = [];
4127
+ let eventsUnreadable = false;
4100
4128
  try {
4101
- child = spawn2(snapshotCommand, [...snapshotArgs], {
4102
- cwd: snapshotCwd,
4103
- env: options.env ?? process.env,
4104
- stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
4105
- shell: false,
4106
- detached: false
4107
- });
4108
- } catch (error) {
4109
- throw classifySpawnError(error);
4110
- }
4111
- if (options.onSpawn) {
4112
- try {
4113
- options.onSpawn(child);
4114
- } catch {
4129
+ for await (const ev of replayEvents(join13(input.paths.sessions, entry.sessionId), {
4130
+ onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4131
+ })) {
4132
+ events.push(ev);
4115
4133
  }
4116
- }
4117
- let timeoutTimer = null;
4118
- let killTimer = null;
4119
- let killed = false;
4120
- let settled = false;
4121
- const triggerKill = () => {
4122
- if (killed || child.exitCode !== null) return;
4123
- killed = true;
4124
- child.kill("SIGTERM");
4125
- killTimer = setTimeout(() => {
4126
- if (child.exitCode === null) {
4127
- child.kill("SIGKILL");
4128
- }
4129
- }, DEFAULT_KILL_GRACE_MS);
4130
- };
4131
- const onAbort = () => {
4132
- triggerKill();
4133
- };
4134
- options.signal?.addEventListener("abort", onAbort);
4135
- if (options.signal?.aborted) {
4136
- triggerKill();
4137
- }
4138
- let stdout = "";
4139
- let stderr = "";
4140
- if (captureMode === "buffer") {
4141
- child.stdout?.setEncoding("utf8");
4142
- child.stderr?.setEncoding("utf8");
4143
- child.stdout?.on("data", (chunk) => {
4144
- stdout += chunk;
4145
- });
4146
- child.stderr?.on("data", (chunk) => {
4147
- stderr += chunk;
4148
- });
4149
- if (options.stdin !== void 0) {
4150
- child.stdin?.end(options.stdin);
4151
- } else {
4152
- child.stdin?.end();
4134
+ } catch {
4135
+ eventsUnreadable = true;
4136
+ if (!unreadableEmitted.has(entry.sessionId)) {
4137
+ wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
4153
4138
  }
4154
4139
  }
4155
- if (options.timeout_ms !== void 0) {
4156
- timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
4157
- }
4158
- const cleanup = () => {
4159
- if (timeoutTimer !== null) clearTimeout(timeoutTimer);
4160
- if (killTimer !== null) clearTimeout(killTimer);
4161
- options.signal?.removeEventListener("abort", onAbort);
4162
- };
4163
- return new Promise((resolve2, reject) => {
4164
- child.once("error", (error) => {
4165
- if (settled) return;
4166
- settled = true;
4167
- cleanup();
4168
- reject(classifySpawnError(error));
4169
- });
4170
- child.once("close", (code, signal) => {
4171
- if (settled) return;
4172
- settled = true;
4173
- cleanup();
4174
- const ended_at = /* @__PURE__ */ new Date();
4175
- resolve2({
4176
- command: snapshotCommand,
4177
- args: snapshotArgs,
4178
- cwd: snapshotCwd,
4179
- exit_code: code,
4180
- signal,
4181
- stdout,
4182
- stderr,
4183
- started_at: started_at.toISOString(),
4184
- ended_at: ended_at.toISOString(),
4185
- duration_ms: ended_at.getTime() - started_at.getTime(),
4186
- pid: child.pid ?? null
4187
- });
4188
- });
4189
- });
4190
- }
4191
- };
4192
- function validateOptions(options) {
4193
- if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
4194
- throw new Error("Invalid timeout_ms");
4140
+ sessions.push(
4141
+ sessionWorkStatsFromEvents(
4142
+ entry.sessionId,
4143
+ entry.session.session,
4144
+ events,
4145
+ now,
4146
+ eventsUnreadable
4147
+ )
4148
+ );
4195
4149
  }
4196
- if (options.capture === "none" && options.stdin !== void 0) {
4197
- throw new Error('Combination of capture: "none" and stdin is not supported');
4150
+ const allIntervals = [];
4151
+ for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
4152
+ const union = unionDurationMs(allIntervals);
4153
+ return {
4154
+ generatedAt: now.toISOString(),
4155
+ activeGapCapMs: ACTIVE_GAP_CAP_MS,
4156
+ timeZone,
4157
+ totals: computeTotals(sessions, union.ms),
4158
+ sessions,
4159
+ bySource: computeBySource(sessions),
4160
+ byStatus: computeByStatus(sessions),
4161
+ byDay: computeByDay(sessions, union.merged, timeZone)
4162
+ };
4163
+ }
4164
+ function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
4165
+ let commandCount = 0;
4166
+ let fileChangedCount = 0;
4167
+ let decisionCount = 0;
4168
+ let commandTimeMs = 0;
4169
+ const timestamps = [];
4170
+ for (const ev of events) {
4171
+ const t = Date.parse(ev.occurred_at);
4172
+ if (Number.isFinite(t)) timestamps.push(t);
4173
+ if (ev.type === "command_executed") {
4174
+ commandCount++;
4175
+ commandTimeMs += ev.duration_ms;
4176
+ } else if (ev.type === "file_changed") {
4177
+ fileChangedCount++;
4178
+ } else if (ev.type === "decision_recorded") {
4179
+ decisionCount++;
4180
+ }
4198
4181
  }
4182
+ const span = computeSpan(inner.started_at, inner.ended_at, now);
4183
+ const tokens = readTokens(inner.metrics);
4184
+ const active = resolveActiveTime(inner.metrics, timestamps);
4185
+ const machineActiveTimeMs = inner.metrics?.machine_active_time_ms ?? 0;
4186
+ return {
4187
+ sessionId,
4188
+ label: inner.label,
4189
+ status: inner.status,
4190
+ sourceKind: inner.source.kind,
4191
+ startedAt: inner.started_at,
4192
+ endedAt: inner.ended_at,
4193
+ open: inner.ended_at === void 0,
4194
+ sessionSpanMs: span.ms,
4195
+ commandTimeMs,
4196
+ activeTimeMs: active.ms,
4197
+ activeTimeBasis: active.basis,
4198
+ activeIntervals: intervalsMsToIso(active.intervals),
4199
+ machineActiveTimeMs,
4200
+ activeTimeMethod: inner.metrics?.active_time_method,
4201
+ commandCount,
4202
+ fileChangedCount,
4203
+ decisionCount,
4204
+ eventCount: events.length,
4205
+ tokens,
4206
+ availability: {
4207
+ span: true,
4208
+ commandTime: inner.source.kind !== "claude-code-import",
4209
+ activeTime: active.intervals.length > 0,
4210
+ tokens: hasTokens(tokens),
4211
+ machineActive: machineActiveTimeMs > 0
4212
+ },
4213
+ spanClamped: span.clamped,
4214
+ eventsUnreadable
4215
+ };
4199
4216
  }
4200
- function classifySpawnError(error) {
4201
- if (findErrorCode(error, "ENOENT")) {
4202
- return new Error("Command not found", { cause: error });
4217
+ function resolveActiveTime(metrics, eventTimestamps) {
4218
+ const stored = metrics?.active_intervals;
4219
+ if (stored !== void 0 && stored.length > 0) {
4220
+ const intervals = intervalsIsoToMs(stored);
4221
+ const ms = intervals.reduce((n, [start, end]) => n + (end - start), 0);
4222
+ return { ms, intervals, basis: "engaged-turns" };
4203
4223
  }
4204
- return new Error("Failed to spawn child process", { cause: error });
4224
+ const derived = activeTimeFromTimestamps(eventTimestamps, ACTIVE_GAP_CAP_MS);
4225
+ return { ms: derived.ms, intervals: derived.intervals, basis: "events" };
4205
4226
  }
4206
-
4207
- // src/schemas/json-schema.ts
4208
- import { z as z11 } from "zod";
4209
-
4210
- // src/schemas/manifest.schema.ts
4211
- import { z as z9 } from "zod";
4212
- var ProjectSchema = z9.object({
4213
- name: z9.string().optional(),
4214
- description: z9.string().optional(),
4215
- repository_url: z9.string().nullable().optional()
4216
- });
4217
- var CapabilitiesSchema = z9.object({
4218
- enabled: z9.array(z9.string())
4219
- });
4220
- var ApprovalConfigSchema = z9.object({
4221
- required_for: z9.array(z9.string()).optional(),
4222
- default_risk_level: z9.enum(["low", "medium", "high", "critical"])
4223
- });
4224
- var ClaudeCodeAdapterConfigSchema = z9.object({
4225
- enabled: z9.boolean(),
4226
- config_path: z9.string().optional()
4227
- });
4228
- var AdaptersSchema = z9.object({
4229
- "claude-code": ClaudeCodeAdapterConfigSchema
4230
- });
4231
- var GitConfigSchema = z9.object({
4232
- events_log: z9.enum(["ignore", "commit"]).default("ignore")
4233
- });
4234
- var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)[^\0\\]+$/;
4235
- var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
4236
- message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
4237
- });
4238
- var ImportConfigSchema = z9.object({
4239
- source_roots: z9.array(SourceRootSchema).min(1).optional()
4240
- });
4241
- var WorkspaceMetaSchema = z9.object({
4242
- id: WorkspaceIdSchema,
4243
- name: z9.string().min(1),
4244
- created_at: IsoTimestampSchema,
4245
- updated_at: IsoTimestampSchema
4246
- });
4247
- var ManifestSchema = z9.object({
4248
- schema_version: SchemaVersionSchema,
4249
- basou_version: z9.literal("0.1.0"),
4250
- workspace: WorkspaceMetaSchema,
4251
- project: ProjectSchema,
4252
- capabilities: CapabilitiesSchema,
4253
- approval: ApprovalConfigSchema,
4254
- adapters: AdaptersSchema,
4255
- git: GitConfigSchema,
4256
- import: ImportConfigSchema.optional()
4257
- });
4258
-
4259
- // src/schemas/session-import.schema.ts
4260
- import { z as z10 } from "zod";
4261
- var SessionInnerImportSchema = z10.object({
4262
- id: SessionIdSchema.optional(),
4263
- label: z10.string().optional(),
4264
- task_id: TaskIdSchema.nullable().optional(),
4265
- workspace_id: WorkspaceIdSchema,
4266
- source: z10.object({
4267
- kind: SessionSourceKindSchema,
4268
- version: z10.literal("0.1.0"),
4269
- // Source-tool-native id (e.g. Claude Code session UUID), retained so
4270
- // re-imports of the same source can be deduplicated.
4271
- external_id: z10.string().optional(),
4272
- // Byte size of the source native log at import time. Declared here too
4273
- // (not only in session.schema.ts) because this inner `source` object is
4274
- // a plain z.object: zod strips keys it does not declare, so a field
4275
- // absent here would be dropped from the parsed payload before persist
4276
- // and the size could never be stored.
4277
- source_size_bytes: z10.number().int().nonnegative().optional()
4278
- }),
4279
- started_at: IsoTimestampSchema,
4280
- ended_at: IsoTimestampSchema.optional(),
4281
- status: SessionStatusSchema,
4282
- working_directory: z10.string().min(1),
4283
- invocation: z10.object({
4284
- command: z10.string().min(1),
4285
- args: z10.array(z10.string()),
4286
- exit_code: z10.number().int().nullable()
4287
- }),
4288
- related_files: z10.array(z10.string()).default([]),
4289
- events_log: z10.string().optional(),
4290
- summary: z10.string().nullable().optional(),
4291
- metrics: SessionMetricsSchema.optional(),
4292
- // Accepted so a payload assembled from an on-disk chained session.yaml
4293
- // round-trips, and DISCARDED by the importer (buildSessionRecord never
4294
- // copies it): the integrity anchor is computed at write time, never
4295
- // imported. Mirrors the accept-and-discard of `prev_hash` on events.
4296
- integrity: SessionIntegritySchema.optional()
4297
- }).strict();
4298
- var SessionImportPayloadSchema = z10.object({
4299
- schema_version: z10.string(),
4300
- session: SessionInnerImportSchema,
4301
- events: z10.array(EventSchema)
4302
- }).strict();
4303
-
4304
- // src/schemas/json-schema.ts
4305
- var JSON_SCHEMA_VERSION = "0.1.0";
4306
- var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
4307
- var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
4308
- var DOCUMENTS = [
4309
- {
4310
- name: "manifest",
4311
- schema: ManifestSchema,
4312
- title: "Basou Manifest",
4313
- description: "The `.basou/manifest.yaml` workspace manifest."
4314
- },
4315
- {
4316
- name: "session",
4317
- schema: SessionSchema,
4318
- title: "Basou Session",
4319
- description: "A `.basou/sessions/<id>/session.yaml` session record."
4320
- },
4321
- {
4322
- name: "event",
4323
- schema: EventSchema,
4324
- title: "Basou Event",
4325
- description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
4326
- },
4327
- {
4328
- name: "task",
4329
- schema: TaskSchema,
4330
- title: "Basou Task",
4331
- description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
4332
- },
4333
- {
4334
- name: "approval",
4335
- schema: ApprovalSchema,
4336
- title: "Basou Approval",
4337
- description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
4338
- },
4339
- {
4340
- name: "status",
4341
- schema: StatusSchema,
4342
- title: "Basou Status",
4343
- description: "The `.basou/status.json` workspace status snapshot."
4344
- },
4345
- {
4346
- name: "task-index",
4347
- schema: TaskIndexSchema,
4348
- title: "Basou Task Index",
4349
- description: "The `.basou/tasks/index.json` task lookup index."
4350
- },
4351
- {
4352
- name: "session-import",
4353
- schema: SessionImportPayloadSchema,
4354
- title: "Basou Session Import Payload",
4355
- description: "The portable session payload consumed by `basou session import`."
4227
+ function computeSpan(startedAt, endedAt, now) {
4228
+ const start = Date.parse(startedAt);
4229
+ const end = endedAt !== void 0 ? Date.parse(endedAt) : now.getTime();
4230
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return { ms: 0, clamped: true };
4231
+ const raw = end - start;
4232
+ return raw < 0 ? { ms: 0, clamped: true } : { ms: raw, clamped: false };
4233
+ }
4234
+ function readTokens(metrics) {
4235
+ return {
4236
+ output: metrics?.output_tokens ?? 0,
4237
+ input: metrics?.input_tokens ?? 0,
4238
+ cached: metrics?.cached_input_tokens ?? 0,
4239
+ reasoning: metrics?.reasoning_output_tokens ?? 0
4240
+ };
4241
+ }
4242
+ function hasTokens(t) {
4243
+ return t.output > 0 || t.input > 0 || t.cached > 0 || t.reasoning > 0;
4244
+ }
4245
+ function emptyTokens() {
4246
+ return { output: 0, input: 0, cached: 0, reasoning: 0 };
4247
+ }
4248
+ function addTokens(a, b) {
4249
+ a.output += b.output;
4250
+ a.input += b.input;
4251
+ a.cached += b.cached;
4252
+ a.reasoning += b.reasoning;
4253
+ }
4254
+ function computeTotals(sessions, billableActiveTimeMs) {
4255
+ const tokens = emptyTokens();
4256
+ const totals = {
4257
+ sessionCount: sessions.length,
4258
+ openSessionCount: 0,
4259
+ sessionSpanMs: 0,
4260
+ commandTimeMs: 0,
4261
+ activeTimeMs: 0,
4262
+ billableActiveTimeMs,
4263
+ machineActiveTimeMs: 0,
4264
+ commandCount: 0,
4265
+ fileChangedCount: 0,
4266
+ decisionCount: 0,
4267
+ eventCount: 0,
4268
+ tokens,
4269
+ commandTimeReliable: true,
4270
+ tokensAvailable: false,
4271
+ machineActiveAvailable: false
4272
+ };
4273
+ for (const s of sessions) {
4274
+ if (s.open) totals.openSessionCount++;
4275
+ totals.sessionSpanMs += s.sessionSpanMs;
4276
+ totals.commandTimeMs += s.commandTimeMs;
4277
+ totals.activeTimeMs += s.activeTimeMs;
4278
+ totals.machineActiveTimeMs += s.machineActiveTimeMs;
4279
+ totals.commandCount += s.commandCount;
4280
+ totals.fileChangedCount += s.fileChangedCount;
4281
+ totals.decisionCount += s.decisionCount;
4282
+ totals.eventCount += s.eventCount;
4283
+ addTokens(tokens, s.tokens);
4284
+ if (!s.availability.commandTime) totals.commandTimeReliable = false;
4285
+ if (s.availability.tokens) totals.tokensAvailable = true;
4286
+ if (s.availability.machineActive) totals.machineActiveAvailable = true;
4356
4287
  }
4357
- ];
4358
- function buildJsonSchemas() {
4359
- return DOCUMENTS.map((doc) => {
4360
- const generated = z11.toJSONSchema(doc.schema, { io: "input" });
4361
- const { $schema, ...rest } = generated;
4362
- const schema = {
4363
- $schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
4364
- $id: `${ID_BASE}/${doc.name}.schema.json`,
4365
- title: doc.title,
4366
- description: doc.description,
4367
- ...rest
4368
- };
4369
- return { name: doc.name, schema };
4370
- });
4288
+ return totals;
4371
4289
  }
4372
- function serializeJsonSchema(schema) {
4373
- return `${JSON.stringify(schema, null, 2)}
4374
- `;
4290
+ function computeBySource(sessions) {
4291
+ const map = /* @__PURE__ */ new Map();
4292
+ for (const s of sessions) {
4293
+ let row = map.get(s.sourceKind);
4294
+ if (row === void 0) {
4295
+ row = {
4296
+ sourceKind: s.sourceKind,
4297
+ sessionCount: 0,
4298
+ sessionSpanMs: 0,
4299
+ commandTimeMs: 0,
4300
+ activeTimeMs: 0,
4301
+ machineActiveTimeMs: 0,
4302
+ commandCount: 0,
4303
+ fileChangedCount: 0,
4304
+ decisionCount: 0,
4305
+ eventCount: 0,
4306
+ tokens: emptyTokens(),
4307
+ commandTimeReliable: true,
4308
+ tokensAvailable: false,
4309
+ machineActiveAvailable: false
4310
+ };
4311
+ map.set(s.sourceKind, row);
4312
+ }
4313
+ row.sessionCount++;
4314
+ row.sessionSpanMs += s.sessionSpanMs;
4315
+ row.commandTimeMs += s.commandTimeMs;
4316
+ row.activeTimeMs += s.activeTimeMs;
4317
+ row.machineActiveTimeMs += s.machineActiveTimeMs;
4318
+ row.commandCount += s.commandCount;
4319
+ row.fileChangedCount += s.fileChangedCount;
4320
+ row.decisionCount += s.decisionCount;
4321
+ row.eventCount += s.eventCount;
4322
+ addTokens(row.tokens, s.tokens);
4323
+ if (!s.availability.commandTime) row.commandTimeReliable = false;
4324
+ if (s.availability.tokens) row.tokensAvailable = true;
4325
+ if (s.availability.machineActive) row.machineActiveAvailable = true;
4326
+ }
4327
+ return [...map.values()].sort((a, b) => a.sourceKind.localeCompare(b.sourceKind));
4375
4328
  }
4376
-
4377
- // src/stats/work-stats.ts
4378
- import { join as join13 } from "path";
4379
- function resolveTimeZone(timeZone) {
4380
- if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
4381
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
4329
+ function computeByStatus(sessions) {
4330
+ const counts = /* @__PURE__ */ new Map();
4331
+ for (const s of sessions) counts.set(s.status, (counts.get(s.status) ?? 0) + 1);
4332
+ const ordered = [];
4333
+ for (const status of STATUS_ORDER) {
4334
+ const count = counts.get(status);
4335
+ if (count !== void 0 && count > 0) ordered.push({ status, count });
4336
+ }
4337
+ return ordered;
4382
4338
  }
4383
- var STATUS_ORDER = [
4339
+ function computeByDay(sessions, unionMerged, timeZone) {
4340
+ const days = /* @__PURE__ */ new Map();
4341
+ const ensure = (date) => {
4342
+ let day = days.get(date);
4343
+ if (day === void 0) {
4344
+ day = {
4345
+ date,
4346
+ billableActiveTimeMs: 0,
4347
+ machineActiveTimeMs: 0,
4348
+ sessionCount: 0,
4349
+ commandCount: 0,
4350
+ fileChangedCount: 0,
4351
+ decisionCount: 0,
4352
+ tokens: emptyTokens()
4353
+ };
4354
+ days.set(date, day);
4355
+ }
4356
+ return day;
4357
+ };
4358
+ for (const [start, end] of unionMerged) {
4359
+ ensure(tzDate(start, timeZone)).billableActiveTimeMs += end - start;
4360
+ }
4361
+ for (const s of sessions) {
4362
+ const startedMs = Date.parse(s.startedAt);
4363
+ if (!Number.isFinite(startedMs)) continue;
4364
+ const day = ensure(tzDate(startedMs, timeZone));
4365
+ day.sessionCount++;
4366
+ day.machineActiveTimeMs += s.machineActiveTimeMs;
4367
+ day.commandCount += s.commandCount;
4368
+ day.fileChangedCount += s.fileChangedCount;
4369
+ day.decisionCount += s.decisionCount;
4370
+ addTokens(day.tokens, s.tokens);
4371
+ }
4372
+ return [...days.values()].sort((a, b) => a.date.localeCompare(b.date));
4373
+ }
4374
+ function tzDate(ms, timeZone) {
4375
+ return new Intl.DateTimeFormat("en-CA", {
4376
+ timeZone,
4377
+ year: "numeric",
4378
+ month: "2-digit",
4379
+ day: "2-digit"
4380
+ }).format(new Date(ms));
4381
+ }
4382
+
4383
+ // src/report/report-renderer.ts
4384
+ var CHANGED_FILES_MARKDOWN_LIMIT = 50;
4385
+ var DECISIONS_MARKDOWN_LIMIT = 20;
4386
+ var SESSIONS_MARKDOWN_LIMIT = 30;
4387
+ var TASKS_MARKDOWN_LIMIT = 30;
4388
+ var APPROVALS_MARKDOWN_LIMIT = 30;
4389
+ var SESSION_STATUS_ORDER = [
4384
4390
  "completed",
4385
4391
  "failed",
4386
4392
  "running",
4387
- "interrupted",
4388
4393
  "waiting_approval",
4394
+ "interrupted",
4389
4395
  "initialized",
4390
4396
  "imported",
4391
4397
  "archived"
4392
4398
  ];
4393
- async function computeWorkStats(input) {
4394
- const { now } = input;
4395
- const timeZone = resolveTimeZone(input.timeZone);
4399
+ var TASK_STATUS_ORDER = ["planned", "in_progress", "done", "cancelled"];
4400
+ async function renderReport(input) {
4401
+ const now = new Date(input.nowIso);
4396
4402
  const unreadableEmitted = /* @__PURE__ */ new Set();
4397
4403
  const wrappedSkip = (sid, reason) => {
4398
4404
  if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
@@ -4401,288 +4407,607 @@ async function computeWorkStats(input) {
4401
4407
  const loadOpts = { now, onSkip: wrappedSkip };
4402
4408
  if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
4403
4409
  const entries = await loadSessionEntries(input.paths, loadOpts);
4404
- const sessions = [];
4410
+ const statsInput = { paths: input.paths, now };
4411
+ if (input.timeZone !== void 0) statsInput.timeZone = input.timeZone;
4412
+ const stats = await computeWorkStats(statsInput);
4413
+ const statsBySession = new Map(stats.sessions.map((s) => [s.sessionId, s]));
4414
+ const decisions = [];
4405
4415
  for (const entry of entries) {
4406
- const events = [];
4407
- let eventsUnreadable = false;
4416
+ const sessionDir = join14(input.paths.sessions, entry.sessionId);
4408
4417
  try {
4409
- for await (const ev of replayEvents(join13(input.paths.sessions, entry.sessionId), {
4418
+ for await (const ev of replayEvents(sessionDir, {
4410
4419
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4411
4420
  })) {
4412
- events.push(ev);
4421
+ if (ev.type === "decision_recorded") {
4422
+ decisions.push({ id: ev.decision_id, title: ev.title, occurredAt: ev.occurred_at });
4423
+ }
4413
4424
  }
4414
4425
  } catch {
4415
- eventsUnreadable = true;
4416
4426
  if (!unreadableEmitted.has(entry.sessionId)) {
4417
4427
  wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
4418
4428
  }
4419
4429
  }
4420
- sessions.push(
4421
- sessionWorkStatsFromEvents(
4422
- entry.sessionId,
4423
- entry.session.session,
4424
- events,
4425
- now,
4426
- eventsUnreadable
4427
- )
4428
- );
4429
4430
  }
4430
- const allIntervals = [];
4431
- for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
4432
- const union = unionDurationMs(allIntervals);
4433
- return {
4434
- generatedAt: now.toISOString(),
4435
- activeGapCapMs: ACTIVE_GAP_CAP_MS,
4436
- timeZone,
4437
- totals: computeTotals(sessions, union.ms),
4438
- sessions,
4439
- bySource: computeBySource(sessions),
4440
- byStatus: computeByStatus(sessions),
4441
- byDay: computeByDay(sessions, union.merged, timeZone)
4431
+ decisions.sort((a, b) => {
4432
+ const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
4433
+ return c !== 0 ? c : a.id.localeCompare(b.id);
4434
+ });
4435
+ const taskLoadOpts = {};
4436
+ if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
4437
+ const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
4438
+ const taskItems = taskEntries.map((t2) => ({
4439
+ id: t2.task.task.id,
4440
+ title: t2.task.task.title,
4441
+ status: t2.task.task.status
4442
+ }));
4443
+ const tasksByStatus = tallyTaskStatus(taskItems);
4444
+ const approvalIds = await enumerateApprovals(input.paths);
4445
+ const resolvedSet = new Set(approvalIds.resolved);
4446
+ const pendingIds = approvalIds.pending.filter((id) => !resolvedSet.has(id));
4447
+ const loadedApprovals = (await Promise.all(
4448
+ [...pendingIds, ...approvalIds.resolved].map((id) => loadApproval(input.paths, id))
4449
+ )).filter((a) => a !== null);
4450
+ const approvalItems = loadedApprovals.map((a) => ({
4451
+ id: a.approval.id,
4452
+ reason: a.approval.reason,
4453
+ status: a.approval.status,
4454
+ riskLevel: a.approval.risk_level
4455
+ }));
4456
+ const approvalCounts = { pending: 0, approved: 0, rejected: 0, expired: 0 };
4457
+ for (const a of approvalItems) approvalCounts[a.status] += 1;
4458
+ const changedSet = /* @__PURE__ */ new Set();
4459
+ for (const entry of entries) {
4460
+ if (entry.session.session.source.kind === "import") continue;
4461
+ for (const f of entry.session.session.related_files) changedSet.add(f);
4462
+ }
4463
+ const changedFiles = [...changedSet].sort();
4464
+ const integrity = {
4465
+ total: 0,
4466
+ verified: 0,
4467
+ unchained: 0,
4468
+ empty: 0,
4469
+ incomplete: 0,
4470
+ in_progress: 0,
4471
+ tampered: 0,
4472
+ tamperedSessions: []
4442
4473
  };
4443
- }
4444
- function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
4445
- let commandCount = 0;
4446
- let fileChangedCount = 0;
4447
- let decisionCount = 0;
4448
- let commandTimeMs = 0;
4449
- const timestamps = [];
4450
- for (const ev of events) {
4451
- const t = Date.parse(ev.occurred_at);
4452
- if (Number.isFinite(t)) timestamps.push(t);
4453
- if (ev.type === "command_executed") {
4454
- commandCount++;
4455
- commandTimeMs += ev.duration_ms;
4456
- } else if (ev.type === "file_changed") {
4457
- fileChangedCount++;
4458
- } else if (ev.type === "decision_recorded") {
4459
- decisionCount++;
4474
+ for (const entry of entries) {
4475
+ const verdict = await verifyEventsChain(input.paths, entry.sessionId).catch(() => null);
4476
+ if (verdict === null) {
4477
+ if (!unreadableEmitted.has(entry.sessionId)) {
4478
+ wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
4479
+ }
4480
+ continue;
4460
4481
  }
4461
- }
4462
- const span = computeSpan(inner.started_at, inner.ended_at, now);
4463
- const tokens = readTokens(inner.metrics);
4464
- const active = resolveActiveTime(inner.metrics, timestamps);
4465
- const machineActiveTimeMs = inner.metrics?.machine_active_time_ms ?? 0;
4466
- return {
4467
- sessionId,
4468
- label: inner.label,
4469
- status: inner.status,
4470
- sourceKind: inner.source.kind,
4471
- startedAt: inner.started_at,
4472
- endedAt: inner.ended_at,
4473
- open: inner.ended_at === void 0,
4474
- sessionSpanMs: span.ms,
4475
- commandTimeMs,
4476
- activeTimeMs: active.ms,
4477
- activeTimeBasis: active.basis,
4478
- activeIntervals: intervalsMsToIso(active.intervals),
4479
- machineActiveTimeMs,
4480
- activeTimeMethod: inner.metrics?.active_time_method,
4481
- commandCount,
4482
- fileChangedCount,
4483
- decisionCount,
4484
- eventCount: events.length,
4485
- tokens,
4486
- availability: {
4487
- span: true,
4488
- commandTime: inner.source.kind !== "claude-code-import",
4489
- activeTime: active.intervals.length > 0,
4490
- tokens: hasTokens(tokens),
4491
- machineActive: machineActiveTimeMs > 0
4482
+ integrity.total += 1;
4483
+ integrity[verdict.status] += 1;
4484
+ if (verdict.status === "tampered") integrity.tamperedSessions.push(entry.sessionId);
4485
+ }
4486
+ const sessionItems = [...entries].sort(
4487
+ (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
4488
+ ).map((e) => {
4489
+ const w = statsBySession.get(e.sessionId);
4490
+ return {
4491
+ id: e.sessionId,
4492
+ label: e.session.session.label ?? null,
4493
+ status: e.session.session.status,
4494
+ source: e.session.session.source.kind,
4495
+ startedAt: e.session.session.started_at,
4496
+ activeMs: w?.activeTimeMs ?? 0,
4497
+ outputTokens: w?.tokens.output ?? 0
4498
+ };
4499
+ });
4500
+ const period = computePeriod(entries, input.nowIso);
4501
+ const t = stats.totals;
4502
+ const data = {
4503
+ generatedAt: input.nowIso,
4504
+ ...input.title !== void 0 ? { title: input.title } : {},
4505
+ period,
4506
+ sessions: { total: entries.length, byStatus: stats.byStatus, items: sessionItems },
4507
+ volume: {
4508
+ outputTokens: t.tokens.output,
4509
+ reasoningTokens: t.tokens.reasoning,
4510
+ commandCount: t.commandCount,
4511
+ fileChangedCount: t.fileChangedCount,
4512
+ decisionCount: t.decisionCount,
4513
+ tokensAvailable: t.tokensAvailable
4492
4514
  },
4493
- spanClamped: span.clamped,
4494
- eventsUnreadable
4515
+ time: {
4516
+ activeMs: t.billableActiveTimeMs,
4517
+ machineActiveMs: t.machineActiveTimeMs,
4518
+ machineAvailable: t.machineActiveAvailable,
4519
+ spanMs: t.sessionSpanMs,
4520
+ commandTimeMs: t.commandTimeMs,
4521
+ timeZone: stats.timeZone
4522
+ },
4523
+ decisions: { count: decisions.length, items: decisions },
4524
+ approvals: { ...approvalCounts, items: approvalItems },
4525
+ tasks: { total: taskEntries.length, byStatus: tasksByStatus, items: taskItems },
4526
+ changedFiles,
4527
+ integrity
4495
4528
  };
4529
+ return { body: formatReportBody(data), data };
4530
+ }
4531
+ function computePeriod(entries, nowIso) {
4532
+ if (entries.length === 0) return { from: null, to: null };
4533
+ let from = entries[0]?.session.session.started_at ?? nowIso;
4534
+ let to = nowIso;
4535
+ let sawEnd = false;
4536
+ for (const e of entries) {
4537
+ const s = e.session.session.started_at;
4538
+ if (Date.parse(s) < Date.parse(from)) from = s;
4539
+ const end = e.session.session.ended_at ?? nowIso;
4540
+ if (!sawEnd || Date.parse(end) > Date.parse(to)) {
4541
+ to = end;
4542
+ sawEnd = true;
4543
+ }
4544
+ }
4545
+ if (Date.parse(to) < Date.parse(from)) to = from;
4546
+ return { from, to };
4547
+ }
4548
+ function tallyTaskStatus(items) {
4549
+ const counts = /* @__PURE__ */ new Map();
4550
+ for (const i of items) counts.set(i.status, (counts.get(i.status) ?? 0) + 1);
4551
+ return TASK_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((status) => ({
4552
+ status,
4553
+ count: counts.get(status)
4554
+ }));
4496
4555
  }
4497
- function resolveActiveTime(metrics, eventTimestamps) {
4498
- const stored = metrics?.active_intervals;
4499
- if (stored !== void 0 && stored.length > 0) {
4500
- const intervals = intervalsIsoToMs(stored);
4501
- const ms = intervals.reduce((n, [start, end]) => n + (end - start), 0);
4502
- return { ms, intervals, basis: "engaged-turns" };
4556
+ function formatReportBody(data) {
4557
+ const lines = [];
4558
+ const titleSuffix = data.title !== void 0 ? ` \u2014 ${data.title}` : "";
4559
+ lines.push(`# Report${titleSuffix}`);
4560
+ lines.push("");
4561
+ const periodSuffix = data.period.from !== null && data.period.to !== null ? ` (${data.period.from.slice(0, 10)}..${data.period.to.slice(0, 10)})` : "";
4562
+ lines.push(`> Generated at ${data.generatedAt}${periodSuffix}`);
4563
+ lines.push("");
4564
+ lines.push("## \u6982\u8981");
4565
+ lines.push("");
4566
+ lines.push(`- ${formatSessionsLine(data)}`);
4567
+ lines.push(
4568
+ `- Active time ${formatDurationMs(data.time.activeMs)}, ${formatInt(data.volume.outputTokens)} output tokens`
4569
+ );
4570
+ lines.push("");
4571
+ lines.push("## \u4F5C\u696D\u91CF");
4572
+ lines.push("");
4573
+ const tokenCaveat = data.volume.tokensAvailable ? "" : " (no token data captured)";
4574
+ lines.push(`- Output tokens: ${formatInt(data.volume.outputTokens)}${tokenCaveat}`);
4575
+ if (data.volume.reasoningTokens > 0) {
4576
+ lines.push(`- Reasoning tokens: ${formatInt(data.volume.reasoningTokens)} (Codex)`);
4503
4577
  }
4504
- const derived = activeTimeFromTimestamps(eventTimestamps, ACTIVE_GAP_CAP_MS);
4505
- return { ms: derived.ms, intervals: derived.intervals, basis: "events" };
4506
- }
4507
- function computeSpan(startedAt, endedAt, now) {
4508
- const start = Date.parse(startedAt);
4509
- const end = endedAt !== void 0 ? Date.parse(endedAt) : now.getTime();
4510
- if (!Number.isFinite(start) || !Number.isFinite(end)) return { ms: 0, clamped: true };
4511
- const raw = end - start;
4512
- return raw < 0 ? { ms: 0, clamped: true } : { ms: raw, clamped: false };
4513
- }
4514
- function readTokens(metrics) {
4515
- return {
4516
- output: metrics?.output_tokens ?? 0,
4517
- input: metrics?.input_tokens ?? 0,
4518
- cached: metrics?.cached_input_tokens ?? 0,
4519
- reasoning: metrics?.reasoning_output_tokens ?? 0
4520
- };
4521
- }
4522
- function hasTokens(t) {
4523
- return t.output > 0 || t.input > 0 || t.cached > 0 || t.reasoning > 0;
4524
- }
4525
- function emptyTokens() {
4526
- return { output: 0, input: 0, cached: 0, reasoning: 0 };
4578
+ lines.push(
4579
+ `- Actions: ${data.volume.commandCount} commands, ${data.volume.fileChangedCount} files, ${data.volume.decisionCount} decisions`
4580
+ );
4581
+ lines.push(
4582
+ `- Active time: ${formatDurationMs(data.time.activeMs)} (union; idle gaps > 5m excluded; tz ${data.time.timeZone})`
4583
+ );
4584
+ if (data.time.machineAvailable) {
4585
+ lines.push(
4586
+ `- Model working: ${formatDurationMs(data.time.machineActiveMs)} (model compute, subset of active)`
4587
+ );
4588
+ }
4589
+ lines.push(`- Span: ${formatDurationMs(data.time.spanMs)} (total elapsed)`);
4590
+ lines.push("");
4591
+ lines.push("## \u5224\u65AD");
4592
+ lines.push("");
4593
+ if (data.decisions.items.length === 0) {
4594
+ lines.push("(no decisions recorded yet)");
4595
+ } else {
4596
+ const total = data.decisions.items.length;
4597
+ const shown = total > DECISIONS_MARKDOWN_LIMIT ? data.decisions.items.slice(-DECISIONS_MARKDOWN_LIMIT) : data.decisions.items;
4598
+ if (total > DECISIONS_MARKDOWN_LIMIT) {
4599
+ lines.push(`(showing the ${DECISIONS_MARKDOWN_LIMIT} most recent of ${total})`);
4600
+ lines.push("");
4601
+ }
4602
+ for (const d of shown) {
4603
+ lines.push(`- ${d.occurredAt.slice(0, 10)} \xB7 ${d.title}`);
4604
+ }
4605
+ }
4606
+ lines.push("");
4607
+ lines.push("## \u627F\u8A8D");
4608
+ lines.push("");
4609
+ if (data.approvals.items.length === 0) {
4610
+ lines.push("(none)");
4611
+ } else {
4612
+ const a = data.approvals;
4613
+ lines.push(
4614
+ `Pending ${a.pending} \xB7 Approved ${a.approved} \xB7 Rejected ${a.rejected} \xB7 Expired ${a.expired}`
4615
+ );
4616
+ lines.push("");
4617
+ for (const item of data.approvals.items.slice(0, APPROVALS_MARKDOWN_LIMIT)) {
4618
+ lines.push(`- ${item.reason} (${item.status}, ${item.riskLevel})`);
4619
+ }
4620
+ const overflow = data.approvals.items.length - APPROVALS_MARKDOWN_LIMIT;
4621
+ if (overflow > 0) lines.push(`- ... +${overflow} more`);
4622
+ }
4623
+ lines.push("");
4624
+ lines.push("## \u30BF\u30B9\u30AF");
4625
+ lines.push("");
4626
+ if (data.tasks.items.length === 0) {
4627
+ lines.push("(no tasks recorded yet)");
4628
+ } else {
4629
+ const breakdown = data.tasks.byStatus.map((s) => `${s.status} ${s.count}`).join(", ");
4630
+ lines.push(`Tasks: ${data.tasks.total} (${breakdown})`);
4631
+ lines.push("");
4632
+ for (const item of data.tasks.items.slice(0, TASKS_MARKDOWN_LIMIT)) {
4633
+ lines.push(`- ${item.title} (${item.status})`);
4634
+ }
4635
+ const overflow = data.tasks.items.length - TASKS_MARKDOWN_LIMIT;
4636
+ if (overflow > 0) lines.push(`- ... +${overflow} more`);
4637
+ }
4638
+ lines.push("");
4639
+ lines.push("## \u5909\u66F4\u30D5\u30A1\u30A4\u30EB");
4640
+ lines.push("");
4641
+ if (data.changedFiles.length === 0) {
4642
+ lines.push("(no related files recorded)");
4643
+ } else {
4644
+ for (const f of data.changedFiles.slice(0, CHANGED_FILES_MARKDOWN_LIMIT)) lines.push(`- ${f}`);
4645
+ const overflow = data.changedFiles.length - CHANGED_FILES_MARKDOWN_LIMIT;
4646
+ if (overflow > 0) lines.push(`- ... +${overflow} more`);
4647
+ }
4648
+ lines.push("");
4649
+ lines.push("## \u30BB\u30C3\u30B7\u30E7\u30F3\u4E00\u89A7");
4650
+ lines.push("");
4651
+ if (data.sessions.items.length === 0) {
4652
+ lines.push("(no sessions yet)");
4653
+ } else {
4654
+ lines.push("| started_at | source | status | active | out tok |");
4655
+ lines.push("|---|---|---|---|---|");
4656
+ for (const s of data.sessions.items.slice(0, SESSIONS_MARKDOWN_LIMIT)) {
4657
+ lines.push(
4658
+ `| ${s.startedAt} | ${s.source} | ${s.status} | ${formatDurationMs(s.activeMs)} | ${formatInt(s.outputTokens)} |`
4659
+ );
4660
+ }
4661
+ const overflow = data.sessions.items.length - SESSIONS_MARKDOWN_LIMIT;
4662
+ if (overflow > 0) {
4663
+ lines.push("");
4664
+ lines.push(`... +${overflow} more sessions`);
4665
+ }
4666
+ }
4667
+ lines.push("");
4668
+ lines.push("## \u6574\u5408\u6027");
4669
+ lines.push("");
4670
+ const i = data.integrity;
4671
+ lines.push(
4672
+ `Provenance internally tamper-checked: ${i.verified} verified, ${i.unchained} unchained, ${i.empty} empty, ${i.incomplete} incomplete, ${i.in_progress} in_progress, ${i.tampered} tampered (of ${i.total} sessions).`
4673
+ );
4674
+ lines.push("");
4675
+ lines.push(
4676
+ "This reflects internal consistency of the local event-log hash chain \u2014 not a third-party cryptographic proof."
4677
+ );
4678
+ if (i.tampered > 0) {
4679
+ lines.push("");
4680
+ for (const id of i.tamperedSessions) lines.push(`- Tampered: ${id}`);
4681
+ }
4682
+ return lines.join("\n");
4527
4683
  }
4528
- function addTokens(a, b) {
4529
- a.output += b.output;
4530
- a.input += b.input;
4531
- a.cached += b.cached;
4532
- a.reasoning += b.reasoning;
4684
+ function formatSessionsLine(data) {
4685
+ const counts = /* @__PURE__ */ new Map();
4686
+ for (const s of data.sessions.byStatus) counts.set(s.status, s.count);
4687
+ const breakdown = SESSION_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((s) => `${s} ${counts.get(s)}`).join(", ");
4688
+ return breakdown !== "" ? `Sessions: ${data.sessions.total} (${breakdown})` : `Sessions: ${data.sessions.total}`;
4533
4689
  }
4534
- function computeTotals(sessions, billableActiveTimeMs) {
4535
- const tokens = emptyTokens();
4536
- const totals = {
4537
- sessionCount: sessions.length,
4538
- openSessionCount: 0,
4539
- sessionSpanMs: 0,
4540
- commandTimeMs: 0,
4541
- activeTimeMs: 0,
4542
- billableActiveTimeMs,
4543
- machineActiveTimeMs: 0,
4544
- commandCount: 0,
4545
- fileChangedCount: 0,
4546
- decisionCount: 0,
4547
- eventCount: 0,
4548
- tokens,
4549
- commandTimeReliable: true,
4550
- tokensAvailable: false,
4551
- machineActiveAvailable: false
4552
- };
4553
- for (const s of sessions) {
4554
- if (s.open) totals.openSessionCount++;
4555
- totals.sessionSpanMs += s.sessionSpanMs;
4556
- totals.commandTimeMs += s.commandTimeMs;
4557
- totals.activeTimeMs += s.activeTimeMs;
4558
- totals.machineActiveTimeMs += s.machineActiveTimeMs;
4559
- totals.commandCount += s.commandCount;
4560
- totals.fileChangedCount += s.fileChangedCount;
4561
- totals.decisionCount += s.decisionCount;
4562
- totals.eventCount += s.eventCount;
4563
- addTokens(tokens, s.tokens);
4564
- if (!s.availability.commandTime) totals.commandTimeReliable = false;
4565
- if (s.availability.tokens) totals.tokensAvailable = true;
4566
- if (s.availability.machineActive) totals.machineActiveAvailable = true;
4567
- }
4568
- return totals;
4690
+ function formatInt(n) {
4691
+ return n.toLocaleString("en-US");
4569
4692
  }
4570
- function computeBySource(sessions) {
4571
- const map = /* @__PURE__ */ new Map();
4572
- for (const s of sessions) {
4573
- let row = map.get(s.sourceKind);
4574
- if (row === void 0) {
4575
- row = {
4576
- sourceKind: s.sourceKind,
4577
- sessionCount: 0,
4578
- sessionSpanMs: 0,
4579
- commandTimeMs: 0,
4580
- activeTimeMs: 0,
4581
- machineActiveTimeMs: 0,
4582
- commandCount: 0,
4583
- fileChangedCount: 0,
4584
- decisionCount: 0,
4585
- eventCount: 0,
4586
- tokens: emptyTokens(),
4587
- commandTimeReliable: true,
4588
- tokensAvailable: false,
4589
- machineActiveAvailable: false
4590
- };
4591
- map.set(s.sourceKind, row);
4693
+
4694
+ // src/runtime/child-process-runner.ts
4695
+ import { spawn as spawn2 } from "child_process";
4696
+ var DEFAULT_KILL_GRACE_MS = 5e3;
4697
+ var ChildProcessRunner = class {
4698
+ async run(command, args, options) {
4699
+ validateOptions(options);
4700
+ if (options.signal?.aborted) {
4701
+ throw new Error("Process aborted before spawn", {
4702
+ cause: options.signal.reason
4703
+ });
4592
4704
  }
4593
- row.sessionCount++;
4594
- row.sessionSpanMs += s.sessionSpanMs;
4595
- row.commandTimeMs += s.commandTimeMs;
4596
- row.activeTimeMs += s.activeTimeMs;
4597
- row.machineActiveTimeMs += s.machineActiveTimeMs;
4598
- row.commandCount += s.commandCount;
4599
- row.fileChangedCount += s.fileChangedCount;
4600
- row.decisionCount += s.decisionCount;
4601
- row.eventCount += s.eventCount;
4602
- addTokens(row.tokens, s.tokens);
4603
- if (!s.availability.commandTime) row.commandTimeReliable = false;
4604
- if (s.availability.tokens) row.tokensAvailable = true;
4605
- if (s.availability.machineActive) row.machineActiveAvailable = true;
4705
+ const snapshotCommand = command;
4706
+ const snapshotArgs = [...args];
4707
+ const snapshotCwd = options.cwd;
4708
+ const captureMode = options.capture ?? "buffer";
4709
+ const started_at = /* @__PURE__ */ new Date();
4710
+ let child;
4711
+ try {
4712
+ child = spawn2(snapshotCommand, [...snapshotArgs], {
4713
+ cwd: snapshotCwd,
4714
+ env: options.env ?? process.env,
4715
+ stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
4716
+ shell: false,
4717
+ detached: false
4718
+ });
4719
+ } catch (error) {
4720
+ throw classifySpawnError(error);
4721
+ }
4722
+ if (options.onSpawn) {
4723
+ try {
4724
+ options.onSpawn(child);
4725
+ } catch {
4726
+ }
4727
+ }
4728
+ let timeoutTimer = null;
4729
+ let killTimer = null;
4730
+ let killed = false;
4731
+ let settled = false;
4732
+ const triggerKill = () => {
4733
+ if (killed || child.exitCode !== null) return;
4734
+ killed = true;
4735
+ child.kill("SIGTERM");
4736
+ killTimer = setTimeout(() => {
4737
+ if (child.exitCode === null) {
4738
+ child.kill("SIGKILL");
4739
+ }
4740
+ }, DEFAULT_KILL_GRACE_MS);
4741
+ };
4742
+ const onAbort = () => {
4743
+ triggerKill();
4744
+ };
4745
+ options.signal?.addEventListener("abort", onAbort);
4746
+ if (options.signal?.aborted) {
4747
+ triggerKill();
4748
+ }
4749
+ let stdout = "";
4750
+ let stderr = "";
4751
+ if (captureMode === "buffer") {
4752
+ child.stdout?.setEncoding("utf8");
4753
+ child.stderr?.setEncoding("utf8");
4754
+ child.stdout?.on("data", (chunk) => {
4755
+ stdout += chunk;
4756
+ });
4757
+ child.stderr?.on("data", (chunk) => {
4758
+ stderr += chunk;
4759
+ });
4760
+ if (options.stdin !== void 0) {
4761
+ child.stdin?.end(options.stdin);
4762
+ } else {
4763
+ child.stdin?.end();
4764
+ }
4765
+ }
4766
+ if (options.timeout_ms !== void 0) {
4767
+ timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
4768
+ }
4769
+ const cleanup = () => {
4770
+ if (timeoutTimer !== null) clearTimeout(timeoutTimer);
4771
+ if (killTimer !== null) clearTimeout(killTimer);
4772
+ options.signal?.removeEventListener("abort", onAbort);
4773
+ };
4774
+ return new Promise((resolve2, reject) => {
4775
+ child.once("error", (error) => {
4776
+ if (settled) return;
4777
+ settled = true;
4778
+ cleanup();
4779
+ reject(classifySpawnError(error));
4780
+ });
4781
+ child.once("close", (code, signal) => {
4782
+ if (settled) return;
4783
+ settled = true;
4784
+ cleanup();
4785
+ const ended_at = /* @__PURE__ */ new Date();
4786
+ resolve2({
4787
+ command: snapshotCommand,
4788
+ args: snapshotArgs,
4789
+ cwd: snapshotCwd,
4790
+ exit_code: code,
4791
+ signal,
4792
+ stdout,
4793
+ stderr,
4794
+ started_at: started_at.toISOString(),
4795
+ ended_at: ended_at.toISOString(),
4796
+ duration_ms: ended_at.getTime() - started_at.getTime(),
4797
+ pid: child.pid ?? null
4798
+ });
4799
+ });
4800
+ });
4606
4801
  }
4607
- return [...map.values()].sort((a, b) => a.sourceKind.localeCompare(b.sourceKind));
4608
- }
4609
- function computeByStatus(sessions) {
4610
- const counts = /* @__PURE__ */ new Map();
4611
- for (const s of sessions) counts.set(s.status, (counts.get(s.status) ?? 0) + 1);
4612
- const ordered = [];
4613
- for (const status of STATUS_ORDER) {
4614
- const count = counts.get(status);
4615
- if (count !== void 0 && count > 0) ordered.push({ status, count });
4802
+ };
4803
+ function validateOptions(options) {
4804
+ if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
4805
+ throw new Error("Invalid timeout_ms");
4806
+ }
4807
+ if (options.capture === "none" && options.stdin !== void 0) {
4808
+ throw new Error('Combination of capture: "none" and stdin is not supported');
4616
4809
  }
4617
- return ordered;
4618
4810
  }
4619
- function computeByDay(sessions, unionMerged, timeZone) {
4620
- const days = /* @__PURE__ */ new Map();
4621
- const ensure = (date) => {
4622
- let day = days.get(date);
4623
- if (day === void 0) {
4624
- day = {
4625
- date,
4626
- billableActiveTimeMs: 0,
4627
- machineActiveTimeMs: 0,
4628
- sessionCount: 0,
4629
- commandCount: 0,
4630
- fileChangedCount: 0,
4631
- decisionCount: 0,
4632
- tokens: emptyTokens()
4633
- };
4634
- days.set(date, day);
4635
- }
4636
- return day;
4637
- };
4638
- for (const [start, end] of unionMerged) {
4639
- ensure(tzDate(start, timeZone)).billableActiveTimeMs += end - start;
4811
+ function classifySpawnError(error) {
4812
+ if (findErrorCode(error, "ENOENT")) {
4813
+ return new Error("Command not found", { cause: error });
4640
4814
  }
4641
- for (const s of sessions) {
4642
- const startedMs = Date.parse(s.startedAt);
4643
- if (!Number.isFinite(startedMs)) continue;
4644
- const day = ensure(tzDate(startedMs, timeZone));
4645
- day.sessionCount++;
4646
- day.machineActiveTimeMs += s.machineActiveTimeMs;
4647
- day.commandCount += s.commandCount;
4648
- day.fileChangedCount += s.fileChangedCount;
4649
- day.decisionCount += s.decisionCount;
4650
- addTokens(day.tokens, s.tokens);
4815
+ return new Error("Failed to spawn child process", { cause: error });
4816
+ }
4817
+
4818
+ // src/schemas/json-schema.ts
4819
+ import { z as z11 } from "zod";
4820
+
4821
+ // src/schemas/manifest.schema.ts
4822
+ import { z as z9 } from "zod";
4823
+ var ProjectSchema = z9.object({
4824
+ name: z9.string().optional(),
4825
+ description: z9.string().optional(),
4826
+ repository_url: z9.string().nullable().optional()
4827
+ });
4828
+ var CapabilitiesSchema = z9.object({
4829
+ enabled: z9.array(z9.string())
4830
+ });
4831
+ var ApprovalConfigSchema = z9.object({
4832
+ required_for: z9.array(z9.string()).optional(),
4833
+ default_risk_level: z9.enum(["low", "medium", "high", "critical"])
4834
+ });
4835
+ var ClaudeCodeAdapterConfigSchema = z9.object({
4836
+ enabled: z9.boolean(),
4837
+ config_path: z9.string().optional()
4838
+ });
4839
+ var AdaptersSchema = z9.object({
4840
+ "claude-code": ClaudeCodeAdapterConfigSchema
4841
+ });
4842
+ var GitConfigSchema = z9.object({
4843
+ events_log: z9.enum(["ignore", "commit"]).default("ignore")
4844
+ });
4845
+ var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)[^\0\\]+$/;
4846
+ var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
4847
+ message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
4848
+ });
4849
+ var ImportConfigSchema = z9.object({
4850
+ source_roots: z9.array(SourceRootSchema).min(1).optional()
4851
+ });
4852
+ var WorkspaceMetaSchema = z9.object({
4853
+ id: WorkspaceIdSchema,
4854
+ name: z9.string().min(1),
4855
+ created_at: IsoTimestampSchema,
4856
+ updated_at: IsoTimestampSchema
4857
+ });
4858
+ var ManifestSchema = z9.object({
4859
+ schema_version: SchemaVersionSchema,
4860
+ basou_version: z9.literal("0.1.0"),
4861
+ workspace: WorkspaceMetaSchema,
4862
+ project: ProjectSchema,
4863
+ capabilities: CapabilitiesSchema,
4864
+ approval: ApprovalConfigSchema,
4865
+ adapters: AdaptersSchema,
4866
+ git: GitConfigSchema,
4867
+ import: ImportConfigSchema.optional()
4868
+ });
4869
+
4870
+ // src/schemas/session-import.schema.ts
4871
+ import { z as z10 } from "zod";
4872
+ var SessionInnerImportSchema = z10.object({
4873
+ id: SessionIdSchema.optional(),
4874
+ label: z10.string().optional(),
4875
+ task_id: TaskIdSchema.nullable().optional(),
4876
+ workspace_id: WorkspaceIdSchema,
4877
+ source: z10.object({
4878
+ kind: SessionSourceKindSchema,
4879
+ version: z10.literal("0.1.0"),
4880
+ // Source-tool-native id (e.g. Claude Code session UUID), retained so
4881
+ // re-imports of the same source can be deduplicated.
4882
+ external_id: z10.string().optional(),
4883
+ // Byte size of the source native log at import time. Declared here too
4884
+ // (not only in session.schema.ts) because this inner `source` object is
4885
+ // a plain z.object: zod strips keys it does not declare, so a field
4886
+ // absent here would be dropped from the parsed payload before persist
4887
+ // and the size could never be stored.
4888
+ source_size_bytes: z10.number().int().nonnegative().optional()
4889
+ }),
4890
+ started_at: IsoTimestampSchema,
4891
+ ended_at: IsoTimestampSchema.optional(),
4892
+ status: SessionStatusSchema,
4893
+ working_directory: z10.string().min(1),
4894
+ invocation: z10.object({
4895
+ command: z10.string().min(1),
4896
+ args: z10.array(z10.string()),
4897
+ exit_code: z10.number().int().nullable()
4898
+ }),
4899
+ related_files: z10.array(z10.string()).default([]),
4900
+ events_log: z10.string().optional(),
4901
+ summary: z10.string().nullable().optional(),
4902
+ metrics: SessionMetricsSchema.optional(),
4903
+ // Accepted so a payload assembled from an on-disk chained session.yaml
4904
+ // round-trips, and DISCARDED by the importer (buildSessionRecord never
4905
+ // copies it): the integrity anchor is computed at write time, never
4906
+ // imported. Mirrors the accept-and-discard of `prev_hash` on events.
4907
+ integrity: SessionIntegritySchema.optional()
4908
+ }).strict();
4909
+ var SessionImportPayloadSchema = z10.object({
4910
+ schema_version: z10.string(),
4911
+ session: SessionInnerImportSchema,
4912
+ events: z10.array(EventSchema)
4913
+ }).strict();
4914
+
4915
+ // src/schemas/json-schema.ts
4916
+ var JSON_SCHEMA_VERSION = "0.1.0";
4917
+ var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
4918
+ var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
4919
+ var DOCUMENTS = [
4920
+ {
4921
+ name: "manifest",
4922
+ schema: ManifestSchema,
4923
+ title: "Basou Manifest",
4924
+ description: "The `.basou/manifest.yaml` workspace manifest."
4925
+ },
4926
+ {
4927
+ name: "session",
4928
+ schema: SessionSchema,
4929
+ title: "Basou Session",
4930
+ description: "A `.basou/sessions/<id>/session.yaml` session record."
4931
+ },
4932
+ {
4933
+ name: "event",
4934
+ schema: EventSchema,
4935
+ title: "Basou Event",
4936
+ description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
4937
+ },
4938
+ {
4939
+ name: "task",
4940
+ schema: TaskSchema,
4941
+ title: "Basou Task",
4942
+ description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
4943
+ },
4944
+ {
4945
+ name: "approval",
4946
+ schema: ApprovalSchema,
4947
+ title: "Basou Approval",
4948
+ description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
4949
+ },
4950
+ {
4951
+ name: "status",
4952
+ schema: StatusSchema,
4953
+ title: "Basou Status",
4954
+ description: "The `.basou/status.json` workspace status snapshot."
4955
+ },
4956
+ {
4957
+ name: "task-index",
4958
+ schema: TaskIndexSchema,
4959
+ title: "Basou Task Index",
4960
+ description: "The `.basou/tasks/index.json` task lookup index."
4961
+ },
4962
+ {
4963
+ name: "session-import",
4964
+ schema: SessionImportPayloadSchema,
4965
+ title: "Basou Session Import Payload",
4966
+ description: "The portable session payload consumed by `basou session import`."
4651
4967
  }
4652
- return [...days.values()].sort((a, b) => a.date.localeCompare(b.date));
4968
+ ];
4969
+ function buildJsonSchemas() {
4970
+ return DOCUMENTS.map((doc) => {
4971
+ const generated = z11.toJSONSchema(doc.schema, { io: "input" });
4972
+ const { $schema, ...rest } = generated;
4973
+ const schema = {
4974
+ $schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
4975
+ $id: `${ID_BASE}/${doc.name}.schema.json`,
4976
+ title: doc.title,
4977
+ description: doc.description,
4978
+ ...rest
4979
+ };
4980
+ return { name: doc.name, schema };
4981
+ });
4653
4982
  }
4654
- function tzDate(ms, timeZone) {
4655
- return new Intl.DateTimeFormat("en-CA", {
4656
- timeZone,
4657
- year: "numeric",
4658
- month: "2-digit",
4659
- day: "2-digit"
4660
- }).format(new Date(ms));
4983
+ function serializeJsonSchema(schema) {
4984
+ return `${JSON.stringify(schema, null, 2)}
4985
+ `;
4661
4986
  }
4662
4987
 
4663
4988
  // src/storage/basou-dir.ts
4664
4989
  import { lstat as lstat3, mkdir as mkdir4 } from "fs/promises";
4665
- import { join as join14 } from "path";
4990
+ import { join as join15 } from "path";
4666
4991
  function basouPaths(repositoryRoot) {
4667
- const root = join14(repositoryRoot, ".basou");
4668
- const approvalsBase = join14(root, "approvals");
4992
+ const root = join15(repositoryRoot, ".basou");
4993
+ const approvalsBase = join15(root, "approvals");
4669
4994
  return {
4670
4995
  root,
4671
- sessions: join14(root, "sessions"),
4672
- tasks: join14(root, "tasks"),
4996
+ sessions: join15(root, "sessions"),
4997
+ tasks: join15(root, "tasks"),
4673
4998
  approvals: {
4674
- pending: join14(approvalsBase, "pending"),
4675
- resolved: join14(approvalsBase, "resolved")
4999
+ pending: join15(approvalsBase, "pending"),
5000
+ resolved: join15(approvalsBase, "resolved")
4676
5001
  },
4677
- locks: join14(root, "locks"),
4678
- logs: join14(root, "logs"),
4679
- raw: join14(root, "raw"),
4680
- tmp: join14(root, "tmp"),
5002
+ locks: join15(root, "locks"),
5003
+ logs: join15(root, "logs"),
5004
+ raw: join15(root, "raw"),
5005
+ tmp: join15(root, "tmp"),
4681
5006
  files: {
4682
- manifest: join14(root, "manifest.yaml"),
4683
- status: join14(root, "status.json"),
4684
- handoff: join14(root, "handoff.md"),
4685
- decisions: join14(root, "decisions.md")
5007
+ manifest: join15(root, "manifest.yaml"),
5008
+ status: join15(root, "status.json"),
5009
+ handoff: join15(root, "handoff.md"),
5010
+ decisions: join15(root, "decisions.md")
4686
5011
  }
4687
5012
  };
4688
5013
  }
@@ -4739,11 +5064,11 @@ function hasErrorCode3(error) {
4739
5064
 
4740
5065
  // src/storage/gitignore.ts
4741
5066
  import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
4742
- import { join as join15 } from "path";
5067
+ import { join as join16 } from "path";
4743
5068
  var MARKER = "# Basou - default ignore";
4744
5069
  var BASOU_GITIGNORE_BLOCK = "# Basou - default ignore\n.basou/logs/\n.basou/raw/\n.basou/tmp/\n.basou/locks/\n.basou/status.json\n.basou/sessions/*/events.jsonl\n.basou/sessions/*/artifacts/\n.basou/approvals/pending/\n.basou/approvals/resolved/\n\n# Basou - default commit\n# .basou/manifest.yaml\n# .basou/handoff.md\n# .basou/decisions.md\n# .basou/tasks/\n# .basou/sessions/*/session.yaml\n# .basou/sessions/*/transcript.md\n# .basou/sessions/*/changed-files.json\n";
4745
5070
  async function appendBasouGitignore(repositoryRoot) {
4746
- const gitignorePath = join15(repositoryRoot, ".gitignore");
5071
+ const gitignorePath = join16(repositoryRoot, ".gitignore");
4747
5072
  let body;
4748
5073
  let existed;
4749
5074
  try {
@@ -4958,7 +5283,7 @@ function hasErrorCode6(error) {
4958
5283
  // src/storage/session-import.ts
4959
5284
  import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
4960
5285
  import { homedir as homedir2 } from "os";
4961
- import { join as join16 } from "path";
5286
+ import { join as join17 } from "path";
4962
5287
  async function importSessionFromJson(paths, manifest, payload, options) {
4963
5288
  if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
4964
5289
  throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
@@ -4983,7 +5308,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
4983
5308
  pathSanitizeReport
4984
5309
  };
4985
5310
  }
4986
- const sessionDir = join16(paths.sessions, newSessionId);
5311
+ const sessionDir = join17(paths.sessions, newSessionId);
4987
5312
  try {
4988
5313
  await mkdir5(sessionDir, { recursive: true });
4989
5314
  } catch (error) {
@@ -4997,7 +5322,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
4997
5322
  throw error;
4998
5323
  }
4999
5324
  try {
5000
- const sessionYamlPath = join16(sessionDir, "session.yaml");
5325
+ const sessionYamlPath = join17(sessionDir, "session.yaml");
5001
5326
  await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
5002
5327
  } catch (error) {
5003
5328
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
@@ -5165,7 +5490,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
5165
5490
  async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
5166
5491
  const sessionId = priorSessionId;
5167
5492
  const importSource = freshPayload.session.source.kind;
5168
- const sessionDir = join16(paths.sessions, priorSessionId);
5493
+ const sessionDir = join17(paths.sessions, priorSessionId);
5169
5494
  const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
5170
5495
  try {
5171
5496
  const priorVerdict = await verifyEventsChain(paths, priorSessionId);
@@ -5207,7 +5532,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5207
5532
  };
5208
5533
  const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
5209
5534
  if (options.dryRun !== true) {
5210
- const eventsPath = join16(sessionDir, "events.jsonl");
5535
+ const eventsPath = join17(sessionDir, "events.jsonl");
5211
5536
  let priorEventsRaw = null;
5212
5537
  try {
5213
5538
  priorEventsRaw = await readFile10(eventsPath);
@@ -5219,7 +5544,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5219
5544
  const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
5220
5545
  try {
5221
5546
  await overwriteYamlFile(
5222
- join16(sessionDir, "session.yaml"),
5547
+ join17(sessionDir, "session.yaml"),
5223
5548
  withIntegrity(updatedRecord, chainResult)
5224
5549
  );
5225
5550
  } catch (error) {
@@ -5243,7 +5568,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5243
5568
  }
5244
5569
  }
5245
5570
  async function rechainSessionInPlace(paths, sessionId, options = {}) {
5246
- const sessionDir = join16(paths.sessions, sessionId);
5571
+ const sessionDir = join17(paths.sessions, sessionId);
5247
5572
  let lock;
5248
5573
  try {
5249
5574
  lock = await acquireLock(paths, "session", sessionId);
@@ -5276,7 +5601,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5276
5601
  if (verdict.status !== "unchained") {
5277
5602
  return { status: "skipped", reason: "tampered" };
5278
5603
  }
5279
- const eventsPath = join16(sessionDir, "events.jsonl");
5604
+ const eventsPath = join17(sessionDir, "events.jsonl");
5280
5605
  let priorRaw;
5281
5606
  try {
5282
5607
  priorRaw = await readFile10(eventsPath);
@@ -5324,7 +5649,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5324
5649
  }
5325
5650
  try {
5326
5651
  await overwriteYamlFile(
5327
- join16(sessionDir, "session.yaml"),
5652
+ join17(sessionDir, "session.yaml"),
5328
5653
  withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
5329
5654
  );
5330
5655
  } catch (error) {
@@ -5406,6 +5731,7 @@ export {
5406
5731
  enumerateTaskIds,
5407
5732
  finalizeSessionYaml,
5408
5733
  findErrorCode,
5734
+ formatDurationMs,
5409
5735
  genesisHash,
5410
5736
  getDiff,
5411
5737
  getSnapshot,
@@ -5438,6 +5764,7 @@ export {
5438
5764
  reimportPreservingId,
5439
5765
  renderDecisions,
5440
5766
  renderHandoff,
5767
+ renderReport,
5441
5768
  renderWithMarkers,
5442
5769
  replayEvents,
5443
5770
  resolveClaudeCodeCommand,