@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.d.ts +255 -117
- package/dist/index.js +875 -548
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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/
|
|
4084
|
-
import {
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
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
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
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
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
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
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
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
|
-
|
|
4197
|
-
|
|
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
|
|
4201
|
-
|
|
4202
|
-
|
|
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
|
-
|
|
4224
|
+
const derived = activeTimeFromTimestamps(eventTimestamps, ACTIVE_GAP_CAP_MS);
|
|
4225
|
+
return { ms: derived.ms, intervals: derived.intervals, basis: "events" };
|
|
4205
4226
|
}
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
}
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
}
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
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
|
|
4373
|
-
|
|
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
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4394
|
-
|
|
4395
|
-
const
|
|
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
|
|
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
|
|
4407
|
-
let eventsUnreadable = false;
|
|
4416
|
+
const sessionDir = join14(input.paths.sessions, entry.sessionId);
|
|
4408
4417
|
try {
|
|
4409
|
-
for await (const ev of replayEvents(
|
|
4418
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
4410
4419
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4411
4420
|
})) {
|
|
4412
|
-
|
|
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
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
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
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
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
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
const
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
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
|
-
|
|
4494
|
-
|
|
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
|
|
4498
|
-
const
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
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
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
if (
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
}
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
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
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
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
|
|
4535
|
-
|
|
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
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
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
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
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
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
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
|
|
4620
|
-
|
|
4621
|
-
|
|
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
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
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
|
-
|
|
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
|
|
4655
|
-
return
|
|
4656
|
-
|
|
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
|
|
4990
|
+
import { join as join15 } from "path";
|
|
4666
4991
|
function basouPaths(repositoryRoot) {
|
|
4667
|
-
const root =
|
|
4668
|
-
const approvalsBase =
|
|
4992
|
+
const root = join15(repositoryRoot, ".basou");
|
|
4993
|
+
const approvalsBase = join15(root, "approvals");
|
|
4669
4994
|
return {
|
|
4670
4995
|
root,
|
|
4671
|
-
sessions:
|
|
4672
|
-
tasks:
|
|
4996
|
+
sessions: join15(root, "sessions"),
|
|
4997
|
+
tasks: join15(root, "tasks"),
|
|
4673
4998
|
approvals: {
|
|
4674
|
-
pending:
|
|
4675
|
-
resolved:
|
|
4999
|
+
pending: join15(approvalsBase, "pending"),
|
|
5000
|
+
resolved: join15(approvalsBase, "resolved")
|
|
4676
5001
|
},
|
|
4677
|
-
locks:
|
|
4678
|
-
logs:
|
|
4679
|
-
raw:
|
|
4680
|
-
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:
|
|
4683
|
-
status:
|
|
4684
|
-
handoff:
|
|
4685
|
-
decisions:
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,
|