@basou/core 0.45.0 → 0.46.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
@@ -328,6 +328,13 @@ function toolUsesOf2(record) {
328
328
  }
329
329
  return result;
330
330
  }
331
+ function transcriptStartedAt(records) {
332
+ for (const record of records) {
333
+ const timestamp = readString2(record.timestamp);
334
+ if (timestamp !== void 0 && !Number.isNaN(Date.parse(timestamp))) return timestamp;
335
+ }
336
+ return void 0;
337
+ }
331
338
 
332
339
  // src/ids/ulid.ts
333
340
  import { isValid as isValidUlid, monotonicFactory } from "ulid";
@@ -2920,32 +2927,48 @@ ${trimmedBody}`;
2920
2927
  }
2921
2928
  var TASK_FILENAME_RE = /^(.+)\.md$/;
2922
2929
  async function enumerateTaskIds(paths) {
2930
+ let indexed = null;
2923
2931
  try {
2924
- const index = await readTaskIndex(paths);
2925
- return index.tasks.map((t) => t.id);
2932
+ indexed = (await readTaskIndex(paths)).tasks.map((t) => t.id);
2926
2933
  } catch {
2927
2934
  }
2928
- const ids = await enumerateTaskIdsFromDisk(paths);
2929
- if (ids.length === 0) {
2930
- return ids;
2935
+ let onDisk;
2936
+ try {
2937
+ onDisk = await enumerateTaskIdsFromDisk(paths);
2938
+ } catch (error) {
2939
+ if (indexed !== null) return indexed;
2940
+ throw error;
2941
+ }
2942
+ if (indexed !== null && sameTaskIdSet(indexed, onDisk)) return indexed;
2943
+ if (onDisk.length === 0) {
2944
+ return onDisk;
2931
2945
  }
2932
2946
  const entries = [];
2933
- for (const id of ids) {
2947
+ let anyUnreadable = false;
2948
+ for (const id of onDisk) {
2934
2949
  try {
2935
2950
  const doc = await readTaskFile(paths, id);
2936
2951
  entries.push(buildTaskIndexEntry(doc.task.task));
2937
2952
  } catch {
2953
+ anyUnreadable = true;
2938
2954
  }
2939
2955
  }
2940
- await rebuildTaskIndex(paths, entries).catch(() => {
2941
- console.warn("Failed to rebuild tasks/index.json; subsequent reads will retry");
2942
- });
2943
- return ids;
2956
+ if (!anyUnreadable) {
2957
+ await rebuildTaskIndex(paths, entries).catch(() => {
2958
+ console.warn("Failed to rebuild tasks/index.json; subsequent reads will retry");
2959
+ });
2960
+ }
2961
+ return onDisk;
2962
+ }
2963
+ function sameTaskIdSet(a, b) {
2964
+ if (a.length !== b.length) return false;
2965
+ const seen = new Set(a);
2966
+ return b.every((id) => seen.has(id));
2944
2967
  }
2945
2968
  async function enumerateTaskIdsFromDisk(paths) {
2946
2969
  let entries;
2947
2970
  try {
2948
- entries = (await readdir3(paths.tasks, { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2971
+ entries = (await readdir3(paths.tasks, { withFileTypes: true })).filter((d) => !d.isDirectory()).map((d) => d.name);
2949
2972
  } catch (error) {
2950
2973
  if (findErrorCode(error, "ENOENT")) return [];
2951
2974
  throw new Error("Failed to enumerate tasks", { cause: error });
@@ -4603,6 +4626,7 @@ var EN = {
4603
4626
  noTasksRecorded: "(no tasks recorded)",
4604
4627
  noTasksInFlight: "(tasks on record, none in flight)",
4605
4628
  tasksUnreadable: "(tasks on record, some unreadable -- in flight unknown)",
4629
+ tasksUnreadableAlongside: (n) => n === 1 ? "(and 1 task file could not be read -- what it holds is unknown)" : `(and ${n} task files could not be read -- what they hold is unknown)`,
4606
4630
  pendingApprovalsHeading: (n) => `### Pending approvals (${n})`,
