@basou/core 0.3.1 → 0.5.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 +1631 -1234
- package/dist/index.js +2554 -1754
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
// src/adapters/claude-code/claude-code-adapter.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
var claudeCodeAdapterMetadata = {
|
|
4
|
+
kind: "claude-code-adapter",
|
|
5
|
+
version: "0.1.0"
|
|
6
|
+
};
|
|
7
|
+
async function resolveClaudeCodeCommand(lookup = isOnPath) {
|
|
8
|
+
for (const candidate of ["claude-code", "claude"]) {
|
|
9
|
+
if (await lookup(candidate)) return { command: candidate };
|
|
10
|
+
}
|
|
11
|
+
throw new Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.");
|
|
12
|
+
}
|
|
13
|
+
async function isOnPath(command) {
|
|
14
|
+
return new Promise((resolve2) => {
|
|
15
|
+
const child = spawn("which", [command], { stdio: "ignore" });
|
|
16
|
+
child.on("error", () => resolve2(false));
|
|
17
|
+
child.on("exit", (code) => resolve2(code === 0));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function summarizeAdapterOutput(_stream, _raw) {
|
|
21
|
+
throw new Error("adapter_output summary is not implemented in this release");
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
// src/ids/ulid.ts
|
|
2
25
|
import { isValid as isValidUlid, monotonicFactory } from "ulid";
|
|
3
26
|
var ID_PREFIXES = Object.freeze(["ws", "task", "ses", "evt", "appr", "decision"]);
|
|
@@ -23,6 +46,500 @@ function isValidPrefixedId(value) {
|
|
|
23
46
|
return isValidUlid(ulidPart);
|
|
24
47
|
}
|
|
25
48
|
|
|
49
|
+
// src/stats/active-time.ts
|
|
50
|
+
var ACTIVE_GAP_CAP_MS = 5 * 60 * 1e3;
|
|
51
|
+
var ENGAGED_TURNS_METHOD = "engaged-turns";
|
|
52
|
+
function activeTimeFromTimestamps(timestampsMs, capMs) {
|
|
53
|
+
const sorted = timestampsMs.filter((t) => Number.isFinite(t)).sort((a, b) => a - b);
|
|
54
|
+
const raw = [];
|
|
55
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
56
|
+
const prev = sorted[i - 1];
|
|
57
|
+
const curr = sorted[i];
|
|
58
|
+
if (prev === void 0 || curr === void 0) continue;
|
|
59
|
+
const gap = curr - prev;
|
|
60
|
+
if (gap <= 0) continue;
|
|
61
|
+
raw.push([prev, prev + Math.min(gap, capMs)]);
|
|
62
|
+
}
|
|
63
|
+
const intervals = mergeIntervals(raw);
|
|
64
|
+
return { ms: sumDurations(intervals), intervals };
|
|
65
|
+
}
|
|
66
|
+
function mergeIntervals(intervals) {
|
|
67
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
68
|
+
const merged = [];
|
|
69
|
+
for (const [start, end] of sorted) {
|
|
70
|
+
const last = merged[merged.length - 1];
|
|
71
|
+
if (last !== void 0 && start <= last[1]) {
|
|
72
|
+
if (end > last[1]) last[1] = end;
|
|
73
|
+
} else {
|
|
74
|
+
merged.push([start, end]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return merged;
|
|
78
|
+
}
|
|
79
|
+
function unionDurationMs(intervals) {
|
|
80
|
+
const merged = mergeIntervals(intervals);
|
|
81
|
+
return { ms: sumDurations(merged), merged };
|
|
82
|
+
}
|
|
83
|
+
function intervalsMsToIso(intervals) {
|
|
84
|
+
return intervals.map(([start, end]) => ({
|
|
85
|
+
start: new Date(start).toISOString(),
|
|
86
|
+
end: new Date(end).toISOString()
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
function intervalsIsoToMs(intervals) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const { start, end } of intervals) {
|
|
92
|
+
const s = Date.parse(start);
|
|
93
|
+
const e = Date.parse(end);
|
|
94
|
+
if (Number.isFinite(s) && Number.isFinite(e) && e >= s) out.push([s, e]);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function sumDurations(intervals) {
|
|
99
|
+
let total = 0;
|
|
100
|
+
for (const [start, end] of intervals) total += end - start;
|
|
101
|
+
return total;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/adapters/claude-code/transcript-importer.ts
|
|
105
|
+
var CLAUDE_IMPORT_SOURCE = "claude-code-import";
|
|
106
|
+
function claudeTranscriptToImportPayload(records, options) {
|
|
107
|
+
const placeholderSessionId = prefixedUlid("ses");
|
|
108
|
+
const askAnswers = indexAskAnswers(records);
|
|
109
|
+
const derived = [];
|
|
110
|
+
const relatedFiles = /* @__PURE__ */ new Set();
|
|
111
|
+
let minTs;
|
|
112
|
+
let maxTs;
|
|
113
|
+
let workingDir;
|
|
114
|
+
let claudeSessionId;
|
|
115
|
+
let outputTokens = 0;
|
|
116
|
+
let inputTokens = 0;
|
|
117
|
+
let cachedInputTokens = 0;
|
|
118
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
119
|
+
const engagementTsMs = [];
|
|
120
|
+
const seenEngagementMessageIds = /* @__PURE__ */ new Set();
|
|
121
|
+
for (const record of records) {
|
|
122
|
+
const ts = readString(record.timestamp);
|
|
123
|
+
if (ts === void 0) continue;
|
|
124
|
+
if (minTs === void 0 || Date.parse(ts) < Date.parse(minTs)) minTs = ts;
|
|
125
|
+
if (maxTs === void 0 || Date.parse(ts) > Date.parse(maxTs)) maxTs = ts;
|
|
126
|
+
if (workingDir === void 0) workingDir = readString(record.cwd);
|
|
127
|
+
if (claudeSessionId === void 0) claudeSessionId = readString(record.sessionId);
|
|
128
|
+
if (record.isSidechain !== true) {
|
|
129
|
+
const tsMs = Date.parse(ts);
|
|
130
|
+
if (Number.isFinite(tsMs)) {
|
|
131
|
+
const recType = readString(record.type);
|
|
132
|
+
if (recType === "user") {
|
|
133
|
+
if (isHumanUserMessage(record)) engagementTsMs.push(tsMs);
|
|
134
|
+
} else if (recType === "assistant") {
|
|
135
|
+
const msg = isObject(record.message) ? record.message : void 0;
|
|
136
|
+
const mid = msg !== void 0 ? readString(msg.id) : void 0;
|
|
137
|
+
if (mid === void 0 || !seenEngagementMessageIds.has(mid)) {
|
|
138
|
+
if (mid !== void 0) seenEngagementMessageIds.add(mid);
|
|
139
|
+
engagementTsMs.push(tsMs);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (readString(record.type) !== "assistant") continue;
|
|
145
|
+
const message = isObject(record.message) ? record.message : void 0;
|
|
146
|
+
const usage = message !== void 0 && isObject(message.usage) ? message.usage : void 0;
|
|
147
|
+
if (usage !== void 0) {
|
|
148
|
+
const messageId = message !== void 0 ? readString(message.id) : void 0;
|
|
149
|
+
const alreadyCounted = messageId !== void 0 && seenMessageIds.has(messageId);
|
|
150
|
+
if (!alreadyCounted) {
|
|
151
|
+
if (messageId !== void 0) seenMessageIds.add(messageId);
|
|
152
|
+
outputTokens += readNonNegInt(usage.output_tokens);
|
|
153
|
+
inputTokens += readNonNegInt(usage.input_tokens);
|
|
154
|
+
cachedInputTokens += readNonNegInt(usage.cache_read_input_tokens);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const cwd = readString(record.cwd) ?? workingDir ?? ".";
|
|
158
|
+
for (const item of toolUses(record)) {
|
|
159
|
+
const name = readString(item.name);
|
|
160
|
+
const input = isObject(item.input) ? item.input : void 0;
|
|
161
|
+
if (input === void 0) continue;
|
|
162
|
+
if (name === "Bash") {
|
|
163
|
+
const command = readString(input.command);
|
|
164
|
+
if (command !== void 0) {
|
|
165
|
+
derived.push(commandExecutedEvent(ts, placeholderSessionId, command, cwd));
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (name === "AskUserQuestion") {
|
|
170
|
+
const useId = readString(item.id);
|
|
171
|
+
const answers = useId !== void 0 ? askAnswers.get(useId) : void 0;
|
|
172
|
+
if (answers !== void 0) {
|
|
173
|
+
for (const [question, answer] of Object.entries(answers)) {
|
|
174
|
+
if (question.length === 0) continue;
|
|
175
|
+
const answerStr = typeof answer === "string" && answer.length > 0 ? answer : void 0;
|
|
176
|
+
const title = answerStr !== void 0 ? `${question} -> ${answerStr}` : question;
|
|
177
|
+
derived.push(decisionRecordedEvent(ts, placeholderSessionId, title));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
|
|
183
|
+
const path2 = readString(input.file_path) ?? readString(input.notebook_path);
|
|
184
|
+
if (path2 !== void 0) {
|
|
185
|
+
const changeType = name === "Write" ? "added" : "modified";
|
|
186
|
+
relatedFiles.add(path2);
|
|
187
|
+
derived.push(fileChangedEvent(ts, placeholderSessionId, path2, changeType));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (minTs === void 0 || maxTs === void 0) return null;
|
|
193
|
+
if (derived.length === 0) return null;
|
|
194
|
+
derived.sort((a, b) => Date.parse(a.occurred_at) - Date.parse(b.occurred_at));
|
|
195
|
+
const events = [
|
|
196
|
+
sessionStartedEvent(minTs, placeholderSessionId),
|
|
197
|
+
...derived,
|
|
198
|
+
sessionEndedEvent(maxTs, placeholderSessionId)
|
|
199
|
+
];
|
|
200
|
+
const externalId = options.externalId ?? claudeSessionId;
|
|
201
|
+
const commandCount = derived.reduce((n, e) => e.type === "command_executed" ? n + 1 : n, 0);
|
|
202
|
+
const fileCount = relatedFiles.size;
|
|
203
|
+
const date = minTs.slice(0, 10);
|
|
204
|
+
const label = `claude-code ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
|
|
205
|
+
const active = engagementTsMs.length >= 2 ? activeTimeFromTimestamps(engagementTsMs, ACTIVE_GAP_CAP_MS) : void 0;
|
|
206
|
+
const metricsFields = {
|
|
207
|
+
...outputTokens > 0 ? { output_tokens: outputTokens } : {},
|
|
208
|
+
...inputTokens > 0 ? { input_tokens: inputTokens } : {},
|
|
209
|
+
...cachedInputTokens > 0 ? { cached_input_tokens: cachedInputTokens } : {},
|
|
210
|
+
...active !== void 0 && active.ms > 0 ? {
|
|
211
|
+
active_time_ms: active.ms,
|
|
212
|
+
active_intervals: intervalsMsToIso(active.intervals),
|
|
213
|
+
active_gap_cap_ms: ACTIVE_GAP_CAP_MS,
|
|
214
|
+
active_time_method: ENGAGED_TURNS_METHOD
|
|
215
|
+
} : {}
|
|
216
|
+
};
|
|
217
|
+
const metrics = Object.keys(metricsFields).length > 0 ? metricsFields : void 0;
|
|
218
|
+
const payload = {
|
|
219
|
+
schema_version: "0.1.0",
|
|
220
|
+
session: {
|
|
221
|
+
label,
|
|
222
|
+
workspace_id: options.workspaceId,
|
|
223
|
+
source: {
|
|
224
|
+
kind: CLAUDE_IMPORT_SOURCE,
|
|
225
|
+
version: "0.1.0",
|
|
226
|
+
...externalId !== void 0 ? { external_id: externalId } : {}
|
|
227
|
+
},
|
|
228
|
+
started_at: minTs,
|
|
229
|
+
ended_at: maxTs,
|
|
230
|
+
// Validated against the canonical enum here; importSessionFromJson
|
|
231
|
+
// overwrites it with the literal "imported" regardless.
|
|
232
|
+
status: "imported",
|
|
233
|
+
working_directory: workingDir ?? ".",
|
|
234
|
+
invocation: { command: "claude", args: [], exit_code: null },
|
|
235
|
+
related_files: [...relatedFiles].sort(),
|
|
236
|
+
summary: null,
|
|
237
|
+
...metrics !== void 0 ? { metrics } : {}
|
|
238
|
+
},
|
|
239
|
+
events
|
|
240
|
+
};
|
|
241
|
+
return payload;
|
|
242
|
+
}
|
|
243
|
+
function baseEvent(occurredAt, sessionId) {
|
|
244
|
+
return {
|
|
245
|
+
schema_version: "0.1.0",
|
|
246
|
+
id: prefixedUlid("evt"),
|
|
247
|
+
session_id: sessionId,
|
|
248
|
+
occurred_at: occurredAt,
|
|
249
|
+
source: CLAUDE_IMPORT_SOURCE
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function sessionStartedEvent(occurredAt, sessionId) {
|
|
253
|
+
return { ...baseEvent(occurredAt, sessionId), type: "session_started" };
|
|
254
|
+
}
|
|
255
|
+
function sessionEndedEvent(occurredAt, sessionId) {
|
|
256
|
+
return { ...baseEvent(occurredAt, sessionId), type: "session_ended" };
|
|
257
|
+
}
|
|
258
|
+
function commandExecutedEvent(occurredAt, sessionId, command, cwd) {
|
|
259
|
+
return {
|
|
260
|
+
...baseEvent(occurredAt, sessionId),
|
|
261
|
+
type: "command_executed",
|
|
262
|
+
command: "bash",
|
|
263
|
+
args: ["-c", command],
|
|
264
|
+
cwd,
|
|
265
|
+
exit_code: null,
|
|
266
|
+
duration_ms: 0
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function fileChangedEvent(occurredAt, sessionId, path2, changeType) {
|
|
270
|
+
return {
|
|
271
|
+
...baseEvent(occurredAt, sessionId),
|
|
272
|
+
type: "file_changed",
|
|
273
|
+
path: path2,
|
|
274
|
+
change_type: changeType
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function decisionRecordedEvent(occurredAt, sessionId, title) {
|
|
278
|
+
return {
|
|
279
|
+
...baseEvent(occurredAt, sessionId),
|
|
280
|
+
type: "decision_recorded",
|
|
281
|
+
decision_id: prefixedUlid("decision"),
|
|
282
|
+
title
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function readString(value) {
|
|
286
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
287
|
+
}
|
|
288
|
+
function readNonNegInt(value) {
|
|
289
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
290
|
+
}
|
|
291
|
+
function isObject(value) {
|
|
292
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
293
|
+
}
|
|
294
|
+
function isHumanUserMessage(record) {
|
|
295
|
+
const message = isObject(record.message) ? record.message : void 0;
|
|
296
|
+
if (message === void 0) return false;
|
|
297
|
+
const content = message.content;
|
|
298
|
+
if (typeof content === "string") return content.length > 0;
|
|
299
|
+
if (Array.isArray(content)) {
|
|
300
|
+
return content.some((block) => {
|
|
301
|
+
if (!isObject(block)) return false;
|
|
302
|
+
const type = readString(block.type);
|
|
303
|
+
return type !== void 0 && type !== "tool_result";
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
function toolUses(record) {
|
|
309
|
+
const message = isObject(record.message) ? record.message : void 0;
|
|
310
|
+
const content = message !== void 0 && Array.isArray(message.content) ? message.content : [];
|
|
311
|
+
const result = [];
|
|
312
|
+
for (const item of content) {
|
|
313
|
+
if (isObject(item) && readString(item.type) === "tool_use") {
|
|
314
|
+
result.push(item);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
function indexAskAnswers(records) {
|
|
320
|
+
const byId = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const record of records) {
|
|
322
|
+
const result = record.toolUseResult;
|
|
323
|
+
if (!isObject(result)) continue;
|
|
324
|
+
const answers = result.answers;
|
|
325
|
+
if (!isObject(answers)) continue;
|
|
326
|
+
const message = isObject(record.message) ? record.message : void 0;
|
|
327
|
+
const content = message !== void 0 && Array.isArray(message.content) ? message.content : [];
|
|
328
|
+
for (const item of content) {
|
|
329
|
+
if (isObject(item) && readString(item.type) === "tool_result") {
|
|
330
|
+
const id = readString(item.tool_use_id);
|
|
331
|
+
if (id !== void 0) byId.set(id, answers);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return byId;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/adapters/codex/rollout-importer.ts
|
|
339
|
+
var CODEX_IMPORT_SOURCE = "codex-import";
|
|
340
|
+
function codexRolloutToImportPayload(records, options) {
|
|
341
|
+
const placeholderSessionId = prefixedUlid("ses");
|
|
342
|
+
const outputsByCallId = indexOutputs(records);
|
|
343
|
+
const derived = [];
|
|
344
|
+
let minTs;
|
|
345
|
+
let maxTs;
|
|
346
|
+
let workingDir;
|
|
347
|
+
let codexSessionId;
|
|
348
|
+
let lastTokenTotals;
|
|
349
|
+
const engagementTsMs = [];
|
|
350
|
+
for (const record of records) {
|
|
351
|
+
const ts = readString2(record.timestamp);
|
|
352
|
+
if (ts === void 0) continue;
|
|
353
|
+
if (minTs === void 0 || Date.parse(ts) < Date.parse(minTs)) minTs = ts;
|
|
354
|
+
if (maxTs === void 0 || Date.parse(ts) > Date.parse(maxTs)) maxTs = ts;
|
|
355
|
+
const payload2 = isObject2(record.payload) ? record.payload : void 0;
|
|
356
|
+
if (payload2 === void 0) continue;
|
|
357
|
+
if (readString2(record.type) === "session_meta") {
|
|
358
|
+
if (workingDir === void 0) workingDir = readString2(payload2.cwd);
|
|
359
|
+
if (codexSessionId === void 0) codexSessionId = readString2(payload2.id);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (readString2(record.type) === "event_msg" && readString2(payload2.type) === "token_count") {
|
|
363
|
+
const info = isObject2(payload2.info) ? payload2.info : void 0;
|
|
364
|
+
const totals = info !== void 0 && isObject2(info.total_token_usage) ? info.total_token_usage : void 0;
|
|
365
|
+
if (totals !== void 0) lastTokenTotals = totals;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (readString2(record.type) === "event_msg") {
|
|
369
|
+
const pt = readString2(payload2.type);
|
|
370
|
+
if (pt === "user_message" || pt === "agent_message" || pt === "task_started" || pt === "task_complete") {
|
|
371
|
+
const tsMs = Date.parse(ts);
|
|
372
|
+
if (Number.isFinite(tsMs)) engagementTsMs.push(tsMs);
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (readString2(record.type) !== "response_item") continue;
|
|
377
|
+
if (readString2(payload2.type) !== "function_call") continue;
|
|
378
|
+
if (readString2(payload2.name) !== "exec_command") continue;
|
|
379
|
+
const command = readExecCommand(payload2.arguments);
|
|
380
|
+
if (command === void 0) continue;
|
|
381
|
+
const cwd = command.workdir ?? workingDir ?? ".";
|
|
382
|
+
const output = readCallId(payload2.call_id, outputsByCallId);
|
|
383
|
+
const execTsMs = Date.parse(ts);
|
|
384
|
+
if (Number.isFinite(execTsMs)) engagementTsMs.push(execTsMs);
|
|
385
|
+
derived.push(
|
|
386
|
+
commandExecutedEvent2(ts, placeholderSessionId, command.cmd, cwd, {
|
|
387
|
+
exitCode: parseExitCode(output),
|
|
388
|
+
durationMs: parseWallTimeMs(output)
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (minTs === void 0 || maxTs === void 0) return null;
|
|
393
|
+
if (derived.length === 0) return null;
|
|
394
|
+
derived.sort((a, b) => Date.parse(a.occurred_at) - Date.parse(b.occurred_at));
|
|
395
|
+
const events = [
|
|
396
|
+
sessionStartedEvent2(minTs, placeholderSessionId),
|
|
397
|
+
...derived,
|
|
398
|
+
sessionEndedEvent2(maxTs, placeholderSessionId)
|
|
399
|
+
];
|
|
400
|
+
const externalId = options.externalId ?? codexSessionId;
|
|
401
|
+
const commandCount = derived.length;
|
|
402
|
+
const date = minTs.slice(0, 10);
|
|
403
|
+
const label = `codex ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
|
|
404
|
+
const active = engagementTsMs.length >= 2 ? activeTimeFromTimestamps(engagementTsMs, ACTIVE_GAP_CAP_MS) : void 0;
|
|
405
|
+
const tokenFields = lastTokenTotals === void 0 ? {} : {
|
|
406
|
+
...readNonNegInt2(lastTokenTotals.output_tokens) > 0 ? { output_tokens: readNonNegInt2(lastTokenTotals.output_tokens) } : {},
|
|
407
|
+
...readNonNegInt2(lastTokenTotals.input_tokens) > 0 ? { input_tokens: readNonNegInt2(lastTokenTotals.input_tokens) } : {},
|
|
408
|
+
...readNonNegInt2(lastTokenTotals.cached_input_tokens) > 0 ? { cached_input_tokens: readNonNegInt2(lastTokenTotals.cached_input_tokens) } : {},
|
|
409
|
+
...readNonNegInt2(lastTokenTotals.reasoning_output_tokens) > 0 ? { reasoning_output_tokens: readNonNegInt2(lastTokenTotals.reasoning_output_tokens) } : {}
|
|
410
|
+
};
|
|
411
|
+
const metricsFields = {
|
|
412
|
+
...tokenFields,
|
|
413
|
+
...active !== void 0 && active.ms > 0 ? {
|
|
414
|
+
active_time_ms: active.ms,
|
|
415
|
+
active_intervals: intervalsMsToIso(active.intervals),
|
|
416
|
+
active_gap_cap_ms: ACTIVE_GAP_CAP_MS,
|
|
417
|
+
active_time_method: ENGAGED_TURNS_METHOD
|
|
418
|
+
} : {}
|
|
419
|
+
};
|
|
420
|
+
const metrics = Object.keys(metricsFields).length > 0 ? metricsFields : void 0;
|
|
421
|
+
const payload = {
|
|
422
|
+
schema_version: "0.1.0",
|
|
423
|
+
session: {
|
|
424
|
+
label,
|
|
425
|
+
workspace_id: options.workspaceId,
|
|
426
|
+
source: {
|
|
427
|
+
kind: CODEX_IMPORT_SOURCE,
|
|
428
|
+
version: "0.1.0",
|
|
429
|
+
...externalId !== void 0 ? { external_id: externalId } : {}
|
|
430
|
+
},
|
|
431
|
+
started_at: minTs,
|
|
432
|
+
ended_at: maxTs,
|
|
433
|
+
// Validated against the canonical enum here; importSessionFromJson
|
|
434
|
+
// overwrites it with the literal "imported" regardless.
|
|
435
|
+
status: "imported",
|
|
436
|
+
working_directory: workingDir ?? ".",
|
|
437
|
+
invocation: { command: "codex", args: [], exit_code: null },
|
|
438
|
+
related_files: [],
|
|
439
|
+
summary: null,
|
|
440
|
+
...metrics !== void 0 ? { metrics } : {}
|
|
441
|
+
},
|
|
442
|
+
events
|
|
443
|
+
};
|
|
444
|
+
return payload;
|
|
445
|
+
}
|
|
446
|
+
function baseEvent2(occurredAt, sessionId) {
|
|
447
|
+
return {
|
|
448
|
+
schema_version: "0.1.0",
|
|
449
|
+
id: prefixedUlid("evt"),
|
|
450
|
+
session_id: sessionId,
|
|
451
|
+
occurred_at: occurredAt,
|
|
452
|
+
source: CODEX_IMPORT_SOURCE
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function sessionStartedEvent2(occurredAt, sessionId) {
|
|
456
|
+
return { ...baseEvent2(occurredAt, sessionId), type: "session_started" };
|
|
457
|
+
}
|
|
458
|
+
function sessionEndedEvent2(occurredAt, sessionId) {
|
|
459
|
+
return { ...baseEvent2(occurredAt, sessionId), type: "session_ended" };
|
|
460
|
+
}
|
|
461
|
+
function commandExecutedEvent2(occurredAt, sessionId, command, cwd, outcome) {
|
|
462
|
+
return {
|
|
463
|
+
...baseEvent2(occurredAt, sessionId),
|
|
464
|
+
type: "command_executed",
|
|
465
|
+
command: "bash",
|
|
466
|
+
args: ["-c", command],
|
|
467
|
+
cwd,
|
|
468
|
+
exit_code: outcome.exitCode,
|
|
469
|
+
duration_ms: outcome.durationMs
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function readString2(value) {
|
|
473
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
474
|
+
}
|
|
475
|
+
function readNonNegInt2(value) {
|
|
476
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
477
|
+
}
|
|
478
|
+
function isObject2(value) {
|
|
479
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
480
|
+
}
|
|
481
|
+
function readExecCommand(value) {
|
|
482
|
+
const raw = readString2(value);
|
|
483
|
+
if (raw === void 0) return void 0;
|
|
484
|
+
let parsed;
|
|
485
|
+
try {
|
|
486
|
+
parsed = JSON.parse(raw);
|
|
487
|
+
} catch {
|
|
488
|
+
return void 0;
|
|
489
|
+
}
|
|
490
|
+
if (!isObject2(parsed)) return void 0;
|
|
491
|
+
const cmd = readString2(parsed.cmd);
|
|
492
|
+
if (cmd === void 0) return void 0;
|
|
493
|
+
return { cmd, workdir: readString2(parsed.workdir) };
|
|
494
|
+
}
|
|
495
|
+
function readCallId(value, outputs) {
|
|
496
|
+
const callId = readString2(value);
|
|
497
|
+
return callId !== void 0 ? outputs.get(callId) : void 0;
|
|
498
|
+
}
|
|
499
|
+
function parseExitCode(output) {
|
|
500
|
+
if (output === void 0) return null;
|
|
501
|
+
const match = output.match(/Process exited with code (-?\d+)/);
|
|
502
|
+
return match?.[1] !== void 0 ? Number.parseInt(match[1], 10) : null;
|
|
503
|
+
}
|
|
504
|
+
function parseWallTimeMs(output) {
|
|
505
|
+
if (output === void 0) return 0;
|
|
506
|
+
const match = output.match(/Wall time:\s*([\d.]+)\s*seconds/);
|
|
507
|
+
if (match?.[1] === void 0) return 0;
|
|
508
|
+
const seconds = Number.parseFloat(match[1]);
|
|
509
|
+
return Number.isFinite(seconds) ? Math.round(seconds * 1e3) : 0;
|
|
510
|
+
}
|
|
511
|
+
function indexOutputs(records) {
|
|
512
|
+
const byId = /* @__PURE__ */ new Map();
|
|
513
|
+
for (const record of records) {
|
|
514
|
+
if (readString2(record.type) !== "response_item") continue;
|
|
515
|
+
const payload = isObject2(record.payload) ? record.payload : void 0;
|
|
516
|
+
if (payload === void 0) continue;
|
|
517
|
+
if (readString2(payload.type) !== "function_call_output") continue;
|
|
518
|
+
const callId = readString2(payload.call_id);
|
|
519
|
+
const output = readString2(payload.output);
|
|
520
|
+
if (callId !== void 0 && output !== void 0) byId.set(callId, output);
|
|
521
|
+
}
|
|
522
|
+
return byId;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/approval/approval-store.ts
|
|
526
|
+
import { readdir } from "fs/promises";
|
|
527
|
+
import { join } from "path";
|
|
528
|
+
|
|
529
|
+
// src/lib/error-codes.ts
|
|
530
|
+
function findErrorCode(error, code, depth = 4) {
|
|
531
|
+
let cur = error;
|
|
532
|
+
for (let i = 0; i < depth && cur instanceof Error; i++) {
|
|
533
|
+
const c = cur.code;
|
|
534
|
+
if (typeof c === "string" && c === code) return true;
|
|
535
|
+
cur = cur.cause;
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/schemas/approval.schema.ts
|
|
541
|
+
import { z as z2 } from "zod";
|
|
542
|
+
|
|
26
543
|
// src/schemas/shared.schema.ts
|
|
27
544
|
import { z } from "zod";
|
|
28
545
|
var SchemaVersionSchema = z.literal("0.1.0");
|
|
@@ -40,395 +557,30 @@ var DecisionIdSchema = createPrefixedIdSchema("decision");
|
|
|
40
557
|
var RiskLevelSchema = z.enum(["low", "medium", "high", "critical"]);
|
|
41
558
|
var EventSourceSchema = z.string().min(1);
|
|
42
559
|
|
|
43
|
-
// src/schemas/
|
|
44
|
-
|
|
45
|
-
var
|
|
46
|
-
name: z2.string().optional(),
|
|
47
|
-
description: z2.string().optional(),
|
|
48
|
-
repository_url: z2.string().nullable().optional()
|
|
49
|
-
});
|
|
50
|
-
var CapabilitiesSchema = z2.object({
|
|
51
|
-
enabled: z2.array(z2.string())
|
|
52
|
-
});
|
|
53
|
-
var ApprovalConfigSchema = z2.object({
|
|
54
|
-
required_for: z2.array(z2.string()).optional(),
|
|
55
|
-
default_risk_level: z2.enum(["low", "medium", "high", "critical"])
|
|
56
|
-
});
|
|
57
|
-
var ClaudeCodeAdapterConfigSchema = z2.object({
|
|
58
|
-
enabled: z2.boolean(),
|
|
59
|
-
config_path: z2.string().optional()
|
|
60
|
-
});
|
|
61
|
-
var AdaptersSchema = z2.object({
|
|
62
|
-
"claude-code": ClaudeCodeAdapterConfigSchema
|
|
63
|
-
});
|
|
64
|
-
var GitConfigSchema = z2.object({
|
|
65
|
-
events_log: z2.enum(["ignore", "commit"]).default("ignore")
|
|
66
|
-
});
|
|
67
|
-
var WorkspaceMetaSchema = z2.object({
|
|
68
|
-
id: WorkspaceIdSchema,
|
|
69
|
-
name: z2.string().min(1),
|
|
70
|
-
created_at: IsoTimestampSchema,
|
|
71
|
-
updated_at: IsoTimestampSchema
|
|
72
|
-
});
|
|
73
|
-
var ManifestSchema = z2.object({
|
|
560
|
+
// src/schemas/approval.schema.ts
|
|
561
|
+
var ApprovalStatusSchema = z2.enum(["pending", "approved", "rejected", "expired"]);
|
|
562
|
+
var ApprovalSchema = z2.object({
|
|
74
563
|
schema_version: SchemaVersionSchema,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
564
|
+
id: ApprovalIdSchema,
|
|
565
|
+
session_id: SessionIdSchema,
|
|
566
|
+
created_at: IsoTimestampSchema,
|
|
567
|
+
status: ApprovalStatusSchema,
|
|
568
|
+
risk_level: RiskLevelSchema,
|
|
569
|
+
action: z2.object({ kind: z2.string() }).passthrough(),
|
|
570
|
+
reason: z2.string(),
|
|
571
|
+
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
572
|
+
// The four fields below are null while `status === "pending"` and set
|
|
573
|
+
// once a resolver records a decision. Defaulting to null keeps the
|
|
574
|
+
// pending YAML free of explicit nulls if a producer omits them.
|
|
575
|
+
resolver: z2.string().nullable().default(null),
|
|
576
|
+
resolved_at: IsoTimestampSchema.nullable().default(null),
|
|
577
|
+
note: z2.string().nullable().default(null),
|
|
578
|
+
rejection_reason: z2.string().nullable().default(null)
|
|
82
579
|
});
|
|
83
580
|
|
|
84
|
-
// src/
|
|
85
|
-
import {
|
|
86
|
-
|
|
87
|
-
schema_version: SchemaVersionSchema,
|
|
88
|
-
generated_at: IsoTimestampSchema,
|
|
89
|
-
workspace: z3.object({
|
|
90
|
-
id: WorkspaceIdSchema,
|
|
91
|
-
name: z3.string().min(1),
|
|
92
|
-
basou_version: z3.literal("0.1.0")
|
|
93
|
-
}).strict(),
|
|
94
|
-
directories_present: z3.object({
|
|
95
|
-
sessions: z3.boolean(),
|
|
96
|
-
tasks: z3.boolean(),
|
|
97
|
-
approvals_pending: z3.boolean(),
|
|
98
|
-
approvals_resolved: z3.boolean(),
|
|
99
|
-
logs: z3.boolean(),
|
|
100
|
-
raw: z3.boolean(),
|
|
101
|
-
tmp: z3.boolean()
|
|
102
|
-
}).strict()
|
|
103
|
-
}).strict();
|
|
104
|
-
|
|
105
|
-
// src/schemas/session.schema.ts
|
|
106
|
-
import { z as z4 } from "zod";
|
|
107
|
-
var SessionStatusSchema = z4.enum([
|
|
108
|
-
"initialized",
|
|
109
|
-
"running",
|
|
110
|
-
"waiting_approval",
|
|
111
|
-
"completed",
|
|
112
|
-
"failed",
|
|
113
|
-
"interrupted",
|
|
114
|
-
"imported",
|
|
115
|
-
"archived"
|
|
116
|
-
]);
|
|
117
|
-
var SessionSourceKindSchema = z4.enum([
|
|
118
|
-
"claude-code-adapter",
|
|
119
|
-
"human",
|
|
120
|
-
"import",
|
|
121
|
-
"terminal"
|
|
122
|
-
]);
|
|
123
|
-
var SessionSourceSchema = z4.object({
|
|
124
|
-
kind: SessionSourceKindSchema,
|
|
125
|
-
version: z4.literal("0.1.0")
|
|
126
|
-
});
|
|
127
|
-
var InvocationSchema = z4.object({
|
|
128
|
-
command: z4.string().min(1),
|
|
129
|
-
args: z4.array(z4.string()).default([]),
|
|
130
|
-
// Nullable to record signal-terminated runs where the child has no exit
|
|
131
|
-
// code; the same nullability is mirrored in CommandExecutedEventSchema.
|
|
132
|
-
exit_code: z4.number().int().nullable()
|
|
133
|
-
});
|
|
134
|
-
var SessionInnerSchema = z4.object({
|
|
135
|
-
id: SessionIdSchema,
|
|
136
|
-
label: z4.string().optional(),
|
|
137
|
-
task_id: TaskIdSchema.nullable().optional(),
|
|
138
|
-
workspace_id: WorkspaceIdSchema,
|
|
139
|
-
source: SessionSourceSchema,
|
|
140
|
-
started_at: IsoTimestampSchema,
|
|
141
|
-
// ended_at is optional because initialized / running sessions have no end time yet.
|
|
142
|
-
ended_at: IsoTimestampSchema.optional(),
|
|
143
|
-
status: SessionStatusSchema,
|
|
144
|
-
working_directory: z4.string().min(1),
|
|
145
|
-
invocation: InvocationSchema,
|
|
146
|
-
related_files: z4.array(z4.string()).default([]),
|
|
147
|
-
events_log: z4.string().default("events.jsonl"),
|
|
148
|
-
summary: z4.string().nullable().optional()
|
|
149
|
-
});
|
|
150
|
-
var SessionSchema = z4.object({
|
|
151
|
-
schema_version: SchemaVersionSchema,
|
|
152
|
-
session: SessionInnerSchema
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// src/schemas/task.schema.ts
|
|
156
|
-
import { z as z5 } from "zod";
|
|
157
|
-
var TaskStatusSchema = z5.enum(["planned", "in_progress", "done", "cancelled"]);
|
|
158
|
-
var TaskInnerSchema = z5.object({
|
|
159
|
-
id: TaskIdSchema,
|
|
160
|
-
title: z5.string().min(1),
|
|
161
|
-
label: z5.string().min(1).optional(),
|
|
162
|
-
status: TaskStatusSchema,
|
|
163
|
-
created_at: IsoTimestampSchema,
|
|
164
|
-
updated_at: IsoTimestampSchema,
|
|
165
|
-
workspace_id: WorkspaceIdSchema,
|
|
166
|
-
/**
|
|
167
|
-
* Session id that anchors this task. For freshly created tasks it is the
|
|
168
|
-
* session that wrote the `task_created` event (= ad-hoc reconcile target
|
|
169
|
-
* for ad-hoc paths, or the target session id for attach paths). After
|
|
170
|
-
* `basou task reconcile --write` repairs a broken anchor the
|
|
171
|
-
* value is replaced with the ad-hoc reconcile session id; the old broken
|
|
172
|
-
* session_id is preserved on the `task_reconciled` event payload via
|
|
173
|
-
* `removed_created_in_session` for audit. So this field always names a
|
|
174
|
-
* reachable session, even after the original anchor is gone.
|
|
175
|
-
*/
|
|
176
|
-
created_in_session: SessionIdSchema,
|
|
177
|
-
/**
|
|
178
|
-
* Snapshot of sessions linked to this task. The events.jsonl history is
|
|
179
|
-
* the source of truth (see
|
|
180
|
-
* `docs/spec/generated-markdown.md#105-decisionsmd-generation-principle`);
|
|
181
|
-
* this field is maintained as a UX-only cache so editors can read the
|
|
182
|
-
* task.md and immediately see related sessions. Defaults to `[]` for
|
|
183
|
-
* backward compatibility.
|
|
184
|
-
*/
|
|
185
|
-
linked_sessions: z5.array(SessionIdSchema).default([])
|
|
186
|
-
});
|
|
187
|
-
var TaskSchema = z5.object({
|
|
188
|
-
schema_version: SchemaVersionSchema,
|
|
189
|
-
task: TaskInnerSchema
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// src/schemas/task-index.schema.ts
|
|
193
|
-
import { z as z6 } from "zod";
|
|
194
|
-
var TaskIndexEntrySchema = z6.object({
|
|
195
|
-
id: TaskIdSchema,
|
|
196
|
-
status: TaskStatusSchema,
|
|
197
|
-
label: z6.string().min(1).optional(),
|
|
198
|
-
updated_at: IsoTimestampSchema
|
|
199
|
-
}).strict();
|
|
200
|
-
var TaskIndexSchema = z6.object({
|
|
201
|
-
schema_version: SchemaVersionSchema,
|
|
202
|
-
tasks: z6.array(TaskIndexEntrySchema),
|
|
203
|
-
last_rebuilt_at: IsoTimestampSchema
|
|
204
|
-
}).strict();
|
|
205
|
-
var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
|
|
206
|
-
|
|
207
|
-
// src/schemas/approval.schema.ts
|
|
208
|
-
import { z as z7 } from "zod";
|
|
209
|
-
var ApprovalStatusSchema = z7.enum(["pending", "approved", "rejected", "expired"]);
|
|
210
|
-
var ApprovalSchema = z7.object({
|
|
211
|
-
schema_version: SchemaVersionSchema,
|
|
212
|
-
id: ApprovalIdSchema,
|
|
213
|
-
session_id: SessionIdSchema,
|
|
214
|
-
created_at: IsoTimestampSchema,
|
|
215
|
-
status: ApprovalStatusSchema,
|
|
216
|
-
risk_level: RiskLevelSchema,
|
|
217
|
-
action: z7.object({ kind: z7.string() }).passthrough(),
|
|
218
|
-
reason: z7.string(),
|
|
219
|
-
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
220
|
-
// The four fields below are null while `status === "pending"` and set
|
|
221
|
-
// once a resolver records a decision. Defaulting to null keeps the
|
|
222
|
-
// pending YAML free of explicit nulls if a producer omits them.
|
|
223
|
-
resolver: z7.string().nullable().default(null),
|
|
224
|
-
resolved_at: IsoTimestampSchema.nullable().default(null),
|
|
225
|
-
note: z7.string().nullable().default(null),
|
|
226
|
-
rejection_reason: z7.string().nullable().default(null)
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// src/schemas/event.schema.ts
|
|
230
|
-
import { z as z8 } from "zod";
|
|
231
|
-
var BaseEventSchema = z8.object({
|
|
232
|
-
schema_version: SchemaVersionSchema,
|
|
233
|
-
id: EventIdSchema,
|
|
234
|
-
session_id: SessionIdSchema,
|
|
235
|
-
occurred_at: IsoTimestampSchema,
|
|
236
|
-
source: EventSourceSchema
|
|
237
|
-
});
|
|
238
|
-
var SessionStartedEventSchema = BaseEventSchema.extend({
|
|
239
|
-
type: z8.literal("session_started")
|
|
240
|
-
});
|
|
241
|
-
var SessionEndedEventSchema = BaseEventSchema.extend({
|
|
242
|
-
type: z8.literal("session_ended"),
|
|
243
|
-
exit_code: z8.number().int().optional()
|
|
244
|
-
});
|
|
245
|
-
var SessionStatusChangedEventSchema = BaseEventSchema.extend({
|
|
246
|
-
type: z8.literal("session_status_changed"),
|
|
247
|
-
from: z8.string(),
|
|
248
|
-
to: z8.string()
|
|
249
|
-
});
|
|
250
|
-
var ApprovalRequestedEventSchema = BaseEventSchema.extend({
|
|
251
|
-
type: z8.literal("approval_requested"),
|
|
252
|
-
approval_id: ApprovalIdSchema,
|
|
253
|
-
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
254
|
-
risk_level: RiskLevelSchema,
|
|
255
|
-
// `action.kind` is required; additional fields are allowed to support
|
|
256
|
-
// future action shapes (shell_command, external_send, ...).
|
|
257
|
-
action: z8.object({ kind: z8.string() }).passthrough(),
|
|
258
|
-
reason: z8.string(),
|
|
259
|
-
status: z8.literal("pending")
|
|
260
|
-
});
|
|
261
|
-
var ApprovalApprovedEventSchema = BaseEventSchema.extend({
|
|
262
|
-
type: z8.literal("approval_approved"),
|
|
263
|
-
approval_id: ApprovalIdSchema,
|
|
264
|
-
resolver: z8.string().optional(),
|
|
265
|
-
note: z8.string().nullable().optional()
|
|
266
|
-
});
|
|
267
|
-
var ApprovalRejectedEventSchema = BaseEventSchema.extend({
|
|
268
|
-
type: z8.literal("approval_rejected"),
|
|
269
|
-
approval_id: ApprovalIdSchema,
|
|
270
|
-
resolver: z8.string().optional(),
|
|
271
|
-
reason: z8.string()
|
|
272
|
-
});
|
|
273
|
-
var ApprovalExpiredEventSchema = BaseEventSchema.extend({
|
|
274
|
-
type: z8.literal("approval_expired"),
|
|
275
|
-
approval_id: ApprovalIdSchema
|
|
276
|
-
});
|
|
277
|
-
var CommandExecutedEventSchema = BaseEventSchema.extend({
|
|
278
|
-
type: z8.literal("command_executed"),
|
|
279
|
-
command: z8.string(),
|
|
280
|
-
args: z8.array(z8.string()),
|
|
281
|
-
cwd: z8.string(),
|
|
282
|
-
exit_code: z8.number().int().nullable(),
|
|
283
|
-
signal: z8.string().nullable().optional(),
|
|
284
|
-
received_signal: z8.string().nullable().optional(),
|
|
285
|
-
duration_ms: z8.number().int().nonnegative()
|
|
286
|
-
});
|
|
287
|
-
var GitSnapshotEventSchema = BaseEventSchema.extend({
|
|
288
|
-
type: z8.literal("git_snapshot"),
|
|
289
|
-
head: z8.string(),
|
|
290
|
-
branch: z8.string(),
|
|
291
|
-
dirty: z8.boolean(),
|
|
292
|
-
staged: z8.array(z8.string()),
|
|
293
|
-
unstaged: z8.array(z8.string()),
|
|
294
|
-
untracked: z8.array(z8.string()),
|
|
295
|
-
ahead: z8.number().int().nonnegative().optional(),
|
|
296
|
-
behind: z8.number().int().nonnegative().optional()
|
|
297
|
-
});
|
|
298
|
-
var FileChangedEventSchema = BaseEventSchema.extend({
|
|
299
|
-
type: z8.literal("file_changed"),
|
|
300
|
-
path: z8.string(),
|
|
301
|
-
change_type: z8.enum(["added", "modified", "deleted", "renamed"]),
|
|
302
|
-
// Renamed entries record the previous path here. Optional + nullable to
|
|
303
|
-
// keep the wire format stable for added / modified / deleted events.
|
|
304
|
-
old_path: z8.string().nullable().optional()
|
|
305
|
-
});
|
|
306
|
-
var DecisionRecordedEventSchema = BaseEventSchema.extend({
|
|
307
|
-
type: z8.literal("decision_recorded"),
|
|
308
|
-
decision_id: DecisionIdSchema,
|
|
309
|
-
title: z8.string(),
|
|
310
|
-
rationale: z8.string().nullable().optional(),
|
|
311
|
-
alternatives: z8.array(z8.string().min(1)).optional(),
|
|
312
|
-
rejected_reason: z8.string().nullable().optional(),
|
|
313
|
-
linked_events: z8.array(EventIdSchema).optional(),
|
|
314
|
-
linked_files: z8.array(z8.string().min(1).max(4096)).optional()
|
|
315
|
-
});
|
|
316
|
-
var TaskCreatedEventSchema = BaseEventSchema.extend({
|
|
317
|
-
type: z8.literal("task_created"),
|
|
318
|
-
task_id: TaskIdSchema,
|
|
319
|
-
title: z8.string()
|
|
320
|
-
});
|
|
321
|
-
var TaskStatusChangedEventSchema = BaseEventSchema.extend({
|
|
322
|
-
type: z8.literal("task_status_changed"),
|
|
323
|
-
task_id: TaskIdSchema,
|
|
324
|
-
from: z8.string(),
|
|
325
|
-
to: z8.string()
|
|
326
|
-
});
|
|
327
|
-
var TaskReconciledEventSchema = BaseEventSchema.extend({
|
|
328
|
-
type: z8.literal("task_reconciled"),
|
|
329
|
-
task_id: TaskIdSchema,
|
|
330
|
-
removed_created_in_session: SessionIdSchema.nullable().default(null),
|
|
331
|
-
created_in_session_replacement: SessionIdSchema.nullable().default(null),
|
|
332
|
-
removed_linked_sessions: z8.array(SessionIdSchema).default([])
|
|
333
|
-
}).strict();
|
|
334
|
-
var TaskLinkageRefreshedEventSchema = BaseEventSchema.extend({
|
|
335
|
-
type: z8.literal("task_linkage_refreshed"),
|
|
336
|
-
task_id: TaskIdSchema,
|
|
337
|
-
added_linked_sessions: z8.array(SessionIdSchema).default([]),
|
|
338
|
-
removed_linked_sessions: z8.array(SessionIdSchema).default([]),
|
|
339
|
-
final_count: z8.number().int().nonnegative().optional()
|
|
340
|
-
}).strict();
|
|
341
|
-
var TaskDeletedEventSchema = BaseEventSchema.extend({
|
|
342
|
-
type: z8.literal("task_deleted"),
|
|
343
|
-
task_id: TaskIdSchema,
|
|
344
|
-
title: z8.string().min(1)
|
|
345
|
-
}).strict();
|
|
346
|
-
var TaskArchivedEventSchema = BaseEventSchema.extend({
|
|
347
|
-
type: z8.literal("task_archived"),
|
|
348
|
-
task_id: TaskIdSchema,
|
|
349
|
-
title: z8.string().min(1)
|
|
350
|
-
}).strict();
|
|
351
|
-
var NoteAddedEventSchema = BaseEventSchema.extend({
|
|
352
|
-
type: z8.literal("note_added"),
|
|
353
|
-
body: z8.string()
|
|
354
|
-
});
|
|
355
|
-
var AdapterOutputEventSchema = BaseEventSchema.extend({
|
|
356
|
-
type: z8.literal("adapter_output"),
|
|
357
|
-
stream: z8.enum(["stdout", "stderr"]),
|
|
358
|
-
summary: z8.string(),
|
|
359
|
-
raw_ref: z8.string(),
|
|
360
|
-
redacted: z8.boolean().optional()
|
|
361
|
-
}).strict();
|
|
362
|
-
var EventSchema = z8.discriminatedUnion("type", [
|
|
363
|
-
SessionStartedEventSchema,
|
|
364
|
-
SessionEndedEventSchema,
|
|
365
|
-
SessionStatusChangedEventSchema,
|
|
366
|
-
ApprovalRequestedEventSchema,
|
|
367
|
-
ApprovalApprovedEventSchema,
|
|
368
|
-
ApprovalRejectedEventSchema,
|
|
369
|
-
ApprovalExpiredEventSchema,
|
|
370
|
-
CommandExecutedEventSchema,
|
|
371
|
-
GitSnapshotEventSchema,
|
|
372
|
-
FileChangedEventSchema,
|
|
373
|
-
DecisionRecordedEventSchema,
|
|
374
|
-
TaskCreatedEventSchema,
|
|
375
|
-
TaskStatusChangedEventSchema,
|
|
376
|
-
TaskReconciledEventSchema,
|
|
377
|
-
TaskLinkageRefreshedEventSchema,
|
|
378
|
-
TaskDeletedEventSchema,
|
|
379
|
-
TaskArchivedEventSchema,
|
|
380
|
-
NoteAddedEventSchema,
|
|
381
|
-
AdapterOutputEventSchema
|
|
382
|
-
]);
|
|
383
|
-
|
|
384
|
-
// src/schemas/session-import.schema.ts
|
|
385
|
-
import { z as z9 } from "zod";
|
|
386
|
-
var SessionInnerImportSchema = z9.object({
|
|
387
|
-
id: SessionIdSchema.optional(),
|
|
388
|
-
label: z9.string().optional(),
|
|
389
|
-
task_id: TaskIdSchema.nullable().optional(),
|
|
390
|
-
workspace_id: WorkspaceIdSchema,
|
|
391
|
-
source: z9.object({
|
|
392
|
-
kind: SessionSourceKindSchema,
|
|
393
|
-
version: z9.literal("0.1.0")
|
|
394
|
-
}),
|
|
395
|
-
started_at: IsoTimestampSchema,
|
|
396
|
-
ended_at: IsoTimestampSchema.optional(),
|
|
397
|
-
status: SessionStatusSchema,
|
|
398
|
-
working_directory: z9.string().min(1),
|
|
399
|
-
invocation: z9.object({
|
|
400
|
-
command: z9.string().min(1),
|
|
401
|
-
args: z9.array(z9.string()),
|
|
402
|
-
exit_code: z9.number().int().nullable()
|
|
403
|
-
}),
|
|
404
|
-
related_files: z9.array(z9.string()).default([]),
|
|
405
|
-
events_log: z9.string().optional(),
|
|
406
|
-
summary: z9.string().nullable().optional()
|
|
407
|
-
}).strict();
|
|
408
|
-
var SessionImportPayloadSchema = z9.object({
|
|
409
|
-
schema_version: z9.string(),
|
|
410
|
-
session: SessionInnerImportSchema,
|
|
411
|
-
events: z9.array(EventSchema)
|
|
412
|
-
}).strict();
|
|
413
|
-
|
|
414
|
-
// src/approval/approval-store.ts
|
|
415
|
-
import { readdir } from "fs/promises";
|
|
416
|
-
import { join } from "path";
|
|
417
|
-
|
|
418
|
-
// src/lib/error-codes.ts
|
|
419
|
-
function findErrorCode(error, code, depth = 4) {
|
|
420
|
-
let cur = error;
|
|
421
|
-
for (let i = 0; i < depth && cur instanceof Error; i++) {
|
|
422
|
-
const c = cur.code;
|
|
423
|
-
if (typeof c === "string" && c === code) return true;
|
|
424
|
-
cur = cur.cause;
|
|
425
|
-
}
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/storage/yaml-store.ts
|
|
430
|
-
import { readFile } from "fs/promises";
|
|
431
|
-
import { parse, stringify } from "yaml";
|
|
581
|
+
// src/storage/yaml-store.ts
|
|
582
|
+
import { readFile } from "fs/promises";
|
|
583
|
+
import { parse, stringify } from "yaml";
|
|
432
584
|
|
|
433
585
|
// src/storage/atomic.ts
|
|
434
586
|
import { randomUUID } from "crypto";
|
|
@@ -553,338 +705,583 @@ function isLazyExpired(approval, now) {
|
|
|
553
705
|
return expiresMs < now.getTime();
|
|
554
706
|
}
|
|
555
707
|
|
|
556
|
-
// src/
|
|
557
|
-
import { lstat
|
|
708
|
+
// src/decisions/decisions-renderer.ts
|
|
709
|
+
import { lstat } from "fs/promises";
|
|
710
|
+
import { dirname, join as join4, resolve } from "path";
|
|
711
|
+
|
|
712
|
+
// src/events/event-replay.ts
|
|
713
|
+
import { createReadStream } from "fs";
|
|
714
|
+
import { stat } from "fs/promises";
|
|
558
715
|
import { join as join2 } from "path";
|
|
559
|
-
function basouPaths(repositoryRoot) {
|
|
560
|
-
const root = join2(repositoryRoot, ".basou");
|
|
561
|
-
const approvalsBase = join2(root, "approvals");
|
|
562
|
-
return {
|
|
563
|
-
root,
|
|
564
|
-
sessions: join2(root, "sessions"),
|
|
565
|
-
tasks: join2(root, "tasks"),
|
|
566
|
-
approvals: {
|
|
567
|
-
pending: join2(approvalsBase, "pending"),
|
|
568
|
-
resolved: join2(approvalsBase, "resolved")
|
|
569
|
-
},
|
|
570
|
-
locks: join2(root, "locks"),
|
|
571
|
-
logs: join2(root, "logs"),
|
|
572
|
-
raw: join2(root, "raw"),
|
|
573
|
-
tmp: join2(root, "tmp"),
|
|
574
|
-
files: {
|
|
575
|
-
manifest: join2(root, "manifest.yaml"),
|
|
576
|
-
status: join2(root, "status.json"),
|
|
577
|
-
handoff: join2(root, "handoff.md"),
|
|
578
|
-
decisions: join2(root, "decisions.md")
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
var PATH_LABELS = {
|
|
583
|
-
sessions: ".basou/sessions",
|
|
584
|
-
tasks: ".basou/tasks",
|
|
585
|
-
approvalsPending: ".basou/approvals/pending",
|
|
586
|
-
approvalsResolved: ".basou/approvals/resolved",
|
|
587
|
-
locks: ".basou/locks",
|
|
588
|
-
logs: ".basou/logs",
|
|
589
|
-
raw: ".basou/raw",
|
|
590
|
-
tmp: ".basou/tmp"
|
|
591
|
-
};
|
|
592
|
-
async function ensureBasouDirectory(repositoryRoot) {
|
|
593
|
-
const paths = basouPaths(repositoryRoot);
|
|
594
|
-
let existing;
|
|
595
|
-
try {
|
|
596
|
-
existing = await lstat(paths.root);
|
|
597
|
-
} catch (error) {
|
|
598
|
-
if (!hasErrorCode2(error) || error.code !== "ENOENT") {
|
|
599
|
-
throw new Error("Failed to inspect .basou directory", { cause: error });
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
if (existing !== void 0 && !existing.isDirectory()) {
|
|
603
|
-
throw new Error("Basou root .basou exists but is not a directory");
|
|
604
|
-
}
|
|
605
|
-
await Promise.all([
|
|
606
|
-
mkdirLabeled(paths.sessions, PATH_LABELS.sessions),
|
|
607
|
-
mkdirLabeled(paths.tasks, PATH_LABELS.tasks),
|
|
608
|
-
mkdirLabeled(paths.approvals.pending, PATH_LABELS.approvalsPending),
|
|
609
|
-
mkdirLabeled(paths.approvals.resolved, PATH_LABELS.approvalsResolved),
|
|
610
|
-
mkdirLabeled(paths.locks, PATH_LABELS.locks),
|
|
611
|
-
mkdirLabeled(paths.logs, PATH_LABELS.logs),
|
|
612
|
-
mkdirLabeled(paths.raw, PATH_LABELS.raw),
|
|
613
|
-
mkdirLabeled(paths.tmp, PATH_LABELS.tmp)
|
|
614
|
-
]);
|
|
615
|
-
return paths;
|
|
616
|
-
}
|
|
617
|
-
async function mkdirLabeled(target, label) {
|
|
618
|
-
try {
|
|
619
|
-
await mkdir(target, { recursive: true });
|
|
620
|
-
} catch (error) {
|
|
621
|
-
if (hasErrorCode2(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
|
|
622
|
-
throw new Error(`${label} exists but is not a directory`, { cause: error });
|
|
623
|
-
}
|
|
624
|
-
throw new Error(`Failed to create ${label}`, { cause: error });
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
function hasErrorCode2(error) {
|
|
628
|
-
if (!(error instanceof Error)) return false;
|
|
629
|
-
const codeProp = error.code;
|
|
630
|
-
return typeof codeProp === "string";
|
|
631
|
-
}
|
|
632
716
|
|
|
633
|
-
// src/
|
|
634
|
-
import {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
717
|
+
// src/schemas/event.schema.ts
|
|
718
|
+
import { z as z3 } from "zod";
|
|
719
|
+
var BaseEventSchema = z3.object({
|
|
720
|
+
schema_version: SchemaVersionSchema,
|
|
721
|
+
id: EventIdSchema,
|
|
722
|
+
session_id: SessionIdSchema,
|
|
723
|
+
occurred_at: IsoTimestampSchema,
|
|
724
|
+
source: EventSourceSchema
|
|
725
|
+
});
|
|
726
|
+
var SessionStartedEventSchema = BaseEventSchema.extend({
|
|
727
|
+
type: z3.literal("session_started")
|
|
728
|
+
});
|
|
729
|
+
var SessionEndedEventSchema = BaseEventSchema.extend({
|
|
730
|
+
type: z3.literal("session_ended"),
|
|
731
|
+
exit_code: z3.number().int().optional()
|
|
732
|
+
});
|
|
733
|
+
var SessionStatusChangedEventSchema = BaseEventSchema.extend({
|
|
734
|
+
type: z3.literal("session_status_changed"),
|
|
735
|
+
from: z3.string(),
|
|
736
|
+
to: z3.string()
|
|
737
|
+
});
|
|
738
|
+
var ApprovalRequestedEventSchema = BaseEventSchema.extend({
|
|
739
|
+
type: z3.literal("approval_requested"),
|
|
740
|
+
approval_id: ApprovalIdSchema,
|
|
741
|
+
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
742
|
+
risk_level: RiskLevelSchema,
|
|
743
|
+
// `action.kind` is required; additional fields are allowed to support
|
|
744
|
+
// future action shapes (shell_command, external_send, ...).
|
|
745
|
+
action: z3.object({ kind: z3.string() }).passthrough(),
|
|
746
|
+
reason: z3.string(),
|
|
747
|
+
status: z3.literal("pending")
|
|
748
|
+
});
|
|
749
|
+
var ApprovalApprovedEventSchema = BaseEventSchema.extend({
|
|
750
|
+
type: z3.literal("approval_approved"),
|
|
751
|
+
approval_id: ApprovalIdSchema,
|
|
752
|
+
resolver: z3.string().optional(),
|
|
753
|
+
note: z3.string().nullable().optional()
|
|
754
|
+
});
|
|
755
|
+
var ApprovalRejectedEventSchema = BaseEventSchema.extend({
|
|
756
|
+
type: z3.literal("approval_rejected"),
|
|
757
|
+
approval_id: ApprovalIdSchema,
|
|
758
|
+
resolver: z3.string().optional(),
|
|
759
|
+
reason: z3.string()
|
|
760
|
+
});
|
|
761
|
+
var ApprovalExpiredEventSchema = BaseEventSchema.extend({
|
|
762
|
+
type: z3.literal("approval_expired"),
|
|
763
|
+
approval_id: ApprovalIdSchema
|
|
764
|
+
});
|
|
765
|
+
var CommandExecutedEventSchema = BaseEventSchema.extend({
|
|
766
|
+
type: z3.literal("command_executed"),
|
|
767
|
+
command: z3.string(),
|
|
768
|
+
args: z3.array(z3.string()),
|
|
769
|
+
cwd: z3.string(),
|
|
770
|
+
exit_code: z3.number().int().nullable(),
|
|
771
|
+
signal: z3.string().nullable().optional(),
|
|
772
|
+
received_signal: z3.string().nullable().optional(),
|
|
773
|
+
duration_ms: z3.number().int().nonnegative()
|
|
774
|
+
});
|
|
775
|
+
var GitSnapshotEventSchema = BaseEventSchema.extend({
|
|
776
|
+
type: z3.literal("git_snapshot"),
|
|
777
|
+
head: z3.string(),
|
|
778
|
+
branch: z3.string(),
|
|
779
|
+
dirty: z3.boolean(),
|
|
780
|
+
staged: z3.array(z3.string()),
|
|
781
|
+
unstaged: z3.array(z3.string()),
|
|
782
|
+
untracked: z3.array(z3.string()),
|
|
783
|
+
ahead: z3.number().int().nonnegative().optional(),
|
|
784
|
+
behind: z3.number().int().nonnegative().optional()
|
|
785
|
+
});
|
|
786
|
+
var FileChangedEventSchema = BaseEventSchema.extend({
|
|
787
|
+
type: z3.literal("file_changed"),
|
|
788
|
+
path: z3.string(),
|
|
789
|
+
change_type: z3.enum(["added", "modified", "deleted", "renamed"]),
|
|
790
|
+
// Renamed entries record the previous path here. Optional + nullable to
|
|
791
|
+
// keep the wire format stable for added / modified / deleted events.
|
|
792
|
+
old_path: z3.string().nullable().optional()
|
|
793
|
+
});
|
|
794
|
+
var DecisionRecordedEventSchema = BaseEventSchema.extend({
|
|
795
|
+
type: z3.literal("decision_recorded"),
|
|
796
|
+
decision_id: DecisionIdSchema,
|
|
797
|
+
title: z3.string(),
|
|
798
|
+
rationale: z3.string().nullable().optional(),
|
|
799
|
+
alternatives: z3.array(z3.string().min(1)).optional(),
|
|
800
|
+
rejected_reason: z3.string().nullable().optional(),
|
|
801
|
+
linked_events: z3.array(EventIdSchema).optional(),
|
|
802
|
+
linked_files: z3.array(z3.string().min(1).max(4096)).optional()
|
|
803
|
+
});
|
|
804
|
+
var TaskCreatedEventSchema = BaseEventSchema.extend({
|
|
805
|
+
type: z3.literal("task_created"),
|
|
806
|
+
task_id: TaskIdSchema,
|
|
807
|
+
title: z3.string()
|
|
808
|
+
});
|
|
809
|
+
var TaskStatusChangedEventSchema = BaseEventSchema.extend({
|
|
810
|
+
type: z3.literal("task_status_changed"),
|
|
811
|
+
task_id: TaskIdSchema,
|
|
812
|
+
from: z3.string(),
|
|
813
|
+
to: z3.string()
|
|
814
|
+
});
|
|
815
|
+
var TaskReconciledEventSchema = BaseEventSchema.extend({
|
|
816
|
+
type: z3.literal("task_reconciled"),
|
|
817
|
+
task_id: TaskIdSchema,
|
|
818
|
+
removed_created_in_session: SessionIdSchema.nullable().default(null),
|
|
819
|
+
created_in_session_replacement: SessionIdSchema.nullable().default(null),
|
|
820
|
+
removed_linked_sessions: z3.array(SessionIdSchema).default([])
|
|
821
|
+
}).strict();
|
|
822
|
+
var TaskLinkageRefreshedEventSchema = BaseEventSchema.extend({
|
|
823
|
+
type: z3.literal("task_linkage_refreshed"),
|
|
824
|
+
task_id: TaskIdSchema,
|
|
825
|
+
added_linked_sessions: z3.array(SessionIdSchema).default([]),
|
|
826
|
+
removed_linked_sessions: z3.array(SessionIdSchema).default([]),
|
|
827
|
+
final_count: z3.number().int().nonnegative().optional()
|
|
828
|
+
}).strict();
|
|
829
|
+
var TaskDeletedEventSchema = BaseEventSchema.extend({
|
|
830
|
+
type: z3.literal("task_deleted"),
|
|
831
|
+
task_id: TaskIdSchema,
|
|
832
|
+
title: z3.string().min(1)
|
|
833
|
+
}).strict();
|
|
834
|
+
var TaskArchivedEventSchema = BaseEventSchema.extend({
|
|
835
|
+
type: z3.literal("task_archived"),
|
|
836
|
+
task_id: TaskIdSchema,
|
|
837
|
+
title: z3.string().min(1)
|
|
838
|
+
}).strict();
|
|
839
|
+
var NoteAddedEventSchema = BaseEventSchema.extend({
|
|
840
|
+
type: z3.literal("note_added"),
|
|
841
|
+
body: z3.string()
|
|
842
|
+
});
|
|
843
|
+
var AdapterOutputEventSchema = BaseEventSchema.extend({
|
|
844
|
+
type: z3.literal("adapter_output"),
|
|
845
|
+
stream: z3.enum(["stdout", "stderr"]),
|
|
846
|
+
summary: z3.string(),
|
|
847
|
+
raw_ref: z3.string(),
|
|
848
|
+
redacted: z3.boolean().optional()
|
|
849
|
+
}).strict();
|
|
850
|
+
var EventSchema = z3.discriminatedUnion("type", [
|
|
851
|
+
SessionStartedEventSchema,
|
|
852
|
+
SessionEndedEventSchema,
|
|
853
|
+
SessionStatusChangedEventSchema,
|
|
854
|
+
ApprovalRequestedEventSchema,
|
|
855
|
+
ApprovalApprovedEventSchema,
|
|
856
|
+
ApprovalRejectedEventSchema,
|
|
857
|
+
ApprovalExpiredEventSchema,
|
|
858
|
+
CommandExecutedEventSchema,
|
|
859
|
+
GitSnapshotEventSchema,
|
|
860
|
+
FileChangedEventSchema,
|
|
861
|
+
DecisionRecordedEventSchema,
|
|
862
|
+
TaskCreatedEventSchema,
|
|
863
|
+
TaskStatusChangedEventSchema,
|
|
864
|
+
TaskReconciledEventSchema,
|
|
865
|
+
TaskLinkageRefreshedEventSchema,
|
|
866
|
+
TaskDeletedEventSchema,
|
|
867
|
+
TaskArchivedEventSchema,
|
|
868
|
+
NoteAddedEventSchema,
|
|
869
|
+
AdapterOutputEventSchema
|
|
870
|
+
]);
|
|
871
|
+
|
|
872
|
+
// src/events/event-replay.ts
|
|
873
|
+
async function* replayEvents(sessionDir, options = {}) {
|
|
874
|
+
const filePath = join2(sessionDir, "events.jsonl");
|
|
875
|
+
try {
|
|
876
|
+
await stat(filePath);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
if (findErrorCode(error, "ENOENT")) return;
|
|
879
|
+
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
880
|
+
}
|
|
881
|
+
let stream;
|
|
882
|
+
try {
|
|
883
|
+
stream = createReadStream(filePath, { encoding: "utf8" });
|
|
884
|
+
} catch (error) {
|
|
885
|
+
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
886
|
+
}
|
|
887
|
+
let buffer = "";
|
|
888
|
+
let lineNo = 0;
|
|
889
|
+
try {
|
|
890
|
+
for await (const chunk of stream) {
|
|
891
|
+
buffer += chunk;
|
|
892
|
+
let newlineIdx = buffer.indexOf("\n");
|
|
893
|
+
while (newlineIdx !== -1) {
|
|
894
|
+
lineNo += 1;
|
|
895
|
+
const rawLine = buffer.slice(0, newlineIdx);
|
|
896
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
897
|
+
const ev = processLine(rawLine, lineNo, options);
|
|
898
|
+
if (ev !== null) yield ev;
|
|
899
|
+
newlineIdx = buffer.indexOf("\n");
|
|
681
900
|
}
|
|
682
901
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
902
|
+
} catch (error) {
|
|
903
|
+
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
686
904
|
}
|
|
687
|
-
|
|
905
|
+
const trimmed = buffer.replace(/[\r\n\t ]+$/u, "");
|
|
906
|
+
if (trimmed.length === 0) return;
|
|
907
|
+
lineNo += 1;
|
|
908
|
+
let parsed;
|
|
909
|
+
try {
|
|
910
|
+
parsed = JSON.parse(trimmed);
|
|
911
|
+
} catch (cause) {
|
|
912
|
+
options.onWarning?.({ kind: "malformed_json", line: lineNo, cause });
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const result = EventSchema.safeParse(parsed);
|
|
916
|
+
if (!result.success) {
|
|
917
|
+
options.onWarning?.({ kind: "schema_violation", line: lineNo, cause: result.error });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
options.onWarning?.({ kind: "partial_trailing_line", line: lineNo });
|
|
688
921
|
}
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
922
|
+
function processLine(rawLine, lineNo, options) {
|
|
923
|
+
const trimmed = rawLine.trim();
|
|
924
|
+
if (trimmed.length === 0) return null;
|
|
925
|
+
let parsed;
|
|
926
|
+
try {
|
|
927
|
+
parsed = JSON.parse(trimmed);
|
|
928
|
+
} catch (cause) {
|
|
929
|
+
options.onWarning?.({ kind: "malformed_json", line: lineNo, cause });
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const result = EventSchema.safeParse(parsed);
|
|
933
|
+
if (!result.success) {
|
|
934
|
+
options.onWarning?.({ kind: "schema_violation", line: lineNo, cause: result.error });
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
return result.data;
|
|
692
938
|
}
|
|
693
|
-
function
|
|
694
|
-
|
|
695
|
-
|
|
939
|
+
async function readAllEvents(sessionDir, options = {}) {
|
|
940
|
+
const out = [];
|
|
941
|
+
for await (const ev of replayEvents(sessionDir, options)) {
|
|
942
|
+
out.push(ev);
|
|
943
|
+
}
|
|
944
|
+
return out;
|
|
696
945
|
}
|
|
697
946
|
|
|
698
|
-
// src/storage/
|
|
699
|
-
import {
|
|
947
|
+
// src/storage/sessions.ts
|
|
948
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
700
949
|
import { join as join3 } from "path";
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
950
|
+
|
|
951
|
+
// src/schemas/session.schema.ts
|
|
952
|
+
import { z as z4 } from "zod";
|
|
953
|
+
var SessionStatusSchema = z4.enum([
|
|
954
|
+
"initialized",
|
|
955
|
+
"running",
|
|
956
|
+
"waiting_approval",
|
|
957
|
+
"completed",
|
|
958
|
+
"failed",
|
|
959
|
+
"interrupted",
|
|
960
|
+
"imported",
|
|
961
|
+
"archived"
|
|
962
|
+
]);
|
|
963
|
+
var SessionSourceKindSchema = z4.enum([
|
|
964
|
+
"claude-code-adapter",
|
|
965
|
+
"claude-code-import",
|
|
966
|
+
"codex-import",
|
|
967
|
+
"human",
|
|
968
|
+
"import",
|
|
969
|
+
"terminal"
|
|
970
|
+
]);
|
|
971
|
+
var SessionSourceSchema = z4.object({
|
|
972
|
+
kind: SessionSourceKindSchema,
|
|
973
|
+
version: z4.literal("0.1.0"),
|
|
974
|
+
// Optional id of the originating session in the SOURCE tool's own
|
|
975
|
+
// namespace (e.g. the Claude Code session UUID for a `claude-code-import`).
|
|
976
|
+
// Lets re-imports of the same source be deduplicated; absent for live runs.
|
|
977
|
+
external_id: z4.string().optional()
|
|
978
|
+
});
|
|
979
|
+
var InvocationSchema = z4.object({
|
|
980
|
+
command: z4.string().min(1),
|
|
981
|
+
args: z4.array(z4.string()).default([]),
|
|
982
|
+
// Nullable to record signal-terminated runs where the child has no exit
|
|
983
|
+
// code; the same nullability is mirrored in CommandExecutedEventSchema.
|
|
984
|
+
exit_code: z4.number().int().nullable()
|
|
985
|
+
});
|
|
986
|
+
var SessionMetricsSchema = z4.object({
|
|
987
|
+
output_tokens: z4.number().int().nonnegative().optional(),
|
|
988
|
+
input_tokens: z4.number().int().nonnegative().optional(),
|
|
989
|
+
cached_input_tokens: z4.number().int().nonnegative().optional(),
|
|
990
|
+
reasoning_output_tokens: z4.number().int().nonnegative().optional(),
|
|
991
|
+
active_time_ms: z4.number().int().nonnegative().optional(),
|
|
992
|
+
active_intervals: z4.array(z4.object({ start: IsoTimestampSchema, end: IsoTimestampSchema })).optional(),
|
|
993
|
+
active_gap_cap_ms: z4.number().int().nonnegative().optional(),
|
|
994
|
+
active_time_method: z4.string().optional()
|
|
995
|
+
});
|
|
996
|
+
var SessionInnerSchema = z4.object({
|
|
997
|
+
id: SessionIdSchema,
|
|
998
|
+
label: z4.string().optional(),
|
|
999
|
+
task_id: TaskIdSchema.nullable().optional(),
|
|
1000
|
+
workspace_id: WorkspaceIdSchema,
|
|
1001
|
+
source: SessionSourceSchema,
|
|
1002
|
+
started_at: IsoTimestampSchema,
|
|
1003
|
+
// ended_at is optional because initialized / running sessions have no end time yet.
|
|
1004
|
+
ended_at: IsoTimestampSchema.optional(),
|
|
1005
|
+
status: SessionStatusSchema,
|
|
1006
|
+
working_directory: z4.string().min(1),
|
|
1007
|
+
invocation: InvocationSchema,
|
|
1008
|
+
related_files: z4.array(z4.string()).default([]),
|
|
1009
|
+
events_log: z4.string().default("events.jsonl"),
|
|
1010
|
+
summary: z4.string().nullable().optional(),
|
|
1011
|
+
metrics: SessionMetricsSchema.optional()
|
|
1012
|
+
});
|
|
1013
|
+
var SessionSchema = z4.object({
|
|
1014
|
+
schema_version: SchemaVersionSchema,
|
|
1015
|
+
session: SessionInnerSchema
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// src/storage/sessions.ts
|
|
1019
|
+
var STUCK_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1020
|
+
async function enumerateSessionDirs(paths) {
|
|
707
1021
|
try {
|
|
708
|
-
|
|
709
|
-
|
|
1022
|
+
const dirents = await readdir2(paths.sessions, { withFileTypes: true });
|
|
1023
|
+
return dirents.filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
710
1024
|
} catch (error) {
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
existed = false;
|
|
714
|
-
} else {
|
|
715
|
-
throw new Error("Failed to read .gitignore", { cause: error });
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (existed && hasBasouMarker(body)) {
|
|
719
|
-
return { appended: false };
|
|
1025
|
+
if (findErrorCode(error, "ENOENT")) return [];
|
|
1026
|
+
throw new Error("Failed to enumerate sessions", { cause: error });
|
|
720
1027
|
}
|
|
721
|
-
|
|
1028
|
+
}
|
|
1029
|
+
async function readSessionYaml(paths, sessionId) {
|
|
1030
|
+
const filePath = join3(paths.sessions, sessionId, "session.yaml");
|
|
1031
|
+
let raw;
|
|
722
1032
|
try {
|
|
723
|
-
await
|
|
1033
|
+
raw = await readYamlFile(filePath);
|
|
724
1034
|
} catch (error) {
|
|
725
|
-
|
|
1035
|
+
if (error instanceof Error && error.message === "YAML file not found") throw error;
|
|
1036
|
+
throw new Error("Failed to read session.yaml", { cause: error });
|
|
726
1037
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
for (const rawLine of body.split("\n")) {
|
|
731
|
-
if (rawLine.trimEnd().startsWith(MARKER)) return true;
|
|
1038
|
+
const result = SessionSchema.safeParse(raw);
|
|
1039
|
+
if (!result.success) {
|
|
1040
|
+
throw new Error("Failed to read session.yaml", { cause: result.error });
|
|
732
1041
|
}
|
|
733
|
-
return
|
|
1042
|
+
return result.data;
|
|
734
1043
|
}
|
|
735
|
-
function
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1044
|
+
async function classifySuspect(paths, sessionId, session, now, onWarning) {
|
|
1045
|
+
if (session.session.status !== "running") {
|
|
1046
|
+
return { suspect: false, suspectReason: null };
|
|
1047
|
+
}
|
|
1048
|
+
const sessionDir = join3(paths.sessions, sessionId);
|
|
1049
|
+
let endedFound = false;
|
|
1050
|
+
let lastEventOccurredAt = null;
|
|
1051
|
+
const replayOpts = onWarning !== void 0 ? { onWarning } : {};
|
|
1052
|
+
for await (const ev of replayEvents(sessionDir, replayOpts)) {
|
|
1053
|
+
lastEventOccurredAt = ev.occurred_at;
|
|
1054
|
+
if (ev.type === "session_ended") endedFound = true;
|
|
1055
|
+
}
|
|
1056
|
+
if (endedFound) {
|
|
1057
|
+
return { suspect: true, suspectReason: "events_say_ended_but_yaml_running" };
|
|
1058
|
+
}
|
|
1059
|
+
if (lastEventOccurredAt !== null) {
|
|
1060
|
+
const ageMs = now.getTime() - Date.parse(lastEventOccurredAt);
|
|
1061
|
+
if (Number.isFinite(ageMs) && ageMs > STUCK_THRESHOLD_MS) {
|
|
1062
|
+
return { suspect: true, suspectReason: "running_no_end_event" };
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return { suspect: false, suspectReason: null };
|
|
741
1066
|
}
|
|
742
|
-
function
|
|
743
|
-
|
|
744
|
-
|
|
1067
|
+
async function loadSessionEntries(paths, options) {
|
|
1068
|
+
const sessionIds = await enumerateSessionDirs(paths);
|
|
1069
|
+
const entries = [];
|
|
1070
|
+
for (const sid of sessionIds) {
|
|
1071
|
+
let session;
|
|
1072
|
+
try {
|
|
1073
|
+
session = await readSessionYaml(paths, sid);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
1076
|
+
options.onSkip?.(sid, "session_yaml_missing");
|
|
1077
|
+
} else {
|
|
1078
|
+
options.onSkip?.(sid, "session_yaml_invalid");
|
|
1079
|
+
}
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
let suspect = false;
|
|
1083
|
+
let suspectReason = null;
|
|
1084
|
+
try {
|
|
1085
|
+
const r = await classifySuspect(
|
|
1086
|
+
paths,
|
|
1087
|
+
sid,
|
|
1088
|
+
session,
|
|
1089
|
+
options.now,
|
|
1090
|
+
(w) => options.onWarning?.(w, sid)
|
|
1091
|
+
);
|
|
1092
|
+
suspect = r.suspect;
|
|
1093
|
+
suspectReason = r.suspectReason;
|
|
1094
|
+
} catch {
|
|
1095
|
+
options.onSkip?.(sid, "events_jsonl_unreadable");
|
|
1096
|
+
}
|
|
1097
|
+
entries.push({ sessionId: sid, session, suspect, suspectReason });
|
|
1098
|
+
}
|
|
1099
|
+
return entries;
|
|
745
1100
|
}
|
|
746
1101
|
|
|
747
|
-
// src/
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
pid: process.pid,
|
|
755
|
-
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1102
|
+
// src/decisions/decisions-renderer.ts
|
|
1103
|
+
async function renderDecisions(input) {
|
|
1104
|
+
const now = new Date(input.nowIso);
|
|
1105
|
+
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
1106
|
+
const wrappedSkip = (sid, reason) => {
|
|
1107
|
+
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
1108
|
+
input.onSessionSkip?.(sid, reason);
|
|
756
1109
|
};
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1110
|
+
const loadOpts = { now, onSkip: wrappedSkip };
|
|
1111
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
1112
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
1113
|
+
const decisions = [];
|
|
1114
|
+
const knownEventIds = /* @__PURE__ */ new Set();
|
|
1115
|
+
for (const entry of entries) {
|
|
1116
|
+
const sessionDir = join4(input.paths.sessions, entry.sessionId);
|
|
1117
|
+
try {
|
|
1118
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
1119
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
1120
|
+
})) {
|
|
1121
|
+
knownEventIds.add(ev.id);
|
|
1122
|
+
if (ev.type === "decision_recorded") {
|
|
1123
|
+
decisions.push({
|
|
1124
|
+
decisionId: ev.decision_id,
|
|
1125
|
+
title: ev.title,
|
|
1126
|
+
occurredAt: ev.occurred_at,
|
|
1127
|
+
sessionId: entry.sessionId,
|
|
1128
|
+
rationale: ev.rationale,
|
|
1129
|
+
alternatives: ev.alternatives,
|
|
1130
|
+
rejectedReason: ev.rejected_reason,
|
|
1131
|
+
linkedEvents: ev.linked_events,
|
|
1132
|
+
linkedFiles: ev.linked_files
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
} catch {
|
|
1137
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
1138
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
1139
|
+
}
|
|
767
1140
|
}
|
|
768
|
-
|
|
1141
|
+
}
|
|
1142
|
+
decisions.sort((a, b) => {
|
|
1143
|
+
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
1144
|
+
return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
|
|
1145
|
+
});
|
|
1146
|
+
const repoRoot = dirname(input.paths.root);
|
|
1147
|
+
const fileExistenceCache = /* @__PURE__ */ new Map();
|
|
1148
|
+
async function fileExists(relPath) {
|
|
1149
|
+
const cached = fileExistenceCache.get(relPath);
|
|
1150
|
+
if (cached !== void 0) return cached;
|
|
1151
|
+
const abs = resolve(repoRoot, relPath);
|
|
1152
|
+
let exists;
|
|
769
1153
|
try {
|
|
770
|
-
await
|
|
771
|
-
|
|
772
|
-
|
|
1154
|
+
await lstat(abs);
|
|
1155
|
+
exists = true;
|
|
1156
|
+
} catch {
|
|
1157
|
+
exists = false;
|
|
773
1158
|
}
|
|
1159
|
+
fileExistenceCache.set(relPath, exists);
|
|
1160
|
+
return exists;
|
|
774
1161
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1162
|
+
const body = await formatDecisionsBody({
|
|
1163
|
+
nowIso: input.nowIso,
|
|
1164
|
+
decisions,
|
|
1165
|
+
knownEventIds,
|
|
1166
|
+
fileExists
|
|
1167
|
+
});
|
|
1168
|
+
return { body, decisionCount: decisions.length };
|
|
1169
|
+
}
|
|
1170
|
+
async function formatDecisionsBody(args) {
|
|
1171
|
+
const lines = [];
|
|
1172
|
+
lines.push("# Decisions");
|
|
1173
|
+
lines.push("");
|
|
1174
|
+
lines.push(`> Generated at ${args.nowIso}`);
|
|
1175
|
+
lines.push("");
|
|
1176
|
+
if (args.decisions.length === 0) {
|
|
1177
|
+
lines.push("(no decisions recorded yet)");
|
|
1178
|
+
return lines.join("\n");
|
|
1179
|
+
}
|
|
1180
|
+
for (const d of args.decisions) {
|
|
1181
|
+
lines.push(`## ${d.decisionId}: ${d.title}`);
|
|
1182
|
+
lines.push("");
|
|
1183
|
+
const occurredDate = d.occurredAt.slice(0, 10);
|
|
1184
|
+
lines.push(`- \u6C7A\u5B9A\u65E5: ${occurredDate}`);
|
|
1185
|
+
lines.push(`- session: ${shortDecisionSessionId(d.sessionId)}`);
|
|
1186
|
+
lines.push(`- \u5224\u65AD: ${d.title}`);
|
|
1187
|
+
if (typeof d.rationale === "string" && d.rationale.length > 0) {
|
|
1188
|
+
lines.push(`- rationale: ${d.rationale}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (d.alternatives !== void 0 && d.alternatives.length > 0) {
|
|
1191
|
+
lines.push(`- alternatives: ${d.alternatives.join(", ")}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (typeof d.rejectedReason === "string" && d.rejectedReason.length > 0) {
|
|
1194
|
+
lines.push(`- rejected_reason: ${d.rejectedReason}`);
|
|
778
1195
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const raw = await readFile3(lockPath, "utf8");
|
|
785
|
-
const parsed = JSON.parse(raw);
|
|
786
|
-
if (typeof parsed !== "object" || parsed === null) return true;
|
|
787
|
-
const candidate = parsed;
|
|
788
|
-
if (typeof candidate.pid !== "number" || typeof candidate.acquired_at !== "string") {
|
|
789
|
-
return true;
|
|
1196
|
+
if (d.linkedEvents !== void 0 && d.linkedEvents.length > 0) {
|
|
1197
|
+
const parts = d.linkedEvents.map(
|
|
1198
|
+
(eid) => args.knownEventIds.has(eid) ? eid : `${eid} (missing)`
|
|
1199
|
+
);
|
|
1200
|
+
lines.push(`- linked_events: ${parts.join(", ")}`);
|
|
790
1201
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
process.kill(body.pid, 0);
|
|
801
|
-
return false;
|
|
802
|
-
} catch (error) {
|
|
803
|
-
if (findErrorCode(error, "ESRCH")) return true;
|
|
804
|
-
return false;
|
|
1202
|
+
if (d.linkedFiles !== void 0 && d.linkedFiles.length > 0) {
|
|
1203
|
+
const parts = await Promise.all(
|
|
1204
|
+
d.linkedFiles.map(
|
|
1205
|
+
async (path2) => await args.fileExists(path2) ? path2 : `${path2} (missing)`
|
|
1206
|
+
)
|
|
1207
|
+
);
|
|
1208
|
+
lines.push(`- linked_files: ${parts.join(", ")}`);
|
|
1209
|
+
}
|
|
1210
|
+
lines.push("");
|
|
805
1211
|
}
|
|
1212
|
+
return lines.join("\n");
|
|
806
1213
|
}
|
|
807
|
-
function
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
return
|
|
1214
|
+
function shortDecisionSessionId(sessionId) {
|
|
1215
|
+
const SES = "ses_";
|
|
1216
|
+
if (sessionId.startsWith(SES)) return sessionId.slice(SES.length, SES.length + 10);
|
|
1217
|
+
return sessionId.slice(0, 10);
|
|
811
1218
|
}
|
|
812
1219
|
|
|
813
|
-
// src/
|
|
814
|
-
import {
|
|
1220
|
+
// src/events/event-writer.ts
|
|
1221
|
+
import { appendFile } from "fs/promises";
|
|
815
1222
|
import { join as join5 } from "path";
|
|
816
|
-
function
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
async function readTaskIndex(paths) {
|
|
820
|
-
const filePath = taskIndexPath(paths);
|
|
821
|
-
let raw;
|
|
1223
|
+
async function appendEvent(sessionDir, event) {
|
|
1224
|
+
let validated;
|
|
822
1225
|
try {
|
|
823
|
-
|
|
1226
|
+
validated = EventSchema.parse(event);
|
|
824
1227
|
} catch (error) {
|
|
825
|
-
|
|
826
|
-
throw new Error("Task index not found", { cause: error });
|
|
827
|
-
}
|
|
828
|
-
throw new Error("Failed to read task index", { cause: error });
|
|
1228
|
+
throw new Error("Invalid Basou event payload", { cause: error });
|
|
829
1229
|
}
|
|
830
|
-
|
|
1230
|
+
const line = `${JSON.stringify(validated)}
|
|
1231
|
+
`;
|
|
831
1232
|
try {
|
|
832
|
-
|
|
1233
|
+
await appendFile(join5(sessionDir, "events.jsonl"), line, "utf8");
|
|
833
1234
|
} catch (error) {
|
|
834
|
-
throw new Error("
|
|
835
|
-
}
|
|
836
|
-
const result = TaskIndexSchema.safeParse(parsedJson);
|
|
837
|
-
if (!result.success) {
|
|
838
|
-
throw new Error("Invalid task index", { cause: result.error });
|
|
839
|
-
}
|
|
840
|
-
if (result.data.schema_version !== TASK_INDEX_SCHEMA_VERSION) {
|
|
841
|
-
throw new Error("Invalid task index", {
|
|
842
|
-
cause: new Error(`Unsupported task index schema_version: ${result.data.schema_version}`)
|
|
843
|
-
});
|
|
1235
|
+
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
844
1236
|
}
|
|
845
|
-
return result.data;
|
|
846
|
-
}
|
|
847
|
-
async function rebuildTaskIndex(paths, entries, now) {
|
|
848
|
-
const sorted = [...entries].sort((a, b) => a.id.localeCompare(b.id));
|
|
849
|
-
const payload = {
|
|
850
|
-
schema_version: TASK_INDEX_SCHEMA_VERSION,
|
|
851
|
-
tasks: sorted,
|
|
852
|
-
last_rebuilt_at: (now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
|
|
853
|
-
};
|
|
854
|
-
TaskIndexSchema.parse(payload);
|
|
855
|
-
await atomicReplace(taskIndexPath(paths), `${JSON.stringify(payload, null, 2)}
|
|
856
|
-
`);
|
|
857
|
-
return payload;
|
|
858
1237
|
}
|
|
859
|
-
async function
|
|
860
|
-
const
|
|
861
|
-
let current;
|
|
1238
|
+
async function writeEventsBulk(sessionDir, events) {
|
|
1239
|
+
const validated = [];
|
|
862
1240
|
try {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
last_rebuilt_at: nowFn().toISOString()
|
|
869
|
-
};
|
|
1241
|
+
for (const event of events) {
|
|
1242
|
+
validated.push(EventSchema.parse(event));
|
|
1243
|
+
}
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
throw new Error("Invalid Basou event payload", { cause: error });
|
|
870
1246
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
break;
|
|
879
|
-
case "remove":
|
|
880
|
-
nextTasks = current.tasks.filter((t) => t.id !== op.id);
|
|
881
|
-
break;
|
|
1247
|
+
const filePath = join5(sessionDir, "events.jsonl");
|
|
1248
|
+
const body = validated.length > 0 ? `${validated.map((e) => JSON.stringify(e)).join("\n")}
|
|
1249
|
+
` : "";
|
|
1250
|
+
try {
|
|
1251
|
+
await atomicReplace(filePath, body);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
882
1254
|
}
|
|
883
|
-
return await rebuildTaskIndex(paths, nextTasks, nowFn);
|
|
884
1255
|
}
|
|
885
1256
|
|
|
1257
|
+
// src/git/snapshot.ts
|
|
1258
|
+
import { simpleGit } from "simple-git";
|
|
1259
|
+
|
|
886
1260
|
// src/storage/status.ts
|
|
887
1261
|
import * as fsp from "fs/promises";
|
|
1262
|
+
|
|
1263
|
+
// src/schemas/status.schema.ts
|
|
1264
|
+
import { z as z5 } from "zod";
|
|
1265
|
+
var StatusSchema = z5.object({
|
|
1266
|
+
schema_version: SchemaVersionSchema,
|
|
1267
|
+
generated_at: IsoTimestampSchema,
|
|
1268
|
+
workspace: z5.object({
|
|
1269
|
+
id: WorkspaceIdSchema,
|
|
1270
|
+
name: z5.string().min(1),
|
|
1271
|
+
basou_version: z5.literal("0.1.0")
|
|
1272
|
+
}).strict(),
|
|
1273
|
+
directories_present: z5.object({
|
|
1274
|
+
sessions: z5.boolean(),
|
|
1275
|
+
tasks: z5.boolean(),
|
|
1276
|
+
approvals_pending: z5.boolean(),
|
|
1277
|
+
approvals_resolved: z5.boolean(),
|
|
1278
|
+
logs: z5.boolean(),
|
|
1279
|
+
raw: z5.boolean(),
|
|
1280
|
+
tmp: z5.boolean()
|
|
1281
|
+
}).strict()
|
|
1282
|
+
}).strict();
|
|
1283
|
+
|
|
1284
|
+
// src/storage/status.ts
|
|
888
1285
|
var DIRECTORY_CHECKS = {
|
|
889
1286
|
sessions: (p) => p.sessions,
|
|
890
1287
|
tasks: (p) => p.tasks,
|
|
@@ -899,7 +1296,7 @@ async function assertBasouRootSafe(rootPath) {
|
|
|
899
1296
|
try {
|
|
900
1297
|
stat3 = await fsp.lstat(rootPath);
|
|
901
1298
|
} catch (error) {
|
|
902
|
-
if (
|
|
1299
|
+
if (hasErrorCode2(error) && error.code === "ENOENT") {
|
|
903
1300
|
throw new Error("Basou workspace not found", { cause: error });
|
|
904
1301
|
}
|
|
905
1302
|
throw new Error("Failed to inspect .basou root", { cause: error });
|
|
@@ -915,7 +1312,7 @@ async function dirPresent(path2) {
|
|
|
915
1312
|
try {
|
|
916
1313
|
return (await fsp.lstat(path2)).isDirectory();
|
|
917
1314
|
} catch (error) {
|
|
918
|
-
if (
|
|
1315
|
+
if (hasErrorCode2(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
919
1316
|
return false;
|
|
920
1317
|
}
|
|
921
1318
|
throw new Error("Failed to inspect .basou subdirectory", { cause: error });
|
|
@@ -939,352 +1336,269 @@ async function buildStatusSnapshot(input) {
|
|
|
939
1336
|
},
|
|
940
1337
|
directories_present: directoriesEntries
|
|
941
1338
|
};
|
|
942
|
-
return StatusSchema.parse(snapshot);
|
|
943
|
-
}
|
|
944
|
-
async function writeStatus(paths, snapshot) {
|
|
945
|
-
const validated = StatusSchema.parse(snapshot);
|
|
946
|
-
const body = `${JSON.stringify(validated, null, 2)}
|
|
947
|
-
`;
|
|
948
|
-
try {
|
|
949
|
-
await atomicReplace(paths.files.status, body);
|
|
950
|
-
} catch (error) {
|
|
951
|
-
throw new Error("Failed to write status file", { cause: error });
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
async function readStatus(paths) {
|
|
955
|
-
let body;
|
|
956
|
-
try {
|
|
957
|
-
body = await fsp.readFile(paths.files.status, "utf8");
|
|
958
|
-
} catch (error) {
|
|
959
|
-
if (hasErrorCode5(error) && error.code === "ENOENT") {
|
|
960
|
-
throw new Error("Status file not found", { cause: error });
|
|
961
|
-
}
|
|
962
|
-
throw new Error("Failed to read status file", { cause: error });
|
|
963
|
-
}
|
|
964
|
-
let parsed;
|
|
965
|
-
try {
|
|
966
|
-
parsed = JSON.parse(body);
|
|
967
|
-
} catch (error) {
|
|
968
|
-
throw new Error("Failed to parse status JSON", { cause: error });
|
|
969
|
-
}
|
|
970
|
-
return StatusSchema.parse(parsed);
|
|
971
|
-
}
|
|
972
|
-
function hasErrorCode5(error) {
|
|
973
|
-
if (!(error instanceof Error)) return false;
|
|
974
|
-
return typeof error.code === "string";
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// src/storage/sessions.ts
|
|
978
|
-
import { readdir as readdir2 } from "fs/promises";
|
|
979
|
-
import { join as join7 } from "path";
|
|
980
|
-
|
|
981
|
-
// src/events/event-replay.ts
|
|
982
|
-
import { createReadStream } from "fs";
|
|
983
|
-
import { stat } from "fs/promises";
|
|
984
|
-
import { join as join6 } from "path";
|
|
985
|
-
async function* replayEvents(sessionDir, options = {}) {
|
|
986
|
-
const filePath = join6(sessionDir, "events.jsonl");
|
|
987
|
-
try {
|
|
988
|
-
await stat(filePath);
|
|
989
|
-
} catch (error) {
|
|
990
|
-
if (findErrorCode(error, "ENOENT")) return;
|
|
991
|
-
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
992
|
-
}
|
|
993
|
-
let stream;
|
|
994
|
-
try {
|
|
995
|
-
stream = createReadStream(filePath, { encoding: "utf8" });
|
|
996
|
-
} catch (error) {
|
|
997
|
-
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
998
|
-
}
|
|
999
|
-
let buffer = "";
|
|
1000
|
-
let lineNo = 0;
|
|
1001
|
-
try {
|
|
1002
|
-
for await (const chunk of stream) {
|
|
1003
|
-
buffer += chunk;
|
|
1004
|
-
let newlineIdx = buffer.indexOf("\n");
|
|
1005
|
-
while (newlineIdx !== -1) {
|
|
1006
|
-
lineNo += 1;
|
|
1007
|
-
const rawLine = buffer.slice(0, newlineIdx);
|
|
1008
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
1009
|
-
const ev = processLine(rawLine, lineNo, options);
|
|
1010
|
-
if (ev !== null) yield ev;
|
|
1011
|
-
newlineIdx = buffer.indexOf("\n");
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
} catch (error) {
|
|
1015
|
-
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
1016
|
-
}
|
|
1017
|
-
const trimmed = buffer.replace(/[\r\n\t ]+$/u, "");
|
|
1018
|
-
if (trimmed.length === 0) return;
|
|
1019
|
-
lineNo += 1;
|
|
1020
|
-
let parsed;
|
|
1021
|
-
try {
|
|
1022
|
-
parsed = JSON.parse(trimmed);
|
|
1023
|
-
} catch (cause) {
|
|
1024
|
-
options.onWarning?.({ kind: "malformed_json", line: lineNo, cause });
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
const result = EventSchema.safeParse(parsed);
|
|
1028
|
-
if (!result.success) {
|
|
1029
|
-
options.onWarning?.({ kind: "schema_violation", line: lineNo, cause: result.error });
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
options.onWarning?.({ kind: "partial_trailing_line", line: lineNo });
|
|
1033
|
-
}
|
|
1034
|
-
function processLine(rawLine, lineNo, options) {
|
|
1035
|
-
const trimmed = rawLine.trim();
|
|
1036
|
-
if (trimmed.length === 0) return null;
|
|
1037
|
-
let parsed;
|
|
1038
|
-
try {
|
|
1039
|
-
parsed = JSON.parse(trimmed);
|
|
1040
|
-
} catch (cause) {
|
|
1041
|
-
options.onWarning?.({ kind: "malformed_json", line: lineNo, cause });
|
|
1042
|
-
return null;
|
|
1043
|
-
}
|
|
1044
|
-
const result = EventSchema.safeParse(parsed);
|
|
1045
|
-
if (!result.success) {
|
|
1046
|
-
options.onWarning?.({ kind: "schema_violation", line: lineNo, cause: result.error });
|
|
1047
|
-
return null;
|
|
1048
|
-
}
|
|
1049
|
-
return result.data;
|
|
1050
|
-
}
|
|
1051
|
-
async function readAllEvents(sessionDir, options = {}) {
|
|
1052
|
-
const out = [];
|
|
1053
|
-
for await (const ev of replayEvents(sessionDir, options)) {
|
|
1054
|
-
out.push(ev);
|
|
1055
|
-
}
|
|
1056
|
-
return out;
|
|
1339
|
+
return StatusSchema.parse(snapshot);
|
|
1057
1340
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1341
|
+
async function writeStatus(paths, snapshot) {
|
|
1342
|
+
const validated = StatusSchema.parse(snapshot);
|
|
1343
|
+
const body = `${JSON.stringify(validated, null, 2)}
|
|
1344
|
+
`;
|
|
1062
1345
|
try {
|
|
1063
|
-
|
|
1064
|
-
return dirents.filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1346
|
+
await atomicReplace(paths.files.status, body);
|
|
1065
1347
|
} catch (error) {
|
|
1066
|
-
|
|
1067
|
-
throw new Error("Failed to enumerate sessions", { cause: error });
|
|
1348
|
+
throw new Error("Failed to write status file", { cause: error });
|
|
1068
1349
|
}
|
|
1069
1350
|
}
|
|
1070
|
-
async function
|
|
1071
|
-
|
|
1072
|
-
let raw;
|
|
1351
|
+
async function readStatus(paths) {
|
|
1352
|
+
let body;
|
|
1073
1353
|
try {
|
|
1074
|
-
|
|
1354
|
+
body = await fsp.readFile(paths.files.status, "utf8");
|
|
1075
1355
|
} catch (error) {
|
|
1076
|
-
if (error
|
|
1077
|
-
|
|
1356
|
+
if (hasErrorCode2(error) && error.code === "ENOENT") {
|
|
1357
|
+
throw new Error("Status file not found", { cause: error });
|
|
1358
|
+
}
|
|
1359
|
+
throw new Error("Failed to read status file", { cause: error });
|
|
1078
1360
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1361
|
+
let parsed;
|
|
1362
|
+
try {
|
|
1363
|
+
parsed = JSON.parse(body);
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
throw new Error("Failed to parse status JSON", { cause: error });
|
|
1082
1366
|
}
|
|
1083
|
-
return
|
|
1367
|
+
return StatusSchema.parse(parsed);
|
|
1084
1368
|
}
|
|
1085
|
-
|
|
1086
|
-
if (
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
if (lastEventOccurredAt !== null) {
|
|
1101
|
-
const ageMs = now.getTime() - Date.parse(lastEventOccurredAt);
|
|
1102
|
-
if (Number.isFinite(ageMs) && ageMs > STUCK_THRESHOLD_MS) {
|
|
1103
|
-
return { suspect: true, suspectReason: "running_no_end_event" };
|
|
1104
|
-
}
|
|
1369
|
+
function hasErrorCode2(error) {
|
|
1370
|
+
if (!(error instanceof Error)) return false;
|
|
1371
|
+
return typeof error.code === "string";
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/git/snapshot.ts
|
|
1375
|
+
function safeSimpleGit(repoRoot) {
|
|
1376
|
+
return simpleGit({ baseDir: repoRoot });
|
|
1377
|
+
}
|
|
1378
|
+
function isGitNotFound(error) {
|
|
1379
|
+
if (findErrorCode(error, "ENOENT")) return true;
|
|
1380
|
+
let cur = error;
|
|
1381
|
+
for (let i = 0; i < 4 && cur instanceof Error; i++) {
|
|
1382
|
+
if (/\bENOENT\b/.test(cur.message)) return true;
|
|
1383
|
+
cur = cur.cause;
|
|
1105
1384
|
}
|
|
1106
|
-
return
|
|
1385
|
+
return false;
|
|
1107
1386
|
}
|
|
1108
|
-
async function
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
session = await readSessionYaml(paths, sid);
|
|
1115
|
-
} catch (error) {
|
|
1116
|
-
if (error instanceof Error && error.message === "YAML file not found") {
|
|
1117
|
-
options.onSkip?.(sid, "session_yaml_missing");
|
|
1118
|
-
} else {
|
|
1119
|
-
options.onSkip?.(sid, "session_yaml_invalid");
|
|
1120
|
-
}
|
|
1121
|
-
continue;
|
|
1387
|
+
async function resolveRepositoryRoot(cwd) {
|
|
1388
|
+
const git = safeSimpleGit(cwd);
|
|
1389
|
+
try {
|
|
1390
|
+
const root = (await git.revparse(["--show-toplevel"])).trimEnd();
|
|
1391
|
+
if (root.length === 0) {
|
|
1392
|
+
throw new Error("Not a git repository");
|
|
1122
1393
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
paths,
|
|
1128
|
-
sid,
|
|
1129
|
-
session,
|
|
1130
|
-
options.now,
|
|
1131
|
-
(w) => options.onWarning?.(w, sid)
|
|
1132
|
-
);
|
|
1133
|
-
suspect = r.suspect;
|
|
1134
|
-
suspectReason = r.suspectReason;
|
|
1135
|
-
} catch {
|
|
1136
|
-
options.onSkip?.(sid, "events_jsonl_unreadable");
|
|
1394
|
+
return root;
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
if (isGitNotFound(error)) {
|
|
1397
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
1137
1398
|
}
|
|
1138
|
-
|
|
1399
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
throw new Error("Not a git repository", { cause: error });
|
|
1139
1403
|
}
|
|
1140
|
-
return entries;
|
|
1141
1404
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
1145
|
-
var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
|
|
1146
|
-
var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
|
|
1147
|
-
async function readMarkdownFile(filePath) {
|
|
1405
|
+
async function tryRemoteUrl(repositoryRoot) {
|
|
1406
|
+
const git = safeSimpleGit(repositoryRoot);
|
|
1148
1407
|
try {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1408
|
+
const result = await git.getConfig("remote.origin.url", "local");
|
|
1409
|
+
const url = (result.value ?? "").trimEnd();
|
|
1410
|
+
return url.length > 0 ? url : void 0;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return void 0;
|
|
1153
1413
|
}
|
|
1154
1414
|
}
|
|
1155
|
-
async function
|
|
1415
|
+
async function getSnapshot(repositoryRoot) {
|
|
1416
|
+
const git = safeSimpleGit(repositoryRoot);
|
|
1417
|
+
let inside;
|
|
1156
1418
|
try {
|
|
1157
|
-
await
|
|
1419
|
+
inside = await git.checkIsRepo();
|
|
1158
1420
|
} catch (error) {
|
|
1159
|
-
|
|
1421
|
+
if (isGitNotFound(error)) {
|
|
1422
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
1423
|
+
}
|
|
1424
|
+
throw new Error("Failed to read git state", { cause: error });
|
|
1160
1425
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const lines = content.split(/\r?\n/);
|
|
1164
|
-
const startLines = [];
|
|
1165
|
-
const endLines = [];
|
|
1166
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1167
|
-
if (lines[i] === GENERATED_START) startLines.push(i);
|
|
1168
|
-
else if (lines[i] === GENERATED_END) endLines.push(i);
|
|
1426
|
+
if (!inside) {
|
|
1427
|
+
throw new Error("Not a git repository");
|
|
1169
1428
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const endLineStart = lineStartOffset(content, endLineIdx);
|
|
1179
|
-
const startLineEnd = startOffset + GENERATED_START.length;
|
|
1180
|
-
const endLineEnd = endLineStart + GENERATED_END.length;
|
|
1181
|
-
const before = content.slice(0, startOffset);
|
|
1182
|
-
const afterStartNewline = skipOneNewline(content, startLineEnd);
|
|
1183
|
-
const beforeEndNewline = trimOneNewline(content, endLineStart);
|
|
1184
|
-
const generated = content.slice(afterStartNewline, beforeEndNewline);
|
|
1185
|
-
const after = content.slice(endLineEnd);
|
|
1186
|
-
return { kind: "ok", before, generated, after };
|
|
1187
|
-
}
|
|
1188
|
-
function renderWithMarkers(existing, generated, fileLabel) {
|
|
1189
|
-
const normalized = generated.endsWith("\n") ? generated : `${generated}
|
|
1190
|
-
`;
|
|
1191
|
-
if (existing === null) {
|
|
1192
|
-
return `${GENERATED_START}
|
|
1193
|
-
${normalized}${GENERATED_END}
|
|
1194
|
-
`;
|
|
1429
|
+
let head;
|
|
1430
|
+
try {
|
|
1431
|
+
head = (await git.revparse(["HEAD"])).trimEnd();
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
if (isGitNotFound(error)) {
|
|
1434
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
1435
|
+
}
|
|
1436
|
+
throw new Error("No commits in repository", { cause: error });
|
|
1195
1437
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
case "ok":
|
|
1199
|
-
return `${section.before}${GENERATED_START}
|
|
1200
|
-
${normalized}${GENERATED_END}${section.after}`;
|
|
1201
|
-
case "no_markers":
|
|
1202
|
-
throw new Error(`Markers missing in ${fileLabel}`);
|
|
1203
|
-
case "missing_start":
|
|
1204
|
-
case "missing_end":
|
|
1205
|
-
case "multiple_pairs":
|
|
1206
|
-
case "wrong_order":
|
|
1207
|
-
throw new Error(`Markers mismatched in ${fileLabel}`);
|
|
1438
|
+
if (head.length === 0) {
|
|
1439
|
+
throw new Error("No commits in repository");
|
|
1208
1440
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
const ch = content[offset];
|
|
1216
|
-
if (ch === "\n") {
|
|
1217
|
-
line += 1;
|
|
1218
|
-
offset += 1;
|
|
1219
|
-
} else if (ch === "\r") {
|
|
1220
|
-
offset += 1;
|
|
1221
|
-
if (content[offset] === "\n") offset += 1;
|
|
1222
|
-
line += 1;
|
|
1223
|
-
} else {
|
|
1224
|
-
offset += 1;
|
|
1225
|
-
}
|
|
1441
|
+
let branch;
|
|
1442
|
+
try {
|
|
1443
|
+
const raw = (await git.raw(["branch", "--show-current"])).trimEnd();
|
|
1444
|
+
branch = raw.length > 0 ? raw : "HEAD";
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
throw new Error("Failed to read git state", { cause: error });
|
|
1226
1447
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1448
|
+
let dirty;
|
|
1449
|
+
const staged = [];
|
|
1450
|
+
const unstaged = [];
|
|
1451
|
+
const untracked = [];
|
|
1452
|
+
try {
|
|
1453
|
+
const status = await git.status();
|
|
1454
|
+
dirty = !status.isClean();
|
|
1455
|
+
for (const f of status.files) {
|
|
1456
|
+
if (f.index === "?" && f.working_dir === "?") {
|
|
1457
|
+
untracked.push(f.path);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (f.index !== " " && f.index !== "?") staged.push(f.path);
|
|
1461
|
+
if (f.working_dir !== " " && f.working_dir !== "?") unstaged.push(f.path);
|
|
1462
|
+
}
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
throw new Error("Failed to read git state", { cause: error });
|
|
1465
|
+
}
|
|
1466
|
+
let ahead;
|
|
1467
|
+
let behind;
|
|
1468
|
+
if (branch !== "HEAD") {
|
|
1469
|
+
try {
|
|
1470
|
+
const upstream = `${branch}@{upstream}`;
|
|
1471
|
+
const counts = (await git.raw(["rev-list", "--left-right", "--count", `${upstream}...HEAD`])).trim();
|
|
1472
|
+
const [behindStr, aheadStr] = counts.split(/\s+/);
|
|
1473
|
+
const parsedBehind = Number.parseInt(behindStr ?? "", 10);
|
|
1474
|
+
const parsedAhead = Number.parseInt(aheadStr ?? "", 10);
|
|
1475
|
+
if (Number.isFinite(parsedBehind) && parsedBehind >= 0) behind = parsedBehind;
|
|
1476
|
+
if (Number.isFinite(parsedAhead) && parsedAhead >= 0) ahead = parsedAhead;
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
const snapshot = {
|
|
1481
|
+
head,
|
|
1482
|
+
branch,
|
|
1483
|
+
dirty,
|
|
1484
|
+
staged,
|
|
1485
|
+
unstaged,
|
|
1486
|
+
untracked,
|
|
1487
|
+
...ahead !== void 0 ? { ahead } : {},
|
|
1488
|
+
...behind !== void 0 ? { behind } : {}
|
|
1489
|
+
};
|
|
1490
|
+
return snapshot;
|
|
1244
1491
|
}
|
|
1245
1492
|
|
|
1246
|
-
// src/
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
import { join as join11 } from "path";
|
|
1250
|
-
|
|
1251
|
-
// src/events/event-writer.ts
|
|
1252
|
-
import { appendFile } from "fs/promises";
|
|
1253
|
-
import { join as join8 } from "path";
|
|
1254
|
-
async function appendEvent(sessionDir, event) {
|
|
1255
|
-
let validated;
|
|
1493
|
+
// src/git/diff.ts
|
|
1494
|
+
async function getDiff(repoRoot, baseRef, headRef) {
|
|
1495
|
+
let git;
|
|
1256
1496
|
try {
|
|
1257
|
-
|
|
1497
|
+
git = safeSimpleGit(repoRoot);
|
|
1258
1498
|
} catch (error) {
|
|
1259
|
-
|
|
1499
|
+
if (isGitNotFound(error)) {
|
|
1500
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
1501
|
+
}
|
|
1502
|
+
throw new Error("Not a git repository", { cause: error });
|
|
1260
1503
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1504
|
+
if (baseRef === headRef) return { changed_files: [] };
|
|
1505
|
+
let raw;
|
|
1263
1506
|
try {
|
|
1264
|
-
await
|
|
1507
|
+
raw = await git.raw(["diff", "--name-status", `${baseRef}..${headRef}`]);
|
|
1265
1508
|
} catch (error) {
|
|
1266
|
-
|
|
1509
|
+
if (isGitNotFound(error)) {
|
|
1510
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
1511
|
+
}
|
|
1512
|
+
const message = error instanceof Error ? error.message : "";
|
|
1513
|
+
if (/not a git repository/i.test(message)) {
|
|
1514
|
+
throw new Error("Not a git repository", { cause: error });
|
|
1515
|
+
}
|
|
1516
|
+
if (message.includes("bad revision") || message.includes("unknown revision") || message.includes("ambiguous argument")) {
|
|
1517
|
+
throw new Error("Invalid ref", { cause: error });
|
|
1518
|
+
}
|
|
1519
|
+
throw new Error("Failed to compute git diff", { cause: error });
|
|
1267
1520
|
}
|
|
1521
|
+
return { changed_files: parseDiffNameStatus(raw) };
|
|
1268
1522
|
}
|
|
1269
|
-
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1523
|
+
function parseDiffNameStatus(raw) {
|
|
1524
|
+
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
1525
|
+
const changes = [];
|
|
1526
|
+
for (const line of lines) {
|
|
1527
|
+
const parts = line.split(" ");
|
|
1528
|
+
const code = parts[0];
|
|
1529
|
+
if (code === void 0 || code.length === 0) continue;
|
|
1530
|
+
if (code.startsWith("R") && parts.length >= 3) {
|
|
1531
|
+
const newPath = parts[2];
|
|
1532
|
+
const oldPath = parts[1];
|
|
1533
|
+
if (newPath === void 0) continue;
|
|
1534
|
+
changes.push({
|
|
1535
|
+
path: newPath,
|
|
1536
|
+
status: "renamed",
|
|
1537
|
+
...oldPath !== void 0 ? { old_path: oldPath } : {}
|
|
1538
|
+
});
|
|
1539
|
+
} else if (code === "A" && parts[1]) {
|
|
1540
|
+
changes.push({ path: parts[1], status: "added" });
|
|
1541
|
+
} else if (code === "M" && parts[1]) {
|
|
1542
|
+
changes.push({ path: parts[1], status: "modified" });
|
|
1543
|
+
} else if (code === "D" && parts[1]) {
|
|
1544
|
+
changes.push({ path: parts[1], status: "deleted" });
|
|
1274
1545
|
}
|
|
1275
|
-
} catch (error) {
|
|
1276
|
-
throw new Error("Invalid Basou event payload", { cause: error });
|
|
1277
|
-
}
|
|
1278
|
-
const filePath = join8(sessionDir, "events.jsonl");
|
|
1279
|
-
const body = validated.length > 0 ? `${validated.map((e) => JSON.stringify(e)).join("\n")}
|
|
1280
|
-
` : "";
|
|
1281
|
-
try {
|
|
1282
|
-
await atomicReplace(filePath, body);
|
|
1283
|
-
} catch (error) {
|
|
1284
|
-
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
1285
1546
|
}
|
|
1547
|
+
return changes;
|
|
1286
1548
|
}
|
|
1287
1549
|
|
|
1550
|
+
// src/handoff/handoff-renderer.ts
|
|
1551
|
+
import { join as join10 } from "path";
|
|
1552
|
+
|
|
1553
|
+
// src/storage/tasks.ts
|
|
1554
|
+
import { createHash } from "crypto";
|
|
1555
|
+
import { mkdir as mkdir2, readdir as readdir3, readFile as readFile5, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
|
|
1556
|
+
import { join as join9 } from "path";
|
|
1557
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1558
|
+
import { z as z8 } from "zod";
|
|
1559
|
+
|
|
1560
|
+
// src/schemas/task.schema.ts
|
|
1561
|
+
import { z as z6 } from "zod";
|
|
1562
|
+
var TaskStatusSchema = z6.enum(["planned", "in_progress", "done", "cancelled"]);
|
|
1563
|
+
var TaskInnerSchema = z6.object({
|
|
1564
|
+
id: TaskIdSchema,
|
|
1565
|
+
title: z6.string().min(1),
|
|
1566
|
+
label: z6.string().min(1).optional(),
|
|
1567
|
+
status: TaskStatusSchema,
|
|
1568
|
+
created_at: IsoTimestampSchema,
|
|
1569
|
+
updated_at: IsoTimestampSchema,
|
|
1570
|
+
workspace_id: WorkspaceIdSchema,
|
|
1571
|
+
/**
|
|
1572
|
+
* Session id that anchors this task. For freshly created tasks it is the
|
|
1573
|
+
* session that wrote the `task_created` event (= ad-hoc reconcile target
|
|
1574
|
+
* for ad-hoc paths, or the target session id for attach paths). After
|
|
1575
|
+
* `basou task reconcile --write` repairs a broken anchor the
|
|
1576
|
+
* value is replaced with the ad-hoc reconcile session id; the old broken
|
|
1577
|
+
* session_id is preserved on the `task_reconciled` event payload via
|
|
1578
|
+
* `removed_created_in_session` for audit. So this field always names a
|
|
1579
|
+
* reachable session, even after the original anchor is gone.
|
|
1580
|
+
*/
|
|
1581
|
+
created_in_session: SessionIdSchema,
|
|
1582
|
+
/**
|
|
1583
|
+
* Snapshot of sessions linked to this task. The events.jsonl history is
|
|
1584
|
+
* the source of truth (see
|
|
1585
|
+
* `docs/spec/generated-markdown.md#105-decisionsmd-generation-principle`);
|
|
1586
|
+
* this field is maintained as a UX-only cache so editors can read the
|
|
1587
|
+
* task.md and immediately see related sessions. Defaults to `[]` for
|
|
1588
|
+
* backward compatibility.
|
|
1589
|
+
*/
|
|
1590
|
+
linked_sessions: z6.array(SessionIdSchema).default([])
|
|
1591
|
+
});
|
|
1592
|
+
var TaskSchema = z6.object({
|
|
1593
|
+
schema_version: SchemaVersionSchema,
|
|
1594
|
+
task: TaskInnerSchema
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
// src/storage/ad-hoc-session.ts
|
|
1598
|
+
import { mkdir, rm } from "fs/promises";
|
|
1599
|
+
import { homedir } from "os";
|
|
1600
|
+
import { join as join6 } from "path";
|
|
1601
|
+
|
|
1288
1602
|
// src/lib/path-sanitizer.ts
|
|
1289
1603
|
import { posix as path } from "path";
|
|
1290
1604
|
function sanitizePath(rawPath, opts) {
|
|
@@ -1326,17 +1640,7 @@ function sanitizeRelatedFiles(paths, opts) {
|
|
|
1326
1640
|
return { sanitized, mutationCount };
|
|
1327
1641
|
}
|
|
1328
1642
|
|
|
1329
|
-
// src/storage/tasks.ts
|
|
1330
|
-
import { createHash } from "crypto";
|
|
1331
|
-
import { mkdir as mkdir3, readFile as readFile7, readdir as readdir3, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
|
|
1332
|
-
import { join as join10 } from "path";
|
|
1333
|
-
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1334
|
-
import { z as z10 } from "zod";
|
|
1335
|
-
|
|
1336
1643
|
// src/storage/ad-hoc-session.ts
|
|
1337
|
-
import { mkdir as mkdir2, rm } from "fs/promises";
|
|
1338
|
-
import { homedir } from "os";
|
|
1339
|
-
import { join as join9 } from "path";
|
|
1340
1644
|
var FailedToFinalizeError = class extends Error {
|
|
1341
1645
|
sessionId;
|
|
1342
1646
|
targetEventIds;
|
|
@@ -1373,13 +1677,13 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
1373
1677
|
taskId: input.taskId ?? null
|
|
1374
1678
|
})
|
|
1375
1679
|
);
|
|
1376
|
-
const sessionDir =
|
|
1680
|
+
const sessionDir = join6(input.paths.sessions, sessionId);
|
|
1377
1681
|
try {
|
|
1378
|
-
await
|
|
1682
|
+
await mkdir(sessionDir, { recursive: true });
|
|
1379
1683
|
} catch (error) {
|
|
1380
1684
|
throw new Error("Failed to create session directory", { cause: error });
|
|
1381
1685
|
}
|
|
1382
|
-
const sessionYamlPath =
|
|
1686
|
+
const sessionYamlPath = join6(sessionDir, "session.yaml");
|
|
1383
1687
|
try {
|
|
1384
1688
|
await linkYamlFile(sessionYamlPath, initialSession);
|
|
1385
1689
|
} catch (error) {
|
|
@@ -1496,24 +1800,180 @@ async function appendEventToExistingSession(input) {
|
|
|
1496
1800
|
if (status === "imported") {
|
|
1497
1801
|
throw new Error("Cannot attach to imported session");
|
|
1498
1802
|
}
|
|
1499
|
-
const attachable = input.attachableStatuses ?? DEFAULT_ATTACHABLE_STATUSES;
|
|
1500
|
-
if (!attachable.has(status)) {
|
|
1501
|
-
throw new Error(`Session is not active: ${status}`);
|
|
1803
|
+
const attachable = input.attachableStatuses ?? DEFAULT_ATTACHABLE_STATUSES;
|
|
1804
|
+
if (!attachable.has(status)) {
|
|
1805
|
+
throw new Error(`Session is not active: ${status}`);
|
|
1806
|
+
}
|
|
1807
|
+
const eventId = prefixedUlid("evt");
|
|
1808
|
+
const event = assertTargetEventIdentity(input.eventBuilder(eventId), input.sessionId, eventId);
|
|
1809
|
+
const sessionDir = join6(input.paths.sessions, input.sessionId);
|
|
1810
|
+
await appendEvent(sessionDir, event);
|
|
1811
|
+
return { eventId, sessionStatus: status };
|
|
1812
|
+
}
|
|
1813
|
+
function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
|
|
1814
|
+
if (event.session_id !== expectedSessionId) {
|
|
1815
|
+
throw new Error("Target event session_id mismatch");
|
|
1816
|
+
}
|
|
1817
|
+
if (event.id !== expectedEventId) {
|
|
1818
|
+
throw new Error("Target event id mismatch");
|
|
1819
|
+
}
|
|
1820
|
+
return event;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/storage/lockfile.ts
|
|
1824
|
+
import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
|
|
1825
|
+
import { join as join7 } from "path";
|
|
1826
|
+
var STALE_LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
1827
|
+
async function acquireLock(paths, scope, resourceId) {
|
|
1828
|
+
const lockPath = lockfilePath(paths, scope, resourceId);
|
|
1829
|
+
const body = {
|
|
1830
|
+
pid: process.pid,
|
|
1831
|
+
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1832
|
+
};
|
|
1833
|
+
const serialised = JSON.stringify(body);
|
|
1834
|
+
try {
|
|
1835
|
+
await atomicCreate(lockPath, serialised);
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
if (!findErrorCode(error, "EEXIST")) {
|
|
1838
|
+
throw error;
|
|
1839
|
+
}
|
|
1840
|
+
const stale = await isStaleLock(lockPath);
|
|
1841
|
+
if (!stale) {
|
|
1842
|
+
throw new Error("Lock is held by another process", { cause: error });
|
|
1843
|
+
}
|
|
1844
|
+
await unlink2(lockPath).catch(() => void 0);
|
|
1845
|
+
try {
|
|
1846
|
+
await atomicCreate(lockPath, serialised);
|
|
1847
|
+
} catch (retryError) {
|
|
1848
|
+
throw new Error("Lock is held by another process", { cause: retryError });
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
release: async () => {
|
|
1853
|
+
await unlink2(lockPath).catch(() => void 0);
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
async function isStaleLock(lockPath) {
|
|
1858
|
+
let body;
|
|
1859
|
+
try {
|
|
1860
|
+
const raw = await readFile3(lockPath, "utf8");
|
|
1861
|
+
const parsed = JSON.parse(raw);
|
|
1862
|
+
if (typeof parsed !== "object" || parsed === null) return true;
|
|
1863
|
+
const candidate = parsed;
|
|
1864
|
+
if (typeof candidate.pid !== "number" || typeof candidate.acquired_at !== "string") {
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
body = { pid: candidate.pid, acquired_at: candidate.acquired_at };
|
|
1868
|
+
} catch {
|
|
1869
|
+
return true;
|
|
1870
|
+
}
|
|
1871
|
+
const ageMs = Date.now() - Date.parse(body.acquired_at);
|
|
1872
|
+
if (!Number.isFinite(ageMs) || ageMs > STALE_LOCK_MAX_AGE_MS) {
|
|
1873
|
+
return true;
|
|
1874
|
+
}
|
|
1875
|
+
try {
|
|
1876
|
+
process.kill(body.pid, 0);
|
|
1877
|
+
return false;
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
if (findErrorCode(error, "ESRCH")) return true;
|
|
1880
|
+
return false;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function lockfilePath(paths, scope, resourceId) {
|
|
1884
|
+
const sep = resourceId.indexOf("_");
|
|
1885
|
+
const ulid2 = sep >= 0 ? resourceId.slice(sep + 1) : resourceId;
|
|
1886
|
+
return join7(paths.locks, `${scope}_${ulid2}.lock`);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/storage/task-index.ts
|
|
1890
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1891
|
+
import { join as join8 } from "path";
|
|
1892
|
+
|
|
1893
|
+
// src/schemas/task-index.schema.ts
|
|
1894
|
+
import { z as z7 } from "zod";
|
|
1895
|
+
var TaskIndexEntrySchema = z7.object({
|
|
1896
|
+
id: TaskIdSchema,
|
|
1897
|
+
status: TaskStatusSchema,
|
|
1898
|
+
label: z7.string().min(1).optional(),
|
|
1899
|
+
updated_at: IsoTimestampSchema
|
|
1900
|
+
}).strict();
|
|
1901
|
+
var TaskIndexSchema = z7.object({
|
|
1902
|
+
schema_version: SchemaVersionSchema,
|
|
1903
|
+
tasks: z7.array(TaskIndexEntrySchema),
|
|
1904
|
+
last_rebuilt_at: IsoTimestampSchema
|
|
1905
|
+
}).strict();
|
|
1906
|
+
var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
|
|
1907
|
+
|
|
1908
|
+
// src/storage/task-index.ts
|
|
1909
|
+
function taskIndexPath(paths) {
|
|
1910
|
+
return join8(paths.tasks, "index.json");
|
|
1911
|
+
}
|
|
1912
|
+
async function readTaskIndex(paths) {
|
|
1913
|
+
const filePath = taskIndexPath(paths);
|
|
1914
|
+
let raw;
|
|
1915
|
+
try {
|
|
1916
|
+
raw = await readFile4(filePath, "utf8");
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
if (findErrorCode(error, "ENOENT")) {
|
|
1919
|
+
throw new Error("Task index not found", { cause: error });
|
|
1920
|
+
}
|
|
1921
|
+
throw new Error("Failed to read task index", { cause: error });
|
|
1922
|
+
}
|
|
1923
|
+
let parsedJson;
|
|
1924
|
+
try {
|
|
1925
|
+
parsedJson = JSON.parse(raw);
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
throw new Error("Invalid task index", { cause: error });
|
|
1502
1928
|
}
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1929
|
+
const result = TaskIndexSchema.safeParse(parsedJson);
|
|
1930
|
+
if (!result.success) {
|
|
1931
|
+
throw new Error("Invalid task index", { cause: result.error });
|
|
1932
|
+
}
|
|
1933
|
+
if (result.data.schema_version !== TASK_INDEX_SCHEMA_VERSION) {
|
|
1934
|
+
throw new Error("Invalid task index", {
|
|
1935
|
+
cause: new Error(`Unsupported task index schema_version: ${result.data.schema_version}`)
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
return result.data;
|
|
1508
1939
|
}
|
|
1509
|
-
function
|
|
1510
|
-
|
|
1511
|
-
|
|
1940
|
+
async function rebuildTaskIndex(paths, entries, now) {
|
|
1941
|
+
const sorted = [...entries].sort((a, b) => a.id.localeCompare(b.id));
|
|
1942
|
+
const payload = {
|
|
1943
|
+
schema_version: TASK_INDEX_SCHEMA_VERSION,
|
|
1944
|
+
tasks: sorted,
|
|
1945
|
+
last_rebuilt_at: (now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
|
|
1946
|
+
};
|
|
1947
|
+
TaskIndexSchema.parse(payload);
|
|
1948
|
+
await atomicReplace(taskIndexPath(paths), `${JSON.stringify(payload, null, 2)}
|
|
1949
|
+
`);
|
|
1950
|
+
return payload;
|
|
1951
|
+
}
|
|
1952
|
+
async function updateTaskIndex(paths, op, options) {
|
|
1953
|
+
const nowFn = options?.now ?? (() => /* @__PURE__ */ new Date());
|
|
1954
|
+
let current;
|
|
1955
|
+
try {
|
|
1956
|
+
current = await readTaskIndex(paths);
|
|
1957
|
+
} catch {
|
|
1958
|
+
current = {
|
|
1959
|
+
schema_version: TASK_INDEX_SCHEMA_VERSION,
|
|
1960
|
+
tasks: [],
|
|
1961
|
+
last_rebuilt_at: nowFn().toISOString()
|
|
1962
|
+
};
|
|
1512
1963
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1964
|
+
let nextTasks;
|
|
1965
|
+
switch (op.kind) {
|
|
1966
|
+
case "add":
|
|
1967
|
+
nextTasks = current.tasks.some((t) => t.id === op.entry.id) ? current.tasks.map((t) => t.id === op.entry.id ? op.entry : t) : [...current.tasks, op.entry];
|
|
1968
|
+
break;
|
|
1969
|
+
case "update":
|
|
1970
|
+
nextTasks = current.tasks.some((t) => t.id === op.entry.id) ? current.tasks.map((t) => t.id === op.entry.id ? op.entry : t) : [...current.tasks, op.entry];
|
|
1971
|
+
break;
|
|
1972
|
+
case "remove":
|
|
1973
|
+
nextTasks = current.tasks.filter((t) => t.id !== op.id);
|
|
1974
|
+
break;
|
|
1515
1975
|
}
|
|
1516
|
-
return
|
|
1976
|
+
return await rebuildTaskIndex(paths, nextTasks, nowFn);
|
|
1517
1977
|
}
|
|
1518
1978
|
|
|
1519
1979
|
// src/storage/tasks.ts
|
|
@@ -1526,8 +1986,8 @@ var DEFAULT_ATTACHABLE_STATUSES2 = /* @__PURE__ */ new Set([
|
|
|
1526
1986
|
"waiting_approval"
|
|
1527
1987
|
]);
|
|
1528
1988
|
var InitialTaskStatusSchema = TaskStatusSchema;
|
|
1529
|
-
var TaskTitleSchema =
|
|
1530
|
-
var TaskLabelSchema =
|
|
1989
|
+
var TaskTitleSchema = z8.string().min(1);
|
|
1990
|
+
var TaskLabelSchema = z8.string().min(1);
|
|
1531
1991
|
var CompletedAtSchema = IsoTimestampSchema;
|
|
1532
1992
|
var TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["done", "cancelled"]);
|
|
1533
1993
|
function isTerminalTaskStatus(status) {
|
|
@@ -1561,10 +2021,10 @@ function splitFrontMatter(raw) {
|
|
|
1561
2021
|
return { yamlText, body };
|
|
1562
2022
|
}
|
|
1563
2023
|
async function readTaskFile(paths, taskId) {
|
|
1564
|
-
const filePath =
|
|
2024
|
+
const filePath = join9(paths.tasks, `${taskId}.md`);
|
|
1565
2025
|
let raw;
|
|
1566
2026
|
try {
|
|
1567
|
-
raw = await
|
|
2027
|
+
raw = await readFile5(filePath, "utf8");
|
|
1568
2028
|
} catch (error) {
|
|
1569
2029
|
if (findErrorCode(error, "ENOENT")) {
|
|
1570
2030
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -1594,7 +2054,7 @@ async function readTaskFile(paths, taskId) {
|
|
|
1594
2054
|
}
|
|
1595
2055
|
async function writeTaskFile(paths, taskId, doc, options) {
|
|
1596
2056
|
const validated = TaskSchema.parse(doc.task);
|
|
1597
|
-
const filePath =
|
|
2057
|
+
const filePath = join9(paths.tasks, `${taskId}.md`);
|
|
1598
2058
|
const yamlText = stringifyYaml(validated);
|
|
1599
2059
|
const trimmedBody = doc.body.length === 0 ? "" : `
|
|
1600
2060
|
${doc.body.endsWith("\n") ? doc.body : `${doc.body}
|
|
@@ -1679,7 +2139,7 @@ async function safeUpdateTaskIndex(paths, op) {
|
|
|
1679
2139
|
}
|
|
1680
2140
|
var ARCHIVE_DIR_NAME = "archive";
|
|
1681
2141
|
function archiveTasksDir(paths) {
|
|
1682
|
-
return
|
|
2142
|
+
return join9(paths.tasks, ARCHIVE_DIR_NAME);
|
|
1683
2143
|
}
|
|
1684
2144
|
async function enumerateArchivedTaskIds(paths) {
|
|
1685
2145
|
let entries;
|
|
@@ -1709,10 +2169,10 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
|
|
|
1709
2169
|
throw error;
|
|
1710
2170
|
}
|
|
1711
2171
|
}
|
|
1712
|
-
const archiveFilePath =
|
|
2172
|
+
const archiveFilePath = join9(archiveTasksDir(paths), `${taskId}.md`);
|
|
1713
2173
|
let raw;
|
|
1714
2174
|
try {
|
|
1715
|
-
raw = await
|
|
2175
|
+
raw = await readFile5(archiveFilePath, "utf8");
|
|
1716
2176
|
} catch (error) {
|
|
1717
2177
|
if (findErrorCode(error, "ENOENT")) {
|
|
1718
2178
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -2003,7 +2463,7 @@ async function createTaskAttachLocked(input) {
|
|
|
2003
2463
|
...sessionDoc,
|
|
2004
2464
|
session: { ...sessionDoc.session, task_id: input.taskId }
|
|
2005
2465
|
};
|
|
2006
|
-
await overwriteYamlFile(
|
|
2466
|
+
await overwriteYamlFile(join9(input.paths.sessions, input.sessionId, "session.yaml"), updated);
|
|
2007
2467
|
} catch (error) {
|
|
2008
2468
|
throw new TaskWriteAfterEventError({
|
|
2009
2469
|
taskId: input.taskId,
|
|
@@ -2262,17 +2722,17 @@ function buildUpdatedDoc(input) {
|
|
|
2262
2722
|
return { task: next, body: input.currentDoc.body };
|
|
2263
2723
|
}
|
|
2264
2724
|
async function computeTaskMdSnapshot(paths, taskId) {
|
|
2265
|
-
const filePath =
|
|
2266
|
-
const [stats, raw] = await Promise.all([stat2(filePath),
|
|
2725
|
+
const filePath = join9(paths.tasks, `${taskId}.md`);
|
|
2726
|
+
const [stats, raw] = await Promise.all([stat2(filePath), readFile5(filePath)]);
|
|
2267
2727
|
const hash = createHash("sha256").update(raw).digest("hex");
|
|
2268
2728
|
return { mtimeMs: stats.mtimeMs, hash };
|
|
2269
2729
|
}
|
|
2270
2730
|
async function readTaskFileWithSnapshot(paths, taskId) {
|
|
2271
|
-
const filePath =
|
|
2731
|
+
const filePath = join9(paths.tasks, `${taskId}.md`);
|
|
2272
2732
|
let rawBuffer;
|
|
2273
2733
|
let stats;
|
|
2274
2734
|
try {
|
|
2275
|
-
[rawBuffer, stats] = await Promise.all([
|
|
2735
|
+
[rawBuffer, stats] = await Promise.all([readFile5(filePath), stat2(filePath)]);
|
|
2276
2736
|
} catch (error) {
|
|
2277
2737
|
if (findErrorCode(error, "ENOENT")) {
|
|
2278
2738
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -2630,416 +3090,230 @@ async function refreshTaskLinkedSessionsLocked(paths, manifest, input) {
|
|
|
2630
3090
|
occurredAt: input.occurredAt
|
|
2631
3091
|
});
|
|
2632
3092
|
try {
|
|
2633
|
-
await writeTaskFile(paths, input.taskId, refreshed, { mode: "overwrite" });
|
|
2634
|
-
} catch (error) {
|
|
2635
|
-
throw new TaskWriteAfterEventError({
|
|
2636
|
-
taskId: input.taskId,
|
|
2637
|
-
eventId: anchorEventId,
|
|
2638
|
-
sessionId: adHoc.sessionId,
|
|
2639
|
-
phase: "linkage-refresh",
|
|
2640
|
-
cause: error
|
|
2641
|
-
});
|
|
2642
|
-
}
|
|
2643
|
-
await safeUpdateTaskIndex(paths, {
|
|
2644
|
-
kind: "update",
|
|
2645
|
-
entry: buildTaskIndexEntry(refreshed.task.task)
|
|
2646
|
-
});
|
|
2647
|
-
return {
|
|
2648
|
-
taskId: input.taskId,
|
|
2649
|
-
clean: false,
|
|
2650
|
-
addedLinkedSessions,
|
|
2651
|
-
removedLinkedSessions,
|
|
2652
|
-
finalCount: finalCountWithRefreshSession,
|
|
2653
|
-
refreshSession: {
|
|
2654
|
-
sessionId: adHoc.sessionId,
|
|
2655
|
-
eventId: anchorEventId
|
|
2656
|
-
}
|
|
2657
|
-
};
|
|
2658
|
-
}
|
|
2659
|
-
async function editTask(input) {
|
|
2660
|
-
TaskIdSchema.parse(input.taskId);
|
|
2661
|
-
if (input.title === void 0 && input.newStatus === void 0) {
|
|
2662
|
-
throw new Error("Nothing to edit: provide --title or --status");
|
|
2663
|
-
}
|
|
2664
|
-
if (input.title !== void 0) {
|
|
2665
|
-
TaskTitleSchema.parse(input.title);
|
|
2666
|
-
}
|
|
2667
|
-
let statusUpdated = false;
|
|
2668
|
-
let previousStatus = null;
|
|
2669
|
-
let newStatus = null;
|
|
2670
|
-
let statusChangeSession = null;
|
|
2671
|
-
if (input.newStatus !== void 0) {
|
|
2672
|
-
if (input.manifest === void 0 || input.workingDirectory === void 0) {
|
|
2673
|
-
throw new Error("editTask requires manifest + workingDirectory when newStatus is supplied");
|
|
2674
|
-
}
|
|
2675
|
-
const result = await updateTaskStatusWithEvent({
|
|
2676
|
-
mode: "ad-hoc",
|
|
2677
|
-
paths: input.paths,
|
|
2678
|
-
manifest: input.manifest,
|
|
2679
|
-
occurredAt: input.occurredAt,
|
|
2680
|
-
taskId: input.taskId,
|
|
2681
|
-
newStatus: input.newStatus,
|
|
2682
|
-
workingDirectory: input.workingDirectory
|
|
2683
|
-
});
|
|
2684
|
-
statusUpdated = true;
|
|
2685
|
-
previousStatus = result.previousStatus;
|
|
2686
|
-
newStatus = result.newStatus;
|
|
2687
|
-
statusChangeSession = { sessionId: result.sessionId, eventId: result.eventId };
|
|
2688
|
-
}
|
|
2689
|
-
let titleUpdated = false;
|
|
2690
|
-
if (input.title !== void 0) {
|
|
2691
|
-
const handle = await acquireLock(input.paths, "task", input.taskId);
|
|
2692
|
-
try {
|
|
2693
|
-
const doc = await readTaskFile(input.paths, input.taskId);
|
|
2694
|
-
if (doc.task.task.title !== input.title) {
|
|
2695
|
-
const next = {
|
|
2696
|
-
...doc.task,
|
|
2697
|
-
task: {
|
|
2698
|
-
...doc.task.task,
|
|
2699
|
-
title: input.title,
|
|
2700
|
-
updated_at: input.occurredAt
|
|
2701
|
-
}
|
|
2702
|
-
};
|
|
2703
|
-
await writeTaskFile(
|
|
2704
|
-
input.paths,
|
|
2705
|
-
input.taskId,
|
|
2706
|
-
{ task: next, body: doc.body },
|
|
2707
|
-
{ mode: "overwrite" }
|
|
2708
|
-
);
|
|
2709
|
-
await safeUpdateTaskIndex(input.paths, {
|
|
2710
|
-
kind: "update",
|
|
2711
|
-
entry: buildTaskIndexEntry(next.task)
|
|
2712
|
-
});
|
|
2713
|
-
titleUpdated = true;
|
|
2714
|
-
}
|
|
2715
|
-
} finally {
|
|
2716
|
-
await handle.release();
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
return {
|
|
2720
|
-
taskId: input.taskId,
|
|
2721
|
-
titleUpdated,
|
|
2722
|
-
statusUpdated,
|
|
2723
|
-
previousStatus,
|
|
2724
|
-
newStatus,
|
|
2725
|
-
statusChangeSession
|
|
2726
|
-
};
|
|
2727
|
-
}
|
|
2728
|
-
async function deleteTask(input) {
|
|
2729
|
-
TaskIdSchema.parse(input.taskId);
|
|
2730
|
-
const handle = await acquireLock(input.paths, "task", input.taskId);
|
|
2731
|
-
try {
|
|
2732
|
-
return await deleteTaskLocked(input);
|
|
2733
|
-
} finally {
|
|
2734
|
-
await handle.release();
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
async function deleteTaskLocked(input) {
|
|
2738
|
-
const doc = await readTaskFile(input.paths, input.taskId);
|
|
2739
|
-
const title = doc.task.task.title;
|
|
2740
|
-
const adHoc = await createAdHocSessionWithEvent({
|
|
2741
|
-
paths: input.paths,
|
|
2742
|
-
manifest: input.manifest,
|
|
2743
|
-
label: buildAdHocDeleteLabel(title),
|
|
2744
|
-
occurredAt: input.occurredAt,
|
|
2745
|
-
sessionSource: "human",
|
|
2746
|
-
workingDirectory: input.workingDirectory,
|
|
2747
|
-
invocation: {
|
|
2748
|
-
command: "basou task delete",
|
|
2749
|
-
args: [input.taskId, "--yes"]
|
|
2750
|
-
},
|
|
2751
|
-
targetEventBuilders: [
|
|
2752
|
-
(sessionId, eventId2) => buildTaskDeletedEvent({
|
|
2753
|
-
eventId: eventId2,
|
|
2754
|
-
sessionId,
|
|
2755
|
-
taskId: input.taskId,
|
|
2756
|
-
title,
|
|
2757
|
-
occurredAt: input.occurredAt
|
|
2758
|
-
})
|
|
2759
|
-
]
|
|
2760
|
-
});
|
|
2761
|
-
const eventId = adHoc.targetEventIds[0];
|
|
2762
|
-
try {
|
|
2763
|
-
await unlink3(join10(input.paths.tasks, `${input.taskId}.md`));
|
|
2764
|
-
} catch (error) {
|
|
2765
|
-
throw new TaskWriteAfterEventError({
|
|
2766
|
-
taskId: input.taskId,
|
|
2767
|
-
eventId,
|
|
2768
|
-
sessionId: adHoc.sessionId,
|
|
2769
|
-
phase: "delete",
|
|
2770
|
-
cause: error
|
|
2771
|
-
});
|
|
2772
|
-
}
|
|
2773
|
-
await safeUpdateTaskIndex(input.paths, { kind: "remove", id: input.taskId });
|
|
2774
|
-
return {
|
|
2775
|
-
taskId: input.taskId,
|
|
2776
|
-
title,
|
|
2777
|
-
sessionId: adHoc.sessionId,
|
|
2778
|
-
eventId
|
|
2779
|
-
};
|
|
2780
|
-
}
|
|
2781
|
-
async function archiveTask(input) {
|
|
2782
|
-
TaskIdSchema.parse(input.taskId);
|
|
2783
|
-
const handle = await acquireLock(input.paths, "task", input.taskId);
|
|
2784
|
-
try {
|
|
2785
|
-
return await archiveTaskLocked(input);
|
|
2786
|
-
} finally {
|
|
2787
|
-
await handle.release();
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
async function archiveTaskLocked(input) {
|
|
2791
|
-
const doc = await readTaskFile(input.paths, input.taskId);
|
|
2792
|
-
const title = doc.task.task.title;
|
|
2793
|
-
const adHoc = await createAdHocSessionWithEvent({
|
|
2794
|
-
paths: input.paths,
|
|
2795
|
-
manifest: input.manifest,
|
|
2796
|
-
label: buildAdHocArchiveLabel(title),
|
|
2797
|
-
occurredAt: input.occurredAt,
|
|
2798
|
-
sessionSource: "human",
|
|
2799
|
-
workingDirectory: input.workingDirectory,
|
|
2800
|
-
invocation: {
|
|
2801
|
-
command: "basou task archive",
|
|
2802
|
-
args: [input.taskId, "--yes"]
|
|
2803
|
-
},
|
|
2804
|
-
taskId: input.taskId,
|
|
2805
|
-
targetEventBuilders: [
|
|
2806
|
-
(sessionId, eventId2) => buildTaskArchivedEvent({
|
|
2807
|
-
eventId: eventId2,
|
|
2808
|
-
sessionId,
|
|
2809
|
-
taskId: input.taskId,
|
|
2810
|
-
title,
|
|
2811
|
-
occurredAt: input.occurredAt
|
|
2812
|
-
})
|
|
2813
|
-
]
|
|
2814
|
-
});
|
|
2815
|
-
const eventId = adHoc.targetEventIds[0];
|
|
2816
|
-
try {
|
|
2817
|
-
const linked = doc.task.task.linked_sessions;
|
|
2818
|
-
const merged = linked.includes(adHoc.sessionId) ? linked : [...linked, adHoc.sessionId];
|
|
2819
|
-
const next = {
|
|
2820
|
-
...doc.task,
|
|
2821
|
-
task: {
|
|
2822
|
-
...doc.task.task,
|
|
2823
|
-
updated_at: input.occurredAt,
|
|
2824
|
-
linked_sessions: merged
|
|
2825
|
-
}
|
|
2826
|
-
};
|
|
2827
|
-
await writeTaskFile(
|
|
2828
|
-
input.paths,
|
|
2829
|
-
input.taskId,
|
|
2830
|
-
{ task: next, body: doc.body },
|
|
2831
|
-
{ mode: "overwrite" }
|
|
2832
|
-
);
|
|
2833
|
-
await mkdir3(archiveTasksDir(input.paths), { recursive: true });
|
|
2834
|
-
await rename2(
|
|
2835
|
-
join10(input.paths.tasks, `${input.taskId}.md`),
|
|
2836
|
-
join10(archiveTasksDir(input.paths), `${input.taskId}.md`)
|
|
2837
|
-
);
|
|
3093
|
+
await writeTaskFile(paths, input.taskId, refreshed, { mode: "overwrite" });
|
|
2838
3094
|
} catch (error) {
|
|
2839
3095
|
throw new TaskWriteAfterEventError({
|
|
2840
3096
|
taskId: input.taskId,
|
|
2841
|
-
eventId,
|
|
3097
|
+
eventId: anchorEventId,
|
|
2842
3098
|
sessionId: adHoc.sessionId,
|
|
2843
|
-
phase: "
|
|
3099
|
+
phase: "linkage-refresh",
|
|
2844
3100
|
cause: error
|
|
2845
3101
|
});
|
|
2846
3102
|
}
|
|
2847
|
-
await safeUpdateTaskIndex(
|
|
3103
|
+
await safeUpdateTaskIndex(paths, {
|
|
3104
|
+
kind: "update",
|
|
3105
|
+
entry: buildTaskIndexEntry(refreshed.task.task)
|
|
3106
|
+
});
|
|
2848
3107
|
return {
|
|
2849
3108
|
taskId: input.taskId,
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
2858
|
-
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
2859
|
-
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
2860
|
-
}
|
|
2861
|
-
const effectiveSessionTaskId = options.taskIdOverride ?? payload.session.task_id ?? null;
|
|
2862
|
-
await assertImportedTaskReferencesAreReachable(paths, payload.events, effectiveSessionTaskId);
|
|
2863
|
-
const newSessionId = prefixedUlid("ses");
|
|
2864
|
-
const rewrittenEvents = rewriteEvents(payload.events, newSessionId);
|
|
2865
|
-
assertChronologicalOrder(rewrittenEvents);
|
|
2866
|
-
const { record: sessionRecord, pathSanitizeReport } = buildSessionRecord(
|
|
2867
|
-
payload.session,
|
|
2868
|
-
manifest,
|
|
2869
|
-
newSessionId,
|
|
2870
|
-
options
|
|
2871
|
-
);
|
|
2872
|
-
if (options.dryRun === true) {
|
|
2873
|
-
return {
|
|
2874
|
-
sessionId: newSessionId,
|
|
2875
|
-
eventCount: rewrittenEvents.length,
|
|
2876
|
-
finalStatus: "imported",
|
|
2877
|
-
finalSourceKind: sessionRecord.session.source.kind,
|
|
2878
|
-
pathSanitizeReport
|
|
2879
|
-
};
|
|
2880
|
-
}
|
|
2881
|
-
const sessionDir = join11(paths.sessions, newSessionId);
|
|
2882
|
-
try {
|
|
2883
|
-
await mkdir4(sessionDir, { recursive: true });
|
|
2884
|
-
} catch (error) {
|
|
2885
|
-
throw new Error("Failed to create session directory", { cause: error });
|
|
2886
|
-
}
|
|
2887
|
-
try {
|
|
2888
|
-
await writeEventsBulk(sessionDir, rewrittenEvents);
|
|
2889
|
-
} catch (error) {
|
|
2890
|
-
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
2891
|
-
throw error;
|
|
2892
|
-
}
|
|
2893
|
-
try {
|
|
2894
|
-
const sessionYamlPath = join11(sessionDir, "session.yaml");
|
|
2895
|
-
await linkYamlFile(sessionYamlPath, sessionRecord);
|
|
2896
|
-
} catch (error) {
|
|
2897
|
-
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
2898
|
-
if (findErrorCode(error, "EEXIST")) {
|
|
2899
|
-
throw new Error("Session directory collision (retry the command)", {
|
|
2900
|
-
cause: error
|
|
2901
|
-
});
|
|
3109
|
+
clean: false,
|
|
3110
|
+
addedLinkedSessions,
|
|
3111
|
+
removedLinkedSessions,
|
|
3112
|
+
finalCount: finalCountWithRefreshSession,
|
|
3113
|
+
refreshSession: {
|
|
3114
|
+
sessionId: adHoc.sessionId,
|
|
3115
|
+
eventId: anchorEventId
|
|
2902
3116
|
}
|
|
2903
|
-
throw error;
|
|
2904
|
-
}
|
|
2905
|
-
return {
|
|
2906
|
-
sessionId: newSessionId,
|
|
2907
|
-
eventCount: rewrittenEvents.length,
|
|
2908
|
-
finalStatus: "imported",
|
|
2909
|
-
finalSourceKind: sessionRecord.session.source.kind,
|
|
2910
|
-
pathSanitizeReport
|
|
2911
3117
|
};
|
|
2912
3118
|
}
|
|
2913
|
-
async function
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
taskIdsToCheck.add(ev.task_id);
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
if (effectiveSessionTaskId !== null) {
|
|
2921
|
-
taskIdsToCheck.add(effectiveSessionTaskId);
|
|
3119
|
+
async function editTask(input) {
|
|
3120
|
+
TaskIdSchema.parse(input.taskId);
|
|
3121
|
+
if (input.title === void 0 && input.newStatus === void 0) {
|
|
3122
|
+
throw new Error("Nothing to edit: provide --title or --status");
|
|
2922
3123
|
}
|
|
2923
|
-
if (
|
|
2924
|
-
|
|
3124
|
+
if (input.title !== void 0) {
|
|
3125
|
+
TaskTitleSchema.parse(input.title);
|
|
2925
3126
|
}
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3127
|
+
let statusUpdated = false;
|
|
3128
|
+
let previousStatus = null;
|
|
3129
|
+
let newStatus = null;
|
|
3130
|
+
let statusChangeSession = null;
|
|
3131
|
+
if (input.newStatus !== void 0) {
|
|
3132
|
+
if (input.manifest === void 0 || input.workingDirectory === void 0) {
|
|
3133
|
+
throw new Error("editTask requires manifest + workingDirectory when newStatus is supplied");
|
|
2930
3134
|
}
|
|
3135
|
+
const result = await updateTaskStatusWithEvent({
|
|
3136
|
+
mode: "ad-hoc",
|
|
3137
|
+
paths: input.paths,
|
|
3138
|
+
manifest: input.manifest,
|
|
3139
|
+
occurredAt: input.occurredAt,
|
|
3140
|
+
taskId: input.taskId,
|
|
3141
|
+
newStatus: input.newStatus,
|
|
3142
|
+
workingDirectory: input.workingDirectory
|
|
3143
|
+
});
|
|
3144
|
+
statusUpdated = true;
|
|
3145
|
+
previousStatus = result.previousStatus;
|
|
3146
|
+
newStatus = result.newStatus;
|
|
3147
|
+
statusChangeSession = { sessionId: result.sessionId, eventId: result.eventId };
|
|
2931
3148
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3149
|
+
let titleUpdated = false;
|
|
3150
|
+
if (input.title !== void 0) {
|
|
3151
|
+
const handle = await acquireLock(input.paths, "task", input.taskId);
|
|
3152
|
+
try {
|
|
3153
|
+
const doc = await readTaskFile(input.paths, input.taskId);
|
|
3154
|
+
if (doc.task.task.title !== input.title) {
|
|
3155
|
+
const next = {
|
|
3156
|
+
...doc.task,
|
|
3157
|
+
task: {
|
|
3158
|
+
...doc.task.task,
|
|
3159
|
+
title: input.title,
|
|
3160
|
+
updated_at: input.occurredAt
|
|
3161
|
+
}
|
|
3162
|
+
};
|
|
3163
|
+
await writeTaskFile(
|
|
3164
|
+
input.paths,
|
|
3165
|
+
input.taskId,
|
|
3166
|
+
{ task: next, body: doc.body },
|
|
3167
|
+
{ mode: "overwrite" }
|
|
3168
|
+
);
|
|
3169
|
+
await safeUpdateTaskIndex(input.paths, {
|
|
3170
|
+
kind: "update",
|
|
3171
|
+
entry: buildTaskIndexEntry(next.task)
|
|
3172
|
+
});
|
|
3173
|
+
titleUpdated = true;
|
|
3174
|
+
}
|
|
3175
|
+
} finally {
|
|
3176
|
+
await handle.release();
|
|
2949
3177
|
}
|
|
2950
3178
|
}
|
|
2951
|
-
}
|
|
2952
|
-
function buildSessionRecord(input, manifest, newSessionId, options) {
|
|
2953
|
-
const home = homedir2();
|
|
2954
|
-
const workingDirectoryRaw = input.working_directory;
|
|
2955
|
-
const workingDirectorySanitized = sanitizeWorkingDirectory(workingDirectoryRaw, {
|
|
2956
|
-
homedir: home
|
|
2957
|
-
});
|
|
2958
|
-
const relatedSanitized = sanitizeRelatedFiles(input.related_files, {
|
|
2959
|
-
workingDirectory: workingDirectoryRaw,
|
|
2960
|
-
homedir: home
|
|
2961
|
-
});
|
|
2962
|
-
const inner = {
|
|
2963
|
-
id: newSessionId,
|
|
2964
|
-
...options.labelOverride !== void 0 || input.label !== void 0 ? { label: options.labelOverride ?? input.label } : {},
|
|
2965
|
-
task_id: options.taskIdOverride !== void 0 ? options.taskIdOverride : input.task_id ?? null,
|
|
2966
|
-
workspace_id: manifest.workspace.id,
|
|
2967
|
-
source: input.source,
|
|
2968
|
-
started_at: input.started_at,
|
|
2969
|
-
...input.ended_at !== void 0 ? { ended_at: input.ended_at } : {},
|
|
2970
|
-
status: "imported",
|
|
2971
|
-
working_directory: workingDirectorySanitized,
|
|
2972
|
-
invocation: input.invocation,
|
|
2973
|
-
related_files: relatedSanitized.sanitized,
|
|
2974
|
-
events_log: "events.jsonl",
|
|
2975
|
-
summary: input.summary ?? null
|
|
2976
|
-
};
|
|
2977
3179
|
return {
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3180
|
+
taskId: input.taskId,
|
|
3181
|
+
titleUpdated,
|
|
3182
|
+
statusUpdated,
|
|
3183
|
+
previousStatus,
|
|
3184
|
+
newStatus,
|
|
3185
|
+
statusChangeSession
|
|
2983
3186
|
};
|
|
2984
3187
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
}
|
|
2993
|
-
var KIND_CONFIG = {
|
|
2994
|
-
session: {
|
|
2995
|
-
prefix: "ses_",
|
|
2996
|
-
noun: "session",
|
|
2997
|
-
nounPlural: "sessions",
|
|
2998
|
-
capNoun: "Session",
|
|
2999
|
-
enumerate: enumerateSessionDirs
|
|
3000
|
-
},
|
|
3001
|
-
task: {
|
|
3002
|
-
prefix: "task_",
|
|
3003
|
-
noun: "task",
|
|
3004
|
-
nounPlural: "tasks",
|
|
3005
|
-
capNoun: "Task",
|
|
3006
|
-
enumerate: enumerateTaskIds
|
|
3007
|
-
}
|
|
3008
|
-
};
|
|
3009
|
-
async function resolveIdInternal(paths, input, kind, options = {}) {
|
|
3010
|
-
const cfg = KIND_CONFIG[kind];
|
|
3011
|
-
const trimmed = input.trim();
|
|
3012
|
-
if (trimmed.length === 0) {
|
|
3013
|
-
throw new Error(`${cfg.capNoun} id is empty`);
|
|
3014
|
-
}
|
|
3015
|
-
const normalized = trimmed.startsWith(cfg.prefix) ? trimmed : `${cfg.prefix}${trimmed}`;
|
|
3016
|
-
if (normalized.length <= cfg.prefix.length) {
|
|
3017
|
-
throw new Error(`${cfg.capNoun} not found: ${input}`);
|
|
3018
|
-
}
|
|
3019
|
-
const primary = await cfg.enumerate(paths);
|
|
3020
|
-
const merged = new Set(primary);
|
|
3021
|
-
if (kind === "task" && options.includeArchived === true) {
|
|
3022
|
-
for (const id of await enumerateArchivedTaskIds(paths)) {
|
|
3023
|
-
merged.add(id);
|
|
3024
|
-
}
|
|
3188
|
+
async function deleteTask(input) {
|
|
3189
|
+
TaskIdSchema.parse(input.taskId);
|
|
3190
|
+
const handle = await acquireLock(input.paths, "task", input.taskId);
|
|
3191
|
+
try {
|
|
3192
|
+
return await deleteTaskLocked(input);
|
|
3193
|
+
} finally {
|
|
3194
|
+
await handle.release();
|
|
3025
3195
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3196
|
+
}
|
|
3197
|
+
async function deleteTaskLocked(input) {
|
|
3198
|
+
const doc = await readTaskFile(input.paths, input.taskId);
|
|
3199
|
+
const title = doc.task.task.title;
|
|
3200
|
+
const adHoc = await createAdHocSessionWithEvent({
|
|
3201
|
+
paths: input.paths,
|
|
3202
|
+
manifest: input.manifest,
|
|
3203
|
+
label: buildAdHocDeleteLabel(title),
|
|
3204
|
+
occurredAt: input.occurredAt,
|
|
3205
|
+
sessionSource: "human",
|
|
3206
|
+
workingDirectory: input.workingDirectory,
|
|
3207
|
+
invocation: {
|
|
3208
|
+
command: "basou task delete",
|
|
3209
|
+
args: [input.taskId, "--yes"]
|
|
3210
|
+
},
|
|
3211
|
+
targetEventBuilders: [
|
|
3212
|
+
(sessionId, eventId2) => buildTaskDeletedEvent({
|
|
3213
|
+
eventId: eventId2,
|
|
3214
|
+
sessionId,
|
|
3215
|
+
taskId: input.taskId,
|
|
3216
|
+
title,
|
|
3217
|
+
occurredAt: input.occurredAt
|
|
3218
|
+
})
|
|
3219
|
+
]
|
|
3220
|
+
});
|
|
3221
|
+
const eventId = adHoc.targetEventIds[0];
|
|
3222
|
+
try {
|
|
3223
|
+
await unlink3(join9(input.paths.tasks, `${input.taskId}.md`));
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
throw new TaskWriteAfterEventError({
|
|
3226
|
+
taskId: input.taskId,
|
|
3227
|
+
eventId,
|
|
3228
|
+
sessionId: adHoc.sessionId,
|
|
3229
|
+
phase: "delete",
|
|
3230
|
+
cause: error
|
|
3231
|
+
});
|
|
3028
3232
|
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3233
|
+
await safeUpdateTaskIndex(input.paths, { kind: "remove", id: input.taskId });
|
|
3234
|
+
return {
|
|
3235
|
+
taskId: input.taskId,
|
|
3236
|
+
title,
|
|
3237
|
+
sessionId: adHoc.sessionId,
|
|
3238
|
+
eventId
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
async function archiveTask(input) {
|
|
3242
|
+
TaskIdSchema.parse(input.taskId);
|
|
3243
|
+
const handle = await acquireLock(input.paths, "task", input.taskId);
|
|
3244
|
+
try {
|
|
3245
|
+
return await archiveTaskLocked(input);
|
|
3246
|
+
} finally {
|
|
3247
|
+
await handle.release();
|
|
3032
3248
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3249
|
+
}
|
|
3250
|
+
async function archiveTaskLocked(input) {
|
|
3251
|
+
const doc = await readTaskFile(input.paths, input.taskId);
|
|
3252
|
+
const title = doc.task.task.title;
|
|
3253
|
+
const adHoc = await createAdHocSessionWithEvent({
|
|
3254
|
+
paths: input.paths,
|
|
3255
|
+
manifest: input.manifest,
|
|
3256
|
+
label: buildAdHocArchiveLabel(title),
|
|
3257
|
+
occurredAt: input.occurredAt,
|
|
3258
|
+
sessionSource: "human",
|
|
3259
|
+
workingDirectory: input.workingDirectory,
|
|
3260
|
+
invocation: {
|
|
3261
|
+
command: "basou task archive",
|
|
3262
|
+
args: [input.taskId, "--yes"]
|
|
3263
|
+
},
|
|
3264
|
+
taskId: input.taskId,
|
|
3265
|
+
targetEventBuilders: [
|
|
3266
|
+
(sessionId, eventId2) => buildTaskArchivedEvent({
|
|
3267
|
+
eventId: eventId2,
|
|
3268
|
+
sessionId,
|
|
3269
|
+
taskId: input.taskId,
|
|
3270
|
+
title,
|
|
3271
|
+
occurredAt: input.occurredAt
|
|
3272
|
+
})
|
|
3273
|
+
]
|
|
3274
|
+
});
|
|
3275
|
+
const eventId = adHoc.targetEventIds[0];
|
|
3276
|
+
try {
|
|
3277
|
+
const linked = doc.task.task.linked_sessions;
|
|
3278
|
+
const merged = linked.includes(adHoc.sessionId) ? linked : [...linked, adHoc.sessionId];
|
|
3279
|
+
const next = {
|
|
3280
|
+
...doc.task,
|
|
3281
|
+
task: {
|
|
3282
|
+
...doc.task.task,
|
|
3283
|
+
updated_at: input.occurredAt,
|
|
3284
|
+
linked_sessions: merged
|
|
3285
|
+
}
|
|
3286
|
+
};
|
|
3287
|
+
await writeTaskFile(
|
|
3288
|
+
input.paths,
|
|
3289
|
+
input.taskId,
|
|
3290
|
+
{ task: next, body: doc.body },
|
|
3291
|
+
{ mode: "overwrite" }
|
|
3036
3292
|
);
|
|
3293
|
+
await mkdir2(archiveTasksDir(input.paths), { recursive: true });
|
|
3294
|
+
await rename2(
|
|
3295
|
+
join9(input.paths.tasks, `${input.taskId}.md`),
|
|
3296
|
+
join9(archiveTasksDir(input.paths), `${input.taskId}.md`)
|
|
3297
|
+
);
|
|
3298
|
+
} catch (error) {
|
|
3299
|
+
throw new TaskWriteAfterEventError({
|
|
3300
|
+
taskId: input.taskId,
|
|
3301
|
+
eventId,
|
|
3302
|
+
sessionId: adHoc.sessionId,
|
|
3303
|
+
phase: "archive",
|
|
3304
|
+
cause: error
|
|
3305
|
+
});
|
|
3037
3306
|
}
|
|
3038
|
-
|
|
3307
|
+
await safeUpdateTaskIndex(input.paths, { kind: "remove", id: input.taskId });
|
|
3308
|
+
return {
|
|
3309
|
+
taskId: input.taskId,
|
|
3310
|
+
title,
|
|
3311
|
+
sessionId: adHoc.sessionId,
|
|
3312
|
+
eventId
|
|
3313
|
+
};
|
|
3039
3314
|
}
|
|
3040
3315
|
|
|
3041
3316
|
// src/handoff/handoff-renderer.ts
|
|
3042
|
-
import { join as join12 } from "path";
|
|
3043
3317
|
async function renderHandoff(input) {
|
|
3044
3318
|
const limit = input.relatedFilesLimit ?? 20;
|
|
3045
3319
|
const now = new Date(input.nowIso);
|
|
@@ -3055,7 +3329,7 @@ async function renderHandoff(input) {
|
|
|
3055
3329
|
const tasksCreated = [];
|
|
3056
3330
|
const tasksStatusChanged = [];
|
|
3057
3331
|
for (const entry of entries) {
|
|
3058
|
-
const sessionDir =
|
|
3332
|
+
const sessionDir = join10(input.paths.sessions, entry.sessionId);
|
|
3059
3333
|
try {
|
|
3060
3334
|
for await (const ev of replayEvents(sessionDir, {
|
|
3061
3335
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -3122,18 +3396,14 @@ async function renderHandoff(input) {
|
|
|
3122
3396
|
const latestSession = [...liveEntries].sort(
|
|
3123
3397
|
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
3124
3398
|
)[0];
|
|
3125
|
-
const
|
|
3126
|
-
|
|
3127
|
-
if (e.session.session.source.kind === "import") continue;
|
|
3128
|
-
for (const f of e.session.session.related_files) allFiles.add(f);
|
|
3129
|
-
}
|
|
3130
|
-
const sortedFiles = [...allFiles].sort();
|
|
3399
|
+
const latestFiles = latestSession?.session.session.related_files ?? [];
|
|
3400
|
+
const sortedFiles = [...new Set(latestFiles)].sort();
|
|
3131
3401
|
const displayedFiles = sortedFiles.slice(0, limit);
|
|
3132
3402
|
const overflow = Math.max(0, sortedFiles.length - limit);
|
|
3133
3403
|
const suspectCount = entries.filter((e) => e.suspect).length;
|
|
3134
3404
|
const firstEntry = entries[0];
|
|
3135
3405
|
const lastEntry = entries[entries.length - 1];
|
|
3136
|
-
const sessionRange = firstEntry !== void 0 && lastEntry !== void 0 ? `${firstEntry.sessionId}..${lastEntry.sessionId}` : "";
|
|
3406
|
+
const sessionRange = firstEntry !== void 0 && lastEntry !== void 0 ? `${shortIdWithPrefix(firstEntry.sessionId)}..${shortIdWithPrefix(lastEntry.sessionId)}` : "";
|
|
3137
3407
|
const body = formatHandoffBody({
|
|
3138
3408
|
nowIso: input.nowIso,
|
|
3139
3409
|
sessionRange,
|
|
@@ -3173,18 +3443,23 @@ function formatHandoffBody(args) {
|
|
|
3173
3443
|
lines.push("## \u73FE\u5728\u306E\u72B6\u614B");
|
|
3174
3444
|
lines.push("");
|
|
3175
3445
|
if (args.latestSession !== void 0) {
|
|
3176
|
-
const sid = args.latestSession.sessionId;
|
|
3177
3446
|
const status = args.latestSession.session.session.status;
|
|
3178
|
-
|
|
3447
|
+
const label = args.latestSession.session.session.label;
|
|
3448
|
+
const shortId = shortIdWithPrefix(args.latestSession.sessionId);
|
|
3449
|
+
if (label !== void 0 && label !== "") {
|
|
3450
|
+
lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId}]`);
|
|
3451
|
+
} else {
|
|
3452
|
+
lines.push(`- \u6700\u7D42 session: ${shortId} (${status})`);
|
|
3453
|
+
}
|
|
3179
3454
|
} else {
|
|
3180
3455
|
lines.push("- \u6700\u7D42 session: (no live sessions)");
|
|
3181
3456
|
}
|
|
3182
3457
|
if (args.latestActivityRecord !== void 0) {
|
|
3183
3458
|
const statusLabel = args.latestTaskDoc !== void 0 ? args.latestTaskDoc.task.task.status : "status unknown \u2014 task.md missing or invalid";
|
|
3184
3459
|
const linkedCount = args.latestTaskDoc?.task.task.linked_sessions?.length;
|
|
3185
|
-
const linkedSuffix = linkedCount !== void 0 && linkedCount > 1 ?
|
|
3460
|
+
const linkedSuffix = linkedCount !== void 0 && linkedCount > 1 ? `, linked_sessions: ${linkedCount}` : "";
|
|
3186
3461
|
lines.push(
|
|
3187
|
-
`- \u6700\u7D42 task: ${args.latestActivityRecord.
|
|
3462
|
+
`- \u6700\u7D42 task: ${args.latestActivityRecord.title} (${statusLabel}${linkedSuffix}) [${shortIdWithPrefix(args.latestActivityRecord.taskId)}]`
|
|
3188
3463
|
);
|
|
3189
3464
|
} else {
|
|
3190
3465
|
lines.push("- \u6700\u7D42 task: (no tasks recorded yet)");
|
|
@@ -3205,7 +3480,7 @@ function formatHandoffBody(args) {
|
|
|
3205
3480
|
lines.push("(no decisions recorded yet)");
|
|
3206
3481
|
} else {
|
|
3207
3482
|
const last = args.decisions[args.decisions.length - 1];
|
|
3208
|
-
lines.push(`- ${last.
|
|
3483
|
+
lines.push(`- ${last.title} [${shortIdWithPrefix(last.decisionId)}]`);
|
|
3209
3484
|
lines.push("");
|
|
3210
3485
|
lines.push(`(${args.decisions.length} decisions total \u2014 see decisions.md)`);
|
|
3211
3486
|
}
|
|
@@ -3233,7 +3508,9 @@ function formatHandoffBody(args) {
|
|
|
3233
3508
|
lines.push("(no pending tasks)");
|
|
3234
3509
|
} else {
|
|
3235
3510
|
for (const t of args.pendingTasks) {
|
|
3236
|
-
lines.push(
|
|
3511
|
+
lines.push(
|
|
3512
|
+
`- ${t.task.task.title} (${t.task.task.status}) [${shortIdWithPrefix(t.task.task.id)}]`
|
|
3513
|
+
);
|
|
3237
3514
|
}
|
|
3238
3515
|
}
|
|
3239
3516
|
lines.push("");
|
|
@@ -3287,144 +3564,119 @@ function formatHandoffBody(args) {
|
|
|
3287
3564
|
"initialized",
|
|
3288
3565
|
"imported"
|
|
3289
3566
|
];
|
|
3290
|
-
const breakdown = orderedStatuses.filter((s) => (statusCounts.get(s) ?? 0) > 0).map((s) => `${s} ${statusCounts.get(s)}`).join(", ");
|
|
3291
|
-
const sessionsLine = breakdown !== "" ? `Sessions: ${args.sessionCount} (${breakdown}). Tasks: ${args.totalTaskCount}.` : `Sessions: ${args.sessionCount}. Tasks: ${args.totalTaskCount}.`;
|
|
3292
|
-
lines.push(sessionsLine);
|
|
3293
|
-
return lines.join("\n");
|
|
3294
|
-
}
|
|
3295
|
-
function suspectLabel(reason) {
|
|
3296
|
-
if (reason === "events_say_ended_but_yaml_running") return " \u26A0 ended (yaml stale)";
|
|
3297
|
-
if (reason === "running_no_end_event") return " \u26A0 no end event";
|
|
3298
|
-
return "";
|
|
3299
|
-
}
|
|
3300
|
-
function shortHandoffId(sessionId) {
|
|
3301
|
-
const SES = "ses_";
|
|
3302
|
-
if (sessionId.startsWith(SES)) return sessionId.slice(SES.length, SES.length + 10);
|
|
3303
|
-
return sessionId.slice(0, 10);
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
// src/decisions/decisions-renderer.ts
|
|
3307
|
-
import { lstat as lstat4 } from "fs/promises";
|
|
3308
|
-
import { dirname, join as join13, resolve } from "path";
|
|
3309
|
-
async function renderDecisions(input) {
|
|
3310
|
-
const now = new Date(input.nowIso);
|
|
3311
|
-
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
3312
|
-
const wrappedSkip = (sid, reason) => {
|
|
3313
|
-
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
3314
|
-
input.onSessionSkip?.(sid, reason);
|
|
3315
|
-
};
|
|
3316
|
-
const loadOpts = { now, onSkip: wrappedSkip };
|
|
3317
|
-
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
3318
|
-
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
3319
|
-
const decisions = [];
|
|
3320
|
-
const knownEventIds = /* @__PURE__ */ new Set();
|
|
3321
|
-
for (const entry of entries) {
|
|
3322
|
-
const sessionDir = join13(input.paths.sessions, entry.sessionId);
|
|
3323
|
-
try {
|
|
3324
|
-
for await (const ev of replayEvents(sessionDir, {
|
|
3325
|
-
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
3326
|
-
})) {
|
|
3327
|
-
knownEventIds.add(ev.id);
|
|
3328
|
-
if (ev.type === "decision_recorded") {
|
|
3329
|
-
decisions.push({
|
|
3330
|
-
decisionId: ev.decision_id,
|
|
3331
|
-
title: ev.title,
|
|
3332
|
-
occurredAt: ev.occurred_at,
|
|
3333
|
-
sessionId: entry.sessionId,
|
|
3334
|
-
rationale: ev.rationale,
|
|
3335
|
-
alternatives: ev.alternatives,
|
|
3336
|
-
rejectedReason: ev.rejected_reason,
|
|
3337
|
-
linkedEvents: ev.linked_events,
|
|
3338
|
-
linkedFiles: ev.linked_files
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
|
-
} catch {
|
|
3343
|
-
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
3344
|
-
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
3345
|
-
}
|
|
3346
|
-
}
|
|
3347
|
-
}
|
|
3348
|
-
decisions.sort((a, b) => {
|
|
3349
|
-
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
3350
|
-
return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
|
|
3351
|
-
});
|
|
3352
|
-
const repoRoot = dirname(input.paths.root);
|
|
3353
|
-
const fileExistenceCache = /* @__PURE__ */ new Map();
|
|
3354
|
-
async function fileExists(relPath) {
|
|
3355
|
-
const cached = fileExistenceCache.get(relPath);
|
|
3356
|
-
if (cached !== void 0) return cached;
|
|
3357
|
-
const abs = resolve(repoRoot, relPath);
|
|
3358
|
-
let exists;
|
|
3359
|
-
try {
|
|
3360
|
-
await lstat4(abs);
|
|
3361
|
-
exists = true;
|
|
3362
|
-
} catch {
|
|
3363
|
-
exists = false;
|
|
3364
|
-
}
|
|
3365
|
-
fileExistenceCache.set(relPath, exists);
|
|
3366
|
-
return exists;
|
|
3367
|
-
}
|
|
3368
|
-
const body = await formatDecisionsBody({
|
|
3369
|
-
nowIso: input.nowIso,
|
|
3370
|
-
decisions,
|
|
3371
|
-
knownEventIds,
|
|
3372
|
-
fileExists
|
|
3373
|
-
});
|
|
3374
|
-
return { body, decisionCount: decisions.length };
|
|
3375
|
-
}
|
|
3376
|
-
async function formatDecisionsBody(args) {
|
|
3377
|
-
const lines = [];
|
|
3378
|
-
lines.push("# Decisions");
|
|
3379
|
-
lines.push("");
|
|
3380
|
-
lines.push(`> Generated at ${args.nowIso}`);
|
|
3381
|
-
lines.push("");
|
|
3382
|
-
if (args.decisions.length === 0) {
|
|
3383
|
-
lines.push("(no decisions recorded yet)");
|
|
3384
|
-
return lines.join("\n");
|
|
3385
|
-
}
|
|
3386
|
-
for (const d of args.decisions) {
|
|
3387
|
-
lines.push(`## ${d.decisionId}: ${d.title}`);
|
|
3388
|
-
lines.push("");
|
|
3389
|
-
const occurredDate = d.occurredAt.slice(0, 10);
|
|
3390
|
-
lines.push(`- \u6C7A\u5B9A\u65E5: ${occurredDate}`);
|
|
3391
|
-
lines.push(`- session: ${shortDecisionSessionId(d.sessionId)}`);
|
|
3392
|
-
lines.push(`- \u5224\u65AD: ${d.title}`);
|
|
3393
|
-
if (typeof d.rationale === "string" && d.rationale.length > 0) {
|
|
3394
|
-
lines.push(`- rationale: ${d.rationale}`);
|
|
3395
|
-
}
|
|
3396
|
-
if (d.alternatives !== void 0 && d.alternatives.length > 0) {
|
|
3397
|
-
lines.push(`- alternatives: ${d.alternatives.join(", ")}`);
|
|
3398
|
-
}
|
|
3399
|
-
if (typeof d.rejectedReason === "string" && d.rejectedReason.length > 0) {
|
|
3400
|
-
lines.push(`- rejected_reason: ${d.rejectedReason}`);
|
|
3401
|
-
}
|
|
3402
|
-
if (d.linkedEvents !== void 0 && d.linkedEvents.length > 0) {
|
|
3403
|
-
const parts = d.linkedEvents.map(
|
|
3404
|
-
(eid) => args.knownEventIds.has(eid) ? eid : `${eid} (missing)`
|
|
3405
|
-
);
|
|
3406
|
-
lines.push(`- linked_events: ${parts.join(", ")}`);
|
|
3407
|
-
}
|
|
3408
|
-
if (d.linkedFiles !== void 0 && d.linkedFiles.length > 0) {
|
|
3409
|
-
const parts = await Promise.all(
|
|
3410
|
-
d.linkedFiles.map(
|
|
3411
|
-
async (path2) => await args.fileExists(path2) ? path2 : `${path2} (missing)`
|
|
3412
|
-
)
|
|
3413
|
-
);
|
|
3414
|
-
lines.push(`- linked_files: ${parts.join(", ")}`);
|
|
3415
|
-
}
|
|
3416
|
-
lines.push("");
|
|
3417
|
-
}
|
|
3567
|
+
const breakdown = orderedStatuses.filter((s) => (statusCounts.get(s) ?? 0) > 0).map((s) => `${s} ${statusCounts.get(s)}`).join(", ");
|
|
3568
|
+
const sessionsLine = breakdown !== "" ? `Sessions: ${args.sessionCount} (${breakdown}). Tasks: ${args.totalTaskCount}.` : `Sessions: ${args.sessionCount}. Tasks: ${args.totalTaskCount}.`;
|
|
3569
|
+
lines.push(sessionsLine);
|
|
3418
3570
|
return lines.join("\n");
|
|
3419
3571
|
}
|
|
3420
|
-
function
|
|
3572
|
+
function suspectLabel(reason) {
|
|
3573
|
+
if (reason === "events_say_ended_but_yaml_running") return " \u26A0 ended (yaml stale)";
|
|
3574
|
+
if (reason === "running_no_end_event") return " \u26A0 no end event";
|
|
3575
|
+
return "";
|
|
3576
|
+
}
|
|
3577
|
+
function shortHandoffId(sessionId) {
|
|
3421
3578
|
const SES = "ses_";
|
|
3422
3579
|
if (sessionId.startsWith(SES)) return sessionId.slice(SES.length, SES.length + 10);
|
|
3423
3580
|
return sessionId.slice(0, 10);
|
|
3424
3581
|
}
|
|
3582
|
+
function shortIdWithPrefix(id) {
|
|
3583
|
+
const sep = id.indexOf("_");
|
|
3584
|
+
if (sep === -1) return id.slice(0, 10);
|
|
3585
|
+
return id.slice(0, sep + 1) + id.slice(sep + 1, sep + 1 + 10);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// src/lib/duration.ts
|
|
3589
|
+
var DURATION_RE = /^([1-9]\d*)(ms|s|m|h)$/;
|
|
3590
|
+
function parseDuration(input) {
|
|
3591
|
+
const trimmed = input.trim();
|
|
3592
|
+
const match = DURATION_RE.exec(trimmed);
|
|
3593
|
+
if (!match) {
|
|
3594
|
+
throw new Error(
|
|
3595
|
+
`Invalid duration: ${trimmed}. Expected format: <positive-integer><unit> where unit is ms/s/m/h`
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
const value = Number(match[1]);
|
|
3599
|
+
const unit = match[2];
|
|
3600
|
+
let ms;
|
|
3601
|
+
switch (unit) {
|
|
3602
|
+
case "ms":
|
|
3603
|
+
ms = value;
|
|
3604
|
+
break;
|
|
3605
|
+
case "s":
|
|
3606
|
+
ms = value * 1e3;
|
|
3607
|
+
break;
|
|
3608
|
+
case "m":
|
|
3609
|
+
ms = value * 6e4;
|
|
3610
|
+
break;
|
|
3611
|
+
case "h":
|
|
3612
|
+
ms = value * 36e5;
|
|
3613
|
+
break;
|
|
3614
|
+
default:
|
|
3615
|
+
throw new Error(`Invalid duration unit: ${unit}`);
|
|
3616
|
+
}
|
|
3617
|
+
if (!Number.isFinite(ms)) {
|
|
3618
|
+
throw new Error(`Duration overflow: ${trimmed}`);
|
|
3619
|
+
}
|
|
3620
|
+
return ms;
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
// src/lib/id-resolver.ts
|
|
3624
|
+
async function resolveSessionId(paths, input) {
|
|
3625
|
+
return resolveIdInternal(paths, input, "session");
|
|
3626
|
+
}
|
|
3627
|
+
async function resolveTaskId(paths, input, options = {}) {
|
|
3628
|
+
return resolveIdInternal(paths, input, "task", options);
|
|
3629
|
+
}
|
|
3630
|
+
var KIND_CONFIG = {
|
|
3631
|
+
session: {
|
|
3632
|
+
prefix: "ses_",
|
|
3633
|
+
noun: "session",
|
|
3634
|
+
nounPlural: "sessions",
|
|
3635
|
+
capNoun: "Session",
|
|
3636
|
+
enumerate: enumerateSessionDirs
|
|
3637
|
+
},
|
|
3638
|
+
task: {
|
|
3639
|
+
prefix: "task_",
|
|
3640
|
+
noun: "task",
|
|
3641
|
+
nounPlural: "tasks",
|
|
3642
|
+
capNoun: "Task",
|
|
3643
|
+
enumerate: enumerateTaskIds
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
async function resolveIdInternal(paths, input, kind, options = {}) {
|
|
3647
|
+
const cfg = KIND_CONFIG[kind];
|
|
3648
|
+
const trimmed = input.trim();
|
|
3649
|
+
if (trimmed.length === 0) {
|
|
3650
|
+
throw new Error(`${cfg.capNoun} id is empty`);
|
|
3651
|
+
}
|
|
3652
|
+
const normalized = trimmed.startsWith(cfg.prefix) ? trimmed : `${cfg.prefix}${trimmed}`;
|
|
3653
|
+
if (normalized.length <= cfg.prefix.length) {
|
|
3654
|
+
throw new Error(`${cfg.capNoun} not found: ${input}`);
|
|
3655
|
+
}
|
|
3656
|
+
const primary = await cfg.enumerate(paths);
|
|
3657
|
+
const merged = new Set(primary);
|
|
3658
|
+
if (kind === "task" && options.includeArchived === true) {
|
|
3659
|
+
for (const id of await enumerateArchivedTaskIds(paths)) {
|
|
3660
|
+
merged.add(id);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
if (merged.size === 0) {
|
|
3664
|
+
throw new Error(`${cfg.capNoun} not found: ${input}`);
|
|
3665
|
+
}
|
|
3666
|
+
const matches = [...merged].filter((e) => e.startsWith(normalized));
|
|
3667
|
+
if (matches.length === 0) {
|
|
3668
|
+
throw new Error(`${cfg.capNoun} not found: ${input}`);
|
|
3669
|
+
}
|
|
3670
|
+
if (matches.length > 1) {
|
|
3671
|
+
throw new Error(
|
|
3672
|
+
`Ambiguous ${cfg.noun} id '${input}': matched ${matches.length} ${cfg.nounPlural}. Disambiguate with a longer prefix.`
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
return matches[0];
|
|
3676
|
+
}
|
|
3425
3677
|
|
|
3426
3678
|
// src/runtime/child-process-runner.ts
|
|
3427
|
-
import { spawn } from "child_process";
|
|
3679
|
+
import { spawn as spawn2 } from "child_process";
|
|
3428
3680
|
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
3429
3681
|
var ChildProcessRunner = class {
|
|
3430
3682
|
async run(command, args, options) {
|
|
@@ -3441,7 +3693,7 @@ var ChildProcessRunner = class {
|
|
|
3441
3693
|
const started_at = /* @__PURE__ */ new Date();
|
|
3442
3694
|
let child;
|
|
3443
3695
|
try {
|
|
3444
|
-
child =
|
|
3696
|
+
child = spawn2(snapshotCommand, [...snapshotArgs], {
|
|
3445
3697
|
cwd: snapshotCwd,
|
|
3446
3698
|
env: options.env ?? process.env,
|
|
3447
3699
|
stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
|
|
@@ -3531,264 +3783,807 @@ var ChildProcessRunner = class {
|
|
|
3531
3783
|
});
|
|
3532
3784
|
});
|
|
3533
3785
|
}
|
|
3534
|
-
};
|
|
3535
|
-
function validateOptions(options) {
|
|
3536
|
-
if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
|
|
3537
|
-
throw new Error("Invalid timeout_ms");
|
|
3786
|
+
};
|
|
3787
|
+
function validateOptions(options) {
|
|
3788
|
+
if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
|
|
3789
|
+
throw new Error("Invalid timeout_ms");
|
|
3790
|
+
}
|
|
3791
|
+
if (options.capture === "none" && options.stdin !== void 0) {
|
|
3792
|
+
throw new Error('Combination of capture: "none" and stdin is not supported');
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
function classifySpawnError(error) {
|
|
3796
|
+
if (findErrorCode(error, "ENOENT")) {
|
|
3797
|
+
return new Error("Command not found", { cause: error });
|
|
3798
|
+
}
|
|
3799
|
+
return new Error("Failed to spawn child process", { cause: error });
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/schemas/manifest.schema.ts
|
|
3803
|
+
import { z as z9 } from "zod";
|
|
3804
|
+
var ProjectSchema = z9.object({
|
|
3805
|
+
name: z9.string().optional(),
|
|
3806
|
+
description: z9.string().optional(),
|
|
3807
|
+
repository_url: z9.string().nullable().optional()
|
|
3808
|
+
});
|
|
3809
|
+
var CapabilitiesSchema = z9.object({
|
|
3810
|
+
enabled: z9.array(z9.string())
|
|
3811
|
+
});
|
|
3812
|
+
var ApprovalConfigSchema = z9.object({
|
|
3813
|
+
required_for: z9.array(z9.string()).optional(),
|
|
3814
|
+
default_risk_level: z9.enum(["low", "medium", "high", "critical"])
|
|
3815
|
+
});
|
|
3816
|
+
var ClaudeCodeAdapterConfigSchema = z9.object({
|
|
3817
|
+
enabled: z9.boolean(),
|
|
3818
|
+
config_path: z9.string().optional()
|
|
3819
|
+
});
|
|
3820
|
+
var AdaptersSchema = z9.object({
|
|
3821
|
+
"claude-code": ClaudeCodeAdapterConfigSchema
|
|
3822
|
+
});
|
|
3823
|
+
var GitConfigSchema = z9.object({
|
|
3824
|
+
events_log: z9.enum(["ignore", "commit"]).default("ignore")
|
|
3825
|
+
});
|
|
3826
|
+
var WorkspaceMetaSchema = z9.object({
|
|
3827
|
+
id: WorkspaceIdSchema,
|
|
3828
|
+
name: z9.string().min(1),
|
|
3829
|
+
created_at: IsoTimestampSchema,
|
|
3830
|
+
updated_at: IsoTimestampSchema
|
|
3831
|
+
});
|
|
3832
|
+
var ManifestSchema = z9.object({
|
|
3833
|
+
schema_version: SchemaVersionSchema,
|
|
3834
|
+
basou_version: z9.literal("0.1.0"),
|
|
3835
|
+
workspace: WorkspaceMetaSchema,
|
|
3836
|
+
project: ProjectSchema,
|
|
3837
|
+
capabilities: CapabilitiesSchema,
|
|
3838
|
+
approval: ApprovalConfigSchema,
|
|
3839
|
+
adapters: AdaptersSchema,
|
|
3840
|
+
git: GitConfigSchema
|
|
3841
|
+
});
|
|
3842
|
+
|
|
3843
|
+
// src/schemas/session-import.schema.ts
|
|
3844
|
+
import { z as z10 } from "zod";
|
|
3845
|
+
var SessionInnerImportSchema = z10.object({
|
|
3846
|
+
id: SessionIdSchema.optional(),
|
|
3847
|
+
label: z10.string().optional(),
|
|
3848
|
+
task_id: TaskIdSchema.nullable().optional(),
|
|
3849
|
+
workspace_id: WorkspaceIdSchema,
|
|
3850
|
+
source: z10.object({
|
|
3851
|
+
kind: SessionSourceKindSchema,
|
|
3852
|
+
version: z10.literal("0.1.0"),
|
|
3853
|
+
// Source-tool-native id (e.g. Claude Code session UUID), retained so
|
|
3854
|
+
// re-imports of the same source can be deduplicated.
|
|
3855
|
+
external_id: z10.string().optional()
|
|
3856
|
+
}),
|
|
3857
|
+
started_at: IsoTimestampSchema,
|
|
3858
|
+
ended_at: IsoTimestampSchema.optional(),
|
|
3859
|
+
status: SessionStatusSchema,
|
|
3860
|
+
working_directory: z10.string().min(1),
|
|
3861
|
+
invocation: z10.object({
|
|
3862
|
+
command: z10.string().min(1),
|
|
3863
|
+
args: z10.array(z10.string()),
|
|
3864
|
+
exit_code: z10.number().int().nullable()
|
|
3865
|
+
}),
|
|
3866
|
+
related_files: z10.array(z10.string()).default([]),
|
|
3867
|
+
events_log: z10.string().optional(),
|
|
3868
|
+
summary: z10.string().nullable().optional(),
|
|
3869
|
+
metrics: SessionMetricsSchema.optional()
|
|
3870
|
+
}).strict();
|
|
3871
|
+
var SessionImportPayloadSchema = z10.object({
|
|
3872
|
+
schema_version: z10.string(),
|
|
3873
|
+
session: SessionInnerImportSchema,
|
|
3874
|
+
events: z10.array(EventSchema)
|
|
3875
|
+
}).strict();
|
|
3876
|
+
|
|
3877
|
+
// src/stats/work-stats.ts
|
|
3878
|
+
import { join as join11 } from "path";
|
|
3879
|
+
function resolveTimeZone(timeZone) {
|
|
3880
|
+
if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
|
|
3881
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
3882
|
+
}
|
|
3883
|
+
var STATUS_ORDER = [
|
|
3884
|
+
"completed",
|
|
3885
|
+
"failed",
|
|
3886
|
+
"running",
|
|
3887
|
+
"interrupted",
|
|
3888
|
+
"waiting_approval",
|
|
3889
|
+
"initialized",
|
|
3890
|
+
"imported",
|
|
3891
|
+
"archived"
|
|
3892
|
+
];
|
|
3893
|
+
async function computeWorkStats(input) {
|
|
3894
|
+
const { now } = input;
|
|
3895
|
+
const timeZone = resolveTimeZone(input.timeZone);
|
|
3896
|
+
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
3897
|
+
const wrappedSkip = (sid, reason) => {
|
|
3898
|
+
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
3899
|
+
input.onSessionSkip?.(sid, reason);
|
|
3900
|
+
};
|
|
3901
|
+
const loadOpts = { now, onSkip: wrappedSkip };
|
|
3902
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
3903
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
3904
|
+
const sessions = [];
|
|
3905
|
+
for (const entry of entries) {
|
|
3906
|
+
const events = [];
|
|
3907
|
+
let eventsUnreadable = false;
|
|
3908
|
+
try {
|
|
3909
|
+
for await (const ev of replayEvents(join11(input.paths.sessions, entry.sessionId), {
|
|
3910
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
3911
|
+
})) {
|
|
3912
|
+
events.push(ev);
|
|
3913
|
+
}
|
|
3914
|
+
} catch {
|
|
3915
|
+
eventsUnreadable = true;
|
|
3916
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
3917
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
sessions.push(
|
|
3921
|
+
sessionWorkStatsFromEvents(
|
|
3922
|
+
entry.sessionId,
|
|
3923
|
+
entry.session.session,
|
|
3924
|
+
events,
|
|
3925
|
+
now,
|
|
3926
|
+
eventsUnreadable
|
|
3927
|
+
)
|
|
3928
|
+
);
|
|
3929
|
+
}
|
|
3930
|
+
const allIntervals = [];
|
|
3931
|
+
for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
|
|
3932
|
+
const union = unionDurationMs(allIntervals);
|
|
3933
|
+
return {
|
|
3934
|
+
generatedAt: now.toISOString(),
|
|
3935
|
+
activeGapCapMs: ACTIVE_GAP_CAP_MS,
|
|
3936
|
+
timeZone,
|
|
3937
|
+
totals: computeTotals(sessions, union.ms),
|
|
3938
|
+
sessions,
|
|
3939
|
+
bySource: computeBySource(sessions),
|
|
3940
|
+
byStatus: computeByStatus(sessions),
|
|
3941
|
+
byDay: computeByDay(sessions, union.merged, timeZone)
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
|
|
3945
|
+
let commandCount = 0;
|
|
3946
|
+
let fileChangedCount = 0;
|
|
3947
|
+
let decisionCount = 0;
|
|
3948
|
+
let commandTimeMs = 0;
|
|
3949
|
+
const timestamps = [];
|
|
3950
|
+
for (const ev of events) {
|
|
3951
|
+
const t = Date.parse(ev.occurred_at);
|
|
3952
|
+
if (Number.isFinite(t)) timestamps.push(t);
|
|
3953
|
+
if (ev.type === "command_executed") {
|
|
3954
|
+
commandCount++;
|
|
3955
|
+
commandTimeMs += ev.duration_ms;
|
|
3956
|
+
} else if (ev.type === "file_changed") {
|
|
3957
|
+
fileChangedCount++;
|
|
3958
|
+
} else if (ev.type === "decision_recorded") {
|
|
3959
|
+
decisionCount++;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
const span = computeSpan(inner.started_at, inner.ended_at, now);
|
|
3963
|
+
const tokens = readTokens(inner.metrics);
|
|
3964
|
+
const active = resolveActiveTime(inner.metrics, timestamps);
|
|
3965
|
+
return {
|
|
3966
|
+
sessionId,
|
|
3967
|
+
label: inner.label,
|
|
3968
|
+
status: inner.status,
|
|
3969
|
+
sourceKind: inner.source.kind,
|
|
3970
|
+
startedAt: inner.started_at,
|
|
3971
|
+
endedAt: inner.ended_at,
|
|
3972
|
+
open: inner.ended_at === void 0,
|
|
3973
|
+
sessionSpanMs: span.ms,
|
|
3974
|
+
commandTimeMs,
|
|
3975
|
+
activeTimeMs: active.ms,
|
|
3976
|
+
activeTimeBasis: active.basis,
|
|
3977
|
+
activeIntervals: intervalsMsToIso(active.intervals),
|
|
3978
|
+
commandCount,
|
|
3979
|
+
fileChangedCount,
|
|
3980
|
+
decisionCount,
|
|
3981
|
+
eventCount: events.length,
|
|
3982
|
+
tokens,
|
|
3983
|
+
availability: {
|
|
3984
|
+
span: true,
|
|
3985
|
+
commandTime: inner.source.kind !== "claude-code-import",
|
|
3986
|
+
activeTime: active.intervals.length > 0,
|
|
3987
|
+
tokens: hasTokens(tokens)
|
|
3988
|
+
},
|
|
3989
|
+
spanClamped: span.clamped,
|
|
3990
|
+
eventsUnreadable
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
function resolveActiveTime(metrics, eventTimestamps) {
|
|
3994
|
+
const stored = metrics?.active_intervals;
|
|
3995
|
+
if (stored !== void 0 && stored.length > 0) {
|
|
3996
|
+
const intervals = intervalsIsoToMs(stored);
|
|
3997
|
+
const ms = intervals.reduce((n, [start, end]) => n + (end - start), 0);
|
|
3998
|
+
return { ms, intervals, basis: "engaged-turns" };
|
|
3999
|
+
}
|
|
4000
|
+
const derived = activeTimeFromTimestamps(eventTimestamps, ACTIVE_GAP_CAP_MS);
|
|
4001
|
+
return { ms: derived.ms, intervals: derived.intervals, basis: "events" };
|
|
4002
|
+
}
|
|
4003
|
+
function computeSpan(startedAt, endedAt, now) {
|
|
4004
|
+
const start = Date.parse(startedAt);
|
|
4005
|
+
const end = endedAt !== void 0 ? Date.parse(endedAt) : now.getTime();
|
|
4006
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return { ms: 0, clamped: true };
|
|
4007
|
+
const raw = end - start;
|
|
4008
|
+
return raw < 0 ? { ms: 0, clamped: true } : { ms: raw, clamped: false };
|
|
4009
|
+
}
|
|
4010
|
+
function readTokens(metrics) {
|
|
4011
|
+
return {
|
|
4012
|
+
output: metrics?.output_tokens ?? 0,
|
|
4013
|
+
input: metrics?.input_tokens ?? 0,
|
|
4014
|
+
cached: metrics?.cached_input_tokens ?? 0,
|
|
4015
|
+
reasoning: metrics?.reasoning_output_tokens ?? 0
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
function hasTokens(t) {
|
|
4019
|
+
return t.output > 0 || t.input > 0 || t.cached > 0 || t.reasoning > 0;
|
|
4020
|
+
}
|
|
4021
|
+
function emptyTokens() {
|
|
4022
|
+
return { output: 0, input: 0, cached: 0, reasoning: 0 };
|
|
4023
|
+
}
|
|
4024
|
+
function addTokens(a, b) {
|
|
4025
|
+
a.output += b.output;
|
|
4026
|
+
a.input += b.input;
|
|
4027
|
+
a.cached += b.cached;
|
|
4028
|
+
a.reasoning += b.reasoning;
|
|
4029
|
+
}
|
|
4030
|
+
function computeTotals(sessions, billableActiveTimeMs) {
|
|
4031
|
+
const tokens = emptyTokens();
|
|
4032
|
+
const totals = {
|
|
4033
|
+
sessionCount: sessions.length,
|
|
4034
|
+
openSessionCount: 0,
|
|
4035
|
+
sessionSpanMs: 0,
|
|
4036
|
+
commandTimeMs: 0,
|
|
4037
|
+
activeTimeMs: 0,
|
|
4038
|
+
billableActiveTimeMs,
|
|
4039
|
+
commandCount: 0,
|
|
4040
|
+
fileChangedCount: 0,
|
|
4041
|
+
decisionCount: 0,
|
|
4042
|
+
eventCount: 0,
|
|
4043
|
+
tokens,
|
|
4044
|
+
commandTimeReliable: true,
|
|
4045
|
+
tokensAvailable: false
|
|
4046
|
+
};
|
|
4047
|
+
for (const s of sessions) {
|
|
4048
|
+
if (s.open) totals.openSessionCount++;
|
|
4049
|
+
totals.sessionSpanMs += s.sessionSpanMs;
|
|
4050
|
+
totals.commandTimeMs += s.commandTimeMs;
|
|
4051
|
+
totals.activeTimeMs += s.activeTimeMs;
|
|
4052
|
+
totals.commandCount += s.commandCount;
|
|
4053
|
+
totals.fileChangedCount += s.fileChangedCount;
|
|
4054
|
+
totals.decisionCount += s.decisionCount;
|
|
4055
|
+
totals.eventCount += s.eventCount;
|
|
4056
|
+
addTokens(tokens, s.tokens);
|
|
4057
|
+
if (!s.availability.commandTime) totals.commandTimeReliable = false;
|
|
4058
|
+
if (s.availability.tokens) totals.tokensAvailable = true;
|
|
4059
|
+
}
|
|
4060
|
+
return totals;
|
|
4061
|
+
}
|
|
4062
|
+
function computeBySource(sessions) {
|
|
4063
|
+
const map = /* @__PURE__ */ new Map();
|
|
4064
|
+
for (const s of sessions) {
|
|
4065
|
+
let row = map.get(s.sourceKind);
|
|
4066
|
+
if (row === void 0) {
|
|
4067
|
+
row = {
|
|
4068
|
+
sourceKind: s.sourceKind,
|
|
4069
|
+
sessionCount: 0,
|
|
4070
|
+
sessionSpanMs: 0,
|
|
4071
|
+
commandTimeMs: 0,
|
|
4072
|
+
activeTimeMs: 0,
|
|
4073
|
+
commandCount: 0,
|
|
4074
|
+
fileChangedCount: 0,
|
|
4075
|
+
decisionCount: 0,
|
|
4076
|
+
eventCount: 0,
|
|
4077
|
+
tokens: emptyTokens(),
|
|
4078
|
+
commandTimeReliable: true,
|
|
4079
|
+
tokensAvailable: false
|
|
4080
|
+
};
|
|
4081
|
+
map.set(s.sourceKind, row);
|
|
4082
|
+
}
|
|
4083
|
+
row.sessionCount++;
|
|
4084
|
+
row.sessionSpanMs += s.sessionSpanMs;
|
|
4085
|
+
row.commandTimeMs += s.commandTimeMs;
|
|
4086
|
+
row.activeTimeMs += s.activeTimeMs;
|
|
4087
|
+
row.commandCount += s.commandCount;
|
|
4088
|
+
row.fileChangedCount += s.fileChangedCount;
|
|
4089
|
+
row.decisionCount += s.decisionCount;
|
|
4090
|
+
row.eventCount += s.eventCount;
|
|
4091
|
+
addTokens(row.tokens, s.tokens);
|
|
4092
|
+
if (!s.availability.commandTime) row.commandTimeReliable = false;
|
|
4093
|
+
if (s.availability.tokens) row.tokensAvailable = true;
|
|
4094
|
+
}
|
|
4095
|
+
return [...map.values()].sort((a, b) => a.sourceKind.localeCompare(b.sourceKind));
|
|
4096
|
+
}
|
|
4097
|
+
function computeByStatus(sessions) {
|
|
4098
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4099
|
+
for (const s of sessions) counts.set(s.status, (counts.get(s.status) ?? 0) + 1);
|
|
4100
|
+
const ordered = [];
|
|
4101
|
+
for (const status of STATUS_ORDER) {
|
|
4102
|
+
const count = counts.get(status);
|
|
4103
|
+
if (count !== void 0 && count > 0) ordered.push({ status, count });
|
|
4104
|
+
}
|
|
4105
|
+
return ordered;
|
|
4106
|
+
}
|
|
4107
|
+
function computeByDay(sessions, unionMerged, timeZone) {
|
|
4108
|
+
const days = /* @__PURE__ */ new Map();
|
|
4109
|
+
const ensure = (date) => {
|
|
4110
|
+
let day = days.get(date);
|
|
4111
|
+
if (day === void 0) {
|
|
4112
|
+
day = {
|
|
4113
|
+
date,
|
|
4114
|
+
billableActiveTimeMs: 0,
|
|
4115
|
+
sessionCount: 0,
|
|
4116
|
+
commandCount: 0,
|
|
4117
|
+
fileChangedCount: 0,
|
|
4118
|
+
decisionCount: 0,
|
|
4119
|
+
tokens: emptyTokens()
|
|
4120
|
+
};
|
|
4121
|
+
days.set(date, day);
|
|
4122
|
+
}
|
|
4123
|
+
return day;
|
|
4124
|
+
};
|
|
4125
|
+
for (const [start, end] of unionMerged) {
|
|
4126
|
+
ensure(tzDate(start, timeZone)).billableActiveTimeMs += end - start;
|
|
4127
|
+
}
|
|
4128
|
+
for (const s of sessions) {
|
|
4129
|
+
const startedMs = Date.parse(s.startedAt);
|
|
4130
|
+
if (!Number.isFinite(startedMs)) continue;
|
|
4131
|
+
const day = ensure(tzDate(startedMs, timeZone));
|
|
4132
|
+
day.sessionCount++;
|
|
4133
|
+
day.commandCount += s.commandCount;
|
|
4134
|
+
day.fileChangedCount += s.fileChangedCount;
|
|
4135
|
+
day.decisionCount += s.decisionCount;
|
|
4136
|
+
addTokens(day.tokens, s.tokens);
|
|
4137
|
+
}
|
|
4138
|
+
return [...days.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
4139
|
+
}
|
|
4140
|
+
function tzDate(ms, timeZone) {
|
|
4141
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
4142
|
+
timeZone,
|
|
4143
|
+
year: "numeric",
|
|
4144
|
+
month: "2-digit",
|
|
4145
|
+
day: "2-digit"
|
|
4146
|
+
}).format(new Date(ms));
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
// src/storage/basou-dir.ts
|
|
4150
|
+
import { lstat as lstat3, mkdir as mkdir3 } from "fs/promises";
|
|
4151
|
+
import { join as join12 } from "path";
|
|
4152
|
+
function basouPaths(repositoryRoot) {
|
|
4153
|
+
const root = join12(repositoryRoot, ".basou");
|
|
4154
|
+
const approvalsBase = join12(root, "approvals");
|
|
4155
|
+
return {
|
|
4156
|
+
root,
|
|
4157
|
+
sessions: join12(root, "sessions"),
|
|
4158
|
+
tasks: join12(root, "tasks"),
|
|
4159
|
+
approvals: {
|
|
4160
|
+
pending: join12(approvalsBase, "pending"),
|
|
4161
|
+
resolved: join12(approvalsBase, "resolved")
|
|
4162
|
+
},
|
|
4163
|
+
locks: join12(root, "locks"),
|
|
4164
|
+
logs: join12(root, "logs"),
|
|
4165
|
+
raw: join12(root, "raw"),
|
|
4166
|
+
tmp: join12(root, "tmp"),
|
|
4167
|
+
files: {
|
|
4168
|
+
manifest: join12(root, "manifest.yaml"),
|
|
4169
|
+
status: join12(root, "status.json"),
|
|
4170
|
+
handoff: join12(root, "handoff.md"),
|
|
4171
|
+
decisions: join12(root, "decisions.md")
|
|
4172
|
+
}
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
var PATH_LABELS = {
|
|
4176
|
+
sessions: ".basou/sessions",
|
|
4177
|
+
tasks: ".basou/tasks",
|
|
4178
|
+
approvalsPending: ".basou/approvals/pending",
|
|
4179
|
+
approvalsResolved: ".basou/approvals/resolved",
|
|
4180
|
+
locks: ".basou/locks",
|
|
4181
|
+
logs: ".basou/logs",
|
|
4182
|
+
raw: ".basou/raw",
|
|
4183
|
+
tmp: ".basou/tmp"
|
|
4184
|
+
};
|
|
4185
|
+
async function ensureBasouDirectory(repositoryRoot) {
|
|
4186
|
+
const paths = basouPaths(repositoryRoot);
|
|
4187
|
+
let existing;
|
|
4188
|
+
try {
|
|
4189
|
+
existing = await lstat3(paths.root);
|
|
4190
|
+
} catch (error) {
|
|
4191
|
+
if (!hasErrorCode3(error) || error.code !== "ENOENT") {
|
|
4192
|
+
throw new Error("Failed to inspect .basou directory", { cause: error });
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
if (existing !== void 0 && !existing.isDirectory()) {
|
|
4196
|
+
throw new Error("Basou root .basou exists but is not a directory");
|
|
4197
|
+
}
|
|
4198
|
+
await Promise.all([
|
|
4199
|
+
mkdirLabeled(paths.sessions, PATH_LABELS.sessions),
|
|
4200
|
+
mkdirLabeled(paths.tasks, PATH_LABELS.tasks),
|
|
4201
|
+
mkdirLabeled(paths.approvals.pending, PATH_LABELS.approvalsPending),
|
|
4202
|
+
mkdirLabeled(paths.approvals.resolved, PATH_LABELS.approvalsResolved),
|
|
4203
|
+
mkdirLabeled(paths.locks, PATH_LABELS.locks),
|
|
4204
|
+
mkdirLabeled(paths.logs, PATH_LABELS.logs),
|
|
4205
|
+
mkdirLabeled(paths.raw, PATH_LABELS.raw),
|
|
4206
|
+
mkdirLabeled(paths.tmp, PATH_LABELS.tmp)
|
|
4207
|
+
]);
|
|
4208
|
+
return paths;
|
|
4209
|
+
}
|
|
4210
|
+
async function mkdirLabeled(target, label) {
|
|
4211
|
+
try {
|
|
4212
|
+
await mkdir3(target, { recursive: true });
|
|
4213
|
+
} catch (error) {
|
|
4214
|
+
if (hasErrorCode3(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
|
|
4215
|
+
throw new Error(`${label} exists but is not a directory`, { cause: error });
|
|
4216
|
+
}
|
|
4217
|
+
throw new Error(`Failed to create ${label}`, { cause: error });
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
function hasErrorCode3(error) {
|
|
4221
|
+
if (!(error instanceof Error)) return false;
|
|
4222
|
+
const codeProp = error.code;
|
|
4223
|
+
return typeof codeProp === "string";
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
// src/storage/gitignore.ts
|
|
4227
|
+
import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
4228
|
+
import { join as join13 } from "path";
|
|
4229
|
+
var MARKER = "# Basou - default ignore";
|
|
4230
|
+
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";
|
|
4231
|
+
async function appendBasouGitignore(repositoryRoot) {
|
|
4232
|
+
const gitignorePath = join13(repositoryRoot, ".gitignore");
|
|
4233
|
+
let body;
|
|
4234
|
+
let existed;
|
|
4235
|
+
try {
|
|
4236
|
+
body = await readFile6(gitignorePath, "utf8");
|
|
4237
|
+
existed = true;
|
|
4238
|
+
} catch (error) {
|
|
4239
|
+
if (hasErrorCode4(error) && error.code === "ENOENT") {
|
|
4240
|
+
body = "";
|
|
4241
|
+
existed = false;
|
|
4242
|
+
} else {
|
|
4243
|
+
throw new Error("Failed to read .gitignore", { cause: error });
|
|
4244
|
+
}
|
|
3538
4245
|
}
|
|
3539
|
-
if (
|
|
3540
|
-
|
|
4246
|
+
if (existed && hasBasouMarker(body)) {
|
|
4247
|
+
return { appended: false };
|
|
4248
|
+
}
|
|
4249
|
+
const next = composeNextBody(body);
|
|
4250
|
+
try {
|
|
4251
|
+
await writeFile2(gitignorePath, next, { encoding: "utf8" });
|
|
4252
|
+
} catch (error) {
|
|
4253
|
+
throw new Error("Failed to write .gitignore", { cause: error });
|
|
3541
4254
|
}
|
|
4255
|
+
return { appended: true };
|
|
3542
4256
|
}
|
|
3543
|
-
function
|
|
3544
|
-
|
|
3545
|
-
|
|
4257
|
+
function hasBasouMarker(body) {
|
|
4258
|
+
for (const rawLine of body.split("\n")) {
|
|
4259
|
+
if (rawLine.trimEnd().startsWith(MARKER)) return true;
|
|
3546
4260
|
}
|
|
3547
|
-
return
|
|
4261
|
+
return false;
|
|
3548
4262
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
return
|
|
4263
|
+
function composeNextBody(existing) {
|
|
4264
|
+
if (existing.length === 0) return BASOU_GITIGNORE_BLOCK;
|
|
4265
|
+
const normalized = existing.endsWith("\n") ? existing : `${existing}
|
|
4266
|
+
`;
|
|
4267
|
+
return `${normalized}
|
|
4268
|
+
${BASOU_GITIGNORE_BLOCK}`;
|
|
3554
4269
|
}
|
|
3555
|
-
function
|
|
3556
|
-
if (
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
4270
|
+
function hasErrorCode4(error) {
|
|
4271
|
+
if (!(error instanceof Error)) return false;
|
|
4272
|
+
return typeof error.code === "string";
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
// src/storage/manifest.ts
|
|
4276
|
+
import { lstat as lstat4 } from "fs/promises";
|
|
4277
|
+
function createManifest(input) {
|
|
4278
|
+
if (input.workspaceName.length === 0) {
|
|
4279
|
+
throw new Error("Workspace name is empty. Pass --name explicitly.");
|
|
3561
4280
|
}
|
|
3562
|
-
|
|
4281
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4282
|
+
const workspaceId = input.workspaceId ?? prefixedUlid("ws");
|
|
4283
|
+
const project = {
|
|
4284
|
+
...input.projectName !== void 0 ? { name: input.projectName } : {},
|
|
4285
|
+
...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
|
|
4286
|
+
...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
|
|
4287
|
+
};
|
|
4288
|
+
const manifest = {
|
|
4289
|
+
schema_version: "0.1.0",
|
|
4290
|
+
basou_version: "0.1.0",
|
|
4291
|
+
workspace: {
|
|
4292
|
+
id: workspaceId,
|
|
4293
|
+
name: input.workspaceName,
|
|
4294
|
+
created_at: now,
|
|
4295
|
+
updated_at: now
|
|
4296
|
+
},
|
|
4297
|
+
project,
|
|
4298
|
+
capabilities: {
|
|
4299
|
+
enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
|
|
4300
|
+
},
|
|
4301
|
+
approval: {
|
|
4302
|
+
required_for: ["destructive_command", "external_send"],
|
|
4303
|
+
default_risk_level: "medium"
|
|
4304
|
+
},
|
|
4305
|
+
adapters: {
|
|
4306
|
+
"claude-code": { enabled: true }
|
|
4307
|
+
},
|
|
4308
|
+
git: { events_log: "ignore" }
|
|
4309
|
+
};
|
|
4310
|
+
return ManifestSchema.parse(manifest);
|
|
3563
4311
|
}
|
|
3564
|
-
async function
|
|
3565
|
-
const
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
4312
|
+
async function writeManifest(paths, manifest, options) {
|
|
4313
|
+
const force = options?.force === true;
|
|
4314
|
+
const validated = ManifestSchema.parse(manifest);
|
|
4315
|
+
if (!force) {
|
|
4316
|
+
let existed = false;
|
|
4317
|
+
try {
|
|
4318
|
+
await lstat4(paths.files.manifest);
|
|
4319
|
+
existed = true;
|
|
4320
|
+
} catch (error) {
|
|
4321
|
+
if (!hasErrorCode5(error) || error.code !== "ENOENT") {
|
|
4322
|
+
throw new Error("Failed to inspect existing manifest", { cause: error });
|
|
4323
|
+
}
|
|
3575
4324
|
}
|
|
3576
|
-
if (
|
|
3577
|
-
throw
|
|
4325
|
+
if (existed) {
|
|
4326
|
+
throw new Error("Already initialized. Use --force to overwrite.");
|
|
3578
4327
|
}
|
|
3579
|
-
throw new Error("Not a git repository", { cause: error });
|
|
3580
4328
|
}
|
|
4329
|
+
await writeYamlFile(paths.files.manifest, validated);
|
|
3581
4330
|
}
|
|
3582
|
-
async function
|
|
3583
|
-
const
|
|
4331
|
+
async function readManifest(paths) {
|
|
4332
|
+
const raw = await readYamlFile(paths.files.manifest);
|
|
4333
|
+
return ManifestSchema.parse(raw);
|
|
4334
|
+
}
|
|
4335
|
+
function hasErrorCode5(error) {
|
|
4336
|
+
if (!(error instanceof Error)) return false;
|
|
4337
|
+
return typeof error.code === "string";
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
// src/storage/markdown-store.ts
|
|
4341
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
4342
|
+
var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
|
|
4343
|
+
var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
|
|
4344
|
+
async function readMarkdownFile(filePath) {
|
|
3584
4345
|
try {
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
return void 0;
|
|
4346
|
+
return await readFile7(filePath, "utf8");
|
|
4347
|
+
} catch (error) {
|
|
4348
|
+
if (hasErrorCode6(error) && error.code === "ENOENT") return null;
|
|
4349
|
+
throw new Error("Failed to read markdown file", { cause: error });
|
|
3590
4350
|
}
|
|
3591
4351
|
}
|
|
3592
|
-
async function
|
|
3593
|
-
const git = safeSimpleGit(repositoryRoot);
|
|
3594
|
-
let inside;
|
|
4352
|
+
async function writeMarkdownFile(filePath, body) {
|
|
3595
4353
|
try {
|
|
3596
|
-
|
|
4354
|
+
await atomicReplace(filePath, body);
|
|
3597
4355
|
} catch (error) {
|
|
3598
|
-
|
|
3599
|
-
|
|
4356
|
+
throw new Error("Failed to write markdown file", { cause: error });
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
function parseMarkers(content) {
|
|
4360
|
+
const lines = content.split(/\r?\n/);
|
|
4361
|
+
const startLines = [];
|
|
4362
|
+
const endLines = [];
|
|
4363
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4364
|
+
if (lines[i] === GENERATED_START) startLines.push(i);
|
|
4365
|
+
else if (lines[i] === GENERATED_END) endLines.push(i);
|
|
4366
|
+
}
|
|
4367
|
+
if (startLines.length === 0 && endLines.length === 0) return { kind: "no_markers" };
|
|
4368
|
+
if (startLines.length === 0) return { kind: "missing_start" };
|
|
4369
|
+
if (endLines.length === 0) return { kind: "missing_end" };
|
|
4370
|
+
if (startLines.length >= 2 || endLines.length >= 2) return { kind: "multiple_pairs" };
|
|
4371
|
+
const startLineIdx = startLines[0];
|
|
4372
|
+
const endLineIdx = endLines[0];
|
|
4373
|
+
if (endLineIdx < startLineIdx) return { kind: "wrong_order" };
|
|
4374
|
+
const startOffset = lineStartOffset(content, startLineIdx);
|
|
4375
|
+
const endLineStart = lineStartOffset(content, endLineIdx);
|
|
4376
|
+
const startLineEnd = startOffset + GENERATED_START.length;
|
|
4377
|
+
const endLineEnd = endLineStart + GENERATED_END.length;
|
|
4378
|
+
const before = content.slice(0, startOffset);
|
|
4379
|
+
const afterStartNewline = skipOneNewline(content, startLineEnd);
|
|
4380
|
+
const beforeEndNewline = trimOneNewline(content, endLineStart);
|
|
4381
|
+
const generated = content.slice(afterStartNewline, beforeEndNewline);
|
|
4382
|
+
const after = content.slice(endLineEnd);
|
|
4383
|
+
return { kind: "ok", before, generated, after };
|
|
4384
|
+
}
|
|
4385
|
+
function renderWithMarkers(existing, generated, fileLabel) {
|
|
4386
|
+
const normalized = generated.endsWith("\n") ? generated : `${generated}
|
|
4387
|
+
`;
|
|
4388
|
+
if (existing === null) {
|
|
4389
|
+
return `${GENERATED_START}
|
|
4390
|
+
${normalized}${GENERATED_END}
|
|
4391
|
+
`;
|
|
4392
|
+
}
|
|
4393
|
+
const section = parseMarkers(existing);
|
|
4394
|
+
switch (section.kind) {
|
|
4395
|
+
case "ok":
|
|
4396
|
+
return `${section.before}${GENERATED_START}
|
|
4397
|
+
${normalized}${GENERATED_END}${section.after}`;
|
|
4398
|
+
case "no_markers":
|
|
4399
|
+
throw new Error(`Markers missing in ${fileLabel}`);
|
|
4400
|
+
case "missing_start":
|
|
4401
|
+
case "missing_end":
|
|
4402
|
+
case "multiple_pairs":
|
|
4403
|
+
case "wrong_order":
|
|
4404
|
+
throw new Error(`Markers mismatched in ${fileLabel}`);
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
function lineStartOffset(content, lineIdx) {
|
|
4408
|
+
if (lineIdx === 0) return 0;
|
|
4409
|
+
let offset = 0;
|
|
4410
|
+
let line = 0;
|
|
4411
|
+
while (offset < content.length && line < lineIdx) {
|
|
4412
|
+
const ch = content[offset];
|
|
4413
|
+
if (ch === "\n") {
|
|
4414
|
+
line += 1;
|
|
4415
|
+
offset += 1;
|
|
4416
|
+
} else if (ch === "\r") {
|
|
4417
|
+
offset += 1;
|
|
4418
|
+
if (content[offset] === "\n") offset += 1;
|
|
4419
|
+
line += 1;
|
|
4420
|
+
} else {
|
|
4421
|
+
offset += 1;
|
|
3600
4422
|
}
|
|
3601
|
-
throw new Error("Failed to read git state", { cause: error });
|
|
3602
4423
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
4424
|
+
return offset;
|
|
4425
|
+
}
|
|
4426
|
+
function skipOneNewline(content, offset) {
|
|
4427
|
+
if (content[offset] === "\r" && content[offset + 1] === "\n") return offset + 2;
|
|
4428
|
+
if (content[offset] === "\n") return offset + 1;
|
|
4429
|
+
return offset;
|
|
4430
|
+
}
|
|
4431
|
+
function trimOneNewline(content, offset) {
|
|
4432
|
+
if (offset >= 2 && content[offset - 2] === "\r" && content[offset - 1] === "\n")
|
|
4433
|
+
return offset - 2;
|
|
4434
|
+
if (offset >= 1 && content[offset - 1] === "\n") return offset - 1;
|
|
4435
|
+
return offset;
|
|
4436
|
+
}
|
|
4437
|
+
function hasErrorCode6(error) {
|
|
4438
|
+
if (!(error instanceof Error)) return false;
|
|
4439
|
+
const codeProp = error.code;
|
|
4440
|
+
return typeof codeProp === "string";
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
// src/storage/session-import.ts
|
|
4444
|
+
import { mkdir as mkdir4, rm as rm2 } from "fs/promises";
|
|
4445
|
+
import { homedir as homedir2 } from "os";
|
|
4446
|
+
import { join as join14 } from "path";
|
|
4447
|
+
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
4448
|
+
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
4449
|
+
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
4450
|
+
}
|
|
4451
|
+
const effectiveSessionTaskId = options.taskIdOverride ?? payload.session.task_id ?? null;
|
|
4452
|
+
await assertImportedTaskReferencesAreReachable(paths, payload.events, effectiveSessionTaskId);
|
|
4453
|
+
const newSessionId = prefixedUlid("ses");
|
|
4454
|
+
const rewrittenEvents = rewriteEvents(payload.events, newSessionId);
|
|
4455
|
+
assertChronologicalOrder(rewrittenEvents);
|
|
4456
|
+
const { record: sessionRecord, pathSanitizeReport } = buildSessionRecord(
|
|
4457
|
+
payload.session,
|
|
4458
|
+
manifest,
|
|
4459
|
+
newSessionId,
|
|
4460
|
+
options
|
|
4461
|
+
);
|
|
4462
|
+
if (options.dryRun === true) {
|
|
4463
|
+
return {
|
|
4464
|
+
sessionId: newSessionId,
|
|
4465
|
+
eventCount: rewrittenEvents.length,
|
|
4466
|
+
finalStatus: "imported",
|
|
4467
|
+
finalSourceKind: sessionRecord.session.source.kind,
|
|
4468
|
+
pathSanitizeReport
|
|
4469
|
+
};
|
|
3605
4470
|
}
|
|
3606
|
-
|
|
4471
|
+
const sessionDir = join14(paths.sessions, newSessionId);
|
|
3607
4472
|
try {
|
|
3608
|
-
|
|
4473
|
+
await mkdir4(sessionDir, { recursive: true });
|
|
3609
4474
|
} catch (error) {
|
|
3610
|
-
|
|
3611
|
-
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
3612
|
-
}
|
|
3613
|
-
throw new Error("No commits in repository", { cause: error });
|
|
3614
|
-
}
|
|
3615
|
-
if (head.length === 0) {
|
|
3616
|
-
throw new Error("No commits in repository");
|
|
4475
|
+
throw new Error("Failed to create session directory", { cause: error });
|
|
3617
4476
|
}
|
|
3618
|
-
let branch;
|
|
3619
4477
|
try {
|
|
3620
|
-
|
|
3621
|
-
branch = raw.length > 0 ? raw : "HEAD";
|
|
4478
|
+
await writeEventsBulk(sessionDir, rewrittenEvents);
|
|
3622
4479
|
} catch (error) {
|
|
3623
|
-
|
|
4480
|
+
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
4481
|
+
throw error;
|
|
3624
4482
|
}
|
|
3625
|
-
let dirty;
|
|
3626
|
-
const staged = [];
|
|
3627
|
-
const unstaged = [];
|
|
3628
|
-
const untracked = [];
|
|
3629
4483
|
try {
|
|
3630
|
-
const
|
|
3631
|
-
|
|
3632
|
-
for (const f of status.files) {
|
|
3633
|
-
if (f.index === "?" && f.working_dir === "?") {
|
|
3634
|
-
untracked.push(f.path);
|
|
3635
|
-
continue;
|
|
3636
|
-
}
|
|
3637
|
-
if (f.index !== " " && f.index !== "?") staged.push(f.path);
|
|
3638
|
-
if (f.working_dir !== " " && f.working_dir !== "?") unstaged.push(f.path);
|
|
3639
|
-
}
|
|
4484
|
+
const sessionYamlPath = join14(sessionDir, "session.yaml");
|
|
4485
|
+
await linkYamlFile(sessionYamlPath, sessionRecord);
|
|
3640
4486
|
} catch (error) {
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
try {
|
|
3647
|
-
const upstream = `${branch}@{upstream}`;
|
|
3648
|
-
const counts = (await git.raw(["rev-list", "--left-right", "--count", `${upstream}...HEAD`])).trim();
|
|
3649
|
-
const [behindStr, aheadStr] = counts.split(/\s+/);
|
|
3650
|
-
const parsedBehind = Number.parseInt(behindStr ?? "", 10);
|
|
3651
|
-
const parsedAhead = Number.parseInt(aheadStr ?? "", 10);
|
|
3652
|
-
if (Number.isFinite(parsedBehind) && parsedBehind >= 0) behind = parsedBehind;
|
|
3653
|
-
if (Number.isFinite(parsedAhead) && parsedAhead >= 0) ahead = parsedAhead;
|
|
3654
|
-
} catch {
|
|
4487
|
+
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
4488
|
+
if (findErrorCode(error, "EEXIST")) {
|
|
4489
|
+
throw new Error("Session directory collision (retry the command)", {
|
|
4490
|
+
cause: error
|
|
4491
|
+
});
|
|
3655
4492
|
}
|
|
4493
|
+
throw error;
|
|
3656
4494
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
untracked,
|
|
3664
|
-
...ahead !== void 0 ? { ahead } : {},
|
|
3665
|
-
...behind !== void 0 ? { behind } : {}
|
|
4495
|
+
return {
|
|
4496
|
+
sessionId: newSessionId,
|
|
4497
|
+
eventCount: rewrittenEvents.length,
|
|
4498
|
+
finalStatus: "imported",
|
|
4499
|
+
finalSourceKind: sessionRecord.session.source.kind,
|
|
4500
|
+
pathSanitizeReport
|
|
3666
4501
|
};
|
|
3667
|
-
return snapshot;
|
|
3668
4502
|
}
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
git = safeSimpleGit(repoRoot);
|
|
3675
|
-
} catch (error) {
|
|
3676
|
-
if (isGitNotFound(error)) {
|
|
3677
|
-
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
4503
|
+
async function assertImportedTaskReferencesAreReachable(paths, events, effectiveSessionTaskId) {
|
|
4504
|
+
const taskIdsToCheck = /* @__PURE__ */ new Set();
|
|
4505
|
+
for (const ev of events) {
|
|
4506
|
+
if (ev.type === "task_created" || ev.type === "task_status_changed" || ev.type === "task_reconciled" || ev.type === "task_linkage_refreshed" || ev.type === "task_deleted" || ev.type === "task_archived") {
|
|
4507
|
+
taskIdsToCheck.add(ev.task_id);
|
|
3678
4508
|
}
|
|
3679
|
-
throw new Error("Not a git repository", { cause: error });
|
|
3680
4509
|
}
|
|
3681
|
-
if (
|
|
3682
|
-
|
|
3683
|
-
try {
|
|
3684
|
-
raw = await git.raw(["diff", "--name-status", `${baseRef}..${headRef}`]);
|
|
3685
|
-
} catch (error) {
|
|
3686
|
-
if (isGitNotFound(error)) {
|
|
3687
|
-
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
3688
|
-
}
|
|
3689
|
-
const message = error instanceof Error ? error.message : "";
|
|
3690
|
-
if (/not a git repository/i.test(message)) {
|
|
3691
|
-
throw new Error("Not a git repository", { cause: error });
|
|
3692
|
-
}
|
|
3693
|
-
if (message.includes("bad revision") || message.includes("unknown revision") || message.includes("ambiguous argument")) {
|
|
3694
|
-
throw new Error("Invalid ref", { cause: error });
|
|
3695
|
-
}
|
|
3696
|
-
throw new Error("Failed to compute git diff", { cause: error });
|
|
4510
|
+
if (effectiveSessionTaskId !== null) {
|
|
4511
|
+
taskIdsToCheck.add(effectiveSessionTaskId);
|
|
3697
4512
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
const
|
|
3702
|
-
const
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
const code = parts[0];
|
|
3706
|
-
if (code === void 0 || code.length === 0) continue;
|
|
3707
|
-
if (code.startsWith("R") && parts.length >= 3) {
|
|
3708
|
-
const newPath = parts[2];
|
|
3709
|
-
const oldPath = parts[1];
|
|
3710
|
-
if (newPath === void 0) continue;
|
|
3711
|
-
changes.push({
|
|
3712
|
-
path: newPath,
|
|
3713
|
-
status: "renamed",
|
|
3714
|
-
...oldPath !== void 0 ? { old_path: oldPath } : {}
|
|
3715
|
-
});
|
|
3716
|
-
} else if (code === "A" && parts[1]) {
|
|
3717
|
-
changes.push({ path: parts[1], status: "added" });
|
|
3718
|
-
} else if (code === "M" && parts[1]) {
|
|
3719
|
-
changes.push({ path: parts[1], status: "modified" });
|
|
3720
|
-
} else if (code === "D" && parts[1]) {
|
|
3721
|
-
changes.push({ path: parts[1], status: "deleted" });
|
|
4513
|
+
if (taskIdsToCheck.size === 0) {
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
const knownTaskIds = new Set(await enumerateTaskIds(paths));
|
|
4517
|
+
for (const id of taskIdsToCheck) {
|
|
4518
|
+
if (!knownTaskIds.has(id)) {
|
|
4519
|
+
throw new Error("Imported session references unknown task_id");
|
|
3722
4520
|
}
|
|
3723
4521
|
}
|
|
3724
|
-
return changes;
|
|
3725
4522
|
}
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
}
|
|
3733
|
-
|
|
3734
|
-
for (
|
|
3735
|
-
|
|
4523
|
+
function rewriteEvents(events, newSessionId) {
|
|
4524
|
+
return events.map((event) => ({
|
|
4525
|
+
...event,
|
|
4526
|
+
id: prefixedUlid("evt"),
|
|
4527
|
+
session_id: newSessionId
|
|
4528
|
+
}));
|
|
4529
|
+
}
|
|
4530
|
+
function assertChronologicalOrder(events) {
|
|
4531
|
+
for (let i = 1; i < events.length; i++) {
|
|
4532
|
+
const prevEvent = events[i - 1];
|
|
4533
|
+
const currEvent = events[i];
|
|
4534
|
+
if (prevEvent === void 0 || currEvent === void 0) continue;
|
|
4535
|
+
const prev = Date.parse(prevEvent.occurred_at);
|
|
4536
|
+
const curr = Date.parse(currEvent.occurred_at);
|
|
4537
|
+
if (!Number.isFinite(prev) || !Number.isFinite(curr) || curr < prev) {
|
|
4538
|
+
throw new Error("Events are not in chronological order");
|
|
4539
|
+
}
|
|
3736
4540
|
}
|
|
3737
|
-
throw new Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.");
|
|
3738
4541
|
}
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
4542
|
+
function buildSessionRecord(input, manifest, newSessionId, options) {
|
|
4543
|
+
const home = homedir2();
|
|
4544
|
+
const workingDirectoryRaw = input.working_directory;
|
|
4545
|
+
const workingDirectorySanitized = sanitizeWorkingDirectory(workingDirectoryRaw, {
|
|
4546
|
+
homedir: home
|
|
3744
4547
|
});
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
}
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
break;
|
|
3773
|
-
case "h":
|
|
3774
|
-
ms = value * 36e5;
|
|
3775
|
-
break;
|
|
3776
|
-
default:
|
|
3777
|
-
throw new Error(`Invalid duration unit: ${unit}`);
|
|
3778
|
-
}
|
|
3779
|
-
if (!Number.isFinite(ms)) {
|
|
3780
|
-
throw new Error(`Duration overflow: ${trimmed}`);
|
|
3781
|
-
}
|
|
3782
|
-
return ms;
|
|
4548
|
+
const relatedSanitized = sanitizeRelatedFiles(input.related_files, {
|
|
4549
|
+
workingDirectory: workingDirectoryRaw,
|
|
4550
|
+
homedir: home
|
|
4551
|
+
});
|
|
4552
|
+
const inner = {
|
|
4553
|
+
id: newSessionId,
|
|
4554
|
+
...options.labelOverride !== void 0 || input.label !== void 0 ? { label: options.labelOverride ?? input.label } : {},
|
|
4555
|
+
task_id: options.taskIdOverride !== void 0 ? options.taskIdOverride : input.task_id ?? null,
|
|
4556
|
+
workspace_id: manifest.workspace.id,
|
|
4557
|
+
source: input.source,
|
|
4558
|
+
started_at: input.started_at,
|
|
4559
|
+
...input.ended_at !== void 0 ? { ended_at: input.ended_at } : {},
|
|
4560
|
+
status: "imported",
|
|
4561
|
+
working_directory: workingDirectorySanitized,
|
|
4562
|
+
invocation: input.invocation,
|
|
4563
|
+
related_files: relatedSanitized.sanitized,
|
|
4564
|
+
events_log: "events.jsonl",
|
|
4565
|
+
summary: input.summary ?? null,
|
|
4566
|
+
...input.metrics !== void 0 ? { metrics: input.metrics } : {}
|
|
4567
|
+
};
|
|
4568
|
+
return {
|
|
4569
|
+
record: { schema_version: "0.1.0", session: inner },
|
|
4570
|
+
pathSanitizeReport: {
|
|
4571
|
+
relatedFiles: relatedSanitized.mutationCount,
|
|
4572
|
+
workingDirectoryRewritten: workingDirectorySanitized !== workingDirectoryRaw
|
|
4573
|
+
}
|
|
4574
|
+
};
|
|
3783
4575
|
}
|
|
3784
4576
|
|
|
3785
4577
|
// src/index.ts
|
|
3786
4578
|
var BASOU_CORE_VERSION = "0.1.0";
|
|
3787
4579
|
export {
|
|
4580
|
+
ACTIVE_GAP_CAP_MS,
|
|
3788
4581
|
ApprovalIdSchema,
|
|
3789
4582
|
ApprovalSchema,
|
|
3790
4583
|
ApprovalStatusSchema,
|
|
3791
4584
|
BASOU_CORE_VERSION,
|
|
4585
|
+
CLAUDE_IMPORT_SOURCE,
|
|
4586
|
+
CODEX_IMPORT_SOURCE,
|
|
3792
4587
|
ChildProcessRunner,
|
|
3793
4588
|
DecisionIdSchema,
|
|
3794
4589
|
EventIdSchema,
|
|
@@ -3806,6 +4601,7 @@ export {
|
|
|
3806
4601
|
SessionIdSchema,
|
|
3807
4602
|
SessionImportPayloadSchema,
|
|
3808
4603
|
SessionInnerImportSchema,
|
|
4604
|
+
SessionMetricsSchema,
|
|
3809
4605
|
SessionSchema,
|
|
3810
4606
|
SessionSourceKindSchema,
|
|
3811
4607
|
SessionStatusSchema,
|
|
@@ -3825,6 +4621,9 @@ export {
|
|
|
3825
4621
|
buildStatusSnapshot,
|
|
3826
4622
|
classifySuspect,
|
|
3827
4623
|
claudeCodeAdapterMetadata,
|
|
4624
|
+
claudeTranscriptToImportPayload,
|
|
4625
|
+
codexRolloutToImportPayload,
|
|
4626
|
+
computeWorkStats,
|
|
3828
4627
|
createAdHocSessionWithEvent,
|
|
3829
4628
|
createManifest,
|
|
3830
4629
|
createTaskWithEvent,
|
|
@@ -3871,6 +4670,7 @@ export {
|
|
|
3871
4670
|
sanitizePath,
|
|
3872
4671
|
sanitizeRelatedFiles,
|
|
3873
4672
|
sanitizeWorkingDirectory,
|
|
4673
|
+
sessionWorkStatsFromEvents,
|
|
3874
4674
|
summarizeAdapterOutput,
|
|
3875
4675
|
tryRemoteUrl,
|
|
3876
4676
|
ulid,
|