@basou/core 0.11.0 → 0.13.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 +1292 -15
- package/dist/index.js +1569 -266
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/event.schema.json +7 -0
- package/schemas/manifest.schema.json +92 -9
- package/schemas/session-import.schema.json +7 -0
package/dist/index.js
CHANGED
|
@@ -102,6 +102,14 @@ function sumDurations(intervals) {
|
|
|
102
102
|
return total;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// src/adapters/session-label.ts
|
|
106
|
+
function sessionLabelDateSpan(startIso, endIso) {
|
|
107
|
+
const a = startIso.slice(0, 10);
|
|
108
|
+
const b = endIso.slice(0, 10);
|
|
109
|
+
if (a === b) return a;
|
|
110
|
+
return a < b ? `${a}..${b}` : `${b}..${a}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
105
113
|
// src/adapters/claude-code/transcript-importer.ts
|
|
106
114
|
var CLAUDE_IMPORT_SOURCE = "claude-code-import";
|
|
107
115
|
function claudeTranscriptToImportPayload(records, options) {
|
|
@@ -201,8 +209,7 @@ function claudeTranscriptToImportPayload(records, options) {
|
|
|
201
209
|
const externalId = options.externalId ?? claudeSessionId;
|
|
202
210
|
const commandCount = derived.reduce((n, e) => e.type === "command_executed" ? n + 1 : n, 0);
|
|
203
211
|
const fileCount = relatedFiles.size;
|
|
204
|
-
const
|
|
205
|
-
const label = `claude-code ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
|
|
212
|
+
const label = `claude-code ${sessionLabelDateSpan(minTs, maxTs)}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
|
|
206
213
|
const active = engagementTsMs.length >= 2 ? activeTimeFromTimestamps(engagementTsMs, ACTIVE_GAP_CAP_MS) : void 0;
|
|
207
214
|
const metricsFields = {
|
|
208
215
|
...outputTokens > 0 ? { output_tokens: outputTokens } : {},
|
|
@@ -414,8 +421,7 @@ function codexRolloutToImportPayload(records, options) {
|
|
|
414
421
|
];
|
|
415
422
|
const externalId = options.externalId ?? codexSessionId;
|
|
416
423
|
const commandCount = derived.length;
|
|
417
|
-
const
|
|
418
|
-
const label = `codex ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
|
|
424
|
+
const label = `codex ${sessionLabelDateSpan(minTs, maxTs)}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
|
|
419
425
|
const minTsMs = Date.parse(minTs);
|
|
420
426
|
const turnIntervals = [];
|
|
421
427
|
let machineActiveMs = 0;
|
|
@@ -905,7 +911,12 @@ var TaskArchivedEventSchema = BaseEventSchema.extend({
|
|
|
905
911
|
}).strict();
|
|
906
912
|
var NoteAddedEventSchema = BaseEventSchema.extend({
|
|
907
913
|
type: z3.literal("note_added"),
|
|
908
|
-
body: z3.string()
|
|
914
|
+
body: z3.string(),
|
|
915
|
+
// `next_step` marks a note authored by `basou note` as the operator's resume
|
|
916
|
+
// hint, which orientation surfaces as the next starting point. Absent (the
|
|
917
|
+
// `basou session note` default) is a plain annotation orientation does not
|
|
918
|
+
// surface. Optional so pre-existing note_added events remain valid.
|
|
919
|
+
kind: z3.enum(["note", "next_step"]).optional()
|
|
909
920
|
});
|
|
910
921
|
var AdapterOutputEventSchema = BaseEventSchema.extend({
|
|
911
922
|
type: z3.literal("adapter_output"),
|
|
@@ -1720,6 +1731,8 @@ function splitLinesBytes2(buf) {
|
|
|
1720
1731
|
}
|
|
1721
1732
|
|
|
1722
1733
|
// src/git/snapshot.ts
|
|
1734
|
+
import { readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
1735
|
+
import { join as join9 } from "path";
|
|
1723
1736
|
import { simpleGit } from "simple-git";
|
|
1724
1737
|
|
|
1725
1738
|
// src/storage/status.ts
|
|
@@ -1757,19 +1770,19 @@ var DIRECTORY_CHECKS = {
|
|
|
1757
1770
|
tmp: (p) => p.tmp
|
|
1758
1771
|
};
|
|
1759
1772
|
async function assertBasouRootSafe(rootPath) {
|
|
1760
|
-
let
|
|
1773
|
+
let stat4;
|
|
1761
1774
|
try {
|
|
1762
|
-
|
|
1775
|
+
stat4 = await fsp.lstat(rootPath);
|
|
1763
1776
|
} catch (error) {
|
|
1764
1777
|
if (hasErrorCode2(error) && error.code === "ENOENT") {
|
|
1765
1778
|
throw new Error("Basou workspace not found", { cause: error });
|
|
1766
1779
|
}
|
|
1767
1780
|
throw new Error("Failed to inspect .basou root", { cause: error });
|
|
1768
1781
|
}
|
|
1769
|
-
if (
|
|
1782
|
+
if (stat4.isSymbolicLink()) {
|
|
1770
1783
|
throw new Error(".basou root is a symlink; refusing to operate");
|
|
1771
1784
|
}
|
|
1772
|
-
if (!
|
|
1785
|
+
if (!stat4.isDirectory()) {
|
|
1773
1786
|
throw new Error(".basou root exists but is not a directory");
|
|
1774
1787
|
}
|
|
1775
1788
|
}
|
|
@@ -1849,6 +1862,14 @@ function isGitNotFound(error) {
|
|
|
1849
1862
|
}
|
|
1850
1863
|
return false;
|
|
1851
1864
|
}
|
|
1865
|
+
function isNotAGitRepository(error) {
|
|
1866
|
+
let cur = error;
|
|
1867
|
+
for (let i = 0; i < 4 && cur instanceof Error; i++) {
|
|
1868
|
+
if (/not a git repository/i.test(cur.message)) return true;
|
|
1869
|
+
cur = cur.cause;
|
|
1870
|
+
}
|
|
1871
|
+
return false;
|
|
1872
|
+
}
|
|
1852
1873
|
async function resolveRepositoryRoot(cwd) {
|
|
1853
1874
|
const git = safeSimpleGit(cwd);
|
|
1854
1875
|
try {
|
|
@@ -1864,8 +1885,53 @@ async function resolveRepositoryRoot(cwd) {
|
|
|
1864
1885
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
1865
1886
|
throw error;
|
|
1866
1887
|
}
|
|
1867
|
-
|
|
1888
|
+
if (isNotAGitRepository(error)) {
|
|
1889
|
+
throw new Error("Not a git repository", { cause: error });
|
|
1890
|
+
}
|
|
1891
|
+
throw new Error("Git command failed", { cause: error });
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
async function resolveBasouRepositoryRoot(cwd, opts) {
|
|
1895
|
+
try {
|
|
1896
|
+
return await resolveRepositoryRoot(cwd);
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
if (!(error instanceof Error) || error.message !== "Not a git repository") throw error;
|
|
1899
|
+
const linked = await findLinkedBasouRepos(cwd);
|
|
1900
|
+
const only = linked[0];
|
|
1901
|
+
if (only !== void 0 && linked.length === 1) {
|
|
1902
|
+
opts?.onRedirect?.({ via: only.name, root: only.root });
|
|
1903
|
+
return only.root;
|
|
1904
|
+
}
|
|
1905
|
+
if (linked.length > 1) {
|
|
1906
|
+
const names = linked.map((l) => l.name).join(", ");
|
|
1907
|
+
throw new Error(
|
|
1908
|
+
`Ambiguous workspace view: ${linked.length} linked repos have a .basou store (${names}). cd into the one you want and re-run.`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
throw error;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
async function findLinkedBasouRepos(dir) {
|
|
1915
|
+
const entries = await readdir3(dir, { withFileTypes: true }).catch(() => null);
|
|
1916
|
+
if (entries === null) return [];
|
|
1917
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
1918
|
+
for (const entry of entries) {
|
|
1919
|
+
if (!entry.isSymbolicLink()) continue;
|
|
1920
|
+
let root;
|
|
1921
|
+
try {
|
|
1922
|
+
root = await resolveRepositoryRoot(join9(dir, entry.name));
|
|
1923
|
+
} catch {
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
try {
|
|
1927
|
+
if (!(await stat2(join9(root, ".basou"))).isDirectory()) continue;
|
|
1928
|
+
} catch {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const existing = byRoot.get(root);
|
|
1932
|
+
if (existing === void 0 || entry.name < existing) byRoot.set(root, entry.name);
|
|
1868
1933
|
}
|
|
1934
|
+
return [...byRoot.entries()].map(([root, name]) => ({ name, root })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1869
1935
|
}
|
|
1870
1936
|
async function tryRemoteUrl(repositoryRoot) {
|
|
1871
1937
|
const git = safeSimpleGit(repositoryRoot);
|
|
@@ -2013,12 +2079,12 @@ function parseDiffNameStatus(raw) {
|
|
|
2013
2079
|
}
|
|
2014
2080
|
|
|
2015
2081
|
// src/handoff/handoff-renderer.ts
|
|
2016
|
-
import { join as
|
|
2082
|
+
import { join as join13 } from "path";
|
|
2017
2083
|
|
|
2018
2084
|
// src/storage/tasks.ts
|
|
2019
2085
|
import { createHash as createHash2 } from "crypto";
|
|
2020
|
-
import { mkdir as mkdir3, readdir as
|
|
2021
|
-
import { join as
|
|
2086
|
+
import { mkdir as mkdir3, readdir as readdir4, readFile as readFile7, rename as rename2, stat as stat3, unlink as unlink3 } from "fs/promises";
|
|
2087
|
+
import { join as join12 } from "path";
|
|
2022
2088
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2023
2089
|
import { z as z8 } from "zod";
|
|
2024
2090
|
|
|
@@ -2062,7 +2128,7 @@ var TaskSchema = z6.object({
|
|
|
2062
2128
|
// src/storage/ad-hoc-session.ts
|
|
2063
2129
|
import { mkdir as mkdir2, rm } from "fs/promises";
|
|
2064
2130
|
import { homedir } from "os";
|
|
2065
|
-
import { join as
|
|
2131
|
+
import { join as join10 } from "path";
|
|
2066
2132
|
|
|
2067
2133
|
// src/lib/path-sanitizer.ts
|
|
2068
2134
|
import { posix as path } from "path";
|
|
@@ -2142,8 +2208,8 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
2142
2208
|
taskId: input.taskId ?? null
|
|
2143
2209
|
})
|
|
2144
2210
|
);
|
|
2145
|
-
const sessionDir =
|
|
2146
|
-
const sessionYamlPath =
|
|
2211
|
+
const sessionDir = join10(input.paths.sessions, sessionId);
|
|
2212
|
+
const sessionYamlPath = join10(sessionDir, "session.yaml");
|
|
2147
2213
|
const lock = await acquireLock(input.paths, "session", sessionId);
|
|
2148
2214
|
let bulkResult = null;
|
|
2149
2215
|
try {
|
|
@@ -2293,7 +2359,7 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
|
|
|
2293
2359
|
|
|
2294
2360
|
// src/storage/task-index.ts
|
|
2295
2361
|
import { readFile as readFile6 } from "fs/promises";
|
|
2296
|
-
import { join as
|
|
2362
|
+
import { join as join11 } from "path";
|
|
2297
2363
|
|
|
2298
2364
|
// src/schemas/task-index.schema.ts
|
|
2299
2365
|
import { z as z7 } from "zod";
|
|
@@ -2312,7 +2378,7 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
|
|
|
2312
2378
|
|
|
2313
2379
|
// src/storage/task-index.ts
|
|
2314
2380
|
function taskIndexPath(paths) {
|
|
2315
|
-
return
|
|
2381
|
+
return join11(paths.tasks, "index.json");
|
|
2316
2382
|
}
|
|
2317
2383
|
async function readTaskIndex(paths) {
|
|
2318
2384
|
const filePath = taskIndexPath(paths);
|
|
@@ -2426,7 +2492,7 @@ function splitFrontMatter(raw) {
|
|
|
2426
2492
|
return { yamlText, body };
|
|
2427
2493
|
}
|
|
2428
2494
|
async function readTaskFile(paths, taskId) {
|
|
2429
|
-
const filePath =
|
|
2495
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
2430
2496
|
let raw;
|
|
2431
2497
|
try {
|
|
2432
2498
|
raw = await readFile7(filePath, "utf8");
|
|
@@ -2459,7 +2525,7 @@ async function readTaskFile(paths, taskId) {
|
|
|
2459
2525
|
}
|
|
2460
2526
|
async function writeTaskFile(paths, taskId, doc, options) {
|
|
2461
2527
|
const validated = TaskSchema.parse(doc.task);
|
|
2462
|
-
const filePath =
|
|
2528
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
2463
2529
|
const yamlText = stringifyYaml(validated);
|
|
2464
2530
|
const trimmedBody = doc.body.length === 0 ? "" : `
|
|
2465
2531
|
${doc.body.endsWith("\n") ? doc.body : `${doc.body}
|
|
@@ -2511,7 +2577,7 @@ async function enumerateTaskIds(paths) {
|
|
|
2511
2577
|
async function enumerateTaskIdsFromDisk(paths) {
|
|
2512
2578
|
let entries;
|
|
2513
2579
|
try {
|
|
2514
|
-
entries = (await
|
|
2580
|
+
entries = (await readdir4(paths.tasks, { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
|
|
2515
2581
|
} catch (error) {
|
|
2516
2582
|
if (findErrorCode(error, "ENOENT")) return [];
|
|
2517
2583
|
throw new Error("Failed to enumerate tasks", { cause: error });
|
|
@@ -2544,12 +2610,12 @@ async function safeUpdateTaskIndex(paths, op) {
|
|
|
2544
2610
|
}
|
|
2545
2611
|
var ARCHIVE_DIR_NAME = "archive";
|
|
2546
2612
|
function archiveTasksDir(paths) {
|
|
2547
|
-
return
|
|
2613
|
+
return join12(paths.tasks, ARCHIVE_DIR_NAME);
|
|
2548
2614
|
}
|
|
2549
2615
|
async function enumerateArchivedTaskIds(paths) {
|
|
2550
2616
|
let entries;
|
|
2551
2617
|
try {
|
|
2552
|
-
entries = (await
|
|
2618
|
+
entries = (await readdir4(archiveTasksDir(paths), { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
|
|
2553
2619
|
} catch (error) {
|
|
2554
2620
|
if (findErrorCode(error, "ENOENT")) return [];
|
|
2555
2621
|
throw new Error("Failed to enumerate archived tasks", { cause: error });
|
|
@@ -2574,7 +2640,7 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
|
|
|
2574
2640
|
throw error;
|
|
2575
2641
|
}
|
|
2576
2642
|
}
|
|
2577
|
-
const archiveFilePath =
|
|
2643
|
+
const archiveFilePath = join12(archiveTasksDir(paths), `${taskId}.md`);
|
|
2578
2644
|
let raw;
|
|
2579
2645
|
try {
|
|
2580
2646
|
raw = await readFile7(archiveFilePath, "utf8");
|
|
@@ -2868,7 +2934,7 @@ async function createTaskAttachLocked(input) {
|
|
|
2868
2934
|
...sessionDoc,
|
|
2869
2935
|
session: { ...sessionDoc.session, task_id: input.taskId }
|
|
2870
2936
|
};
|
|
2871
|
-
await overwriteYamlFile(
|
|
2937
|
+
await overwriteYamlFile(join12(input.paths.sessions, input.sessionId, "session.yaml"), updated);
|
|
2872
2938
|
} catch (error) {
|
|
2873
2939
|
throw new TaskWriteAfterEventError({
|
|
2874
2940
|
taskId: input.taskId,
|
|
@@ -3127,17 +3193,17 @@ function buildUpdatedDoc(input) {
|
|
|
3127
3193
|
return { task: next, body: input.currentDoc.body };
|
|
3128
3194
|
}
|
|
3129
3195
|
async function computeTaskMdSnapshot(paths, taskId) {
|
|
3130
|
-
const filePath =
|
|
3131
|
-
const [stats, raw] = await Promise.all([
|
|
3196
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
3197
|
+
const [stats, raw] = await Promise.all([stat3(filePath), readFile7(filePath)]);
|
|
3132
3198
|
const hash = createHash2("sha256").update(raw).digest("hex");
|
|
3133
3199
|
return { mtimeMs: stats.mtimeMs, hash };
|
|
3134
3200
|
}
|
|
3135
3201
|
async function readTaskFileWithSnapshot(paths, taskId) {
|
|
3136
|
-
const filePath =
|
|
3202
|
+
const filePath = join12(paths.tasks, `${taskId}.md`);
|
|
3137
3203
|
let rawBuffer;
|
|
3138
3204
|
let stats;
|
|
3139
3205
|
try {
|
|
3140
|
-
[rawBuffer, stats] = await Promise.all([readFile7(filePath),
|
|
3206
|
+
[rawBuffer, stats] = await Promise.all([readFile7(filePath), stat3(filePath)]);
|
|
3141
3207
|
} catch (error) {
|
|
3142
3208
|
if (findErrorCode(error, "ENOENT")) {
|
|
3143
3209
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -3625,7 +3691,7 @@ async function deleteTaskLocked(input) {
|
|
|
3625
3691
|
});
|
|
3626
3692
|
const eventId = adHoc.targetEventIds[0];
|
|
3627
3693
|
try {
|
|
3628
|
-
await unlink3(
|
|
3694
|
+
await unlink3(join12(input.paths.tasks, `${input.taskId}.md`));
|
|
3629
3695
|
} catch (error) {
|
|
3630
3696
|
throw new TaskWriteAfterEventError({
|
|
3631
3697
|
taskId: input.taskId,
|
|
@@ -3697,8 +3763,8 @@ async function archiveTaskLocked(input) {
|
|
|
3697
3763
|
);
|
|
3698
3764
|
await mkdir3(archiveTasksDir(input.paths), { recursive: true });
|
|
3699
3765
|
await rename2(
|
|
3700
|
-
|
|
3701
|
-
|
|
3766
|
+
join12(input.paths.tasks, `${input.taskId}.md`),
|
|
3767
|
+
join12(archiveTasksDir(input.paths), `${input.taskId}.md`)
|
|
3702
3768
|
);
|
|
3703
3769
|
} catch (error) {
|
|
3704
3770
|
throw new TaskWriteAfterEventError({
|
|
@@ -3734,7 +3800,7 @@ async function renderHandoff(input) {
|
|
|
3734
3800
|
const tasksCreated = [];
|
|
3735
3801
|
const tasksStatusChanged = [];
|
|
3736
3802
|
for (const entry of entries) {
|
|
3737
|
-
const sessionDir =
|
|
3803
|
+
const sessionDir = join13(input.paths.sessions, entry.sessionId);
|
|
3738
3804
|
try {
|
|
3739
3805
|
for await (const ev of replayEvents(sessionDir, {
|
|
3740
3806
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -3850,11 +3916,11 @@ function formatHandoffBody(args) {
|
|
|
3850
3916
|
if (args.latestSession !== void 0) {
|
|
3851
3917
|
const status = args.latestSession.session.session.status;
|
|
3852
3918
|
const label = args.latestSession.session.session.label;
|
|
3853
|
-
const
|
|
3919
|
+
const shortId2 = shortIdWithPrefix(args.latestSession.sessionId);
|
|
3854
3920
|
if (label !== void 0 && label !== "") {
|
|
3855
|
-
lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${
|
|
3921
|
+
lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId2}]`);
|
|
3856
3922
|
} else {
|
|
3857
|
-
lines.push(`- \u6700\u7D42 session: ${
|
|
3923
|
+
lines.push(`- \u6700\u7D42 session: ${shortId2} (${status})`);
|
|
3858
3924
|
}
|
|
3859
3925
|
} else {
|
|
3860
3926
|
lines.push("- \u6700\u7D42 session: (no live sessions)");
|
|
@@ -4091,94 +4157,1194 @@ async function resolveIdInternal(paths, input, kind, options = {}) {
|
|
|
4091
4157
|
return matches[0];
|
|
4092
4158
|
}
|
|
4093
4159
|
|
|
4094
|
-
// src/
|
|
4160
|
+
// src/orientation/orientation-renderer.ts
|
|
4095
4161
|
import { join as join14 } from "path";
|
|
4096
4162
|
|
|
4097
|
-
// src/
|
|
4098
|
-
import {
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4163
|
+
// src/storage/manifest.ts
|
|
4164
|
+
import { lstat as lstat3 } from "fs/promises";
|
|
4165
|
+
|
|
4166
|
+
// src/schemas/manifest.schema.ts
|
|
4167
|
+
import { z as z9 } from "zod";
|
|
4168
|
+
var ProjectSchema = z9.looseObject({
|
|
4169
|
+
name: z9.string().optional(),
|
|
4170
|
+
description: z9.string().optional(),
|
|
4171
|
+
repository_url: z9.string().nullable().optional()
|
|
4172
|
+
});
|
|
4173
|
+
var CapabilitiesSchema = z9.looseObject({
|
|
4174
|
+
enabled: z9.array(z9.string())
|
|
4175
|
+
});
|
|
4176
|
+
var ApprovalConfigSchema = z9.looseObject({
|
|
4177
|
+
required_for: z9.array(z9.string()).optional(),
|
|
4178
|
+
default_risk_level: z9.enum(["low", "medium", "high", "critical"])
|
|
4179
|
+
});
|
|
4180
|
+
var ClaudeCodeAdapterConfigSchema = z9.looseObject({
|
|
4181
|
+
enabled: z9.boolean(),
|
|
4182
|
+
config_path: z9.string().optional()
|
|
4183
|
+
});
|
|
4184
|
+
var AdaptersSchema = z9.looseObject({
|
|
4185
|
+
"claude-code": ClaudeCodeAdapterConfigSchema
|
|
4186
|
+
});
|
|
4187
|
+
var GitConfigSchema = z9.looseObject({
|
|
4188
|
+
events_log: z9.enum(["ignore", "commit"]).default("ignore")
|
|
4189
|
+
});
|
|
4190
|
+
var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)(?!\s)[^\0\\]*[^\0\\\s]$/;
|
|
4191
|
+
var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
|
|
4192
|
+
message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
|
|
4193
|
+
});
|
|
4194
|
+
var ImportConfigSchema = z9.looseObject({
|
|
4195
|
+
source_roots: z9.array(SourceRootSchema).min(1).optional()
|
|
4196
|
+
});
|
|
4197
|
+
var RepoVisibilitySchema = z9.enum(["public", "private", "future-public"]);
|
|
4198
|
+
var RepoLanguageSchema = z9.enum(["en", "ja", "en+ja"]);
|
|
4199
|
+
var PublishKindSchema = z9.enum(["web", "npm"]);
|
|
4200
|
+
var PublishTargetSchema = z9.looseObject({
|
|
4201
|
+
kind: PublishKindSchema,
|
|
4202
|
+
visibility: RepoVisibilitySchema.optional(),
|
|
4203
|
+
language: RepoLanguageSchema.optional()
|
|
4204
|
+
});
|
|
4205
|
+
var RepoEntrySchema = z9.looseObject({
|
|
4206
|
+
path: SourceRootSchema,
|
|
4207
|
+
visibility: RepoVisibilitySchema.optional(),
|
|
4208
|
+
language: RepoLanguageSchema.optional(),
|
|
4209
|
+
publishes: z9.array(PublishTargetSchema).optional()
|
|
4210
|
+
});
|
|
4211
|
+
var WorkspaceMetaSchema = z9.looseObject({
|
|
4212
|
+
id: WorkspaceIdSchema,
|
|
4213
|
+
name: z9.string().min(1),
|
|
4214
|
+
created_at: IsoTimestampSchema,
|
|
4215
|
+
updated_at: IsoTimestampSchema,
|
|
4216
|
+
/**
|
|
4217
|
+
* The generated workspace view: a throwaway directory that aggregates the
|
|
4218
|
+
* roster repos via symlinks (one `<repo-basename>` symlink per repo). A path
|
|
4219
|
+
* relative to the manifest root, reusing the machine-portable source-root
|
|
4220
|
+
* constraint. Absent for a solo project (no view needed); `basou project
|
|
4221
|
+
* workspace` reconciles the view's symlinks to the declared roster.
|
|
4222
|
+
*/
|
|
4223
|
+
view: SourceRootSchema.optional()
|
|
4224
|
+
});
|
|
4225
|
+
var ManifestSchema = z9.looseObject({
|
|
4226
|
+
schema_version: SchemaVersionSchema,
|
|
4227
|
+
basou_version: z9.literal("0.1.0"),
|
|
4228
|
+
workspace: WorkspaceMetaSchema,
|
|
4229
|
+
project: ProjectSchema,
|
|
4230
|
+
capabilities: CapabilitiesSchema,
|
|
4231
|
+
approval: ApprovalConfigSchema,
|
|
4232
|
+
adapters: AdaptersSchema,
|
|
4233
|
+
git: GitConfigSchema,
|
|
4234
|
+
import: ImportConfigSchema.optional(),
|
|
4235
|
+
repos: z9.array(RepoEntrySchema).min(1).optional()
|
|
4236
|
+
});
|
|
4237
|
+
var KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(ManifestSchema.shape));
|
|
4238
|
+
function unknownManifestKeys(manifest) {
|
|
4239
|
+
return Object.keys(manifest).filter((k) => !KNOWN_TOP_LEVEL_KEYS.has(k)).sort();
|
|
4102
4240
|
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
"
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
const timeZone = resolveTimeZone(input.timeZone);
|
|
4116
|
-
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
4117
|
-
const wrappedSkip = (sid, reason) => {
|
|
4118
|
-
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
4119
|
-
input.onSessionSkip?.(sid, reason);
|
|
4241
|
+
|
|
4242
|
+
// src/storage/manifest.ts
|
|
4243
|
+
function createManifest(input) {
|
|
4244
|
+
if (input.workspaceName.length === 0) {
|
|
4245
|
+
throw new Error("Workspace name is empty. Pass --name explicitly.");
|
|
4246
|
+
}
|
|
4247
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4248
|
+
const workspaceId = input.workspaceId ?? prefixedUlid("ws");
|
|
4249
|
+
const project = {
|
|
4250
|
+
...input.projectName !== void 0 ? { name: input.projectName } : {},
|
|
4251
|
+
...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
|
|
4252
|
+
...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
|
|
4120
4253
|
};
|
|
4121
|
-
const
|
|
4254
|
+
const manifest = {
|
|
4255
|
+
schema_version: "0.1.0",
|
|
4256
|
+
basou_version: "0.1.0",
|
|
4257
|
+
workspace: {
|
|
4258
|
+
id: workspaceId,
|
|
4259
|
+
name: input.workspaceName,
|
|
4260
|
+
created_at: now,
|
|
4261
|
+
updated_at: now
|
|
4262
|
+
},
|
|
4263
|
+
project,
|
|
4264
|
+
capabilities: {
|
|
4265
|
+
enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
|
|
4266
|
+
},
|
|
4267
|
+
approval: {
|
|
4268
|
+
required_for: ["destructive_command", "external_send"],
|
|
4269
|
+
default_risk_level: "medium"
|
|
4270
|
+
},
|
|
4271
|
+
adapters: {
|
|
4272
|
+
"claude-code": { enabled: true }
|
|
4273
|
+
},
|
|
4274
|
+
git: { events_log: "ignore" },
|
|
4275
|
+
...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
|
|
4276
|
+
};
|
|
4277
|
+
return ManifestSchema.parse(manifest);
|
|
4278
|
+
}
|
|
4279
|
+
async function writeManifest(paths, manifest, options) {
|
|
4280
|
+
const force = options?.force === true;
|
|
4281
|
+
const validated = ManifestSchema.parse(manifest);
|
|
4282
|
+
if (!force) {
|
|
4283
|
+
let existed = false;
|
|
4284
|
+
try {
|
|
4285
|
+
await lstat3(paths.files.manifest);
|
|
4286
|
+
existed = true;
|
|
4287
|
+
} catch (error) {
|
|
4288
|
+
if (!hasErrorCode3(error) || error.code !== "ENOENT") {
|
|
4289
|
+
throw new Error("Failed to inspect existing manifest", { cause: error });
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
if (existed) {
|
|
4293
|
+
throw new Error("Already initialized. Use --force to overwrite.");
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
await writeYamlFile(paths.files.manifest, validated);
|
|
4297
|
+
}
|
|
4298
|
+
async function readManifest(paths) {
|
|
4299
|
+
const raw = await readYamlFile(paths.files.manifest);
|
|
4300
|
+
return ManifestSchema.parse(raw);
|
|
4301
|
+
}
|
|
4302
|
+
function hasErrorCode3(error) {
|
|
4303
|
+
if (!(error instanceof Error)) return false;
|
|
4304
|
+
return typeof error.code === "string";
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
// src/orientation/orientation-renderer.ts
|
|
4308
|
+
var DECISION_TRAILING_ACTIVITY_GAP_MS = 60 * 60 * 1e3;
|
|
4309
|
+
async function summarizeOrientation(input) {
|
|
4310
|
+
const limit = input.relatedFilesLimit ?? 10;
|
|
4311
|
+
const now = new Date(input.nowIso);
|
|
4312
|
+
const loadOpts = { now };
|
|
4313
|
+
if (input.onSessionSkip !== void 0) loadOpts.onSkip = input.onSessionSkip;
|
|
4122
4314
|
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
4123
4315
|
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
4124
|
-
const
|
|
4316
|
+
const decisions = [];
|
|
4317
|
+
let latestActivityAt = null;
|
|
4318
|
+
let latestNote = null;
|
|
4319
|
+
const noteActivity = (iso) => {
|
|
4320
|
+
if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
|
|
4321
|
+
latestActivityAt = iso;
|
|
4322
|
+
}
|
|
4323
|
+
};
|
|
4125
4324
|
for (const entry of entries) {
|
|
4126
|
-
const
|
|
4127
|
-
|
|
4325
|
+
const sessionDir = join14(input.paths.sessions, entry.sessionId);
|
|
4326
|
+
const counted = entry.session.session.status !== "archived";
|
|
4327
|
+
if (counted) noteActivity(entry.session.session.ended_at ?? entry.session.session.started_at);
|
|
4128
4328
|
try {
|
|
4129
|
-
for await (const ev of replayEvents(
|
|
4329
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
4130
4330
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4131
4331
|
})) {
|
|
4132
|
-
|
|
4332
|
+
if (ev.type === "decision_recorded") {
|
|
4333
|
+
decisions.push({
|
|
4334
|
+
decisionId: ev.decision_id,
|
|
4335
|
+
title: ev.title,
|
|
4336
|
+
occurredAt: ev.occurred_at
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
if (counted && ev.type === "note_added" && ev.kind === "next_step") {
|
|
4340
|
+
if (latestNote === null || Date.parse(ev.occurred_at) > Date.parse(latestNote.occurredAt)) {
|
|
4341
|
+
latestNote = { body: ev.body, sessionId: entry.sessionId, occurredAt: ev.occurred_at };
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
if (counted) noteActivity(ev.occurred_at);
|
|
4133
4345
|
}
|
|
4134
4346
|
} catch {
|
|
4135
|
-
|
|
4136
|
-
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
4137
|
-
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
4138
|
-
}
|
|
4347
|
+
input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
|
|
4139
4348
|
}
|
|
4140
|
-
sessions.push(
|
|
4141
|
-
sessionWorkStatsFromEvents(
|
|
4142
|
-
entry.sessionId,
|
|
4143
|
-
entry.session.session,
|
|
4144
|
-
events,
|
|
4145
|
-
now,
|
|
4146
|
-
eventsUnreadable
|
|
4147
|
-
)
|
|
4148
|
-
);
|
|
4149
4349
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4350
|
+
decisions.sort((a, b) => {
|
|
4351
|
+
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
4352
|
+
return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
|
|
4353
|
+
});
|
|
4354
|
+
const latestDecision = decisions[decisions.length - 1];
|
|
4355
|
+
const taskLoadOpts = {};
|
|
4356
|
+
if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
|
|
4357
|
+
const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
|
|
4358
|
+
const inFlightTasks = taskEntries.filter((t) => t.task.task.status === "in_progress" || t.task.task.status === "planned").map((t) => ({
|
|
4359
|
+
id: t.task.task.id,
|
|
4360
|
+
title: t.task.task.title,
|
|
4361
|
+
status: t.task.task.status,
|
|
4362
|
+
linkedSessions: t.task.task.linked_sessions?.length ?? 0
|
|
4363
|
+
}));
|
|
4364
|
+
const plannedTasks = taskEntries.filter((t) => t.task.task.status === "planned").map((t) => ({ id: t.task.task.id, title: t.task.task.title }));
|
|
4365
|
+
const { pending: pendingIds } = await enumerateApprovals(input.paths);
|
|
4366
|
+
const pendingApprovals = [];
|
|
4367
|
+
for (const id of [...pendingIds].sort()) {
|
|
4368
|
+
const loaded = await loadApproval(input.paths, id);
|
|
4369
|
+
if (loaded === null) continue;
|
|
4370
|
+
const a = loaded.approval;
|
|
4371
|
+
pendingApprovals.push({
|
|
4372
|
+
id,
|
|
4373
|
+
risk: a.risk_level,
|
|
4374
|
+
kind: a.action.kind,
|
|
4375
|
+
reason: a.reason,
|
|
4376
|
+
sessionId: a.session_id,
|
|
4377
|
+
createdAt: a.created_at,
|
|
4378
|
+
expired: isLazyExpired(a, now)
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
4381
|
+
const suspects = entries.filter((e) => e.suspect).map((e) => ({
|
|
4382
|
+
sessionId: e.sessionId,
|
|
4383
|
+
status: e.session.session.status,
|
|
4384
|
+
reason: e.suspectReason
|
|
4385
|
+
}));
|
|
4386
|
+
const liveEntries = entries.filter(
|
|
4387
|
+
(e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
|
|
4388
|
+
);
|
|
4389
|
+
const latestEntry = [...liveEntries].sort(
|
|
4390
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
4391
|
+
)[0];
|
|
4392
|
+
const latestSession = latestEntry !== void 0 ? {
|
|
4393
|
+
sessionId: latestEntry.sessionId,
|
|
4394
|
+
label: latestEntry.session.session.label ?? null,
|
|
4395
|
+
status: latestEntry.session.session.status
|
|
4396
|
+
} : null;
|
|
4397
|
+
const activityEntries = entries.filter((e) => e.session.session.status !== "archived");
|
|
4398
|
+
const newest = [...activityEntries].sort(
|
|
4399
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
4400
|
+
)[0];
|
|
4401
|
+
const bySourceMap = /* @__PURE__ */ new Map();
|
|
4402
|
+
for (const e of entries) {
|
|
4403
|
+
const k = e.session.session.source.kind;
|
|
4404
|
+
bySourceMap.set(k, (bySourceMap.get(k) ?? 0) + 1);
|
|
4405
|
+
}
|
|
4406
|
+
const bySource = [...bySourceMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([kind, count]) => ({ kind, count }));
|
|
4407
|
+
let sourceRoots = null;
|
|
4408
|
+
try {
|
|
4409
|
+
const manifest = await readManifest(input.paths);
|
|
4410
|
+
sourceRoots = manifest.import?.source_roots ?? null;
|
|
4411
|
+
} catch {
|
|
4412
|
+
sourceRoots = null;
|
|
4413
|
+
}
|
|
4414
|
+
const latestFiles = latestEntry?.session.session.related_files ?? [];
|
|
4415
|
+
const uniqueFiles = new Set(latestFiles);
|
|
4416
|
+
const displayed = [...uniqueFiles].sort().slice(0, limit);
|
|
4417
|
+
const overflow = Math.max(0, uniqueFiles.size - limit);
|
|
4153
4418
|
return {
|
|
4154
|
-
generatedAt:
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4419
|
+
generatedAt: input.nowIso,
|
|
4420
|
+
sessionCount: entries.length,
|
|
4421
|
+
latestSession,
|
|
4422
|
+
latestDecision: latestDecision ?? null,
|
|
4423
|
+
decisionCount: decisions.length,
|
|
4424
|
+
latestNote,
|
|
4425
|
+
relatedFiles: { displayed, overflow },
|
|
4426
|
+
inFlightTasks,
|
|
4427
|
+
plannedTasks,
|
|
4428
|
+
pendingApprovals,
|
|
4429
|
+
suspects,
|
|
4430
|
+
freshness: {
|
|
4431
|
+
newestStartedAt: newest?.session.session.started_at ?? null,
|
|
4432
|
+
newestSource: newest?.session.session.source.kind ?? null,
|
|
4433
|
+
latestActivityAt,
|
|
4434
|
+
bySource,
|
|
4435
|
+
sourceRoots
|
|
4436
|
+
}
|
|
4162
4437
|
};
|
|
4163
4438
|
}
|
|
4164
|
-
function
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4439
|
+
async function renderOrientation(input) {
|
|
4440
|
+
const summary = await summarizeOrientation(input);
|
|
4441
|
+
return {
|
|
4442
|
+
body: formatOrientationBody(summary, {
|
|
4443
|
+
staleness: input.staleness ?? null,
|
|
4444
|
+
verbose: input.verbose === true
|
|
4445
|
+
}),
|
|
4446
|
+
sessionCount: summary.sessionCount,
|
|
4447
|
+
pendingApprovalsCount: summary.pendingApprovals.length,
|
|
4448
|
+
suspectCount: summary.suspects.length,
|
|
4449
|
+
inFlightTaskCount: summary.inFlightTasks.length,
|
|
4450
|
+
decisionCount: summary.decisionCount
|
|
4451
|
+
};
|
|
4452
|
+
}
|
|
4453
|
+
function formatOrientationBody(summary, opts) {
|
|
4454
|
+
const lines = [];
|
|
4455
|
+
const now = new Date(summary.generatedAt);
|
|
4456
|
+
const newestRel = relativeAge(summary.freshness.newestStartedAt ?? void 0, now);
|
|
4457
|
+
lines.push("# Orientation");
|
|
4458
|
+
lines.push("");
|
|
4459
|
+
lines.push(
|
|
4460
|
+
`> Generated at ${summary.generatedAt} \xB7 sessions ${summary.sessionCount} \xB7 newest ${newestRel} \xB7 pending ${summary.pendingApprovals.length} \xB7 suspect ${summary.suspects.length}`
|
|
4461
|
+
);
|
|
4462
|
+
lines.push("");
|
|
4463
|
+
lines.push("## \u4ECA\u3069\u3053\u306B\u3044\u308B");
|
|
4464
|
+
lines.push("");
|
|
4465
|
+
if (summary.latestSession !== null) {
|
|
4466
|
+
const s = summary.latestSession;
|
|
4467
|
+
const sid = shortId(s.sessionId);
|
|
4468
|
+
if (s.label !== null && s.label !== "") {
|
|
4469
|
+
lines.push(`- \u6700\u7D42 session: ${s.label} (${s.status}) [${sid}]`);
|
|
4470
|
+
} else {
|
|
4471
|
+
lines.push(`- \u6700\u7D42 session: ${sid} (${s.status})`);
|
|
4472
|
+
}
|
|
4473
|
+
} else {
|
|
4474
|
+
lines.push("- \u6700\u7D42 session: (no live sessions)");
|
|
4475
|
+
}
|
|
4476
|
+
if (summary.latestDecision !== null) {
|
|
4477
|
+
const decAge = relativeAgeJa(summary.latestDecision.occurredAt, now);
|
|
4478
|
+
lines.push(
|
|
4479
|
+
`- \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title} [${shortId(summary.latestDecision.decisionId)}] (${decAge})`
|
|
4480
|
+
);
|
|
4481
|
+
const activityAt = summary.freshness.latestActivityAt;
|
|
4482
|
+
if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestDecision.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
|
|
4483
|
+
lines.push(
|
|
4484
|
+
` - \u6CE8: \u3053\u308C\u306F\u6700\u5F8C\u306B\u300C\u8A18\u9332\u3055\u308C\u305F\u300D\u5224\u65AD\u3067\u3059\u3002\u6700\u7D42\u6D3B\u52D5 (${relativeAgeJa(activityAt, now)}) \u306F\u3053\u308C\u3088\u308A\u5F8C\u306E\u305F\u3081\u3001\u73FE\u5728\u306E\u65B9\u91DD\u304C\u53CD\u6620\u3055\u308C\u3066\u3044\u306A\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059(\u4F1A\u8A71\u3067\u306E\u610F\u601D\u6C7A\u5B9A\u306F\u81EA\u52D5\u8A18\u9332\u3055\u308C\u307E\u305B\u3093)\u3002`
|
|
4485
|
+
);
|
|
4486
|
+
}
|
|
4487
|
+
if (summary.decisionCount > 1) {
|
|
4488
|
+
lines.push(` - ${summary.decisionCount} decisions total \u2014 see decisions.md`);
|
|
4489
|
+
}
|
|
4490
|
+
} else {
|
|
4491
|
+
lines.push("- \u76F4\u8FD1\u306E\u5224\u65AD: (no decisions recorded yet)");
|
|
4492
|
+
}
|
|
4493
|
+
if (summary.relatedFiles.displayed.length > 0) {
|
|
4494
|
+
const shown = summary.relatedFiles.displayed.join(", ");
|
|
4495
|
+
const more = summary.relatedFiles.overflow > 0 ? ` (... +${summary.relatedFiles.overflow} more)` : "";
|
|
4496
|
+
lines.push(`- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: ${shown}${more}`);
|
|
4497
|
+
} else {
|
|
4498
|
+
lines.push("- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: (none recorded)");
|
|
4499
|
+
}
|
|
4500
|
+
lines.push("");
|
|
4501
|
+
lines.push("## \u4F55\u304C\u52D5\u304F");
|
|
4502
|
+
lines.push("");
|
|
4503
|
+
lines.push(`### \u9032\u884C\u4E2D task (${summary.inFlightTasks.length})`);
|
|
4504
|
+
if (summary.inFlightTasks.length === 0) {
|
|
4505
|
+
lines.push("- (none)");
|
|
4506
|
+
} else {
|
|
4507
|
+
for (const t of summary.inFlightTasks) {
|
|
4508
|
+
const linkedSuffix = t.linkedSessions > 1 ? ` \u2014 linked_sessions: ${t.linkedSessions}` : "";
|
|
4509
|
+
lines.push(`- ${t.title} (${t.status}) [${shortId(t.id)}]${linkedSuffix}`);
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
lines.push("");
|
|
4513
|
+
lines.push(`### \u627F\u8A8D\u5F85\u3061 (${summary.pendingApprovals.length})`);
|
|
4514
|
+
if (summary.pendingApprovals.length === 0) {
|
|
4515
|
+
lines.push("- (none)");
|
|
4516
|
+
} else {
|
|
4517
|
+
for (const a of summary.pendingApprovals) {
|
|
4518
|
+
const expired = a.expired ? " (expired)" : "";
|
|
4519
|
+
lines.push(
|
|
4520
|
+
`- [${a.risk}] ${a.kind}: ${a.reason} \u2014 session ${shortId(a.sessionId)}, since ${a.createdAt}${expired}`
|
|
4521
|
+
);
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
lines.push("");
|
|
4525
|
+
lines.push(`### \u8981\u6CE8\u610F session (${summary.suspects.length})`);
|
|
4526
|
+
if (summary.suspects.length === 0) {
|
|
4527
|
+
lines.push("- (none)");
|
|
4528
|
+
} else {
|
|
4529
|
+
for (const e of summary.suspects) {
|
|
4530
|
+
lines.push(`- ${shortId(e.sessionId)} (${e.status}) \u2014 ${suspectText(e.reason)}`);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
lines.push("");
|
|
4534
|
+
lines.push("## \u3069\u3053\u3078\u5411\u304B\u3046");
|
|
4535
|
+
lines.push("");
|
|
4536
|
+
if (summary.latestNote !== null) {
|
|
4537
|
+
const noteAge = relativeAgeJa(summary.latestNote.occurredAt, now);
|
|
4538
|
+
lines.push(
|
|
4539
|
+
`- \u6B21\u306E\u8D77\u70B9 (\u8A18\u9332\u6E08\u307F, ${noteAge}): ${noteSummary(summary.latestNote.body)} [session ${shortId(summary.latestNote.sessionId)}]`
|
|
4540
|
+
);
|
|
4541
|
+
const activityAt = summary.freshness.latestActivityAt;
|
|
4542
|
+
if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestNote.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
|
|
4543
|
+
lines.push(
|
|
4544
|
+
` - \u6CE8: \u3053\u306E\u8D77\u70B9\u306E\u8A18\u9332\u5F8C (\u6700\u7D42\u6D3B\u52D5 ${relativeAgeJa(activityAt, now)}) \u3082\u4F5C\u696D\u304C\u7D9A\u3044\u3066\u3044\u307E\u3059\u3002\u518D\u958B\u70B9\u304C\u53E4\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
4545
|
+
);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
for (const t of summary.plannedTasks) {
|
|
4549
|
+
lines.push(`- ${t.title} [${shortId(t.id)}]`);
|
|
4550
|
+
}
|
|
4551
|
+
if (summary.latestNote === null && summary.plannedTasks.length === 0) {
|
|
4552
|
+
lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
|
|
4553
|
+
if (summary.latestDecision !== null) {
|
|
4554
|
+
lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title}`);
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
lines.push("");
|
|
4558
|
+
lines.push("## \u3053\u308C\u306F\u6700\u65B0\u304B");
|
|
4559
|
+
lines.push("");
|
|
4560
|
+
for (const line of freshnessVerdict(summary, opts.staleness, now)) lines.push(line);
|
|
4561
|
+
if (opts.verbose) {
|
|
4562
|
+
lines.push("");
|
|
4563
|
+
lines.push("<!-- verbose: raw freshness telemetry -->");
|
|
4564
|
+
if (summary.freshness.newestStartedAt !== null) {
|
|
4565
|
+
lines.push(`- newest captured session: ${summary.freshness.newestStartedAt} (${newestRel})`);
|
|
4566
|
+
} else {
|
|
4567
|
+
lines.push("- newest captured session: (no sessions captured yet)");
|
|
4568
|
+
}
|
|
4569
|
+
if (summary.freshness.latestActivityAt !== null) {
|
|
4570
|
+
lines.push(
|
|
4571
|
+
`- latest activity: ${summary.freshness.latestActivityAt} (${relativeAge(summary.freshness.latestActivityAt, now)})`
|
|
4572
|
+
);
|
|
4573
|
+
}
|
|
4574
|
+
const sourceBreakdown = summary.freshness.bySource.map(({ kind, count }) => `${kind} ${count}`).join(", ");
|
|
4575
|
+
lines.push(
|
|
4576
|
+
`- sessions: ${summary.sessionCount}${sourceBreakdown !== "" ? ` (${sourceBreakdown})` : ""}`
|
|
4577
|
+
);
|
|
4578
|
+
if (summary.freshness.sourceRoots !== null && summary.freshness.sourceRoots.length > 0) {
|
|
4579
|
+
lines.push(`- source roots: ${summary.freshness.sourceRoots.join(", ")}`);
|
|
4580
|
+
} else {
|
|
4581
|
+
lines.push("- source roots: (single root)");
|
|
4582
|
+
}
|
|
4583
|
+
lines.push(`- suspect sessions: ${summary.suspects.length}`);
|
|
4584
|
+
const probe = opts.staleness === null ? "not run" : `new ${opts.staleness.newSessions}, updated ${opts.staleness.updatedSessions}, unverifiable ${opts.staleness.unverifiableSessions ?? 0}`;
|
|
4585
|
+
lines.push(`- staleness probe: ${probe}`);
|
|
4586
|
+
}
|
|
4587
|
+
return lines.join("\n");
|
|
4588
|
+
}
|
|
4589
|
+
function toolDisplayName(kind) {
|
|
4590
|
+
switch (kind) {
|
|
4591
|
+
case "claude-code-import":
|
|
4592
|
+
case "claude-code-adapter":
|
|
4593
|
+
return "Claude Code";
|
|
4594
|
+
case "codex-import":
|
|
4595
|
+
return "Codex";
|
|
4596
|
+
case "terminal":
|
|
4597
|
+
return "\u30BF\u30FC\u30DF\u30CA\u30EB";
|
|
4598
|
+
case "human":
|
|
4599
|
+
return "\u624B\u52D5\u30E1\u30E2";
|
|
4600
|
+
case "import":
|
|
4601
|
+
return "\u4ED6\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9";
|
|
4602
|
+
default:
|
|
4603
|
+
return kind ?? "\u4E0D\u660E";
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
function freshnessVerdict(summary, staleness, now) {
|
|
4607
|
+
if (staleness !== null && (staleness.unverifiableSessions ?? 0) > 0) {
|
|
4608
|
+
return [
|
|
4609
|
+
`\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`,
|
|
4610
|
+
"`basou verify` \u3067\u78BA\u8A8D\u3057\u3001`basou refresh --force` \u3067\u518D\u53D6\u308A\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4611
|
+
];
|
|
4612
|
+
}
|
|
4613
|
+
if (staleness !== null && (staleness.newSessions > 0 || staleness.updatedSessions > 0)) {
|
|
4614
|
+
const parts = [];
|
|
4615
|
+
if (staleness.newSessions > 0) parts.push(`\u65B0\u898F ${staleness.newSessions} \u4EF6`);
|
|
4616
|
+
if (staleness.updatedSessions > 0) parts.push(`\u66F4\u65B0 ${staleness.updatedSessions} \u4EF6`);
|
|
4617
|
+
return [
|
|
4618
|
+
`\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`,
|
|
4619
|
+
"`basou refresh` \u3067\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4620
|
+
];
|
|
4621
|
+
}
|
|
4622
|
+
if (summary.freshness.newestStartedAt === null) {
|
|
4623
|
+
return [
|
|
4624
|
+
"\u2139\uFE0F \u307E\u3060\u8A18\u9332\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
4625
|
+
"\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"
|
|
4626
|
+
];
|
|
4627
|
+
}
|
|
4628
|
+
const rel = relativeAgeJa(summary.freshness.newestStartedAt, now);
|
|
4629
|
+
const tool = toolDisplayName(summary.freshness.newestSource);
|
|
4630
|
+
const suspectCount = summary.suspects.length;
|
|
4631
|
+
if (staleness === null) {
|
|
4632
|
+
return [
|
|
4633
|
+
`\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`,
|
|
4634
|
+
"\u6700\u65B0\u304B\u78BA\u8A8D\u3059\u308B\u306B\u306F `basou refresh` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4635
|
+
];
|
|
4636
|
+
}
|
|
4637
|
+
const lines = [
|
|
4638
|
+
`\u2705 \u53D6\u308A\u8FBC\u307F\u306F\u6700\u65B0\u3067\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002\u672A\u53D6\u308A\u8FBC\u307F\u306E native \u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u3042\u308A\u307E\u305B\u3093\u3002`
|
|
4639
|
+
];
|
|
4640
|
+
if (suspectCount > 0) {
|
|
4641
|
+
lines.push(`\u305F\u3060\u3057\u8981\u6CE8\u610F\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${suspectCount} \u4EF6\u3042\u308A\u307E\u3059(\u4E0A\u8A18\u300C\u8981\u6CE8\u610F session\u300D\u53C2\u7167)\u3002`);
|
|
4642
|
+
}
|
|
4643
|
+
lines.push(
|
|
4644
|
+
"\u6CE8: \u3053\u306E\u5224\u5B9A\u306F\u53D6\u308A\u8FBC\u307F\u6E08\u307F native \u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u9BAE\u5EA6\u3068 suspect \u306E\u6709\u7121\u3060\u3051\u3092\u898B\u307E\u3059\u3002\u8A08\u753B\u2194\u5B9F\u88C5\u306E\u30C9\u30EA\u30D5\u30C8\u3084\u672A\u8A18\u9332\u306E\u610F\u601D\u6C7A\u5B9A\u307E\u3067\u306F\u691C\u77E5\u3057\u307E\u305B\u3093\u3002"
|
|
4645
|
+
);
|
|
4646
|
+
return lines;
|
|
4647
|
+
}
|
|
4648
|
+
function relativeAgeJa(startedAt, now) {
|
|
4649
|
+
if (startedAt === null) return "(\u4E0D\u660E)";
|
|
4650
|
+
const ms = now.getTime() - Date.parse(startedAt);
|
|
4651
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
|
|
4652
|
+
if (ms < 6e4) return "\u305F\u3063\u305F\u4ECA";
|
|
4653
|
+
const totalMin = Math.floor(ms / 6e4);
|
|
4654
|
+
const days = Math.floor(totalMin / 1440);
|
|
4655
|
+
const hours = Math.floor(totalMin % 1440 / 60);
|
|
4656
|
+
const mins = totalMin % 60;
|
|
4657
|
+
if (days > 0) return hours > 0 ? `${days}\u65E5${hours}\u6642\u9593\u524D` : `${days}\u65E5\u524D`;
|
|
4658
|
+
if (hours > 0) return mins > 0 ? `${hours}\u6642\u9593${mins}\u5206\u524D` : `${hours}\u6642\u9593\u524D`;
|
|
4659
|
+
return `${mins}\u5206\u524D`;
|
|
4660
|
+
}
|
|
4661
|
+
function relativeAge(startedAt, now) {
|
|
4662
|
+
if (startedAt === void 0) return "(unknown)";
|
|
4663
|
+
const ms = now.getTime() - Date.parse(startedAt);
|
|
4664
|
+
if (!Number.isFinite(ms)) return "(unknown)";
|
|
4665
|
+
if (ms < 0) return "just now";
|
|
4666
|
+
if (ms < 1e3) return "just now";
|
|
4667
|
+
return `${formatDurationMs(ms)} ago`;
|
|
4668
|
+
}
|
|
4669
|
+
var NOTE_SUMMARY_MAX = 200;
|
|
4670
|
+
function noteSummary(body) {
|
|
4671
|
+
const oneLine = body.replace(/\s+/g, " ").trim();
|
|
4672
|
+
return oneLine.length > NOTE_SUMMARY_MAX ? `${oneLine.slice(0, NOTE_SUMMARY_MAX - 1)}\u2026` : oneLine;
|
|
4673
|
+
}
|
|
4674
|
+
function suspectText(reason) {
|
|
4675
|
+
if (reason === "events_say_ended_but_yaml_running") return "ended (yaml stale)";
|
|
4676
|
+
if (reason === "running_no_end_event") return "no end event";
|
|
4677
|
+
return "suspect";
|
|
4678
|
+
}
|
|
4679
|
+
function shortId(id) {
|
|
4680
|
+
const sep = id.indexOf("_");
|
|
4681
|
+
if (sep === -1) return id.slice(0, 10);
|
|
4682
|
+
return id.slice(0, sep + 1) + id.slice(sep + 1, sep + 1 + 10);
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
// src/project/relative-path.ts
|
|
4686
|
+
function normalizeRelativePath(p) {
|
|
4687
|
+
const trimmed = p.trim();
|
|
4688
|
+
const absolute = trimmed.startsWith("/");
|
|
4689
|
+
const out = [];
|
|
4690
|
+
for (const seg of trimmed.split("/")) {
|
|
4691
|
+
if (seg === "" || seg === ".") continue;
|
|
4692
|
+
if (seg === "..") {
|
|
4693
|
+
const top = out[out.length - 1];
|
|
4694
|
+
if (top !== void 0 && top !== "..") {
|
|
4695
|
+
out.pop();
|
|
4696
|
+
} else if (!absolute) {
|
|
4697
|
+
out.push("..");
|
|
4698
|
+
}
|
|
4699
|
+
continue;
|
|
4700
|
+
}
|
|
4701
|
+
out.push(seg);
|
|
4702
|
+
}
|
|
4703
|
+
const joined = out.join("/");
|
|
4704
|
+
if (absolute) return `/${joined}`;
|
|
4705
|
+
return joined.length === 0 ? "." : joined;
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
// src/project/archive.ts
|
|
4709
|
+
function planArchive(input) {
|
|
4710
|
+
const target = normalizeRelativePath(input.target);
|
|
4711
|
+
const repos = input.repos ?? [];
|
|
4712
|
+
const isAnchor = input.targetIsAnchor === true || target === ".";
|
|
4713
|
+
const matched = repos.filter((r) => normalizeRelativePath(r.path) === target);
|
|
4714
|
+
const found = matched.length > 0;
|
|
4715
|
+
if (isAnchor || !found) {
|
|
4716
|
+
return {
|
|
4717
|
+
target,
|
|
4718
|
+
found,
|
|
4719
|
+
isAnchor,
|
|
4720
|
+
nextRepos: repos,
|
|
4721
|
+
reposEmptied: false,
|
|
4722
|
+
remainingCount: repos.length,
|
|
4723
|
+
becomesSolo: false
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
const nextRepos = repos.filter((r) => normalizeRelativePath(r.path) !== target);
|
|
4727
|
+
const remainingCount = nextRepos.length;
|
|
4728
|
+
let sourceRootRemoval;
|
|
4729
|
+
let nextSourceRoots;
|
|
4730
|
+
if (input.sourceRoots !== void 0) {
|
|
4731
|
+
const pruned = input.sourceRoots.filter((s) => normalizeRelativePath(s) !== target);
|
|
4732
|
+
if (pruned.length !== input.sourceRoots.length) {
|
|
4733
|
+
sourceRootRemoval = target;
|
|
4734
|
+
nextSourceRoots = pruned;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
return {
|
|
4738
|
+
target,
|
|
4739
|
+
found: true,
|
|
4740
|
+
isAnchor: false,
|
|
4741
|
+
rosterEntry: matched[matched.length - 1],
|
|
4742
|
+
nextRepos,
|
|
4743
|
+
reposEmptied: remainingCount === 0,
|
|
4744
|
+
...sourceRootRemoval !== void 0 ? { sourceRootRemoval } : {},
|
|
4745
|
+
...nextSourceRoots !== void 0 ? { nextSourceRoots } : {},
|
|
4746
|
+
remainingCount,
|
|
4747
|
+
becomesSolo: remainingCount === 1
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
// src/project/gitignore-plan.ts
|
|
4752
|
+
function isPublicFacing(v) {
|
|
4753
|
+
return v === "public" || v === "future-public";
|
|
4754
|
+
}
|
|
4755
|
+
function planGitignore(input) {
|
|
4756
|
+
const plans = [];
|
|
4757
|
+
const unknown = [];
|
|
4758
|
+
const unreachable = [];
|
|
4759
|
+
for (const repo of input.repos) {
|
|
4760
|
+
if (!repo.reachable) {
|
|
4761
|
+
unreachable.push(repo.path);
|
|
4762
|
+
continue;
|
|
4763
|
+
}
|
|
4764
|
+
if (repo.visibility === void 0) {
|
|
4765
|
+
unknown.push(repo.path);
|
|
4766
|
+
continue;
|
|
4767
|
+
}
|
|
4768
|
+
if (!isPublicFacing(repo.visibility)) continue;
|
|
4769
|
+
const present = /* @__PURE__ */ new Set();
|
|
4770
|
+
for (const line of repo.currentLines) {
|
|
4771
|
+
const trimmed = line.trim();
|
|
4772
|
+
present.add(trimmed);
|
|
4773
|
+
if (trimmed.startsWith("/")) present.add(trimmed.slice(1));
|
|
4774
|
+
}
|
|
4775
|
+
const toAdd = input.required.filter((p) => !present.has(p));
|
|
4776
|
+
if (toAdd.length > 0) plans.push({ path: repo.path, toAdd });
|
|
4777
|
+
}
|
|
4778
|
+
return {
|
|
4779
|
+
plans,
|
|
4780
|
+
unknown,
|
|
4781
|
+
unreachable,
|
|
4782
|
+
ok: plans.length === 0 && unknown.length === 0 && unreachable.length === 0
|
|
4783
|
+
};
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
// src/project/preset.ts
|
|
4787
|
+
function visibilityLabel(v) {
|
|
4788
|
+
switch (v) {
|
|
4789
|
+
case "public":
|
|
4790
|
+
return "public(git \u5C65\u6B74\u306F\u516C\u958B)";
|
|
4791
|
+
case "private":
|
|
4792
|
+
return "private(git \u5C65\u6B74\u306F\u975E\u516C\u958B)";
|
|
4793
|
+
case "future-public":
|
|
4794
|
+
return "future-public(\u73FE\u5728\u306F\u975E\u516C\u958B\u30FB\u5C06\u6765\u516C\u958B\u4E88\u5B9A)";
|
|
4795
|
+
default:
|
|
4796
|
+
return "\u672A\u8A2D\u5B9A";
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
function sourceLanguageLabel(l) {
|
|
4800
|
+
switch (l) {
|
|
4801
|
+
case "en":
|
|
4802
|
+
return "en(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u82F1\u8A9E)";
|
|
4803
|
+
case "ja":
|
|
4804
|
+
return "ja(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u65E5\u672C\u8A9E)";
|
|
4805
|
+
case "en+ja":
|
|
4806
|
+
return "en+ja(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u65E5\u82F1)";
|
|
4807
|
+
default:
|
|
4808
|
+
return "\u672A\u8A2D\u5B9A";
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
function publishKindLabel(k) {
|
|
4812
|
+
return k === "web" ? "web(\u30C7\u30D7\u30ED\u30A4)" : "npm(\u30D1\u30C3\u30B1\u30FC\u30B8)";
|
|
4813
|
+
}
|
|
4814
|
+
function publishVisibilityLabel(v) {
|
|
4815
|
+
switch (v) {
|
|
4816
|
+
case "public":
|
|
4817
|
+
return "\u516C\u958B";
|
|
4818
|
+
case "private":
|
|
4819
|
+
return "\u975E\u516C\u958B";
|
|
4820
|
+
case "future-public":
|
|
4821
|
+
return "\u5C06\u6765\u516C\u958B";
|
|
4822
|
+
default:
|
|
4823
|
+
return "\u53EF\u8996\u6027\u672A\u8A2D\u5B9A";
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
function contentLanguageLabel(l) {
|
|
4827
|
+
return l ?? "\u8A00\u8A9E\u672A\u8A2D\u5B9A";
|
|
4828
|
+
}
|
|
4829
|
+
function isRenderable(repo) {
|
|
4830
|
+
return repo.visibility !== void 0 || repo.language !== void 0 || repo.publishes !== void 0 && repo.publishes.length > 0;
|
|
4831
|
+
}
|
|
4832
|
+
function renderPresetBlock(repo) {
|
|
4833
|
+
const lines = [];
|
|
4834
|
+
lines.push("## \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u69CB\u6210(basou \u304C\u751F\u6210 \u2014 manifest \u304C\u6B63\u672C)");
|
|
4835
|
+
lines.push("");
|
|
4836
|
+
lines.push(
|
|
4837
|
+
"\u3053\u306E\u30BB\u30AF\u30B7\u30E7\u30F3\u306F `.basou/manifest.yaml` \u306E\u5BA3\u8A00\u304B\u3089 `basou project preset` \u304C\u751F\u6210\u3057\u307E\u3059\u3002\u7DE8\u96C6\u306F manifest \u5074\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044(\u30DE\u30FC\u30AB\u30FC\u5916\u306E\u8A18\u8FF0\u306F\u4FDD\u6301\u3055\u308C\u307E\u3059)\u3002"
|
|
4838
|
+
);
|
|
4839
|
+
lines.push("");
|
|
4840
|
+
lines.push(`- \u30BD\u30FC\u30B9\u53EF\u8996\u6027: ${visibilityLabel(repo.visibility)}`);
|
|
4841
|
+
lines.push(`- \u30BD\u30FC\u30B9\u8A00\u8A9E: ${sourceLanguageLabel(repo.language)}`);
|
|
4842
|
+
const publishes = repo.publishes ?? [];
|
|
4843
|
+
if (publishes.length === 0) {
|
|
4844
|
+
lines.push("- \u914D\u4FE1\u7269: \u306A\u3057");
|
|
4845
|
+
} else {
|
|
4846
|
+
lines.push("- \u914D\u4FE1\u7269:");
|
|
4847
|
+
for (const p of publishes) {
|
|
4848
|
+
lines.push(
|
|
4849
|
+
` - ${publishKindLabel(p.kind)} \u2014 ${publishVisibilityLabel(p.visibility)} / ${contentLanguageLabel(p.language)}`
|
|
4850
|
+
);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
return lines.join("\n");
|
|
4854
|
+
}
|
|
4855
|
+
function normalizeBlock(s) {
|
|
4856
|
+
return s.replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
4857
|
+
}
|
|
4858
|
+
function summarizePresetPlan(facts) {
|
|
4859
|
+
const deduped = [];
|
|
4860
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
4861
|
+
for (const f of facts) {
|
|
4862
|
+
const key = normalizeRelativePath(f.path);
|
|
4863
|
+
if (seenPath.has(key)) continue;
|
|
4864
|
+
seenPath.add(key);
|
|
4865
|
+
deduped.push(f);
|
|
4866
|
+
}
|
|
4867
|
+
const byCanonical = /* @__PURE__ */ new Map();
|
|
4868
|
+
for (const f of deduped) {
|
|
4869
|
+
if (f.isAnchor || !f.reachable || f.canonicalName === void 0 || !isRenderable(f)) continue;
|
|
4870
|
+
const repos = byCanonical.get(f.canonicalName) ?? [];
|
|
4871
|
+
repos.push(f.path);
|
|
4872
|
+
byCanonical.set(f.canonicalName, repos);
|
|
4873
|
+
}
|
|
4874
|
+
const collisions = [];
|
|
4875
|
+
const collidingPaths = /* @__PURE__ */ new Set();
|
|
4876
|
+
for (const [canonicalName, repos] of byCanonical) {
|
|
4877
|
+
if (repos.length > 1) {
|
|
4878
|
+
collisions.push({ canonicalName, repos });
|
|
4879
|
+
for (const r of repos) collidingPaths.add(r);
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
const plans = [];
|
|
4883
|
+
const inSync = [];
|
|
4884
|
+
const undeclared = [];
|
|
4885
|
+
const markerConflicts = [];
|
|
4886
|
+
const unreadable = [];
|
|
4887
|
+
const anchors = [];
|
|
4888
|
+
const unreachable = [];
|
|
4889
|
+
for (const f of deduped) {
|
|
4890
|
+
if (f.isAnchor) {
|
|
4891
|
+
anchors.push(f.path);
|
|
4892
|
+
continue;
|
|
4893
|
+
}
|
|
4894
|
+
if (!f.reachable) {
|
|
4895
|
+
unreachable.push(f.path);
|
|
4896
|
+
continue;
|
|
4897
|
+
}
|
|
4898
|
+
if (!isRenderable(f)) {
|
|
4899
|
+
undeclared.push(f.path);
|
|
4900
|
+
continue;
|
|
4901
|
+
}
|
|
4902
|
+
if (collidingPaths.has(f.path)) continue;
|
|
4903
|
+
if (f.canonicalName === void 0) {
|
|
4904
|
+
unreachable.push(f.path);
|
|
4905
|
+
continue;
|
|
4906
|
+
}
|
|
4907
|
+
const desiredBlock = renderPresetBlock(f);
|
|
4908
|
+
if (!f.canonicalPresent) {
|
|
4909
|
+
plans.push({ path: f.path, canonicalName: f.canonicalName, action: "create", desiredBlock });
|
|
4910
|
+
continue;
|
|
4911
|
+
}
|
|
4912
|
+
if (f.canonicalReadable === false) {
|
|
4913
|
+
unreadable.push(f.path);
|
|
4914
|
+
continue;
|
|
4915
|
+
}
|
|
4916
|
+
if (f.markerKind === "ok") {
|
|
4917
|
+
if (normalizeBlock(f.currentBlock ?? "") === normalizeBlock(desiredBlock)) {
|
|
4918
|
+
inSync.push(f.path);
|
|
4919
|
+
} else {
|
|
4920
|
+
plans.push({
|
|
4921
|
+
path: f.path,
|
|
4922
|
+
canonicalName: f.canonicalName,
|
|
4923
|
+
action: "update",
|
|
4924
|
+
desiredBlock
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4927
|
+
continue;
|
|
4928
|
+
}
|
|
4929
|
+
markerConflicts.push({ repo: f.path, reason: f.markerKind ?? "no_markers" });
|
|
4930
|
+
}
|
|
4931
|
+
return {
|
|
4932
|
+
plans,
|
|
4933
|
+
inSync,
|
|
4934
|
+
undeclared,
|
|
4935
|
+
markerConflicts,
|
|
4936
|
+
unreadable,
|
|
4937
|
+
collisions,
|
|
4938
|
+
anchors,
|
|
4939
|
+
unreachable,
|
|
4940
|
+
ok: plans.length === 0 && markerConflicts.length === 0 && unreadable.length === 0 && collisions.length === 0 && unreachable.length === 0 && undeclared.length === 0
|
|
4941
|
+
};
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
// src/project/rename.ts
|
|
4945
|
+
function pathBasename(p) {
|
|
4946
|
+
const parts = normalizeRelativePath(p).split("/");
|
|
4947
|
+
return parts[parts.length - 1];
|
|
4948
|
+
}
|
|
4949
|
+
function dedupRepos(entries) {
|
|
4950
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4951
|
+
const out = [];
|
|
4952
|
+
for (const e of entries) {
|
|
4953
|
+
const k = normalizeRelativePath(e.path);
|
|
4954
|
+
if (seen.has(k)) continue;
|
|
4955
|
+
seen.add(k);
|
|
4956
|
+
out.push(e);
|
|
4957
|
+
}
|
|
4958
|
+
return out;
|
|
4959
|
+
}
|
|
4960
|
+
function dedupNorm(items) {
|
|
4961
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4962
|
+
const out = [];
|
|
4963
|
+
for (const s of items) {
|
|
4964
|
+
const k = normalizeRelativePath(s);
|
|
4965
|
+
if (seen.has(k)) continue;
|
|
4966
|
+
seen.add(k);
|
|
4967
|
+
out.push(s);
|
|
4968
|
+
}
|
|
4969
|
+
return out;
|
|
4970
|
+
}
|
|
4971
|
+
function planRename(input) {
|
|
4972
|
+
const oldTarget = normalizeRelativePath(input.oldPath);
|
|
4973
|
+
const newTarget = normalizeRelativePath(input.newPath);
|
|
4974
|
+
const repos = input.repos ?? [];
|
|
4975
|
+
const basenameChanged = pathBasename(oldTarget) !== pathBasename(newTarget);
|
|
4976
|
+
const noop = oldTarget === newTarget;
|
|
4977
|
+
const isAnchor = input.oldIsAnchor === true || oldTarget === ".";
|
|
4978
|
+
const found = repos.some((r) => normalizeRelativePath(r.path) === oldTarget);
|
|
4979
|
+
const collision = !noop && repos.some((r) => normalizeRelativePath(r.path) === newTarget);
|
|
4980
|
+
if (noop || isAnchor || !found || collision) {
|
|
4981
|
+
return {
|
|
4982
|
+
oldTarget,
|
|
4983
|
+
newTarget,
|
|
4984
|
+
noop,
|
|
4985
|
+
isAnchor,
|
|
4986
|
+
found,
|
|
4987
|
+
collision,
|
|
4988
|
+
nextRepos: repos,
|
|
4989
|
+
reposChanged: false,
|
|
4990
|
+
basenameChanged
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
const rosterEntry = repos.find((r) => normalizeRelativePath(r.path) === oldTarget);
|
|
4994
|
+
const nextRepos = dedupRepos(
|
|
4995
|
+
repos.map((r) => normalizeRelativePath(r.path) === oldTarget ? { ...r, path: newTarget } : r)
|
|
4996
|
+
);
|
|
4997
|
+
let sourceRootRenamed;
|
|
4998
|
+
let nextSourceRoots;
|
|
4999
|
+
if (input.sourceRoots !== void 0 && input.sourceRoots.some((s) => normalizeRelativePath(s) === oldTarget)) {
|
|
5000
|
+
nextSourceRoots = dedupNorm(
|
|
5001
|
+
input.sourceRoots.map((s) => normalizeRelativePath(s) === oldTarget ? newTarget : s)
|
|
5002
|
+
);
|
|
5003
|
+
sourceRootRenamed = oldTarget;
|
|
5004
|
+
}
|
|
5005
|
+
return {
|
|
5006
|
+
oldTarget,
|
|
5007
|
+
newTarget,
|
|
5008
|
+
noop: false,
|
|
5009
|
+
isAnchor: false,
|
|
5010
|
+
found: true,
|
|
5011
|
+
collision: false,
|
|
5012
|
+
rosterEntry,
|
|
5013
|
+
nextRepos,
|
|
5014
|
+
reposChanged: true,
|
|
5015
|
+
...sourceRootRenamed !== void 0 ? { sourceRootRenamed } : {},
|
|
5016
|
+
...nextSourceRoots !== void 0 ? { nextSourceRoots } : {},
|
|
5017
|
+
basenameChanged
|
|
5018
|
+
};
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
// src/project/roster.ts
|
|
5022
|
+
function summarizeRosterDrift(input) {
|
|
5023
|
+
const captured = new Set((input.sourceRoots ?? []).map(normalizeRelativePath));
|
|
5024
|
+
const declared = /* @__PURE__ */ new Map();
|
|
5025
|
+
for (const r of input.repos ?? []) declared.set(normalizeRelativePath(r.path), r);
|
|
5026
|
+
const gaps = [];
|
|
5027
|
+
const matched = [];
|
|
5028
|
+
for (const [norm, entry] of declared) {
|
|
5029
|
+
if (captured.has(norm)) matched.push(norm);
|
|
5030
|
+
else gaps.push(entry);
|
|
5031
|
+
}
|
|
5032
|
+
const extra = [...captured].filter((c) => !declared.has(c)).sort();
|
|
5033
|
+
return {
|
|
5034
|
+
declaredCount: declared.size,
|
|
5035
|
+
capturedCount: captured.size,
|
|
5036
|
+
gaps,
|
|
5037
|
+
extra,
|
|
5038
|
+
matched: matched.sort(),
|
|
5039
|
+
ok: gaps.length === 0
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
5042
|
+
function reconcileSourceRoots(input) {
|
|
5043
|
+
const current = input.sourceRoots ?? [];
|
|
5044
|
+
const seen = new Set(current.map(normalizeRelativePath));
|
|
5045
|
+
const added = [];
|
|
5046
|
+
for (const r of input.repos ?? []) {
|
|
5047
|
+
const norm = normalizeRelativePath(r.path);
|
|
5048
|
+
if (seen.has(norm)) continue;
|
|
5049
|
+
seen.add(norm);
|
|
5050
|
+
added.push(norm);
|
|
5051
|
+
}
|
|
5052
|
+
return {
|
|
5053
|
+
next: [...current, ...added],
|
|
5054
|
+
added,
|
|
5055
|
+
unchanged: added.length === 0
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
function planRosterAdoption(candidates) {
|
|
5059
|
+
const repos = [];
|
|
5060
|
+
const excluded = [];
|
|
5061
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5062
|
+
for (const c of candidates) {
|
|
5063
|
+
const norm = normalizeRelativePath(c.path);
|
|
5064
|
+
if (seen.has(norm)) continue;
|
|
5065
|
+
seen.add(norm);
|
|
5066
|
+
if (c.kind === "repo") repos.push({ path: c.path });
|
|
5067
|
+
else excluded.push({ path: c.path, kind: c.kind });
|
|
5068
|
+
}
|
|
5069
|
+
return { repos, excluded };
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
// src/project/symlinks.ts
|
|
5073
|
+
function summarizeSymlinkPlan(facts) {
|
|
5074
|
+
const deduped = [];
|
|
5075
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
5076
|
+
for (const f of facts) {
|
|
5077
|
+
const key = normalizeRelativePath(f.path);
|
|
5078
|
+
if (seenPath.has(key)) continue;
|
|
5079
|
+
seenPath.add(key);
|
|
5080
|
+
deduped.push(f);
|
|
5081
|
+
}
|
|
5082
|
+
const byCanonical = /* @__PURE__ */ new Map();
|
|
5083
|
+
for (const f of deduped) {
|
|
5084
|
+
if (f.isAnchor || !f.reachable || !f.canonicalPresent || f.canonicalName === void 0) {
|
|
5085
|
+
continue;
|
|
5086
|
+
}
|
|
5087
|
+
const repos = byCanonical.get(f.canonicalName) ?? [];
|
|
5088
|
+
repos.push(f.path);
|
|
5089
|
+
byCanonical.set(f.canonicalName, repos);
|
|
5090
|
+
}
|
|
5091
|
+
const collisions = [];
|
|
5092
|
+
const collidingPaths = /* @__PURE__ */ new Set();
|
|
5093
|
+
for (const [canonicalName, repos] of byCanonical) {
|
|
5094
|
+
if (repos.length > 1) {
|
|
5095
|
+
collisions.push({ canonicalName, repos });
|
|
5096
|
+
for (const r of repos) collidingPaths.add(r);
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
const plans = [];
|
|
5100
|
+
const conflicts = [];
|
|
5101
|
+
const missingCanonical = [];
|
|
5102
|
+
const unreachable = [];
|
|
5103
|
+
for (const f of deduped) {
|
|
5104
|
+
if (f.isAnchor) continue;
|
|
5105
|
+
if (!f.reachable) {
|
|
5106
|
+
unreachable.push(f.path);
|
|
5107
|
+
continue;
|
|
5108
|
+
}
|
|
5109
|
+
if (!f.canonicalPresent) {
|
|
5110
|
+
missingCanonical.push(f.path);
|
|
5111
|
+
continue;
|
|
5112
|
+
}
|
|
5113
|
+
if (collidingPaths.has(f.path)) continue;
|
|
5114
|
+
const toCreate = [];
|
|
5115
|
+
for (const file of f.files) {
|
|
5116
|
+
if (file.state === "missing") {
|
|
5117
|
+
toCreate.push({ name: file.name, target: file.expectedTarget });
|
|
5118
|
+
} else if (file.state === "mismatch") {
|
|
5119
|
+
conflicts.push({
|
|
5120
|
+
repo: f.path,
|
|
5121
|
+
file: file.name,
|
|
5122
|
+
reason: "mismatch",
|
|
5123
|
+
...file.actualTarget !== void 0 ? { actualTarget: file.actualTarget } : {}
|
|
5124
|
+
});
|
|
5125
|
+
} else if (file.state === "occupied") {
|
|
5126
|
+
conflicts.push({ repo: f.path, file: file.name, reason: "occupied" });
|
|
5127
|
+
} else if (file.state === "blocked") {
|
|
5128
|
+
conflicts.push({ repo: f.path, file: file.name, reason: "blocked" });
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
if (toCreate.length > 0) plans.push({ path: f.path, toCreate });
|
|
5132
|
+
}
|
|
5133
|
+
return {
|
|
5134
|
+
plans,
|
|
5135
|
+
conflicts,
|
|
5136
|
+
missingCanonical,
|
|
5137
|
+
unreachable,
|
|
5138
|
+
collisions,
|
|
5139
|
+
ok: plans.length === 0 && conflicts.length === 0 && missingCanonical.length === 0 && unreachable.length === 0 && collisions.length === 0
|
|
5140
|
+
};
|
|
5141
|
+
}
|
|
5142
|
+
|
|
5143
|
+
// src/project/wiring.ts
|
|
5144
|
+
function isPublicFacing2(v) {
|
|
5145
|
+
return v === "public" || v === "future-public";
|
|
5146
|
+
}
|
|
5147
|
+
function summarizeWiring(facts) {
|
|
5148
|
+
const risks = [];
|
|
5149
|
+
const unknown = [];
|
|
5150
|
+
const incomplete = [];
|
|
5151
|
+
const unreachable = [];
|
|
5152
|
+
for (const f of facts) {
|
|
5153
|
+
if (!f.reachable) {
|
|
5154
|
+
unreachable.push(f.path);
|
|
5155
|
+
continue;
|
|
5156
|
+
}
|
|
5157
|
+
if (isPublicFacing2(f.visibility)) {
|
|
5158
|
+
for (const file of f.instructionFiles) {
|
|
5159
|
+
if (file.tracked) risks.push({ repo: f.path, visibility: f.visibility, file: file.name });
|
|
5160
|
+
}
|
|
5161
|
+
} else if (f.visibility === void 0) {
|
|
5162
|
+
unknown.push(f.path);
|
|
5163
|
+
}
|
|
5164
|
+
const missing = f.instructionFiles.filter((file) => !file.present).map((file) => file.name);
|
|
5165
|
+
if (missing.length > 0) incomplete.push({ repo: f.path, missing });
|
|
5166
|
+
}
|
|
5167
|
+
return {
|
|
5168
|
+
repos: facts,
|
|
5169
|
+
risks,
|
|
5170
|
+
unknown,
|
|
5171
|
+
incomplete,
|
|
5172
|
+
unreachable,
|
|
5173
|
+
ok: risks.length === 0 && unknown.length === 0 && unreachable.length === 0
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
// src/project/workspace-view.ts
|
|
5178
|
+
function planWorkspaceView(facts, existing = [], rosterNames = []) {
|
|
5179
|
+
const deduped = [];
|
|
5180
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
5181
|
+
for (const f of facts) {
|
|
5182
|
+
const key = normalizeRelativePath(f.path);
|
|
5183
|
+
if (seenPath.has(key)) continue;
|
|
5184
|
+
seenPath.add(key);
|
|
5185
|
+
deduped.push(f);
|
|
5186
|
+
}
|
|
5187
|
+
const byLinkName = /* @__PURE__ */ new Map();
|
|
5188
|
+
for (const f of deduped) {
|
|
5189
|
+
if (!f.reachable || f.linkName === void 0) continue;
|
|
5190
|
+
const repos = byLinkName.get(f.linkName) ?? [];
|
|
5191
|
+
repos.push(f.path);
|
|
5192
|
+
byLinkName.set(f.linkName, repos);
|
|
5193
|
+
}
|
|
5194
|
+
const collisions = [];
|
|
5195
|
+
const collidingPaths = /* @__PURE__ */ new Set();
|
|
5196
|
+
for (const [linkName, repos] of byLinkName) {
|
|
5197
|
+
if (repos.length > 1) {
|
|
5198
|
+
collisions.push({ linkName, repos });
|
|
5199
|
+
for (const r of repos) collidingPaths.add(r);
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
const toCreate = [];
|
|
5203
|
+
const conflicts = [];
|
|
5204
|
+
const unreachable = [];
|
|
5205
|
+
let correctCount = 0;
|
|
5206
|
+
for (const f of deduped) {
|
|
5207
|
+
if (!f.reachable) {
|
|
5208
|
+
unreachable.push(f.path);
|
|
5209
|
+
continue;
|
|
5210
|
+
}
|
|
5211
|
+
if (collidingPaths.has(f.path)) continue;
|
|
5212
|
+
if (f.linkName === void 0 || f.expectedTarget === void 0 || f.state === void 0) {
|
|
5213
|
+
continue;
|
|
5214
|
+
}
|
|
5215
|
+
if (f.state === "missing") {
|
|
5216
|
+
toCreate.push({ name: f.linkName, target: f.expectedTarget });
|
|
5217
|
+
} else if (f.state === "mismatch") {
|
|
5218
|
+
conflicts.push({
|
|
5219
|
+
name: f.linkName,
|
|
5220
|
+
reason: "mismatch",
|
|
5221
|
+
...f.actualTarget !== void 0 ? { actualTarget: f.actualTarget } : {}
|
|
5222
|
+
});
|
|
5223
|
+
} else if (f.state === "occupied") {
|
|
5224
|
+
conflicts.push({ name: f.linkName, reason: "occupied" });
|
|
5225
|
+
} else if (f.state === "blocked") {
|
|
5226
|
+
conflicts.push({ name: f.linkName, reason: "blocked" });
|
|
5227
|
+
} else {
|
|
5228
|
+
correctCount += 1;
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
const ownedNames = new Set(rosterNames);
|
|
5232
|
+
for (const f of deduped) {
|
|
5233
|
+
if (f.reachable && f.linkName !== void 0) ownedNames.add(f.linkName);
|
|
5234
|
+
}
|
|
5235
|
+
const toPrune = [];
|
|
5236
|
+
const strayUnknown = [];
|
|
5237
|
+
const seenExisting = /* @__PURE__ */ new Set();
|
|
5238
|
+
for (const e of existing) {
|
|
5239
|
+
if (ownedNames.has(e.name)) continue;
|
|
5240
|
+
if (seenExisting.has(e.name)) continue;
|
|
5241
|
+
seenExisting.add(e.name);
|
|
5242
|
+
if (e.kind === "repo") {
|
|
5243
|
+
toPrune.push({ name: e.name, target: e.target });
|
|
5244
|
+
} else {
|
|
5245
|
+
strayUnknown.push({ name: e.name, target: e.target, reason: e.kind });
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
return {
|
|
5249
|
+
toCreate,
|
|
5250
|
+
conflicts,
|
|
5251
|
+
collisions,
|
|
5252
|
+
unreachable,
|
|
5253
|
+
toPrune,
|
|
5254
|
+
strayUnknown,
|
|
5255
|
+
correctCount,
|
|
5256
|
+
ok: toCreate.length === 0 && conflicts.length === 0 && collisions.length === 0 && unreachable.length === 0 && toPrune.length === 0 && strayUnknown.length === 0
|
|
5257
|
+
};
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
// src/report/report-renderer.ts
|
|
5261
|
+
import { join as join16 } from "path";
|
|
5262
|
+
|
|
5263
|
+
// src/stats/work-stats.ts
|
|
5264
|
+
import { join as join15 } from "path";
|
|
5265
|
+
function resolveTimeZone(timeZone) {
|
|
5266
|
+
if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
|
|
5267
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
5268
|
+
}
|
|
5269
|
+
var STATUS_ORDER = [
|
|
5270
|
+
"completed",
|
|
5271
|
+
"failed",
|
|
5272
|
+
"running",
|
|
5273
|
+
"interrupted",
|
|
5274
|
+
"waiting_approval",
|
|
5275
|
+
"initialized",
|
|
5276
|
+
"imported",
|
|
5277
|
+
"archived"
|
|
5278
|
+
];
|
|
5279
|
+
async function computeWorkStats(input) {
|
|
5280
|
+
const { now } = input;
|
|
5281
|
+
const timeZone = resolveTimeZone(input.timeZone);
|
|
5282
|
+
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
5283
|
+
const wrappedSkip = (sid, reason) => {
|
|
5284
|
+
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
5285
|
+
input.onSessionSkip?.(sid, reason);
|
|
5286
|
+
};
|
|
5287
|
+
const loadOpts = { now, onSkip: wrappedSkip };
|
|
5288
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
5289
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
5290
|
+
const sessions = [];
|
|
5291
|
+
for (const entry of entries) {
|
|
5292
|
+
const events = [];
|
|
5293
|
+
let eventsUnreadable = false;
|
|
5294
|
+
try {
|
|
5295
|
+
for await (const ev of replayEvents(join15(input.paths.sessions, entry.sessionId), {
|
|
5296
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
5297
|
+
})) {
|
|
5298
|
+
events.push(ev);
|
|
5299
|
+
}
|
|
5300
|
+
} catch {
|
|
5301
|
+
eventsUnreadable = true;
|
|
5302
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
5303
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
sessions.push(
|
|
5307
|
+
sessionWorkStatsFromEvents(
|
|
5308
|
+
entry.sessionId,
|
|
5309
|
+
entry.session.session,
|
|
5310
|
+
events,
|
|
5311
|
+
now,
|
|
5312
|
+
eventsUnreadable
|
|
5313
|
+
)
|
|
5314
|
+
);
|
|
5315
|
+
}
|
|
5316
|
+
const allIntervals = [];
|
|
5317
|
+
for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
|
|
5318
|
+
const union = unionDurationMs(allIntervals);
|
|
5319
|
+
return {
|
|
5320
|
+
generatedAt: now.toISOString(),
|
|
5321
|
+
activeGapCapMs: ACTIVE_GAP_CAP_MS,
|
|
5322
|
+
timeZone,
|
|
5323
|
+
totals: computeTotals(sessions, union.ms),
|
|
5324
|
+
sessions,
|
|
5325
|
+
bySource: computeBySource(sessions),
|
|
5326
|
+
byStatus: computeByStatus(sessions),
|
|
5327
|
+
byDay: computeByDay(sessions, union.merged, timeZone)
|
|
5328
|
+
};
|
|
5329
|
+
}
|
|
5330
|
+
function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
|
|
5331
|
+
let commandCount = 0;
|
|
5332
|
+
let fileChangedCount = 0;
|
|
5333
|
+
let decisionCount = 0;
|
|
5334
|
+
let commandTimeMs = 0;
|
|
5335
|
+
const timestamps = [];
|
|
5336
|
+
for (const ev of events) {
|
|
5337
|
+
const t = Date.parse(ev.occurred_at);
|
|
5338
|
+
if (Number.isFinite(t)) timestamps.push(t);
|
|
5339
|
+
if (ev.type === "command_executed") {
|
|
5340
|
+
commandCount++;
|
|
5341
|
+
commandTimeMs += ev.duration_ms;
|
|
5342
|
+
} else if (ev.type === "file_changed") {
|
|
5343
|
+
fileChangedCount++;
|
|
5344
|
+
} else if (ev.type === "decision_recorded") {
|
|
5345
|
+
decisionCount++;
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
4182
5348
|
const span = computeSpan(inner.started_at, inner.ended_at, now);
|
|
4183
5349
|
const tokens = readTokens(inner.metrics);
|
|
4184
5350
|
const active = resolveActiveTime(inner.metrics, timestamps);
|
|
@@ -4413,7 +5579,7 @@ async function renderReport(input) {
|
|
|
4413
5579
|
const statsBySession = new Map(stats.sessions.map((s) => [s.sessionId, s]));
|
|
4414
5580
|
const decisions = [];
|
|
4415
5581
|
for (const entry of entries) {
|
|
4416
|
-
const sessionDir =
|
|
5582
|
+
const sessionDir = join16(input.paths.sessions, entry.sessionId);
|
|
4417
5583
|
try {
|
|
4418
5584
|
for await (const ev of replayEvents(sessionDir, {
|
|
4419
5585
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -4691,6 +5857,231 @@ function formatInt(n) {
|
|
|
4691
5857
|
return n.toLocaleString("en-US");
|
|
4692
5858
|
}
|
|
4693
5859
|
|
|
5860
|
+
// src/review/review-gaps.ts
|
|
5861
|
+
import { existsSync, realpathSync } from "fs";
|
|
5862
|
+
import { homedir as homedir2 } from "os";
|
|
5863
|
+
import { basename as basename2, isAbsolute, join as join17 } from "path";
|
|
5864
|
+
function stripQuotes(s) {
|
|
5865
|
+
if (s.length >= 2 && (s[0] === '"' && s.at(-1) === '"' || s[0] === "'" && s.at(-1) === "'")) {
|
|
5866
|
+
return s.slice(1, -1);
|
|
5867
|
+
}
|
|
5868
|
+
return s;
|
|
5869
|
+
}
|
|
5870
|
+
var realpathCache = /* @__PURE__ */ new Map();
|
|
5871
|
+
function resolveRealpath(absPath) {
|
|
5872
|
+
const cached = realpathCache.get(absPath);
|
|
5873
|
+
if (cached !== void 0) return cached;
|
|
5874
|
+
let resolved;
|
|
5875
|
+
try {
|
|
5876
|
+
resolved = realpathSync(absPath);
|
|
5877
|
+
} catch {
|
|
5878
|
+
resolved = null;
|
|
5879
|
+
}
|
|
5880
|
+
realpathCache.set(absPath, resolved);
|
|
5881
|
+
return resolved;
|
|
5882
|
+
}
|
|
5883
|
+
var repoRootCache = /* @__PURE__ */ new Map();
|
|
5884
|
+
function isRepoRoot(realPath) {
|
|
5885
|
+
const cached = repoRootCache.get(realPath);
|
|
5886
|
+
if (cached !== void 0) return cached;
|
|
5887
|
+
const result = existsSync(join17(realPath, ".git"));
|
|
5888
|
+
repoRootCache.set(realPath, result);
|
|
5889
|
+
return result;
|
|
5890
|
+
}
|
|
5891
|
+
function normalizeRepoPath(p) {
|
|
5892
|
+
if (!p) return null;
|
|
5893
|
+
let s = stripQuotes(p.trim()).replace(/\/+$/, "");
|
|
5894
|
+
if (s.length === 0 || s === "~") return null;
|
|
5895
|
+
if (s.startsWith("~/")) s = homedir2() + s.slice(1);
|
|
5896
|
+
if (isAbsolute(s)) {
|
|
5897
|
+
const real = resolveRealpath(s);
|
|
5898
|
+
if (real !== null) {
|
|
5899
|
+
return isRepoRoot(real) ? real : null;
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
s = s.replace(/\/[^/]*-workspace\/([^/]+)/, "/$1");
|
|
5903
|
+
const seg = s.split("/").filter((x) => x.length > 0).pop();
|
|
5904
|
+
if (seg === void 0) return null;
|
|
5905
|
+
if (/-workspace$/.test(seg) || seg.includes("$")) return null;
|
|
5906
|
+
return s;
|
|
5907
|
+
}
|
|
5908
|
+
function normalizeRepoKey(p) {
|
|
5909
|
+
const full = normalizeRepoPath(p);
|
|
5910
|
+
return full === null ? null : basename2(full);
|
|
5911
|
+
}
|
|
5912
|
+
function inspectCommand(args) {
|
|
5913
|
+
const a = args.join(" ");
|
|
5914
|
+
const files = /* @__PURE__ */ new Set();
|
|
5915
|
+
const examinedDiff = /\bgit\s+(?:diff|show|log\s+-p|add\s+-p)\b/.test(a);
|
|
5916
|
+
for (const re of [
|
|
5917
|
+
/\b(?:cat|less|bat|head|tail)\s+([^\s|&;<>]+)/g,
|
|
5918
|
+
/\bsed\s+-n\s+'[^']*'\s+([^\s|&;<>]+)/g,
|
|
5919
|
+
/\b(?:rg|grep)\b[^|&;]*?\s([^\s|&;<>]+\.[A-Za-z0-9]+)(?:\s|$)/g
|
|
5920
|
+
]) {
|
|
5921
|
+
let m;
|
|
5922
|
+
while ((m = re.exec(a)) !== null) {
|
|
5923
|
+
const f = m[1];
|
|
5924
|
+
if (f !== void 0) files.add(basename2(f));
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
return { files: [...files], examinedDiff };
|
|
5928
|
+
}
|
|
5929
|
+
function commandRepo(args, cwd) {
|
|
5930
|
+
const cd = args.join(" ").match(/\bcd\s+("[^"]+"|'[^']+'|[^\s&]+)\s*&&/);
|
|
5931
|
+
if (cd) return normalizeRepoPath(cd[1]);
|
|
5932
|
+
return normalizeRepoPath(cwd);
|
|
5933
|
+
}
|
|
5934
|
+
function commandFailed(exitCode) {
|
|
5935
|
+
return exitCode !== null && exitCode !== 0;
|
|
5936
|
+
}
|
|
5937
|
+
function commitFiles(args) {
|
|
5938
|
+
const a = args.join(" ");
|
|
5939
|
+
const add = a.match(/git add\s+([^&|;]+)/);
|
|
5940
|
+
if (!add?.[1]) return [];
|
|
5941
|
+
return add[1].split(/\s+/).filter((t) => /\.[A-Za-z]/.test(t) && !t.startsWith("-")).map((t) => basename2(t));
|
|
5942
|
+
}
|
|
5943
|
+
var REVIEW_SOURCE = "codex-import";
|
|
5944
|
+
var DEFAULT_WINDOW_HOURS = 24;
|
|
5945
|
+
async function findReviewGaps(input) {
|
|
5946
|
+
const now = new Date(input.nowIso);
|
|
5947
|
+
const windowHours = input.windowHours ?? DEFAULT_WINDOW_HOURS;
|
|
5948
|
+
const scope = input.scope && input.scope.length > 0 ? input.scope : null;
|
|
5949
|
+
const loadOpts = { now };
|
|
5950
|
+
if (input.onSessionSkip !== void 0) loadOpts.onSkip = input.onSessionSkip;
|
|
5951
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
5952
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
5953
|
+
const reviews = [];
|
|
5954
|
+
const workUnits = /* @__PURE__ */ new Map();
|
|
5955
|
+
const unknownCommits = /* @__PURE__ */ new Map();
|
|
5956
|
+
for (const entry of entries) {
|
|
5957
|
+
const sessionDir = join17(input.paths.sessions, entry.sessionId);
|
|
5958
|
+
const isReview = entry.session.session.source.kind === REVIEW_SOURCE;
|
|
5959
|
+
const reviewRepos = /* @__PURE__ */ new Map();
|
|
5960
|
+
let reviewEnd = null;
|
|
5961
|
+
try {
|
|
5962
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
5963
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
5964
|
+
})) {
|
|
5965
|
+
if (ev.type !== "command_executed") continue;
|
|
5966
|
+
if (commandFailed(ev.exit_code)) continue;
|
|
5967
|
+
const at = Date.parse(ev.occurred_at);
|
|
5968
|
+
if (isReview) {
|
|
5969
|
+
const repo2 = commandRepo(ev.args, ev.cwd);
|
|
5970
|
+
if (repo2 === null) continue;
|
|
5971
|
+
const ins = inspectCommand(ev.args);
|
|
5972
|
+
const slot = reviewRepos.get(repo2) ?? { examinedDiff: false, files: /* @__PURE__ */ new Set() };
|
|
5973
|
+
if (ins.examinedDiff) slot.examinedDiff = true;
|
|
5974
|
+
for (const f of ins.files) slot.files.add(f);
|
|
5975
|
+
reviewRepos.set(repo2, slot);
|
|
5976
|
+
if (!Number.isNaN(at)) reviewEnd = reviewEnd === null ? at : Math.max(reviewEnd, at);
|
|
5977
|
+
continue;
|
|
5978
|
+
}
|
|
5979
|
+
if (!ev.args.join(" ").includes("git commit")) continue;
|
|
5980
|
+
const repo = commandRepo(ev.args, ev.cwd);
|
|
5981
|
+
if (repo === null || Number.isNaN(at)) {
|
|
5982
|
+
const list2 = unknownCommits.get(entry.sessionId) ?? [];
|
|
5983
|
+
list2.push(Number.isNaN(at) ? null : at);
|
|
5984
|
+
unknownCommits.set(entry.sessionId, list2);
|
|
5985
|
+
continue;
|
|
5986
|
+
}
|
|
5987
|
+
const byRepo = workUnits.get(entry.sessionId) ?? /* @__PURE__ */ new Map();
|
|
5988
|
+
const list = byRepo.get(repo) ?? [];
|
|
5989
|
+
list.push({ repo, at, files: commitFiles(ev.args) });
|
|
5990
|
+
byRepo.set(repo, list);
|
|
5991
|
+
workUnits.set(entry.sessionId, byRepo);
|
|
5992
|
+
}
|
|
5993
|
+
} catch {
|
|
5994
|
+
input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
|
|
5995
|
+
continue;
|
|
5996
|
+
}
|
|
5997
|
+
if (isReview && reviewRepos.size > 0) {
|
|
5998
|
+
reviews.push({ sessionId: entry.sessionId, endedAt: reviewEnd, repos: reviewRepos });
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
const windowMs = windowHours * 3600 * 1e3;
|
|
6002
|
+
const units = [];
|
|
6003
|
+
let newestCommit = null;
|
|
6004
|
+
for (const [sessionId, byRepo] of workUnits) {
|
|
6005
|
+
for (const [repoPath, commits] of byRepo) {
|
|
6006
|
+
const label = basename2(repoPath);
|
|
6007
|
+
if (scope !== null && !scope.includes(label)) continue;
|
|
6008
|
+
const times = commits.map((c) => c.at).sort((a, b) => a - b);
|
|
6009
|
+
const first = times[0] ?? null;
|
|
6010
|
+
const last = times[times.length - 1] ?? null;
|
|
6011
|
+
if (last !== null) newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
|
|
6012
|
+
const changedFiles = new Set(commits.flatMap((c) => c.files));
|
|
6013
|
+
const before = first ?? last ?? 0;
|
|
6014
|
+
const nearby = reviews.filter((r) => {
|
|
6015
|
+
if (!r.repos.has(repoPath) || r.endedAt === null) return false;
|
|
6016
|
+
return r.endedAt <= before && r.endedAt >= before - windowMs;
|
|
6017
|
+
});
|
|
6018
|
+
const bound = nearby.filter((r) => {
|
|
6019
|
+
const touched = r.repos.get(repoPath);
|
|
6020
|
+
if (touched === void 0) return false;
|
|
6021
|
+
if (touched.examinedDiff) return true;
|
|
6022
|
+
for (const f of changedFiles) if (touched.files.has(f)) return true;
|
|
6023
|
+
return false;
|
|
6024
|
+
});
|
|
6025
|
+
const verdict = bound.length > 0 ? "candidate" : nearby.length > 0 ? "near_unbound" : "omission";
|
|
6026
|
+
const cited = verdict === "candidate" ? bound : verdict === "near_unbound" ? nearby : [];
|
|
6027
|
+
units.push({
|
|
6028
|
+
repo: label,
|
|
6029
|
+
sessionId,
|
|
6030
|
+
commitCount: commits.length,
|
|
6031
|
+
firstCommitAt: first === null ? null : new Date(first).toISOString(),
|
|
6032
|
+
lastCommitAt: last === null ? null : new Date(last).toISOString(),
|
|
6033
|
+
verdict,
|
|
6034
|
+
reviews: cited.map((r) => ({
|
|
6035
|
+
sessionId: r.sessionId,
|
|
6036
|
+
examinedDiff: r.repos.get(repoPath)?.examinedDiff ?? false,
|
|
6037
|
+
files: [...r.repos.get(repoPath)?.files ?? []].slice(0, 8),
|
|
6038
|
+
endedAt: r.endedAt === null ? null : new Date(r.endedAt).toISOString()
|
|
6039
|
+
}))
|
|
6040
|
+
});
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
if (scope === null) {
|
|
6044
|
+
for (const [sessionId, times] of unknownCommits) {
|
|
6045
|
+
const valid = times.filter((t) => t !== null).sort((a, b) => a - b);
|
|
6046
|
+
const first = valid[0] ?? null;
|
|
6047
|
+
const last = valid[valid.length - 1] ?? null;
|
|
6048
|
+
if (last !== null) newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
|
|
6049
|
+
units.push({
|
|
6050
|
+
repo: "(unknown)",
|
|
6051
|
+
sessionId,
|
|
6052
|
+
commitCount: times.length,
|
|
6053
|
+
firstCommitAt: first === null ? null : new Date(first).toISOString(),
|
|
6054
|
+
lastCommitAt: last === null ? null : new Date(last).toISOString(),
|
|
6055
|
+
verdict: "unknown",
|
|
6056
|
+
reviews: []
|
|
6057
|
+
});
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
const recentFirst = (a, b) => (Date.parse(b.lastCommitAt ?? "") || 0) - (Date.parse(a.lastCommitAt ?? "") || 0);
|
|
6061
|
+
const repoKeys = [...new Set(units.map((u) => u.repo))].sort();
|
|
6062
|
+
const repos = repoKeys.map((repo) => {
|
|
6063
|
+
const us = units.filter((u) => u.repo === repo);
|
|
6064
|
+
return {
|
|
6065
|
+
repo,
|
|
6066
|
+
units: us.length,
|
|
6067
|
+
omissionUnits: us.filter((u) => u.verdict === "omission").length,
|
|
6068
|
+
nearUnboundUnits: us.filter((u) => u.verdict === "near_unbound").length,
|
|
6069
|
+
candidateUnits: us.filter((u) => u.verdict === "candidate").length,
|
|
6070
|
+
unknownUnits: us.filter((u) => u.verdict === "unknown").length
|
|
6071
|
+
};
|
|
6072
|
+
});
|
|
6073
|
+
return {
|
|
6074
|
+
generatedAt: input.nowIso,
|
|
6075
|
+
windowHours,
|
|
6076
|
+
scope,
|
|
6077
|
+
repos,
|
|
6078
|
+
gaps: units.filter((u) => u.verdict === "omission" || u.verdict === "near_unbound").sort(recentFirst),
|
|
6079
|
+
candidates: units.filter((u) => u.verdict === "candidate").sort(recentFirst),
|
|
6080
|
+
unknowns: units.filter((u) => u.verdict === "unknown").sort(recentFirst),
|
|
6081
|
+
newestCommitAt: newestCommit === null ? null : new Date(newestCommit).toISOString()
|
|
6082
|
+
};
|
|
6083
|
+
}
|
|
6084
|
+
|
|
4694
6085
|
// src/runtime/child-process-runner.ts
|
|
4695
6086
|
import { spawn as spawn2 } from "child_process";
|
|
4696
6087
|
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
@@ -4818,55 +6209,6 @@ function classifySpawnError(error) {
|
|
|
4818
6209
|
// src/schemas/json-schema.ts
|
|
4819
6210
|
import { z as z11 } from "zod";
|
|
4820
6211
|
|
|
4821
|
-
// src/schemas/manifest.schema.ts
|
|
4822
|
-
import { z as z9 } from "zod";
|
|
4823
|
-
var ProjectSchema = z9.object({
|
|
4824
|
-
name: z9.string().optional(),
|
|
4825
|
-
description: z9.string().optional(),
|
|
4826
|
-
repository_url: z9.string().nullable().optional()
|
|
4827
|
-
});
|
|
4828
|
-
var CapabilitiesSchema = z9.object({
|
|
4829
|
-
enabled: z9.array(z9.string())
|
|
4830
|
-
});
|
|
4831
|
-
var ApprovalConfigSchema = z9.object({
|
|
4832
|
-
required_for: z9.array(z9.string()).optional(),
|
|
4833
|
-
default_risk_level: z9.enum(["low", "medium", "high", "critical"])
|
|
4834
|
-
});
|
|
4835
|
-
var ClaudeCodeAdapterConfigSchema = z9.object({
|
|
4836
|
-
enabled: z9.boolean(),
|
|
4837
|
-
config_path: z9.string().optional()
|
|
4838
|
-
});
|
|
4839
|
-
var AdaptersSchema = z9.object({
|
|
4840
|
-
"claude-code": ClaudeCodeAdapterConfigSchema
|
|
4841
|
-
});
|
|
4842
|
-
var GitConfigSchema = z9.object({
|
|
4843
|
-
events_log: z9.enum(["ignore", "commit"]).default("ignore")
|
|
4844
|
-
});
|
|
4845
|
-
var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)[^\0\\]+$/;
|
|
4846
|
-
var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
|
|
4847
|
-
message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
|
|
4848
|
-
});
|
|
4849
|
-
var ImportConfigSchema = z9.object({
|
|
4850
|
-
source_roots: z9.array(SourceRootSchema).min(1).optional()
|
|
4851
|
-
});
|
|
4852
|
-
var WorkspaceMetaSchema = z9.object({
|
|
4853
|
-
id: WorkspaceIdSchema,
|
|
4854
|
-
name: z9.string().min(1),
|
|
4855
|
-
created_at: IsoTimestampSchema,
|
|
4856
|
-
updated_at: IsoTimestampSchema
|
|
4857
|
-
});
|
|
4858
|
-
var ManifestSchema = z9.object({
|
|
4859
|
-
schema_version: SchemaVersionSchema,
|
|
4860
|
-
basou_version: z9.literal("0.1.0"),
|
|
4861
|
-
workspace: WorkspaceMetaSchema,
|
|
4862
|
-
project: ProjectSchema,
|
|
4863
|
-
capabilities: CapabilitiesSchema,
|
|
4864
|
-
approval: ApprovalConfigSchema,
|
|
4865
|
-
adapters: AdaptersSchema,
|
|
4866
|
-
git: GitConfigSchema,
|
|
4867
|
-
import: ImportConfigSchema.optional()
|
|
4868
|
-
});
|
|
4869
|
-
|
|
4870
6212
|
// src/schemas/session-import.schema.ts
|
|
4871
6213
|
import { z as z10 } from "zod";
|
|
4872
6214
|
var SessionInnerImportSchema = z10.object({
|
|
@@ -4986,28 +6328,29 @@ function serializeJsonSchema(schema) {
|
|
|
4986
6328
|
}
|
|
4987
6329
|
|
|
4988
6330
|
// src/storage/basou-dir.ts
|
|
4989
|
-
import { lstat as
|
|
4990
|
-
import { join as
|
|
6331
|
+
import { lstat as lstat4, mkdir as mkdir4 } from "fs/promises";
|
|
6332
|
+
import { join as join18 } from "path";
|
|
4991
6333
|
function basouPaths(repositoryRoot) {
|
|
4992
|
-
const root =
|
|
4993
|
-
const approvalsBase =
|
|
6334
|
+
const root = join18(repositoryRoot, ".basou");
|
|
6335
|
+
const approvalsBase = join18(root, "approvals");
|
|
4994
6336
|
return {
|
|
4995
6337
|
root,
|
|
4996
|
-
sessions:
|
|
4997
|
-
tasks:
|
|
6338
|
+
sessions: join18(root, "sessions"),
|
|
6339
|
+
tasks: join18(root, "tasks"),
|
|
4998
6340
|
approvals: {
|
|
4999
|
-
pending:
|
|
5000
|
-
resolved:
|
|
6341
|
+
pending: join18(approvalsBase, "pending"),
|
|
6342
|
+
resolved: join18(approvalsBase, "resolved")
|
|
5001
6343
|
},
|
|
5002
|
-
locks:
|
|
5003
|
-
logs:
|
|
5004
|
-
raw:
|
|
5005
|
-
tmp:
|
|
6344
|
+
locks: join18(root, "locks"),
|
|
6345
|
+
logs: join18(root, "logs"),
|
|
6346
|
+
raw: join18(root, "raw"),
|
|
6347
|
+
tmp: join18(root, "tmp"),
|
|
5006
6348
|
files: {
|
|
5007
|
-
manifest:
|
|
5008
|
-
status:
|
|
5009
|
-
handoff:
|
|
5010
|
-
decisions:
|
|
6349
|
+
manifest: join18(root, "manifest.yaml"),
|
|
6350
|
+
status: join18(root, "status.json"),
|
|
6351
|
+
handoff: join18(root, "handoff.md"),
|
|
6352
|
+
decisions: join18(root, "decisions.md"),
|
|
6353
|
+
orientation: join18(root, "orientation.md")
|
|
5011
6354
|
}
|
|
5012
6355
|
};
|
|
5013
6356
|
}
|
|
@@ -5025,9 +6368,9 @@ async function ensureBasouDirectory(repositoryRoot) {
|
|
|
5025
6368
|
const paths = basouPaths(repositoryRoot);
|
|
5026
6369
|
let existing;
|
|
5027
6370
|
try {
|
|
5028
|
-
existing = await
|
|
6371
|
+
existing = await lstat4(paths.root);
|
|
5029
6372
|
} catch (error) {
|
|
5030
|
-
if (!
|
|
6373
|
+
if (!hasErrorCode4(error) || error.code !== "ENOENT") {
|
|
5031
6374
|
throw new Error("Failed to inspect .basou directory", { cause: error });
|
|
5032
6375
|
}
|
|
5033
6376
|
}
|
|
@@ -5050,13 +6393,13 @@ async function mkdirLabeled(target, label) {
|
|
|
5050
6393
|
try {
|
|
5051
6394
|
await mkdir4(target, { recursive: true });
|
|
5052
6395
|
} catch (error) {
|
|
5053
|
-
if (
|
|
6396
|
+
if (hasErrorCode4(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
|
|
5054
6397
|
throw new Error(`${label} exists but is not a directory`, { cause: error });
|
|
5055
6398
|
}
|
|
5056
6399
|
throw new Error(`Failed to create ${label}`, { cause: error });
|
|
5057
6400
|
}
|
|
5058
6401
|
}
|
|
5059
|
-
function
|
|
6402
|
+
function hasErrorCode4(error) {
|
|
5060
6403
|
if (!(error instanceof Error)) return false;
|
|
5061
6404
|
const codeProp = error.code;
|
|
5062
6405
|
return typeof codeProp === "string";
|
|
@@ -5064,28 +6407,30 @@ function hasErrorCode3(error) {
|
|
|
5064
6407
|
|
|
5065
6408
|
// src/storage/gitignore.ts
|
|
5066
6409
|
import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
|
|
5067
|
-
import { join as
|
|
6410
|
+
import { join as join19 } from "path";
|
|
5068
6411
|
var MARKER = "# Basou - default ignore";
|
|
5069
|
-
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";
|
|
5070
|
-
|
|
5071
|
-
|
|
6412
|
+
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";
|
|
6413
|
+
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";
|
|
6414
|
+
async function appendBasouGitignore(repositoryRoot, options = {}) {
|
|
6415
|
+
const gitignorePath = join19(repositoryRoot, ".gitignore");
|
|
5072
6416
|
let body;
|
|
5073
6417
|
let existed;
|
|
5074
6418
|
try {
|
|
5075
6419
|
body = await readFile8(gitignorePath, "utf8");
|
|
5076
6420
|
existed = true;
|
|
5077
6421
|
} catch (error) {
|
|
5078
|
-
if (
|
|
6422
|
+
if (hasErrorCode5(error) && error.code === "ENOENT") {
|
|
5079
6423
|
body = "";
|
|
5080
6424
|
existed = false;
|
|
5081
6425
|
} else {
|
|
5082
6426
|
throw new Error("Failed to read .gitignore", { cause: error });
|
|
5083
6427
|
}
|
|
5084
6428
|
}
|
|
5085
|
-
if (existed &&
|
|
6429
|
+
if (existed && hasBasouGitignore(body)) {
|
|
5086
6430
|
return { appended: false };
|
|
5087
6431
|
}
|
|
5088
|
-
const
|
|
6432
|
+
const block = options.localOnly === true ? BASOU_GITIGNORE_BLOCK_LOCAL_ONLY : BASOU_GITIGNORE_BLOCK;
|
|
6433
|
+
const next = composeNextBody(body, block);
|
|
5089
6434
|
try {
|
|
5090
6435
|
await writeFile2(gitignorePath, next, { encoding: "utf8" });
|
|
5091
6436
|
} catch (error) {
|
|
@@ -5093,84 +6438,20 @@ async function appendBasouGitignore(repositoryRoot) {
|
|
|
5093
6438
|
}
|
|
5094
6439
|
return { appended: true };
|
|
5095
6440
|
}
|
|
5096
|
-
function
|
|
6441
|
+
function hasBasouGitignore(body) {
|
|
5097
6442
|
for (const rawLine of body.split("\n")) {
|
|
5098
|
-
|
|
6443
|
+
const line = rawLine.trimEnd();
|
|
6444
|
+
if (line.startsWith(MARKER)) return true;
|
|
6445
|
+
if (line === ".basou/" || line === "/.basou/") return true;
|
|
5099
6446
|
}
|
|
5100
6447
|
return false;
|
|
5101
6448
|
}
|
|
5102
|
-
function composeNextBody(existing) {
|
|
5103
|
-
if (existing.length === 0) return
|
|
6449
|
+
function composeNextBody(existing, block) {
|
|
6450
|
+
if (existing.length === 0) return block;
|
|
5104
6451
|
const normalized = existing.endsWith("\n") ? existing : `${existing}
|
|
5105
6452
|
`;
|
|
5106
6453
|
return `${normalized}
|
|
5107
|
-
${
|
|
5108
|
-
}
|
|
5109
|
-
function hasErrorCode4(error) {
|
|
5110
|
-
if (!(error instanceof Error)) return false;
|
|
5111
|
-
return typeof error.code === "string";
|
|
5112
|
-
}
|
|
5113
|
-
|
|
5114
|
-
// src/storage/manifest.ts
|
|
5115
|
-
import { lstat as lstat4 } from "fs/promises";
|
|
5116
|
-
function createManifest(input) {
|
|
5117
|
-
if (input.workspaceName.length === 0) {
|
|
5118
|
-
throw new Error("Workspace name is empty. Pass --name explicitly.");
|
|
5119
|
-
}
|
|
5120
|
-
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
5121
|
-
const workspaceId = input.workspaceId ?? prefixedUlid("ws");
|
|
5122
|
-
const project = {
|
|
5123
|
-
...input.projectName !== void 0 ? { name: input.projectName } : {},
|
|
5124
|
-
...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
|
|
5125
|
-
...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
|
|
5126
|
-
};
|
|
5127
|
-
const manifest = {
|
|
5128
|
-
schema_version: "0.1.0",
|
|
5129
|
-
basou_version: "0.1.0",
|
|
5130
|
-
workspace: {
|
|
5131
|
-
id: workspaceId,
|
|
5132
|
-
name: input.workspaceName,
|
|
5133
|
-
created_at: now,
|
|
5134
|
-
updated_at: now
|
|
5135
|
-
},
|
|
5136
|
-
project,
|
|
5137
|
-
capabilities: {
|
|
5138
|
-
enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
|
|
5139
|
-
},
|
|
5140
|
-
approval: {
|
|
5141
|
-
required_for: ["destructive_command", "external_send"],
|
|
5142
|
-
default_risk_level: "medium"
|
|
5143
|
-
},
|
|
5144
|
-
adapters: {
|
|
5145
|
-
"claude-code": { enabled: true }
|
|
5146
|
-
},
|
|
5147
|
-
git: { events_log: "ignore" },
|
|
5148
|
-
...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
|
|
5149
|
-
};
|
|
5150
|
-
return ManifestSchema.parse(manifest);
|
|
5151
|
-
}
|
|
5152
|
-
async function writeManifest(paths, manifest, options) {
|
|
5153
|
-
const force = options?.force === true;
|
|
5154
|
-
const validated = ManifestSchema.parse(manifest);
|
|
5155
|
-
if (!force) {
|
|
5156
|
-
let existed = false;
|
|
5157
|
-
try {
|
|
5158
|
-
await lstat4(paths.files.manifest);
|
|
5159
|
-
existed = true;
|
|
5160
|
-
} catch (error) {
|
|
5161
|
-
if (!hasErrorCode5(error) || error.code !== "ENOENT") {
|
|
5162
|
-
throw new Error("Failed to inspect existing manifest", { cause: error });
|
|
5163
|
-
}
|
|
5164
|
-
}
|
|
5165
|
-
if (existed) {
|
|
5166
|
-
throw new Error("Already initialized. Use --force to overwrite.");
|
|
5167
|
-
}
|
|
5168
|
-
}
|
|
5169
|
-
await writeYamlFile(paths.files.manifest, validated);
|
|
5170
|
-
}
|
|
5171
|
-
async function readManifest(paths) {
|
|
5172
|
-
const raw = await readYamlFile(paths.files.manifest);
|
|
5173
|
-
return ManifestSchema.parse(raw);
|
|
6454
|
+
${block}`;
|
|
5174
6455
|
}
|
|
5175
6456
|
function hasErrorCode5(error) {
|
|
5176
6457
|
if (!(error instanceof Error)) return false;
|
|
@@ -5282,8 +6563,8 @@ function hasErrorCode6(error) {
|
|
|
5282
6563
|
|
|
5283
6564
|
// src/storage/session-import.ts
|
|
5284
6565
|
import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
|
|
5285
|
-
import { homedir as
|
|
5286
|
-
import { join as
|
|
6566
|
+
import { homedir as homedir3 } from "os";
|
|
6567
|
+
import { join as join20 } from "path";
|
|
5287
6568
|
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
5288
6569
|
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
5289
6570
|
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
@@ -5308,7 +6589,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
5308
6589
|
pathSanitizeReport
|
|
5309
6590
|
};
|
|
5310
6591
|
}
|
|
5311
|
-
const sessionDir =
|
|
6592
|
+
const sessionDir = join20(paths.sessions, newSessionId);
|
|
5312
6593
|
try {
|
|
5313
6594
|
await mkdir5(sessionDir, { recursive: true });
|
|
5314
6595
|
} catch (error) {
|
|
@@ -5322,7 +6603,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
5322
6603
|
throw error;
|
|
5323
6604
|
}
|
|
5324
6605
|
try {
|
|
5325
|
-
const sessionYamlPath =
|
|
6606
|
+
const sessionYamlPath = join20(sessionDir, "session.yaml");
|
|
5326
6607
|
await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
|
|
5327
6608
|
} catch (error) {
|
|
5328
6609
|
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
@@ -5391,7 +6672,7 @@ function withIntegrity(record, chainResult) {
|
|
|
5391
6672
|
};
|
|
5392
6673
|
}
|
|
5393
6674
|
function buildSessionRecord(input, manifest, newSessionId, options) {
|
|
5394
|
-
const home =
|
|
6675
|
+
const home = homedir3();
|
|
5395
6676
|
const workingDirectoryRaw = input.working_directory;
|
|
5396
6677
|
const workingDirectorySanitized = sanitizeWorkingDirectory(workingDirectoryRaw, {
|
|
5397
6678
|
homedir: home
|
|
@@ -5490,7 +6771,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
|
|
|
5490
6771
|
async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
|
|
5491
6772
|
const sessionId = priorSessionId;
|
|
5492
6773
|
const importSource = freshPayload.session.source.kind;
|
|
5493
|
-
const sessionDir =
|
|
6774
|
+
const sessionDir = join20(paths.sessions, priorSessionId);
|
|
5494
6775
|
const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
|
|
5495
6776
|
try {
|
|
5496
6777
|
const priorVerdict = await verifyEventsChain(paths, priorSessionId);
|
|
@@ -5532,7 +6813,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5532
6813
|
};
|
|
5533
6814
|
const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
|
|
5534
6815
|
if (options.dryRun !== true) {
|
|
5535
|
-
const eventsPath =
|
|
6816
|
+
const eventsPath = join20(sessionDir, "events.jsonl");
|
|
5536
6817
|
let priorEventsRaw = null;
|
|
5537
6818
|
try {
|
|
5538
6819
|
priorEventsRaw = await readFile10(eventsPath);
|
|
@@ -5544,7 +6825,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5544
6825
|
const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
|
|
5545
6826
|
try {
|
|
5546
6827
|
await overwriteYamlFile(
|
|
5547
|
-
|
|
6828
|
+
join20(sessionDir, "session.yaml"),
|
|
5548
6829
|
withIntegrity(updatedRecord, chainResult)
|
|
5549
6830
|
);
|
|
5550
6831
|
} catch (error) {
|
|
@@ -5568,7 +6849,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5568
6849
|
}
|
|
5569
6850
|
}
|
|
5570
6851
|
async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
5571
|
-
const sessionDir =
|
|
6852
|
+
const sessionDir = join20(paths.sessions, sessionId);
|
|
5572
6853
|
let lock;
|
|
5573
6854
|
try {
|
|
5574
6855
|
lock = await acquireLock(paths, "session", sessionId);
|
|
@@ -5601,7 +6882,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
5601
6882
|
if (verdict.status !== "unchained") {
|
|
5602
6883
|
return { status: "skipped", reason: "tampered" };
|
|
5603
6884
|
}
|
|
5604
|
-
const eventsPath =
|
|
6885
|
+
const eventsPath = join20(sessionDir, "events.jsonl");
|
|
5605
6886
|
let priorRaw;
|
|
5606
6887
|
try {
|
|
5607
6888
|
priorRaw = await readFile10(eventsPath);
|
|
@@ -5649,7 +6930,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
5649
6930
|
}
|
|
5650
6931
|
try {
|
|
5651
6932
|
await overwriteYamlFile(
|
|
5652
|
-
|
|
6933
|
+
join20(sessionDir, "session.yaml"),
|
|
5653
6934
|
withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
|
|
5654
6935
|
);
|
|
5655
6936
|
} catch (error) {
|
|
@@ -5731,23 +7012,34 @@ export {
|
|
|
5731
7012
|
enumerateTaskIds,
|
|
5732
7013
|
finalizeSessionYaml,
|
|
5733
7014
|
findErrorCode,
|
|
7015
|
+
findReviewGaps,
|
|
5734
7016
|
formatDurationMs,
|
|
5735
7017
|
genesisHash,
|
|
5736
7018
|
getDiff,
|
|
5737
7019
|
getSnapshot,
|
|
5738
7020
|
importSessionFromJson,
|
|
5739
7021
|
inspectChainTail,
|
|
7022
|
+
isGitNotFound,
|
|
5740
7023
|
isImportDerivedSource,
|
|
5741
7024
|
isLazyExpired,
|
|
7025
|
+
isRenderable,
|
|
5742
7026
|
isValidPrefixedId,
|
|
5743
7027
|
lineHash,
|
|
5744
7028
|
linkYamlFile,
|
|
5745
7029
|
loadApproval,
|
|
5746
7030
|
loadSessionEntries,
|
|
5747
7031
|
loadTaskEntries,
|
|
7032
|
+
normalizeRepoKey,
|
|
7033
|
+
normalizeRepoPath,
|
|
5748
7034
|
overwriteYamlFile,
|
|
5749
7035
|
parseDuration,
|
|
5750
7036
|
parseMarkers,
|
|
7037
|
+
pathBasename,
|
|
7038
|
+
planArchive,
|
|
7039
|
+
planGitignore,
|
|
7040
|
+
planRename,
|
|
7041
|
+
planRosterAdoption,
|
|
7042
|
+
planWorkspaceView,
|
|
5751
7043
|
prefixedUlid,
|
|
5752
7044
|
readAllEvents,
|
|
5753
7045
|
readManifest,
|
|
@@ -5759,18 +7051,23 @@ export {
|
|
|
5759
7051
|
readYamlFile,
|
|
5760
7052
|
rechainSessionInPlace,
|
|
5761
7053
|
reconcileAllTasks,
|
|
7054
|
+
reconcileSourceRoots,
|
|
5762
7055
|
reconcileTask,
|
|
5763
7056
|
refreshTaskLinkedSessions,
|
|
5764
7057
|
reimportPreservingId,
|
|
5765
7058
|
renderDecisions,
|
|
5766
7059
|
renderHandoff,
|
|
7060
|
+
renderOrientation,
|
|
7061
|
+
renderPresetBlock,
|
|
5767
7062
|
renderReport,
|
|
5768
7063
|
renderWithMarkers,
|
|
5769
7064
|
replayEvents,
|
|
7065
|
+
resolveBasouRepositoryRoot,
|
|
5770
7066
|
resolveClaudeCodeCommand,
|
|
5771
7067
|
resolveRepositoryRoot,
|
|
5772
7068
|
resolveSessionId,
|
|
5773
7069
|
resolveTaskId,
|
|
7070
|
+
safeSimpleGit,
|
|
5774
7071
|
sanitizePath,
|
|
5775
7072
|
sanitizeRelatedFiles,
|
|
5776
7073
|
sanitizeWorkingDirectory,
|
|
@@ -5778,8 +7075,14 @@ export {
|
|
|
5778
7075
|
serializeJsonSchema,
|
|
5779
7076
|
sessionWorkStatsFromEvents,
|
|
5780
7077
|
summarizeAdapterOutput,
|
|
7078
|
+
summarizeOrientation,
|
|
7079
|
+
summarizePresetPlan,
|
|
7080
|
+
summarizeRosterDrift,
|
|
7081
|
+
summarizeSymlinkPlan,
|
|
7082
|
+
summarizeWiring,
|
|
5781
7083
|
tryRemoteUrl,
|
|
5782
7084
|
ulid,
|
|
7085
|
+
unknownManifestKeys,
|
|
5783
7086
|
updateTaskStatusWithEvent,
|
|
5784
7087
|
verifyEventsChain,
|
|
5785
7088
|
writeEventsBulk,
|