@basou/cli 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +919 -409
- package/dist/index.js.map +1 -1
- package/dist/program.js +919 -409
- package/dist/program.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -2034,11 +2034,73 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
2034
2034
|
}
|
|
2035
2035
|
|
|
2036
2036
|
// src/commands/hook.ts
|
|
2037
|
-
import { open, readFile as readFile2, stat as
|
|
2037
|
+
import { open as open2, readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
2038
|
+
import { homedir as homedir4 } from "os";
|
|
2039
|
+
import { join as join6 } from "path";
|
|
2040
|
+
import { fileURLToPath } from "url";
|
|
2038
2041
|
import {
|
|
2042
|
+
buildStopHookCommand,
|
|
2039
2043
|
DEFAULT_STOP_HOOK_MIN_EDITS,
|
|
2040
|
-
evaluateStopHook
|
|
2044
|
+
evaluateStopHook,
|
|
2045
|
+
findBasouStopHookCommand,
|
|
2046
|
+
removeStopHook,
|
|
2047
|
+
upsertStopHook
|
|
2041
2048
|
} from "@basou/core";
|
|
2049
|
+
|
|
2050
|
+
// src/lib/durable-write.ts
|
|
2051
|
+
import { randomUUID } from "crypto";
|
|
2052
|
+
import { lstat, open, rename, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
2053
|
+
import { basename as basename2, dirname, join as join5 } from "path";
|
|
2054
|
+
async function assertNotSymlink(targetPath) {
|
|
2055
|
+
try {
|
|
2056
|
+
const st = await lstat(targetPath);
|
|
2057
|
+
if (st.isSymbolicLink()) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
"Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
if (error instanceof Error && error.code === "ENOENT") return;
|
|
2064
|
+
throw error;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
async function writeFileDurable(targetPath, content) {
|
|
2068
|
+
const dir = dirname(targetPath);
|
|
2069
|
+
const tmpPath = join5(dir, `.${basename2(targetPath)}.tmp.${randomUUID()}`);
|
|
2070
|
+
let mode = 420;
|
|
2071
|
+
try {
|
|
2072
|
+
mode = (await stat2(targetPath)).mode & 511;
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
if (!(error instanceof Error && error.code === "ENOENT")) {
|
|
2075
|
+
throw error;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
let handle;
|
|
2079
|
+
try {
|
|
2080
|
+
handle = await open(tmpPath, "wx", mode);
|
|
2081
|
+
await handle.writeFile(content, "utf8");
|
|
2082
|
+
await handle.chmod(mode);
|
|
2083
|
+
await handle.sync();
|
|
2084
|
+
await handle.close();
|
|
2085
|
+
handle = void 0;
|
|
2086
|
+
await rename(tmpPath, targetPath);
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
if (handle) await handle.close().catch(() => void 0);
|
|
2089
|
+
await unlink2(tmpPath).catch(() => void 0);
|
|
2090
|
+
throw error;
|
|
2091
|
+
}
|
|
2092
|
+
try {
|
|
2093
|
+
const dirHandle = await open(dir, "r");
|
|
2094
|
+
try {
|
|
2095
|
+
await dirHandle.sync();
|
|
2096
|
+
} finally {
|
|
2097
|
+
await dirHandle.close();
|
|
2098
|
+
}
|
|
2099
|
+
} catch {
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/commands/hook.ts
|
|
2042
2104
|
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
2043
2105
|
function registerHookCommand(program2) {
|
|
2044
2106
|
const hook = program2.command("hook").description(
|
|
@@ -2049,29 +2111,58 @@ function registerHookCommand(program2) {
|
|
|
2049
2111
|
).option(
|
|
2050
2112
|
"--min-edits <n>",
|
|
2051
2113
|
`Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
|
|
2114
|
+
).option(
|
|
2115
|
+
"--block",
|
|
2116
|
+
"Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking reminder"
|
|
2117
|
+
).option(
|
|
2118
|
+
"--require-review",
|
|
2119
|
+
"Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
|
|
2052
2120
|
).addHelpText("after", HOOK_STOP_HELP).action(async (options) => {
|
|
2053
2121
|
const minEdits = parseMinEdits(options.minEdits);
|
|
2054
|
-
await runHookStop(
|
|
2122
|
+
await runHookStop({
|
|
2123
|
+
...minEdits !== void 0 ? { minEdits } : {},
|
|
2124
|
+
...options.block === true ? { block: true } : {},
|
|
2125
|
+
...options.requireReview === true ? { requireReview: true } : {}
|
|
2126
|
+
});
|
|
2127
|
+
});
|
|
2128
|
+
hook.command("install").description(
|
|
2129
|
+
"Register the Stop hook in ~/.claude/settings.json (reproducible, idempotent). Default is advisory capture-only; --block opts into in-turn enforcement, --require-review opts into the review gate."
|
|
2130
|
+
).option("--block", "Register the blocking (opt-in enforcement) form instead of advisory").option("--require-review", "Register with the opt-in review gate enabled").option("--min-edits <n>", "Pass a custom file-edit threshold to the registered hook").option("--settings <path>", "Override the settings.json path (intended for tests)").option("--dry-run", "Print what would change without writing").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2131
|
+
await runHookInstall(opts);
|
|
2132
|
+
});
|
|
2133
|
+
hook.command("uninstall").description(
|
|
2134
|
+
"Remove the basou Stop hook from ~/.claude/settings.json (leaves other hooks intact)"
|
|
2135
|
+
).option("--settings <path>", "Override the settings.json path (intended for tests)").option("--dry-run", "Print what would change without writing").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2136
|
+
await runHookUninstall(opts);
|
|
2137
|
+
});
|
|
2138
|
+
hook.command("status").description("Report whether the basou Stop hook is registered, and in which mode").option("--settings <path>", "Override the settings.json path (intended for tests)").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2139
|
+
await runHookStatus(opts);
|
|
2055
2140
|
});
|
|
2056
2141
|
}
|
|
2057
2142
|
var HOOK_STOP_HELP = `
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
"Stop": [
|
|
2062
|
-
{ "hooks": [ { "type": "command", "command": "basou hook stop" } ] }
|
|
2063
|
-
]
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2143
|
+
Register this Stop hook reproducibly with 'basou hook install' (it writes the
|
|
2144
|
+
correct node-path command into ~/.claude/settings.json). 'basou hook uninstall'
|
|
2145
|
+
removes it; 'basou hook status' reports whether it is registered.
|
|
2066
2146
|
|
|
2067
2147
|
On every turn end basou inspects the session transcript. If the session did
|
|
2068
2148
|
content-substantive work but ran no capture verb ('basou decision capture' /
|
|
2069
|
-
'decision record' / 'note'), it
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2149
|
+
'decision record' / 'note'), it reminds the agent to record the why / next step.
|
|
2150
|
+
Substantive = EITHER >= ${DEFAULT_STOP_HOOK_MIN_EDITS} file edits (default) OR a free-form AskUserQuestion
|
|
2151
|
+
answer (an uncaptured conversational decision). Read-only Bash (ls / grep /
|
|
2152
|
+
git status) does NOT count.
|
|
2153
|
+
|
|
2154
|
+
With --require-review (opt-in, 'basou hook install --require-review') it also
|
|
2155
|
+
reminds when the session SHIPPED substantive code (git push / git merge /
|
|
2156
|
+
gh pr create|merge) without recording a review ('basou review record'). This
|
|
2157
|
+
gate is off by default; when on, its reminder is composed into the same
|
|
2158
|
+
envelope as the capture reminder.
|
|
2159
|
+
|
|
2160
|
+
By default the reminder is non-blocking: Claude sees it and may act on it or
|
|
2161
|
+
stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
|
|
2162
|
+
returns decision:block, holding the agent in-turn to act on the reminder; the
|
|
2163
|
+
'stop_hook_active' flag and Claude Code's own loop prevention bound it to a
|
|
2164
|
+
single turn. Either way the hook fails open: a bad payload or unreadable
|
|
2165
|
+
transcript exits cleanly with no output.
|
|
2075
2166
|
`;
|
|
2076
2167
|
async function runHookStop(options, ctx = {}) {
|
|
2077
2168
|
try {
|
|
@@ -2109,16 +2200,21 @@ async function doRunHookStop(options, ctx) {
|
|
|
2109
2200
|
stopHookActive: false,
|
|
2110
2201
|
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
2111
2202
|
});
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2203
|
+
const parts = [];
|
|
2204
|
+
if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
|
|
2205
|
+
if (options.requireReview === true && evaluation.review.fires) {
|
|
2206
|
+
parts.push(evaluation.review.additionalContext);
|
|
2207
|
+
}
|
|
2208
|
+
if (parts.length === 0) return;
|
|
2209
|
+
const reason = parts.join("\n\n");
|
|
2210
|
+
const payloadJson = options.block === true ? JSON.stringify({ decision: "block", reason }) : JSON.stringify({
|
|
2211
|
+
hookSpecificOutput: {
|
|
2212
|
+
hookEventName: "Stop",
|
|
2213
|
+
additionalContext: reason
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
write(`${payloadJson}
|
|
2217
|
+
`);
|
|
2122
2218
|
}
|
|
2123
2219
|
function parseTranscript(transcript) {
|
|
2124
2220
|
const records = [];
|
|
@@ -2143,9 +2239,9 @@ async function defaultReadStdin() {
|
|
|
2143
2239
|
return Buffer.concat(chunks).toString("utf8");
|
|
2144
2240
|
}
|
|
2145
2241
|
async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
|
|
2146
|
-
const { size } = await
|
|
2242
|
+
const { size } = await stat3(path);
|
|
2147
2243
|
if (size <= maxBytes) return readFile2(path, "utf8");
|
|
2148
|
-
const handle = await
|
|
2244
|
+
const handle = await open2(path, "r");
|
|
2149
2245
|
try {
|
|
2150
2246
|
const buffer = Buffer.alloc(maxBytes);
|
|
2151
2247
|
const { bytesRead } = await handle.read(buffer, 0, maxBytes, size - maxBytes);
|
|
@@ -2160,12 +2256,173 @@ function parseMinEdits(raw) {
|
|
|
2160
2256
|
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
2161
2257
|
return Number(raw);
|
|
2162
2258
|
}
|
|
2259
|
+
var DEFAULT_CLAUDE_SETTINGS_PATH = join6(homedir4(), ".claude", "settings.json");
|
|
2260
|
+
function resolveCliEntry() {
|
|
2261
|
+
return fileURLToPath(import.meta.url);
|
|
2262
|
+
}
|
|
2263
|
+
function normalizeInstallOptions(raw) {
|
|
2264
|
+
const out = {};
|
|
2265
|
+
if (raw.block === true) out.block = true;
|
|
2266
|
+
if (raw.requireReview === true) out.requireReview = true;
|
|
2267
|
+
if (raw.settings !== void 0) out.settings = raw.settings;
|
|
2268
|
+
if (raw.dryRun === true) out.dryRun = true;
|
|
2269
|
+
if (raw.verbose === true) out.verbose = true;
|
|
2270
|
+
if (raw.minEdits !== void 0) {
|
|
2271
|
+
const parsed = parseMinEdits(raw.minEdits);
|
|
2272
|
+
if (parsed === void 0) {
|
|
2273
|
+
throw new Error("--min-edits must be a non-negative integer.");
|
|
2274
|
+
}
|
|
2275
|
+
out.minEdits = parsed;
|
|
2276
|
+
}
|
|
2277
|
+
return out;
|
|
2278
|
+
}
|
|
2279
|
+
async function readSettings(path) {
|
|
2280
|
+
let raw;
|
|
2281
|
+
try {
|
|
2282
|
+
raw = await readFile2(path, "utf8");
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
2285
|
+
return { raw: null, parsed: void 0 };
|
|
2286
|
+
}
|
|
2287
|
+
throw error;
|
|
2288
|
+
}
|
|
2289
|
+
if (raw.trim().length === 0) return { raw, parsed: void 0 };
|
|
2290
|
+
try {
|
|
2291
|
+
return { raw, parsed: JSON.parse(raw) };
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
throw new Error(
|
|
2294
|
+
"The Claude settings.json is not valid JSON. Fix it (or remove it) and retry.",
|
|
2295
|
+
{
|
|
2296
|
+
cause: error
|
|
2297
|
+
}
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
async function backupSettingsOnce(path, raw) {
|
|
2302
|
+
if (raw === null) return;
|
|
2303
|
+
const bak = `${path}.basou-bak`;
|
|
2304
|
+
try {
|
|
2305
|
+
await stat3(bak);
|
|
2306
|
+
return;
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
if (!(error instanceof Error && error.code === "ENOENT")) throw error;
|
|
2309
|
+
}
|
|
2310
|
+
await writeFileDurable(bak, raw);
|
|
2311
|
+
}
|
|
2312
|
+
async function runHookInstall(options, ctx = {}) {
|
|
2313
|
+
try {
|
|
2314
|
+
await doRunHookInstall(normalizeInstallOptions(options), ctx);
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2317
|
+
process.exitCode = 1;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
async function doRunHookInstall(options, ctx = {}) {
|
|
2321
|
+
const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
|
|
2322
|
+
const cliEntry = (ctx.resolveCliEntry ?? resolveCliEntry)();
|
|
2323
|
+
const command = buildStopHookCommand({
|
|
2324
|
+
cliEntry,
|
|
2325
|
+
...options.block === true ? { block: true } : {},
|
|
2326
|
+
...options.requireReview === true ? { requireReview: true } : {},
|
|
2327
|
+
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
2328
|
+
});
|
|
2329
|
+
const mode = describeHookMode({
|
|
2330
|
+
block: options.block === true,
|
|
2331
|
+
review: options.requireReview === true
|
|
2332
|
+
});
|
|
2333
|
+
await assertNotSymlink(settingsPath);
|
|
2334
|
+
const { raw, parsed } = await readSettings(settingsPath);
|
|
2335
|
+
const { settings, action } = upsertStopHook(parsed, command);
|
|
2336
|
+
const newBody = `${JSON.stringify(settings, null, 2)}
|
|
2337
|
+
`;
|
|
2338
|
+
if (raw !== null && newBody === raw) {
|
|
2339
|
+
console.log(`The basou Stop hook is already registered (${mode}); no change.`);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
if (options.dryRun === true) {
|
|
2343
|
+
console.log(`[dry-run] Would ${action} the basou Stop hook (${mode}).`);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const recheck = await readSettings(settingsPath);
|
|
2347
|
+
if (recheck.raw !== raw) {
|
|
2348
|
+
throw new Error(
|
|
2349
|
+
"The settings.json changed during install; aborting so a concurrent edit is not overwritten. Re-run 'basou hook install'."
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
await backupSettingsOnce(settingsPath, raw);
|
|
2353
|
+
await writeFileDurable(settingsPath, newBody);
|
|
2354
|
+
console.log(`${action === "installed" ? "Installed" : "Updated"} the basou Stop hook (${mode}).`);
|
|
2355
|
+
}
|
|
2356
|
+
async function runHookUninstall(options) {
|
|
2357
|
+
try {
|
|
2358
|
+
await doRunHookUninstall(normalizeInstallOptions(options));
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2361
|
+
process.exitCode = 1;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
async function doRunHookUninstall(options) {
|
|
2365
|
+
const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
|
|
2366
|
+
await assertNotSymlink(settingsPath);
|
|
2367
|
+
const { raw, parsed } = await readSettings(settingsPath);
|
|
2368
|
+
if (raw === null) {
|
|
2369
|
+
console.log("No settings.json; nothing to remove.");
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const { settings, action } = removeStopHook(parsed);
|
|
2373
|
+
if (action === "absent") {
|
|
2374
|
+
console.log("No basou Stop hook found; nothing removed.");
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
const newBody = `${JSON.stringify(settings, null, 2)}
|
|
2378
|
+
`;
|
|
2379
|
+
if (options.dryRun === true) {
|
|
2380
|
+
console.log("[dry-run] Would remove the basou Stop hook from settings.json.");
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const recheck = await readSettings(settingsPath);
|
|
2384
|
+
if (recheck.raw !== raw) {
|
|
2385
|
+
throw new Error(
|
|
2386
|
+
"The settings.json changed during uninstall; aborting so a concurrent edit is not overwritten. Re-run 'basou hook uninstall'."
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
await backupSettingsOnce(settingsPath, raw);
|
|
2390
|
+
await writeFileDurable(settingsPath, newBody);
|
|
2391
|
+
console.log("Removed the basou Stop hook from settings.json.");
|
|
2392
|
+
}
|
|
2393
|
+
async function runHookStatus(options) {
|
|
2394
|
+
try {
|
|
2395
|
+
await doRunHookStatus(normalizeInstallOptions(options));
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2398
|
+
process.exitCode = 1;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
async function doRunHookStatus(options) {
|
|
2402
|
+
const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
|
|
2403
|
+
const { parsed } = await readSettings(settingsPath);
|
|
2404
|
+
const command = findBasouStopHookCommand(parsed);
|
|
2405
|
+
if (command === null) {
|
|
2406
|
+
console.log("basou Stop hook: not registered. Run 'basou hook install' to register it.");
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
const mode = describeHookMode({
|
|
2410
|
+
block: / --block\b/.test(command),
|
|
2411
|
+
review: / --require-review\b/.test(command)
|
|
2412
|
+
});
|
|
2413
|
+
console.log(`basou Stop hook: registered, ${mode}.`);
|
|
2414
|
+
}
|
|
2415
|
+
function describeHookMode(tiers) {
|
|
2416
|
+
const enforcement = tiers.block ? "blocking (opt-in enforcement)" : "advisory (non-blocking)";
|
|
2417
|
+
const gates = tiers.review ? "capture + review" : "capture";
|
|
2418
|
+
return `${enforcement}, ${gates}`;
|
|
2419
|
+
}
|
|
2163
2420
|
|
|
2164
2421
|
// src/commands/import.ts
|
|
2165
2422
|
import { createReadStream } from "fs";
|
|
2166
|
-
import { readdir, readFile as readFile3, rm, stat as
|
|
2167
|
-
import { homedir as
|
|
2168
|
-
import { basename as
|
|
2423
|
+
import { readdir, readFile as readFile3, rm, stat as stat4 } from "fs/promises";
|
|
2424
|
+
import { homedir as homedir5 } from "os";
|
|
2425
|
+
import { basename as basename3, dirname as dirname2, join as join7, resolve as resolve4 } from "path";
|
|
2169
2426
|
import { createInterface } from "readline";
|
|
2170
2427
|
import {
|
|
2171
2428
|
AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
|
|
@@ -2251,11 +2508,11 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
2251
2508
|
repoRoot: repositoryRoot,
|
|
2252
2509
|
cwd: ctx.cwd ?? process.cwd()
|
|
2253
2510
|
});
|
|
2254
|
-
const projectsRoot = ctx.claudeProjectsDir ??
|
|
2511
|
+
const projectsRoot = ctx.claudeProjectsDir ?? join7(homedir5(), ".claude", "projects");
|
|
2255
2512
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
2256
2513
|
const projectSet = new Set(projectPaths);
|
|
2257
2514
|
const candidates = files.map((file) => {
|
|
2258
|
-
const externalId =
|
|
2515
|
+
const externalId = basename3(file, ".jsonl");
|
|
2259
2516
|
return {
|
|
2260
2517
|
externalId,
|
|
2261
2518
|
sourcePath: file,
|
|
@@ -2290,7 +2547,7 @@ async function doRunImportCodex(options, ctx) {
|
|
|
2290
2547
|
repoRoot: repositoryRoot,
|
|
2291
2548
|
cwd: ctx.cwd ?? process.cwd()
|
|
2292
2549
|
});
|
|
2293
|
-
const sessionsRoot = ctx.codexSessionsDir ??
|
|
2550
|
+
const sessionsRoot = ctx.codexSessionsDir ?? join7(homedir5(), ".codex", "sessions");
|
|
2294
2551
|
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
2295
2552
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
2296
2553
|
externalId,
|
|
@@ -2345,7 +2602,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2345
2602
|
files: payload.session.related_files ?? [],
|
|
2346
2603
|
workingDirectory: payload.session.working_directory,
|
|
2347
2604
|
sourceRoots: projectPaths,
|
|
2348
|
-
masterRoot:
|
|
2605
|
+
masterRoot: dirname2(paths.root),
|
|
2349
2606
|
extraInRoot: AGENT_INFRA_DIRS2
|
|
2350
2607
|
});
|
|
2351
2608
|
if (scope.outOfRoot.length > 0) crossProject.push({ externalId, outOfRoot: scope.outOfRoot });
|
|
@@ -2419,7 +2676,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2419
2676
|
if (priors.length > 0 && options.force === true) {
|
|
2420
2677
|
if (options.dryRun !== true) {
|
|
2421
2678
|
for (const { sessionId } of priors) {
|
|
2422
|
-
await rm(
|
|
2679
|
+
await rm(join7(paths.sessions, sessionId), { recursive: true, force: true });
|
|
2423
2680
|
}
|
|
2424
2681
|
}
|
|
2425
2682
|
counts.replaced++;
|
|
@@ -2530,7 +2787,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2530
2787
|
if (options.session !== void 0) {
|
|
2531
2788
|
const matches = [];
|
|
2532
2789
|
for (const projectPath of projectPaths) {
|
|
2533
|
-
const file =
|
|
2790
|
+
const file = join7(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
2534
2791
|
if (await pathExists(file)) matches.push(file);
|
|
2535
2792
|
}
|
|
2536
2793
|
if (matches.length === 0) {
|
|
@@ -2541,7 +2798,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2541
2798
|
const files = [];
|
|
2542
2799
|
let anyDirFound = false;
|
|
2543
2800
|
for (const projectPath of projectPaths) {
|
|
2544
|
-
const transcriptDir =
|
|
2801
|
+
const transcriptDir = join7(projectsRoot, encodeProjectDir(projectPath));
|
|
2545
2802
|
let entries;
|
|
2546
2803
|
try {
|
|
2547
2804
|
entries = await readdir(transcriptDir);
|
|
@@ -2551,7 +2808,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2551
2808
|
}
|
|
2552
2809
|
anyDirFound = true;
|
|
2553
2810
|
for (const name of entries) {
|
|
2554
|
-
if (name.endsWith(".jsonl")) files.push(
|
|
2811
|
+
if (name.endsWith(".jsonl")) files.push(join7(transcriptDir, name));
|
|
2555
2812
|
}
|
|
2556
2813
|
}
|
|
2557
2814
|
if (!anyDirFound) {
|
|
@@ -2561,7 +2818,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2561
2818
|
}
|
|
2562
2819
|
async function pathExists(file) {
|
|
2563
2820
|
try {
|
|
2564
|
-
await
|
|
2821
|
+
await stat4(file);
|
|
2565
2822
|
return true;
|
|
2566
2823
|
} catch (error) {
|
|
2567
2824
|
if (findErrorCode5(error, "ENOENT")) return false;
|
|
@@ -2570,7 +2827,7 @@ async function pathExists(file) {
|
|
|
2570
2827
|
}
|
|
2571
2828
|
async function statSize(file) {
|
|
2572
2829
|
try {
|
|
2573
|
-
return (await
|
|
2830
|
+
return (await stat4(file)).size;
|
|
2574
2831
|
} catch (error) {
|
|
2575
2832
|
if (findErrorCode5(error, "ENOENT")) return void 0;
|
|
2576
2833
|
throw error;
|
|
@@ -2608,7 +2865,7 @@ async function findRolloutFiles(sessionsRoot) {
|
|
|
2608
2865
|
throw new Error("Failed to read Codex sessions directory", { cause: error });
|
|
2609
2866
|
}
|
|
2610
2867
|
for (const entry of entries) {
|
|
2611
|
-
const full =
|
|
2868
|
+
const full = join7(dir, entry.name);
|
|
2612
2869
|
if (entry.isDirectory()) {
|
|
2613
2870
|
await walk(full, false);
|
|
2614
2871
|
} else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
@@ -2786,7 +3043,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
|
|
|
2786
3043
|
}
|
|
2787
3044
|
|
|
2788
3045
|
// src/commands/init.ts
|
|
2789
|
-
import { basename as
|
|
3046
|
+
import { basename as basename4, relative, resolve as resolve5 } from "path";
|
|
2790
3047
|
import {
|
|
2791
3048
|
appendBasouGitignore,
|
|
2792
3049
|
createManifest,
|
|
@@ -2825,7 +3082,7 @@ async function runInit(options, ctx = {}) {
|
|
|
2825
3082
|
async function doRunInit(options, ctx) {
|
|
2826
3083
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2827
3084
|
const repositoryRoot = await resolveRepositoryRootForInit(cwd);
|
|
2828
|
-
const workspaceName = options.name ??
|
|
3085
|
+
const workspaceName = options.name ?? basename4(repositoryRoot);
|
|
2829
3086
|
let repositoryUrl;
|
|
2830
3087
|
if (options.repoUrl !== void 0) {
|
|
2831
3088
|
repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
|
|
@@ -3050,13 +3307,13 @@ import {
|
|
|
3050
3307
|
} from "@basou/core";
|
|
3051
3308
|
|
|
3052
3309
|
// src/lib/hosts-config.ts
|
|
3053
|
-
import { homedir as
|
|
3054
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3310
|
+
import { homedir as homedir6 } from "os";
|
|
3311
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve6 } from "path";
|
|
3055
3312
|
import { readYamlFile as readYamlFile4 } from "@basou/core";
|
|
3056
|
-
var DEFAULT_HOSTS_CONFIG_PATH =
|
|
3313
|
+
var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
|
|
3057
3314
|
function expandTilde2(p) {
|
|
3058
|
-
if (p === "~") return
|
|
3059
|
-
if (p.startsWith("~/")) return
|
|
3315
|
+
if (p === "~") return homedir6();
|
|
3316
|
+
if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
|
|
3060
3317
|
return p;
|
|
3061
3318
|
}
|
|
3062
3319
|
function isRecord2(value) {
|
|
@@ -3382,7 +3639,7 @@ import {
|
|
|
3382
3639
|
writeFileSync,
|
|
3383
3640
|
writeSync
|
|
3384
3641
|
} from "fs";
|
|
3385
|
-
import { basename as
|
|
3642
|
+
import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve7 } from "path";
|
|
3386
3643
|
import {
|
|
3387
3644
|
appendBasouGitignore as appendBasouGitignore2,
|
|
3388
3645
|
basouPaths as basouPaths10,
|
|
@@ -3686,7 +3943,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
|
3686
3943
|
} catch {
|
|
3687
3944
|
return { path: declaredPath, kind: "unresolved" };
|
|
3688
3945
|
}
|
|
3689
|
-
return { path: declaredPath, kind: existsSync(
|
|
3946
|
+
return { path: declaredPath, kind: existsSync(join9(real, ".git")) ? "repo" : "non-repo" };
|
|
3690
3947
|
}
|
|
3691
3948
|
async function doRunProjectAdopt(options, ctx) {
|
|
3692
3949
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -3790,7 +4047,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
3790
4047
|
} catch {
|
|
3791
4048
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
3792
4049
|
}
|
|
3793
|
-
if (!existsSync(
|
|
4050
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
3794
4051
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
3795
4052
|
}
|
|
3796
4053
|
try {
|
|
@@ -3798,7 +4055,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
3798
4055
|
for (const name of INSTRUCTION_FILES) {
|
|
3799
4056
|
let present = true;
|
|
3800
4057
|
try {
|
|
3801
|
-
lstatSync(
|
|
4058
|
+
lstatSync(join9(real, name));
|
|
3802
4059
|
} catch {
|
|
3803
4060
|
present = false;
|
|
3804
4061
|
}
|
|
@@ -3909,10 +4166,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
|
|
|
3909
4166
|
} catch {
|
|
3910
4167
|
return { ...base, reachable: false, currentLines: [] };
|
|
3911
4168
|
}
|
|
3912
|
-
if (!existsSync(
|
|
4169
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
3913
4170
|
return { ...base, reachable: false, currentLines: [] };
|
|
3914
4171
|
}
|
|
3915
|
-
return { ...base, reachable: true, currentLines: readGitignoreLines(
|
|
4172
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join9(real, ".gitignore")) };
|
|
3916
4173
|
}
|
|
3917
4174
|
function hasErrorCode(error) {
|
|
3918
4175
|
return error instanceof Error && typeof error.code === "string";
|
|
@@ -3926,7 +4183,7 @@ function readGitignoreLines(file) {
|
|
|
3926
4183
|
}
|
|
3927
4184
|
}
|
|
3928
4185
|
function applyGitignorePlan(repositoryRoot, plan) {
|
|
3929
|
-
const file =
|
|
4186
|
+
const file = join9(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
|
|
3930
4187
|
let existing = "";
|
|
3931
4188
|
try {
|
|
3932
4189
|
existing = readFileSync(file, "utf8");
|
|
@@ -4057,16 +4314,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4057
4314
|
if (real === anchorReal) {
|
|
4058
4315
|
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
4059
4316
|
}
|
|
4060
|
-
if (!existsSync(
|
|
4317
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
4061
4318
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4062
4319
|
}
|
|
4063
|
-
const canonicalFile = isSelf ?
|
|
4320
|
+
const canonicalFile = isSelf ? join9(real, CANONICAL_FILE) : join9(anchorReal, "agents", basename5(real), CANONICAL_FILE);
|
|
4064
4321
|
if (!existsSync(canonicalFile)) {
|
|
4065
4322
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
4066
4323
|
}
|
|
4067
4324
|
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
4068
4325
|
(spec) => {
|
|
4069
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4326
|
+
const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
|
|
4070
4327
|
return {
|
|
4071
4328
|
name: spec.name,
|
|
4072
4329
|
expectedTarget: spec.target,
|
|
@@ -4080,7 +4337,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4080
4337
|
isAnchor: false,
|
|
4081
4338
|
reachable: true,
|
|
4082
4339
|
canonicalPresent: true,
|
|
4083
|
-
canonicalName:
|
|
4340
|
+
canonicalName: basename5(real),
|
|
4084
4341
|
files
|
|
4085
4342
|
};
|
|
4086
4343
|
}
|
|
@@ -4095,9 +4352,9 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
4095
4352
|
const created = [];
|
|
4096
4353
|
const failed = [];
|
|
4097
4354
|
for (const { name, target } of plan.toCreate) {
|
|
4098
|
-
const filePath =
|
|
4355
|
+
const filePath = join9(real, name);
|
|
4099
4356
|
try {
|
|
4100
|
-
mkdirSync(
|
|
4357
|
+
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4101
4358
|
symlinkSync(target, filePath);
|
|
4102
4359
|
created.push(name);
|
|
4103
4360
|
} catch (error) {
|
|
@@ -4248,7 +4505,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
4248
4505
|
return realpathSync(abs);
|
|
4249
4506
|
} catch {
|
|
4250
4507
|
try {
|
|
4251
|
-
return
|
|
4508
|
+
return join9(realpathSync(dirname3(abs)), basename5(abs));
|
|
4252
4509
|
} catch {
|
|
4253
4510
|
return abs;
|
|
4254
4511
|
}
|
|
@@ -4265,8 +4522,8 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
4265
4522
|
if (expectedTarget === "" || expectedTarget === ".") {
|
|
4266
4523
|
return { path: entry.path, reachable: false };
|
|
4267
4524
|
}
|
|
4268
|
-
const linkName =
|
|
4269
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4525
|
+
const linkName = basename5(repoReal);
|
|
4526
|
+
const { state, actualTarget } = inspectSymlink(join9(viewDir, linkName), expectedTarget);
|
|
4270
4527
|
return {
|
|
4271
4528
|
path: entry.path,
|
|
4272
4529
|
reachable: true,
|
|
@@ -4280,9 +4537,9 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
4280
4537
|
const created = [];
|
|
4281
4538
|
const failed = [];
|
|
4282
4539
|
for (const { name, target } of toCreate) {
|
|
4283
|
-
const filePath =
|
|
4540
|
+
const filePath = join9(viewDir, name);
|
|
4284
4541
|
try {
|
|
4285
|
-
mkdirSync(
|
|
4542
|
+
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4286
4543
|
symlinkSync(target, filePath);
|
|
4287
4544
|
created.push(name);
|
|
4288
4545
|
} catch (error) {
|
|
@@ -4295,7 +4552,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
|
4295
4552
|
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
4296
4553
|
);
|
|
4297
4554
|
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
4298
|
-
const filePath =
|
|
4555
|
+
const filePath = join9(viewDir, name);
|
|
4299
4556
|
let isLink;
|
|
4300
4557
|
try {
|
|
4301
4558
|
isLink = lstatSync(filePath).isSymbolicLink();
|
|
@@ -4324,7 +4581,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
4324
4581
|
if (!isDir) {
|
|
4325
4582
|
return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
|
|
4326
4583
|
}
|
|
4327
|
-
return { target, kind: existsSync(
|
|
4584
|
+
return { target, kind: existsSync(join9(resolved, ".git")) ? "repo" : "non-repo" };
|
|
4328
4585
|
}
|
|
4329
4586
|
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
4330
4587
|
let names;
|
|
@@ -4349,7 +4606,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
4349
4606
|
const pruned = [];
|
|
4350
4607
|
const failed = [];
|
|
4351
4608
|
for (const { name } of toPrune) {
|
|
4352
|
-
const filePath =
|
|
4609
|
+
const filePath = join9(viewDir, name);
|
|
4353
4610
|
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
4354
4611
|
if (c === null || c.kind !== "repo") {
|
|
4355
4612
|
failed.push({
|
|
@@ -4395,7 +4652,7 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
4395
4652
|
} else {
|
|
4396
4653
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
4397
4654
|
const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
|
|
4398
|
-
const rosterNames = roster.map((entry) =>
|
|
4655
|
+
const rosterNames = roster.map((entry) => basename5(resolve7(repositoryRoot, entry.path)));
|
|
4399
4656
|
const rosterRealpaths = /* @__PURE__ */ new Set();
|
|
4400
4657
|
for (const entry of roster) {
|
|
4401
4658
|
try {
|
|
@@ -4562,10 +4819,10 @@ async function runProjectPreset(options, ctx = {}) {
|
|
|
4562
4819
|
}
|
|
4563
4820
|
}
|
|
4564
4821
|
function canonicalFileFor(anchorReal, canonicalName) {
|
|
4565
|
-
return
|
|
4822
|
+
return join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4566
4823
|
}
|
|
4567
4824
|
function canonicalLabelFor(canonicalName) {
|
|
4568
|
-
return
|
|
4825
|
+
return join9("agents", canonicalName, CANONICAL_FILE);
|
|
4569
4826
|
}
|
|
4570
4827
|
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
4571
4828
|
const declared = {
|
|
@@ -4586,10 +4843,10 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
4586
4843
|
if (real === anchorReal) {
|
|
4587
4844
|
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
4588
4845
|
}
|
|
4589
|
-
if (!existsSync(
|
|
4846
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
4590
4847
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
4591
4848
|
}
|
|
4592
|
-
const canonicalName =
|
|
4849
|
+
const canonicalName = basename5(real);
|
|
4593
4850
|
let content;
|
|
4594
4851
|
try {
|
|
4595
4852
|
content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
|
|
@@ -4634,7 +4891,7 @@ async function applyPresetPlan(anchorReal, plan) {
|
|
|
4634
4891
|
isLink = false;
|
|
4635
4892
|
}
|
|
4636
4893
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
4637
|
-
if (plan.action === "create") mkdirSync(
|
|
4894
|
+
if (plan.action === "create") mkdirSync(dirname3(file), { recursive: true });
|
|
4638
4895
|
const existing = await readMarkdownFile4(file);
|
|
4639
4896
|
await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
|
|
4640
4897
|
}
|
|
@@ -4824,28 +5081,28 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
4824
5081
|
return empty;
|
|
4825
5082
|
}
|
|
4826
5083
|
const anchorReal = realpathSync(repositoryRoot);
|
|
4827
|
-
const canonicalName =
|
|
5084
|
+
const canonicalName = basename5(real);
|
|
4828
5085
|
const instructionFiles = [];
|
|
4829
5086
|
for (const name of INSTRUCTION_FILES) {
|
|
4830
5087
|
try {
|
|
4831
|
-
lstatSync(
|
|
5088
|
+
lstatSync(join9(real, name));
|
|
4832
5089
|
instructionFiles.push(name);
|
|
4833
5090
|
} catch {
|
|
4834
5091
|
}
|
|
4835
5092
|
}
|
|
4836
5093
|
let ignored;
|
|
4837
5094
|
try {
|
|
4838
|
-
ignored = new Set(readGitignoreLines(
|
|
5095
|
+
ignored = new Set(readGitignoreLines(join9(real, ".gitignore")).map((l) => l.trim()));
|
|
4839
5096
|
} catch {
|
|
4840
5097
|
ignored = /* @__PURE__ */ new Set();
|
|
4841
5098
|
}
|
|
4842
5099
|
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
4843
|
-
const canonical2 = existsSync(
|
|
5100
|
+
const canonical2 = existsSync(join9(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
4844
5101
|
let viewLink = false;
|
|
4845
5102
|
const viewPath = manifest.workspace.view;
|
|
4846
5103
|
if (viewPath !== void 0) {
|
|
4847
5104
|
try {
|
|
4848
|
-
lstatSync(
|
|
5105
|
+
lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
4849
5106
|
viewLink = true;
|
|
4850
5107
|
} catch {
|
|
4851
5108
|
}
|
|
@@ -4859,11 +5116,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
4859
5116
|
};
|
|
4860
5117
|
}
|
|
4861
5118
|
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
4862
|
-
const canonicalFile =
|
|
5119
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4863
5120
|
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
4864
5121
|
}
|
|
4865
5122
|
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
4866
|
-
const filePath =
|
|
5123
|
+
const filePath = join9(viewDir, name);
|
|
4867
5124
|
try {
|
|
4868
5125
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4869
5126
|
const target = readlinkSync(filePath);
|
|
@@ -4874,7 +5131,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
|
4874
5131
|
}
|
|
4875
5132
|
}
|
|
4876
5133
|
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
4877
|
-
const filePath =
|
|
5134
|
+
const filePath = join9(viewDir, name);
|
|
4878
5135
|
try {
|
|
4879
5136
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4880
5137
|
const target = readlinkSync(filePath);
|
|
@@ -4894,7 +5151,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4894
5151
|
}
|
|
4895
5152
|
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
4896
5153
|
const targetAbs = resolve7(repositoryRoot, target);
|
|
4897
|
-
const canonicalName =
|
|
5154
|
+
const canonicalName = basename5(repoReal ?? targetAbs);
|
|
4898
5155
|
const roster = manifest.repos ?? [];
|
|
4899
5156
|
const declaredEntry = roster.find((r) => {
|
|
4900
5157
|
try {
|
|
@@ -4915,17 +5172,17 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4915
5172
|
}
|
|
4916
5173
|
if (rReal !== null) {
|
|
4917
5174
|
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
4918
|
-
return
|
|
5175
|
+
return basename5(rReal).toLowerCase() === cnFold;
|
|
4919
5176
|
}
|
|
4920
5177
|
if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
|
|
4921
|
-
return
|
|
5178
|
+
return basename5(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
4922
5179
|
});
|
|
4923
5180
|
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
4924
5181
|
const items = [];
|
|
4925
5182
|
if (!isAnchor) {
|
|
4926
5183
|
if (repoReal !== void 0) {
|
|
4927
5184
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
4928
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5185
|
+
const { state, actualTarget } = inspectSymlink(join9(repoReal, spec.name), spec.target);
|
|
4929
5186
|
if (isSelf) {
|
|
4930
5187
|
if (state !== "missing")
|
|
4931
5188
|
items.push({
|
|
@@ -4962,7 +5219,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4962
5219
|
}
|
|
4963
5220
|
let ignored;
|
|
4964
5221
|
try {
|
|
4965
|
-
ignored = new Set(readGitignoreLines(
|
|
5222
|
+
ignored = new Set(readGitignoreLines(join9(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
4966
5223
|
for (const p of INSTRUCTION_FILES) {
|
|
4967
5224
|
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
4968
5225
|
items.push({
|
|
@@ -4985,7 +5242,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4985
5242
|
const viewPath = manifest.workspace.view;
|
|
4986
5243
|
if (viewPath !== void 0) {
|
|
4987
5244
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
4988
|
-
const linkPath =
|
|
5245
|
+
const linkPath = join9(viewDir, canonicalName);
|
|
4989
5246
|
let isLink = false;
|
|
4990
5247
|
try {
|
|
4991
5248
|
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
@@ -5011,8 +5268,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5011
5268
|
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
5012
5269
|
}
|
|
5013
5270
|
}
|
|
5014
|
-
const canonicalFile =
|
|
5015
|
-
const canonicalLabel =
|
|
5271
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5272
|
+
const canonicalLabel = join9("agents", canonicalName, CANONICAL_FILE);
|
|
5016
5273
|
let canonicalIsLink = false;
|
|
5017
5274
|
try {
|
|
5018
5275
|
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
@@ -5116,12 +5373,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5116
5373
|
);
|
|
5117
5374
|
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
5118
5375
|
const expected = expectedByName.get(item.label);
|
|
5119
|
-
if (repoReal === null || expected === void 0 || inspectSymlink(
|
|
5376
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join9(repoReal, item.label), expected).state !== "correct") {
|
|
5120
5377
|
changed(item.label);
|
|
5121
5378
|
continue;
|
|
5122
5379
|
}
|
|
5123
5380
|
try {
|
|
5124
|
-
unlinkSync(
|
|
5381
|
+
unlinkSync(join9(repoReal, item.label));
|
|
5125
5382
|
removed.push(item.label);
|
|
5126
5383
|
} catch (error) {
|
|
5127
5384
|
failed.push({ label: item.label, message: failureReason(error) });
|
|
@@ -5140,7 +5397,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5140
5397
|
continue;
|
|
5141
5398
|
}
|
|
5142
5399
|
try {
|
|
5143
|
-
unlinkSync(
|
|
5400
|
+
unlinkSync(join9(viewDir, item.label));
|
|
5144
5401
|
removed.push(`view/${item.label}`);
|
|
5145
5402
|
} catch (error) {
|
|
5146
5403
|
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
@@ -5148,7 +5405,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5148
5405
|
}
|
|
5149
5406
|
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
5150
5407
|
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
5151
|
-
const canonicalFile =
|
|
5408
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5152
5409
|
try {
|
|
5153
5410
|
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
5154
5411
|
changed(item.label);
|
|
@@ -5383,7 +5640,7 @@ function renderProjectArchive(result) {
|
|
|
5383
5640
|
if (t.gitignorePatterns.length > 0)
|
|
5384
5641
|
items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
|
|
5385
5642
|
if (t.canonical)
|
|
5386
|
-
items.push(`the anchor's canonical (agents/${
|
|
5643
|
+
items.push(`the anchor's canonical (agents/${basename5(result.target)}/AGENTS.md)`);
|
|
5387
5644
|
if (!t.inspected) {
|
|
5388
5645
|
lines.push(
|
|
5389
5646
|
"## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
|
|
@@ -5422,12 +5679,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
|
5422
5679
|
} catch {
|
|
5423
5680
|
return { canonicalDirOld: false, viewLinkOld: false };
|
|
5424
5681
|
}
|
|
5425
|
-
const canonicalDirOld = existsSync(
|
|
5682
|
+
const canonicalDirOld = existsSync(join9(anchorReal, "agents", oldBasename));
|
|
5426
5683
|
let viewLinkOld = false;
|
|
5427
5684
|
const viewPath = manifest.workspace.view;
|
|
5428
5685
|
if (viewPath !== void 0) {
|
|
5429
5686
|
try {
|
|
5430
|
-
lstatSync(
|
|
5687
|
+
lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
5431
5688
|
viewLinkOld = true;
|
|
5432
5689
|
} catch {
|
|
5433
5690
|
}
|
|
@@ -5586,7 +5843,7 @@ async function resolveRepositoryRootForNew(cwd) {
|
|
|
5586
5843
|
async function doRunProjectNew(repos, options, ctx) {
|
|
5587
5844
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5588
5845
|
const repositoryRoot = await resolveRepositoryRootForNew(cwd);
|
|
5589
|
-
const workspaceName =
|
|
5846
|
+
const workspaceName = basename5(repositoryRoot);
|
|
5590
5847
|
const declared = repos.map((p) => {
|
|
5591
5848
|
const abs = resolve7(cwd, p);
|
|
5592
5849
|
let real;
|
|
@@ -5768,7 +6025,7 @@ function regularFileSpokes(repoReal) {
|
|
|
5768
6025
|
const out = [];
|
|
5769
6026
|
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
5770
6027
|
try {
|
|
5771
|
-
const st = lstatSync(
|
|
6028
|
+
const st = lstatSync(join9(repoReal, spoke));
|
|
5772
6029
|
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
5773
6030
|
} catch {
|
|
5774
6031
|
}
|
|
@@ -5798,7 +6055,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
5798
6055
|
const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
|
|
5799
6056
|
const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
|
|
5800
6057
|
const path = displayRel === "" ? "." : displayRel;
|
|
5801
|
-
const canonicalName =
|
|
6058
|
+
const canonicalName = basename5(argReal ?? argAbs);
|
|
5802
6059
|
if (argReal === void 0) {
|
|
5803
6060
|
return {
|
|
5804
6061
|
path,
|
|
@@ -5813,8 +6070,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
5813
6070
|
};
|
|
5814
6071
|
}
|
|
5815
6072
|
const isAnchor = argReal === anchorReal;
|
|
5816
|
-
const reachable = existsSync(
|
|
5817
|
-
const canonicalFile =
|
|
6073
|
+
const reachable = existsSync(join9(argReal, ".git"));
|
|
6074
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5818
6075
|
return {
|
|
5819
6076
|
path,
|
|
5820
6077
|
declared,
|
|
@@ -5822,15 +6079,15 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
5822
6079
|
isAnchor,
|
|
5823
6080
|
reachable,
|
|
5824
6081
|
canonicalName,
|
|
5825
|
-
agentsState: inspectAgentsState(
|
|
6082
|
+
agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
|
|
5826
6083
|
canonicalExists: pathPresent(canonicalFile),
|
|
5827
6084
|
regularSpokes: regularFileSpokes(argReal)
|
|
5828
6085
|
};
|
|
5829
6086
|
}
|
|
5830
6087
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
5831
|
-
const agentsFile =
|
|
6088
|
+
const agentsFile = join9(repoReal, CANONICAL_FILE);
|
|
5832
6089
|
try {
|
|
5833
|
-
mkdirSync(
|
|
6090
|
+
mkdirSync(dirname3(canonicalFile), { recursive: true });
|
|
5834
6091
|
} catch (error) {
|
|
5835
6092
|
return { ok: false, message: failureReason(error), partial: false };
|
|
5836
6093
|
}
|
|
@@ -5868,7 +6125,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
5868
6125
|
let failure;
|
|
5869
6126
|
let partial = false;
|
|
5870
6127
|
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
5871
|
-
const canonicalFile =
|
|
6128
|
+
const canonicalFile = join9(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
5872
6129
|
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
5873
6130
|
if (res.ok) {
|
|
5874
6131
|
applied = true;
|
|
@@ -5992,78 +6249,131 @@ function renderProjectRetrofit(result) {
|
|
|
5992
6249
|
|
|
5993
6250
|
// src/commands/protocol.ts
|
|
5994
6251
|
import { readFile as readFile4 } from "fs/promises";
|
|
6252
|
+
import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdownFile as readMarkdownFile6 } from "@basou/core";
|
|
6253
|
+
|
|
6254
|
+
// src/lib/context-channel.ts
|
|
6255
|
+
import { homedir as homedir7 } from "os";
|
|
6256
|
+
import { join as join10 } from "path";
|
|
5995
6257
|
import {
|
|
5996
|
-
|
|
5997
|
-
|
|
6258
|
+
ORIENTATION_END,
|
|
6259
|
+
ORIENTATION_START,
|
|
5998
6260
|
parseMarkers as parseMarkers2,
|
|
5999
6261
|
readMarkdownFile as readMarkdownFile5,
|
|
6000
6262
|
removeMarkerSection as removeMarkerSection2
|
|
6001
6263
|
} from "@basou/core";
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6264
|
+
var CODEX_TARGET_PATH = join10(homedir7(), ".codex", "AGENTS.md");
|
|
6265
|
+
var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
|
|
6266
|
+
var ORIENTATION_MANAGED_NOTE = "<!-- Managed by basou: 'basou refresh' regenerates everything between the BASOU:ORIENTATION markers with the workspace's current position. This block is transient \u2014 it changes every refresh; do not edit it. -->";
|
|
6267
|
+
function buildTargetBody(existing, block, markers) {
|
|
6268
|
+
const wrapped = `${markers.start}
|
|
6269
|
+
${block}${markers.end}
|
|
6270
|
+
`;
|
|
6271
|
+
if (existing === null || existing === "") return wrapped;
|
|
6272
|
+
const section = parseMarkers2(existing, markers);
|
|
6273
|
+
switch (section.kind) {
|
|
6274
|
+
case "ok":
|
|
6275
|
+
return `${section.before}${markers.start}
|
|
6276
|
+
${block}${markers.end}${section.after}`;
|
|
6277
|
+
case "no_markers": {
|
|
6278
|
+
const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
6279
|
+
return `${existing}${sep}${wrapped}`;
|
|
6280
|
+
}
|
|
6281
|
+
default:
|
|
6011
6282
|
throw new Error(
|
|
6012
|
-
"
|
|
6283
|
+
"The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
|
|
6013
6284
|
);
|
|
6014
|
-
}
|
|
6015
|
-
} catch (error) {
|
|
6016
|
-
if (error instanceof Error && error.code === "ENOENT") return;
|
|
6017
|
-
throw error;
|
|
6018
6285
|
}
|
|
6019
6286
|
}
|
|
6020
|
-
async function
|
|
6021
|
-
|
|
6022
|
-
const
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
if (handle) await handle.close().catch(() => void 0);
|
|
6042
|
-
await unlink2(tmpPath).catch(() => void 0);
|
|
6043
|
-
throw error;
|
|
6287
|
+
async function backupOnce(target, existing) {
|
|
6288
|
+
if (existing === null) return;
|
|
6289
|
+
const bak = `${target}.basou-bak`;
|
|
6290
|
+
const already = await readMarkdownFile5(bak);
|
|
6291
|
+
if (already !== null) return;
|
|
6292
|
+
await writeFileDurable(bak, existing);
|
|
6293
|
+
}
|
|
6294
|
+
async function syncMarkerBlock(opts) {
|
|
6295
|
+
const { target, markers, block } = opts;
|
|
6296
|
+
await assertNotSymlink(target);
|
|
6297
|
+
const existing = await readMarkdownFile5(target);
|
|
6298
|
+
const newBody = buildTargetBody(existing, block, markers);
|
|
6299
|
+
if (newBody === existing) return { action: "unchanged" };
|
|
6300
|
+
const hadBlock = existing !== null && parseMarkers2(existing, markers).kind === "ok";
|
|
6301
|
+
const action = hadBlock ? "updated" : "installed";
|
|
6302
|
+
if (opts.dryRun === true) return { action };
|
|
6303
|
+
const recheck = await readMarkdownFile5(target);
|
|
6304
|
+
if (recheck !== existing) {
|
|
6305
|
+
throw new Error(
|
|
6306
|
+
"The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
6307
|
+
);
|
|
6044
6308
|
}
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6309
|
+
await backupOnce(target, existing);
|
|
6310
|
+
await writeFileDurable(target, newBody);
|
|
6311
|
+
return { action };
|
|
6312
|
+
}
|
|
6313
|
+
function assertNoMarkerLine(body, markers) {
|
|
6314
|
+
for (const line of body.split(/\r?\n/)) {
|
|
6315
|
+
if (line === markers.start || line === markers.end) {
|
|
6316
|
+
throw new Error(
|
|
6317
|
+
"The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
|
|
6318
|
+
);
|
|
6051
6319
|
}
|
|
6052
|
-
} catch {
|
|
6053
6320
|
}
|
|
6054
6321
|
}
|
|
6322
|
+
async function removeMarkerBlock(opts) {
|
|
6323
|
+
const { target, markers, fileLabel } = opts;
|
|
6324
|
+
await assertNotSymlink(target);
|
|
6325
|
+
const existing = await readMarkdownFile5(target);
|
|
6326
|
+
if (existing === null) return { removed: false };
|
|
6327
|
+
const newBody = removeMarkerSection2(existing, fileLabel, markers);
|
|
6328
|
+
if (newBody === existing) return { removed: false };
|
|
6329
|
+
if (opts.dryRun === true) return { removed: true };
|
|
6330
|
+
const recheck = await readMarkdownFile5(target);
|
|
6331
|
+
if (recheck !== existing) {
|
|
6332
|
+
throw new Error(
|
|
6333
|
+
"The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
6334
|
+
);
|
|
6335
|
+
}
|
|
6336
|
+
await backupOnce(target, existing);
|
|
6337
|
+
await writeFileDurable(target, newBody);
|
|
6338
|
+
return { removed: true };
|
|
6339
|
+
}
|
|
6340
|
+
async function syncOrientationChannel(opts) {
|
|
6341
|
+
assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
|
|
6342
|
+
const block = `${ORIENTATION_MANAGED_NOTE}
|
|
6343
|
+
|
|
6344
|
+
${opts.body.replace(/\s+$/, "")}
|
|
6345
|
+
`;
|
|
6346
|
+
return syncMarkerBlock({
|
|
6347
|
+
target: opts.target ?? CODEX_TARGET_PATH,
|
|
6348
|
+
markers: ORIENTATION_MARKERS,
|
|
6349
|
+
block,
|
|
6350
|
+
...opts.dryRun === true ? { dryRun: true } : {}
|
|
6351
|
+
});
|
|
6352
|
+
}
|
|
6353
|
+
async function renderOrientationToCodexChannel(opts) {
|
|
6354
|
+
const body = await readMarkdownFile5(opts.orientationPath);
|
|
6355
|
+
if (body === null) return null;
|
|
6356
|
+
const { action } = await syncOrientationChannel({
|
|
6357
|
+
body,
|
|
6358
|
+
...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
|
|
6359
|
+
});
|
|
6360
|
+
return {
|
|
6361
|
+
action,
|
|
6362
|
+
line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
|
|
6363
|
+
};
|
|
6364
|
+
}
|
|
6055
6365
|
|
|
6056
6366
|
// src/lib/protocols-config.ts
|
|
6057
|
-
import { homedir as
|
|
6058
|
-
import { isAbsolute as isAbsolute4, join as
|
|
6367
|
+
import { homedir as homedir8 } from "os";
|
|
6368
|
+
import { isAbsolute as isAbsolute4, join as join11, resolve as resolve8 } from "path";
|
|
6059
6369
|
import { readYamlFile as readYamlFile5 } from "@basou/core";
|
|
6060
|
-
var DEFAULT_PROTOCOLS_CONFIG_PATH =
|
|
6061
|
-
var DEFAULT_TARGET_PATH =
|
|
6370
|
+
var DEFAULT_PROTOCOLS_CONFIG_PATH = join11(homedir8(), ".basou", "protocols.yaml");
|
|
6371
|
+
var DEFAULT_TARGET_PATH = join11(homedir8(), ".claude", "CLAUDE.md");
|
|
6062
6372
|
var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
|
|
6063
6373
|
var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
|
|
6064
6374
|
function expandTilde3(p) {
|
|
6065
|
-
if (p === "~") return
|
|
6066
|
-
if (p.startsWith("~/")) return
|
|
6375
|
+
if (p === "~") return homedir8();
|
|
6376
|
+
if (p.startsWith("~/")) return join11(homedir8(), p.slice(2));
|
|
6067
6377
|
return p;
|
|
6068
6378
|
}
|
|
6069
6379
|
function isRecord3(value) {
|
|
@@ -6184,13 +6494,7 @@ async function readProtocolSources(entries) {
|
|
|
6184
6494
|
}
|
|
6185
6495
|
throw new Error("Failed to read a protocol source file.", { cause: error });
|
|
6186
6496
|
}
|
|
6187
|
-
|
|
6188
|
-
if (line === PROTOCOL_START || line === PROTOCOL_END) {
|
|
6189
|
-
throw new Error(
|
|
6190
|
-
"A protocol source contains a BASOU:PROTOCOLS marker line, which would corrupt the managed block. Remove that line from the source."
|
|
6191
|
-
);
|
|
6192
|
-
}
|
|
6193
|
-
}
|
|
6497
|
+
assertNoMarkerLine(content, PROTOCOL_MARKERS);
|
|
6194
6498
|
out.push({ entry, content });
|
|
6195
6499
|
}
|
|
6196
6500
|
return out;
|
|
@@ -6207,74 +6511,41 @@ ${body}` : body;
|
|
|
6207
6511
|
${sections.join("\n\n")}
|
|
6208
6512
|
`;
|
|
6209
6513
|
}
|
|
6210
|
-
function buildTargetBody(existing, block) {
|
|
6211
|
-
const wrapped = `${PROTOCOL_START}
|
|
6212
|
-
${block}${PROTOCOL_END}
|
|
6213
|
-
`;
|
|
6214
|
-
if (existing === null || existing === "") return wrapped;
|
|
6215
|
-
const section = parseMarkers2(existing, PROTOCOL_MARKERS);
|
|
6216
|
-
switch (section.kind) {
|
|
6217
|
-
case "ok":
|
|
6218
|
-
return `${section.before}${PROTOCOL_START}
|
|
6219
|
-
${block}${PROTOCOL_END}${section.after}`;
|
|
6220
|
-
case "no_markers": {
|
|
6221
|
-
const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
6222
|
-
return `${existing}${sep}${wrapped}`;
|
|
6223
|
-
}
|
|
6224
|
-
default:
|
|
6225
|
-
throw new Error(
|
|
6226
|
-
"The BASOU:PROTOCOLS markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
|
|
6227
|
-
);
|
|
6228
|
-
}
|
|
6229
|
-
}
|
|
6230
|
-
async function backupOnce(target, existing) {
|
|
6231
|
-
if (existing === null) return;
|
|
6232
|
-
const bak = `${target}.basou-bak`;
|
|
6233
|
-
const already = await readMarkdownFile5(bak);
|
|
6234
|
-
if (already !== null) return;
|
|
6235
|
-
await writeFileDurable(bak, existing);
|
|
6236
|
-
}
|
|
6237
6514
|
async function doRunProtocolSync(options) {
|
|
6238
6515
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
6239
6516
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
6240
6517
|
const entries = await loadProtocolsConfig(configPath);
|
|
6241
6518
|
const sources = await readProtocolSources(entries);
|
|
6242
6519
|
const block = buildBlock(sources);
|
|
6243
|
-
await
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6520
|
+
const result = await syncMarkerBlock({
|
|
6521
|
+
target,
|
|
6522
|
+
markers: PROTOCOL_MARKERS,
|
|
6523
|
+
block,
|
|
6524
|
+
...options.dryRun === true ? { dryRun: true } : {}
|
|
6525
|
+
});
|
|
6526
|
+
if (result.action === "unchanged") {
|
|
6247
6527
|
console.log(`The basou:protocols block is already up to date (${entries.length} protocol(s)).`);
|
|
6248
6528
|
return;
|
|
6249
6529
|
}
|
|
6250
|
-
const hadBlock = existing !== null && parseMarkers2(existing, PROTOCOL_MARKERS).kind === "ok";
|
|
6251
6530
|
if (options.dryRun === true) {
|
|
6252
6531
|
console.log(
|
|
6253
|
-
`[dry-run] Would ${
|
|
6532
|
+
`[dry-run] Would ${result.action === "updated" ? "update" : "install"} the basou:protocols block (${entries.length} protocol(s)).`
|
|
6254
6533
|
);
|
|
6255
6534
|
for (const { entry } of sources) {
|
|
6256
6535
|
console.log(` - ${entry.title ?? entry.source}`);
|
|
6257
6536
|
}
|
|
6258
6537
|
return;
|
|
6259
6538
|
}
|
|
6260
|
-
const recheck = await readMarkdownFile5(target);
|
|
6261
|
-
if (recheck !== existing) {
|
|
6262
|
-
throw new Error(
|
|
6263
|
-
"The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol sync'."
|
|
6264
|
-
);
|
|
6265
|
-
}
|
|
6266
|
-
await backupOnce(target, existing);
|
|
6267
|
-
await writeFileDurable(target, newBody);
|
|
6268
6539
|
console.log(
|
|
6269
|
-
`${
|
|
6540
|
+
`${result.action === "updated" ? "Updated" : "Installed"} the basou:protocols block in the global CLAUDE.md (${entries.length} protocol(s)).`
|
|
6270
6541
|
);
|
|
6271
6542
|
}
|
|
6272
6543
|
async function doRunProtocolList(options) {
|
|
6273
6544
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
6274
6545
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
6275
6546
|
const entries = await loadProtocolsConfig(configPath);
|
|
6276
|
-
const existing = await
|
|
6277
|
-
const installed = existing !== null &&
|
|
6547
|
+
const existing = await readMarkdownFile6(target);
|
|
6548
|
+
const installed = existing !== null && parseMarkers3(existing, PROTOCOL_MARKERS).kind === "ok";
|
|
6278
6549
|
console.log(`Declared protocols (${entries.length}):`);
|
|
6279
6550
|
for (const entry of entries) {
|
|
6280
6551
|
console.log(` - ${entry.title ?? entry.source}`);
|
|
@@ -6283,14 +6554,13 @@ async function doRunProtocolList(options) {
|
|
|
6283
6554
|
}
|
|
6284
6555
|
async function doRunProtocolUnsync(options) {
|
|
6285
6556
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
6286
|
-
await
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
}
|
|
6292
|
-
|
|
6293
|
-
if (newBody === existing) {
|
|
6557
|
+
const result = await removeMarkerBlock({
|
|
6558
|
+
target,
|
|
6559
|
+
markers: PROTOCOL_MARKERS,
|
|
6560
|
+
fileLabel: "CLAUDE.md",
|
|
6561
|
+
...options.dryRun === true ? { dryRun: true } : {}
|
|
6562
|
+
});
|
|
6563
|
+
if (!result.removed) {
|
|
6294
6564
|
console.log("No basou:protocols block found; nothing removed.");
|
|
6295
6565
|
return;
|
|
6296
6566
|
}
|
|
@@ -6298,14 +6568,6 @@ async function doRunProtocolUnsync(options) {
|
|
|
6298
6568
|
console.log("[dry-run] Would remove the basou:protocols block from the global CLAUDE.md.");
|
|
6299
6569
|
return;
|
|
6300
6570
|
}
|
|
6301
|
-
const recheck = await readMarkdownFile5(target);
|
|
6302
|
-
if (recheck !== existing) {
|
|
6303
|
-
throw new Error(
|
|
6304
|
-
"The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol unsync'."
|
|
6305
|
-
);
|
|
6306
|
-
}
|
|
6307
|
-
await backupOnce(target, existing);
|
|
6308
|
-
await writeFileDurable(target, newBody);
|
|
6309
6571
|
console.log("Removed the basou:protocols block from the global CLAUDE.md.");
|
|
6310
6572
|
}
|
|
6311
6573
|
|
|
@@ -6315,16 +6577,16 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
|
6315
6577
|
|
|
6316
6578
|
// src/commands/refresh-watch.ts
|
|
6317
6579
|
import { readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
6318
|
-
import { homedir as
|
|
6319
|
-
import { join as
|
|
6580
|
+
import { homedir as homedir9 } from "os";
|
|
6581
|
+
import { join as join12 } from "path";
|
|
6320
6582
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
6321
6583
|
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
6322
6584
|
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
6323
6585
|
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
6324
6586
|
function watchedRoots(ctx) {
|
|
6325
6587
|
return [
|
|
6326
|
-
ctx.codexSessionsDir ??
|
|
6327
|
-
ctx.claudeProjectsDir ??
|
|
6588
|
+
ctx.codexSessionsDir ?? join12(homedir9(), ".codex", "sessions"),
|
|
6589
|
+
ctx.claudeProjectsDir ?? join12(homedir9(), ".claude", "projects")
|
|
6328
6590
|
];
|
|
6329
6591
|
}
|
|
6330
6592
|
async function scanSourceLogs(roots) {
|
|
@@ -6338,7 +6600,7 @@ async function scanSourceLogs(roots) {
|
|
|
6338
6600
|
throw new Error("Failed to read a source log directory", { cause: error });
|
|
6339
6601
|
}
|
|
6340
6602
|
for (const entry of entries) {
|
|
6341
|
-
const full =
|
|
6603
|
+
const full = join12(dir, entry.name);
|
|
6342
6604
|
if (entry.isDirectory()) {
|
|
6343
6605
|
await walk(full);
|
|
6344
6606
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -6445,19 +6707,19 @@ function parseInterval(value) {
|
|
|
6445
6707
|
return seconds;
|
|
6446
6708
|
}
|
|
6447
6709
|
function abortableSleep(ms, signal) {
|
|
6448
|
-
return new Promise((
|
|
6710
|
+
return new Promise((resolve13) => {
|
|
6449
6711
|
if (signal.aborted) {
|
|
6450
|
-
|
|
6712
|
+
resolve13();
|
|
6451
6713
|
return;
|
|
6452
6714
|
}
|
|
6453
6715
|
let timer;
|
|
6454
6716
|
const onAbort = () => {
|
|
6455
6717
|
clearTimeout(timer);
|
|
6456
|
-
|
|
6718
|
+
resolve13();
|
|
6457
6719
|
};
|
|
6458
6720
|
timer = setTimeout(() => {
|
|
6459
6721
|
signal.removeEventListener("abort", onAbort);
|
|
6460
|
-
|
|
6722
|
+
resolve13();
|
|
6461
6723
|
}, ms);
|
|
6462
6724
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
6463
6725
|
});
|
|
@@ -6510,7 +6772,7 @@ async function doRunRefreshPortfolio(options, ctx) {
|
|
|
6510
6772
|
for (const ws of workspaces) {
|
|
6511
6773
|
const label = ws.label ?? ws.path;
|
|
6512
6774
|
try {
|
|
6513
|
-
const result = await computeRefresh(
|
|
6775
|
+
const { result } = await computeRefresh(
|
|
6514
6776
|
{ ...options, portfolio: false },
|
|
6515
6777
|
{ ...ctx, cwd: ws.path }
|
|
6516
6778
|
);
|
|
@@ -6579,7 +6841,7 @@ async function computeRefresh(options, ctx) {
|
|
|
6579
6841
|
const paths = basouPaths11(repositoryRoot);
|
|
6580
6842
|
await assertWorkspaceInitialized8(paths.root);
|
|
6581
6843
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
6582
|
-
|
|
6844
|
+
const result = await refreshAll({
|
|
6583
6845
|
options: {
|
|
6584
6846
|
...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
6585
6847
|
...options.force === true ? { force: true } : {},
|
|
@@ -6591,16 +6853,30 @@ async function computeRefresh(options, ctx) {
|
|
|
6591
6853
|
paths,
|
|
6592
6854
|
nowIso
|
|
6593
6855
|
});
|
|
6856
|
+
return { result, paths };
|
|
6594
6857
|
}
|
|
6595
6858
|
async function doRunRefresh(options, ctx) {
|
|
6596
|
-
const result = await computeRefresh(options, ctx);
|
|
6859
|
+
const { result, paths } = await computeRefresh(options, ctx);
|
|
6860
|
+
const channelLine = options.dryRun === true ? null : await syncCodexOrientationChannel(paths, ctx.codexChannelPath);
|
|
6597
6861
|
if (options.json === true) {
|
|
6598
6862
|
console.log(JSON.stringify(result));
|
|
6599
6863
|
} else {
|
|
6600
6864
|
printRefreshSummary(result);
|
|
6865
|
+
if (channelLine !== null) console.log(channelLine);
|
|
6601
6866
|
}
|
|
6602
6867
|
return result;
|
|
6603
6868
|
}
|
|
6869
|
+
async function syncCodexOrientationChannel(paths, channelPath) {
|
|
6870
|
+
try {
|
|
6871
|
+
const rendered = await renderOrientationToCodexChannel({
|
|
6872
|
+
orientationPath: paths.files.orientation,
|
|
6873
|
+
...channelPath !== void 0 ? { channelPath } : {}
|
|
6874
|
+
});
|
|
6875
|
+
return rendered === null ? null : rendered.line;
|
|
6876
|
+
} catch (error) {
|
|
6877
|
+
return `codex channel skipped: ${error instanceof Error ? error.message : String(error)}`;
|
|
6878
|
+
}
|
|
6879
|
+
}
|
|
6604
6880
|
function describeImport(outcome) {
|
|
6605
6881
|
if (outcome.status === "skipped") {
|
|
6606
6882
|
return `${outcome.adapter}: skipped (${outcome.reason})`;
|
|
@@ -6733,9 +7009,195 @@ async function assertWorkspaceInitialized9(basouRoot) {
|
|
|
6733
7009
|
}
|
|
6734
7010
|
}
|
|
6735
7011
|
|
|
6736
|
-
// src/commands/review
|
|
7012
|
+
// src/commands/review.ts
|
|
7013
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
7014
|
+
import { homedir as homedir10 } from "os";
|
|
7015
|
+
import { resolve as resolve10 } from "path";
|
|
6737
7016
|
import {
|
|
7017
|
+
assertBasouRootSafe as assertBasouRootSafe11,
|
|
6738
7018
|
basouPaths as basouPaths13,
|
|
7019
|
+
buildReviewRecordedEvent,
|
|
7020
|
+
buildReviewRecordLabel,
|
|
7021
|
+
createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
|
|
7022
|
+
findErrorCode as findErrorCode11,
|
|
7023
|
+
parseReviewRecordInput,
|
|
7024
|
+
readManifest as readManifest7,
|
|
7025
|
+
sanitizePath as sanitizePath2
|
|
7026
|
+
} from "@basou/core";
|
|
7027
|
+
function registerReviewCommand(program2) {
|
|
7028
|
+
const review = program2.command("review").description("Record reviews that ran (the durable signal a review happened)");
|
|
7029
|
+
review.command("record").description(
|
|
7030
|
+
"Record that a review ran, from a JSON object (stdin or --file). The in-loop agent runs an adversarial / second-opinion review and pipes a description -- reviewer, target, optional verdict/findings/blocked -- and basou writes one review_recorded event deterministically."
|
|
7031
|
+
).option("--file <path>", "Read the JSON object from a file instead of stdin").option("--dry-run", "Validate and preview the review without writing it").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").addHelpText("after", REVIEW_RECORD_HELP).action(async (options) => {
|
|
7032
|
+
await runReviewRecord(options);
|
|
7033
|
+
});
|
|
7034
|
+
}
|
|
7035
|
+
var REVIEW_RECORD_HELP = `
|
|
7036
|
+
Input format (a single JSON object describing one review):
|
|
7037
|
+
{
|
|
7038
|
+
"reviewer": "codex",
|
|
7039
|
+
"target": "working-tree",
|
|
7040
|
+
"verdict": "needs-attention",
|
|
7041
|
+
"findings": [
|
|
7042
|
+
{ "title": "Off-by-one in pager", "severity": "medium", "location": "src/page.ts:42", "summary": "..." }
|
|
7043
|
+
],
|
|
7044
|
+
"blocked": [
|
|
7045
|
+
{ "title": "Reviewer wanted to drop the singleton", "reason": "design-reversal", "why": "Settled in decision_X" }
|
|
7046
|
+
]
|
|
7047
|
+
}
|
|
7048
|
+
|
|
7049
|
+
Only "reviewer" and "target" are required; verdict / findings / blocked are
|
|
7050
|
+
optional. Record blocked findings (spec-deviation / design-reversal) here so the
|
|
7051
|
+
adversarial-review protocol's "always report what you blocked" becomes a durable
|
|
7052
|
+
trail artifact -- an explicit empty "blocked": [] is encouraged to record that
|
|
7053
|
+
you blocked nothing. The review is written into one ad-hoc session timestamped
|
|
7054
|
+
now. Run from a workspace-view directory and it resolves to the planning repo,
|
|
7055
|
+
like 'basou decision capture' / 'basou note'.
|
|
7056
|
+
|
|
7057
|
+
Example (heredoc on stdin):
|
|
7058
|
+
basou review record <<'JSON'
|
|
7059
|
+
{ "reviewer": "codex", "target": "working-tree", "verdict": "pass", "blocked": [] }
|
|
7060
|
+
JSON
|
|
7061
|
+
`;
|
|
7062
|
+
async function runReviewRecord(options, ctx = {}) {
|
|
7063
|
+
try {
|
|
7064
|
+
await doRunReviewRecord(options, ctx);
|
|
7065
|
+
} catch (error) {
|
|
7066
|
+
renderCliError(error, {
|
|
7067
|
+
verbose: isVerbose(options),
|
|
7068
|
+
classifiers: [failedToFinalizeClassifier]
|
|
7069
|
+
});
|
|
7070
|
+
process.exitCode = 1;
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
7073
|
+
async function doRunReviewRecord(options, ctx) {
|
|
7074
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
7075
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "review record");
|
|
7076
|
+
const paths = basouPaths13(repositoryRoot);
|
|
7077
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
7078
|
+
const raw = await readReviewInput(options, ctx);
|
|
7079
|
+
const review = parseReviewRecordInput(raw);
|
|
7080
|
+
if (options.dryRun === true) {
|
|
7081
|
+
printReviewPreview(options, review);
|
|
7082
|
+
return;
|
|
7083
|
+
}
|
|
7084
|
+
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
7085
|
+
const occurredAt = now.toISOString();
|
|
7086
|
+
const manifest = await readManifest7(paths);
|
|
7087
|
+
const invocationArgs = options.file !== void 0 ? [
|
|
7088
|
+
"--file",
|
|
7089
|
+
sanitizePath2(resolve10(cwd, options.file), {
|
|
7090
|
+
workingDirectory: repositoryRoot,
|
|
7091
|
+
homedir: homedir10()
|
|
7092
|
+
})
|
|
7093
|
+
] : [];
|
|
7094
|
+
const adHoc = await createAdHocSessionWithEvent3({
|
|
7095
|
+
paths,
|
|
7096
|
+
manifest,
|
|
7097
|
+
label: buildReviewRecordLabel(review),
|
|
7098
|
+
occurredAt,
|
|
7099
|
+
sessionSource: "human",
|
|
7100
|
+
workingDirectory: repositoryRoot,
|
|
7101
|
+
invocation: { command: "basou review record", args: invocationArgs },
|
|
7102
|
+
targetEventBuilders: [
|
|
7103
|
+
(sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review })
|
|
7104
|
+
]
|
|
7105
|
+
});
|
|
7106
|
+
printReviewResult(options, {
|
|
7107
|
+
sessionId: adHoc.sessionId,
|
|
7108
|
+
eventId: adHoc.targetEventIds[0],
|
|
7109
|
+
review
|
|
7110
|
+
});
|
|
7111
|
+
}
|
|
7112
|
+
async function readReviewInput(options, ctx) {
|
|
7113
|
+
if (options.file !== void 0) {
|
|
7114
|
+
try {
|
|
7115
|
+
return await readFile5(options.file, "utf8");
|
|
7116
|
+
} catch (error) {
|
|
7117
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
7118
|
+
throw new Error(`Input file not found: ${options.file}`);
|
|
7119
|
+
}
|
|
7120
|
+
throw error;
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
if (ctx.readInput !== void 0) {
|
|
7124
|
+
return await ctx.readInput();
|
|
7125
|
+
}
|
|
7126
|
+
if (process.stdin.isTTY === true) {
|
|
7127
|
+
throw new Error(NO_INPUT_HINT2);
|
|
7128
|
+
}
|
|
7129
|
+
return await readStdinToEnd2();
|
|
7130
|
+
}
|
|
7131
|
+
async function readStdinToEnd2() {
|
|
7132
|
+
const chunks = [];
|
|
7133
|
+
for await (const chunk of process.stdin) {
|
|
7134
|
+
chunks.push(Buffer.from(chunk));
|
|
7135
|
+
}
|
|
7136
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
7137
|
+
}
|
|
7138
|
+
var NO_INPUT_HINT2 = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
|
|
7139
|
+
function reviewToPayload(review) {
|
|
7140
|
+
const payload = {
|
|
7141
|
+
reviewer: review.reviewer,
|
|
7142
|
+
target: review.target
|
|
7143
|
+
};
|
|
7144
|
+
if (review.verdict !== void 0) payload.verdict = review.verdict;
|
|
7145
|
+
if (review.findings !== void 0) payload.findings = review.findings;
|
|
7146
|
+
if (review.blocked !== void 0) payload.blocked = review.blocked;
|
|
7147
|
+
return payload;
|
|
7148
|
+
}
|
|
7149
|
+
function reviewSummaryLine(review) {
|
|
7150
|
+
const parts = [];
|
|
7151
|
+
if (review.verdict !== void 0) parts.push(`verdict: ${review.verdict}`);
|
|
7152
|
+
if (review.findings !== void 0) {
|
|
7153
|
+
parts.push(`${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}`);
|
|
7154
|
+
}
|
|
7155
|
+
if (review.blocked !== void 0) {
|
|
7156
|
+
parts.push(`${review.blocked.length} blocked`);
|
|
7157
|
+
}
|
|
7158
|
+
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
7159
|
+
}
|
|
7160
|
+
function printReviewPreview(options, review) {
|
|
7161
|
+
if (options.json === true) {
|
|
7162
|
+
console.log(JSON.stringify({ dry_run: true, review: reviewToPayload(review) }));
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
7165
|
+
console.log(
|
|
7166
|
+
`Would record review by ${review.reviewer} of ${review.target}${reviewSummaryLine(review)} (dry run; nothing written).`
|
|
7167
|
+
);
|
|
7168
|
+
}
|
|
7169
|
+
function printReviewResult(options, result) {
|
|
7170
|
+
const sid = shortSessionId(result.sessionId);
|
|
7171
|
+
if (options.json === true) {
|
|
7172
|
+
console.log(
|
|
7173
|
+
JSON.stringify({
|
|
7174
|
+
mode: "ad-hoc",
|
|
7175
|
+
session_id: result.sessionId,
|
|
7176
|
+
session_status: "completed",
|
|
7177
|
+
event_id: result.eventId,
|
|
7178
|
+
review: reviewToPayload(result.review)
|
|
7179
|
+
})
|
|
7180
|
+
);
|
|
7181
|
+
return;
|
|
7182
|
+
}
|
|
7183
|
+
console.log(
|
|
7184
|
+
`Recorded review by ${result.review.reviewer} of ${result.review.target}${reviewSummaryLine(result.review)} in ad-hoc session ${sid}.`
|
|
7185
|
+
);
|
|
7186
|
+
}
|
|
7187
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
7188
|
+
try {
|
|
7189
|
+
await assertBasouRootSafe11(basouRoot);
|
|
7190
|
+
} catch (error) {
|
|
7191
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
7192
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
7193
|
+
}
|
|
7194
|
+
throw error;
|
|
7195
|
+
}
|
|
7196
|
+
}
|
|
7197
|
+
|
|
7198
|
+
// src/commands/review-gaps.ts
|
|
7199
|
+
import {
|
|
7200
|
+
basouPaths as basouPaths14,
|
|
6739
7201
|
findReviewGaps
|
|
6740
7202
|
} from "@basou/core";
|
|
6741
7203
|
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
@@ -6776,7 +7238,7 @@ async function runReviewGaps(options, ctx = {}) {
|
|
|
6776
7238
|
async function doRunReviewGaps(options, ctx) {
|
|
6777
7239
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6778
7240
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
|
|
6779
|
-
const paths =
|
|
7241
|
+
const paths = basouPaths14(repositoryRoot);
|
|
6780
7242
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
6781
7243
|
const summary = await findReviewGaps({
|
|
6782
7244
|
paths,
|
|
@@ -6859,23 +7321,25 @@ function renderReviewGaps(summary) {
|
|
|
6859
7321
|
|
|
6860
7322
|
// src/commands/run.ts
|
|
6861
7323
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
6862
|
-
import { homedir as
|
|
6863
|
-
import { join as
|
|
7324
|
+
import { homedir as homedir11 } from "os";
|
|
7325
|
+
import { join as join13 } from "path";
|
|
6864
7326
|
import {
|
|
6865
7327
|
acquireLock as acquireLock5,
|
|
6866
|
-
assertBasouRootSafe as
|
|
6867
|
-
basouPaths as
|
|
7328
|
+
assertBasouRootSafe as assertBasouRootSafe12,
|
|
7329
|
+
basouPaths as basouPaths15,
|
|
6868
7330
|
ChildProcessRunner as ChildProcessRunner2,
|
|
6869
7331
|
claudeCodeAdapterMetadata,
|
|
7332
|
+
codexAdapterMetadata,
|
|
6870
7333
|
appendChainedEvent as coreAppendChainedEvent2,
|
|
6871
7334
|
finalizeSessionYaml as finalizeSessionYaml2,
|
|
6872
7335
|
getDiff,
|
|
6873
7336
|
getSnapshot as getSnapshot2,
|
|
6874
7337
|
overwriteYamlFile as overwriteYamlFile2,
|
|
6875
7338
|
prefixedUlid as prefixedUlid4,
|
|
6876
|
-
readManifest as
|
|
7339
|
+
readManifest as readManifest8,
|
|
6877
7340
|
readYamlFile as readYamlFile6,
|
|
6878
7341
|
resolveClaudeCodeCommand,
|
|
7342
|
+
resolveCodexCommand,
|
|
6879
7343
|
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
6880
7344
|
SessionSchema as SessionSchema2,
|
|
6881
7345
|
sanitizeRelatedFiles,
|
|
@@ -6883,50 +7347,72 @@ import {
|
|
|
6883
7347
|
writeYamlFile as writeYamlFile2
|
|
6884
7348
|
} from "@basou/core";
|
|
6885
7349
|
function registerRunCommand(program2, ctx = {}) {
|
|
6886
|
-
const runCommand =
|
|
6887
|
-
|
|
7350
|
+
const runCommand = addRunOptions(
|
|
7351
|
+
program2.command("run").description("Run an AI coding tool through Basou as a tracked session").enablePositionalOptions()
|
|
7352
|
+
);
|
|
7353
|
+
const dispatch = async (args, options, command, run) => {
|
|
6888
7354
|
const parentOptions = command.parent?.opts() ?? {};
|
|
6889
7355
|
const snapshotOn = parentOptions.snapshot !== false && options.snapshot !== false;
|
|
6890
|
-
const merged = {
|
|
6891
|
-
...parentOptions,
|
|
6892
|
-
...options,
|
|
6893
|
-
snapshot: snapshotOn
|
|
6894
|
-
};
|
|
7356
|
+
const merged = { ...parentOptions, ...options, snapshot: snapshotOn };
|
|
6895
7357
|
try {
|
|
6896
|
-
const exitCode = await
|
|
7358
|
+
const exitCode = await run(args, merged, ctx);
|
|
6897
7359
|
process.exit(exitCode);
|
|
6898
7360
|
} catch (error) {
|
|
6899
7361
|
renderCliError(error, { verbose: isVerbose(merged) });
|
|
6900
7362
|
process.exit(1);
|
|
6901
7363
|
}
|
|
7364
|
+
};
|
|
7365
|
+
addRunOptions(runCommand.command("claude-code [args...]")).description("Run Claude Code CLI as a Basou-tracked session").passThroughOptions().action(
|
|
7366
|
+
(args, options, command) => dispatch(args, options, command, runClaudeCode)
|
|
7367
|
+
);
|
|
7368
|
+
addRunOptions(runCommand.command("codex [args...]")).description("Run the Codex CLI as a Basou-tracked session").passThroughOptions().action(
|
|
7369
|
+
(args, options, command) => dispatch(args, options, command, runCodex)
|
|
7370
|
+
);
|
|
7371
|
+
}
|
|
7372
|
+
function addRunOptions(command) {
|
|
7373
|
+
return command.option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes");
|
|
7374
|
+
}
|
|
7375
|
+
function runClaudeCode(args, options, ctx = {}) {
|
|
7376
|
+
return runTrackedTool(args, options, ctx, {
|
|
7377
|
+
resolveCommand: ctx.resolveCommand ?? resolveClaudeCodeCommand,
|
|
7378
|
+
metadata: claudeCodeAdapterMetadata
|
|
6902
7379
|
});
|
|
6903
7380
|
}
|
|
6904
|
-
|
|
7381
|
+
function runCodex(args, options, ctx = {}) {
|
|
7382
|
+
return runTrackedTool(args, options, ctx, {
|
|
7383
|
+
resolveCommand: ctx.resolveCodexCommand ?? resolveCodexCommand,
|
|
7384
|
+
metadata: codexAdapterMetadata,
|
|
7385
|
+
transformArgs: (a) => ["-c", "shell_environment_policy.inherit=all", ...a],
|
|
7386
|
+
preSpawn: syncCodexOrientationChannelPreSpawn
|
|
7387
|
+
});
|
|
7388
|
+
}
|
|
7389
|
+
async function runTrackedTool(args, options, ctx, adapter) {
|
|
6905
7390
|
const runner = ctx.runner ?? new ChildProcessRunner2();
|
|
6906
7391
|
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
6907
|
-
const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
|
|
6908
7392
|
const getDiffFn = ctx.getDiff ?? getDiff;
|
|
6909
|
-
const { command } = await resolveCommand();
|
|
7393
|
+
const { command } = await adapter.resolveCommand();
|
|
7394
|
+
const childArgs = adapter.transformArgs ? adapter.transformArgs(args) : args;
|
|
6910
7395
|
const cwd = options.cwd ?? process.cwd();
|
|
6911
7396
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
6912
|
-
const paths =
|
|
6913
|
-
await
|
|
6914
|
-
const manifest = await
|
|
7397
|
+
const paths = basouPaths15(repoRoot);
|
|
7398
|
+
await assertBasouRootSafe12(paths.root);
|
|
7399
|
+
const manifest = await readManifest8(paths);
|
|
6915
7400
|
const sessionId = prefixedUlid4("ses");
|
|
6916
|
-
const sessionDir =
|
|
7401
|
+
const sessionDir = join13(paths.sessions, sessionId);
|
|
6917
7402
|
await mkdir2(sessionDir, { recursive: true });
|
|
6918
7403
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
6919
7404
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
6920
7405
|
});
|
|
6921
7406
|
const startedAt = now().toISOString();
|
|
6922
|
-
const sessionYamlPath =
|
|
7407
|
+
const sessionYamlPath = join13(sessionDir, "session.yaml");
|
|
6923
7408
|
const session = buildInitialSession2({
|
|
6924
7409
|
id: sessionId,
|
|
6925
7410
|
command,
|
|
6926
|
-
args,
|
|
7411
|
+
args: childArgs,
|
|
6927
7412
|
cwd: repoRoot,
|
|
6928
7413
|
workspaceId: manifest.workspace.id,
|
|
6929
|
-
startedAt
|
|
7414
|
+
startedAt,
|
|
7415
|
+
source: adapter.metadata
|
|
6930
7416
|
});
|
|
6931
7417
|
await writeYamlFile2(sessionYamlPath, session);
|
|
6932
7418
|
await appendEvent(sessionDir, {
|
|
@@ -6935,7 +7421,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
6935
7421
|
id: prefixedUlid4("evt"),
|
|
6936
7422
|
session_id: sessionId,
|
|
6937
7423
|
occurred_at: startedAt,
|
|
6938
|
-
source:
|
|
7424
|
+
source: adapter.metadata.kind
|
|
6939
7425
|
});
|
|
6940
7426
|
let preSnapshot = null;
|
|
6941
7427
|
if (options.snapshot !== false) {
|
|
@@ -6948,7 +7434,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
6948
7434
|
id: prefixedUlid4("evt"),
|
|
6949
7435
|
session_id: sessionId,
|
|
6950
7436
|
occurred_at: runningAt,
|
|
6951
|
-
source:
|
|
7437
|
+
source: adapter.metadata.kind,
|
|
6952
7438
|
from: "initialized",
|
|
6953
7439
|
to: "running"
|
|
6954
7440
|
});
|
|
@@ -6982,10 +7468,14 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
6982
7468
|
process.on("SIGTERM", onSigTerm);
|
|
6983
7469
|
process.on("exit", exitHandler);
|
|
6984
7470
|
ctx.onExitHookInstalled?.(exitHandler);
|
|
7471
|
+
if (adapter.preSpawn !== void 0) {
|
|
7472
|
+
const line = await adapter.preSpawn(cwd, ctx);
|
|
7473
|
+
if (line !== null) console.log(line);
|
|
7474
|
+
}
|
|
6985
7475
|
let result;
|
|
6986
7476
|
try {
|
|
6987
7477
|
try {
|
|
6988
|
-
result = await runner.run(command,
|
|
7478
|
+
result = await runner.run(command, childArgs, {
|
|
6989
7479
|
cwd: repoRoot,
|
|
6990
7480
|
capture: "none",
|
|
6991
7481
|
signal: controller.signal,
|
|
@@ -6996,10 +7486,11 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
6996
7486
|
} catch (spawnError) {
|
|
6997
7487
|
await finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, {
|
|
6998
7488
|
command,
|
|
6999
|
-
args,
|
|
7489
|
+
args: childArgs,
|
|
7000
7490
|
cwd: repoRoot,
|
|
7001
7491
|
occurredAt: now().toISOString(),
|
|
7002
|
-
signalReceived
|
|
7492
|
+
signalReceived,
|
|
7493
|
+
sourceKind: adapter.metadata.kind
|
|
7003
7494
|
});
|
|
7004
7495
|
throw spawnError;
|
|
7005
7496
|
}
|
|
@@ -7018,7 +7509,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7018
7509
|
occurred_at: endedAt,
|
|
7019
7510
|
source: "terminal-recording",
|
|
7020
7511
|
command,
|
|
7021
|
-
args,
|
|
7512
|
+
args: childArgs,
|
|
7022
7513
|
cwd: repoRoot,
|
|
7023
7514
|
exit_code: result.exit_code,
|
|
7024
7515
|
...result.signal !== null ? { signal: result.signal } : {},
|
|
@@ -7045,7 +7536,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7045
7536
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
7046
7537
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
7047
7538
|
workingDirectory: repoRoot,
|
|
7048
|
-
homedir:
|
|
7539
|
+
homedir: homedir11()
|
|
7049
7540
|
}).sanitized;
|
|
7050
7541
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
7051
7542
|
await appendEvent(sessionDir, {
|
|
@@ -7054,7 +7545,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7054
7545
|
id: prefixedUlid4("evt"),
|
|
7055
7546
|
session_id: sessionId,
|
|
7056
7547
|
occurred_at: endedAt,
|
|
7057
|
-
source:
|
|
7548
|
+
source: adapter.metadata.kind,
|
|
7058
7549
|
from: "running",
|
|
7059
7550
|
to: finalStatus
|
|
7060
7551
|
});
|
|
@@ -7064,7 +7555,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7064
7555
|
id: prefixedUlid4("evt"),
|
|
7065
7556
|
session_id: sessionId,
|
|
7066
7557
|
occurred_at: endedAt,
|
|
7067
|
-
source:
|
|
7558
|
+
source: adapter.metadata.kind,
|
|
7068
7559
|
...result.exit_code !== null ? { exit_code: result.exit_code } : {}
|
|
7069
7560
|
});
|
|
7070
7561
|
await finalizeSessionYaml2(paths, sessionId, (s) => {
|
|
@@ -7186,10 +7677,10 @@ function buildInitialSession2(input) {
|
|
|
7186
7677
|
label: `basou run ${cmdline} (${input.startedAt})`,
|
|
7187
7678
|
task_id: null,
|
|
7188
7679
|
workspace_id: input.workspaceId,
|
|
7189
|
-
source: { ...
|
|
7680
|
+
source: { ...input.source },
|
|
7190
7681
|
started_at: input.startedAt,
|
|
7191
7682
|
status: "initialized",
|
|
7192
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
7683
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir11() }),
|
|
7193
7684
|
invocation: {
|
|
7194
7685
|
command: input.command,
|
|
7195
7686
|
args: [...input.args],
|
|
@@ -7229,7 +7720,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
7229
7720
|
id: prefixedUlid4("evt"),
|
|
7230
7721
|
session_id: sessionId,
|
|
7231
7722
|
occurred_at: ctx.occurredAt,
|
|
7232
|
-
source:
|
|
7723
|
+
source: ctx.sourceKind,
|
|
7233
7724
|
from: "running",
|
|
7234
7725
|
to: "failed"
|
|
7235
7726
|
});
|
|
@@ -7239,7 +7730,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
7239
7730
|
id: prefixedUlid4("evt"),
|
|
7240
7731
|
session_id: sessionId,
|
|
7241
7732
|
occurred_at: ctx.occurredAt,
|
|
7242
|
-
source:
|
|
7733
|
+
source: ctx.sourceKind
|
|
7243
7734
|
});
|
|
7244
7735
|
await finalizeSessionYaml2(paths, sessionId, (s) => {
|
|
7245
7736
|
s.session.status = "failed";
|
|
@@ -7259,21 +7750,34 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
7259
7750
|
throw error;
|
|
7260
7751
|
}
|
|
7261
7752
|
}
|
|
7753
|
+
async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
|
|
7754
|
+
try {
|
|
7755
|
+
const root = await resolveBasouRootForCommand(cwd, "run");
|
|
7756
|
+
const paths = basouPaths15(root);
|
|
7757
|
+
const rendered = await renderOrientationToCodexChannel({
|
|
7758
|
+
orientationPath: paths.files.orientation,
|
|
7759
|
+
...ctx.codexChannelPath !== void 0 ? { channelPath: ctx.codexChannelPath } : {}
|
|
7760
|
+
});
|
|
7761
|
+
return rendered === null ? null : rendered.line;
|
|
7762
|
+
} catch {
|
|
7763
|
+
return null;
|
|
7764
|
+
}
|
|
7765
|
+
}
|
|
7262
7766
|
|
|
7263
7767
|
// src/commands/session.ts
|
|
7264
|
-
import { readFile as
|
|
7265
|
-
import { basename as basename6, isAbsolute as isAbsolute6, join as
|
|
7768
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
7769
|
+
import { basename as basename6, isAbsolute as isAbsolute6, join as join14, relative as relative3 } from "path";
|
|
7266
7770
|
import {
|
|
7267
7771
|
acquireLock as acquireLock6,
|
|
7268
7772
|
appendEventToExistingSession as appendEventToExistingSession3,
|
|
7269
|
-
assertBasouRootSafe as
|
|
7270
|
-
basouPaths as
|
|
7773
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
7774
|
+
basouPaths as basouPaths16,
|
|
7271
7775
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
7272
|
-
findErrorCode as
|
|
7776
|
+
findErrorCode as findErrorCode12,
|
|
7273
7777
|
importSessionFromJson as importSessionFromJson2,
|
|
7274
7778
|
loadSessionEntries as loadSessionEntries2,
|
|
7275
7779
|
readAllEvents,
|
|
7276
|
-
readManifest as
|
|
7780
|
+
readManifest as readManifest9,
|
|
7277
7781
|
readYamlFile as readYamlFile7,
|
|
7278
7782
|
rechainSessionInPlace,
|
|
7279
7783
|
resolveSessionId as resolveSessionId3,
|
|
@@ -7332,8 +7836,8 @@ async function runSessionList(options, ctx = {}) {
|
|
|
7332
7836
|
async function doRunSessionList(options, ctx) {
|
|
7333
7837
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7334
7838
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
|
|
7335
|
-
const paths =
|
|
7336
|
-
await
|
|
7839
|
+
const paths = basouPaths16(repositoryRoot);
|
|
7840
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
7337
7841
|
const now = /* @__PURE__ */ new Date();
|
|
7338
7842
|
const records = (await loadSessionEntries2(paths, {
|
|
7339
7843
|
now,
|
|
@@ -7384,17 +7888,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
|
|
|
7384
7888
|
async function doRunSessionShow(idInput, options, ctx) {
|
|
7385
7889
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7386
7890
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
|
|
7387
|
-
const paths =
|
|
7388
|
-
await
|
|
7891
|
+
const paths = basouPaths16(repositoryRoot);
|
|
7892
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
7389
7893
|
const sessionId = await resolveSessionId3(paths, idInput);
|
|
7390
|
-
const sessionDir =
|
|
7391
|
-
const sessionYamlPath =
|
|
7894
|
+
const sessionDir = join14(paths.sessions, sessionId);
|
|
7895
|
+
const sessionYamlPath = join14(sessionDir, "session.yaml");
|
|
7392
7896
|
let session;
|
|
7393
7897
|
try {
|
|
7394
7898
|
const raw = await readYamlFile7(sessionYamlPath);
|
|
7395
7899
|
session = SessionSchema3.parse(raw);
|
|
7396
7900
|
} catch (error) {
|
|
7397
|
-
if (
|
|
7901
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
7398
7902
|
throw new Error(`Session not found: ${idInput}`);
|
|
7399
7903
|
}
|
|
7400
7904
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -7588,6 +8092,11 @@ function eventVariantSummary(ev) {
|
|
|
7588
8092
|
return `task ${shortTaskId2(ev.task_id)}: ${ev.title} (archived)`;
|
|
7589
8093
|
case "note_added":
|
|
7590
8094
|
return ev.body.length > 80 ? `${ev.body.slice(0, 77)}...` : ev.body;
|
|
8095
|
+
case "review_recorded": {
|
|
8096
|
+
const verdict = ev.verdict !== void 0 ? ` (${ev.verdict})` : "";
|
|
8097
|
+
const blocked = ev.blocked !== void 0 && ev.blocked.length > 0 ? ` ${ev.blocked.length} blocked` : "";
|
|
8098
|
+
return `${ev.reviewer} -> ${ev.target}${verdict}${blocked}`;
|
|
8099
|
+
}
|
|
7591
8100
|
case "adapter_output":
|
|
7592
8101
|
return `${ev.stream} "${ev.summary}" raw_ref=${ev.raw_ref}`;
|
|
7593
8102
|
}
|
|
@@ -7635,11 +8144,11 @@ function maxLen2(values, floor) {
|
|
|
7635
8144
|
async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
7636
8145
|
return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
|
|
7637
8146
|
}
|
|
7638
|
-
async function
|
|
8147
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
7639
8148
|
try {
|
|
7640
|
-
await
|
|
8149
|
+
await assertBasouRootSafe13(basouRoot);
|
|
7641
8150
|
} catch (error) {
|
|
7642
|
-
if (
|
|
8151
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
7643
8152
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
7644
8153
|
}
|
|
7645
8154
|
throw error;
|
|
@@ -7677,9 +8186,9 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
7677
8186
|
async function doRunSessionImport(options, ctx) {
|
|
7678
8187
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7679
8188
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
7680
|
-
const paths =
|
|
7681
|
-
await
|
|
7682
|
-
const manifest = await
|
|
8189
|
+
const paths = basouPaths16(repositoryRoot);
|
|
8190
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
8191
|
+
const manifest = await readManifest9(paths);
|
|
7683
8192
|
const rawBody = await readInputFile(options.from);
|
|
7684
8193
|
const json = parseJsonStrict(rawBody);
|
|
7685
8194
|
const parsed = SessionImportPayloadSchema2.safeParse(json);
|
|
@@ -7706,12 +8215,12 @@ async function doRunSessionImport(options, ctx) {
|
|
|
7706
8215
|
}
|
|
7707
8216
|
async function readInputFile(path) {
|
|
7708
8217
|
try {
|
|
7709
|
-
return await
|
|
8218
|
+
return await readFile6(path, "utf8");
|
|
7710
8219
|
} catch (error) {
|
|
7711
|
-
if (
|
|
8220
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
7712
8221
|
throw new Error("Import source not found", { cause: error });
|
|
7713
8222
|
}
|
|
7714
|
-
if (
|
|
8223
|
+
if (findErrorCode12(error, "EISDIR")) {
|
|
7715
8224
|
throw new Error("Import source is not a file", { cause: error });
|
|
7716
8225
|
}
|
|
7717
8226
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -7791,8 +8300,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
7791
8300
|
}
|
|
7792
8301
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7793
8302
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
7794
|
-
const paths =
|
|
7795
|
-
await
|
|
8303
|
+
const paths = basouPaths16(repositoryRoot);
|
|
8304
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
7796
8305
|
const sessionId = await resolveSessionId3(paths, sessionIdInput);
|
|
7797
8306
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
7798
8307
|
if (body.length === 0) {
|
|
@@ -7823,12 +8332,12 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
7823
8332
|
}
|
|
7824
8333
|
async function readNoteFile(path) {
|
|
7825
8334
|
try {
|
|
7826
|
-
return await
|
|
8335
|
+
return await readFile6(path, "utf8");
|
|
7827
8336
|
} catch (error) {
|
|
7828
|
-
if (
|
|
8337
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
7829
8338
|
throw new Error("Note source not found", { cause: error });
|
|
7830
8339
|
}
|
|
7831
|
-
if (
|
|
8340
|
+
if (findErrorCode12(error, "EISDIR")) {
|
|
7832
8341
|
throw new Error("Note source is not a file", { cause: error });
|
|
7833
8342
|
}
|
|
7834
8343
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -7873,8 +8382,8 @@ async function doRunSessionRechain(options, ctx) {
|
|
|
7873
8382
|
}
|
|
7874
8383
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7875
8384
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
7876
|
-
const paths =
|
|
7877
|
-
await
|
|
8385
|
+
const paths = basouPaths16(repositoryRoot);
|
|
8386
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
7878
8387
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
7879
8388
|
const dryRun = options.dryRun === true;
|
|
7880
8389
|
const rows = [];
|
|
@@ -7927,10 +8436,10 @@ function renderRechainRow(row, dryRun) {
|
|
|
7927
8436
|
|
|
7928
8437
|
// src/commands/stats.ts
|
|
7929
8438
|
import {
|
|
7930
|
-
assertBasouRootSafe as
|
|
7931
|
-
basouPaths as
|
|
8439
|
+
assertBasouRootSafe as assertBasouRootSafe14,
|
|
8440
|
+
basouPaths as basouPaths17,
|
|
7932
8441
|
computeWorkStats,
|
|
7933
|
-
findErrorCode as
|
|
8442
|
+
findErrorCode as findErrorCode13,
|
|
7934
8443
|
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
7935
8444
|
} from "@basou/core";
|
|
7936
8445
|
function registerStatsCommand(program2) {
|
|
@@ -7949,8 +8458,8 @@ async function runStats(options, ctx = {}) {
|
|
|
7949
8458
|
async function doRunStats(options, ctx) {
|
|
7950
8459
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7951
8460
|
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
7952
|
-
const paths =
|
|
7953
|
-
await
|
|
8461
|
+
const paths = basouPaths17(repositoryRoot);
|
|
8462
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
7954
8463
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
7955
8464
|
const result = await computeWorkStats({
|
|
7956
8465
|
paths,
|
|
@@ -8044,11 +8553,11 @@ async function resolveRepositoryRootForStats(cwd) {
|
|
|
8044
8553
|
throw error;
|
|
8045
8554
|
}
|
|
8046
8555
|
}
|
|
8047
|
-
async function
|
|
8556
|
+
async function assertWorkspaceInitialized12(basouRoot) {
|
|
8048
8557
|
try {
|
|
8049
|
-
await
|
|
8558
|
+
await assertBasouRootSafe14(basouRoot);
|
|
8050
8559
|
} catch (error) {
|
|
8051
|
-
if (
|
|
8560
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
8052
8561
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
8053
8562
|
}
|
|
8054
8563
|
throw error;
|
|
@@ -8057,11 +8566,11 @@ async function assertWorkspaceInitialized11(basouRoot) {
|
|
|
8057
8566
|
|
|
8058
8567
|
// src/commands/status.ts
|
|
8059
8568
|
import {
|
|
8060
|
-
assertBasouRootSafe as
|
|
8061
|
-
basouPaths as
|
|
8569
|
+
assertBasouRootSafe as assertBasouRootSafe15,
|
|
8570
|
+
basouPaths as basouPaths18,
|
|
8062
8571
|
buildStatusSnapshot,
|
|
8063
|
-
findErrorCode as
|
|
8064
|
-
readManifest as
|
|
8572
|
+
findErrorCode as findErrorCode14,
|
|
8573
|
+
readManifest as readManifest10,
|
|
8065
8574
|
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
8066
8575
|
writeStatus
|
|
8067
8576
|
} from "@basou/core";
|
|
@@ -8081,20 +8590,20 @@ async function runStatus(options, ctx = {}) {
|
|
|
8081
8590
|
async function doRunStatus(options, ctx) {
|
|
8082
8591
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8083
8592
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
8084
|
-
const paths =
|
|
8593
|
+
const paths = basouPaths18(repositoryRoot);
|
|
8085
8594
|
try {
|
|
8086
|
-
await
|
|
8595
|
+
await assertBasouRootSafe15(paths.root);
|
|
8087
8596
|
} catch (error) {
|
|
8088
|
-
if (
|
|
8597
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
8089
8598
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
8090
8599
|
}
|
|
8091
8600
|
throw error;
|
|
8092
8601
|
}
|
|
8093
8602
|
let manifest;
|
|
8094
8603
|
try {
|
|
8095
|
-
manifest = await
|
|
8604
|
+
manifest = await readManifest10(paths);
|
|
8096
8605
|
} catch (error) {
|
|
8097
|
-
if (
|
|
8606
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
8098
8607
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
8099
8608
|
}
|
|
8100
8609
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -8130,21 +8639,21 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
8130
8639
|
}
|
|
8131
8640
|
|
|
8132
8641
|
// src/commands/task.ts
|
|
8133
|
-
import { readFile as
|
|
8134
|
-
import { join as
|
|
8642
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
8643
|
+
import { join as join15 } from "path";
|
|
8135
8644
|
import {
|
|
8136
8645
|
archiveTask,
|
|
8137
|
-
assertBasouRootSafe as
|
|
8138
|
-
basouPaths as
|
|
8646
|
+
assertBasouRootSafe as assertBasouRootSafe16,
|
|
8647
|
+
basouPaths as basouPaths19,
|
|
8139
8648
|
createTaskWithEvent,
|
|
8140
8649
|
deleteTask,
|
|
8141
8650
|
editTask,
|
|
8142
8651
|
enumerateArchivedTaskIds,
|
|
8143
|
-
findErrorCode as
|
|
8652
|
+
findErrorCode as findErrorCode15,
|
|
8144
8653
|
loadSessionEntries as loadSessionEntries3,
|
|
8145
8654
|
loadTaskEntries,
|
|
8146
8655
|
prefixedUlid as prefixedUlid5,
|
|
8147
|
-
readManifest as
|
|
8656
|
+
readManifest as readManifest11,
|
|
8148
8657
|
readTaskFile,
|
|
8149
8658
|
readTaskFileWithArchiveFallback,
|
|
8150
8659
|
reconcileAllTasks,
|
|
@@ -8236,8 +8745,8 @@ async function doRunTaskNew(options, ctx) {
|
|
|
8236
8745
|
}
|
|
8237
8746
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8238
8747
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
8239
|
-
const paths =
|
|
8240
|
-
await
|
|
8748
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8749
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8241
8750
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
8242
8751
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
8243
8752
|
const occurredAt = now.toISOString();
|
|
@@ -8271,7 +8780,7 @@ async function doRunTaskNew(options, ctx) {
|
|
|
8271
8780
|
});
|
|
8272
8781
|
return;
|
|
8273
8782
|
}
|
|
8274
|
-
const manifest = await
|
|
8783
|
+
const manifest = await readManifest11(paths);
|
|
8275
8784
|
const result = await createTaskWithEvent({
|
|
8276
8785
|
mode: "ad-hoc",
|
|
8277
8786
|
paths,
|
|
@@ -8345,8 +8854,8 @@ async function runTaskList(options, ctx = {}) {
|
|
|
8345
8854
|
async function doRunTaskList(options, ctx) {
|
|
8346
8855
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8347
8856
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
8348
|
-
const paths =
|
|
8349
|
-
await
|
|
8857
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8858
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8350
8859
|
const entries = await loadTaskEntries(paths, {
|
|
8351
8860
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
8352
8861
|
});
|
|
@@ -8449,15 +8958,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
8449
8958
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
8450
8959
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8451
8960
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
8452
|
-
const paths =
|
|
8453
|
-
await
|
|
8961
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8962
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8454
8963
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
8455
8964
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
8456
8965
|
const sessions = await loadSessionEntries3(paths, { now: /* @__PURE__ */ new Date() });
|
|
8457
8966
|
const events = [];
|
|
8458
8967
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
8459
8968
|
for (const s of sessions) {
|
|
8460
|
-
const sessionDir =
|
|
8969
|
+
const sessionDir = join15(paths.sessions, s.sessionId);
|
|
8461
8970
|
try {
|
|
8462
8971
|
for await (const ev of replayEvents3(sessionDir, {
|
|
8463
8972
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -8593,8 +9102,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
8593
9102
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
8594
9103
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8595
9104
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
8596
|
-
const paths =
|
|
8597
|
-
await
|
|
9105
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9106
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8598
9107
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
8599
9108
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
8600
9109
|
const occurredAt = now.toISOString();
|
|
@@ -8619,7 +9128,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
8619
9128
|
});
|
|
8620
9129
|
return;
|
|
8621
9130
|
}
|
|
8622
|
-
const manifest = await
|
|
9131
|
+
const manifest = await readManifest11(paths);
|
|
8623
9132
|
const result = await updateTaskStatusWithEvent({
|
|
8624
9133
|
mode: "ad-hoc",
|
|
8625
9134
|
paths,
|
|
@@ -8670,9 +9179,9 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
8670
9179
|
async function doRunTaskReconcile(options, ctx) {
|
|
8671
9180
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8672
9181
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
8673
|
-
const paths =
|
|
8674
|
-
await
|
|
8675
|
-
const manifest = await
|
|
9182
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9183
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9184
|
+
const manifest = await readManifest11(paths);
|
|
8676
9185
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
8677
9186
|
const write = options.write === true;
|
|
8678
9187
|
const verbose = isVerbose(options);
|
|
@@ -8850,9 +9359,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
8850
9359
|
}
|
|
8851
9360
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8852
9361
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
8853
|
-
const paths =
|
|
8854
|
-
await
|
|
8855
|
-
const manifest = await
|
|
9362
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9363
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9364
|
+
const manifest = await readManifest11(paths);
|
|
8856
9365
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
8857
9366
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
8858
9367
|
const write = options.write === true;
|
|
@@ -8930,9 +9439,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
8930
9439
|
}
|
|
8931
9440
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8932
9441
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
8933
|
-
const paths =
|
|
8934
|
-
await
|
|
8935
|
-
const manifest = await
|
|
9442
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9443
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9444
|
+
const manifest = await readManifest11(paths);
|
|
8936
9445
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
8937
9446
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
8938
9447
|
const occurredAt = now.toISOString();
|
|
@@ -8986,9 +9495,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
8986
9495
|
}
|
|
8987
9496
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8988
9497
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
8989
|
-
const paths =
|
|
8990
|
-
await
|
|
8991
|
-
const manifest = await
|
|
9498
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9499
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9500
|
+
const manifest = await readManifest11(paths);
|
|
8992
9501
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
8993
9502
|
if (options.yes !== true) {
|
|
8994
9503
|
await confirmDestructiveAction("delete", taskId);
|
|
@@ -9031,9 +9540,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
9031
9540
|
}
|
|
9032
9541
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9033
9542
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
9034
|
-
const paths =
|
|
9035
|
-
await
|
|
9036
|
-
const manifest = await
|
|
9543
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9544
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9545
|
+
const manifest = await readManifest11(paths);
|
|
9037
9546
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
9038
9547
|
if (options.yes !== true) {
|
|
9039
9548
|
await confirmDestructiveAction("archive", taskId);
|
|
@@ -9145,12 +9654,12 @@ function parsePositiveInt2(raw) {
|
|
|
9145
9654
|
}
|
|
9146
9655
|
async function readDescriptionFile(path) {
|
|
9147
9656
|
try {
|
|
9148
|
-
return await
|
|
9657
|
+
return await readFile7(path, "utf8");
|
|
9149
9658
|
} catch (error) {
|
|
9150
|
-
if (
|
|
9659
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
9151
9660
|
throw new Error("Description source not found", { cause: error });
|
|
9152
9661
|
}
|
|
9153
|
-
if (
|
|
9662
|
+
if (findErrorCode15(error, "EISDIR")) {
|
|
9154
9663
|
throw new Error("Description source is not a file", { cause: error });
|
|
9155
9664
|
}
|
|
9156
9665
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -9159,11 +9668,11 @@ async function readDescriptionFile(path) {
|
|
|
9159
9668
|
async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
9160
9669
|
return resolveBasouRootForCommand(cwd, `task ${subcmd}`);
|
|
9161
9670
|
}
|
|
9162
|
-
async function
|
|
9671
|
+
async function assertWorkspaceInitialized13(basouRoot) {
|
|
9163
9672
|
try {
|
|
9164
|
-
await
|
|
9673
|
+
await assertBasouRootSafe16(basouRoot);
|
|
9165
9674
|
} catch (error) {
|
|
9166
|
-
if (
|
|
9675
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
9167
9676
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
9168
9677
|
}
|
|
9169
9678
|
throw error;
|
|
@@ -9251,10 +9760,10 @@ function maxLen3(values, floor) {
|
|
|
9251
9760
|
|
|
9252
9761
|
// src/commands/verify.ts
|
|
9253
9762
|
import {
|
|
9254
|
-
assertBasouRootSafe as
|
|
9255
|
-
basouPaths as
|
|
9763
|
+
assertBasouRootSafe as assertBasouRootSafe17,
|
|
9764
|
+
basouPaths as basouPaths20,
|
|
9256
9765
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
9257
|
-
findErrorCode as
|
|
9766
|
+
findErrorCode as findErrorCode16,
|
|
9258
9767
|
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
9259
9768
|
resolveSessionId as resolveSessionId5,
|
|
9260
9769
|
verifyEventsChain
|
|
@@ -9278,8 +9787,8 @@ async function doRunVerify(options, ctx) {
|
|
|
9278
9787
|
}
|
|
9279
9788
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9280
9789
|
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
9281
|
-
const paths =
|
|
9282
|
-
await
|
|
9790
|
+
const paths = basouPaths20(repositoryRoot);
|
|
9791
|
+
await assertWorkspaceInitialized14(paths.root);
|
|
9283
9792
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
9284
9793
|
const rows = [];
|
|
9285
9794
|
for (const sessionId of sessionIds) {
|
|
@@ -9336,11 +9845,11 @@ async function resolveRepositoryRootForVerify(cwd) {
|
|
|
9336
9845
|
throw error;
|
|
9337
9846
|
}
|
|
9338
9847
|
}
|
|
9339
|
-
async function
|
|
9848
|
+
async function assertWorkspaceInitialized14(basouRoot) {
|
|
9340
9849
|
try {
|
|
9341
|
-
await
|
|
9850
|
+
await assertBasouRootSafe17(basouRoot);
|
|
9342
9851
|
} catch (error) {
|
|
9343
|
-
if (
|
|
9852
|
+
if (findErrorCode16(error, "ENOENT")) {
|
|
9344
9853
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
9345
9854
|
}
|
|
9346
9855
|
throw error;
|
|
@@ -9350,12 +9859,12 @@ async function assertWorkspaceInitialized13(basouRoot) {
|
|
|
9350
9859
|
// src/commands/view.ts
|
|
9351
9860
|
import { spawn } from "child_process";
|
|
9352
9861
|
import { createHash } from "crypto";
|
|
9353
|
-
import { basename as basename7, resolve as
|
|
9862
|
+
import { basename as basename7, resolve as resolve12 } from "path";
|
|
9354
9863
|
import {
|
|
9355
|
-
assertBasouRootSafe as
|
|
9356
|
-
basouPaths as
|
|
9357
|
-
findErrorCode as
|
|
9358
|
-
readManifest as
|
|
9864
|
+
assertBasouRootSafe as assertBasouRootSafe18,
|
|
9865
|
+
basouPaths as basouPaths21,
|
|
9866
|
+
findErrorCode as findErrorCode18,
|
|
9867
|
+
readManifest as readManifest14,
|
|
9359
9868
|
resolveRepositoryRoot as resolveRepositoryRoot14
|
|
9360
9869
|
} from "@basou/core";
|
|
9361
9870
|
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
@@ -9363,9 +9872,9 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
|
9363
9872
|
// src/lib/portfolio-safety.ts
|
|
9364
9873
|
import { execFile } from "child_process";
|
|
9365
9874
|
import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
|
|
9366
|
-
import { isAbsolute as isAbsolute7, join as
|
|
9875
|
+
import { isAbsolute as isAbsolute7, join as join16, relative as relative4, resolve as resolve11 } from "path";
|
|
9367
9876
|
import { promisify } from "util";
|
|
9368
|
-
import { readManifest as
|
|
9877
|
+
import { readManifest as readManifest12 } from "@basou/core";
|
|
9369
9878
|
var execFileAsync = promisify(execFile);
|
|
9370
9879
|
function errorCode(error) {
|
|
9371
9880
|
return error instanceof Error ? error.code : void 0;
|
|
@@ -9374,7 +9883,7 @@ async function canonical(p) {
|
|
|
9374
9883
|
try {
|
|
9375
9884
|
return await realpath2(p);
|
|
9376
9885
|
} catch {
|
|
9377
|
-
return
|
|
9886
|
+
return resolve11(p);
|
|
9378
9887
|
}
|
|
9379
9888
|
}
|
|
9380
9889
|
function isInside(child, parent) {
|
|
@@ -9387,7 +9896,7 @@ function isBasouPath(p) {
|
|
|
9387
9896
|
async function inspectRepo(repoPath) {
|
|
9388
9897
|
let hasEntry = false;
|
|
9389
9898
|
try {
|
|
9390
|
-
await lstat2(
|
|
9899
|
+
await lstat2(join16(repoPath, ".basou"));
|
|
9391
9900
|
hasEntry = true;
|
|
9392
9901
|
} catch (error) {
|
|
9393
9902
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -9418,7 +9927,7 @@ async function checkPortfolioSafety(workspaces) {
|
|
|
9418
9927
|
const wsReal = await canonical(ws.repoRoot);
|
|
9419
9928
|
let sourceRoots = [];
|
|
9420
9929
|
try {
|
|
9421
|
-
const manifest = await
|
|
9930
|
+
const manifest = await readManifest12(ws.paths);
|
|
9422
9931
|
sourceRoots = manifest.import?.source_roots ?? [];
|
|
9423
9932
|
} catch (error) {
|
|
9424
9933
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
@@ -9436,7 +9945,7 @@ async function checkPortfolioSafety(workspaces) {
|
|
|
9436
9945
|
}
|
|
9437
9946
|
const monitored = /* @__PURE__ */ new Map();
|
|
9438
9947
|
for (const root of sourceRoots) {
|
|
9439
|
-
const display =
|
|
9948
|
+
const display = resolve11(ws.repoRoot, root);
|
|
9440
9949
|
const real = await canonical(display);
|
|
9441
9950
|
if (real !== wsReal) monitored.set(real, display);
|
|
9442
9951
|
}
|
|
@@ -9488,18 +9997,18 @@ function formatSafetyReport(result) {
|
|
|
9488
9997
|
|
|
9489
9998
|
// src/lib/view-server.ts
|
|
9490
9999
|
import { createServer } from "http";
|
|
9491
|
-
import { join as
|
|
10000
|
+
import { join as join17 } from "path";
|
|
9492
10001
|
import {
|
|
9493
10002
|
computeWorkStats as computeWorkStats2,
|
|
9494
10003
|
enumerateApprovals as enumerateApprovals2,
|
|
9495
|
-
findErrorCode as
|
|
10004
|
+
findErrorCode as findErrorCode17,
|
|
9496
10005
|
isLazyExpired as isLazyExpired2,
|
|
9497
10006
|
loadApproval as loadApproval2,
|
|
9498
10007
|
loadSessionEntries as loadSessionEntries4,
|
|
9499
10008
|
loadTaskEntries as loadTaskEntries2,
|
|
9500
10009
|
readAllEvents as readAllEvents2,
|
|
9501
|
-
readManifest as
|
|
9502
|
-
readMarkdownFile as
|
|
10010
|
+
readManifest as readManifest13,
|
|
10011
|
+
readMarkdownFile as readMarkdownFile7,
|
|
9503
10012
|
readSessionYaml as readSessionYaml3,
|
|
9504
10013
|
readTaskFile as readTaskFile2,
|
|
9505
10014
|
renderDecisions as renderDecisions3,
|
|
@@ -10149,7 +10658,7 @@ function startViewServer(opts) {
|
|
|
10149
10658
|
};
|
|
10150
10659
|
let boundPort = port;
|
|
10151
10660
|
const getPort = () => boundPort;
|
|
10152
|
-
return new Promise((
|
|
10661
|
+
return new Promise((resolve13, reject) => {
|
|
10153
10662
|
const server = createServer((req, res) => {
|
|
10154
10663
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
10155
10664
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -10160,7 +10669,7 @@ function startViewServer(opts) {
|
|
|
10160
10669
|
const address = server.address();
|
|
10161
10670
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
10162
10671
|
server.off("error", reject);
|
|
10163
|
-
|
|
10672
|
+
resolve13({
|
|
10164
10673
|
url: `http://${host}:${boundPort}`,
|
|
10165
10674
|
port: boundPort,
|
|
10166
10675
|
close: () => closeServer(server)
|
|
@@ -10172,8 +10681,8 @@ function isAddressInfo(value) {
|
|
|
10172
10681
|
return value !== null && typeof value === "object";
|
|
10173
10682
|
}
|
|
10174
10683
|
function closeServer(server) {
|
|
10175
|
-
return new Promise((
|
|
10176
|
-
server.close(() =>
|
|
10684
|
+
return new Promise((resolve13) => {
|
|
10685
|
+
server.close(() => resolve13());
|
|
10177
10686
|
server.closeAllConnections();
|
|
10178
10687
|
});
|
|
10179
10688
|
}
|
|
@@ -10390,9 +10899,9 @@ async function captureStaleness(ws, nowIso) {
|
|
|
10390
10899
|
async function overview(ws, nowProvider) {
|
|
10391
10900
|
let manifest;
|
|
10392
10901
|
try {
|
|
10393
|
-
manifest = await
|
|
10902
|
+
manifest = await readManifest13(ws.paths);
|
|
10394
10903
|
} catch (error) {
|
|
10395
|
-
if (
|
|
10904
|
+
if (findErrorCode17(error, "ENOENT")) {
|
|
10396
10905
|
return { initialized: false, repoRoot: ws.repoRoot };
|
|
10397
10906
|
}
|
|
10398
10907
|
throw error;
|
|
@@ -10447,7 +10956,7 @@ async function sessionDetail(ws, sessionId) {
|
|
|
10447
10956
|
throw error;
|
|
10448
10957
|
}
|
|
10449
10958
|
try {
|
|
10450
|
-
const events = await readAllEvents2(
|
|
10959
|
+
const events = await readAllEvents2(join17(ws.paths.sessions, sessionId));
|
|
10451
10960
|
return { session, events };
|
|
10452
10961
|
} catch {
|
|
10453
10962
|
return { session, events: [], degraded: true };
|
|
@@ -10469,7 +10978,7 @@ async function taskDetail(ws, taskId) {
|
|
|
10469
10978
|
}
|
|
10470
10979
|
}
|
|
10471
10980
|
async function decisionsView(ws, nowProvider) {
|
|
10472
|
-
const fromDisk = await
|
|
10981
|
+
const fromDisk = await readMarkdownFile7(ws.paths.files.decisions);
|
|
10473
10982
|
if (fromDisk !== null) {
|
|
10474
10983
|
return { body: fromDisk, fromDisk: true };
|
|
10475
10984
|
}
|
|
@@ -10492,7 +11001,7 @@ async function approvalsView(ws, nowProvider) {
|
|
|
10492
11001
|
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
10493
11002
|
}
|
|
10494
11003
|
async function handoffView(ws, nowProvider) {
|
|
10495
|
-
const fromDisk = await
|
|
11004
|
+
const fromDisk = await readMarkdownFile7(ws.paths.files.handoff);
|
|
10496
11005
|
if (fromDisk !== null) {
|
|
10497
11006
|
return { body: fromDisk, fromDisk: true };
|
|
10498
11007
|
}
|
|
@@ -10661,18 +11170,18 @@ async function doRunView(options, ctx) {
|
|
|
10661
11170
|
}
|
|
10662
11171
|
async function buildSingleDeps(ctx, cwd) {
|
|
10663
11172
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
10664
|
-
const paths =
|
|
10665
|
-
await
|
|
11173
|
+
const paths = basouPaths21(repositoryRoot);
|
|
11174
|
+
await assertWorkspaceInitialized15(paths.root);
|
|
10666
11175
|
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
10667
11176
|
return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
|
|
10668
11177
|
}
|
|
10669
11178
|
async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
10670
|
-
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path:
|
|
11179
|
+
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve12(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
10671
11180
|
const entries = [];
|
|
10672
11181
|
const seenPath = /* @__PURE__ */ new Set();
|
|
10673
11182
|
const seenKey = /* @__PURE__ */ new Set();
|
|
10674
11183
|
for (const spec of specs) {
|
|
10675
|
-
const repoRoot =
|
|
11184
|
+
const repoRoot = resolve12(spec.path);
|
|
10676
11185
|
if (seenPath.has(repoRoot)) continue;
|
|
10677
11186
|
seenPath.add(repoRoot);
|
|
10678
11187
|
const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
|
|
@@ -10685,14 +11194,14 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
|
10685
11194
|
return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
|
|
10686
11195
|
}
|
|
10687
11196
|
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
10688
|
-
const paths =
|
|
11197
|
+
const paths = basouPaths21(repoRoot);
|
|
10689
11198
|
const importCtx = {
|
|
10690
11199
|
cwd: repoRoot,
|
|
10691
11200
|
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
10692
11201
|
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
10693
11202
|
};
|
|
10694
11203
|
try {
|
|
10695
|
-
const manifest = await
|
|
11204
|
+
const manifest = await readManifest14(paths);
|
|
10696
11205
|
return {
|
|
10697
11206
|
key: manifest.workspace.id,
|
|
10698
11207
|
label: labelOverride ?? manifest.workspace.name,
|
|
@@ -10721,7 +11230,7 @@ async function startListening(port, deps) {
|
|
|
10721
11230
|
try {
|
|
10722
11231
|
return await startViewServer({ port, deps });
|
|
10723
11232
|
} catch (error) {
|
|
10724
|
-
if (
|
|
11233
|
+
if (findErrorCode18(error, "EADDRINUSE")) {
|
|
10725
11234
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
10726
11235
|
cause: error
|
|
10727
11236
|
});
|
|
@@ -10744,7 +11253,7 @@ function openInBrowser(url, override) {
|
|
|
10744
11253
|
}
|
|
10745
11254
|
}
|
|
10746
11255
|
function waitForShutdown(signal) {
|
|
10747
|
-
return new Promise((
|
|
11256
|
+
return new Promise((resolve13) => {
|
|
10748
11257
|
const cleanup = () => {
|
|
10749
11258
|
process.off("SIGINT", onSignal);
|
|
10750
11259
|
process.off("SIGTERM", onSignal);
|
|
@@ -10752,18 +11261,18 @@ function waitForShutdown(signal) {
|
|
|
10752
11261
|
};
|
|
10753
11262
|
const onSignal = () => {
|
|
10754
11263
|
cleanup();
|
|
10755
|
-
|
|
11264
|
+
resolve13();
|
|
10756
11265
|
};
|
|
10757
11266
|
const onAbort = () => {
|
|
10758
11267
|
cleanup();
|
|
10759
|
-
|
|
11268
|
+
resolve13();
|
|
10760
11269
|
};
|
|
10761
11270
|
process.on("SIGINT", onSignal);
|
|
10762
11271
|
process.on("SIGTERM", onSignal);
|
|
10763
11272
|
if (signal !== void 0) {
|
|
10764
11273
|
if (signal.aborted) {
|
|
10765
11274
|
cleanup();
|
|
10766
|
-
|
|
11275
|
+
resolve13();
|
|
10767
11276
|
return;
|
|
10768
11277
|
}
|
|
10769
11278
|
signal.addEventListener("abort", onAbort);
|
|
@@ -10782,11 +11291,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
10782
11291
|
throw error;
|
|
10783
11292
|
}
|
|
10784
11293
|
}
|
|
10785
|
-
async function
|
|
11294
|
+
async function assertWorkspaceInitialized15(basouRoot) {
|
|
10786
11295
|
try {
|
|
10787
|
-
await
|
|
11296
|
+
await assertBasouRootSafe18(basouRoot);
|
|
10788
11297
|
} catch (error) {
|
|
10789
|
-
if (
|
|
11298
|
+
if (findErrorCode18(error, "ENOENT")) {
|
|
10790
11299
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
10791
11300
|
}
|
|
10792
11301
|
throw error;
|
|
@@ -10818,6 +11327,7 @@ function buildProgram() {
|
|
|
10818
11327
|
registerDecisionsCommand(program2);
|
|
10819
11328
|
registerReportCommand(program2);
|
|
10820
11329
|
registerOrientCommand(program2);
|
|
11330
|
+
registerReviewCommand(program2);
|
|
10821
11331
|
registerReviewGapsCommand(program2);
|
|
10822
11332
|
registerProjectCommand(program2);
|
|
10823
11333
|
registerProtocolCommand(program2);
|