4607
4631
  suspectSessionsHeading: (n) => `### Suspect sessions (${n})`,
4608
4632
  openTracksHeading: (n) => `### Open tracks (shown until closed) (${n})`,
@@ -4705,6 +4729,7 @@ var JA = {
4705
4729
  noTasksRecorded: "(task \u304C 1 \u4EF6\u3082\u8A18\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093)",
4706
4730
  noTasksInFlight: "(\u8A18\u9332\u6E08\u307F\u306E task \u306F\u3042\u308A\u307E\u3059\u304C\u3001\u9032\u884C\u4E2D\u306F\u3042\u308A\u307E\u305B\u3093)",
4707
4731
  tasksUnreadable: "(\u8A18\u9332\u6E08\u307F\u306E task \u306B\u8AAD\u3081\u306A\u3044\u3082\u306E\u304C\u3042\u308A\u3001\u9032\u884C\u4E2D\u304B\u306F\u4E0D\u660E\u3067\u3059)",
4732
+ tasksUnreadableAlongside: (n) => `(\u307B\u304B\u306B\u8AAD\u3081\u306A\u3044 task \u30D5\u30A1\u30A4\u30EB\u304C ${n} \u4EF6\u3042\u308A\u3001\u5185\u5BB9\u306F\u4E0D\u660E\u3067\u3059)`,
4708
4733
  pendingApprovalsHeading: (n) => `### \u627F\u8A8D\u5F85\u3061 (${n})`,
4709
4734
  suspectSessionsHeading: (n) => `### \u8981\u6CE8\u610F session (${n})`,
4710
4735
  openTracksHeading: (n) => `### \u672A\u5B8C\u30C8\u30E9\u30C3\u30AF (close \u307E\u3067\u7D99\u7D9A\u8868\u793A) (${n})`,
@@ -6045,7 +6070,7 @@ function parseBuildStamp(raw) {
6045
6070
  }
6046
6071
  }
6047
6072
  var BASOU_CORE_BUILD = parseBuildStamp(
6048
- true ? '{"version":"0.45.0","commit":"6b04208","committedAt":"2026-09-19T01:07:28+09:00"}' : void 0
6073
+ true ? '{"version":"0.46.0","commit":"dbc9c57","committedAt":"2026-09-19T15:43:35+09:00"}' : void 0
6049
6074
  );
6050
6075
 
6051
6076
  // src/lib/duration.ts
@@ -6624,6 +6649,9 @@ function formatOrientationBody(summary, opts) {
6624
6649
  const linkedSuffix = t2.linkedSessions > 1 ? ` \u2014 linked_sessions: ${t2.linkedSessions}` : "";
6625
6650
  lines.push(`- ${oneLine(t2.title)} (${t2.status}) [${shortId(t2.id)}]${linkedSuffix}`);
6626
6651
  }
6652
+ if (summary.unreadableTaskCount > 0) {
6653
+ lines.push(`- ${t.orientation.tasksUnreadableAlongside(summary.unreadableTaskCount)}`);
6654
+ }
6627
6655
  }
6628
6656
  lines.push("");
6629
6657
  lines.push(t.orientation.pendingApprovalsHeading(summary.pendingApprovals.length));
@@ -7560,6 +7588,253 @@ function planWorkspaceView(facts, existing = [], rosterNames = []) {
7560
7588
  };
7561
7589
  }
7562
7590
 
