@basou/core 0.18.0 → 0.20.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
@@ -21,6 +21,73 @@ function summarizeAdapterOutput(_stream, _raw) {
21
21
  throw new Error("adapter_output summary is not implemented in this release");
22
22
  }
23
23
 
24
+ // src/adapters/claude-code/stop-hook.ts
25
+ var DEFAULT_STOP_HOOK_MIN_ACTIONS = 5;
26
+ var CAPTURE_COMMAND_PATTERN = /(?:^|[\n;&|(])\s*basou\s+(?:decision\s+(?:capture|record)|note)\b/;
27
+ var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit"]);
28
+ function evaluateStopHook(input) {
29
+ const minActions = input.minActions ?? DEFAULT_STOP_HOOK_MIN_ACTIONS;
30
+ if (input.stopHookActive) {
31
+ return { kind: "silent", reason: "stop_hook_active", commandCount: 0, fileCount: 0 };
32
+ }
33
+ let commandCount = 0;
34
+ let fileCount = 0;
35
+ let captured = false;
36
+ for (const record of input.records) {
37
+ if (readString(record.type) !== "assistant") continue;
38
+ for (const tool of toolUsesOf(record)) {
39
+ const name = readString(tool.name);
40
+ if (name === void 0) continue;
41
+ if (name === "Bash") {
42
+ commandCount += 1;
43
+ const input2 = isObject(tool.input) ? tool.input : void 0;
44
+ const command = input2 !== void 0 ? readString(input2.command) : void 0;
45
+ if (command !== void 0 && CAPTURE_COMMAND_PATTERN.test(command)) captured = true;
46
+ } else if (FILE_EDIT_TOOLS.has(name)) {
47
+ fileCount += 1;
48
+ }
49
+ }
50
+ }
51
+ if (captured) {
52
+ return { kind: "silent", reason: "already_captured", commandCount, fileCount };
53
+ }
54
+ if (commandCount + fileCount < minActions) {
55
+ return { kind: "silent", reason: "not_substantive", commandCount, fileCount };
56
+ }
57
+ return {
58
+ kind: "nudge",
59
+ additionalContext: renderNudge(commandCount, fileCount),
60
+ commandCount,
61
+ fileCount
62
+ };
63
+ }
64
+ function renderNudge(commandCount, fileCount) {
65
+ const ran = `${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
66
+ const edited = `${fileCount} ${fileCount === 1 ? "file" : "files"}`;
67
+ return [
68
+ `This session ran ${ran} and edited ${edited} but recorded no decisions or next step.`,
69
+ "If meaningful decisions were made (the chosen approach, rejected alternatives, and why) or there is a clear next step, capture them now so the next session can resume correctly:",
70
+ ' - Decisions: run `basou decision capture` and pipe a JSON array (one object per decision; "title" required, plus optional rationale/alternatives/rejected_reason/linked_files; set "kind":"track" for an unfinished strategic direction).',
71
+ ' - Next step: run `basou note "<what you would do next>"`.',
72
+ "If nothing is worth capturing, just stop \u2014 do not invent decisions."
73
+ ].join("\n");
74
+ }
75
+ function readString(value) {
76
+ return typeof value === "string" && value.length > 0 ? value : void 0;
77
+ }
78
+ function isObject(value) {
79
+ return typeof value === "object" && value !== null && !Array.isArray(value);
80
+ }
81
+ function toolUsesOf(record) {
82
+ const message = isObject(record.message) ? record.message : void 0;
83
+ const content = message !== void 0 && Array.isArray(message.content) ? message.content : [];
84
+ const result = [];
85
+ for (const item of content) {
86
+ if (isObject(item) && readString(item.type) === "tool_use") result.push(item);
87
+ }
88
+ return result;
89
+ }
90
+
24
91
  // src/ids/ulid.ts
25
92
  import { isValid as isValidUlid, monotonicFactory } from "ulid";
26
93
  var ID_PREFIXES = Object.freeze(["ws", "task", "ses", "evt", "appr", "decision"]);
@@ -128,21 +195,21 @@ function claudeTranscriptToImportPayload(records, options) {
128
195
  const engagementTsMs = [];
129
196
  const seenEngagementMessageIds = /* @__PURE__ */ new Set();
130
197
  for (const record of records) {
131
- const ts = readString(record.timestamp);
198
+ const ts = readString2(record.timestamp);
132
199
  if (ts === void 0) continue;
133
200
  if (minTs === void 0 || Date.parse(ts) < Date.parse(minTs)) minTs = ts;
134
201
  if (maxTs === void 0 || Date.parse(ts) > Date.parse(maxTs)) maxTs = ts;
135
- if (workingDir === void 0) workingDir = readString(record.cwd);
136
- if (claudeSessionId === void 0) claudeSessionId = readString(record.sessionId);
202
+ if (workingDir === void 0) workingDir = readString2(record.cwd);
203
+ if (claudeSessionId === void 0) claudeSessionId = readString2(record.sessionId);
137
204
  if (record.isSidechain !== true) {
138
205
  const tsMs = Date.parse(ts);
139
206
  if (Number.isFinite(tsMs)) {
140
- const recType = readString(record.type);
207
+ const recType = readString2(record.type);
141
208
  if (recType === "user") {
142
209
  if (isHumanUserMessage(record)) engagementTsMs.push(tsMs);
143
210
  } else if (recType === "assistant") {
144
- const msg = isObject(record.message) ? record.message : void 0;
145
- const mid = msg !== void 0 ? readString(msg.id) : void 0;
211
+ const msg = isObject2(record.message) ? record.message : void 0;
212
+ const mid = msg !== void 0 ? readString2(msg.id) : void 0;
146
213
  if (mid === void 0 || !seenEngagementMessageIds.has(mid)) {
147
214
  if (mid !== void 0) seenEngagementMessageIds.add(mid);
148
215
  engagementTsMs.push(tsMs);
@@ -150,11 +217,11 @@ function claudeTranscriptToImportPayload(records, options) {
150
217
  }
151
218
  }
152
219
  }
153
- if (readString(record.type) !== "assistant") continue;
154
- const message = isObject(record.message) ? record.message : void 0;
155
- const usage = message !== void 0 && isObject(message.usage) ? message.usage : void 0;
220
+ if (readString2(record.type) !== "assistant") continue;
221
+ const message = isObject2(record.message) ? record.message : void 0;
222
+ const usage = message !== void 0 && isObject2(message.usage) ? message.usage : void 0;
156
223
  if (usage !== void 0) {
157
- const messageId = message !== void 0 ? readString(message.id) : void 0;
224
+ const messageId = message !== void 0 ? readString2(message.id) : void 0;
158
225
  const alreadyCounted = messageId !== void 0 && seenMessageIds.has(messageId);
159
226
  if (!alreadyCounted) {
160
227
  if (messageId !== void 0) seenMessageIds.add(messageId);
@@ -163,20 +230,20 @@ function claudeTranscriptToImportPayload(records, options) {
163
230
  cachedInputTokens += readNonNegInt(usage.cache_read_input_tokens);
164
231
  }
165
232
  }
166
- const cwd = readString(record.cwd) ?? workingDir ?? ".";
233
+ const cwd = readString2(record.cwd) ?? workingDir ?? ".";
167
234
  for (const item of toolUses(record)) {
168
- const name = readString(item.name);
169
- const input = isObject(item.input) ? item.input : void 0;
235
+ const name = readString2(item.name);
236
+ const input = isObject2(item.input) ? item.input : void 0;
170
237
  if (input === void 0) continue;
171
238
  if (name === "Bash") {
172
- const command = readString(input.command);
239
+ const command = readString2(input.command);
173
240
  if (command !== void 0) {
174
241
  derived.push(commandExecutedEvent(ts, placeholderSessionId, command, cwd));
175
242
  }
176
243
  continue;
177
244
  }
178
245
  if (name === "AskUserQuestion") {
179
- const useId = readString(item.id);
246
+ const useId = readString2(item.id);
180
247
  const answers = useId !== void 0 ? askAnswers.get(useId) : void 0;
181
248
  if (answers !== void 0) {
182
249
  for (const [question, answer] of Object.entries(answers)) {
@@ -189,7 +256,7 @@ function claudeTranscriptToImportPayload(records, options) {
189
256
  continue;
190
257
  }
191
258
  if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
192
- const path2 = readString(input.file_path) ?? readString(input.notebook_path);
259
+ const path2 = readString2(input.file_path) ?? readString2(input.notebook_path);
193
260
  if (path2 !== void 0) {
194
261
  const changeType = name === "Write" ? "added" : "modified";
195
262
  relatedFiles.add(path2);
@@ -291,35 +358,35 @@ function decisionRecordedEvent(occurredAt, sessionId, title) {
291
358
  title
292
359
  };
293
360
  }
294
- function readString(value) {
361
+ function readString2(value) {
295
362
  return typeof value === "string" && value.length > 0 ? value : void 0;
296
363
  }
297
364
  function readNonNegInt(value) {
298
365
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
299
366
  }
300
- function isObject(value) {
367
+ function isObject2(value) {
301
368
  return typeof value === "object" && value !== null && !Array.isArray(value);
302
369
  }
303
370
  function isHumanUserMessage(record) {
304
- const message = isObject(record.message) ? record.message : void 0;
371
+ const message = isObject2(record.message) ? record.message : void 0;
305
372
  if (message === void 0) return false;
306
373
  const content = message.content;
307
374
  if (typeof content === "string") return content.length > 0;
308
375
  if (Array.isArray(content)) {
309
376
  return content.some((block) => {
310
- if (!isObject(block)) return false;
311
- const type = readString(block.type);
377
+ if (!isObject2(block)) return false;
378
+ const type = readString2(block.type);
312
379
  return type !== void 0 && type !== "tool_result";
313
380
  });
314
381
  }
315
382
  return false;
316
383
  }
317
384
  function toolUses(record) {
318
- const message = isObject(record.message) ? record.message : void 0;
385
+ const message = isObject2(record.message) ? record.message : void 0;
319
386
  const content = message !== void 0 && Array.isArray(message.content) ? message.content : [];
320
387
  const result = [];
321
388
  for (const item of content) {
322
- if (isObject(item) && readString(item.type) === "tool_use") {
389
+ if (isObject2(item) && readString2(item.type) === "tool_use") {
323
390
  result.push(item);
324
391
  }
325
392
  }
@@ -329,14 +396,14 @@ function indexAskAnswers(records) {
329
396
  const byId = /* @__PURE__ */ new Map();
330
397
  for (const record of records) {
331
398
  const result = record.toolUseResult;
332
- if (!isObject(result)) continue;
399
+ if (!isObject2(result)) continue;
333
400
  const answers = result.answers;
334
- if (!isObject(answers)) continue;
335
- const message = isObject(record.message) ? record.message : void 0;
401
+ if (!isObject2(answers)) continue;
402
+ const message = isObject2(record.message) ? record.message : void 0;
336
403
  const content = message !== void 0 && Array.isArray(message.content) ? message.content : [];
337
404
  for (const item of content) {
338
- if (isObject(item) && readString(item.type) === "tool_result") {
339
- const id = readString(item.tool_use_id);
405
+ if (isObject2(item) && readString2(item.type) === "tool_result") {
406
+ const id = readString2(item.tool_use_id);
340
407
  if (id !== void 0) byId.set(id, answers);
341
408
  }
342
409
  }
@@ -360,31 +427,31 @@ function codexRolloutToImportPayload(records, options) {
360
427
  const completions = [];
361
428
  const completedTurnIds = /* @__PURE__ */ new Set();
362
429
  for (const record of records) {
363
- const ts = readString2(record.timestamp);
430
+ const ts = readString3(record.timestamp);
364
431
  if (ts === void 0) continue;
365
432
  if (minTs === void 0 || Date.parse(ts) < Date.parse(minTs)) minTs = ts;
366
433
  if (maxTs === void 0 || Date.parse(ts) > Date.parse(maxTs)) maxTs = ts;
367
- const payload2 = isObject2(record.payload) ? record.payload : void 0;
434
+ const payload2 = isObject3(record.payload) ? record.payload : void 0;
368
435
  if (payload2 === void 0) continue;
369
- if (readString2(record.type) === "session_meta") {
370
- if (workingDir === void 0) workingDir = readString2(payload2.cwd);
371
- if (codexSessionId === void 0) codexSessionId = readString2(payload2.id);
436
+ if (readString3(record.type) === "session_meta") {
437
+ if (workingDir === void 0) workingDir = readString3(payload2.cwd);
438
+ if (codexSessionId === void 0) codexSessionId = readString3(payload2.id);
372
439
  continue;
373
440
  }
374
- if (readString2(record.type) === "event_msg" && readString2(payload2.type) === "token_count") {
375
- const info = isObject2(payload2.info) ? payload2.info : void 0;
376
- const totals = info !== void 0 && isObject2(info.total_token_usage) ? info.total_token_usage : void 0;
441
+ if (readString3(record.type) === "event_msg" && readString3(payload2.type) === "token_count") {
442
+ const info = isObject3(payload2.info) ? payload2.info : void 0;
443
+ const totals = info !== void 0 && isObject3(info.total_token_usage) ? info.total_token_usage : void 0;
377
444
  if (totals !== void 0) lastTokenTotals = totals;
378
445
  continue;
379
446
  }
380
- if (readString2(record.type) === "event_msg") {
381
- const pt = readString2(payload2.type);
447
+ if (readString3(record.type) === "event_msg") {
448
+ const pt = readString3(payload2.type);
382
449
  if (pt === "user_message" || pt === "agent_message" || pt === "task_started" || pt === "task_complete") {
383
450
  const tsMs = Date.parse(ts);
384
451
  if (Number.isFinite(tsMs)) engagementTsMs.push(tsMs);
385
452
  }
386
453
  if (pt === "task_complete") {
387
- const turnId = readString2(payload2.turn_id);
454
+ const turnId = readString3(payload2.turn_id);
388
455
  if (turnId === void 0 || !completedTurnIds.has(turnId)) {
389
456
  if (turnId !== void 0) completedTurnIds.add(turnId);
390
457
  completions.push({
@@ -395,9 +462,9 @@ function codexRolloutToImportPayload(records, options) {
395
462
  }
396
463
  continue;
397
464
  }
398
- if (readString2(record.type) !== "response_item") continue;
399
- if (readString2(payload2.type) !== "function_call") continue;
400
- if (readString2(payload2.name) !== "exec_command") continue;
465
+ if (readString3(record.type) !== "response_item") continue;
466
+ if (readString3(payload2.type) !== "function_call") continue;
467
+ if (readString3(payload2.name) !== "exec_command") continue;
401
468
  const command = readExecCommand(payload2.arguments);
402
469
  if (command === void 0) continue;
403
470
  const cwd = command.workdir ?? workingDir ?? ".";
@@ -508,17 +575,17 @@ function commandExecutedEvent2(occurredAt, sessionId, command, cwd, outcome) {
508
575
  duration_ms: outcome.durationMs
509
576
  };
510
577
  }
511
- function readString2(value) {
578
+ function readString3(value) {
512
579
  return typeof value === "string" && value.length > 0 ? value : void 0;
513
580
  }
514
581
  function readNonNegInt2(value) {
515
582
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
516
583
  }
517
- function isObject2(value) {
584
+ function isObject3(value) {
518
585
  return typeof value === "object" && value !== null && !Array.isArray(value);
519
586
  }
520
587
  function readExecCommand(value) {
521
- const raw = readString2(value);
588
+ const raw = readString3(value);
522
589
  if (raw === void 0) return void 0;
523
590
  let parsed;
524
591
  try {
@@ -526,19 +593,19 @@ function readExecCommand(value) {
526
593
  } catch {
527
594
  return void 0;
528
595
  }
529
- if (!isObject2(parsed)) return void 0;
530
- const cmd = readString2(parsed.cmd);
596
+ if (!isObject3(parsed)) return void 0;
597
+ const cmd = readString3(parsed.cmd);
531
598
  if (cmd === void 0) return void 0;
532
- return { cmd, workdir: readString2(parsed.workdir) };
599
+ return { cmd, workdir: readString3(parsed.workdir) };
533
600
  }
534
601
  function readCallId(value, outputs) {
535
- const callId = readString2(value);
602
+ const callId = readString3(value);
536
603
  return callId !== void 0 ? outputs.get(callId) : void 0;
537
604
  }
538
605
  function turnIntervalFromComplete(endTs, payload, startMsByTurnId) {
539
606
  const endMs = Date.parse(endTs);
540
607
  if (!Number.isFinite(endMs)) return void 0;
541
- const turnId = readString2(payload.turn_id);
608
+ const turnId = readString3(payload.turn_id);
542
609
  const indexedStart = turnId !== void 0 ? startMsByTurnId.get(turnId) : void 0;
543
610
  const durationMs = readNonNegInt2(payload.duration_ms);
544
611
  const startMs = indexedStart !== void 0 ? indexedStart : durationMs > 0 ? endMs - durationMs : void 0;
@@ -548,11 +615,11 @@ function turnIntervalFromComplete(endTs, payload, startMsByTurnId) {
548
615
  function indexTaskStarts(records) {
549
616
  const byTurnId = /* @__PURE__ */ new Map();
550
617
  for (const record of records) {
551
- if (readString2(record.type) !== "event_msg") continue;
552
- const payload = isObject2(record.payload) ? record.payload : void 0;
553
- if (payload === void 0 || readString2(payload.type) !== "task_started") continue;
554
- const turnId = readString2(payload.turn_id);
555
- const startMs = Date.parse(readString2(record.timestamp) ?? "");
618
+ if (readString3(record.type) !== "event_msg") continue;
619
+ const payload = isObject3(record.payload) ? record.payload : void 0;
620
+ if (payload === void 0 || readString3(payload.type) !== "task_started") continue;
621
+ const turnId = readString3(payload.turn_id);
622
+ const startMs = Date.parse(readString3(record.timestamp) ?? "");
556
623
  if (turnId !== void 0 && Number.isFinite(startMs) && !byTurnId.has(turnId)) {
557
624
  byTurnId.set(turnId, startMs);
558
625
  }
@@ -574,12 +641,12 @@ function parseWallTimeMs(output) {
574
641
  function indexOutputs(records) {
575
642
  const byId = /* @__PURE__ */ new Map();
576
643
  for (const record of records) {
577
- if (readString2(record.type) !== "response_item") continue;
578
- const payload = isObject2(record.payload) ? record.payload : void 0;
644
+ if (readString3(record.type) !== "response_item") continue;
645
+ const payload = isObject3(record.payload) ? record.payload : void 0;
579
646
  if (payload === void 0) continue;
580
- if (readString2(payload.type) !== "function_call_output") continue;
581
- const callId = readString2(payload.call_id);
582
- const output = readString2(payload.output);
647
+ if (readString3(payload.type) !== "function_call_output") continue;
648
+ const callId = readString3(payload.call_id);
649
+ const output = readString3(payload.output);
583
650
  if (callId !== void 0 && output !== void 0) byId.set(callId, output);
584
651
  }
585
652
  return byId;
@@ -872,7 +939,17 @@ var DecisionRecordedEventSchema = BaseEventSchema.extend({
872
939
  alternatives: z3.array(z3.string().min(1)).optional(),
873
940
  rejected_reason: z3.string().nullable().optional(),
874
941
  linked_events: z3.array(EventIdSchema).optional(),
875
- linked_files: z3.array(z3.string().min(1).max(4096)).optional()
942
+ linked_files: z3.array(z3.string().min(1).max(4096)).optional(),
943
+ // `track` promotes a decision to a strategic, unfinished DIRECTION ("the next
944
+ // essential thing to build, and why") that orientation/handoff resurface every
945
+ // time until it is explicitly closed with `decision void` / supersede — as
946
+ // opposed to a point-in-time `decision`, which is only ever surfaced as the
947
+ // single latest one. This is the intent-continuity layer: a direction agreed
948
+ // in conversation otherwise sinks into the flat decision list and never carries
949
+ // to the next session. Absent (the default) is a plain `decision`, so all
950
+ // pre-existing decision_recorded events round-trip unchanged (additive optional
951
+ // => no schema_version bump; mirrors `note_added.kind`).
952
+ kind: z3.enum(["decision", "track"]).optional()
876
953
  });
877
954
  var DecisionVoidedEventSchema = BaseEventSchema.extend({
878
955
  type: z3.literal("decision_voided"),
@@ -1487,6 +1564,7 @@ async function renderDecisions(input) {
1487
1564
  rejectedReason: ev.rejected_reason,
1488
1565
  linkedEvents: ev.linked_events,
1489
1566
  linkedFiles: ev.linked_files,
1567
+ kind: ev.kind,
1490
1568
  voided: void 0
1491
1569
  });
1492
1570
  } else if (ev.type === "decision_voided") {
@@ -1542,18 +1620,22 @@ async function formatDecisionsBody(args) {
1542
1620
  return lines.join("\n");
1543
1621
  }
1544
1622
  for (const d of args.decisions) {
1623
+ const trackMark = d.kind === "track" ? " [TRACK]" : "";
1545
1624
  if (d.voided !== void 0) {
1546
- lines.push(`## ~~${d.decisionId}: ${d.title}~~ [VOIDED]`);
1625
+ lines.push(`## ~~${d.decisionId}: ${d.title}~~ [VOIDED]${trackMark}`);
1547
1626
  lines.push("");
1548
1627
  const supersededBy = d.voided.supersededBy !== void 0 ? `, superseded by ${d.voided.supersededBy}` : "";
1549
1628
  const reason = typeof d.voided.reason === "string" && d.voided.reason.length > 0 ? `: ${d.voided.reason}` : "";
1550
1629
  lines.push(`- \u26A0 VOIDED${reason}${supersededBy}`);
1551
1630
  } else {
1552
- lines.push(`## ${d.decisionId}: ${d.title}`);
1631
+ lines.push(`## ${d.decisionId}: ${d.title}${trackMark}`);
1553
1632
  lines.push("");
1554
1633
  }
1555
1634
  const occurredDate = d.occurredAt.slice(0, 10);
1556
1635
  lines.push(`- \u6C7A\u5B9A\u65E5: ${occurredDate}`);
1636
+ if (d.kind === "track" && d.voided === void 0) {
1637
+ lines.push("- \u7A2E\u5225: track (close \u307E\u3067 orient/handoff \u306B\u7D99\u7D9A\u8868\u793A)");
1638
+ }
1557
1639
  lines.push(`- session: ${shortDecisionSessionId(d.sessionId)}`);
1558
1640
  lines.push(`- \u5224\u65AD: ${d.title}`);
1559
1641
  if (typeof d.rationale === "string" && d.rationale.length > 0) {
@@ -3876,6 +3958,7 @@ async function renderHandoff(input) {
3876
3958
  if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
3877
3959
  const entries = await loadSessionEntries(input.paths, loadOpts);
3878
3960
  const decisions = [];
3961
+ const tracks = [];
3879
3962
  const voidedDecisionIds = /* @__PURE__ */ new Set();
3880
3963
  const tasksCreated = [];
3881
3964
  const tasksStatusChanged = [];
@@ -3901,6 +3984,15 @@ async function renderHandoff(input) {
3901
3984
  occurredAt: ev.occurred_at,
3902
3985
  sessionId: entry.sessionId
3903
3986
  });
3987
+ if (ev.kind === "track") {
3988
+ tracks.push({
3989
+ decisionId: ev.decision_id,
3990
+ title: ev.title,
3991
+ rationale: ev.rationale ?? null,
3992
+ occurredAt: ev.occurred_at,
3993
+ sessionId: entry.sessionId
3994
+ });
3995
+ }
3904
3996
  } else if (ev.type === "decision_voided") {
3905
3997
  voidedDecisionIds.add(ev.decision_id);
3906
3998
  } else if (ev.type === "task_created") {
@@ -3936,6 +4028,10 @@ async function renderHandoff(input) {
3936
4028
  break;
3937
4029
  }
3938
4030
  }
4031
+ const openTracks = tracks.filter((t) => !voidedDecisionIds.has(t.decisionId)).sort((a, b) => {
4032
+ const c = Date.parse(b.occurredAt) - Date.parse(a.occurredAt);
4033
+ return c !== 0 ? c : b.decisionId.localeCompare(a.decisionId);
4034
+ });
3939
4035
  tasksCreated.sort((a, b) => {
3940
4036
  const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
3941
4037
  return c !== 0 ? c : a.taskId.localeCompare(b.taskId);
@@ -3980,6 +4076,7 @@ async function renderHandoff(input) {
3980
4076
  latestActivityAt,
3981
4077
  decisions,
3982
4078
  latestDecision,
4079
+ openTracks,
3983
4080
  pendingApprovalsCount,
3984
4081
  suspectCount,
3985
4082
  displayedFiles,
@@ -4065,6 +4162,23 @@ function formatHandoffBody(args) {
4065
4162
  lines.push(`(${args.decisions.length} decisions total \u2014 see decisions.md)`);
4066
4163
  }
4067
4164
  lines.push("");
4165
+ if (args.openTracks.length > 0) {
4166
+ const TRACK_DISPLAY_LIMIT = 10;
4167
+ const shown = args.openTracks.slice(0, TRACK_DISPLAY_LIMIT);
4168
+ const overflow = args.openTracks.length - shown.length;
4169
+ lines.push("## \u672A\u5B8C\u30C8\u30E9\u30C3\u30AF (close \u307E\u3067\u7D99\u7D9A\u8868\u793A)");
4170
+ lines.push("");
4171
+ for (const t of shown) {
4172
+ lines.push(`- ${t.title} [${shortIdWithPrefix(t.decisionId)}]`);
4173
+ if (t.rationale !== null && t.rationale.trim() !== "") {
4174
+ lines.push(` - \u7406\u7531: ${handoffRationale(t.rationale)}`);
4175
+ }
4176
+ }
4177
+ if (overflow > 0) lines.push(`- ... +${overflow} more (see decisions.md)`);
4178
+ lines.push("");
4179
+ lines.push("\u5B8C\u4E86\u3057\u305F\u3089 `basou decision void <decision_id>` \u3067\u9589\u3058\u3066\u304F\u3060\u3055\u3044\u3002");
4180
+ lines.push("");
4181
+ }
4068
4182
  lines.push("## \u672A\u6C7A\u4E8B\u9805");
4069
4183
  lines.push("");
4070
4184
  if (args.pendingApprovalsCount > 0) {
@@ -4149,6 +4263,11 @@ function formatHandoffBody(args) {
4149
4263
  lines.push(sessionsLine);
4150
4264
  return lines.join("\n");
4151
4265
  }
4266
+ var HANDOFF_TRACK_RATIONALE_MAX = 240;
4267
+ function handoffRationale(rationale) {
4268
+ const oneLine = rationale.replace(/\s+/g, " ").trim();
4269
+ return oneLine.length > HANDOFF_TRACK_RATIONALE_MAX ? `${oneLine.slice(0, HANDOFF_TRACK_RATIONALE_MAX - 1)}\u2026` : oneLine;
4270
+ }
4152
4271
  function suspectLabel(reason) {
4153
4272
  if (reason === "events_say_ended_but_yaml_running") return " \u26A0 ended (yaml stale)";
4154
4273
  if (reason === "running_no_end_event") return " \u26A0 no end event";
@@ -4502,6 +4621,7 @@ async function summarizeOrientation(input) {
4502
4621
  }
4503
4622
  ) : await loadSessionEntries(input.paths, loadOpts);
4504
4623
  const decisions = [];
4624
+ const tracks = [];
4505
4625
  const voidedDecisionIds = /* @__PURE__ */ new Set();
4506
4626
  let latestActivityAt = null;
4507
4627
  let latestNote = null;
@@ -4526,6 +4646,16 @@ async function summarizeOrientation(input) {
4526
4646
  sessionId: entry.sessionId,
4527
4647
  host: entry.host
4528
4648
  });
4649
+ if (ev.kind === "track") {
4650
+ tracks.push({
4651
+ decisionId: ev.decision_id,
4652
+ title: ev.title,
4653
+ rationale: ev.rationale ?? null,
4654
+ occurredAt: ev.occurred_at,
4655
+ sessionId: entry.sessionId,
4656
+ host: entry.host
4657
+ });
4658
+ }
4529
4659
  } else if (ev.type === "decision_voided") {
4530
4660
  voidedDecisionIds.add(ev.decision_id);
4531
4661
  }
@@ -4557,6 +4687,10 @@ async function summarizeOrientation(input) {
4557
4687
  break;
4558
4688
  }
4559
4689
  }
4690
+ const openTracks = tracks.filter((t) => !voidedDecisionIds.has(t.decisionId)).sort((a, b) => {
4691
+ const c = Date.parse(b.occurredAt) - Date.parse(a.occurredAt);
4692
+ return c !== 0 ? c : b.decisionId.localeCompare(a.decisionId);
4693
+ });
4560
4694
  const taskLoadOpts = {};
4561
4695
  if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
4562
4696
  const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
@@ -4645,6 +4779,7 @@ async function summarizeOrientation(input) {
4645
4779
  latestSession,
4646
4780
  latestDecision: latestDecision ?? null,
4647
4781
  decisionCount: decisions.length,
4782
+ openTracks,
4648
4783
  latestNote,
4649
4784
  relatedFiles: { displayed, overflow, outOfRoot },
4650
4785
  inFlightTasks,
@@ -4672,7 +4807,8 @@ async function renderOrientation(input) {
4672
4807
  pendingApprovalsCount: summary.pendingApprovals.length,
4673
4808
  suspectCount: summary.suspects.length,
4674
4809
  inFlightTaskCount: summary.inFlightTasks.length,
4675
- decisionCount: summary.decisionCount
4810
+ decisionCount: summary.decisionCount,
4811
+ openTrackCount: summary.openTracks.length
4676
4812
  };
4677
4813
  }
4678
4814
  function formatOrientationBody(summary, opts) {
@@ -4779,6 +4915,26 @@ function formatOrientationBody(summary, opts) {
4779
4915
  lines.push("");
4780
4916
  lines.push("## \u3069\u3053\u3078\u5411\u304B\u3046");
4781
4917
  lines.push("");
4918
+ if (summary.openTracks.length > 0) {
4919
+ const TRACK_DISPLAY_LIMIT = 10;
4920
+ const shownTracks = summary.openTracks.slice(0, TRACK_DISPLAY_LIMIT);
4921
+ const trackOverflow = summary.openTracks.length - shownTracks.length;
4922
+ lines.push(`### \u672A\u5B8C\u30C8\u30E9\u30C3\u30AF (close \u307E\u3067\u7D99\u7D9A\u8868\u793A) (${summary.openTracks.length})`);
4923
+ for (const t of shownTracks) {
4924
+ const trackAge = relativeAgeJa(t.occurredAt, now);
4925
+ lines.push(`- ${t.title} [${shortId(t.decisionId)}] (${trackAge})${hostSuffix(t.host)}`);
4926
+ if (t.rationale !== null && t.rationale.trim() !== "") {
4927
+ lines.push(` - \u7406\u7531: ${trackRationale(t.rationale)}`);
4928
+ }
4929
+ }
4930
+ if (trackOverflow > 0) {
4931
+ lines.push(`- ... +${trackOverflow} more (see decisions.md)`);
4932
+ }
4933
+ lines.push(
4934
+ "\u5B8C\u4E86\u3057\u305F\u3089 `basou decision void <decision_id>` \u3067\u9589\u3058\u3066\u304F\u3060\u3055\u3044\u3002\u9589\u3058\u308B\u307E\u3067\u6BCE\u56DE\u3053\u3053\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002"
4935
+ );
4936
+ lines.push("");
4937
+ }
4782
4938
  if (summary.latestNote !== null) {
4783
4939
  const noteAge = relativeAgeJa(summary.latestNote.occurredAt, now);
4784
4940
  lines.push(
@@ -4794,7 +4950,7 @@ function formatOrientationBody(summary, opts) {
4794
4950
  for (const t of summary.plannedTasks) {
4795
4951
  lines.push(`- ${t.title} [${shortId(t.id)}]`);
4796
4952
  }
4797
- if (summary.latestNote === null && summary.plannedTasks.length === 0) {
4953
+ if (summary.openTracks.length === 0 && summary.latestNote === null && summary.plannedTasks.length === 0) {
4798
4954
  const dec = summary.latestDecision;
4799
4955
  if (dec === null) {
4800
4956
  lines.push("- (no planned tasks or recorded next step yet)");
@@ -4807,6 +4963,11 @@ function formatOrientationBody(summary, opts) {
4807
4963
  lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
4808
4964
  lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${dec.title}`);
4809
4965
  }
4966
+ if (dec !== null) {
4967
+ lines.push(
4968
+ ' - \u6B21\u306B\u4F5C\u308B\u3079\u304D\u672C\u8CEA\u7684\u306A\u65B9\u5411\u6027\u304C\u5B9A\u307E\u3063\u305F\u3089 `basou decision capture` (`"kind":"track"`) / `basou decision record --track` \u3067 track \u5316\u3059\u308B\u3068\u3001close \u307E\u3067\u6BCE session \u3053\u3053\u306B\u7D99\u7D9A\u8868\u793A\u3055\u308C\u307E\u3059\u3002'
4969
+ );
4970
+ }
4810
4971
  }
4811
4972
  lines.push("");
4812
4973
  lines.push("## \u3053\u308C\u306F\u6700\u65B0\u304B");
@@ -4932,6 +5093,11 @@ function noteSummary(body) {
4932
5093
  const oneLine = body.replace(/\s+/g, " ").trim();
4933
5094
  return oneLine.length > NOTE_SUMMARY_MAX ? `${oneLine.slice(0, NOTE_SUMMARY_MAX - 1)}\u2026` : oneLine;
4934
5095
  }
5096
+ var TRACK_RATIONALE_MAX = 240;
5097
+ function trackRationale(rationale) {
5098
+ const oneLine = rationale.replace(/\s+/g, " ").trim();
5099
+ return oneLine.length > TRACK_RATIONALE_MAX ? `${oneLine.slice(0, TRACK_RATIONALE_MAX - 1)}\u2026` : oneLine;
5100
+ }
4935
5101
  function suspectText(reason) {
4936
5102
  if (reason === "events_say_ended_but_yaml_running") return "ended (yaml stale)";
4937
5103
  if (reason === "running_no_end_event") return "no end event";
@@ -5847,7 +6013,12 @@ async function renderReport(input) {
5847
6013
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
5848
6014
  })) {
5849
6015
  if (ev.type === "decision_recorded") {
5850
- decisions.push({ id: ev.decision_id, title: ev.title, occurredAt: ev.occurred_at });
6016
+ decisions.push({
6017
+ id: ev.decision_id,
6018
+ title: ev.title,
6019
+ occurredAt: ev.occurred_at,
6020
+ ...ev.kind === "track" ? { track: true } : {}
6021
+ });
5851
6022
  } else if (ev.type === "decision_voided") {
5852
6023
  voidedDecisionIds.add(ev.decision_id);
5853
6024
  }
@@ -6033,8 +6204,9 @@ function formatReportBody(data) {
6033
6204
  lines.push("");
6034
6205
  }
6035
6206
  for (const d of shown) {
6207
+ const trackTag = d.track === true ? " [track]" : "";
6036
6208
  const voidedTag = d.voided === true ? " (voided)" : "";
6037
- lines.push(`- ${d.occurredAt.slice(0, 10)} \xB7 ${d.title}${voidedTag}`);
6209
+ lines.push(`- ${d.occurredAt.slice(0, 10)} \xB7 ${d.title}${trackTag}${voidedTag}`);
6038
6210
  }
6039
6211
  }
6040
6212
  lines.push("");
@@ -7244,6 +7416,7 @@ export {
7244
7416
  CLAUDE_IMPORT_SOURCE,
7245
7417
  CODEX_IMPORT_SOURCE,
7246
7418
  ChildProcessRunner,
7419
+ DEFAULT_STOP_HOOK_MIN_ACTIONS,
7247
7420
  DecisionIdSchema,
7248
7421
  EventIdSchema,
7249
7422
  EventSchema,
@@ -7303,6 +7476,7 @@ export {
7303
7476
  enumerateArchivedTaskIds,
7304
7477
  enumerateSessionDirs,
7305
7478
  enumerateTaskIds,
7479
+ evaluateStopHook,
7306
7480
  finalizeSessionYaml,
7307
7481
  findErrorCode,
7308
7482
  findReviewGaps,