@basou/core 0.48.0 → 0.49.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 +297 -49
- package/dist/index.js +416 -59
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/adapters/command-lookup.ts
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
async function isOnPath(command) {
|
|
4
|
-
return new Promise((
|
|
4
|
+
return new Promise((resolve5) => {
|
|
5
5
|
const child = spawn("which", [command], { stdio: "ignore" });
|
|
6
|
-
child.on("error", () =>
|
|
7
|
-
child.on("exit", (code) =>
|
|
6
|
+
child.on("error", () => resolve5(false));
|
|
7
|
+
child.on("exit", (code) => resolve5(code === 0));
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -913,6 +913,9 @@ function claudeTranscriptToImportPayload(records, options) {
|
|
|
913
913
|
}
|
|
914
914
|
if (minTs === void 0 || maxTs === void 0) return null;
|
|
915
915
|
if (derived.length === 0) return null;
|
|
916
|
+
for (const observed of options.observedFiles ?? []) {
|
|
917
|
+
relatedFiles.add(observed.path);
|
|
918
|
+
}
|
|
916
919
|
derived.sort((a, b) => Date.parse(a.occurred_at) - Date.parse(b.occurred_at));
|
|
917
920
|
const events = [
|
|
918
921
|
sessionStartedEvent(minTs, placeholderSessionId),
|
|
@@ -5619,20 +5622,41 @@ async function getDiff(repoRoot, baseRef, headRef) {
|
|
|
5619
5622
|
let raw;
|
|
5620
5623
|
try {
|
|
5621
5624
|
raw = await git.raw(["diff", "--name-status", `${baseRef}..${headRef}`]);
|
|
5625
|
+
} catch (error) {
|
|
5626
|
+
throw translateDiffError(error);
|
|
5627
|
+
}
|
|
5628
|
+
return { changed_files: parseDiffNameStatus(raw) };
|
|
5629
|
+
}
|
|
5630
|
+
async function getChangesSince(repoRoot, baseRef) {
|
|
5631
|
+
let git;
|
|
5632
|
+
try {
|
|
5633
|
+
git = safeSimpleGit(repoRoot);
|
|
5622
5634
|
} catch (error) {
|
|
5623
5635
|
if (isGitNotFound(error)) {
|
|
5624
5636
|
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5625
5637
|
}
|
|
5626
|
-
|
|
5627
|
-
if (/not a git repository/i.test(message)) {
|
|
5628
|
-
throw new Error("Not a git repository", { cause: error });
|
|
5629
|
-
}
|
|
5630
|
-
if (message.includes("bad revision") || message.includes("unknown revision") || message.includes("ambiguous argument")) {
|
|
5631
|
-
throw new Error("Invalid ref", { cause: error });
|
|
5632
|
-
}
|
|
5633
|
-
throw new Error("Failed to compute git diff", { cause: error });
|
|
5638
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5634
5639
|
}
|
|
5635
|
-
|
|
5640
|
+
let raw;
|
|
5641
|
+
try {
|
|
5642
|
+
raw = await git.raw(["-c", "core.quotePath=false", "diff", "--name-status", baseRef]);
|
|
5643
|
+
} catch (error) {
|
|
5644
|
+
throw translateDiffError(error);
|
|
5645
|
+
}
|
|
5646
|
+
return parseDiffNameStatus(raw);
|
|
5647
|
+
}
|
|
5648
|
+
function translateDiffError(error) {
|
|
5649
|
+
if (isGitNotFound(error)) {
|
|
5650
|
+
return new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5651
|
+
}
|
|
5652
|
+
const message = error instanceof Error ? error.message : "";
|
|
5653
|
+
if (/not a git repository/i.test(message)) {
|
|
5654
|
+
return new Error("Not a git repository", { cause: error });
|
|
5655
|
+
}
|
|
5656
|
+
if (message.includes("bad revision") || message.includes("unknown revision") || message.includes("ambiguous argument") || message.includes("bad object") || message.includes("Invalid revision range")) {
|
|
5657
|
+
return new Error("Invalid ref", { cause: error });
|
|
5658
|
+
}
|
|
5659
|
+
return new Error("Failed to compute git diff", { cause: error });
|
|
5636
5660
|
}
|
|
5637
5661
|
function parseDiffNameStatus(raw) {
|
|
5638
5662
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
@@ -5661,6 +5685,135 @@ function parseDiffNameStatus(raw) {
|
|
|
5661
5685
|
return changes;
|
|
5662
5686
|
}
|
|
5663
5687
|
|
|
5688
|
+
// src/git/working-tree.ts
|
|
5689
|
+
async function getWorkingTreeChanges(repoRoot) {
|
|
5690
|
+
let git;
|
|
5691
|
+
try {
|
|
5692
|
+
git = safeSimpleGit(repoRoot);
|
|
5693
|
+
} catch (error) {
|
|
5694
|
+
if (isGitNotFound(error)) {
|
|
5695
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5696
|
+
}
|
|
5697
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5698
|
+
}
|
|
5699
|
+
let status;
|
|
5700
|
+
try {
|
|
5701
|
+
status = await git.status();
|
|
5702
|
+
} catch (error) {
|
|
5703
|
+
if (isGitNotFound(error)) {
|
|
5704
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5705
|
+
}
|
|
5706
|
+
const message = error instanceof Error ? error.message : "";
|
|
5707
|
+
if (/not a git repository/i.test(message)) {
|
|
5708
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5709
|
+
}
|
|
5710
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5711
|
+
}
|
|
5712
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5713
|
+
const put = (change) => {
|
|
5714
|
+
if (!byPath.has(change.path)) byPath.set(change.path, change);
|
|
5715
|
+
};
|
|
5716
|
+
const conflicted = new Set(status.conflicted);
|
|
5717
|
+
for (const entry of status.renamed) {
|
|
5718
|
+
if (conflicted.has(entry.to)) continue;
|
|
5719
|
+
put({ path: entry.to, status: "renamed", old_path: entry.from });
|
|
5720
|
+
}
|
|
5721
|
+
for (const path2 of status.deleted) {
|
|
5722
|
+
if (conflicted.has(path2)) continue;
|
|
5723
|
+
put({ path: path2, status: "deleted" });
|
|
5724
|
+
}
|
|
5725
|
+
for (const path2 of [...status.created, ...status.not_added]) {
|
|
5726
|
+
if (conflicted.has(path2)) continue;
|
|
5727
|
+
put({ path: path2, status: "added" });
|
|
5728
|
+
}
|
|
5729
|
+
for (const path2 of status.modified) {
|
|
5730
|
+
if (conflicted.has(path2)) continue;
|
|
5731
|
+
put({ path: path2, status: "modified" });
|
|
5732
|
+
}
|
|
5733
|
+
return [...byPath.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
5734
|
+
}
|
|
5735
|
+
async function readHeadSha(repoRoot) {
|
|
5736
|
+
let git;
|
|
5737
|
+
try {
|
|
5738
|
+
git = safeSimpleGit(repoRoot);
|
|
5739
|
+
} catch (error) {
|
|
5740
|
+
if (isGitNotFound(error)) {
|
|
5741
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5742
|
+
}
|
|
5743
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5744
|
+
}
|
|
5745
|
+
let inside;
|
|
5746
|
+
try {
|
|
5747
|
+
inside = await git.checkIsRepo();
|
|
5748
|
+
} catch (error) {
|
|
5749
|
+
if (isGitNotFound(error)) {
|
|
5750
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5751
|
+
}
|
|
5752
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5753
|
+
}
|
|
5754
|
+
if (!inside) throw new Error("Not a git repository");
|
|
5755
|
+
try {
|
|
5756
|
+
const head = (await git.revparse(["HEAD"])).trimEnd();
|
|
5757
|
+
return head.length > 0 ? head : null;
|
|
5758
|
+
} catch {
|
|
5759
|
+
return null;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
async function getUntrackedFiles(repoRoot) {
|
|
5763
|
+
let git;
|
|
5764
|
+
try {
|
|
5765
|
+
git = safeSimpleGit(repoRoot);
|
|
5766
|
+
} catch (error) {
|
|
5767
|
+
if (isGitNotFound(error)) {
|
|
5768
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5769
|
+
}
|
|
5770
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5771
|
+
}
|
|
5772
|
+
let raw;
|
|
5773
|
+
try {
|
|
5774
|
+
raw = await git.raw([
|
|
5775
|
+
"-c",
|
|
5776
|
+
"core.quotePath=false",
|
|
5777
|
+
"ls-files",
|
|
5778
|
+
"--others",
|
|
5779
|
+
"--exclude-standard",
|
|
5780
|
+
"-z"
|
|
5781
|
+
]);
|
|
5782
|
+
} catch (error) {
|
|
5783
|
+
if (isGitNotFound(error)) {
|
|
5784
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5785
|
+
}
|
|
5786
|
+
const message = error instanceof Error ? error.message : "";
|
|
5787
|
+
if (/not a git repository/i.test(message)) {
|
|
5788
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5789
|
+
}
|
|
5790
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5791
|
+
}
|
|
5792
|
+
const changes = [];
|
|
5793
|
+
for (const path2 of raw.split("\0")) {
|
|
5794
|
+
if (path2.length === 0) continue;
|
|
5795
|
+
if (path2.endsWith("/")) continue;
|
|
5796
|
+
changes.push({ path: path2, status: "added" });
|
|
5797
|
+
}
|
|
5798
|
+
return changes.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
5799
|
+
}
|
|
5800
|
+
async function readEmptyTreeSha(repoRoot) {
|
|
5801
|
+
let git;
|
|
5802
|
+
try {
|
|
5803
|
+
git = safeSimpleGit(repoRoot);
|
|
5804
|
+
} catch (error) {
|
|
5805
|
+
if (isGitNotFound(error)) {
|
|
5806
|
+
throw new Error("Git executable not found in PATH. Install git first.", { cause: error });
|
|
5807
|
+
}
|
|
5808
|
+
throw new Error("Not a git repository", { cause: error });
|
|
5809
|
+
}
|
|
5810
|
+
try {
|
|
5811
|
+
return (await git.raw(["hash-object", "-t", "tree", "/dev/null"])).trimEnd();
|
|
5812
|
+
} catch (error) {
|
|
5813
|
+
throw new Error("Failed to read git status", { cause: error });
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5664
5817
|
// src/handoff/handoff-renderer.ts
|
|
5665
5818
|
import { join as join14 } from "path";
|
|
5666
5819
|
|
|
@@ -5670,13 +5823,36 @@ function isTrailingStale(latestActivityAt, recordedAt) {
|
|
|
5670
5823
|
if (latestActivityAt === null) return false;
|
|
5671
5824
|
return Date.parse(latestActivityAt) - Date.parse(recordedAt) > DECISION_TRAILING_ACTIVITY_GAP_MS;
|
|
5672
5825
|
}
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
if (
|
|
5678
|
-
return
|
|
5679
|
-
}
|
|
5826
|
+
var WRAPPER_SESSION_COMMAND_COUNT = 1;
|
|
5827
|
+
function pickLatestSubstantiveEntry(entries, commandCounts, unmeasured) {
|
|
5828
|
+
const didWork = (e) => {
|
|
5829
|
+
if ((e.session.session.related_files?.length ?? 0) > 0) return true;
|
|
5830
|
+
if (unmeasured.has(e.sessionId)) return true;
|
|
5831
|
+
return (commandCounts.get(e.sessionId) ?? 0) > WRAPPER_SESSION_COMMAND_COUNT;
|
|
5832
|
+
};
|
|
5833
|
+
const working = entries.filter(didWork);
|
|
5834
|
+
const outermost = working.filter((e) => !isNestedInAnother(e, working));
|
|
5835
|
+
const pool = outermost.length > 0 ? outermost : working.length > 0 ? working : entries;
|
|
5836
|
+
return [...pool].sort(
|
|
5837
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
5838
|
+
)[0];
|
|
5839
|
+
}
|
|
5840
|
+
function isNestedInAnother(entry, working) {
|
|
5841
|
+
const end = entry.session.session.ended_at;
|
|
5842
|
+
if (end === void 0) return false;
|
|
5843
|
+
const start = Date.parse(entry.session.session.started_at);
|
|
5844
|
+
const finish = Date.parse(end);
|
|
5845
|
+
if (!Number.isFinite(start) || !Number.isFinite(finish)) return false;
|
|
5846
|
+
return working.some((other) => {
|
|
5847
|
+
if (other.sessionId === entry.sessionId) return false;
|
|
5848
|
+
const otherEnd = other.session.session.ended_at;
|
|
5849
|
+
if (otherEnd === void 0) return false;
|
|
5850
|
+
const oStart = Date.parse(other.session.session.started_at);
|
|
5851
|
+
const oFinish = Date.parse(otherEnd);
|
|
5852
|
+
if (!Number.isFinite(oStart) || !Number.isFinite(oFinish)) return false;
|
|
5853
|
+
if (oStart > start || oFinish < finish) return false;
|
|
5854
|
+
return oStart < start || oFinish > finish;
|
|
5855
|
+
});
|
|
5680
5856
|
}
|
|
5681
5857
|
|
|
5682
5858
|
// src/lib/transient-paths.ts
|
|
@@ -5720,6 +5896,8 @@ async function renderHandoff(input) {
|
|
|
5720
5896
|
const tasksCreated = [];
|
|
5721
5897
|
const tasksStatusChanged = [];
|
|
5722
5898
|
let latestActivityAt = null;
|
|
5899
|
+
const commandCounts = /* @__PURE__ */ new Map();
|
|
5900
|
+
const unmeasuredSessions = /* @__PURE__ */ new Set();
|
|
5723
5901
|
const noteActivity = (iso) => {
|
|
5724
5902
|
if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
|
|
5725
5903
|
latestActivityAt = iso;
|
|
@@ -5750,6 +5928,8 @@ async function renderHandoff(input) {
|
|
|
5750
5928
|
sessionId: entry.sessionId
|
|
5751
5929
|
});
|
|
5752
5930
|
}
|
|
5931
|
+
} else if (ev.type === "command_executed") {
|
|
5932
|
+
commandCounts.set(entry.sessionId, (commandCounts.get(entry.sessionId) ?? 0) + 1);
|
|
5753
5933
|
} else if (ev.type === "decision_voided") {
|
|
5754
5934
|
voidedDecisionIds.add(ev.decision_id);
|
|
5755
5935
|
} else if (ev.type === "task_created") {
|
|
@@ -5768,6 +5948,7 @@ async function renderHandoff(input) {
|
|
|
5768
5948
|
}
|
|
5769
5949
|
}
|
|
5770
5950
|
} catch {
|
|
5951
|
+
unmeasuredSessions.add(entry.sessionId);
|
|
5771
5952
|
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
5772
5953
|
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
5773
5954
|
}
|
|
@@ -5821,7 +6002,7 @@ async function renderHandoff(input) {
|
|
|
5821
6002
|
const liveEntries = entries.filter(
|
|
5822
6003
|
(e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
|
|
5823
6004
|
);
|
|
5824
|
-
const latestSession = pickLatestSubstantiveEntry(liveEntries);
|
|
6005
|
+
const latestSession = pickLatestSubstantiveEntry(liveEntries, commandCounts, unmeasuredSessions);
|
|
5825
6006
|
const latestFiles = (latestSession?.session.session.related_files ?? []).filter(
|
|
5826
6007
|
(file) => !isTransientToolPath(file)
|
|
5827
6008
|
);
|
|
@@ -6070,7 +6251,7 @@ function parseBuildStamp(raw) {
|
|
|
6070
6251
|
}
|
|
6071
6252
|
}
|
|
6072
6253
|
var BASOU_CORE_BUILD = parseBuildStamp(
|
|
6073
|
-
true ? '{"version":"0.
|
|
6254
|
+
true ? '{"version":"0.49.0","commit":"6c4704a","committedAt":"2026-09-23T11:46:39+09:00"}' : void 0
|
|
6074
6255
|
);
|
|
6075
6256
|
|
|
6076
6257
|
// src/lib/duration.ts
|
|
@@ -6280,6 +6461,8 @@ async function summarizeOrientation(input) {
|
|
|
6280
6461
|
let latestActivityAt = null;
|
|
6281
6462
|
let taskCreatedSeen = false;
|
|
6282
6463
|
let latestNote = null;
|
|
6464
|
+
const commandCounts = /* @__PURE__ */ new Map();
|
|
6465
|
+
const unmeasuredSessions = /* @__PURE__ */ new Set();
|
|
6283
6466
|
const noteActivity = (iso) => {
|
|
6284
6467
|
if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
|
|
6285
6468
|
latestActivityAt = iso;
|
|
@@ -6329,6 +6512,8 @@ async function summarizeOrientation(input) {
|
|
|
6329
6512
|
taskCreatedSeen = true;
|
|
6330
6513
|
} else if (ev.type === "decision_voided") {
|
|
6331
6514
|
voidedDecisionIds.add(ev.decision_id);
|
|
6515
|
+
} else if (ev.type === "command_executed") {
|
|
6516
|
+
commandCounts.set(entry.sessionId, (commandCounts.get(entry.sessionId) ?? 0) + 1);
|
|
6332
6517
|
}
|
|
6333
6518
|
if (counted && ev.type === "note_added" && ev.kind === "next_step") {
|
|
6334
6519
|
recordDirection(entry.sessionId, "note", ev.body);
|
|
@@ -6344,6 +6529,7 @@ async function summarizeOrientation(input) {
|
|
|
6344
6529
|
if (counted) noteActivity(ev.occurred_at);
|
|
6345
6530
|
}
|
|
6346
6531
|
} catch {
|
|
6532
|
+
unmeasuredSessions.add(entry.sessionId);
|
|
6347
6533
|
input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
|
|
6348
6534
|
}
|
|
6349
6535
|
}
|
|
@@ -6437,7 +6623,7 @@ async function summarizeOrientation(input) {
|
|
|
6437
6623
|
const liveEntries = entries.filter(
|
|
6438
6624
|
(e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
|
|
6439
6625
|
);
|
|
6440
|
-
const latestEntry = pickLatestSubstantiveEntry(liveEntries);
|
|
6626
|
+
const latestEntry = pickLatestSubstantiveEntry(liveEntries, commandCounts, unmeasuredSessions);
|
|
6441
6627
|
const latestSession = latestEntry !== void 0 ? {
|
|
6442
6628
|
sessionId: latestEntry.sessionId,
|
|
6443
6629
|
label: latestEntry.session.session.label ?? null,
|
|
@@ -9486,7 +9672,7 @@ var ChildProcessRunner = class {
|
|
|
9486
9672
|
if (killTimer !== null) clearTimeout(killTimer);
|
|
9487
9673
|
options.signal?.removeEventListener("abort", onAbort);
|
|
9488
9674
|
};
|
|
9489
|
-
return new Promise((
|
|
9675
|
+
return new Promise((resolve5, reject) => {
|
|
9490
9676
|
child.once("error", (error) => {
|
|
9491
9677
|
if (settled) return;
|
|
9492
9678
|
settled = true;
|
|
@@ -9498,7 +9684,7 @@ var ChildProcessRunner = class {
|
|
|
9498
9684
|
settled = true;
|
|
9499
9685
|
cleanup();
|
|
9500
9686
|
const ended_at = /* @__PURE__ */ new Date();
|
|
9501
|
-
|
|
9687
|
+
resolve5({
|
|
9502
9688
|
command: snapshotCommand,
|
|
9503
9689
|
args: snapshotArgs,
|
|
9504
9690
|
cwd: snapshotCwd,
|
|
@@ -9613,30 +9799,190 @@ function serializeJsonSchema(schema) {
|
|
|
9613
9799
|
`;
|
|
9614
9800
|
}
|
|
9615
9801
|
|
|
9616
|
-
// src/
|
|
9617
|
-
import {
|
|
9802
|
+
// src/session/observation.ts
|
|
9803
|
+
import { mkdir as mkdir4, readFile as readFile10 } from "fs/promises";
|
|
9618
9804
|
import { join as join20 } from "path";
|
|
9805
|
+
import { z as z12 } from "zod";
|
|
9806
|
+
var SESSION_OBSERVATION_SCHEMA_VERSION = "0.1.0";
|
|
9807
|
+
var ObservedFileSchema = z12.object({
|
|
9808
|
+
/** Absolute path, matching the paths the transcript importer records. */
|
|
9809
|
+
path: z12.string().min(1),
|
|
9810
|
+
change_type: z12.enum(["added", "modified", "deleted", "renamed"]),
|
|
9811
|
+
old_path: z12.string().min(1).optional()
|
|
9812
|
+
});
|
|
9813
|
+
var ObservedRepoSchema = z12.object({
|
|
9814
|
+
/** Absolute path of the git repository root this entry speaks for. */
|
|
9815
|
+
path: z12.string().min(1),
|
|
9816
|
+
/**
|
|
9817
|
+
* `HEAD` as it stood when the session started. `null` when the repository
|
|
9818
|
+
* had no commits yet (a fresh `git init`), in which case only working-tree
|
|
9819
|
+
* changes are observable.
|
|
9820
|
+
*/
|
|
9821
|
+
base_head: z12.string().nullable(),
|
|
9822
|
+
/**
|
|
9823
|
+
* Paths already dirty at session start. They are SUBTRACTED from the
|
|
9824
|
+
* working-tree observation: a file the operator left modified before the
|
|
9825
|
+
* session opened was not changed BY this session, and attributing it would
|
|
9826
|
+
* make the first session after any interrupted work claim the interruption.
|
|
9827
|
+
*/
|
|
9828
|
+
base_dirty: z12.array(z12.string()).default([]),
|
|
9829
|
+
/** Latest full recomputation for this repository (see `observeSession`). */
|
|
9830
|
+
files: z12.array(ObservedFileSchema).default([])
|
|
9831
|
+
});
|
|
9832
|
+
var SessionObservationSchema = z12.object({
|
|
9833
|
+
schema_version: z12.string().min(1),
|
|
9834
|
+
external_id: z12.string().min(1),
|
|
9835
|
+
started_at: z12.string().min(1),
|
|
9836
|
+
updated_at: z12.string().min(1),
|
|
9837
|
+
repos: z12.array(ObservedRepoSchema).default([])
|
|
9838
|
+
});
|
|
9839
|
+
var SAFE_EXTERNAL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
9840
|
+
function sessionObservationPath(observationsDir, externalId) {
|
|
9841
|
+
if (!SAFE_EXTERNAL_ID.test(externalId)) return null;
|
|
9842
|
+
if (externalId === "." || externalId === "..") return null;
|
|
9843
|
+
return join20(observationsDir, `${externalId}.json`);
|
|
9844
|
+
}
|
|
9845
|
+
async function readSessionObservation(observationsDir, externalId) {
|
|
9846
|
+
const file = sessionObservationPath(observationsDir, externalId);
|
|
9847
|
+
if (file === null) return null;
|
|
9848
|
+
let raw;
|
|
9849
|
+
try {
|
|
9850
|
+
raw = await readFile10(file, "utf8");
|
|
9851
|
+
} catch {
|
|
9852
|
+
return null;
|
|
9853
|
+
}
|
|
9854
|
+
let parsed;
|
|
9855
|
+
try {
|
|
9856
|
+
parsed = JSON.parse(raw);
|
|
9857
|
+
} catch {
|
|
9858
|
+
return null;
|
|
9859
|
+
}
|
|
9860
|
+
const result = SessionObservationSchema.safeParse(parsed);
|
|
9861
|
+
if (!result.success) return null;
|
|
9862
|
+
if (result.data.schema_version !== SESSION_OBSERVATION_SCHEMA_VERSION) return null;
|
|
9863
|
+
return result.data;
|
|
9864
|
+
}
|
|
9865
|
+
async function writeSessionObservation(observationsDir, observation) {
|
|
9866
|
+
const file = sessionObservationPath(observationsDir, observation.external_id);
|
|
9867
|
+
if (file === null) return;
|
|
9868
|
+
await mkdir4(observationsDir, { recursive: true });
|
|
9869
|
+
await atomicReplace(file, `${JSON.stringify(observation, null, 2)}
|
|
9870
|
+
`);
|
|
9871
|
+
}
|
|
9872
|
+
function observedFilesOf(observation) {
|
|
9873
|
+
const files = [];
|
|
9874
|
+
for (const repo of observation.repos) files.push(...repo.files);
|
|
9875
|
+
return files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
9876
|
+
}
|
|
9877
|
+
function observedFileFrom(repoRoot, change) {
|
|
9878
|
+
return {
|
|
9879
|
+
path: join20(repoRoot, change.path),
|
|
9880
|
+
change_type: change.status,
|
|
9881
|
+
...change.old_path !== void 0 ? { old_path: join20(repoRoot, change.old_path) } : {}
|
|
9882
|
+
};
|
|
9883
|
+
}
|
|
9884
|
+
|
|
9885
|
+
// src/session/observe.ts
|
|
9886
|
+
import { resolve as resolve4 } from "path";
|
|
9887
|
+
function observedRepoRoots(root, manifest) {
|
|
9888
|
+
const declared = manifest.repos ?? [];
|
|
9889
|
+
const roots = declared.length > 0 ? declared.map((repo) => resolve4(root, repo.path)) : [root];
|
|
9890
|
+
return [...new Set(roots)];
|
|
9891
|
+
}
|
|
9892
|
+
async function recordSessionBaseline(input) {
|
|
9893
|
+
const existing = await readSessionObservation(input.observationsDir, input.externalId);
|
|
9894
|
+
if (existing !== null) return existing;
|
|
9895
|
+
const repos = [];
|
|
9896
|
+
for (const repoRoot of input.repoRoots) {
|
|
9897
|
+
let baseHead;
|
|
9898
|
+
try {
|
|
9899
|
+
baseHead = await readHeadSha(repoRoot);
|
|
9900
|
+
} catch {
|
|
9901
|
+
continue;
|
|
9902
|
+
}
|
|
9903
|
+
let baseDirty = [];
|
|
9904
|
+
try {
|
|
9905
|
+
baseDirty = (await getWorkingTreeChanges(repoRoot)).map(
|
|
9906
|
+
(change) => observedFileFrom(repoRoot, change).path
|
|
9907
|
+
);
|
|
9908
|
+
} catch {
|
|
9909
|
+
}
|
|
9910
|
+
repos.push({ path: repoRoot, base_head: baseHead, base_dirty: baseDirty, files: [] });
|
|
9911
|
+
}
|
|
9912
|
+
if (repos.length === 0) return null;
|
|
9913
|
+
const observation = {
|
|
9914
|
+
schema_version: SESSION_OBSERVATION_SCHEMA_VERSION,
|
|
9915
|
+
external_id: input.externalId,
|
|
9916
|
+
started_at: input.nowIso,
|
|
9917
|
+
updated_at: input.nowIso,
|
|
9918
|
+
repos
|
|
9919
|
+
};
|
|
9920
|
+
await writeSessionObservation(input.observationsDir, observation);
|
|
9921
|
+
return observation;
|
|
9922
|
+
}
|
|
9923
|
+
async function observeSessionChanges(input) {
|
|
9924
|
+
const existing = await readSessionObservation(input.observationsDir, input.externalId);
|
|
9925
|
+
if (existing === null) return null;
|
|
9926
|
+
const repos = [];
|
|
9927
|
+
for (const repo of existing.repos) {
|
|
9928
|
+
let files;
|
|
9929
|
+
try {
|
|
9930
|
+
files = await changedSinceBaseline(repo);
|
|
9931
|
+
} catch {
|
|
9932
|
+
repos.push(repo);
|
|
9933
|
+
continue;
|
|
9934
|
+
}
|
|
9935
|
+
repos.push({ ...repo, files });
|
|
9936
|
+
}
|
|
9937
|
+
const updated = { ...existing, updated_at: input.nowIso, repos };
|
|
9938
|
+
await writeSessionObservation(input.observationsDir, updated);
|
|
9939
|
+
return updated;
|
|
9940
|
+
}
|
|
9941
|
+
function isBasouStorePath(relativePath) {
|
|
9942
|
+
return relativePath === ".basou" || relativePath.startsWith(".basou/");
|
|
9943
|
+
}
|
|
9944
|
+
async function changedSinceBaseline(repo) {
|
|
9945
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
9946
|
+
const base = repo.base_head ?? await readEmptyTreeSha(repo.path);
|
|
9947
|
+
for (const change of await getChangesSince(repo.path, base)) {
|
|
9948
|
+
if (isBasouStorePath(change.path)) continue;
|
|
9949
|
+
const file = observedFileFrom(repo.path, change);
|
|
9950
|
+
byPath.set(file.path, file);
|
|
9951
|
+
}
|
|
9952
|
+
for (const change of await getUntrackedFiles(repo.path)) {
|
|
9953
|
+
if (isBasouStorePath(change.path)) continue;
|
|
9954
|
+
const file = observedFileFrom(repo.path, change);
|
|
9955
|
+
if (!byPath.has(file.path)) byPath.set(file.path, file);
|
|
9956
|
+
}
|
|
9957
|
+
const preexisting = new Set(repo.base_dirty);
|
|
9958
|
+
return [...byPath.values()].filter((file) => !preexisting.has(file.path)).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
9959
|
+
}
|
|
9960
|
+
|
|
9961
|
+
// src/storage/basou-dir.ts
|
|
9962
|
+
import { lstat as lstat4, mkdir as mkdir5 } from "fs/promises";
|
|
9963
|
+
import { join as join21 } from "path";
|
|
9619
9964
|
function basouPaths(repositoryRoot) {
|
|
9620
|
-
const root =
|
|
9621
|
-
const approvalsBase =
|
|
9965
|
+
const root = join21(repositoryRoot, ".basou");
|
|
9966
|
+
const approvalsBase = join21(root, "approvals");
|
|
9622
9967
|
return {
|
|
9623
9968
|
root,
|
|
9624
|
-
sessions:
|
|
9625
|
-
tasks:
|
|
9969
|
+
sessions: join21(root, "sessions"),
|
|
9970
|
+
tasks: join21(root, "tasks"),
|
|
9626
9971
|
approvals: {
|
|
9627
|
-
pending:
|
|
9628
|
-
resolved:
|
|
9972
|
+
pending: join21(approvalsBase, "pending"),
|
|
9973
|
+
resolved: join21(approvalsBase, "resolved")
|
|
9629
9974
|
},
|
|
9630
|
-
locks:
|
|
9631
|
-
logs:
|
|
9632
|
-
raw:
|
|
9633
|
-
tmp:
|
|
9975
|
+
locks: join21(root, "locks"),
|
|
9976
|
+
logs: join21(root, "logs"),
|
|
9977
|
+
raw: join21(root, "raw"),
|
|
9978
|
+
tmp: join21(root, "tmp"),
|
|
9979
|
+
observations: join21(root, "tmp", "observations"),
|
|
9634
9980
|
files: {
|
|
9635
|
-
manifest:
|
|
9636
|
-
status:
|
|
9637
|
-
handoff:
|
|
9638
|
-
decisions:
|
|
9639
|
-
orientation:
|
|
9981
|
+
manifest: join21(root, "manifest.yaml"),
|
|
9982
|
+
status: join21(root, "status.json"),
|
|
9983
|
+
handoff: join21(root, "handoff.md"),
|
|
9984
|
+
decisions: join21(root, "decisions.md"),
|
|
9985
|
+
orientation: join21(root, "orientation.md")
|
|
9640
9986
|
}
|
|
9641
9987
|
};
|
|
9642
9988
|
}
|
|
@@ -9677,7 +10023,7 @@ async function ensureBasouDirectory(repositoryRoot) {
|
|
|
9677
10023
|
}
|
|
9678
10024
|
async function mkdirLabeled(target, label) {
|
|
9679
10025
|
try {
|
|
9680
|
-
await
|
|
10026
|
+
await mkdir5(target, { recursive: true });
|
|
9681
10027
|
} catch (error) {
|
|
9682
10028
|
if (hasErrorCode5(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
|
|
9683
10029
|
throw new Error(`${label} exists but is not a directory`, { cause: error });
|
|
@@ -9692,17 +10038,17 @@ function hasErrorCode5(error) {
|
|
|
9692
10038
|
}
|
|
9693
10039
|
|
|
9694
10040
|
// src/storage/gitignore.ts
|
|
9695
|
-
import { readFile as
|
|
9696
|
-
import { join as
|
|
10041
|
+
import { readFile as readFile11, writeFile as writeFile2 } from "fs/promises";
|
|
10042
|
+
import { join as join22 } from "path";
|
|
9697
10043
|
var MARKER = "# Basou - default ignore";
|
|
9698
10044
|
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";
|
|
9699
10045
|
var BASOU_GITIGNORE_BLOCK_LOCAL_ONLY = "# Basou - default ignore\n# Local-only: basou's trail is never committed (personal/local state,\n# regenerable by re-importing from the agents' own logs). Recommended for\n# monitored repos and any workspace kept out of version control.\n.basou/\n";
|
|
9700
10046
|
async function appendBasouGitignore(repositoryRoot, options = {}) {
|
|
9701
|
-
const gitignorePath =
|
|
10047
|
+
const gitignorePath = join22(repositoryRoot, ".gitignore");
|
|
9702
10048
|
let body;
|
|
9703
10049
|
let existed;
|
|
9704
10050
|
try {
|
|
9705
|
-
body = await
|
|
10051
|
+
body = await readFile11(gitignorePath, "utf8");
|
|
9706
10052
|
existed = true;
|
|
9707
10053
|
} catch (error) {
|
|
9708
10054
|
if (hasErrorCode6(error) && error.code === "ENOENT") {
|
|
@@ -9745,9 +10091,9 @@ function hasErrorCode6(error) {
|
|
|
9745
10091
|
}
|
|
9746
10092
|
|
|
9747
10093
|
// src/storage/session-import.ts
|
|
9748
|
-
import { mkdir as
|
|
10094
|
+
import { mkdir as mkdir6, readFile as readFile12, rm as rm2 } from "fs/promises";
|
|
9749
10095
|
import { homedir as homedir4 } from "os";
|
|
9750
|
-
import { join as
|
|
10096
|
+
import { join as join23 } from "path";
|
|
9751
10097
|
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
9752
10098
|
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
9753
10099
|
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
@@ -9772,9 +10118,9 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
9772
10118
|
pathSanitizeReport
|
|
9773
10119
|
};
|
|
9774
10120
|
}
|
|
9775
|
-
const sessionDir =
|
|
10121
|
+
const sessionDir = join23(paths.sessions, newSessionId);
|
|
9776
10122
|
try {
|
|
9777
|
-
await
|
|
10123
|
+
await mkdir6(sessionDir, { recursive: true });
|
|
9778
10124
|
} catch (error) {
|
|
9779
10125
|
throw new Error("Failed to create session directory", { cause: error });
|
|
9780
10126
|
}
|
|
@@ -9786,7 +10132,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
9786
10132
|
throw error;
|
|
9787
10133
|
}
|
|
9788
10134
|
try {
|
|
9789
|
-
const sessionYamlPath =
|
|
10135
|
+
const sessionYamlPath = join23(sessionDir, "session.yaml");
|
|
9790
10136
|
await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
|
|
9791
10137
|
} catch (error) {
|
|
9792
10138
|
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
@@ -9954,7 +10300,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
|
|
|
9954
10300
|
async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
|
|
9955
10301
|
const sessionId = priorSessionId;
|
|
9956
10302
|
const importSource = freshPayload.session.source.kind;
|
|
9957
|
-
const sessionDir =
|
|
10303
|
+
const sessionDir = join23(paths.sessions, priorSessionId);
|
|
9958
10304
|
const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
|
|
9959
10305
|
try {
|
|
9960
10306
|
const priorVerdict = await verifyEventsChain(paths, priorSessionId);
|
|
@@ -9999,10 +10345,10 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
9999
10345
|
session: preservedInner
|
|
10000
10346
|
};
|
|
10001
10347
|
if (options.dryRun !== true) {
|
|
10002
|
-
const eventsPath =
|
|
10348
|
+
const eventsPath = join23(sessionDir, "events.jsonl");
|
|
10003
10349
|
let priorEventsRaw = null;
|
|
10004
10350
|
try {
|
|
10005
|
-
priorEventsRaw = await
|
|
10351
|
+
priorEventsRaw = await readFile12(eventsPath);
|
|
10006
10352
|
} catch (error) {
|
|
10007
10353
|
if (!findErrorCode(error, "ENOENT")) {
|
|
10008
10354
|
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
@@ -10011,7 +10357,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
10011
10357
|
const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
|
|
10012
10358
|
try {
|
|
10013
10359
|
await overwriteYamlFile(
|
|
10014
|
-
|
|
10360
|
+
join23(sessionDir, "session.yaml"),
|
|
10015
10361
|
withIntegrity(updatedRecord, chainResult)
|
|
10016
10362
|
);
|
|
10017
10363
|
} catch (error) {
|
|
@@ -10035,7 +10381,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
10035
10381
|
}
|
|
10036
10382
|
}
|
|
10037
10383
|
async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
10038
|
-
const sessionDir =
|
|
10384
|
+
const sessionDir = join23(paths.sessions, sessionId);
|
|
10039
10385
|
let lock;
|
|
10040
10386
|
try {
|
|
10041
10387
|
lock = await acquireLock(paths, "session", sessionId);
|
|
@@ -10068,10 +10414,10 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
10068
10414
|
if (verdict.status !== "unchained") {
|
|
10069
10415
|
return { status: "skipped", reason: "tampered" };
|
|
10070
10416
|
}
|
|
10071
|
-
const eventsPath =
|
|
10417
|
+
const eventsPath = join23(sessionDir, "events.jsonl");
|
|
10072
10418
|
let priorRaw;
|
|
10073
10419
|
try {
|
|
10074
|
-
priorRaw = await
|
|
10420
|
+
priorRaw = await readFile12(eventsPath);
|
|
10075
10421
|
} catch (error) {
|
|
10076
10422
|
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
10077
10423
|
}
|
|
@@ -10116,7 +10462,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
10116
10462
|
}
|
|
10117
10463
|
try {
|
|
10118
10464
|
await overwriteYamlFile(
|
|
10119
|
-
|
|
10465
|
+
join23(sessionDir, "session.yaml"),
|
|
10120
10466
|
withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
|
|
10121
10467
|
);
|
|
10122
10468
|
} catch (error) {
|
|
@@ -10167,6 +10513,7 @@ export {
|
|
|
10167
10513
|
REVIEW_RECORD_NO_INPUT_HINT,
|
|
10168
10514
|
RiskLevelSchema,
|
|
10169
10515
|
SESSION_IMPORT_SCHEMA_VERSION,
|
|
10516
|
+
SESSION_OBSERVATION_SCHEMA_VERSION,
|
|
10170
10517
|
SESSION_SCHEMA_VERSION,
|
|
10171
10518
|
SESSION_START_HOOK_CONTEXT_LIMIT,
|
|
10172
10519
|
SESSION_START_HOOK_MATCHER,
|
|
@@ -10237,8 +10584,10 @@ export {
|
|
|
10237
10584
|
findUnbindableRepos,
|
|
10238
10585
|
formatDurationMs,
|
|
10239
10586
|
genesisHash,
|
|
10587
|
+
getChangesSince,
|
|
10240
10588
|
getDiff,
|
|
10241
10589
|
getSnapshot,
|
|
10590
|
+
getWorkingTreeChanges,
|
|
10242
10591
|
hasRetiredZeroDuration,
|
|
10243
10592
|
importSessionFromJson,
|
|
10244
10593
|
inspectChainTail,
|
|
@@ -10259,6 +10608,9 @@ export {
|
|
|
10259
10608
|
loadTaskEntries,
|
|
10260
10609
|
normalizeRepoKey,
|
|
10261
10610
|
normalizeRepoPath,
|
|
10611
|
+
observeSessionChanges,
|
|
10612
|
+
observedFilesOf,
|
|
10613
|
+
observedRepoRoots,
|
|
10262
10614
|
overwriteYamlFile,
|
|
10263
10615
|
parseBuildStamp,
|
|
10264
10616
|
parseDuration,
|
|
@@ -10277,9 +10629,11 @@ export {
|
|
|
10277
10629
|
protocolSectionsFrom,
|
|
10278
10630
|
protocolUpdateToken,
|
|
10279
10631
|
readAllEvents,
|
|
10632
|
+
readHeadSha,
|
|
10280
10633
|
readManifest,
|
|
10281
10634
|
readMarkdownFile,
|
|
10282
10635
|
readObservedDuration,
|
|
10636
|
+
readSessionObservation,
|
|
10283
10637
|
readSessionYaml,
|
|
10284
10638
|
readStatus,
|
|
10285
10639
|
readTaskFile,
|
|
@@ -10289,6 +10643,7 @@ export {
|
|
|
10289
10643
|
reconcileAllTasks,
|
|
10290
10644
|
reconcileSourceRoots,
|
|
10291
10645
|
reconcileTask,
|
|
10646
|
+
recordSessionBaseline,
|
|
10292
10647
|
refreshTaskLinkedSessions,
|
|
10293
10648
|
reimportPreservingId,
|
|
10294
10649
|
removeMarkerSection,
|
|
@@ -10323,6 +10678,7 @@ export {
|
|
|
10323
10678
|
seedMarkers,
|
|
10324
10679
|
serializeEventLine,
|
|
10325
10680
|
serializeJsonSchema,
|
|
10681
|
+
sessionObservationPath,
|
|
10326
10682
|
sessionWorkStatsFromEvents,
|
|
10327
10683
|
summarizeAdapterOutput,
|
|
10328
10684
|
summarizeOrientation,
|
|
@@ -10345,6 +10701,7 @@ export {
|
|
|
10345
10701
|
writeManifest,
|
|
10346
10702
|
writeMarkdownFile,
|
|
10347
10703
|
writeObservedDuration,
|
|
10704
|
+
writeSessionObservation,
|
|
10348
10705
|
writeStatus,
|
|
10349
10706
|
writeTaskFile,
|
|
10350
10707
|
writeYamlFile
|