@basou/core 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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 shellQuote(value) {
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 ${shellQuote(options.cliEntry)} hook stop${suffix} 2>/dev/null || true`;
170
+ return `node ${shellQuote2(options.cliEntry)} hook stop${suffix} 2>/dev/null || true`;
42
171
  }
43
- function isRecord(value) {
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 (!isRecord(settings)) {
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 (!isRecord(root.hooks)) {
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 (!isRecord(group) || !Array.isArray(group.hooks)) continue;
197
+ if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
69
198
  for (const entry of group.hooks) {
70
- if (!isRecord(entry)) continue;
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 (!isRecord(root.hooks) || !Array.isArray(root.hooks.Stop)) {
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 (!isRecord(group) || !Array.isArray(group.hooks)) {
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 (isRecord(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
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 (!isRecord(settings) || !isRecord(settings.hooks) || !Array.isArray(settings.hooks.Stop)) {
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 (!isRecord(group) || !Array.isArray(group.hooks)) continue;
258
+ if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
130
259
  for (const entry of group.hooks) {
131
- if (isRecord(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
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) {
@@ -1065,135 +1373,6 @@ async function resolveCodexCommand(lookup = isOnPath) {
1065
1373
  throw new Error("Codex CLI not found in PATH. Install codex first.");
1066
1374
  }
1067
1375
 
1068
- // src/adapters/codex/hooks-json.ts
1069
- var SESSION_START_HOOK_TIMEOUT_SECONDS = 30;
1070
- var SESSION_START_HOOK_MATCHER = "startup|resume|clear";
1071
- var SESSION_START_HOOK_CONTEXT_LIMIT = 0;
1072
- var SESSION_START_HOOK_STATUS_MESSAGE = "basou orient";
1073
- var BASOU_SESSION_START_HOOK = /(?:\bbasou|(?:@basou|packages)\/cli\/dist\/index\.js['"]?)\s+hook\s+session-start\b/;
1074
- function isBasouSessionStartHookCommand(command) {
1075
- return BASOU_SESSION_START_HOOK.test(command);
1076
- }
1077
- function shellQuote2(value) {
1078
- return `'${value.replace(/'/g, "'\\''")}'`;
1079
- }
1080
- function buildSessionStartHookCommand(options) {
1081
- return `node ${shellQuote2(options.cliEntry)} hook session-start 2>/dev/null || true`;
1082
- }
1083
- function isRecord2(value) {
1084
- return typeof value === "object" && value !== null && !Array.isArray(value);
1085
- }
1086
- function cloneHooksFile(hooksFile) {
1087
- if (hooksFile === void 0 || hooksFile === null) return {};
1088
- if (!isRecord2(hooksFile)) {
1089
- throw new Error("The Codex hooks.json is not a JSON object.");
1090
- }
1091
- return structuredClone(hooksFile);
1092
- }
1093
- function canonicalHandler(command) {
1094
- return {
1095
- type: "command",
1096
- command,
1097
- timeout: SESSION_START_HOOK_TIMEOUT_SECONDS,
1098
- statusMessage: SESSION_START_HOOK_STATUS_MESSAGE,
1099
- additionalContextLimit: SESSION_START_HOOK_CONTEXT_LIMIT
1100
- };
1101
- }
1102
- function handlerIsCanonical(entry, command) {
1103
- const want = canonicalHandler(command);
1104
- return Object.keys(want).every((k) => entry[k] === want[k]);
1105
- }
1106
- function upsertSessionStartHook(hooksFile, command) {
1107
- const root = cloneHooksFile(hooksFile);
1108
- if (root.hooks === void 0) {
1109
- root.hooks = {};
1110
- } else if (!isRecord2(root.hooks)) {
1111
- throw new Error("The 'hooks' key in the Codex hooks.json is not an object.");
1112
- }
1113
- const hooks = root.hooks;
1114
- if (hooks.SessionStart === void 0) {
1115
- hooks.SessionStart = [];
1116
- } else if (!Array.isArray(hooks.SessionStart)) {
1117
- throw new Error("The 'hooks.SessionStart' key in the Codex hooks.json is not an array.");
1118
- }
1119
- const groups = hooks.SessionStart;
1120
- for (const group of groups) {
1121
- if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
1122
- for (const entry of group.hooks) {
1123
- if (!isRecord2(entry)) continue;
1124
- if (typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
1125
- const unchanged = handlerIsCanonical(entry, command);
1126
- Object.assign(entry, canonicalHandler(command));
1127
- return { hooksFile: root, action: unchanged ? "unchanged" : "updated" };
1128
- }
1129
- }
1130
- }
1131
- groups.push({ matcher: SESSION_START_HOOK_MATCHER, hooks: [canonicalHandler(command)] });
1132
- return { hooksFile: root, action: "installed" };
1133
- }
1134
- function removeSessionStartHook(hooksFile) {
1135
- const root = cloneHooksFile(hooksFile);
1136
- if (!isRecord2(root.hooks) || !Array.isArray(root.hooks.SessionStart)) {
1137
- return { hooksFile: root, action: "absent" };
1138
- }
1139
- const hooks = root.hooks;
1140
- const groups = hooks.SessionStart;
1141
- let removed = false;
1142
- const kept = [];
1143
- for (const group of groups) {
1144
- if (!isRecord2(group) || !Array.isArray(group.hooks)) {
1145
- kept.push(group);
1146
- continue;
1147
- }
1148
- const keptHandlers = group.hooks.filter((entry) => {
1149
- if (isRecord2(entry) && typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
1150
- removed = true;
1151
- return false;
1152
- }
1153
- return true;
1154
- });
1155
- if (keptHandlers.length === group.hooks.length) {
1156
- kept.push(group);
1157
- } else if (keptHandlers.length > 0) {
1158
- group.hooks = keptHandlers;
1159
- kept.push(group);
1160
- }
1161
- }
1162
- if (!removed) return { hooksFile: root, action: "absent" };
1163
- if (kept.length === 0) {
1164
- delete hooks.SessionStart;
1165
- } else {
1166
- hooks.SessionStart = kept;
1167
- }
1168
- if (Object.keys(hooks).length === 0) {
1169
- delete root.hooks;
1170
- }
1171
- return { hooksFile: root, action: "removed" };
1172
- }
1173
- function findBasouSessionStartHook(hooksFile) {
1174
- if (!isRecord2(hooksFile) || !isRecord2(hooksFile.hooks) || !Array.isArray(hooksFile.hooks.SessionStart)) {
1175
- return null;
1176
- }
1177
- const groups = hooksFile.hooks.SessionStart;
1178
- for (let g = 0; g < groups.length; g++) {
1179
- const group = groups[g];
1180
- if (!isRecord2(group) || !Array.isArray(group.hooks)) continue;
1181
- for (let h = 0; h < group.hooks.length; h++) {
1182
- const entry = group.hooks[h];
1183
- if (isRecord2(entry) && typeof entry.command === "string" && isBasouSessionStartHookCommand(entry.command)) {
1184
- return {
1185
- command: entry.command,
1186
- groupIndex: g,
1187
- handlerIndex: h,
1188
- matcher: typeof group.matcher === "string" ? group.matcher : void 0,
1189
- handler: structuredClone(entry)
1190
- };
1191
- }
1192
- }
1193
- }
1194
- return null;
1195
- }
1196
-
1197
1376
  // src/schemas/observed-duration.ts