7591
+ // src/protocol/protocol-stamp.ts
7592
+ import { createHash as createHash3 } from "crypto";
7593
+
7594
+ // src/storage/markdown-store.ts
7595
+ import { readFile as readFile9 } from "fs/promises";
7596
+ var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
7597
+ var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
7598
+ var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
7599
+ var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
7600
+ var ORIENTATION_START = "<!-- BASOU:ORIENTATION:START -->";
7601
+ var ORIENTATION_END = "<!-- BASOU:ORIENTATION:END -->";
7602
+ var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
7603
+ async function readMarkdownFile(filePath) {
7604
+ try {
7605
+ return await readFile9(filePath, "utf8");
7606
+ } catch (error) {
7607
+ if (hasErrorCode4(error) && error.code === "ENOENT") return null;
7608
+ throw new Error("Failed to read markdown file", { cause: error });
7609
+ }
7610
+ }
7611
+ async function writeMarkdownFile(filePath, body) {
7612
+ try {
7613
+ await atomicReplace(filePath, body);
7614
+ } catch (error) {
7615
+ throw new Error("Failed to write markdown file", { cause: error });
7616
+ }
7617
+ }
7618
+ function parseMarkers(content, markers = DEFAULT_MARKERS) {
7619
+ const bom = content.charCodeAt(0) === 65279 ? "\uFEFF" : "";
7620
+ const body = bom === "" ? content : content.slice(1);
7621
+ const lines = body.split(/\r?\n/);
7622
+ const startLines = [];
7623
+ const endLines = [];
7624
+ for (let i = 0; i < lines.length; i++) {
7625
+ if (lines[i] === markers.start) startLines.push(i);
7626
+ else if (lines[i] === markers.end) endLines.push(i);
7627
+ }
7628
+ if (startLines.length === 0 && endLines.length === 0) return { kind: "no_markers" };
7629
+ if (startLines.length === 0) return { kind: "missing_start" };
7630
+ if (endLines.length === 0) return { kind: "missing_end" };
7631
+ if (startLines.length >= 2 || endLines.length >= 2) return { kind: "multiple_pairs" };
7632
+ const startLineIdx = startLines[0];
7633
+ const endLineIdx = endLines[0];
7634
+ if (endLineIdx < startLineIdx) return { kind: "wrong_order" };
7635
+ const startOffset = lineStartOffset(body, startLineIdx);
7636
+ const endLineStart = lineStartOffset(body, endLineIdx);
7637
+ const startLineEnd = startOffset + markers.start.length;
7638
+ const endLineEnd = endLineStart + markers.end.length;
7639
+ const before = bom + body.slice(0, startOffset);
7640
+ const afterStartNewline = skipOneNewline(body, startLineEnd);
7641
+ const beforeEndNewline = trimOneNewline(body, endLineStart);
7642
+ const generated = body.slice(afterStartNewline, beforeEndNewline);
7643
+ const after = body.slice(endLineEnd);
7644
+ return { kind: "ok", before, generated, after };
7645
+ }
7646
+ function defuseMarkerLines(body, markers) {
7647
+ const isMarker = (line) => {
7648
+ const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
7649
+ return bare === markers.start || bare === markers.end;
7650
+ };
7651
+ const lines = body.split("\n");
7652
+ if (!lines.some(isMarker)) return body;
7653
+ return lines.map((line) => isMarker(line) ? ` ${line}` : line).join("\n");
7654
+ }
7655
+ function renderWithMarkers(existing, generated, fileLabel, markers = DEFAULT_MARKERS) {
7656
+ const normalized = defuseMarkerLines(
7657
+ generated.endsWith("\n") ? generated : `${generated}
7658
+ `,
7659
+ markers
7660
+ );
7661
+ if (existing === null) {
7662
+ return `${markers.start}
7663
+ ${normalized}${markers.end}
7664
+ `;
7665
+ }
7666
+ const section = parseMarkers(existing, markers);
7667
+ switch (section.kind) {
7668
+ case "ok":
7669
+ return `${section.before}${markers.start}
7670
+ ${normalized}${markers.end}${section.after}`;
7671
+ case "no_markers":
7672
+ throw new Error(`Markers missing in ${fileLabel}`);
7673
+ case "missing_start":
7674
+ case "missing_end":
7675
+ case "multiple_pairs":
7676
+ case "wrong_order":
7677
+ throw new Error(`Markers mismatched in ${fileLabel}`);
7678
+ }
7679
+ }
7680
+ function seedMarkers(existing, generated, fileLabel, markers = DEFAULT_MARKERS) {
7681
+ const normalized = generated.endsWith("\n") ? generated : `${generated}
7682
+ `;
7683
+ if (existing === null) {
7684
+ return `${markers.start}
7685
+ ${normalized}${markers.end}
7686
+ `;
7687
+ }
7688
+ const section = parseMarkers(existing, markers);
7689
+ switch (section.kind) {
7690
+ case "ok":
7691
+ return renderWithMarkers(existing, generated, fileLabel, markers);
7692
+ case "no_markers": {
7693
+ const bom = existing.charCodeAt(0) === 65279 ? "\uFEFF" : "";
7694
+ const body = bom === "" ? existing : existing.slice(1);
7695
+ return `${bom}${markers.start}
7696
+ ${normalized}${markers.end}
7697
+
7698
+ ${body}`;
7699
+ }
7700
+ case "missing_start":
7701
+ case "missing_end":
7702
+ case "multiple_pairs":
7703
+ case "wrong_order":
7704
+ throw new Error(`Markers mismatched in ${fileLabel}`);
7705
+ }
7706
+ }
7707
+ function removeMarkerSection(existing, fileLabel, markers = DEFAULT_MARKERS) {
7708
+ const section = parseMarkers(existing, markers);
7709
+ switch (section.kind) {
7710
+ case "no_markers":
7711
+ return existing;
7712
+ case "ok": {
7713
+ const after = section.after.replace(/^\r?\n/, "");
7714
+ return section.before + after;
7715
+ }
7716
+ case "missing_start":
7717
+ case "missing_end":
7718
+ case "multiple_pairs":
7719
+ case "wrong_order":
7720
+ throw new Error(`Markers mismatched in ${fileLabel}`);
7721
+ }
7722
+ }
7723
+ function lineStartOffset(content, lineIdx) {
7724
+ if (lineIdx === 0) return 0;
7725
+ let offset = 0;
7726
+ let line = 0;
7727
+ while (offset < content.length && line < lineIdx) {
7728
+ const ch = content[offset];
7729
+ if (ch === "\n") {
7730
+ line += 1;
7731
+ offset += 1;
7732
+ } else if (ch === "\r") {
7733
+ offset += 1;
7734
+ if (content[offset] === "\n") offset += 1;
7735
+ line += 1;
7736
+ } else {
7737
+ offset += 1;
7738
+ }
7739
+ }
7740
+ return offset;
7741
+ }
7742
+ function skipOneNewline(content, offset) {
7743
+ if (content[offset] === "\r" && content[offset + 1] === "\n") return offset + 2;
7744
+ if (content[offset] === "\n") return offset + 1;
7745
+ return offset;
7746
+ }
7747
+ function trimOneNewline(content, offset) {
7748
+ if (offset >= 2 && content[offset - 2] === "\r" && content[offset - 1] === "\n")
7749
+ return offset - 2;
7750
+ if (offset >= 1 && content[offset - 1] === "\n") return offset - 1;
7751
+ return offset;
7752
+ }
7753
+ function hasErrorCode4(error) {
7754
+ if (!(error instanceof Error)) return false;
7755
+ const codeProp = error.code;
7756
+ return typeof codeProp === "string";
7757
+ }
7758
+
7759
+ // src/protocol/protocol-stamp.ts
7760
+ var STAMP_PREFIX = "<!-- basou:protocols v1 ";
7761
+ var CONTENT_HASH_LENGTH = 16;
7762
+ var PROTOCOL_UPDATE_TOKEN_PREFIX = "basou:protocol-updated";
7763
+ function protocolBlockHash(sections) {
7764
+ return createHash3("sha256").update(sections, "utf8").digest("hex").slice(0, CONTENT_HASH_LENGTH);
7765
+ }
7766
+ function protocolUpdateToken(contentHash) {
7767
+ return `${PROTOCOL_UPDATE_TOKEN_PREFIX}:${contentHash}`;
7768
+ }
7769
+ function renderProtocolStamp(stamp) {
7770
+ return `${STAMP_PREFIX}changed=${stamp.changedAt} content=${stamp.contentHash} -->`;
7771
+ }
7772
+ function parseProtocolStamp(blockBody) {
7773
+ const line = blockBody.split(/\r?\n/).find((l) => l.startsWith(STAMP_PREFIX));
7774
+ if (line === void 0) return null;
7775
+ const inner = line.slice(STAMP_PREFIX.length).replace(/-->\s*$/, "").trim();
7776
+ let changedAt;
7777
+ let contentHash;
7778
+ for (const field of inner.split(/\s+/)) {
7779
+ const eq = field.indexOf("=");
7780
+ if (eq <= 0) continue;
7781
+ const name = field.slice(0, eq);
7782
+ const value = field.slice(eq + 1);
7783
+ if (name === "changed") changedAt = value;
7784
+ else if (name === "content") contentHash = value;
7785
+ }
7786
+ if (changedAt === void 0 || contentHash === void 0) return null;
7787
+ if (changedAt.length === 0 || contentHash.length === 0) return null;
7788
+ if (Number.isNaN(Date.parse(changedAt))) return null;
7789
+ return { changedAt, contentHash };
7790
+ }
7791
+ function carryForwardProtocolStamp(input) {
7792
+ const contentHash = protocolBlockHash(input.sections);
7793
+ if (input.previous !== null && input.previous.contentHash === contentHash) {
7794
+ return input.previous;
7795
+ }
7796
+ return { changedAt: forwardOf(input.now, input.previous), contentHash };
7797
+ }
7798
+ function forwardOf(now, previous) {
7799
+ if (previous === null) return now;
7800
+ const nowMs = Date.parse(now);
7801
+ const beforeMs = Date.parse(previous.changedAt);
7802
+ if (Number.isNaN(nowMs) || Number.isNaN(beforeMs) || nowMs > beforeMs) return now;
7803
+ return new Date(beforeMs + 1).toISOString();
7804
+ }
7805
+ function protocolSectionsFrom(blockBody) {
7806
+ const lines = blockBody.split(/\r?\n/);
7807
+ const at = lines.findIndex((l) => l.startsWith(STAMP_PREFIX));
7808
+ if (at === -1) return null;
7809
+ return sectionsAfter(lines.slice(at + 1));
7810
+ }
7811
+ function unstampedProtocolSectionsFrom(blockBody) {
7812
+ const lines = blockBody.split(/\r?\n/);
7813
+ while (lines.length > 0) {
7814
+ const first = (lines[0] ?? "").trim();
7815
+ if (first.length === 0 || first.startsWith("<!--")) lines.shift();
7816
+ else break;
7817
+ }
7818
+ return sectionsAfter(lines);
7819
+ }
7820
+ function sectionsAfter(lines) {
7821
+ const closing = lines.indexOf(PROTOCOL_END);
7822
+ const body = closing === -1 ? [...lines] : lines.slice(0, closing);
7823
+ while (body.length > 0 && (body[0] ?? "").trim().length === 0) body.shift();
7824
+ return body.join("\n").replace(/\s+$/, "");
7825
+ }
7826
+ function isProtocolUpdateDue(input) {
7827
+ const startedMs = Date.parse(input.sessionStartedAt);
7828
+ if (Number.isNaN(startedMs)) return false;
7829
+ return Date.parse(input.stamp.changedAt) > startedMs;
7830
+ }
7831
+ function renderProtocolUpdate(sections, stamp) {
7832
+ return [
7833
+ `The standing protocols changed after this session started (${protocolUpdateToken(stamp.contentHash)}). What follows is the COMPLETE current set and SUPERSEDES the copy read at session start \u2014 follow it for the rest of the session, and treat anything read earlier but absent below as withdrawn.`,
7834
+ sections
7835
+ ].join("\n\n");
7836
+ }
7837
+
7563
7838
  // src/report/report-renderer.ts
