@basou/core 0.45.0 → 0.47.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 +175 -18
- package/dist/index.js +308 -187
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -300,7 +300,7 @@ function renderNudge(counts) {
|
|
|
300
300
|
return [
|
|
301
301
|
`This session ${summary} but recorded no decisions or next step.`,
|
|
302
302
|
"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:",
|
|
303
|
-
' - Decisions: run `basou decision capture` and pipe a JSON array (one object per decision; "title" required, plus optional rationale/alternatives/rejected_reason/linked_files
|
|
303
|
+
' - Decisions: run `basou decision capture` and pipe a JSON array (one object per decision; "title" and "kind" required, plus optional rationale/alternatives/rejected_reason/linked_files). "kind" names the vessel: "track" for an unfinished strategic direction that must resurface until closed, "decision" for a settled point-in-time call.',
|
|
304
304
|
' - Next step: run `basou note "<what you would do next>"`.',
|
|
305
305
|
"If nothing is worth capturing, just stop \u2014 do not invent decisions."
|
|
306
306
|
].join("\n");
|
|
@@ -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
|
-
|
|
2925
|
-
return index.tasks.map((t) => t.id);
|
|
2932
|
+
indexed = (await readTaskIndex(paths)).tasks.map((t) => t.id);
|
|
2926
2933
|
} catch {
|
|
2927
2934
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
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.
|
|
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})`,
|
|
@@ -4619,7 +4643,7 @@ var EN = {
|
|
|
4619
4643
|
noteStaleNote: (age) => `Note: work continued after this was recorded (latest activity ${age}), so this starting point may be stale.`,
|
|
4620
4644
|
fallbackStaleDirection: "- (no planned tasks or recorded next step \u2014 the latest activity postdates the latest decision; ask the user for the continuation point)",
|
|
4621
4645
|
fallbackStaleReferenceLabel: "Reference (possibly stale \u2014 not the current direction)",
|
|
4622
|
-
trackNudge: 'Once the next essential direction is settled, record it as a track
|
|
4646
|
+
trackNudge: 'Once the next essential direction is settled, record it as a track: `basou decision capture` with `"kind":"track"` (the batch form, piped as JSON), or `basou decision record --track` for a single one typed by hand \u2014 it stays surfaced here every session until closed.',
|
|
4623
4647
|
federatedFreshnessNote: "Note: the freshness verdict covers only this machine's local store. Missed work on other hosts cannot be assessed here (run `basou refresh` on each host to sync).",
|
|
4624
4648
|
bannerUnverifiable: (n) => `> \u26A0\uFE0F **Re-import needed** \u2014 ${n} session(s) changed in the native logs but cannot be imported by a plain refresh. Re-import with \`basou refresh --force\` (details under "Is this current" at the bottom).`,
|
|
4625
4649
|
bannerStale: (parts) => `> \u26A0\uFE0F **Stale (uncaptured: ${parts})** \u2014 run \`basou refresh\` before starting work (details under "Is this current" at the bottom).`,
|
|
@@ -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})`,
|
|
@@ -4721,7 +4746,7 @@ var JA = {
|
|
|
4721
4746
|
noteStaleNote: (age) => `\u6CE8: \u3053\u306E\u8D77\u70B9\u306E\u8A18\u9332\u5F8C (\u6700\u7D42\u6D3B\u52D5 ${age}) \u3082\u4F5C\u696D\u304C\u7D9A\u3044\u3066\u3044\u307E\u3059\u3002\u518D\u958B\u70B9\u304C\u53E4\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`,
|
|
4722
4747
|
fallbackStaleDirection: "- (no planned tasks or recorded next step \u2014 \u6700\u7D42\u6D3B\u52D5\u306F\u76F4\u8FD1\u306E\u5224\u65AD\u3088\u308A\u5F8C\u3067\u3059\u3002\u7D99\u7D9A\u70B9\u3092\u30E6\u30FC\u30B6\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044)",
|
|
4723
4748
|
fallbackStaleReferenceLabel: "\u53C2\u8003 (\u53E4\u3044\u53EF\u80FD\u6027\u30FB\u65B9\u91DD\u3067\u306F\u306A\u3044)",
|
|
4724
|
-
trackNudge: '\u6B21\u306B\u4F5C\u308B\u3079\u304D\u672C\u8CEA\u7684\u306A\u65B9\u5411\u6027\u304C\u5B9A\u307E\u3063\u305F\u3089
|
|
4749
|
+
trackNudge: '\u6B21\u306B\u4F5C\u308B\u3079\u304D\u672C\u8CEA\u7684\u306A\u65B9\u5411\u6027\u304C\u5B9A\u307E\u3063\u305F\u3089 track \u5316\u3059\u308B\u3068\u3001close \u307E\u3067\u6BCE session \u3053\u3053\u306B\u7D99\u7D9A\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u307E\u3068\u3081\u3066 JSON \u3067\u6E21\u3059\u306A\u3089 `basou decision capture` \u306B `"kind":"track"` \u3092\u3001\u624B\u3067 1 \u4EF6\u66F8\u304F\u306A\u3089 `basou decision record --track` \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002',
|
|
4725
4750
|
federatedFreshnessNote: "\u6CE8: \u9BAE\u5EA6\u5224\u5B9A\u306F\u3053\u306E\u30DE\u30B7\u30F3\u306E\u30ED\u30FC\u30AB\u30EB\u30B9\u30C8\u30A2\u306E\u307F\u304C\u5BFE\u8C61\u3067\u3059\u3002\u4ED6\u30DB\u30B9\u30C8\u306E\u53D6\u308A\u3053\u307C\u3057\u306F\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093(\u5404\u30DB\u30B9\u30C8\u3067 basou refresh \u3092\u5B9F\u884C\u3057\u540C\u671F\u3057\u3066\u304F\u3060\u3055\u3044)\u3002",
|
|
4726
4751
|
bannerUnverifiable: (n) => `> \u26A0\uFE0F **\u518D\u53D6\u308A\u8FBC\u307F\u304C\u5FC5\u8981** \u2014 native \u30ED\u30B0\u304C\u5909\u5316\u3057\u305F\u304C\u901A\u5E38\u306E refresh \u3067\u306F\u53D6\u308A\u8FBC\u3081\u306A\u3044\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${n} \u4EF6\u3042\u308A\u307E\u3059\u3002\`basou refresh --force\` \u3067\u518D\u53D6\u308A\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044(\u8A73\u7D30\u306F\u672B\u5C3E\u300C\u3053\u308C\u306F\u6700\u65B0\u304B\u300D)\u3002`,
|
|
4727
4752
|
bannerStale: (parts) => `> \u26A0\uFE0F **\u53E4\u3044\u3067\u3059\uFF08\u672A\u53D6\u308A\u8FBC\u307F ${parts}\uFF09** \u2014 \u7740\u624B\u524D\u306B\u5FC5\u305A \`basou refresh\` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044(\u8A73\u7D30\u306F\u672B\u5C3E\u300C\u3053\u308C\u306F\u6700\u65B0\u304B\u300D)\u3002`,
|
|
@@ -6045,7 +6070,7 @@ function parseBuildStamp(raw) {
|
|
|
6045
6070
|
}
|
|
6046
6071
|
}
|
|
6047
6072
|
var BASOU_CORE_BUILD = parseBuildStamp(
|
|
6048
|
-
true ? '{"version":"0.
|
|
6073
|
+
true ? '{"version":"0.47.0","commit":"831e0cb","committedAt":"2026-09-20T22:28:13+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 (!
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
9705
|
+
body = await readFile10(gitignorePath, "utf8");
|
|
9431
9706
|
existed = true;
|
|
9432
9707
|
} catch (error) {
|
|
9433
|
-
if (
|
|
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
|
-
|
|
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,
|