@basou/core 0.10.0 → 0.12.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 +431 -110
- package/dist/index.js +1131 -412
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1720,6 +1720,8 @@ function splitLinesBytes2(buf) {
|
|
|
1720
1720
|
}
|
|
1721
1721
|
|
|
1722
1722
|
// src/git/snapshot.ts
|
|
1723
|
+
import { readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
1724
|
+
import { join as join9 } from "path";
|
|
1723
1725
|
import { simpleGit } from "simple-git";
|
|
1724
1726
|
|
|
1725
1727
|
// src/storage/status.ts
|
|
@@ -1757,19 +1759,19 @@ var DIRECTORY_CHECKS = {
|
|
|
1757
1759
|
tmp: (p) => p.tmp
|
|
1758
1760
|
};
|
|
1759
1761
|
async function assertBasouRootSafe(rootPath) {
|
|
1760
|
-
let
|
|
1762
|
+
let stat4;
|
|
1761
1763
|
try {
|
|
1762
|
-
|
|
1764
|
+
stat4 = await fsp.lstat(rootPath);
|
|
1763
1765
|
} catch (error) {
|
|
1764
1766
|
if (hasErrorCode2(error) && error.code === "ENOENT") {
|
|
1765
1767
|
throw new Error("Basou workspace not found", { cause: error });
|
|
1766
1768
|
}
|
|
1767
1769
|
throw new Error("Failed to inspect .basou root", { cause: error });
|
|
1768
1770
|
}
|
|
1769
|
-
if (
|
|
1771
|
+
if (stat4.isSymbolicLink()) {
|
|
1770
1772
|
throw new Error(".basou root is a symlink; refusing to operate");
|
|
1771
1773
|
}
|
|
1772
|
-
if (!
|
|
1774
|
+
if (!stat4.isDirectory()) {
|
|
1773
1775
|
throw new Error(".basou root exists but is not a directory");
|
|
1774
1776
|
}
|
|
1775
1777
|
}
|
|
@@ -1849,6 +1851,14 @@ function isGitNotFound(error) {
|
|
|
1849
1851
|
}
|
|
1850
1852
|
return false;
|
|
1851
1853
|
}
|
|
1854
|
+
function isNotAGitRepository(error) {
|
|
1855
|
+
let cur = error;
|
|
1856
|
+
for (let i = 0; i < 4 && cur instanceof Error; i++) {
|
|
1857
|
+
if (/not a git repository/i.test(cur.message)) return true;
|
|
1858
|
+
cur = cur.cause;
|
|
1859
|
+
}
|
|
1860
|
+
return false;
|
|
1861
|
+
}
|
|
1852
1862
|
async function resolveRepositoryRoot(cwd) {
|
|
1853
1863
|
const git = safeSimpleGit(cwd);
|
|
1854
1864
|
try {
|
|
@@ -1864,8 +1874,53 @@ async function resolveRepositoryRoot(cwd) {
|
|
|
1864
1874
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
1865
1875
|
throw error;
|
|
1866
1876
|
}
|
|
1867
|
-
|
|
1877
|
+
if (isNotAGitRepository(error)) {
|
|
1878
|
+
throw new Error("Not a git repository", { cause: error });
|
|
1879
|
+
}
|
|
1880
|
+
throw new Error("Git command failed", { cause: error });
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
async function resolveBasouRepositoryRoot(cwd, opts) {
|
|
1884
|
+
try {
|
|
1885
|
+
return await resolveRepositoryRoot(cwd);
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
if (!(error instanceof Error) || error.message !== "Not a git repository") throw error;
|
|
1888
|
+
const linked = await findLinkedBasouRepos(cwd);
|
|
1889
|
+
const only = linked[0];
|
|
1890
|
+
if (only !== void 0 && linked.length === 1) {
|
|
1891
|
+
opts?.onRedirect?.({ via: only.name, root: only.root });
|
|
1892
|
+
return only.root;
|
|
1893
|
+
}
|
|
1894
|
+
if (linked.length > 1) {
|
|
1895
|
+
const names = linked.map((l) => l.name).join(", ");
|
|
1896
|
+
throw new Error(
|
|
1897
|
+
`Ambiguous workspace view: ${linked.length} linked repos have a .basou store (${names}). cd into the one you want and re-run.`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
throw error;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async function findLinkedBasouRepos(dir) {
|
|
1904
|
+
const entries = await readdir3(dir, { withFileTypes: true }).catch(() => null);
|
|
1905
|
+
if (entries === null) return [];
|
|
1906
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
1907
|
+
for (const entry of entries) {
|
|
1908
|
+
if (!entry.isSymbolicLink()) continue;
|
|
1909
|
+
let root;
|
|
1910
|
+
try {
|
|
1911
|
+
root = await resolveRepositoryRoot(join9(dir, entry.name));
|
|
1912
|
+
} catch {
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
try {
|
|
1916
|
+
if (!(await stat2(join9(root, ".basou"))).isDirectory()) continue;
|
|
1917
|
+
} catch {
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
const existing = byRoot.get(root);
|
|
1921
|
+
if (existing === void 0 || entry.name < existing) byRoot.set(root, entry.name);
|
|
1868
1922
|
}
|
|
1923
|
+
return [...byRoot.entries()].map(([root, name]) => ({ name, root })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1869
1924
|
}
|
|
1870
1925
|
async function tryRemoteUrl(repositoryRoot) {
|
|
1871
1926
|
const git = safeSimpleGit(repositoryRoot);
|
|
@@ -2013,12 +2068,12 @@ function parseDiffNameStatus(raw) {
|
|
|
2013
2068
|
}
|
|
2014
2069
|
|
|
2015
2070
|
// src/handoff/handoff-renderer.ts
|
|
2016
|
-
import { join as
|
|
2071
|
+
import { join as join13 } from "path";
|
|
2017
2072
|
|
|
2018
2073
|
// src/storage/tasks.ts
|
|
2019
2074
|
import { createHash as createHash2 } from "crypto";
|
|
2020
|
-
import { mkdir as mkdir3, readdir as
|
|
2021
|
-
import { join as
|
|
2075
|
+
import { mkdir as mkdir3, readdir as readdir4, readFile as readFile7, rename as rename2, stat as stat3, unlink as unlink3 } from "fs/promises";
|
|
2076
|
+
import { join as join12 } from "path";
|
|
2022
2077
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2023
2078
|
import { z as z8 } from "zod";
|
|
2024
2079
|
|
|
@@ -2062,7 +2117,7 @@ var TaskSchema = z6.object({
|
|
|
2062
2117
|
// src/storage/ad-hoc-session.ts
|
|
2063
2118
|
import { mkdir as mkdir2, rm } from "fs/promises";
|
|
2064
2119
|
import { homedir } from "os";
|
|
2065
|
-
import { join as
|
|
2120
|
+
import { join as join10 } from "path";
|
|
2066
2121
|
|
|
2067
2122
|
// src/lib/path-sanitizer.ts
|
|
2068
2123
|
import { posix as path } from "path";
|
|
@@ -2142,8 +2197,8 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
2142
2197
|
taskId: input.taskId ?? null
|
|
2143
2198
|
})
|
|
2144
2199
|
);
|
|
2145
|
-
const sessionDir =
|
|
2146
|
-
const sessionYamlPath =
|
|
2200
|
+
const sessionDir = join10(input.paths.sessions, sessionId);
|
|
2201
|
+
const sessionYamlPath = join10(sessionDir, "session.yaml");
|
|
2147
2202
|
const lock = await acquireLock(input.paths, "session", sessionId);
|
|
2148
2203
|
let bulkResult = null;
|
|
2149
2204
|
try {
|
|
@@ -2293,7 +2348,7 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
|
|
|
2293
2348
|
|
|
2294
2349
|
// src/storage/task-index.ts
|
|
2295
2350
|
import { readFile as readFile6 } from "fs/promises";
|
|
2296
|
-
import { join as
|
|
2351
|
+
import { join as join11 } from "path";
|
|
2297
2352
|
|
|
2298
2353
|
// src/schemas/task-index.schema.ts
|
|
2299
2354
|
import { z as z7 } from "zod";
|
|
@@ -2312,7 +2367,7 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
|
|
|
2312
2367
|
|
|
2313
2368
|
// src/storage/task-index.ts
|
|
2314
2369
|
function taskIndexPath(paths) {
|
|
2315
|
-
return
|
|
2370
|
+
return join11(paths.tasks, "index.json");
|
|
2316
2371
|
}
|
|
2317
2372
|
async function readTaskIndex(paths) {
|
|
2318
2373
|
const filePath = taskIndexPath(paths);
|
|
@@ -2426,7 +2481,7 @@ function splitFrontMatter(raw) {
|
|
|
2426
2481
|
return { yamlText, body };
|
|
2427
2482
|
}
|
|
2428
2483
|
async function readTaskFile(paths, taskId) {
|
|
2429
|
-
const filePath =
|
|
2484
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
2430
2485
|
let raw;
|
|
2431
2486
|
try {
|
|
2432
2487
|
raw = await readFile7(filePath, "utf8");
|
|
@@ -2459,7 +2514,7 @@ async function readTaskFile(paths, taskId) {
|
|
|
2459
2514
|
}
|
|
2460
2515
|
async function writeTaskFile(paths, taskId, doc, options) {
|
|
2461
2516
|
const validated = TaskSchema.parse(doc.task);
|
|
2462
|
-
const filePath =
|
|
2517
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
2463
2518
|
const yamlText = stringifyYaml(validated);
|
|
2464
2519
|
const trimmedBody = doc.body.length === 0 ? "" : `
|
|
2465
2520
|
${doc.body.endsWith("\n") ? doc.body : `${doc.body}
|
|
@@ -2511,7 +2566,7 @@ async function enumerateTaskIds(paths) {
|
|
|
2511
2566
|
async function enumerateTaskIdsFromDisk(paths) {
|
|
2512
2567
|
let entries;
|
|
2513
2568
|
try {
|
|
2514
|
-
entries = (await
|
|
2569
|
+
entries = (await readdir4(paths.tasks, { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
|
|
2515
2570
|
} catch (error) {
|
|
2516
2571
|
if (findErrorCode(error, "ENOENT")) return [];
|
|
2517
2572
|
throw new Error("Failed to enumerate tasks", { cause: error });
|
|
@@ -2544,12 +2599,12 @@ async function safeUpdateTaskIndex(paths, op) {
|
|
|
2544
2599
|
}
|
|
2545
2600
|
var ARCHIVE_DIR_NAME = "archive";
|
|
2546
2601
|
function archiveTasksDir(paths) {
|
|
2547
|
-
return
|
|
2602
|
+
return join12(paths.tasks, ARCHIVE_DIR_NAME);
|
|
2548
2603
|
}
|
|
2549
2604
|
async function enumerateArchivedTaskIds(paths) {
|
|
2550
2605
|
let entries;
|
|
2551
2606
|
try {
|
|
2552
|
-
entries = (await
|
|
2607
|
+
entries = (await readdir4(archiveTasksDir(paths), { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
|
|
2553
2608
|
} catch (error) {
|
|
2554
2609
|
if (findErrorCode(error, "ENOENT")) return [];
|
|
2555
2610
|
throw new Error("Failed to enumerate archived tasks", { cause: error });
|
|
@@ -2574,7 +2629,7 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
|
|
|
2574
2629
|
throw error;
|
|
2575
2630
|
}
|
|
2576
2631
|
}
|
|
2577
|
-
const archiveFilePath =
|
|
2632
|
+
const archiveFilePath = join12(archiveTasksDir(paths), `${taskId}.md`);
|
|
2578
2633
|
let raw;
|
|
2579
2634
|
try {
|
|
2580
2635
|
raw = await readFile7(archiveFilePath, "utf8");
|
|
@@ -2868,7 +2923,7 @@ async function createTaskAttachLocked(input) {
|
|
|
2868
2923
|
...sessionDoc,
|
|
2869
2924
|
session: { ...sessionDoc.session, task_id: input.taskId }
|
|
2870
2925
|
};
|
|
2871
|
-
await overwriteYamlFile(
|
|
2926
|
+
await overwriteYamlFile(join12(input.paths.sessions, input.sessionId, "session.yaml"), updated);
|
|
2872
2927
|
} catch (error) {
|
|
2873
2928
|
throw new TaskWriteAfterEventError({
|
|
2874
2929
|
taskId: input.taskId,
|
|
@@ -3127,17 +3182,17 @@ function buildUpdatedDoc(input) {
|
|
|
3127
3182
|
return { task: next, body: input.currentDoc.body };
|
|
3128
3183
|
}
|
|
3129
3184
|
async function computeTaskMdSnapshot(paths, taskId) {
|
|
3130
|
-
const filePath =
|
|
3131
|
-
const [stats, raw] = await Promise.all([
|
|
3185
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
3186
|
+
const [stats, raw] = await Promise.all([stat3(filePath), readFile7(filePath)]);
|
|
3132
3187
|
const hash = createHash2("sha256").update(raw).digest("hex");
|
|
3133
3188
|
return { mtimeMs: stats.mtimeMs, hash };
|
|
3134
3189
|
}
|
|
3135
3190
|
async function readTaskFileWithSnapshot(paths, taskId) {
|
|
3136
|
-
const filePath =
|
|
3191
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
3137
3192
|
let rawBuffer;
|
|
3138
3193
|
let stats;
|
|
3139
3194
|
try {
|
|
3140
|
-
[rawBuffer, stats] = await Promise.all([readFile7(filePath),
|
|
3195
|
+
[rawBuffer, stats] = await Promise.all([readFile7(filePath), stat3(filePath)]);
|
|
3141
3196
|
} catch (error) {
|
|
3142
3197
|
if (findErrorCode(error, "ENOENT")) {
|
|
3143
3198
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -3625,7 +3680,7 @@ async function deleteTaskLocked(input) {
|
|
|
3625
3680
|
});
|
|
3626
3681
|
const eventId = adHoc.targetEventIds[0];
|
|
3627
3682
|
try {
|
|
3628
|
-
await unlink3(
|
|
3683
|
+
await unlink3(join12(input.paths.tasks, `${input.taskId}.md`));
|
|
3629
3684
|
} catch (error) {
|
|
3630
3685
|
throw new TaskWriteAfterEventError({
|
|
3631
3686
|
taskId: input.taskId,
|
|
@@ -3697,8 +3752,8 @@ async function archiveTaskLocked(input) {
|
|
|
3697
3752
|
);
|
|
3698
3753
|
await mkdir3(archiveTasksDir(input.paths), { recursive: true });
|
|
3699
3754
|
await rename2(
|
|
3700
|
-
|
|
3701
|
-
|
|
3755
|
+
join12(input.paths.tasks, `${input.taskId}.md`),
|
|
3756
|
+
join12(archiveTasksDir(input.paths), `${input.taskId}.md`)
|
|
3702
3757
|
);
|
|
3703
3758
|
} catch (error) {
|
|
3704
3759
|
throw new TaskWriteAfterEventError({
|
|
@@ -3734,7 +3789,7 @@ async function renderHandoff(input) {
|
|
|
3734
3789
|
const tasksCreated = [];
|
|
3735
3790
|
const tasksStatusChanged = [];
|
|
3736
3791
|
for (const entry of entries) {
|
|
3737
|
-
const sessionDir =
|
|
3792
|
+
const sessionDir = join13(input.paths.sessions, entry.sessionId);
|
|
3738
3793
|
try {
|
|
3739
3794
|
for await (const ev of replayEvents(sessionDir, {
|
|
3740
3795
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -3850,11 +3905,11 @@ function formatHandoffBody(args) {
|
|
|
3850
3905
|
if (args.latestSession !== void 0) {
|
|
3851
3906
|
const status = args.latestSession.session.session.status;
|
|
3852
3907
|
const label = args.latestSession.session.session.label;
|
|
3853
|
-
const
|
|
3908
|
+
const shortId2 = shortIdWithPrefix(args.latestSession.sessionId);
|
|
3854
3909
|
if (label !== void 0 && label !== "") {
|
|
3855
|
-
lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${
|
|
3910
|
+
lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId2}]`);
|
|
3856
3911
|
} else {
|
|
3857
|
-
lines.push(`- \u6700\u7D42 session: ${
|
|
3912
|
+
lines.push(`- \u6700\u7D42 session: ${shortId2} (${status})`);
|
|
3858
3913
|
}
|
|
3859
3914
|
} else {
|
|
3860
3915
|
lines.push("- \u6700\u7D42 session: (no live sessions)");
|
|
@@ -4025,6 +4080,17 @@ function parseDuration(input) {
|
|
|
4025
4080
|
return ms;
|
|
4026
4081
|
}
|
|
4027
4082
|
|
|
4083
|
+
// src/lib/format-duration.ts
|
|
4084
|
+
function formatDurationMs(ms) {
|
|
4085
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
4086
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
4087
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
4088
|
+
const seconds = totalSeconds % 60;
|
|
4089
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
4090
|
+
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
4091
|
+
return `${seconds}s`;
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4028
4094
|
// src/lib/id-resolver.ts
|
|
4029
4095
|
async function resolveSessionId(paths, input) {
|
|
4030
4096
|
return resolveIdInternal(paths, input, "session");
|
|
@@ -4080,132 +4146,11 @@ async function resolveIdInternal(paths, input, kind, options = {}) {
|
|
|
4080
4146
|
return matches[0];
|
|
4081
4147
|
}
|
|
4082
4148
|
|
|
4083
|
-
// src/
|
|
4084
|
-
import {
|
|
4085
|
-
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
4086
|
-
var ChildProcessRunner = class {
|
|
4087
|
-
async run(command, args, options) {
|
|
4088
|
-
validateOptions(options);
|
|
4089
|
-
if (options.signal?.aborted) {
|
|
4090
|
-
throw new Error("Process aborted before spawn", {
|
|
4091
|
-
cause: options.signal.reason
|
|
4092
|
-
});
|
|
4093
|
-
}
|
|
4094
|
-
const snapshotCommand = command;
|
|
4095
|
-
const snapshotArgs = [...args];
|
|
4096
|
-
const snapshotCwd = options.cwd;
|
|
4097
|
-
const captureMode = options.capture ?? "buffer";
|
|
4098
|
-
const started_at = /* @__PURE__ */ new Date();
|
|
4099
|
-
let child;
|
|
4100
|
-
try {
|
|
4101
|
-
child = spawn2(snapshotCommand, [...snapshotArgs], {
|
|
4102
|
-
cwd: snapshotCwd,
|
|
4103
|
-
env: options.env ?? process.env,
|
|
4104
|
-
stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
|
|
4105
|
-
shell: false,
|
|
4106
|
-
detached: false
|
|
4107
|
-
});
|
|
4108
|
-
} catch (error) {
|
|
4109
|
-
throw classifySpawnError(error);
|
|
4110
|
-
}
|
|
4111
|
-
if (options.onSpawn) {
|
|
4112
|
-
try {
|
|
4113
|
-
options.onSpawn(child);
|
|
4114
|
-
} catch {
|
|
4115
|
-
}
|
|
4116
|
-
}
|
|
4117
|
-
let timeoutTimer = null;
|
|
4118
|
-
let killTimer = null;
|
|
4119
|
-
let killed = false;
|
|
4120
|
-
let settled = false;
|
|
4121
|
-
const triggerKill = () => {
|
|
4122
|
-
if (killed || child.exitCode !== null) return;
|
|
4123
|
-
killed = true;
|
|
4124
|
-
child.kill("SIGTERM");
|
|
4125
|
-
killTimer = setTimeout(() => {
|
|
4126
|
-
if (child.exitCode === null) {
|
|
4127
|
-
child.kill("SIGKILL");
|
|
4128
|
-
}
|
|
4129
|
-
}, DEFAULT_KILL_GRACE_MS);
|
|
4130
|
-
};
|
|
4131
|
-
const onAbort = () => {
|
|
4132
|
-
triggerKill();
|
|
4133
|
-
};
|
|
4134
|
-
options.signal?.addEventListener("abort", onAbort);
|
|
4135
|
-
if (options.signal?.aborted) {
|
|
4136
|
-
triggerKill();
|
|
4137
|
-
}
|
|
4138
|
-
let stdout = "";
|
|
4139
|
-
let stderr = "";
|
|
4140
|
-
if (captureMode === "buffer") {
|
|
4141
|
-
child.stdout?.setEncoding("utf8");
|
|
4142
|
-
child.stderr?.setEncoding("utf8");
|
|
4143
|
-
child.stdout?.on("data", (chunk) => {
|
|
4144
|
-
stdout += chunk;
|
|
4145
|
-
});
|
|
4146
|
-
child.stderr?.on("data", (chunk) => {
|
|
4147
|
-
stderr += chunk;
|
|
4148
|
-
});
|
|
4149
|
-
if (options.stdin !== void 0) {
|
|
4150
|
-
child.stdin?.end(options.stdin);
|
|
4151
|
-
} else {
|
|
4152
|
-
child.stdin?.end();
|
|
4153
|
-
}
|
|
4154
|
-
}
|
|
4155
|
-
if (options.timeout_ms !== void 0) {
|
|
4156
|
-
timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
|
|
4157
|
-
}
|
|
4158
|
-
const cleanup = () => {
|
|
4159
|
-
if (timeoutTimer !== null) clearTimeout(timeoutTimer);
|
|
4160
|
-
if (killTimer !== null) clearTimeout(killTimer);
|
|
4161
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
4162
|
-
};
|
|
4163
|
-
return new Promise((resolve2, reject) => {
|
|
4164
|
-
child.once("error", (error) => {
|
|
4165
|
-
if (settled) return;
|
|
4166
|
-
settled = true;
|
|
4167
|
-
cleanup();
|
|
4168
|
-
reject(classifySpawnError(error));
|
|
4169
|
-
});
|
|
4170
|
-
child.once("close", (code, signal) => {
|
|
4171
|
-
if (settled) return;
|
|
4172
|
-
settled = true;
|
|
4173
|
-
cleanup();
|
|
4174
|
-
const ended_at = /* @__PURE__ */ new Date();
|
|
4175
|
-
resolve2({
|
|
4176
|
-
command: snapshotCommand,
|
|
4177
|
-
args: snapshotArgs,
|
|
4178
|
-
cwd: snapshotCwd,
|
|
4179
|
-
exit_code: code,
|
|
4180
|
-
signal,
|
|
4181
|
-
stdout,
|
|
4182
|
-
stderr,
|
|
4183
|
-
started_at: started_at.toISOString(),
|
|
4184
|
-
ended_at: ended_at.toISOString(),
|
|
4185
|
-
duration_ms: ended_at.getTime() - started_at.getTime(),
|
|
4186
|
-
pid: child.pid ?? null
|
|
4187
|
-
});
|
|
4188
|
-
});
|
|
4189
|
-
});
|
|
4190
|
-
}
|
|
4191
|
-
};
|
|
4192
|
-
function validateOptions(options) {
|
|
4193
|
-
if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
|
|
4194
|
-
throw new Error("Invalid timeout_ms");
|
|
4195
|
-
}
|
|
4196
|
-
if (options.capture === "none" && options.stdin !== void 0) {
|
|
4197
|
-
throw new Error('Combination of capture: "none" and stdin is not supported');
|
|
4198
|
-
}
|
|
4199
|
-
}
|
|
4200
|
-
function classifySpawnError(error) {
|
|
4201
|
-
if (findErrorCode(error, "ENOENT")) {
|
|
4202
|
-
return new Error("Command not found", { cause: error });
|
|
4203
|
-
}
|
|
4204
|
-
return new Error("Failed to spawn child process", { cause: error });
|
|
4205
|
-
}
|
|
4149
|
+
// src/orientation/orientation-renderer.ts
|
|
4150
|
+
import { join as join14 } from "path";
|
|
4206
4151
|
|
|
4207
|
-
// src/
|
|
4208
|
-
import {
|
|
4152
|
+
// src/storage/manifest.ts
|
|
4153
|
+
import { lstat as lstat3 } from "fs/promises";
|
|
4209
4154
|
|
|
4210
4155
|
// src/schemas/manifest.schema.ts
|
|
4211
4156
|
import { z as z9 } from "zod";
|
|
@@ -4256,126 +4201,400 @@ var ManifestSchema = z9.object({
|
|
|
4256
4201
|
import: ImportConfigSchema.optional()
|
|
4257
4202
|
});
|
|
4258
4203
|
|
|
4259
|
-
// src/
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
label: z10.string().optional(),
|
|
4264
|
-
task_id: TaskIdSchema.nullable().optional(),
|
|
4265
|
-
workspace_id: WorkspaceIdSchema,
|
|
4266
|
-
source: z10.object({
|
|
4267
|
-
kind: SessionSourceKindSchema,
|
|
4268
|
-
version: z10.literal("0.1.0"),
|
|
4269
|
-
// Source-tool-native id (e.g. Claude Code session UUID), retained so
|
|
4270
|
-
// re-imports of the same source can be deduplicated.
|
|
4271
|
-
external_id: z10.string().optional(),
|
|
4272
|
-
// Byte size of the source native log at import time. Declared here too
|
|
4273
|
-
// (not only in session.schema.ts) because this inner `source` object is
|
|
4274
|
-
// a plain z.object: zod strips keys it does not declare, so a field
|
|
4275
|
-
// absent here would be dropped from the parsed payload before persist
|
|
4276
|
-
// and the size could never be stored.
|
|
4277
|
-
source_size_bytes: z10.number().int().nonnegative().optional()
|
|
4278
|
-
}),
|
|
4279
|
-
started_at: IsoTimestampSchema,
|
|
4280
|
-
ended_at: IsoTimestampSchema.optional(),
|
|
4281
|
-
status: SessionStatusSchema,
|
|
4282
|
-
working_directory: z10.string().min(1),
|
|
4283
|
-
invocation: z10.object({
|
|
4284
|
-
command: z10.string().min(1),
|
|
4285
|
-
args: z10.array(z10.string()),
|
|
4286
|
-
exit_code: z10.number().int().nullable()
|
|
4287
|
-
}),
|
|
4288
|
-
related_files: z10.array(z10.string()).default([]),
|
|
4289
|
-
events_log: z10.string().optional(),
|
|
4290
|
-
summary: z10.string().nullable().optional(),
|
|
4291
|
-
metrics: SessionMetricsSchema.optional(),
|
|
4292
|
-
// Accepted so a payload assembled from an on-disk chained session.yaml
|
|
4293
|
-
// round-trips, and DISCARDED by the importer (buildSessionRecord never
|
|
4294
|
-
// copies it): the integrity anchor is computed at write time, never
|
|
4295
|
-
// imported. Mirrors the accept-and-discard of `prev_hash` on events.
|
|
4296
|
-
integrity: SessionIntegritySchema.optional()
|
|
4297
|
-
}).strict();
|
|
4298
|
-
var SessionImportPayloadSchema = z10.object({
|
|
4299
|
-
schema_version: z10.string(),
|
|
4300
|
-
session: SessionInnerImportSchema,
|
|
4301
|
-
events: z10.array(EventSchema)
|
|
4302
|
-
}).strict();
|
|
4303
|
-
|
|
4304
|
-
// src/schemas/json-schema.ts
|
|
4305
|
-
var JSON_SCHEMA_VERSION = "0.1.0";
|
|
4306
|
-
var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
|
|
4307
|
-
var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
4308
|
-
var DOCUMENTS = [
|
|
4309
|
-
{
|
|
4310
|
-
name: "manifest",
|
|
4311
|
-
schema: ManifestSchema,
|
|
4312
|
-
title: "Basou Manifest",
|
|
4313
|
-
description: "The `.basou/manifest.yaml` workspace manifest."
|
|
4314
|
-
},
|
|
4315
|
-
{
|
|
4316
|
-
name: "session",
|
|
4317
|
-
schema: SessionSchema,
|
|
4318
|
-
title: "Basou Session",
|
|
4319
|
-
description: "A `.basou/sessions/<id>/session.yaml` session record."
|
|
4320
|
-
},
|
|
4321
|
-
{
|
|
4322
|
-
name: "event",
|
|
4323
|
-
schema: EventSchema,
|
|
4324
|
-
title: "Basou Event",
|
|
4325
|
-
description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
|
|
4326
|
-
},
|
|
4327
|
-
{
|
|
4328
|
-
name: "task",
|
|
4329
|
-
schema: TaskSchema,
|
|
4330
|
-
title: "Basou Task",
|
|
4331
|
-
description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
|
|
4332
|
-
},
|
|
4333
|
-
{
|
|
4334
|
-
name: "approval",
|
|
4335
|
-
schema: ApprovalSchema,
|
|
4336
|
-
title: "Basou Approval",
|
|
4337
|
-
description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
|
|
4338
|
-
},
|
|
4339
|
-
{
|
|
4340
|
-
name: "status",
|
|
4341
|
-
schema: StatusSchema,
|
|
4342
|
-
title: "Basou Status",
|
|
4343
|
-
description: "The `.basou/status.json` workspace status snapshot."
|
|
4344
|
-
},
|
|
4345
|
-
{
|
|
4346
|
-
name: "task-index",
|
|
4347
|
-
schema: TaskIndexSchema,
|
|
4348
|
-
title: "Basou Task Index",
|
|
4349
|
-
description: "The `.basou/tasks/index.json` task lookup index."
|
|
4350
|
-
},
|
|
4351
|
-
{
|
|
4352
|
-
name: "session-import",
|
|
4353
|
-
schema: SessionImportPayloadSchema,
|
|
4354
|
-
title: "Basou Session Import Payload",
|
|
4355
|
-
description: "The portable session payload consumed by `basou session import`."
|
|
4204
|
+
// src/storage/manifest.ts
|
|
4205
|
+
function createManifest(input) {
|
|
4206
|
+
if (input.workspaceName.length === 0) {
|
|
4207
|
+
throw new Error("Workspace name is empty. Pass --name explicitly.");
|
|
4356
4208
|
}
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4209
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4210
|
+
const workspaceId = input.workspaceId ?? prefixedUlid("ws");
|
|
4211
|
+
const project = {
|
|
4212
|
+
...input.projectName !== void 0 ? { name: input.projectName } : {},
|
|
4213
|
+
...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
|
|
4214
|
+
...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
|
|
4215
|
+
};
|
|
4216
|
+
const manifest = {
|
|
4217
|
+
schema_version: "0.1.0",
|
|
4218
|
+
basou_version: "0.1.0",
|
|
4219
|
+
workspace: {
|
|
4220
|
+
id: workspaceId,
|
|
4221
|
+
name: input.workspaceName,
|
|
4222
|
+
created_at: now,
|
|
4223
|
+
updated_at: now
|
|
4224
|
+
},
|
|
4225
|
+
project,
|
|
4226
|
+
capabilities: {
|
|
4227
|
+
enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
|
|
4228
|
+
},
|
|
4229
|
+
approval: {
|
|
4230
|
+
required_for: ["destructive_command", "external_send"],
|
|
4231
|
+
default_risk_level: "medium"
|
|
4232
|
+
},
|
|
4233
|
+
adapters: {
|
|
4234
|
+
"claude-code": { enabled: true }
|
|
4235
|
+
},
|
|
4236
|
+
git: { events_log: "ignore" },
|
|
4237
|
+
...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
|
|
4238
|
+
};
|
|
4239
|
+
return ManifestSchema.parse(manifest);
|
|
4240
|
+
}
|
|
4241
|
+
async function writeManifest(paths, manifest, options) {
|
|
4242
|
+
const force = options?.force === true;
|
|
4243
|
+
const validated = ManifestSchema.parse(manifest);
|
|
4244
|
+
if (!force) {
|
|
4245
|
+
let existed = false;
|
|
4246
|
+
try {
|
|
4247
|
+
await lstat3(paths.files.manifest);
|
|
4248
|
+
existed = true;
|
|
4249
|
+
} catch (error) {
|
|
4250
|
+
if (!hasErrorCode3(error) || error.code !== "ENOENT") {
|
|
4251
|
+
throw new Error("Failed to inspect existing manifest", { cause: error });
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
if (existed) {
|
|
4255
|
+
throw new Error("Already initialized. Use --force to overwrite.");
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
await writeYamlFile(paths.files.manifest, validated);
|
|
4259
|
+
}
|
|
4260
|
+
async function readManifest(paths) {
|
|
4261
|
+
const raw = await readYamlFile(paths.files.manifest);
|
|
4262
|
+
return ManifestSchema.parse(raw);
|
|
4263
|
+
}
|
|
4264
|
+
function hasErrorCode3(error) {
|
|
4265
|
+
if (!(error instanceof Error)) return false;
|
|
4266
|
+
return typeof error.code === "string";
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
// src/orientation/orientation-renderer.ts
|
|
4270
|
+
async function summarizeOrientation(input) {
|
|
4271
|
+
const limit = input.relatedFilesLimit ?? 10;
|
|
4272
|
+
const now = new Date(input.nowIso);
|
|
4273
|
+
const loadOpts = { now };
|
|
4274
|
+
if (input.onSessionSkip !== void 0) loadOpts.onSkip = input.onSessionSkip;
|
|
4275
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
4276
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
4277
|
+
const decisions = [];
|
|
4278
|
+
for (const entry of entries) {
|
|
4279
|
+
const sessionDir = join14(input.paths.sessions, entry.sessionId);
|
|
4280
|
+
try {
|
|
4281
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
4282
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4283
|
+
})) {
|
|
4284
|
+
if (ev.type === "decision_recorded") {
|
|
4285
|
+
decisions.push({
|
|
4286
|
+
decisionId: ev.decision_id,
|
|
4287
|
+
title: ev.title,
|
|
4288
|
+
occurredAt: ev.occurred_at
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
} catch {
|
|
4293
|
+
input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
decisions.sort((a, b) => {
|
|
4297
|
+
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
4298
|
+
return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
|
|
4370
4299
|
});
|
|
4300
|
+
const latestDecision = decisions[decisions.length - 1];
|
|
4301
|
+
const taskLoadOpts = {};
|
|
4302
|
+
if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
|
|
4303
|
+
const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
|
|
4304
|
+
const inFlightTasks = taskEntries.filter((t) => t.task.task.status === "in_progress" || t.task.task.status === "planned").map((t) => ({
|
|
4305
|
+
id: t.task.task.id,
|
|
4306
|
+
title: t.task.task.title,
|
|
4307
|
+
status: t.task.task.status,
|
|
4308
|
+
linkedSessions: t.task.task.linked_sessions?.length ?? 0
|
|
4309
|
+
}));
|
|
4310
|
+
const plannedTasks = taskEntries.filter((t) => t.task.task.status === "planned").map((t) => ({ id: t.task.task.id, title: t.task.task.title }));
|
|
4311
|
+
const { pending: pendingIds } = await enumerateApprovals(input.paths);
|
|
4312
|
+
const pendingApprovals = [];
|
|
4313
|
+
for (const id of [...pendingIds].sort()) {
|
|
4314
|
+
const loaded = await loadApproval(input.paths, id);
|
|
4315
|
+
if (loaded === null) continue;
|
|
4316
|
+
const a = loaded.approval;
|
|
4317
|
+
pendingApprovals.push({
|
|
4318
|
+
id,
|
|
4319
|
+
risk: a.risk_level,
|
|
4320
|
+
kind: a.action.kind,
|
|
4321
|
+
reason: a.reason,
|
|
4322
|
+
sessionId: a.session_id,
|
|
4323
|
+
createdAt: a.created_at,
|
|
4324
|
+
expired: isLazyExpired(a, now)
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
const suspects = entries.filter((e) => e.suspect).map((e) => ({
|
|
4328
|
+
sessionId: e.sessionId,
|
|
4329
|
+
status: e.session.session.status,
|
|
4330
|
+
reason: e.suspectReason
|
|
4331
|
+
}));
|
|
4332
|
+
const liveEntries = entries.filter(
|
|
4333
|
+
(e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
|
|
4334
|
+
);
|
|
4335
|
+
const latestEntry = [...liveEntries].sort(
|
|
4336
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
4337
|
+
)[0];
|
|
4338
|
+
const latestSession = latestEntry !== void 0 ? {
|
|
4339
|
+
sessionId: latestEntry.sessionId,
|
|
4340
|
+
label: latestEntry.session.session.label ?? null,
|
|
4341
|
+
status: latestEntry.session.session.status
|
|
4342
|
+
} : null;
|
|
4343
|
+
const activityEntries = entries.filter((e) => e.session.session.status !== "archived");
|
|
4344
|
+
const newest = [...activityEntries].sort(
|
|
4345
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
4346
|
+
)[0];
|
|
4347
|
+
const bySourceMap = /* @__PURE__ */ new Map();
|
|
4348
|
+
for (const e of entries) {
|
|
4349
|
+
const k = e.session.session.source.kind;
|
|
4350
|
+
bySourceMap.set(k, (bySourceMap.get(k) ?? 0) + 1);
|
|
4351
|
+
}
|
|
4352
|
+
const bySource = [...bySourceMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([kind, count]) => ({ kind, count }));
|
|
4353
|
+
let sourceRoots = null;
|
|
4354
|
+
try {
|
|
4355
|
+
const manifest = await readManifest(input.paths);
|
|
4356
|
+
sourceRoots = manifest.import?.source_roots ?? null;
|
|
4357
|
+
} catch {
|
|
4358
|
+
sourceRoots = null;
|
|
4359
|
+
}
|
|
4360
|
+
const latestFiles = latestEntry?.session.session.related_files ?? [];
|
|
4361
|
+
const uniqueFiles = new Set(latestFiles);
|
|
4362
|
+
const displayed = [...uniqueFiles].sort().slice(0, limit);
|
|
4363
|
+
const overflow = Math.max(0, uniqueFiles.size - limit);
|
|
4364
|
+
return {
|
|
4365
|
+
generatedAt: input.nowIso,
|
|
4366
|
+
sessionCount: entries.length,
|
|
4367
|
+
latestSession,
|
|
4368
|
+
latestDecision: latestDecision ?? null,
|
|
4369
|
+
decisionCount: decisions.length,
|
|
4370
|
+
relatedFiles: { displayed, overflow },
|
|
4371
|
+
inFlightTasks,
|
|
4372
|
+
plannedTasks,
|
|
4373
|
+
pendingApprovals,
|
|
4374
|
+
suspects,
|
|
4375
|
+
freshness: {
|
|
4376
|
+
newestStartedAt: newest?.session.session.started_at ?? null,
|
|
4377
|
+
newestSource: newest?.session.session.source.kind ?? null,
|
|
4378
|
+
bySource,
|
|
4379
|
+
sourceRoots
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4371
4382
|
}
|
|
4372
|
-
function
|
|
4373
|
-
|
|
4374
|
-
|
|
4383
|
+
async function renderOrientation(input) {
|
|
4384
|
+
const summary = await summarizeOrientation(input);
|
|
4385
|
+
return {
|
|
4386
|
+
body: formatOrientationBody(summary, {
|
|
4387
|
+
staleness: input.staleness ?? null,
|
|
4388
|
+
verbose: input.verbose === true
|
|
4389
|
+
}),
|
|
4390
|
+
sessionCount: summary.sessionCount,
|
|
4391
|
+
pendingApprovalsCount: summary.pendingApprovals.length,
|
|
4392
|
+
suspectCount: summary.suspects.length,
|
|
4393
|
+
inFlightTaskCount: summary.inFlightTasks.length,
|
|
4394
|
+
decisionCount: summary.decisionCount
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
function formatOrientationBody(summary, opts) {
|
|
4398
|
+
const lines = [];
|
|
4399
|
+
const now = new Date(summary.generatedAt);
|
|
4400
|
+
const newestRel = relativeAge(summary.freshness.newestStartedAt ?? void 0, now);
|
|
4401
|
+
lines.push("# Orientation");
|
|
4402
|
+
lines.push("");
|
|
4403
|
+
lines.push(
|
|
4404
|
+
`> Generated at ${summary.generatedAt} \xB7 sessions ${summary.sessionCount} \xB7 newest ${newestRel} \xB7 pending ${summary.pendingApprovals.length} \xB7 suspect ${summary.suspects.length}`
|
|
4405
|
+
);
|
|
4406
|
+
lines.push("");
|
|
4407
|
+
lines.push("## \u4ECA\u3069\u3053\u306B\u3044\u308B");
|
|
4408
|
+
lines.push("");
|
|
4409
|
+
if (summary.latestSession !== null) {
|
|
4410
|
+
const s = summary.latestSession;
|
|
4411
|
+
const sid = shortId(s.sessionId);
|
|
4412
|
+
if (s.label !== null && s.label !== "") {
|
|
4413
|
+
lines.push(`- \u6700\u7D42 session: ${s.label} (${s.status}) [${sid}]`);
|
|
4414
|
+
} else {
|
|
4415
|
+
lines.push(`- \u6700\u7D42 session: ${sid} (${s.status})`);
|
|
4416
|
+
}
|
|
4417
|
+
} else {
|
|
4418
|
+
lines.push("- \u6700\u7D42 session: (no live sessions)");
|
|
4419
|
+
}
|
|
4420
|
+
if (summary.latestDecision !== null) {
|
|
4421
|
+
lines.push(
|
|
4422
|
+
`- \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title} [${shortId(summary.latestDecision.decisionId)}]`
|
|
4423
|
+
);
|
|
4424
|
+
if (summary.decisionCount > 1) {
|
|
4425
|
+
lines.push(` - ${summary.decisionCount} decisions total \u2014 see decisions.md`);
|
|
4426
|
+
}
|
|
4427
|
+
} else {
|
|
4428
|
+
lines.push("- \u76F4\u8FD1\u306E\u5224\u65AD: (no decisions recorded yet)");
|
|
4429
|
+
}
|
|
4430
|
+
if (summary.relatedFiles.displayed.length > 0) {
|
|
4431
|
+
const shown = summary.relatedFiles.displayed.join(", ");
|
|
4432
|
+
const more = summary.relatedFiles.overflow > 0 ? ` (... +${summary.relatedFiles.overflow} more)` : "";
|
|
4433
|
+
lines.push(`- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: ${shown}${more}`);
|
|
4434
|
+
} else {
|
|
4435
|
+
lines.push("- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: (none recorded)");
|
|
4436
|
+
}
|
|
4437
|
+
lines.push("");
|
|
4438
|
+
lines.push("## \u4F55\u304C\u52D5\u304F");
|
|
4439
|
+
lines.push("");
|
|
4440
|
+
lines.push(`### \u9032\u884C\u4E2D task (${summary.inFlightTasks.length})`);
|
|
4441
|
+
if (summary.inFlightTasks.length === 0) {
|
|
4442
|
+
lines.push("- (none)");
|
|
4443
|
+
} else {
|
|
4444
|
+
for (const t of summary.inFlightTasks) {
|
|
4445
|
+
const linkedSuffix = t.linkedSessions > 1 ? ` \u2014 linked_sessions: ${t.linkedSessions}` : "";
|
|
4446
|
+
lines.push(`- ${t.title} (${t.status}) [${shortId(t.id)}]${linkedSuffix}`);
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
lines.push("");
|
|
4450
|
+
lines.push(`### \u627F\u8A8D\u5F85\u3061 (${summary.pendingApprovals.length})`);
|
|
4451
|
+
if (summary.pendingApprovals.length === 0) {
|
|
4452
|
+
lines.push("- (none)");
|
|
4453
|
+
} else {
|
|
4454
|
+
for (const a of summary.pendingApprovals) {
|
|
4455
|
+
const expired = a.expired ? " (expired)" : "";
|
|
4456
|
+
lines.push(
|
|
4457
|
+
`- [${a.risk}] ${a.kind}: ${a.reason} \u2014 session ${shortId(a.sessionId)}, since ${a.createdAt}${expired}`
|
|
4458
|
+
);
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
lines.push("");
|
|
4462
|
+
lines.push(`### \u8981\u6CE8\u610F session (${summary.suspects.length})`);
|
|
4463
|
+
if (summary.suspects.length === 0) {
|
|
4464
|
+
lines.push("- (none)");
|
|
4465
|
+
} else {
|
|
4466
|
+
for (const e of summary.suspects) {
|
|
4467
|
+
lines.push(`- ${shortId(e.sessionId)} (${e.status}) \u2014 ${suspectText(e.reason)}`);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
lines.push("");
|
|
4471
|
+
lines.push("## \u3069\u3053\u3078\u5411\u304B\u3046");
|
|
4472
|
+
lines.push("");
|
|
4473
|
+
if (summary.plannedTasks.length === 0) {
|
|
4474
|
+
lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
|
|
4475
|
+
if (summary.latestDecision !== null) {
|
|
4476
|
+
lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title}`);
|
|
4477
|
+
}
|
|
4478
|
+
} else {
|
|
4479
|
+
for (const t of summary.plannedTasks) {
|
|
4480
|
+
lines.push(`- ${t.title} [${shortId(t.id)}]`);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
lines.push("");
|
|
4484
|
+
lines.push("## \u3053\u308C\u306F\u6700\u65B0\u304B");
|
|
4485
|
+
lines.push("");
|
|
4486
|
+
for (const line of freshnessVerdict(summary, opts.staleness, now)) lines.push(line);
|
|
4487
|
+
if (opts.verbose) {
|
|
4488
|
+
lines.push("");
|
|
4489
|
+
lines.push("<!-- verbose: raw freshness telemetry -->");
|
|
4490
|
+
if (summary.freshness.newestStartedAt !== null) {
|
|
4491
|
+
lines.push(`- newest captured session: ${summary.freshness.newestStartedAt} (${newestRel})`);
|
|
4492
|
+
} else {
|
|
4493
|
+
lines.push("- newest captured session: (no sessions captured yet)");
|
|
4494
|
+
}
|
|
4495
|
+
const sourceBreakdown = summary.freshness.bySource.map(({ kind, count }) => `${kind} ${count}`).join(", ");
|
|
4496
|
+
lines.push(
|
|
4497
|
+
`- sessions: ${summary.sessionCount}${sourceBreakdown !== "" ? ` (${sourceBreakdown})` : ""}`
|
|
4498
|
+
);
|
|
4499
|
+
if (summary.freshness.sourceRoots !== null && summary.freshness.sourceRoots.length > 0) {
|
|
4500
|
+
lines.push(`- source roots: ${summary.freshness.sourceRoots.join(", ")}`);
|
|
4501
|
+
} else {
|
|
4502
|
+
lines.push("- source roots: (single root)");
|
|
4503
|
+
}
|
|
4504
|
+
lines.push(`- suspect sessions: ${summary.suspects.length}`);
|
|
4505
|
+
const probe = opts.staleness === null ? "not run" : `new ${opts.staleness.newSessions}, updated ${opts.staleness.updatedSessions}, unverifiable ${opts.staleness.unverifiableSessions ?? 0}`;
|
|
4506
|
+
lines.push(`- staleness probe: ${probe}`);
|
|
4507
|
+
}
|
|
4508
|
+
return lines.join("\n");
|
|
4509
|
+
}
|
|
4510
|
+
function toolDisplayName(kind) {
|
|
4511
|
+
switch (kind) {
|
|
4512
|
+
case "claude-code-import":
|
|
4513
|
+
case "claude-code-adapter":
|
|
4514
|
+
return "Claude Code";
|
|
4515
|
+
case "codex-import":
|
|
4516
|
+
return "Codex";
|
|
4517
|
+
case "terminal":
|
|
4518
|
+
return "\u30BF\u30FC\u30DF\u30CA\u30EB";
|
|
4519
|
+
case "human":
|
|
4520
|
+
return "\u624B\u52D5\u30E1\u30E2";
|
|
4521
|
+
case "import":
|
|
4522
|
+
return "\u4ED6\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9";
|
|
4523
|
+
default:
|
|
4524
|
+
return kind ?? "\u4E0D\u660E";
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
function freshnessVerdict(summary, staleness, now) {
|
|
4528
|
+
if (staleness !== null && (staleness.unverifiableSessions ?? 0) > 0) {
|
|
4529
|
+
return [
|
|
4530
|
+
`\u26A0\uFE0F \u6700\u65B0\u304B\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3002\u5909\u5316\u3057\u305F\u304C\u5B89\u5168\u306B\u53D6\u308A\u8FBC\u3081\u306A\u3044\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${staleness.unverifiableSessions} \u4EF6\u3042\u308A\u307E\u3059(\u30CF\u30C3\u30B7\u30E5\u30C1\u30A7\u30FC\u30F3\u7834\u640D\u30FB\u975E\u8FFD\u8A18\u5909\u66F4\u306A\u3069)\u3002`,
|
|
4531
|
+
"`basou verify` \u3067\u78BA\u8A8D\u3057\u3001`basou refresh --force` \u3067\u518D\u53D6\u308A\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4532
|
+
];
|
|
4533
|
+
}
|
|
4534
|
+
if (staleness !== null && (staleness.newSessions > 0 || staleness.updatedSessions > 0)) {
|
|
4535
|
+
const parts = [];
|
|
4536
|
+
if (staleness.newSessions > 0) parts.push(`\u65B0\u898F ${staleness.newSessions} \u4EF6`);
|
|
4537
|
+
if (staleness.updatedSessions > 0) parts.push(`\u66F4\u65B0 ${staleness.updatedSessions} \u4EF6`);
|
|
4538
|
+
return [
|
|
4539
|
+
`\u26A0\uFE0F \u53E4\u3044\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002\u6700\u5F8C\u306E\u53D6\u308A\u8FBC\u307F\u4EE5\u964D\u306B\u672A\u53D6\u308A\u8FBC\u307F\u306E\u4F5C\u696D\u304C\u3042\u308A\u307E\u3059(${parts.join("\u30FB")})\u3002`,
|
|
4540
|
+
"`basou refresh` \u3067\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4541
|
+
];
|
|
4542
|
+
}
|
|
4543
|
+
if (summary.freshness.newestStartedAt === null) {
|
|
4544
|
+
return [
|
|
4545
|
+
"\u2139\uFE0F \u307E\u3060\u8A18\u9332\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
4546
|
+
"\u3053\u306E\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u3067\u4F5C\u696D\u3059\u308B\u3068\u3001\u3053\u3053\u306B\u73FE\u5728\u5730\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002"
|
|
4547
|
+
];
|
|
4548
|
+
}
|
|
4549
|
+
const rel = relativeAgeJa(summary.freshness.newestStartedAt, now);
|
|
4550
|
+
const tool = toolDisplayName(summary.freshness.newestSource);
|
|
4551
|
+
const suspectCount = summary.suspects.length;
|
|
4552
|
+
const suspectClause = suspectCount > 0 ? `\u8981\u6CE8\u610F\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${suspectCount} \u4EF6\u3042\u308A\u307E\u3059\u3002` : "\u53D6\u308A\u3053\u307C\u3057\u30FB\u8981\u6CE8\u610F\u306A\u3057\u3002";
|
|
4553
|
+
if (staleness === null) {
|
|
4554
|
+
return [
|
|
4555
|
+
`\u2139\uFE0F \u53D6\u308A\u8FBC\u307F\u6E08\u307F\u306E\u72B6\u614B\u3092\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002`,
|
|
4556
|
+
"\u6700\u65B0\u304B\u78BA\u8A8D\u3059\u308B\u306B\u306F `basou refresh` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4557
|
+
];
|
|
4558
|
+
}
|
|
4559
|
+
return [`\u2705 \u6700\u65B0\u3067\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002${suspectClause}`];
|
|
4560
|
+
}
|
|
4561
|
+
function relativeAgeJa(startedAt, now) {
|
|
4562
|
+
if (startedAt === null) return "(\u4E0D\u660E)";
|
|
4563
|
+
const ms = now.getTime() - Date.parse(startedAt);
|
|
4564
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
|
|
4565
|
+
if (ms < 6e4) return "\u305F\u3063\u305F\u4ECA";
|
|
4566
|
+
const totalMin = Math.floor(ms / 6e4);
|
|
4567
|
+
const days = Math.floor(totalMin / 1440);
|
|
4568
|
+
const hours = Math.floor(totalMin % 1440 / 60);
|
|
4569
|
+
const mins = totalMin % 60;
|
|
4570
|
+
if (days > 0) return hours > 0 ? `${days}\u65E5${hours}\u6642\u9593\u524D` : `${days}\u65E5\u524D`;
|
|
4571
|
+
if (hours > 0) return mins > 0 ? `${hours}\u6642\u9593${mins}\u5206\u524D` : `${hours}\u6642\u9593\u524D`;
|
|
4572
|
+
return `${mins}\u5206\u524D`;
|
|
4573
|
+
}
|
|
4574
|
+
function relativeAge(startedAt, now) {
|
|
4575
|
+
if (startedAt === void 0) return "(unknown)";
|
|
4576
|
+
const ms = now.getTime() - Date.parse(startedAt);
|
|
4577
|
+
if (!Number.isFinite(ms)) return "(unknown)";
|
|
4578
|
+
if (ms < 0) return "just now";
|
|
4579
|
+
if (ms < 1e3) return "just now";
|
|
4580
|
+
return `${formatDurationMs(ms)} ago`;
|
|
4581
|
+
}
|
|
4582
|
+
function suspectText(reason) {
|
|
4583
|
+
if (reason === "events_say_ended_but_yaml_running") return "ended (yaml stale)";
|
|
4584
|
+
if (reason === "running_no_end_event") return "no end event";
|
|
4585
|
+
return "suspect";
|
|
4586
|
+
}
|
|
4587
|
+
function shortId(id) {
|
|
4588
|
+
const sep = id.indexOf("_");
|
|
4589
|
+
if (sep === -1) return id.slice(0, 10);
|
|
4590
|
+
return id.slice(0, sep + 1) + id.slice(sep + 1, sep + 1 + 10);
|
|
4375
4591
|
}
|
|
4376
4592
|
|
|
4593
|
+
// src/report/report-renderer.ts
|
|
4594
|
+
import { join as join16 } from "path";
|
|
4595
|
+
|
|
4377
4596
|
// src/stats/work-stats.ts
|
|
4378
|
-
import { join as
|
|
4597
|
+
import { join as join15 } from "path";
|
|
4379
4598
|
function resolveTimeZone(timeZone) {
|
|
4380
4599
|
if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
|
|
4381
4600
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -4406,7 +4625,7 @@ async function computeWorkStats(input) {
|
|
|
4406
4625
|
const events = [];
|
|
4407
4626
|
let eventsUnreadable = false;
|
|
4408
4627
|
try {
|
|
4409
|
-
for await (const ev of replayEvents(
|
|
4628
|
+
for await (const ev of replayEvents(join15(input.paths.sessions, entry.sessionId), {
|
|
4410
4629
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4411
4630
|
})) {
|
|
4412
4631
|
events.push(ev);
|
|
@@ -4660,61 +4879,618 @@ function tzDate(ms, timeZone) {
|
|
|
4660
4879
|
}).format(new Date(ms));
|
|
4661
4880
|
}
|
|
4662
4881
|
|
|
4663
|
-
// src/
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
}
|
|
4882
|
+
// src/report/report-renderer.ts
|
|
4883
|
+
var CHANGED_FILES_MARKDOWN_LIMIT = 50;
|
|
4884
|
+
var DECISIONS_MARKDOWN_LIMIT = 20;
|
|
4885
|
+
var SESSIONS_MARKDOWN_LIMIT = 30;
|
|
4886
|
+
var TASKS_MARKDOWN_LIMIT = 30;
|
|
4887
|
+
var APPROVALS_MARKDOWN_LIMIT = 30;
|
|
4888
|
+
var SESSION_STATUS_ORDER = [
|
|
4889
|
+
"completed",
|
|
4890
|
+
"failed",
|
|
4891
|
+
"running",
|
|
4892
|
+
"waiting_approval",
|
|
4893
|
+
"interrupted",
|
|
4894
|
+
"initialized",
|
|
4895
|
+
"imported",
|
|
4896
|
+
"archived"
|
|
4897
|
+
];
|
|
4898
|
+
var TASK_STATUS_ORDER = ["planned", "in_progress", "done", "cancelled"];
|
|
4899
|
+
async function renderReport(input) {
|
|
4900
|
+
const now = new Date(input.nowIso);
|
|
4901
|
+
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
4902
|
+
const wrappedSkip = (sid, reason) => {
|
|
4903
|
+
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
4904
|
+
input.onSessionSkip?.(sid, reason);
|
|
4687
4905
|
};
|
|
4688
|
-
}
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4906
|
+
const loadOpts = { now, onSkip: wrappedSkip };
|
|
4907
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
4908
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
4909
|
+
const statsInput = { paths: input.paths, now };
|
|
4910
|
+
if (input.timeZone !== void 0) statsInput.timeZone = input.timeZone;
|
|
4911
|
+
const stats = await computeWorkStats(statsInput);
|
|
4912
|
+
const statsBySession = new Map(stats.sessions.map((s) => [s.sessionId, s]));
|
|
4913
|
+
const decisions = [];
|
|
4914
|
+
for (const entry of entries) {
|
|
4915
|
+
const sessionDir = join16(input.paths.sessions, entry.sessionId);
|
|
4916
|
+
try {
|
|
4917
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
4918
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4919
|
+
})) {
|
|
4920
|
+
if (ev.type === "decision_recorded") {
|
|
4921
|
+
decisions.push({ id: ev.decision_id, title: ev.title, occurredAt: ev.occurred_at });
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
} catch {
|
|
4925
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
4926
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
4927
|
+
}
|
|
4707
4928
|
}
|
|
4708
4929
|
}
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4930
|
+
decisions.sort((a, b) => {
|
|
4931
|
+
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
4932
|
+
return c !== 0 ? c : a.id.localeCompare(b.id);
|
|
4933
|
+
});
|
|
4934
|
+
const taskLoadOpts = {};
|
|
4935
|
+
if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
|
|
4936
|
+
const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
|
|
4937
|
+
const taskItems = taskEntries.map((t2) => ({
|
|
4938
|
+
id: t2.task.task.id,
|
|
4939
|
+
title: t2.task.task.title,
|
|
4940
|
+
status: t2.task.task.status
|
|
4941
|
+
}));
|
|
4942
|
+
const tasksByStatus = tallyTaskStatus(taskItems);
|
|
4943
|
+
const approvalIds = await enumerateApprovals(input.paths);
|
|
4944
|
+
const resolvedSet = new Set(approvalIds.resolved);
|
|
4945
|
+
const pendingIds = approvalIds.pending.filter((id) => !resolvedSet.has(id));
|
|
4946
|
+
const loadedApprovals = (await Promise.all(
|
|
4947
|
+
[...pendingIds, ...approvalIds.resolved].map((id) => loadApproval(input.paths, id))
|
|
4948
|
+
)).filter((a) => a !== null);
|
|
4949
|
+
const approvalItems = loadedApprovals.map((a) => ({
|
|
4950
|
+
id: a.approval.id,
|
|
4951
|
+
reason: a.approval.reason,
|
|
4952
|
+
status: a.approval.status,
|
|
4953
|
+
riskLevel: a.approval.risk_level
|
|
4954
|
+
}));
|
|
4955
|
+
const approvalCounts = { pending: 0, approved: 0, rejected: 0, expired: 0 };
|
|
4956
|
+
for (const a of approvalItems) approvalCounts[a.status] += 1;
|
|
4957
|
+
const changedSet = /* @__PURE__ */ new Set();
|
|
4958
|
+
for (const entry of entries) {
|
|
4959
|
+
if (entry.session.session.source.kind === "import") continue;
|
|
4960
|
+
for (const f of entry.session.session.related_files) changedSet.add(f);
|
|
4961
|
+
}
|
|
4962
|
+
const changedFiles = [...changedSet].sort();
|
|
4963
|
+
const integrity = {
|
|
4964
|
+
total: 0,
|
|
4965
|
+
verified: 0,
|
|
4966
|
+
unchained: 0,
|
|
4967
|
+
empty: 0,
|
|
4968
|
+
incomplete: 0,
|
|
4969
|
+
in_progress: 0,
|
|
4970
|
+
tampered: 0,
|
|
4971
|
+
tamperedSessions: []
|
|
4972
|
+
};
|
|
4973
|
+
for (const entry of entries) {
|
|
4974
|
+
const verdict = await verifyEventsChain(input.paths, entry.sessionId).catch(() => null);
|
|
4975
|
+
if (verdict === null) {
|
|
4976
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
4977
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
4978
|
+
}
|
|
4979
|
+
continue;
|
|
4980
|
+
}
|
|
4981
|
+
integrity.total += 1;
|
|
4982
|
+
integrity[verdict.status] += 1;
|
|
4983
|
+
if (verdict.status === "tampered") integrity.tamperedSessions.push(entry.sessionId);
|
|
4984
|
+
}
|
|
4985
|
+
const sessionItems = [...entries].sort(
|
|
4986
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
4987
|
+
).map((e) => {
|
|
4988
|
+
const w = statsBySession.get(e.sessionId);
|
|
4989
|
+
return {
|
|
4990
|
+
id: e.sessionId,
|
|
4991
|
+
label: e.session.session.label ?? null,
|
|
4992
|
+
status: e.session.session.status,
|
|
4993
|
+
source: e.session.session.source.kind,
|
|
4994
|
+
startedAt: e.session.session.started_at,
|
|
4995
|
+
activeMs: w?.activeTimeMs ?? 0,
|
|
4996
|
+
outputTokens: w?.tokens.output ?? 0
|
|
4997
|
+
};
|
|
4998
|
+
});
|
|
4999
|
+
const period = computePeriod(entries, input.nowIso);
|
|
5000
|
+
const t = stats.totals;
|
|
5001
|
+
const data = {
|
|
5002
|
+
generatedAt: input.nowIso,
|
|
5003
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
5004
|
+
period,
|
|
5005
|
+
sessions: { total: entries.length, byStatus: stats.byStatus, items: sessionItems },
|
|
5006
|
+
volume: {
|
|
5007
|
+
outputTokens: t.tokens.output,
|
|
5008
|
+
reasoningTokens: t.tokens.reasoning,
|
|
5009
|
+
commandCount: t.commandCount,
|
|
5010
|
+
fileChangedCount: t.fileChangedCount,
|
|
5011
|
+
decisionCount: t.decisionCount,
|
|
5012
|
+
tokensAvailable: t.tokensAvailable
|
|
5013
|
+
},
|
|
5014
|
+
time: {
|
|
5015
|
+
activeMs: t.billableActiveTimeMs,
|
|
5016
|
+
machineActiveMs: t.machineActiveTimeMs,
|
|
5017
|
+
machineAvailable: t.machineActiveAvailable,
|
|
5018
|
+
spanMs: t.sessionSpanMs,
|
|
5019
|
+
commandTimeMs: t.commandTimeMs,
|
|
5020
|
+
timeZone: stats.timeZone
|
|
5021
|
+
},
|
|
5022
|
+
decisions: { count: decisions.length, items: decisions },
|
|
5023
|
+
approvals: { ...approvalCounts, items: approvalItems },
|
|
5024
|
+
tasks: { total: taskEntries.length, byStatus: tasksByStatus, items: taskItems },
|
|
5025
|
+
changedFiles,
|
|
5026
|
+
integrity
|
|
5027
|
+
};
|
|
5028
|
+
return { body: formatReportBody(data), data };
|
|
5029
|
+
}
|
|
5030
|
+
function computePeriod(entries, nowIso) {
|
|
5031
|
+
if (entries.length === 0) return { from: null, to: null };
|
|
5032
|
+
let from = entries[0]?.session.session.started_at ?? nowIso;
|
|
5033
|
+
let to = nowIso;
|
|
5034
|
+
let sawEnd = false;
|
|
5035
|
+
for (const e of entries) {
|
|
5036
|
+
const s = e.session.session.started_at;
|
|
5037
|
+
if (Date.parse(s) < Date.parse(from)) from = s;
|
|
5038
|
+
const end = e.session.session.ended_at ?? nowIso;
|
|
5039
|
+
if (!sawEnd || Date.parse(end) > Date.parse(to)) {
|
|
5040
|
+
to = end;
|
|
5041
|
+
sawEnd = true;
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
if (Date.parse(to) < Date.parse(from)) to = from;
|
|
5045
|
+
return { from, to };
|
|
5046
|
+
}
|
|
5047
|
+
function tallyTaskStatus(items) {
|
|
5048
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5049
|
+
for (const i of items) counts.set(i.status, (counts.get(i.status) ?? 0) + 1);
|
|
5050
|
+
return TASK_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((status) => ({
|
|
5051
|
+
status,
|
|
5052
|
+
count: counts.get(status)
|
|
5053
|
+
}));
|
|
5054
|
+
}
|
|
5055
|
+
function formatReportBody(data) {
|
|
5056
|
+
const lines = [];
|
|
5057
|
+
const titleSuffix = data.title !== void 0 ? ` \u2014 ${data.title}` : "";
|
|
5058
|
+
lines.push(`# Report${titleSuffix}`);
|
|
5059
|
+
lines.push("");
|
|
5060
|
+
const periodSuffix = data.period.from !== null && data.period.to !== null ? ` (${data.period.from.slice(0, 10)}..${data.period.to.slice(0, 10)})` : "";
|
|
5061
|
+
lines.push(`> Generated at ${data.generatedAt}${periodSuffix}`);
|
|
5062
|
+
lines.push("");
|
|
5063
|
+
lines.push("## \u6982\u8981");
|
|
5064
|
+
lines.push("");
|
|
5065
|
+
lines.push(`- ${formatSessionsLine(data)}`);
|
|
5066
|
+
lines.push(
|
|
5067
|
+
`- Active time ${formatDurationMs(data.time.activeMs)}, ${formatInt(data.volume.outputTokens)} output tokens`
|
|
5068
|
+
);
|
|
5069
|
+
lines.push("");
|
|
5070
|
+
lines.push("## \u4F5C\u696D\u91CF");
|
|
5071
|
+
lines.push("");
|
|
5072
|
+
const tokenCaveat = data.volume.tokensAvailable ? "" : " (no token data captured)";
|
|
5073
|
+
lines.push(`- Output tokens: ${formatInt(data.volume.outputTokens)}${tokenCaveat}`);
|
|
5074
|
+
if (data.volume.reasoningTokens > 0) {
|
|
5075
|
+
lines.push(`- Reasoning tokens: ${formatInt(data.volume.reasoningTokens)} (Codex)`);
|
|
5076
|
+
}
|
|
5077
|
+
lines.push(
|
|
5078
|
+
`- Actions: ${data.volume.commandCount} commands, ${data.volume.fileChangedCount} files, ${data.volume.decisionCount} decisions`
|
|
5079
|
+
);
|
|
5080
|
+
lines.push(
|
|
5081
|
+
`- Active time: ${formatDurationMs(data.time.activeMs)} (union; idle gaps > 5m excluded; tz ${data.time.timeZone})`
|
|
5082
|
+
);
|
|
5083
|
+
if (data.time.machineAvailable) {
|
|
5084
|
+
lines.push(
|
|
5085
|
+
`- Model working: ${formatDurationMs(data.time.machineActiveMs)} (model compute, subset of active)`
|
|
5086
|
+
);
|
|
5087
|
+
}
|
|
5088
|
+
lines.push(`- Span: ${formatDurationMs(data.time.spanMs)} (total elapsed)`);
|
|
5089
|
+
lines.push("");
|
|
5090
|
+
lines.push("## \u5224\u65AD");
|
|
5091
|
+
lines.push("");
|
|
5092
|
+
if (data.decisions.items.length === 0) {
|
|
5093
|
+
lines.push("(no decisions recorded yet)");
|
|
5094
|
+
} else {
|
|
5095
|
+
const total = data.decisions.items.length;
|
|
5096
|
+
const shown = total > DECISIONS_MARKDOWN_LIMIT ? data.decisions.items.slice(-DECISIONS_MARKDOWN_LIMIT) : data.decisions.items;
|
|
5097
|
+
if (total > DECISIONS_MARKDOWN_LIMIT) {
|
|
5098
|
+
lines.push(`(showing the ${DECISIONS_MARKDOWN_LIMIT} most recent of ${total})`);
|
|
5099
|
+
lines.push("");
|
|
5100
|
+
}
|
|
5101
|
+
for (const d of shown) {
|
|
5102
|
+
lines.push(`- ${d.occurredAt.slice(0, 10)} \xB7 ${d.title}`);
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
lines.push("");
|
|
5106
|
+
lines.push("## \u627F\u8A8D");
|
|
5107
|
+
lines.push("");
|
|
5108
|
+
if (data.approvals.items.length === 0) {
|
|
5109
|
+
lines.push("(none)");
|
|
5110
|
+
} else {
|
|
5111
|
+
const a = data.approvals;
|
|
5112
|
+
lines.push(
|
|
5113
|
+
`Pending ${a.pending} \xB7 Approved ${a.approved} \xB7 Rejected ${a.rejected} \xB7 Expired ${a.expired}`
|
|
5114
|
+
);
|
|
5115
|
+
lines.push("");
|
|
5116
|
+
for (const item of data.approvals.items.slice(0, APPROVALS_MARKDOWN_LIMIT)) {
|
|
5117
|
+
lines.push(`- ${item.reason} (${item.status}, ${item.riskLevel})`);
|
|
5118
|
+
}
|
|
5119
|
+
const overflow = data.approvals.items.length - APPROVALS_MARKDOWN_LIMIT;
|
|
5120
|
+
if (overflow > 0) lines.push(`- ... +${overflow} more`);
|
|
5121
|
+
}
|
|
5122
|
+
lines.push("");
|
|
5123
|
+
lines.push("## \u30BF\u30B9\u30AF");
|
|
5124
|
+
lines.push("");
|
|
5125
|
+
if (data.tasks.items.length === 0) {
|
|
5126
|
+
lines.push("(no tasks recorded yet)");
|
|
5127
|
+
} else {
|
|
5128
|
+
const breakdown = data.tasks.byStatus.map((s) => `${s.status} ${s.count}`).join(", ");
|
|
5129
|
+
lines.push(`Tasks: ${data.tasks.total} (${breakdown})`);
|
|
5130
|
+
lines.push("");
|
|
5131
|
+
for (const item of data.tasks.items.slice(0, TASKS_MARKDOWN_LIMIT)) {
|
|
5132
|
+
lines.push(`- ${item.title} (${item.status})`);
|
|
5133
|
+
}
|
|
5134
|
+
const overflow = data.tasks.items.length - TASKS_MARKDOWN_LIMIT;
|
|
5135
|
+
if (overflow > 0) lines.push(`- ... +${overflow} more`);
|
|
5136
|
+
}
|
|
5137
|
+
lines.push("");
|
|
5138
|
+
lines.push("## \u5909\u66F4\u30D5\u30A1\u30A4\u30EB");
|
|
5139
|
+
lines.push("");
|
|
5140
|
+
if (data.changedFiles.length === 0) {
|
|
5141
|
+
lines.push("(no related files recorded)");
|
|
5142
|
+
} else {
|
|
5143
|
+
for (const f of data.changedFiles.slice(0, CHANGED_FILES_MARKDOWN_LIMIT)) lines.push(`- ${f}`);
|
|
5144
|
+
const overflow = data.changedFiles.length - CHANGED_FILES_MARKDOWN_LIMIT;
|
|
5145
|
+
if (overflow > 0) lines.push(`- ... +${overflow} more`);
|
|
5146
|
+
}
|
|
5147
|
+
lines.push("");
|
|
5148
|
+
lines.push("## \u30BB\u30C3\u30B7\u30E7\u30F3\u4E00\u89A7");
|
|
5149
|
+
lines.push("");
|
|
5150
|
+
if (data.sessions.items.length === 0) {
|
|
5151
|
+
lines.push("(no sessions yet)");
|
|
5152
|
+
} else {
|
|
5153
|
+
lines.push("| started_at | source | status | active | out tok |");
|
|
5154
|
+
lines.push("|---|---|---|---|---|");
|
|
5155
|
+
for (const s of data.sessions.items.slice(0, SESSIONS_MARKDOWN_LIMIT)) {
|
|
5156
|
+
lines.push(
|
|
5157
|
+
`| ${s.startedAt} | ${s.source} | ${s.status} | ${formatDurationMs(s.activeMs)} | ${formatInt(s.outputTokens)} |`
|
|
5158
|
+
);
|
|
5159
|
+
}
|
|
5160
|
+
const overflow = data.sessions.items.length - SESSIONS_MARKDOWN_LIMIT;
|
|
5161
|
+
if (overflow > 0) {
|
|
5162
|
+
lines.push("");
|
|
5163
|
+
lines.push(`... +${overflow} more sessions`);
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
lines.push("");
|
|
5167
|
+
lines.push("## \u6574\u5408\u6027");
|
|
5168
|
+
lines.push("");
|
|
5169
|
+
const i = data.integrity;
|
|
5170
|
+
lines.push(
|
|
5171
|
+
`Provenance internally tamper-checked: ${i.verified} verified, ${i.unchained} unchained, ${i.empty} empty, ${i.incomplete} incomplete, ${i.in_progress} in_progress, ${i.tampered} tampered (of ${i.total} sessions).`
|
|
5172
|
+
);
|
|
5173
|
+
lines.push("");
|
|
5174
|
+
lines.push(
|
|
5175
|
+
"This reflects internal consistency of the local event-log hash chain \u2014 not a third-party cryptographic proof."
|
|
5176
|
+
);
|
|
5177
|
+
if (i.tampered > 0) {
|
|
5178
|
+
lines.push("");
|
|
5179
|
+
for (const id of i.tamperedSessions) lines.push(`- Tampered: ${id}`);
|
|
5180
|
+
}
|
|
5181
|
+
return lines.join("\n");
|
|
5182
|
+
}
|
|
5183
|
+
function formatSessionsLine(data) {
|
|
5184
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5185
|
+
for (const s of data.sessions.byStatus) counts.set(s.status, s.count);
|
|
5186
|
+
const breakdown = SESSION_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((s) => `${s} ${counts.get(s)}`).join(", ");
|
|
5187
|
+
return breakdown !== "" ? `Sessions: ${data.sessions.total} (${breakdown})` : `Sessions: ${data.sessions.total}`;
|
|
5188
|
+
}
|
|
5189
|
+
function formatInt(n) {
|
|
5190
|
+
return n.toLocaleString("en-US");
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
// src/runtime/child-process-runner.ts
|
|
5194
|
+
import { spawn as spawn2 } from "child_process";
|
|
5195
|
+
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
5196
|
+
var ChildProcessRunner = class {
|
|
5197
|
+
async run(command, args, options) {
|
|
5198
|
+
validateOptions(options);
|
|
5199
|
+
if (options.signal?.aborted) {
|
|
5200
|
+
throw new Error("Process aborted before spawn", {
|
|
5201
|
+
cause: options.signal.reason
|
|
5202
|
+
});
|
|
5203
|
+
}
|
|
5204
|
+
const snapshotCommand = command;
|
|
5205
|
+
const snapshotArgs = [...args];
|
|
5206
|
+
const snapshotCwd = options.cwd;
|
|
5207
|
+
const captureMode = options.capture ?? "buffer";
|
|
5208
|
+
const started_at = /* @__PURE__ */ new Date();
|
|
5209
|
+
let child;
|
|
5210
|
+
try {
|
|
5211
|
+
child = spawn2(snapshotCommand, [...snapshotArgs], {
|
|
5212
|
+
cwd: snapshotCwd,
|
|
5213
|
+
env: options.env ?? process.env,
|
|
5214
|
+
stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
|
|
5215
|
+
shell: false,
|
|
5216
|
+
detached: false
|
|
5217
|
+
});
|
|
5218
|
+
} catch (error) {
|
|
5219
|
+
throw classifySpawnError(error);
|
|
5220
|
+
}
|
|
5221
|
+
if (options.onSpawn) {
|
|
5222
|
+
try {
|
|
5223
|
+
options.onSpawn(child);
|
|
5224
|
+
} catch {
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
let timeoutTimer = null;
|
|
5228
|
+
let killTimer = null;
|
|
5229
|
+
let killed = false;
|
|
5230
|
+
let settled = false;
|
|
5231
|
+
const triggerKill = () => {
|
|
5232
|
+
if (killed || child.exitCode !== null) return;
|
|
5233
|
+
killed = true;
|
|
5234
|
+
child.kill("SIGTERM");
|
|
5235
|
+
killTimer = setTimeout(() => {
|
|
5236
|
+
if (child.exitCode === null) {
|
|
5237
|
+
child.kill("SIGKILL");
|
|
5238
|
+
}
|
|
5239
|
+
}, DEFAULT_KILL_GRACE_MS);
|
|
5240
|
+
};
|
|
5241
|
+
const onAbort = () => {
|
|
5242
|
+
triggerKill();
|
|
5243
|
+
};
|
|
5244
|
+
options.signal?.addEventListener("abort", onAbort);
|
|
5245
|
+
if (options.signal?.aborted) {
|
|
5246
|
+
triggerKill();
|
|
5247
|
+
}
|
|
5248
|
+
let stdout = "";
|
|
5249
|
+
let stderr = "";
|
|
5250
|
+
if (captureMode === "buffer") {
|
|
5251
|
+
child.stdout?.setEncoding("utf8");
|
|
5252
|
+
child.stderr?.setEncoding("utf8");
|
|
5253
|
+
child.stdout?.on("data", (chunk) => {
|
|
5254
|
+
stdout += chunk;
|
|
5255
|
+
});
|
|
5256
|
+
child.stderr?.on("data", (chunk) => {
|
|
5257
|
+
stderr += chunk;
|
|
5258
|
+
});
|
|
5259
|
+
if (options.stdin !== void 0) {
|
|
5260
|
+
child.stdin?.end(options.stdin);
|
|
5261
|
+
} else {
|
|
5262
|
+
child.stdin?.end();
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
if (options.timeout_ms !== void 0) {
|
|
5266
|
+
timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
|
|
5267
|
+
}
|
|
5268
|
+
const cleanup = () => {
|
|
5269
|
+
if (timeoutTimer !== null) clearTimeout(timeoutTimer);
|
|
5270
|
+
if (killTimer !== null) clearTimeout(killTimer);
|
|
5271
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
5272
|
+
};
|
|
5273
|
+
return new Promise((resolve2, reject) => {
|
|
5274
|
+
child.once("error", (error) => {
|
|
5275
|
+
if (settled) return;
|
|
5276
|
+
settled = true;
|
|
5277
|
+
cleanup();
|
|
5278
|
+
reject(classifySpawnError(error));
|
|
5279
|
+
});
|
|
5280
|
+
child.once("close", (code, signal) => {
|
|
5281
|
+
if (settled) return;
|
|
5282
|
+
settled = true;
|
|
5283
|
+
cleanup();
|
|
5284
|
+
const ended_at = /* @__PURE__ */ new Date();
|
|
5285
|
+
resolve2({
|
|
5286
|
+
command: snapshotCommand,
|
|
5287
|
+
args: snapshotArgs,
|
|
5288
|
+
cwd: snapshotCwd,
|
|
5289
|
+
exit_code: code,
|
|
5290
|
+
signal,
|
|
5291
|
+
stdout,
|
|
5292
|
+
stderr,
|
|
5293
|
+
started_at: started_at.toISOString(),
|
|
5294
|
+
ended_at: ended_at.toISOString(),
|
|
5295
|
+
duration_ms: ended_at.getTime() - started_at.getTime(),
|
|
5296
|
+
pid: child.pid ?? null
|
|
5297
|
+
});
|
|
5298
|
+
});
|
|
5299
|
+
});
|
|
5300
|
+
}
|
|
5301
|
+
};
|
|
5302
|
+
function validateOptions(options) {
|
|
5303
|
+
if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
|
|
5304
|
+
throw new Error("Invalid timeout_ms");
|
|
5305
|
+
}
|
|
5306
|
+
if (options.capture === "none" && options.stdin !== void 0) {
|
|
5307
|
+
throw new Error('Combination of capture: "none" and stdin is not supported');
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
function classifySpawnError(error) {
|
|
5311
|
+
if (findErrorCode(error, "ENOENT")) {
|
|
5312
|
+
return new Error("Command not found", { cause: error });
|
|
5313
|
+
}
|
|
5314
|
+
return new Error("Failed to spawn child process", { cause: error });
|
|
5315
|
+
}
|
|
5316
|
+
|
|
5317
|
+
// src/schemas/json-schema.ts
|
|
5318
|
+
import { z as z11 } from "zod";
|
|
5319
|
+
|
|
5320
|
+
// src/schemas/session-import.schema.ts
|
|
5321
|
+
import { z as z10 } from "zod";
|
|
5322
|
+
var SessionInnerImportSchema = z10.object({
|
|
5323
|
+
id: SessionIdSchema.optional(),
|
|
5324
|
+
label: z10.string().optional(),
|
|
5325
|
+
task_id: TaskIdSchema.nullable().optional(),
|
|
5326
|
+
workspace_id: WorkspaceIdSchema,
|
|
5327
|
+
source: z10.object({
|
|
5328
|
+
kind: SessionSourceKindSchema,
|
|
5329
|
+
version: z10.literal("0.1.0"),
|
|
5330
|
+
// Source-tool-native id (e.g. Claude Code session UUID), retained so
|
|
5331
|
+
// re-imports of the same source can be deduplicated.
|
|
5332
|
+
external_id: z10.string().optional(),
|
|
5333
|
+
// Byte size of the source native log at import time. Declared here too
|
|
5334
|
+
// (not only in session.schema.ts) because this inner `source` object is
|
|
5335
|
+
// a plain z.object: zod strips keys it does not declare, so a field
|
|
5336
|
+
// absent here would be dropped from the parsed payload before persist
|
|
5337
|
+
// and the size could never be stored.
|
|
5338
|
+
source_size_bytes: z10.number().int().nonnegative().optional()
|
|
5339
|
+
}),
|
|
5340
|
+
started_at: IsoTimestampSchema,
|
|
5341
|
+
ended_at: IsoTimestampSchema.optional(),
|
|
5342
|
+
status: SessionStatusSchema,
|
|
5343
|
+
working_directory: z10.string().min(1),
|
|
5344
|
+
invocation: z10.object({
|
|
5345
|
+
command: z10.string().min(1),
|
|
5346
|
+
args: z10.array(z10.string()),
|
|
5347
|
+
exit_code: z10.number().int().nullable()
|
|
5348
|
+
}),
|
|
5349
|
+
related_files: z10.array(z10.string()).default([]),
|
|
5350
|
+
events_log: z10.string().optional(),
|
|
5351
|
+
summary: z10.string().nullable().optional(),
|
|
5352
|
+
metrics: SessionMetricsSchema.optional(),
|
|
5353
|
+
// Accepted so a payload assembled from an on-disk chained session.yaml
|
|
5354
|
+
// round-trips, and DISCARDED by the importer (buildSessionRecord never
|
|
5355
|
+
// copies it): the integrity anchor is computed at write time, never
|
|
5356
|
+
// imported. Mirrors the accept-and-discard of `prev_hash` on events.
|
|
5357
|
+
integrity: SessionIntegritySchema.optional()
|
|
5358
|
+
}).strict();
|
|
5359
|
+
var SessionImportPayloadSchema = z10.object({
|
|
5360
|
+
schema_version: z10.string(),
|
|
5361
|
+
session: SessionInnerImportSchema,
|
|
5362
|
+
events: z10.array(EventSchema)
|
|
5363
|
+
}).strict();
|
|
5364
|
+
|
|
5365
|
+
// src/schemas/json-schema.ts
|
|
5366
|
+
var JSON_SCHEMA_VERSION = "0.1.0";
|
|
5367
|
+
var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
|
|
5368
|
+
var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
5369
|
+
var DOCUMENTS = [
|
|
5370
|
+
{
|
|
5371
|
+
name: "manifest",
|
|
5372
|
+
schema: ManifestSchema,
|
|
5373
|
+
title: "Basou Manifest",
|
|
5374
|
+
description: "The `.basou/manifest.yaml` workspace manifest."
|
|
5375
|
+
},
|
|
5376
|
+
{
|
|
5377
|
+
name: "session",
|
|
5378
|
+
schema: SessionSchema,
|
|
5379
|
+
title: "Basou Session",
|
|
5380
|
+
description: "A `.basou/sessions/<id>/session.yaml` session record."
|
|
5381
|
+
},
|
|
5382
|
+
{
|
|
5383
|
+
name: "event",
|
|
5384
|
+
schema: EventSchema,
|
|
5385
|
+
title: "Basou Event",
|
|
5386
|
+
description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
|
|
5387
|
+
},
|
|
5388
|
+
{
|
|
5389
|
+
name: "task",
|
|
5390
|
+
schema: TaskSchema,
|
|
5391
|
+
title: "Basou Task",
|
|
5392
|
+
description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
|
|
5393
|
+
},
|
|
5394
|
+
{
|
|
5395
|
+
name: "approval",
|
|
5396
|
+
schema: ApprovalSchema,
|
|
5397
|
+
title: "Basou Approval",
|
|
5398
|
+
description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
|
|
5399
|
+
},
|
|
5400
|
+
{
|
|
5401
|
+
name: "status",
|
|
5402
|
+
schema: StatusSchema,
|
|
5403
|
+
title: "Basou Status",
|
|
5404
|
+
description: "The `.basou/status.json` workspace status snapshot."
|
|
5405
|
+
},
|
|
5406
|
+
{
|
|
5407
|
+
name: "task-index",
|
|
5408
|
+
schema: TaskIndexSchema,
|
|
5409
|
+
title: "Basou Task Index",
|
|
5410
|
+
description: "The `.basou/tasks/index.json` task lookup index."
|
|
5411
|
+
},
|
|
5412
|
+
{
|
|
5413
|
+
name: "session-import",
|
|
5414
|
+
schema: SessionImportPayloadSchema,
|
|
5415
|
+
title: "Basou Session Import Payload",
|
|
5416
|
+
description: "The portable session payload consumed by `basou session import`."
|
|
5417
|
+
}
|
|
5418
|
+
];
|
|
5419
|
+
function buildJsonSchemas() {
|
|
5420
|
+
return DOCUMENTS.map((doc) => {
|
|
5421
|
+
const generated = z11.toJSONSchema(doc.schema, { io: "input" });
|
|
5422
|
+
const { $schema, ...rest } = generated;
|
|
5423
|
+
const schema = {
|
|
5424
|
+
$schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
|
|
5425
|
+
$id: `${ID_BASE}/${doc.name}.schema.json`,
|
|
5426
|
+
title: doc.title,
|
|
5427
|
+
description: doc.description,
|
|
5428
|
+
...rest
|
|
5429
|
+
};
|
|
5430
|
+
return { name: doc.name, schema };
|
|
5431
|
+
});
|
|
5432
|
+
}
|
|
5433
|
+
function serializeJsonSchema(schema) {
|
|
5434
|
+
return `${JSON.stringify(schema, null, 2)}
|
|
5435
|
+
`;
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
// src/storage/basou-dir.ts
|
|
5439
|
+
import { lstat as lstat4, mkdir as mkdir4 } from "fs/promises";
|
|
5440
|
+
import { join as join17 } from "path";
|
|
5441
|
+
function basouPaths(repositoryRoot) {
|
|
5442
|
+
const root = join17(repositoryRoot, ".basou");
|
|
5443
|
+
const approvalsBase = join17(root, "approvals");
|
|
5444
|
+
return {
|
|
5445
|
+
root,
|
|
5446
|
+
sessions: join17(root, "sessions"),
|
|
5447
|
+
tasks: join17(root, "tasks"),
|
|
5448
|
+
approvals: {
|
|
5449
|
+
pending: join17(approvalsBase, "pending"),
|
|
5450
|
+
resolved: join17(approvalsBase, "resolved")
|
|
5451
|
+
},
|
|
5452
|
+
locks: join17(root, "locks"),
|
|
5453
|
+
logs: join17(root, "logs"),
|
|
5454
|
+
raw: join17(root, "raw"),
|
|
5455
|
+
tmp: join17(root, "tmp"),
|
|
5456
|
+
files: {
|
|
5457
|
+
manifest: join17(root, "manifest.yaml"),
|
|
5458
|
+
status: join17(root, "status.json"),
|
|
5459
|
+
handoff: join17(root, "handoff.md"),
|
|
5460
|
+
decisions: join17(root, "decisions.md"),
|
|
5461
|
+
orientation: join17(root, "orientation.md")
|
|
5462
|
+
}
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
var PATH_LABELS = {
|
|
5466
|
+
sessions: ".basou/sessions",
|
|
5467
|
+
tasks: ".basou/tasks",
|
|
5468
|
+
approvalsPending: ".basou/approvals/pending",
|
|
5469
|
+
approvalsResolved: ".basou/approvals/resolved",
|
|
5470
|
+
locks: ".basou/locks",
|
|
5471
|
+
logs: ".basou/logs",
|
|
5472
|
+
raw: ".basou/raw",
|
|
5473
|
+
tmp: ".basou/tmp"
|
|
5474
|
+
};
|
|
5475
|
+
async function ensureBasouDirectory(repositoryRoot) {
|
|
5476
|
+
const paths = basouPaths(repositoryRoot);
|
|
5477
|
+
let existing;
|
|
5478
|
+
try {
|
|
5479
|
+
existing = await lstat4(paths.root);
|
|
5480
|
+
} catch (error) {
|
|
5481
|
+
if (!hasErrorCode4(error) || error.code !== "ENOENT") {
|
|
5482
|
+
throw new Error("Failed to inspect .basou directory", { cause: error });
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
if (existing !== void 0 && !existing.isDirectory()) {
|
|
5486
|
+
throw new Error("Basou root .basou exists but is not a directory");
|
|
5487
|
+
}
|
|
5488
|
+
await Promise.all([
|
|
5489
|
+
mkdirLabeled(paths.sessions, PATH_LABELS.sessions),
|
|
5490
|
+
mkdirLabeled(paths.tasks, PATH_LABELS.tasks),
|
|
5491
|
+
mkdirLabeled(paths.approvals.pending, PATH_LABELS.approvalsPending),
|
|
5492
|
+
mkdirLabeled(paths.approvals.resolved, PATH_LABELS.approvalsResolved),
|
|
5493
|
+
mkdirLabeled(paths.locks, PATH_LABELS.locks),
|
|
4718
5494
|
mkdirLabeled(paths.logs, PATH_LABELS.logs),
|
|
4719
5495
|
mkdirLabeled(paths.raw, PATH_LABELS.raw),
|
|
4720
5496
|
mkdirLabeled(paths.tmp, PATH_LABELS.tmp)
|
|
@@ -4725,13 +5501,13 @@ async function mkdirLabeled(target, label) {
|
|
|
4725
5501
|
try {
|
|
4726
5502
|
await mkdir4(target, { recursive: true });
|
|
4727
5503
|
} catch (error) {
|
|
4728
|
-
if (
|
|
5504
|
+
if (hasErrorCode4(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
|
|
4729
5505
|
throw new Error(`${label} exists but is not a directory`, { cause: error });
|
|
4730
5506
|
}
|
|
4731
5507
|
throw new Error(`Failed to create ${label}`, { cause: error });
|
|
4732
5508
|
}
|
|
4733
5509
|
}
|
|
4734
|
-
function
|
|
5510
|
+
function hasErrorCode4(error) {
|
|
4735
5511
|
if (!(error instanceof Error)) return false;
|
|
4736
5512
|
const codeProp = error.code;
|
|
4737
5513
|
return typeof codeProp === "string";
|
|
@@ -4739,28 +5515,30 @@ function hasErrorCode3(error) {
|
|
|
4739
5515
|
|
|
4740
5516
|
// src/storage/gitignore.ts
|
|
4741
5517
|
import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
|
|
4742
|
-
import { join as
|
|
5518
|
+
import { join as join18 } from "path";
|
|
4743
5519
|
var MARKER = "# Basou - default ignore";
|
|
4744
|
-
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/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";
|
|
4745
|
-
|
|
4746
|
-
|
|
5520
|
+
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";
|
|
5521
|
+
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";
|
|
5522
|
+
async function appendBasouGitignore(repositoryRoot, options = {}) {
|
|
5523
|
+
const gitignorePath = join18(repositoryRoot, ".gitignore");
|
|
4747
5524
|
let body;
|
|
4748
5525
|
let existed;
|
|
4749
5526
|
try {
|
|
4750
5527
|
body = await readFile8(gitignorePath, "utf8");
|
|
4751
5528
|
existed = true;
|
|
4752
5529
|
} catch (error) {
|
|
4753
|
-
if (
|
|
5530
|
+
if (hasErrorCode5(error) && error.code === "ENOENT") {
|
|
4754
5531
|
body = "";
|
|
4755
5532
|
existed = false;
|
|
4756
5533
|
} else {
|
|
4757
5534
|
throw new Error("Failed to read .gitignore", { cause: error });
|
|
4758
5535
|
}
|
|
4759
5536
|
}
|
|
4760
|
-
if (existed &&
|
|
5537
|
+
if (existed && hasBasouGitignore(body)) {
|
|
4761
5538
|
return { appended: false };
|
|
4762
5539
|
}
|
|
4763
|
-
const
|
|
5540
|
+
const block = options.localOnly === true ? BASOU_GITIGNORE_BLOCK_LOCAL_ONLY : BASOU_GITIGNORE_BLOCK;
|
|
5541
|
+
const next = composeNextBody(body, block);
|
|
4764
5542
|
try {
|
|
4765
5543
|
await writeFile2(gitignorePath, next, { encoding: "utf8" });
|
|
4766
5544
|
} catch (error) {
|
|
@@ -4768,84 +5546,20 @@ async function appendBasouGitignore(repositoryRoot) {
|
|
|
4768
5546
|
}
|
|
4769
5547
|
return { appended: true };
|
|
4770
5548
|
}
|
|
4771
|
-
function
|
|
5549
|
+
function hasBasouGitignore(body) {
|
|
4772
5550
|
for (const rawLine of body.split("\n")) {
|
|
4773
|
-
|
|
5551
|
+
const line = rawLine.trimEnd();
|
|
5552
|
+
if (line.startsWith(MARKER)) return true;
|
|
5553
|
+
if (line === ".basou/" || line === "/.basou/") return true;
|
|
4774
5554
|
}
|
|
4775
5555
|
return false;
|
|
4776
5556
|
}
|
|
4777
|
-
function composeNextBody(existing) {
|
|
4778
|
-
if (existing.length === 0) return
|
|
5557
|
+
function composeNextBody(existing, block) {
|
|
5558
|
+
if (existing.length === 0) return block;
|
|
4779
5559
|
const normalized = existing.endsWith("\n") ? existing : `${existing}
|
|
4780
5560
|
`;
|
|
4781
5561
|
return `${normalized}
|
|
4782
|
-
${
|
|
4783
|
-
}
|
|
4784
|
-
function hasErrorCode4(error) {
|
|
4785
|
-
if (!(error instanceof Error)) return false;
|
|
4786
|
-
return typeof error.code === "string";
|
|
4787
|
-
}
|
|
4788
|
-
|
|
4789
|
-
// src/storage/manifest.ts
|
|
4790
|
-
import { lstat as lstat4 } from "fs/promises";
|
|
4791
|
-
function createManifest(input) {
|
|
4792
|
-
if (input.workspaceName.length === 0) {
|
|
4793
|
-
throw new Error("Workspace name is empty. Pass --name explicitly.");
|
|
4794
|
-
}
|
|
4795
|
-
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4796
|
-
const workspaceId = input.workspaceId ?? prefixedUlid("ws");
|
|
4797
|
-
const project = {
|
|
4798
|
-
...input.projectName !== void 0 ? { name: input.projectName } : {},
|
|
4799
|
-
...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
|
|
4800
|
-
...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
|
|
4801
|
-
};
|
|
4802
|
-
const manifest = {
|
|
4803
|
-
schema_version: "0.1.0",
|
|
4804
|
-
basou_version: "0.1.0",
|
|
4805
|
-
workspace: {
|
|
4806
|
-
id: workspaceId,
|
|
4807
|
-
name: input.workspaceName,
|
|
4808
|
-
created_at: now,
|
|
4809
|
-
updated_at: now
|
|
4810
|
-
},
|
|
4811
|
-
project,
|
|
4812
|
-
capabilities: {
|
|
4813
|
-
enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
|
|
4814
|
-
},
|
|
4815
|
-
approval: {
|
|
4816
|
-
required_for: ["destructive_command", "external_send"],
|
|
4817
|
-
default_risk_level: "medium"
|
|
4818
|
-
},
|
|
4819
|
-
adapters: {
|
|
4820
|
-
"claude-code": { enabled: true }
|
|
4821
|
-
},
|
|
4822
|
-
git: { events_log: "ignore" },
|
|
4823
|
-
...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
|
|
4824
|
-
};
|
|
4825
|
-
return ManifestSchema.parse(manifest);
|
|
4826
|
-
}
|
|
4827
|
-
async function writeManifest(paths, manifest, options) {
|
|
4828
|
-
const force = options?.force === true;
|
|
4829
|
-
const validated = ManifestSchema.parse(manifest);
|
|
4830
|
-
if (!force) {
|
|
4831
|
-
let existed = false;
|
|
4832
|
-
try {
|
|
4833
|
-
await lstat4(paths.files.manifest);
|
|
4834
|
-
existed = true;
|
|
4835
|
-
} catch (error) {
|
|
4836
|
-
if (!hasErrorCode5(error) || error.code !== "ENOENT") {
|
|
4837
|
-
throw new Error("Failed to inspect existing manifest", { cause: error });
|
|
4838
|
-
}
|
|
4839
|
-
}
|
|
4840
|
-
if (existed) {
|
|
4841
|
-
throw new Error("Already initialized. Use --force to overwrite.");
|
|
4842
|
-
}
|
|
4843
|
-
}
|
|
4844
|
-
await writeYamlFile(paths.files.manifest, validated);
|
|
4845
|
-
}
|
|
4846
|
-
async function readManifest(paths) {
|
|
4847
|
-
const raw = await readYamlFile(paths.files.manifest);
|
|
4848
|
-
return ManifestSchema.parse(raw);
|
|
5562
|
+
${block}`;
|
|
4849
5563
|
}
|
|
4850
5564
|
function hasErrorCode5(error) {
|
|
4851
5565
|
if (!(error instanceof Error)) return false;
|
|
@@ -4958,7 +5672,7 @@ function hasErrorCode6(error) {
|
|
|
4958
5672
|
// src/storage/session-import.ts
|
|
4959
5673
|
import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
|
|
4960
5674
|
import { homedir as homedir2 } from "os";
|
|
4961
|
-
import { join as
|
|
5675
|
+
import { join as join19 } from "path";
|
|
4962
5676
|
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
4963
5677
|
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
4964
5678
|
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
@@ -4983,7 +5697,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
4983
5697
|
pathSanitizeReport
|
|
4984
5698
|
};
|
|
4985
5699
|
}
|
|
4986
|
-
const sessionDir =
|
|
5700
|
+
const sessionDir = join19(paths.sessions, newSessionId);
|
|
4987
5701
|
try {
|
|
4988
5702
|
await mkdir5(sessionDir, { recursive: true });
|
|
4989
5703
|
} catch (error) {
|
|
@@ -4997,7 +5711,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
4997
5711
|
throw error;
|
|
4998
5712
|
}
|
|
4999
5713
|
try {
|
|
5000
|
-
const sessionYamlPath =
|
|
5714
|
+
const sessionYamlPath = join19(sessionDir, "session.yaml");
|
|
5001
5715
|
await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
|
|
5002
5716
|
} catch (error) {
|
|
5003
5717
|
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
@@ -5165,7 +5879,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
|
|
|
5165
5879
|
async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
|
|
5166
5880
|
const sessionId = priorSessionId;
|
|
5167
5881
|
const importSource = freshPayload.session.source.kind;
|
|
5168
|
-
const sessionDir =
|
|
5882
|
+
const sessionDir = join19(paths.sessions, priorSessionId);
|
|
5169
5883
|
const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
|
|
5170
5884
|
try {
|
|
5171
5885
|
const priorVerdict = await verifyEventsChain(paths, priorSessionId);
|
|
@@ -5207,7 +5921,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5207
5921
|
};
|
|
5208
5922
|
const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
|
|
5209
5923
|
if (options.dryRun !== true) {
|
|
5210
|
-
const eventsPath =
|
|
5924
|
+
const eventsPath = join19(sessionDir, "events.jsonl");
|
|
5211
5925
|
let priorEventsRaw = null;
|
|
5212
5926
|
try {
|
|
5213
5927
|
priorEventsRaw = await readFile10(eventsPath);
|
|
@@ -5219,7 +5933,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5219
5933
|
const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
|
|
5220
5934
|
try {
|
|
5221
5935
|
await overwriteYamlFile(
|
|
5222
|
-
|
|
5936
|
+
join19(sessionDir, "session.yaml"),
|
|
5223
5937
|
withIntegrity(updatedRecord, chainResult)
|
|
5224
5938
|
);
|
|
5225
5939
|
} catch (error) {
|
|
@@ -5243,7 +5957,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5243
5957
|
}
|
|
5244
5958
|
}
|
|
5245
5959
|
async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
5246
|
-
const sessionDir =
|
|
5960
|
+
const sessionDir = join19(paths.sessions, sessionId);
|
|
5247
5961
|
let lock;
|
|
5248
5962
|
try {
|
|
5249
5963
|
lock = await acquireLock(paths, "session", sessionId);
|
|
@@ -5276,7 +5990,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
5276
5990
|
if (verdict.status !== "unchained") {
|
|
5277
5991
|
return { status: "skipped", reason: "tampered" };
|
|
5278
5992
|
}
|
|
5279
|
-
const eventsPath =
|
|
5993
|
+
const eventsPath = join19(sessionDir, "events.jsonl");
|
|
5280
5994
|
let priorRaw;
|
|
5281
5995
|
try {
|
|
5282
5996
|
priorRaw = await readFile10(eventsPath);
|
|
@@ -5324,7 +6038,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
5324
6038
|
}
|
|
5325
6039
|
try {
|
|
5326
6040
|
await overwriteYamlFile(
|
|
5327
|
-
|
|
6041
|
+
join19(sessionDir, "session.yaml"),
|
|
5328
6042
|
withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
|
|
5329
6043
|
);
|
|
5330
6044
|
} catch (error) {
|
|
@@ -5406,6 +6120,7 @@ export {
|
|
|
5406
6120
|
enumerateTaskIds,
|
|
5407
6121
|
finalizeSessionYaml,
|
|
5408
6122
|
findErrorCode,
|
|
6123
|
+
formatDurationMs,
|
|
5409
6124
|
genesisHash,
|
|
5410
6125
|
getDiff,
|
|
5411
6126
|
getSnapshot,
|
|
@@ -5438,8 +6153,11 @@ export {
|
|
|
5438
6153
|
reimportPreservingId,
|
|
5439
6154
|
renderDecisions,
|
|
5440
6155
|
renderHandoff,
|
|
6156
|
+
renderOrientation,
|
|
6157
|
+
renderReport,
|
|
5441
6158
|
renderWithMarkers,
|
|
5442
6159
|
replayEvents,
|
|
6160
|
+
resolveBasouRepositoryRoot,
|
|
5443
6161
|
resolveClaudeCodeCommand,
|
|
5444
6162
|
resolveRepositoryRoot,
|
|
5445
6163
|
resolveSessionId,
|
|
@@ -5451,6 +6169,7 @@ export {
|
|
|
5451
6169
|
serializeJsonSchema,
|
|
5452
6170
|
sessionWorkStatsFromEvents,
|
|
5453
6171
|
summarizeAdapterOutput,
|
|
6172
|
+
summarizeOrientation,
|
|
5454
6173
|
tryRemoteUrl,
|
|
5455
6174
|
ulid,
|
|
5456
6175
|
updateTaskStatusWithEvent,
|