@basou/core 0.20.0 → 0.22.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 CHANGED
@@ -570,10 +570,14 @@ type ClaudeTranscriptToPayloadOptions = {
570
570
  * - `command_executed` from each `Bash` tool use, recorded as `bash -c "<cmd>"`
571
571
  * (the transcript carries the shell line, not a parsed argv).
572
572
  * - `file_changed` from each `Edit` / `Write` / `NotebookEdit` tool use.
573
- * - `decision_recorded` from each `AskUserQuestion` tool use: one decision per
574
- * question, titled `<question> -> <chosen answer>`. The chosen answer is read
575
- * from the paired result record's structured `toolUseResult.answers` map; a
576
- * question with no recorded string answer is skipped.
573
+ * - `decision_recorded` from each `AskUserQuestion` tool use, but ONLY when the
574
+ * recorded answer is a confirmed SELECTION it exactly matches an option the
575
+ * question offered (`input.questions[].options[].label`). One decision per
576
+ * such question, titled `<question> -> <chosen answer>`. The answer is read
577
+ * from the paired result record's `toolUseResult.answers` map; a question with
578
+ * no recorded answer, or a free-text "Other" reply that matches no offered
579
+ * label, is skipped (it is not a decision, and would otherwise pollute
580
+ * decisions.md / orientation's latest-decision surface).
577
581
  *
578
582
  * Exit codes and per-command durations are not present in the transcript, so
579
583
  * `command_executed.exit_code` is `null` and `duration_ms` is `0`.
@@ -3397,6 +3401,31 @@ type NoteRecord = {
3397
3401
  occurredAt: string;
3398
3402
  host: string | null;
3399
3403
  };
3404
+ /**
3405
+ * One recent session condensed to its direction signal, for the "最近の流れ"
3406
+ * (recent direction) section. Across the last N non-archived sessions
3407
+ * (newest first), this surfaces the ARC of recent intent — the decision titles
3408
+ * and next-step notes recorded in EACH session — rather than orientation's
3409
+ * single latest decision/note, which can be stale or missing. When a session
3410
+ * recorded neither a decision nor a note, its top changed files stand in as the
3411
+ * activity signal, so the trajectory stays visible even when explicit capture
3412
+ * was missed (the read-side safety net for the intent-leak gap).
3413
+ */
3414
+ type RecentSessionDigest = {
3415
+ sessionId: string;
3416
+ label: string | null;
3417
+ /** Session boundary (ended_at ?? started_at); the basis for its relative age. */
3418
+ occurredAt: string;
3419
+ host: string | null;
3420
+ /** Non-voided decision titles recorded in this session, chronological, capped. */
3421
+ decisions: string[];
3422
+ /** Count of this session's non-voided decisions beyond those in `decisions`. */
3423
+ decisionsOverflow: number;
3424
+ /** next_step note bodies recorded in this session, chronological, capped. */
3425
+ notes: string[];
3426
+ /** Top changed files, populated ONLY when the session has no decision and no note. */
3427
+ files: string[];
3428
+ };
3400
3429
  type PendingApproval = {
3401
3430
  id: string;
3402
3431
  risk: string;
@@ -3470,6 +3499,15 @@ type OrientationSummary = {
3470
3499
  * step / handoff ("次の起点") surfaced in the forward section; null when none.
3471
3500
  */
3472
3501
  latestNote: NoteRecord | null;
3502
+ /**
3503
+ * The last N non-archived sessions (newest first) condensed to their direction
3504
+ * signal — the "最近の流れ" (recent direction) arc. Distinct from the single
3505
+ * latest decision/note: it shows the trajectory of intent across recent
3506
+ * sessions, and falls back to each session's changed files when no decision or
3507
+ * note was captured, so a resuming agent has grounding even when explicit
3508
+ * capture was missed. Empty when no non-archived sessions exist.
3509
+ */
3510
+ recentDirection: RecentSessionDigest[];
3473
3511
  /**
3474
3512
  * related_files of the latest session, deduped + sorted + capped at the
3475
3513
  * display limit. `outOfRoot` lists the entries (over the FULL deduped set,
package/dist/index.js CHANGED
@@ -246,11 +246,16 @@ function claudeTranscriptToImportPayload(records, options) {
246
246
  const useId = readString2(item.id);
247
247
  const answers = useId !== void 0 ? askAnswers.get(useId) : void 0;
248
248
  if (answers !== void 0) {
249
+ const offeredByQuestion = readOfferedOptions(input);
249
250
  for (const [question, answer] of Object.entries(answers)) {
250
251
  if (question.length === 0) continue;
251
252
  const answerStr = typeof answer === "string" && answer.length > 0 ? answer : void 0;
252
- const title = answerStr !== void 0 ? `${question} -> ${answerStr}` : question;
253
- derived.push(decisionRecordedEvent(ts, placeholderSessionId, title));
253
+ if (answerStr === void 0) continue;
254
+ const offered = offeredByQuestion.get(question);
255
+ if (offered === void 0 || !offered.has(answerStr.trim())) continue;
256
+ derived.push(
257
+ decisionRecordedEvent(ts, placeholderSessionId, `${question} -> ${answerStr}`)
258
+ );
254
259
  }
255
260
  }
256
261
  continue;
@@ -410,6 +415,24 @@ function indexAskAnswers(records) {
410
415
  }
411
416
  return byId;
412
417
  }
418
+ function readOfferedOptions(input) {
419
+ const byQuestion = /* @__PURE__ */ new Map();
420
+ const questions = Array.isArray(input.questions) ? input.questions : [];
421
+ for (const q of questions) {
422
+ if (!isObject2(q)) continue;
423
+ const text = readString2(q.question);
424
+ if (text === void 0) continue;
425
+ const labels = /* @__PURE__ */ new Set();
426
+ const options = Array.isArray(q.options) ? q.options : [];
427
+ for (const o of options) {
428
+ if (!isObject2(o)) continue;
429
+ const label = readString2(o.label);
430
+ if (label !== void 0) labels.add(label.trim());
431
+ }
432
+ byQuestion.set(text, labels);
433
+ }
434
+ return byQuestion;
435
+ }
413
436
 
414
437
  // src/adapters/codex/rollout-importer.ts
415
438
  var CODEX_IMPORT_SOURCE = "codex-import";
@@ -4623,6 +4646,16 @@ async function summarizeOrientation(input) {
4623
4646
  const decisions = [];
4624
4647
  const tracks = [];
4625
4648
  const voidedDecisionIds = /* @__PURE__ */ new Set();
4649
+ const perSession = /* @__PURE__ */ new Map();
4650
+ const recordDirection = (sessionId, kind, value) => {
4651
+ let bucket = perSession.get(sessionId);
4652
+ if (bucket === void 0) {
4653
+ bucket = { decisions: [], notes: [] };
4654
+ perSession.set(sessionId, bucket);
4655
+ }
4656
+ if (kind === "decision" && typeof value !== "string") bucket.decisions.push(value);
4657
+ else if (kind === "note" && typeof value === "string") bucket.notes.push(value);
4658
+ };
4626
4659
  let latestActivityAt = null;
4627
4660
  let latestNote = null;
4628
4661
  const noteActivity = (iso) => {
@@ -4646,6 +4679,12 @@ async function summarizeOrientation(input) {
4646
4679
  sessionId: entry.sessionId,
4647
4680
  host: entry.host
4648
4681
  });
4682
+ if (counted) {
4683
+ recordDirection(entry.sessionId, "decision", {
4684
+ decisionId: ev.decision_id,
4685
+ title: ev.title
4686
+ });
4687
+ }
4649
4688
  if (ev.kind === "track") {
4650
4689
  tracks.push({
4651
4690
  decisionId: ev.decision_id,
@@ -4660,6 +4699,7 @@ async function summarizeOrientation(input) {
4660
4699
  voidedDecisionIds.add(ev.decision_id);
4661
4700
  }
4662
4701
  if (counted && ev.type === "note_added" && ev.kind === "next_step") {
4702
+ recordDirection(entry.sessionId, "note", ev.body);
4663
4703
  if (latestNote === null || Date.parse(ev.occurred_at) > Date.parse(latestNote.occurredAt)) {
4664
4704
  latestNote = {
4665
4705
  body: ev.body,
@@ -4691,6 +4731,29 @@ async function summarizeOrientation(input) {
4691
4731
  const c = Date.parse(b.occurredAt) - Date.parse(a.occurredAt);
4692
4732
  return c !== 0 ? c : b.decisionId.localeCompare(a.decisionId);
4693
4733
  });
4734
+ const recentDirection = entries.filter((e) => e.session.session.status !== "archived").map((e) => ({
4735
+ entry: e,
4736
+ boundary: e.session.session.ended_at ?? e.session.session.started_at
4737
+ })).sort((a, b) => {
4738
+ const c = Date.parse(b.boundary) - Date.parse(a.boundary);
4739
+ return c !== 0 ? c : b.entry.sessionId.localeCompare(a.entry.sessionId);
4740
+ }).slice(0, RECENT_DIRECTION_SESSIONS).map(({ entry, boundary }) => {
4741
+ const bucket = perSession.get(entry.sessionId);
4742
+ const decisionTitles = (bucket?.decisions ?? []).filter((d) => !voidedDecisionIds.has(d.decisionId)).map((d) => d.title);
4743
+ const notes = bucket?.notes ?? [];
4744
+ const hasIntent = decisionTitles.length > 0 || notes.length > 0;
4745
+ const files = hasIntent ? [] : [...new Set(entry.session.session.related_files ?? [])].sort().slice(0, FILES_PER_DIGEST);
4746
+ return {
4747
+ sessionId: entry.sessionId,
4748
+ label: entry.session.session.label ?? null,
4749
+ occurredAt: boundary,
4750
+ host: entry.host,
4751
+ decisions: decisionTitles.slice(0, DECISIONS_PER_DIGEST),
4752
+ decisionsOverflow: Math.max(0, decisionTitles.length - DECISIONS_PER_DIGEST),
4753
+ notes: notes.slice(0, NOTES_PER_DIGEST),
4754
+ files
4755
+ };
4756
+ });
4694
4757
  const taskLoadOpts = {};
4695
4758
  if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
4696
4759
  const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
@@ -4781,6 +4844,7 @@ async function summarizeOrientation(input) {
4781
4844
  decisionCount: decisions.length,
4782
4845
  openTracks,
4783
4846
  latestNote,
4847
+ recentDirection,
4784
4848
  relatedFiles: { displayed, overflow, outOfRoot },
4785
4849
  inFlightTasks,
4786
4850
  plannedTasks,
@@ -4825,6 +4889,11 @@ function formatOrientationBody(summary, opts) {
4825
4889
  lines.push(`> hosts: local, ${summary.hosts.join(", ")}`);
4826
4890
  }
4827
4891
  lines.push("");
4892
+ const banner = stalenessBanner(opts.staleness);
4893
+ if (banner.length > 0) {
4894
+ for (const line of banner) lines.push(line);
4895
+ lines.push("");
4896
+ }
4828
4897
  lines.push("## \u4ECA\u3069\u3053\u306B\u3044\u308B");
4829
4898
  lines.push("");
4830
4899
  if (summary.latestSession !== null) {
@@ -4878,6 +4947,29 @@ function formatOrientationBody(summary, opts) {
4878
4947
  lines.push("- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: (none recorded)");
4879
4948
  }
4880
4949
  lines.push("");
4950
+ lines.push(`## \u6700\u8FD1\u306E\u6D41\u308C (\u76F4\u8FD1 ${RECENT_DIRECTION_SESSIONS} session)`);
4951
+ lines.push("");
4952
+ if (summary.recentDirection.length === 0) {
4953
+ lines.push("- (\u307E\u3060\u8A18\u9332\u304C\u3042\u308A\u307E\u305B\u3093)");
4954
+ } else {
4955
+ for (const s of summary.recentDirection) {
4956
+ const sid = shortId(s.sessionId);
4957
+ const age = relativeAgeJa(s.occurredAt, now);
4958
+ const head = s.label !== null && s.label !== "" ? s.label : sid;
4959
+ lines.push(`- ${head} (${age})${hostSuffix(s.host)}`);
4960
+ if (s.decisions.length > 0) {
4961
+ const more = s.decisionsOverflow > 0 ? ` (+${s.decisionsOverflow})` : "";
4962
+ lines.push(` - \u5224\u65AD: ${s.decisions.map(noteSummary).join("; ")}${more}`);
4963
+ }
4964
+ for (const note of s.notes) {
4965
+ lines.push(` - \u6B21\u306E\u8D77\u70B9: ${noteSummary(note)}`);
4966
+ }
4967
+ if (s.files.length > 0) {
4968
+ lines.push(` - \u5909\u66F4: ${s.files.join(", ")}`);
4969
+ }
4970
+ }
4971
+ }
4972
+ lines.push("");
4881
4973
  lines.push("## \u4F55\u304C\u52D5\u304F");
4882
4974
  lines.push("");
4883
4975
  lines.push(`### \u9032\u884C\u4E2D task (${summary.inFlightTasks.length})`);
@@ -5024,6 +5116,23 @@ function toolDisplayName(kind) {
5024
5116
  return kind ?? "\u4E0D\u660E";
5025
5117
  }
5026
5118
  }
5119
+ function stalenessBanner(staleness) {
5120
+ if (staleness === null) return [];
5121
+ if ((staleness.unverifiableSessions ?? 0) > 0) {
5122
+ return [
5123
+ `> \u26A0\uFE0F **\u6700\u65B0\u3067\u306F\u306A\u3044\u53EF\u80FD\u6027** \u2014 \u5909\u5316\u3057\u305F\u304C\u5B89\u5168\u306B\u53D6\u308A\u8FBC\u3081\u306A\u3044\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${staleness.unverifiableSessions} \u4EF6\u3042\u308A\u307E\u3059\u3002\u7740\u624B\u524D\u306B \`basou verify\` / \`basou refresh --force\`(\u8A73\u7D30\u306F\u672B\u5C3E\u300C\u3053\u308C\u306F\u6700\u65B0\u304B\u300D)\u3002`
5124
+ ];
5125
+ }
5126
+ if (staleness.newSessions > 0 || staleness.updatedSessions > 0) {
5127
+ const parts = [];
5128
+ if (staleness.newSessions > 0) parts.push(`\u65B0\u898F ${staleness.newSessions} \u4EF6`);
5129
+ if (staleness.updatedSessions > 0) parts.push(`\u66F4\u65B0 ${staleness.updatedSessions} \u4EF6`);
5130
+ return [
5131
+ `> \u26A0\uFE0F **\u53E4\u3044\u304B\u3082\u3057\u308C\u307E\u305B\u3093** \u2014 \u672A\u53D6\u308A\u8FBC\u307F\u306E\u4F5C\u696D\u304C\u3042\u308A\u307E\u3059(${parts.join("\u30FB")})\u3002\u7740\u624B\u524D\u306B \`basou refresh\` \u3067\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044(\u8A73\u7D30\u306F\u672B\u5C3E\u300C\u3053\u308C\u306F\u6700\u65B0\u304B\u300D)\u3002`
5132
+ ];
5133
+ }
5134
+ return [];
5135
+ }
5027
5136
  function freshnessVerdict(summary, staleness, now) {
5028
5137
  if (staleness !== null && (staleness.unverifiableSessions ?? 0) > 0) {
5029
5138
  return [
@@ -5088,6 +5197,10 @@ function relativeAge(startedAt, now) {
5088
5197
  if (ms < 1e3) return "just now";
5089
5198
  return `${formatDurationMs(ms)} ago`;
5090
5199
  }
5200
+ var RECENT_DIRECTION_SESSIONS = 5;
5201
+ var DECISIONS_PER_DIGEST = 3;
5202
+ var NOTES_PER_DIGEST = 2;
5203
+ var FILES_PER_DIGEST = 3;
5091
5204
  var NOTE_SUMMARY_MAX = 200;
5092
5205
  function noteSummary(body) {
5093
5206
  const oneLine = body.replace(/\s+/g, " ").trim();