1198
1377
  function readObservedDuration(ev) {
1199
1378
  const stored = ev.duration_ms;
@@ -4356,6 +4535,31 @@ async function countOpenDecisionGaps(input) {
4356
4535
  import { lstat as lstat2 } from "fs/promises";
4357
4536
  import { dirname as dirname2, join as join11, resolve } from "path";
4358
4537
 
4538
+ // src/lib/display-path.ts
4539
+ function displayPath(path2) {
4540
+ let shown = "";
4541
+ for (const c of path2) {
4542
+ const code = c.codePointAt(0) ?? 0;
4543
+ if (!isActing(code)) {
4544
+ shown += c;
4545
+ } else if (c === "\n") {
4546
+ shown += "\\n";
4547
+ } else if (c === "\r") {
4548
+ shown += "\\r";
4549
+ } else if (c === " ") {
4550
+ shown += "\\t";
4551
+ } else if (code <= 255) {
4552
+ shown += `\\x${code.toString(16).padStart(2, "0")}`;
4553
+ } else {
4554
+ shown += `\\u${code.toString(16).padStart(4, "0")}`;
4555
+ }
4556
+ }
4557
+ return shown;
4558
+ }
4559
+ function isActing(code) {
4560
+ return code <= 31 || code >= 127 && code <= 159 || code === 8232 || code === 8233 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297;
4561
+ }
4562
+
4359
4563
  // src/lib/one-line.ts
4360
4564
  function oneLine(text) {
4361
4565
  return text.replace(/\s+/g, " ").trim();
@@ -5139,10 +5343,10 @@ async function formatDecisionsBody(args) {
5139
5343
  if (d.linkedFiles !== void 0 && d.linkedFiles.length > 0) {
5140
5344
  const parts = await Promise.all(
5141
5345
  d.linkedFiles.map(
5142
- async (path2) => await args.fileExists(path2) ? path2 : `${path2} (missing)`
5346
+ async (path2) => await args.fileExists(path2) ? displayPath(path2) : `${displayPath(path2)} (missing)`
5143
5347
  )
5144
5348
  );
5145
- lines.push(`- linked_files: ${oneLine(parts.join(", "))}`);
5349
+ lines.push(`- linked_files: ${parts.join(", ")}`);
5146
5350
  }
5147
5351
  lines.push("");
5148
5352
  }
@@ -5621,7 +5825,7 @@ async function getDiff(repoRoot, baseRef, headRef) {
5621
5825
  if (baseRef === headRef) return { changed_files: [] };
5622
5826
  let raw;
5623
5827
  try {
5624
- raw = await git.raw(["diff", "--name-status", `${baseRef}..${headRef}`]);
5828
+ raw = await git.raw(["diff", "--name-status", "-z", `${baseRef}..${headRef}`]);
5625
5829
  } catch (error) {
5626
5830
  throw translateDiffError(error);
5627
5831
  }
@@ -5639,7 +5843,7 @@ async function getChangesSince(repoRoot, baseRef) {
5639
5843
  }
5640
5844
  let raw;
5641
5845
  try {
5642
- raw = await git.raw(["-c", "core.quotePath=false", "diff", "--name-status", baseRef]);
5846
+ raw = await git.raw(["diff", "--name-status", "-z", baseRef]);
5643
5847
  } catch (error) {
5644
5848
  throw translateDiffError(error);
5645
5849
  }
@@ -5659,27 +5863,25 @@ function translateDiffError(error) {
5659
5863
  return new Error("Failed to compute git diff", { cause: error });
5660
5864
  }
5661
5865
  function parseDiffNameStatus(raw) {
5662
- const lines = raw.split("\n").filter((l) => l.trim() !== "");
5866
+ const fields = raw.split("\0");
5663
5867
  const changes = [];
5664
- for (const line of lines) {
5665
- const parts = line.split(" ");
5666
- const code = parts[0];
5667
- if (code === void 0 || code.length === 0) continue;
5668
- if (code.startsWith("R") && parts.length >= 3) {
5669
- const newPath = parts[2];
5670
- const oldPath = parts[1];
5671
- if (newPath === void 0) continue;
5672
- changes.push({
5673
- path: newPath,
5674
- status: "renamed",
5675
- ...oldPath !== void 0 ? { old_path: oldPath } : {}
5676
- });
5677
- } else if (code === "A" && parts[1]) {
5678
- changes.push({ path: parts[1], status: "added" });
5679
- } else if (code === "M" && parts[1]) {
5680
- changes.push({ path: parts[1], status: "modified" });
5681
- } else if (code === "D" && parts[1]) {
5682
- changes.push({ path: parts[1], status: "deleted" });
5868
+ let i = 0;
5869
+ while (i < fields.length) {
5870
+ const code = fields[i] ?? "";
5871
+ const twoPaths = code.startsWith("R") || code.startsWith("C");
5872
+ const first = fields[i + 1];
5873
+ const second = twoPaths ? fields[i + 2] : void 0;
5874
+ i += twoPaths ? 3 : 2;
5875
+ if (first === void 0 || first.length === 0) continue;
5876
+ if (code.startsWith("R")) {
5877
+ if (second === void 0 || second.length === 0) continue;
5878
+ changes.push({ path: second, status: "renamed", old_path: first });
5879
+ } else if (code === "A") {
5880
+ changes.push({ path: first, status: "added" });
5881
+ } else if (code === "M") {
5882
+ changes.push({ path: first, status: "modified" });
5883
+ } else if (code === "D") {
5884
+ changes.push({ path: first, status: "deleted" });
5683
5885
  }
5684
5886
  }
5685
5887
  return changes;
@@ -6091,7 +6293,7 @@ function formatHandoffBody(args) {
6091
6293
  if (args.displayedFiles.length === 0) {
6092
6294
  lines.push("(no related files recorded)");
6093
6295
  } else {
6094
- for (const f of args.displayedFiles) lines.push(`- ${f}`);
6296
+ for (const f of args.displayedFiles) lines.push(`- ${displayPath(f)}`);
6095
6297
  if (args.overflow > 0) lines.push(`- ... +${args.overflow} more`);
6096
6298
  }
6097
6299
  lines.push("");
@@ -6144,7 +6346,7 @@ function formatHandoffBody(args) {
6144
6346
  lines.push(t.handoff.headingReadNext);
6145
6347
  lines.push("");
6146
6348
  lines.push("- .basou/decisions.md");
6147
- for (const f of args.displayedFiles.slice(0, 3)) lines.push(`- ${f}`);
6349
+ for (const f of args.displayedFiles.slice(0, 3)) lines.push(`- ${displayPath(f)}`);
6148
6350
  lines.push("");
6149
6351
  lines.push(t.handoff.headingNextWork);
6150
6352
  lines.push("");
@@ -6251,7 +6453,7 @@ function parseBuildStamp(raw) {
6251
6453
  }
6252
6454
  }
6253
6455
  var BASOU_CORE_BUILD = parseBuildStamp(
6254
- true ? '{"version":"0.49.0","commit":"6c4704a","committedAt":"2026-09-23T11:46:39+09:00"}' : void 0
6456
+ true ? '{"version":"0.51.0","commit":"22ef44f","committedAt":"2026-09-23T22:27:59+09:00"}' : void 0
6255
6457
  );
6256
6458
 
6257
6459
  // src/lib/duration.ts
@@ -6781,7 +6983,7 @@ function formatOrientationBody(summary, opts) {
6781
6983
  const parts = [];
6782
6984
  if (summary.relatedFiles.displayed.length > 0) {
6783
6985
  const more = summary.relatedFiles.overflow > 0 ? ` (... +${summary.relatedFiles.overflow} more)` : "";
6784
- parts.push(`${summary.relatedFiles.displayed.join(", ")}${more}`);
6986
+ parts.push(`${summary.relatedFiles.displayed.map(displayPath).join(", ")}${more}`);
6785
6987
  }
6786
6988
  if (summary.relatedFiles.omitted > 0) {
6787
6989
  parts.push(t.orientation.scratchOmitted(summary.relatedFiles.omitted));
@@ -6790,7 +6992,7 @@ function formatOrientationBody(summary, opts) {
6790
6992
  if (summary.relatedFiles.outOfRoot.length > 0) {
6791
6993
  const OUT_OF_ROOT_DISPLAY = 10;
6792
6994
  const out = summary.relatedFiles.outOfRoot;
6793
- const shownOut = out.slice(0, OUT_OF_ROOT_DISPLAY).join(", ");
6995
+ const shownOut = out.slice(0, OUT_OF_ROOT_DISPLAY).map(displayPath).join(", ");
6794
6996
  const outMore = out.length > OUT_OF_ROOT_DISPLAY ? ` (... +${out.length - OUT_OF_ROOT_DISPLAY} more)` : "";
6795
6997
  lines.push(` - ${t.orientation.outOfRootWarning(out.length, `${shownOut}${outMore}`)}`);
6796
6998
  }
@@ -6818,7 +7020,9 @@ function formatOrientationBody(summary, opts) {
6818
7020
  lines.push(` - ${t.orientation.recentNextStepLabel}: ${noteSummary(note)}`);
6819
7021
  }
6820
7022
  if (s.files.length > 0) {
6821
- lines.push(` - ${t.orientation.recentChangedLabel}: ${s.files.join(", ")}`);
7023
+ lines.push(
7024
+ ` - ${t.orientation.recentChangedLabel}: ${s.files.map(displayPath).join(", ")}`
7025
+ );
6822
7026
  }
6823
7027
  }
6824
7028
  }
@@ -8614,7 +8818,9 @@ function formatReportBody(data, t) {
8614
8818
  if (data.changedFiles.length === 0) {
8615
8819
  lines.push("(no related files recorded)");
8616
8820
  } else {
8617
- for (const f of data.changedFiles.slice(0, CHANGED_FILES_MARKDOWN_LIMIT)) lines.push(`- ${f}`);
8821
+ for (const f of data.changedFiles.slice(0, CHANGED_FILES_MARKDOWN_LIMIT)) {
8822
+ lines.push(`- ${displayPath(f)}`);
8823
+ }
8618
8824
  const overflow = data.changedFiles.length - CHANGED_FILES_MARKDOWN_LIMIT;
8619
8825
  if (overflow > 0) lines.push(`- ... +${overflow} more`);
8620
8826
  }
@@ -10568,6 +10774,7 @@ export {
10568
10774
  createManifest,
10569
10775
  createTaskWithEvent,
10570
10776
  deleteTask,
10777
+ displayPath,
10571
10778
  editTask,
10572
10779
  ensureBasouDirectory,
10573
10780
  enumerateApprovals,
@@ -10578,10 +10785,12 @@ export {
10578
10785
  finalizeSessionYaml,
10579
10786
  findBasouSessionStartHook,
10580
10787
  findBasouStopHookCommand,
10788
+ findClaudeSessionStartHooks,
10581
10789
  findDecisionGaps,
10582
10790
  findErrorCode,
10583
10791
  findReviewGaps,
10584
10792
  findUnbindableRepos,
10793
+ findUnrecognizedSessionStart,
10585
10794
  formatDurationMs,
10586
10795
  genesisHash,
10587
10796
  getChangesSince,
@@ -10592,8 +10801,11 @@ export {
10592
10801
  importSessionFromJson,
10593
10802
  inspectChainTail,
10594
10803
  instructionMode,
10804
+ isBasouOrientSessionStartCommand,
10595
10805
  isBasouSessionStartHookCommand,
10596
10806
  isBasouStopHookCommand,
10807
+ isClaudeSessionStartHookCommand,
10808
+ isClaudeSessionStartMalformed,
10597
10809
  isGitNotFound,
10598
10810
  isImportDerivedSource,
10599
10811
  isLazyExpired,
@@ -10646,6 +10858,7 @@ export {
10646
10858
  recordSessionBaseline,
10647
10859
  refreshTaskLinkedSessions,
10648
10860
  reimportPreservingId,
10861
+ removeClaudeSessionStartHook,
10649
10862
  removeMarkerSection,
10650
10863
  removeSessionStartHook,
10651
10864
  removeStopHook,
@@ -10693,6 +10906,7 @@ export {
10693
10906
  unknownManifestKeys,
10694
10907
  unstampedProtocolSectionsFrom,
10695
10908
  updateTaskStatusWithEvent,
10909
+ upsertClaudeSessionStartHook,
10696
10910
  upsertSessionStartHook,
10697
10911
  upsertStopHook,
10698
10912
  verifyEventsChain,