@basou/core 0.48.1 → 0.50.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 +385 -52
- package/dist/index.js +744 -201
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/adapters/command-lookup.ts
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
async function isOnPath(command) {
|
|
4
|
-
return new Promise((
|
|
4
|
+
return new Promise((resolve5) => {
|
|
5
5
|
const child = spawn("which", [command], { stdio: "ignore" });
|
|
6
|
-
child.on("error", () =>
|
|
7
|
-
child.on("exit", (code) =>
|
|
6
|
+
child.on("error", () => resolve5(false));
|
|
7
|
+
child.on("exit", (code) => resolve5(code === 0));
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -23,13 +23,142 @@ function summarizeAdapterOutput(_stream, _raw) {
|
|
|
23
23
|
throw new Error("adapter_output summary is not implemented in this release");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// src/adapters/codex/hooks-json.ts
|
|
27
|
+
var SESSION_START_HOOK_TIMEOUT_SECONDS = 30;
|
|
28
|
+
var SESSION_START_HOOK_MATCHER = "startup|resume|clear";
|
|
29
|
+
var SESSION_START_HOOK_CONTEXT_LIMIT = 0;
|
|
30
|
+
var SESSION_START_HOOK_STATUS_MESSAGE = "basou orient";
|
|
31
|
+
var BASOU_SESSION_START_HOOK = /(?:\bbasou|(?:@basou|packages)\/cli\/dist\/index\.js['"]?)\s+hook\s+session-start\b/;
|
|
32
|
+
function isBasouSessionStartHookCommand(command) {
|
|
33
|
+
return BASOU_SESSION_START_HOOK.test(command);
|
|
34
|
+
}
|
|
35
|
+
function shellQuote(value) {
|
|
36
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
37
|
+
}
|
|
38
|
+
function buildSessionStartHookCommand(options) {
|
|
39
|
+
return `node ${shellQuote(options.cliEntry)} hook session-start 2>/dev/null || true`;
|
|
40
|
+
}
|
|
41
|
+
function isRecord(value) {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function cloneHooksFile(hooksFile) {
|
|
45
|
+
if (hooksFile === void 0 || hooksFile === null) return {};
|
|
46
|
+
if (!isRecord(hooksFile)) {
|
|
47
|
+
throw new Error("The Codex hooks.json is not a JSON object.");
|
|
48
|
+
}
|
|
49
|
+
return structuredClone(hooksFile);
|
|
50
|
+
}
|
|
51
|
+
function canonicalHandler(command) {
|
|
52
|
+
return {
|
|
53
|
+
type: "command",
|
|
54
|
+
command,
|
|
55
|
+
timeout: SESSION_START_HOOK_TIMEOUT_SECONDS,
|
|
56
|
+
statusMessage: SESSION_START_HOOK_STATUS_MESSAGE,
|
|
57
|
+
additionalContextLimit: SESSION_START_HOOK_CONTEXT_LIMIT
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function handlerIsCanonical(entry, command) {
|
|
61
|
+
const want = canonicalHandler(command);
|
|
62
|
+
return Object.keys(want).every((k) => entry[k] === want[k]);
|
|
63
|
+
}
|
|
64
|
+
function upsertSessionStartHook(hooksFile, command) {
|
|
65
|
+
const root = cloneHooksFile(hooksFile);
|
|
66
|
+
if (root.hooks === void 0) {
|
|
67
|
+
root.hooks = {};
|
|
68
|
+
} else if (!isRecord(root.hooks)) {
|
|
69
|
+
throw new Error("The 'hooks' key in the Codex hooks.json is not an object.");
|
|
70
|
+
}
|
|
71
|
+
const hooks = root.hooks;
|
|
72
|
+
if (hooks.SessionStart === void 0) {
|
|
73
|
+
hooks.SessionStart = [];
|
|
74
|
+
} else if (!Array.isArray(hooks.SessionStart)) {
|
|
75
|
+
throw new Error("The 'hooks.SessionStart' key in the Codex hooks.json is not an array.");
|
|
76
|
+
}
|
|
77
|
+
const groups = hooks.SessionStart;
|
|
78
|
+
for (const group of groups) {
|
|
79
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
80
|
+
for (const entry of group.hooks) {
|
|
81
|
+
if (!isRecord(entry)) continue;
|
|
82
|
+
if (typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
|
|
83
|
+
const unchanged = handlerIsCanonical(entry, command);
|
|
84
|
+
Object.assign(entry, canonicalHandler(command));
|
|
85
|
+
return { hooksFile: root, action: unchanged ? "unchanged" : "updated" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
groups.push({ matcher: SESSION_START_HOOK_MATCHER, hooks: [canonicalHandler(command)] });
|
|
90
|
+
return { hooksFile: root, action: "installed" };
|
|
91
|
+
}
|
|
92
|
+
function removeSessionStartHook(hooksFile) {
|
|
93
|
+
const root = cloneHooksFile(hooksFile);
|
|
94
|
+
if (!isRecord(root.hooks) || !Array.isArray(root.hooks.SessionStart)) {
|
|
95
|
+
return { hooksFile: root, action: "absent" };
|
|
96
|
+
}
|
|
97
|
+
const hooks = root.hooks;
|
|
98
|
+
const groups = hooks.SessionStart;
|
|
99
|
+
let removed = false;
|
|
100
|
+
const kept = [];
|
|
101
|
+
for (const group of groups) {
|
|
102
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
103
|
+
kept.push(group);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const keptHandlers = group.hooks.filter((entry) => {
|
|
107
|
+
if (isRecord(entry) && typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
|
|
108
|
+
removed = true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
if (keptHandlers.length === group.hooks.length) {
|
|
114
|
+
kept.push(group);
|
|
115
|
+
} else if (keptHandlers.length > 0) {
|
|
116
|
+
group.hooks = keptHandlers;
|
|
117
|
+
kept.push(group);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!removed) return { hooksFile: root, action: "absent" };
|
|
121
|
+
if (kept.length === 0) {
|
|
122
|
+
delete hooks.SessionStart;
|
|
123
|
+
} else {
|
|
124
|
+
hooks.SessionStart = kept;
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(hooks).length === 0) {
|
|
127
|
+
delete root.hooks;
|
|
128
|
+
}
|
|
129
|
+
return { hooksFile: root, action: "removed" };
|
|
130
|
+
}
|
|
131
|
+
function findBasouSessionStartHook(hooksFile) {
|
|
132
|
+
if (!isRecord(hooksFile) || !isRecord(hooksFile.hooks) || !Array.isArray(hooksFile.hooks.SessionStart)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const groups = hooksFile.hooks.SessionStart;
|
|
136
|
+
for (let g = 0; g < groups.length; g++) {
|
|
137
|
+
const group = groups[g];
|
|
138
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
139
|
+
for (let h = 0; h < group.hooks.length; h++) {
|
|
140
|
+
const entry = group.hooks[h];
|
|
141
|
+
if (isRecord(entry) && typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
|
|
142
|
+
return {
|
|
143
|
+
command: entry.command,
|
|
144
|
+
groupIndex: g,
|
|
145
|
+
handlerIndex: h,
|
|
146
|
+
matcher: typeof group.matcher === "string" ? group.matcher : void 0,
|
|
147
|
+
handler: structuredClone(entry)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
26
155
|
// src/adapters/claude-code/settings-hook.ts
|
|
27
156
|
var STOP_HOOK_TIMEOUT_SECONDS = 20;
|
|
28
157
|
var BASOU_STOP_HOOK = /(?:\bbasou|(?:@basou|packages)\/cli\/dist\/index\.js['"]?)\s+hook\s+stop\b/;
|
|
29
158
|
function isBasouStopHookCommand(command) {
|
|
30
159
|
return BASOU_STOP_HOOK.test(command);
|
|
31
160
|
}
|
|
32
|
-
function
|
|
161
|
+
function shellQuote2(value) {
|
|
33
162
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
34
163
|
}
|
|
35
164
|
function buildStopHookCommand(options) {
|
|
@@ -38,14 +167,14 @@ function buildStopHookCommand(options) {
|
|
|
38
167
|
if (options.requireReview === true) flags.push("--require-review");
|
|
39
168
|
if (options.minEdits !== void 0) flags.push(`--min-edits ${options.minEdits}`);
|
|
40
169
|
const suffix = flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
|
41
|
-
return `node ${
|
|
170
|
+
return `node ${shellQuote2(options.cliEntry)} hook stop${suffix} 2>/dev/null || true`;
|
|
42
171
|
}
|
|
43
|
-
function
|
|
172
|
+
function isRecord2(value) {
|
|
44
173
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
174
|
}
|
|
46
175
|
function cloneSettings(settings) {
|
|
47
176
|
if (settings === void 0 || settings === null) return {};
|
|
48
|
-
if (!
|
|
177
|
+
if (!isRecord2(settings)) {
|
|
49
178
|
throw new Error("Claude settings is not a JSON object.");
|
|
50
179
|
}
|
|
51
180
|
return structuredClone(settings);
|
|
@@ -54,7 +183,7 @@ function upsertStopHook(settings, command) {
|
|
|
54
183
|
const root = cloneSettings(settings);
|
|
55
184
|
if (root.hooks === void 0) {
|
|
56
185
|
root.hooks = {};
|
|
57
|
-
} else if (!
|
|
186
|
+
} else if (!isRecord2(root.hooks)) {
|
|
58
187
|
throw new Error("The 'hooks' key in Claude settings is not an object.");
|
|
59
188
|
}
|
|
60
189
|
const hooks = root.hooks;
|
|
@@ -65,9 +194,9 @@ function upsertStopHook(settings, command) {
|
|
|
65
194
|
}
|
|
66
195
|
const stop = hooks.Stop;
|
|
67
196
|
for (const group of stop) {
|
|
68
|
-
if (!
|
|
197
|
+
if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
|
|
69
198
|
for (const entry of group.hooks) {
|
|
70
|
-
if (!
|
|
199
|
+
if (!isRecord2(entry)) continue;
|
|
71
200
|
if (typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
|
|
72
201
|
const unchanged = entry.type === "command" && entry.command === command && entry.timeout === STOP_HOOK_TIMEOUT_SECONDS;
|
|
73
202
|
entry.type = "command";
|
|
@@ -82,7 +211,7 @@ function upsertStopHook(settings, command) {
|
|
|
82
211
|
}
|
|
83
212
|
function removeStopHook(settings) {
|
|
84
213
|
const root = cloneSettings(settings);
|
|
85
|
-
if (!
|
|
214
|
+
if (!isRecord2(root.hooks) || !Array.isArray(root.hooks.Stop)) {
|
|
86
215
|
return { settings: root, action: "absent" };
|
|
87
216
|
}
|
|
88
217
|
const hooks = root.hooks;
|
|
@@ -90,12 +219,12 @@ function removeStopHook(settings) {
|
|
|
90
219
|
let removed = false;
|
|
91
220
|
const newStop = [];
|
|
92
221
|
for (const group of stop) {
|
|
93
|
-
if (!
|
|
222
|
+
if (!isRecord2(group) || !Array.isArray(group.hooks)) {
|
|
94
223
|
newStop.push(group);
|
|
95
224
|
continue;
|
|
96
225
|
}
|
|
97
226
|
const keptHooks = group.hooks.filter((entry) => {
|
|
98
|
-
if (
|
|
227
|
+
if (isRecord2(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
|
|
99
228
|
removed = true;
|
|
100
229
|
return false;
|
|
101
230
|
}
|
|
@@ -122,19 +251,198 @@ function removeStopHook(settings) {
|
|
|
122
251
|
return { settings: root, action: "removed" };
|
|
123
252
|
}
|
|
124
253
|
function findBasouStopHookCommand(settings) {
|
|
125
|
-
if (!
|
|
254
|
+
if (!isRecord2(settings) || !isRecord2(settings.hooks) || !Array.isArray(settings.hooks.Stop)) {
|
|
126
255
|
return null;
|
|
127
256
|
}
|
|
128
257
|
for (const group of settings.hooks.Stop) {
|
|
129
|
-
if (!
|
|
258
|
+
if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
|
|
130
259
|
for (const entry of group.hooks) {
|
|
131
|
-
if (
|
|
260
|
+
if (isRecord2(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
|
|
132
261
|
return entry.command;
|
|
133
262
|
}
|
|
134
263
|
}
|
|
135
264
|
}
|
|
136
265
|
return null;
|
|
137
266
|
}
|
|
267
|
+
var ENTRY = String.raw`(?:[^\s'"]*/)?(?:@basou|packages)/cli/dist/index\.js`;
|
|
268
|
+
var INVOCATION = String.raw`(?:basou|node[ \t]+(?:'(?:[^']*/)?(?:@basou|packages)/cli/dist/index\.js'|"(?:[^"]*/)?(?:@basou|packages)/cli/dist/index\.js"|${ENTRY}))`;
|
|
269
|
+
var WRAPPER = String.raw`(?:[ \t]+2>[ \t]*/dev/null)?(?:[ \t]*\|\|[ \t]*true)?`;
|
|
270
|
+
var CLAUDE_SESSION_START = new RegExp(
|
|
271
|
+
String.raw`^[ \t]*${INVOCATION}[ \t]+hook[ \t]+session-start${WRAPPER}[ \t]*$`
|
|
272
|
+
);
|
|
273
|
+
var CLAUDE_ORIENT_SESSION_START = new RegExp(
|
|
274
|
+
String.raw`^[ \t]*${INVOCATION}[ \t]+orient${WRAPPER}[ \t]*$`
|
|
275
|
+
);
|
|
276
|
+
function isClaudeSessionStartHookCommand(command) {
|
|
277
|
+
return CLAUDE_SESSION_START.test(command);
|
|
278
|
+
}
|
|
279
|
+
function isBasouOrientSessionStartCommand(command) {
|
|
280
|
+
return CLAUDE_ORIENT_SESSION_START.test(command);
|
|
281
|
+
}
|
|
282
|
+
function basouSessionStartKind(entry) {
|
|
283
|
+
if (!isRecord2(entry) || typeof entry.command !== "string") return null;
|
|
284
|
+
if (isClaudeSessionStartHookCommand(entry.command)) return "session-start";
|
|
285
|
+
if (isBasouOrientSessionStartCommand(entry.command)) return "orient";
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function matcherKey(group) {
|
|
289
|
+
const m = group.matcher;
|
|
290
|
+
return m === void 0 || m === "" || m === "*" ? "*" : String(m);
|
|
291
|
+
}
|
|
292
|
+
function upsertClaudeSessionStartHook(settings, command) {
|
|
293
|
+
const root = cloneSettings(settings);
|
|
294
|
+
if (root.hooks === void 0) {
|
|
295
|
+
root.hooks = {};
|
|
296
|
+
} else if (!isRecord2(root.hooks)) {
|
|
297
|
+
throw new Error("The 'hooks' key in Claude settings is not an object.");
|
|
298
|
+
}
|
|
299
|
+
const hooks = root.hooks;
|
|
300
|
+
if (hooks.SessionStart === void 0) {
|
|
301
|
+
hooks.SessionStart = [];
|
|
302
|
+
} else if (!Array.isArray(hooks.SessionStart)) {
|
|
303
|
+
throw new Error("The 'hooks.SessionStart' key in Claude settings is not an array.");
|
|
304
|
+
}
|
|
305
|
+
const groups = hooks.SessionStart;
|
|
306
|
+
let found = false;
|
|
307
|
+
let changed = false;
|
|
308
|
+
let replacedOrient = false;
|
|
309
|
+
const keptUnder = /* @__PURE__ */ new Set();
|
|
310
|
+
const nextGroups = [];
|
|
311
|
+
for (const group of groups) {
|
|
312
|
+
if (!isRecord2(group) || !Array.isArray(group.hooks)) {
|
|
313
|
+
nextGroups.push(group);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const key = matcherKey(group);
|
|
317
|
+
const nextHooks = [];
|
|
318
|
+
let removedHere = false;
|
|
319
|
+
for (const entry of group.hooks) {
|
|
320
|
+
const kind = basouSessionStartKind(entry);
|
|
321
|
+
if (kind === null || !isRecord2(entry)) {
|
|
322
|
+
nextHooks.push(entry);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
found = true;
|
|
326
|
+
if (kind === "orient") replacedOrient = true;
|
|
327
|
+
if (keptUnder.has(key)) {
|
|
328
|
+
changed = true;
|
|
329
|
+
removedHere = true;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (entry.type !== "command" || entry.command !== command || entry.timeout !== SESSION_START_HOOK_TIMEOUT_SECONDS) {
|
|
333
|
+
changed = true;
|
|
334
|
+
}
|
|
335
|
+
entry.type = "command";
|
|
336
|
+
entry.command = command;
|
|
337
|
+
entry.timeout = SESSION_START_HOOK_TIMEOUT_SECONDS;
|
|
338
|
+
keptUnder.add(key);
|
|
339
|
+
nextHooks.push(entry);
|
|
340
|
+
}
|
|
341
|
+
if (removedHere && nextHooks.length === 0) continue;
|
|
342
|
+
group.hooks = nextHooks;
|
|
343
|
+
nextGroups.push(group);
|
|
344
|
+
}
|
|
345
|
+
hooks.SessionStart = nextGroups;
|
|
346
|
+
if (!found) {
|
|
347
|
+
nextGroups.push({
|
|
348
|
+
matcher: SESSION_START_HOOK_MATCHER,
|
|
349
|
+
hooks: [{ type: "command", command, timeout: SESSION_START_HOOK_TIMEOUT_SECONDS }]
|
|
350
|
+
});
|
|
351
|
+
return { settings: root, action: "installed" };
|
|
352
|
+
}
|
|
353
|
+
if (replacedOrient) return { settings: root, action: "replaced" };
|
|
354
|
+
return { settings: root, action: changed ? "updated" : "unchanged" };
|
|
355
|
+
}
|
|
356
|
+
function removeClaudeSessionStartHook(settings) {
|
|
357
|
+
const root = cloneSettings(settings);
|
|
358
|
+
if (!isRecord2(root.hooks) || !Array.isArray(root.hooks.SessionStart)) {
|
|
359
|
+
return { settings: root, action: "absent" };
|
|
360
|
+
}
|
|
361
|
+
const hooks = root.hooks;
|
|
362
|
+
let removed = false;
|
|
363
|
+
const nextGroups = [];
|
|
364
|
+
for (const group of hooks.SessionStart) {
|
|
365
|
+
if (!isRecord2(group) || !Array.isArray(group.hooks)) {
|
|
366
|
+
nextGroups.push(group);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const keptHooks = group.hooks.filter((entry) => basouSessionStartKind(entry) === null);
|
|
370
|
+
if (keptHooks.length === group.hooks.length) {
|
|
371
|
+
nextGroups.push(group);
|
|
372
|
+
} else {
|
|
373
|
+
removed = true;
|
|
374
|
+
if (keptHooks.length > 0) {
|
|
375
|
+
group.hooks = keptHooks;
|
|
376
|
+
nextGroups.push(group);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (!removed) return { settings: root, action: "absent" };
|
|
381
|
+
if (nextGroups.length === 0) {
|
|
382
|
+
delete hooks.SessionStart;
|
|
383
|
+
} else {
|
|
384
|
+
hooks.SessionStart = nextGroups;
|
|
385
|
+
}
|
|
386
|
+
if (Object.keys(hooks).length === 0) delete root.hooks;
|
|
387
|
+
return { settings: root, action: "removed" };
|
|
388
|
+
}
|
|
389
|
+
function findClaudeSessionStartHooks(settings) {
|
|
390
|
+
const found = [];
|
|
391
|
+
if (!isRecord2(settings) || !isRecord2(settings.hooks)) return found;
|
|
392
|
+
const groups = settings.hooks.SessionStart;
|
|
393
|
+
if (!Array.isArray(groups)) return found;
|
|
394
|
+
for (const group of groups) {
|
|
395
|
+
if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
|
|
396
|
+
for (const entry of group.hooks) {
|
|
397
|
+
const kind = basouSessionStartKind(entry);
|
|
398
|
+
if (kind === null || !isRecord2(entry) || typeof entry.command !== "string") continue;
|
|
399
|
+
found.push({
|
|
400
|
+
command: entry.command,
|
|
401
|
+
kind,
|
|
402
|
+
matcher: typeof group.matcher === "string" ? group.matcher : void 0
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return found;
|
|
407
|
+
}
|
|
408
|
+
function isClaudeSessionStartMalformed(settings) {
|
|
409
|
+
return isRecord2(settings) && isRecord2(settings.hooks) && settings.hooks.SessionStart !== void 0 && !Array.isArray(settings.hooks.SessionStart);
|
|
410
|
+
}
|
|
411
|
+
var RUNS_BASOU = new RegExp(
|
|
412
|
+
String.raw`(?:^|[\s;&|(])(?:basou|node(?:[ \t]+-[^\s]+)*[ \t]+(?:'[^']*(?:@basou|packages)/cli/dist/index\.js'|"[^"]*(?:@basou|packages)/cli/dist/index\.js"|${ENTRY})|npx(?:[ \t]+-y)?[ \t]+@basou/cli)[ \t]+(orient|hook[ \t]+session-start)(?=$|[\s;&|)])`,
|
|
413
|
+
"g"
|
|
414
|
+
);
|
|
415
|
+
function isInsideQuotes(text, index) {
|
|
416
|
+
let single = false;
|
|
417
|
+
let double = false;
|
|
418
|
+
for (let i = 0; i < index; i++) {
|
|
419
|
+
const c = text[i];
|
|
420
|
+
if (c === "'" && !double) single = !single;
|
|
421
|
+
else if (c === '"' && !single) double = !double;
|
|
422
|
+
}
|
|
423
|
+
return single || double;
|
|
424
|
+
}
|
|
425
|
+
function findUnrecognizedSessionStart(settings) {
|
|
426
|
+
const found = [];
|
|
427
|
+
if (!isRecord2(settings) || !isRecord2(settings.hooks)) return found;
|
|
428
|
+
const groups = settings.hooks.SessionStart;
|
|
429
|
+
if (!Array.isArray(groups)) return found;
|
|
430
|
+
for (const group of groups) {
|
|
431
|
+
if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
|
|
432
|
+
for (const entry of group.hooks) {
|
|
433
|
+
if (!isRecord2(entry) || typeof entry.command !== "string") continue;
|
|
434
|
+
if (basouSessionStartKind(entry) !== null) continue;
|
|
435
|
+
const command = entry.command;
|
|
436
|
+
for (const m of command.matchAll(RUNS_BASOU)) {
|
|
437
|
+
const at = (m.index ?? 0) + (m[0].length - m[0].trimStart().length);
|
|
438
|
+
if (isInsideQuotes(command, at)) continue;
|
|
439
|
+
found.push({ command, runs: m[1] === "orient" ? "orient" : "session-start" });
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return found;
|
|
445
|
+
}
|
|
138
446
|
|
|
139
447
|
// src/adapters/claude-code/ask-user-question.ts
|
|
140
448
|
function readString(value) {
|
|
@@ -913,6 +1221,9 @@ function claudeTranscriptToImportPayload(records, options) {
|
|
|
913
1221
|
}
|
|
914
1222
|
if (minTs === void 0 || maxTs === void 0) return null;
|
|
915
1223
|
if (derived.length === 0) return null;
|
|
1224
|
+
for (const observed of options.observedFiles ?? []) {
|
|
1225
|
+
relatedFiles.add(observed.path);
|
|
1226
|
+
}
|
|
916
1227
|
derived.sort((a, b) => Date.parse(a.occurred_at) - Date.parse(b.occurred_at));
|
|
917
1228
|
const events = [
|
|
918
1229
|
sessionStartedEvent(minTs, placeholderSessionId),
|
|
@@ -1062,135 +1373,6 @@ async function resolveCodexCommand(lookup = isOnPath) {
|
|
|
1062
1373
|
throw new Error("Codex CLI not found in PATH. Install codex first.");
|
|
1063
1374
|
}
|
|
1064
1375
|
|
|
1065
|
-
// src/adapters/codex/hooks-json.ts
|
|
1066
|
-
var SESSION_START_HOOK_TIMEOUT_SECONDS = 30;
|
|
1067
|
-
var SESSION_START_HOOK_MATCHER = "startup|resume|clear";
|
|
1068
|
-
var SESSION_START_HOOK_CONTEXT_LIMIT = 0;
|
|
1069
|
-
var SESSION_START_HOOK_STATUS_MESSAGE = "basou orient";
|
|
1070
|
-
var BASOU_SESSION_START_HOOK = /(?:\bbasou|(?:@basou|packages)\/cli\/dist\/index\.js['"]?)\s+hook\s+session-start\b/;
|
|
1071
|
-
function isBasouSessionStartHookCommand(command) {
|
|
1072
|
-
return BASOU_SESSION_START_HOOK.test(command);
|
|
1073
|
-
}
|
|
1074
|
-
function shellQuote2(value) {
|
|
1075
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
1076
|
-
}
|
|
1077
|
-
function buildSessionStartHookCommand(options) {
|
|
1078
|
-
return `node ${shellQuote2(options.cliEntry)} hook session-start 2>/dev/null || true`;
|
|
1079
|
-
}
|
|
1080
|
-
function isRecord2(value) {
|
|
1081
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1082
|
-
}
|
|
1083
|
-
function cloneHooksFile(hooksFile) {
|
|
1084
|
-
if (hooksFile === void 0 || hooksFile === null) return {};
|
|
1085
|
-
if (!isRecord2(hooksFile)) {
|
|
1086
|
-
throw new Error("The Codex hooks.json is not a JSON object.");
|
|
1087
|
-
}
|
|
1088
|
-
return structuredClone(hooksFile);
|
|
1089
|
-
}
|
|
1090
|
-
function canonicalHandler(command) {
|
|
1091
|
-
return {
|
|
1092
|
-
type: "command",
|
|
1093
|
-
command,
|
|
1094
|
-
timeout: SESSION_START_HOOK_TIMEOUT_SECONDS,
|
|
1095
|
-
statusMessage: SESSION_START_HOOK_STATUS_MESSAGE,
|
|
1096
|
-
additionalContextLimit: SESSION_START_HOOK_CONTEXT_LIMIT
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
function handlerIsCanonical(entry, command) {
|
|
1100
|
-
const want = canonicalHandler(command);
|
|
1101
|
-
return Object.keys(want).every((k) => entry[k] === want[k]);
|
|
1102
|
-
}
|
|
1103
|
-
function upsertSessionStartHook(hooksFile, command) {
|
|
1104
|
-
const root = cloneHooksFile(hooksFile);
|
|
1105
|
-
if (root.hooks === void 0) {
|
|
1106
|
-
root.hooks = {};
|
|
1107
|
-
} else if (!isRecord2(root.hooks)) {
|
|
1108
|
-
throw new Error("The 'hooks' key in the Codex hooks.json is not an object.");
|
|
1109
|
-
}
|
|
1110
|
-
const hooks = root.hooks;
|
|
1111
|
-
if (hooks.SessionStart === void 0) {
|
|
1112
|
-
hooks.SessionStart = [];
|
|
1113
|
-
} else if (!Array.isArray(hooks.SessionStart)) {
|
|
1114
|
-
throw new Error("The 'hooks.SessionStart' key in the Codex hooks.json is not an array.");
|
|
1115
|
-
}
|
|
1116
|
-
const groups = hooks.SessionStart;
|
|
1117
|
-
for (const group of groups) {
|
|
1118
|
-
if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
|
|
1119
|
-
for (const entry of group.hooks) {
|
|
1120
|
-
if (!isRecord2(entry)) continue;
|
|
1121
|
-
if (typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
|
|
1122
|
-
const unchanged = handlerIsCanonical(entry, command);
|
|
1123
|
-
Object.assign(entry, canonicalHandler(command));
|
|
1124
|
-
return { hooksFile: root, action: unchanged ? "unchanged" : "updated" };
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
groups.push({ matcher: SESSION_START_HOOK_MATCHER, hooks: [canonicalHandler(command)] });
|
|
1129
|
-
return { hooksFile: root, action: "installed" };
|
|
1130
|
-
}
|
|
1131
|
-
function removeSessionStartHook(hooksFile) {
|
|
1132
|
-
const root = cloneHooksFile(hooksFile);
|
|
1133
|
-
if (!isRecord2(root.hooks) || !Array.isArray(root.hooks.SessionStart)) {
|
|
1134
|
-
return { hooksFile: root, action: "absent" };
|
|
1135
|
-
}
|
|
1136
|
-
const hooks = root.hooks;
|
|
1137
|
-
const groups = hooks.SessionStart;
|
|
1138
|
-
let removed = false;
|
|
1139
|
-
const kept = [];
|
|
1140
|
-
for (const group of groups) {
|
|
1141
|
-
if (!isRecord2(group) || !Array.isArray(group.hooks)) {
|
|
1142
|
-
kept.push(group);
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
const keptHandlers = group.hooks.filter((entry) => {
|
|
1146
|
-
if (isRecord2(entry) && typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
|
|
1147
|
-
removed = true;
|
|
1148
|
-
return false;
|
|
1149
|
-
}
|
|
1150
|
-
return true;
|
|
1151
|
-
});
|
|
1152
|
-
if (keptHandlers.length === group.hooks.length) {
|
|
1153
|
-
kept.push(group);
|
|
1154
|
-
} else if (keptHandlers.length > 0) {
|
|
1155
|
-
group.hooks = keptHandlers;
|
|
1156
|
-
kept.push(group);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
if (!removed) return { hooksFile: root, action: "absent" };
|
|
1160
|
-
if (kept.length === 0) {
|
|
1161
|
-
delete hooks.SessionStart;
|
|
1162
|
-
} else {
|
|
1163
|
-
hooks.SessionStart = kept;
|
|
1164
|
-
}
|
|
1165
|
-
if (Object.keys(hooks).length === 0) {
|
|
1166
|
-
delete root.hooks;
|
|
1167
|
-
}
|
|
1168
|
-
return { hooksFile: root, action: "removed" };
|
|
1169
|
-
}
|
|
1170
|
-
function findBasouSessionStartHook(hooksFile) {
|
|
1171
|
-
if (!isRecord2(hooksFile) || !isRecord2(hooksFile.hooks) || !Array.isArray(hooksFile.hooks.SessionStart)) {
|
|
1172
|
-
return null;
|
|
1173
|
-
}
|
|
1174
|
-
const groups = hooksFile.hooks.SessionStart;
|
|
1175
|
-
for (let g = 0; g < groups.length; g++) {
|
|
1176
|
-
const group = groups[g];
|
|
1177
|
-
if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
|
|
1178
|
-
for (let h = 0; h < group.hooks.length; h++) {
|
|
1179
|
-
const entry = group.hooks[h];
|
|
1180
|
-
if (isRecord2(entry) && typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
|
|
1181
|
-
return {
|
|
1182
|
-
command: entry.command,
|
|
1183
|
-
groupIndex: g,
|
|
1184
|
-
handlerIndex: h,
|
|
1185
|
-
matcher: typeof group.matcher === "string" ? group.matcher : void 0,
|
|
1186
|
-
handler: structuredClone(entry)
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
return null;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
1376
|
// src/schemas/observed-duration.ts
|
|
1195
1377
|
function readObservedDuration(ev) {
|
|
1196
1378
|
const stored = ev.duration_ms;
|
|
@@ -5619,20 +5801,41 @@ async function getDiff(repoRoot, baseRef, headRef) {
|
|
|
5619
5801
|
let raw;
|
|
5620
5802
|
try {
|
|
5621
5803
|
raw = await git.raw(["diff", "--name-status", `${baseRef}..${headRef}`]);
|
|
5804
|
+
} catch (error) {
|
|
5805
|
+
throw translateDiffError(error);
|
|
5806
|
+
}
|
|
5807
|
+
return { changed_files: parseDiffNameStatus(raw) };
|
|
5808
|
+
}
|
|
5809
|
+
async function getChangesSince(repoRoot, baseRef) {
|
|
5810
|
+
let git;
|
|
5811
|
+
try {
|
|
5812
|
+
git = safeSimpleGit(repoRoot);
|
|
5622
5813
|
} catch (error) {
|
|
5623
5814
|
if (isGitNotFound(error)) {
|
|
5624
5815
|
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5625
5816
|
}
|
|
5626
|
-
|
|
5627
|
-
if (/not a git repository/i.test(message)) {
|
|
5628
|
-
throw new Error("Not a git repository", { cause: error });
|
|
5629
|
-
}
|
|
5630
|
-
if (message.includes("bad revision") || message.includes("unknown revision") || message.includes("ambiguous argument")) {
|
|
5631
|
-
throw new Error("Invalid ref", { cause: error });
|
|
5632
|
-
}
|
|
5633
|
-
throw new Error("Failed to compute git diff", { cause: error });
|
|
5817
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5634
5818
|
}
|
|
5635
|
-
|
|
5819
|
+
let raw;
|
|
5820
|
+
try {
|
|
5821
|
+
raw = await git.raw(["-c", "core.quotePath=false", "diff", "--name-status", baseRef]);
|
|
5822
|
+
} catch (error) {
|
|
5823
|
+
throw translateDiffError(error);
|
|
5824
|
+
}
|
|
5825
|
+
return parseDiffNameStatus(raw);
|
|
5826
|
+
}
|
|
5827
|
+
function translateDiffError(error) {
|
|
5828
|
+
if (isGitNotFound(error)) {
|
|
5829
|
+
return new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5830
|
+
}
|
|
5831
|
+
const message = error instanceof Error ? error.message : "";
|
|
5832
|
+
if (/not a git repository/i.test(message)) {
|
|
5833
|
+
return new Error("Not a git repository", { cause: error });
|
|
5834
|
+
}
|
|
5835
|
+
if (message.includes("bad revision") || message.includes("unknown revision") || message.includes("ambiguous argument") || message.includes("bad object") || message.includes("Invalid revision range")) {
|
|
5836
|
+
return new Error("Invalid ref", { cause: error });
|
|
5837
|
+
}
|
|
5838
|
+
return new Error("Failed to compute git diff", { cause: error });
|
|
5636
5839
|
}
|
|
5637
5840
|
function parseDiffNameStatus(raw) {
|
|
5638
5841
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
@@ -5661,6 +5864,135 @@ function parseDiffNameStatus(raw) {
|
|
|
5661
5864
|
return changes;
|
|
5662
5865
|
}
|
|
5663
5866
|
|
|
5867
|
+
// src/git/working-tree.ts
|
|
5868
|
+
async function getWorkingTreeChanges(repoRoot) {
|
|
5869
|
+
let git;
|
|
5870
|
+
try {
|
|
5871
|
+
git = safeSimpleGit(repoRoot);
|
|
5872
|
+
} catch (error) {
|
|
5873
|
+
if (isGitNotFound(error)) {
|
|
5874
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5875
|
+
}
|
|
5876
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5877
|
+
}
|
|
5878
|
+
let status;
|
|
5879
|
+
try {
|
|
5880
|
+
status = await git.status();
|
|
5881
|
+
} catch (error) {
|
|
5882
|
+
if (isGitNotFound(error)) {
|
|
5883
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5884
|
+
}
|
|
5885
|
+
const message = error instanceof Error ? error.message : "";
|
|
5886
|
+
if (/not a git repository/i.test(message)) {
|
|
5887
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5888
|
+
}
|
|
5889
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5890
|
+
}
|
|
5891
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5892
|
+
const put = (change) => {
|
|
5893
|
+
if (!byPath.has(change.path)) byPath.set(change.path, change);
|
|
5894
|
+
};
|
|
5895
|
+
const conflicted = new Set(status.conflicted);
|
|
5896
|
+
for (const entry of status.renamed) {
|
|
5897
|
+
if (conflicted.has(entry.to)) continue;
|
|
5898
|
+
put({ path: entry.to, status: "renamed", old_path: entry.from });
|
|
5899
|
+
}
|
|
5900
|
+
for (const path2 of status.deleted) {
|
|
5901
|
+
if (conflicted.has(path2)) continue;
|
|
5902
|
+
put({ path: path2, status: "deleted" });
|
|
5903
|
+
}
|
|
5904
|
+
for (const path2 of [...status.created, ...status.not_added]) {
|
|
5905
|
+
if (conflicted.has(path2)) continue;
|
|
5906
|
+
put({ path: path2, status: "added" });
|
|
5907
|
+
}
|
|
5908
|
+
for (const path2 of status.modified) {
|
|
5909
|
+
if (conflicted.has(path2)) continue;
|
|
5910
|
+
put({ path: path2, status: "modified" });
|
|
5911
|
+
}
|
|
5912
|
+
return [...byPath.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
5913
|
+
}
|
|
5914
|
+
async function readHeadSha(repoRoot) {
|
|
5915
|
+
let git;
|
|
5916
|
+
try {
|
|
5917
|
+
git = safeSimpleGit(repoRoot);
|
|
5918
|
+
} catch (error) {
|
|
5919
|
+
if (isGitNotFound(error)) {
|
|
5920
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5921
|
+
}
|
|
5922
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5923
|
+
}
|
|
5924
|
+
let inside;
|
|
5925
|
+
try {
|
|
5926
|
+
inside = await git.checkIsRepo();
|
|
5927
|
+
} catch (error) {
|
|
5928
|
+
if (isGitNotFound(error)) {
|
|
5929
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5930
|
+
}
|
|
5931
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5932
|
+
}
|
|
5933
|
+
if (!inside) throw new Error("Not a git repository");
|
|
5934
|
+
try {
|
|
5935
|
+
const head = (await git.revparse(["HEAD"])).trimEnd();
|
|
5936
|
+
return head.length > 0 ? head : null;
|
|
5937
|
+
} catch {
|
|
5938
|
+
return null;
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
async function getUntrackedFiles(repoRoot) {
|
|
5942
|
+
let git;
|
|
5943
|
+
try {
|
|
5944
|
+
git = safeSimpleGit(repoRoot);
|
|
5945
|
+
} catch (error) {
|
|
5946
|
+
if (isGitNotFound(error)) {
|
|
5947
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5948
|
+
}
|
|
5949
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5950
|
+
}
|
|
5951
|
+
let raw;
|
|
5952
|
+
try {
|
|
5953
|
+
raw = await git.raw([
|
|
5954
|
+
"-c",
|
|
5955
|
+
"core.quotePath=false",
|
|
5956
|
+
"ls-files",
|
|
5957
|
+
"--others",
|
|
5958
|
+
"--exclude-standard",
|
|
5959
|
+
"-z"
|
|
5960
|
+
]);
|
|
5961
|
+
} catch (error) {
|
|
5962
|
+
if (isGitNotFound(error)) {
|
|
5963
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5964
|
+
}
|
|
5965
|
+
const message = error instanceof Error ? error.message : "";
|
|
5966
|
+
if (/not a git repository/i.test(message)) {
|
|
5967
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5968
|
+
}
|
|
5969
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5970
|
+
}
|
|
5971
|
+
const changes = [];
|
|
5972
|
+
for (const path2 of raw.split("\0")) {
|
|
5973
|
+
if (path2.length === 0) continue;
|
|
5974
|
+
if (path2.endsWith("/")) continue;
|
|
5975
|
+
changes.push({ path: path2, status: "added" });
|
|
5976
|
+
}
|
|
5977
|
+
return changes.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
5978
|
+
}
|
|
5979
|
+
async function readEmptyTreeSha(repoRoot) {
|
|
5980
|
+
let git;
|
|
5981
|
+
try {
|
|
5982
|
+
git = safeSimpleGit(repoRoot);
|
|
5983
|
+
} catch (error) {
|
|
5984
|
+
if (isGitNotFound(error)) {
|
|
5985
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5986
|
+
}
|
|
5987
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5988
|
+
}
|
|
5989
|
+
try {
|
|
5990
|
+
return (await git.raw(["hash-object", "-t", "tree", "/dev/null"])).trimEnd();
|
|
5991
|
+
} catch (error) {
|
|
5992
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5664
5996
|
// src/handoff/handoff-renderer.ts
|
|
5665
5997
|
import { join as join14 } from "path";
|
|
5666
5998
|
|
|
@@ -5670,13 +6002,36 @@ function isTrailingStale(latestActivityAt, recordedAt) {
|
|
|
5670
6002
|
if (latestActivityAt === null) return false;
|
|
5671
6003
|
return Date.parse(latestActivityAt) - Date.parse(recordedAt) > DECISION_TRAILING_ACTIVITY_GAP_MS;
|
|
5672
6004
|
}
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
if (
|
|
5678
|
-
return
|
|
5679
|
-
}
|
|
6005
|
+
var WRAPPER_SESSION_COMMAND_COUNT = 1;
|
|
6006
|
+
function pickLatestSubstantiveEntry(entries, commandCounts, unmeasured) {
|
|
6007
|
+
const didWork = (e) => {
|
|
6008
|
+
if ((e.session.session.related_files?.length ?? 0) > 0) return true;
|
|
6009
|
+
if (unmeasured.has(e.sessionId)) return true;
|
|
6010
|
+
return (commandCounts.get(e.sessionId) ?? 0) > WRAPPER_SESSION_COMMAND_COUNT;
|
|
6011
|
+
};
|
|
6012
|
+
const working = entries.filter(didWork);
|
|
6013
|
+
const outermost = working.filter((e) => !isNestedInAnother(e, working));
|
|
6014
|
+
const pool = outermost.length > 0 ? outermost : working.length > 0 ? working : entries;
|
|
6015
|
+
return [...pool].sort(
|
|
6016
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
6017
|
+
)[0];
|
|
6018
|
+
}
|
|
6019
|
+
function isNestedInAnother(entry, working) {
|
|
6020
|
+
const end = entry.session.session.ended_at;
|
|
6021
|
+
if (end === void 0) return false;
|
|
6022
|
+
const start = Date.parse(entry.session.session.started_at);
|
|
6023
|
+
const finish = Date.parse(end);
|
|
6024
|
+
if (!Number.isFinite(start) || !Number.isFinite(finish)) return false;
|
|
6025
|
+
return working.some((other) => {
|
|
6026
|
+
if (other.sessionId === entry.sessionId) return false;
|
|
6027
|
+
const otherEnd = other.session.session.ended_at;
|
|
6028
|
+
if (otherEnd === void 0) return false;
|
|
6029
|
+
const oStart = Date.parse(other.session.session.started_at);
|
|
6030
|
+
const oFinish = Date.parse(otherEnd);
|
|
6031
|
+
if (!Number.isFinite(oStart) || !Number.isFinite(oFinish)) return false;
|
|
6032
|
+
if (oStart > start || oFinish < finish) return false;
|
|
6033
|
+
return oStart < start || oFinish > finish;
|
|
6034
|
+
});
|
|
5680
6035
|
}
|
|
5681
6036
|
|
|
5682
6037
|
// src/lib/transient-paths.ts
|
|
@@ -5720,6 +6075,8 @@ async function renderHandoff(input) {
|
|
|
5720
6075
|
const tasksCreated = [];
|
|
5721
6076
|
const tasksStatusChanged = [];
|
|
5722
6077
|
let latestActivityAt = null;
|
|
6078
|
+
const commandCounts = /* @__PURE__ */ new Map();
|
|
6079
|
+
const unmeasuredSessions = /* @__PURE__ */ new Set();
|
|
5723
6080
|
const noteActivity = (iso) => {
|
|
5724
6081
|
if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
|
|
5725
6082
|
latestActivityAt = iso;
|
|
@@ -5750,6 +6107,8 @@ async function renderHandoff(input) {
|
|
|
5750
6107
|
sessionId: entry.sessionId
|
|
5751
6108
|
});
|
|
5752
6109
|
}
|
|
6110
|
+
} else if (ev.type === "command_executed") {
|
|
6111
|
+
commandCounts.set(entry.sessionId, (commandCounts.get(entry.sessionId) ?? 0) + 1);
|
|
5753
6112
|
} else if (ev.type === "decision_voided") {
|
|
5754
6113
|
voidedDecisionIds.add(ev.decision_id);
|
|
5755
6114
|
} else if (ev.type === "task_created") {
|
|
@@ -5768,6 +6127,7 @@ async function renderHandoff(input) {
|
|
|
5768
6127
|
}
|
|
5769
6128
|
}
|
|
5770
6129
|
} catch {
|
|
6130
|
+
unmeasuredSessions.add(entry.sessionId);
|
|
5771
6131
|
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
5772
6132
|
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
5773
6133
|
}
|
|
@@ -5821,7 +6181,7 @@ async function renderHandoff(input) {
|
|
|
5821
6181
|
const liveEntries = entries.filter(
|
|
5822
6182
|
(e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
|
|
5823
6183
|
);
|
|
5824
|
-
const latestSession = pickLatestSubstantiveEntry(liveEntries);
|
|
6184
|
+
const latestSession = pickLatestSubstantiveEntry(liveEntries, commandCounts, unmeasuredSessions);
|
|
5825
6185
|
const latestFiles = (latestSession?.session.session.related_files ?? []).filter(
|
|
5826
6186
|
(file) => !isTransientToolPath(file)
|
|
5827
6187
|
);
|
|
@@ -6070,7 +6430,7 @@ function parseBuildStamp(raw) {
|
|
|
6070
6430
|
}
|
|
6071
6431
|
}
|
|
6072
6432
|
var BASOU_CORE_BUILD = parseBuildStamp(
|
|
6073
|
-
true ? '{"version":"0.
|
|
6433
|
+
true ? '{"version":"0.50.0","commit":"5b4c5dc","committedAt":"2026-09-23T19:12:06+09:00"}' : void 0
|
|
6074
6434
|
);
|
|
6075
6435
|
|
|
6076
6436
|
// src/lib/duration.ts
|
|
@@ -6280,6 +6640,8 @@ async function summarizeOrientation(input) {
|
|
|
6280
6640
|
let latestActivityAt = null;
|
|
6281
6641
|
let taskCreatedSeen = false;
|
|
6282
6642
|
let latestNote = null;
|
|
6643
|
+
const commandCounts = /* @__PURE__ */ new Map();
|
|
6644
|
+
const unmeasuredSessions = /* @__PURE__ */ new Set();
|
|
6283
6645
|
const noteActivity = (iso) => {
|
|
6284
6646
|
if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
|
|
6285
6647
|
latestActivityAt = iso;
|
|
@@ -6329,6 +6691,8 @@ async function summarizeOrientation(input) {
|
|
|
6329
6691
|
taskCreatedSeen = true;
|
|
6330
6692
|
} else if (ev.type === "decision_voided") {
|
|
6331
6693
|
voidedDecisionIds.add(ev.decision_id);
|
|
6694
|
+
} else if (ev.type === "command_executed") {
|
|
6695
|
+
commandCounts.set(entry.sessionId, (commandCounts.get(entry.sessionId) ?? 0) + 1);
|
|
6332
6696
|
}
|
|
6333
6697
|
if (counted && ev.type === "note_added" && ev.kind === "next_step") {
|
|
6334
6698
|
recordDirection(entry.sessionId, "note", ev.body);
|
|
@@ -6344,6 +6708,7 @@ async function summarizeOrientation(input) {
|
|
|
6344
6708
|
if (counted) noteActivity(ev.occurred_at);
|
|
6345
6709
|
}
|
|
6346
6710
|
} catch {
|
|
6711
|
+
unmeasuredSessions.add(entry.sessionId);
|
|
6347
6712
|
input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
|
|
6348
6713
|
}
|
|
6349
6714
|
}
|
|
@@ -6437,7 +6802,7 @@ async function summarizeOrientation(input) {
|
|
|
6437
6802
|
const liveEntries = entries.filter(
|
|
6438
6803
|
(e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
|
|
6439
6804
|
);
|
|
6440
|
-
const latestEntry = pickLatestSubstantiveEntry(liveEntries);
|
|
6805
|
+
const latestEntry = pickLatestSubstantiveEntry(liveEntries, commandCounts, unmeasuredSessions);
|
|
6441
6806
|
const latestSession = latestEntry !== void 0 ? {
|
|
6442
6807
|
sessionId: latestEntry.sessionId,
|
|
6443
6808
|
label: latestEntry.session.session.label ?? null,
|
|
@@ -9486,7 +9851,7 @@ var ChildProcessRunner = class {
|
|
|
9486
9851
|
if (killTimer !== null) clearTimeout(killTimer);
|
|
9487
9852
|
options.signal?.removeEventListener("abort", onAbort);
|
|
9488
9853
|
};
|
|
9489
|
-
return new Promise((
|
|
9854
|
+
return new Promise((resolve5, reject) => {
|
|
9490
9855
|
child.once("error", (error) => {
|
|
9491
9856
|
if (settled) return;
|
|
9492
9857
|
settled = true;
|
|
@@ -9498,7 +9863,7 @@ var ChildProcessRunner = class {
|
|
|
9498
9863
|
settled = true;
|
|
9499
9864
|
cleanup();
|
|
9500
9865
|
const ended_at = /* @__PURE__ */ new Date();
|
|
9501
|
-
|
|
9866
|
+
resolve5({
|
|
9502
9867
|
command: snapshotCommand,
|
|
9503
9868
|
args: snapshotArgs,
|
|
9504
9869
|
cwd: snapshotCwd,
|
|
@@ -9613,30 +9978,190 @@ function serializeJsonSchema(schema) {
|
|
|
9613
9978
|
`;
|
|
9614
9979
|
}
|
|
9615
9980
|
|
|
9616
|
-
// src/
|
|
9617
|
-
import {
|
|
9981
|
+
// src/session/observation.ts
|
|
9982
|
+
import { mkdir as mkdir4, readFile as readFile10 } from "fs/promises";
|
|
9618
9983
|
import { join as join20 } from "path";
|
|
9984
|
+
import { z as z12 } from "zod";
|
|
9985
|
+
var SESSION_OBSERVATION_SCHEMA_VERSION = "0.1.0";
|
|
9986
|
+
var ObservedFileSchema = z12.object({
|
|
9987
|
+
/** Absolute path, matching the paths the transcript importer records. */
|
|
9988
|
+
path: z12.string().min(1),
|
|
9989
|
+
change_type: z12.enum(["added", "modified", "deleted", "renamed"]),
|
|
9990
|
+
old_path: z12.string().min(1).optional()
|
|
9991
|
+
});
|
|
9992
|
+
var ObservedRepoSchema = z12.object({
|
|
9993
|
+
/** Absolute path of the git repository root this entry speaks for. */
|
|
9994
|
+
path: z12.string().min(1),
|
|
9995
|
+
/**
|
|
9996
|
+
* `HEAD` as it stood when the session started. `null` when the repository
|
|
9997
|
+
* had no commits yet (a fresh `git init`), in which case only working-tree
|
|
9998
|
+
* changes are observable.
|
|
9999
|
+
*/
|
|
10000
|
+
base_head: z12.string().nullable(),
|
|
10001
|
+
/**
|
|
10002
|
+
* Paths already dirty at session start. They are SUBTRACTED from the
|
|
10003
|
+
* working-tree observation: a file the operator left modified before the
|
|
10004
|
+
* session opened was not changed BY this session, and attributing it would
|
|
10005
|
+
* make the first session after any interrupted work claim the interruption.
|
|
10006
|
+
*/
|
|
10007
|
+
base_dirty: z12.array(z12.string()).default([]),
|
|
10008
|
+
/** Latest full recomputation for this repository (see `observeSession`). */
|
|
10009
|
+
files: z12.array(ObservedFileSchema).default([])
|
|
10010
|
+
});
|
|
10011
|
+
var SessionObservationSchema = z12.object({
|
|
10012
|
+
schema_version: z12.string().min(1),
|
|
10013
|
+
external_id: z12.string().min(1),
|
|
10014
|
+
started_at: z12.string().min(1),
|
|
10015
|
+
updated_at: z12.string().min(1),
|
|
10016
|
+
repos: z12.array(ObservedRepoSchema).default([])
|
|
10017
|
+
});
|
|
10018
|
+
var SAFE_EXTERNAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10019
|
+
function sessionObservationPath(observationsDir, externalId) {
|
|
10020
|
+
if (!SAFE_EXTERNAL_ID.test(externalId)) return null;
|
|
10021
|
+
if (externalId === "." || externalId === "..") return null;
|
|
10022
|
+
return join20(observationsDir, `${externalId}.json`);
|
|
10023
|
+
}
|
|
10024
|
+
async function readSessionObservation(observationsDir, externalId) {
|
|
10025
|
+
const file = sessionObservationPath(observationsDir, externalId);
|
|
10026
|
+
if (file === null) return null;
|
|
10027
|
+
let raw;
|
|
10028
|
+
try {
|
|
10029
|
+
raw = await readFile10(file, "utf8");
|
|
10030
|
+
} catch {
|
|
10031
|
+
return null;
|
|
10032
|
+
}
|
|
10033
|
+
let parsed;
|
|
10034
|
+
try {
|
|
10035
|
+
parsed = JSON.parse(raw);
|
|
10036
|
+
} catch {
|
|
10037
|
+
return null;
|
|
10038
|
+
}
|
|
10039
|
+
const result = SessionObservationSchema.safeParse(parsed);
|
|
10040
|
+
if (!result.success) return null;
|
|
10041
|
+
if (result.data.schema_version !== SESSION_OBSERVATION_SCHEMA_VERSION) return null;
|
|
10042
|
+
return result.data;
|
|
10043
|
+
}
|
|
10044
|
+
async function writeSessionObservation(observationsDir, observation) {
|
|
10045
|
+
const file = sessionObservationPath(observationsDir, observation.external_id);
|
|
10046
|
+
if (file === null) return;
|
|
10047
|
+
await mkdir4(observationsDir, { recursive: true });
|
|
10048
|
+
await atomicReplace(file, `${JSON.stringify(observation, null, 2)}
|
|
10049
|
+
`);
|
|
10050
|
+
}
|
|
10051
|
+
function observedFilesOf(observation) {
|
|
10052
|
+
const files = [];
|
|
10053
|
+
for (const repo of observation.repos) files.push(...repo.files);
|
|
10054
|
+
return files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
10055
|
+
}
|
|
10056
|
+
function observedFileFrom(repoRoot, change) {
|
|
10057
|
+
return {
|
|
10058
|
+
path: join20(repoRoot, change.path),
|
|
10059
|
+
change_type: change.status,
|
|
10060
|
+
...change.old_path !== void 0 ? { old_path: join20(repoRoot, change.old_path) } : {}
|
|
10061
|
+
};
|
|
10062
|
+
}
|
|
10063
|
+
|
|
10064
|
+
// src/session/observe.ts
|
|
10065
|
+
import { resolve as resolve4 } from "path";
|
|
10066
|
+
function observedRepoRoots(root, manifest) {
|
|
10067
|
+
const declared = manifest.repos ?? [];
|
|
10068
|
+
const roots = declared.length > 0 ? declared.map((repo) => resolve4(root, repo.path)) : [root];
|
|
10069
|
+
return [...new Set(roots)];
|
|
10070
|
+
}
|
|
10071
|
+
async function recordSessionBaseline(input) {
|
|
10072
|
+
const existing = await readSessionObservation(input.observationsDir, input.externalId);
|
|
10073
|
+
if (existing !== null) return existing;
|
|
10074
|
+
const repos = [];
|
|
10075
|
+
for (const repoRoot of input.repoRoots) {
|
|
10076
|
+
let baseHead;
|
|
10077
|
+
try {
|
|
10078
|
+
baseHead = await readHeadSha(repoRoot);
|
|
10079
|
+
} catch {
|
|
10080
|
+
continue;
|
|
10081
|
+
}
|
|
10082
|
+
let baseDirty = [];
|
|
10083
|
+
try {
|
|
10084
|
+
baseDirty = (await getWorkingTreeChanges(repoRoot)).map(
|
|
10085
|
+
(change) => observedFileFrom(repoRoot, change).path
|
|
10086
|
+
);
|
|
10087
|
+
} catch {
|
|
10088
|
+
}
|
|
10089
|
+
repos.push({ path: repoRoot, base_head: baseHead, base_dirty: baseDirty, files: [] });
|
|
10090
|
+
}
|
|
10091
|
+
if (repos.length === 0) return null;
|
|
10092
|
+
const observation = {
|
|
10093
|
+
schema_version: SESSION_OBSERVATION_SCHEMA_VERSION,
|
|
10094
|
+
external_id: input.externalId,
|
|
10095
|
+
started_at: input.nowIso,
|
|
10096
|
+
updated_at: input.nowIso,
|
|
10097
|
+
repos
|
|
10098
|
+
};
|
|
10099
|
+
await writeSessionObservation(input.observationsDir, observation);
|
|
10100
|
+
return observation;
|
|
10101
|
+
}
|
|
10102
|
+
async function observeSessionChanges(input) {
|
|
10103
|
+
const existing = await readSessionObservation(input.observationsDir, input.externalId);
|
|
10104
|
+
if (existing === null) return null;
|
|
10105
|
+
const repos = [];
|
|
10106
|
+
for (const repo of existing.repos) {
|
|
10107
|
+
let files;
|
|
10108
|
+
try {
|
|
10109
|
+
files = await changedSinceBaseline(repo);
|
|
10110
|
+
} catch {
|
|
10111
|
+
repos.push(repo);
|
|
10112
|
+
continue;
|
|
10113
|
+
}
|
|
10114
|
+
repos.push({ ...repo, files });
|
|
10115
|
+
}
|
|
10116
|
+
const updated = { ...existing, updated_at: input.nowIso, repos };
|
|
10117
|
+
await writeSessionObservation(input.observationsDir, updated);
|
|
10118
|
+
return updated;
|
|
10119
|
+
}
|
|
10120
|
+
function isBasouStorePath(relativePath) {
|
|
10121
|
+
return relativePath === ".basou" || relativePath.startsWith(".basou/");
|
|
10122
|
+
}
|
|
10123
|
+
async function changedSinceBaseline(repo) {
|
|
10124
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
10125
|
+
const base = repo.base_head ?? await readEmptyTreeSha(repo.path);
|
|
10126
|
+
for (const change of await getChangesSince(repo.path, base)) {
|
|
10127
|
+
if (isBasouStorePath(change.path)) continue;
|
|
10128
|
+
const file = observedFileFrom(repo.path, change);
|
|
10129
|
+
byPath.set(file.path, file);
|
|
10130
|
+
}
|
|
10131
|
+
for (const change of await getUntrackedFiles(repo.path)) {
|
|
10132
|
+
if (isBasouStorePath(change.path)) continue;
|
|
10133
|
+
const file = observedFileFrom(repo.path, change);
|
|
10134
|
+
if (!byPath.has(file.path)) byPath.set(file.path, file);
|
|
10135
|
+
}
|
|
10136
|
+
const preexisting = new Set(repo.base_dirty);
|
|
10137
|
+
return [...byPath.values()].filter((file) => !preexisting.has(file.path)).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
10138
|
+
}
|
|
10139
|
+
|
|
10140
|
+
// src/storage/basou-dir.ts
|
|
10141
|
+
import { lstat as lstat4, mkdir as mkdir5 } from "fs/promises";
|
|
10142
|
+
import { join as join21 } from "path";
|
|
9619
10143
|
function basouPaths(repositoryRoot) {
|
|
9620
|
-
const root =
|
|
9621
|
-
const approvalsBase =
|
|
10144
|
+
const root = join21(repositoryRoot, ".basou");
|
|
10145
|
+
const approvalsBase = join21(root, "approvals");
|
|
9622
10146
|
return {
|
|
9623
10147
|
root,
|
|
9624
|
-
sessions:
|
|
9625
|
-
tasks:
|
|
10148
|
+
sessions: join21(root, "sessions"),
|
|
10149
|
+
tasks: join21(root, "tasks"),
|
|
9626
10150
|
approvals: {
|
|
9627
|
-
pending:
|
|
9628
|
-
resolved:
|
|
10151
|
+
pending: join21(approvalsBase, "pending"),
|
|
10152
|
+
resolved: join21(approvalsBase, "resolved")
|
|
9629
10153
|
},
|
|
9630
|
-
locks:
|
|
9631
|
-
logs:
|
|
9632
|
-
raw:
|
|
9633
|
-
tmp:
|
|
10154
|
+
locks: join21(root, "locks"),
|
|
10155
|
+
logs: join21(root, "logs"),
|
|
10156
|
+
raw: join21(root, "raw"),
|
|
10157
|
+
tmp: join21(root, "tmp"),
|
|
10158
|
+
observations: join21(root, "tmp", "observations"),
|
|
9634
10159
|
files: {
|
|
9635
|
-
manifest:
|
|
9636
|
-
status:
|
|
9637
|
-
handoff:
|
|
9638
|
-
decisions:
|
|
9639
|
-
orientation:
|
|
10160
|
+
manifest: join21(root, "manifest.yaml"),
|
|
10161
|
+
status: join21(root, "status.json"),
|
|
10162
|
+
handoff: join21(root, "handoff.md"),
|
|
10163
|
+
decisions: join21(root, "decisions.md"),
|
|
10164
|
+
orientation: join21(root, "orientation.md")
|
|
9640
10165
|
}
|
|
9641
10166
|
};
|
|
9642
10167
|
}
|
|
@@ -9677,7 +10202,7 @@ async function ensureBasouDirectory(repositoryRoot) {
|
|
|
9677
10202
|
}
|
|
9678
10203
|
async function mkdirLabeled(target, label) {
|
|
9679
10204
|
try {
|
|
9680
|
-
await
|
|
10205
|
+
await mkdir5(target, { recursive: true });
|
|
9681
10206
|
} catch (error) {
|
|
9682
10207
|
if (hasErrorCode5(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
|
|
9683
10208
|
throw new Error(`${label} exists but is not a directory`, { cause: error });
|
|
@@ -9692,17 +10217,17 @@ function hasErrorCode5(error) {
|
|
|
9692
10217
|
}
|
|
9693
10218
|
|
|
9694
10219
|
// src/storage/gitignore.ts
|
|
9695
|
-
import { readFile as
|
|
9696
|
-
import { join as
|
|
10220
|
+
import { readFile as readFile11, writeFile as writeFile2 } from "fs/promises";
|
|
10221
|
+
import { join as join22 } from "path";
|
|
9697
10222
|
var MARKER = "# Basou - default ignore";
|
|
9698
10223
|
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/orientation.md\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";
|
|
9699
10224
|
var BASOU_GITIGNORE_BLOCK_LOCAL_ONLY = "# Basou - default ignore\n# Local-only: basou's trail is never committed (personal/local state,\n# regenerable by re-importing from the agents' own logs). Recommended for\n# monitored repos and any workspace kept out of version control.\n.basou/\n";
|
|
9700
10225
|
async function appendBasouGitignore(repositoryRoot, options = {}) {
|
|
9701
|
-
const gitignorePath =
|
|
10226
|
+
const gitignorePath = join22(repositoryRoot, ".gitignore");
|
|
9702
10227
|
let body;
|
|
9703
10228
|
let existed;
|
|
9704
10229
|
try {
|
|
9705
|
-
body = await
|
|
10230
|
+
body = await readFile11(gitignorePath, "utf8");
|
|
9706
10231
|
existed = true;
|
|
9707
10232
|
} catch (error) {
|
|
9708
10233
|
if (hasErrorCode6(error) && error.code === "ENOENT") {
|
|
@@ -9745,9 +10270,9 @@ function hasErrorCode6(error) {
|
|
|
9745
10270
|
}
|
|
9746
10271
|
|
|
9747
10272
|
// src/storage/session-import.ts
|
|
9748
|
-
import { mkdir as
|
|
10273
|
+
import { mkdir as mkdir6, readFile as readFile12, rm as rm2 } from "fs/promises";
|
|
9749
10274
|
import { homedir as homedir4 } from "os";
|
|
9750
|
-
import { join as
|
|
10275
|
+
import { join as join23 } from "path";
|
|
9751
10276
|
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
9752
10277
|
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
9753
10278
|
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
@@ -9772,9 +10297,9 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
9772
10297
|
pathSanitizeReport
|
|
9773
10298
|
};
|
|
9774
10299
|
}
|
|
9775
|
-
const sessionDir =
|
|
10300
|
+
const sessionDir = join23(paths.sessions, newSessionId);
|
|
9776
10301
|
try {
|
|
9777
|
-
await
|
|
10302
|
+
await mkdir6(sessionDir, { recursive: true });
|
|
9778
10303
|
} catch (error) {
|
|
9779
10304
|
throw new Error("Failed to create session directory", { cause: error });
|
|
9780
10305
|
}
|
|
@@ -9786,7 +10311,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
9786
10311
|
throw error;
|
|
9787
10312
|
}
|
|
9788
10313
|
try {
|
|
9789
|
-
const sessionYamlPath =
|
|
10314
|
+
const sessionYamlPath = join23(sessionDir, "session.yaml");
|
|
9790
10315
|
await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
|
|
9791
10316
|
} catch (error) {
|
|
9792
10317
|
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
@@ -9954,7 +10479,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
|
|
|
9954
10479
|
async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
|
|
9955
10480
|
const sessionId = priorSessionId;
|
|
9956
10481
|
const importSource = freshPayload.session.source.kind;
|
|
9957
|
-
const sessionDir =
|
|
10482
|
+
const sessionDir = join23(paths.sessions, priorSessionId);
|
|
9958
10483
|
const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
|
|
9959
10484
|
try {
|
|
9960
10485
|
const priorVerdict = await verifyEventsChain(paths, priorSessionId);
|
|
@@ -9999,10 +10524,10 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
9999
10524
|
session: preservedInner
|
|
10000
10525
|
};
|
|
10001
10526
|
if (options.dryRun !== true) {
|
|
10002
|
-
const eventsPath =
|
|
10527
|
+
const eventsPath = join23(sessionDir, "events.jsonl");
|
|
10003
10528
|
let priorEventsRaw = null;
|
|
10004
10529
|
try {
|
|
10005
|
-
priorEventsRaw = await
|
|
10530
|
+
priorEventsRaw = await readFile12(eventsPath);
|
|
10006
10531
|
} catch (error) {
|
|
10007
10532
|
if (!findErrorCode(error, "ENOENT")) {
|
|
10008
10533
|
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
@@ -10011,7 +10536,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
10011
10536
|
const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
|
|
10012
10537
|
try {
|
|
10013
10538
|
await overwriteYamlFile(
|
|
10014
|
-
|
|
10539
|
+
join23(sessionDir, "session.yaml"),
|
|
10015
10540
|
withIntegrity(updatedRecord, chainResult)
|
|
10016
10541
|
);
|
|
10017
10542
|
} catch (error) {
|
|
@@ -10035,7 +10560,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
10035
10560
|
}
|
|
10036
10561
|
}
|
|
10037
10562
|
async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
10038
|
-
const sessionDir =
|
|
10563
|
+
const sessionDir = join23(paths.sessions, sessionId);
|
|
10039
10564
|
let lock;
|
|
10040
10565
|
try {
|
|
10041
10566
|
lock = await acquireLock(paths, "session", sessionId);
|
|
@@ -10068,10 +10593,10 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
10068
10593
|
if (verdict.status !== "unchained") {
|
|
10069
10594
|
return { status: "skipped", reason: "tampered" };
|
|
10070
10595
|
}
|
|
10071
|
-
const eventsPath =
|
|
10596
|
+
const eventsPath = join23(sessionDir, "events.jsonl");
|
|
10072
10597
|
let priorRaw;
|
|
10073
10598
|
try {
|
|
10074
|
-
priorRaw = await
|
|
10599
|
+
priorRaw = await readFile12(eventsPath);
|
|
10075
10600
|
} catch (error) {
|
|
10076
10601
|
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
10077
10602
|
}
|
|
@@ -10116,7 +10641,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
10116
10641
|
}
|
|
10117
10642
|
try {
|
|
10118
10643
|
await overwriteYamlFile(
|
|
10119
|
-
|
|
10644
|
+
join23(sessionDir, "session.yaml"),
|
|
10120
10645
|
withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
|
|
10121
10646
|
);
|
|
10122
10647
|
} catch (error) {
|
|
@@ -10167,6 +10692,7 @@ export {
|
|
|
10167
10692
|
REVIEW_RECORD_NO_INPUT_HINT,
|
|
10168
10693
|
RiskLevelSchema,
|
|
10169
10694
|
SESSION_IMPORT_SCHEMA_VERSION,
|
|
10695
|
+
SESSION_OBSERVATION_SCHEMA_VERSION,
|
|
10170
10696
|
SESSION_SCHEMA_VERSION,
|
|
10171
10697
|
SESSION_START_HOOK_CONTEXT_LIMIT,
|
|
10172
10698
|
SESSION_START_HOOK_MATCHER,
|
|
@@ -10231,20 +10757,27 @@ export {
|
|
|
10231
10757
|
finalizeSessionYaml,
|
|
10232
10758
|
findBasouSessionStartHook,
|
|
10233
10759
|
findBasouStopHookCommand,
|
|
10760
|
+
findClaudeSessionStartHooks,
|
|
10234
10761
|
findDecisionGaps,
|
|
10235
10762
|
findErrorCode,
|
|
10236
10763
|
findReviewGaps,
|
|
10237
10764
|
findUnbindableRepos,
|
|
10765
|
+
findUnrecognizedSessionStart,
|
|
10238
10766
|
formatDurationMs,
|
|
10239
10767
|
genesisHash,
|
|
10768
|
+
getChangesSince,
|
|
10240
10769
|
getDiff,
|
|
10241
10770
|
getSnapshot,
|
|
10771
|
+
getWorkingTreeChanges,
|
|
10242
10772
|
hasRetiredZeroDuration,
|
|
10243
10773
|
importSessionFromJson,
|
|
10244
10774
|
inspectChainTail,
|
|
10245
10775
|
instructionMode,
|
|
10776
|
+
isBasouOrientSessionStartCommand,
|
|
10246
10777
|
isBasouSessionStartHookCommand,
|
|
10247
10778
|
isBasouStopHookCommand,
|
|
10779
|
+
isClaudeSessionStartHookCommand,
|
|
10780
|
+
isClaudeSessionStartMalformed,
|
|
10248
10781
|
isGitNotFound,
|
|
10249
10782
|
isImportDerivedSource,
|
|
10250
10783
|
isLazyExpired,
|
|
@@ -10259,6 +10792,9 @@ export {
|
|
|
10259
10792
|
loadTaskEntries,
|
|
10260
10793
|
normalizeRepoKey,
|
|
10261
10794
|
normalizeRepoPath,
|
|
10795
|
+
observeSessionChanges,
|
|
10796
|
+
observedFilesOf,
|
|
10797
|
+
observedRepoRoots,
|
|
10262
10798
|
overwriteYamlFile,
|
|
10263
10799
|
parseBuildStamp,
|
|
10264
10800
|
parseDuration,
|
|
@@ -10277,9 +10813,11 @@ export {
|
|
|
10277
10813
|
protocolSectionsFrom,
|
|
10278
10814
|
protocolUpdateToken,
|
|
10279
10815
|
readAllEvents,
|
|
10816
|
+
readHeadSha,
|
|
10280
10817
|
readManifest,
|
|
10281
10818
|
readMarkdownFile,
|
|
10282
10819
|
readObservedDuration,
|
|
10820
|
+
readSessionObservation,
|
|
10283
10821
|
readSessionYaml,
|
|
10284
10822
|
readStatus,
|
|
10285
10823
|
readTaskFile,
|
|
@@ -10289,8 +10827,10 @@ export {
|
|
|
10289
10827
|
reconcileAllTasks,
|
|
10290
10828
|
reconcileSourceRoots,
|
|
10291
10829
|
reconcileTask,
|
|
10830
|
+
recordSessionBaseline,
|
|
10292
10831
|
refreshTaskLinkedSessions,
|
|
10293
10832
|
reimportPreservingId,
|
|
10833
|
+
removeClaudeSessionStartHook,
|
|
10294
10834
|
removeMarkerSection,
|
|
10295
10835
|
removeSessionStartHook,
|
|
10296
10836
|
removeStopHook,
|
|
@@ -10323,6 +10863,7 @@ export {
|
|
|
10323
10863
|
seedMarkers,
|
|
10324
10864
|
serializeEventLine,
|
|
10325
10865
|
serializeJsonSchema,
|
|
10866
|
+
sessionObservationPath,
|
|
10326
10867
|
sessionWorkStatsFromEvents,
|
|
10327
10868
|
summarizeAdapterOutput,
|
|
10328
10869
|
summarizeOrientation,
|
|
@@ -10337,6 +10878,7 @@ export {
|
|
|
10337
10878
|
unknownManifestKeys,
|
|
10338
10879
|
unstampedProtocolSectionsFrom,
|
|
10339
10880
|
updateTaskStatusWithEvent,
|
|
10881
|
+
upsertClaudeSessionStartHook,
|
|
10340
10882
|
upsertSessionStartHook,
|
|
10341
10883
|
upsertStopHook,
|
|
10342
10884
|
verifyEventsChain,
|
|
@@ -10345,6 +10887,7 @@ export {
|
|
|
10345
10887
|
writeManifest,
|
|
10346
10888
|
writeMarkdownFile,
|
|
10347
10889
|
writeObservedDuration,
|
|
10890
|
+
writeSessionObservation,
|
|
10348
10891
|
writeStatus,
|
|
10349
10892
|
writeTaskFile,
|
|
10350
10893
|
writeYamlFile
|