@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.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((resolve4) => {
4
+ return new Promise((resolve5) => {
5
5
  const child = spawn("which", [command], { stdio: "ignore" });
6
- child.on("error", () => resolve4(false));
7
- child.on("exit", (code) => resolve4(code === 0));
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
- const message = error instanceof Error ? error.message : "";
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
- return { changed_files: parseDiffNameStatus(raw) };
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
- function pickLatestSubstantiveEntry(entries) {
5674
- return [...entries].sort((a, b) => {
5675
- const aSubstantive = (a.session.session.related_files?.length ?? 0) > 0 ? 1 : 0;
5676
- const bSubstantive = (b.session.session.related_files?.length ?? 0) > 0 ? 1 : 0;
5677
- if (aSubstantive !== bSubstantive) return bSubstantive - aSubstantive;
5678
- return Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at);
5679
- })[0];
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.48.0","commit":"2e8fee7","committedAt":"2026-09-21T11:35:12+09:00"}' : void 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((resolve4, reject) => {
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
- resolve4({
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/storage/basou-dir.ts
9617
- import { lstat as lstat4, mkdir as mkdir4 } from "fs/promises";
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 = join20(repositoryRoot, ".basou");
9621
- const approvalsBase = join20(root, "approvals");
9965
+ const root = join21(repositoryRoot, ".basou");
9966
+ const approvalsBase = join21(root, "approvals");
9622
9967
  return {
9623
9968
  root,
9624
- sessions: join20(root, "sessions"),
9625
- tasks: join20(root, "tasks"),
9969
+ sessions: join21(root, "sessions"),
9970
+ tasks: join21(root, "tasks"),
9626
9971
  approvals: {
9627
- pending: join20(approvalsBase, "pending"),
9628
- resolved: join20(approvalsBase, "resolved")
9972
+ pending: join21(approvalsBase, "pending"),
9973
+ resolved: join21(approvalsBase, "resolved")
9629
9974
  },
9630
- locks: join20(root, "locks"),
9631
- logs: join20(root, "logs"),
9632
- raw: join20(root, "raw"),
9633
- tmp: join20(root, "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: join20(root, "manifest.yaml"),
9636
- status: join20(root, "status.json"),
9637
- handoff: join20(root, "handoff.md"),
9638
- decisions: join20(root, "decisions.md"),
9639
- orientation: join20(root, "orientation.md")
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 mkdir4(target, { recursive: true });
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 readFile10, writeFile as writeFile2 } from "fs/promises";
9696
- import { join as join21 } from "path";
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 = join21(repositoryRoot, ".gitignore");
10047
+ const gitignorePath = join22(repositoryRoot, ".gitignore");
9702
10048
  let body;
9703
10049
  let existed;
9704
10050
  try {
9705
- body = await readFile10(gitignorePath, "utf8");
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 mkdir5, readFile as readFile11, rm as rm2 } from "fs/promises";
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 join22 } from "path";
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 = join22(paths.sessions, newSessionId);
10121
+ const sessionDir = join23(paths.sessions, newSessionId);
9776
10122
  try {
9777
- await mkdir5(sessionDir, { recursive: true });
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 = join22(sessionDir, "session.yaml");
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 = join22(paths.sessions, priorSessionId);
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 = join22(sessionDir, "events.jsonl");
10348
+ const eventsPath = join23(sessionDir, "events.jsonl");
10003
10349
  let priorEventsRaw = null;
10004
10350
  try {
10005
- priorEventsRaw = await readFile11(eventsPath);
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
- join22(sessionDir, "session.yaml"),
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 = join22(paths.sessions, sessionId);
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 = join22(sessionDir, "events.jsonl");
10417
+ const eventsPath = join23(sessionDir, "events.jsonl");
10072
10418
  let priorRaw;
10073
10419
  try {
10074
- priorRaw = await readFile11(eventsPath);
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
- join22(sessionDir, "session.yaml"),
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