7564
7839
  import { join as join18 } from "path";
7565
7840
 
@@ -9381,7 +9656,7 @@ async function ensureBasouDirectory(repositoryRoot) {
9381
9656
  try {
9382
9657
  existing = await lstat4(paths.root);
9383
9658
  } catch (error) {
9384
- if (!hasErrorCode4(error) || error.code !== "ENOENT") {
9659
+ if (!hasErrorCode5(error) || error.code !== "ENOENT") {
9385
9660
  throw new Error("Failed to inspect .basou directory", { cause: error });
9386
9661
  }
9387
9662
  }
@@ -9404,20 +9679,20 @@ async function mkdirLabeled(target, label) {
9404
9679
  try {
9405
9680
  await mkdir4(target, { recursive: true });
9406
9681
  } catch (error) {
9407
- if (hasErrorCode4(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
9682
+ if (hasErrorCode5(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
9408
9683
  throw new Error(`${label} exists but is not a directory`, { cause: error });
9409
9684
  }
9410
9685
  throw new Error(`Failed to create ${label}`, { cause: error });
9411
9686
  }
9412
9687
  }
9413
- function hasErrorCode4(error) {
9688
+ function hasErrorCode5(error) {
9414
9689
  if (!(error instanceof Error)) return false;
9415
9690
  const codeProp = error.code;
9416
9691
  return typeof codeProp === "string";
9417
9692
  }
9418
9693
 
9419
9694
  // src/storage/gitignore.ts
9420
- import { readFile as readFile9, writeFile as writeFile2 } from "fs/promises";
9695
+ import { readFile as readFile10, writeFile as writeFile2 } from "fs/promises";
9421
9696
  import { join as join21 } from "path";
9422
9697
  var MARKER = "# Basou - default ignore";
9423
9698
  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";
@@ -9427,10 +9702,10 @@ async function appendBasouGitignore(repositoryRoot, options = {}) {
9427
9702
  let body;
9428
9703
  let existed;
9429
9704
  try {
9430
- body = await readFile9(gitignorePath, "utf8");
9705
+ body = await readFile10(gitignorePath, "utf8");
9431
9706
  existed = true;
9432
9707
  } catch (error) {
9433
- if (hasErrorCode5(error) && error.code === "ENOENT") {
9708
+ if (hasErrorCode6(error) && error.code === "ENOENT") {
9434
9709
  body = "";
9435
9710
  existed = false;
9436
9711
  } else {
@@ -9464,174 +9739,9 @@ function composeNextBody(existing, block) {
9464
9739
  return `${normalized}
9465
9740
  ${block}`;
9466
9741
  }
9467
- function hasErrorCode5(error) {
9468
- if (!(error instanceof Error)) return false;
9469
- return typeof error.code === "string";
9470
- }
9471
-
9472
- // src/storage/markdown-store.ts
9473
- import { readFile as readFile10 } from "fs/promises";
9474
- var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
9475
- var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
9476
- var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
9477
- var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
9478
- var ORIENTATION_START = "<!-- BASOU:ORIENTATION:START -->";
9479
- var ORIENTATION_END = "<!-- BASOU:ORIENTATION:END -->";
9480
- var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
9481
- async function readMarkdownFile(filePath) {
9482
- try {
9483
- return await readFile10(filePath, "utf8");
9484
- } catch (error) {
9485
- if (hasErrorCode6(error) && error.code === "ENOENT") return null;
9486
- throw new Error("Failed to read markdown file", { cause: error });
9487
- }
9488
- }
9489
- async function writeMarkdownFile(filePath, body) {
9490
- try {
9491
- await atomicReplace(filePath, body);
9492
- } catch (error) {
9493
- throw new Error("Failed to write markdown file", { cause: error });
9494
- }
9495
- }
9496
- function parseMarkers(content, markers = DEFAULT_MARKERS) {
9497
- const bom = content.charCodeAt(0) === 65279 ? "\uFEFF" : "";
9498
- const body = bom === "" ? content : content.slice(1);
9499
- const lines = body.split(/\r?\n/);
9500
- const startLines = [];
9501
- const endLines = [];
9502
- for (let i = 0; i < lines.length; i++) {
9503
- if (lines[i] === markers.start) startLines.push(i);
9504
- else if (lines[i] === markers.end) endLines.push(i);
9505
- }
9506
- if (startLines.length === 0 && endLines.length === 0) return { kind: "no_markers" };
9507
- if (startLines.length === 0) return { kind: "missing_start" };
9508
- if (endLines.length === 0) return { kind: "missing_end" };
9509
- if (startLines.length >= 2 || endLines.length >= 2) return { kind: "multiple_pairs" };
9510
- const startLineIdx = startLines[0];
9511
- const endLineIdx = endLines[0];
9512
- if (endLineIdx < startLineIdx) return { kind: "wrong_order" };
9513
- const startOffset = lineStartOffset(body, startLineIdx);
9514
- const endLineStart = lineStartOffset(body, endLineIdx);
9515
- const startLineEnd = startOffset + markers.start.length;
9516
- const endLineEnd = endLineStart + markers.end.length;
9517
- const before = bom + body.slice(0, startOffset);
9518
- const afterStartNewline = skipOneNewline(body, startLineEnd);
9519
- const beforeEndNewline = trimOneNewline(body, endLineStart);
9520
- const generated = body.slice(afterStartNewline, beforeEndNewline);
9521
- const after = body.slice(endLineEnd);
9522
- return { kind: "ok", before, generated, after };
9523
- }
9524
- function defuseMarkerLines(body, markers) {
9525
- const isMarker = (line) => {
9526
- const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
9527
- return bare === markers.start || bare === markers.end;
9528
- };
9529
- const lines = body.split("\n");
9530
- if (!lines.some(isMarker)) return body;
9531
- return lines.map((line) => isMarker(line) ? ` ${line}` : line).join("\n");
9532
- }
9533
- function renderWithMarkers(existing, generated, fileLabel, markers = DEFAULT_MARKERS) {
9534
- const normalized = defuseMarkerLines(
9535
- generated.endsWith("\n") ? generated : `${generated}
9536
- `,
9537
- markers
9538
- );
9539
- if (existing === null) {
9540
- return `${markers.start}
9541
- ${normalized}${markers.end}
9542
- `;
9543
- }
9544
- const section = parseMarkers(existing, markers);
9545
- switch (section.kind) {
9546
- case "ok":
9547
- return `${section.before}${markers.start}
9548
- ${normalized}${markers.end}${section.after}`;
9549
- case "no_markers":
9550
- throw new Error(`Markers missing in ${fileLabel}`);
9551
- case "missing_start":
9552
- case "missing_end":
9553
- case "multiple_pairs":
9554
- case "wrong_order":
9555
- throw new Error(`Markers mismatched in ${fileLabel}`);
9556
- }
9557
- }
9558
- function seedMarkers(existing, generated, fileLabel, markers = DEFAULT_MARKERS) {
9559
- const normalized = generated.endsWith("\n") ? generated : `${generated}
9560
- `;
9561
- if (existing === null) {
9562
- return `${markers.start}
9563
- ${normalized}${markers.end}
9564
- `;
9565
- }
9566
- const section = parseMarkers(existing, markers);
9567
- switch (section.kind) {
9568
- case "ok":
9569
- return renderWithMarkers(existing, generated, fileLabel, markers);
9570
- case "no_markers": {
9571
- const bom = existing.charCodeAt(0) === 65279 ? "\uFEFF" : "";
9572
- const body = bom === "" ? existing : existing.slice(1);
9573
- return `${bom}${markers.start}
9574
- ${normalized}${markers.end}
9575
-
9576
- ${body}`;
9577
- }
9578
- case "missing_start":
9579
- case "missing_end":
9580
- case "multiple_pairs":
9581
- case "wrong_order":
9582
- throw new Error(`Markers mismatched in ${fileLabel}`);
9583
- }
9584
- }
9585
- function removeMarkerSection(existing, fileLabel, markers = DEFAULT_MARKERS) {
9586
- const section = parseMarkers(existing, markers);
9587
- switch (section.kind) {
9588
- case "no_markers":
9589
- return existing;
9590
- case "ok": {
9591
- const after = section.after.replace(/^\r?\n/, "");
9592
- return section.before + after;
9593
- }
9594
- case "missing_start":
9595
- case "missing_end":
9596
- case "multiple_pairs":
9597
- case "wrong_order":
9598
- throw new Error(`Markers mismatched in ${fileLabel}`);
9599
- }
9600
- }
9601
- function lineStartOffset(content, lineIdx) {
9602
- if (lineIdx === 0) return 0;
9603
- let offset = 0;
9604
- let line = 0;
9605
- while (offset < content.length && line < lineIdx) {
9606
- const ch = content[offset];
9607
- if (ch === "\n") {
9608
- line += 1;
9609
- offset += 1;
9610
- } else if (ch === "\r") {
9611
- offset += 1;
9612
- if (content[offset] === "\n") offset += 1;
9613
- line += 1;
9614
- } else {
9615
- offset += 1;
9616
- }
9617
- }
9618
- return offset;
9619
- }
9620
- function skipOneNewline(content, offset) {
9621
- if (content[offset] === "\r" && content[offset + 1] === "\n") return offset + 2;
9622
- if (content[offset] === "\n") return offset + 1;
9623
- return offset;
9624
- }
9625
- function trimOneNewline(content, offset) {
9626
- if (offset >= 2 && content[offset - 2] === "\r" && content[offset - 1] === "\n")
9627
- return offset - 2;
9628
- if (offset >= 1 && content[offset - 1] === "\n") return offset - 1;
9629
- return offset;
9630
- }
9631
9742
  function hasErrorCode6(error) {
9632
9743
  if (!(error instanceof Error)) return false;
9633
- const codeProp = error.code;
9634
- return typeof codeProp === "string";
9744
+ return typeof error.code === "string";
9635
9745
  }
9636
9746
 
9637
9747
  // src/storage/session-import.ts
@@ -10053,6 +10163,7 @@ export {
10053
10163
  ORIENTATION_START,
10054
10164
  PROTOCOL_END,
10055
10165
  PROTOCOL_START,
10166
+ PROTOCOL_UPDATE_TOKEN_PREFIX,
10056
10167
  REVIEW_RECORD_NO_INPUT_HINT,
10057
10168
  RiskLevelSchema,
10058
10169
  SESSION_IMPORT_SCHEMA_VERSION,
@@ -10095,6 +10206,7 @@ export {
10095
10206
  buildSessionStartHookCommand,
10096
10207
  buildStatusSnapshot,
10097
10208
  buildStopHookCommand,
10209
+ carryForwardProtocolStamp,
10098
10210
  chainEvents,
10099
10211
  chainRawJsonLines,
10100
10212
  classifyFilesBySourceRoot,
@@ -10136,6 +10248,7 @@ export {
10136
10248
  isGitNotFound,
10137
10249
  isImportDerivedSource,
10138
10250
  isLazyExpired,
10251
+ isProtocolUpdateDue,
10139
10252
  isRenderable,
10140
10253
  isValidPrefixedId,
10141
10254
  lineHash,
@@ -10150,6 +10263,7 @@ export {
10150
10263
  parseBuildStamp,
10151
10264
  parseDuration,
10152
10265
  parseMarkers,
10266
+ parseProtocolStamp,
10153
10267
  parseReviewRecordInput,
10154
10268
  pathBasename,
10155
10269
  planArchive,
@@ -10159,6 +10273,9 @@ export {
10159
10273
  planWorkspaceView,
10160
10274
  prefixedUlid,
10161
10275
  presetStrings,
10276
+ protocolBlockHash,
10277
+ protocolSectionsFrom,
10278
+ protocolUpdateToken,
10162
10279
  readAllEvents,
10163
10280
  readManifest,
10164
10281
  readMarkdownFile,
@@ -10182,6 +10299,8 @@ export {
10182
10299
  renderHandoff,
10183
10300
  renderOrientation,
10184
10301
  renderPresetBlock,
10302
+ renderProtocolStamp,
10303
+ renderProtocolUpdate,
10185
10304
  renderReport,
10186
10305
  renderViewPresetBlock,
10187
10306
  renderWithMarkers,
@@ -10212,9 +10331,11 @@ export {
10212
10331
  summarizeSymlinkPlan,
10213
10332
  summarizeWiring,
10214
10333
  summarizeWiringDrift,
10334
+ transcriptStartedAt,
10215
10335
  tryRemoteUrl,
10216
10336
  ulid,
10217
10337
  unknownManifestKeys,
10338
+ unstampedProtocolSectionsFrom,
10218
10339
  updateTaskStatusWithEvent,
10219
10340
  upsertSessionStartHook,
10220
10341
  upsertStopHook,