@basou/cli 0.27.0 → 0.29.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 +428 -196
- package/dist/index.js.map +1 -1
- package/dist/program.js +428 -196
- package/dist/program.js.map +1 -1
- package/package.json +3 -2
package/dist/program.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(program) {
|
|
2044
2106
|
const hook = program.command("hook").description(
|
|
@@ -2049,29 +2111,48 @@ function registerHookCommand(program) {
|
|
|
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"
|
|
2052
2117
|
).addHelpText("after", HOOK_STOP_HELP).action(async (options) => {
|
|
2053
2118
|
const minEdits = parseMinEdits(options.minEdits);
|
|
2054
|
-
await runHookStop(
|
|
2119
|
+
await runHookStop({
|
|
2120
|
+
...minEdits !== void 0 ? { minEdits } : {},
|
|
2121
|
+
...options.block === true ? { block: true } : {}
|
|
2122
|
+
});
|
|
2123
|
+
});
|
|
2124
|
+
hook.command("install").description(
|
|
2125
|
+
"Register the Stop hook in ~/.claude/settings.json (reproducible, idempotent). Default is advisory; --block opts into in-turn enforcement."
|
|
2126
|
+
).option("--block", "Register the blocking (opt-in enforcement) form instead of advisory").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) => {
|
|
2127
|
+
await runHookInstall(opts);
|
|
2128
|
+
});
|
|
2129
|
+
hook.command("uninstall").description(
|
|
2130
|
+
"Remove the basou Stop hook from ~/.claude/settings.json (leaves other hooks intact)"
|
|
2131
|
+
).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) => {
|
|
2132
|
+
await runHookUninstall(opts);
|
|
2133
|
+
});
|
|
2134
|
+
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) => {
|
|
2135
|
+
await runHookStatus(opts);
|
|
2055
2136
|
});
|
|
2056
2137
|
}
|
|
2057
2138
|
var HOOK_STOP_HELP = `
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
"Stop": [
|
|
2062
|
-
{ "hooks": [ { "type": "command", "command": "basou hook stop" } ] }
|
|
2063
|
-
]
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2139
|
+
Register this Stop hook reproducibly with 'basou hook install' (it writes the
|
|
2140
|
+
correct node-path command into ~/.claude/settings.json). 'basou hook uninstall'
|
|
2141
|
+
removes it; 'basou hook status' reports whether it is registered.
|
|
2066
2142
|
|
|
2067
2143
|
On every turn end basou inspects the session transcript. If the session did
|
|
2068
2144
|
content-substantive work but ran no capture verb ('basou decision capture' /
|
|
2069
|
-
'decision record' / 'note'), it
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2145
|
+
'decision record' / 'note'), it reminds the agent to record the why / next step.
|
|
2146
|
+
Substantive = EITHER >= ${DEFAULT_STOP_HOOK_MIN_EDITS} file edits (default) OR a free-form AskUserQuestion
|
|
2147
|
+
answer (an uncaptured conversational decision). Read-only Bash (ls / grep /
|
|
2148
|
+
git status) does NOT count.
|
|
2149
|
+
|
|
2150
|
+
By default the reminder is non-blocking: Claude sees it and may act on it or
|
|
2151
|
+
stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
|
|
2152
|
+
returns decision:block, holding the agent in-turn to act on the reminder; the
|
|
2153
|
+
'stop_hook_active' flag and Claude Code's own loop prevention bound it to a
|
|
2154
|
+
single turn. Either way the hook fails open: a bad payload or unreadable
|
|
2155
|
+
transcript exits cleanly with no output.
|
|
2075
2156
|
`;
|
|
2076
2157
|
async function runHookStop(options, ctx = {}) {
|
|
2077
2158
|
try {
|
|
@@ -2110,15 +2191,14 @@ async function doRunHookStop(options, ctx) {
|
|
|
2110
2191
|
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
2111
2192
|
});
|
|
2112
2193
|
if (evaluation.kind !== "nudge") return;
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
`
|
|
2121
|
-
);
|
|
2194
|
+
const payloadJson = options.block === true ? JSON.stringify({ decision: "block", reason: evaluation.additionalContext }) : JSON.stringify({
|
|
2195
|
+
hookSpecificOutput: {
|
|
2196
|
+
hookEventName: "Stop",
|
|
2197
|
+
additionalContext: evaluation.additionalContext
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
write(`${payloadJson}
|
|
2201
|
+
`);
|
|
2122
2202
|
}
|
|
2123
2203
|
function parseTranscript(transcript) {
|
|
2124
2204
|
const records = [];
|
|
@@ -2143,9 +2223,9 @@ async function defaultReadStdin() {
|
|
|
2143
2223
|
return Buffer.concat(chunks).toString("utf8");
|
|
2144
2224
|
}
|
|
2145
2225
|
async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
|
|
2146
|
-
const { size } = await
|
|
2226
|
+
const { size } = await stat3(path);
|
|
2147
2227
|
if (size <= maxBytes) return readFile2(path, "utf8");
|
|
2148
|
-
const handle = await
|
|
2228
|
+
const handle = await open2(path, "r");
|
|
2149
2229
|
try {
|
|
2150
2230
|
const buffer = Buffer.alloc(maxBytes);
|
|
2151
2231
|
const { bytesRead } = await handle.read(buffer, 0, maxBytes, size - maxBytes);
|
|
@@ -2160,12 +2240,160 @@ function parseMinEdits(raw) {
|
|
|
2160
2240
|
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
2161
2241
|
return Number(raw);
|
|
2162
2242
|
}
|
|
2243
|
+
var DEFAULT_CLAUDE_SETTINGS_PATH = join6(homedir4(), ".claude", "settings.json");
|
|
2244
|
+
function resolveCliEntry() {
|
|
2245
|
+
return fileURLToPath(import.meta.url);
|
|
2246
|
+
}
|
|
2247
|
+
function normalizeInstallOptions(raw) {
|
|
2248
|
+
const out = {};
|
|
2249
|
+
if (raw.block === true) out.block = true;
|
|
2250
|
+
if (raw.settings !== void 0) out.settings = raw.settings;
|
|
2251
|
+
if (raw.dryRun === true) out.dryRun = true;
|
|
2252
|
+
if (raw.verbose === true) out.verbose = true;
|
|
2253
|
+
if (raw.minEdits !== void 0) {
|
|
2254
|
+
const parsed = parseMinEdits(raw.minEdits);
|
|
2255
|
+
if (parsed === void 0) {
|
|
2256
|
+
throw new Error("--min-edits must be a non-negative integer.");
|
|
2257
|
+
}
|
|
2258
|
+
out.minEdits = parsed;
|
|
2259
|
+
}
|
|
2260
|
+
return out;
|
|
2261
|
+
}
|
|
2262
|
+
async function readSettings(path) {
|
|
2263
|
+
let raw;
|
|
2264
|
+
try {
|
|
2265
|
+
raw = await readFile2(path, "utf8");
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
2268
|
+
return { raw: null, parsed: void 0 };
|
|
2269
|
+
}
|
|
2270
|
+
throw error;
|
|
2271
|
+
}
|
|
2272
|
+
if (raw.trim().length === 0) return { raw, parsed: void 0 };
|
|
2273
|
+
try {
|
|
2274
|
+
return { raw, parsed: JSON.parse(raw) };
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
throw new Error(
|
|
2277
|
+
"The Claude settings.json is not valid JSON. Fix it (or remove it) and retry.",
|
|
2278
|
+
{
|
|
2279
|
+
cause: error
|
|
2280
|
+
}
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
async function backupSettingsOnce(path, raw) {
|
|
2285
|
+
if (raw === null) return;
|
|
2286
|
+
const bak = `${path}.basou-bak`;
|
|
2287
|
+
try {
|
|
2288
|
+
await stat3(bak);
|
|
2289
|
+
return;
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
if (!(error instanceof Error && error.code === "ENOENT")) throw error;
|
|
2292
|
+
}
|
|
2293
|
+
await writeFileDurable(bak, raw);
|
|
2294
|
+
}
|
|
2295
|
+
async function runHookInstall(options, ctx = {}) {
|
|
2296
|
+
try {
|
|
2297
|
+
await doRunHookInstall(normalizeInstallOptions(options), ctx);
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2300
|
+
process.exitCode = 1;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
async function doRunHookInstall(options, ctx = {}) {
|
|
2304
|
+
const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
|
|
2305
|
+
const cliEntry = (ctx.resolveCliEntry ?? resolveCliEntry)();
|
|
2306
|
+
const command = buildStopHookCommand({
|
|
2307
|
+
cliEntry,
|
|
2308
|
+
...options.block === true ? { block: true } : {},
|
|
2309
|
+
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
2310
|
+
});
|
|
2311
|
+
const mode = options.block === true ? "blocking" : "advisory";
|
|
2312
|
+
await assertNotSymlink(settingsPath);
|
|
2313
|
+
const { raw, parsed } = await readSettings(settingsPath);
|
|
2314
|
+
const { settings, action } = upsertStopHook(parsed, command);
|
|
2315
|
+
const newBody = `${JSON.stringify(settings, null, 2)}
|
|
2316
|
+
`;
|
|
2317
|
+
if (raw !== null && newBody === raw) {
|
|
2318
|
+
console.log(`The basou Stop hook is already registered (${mode}); no change.`);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (options.dryRun === true) {
|
|
2322
|
+
console.log(`[dry-run] Would ${action} the basou Stop hook (${mode}).`);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const recheck = await readSettings(settingsPath);
|
|
2326
|
+
if (recheck.raw !== raw) {
|
|
2327
|
+
throw new Error(
|
|
2328
|
+
"The settings.json changed during install; aborting so a concurrent edit is not overwritten. Re-run 'basou hook install'."
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
await backupSettingsOnce(settingsPath, raw);
|
|
2332
|
+
await writeFileDurable(settingsPath, newBody);
|
|
2333
|
+
console.log(`${action === "installed" ? "Installed" : "Updated"} the basou Stop hook (${mode}).`);
|
|
2334
|
+
}
|
|
2335
|
+
async function runHookUninstall(options) {
|
|
2336
|
+
try {
|
|
2337
|
+
await doRunHookUninstall(normalizeInstallOptions(options));
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2340
|
+
process.exitCode = 1;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
async function doRunHookUninstall(options) {
|
|
2344
|
+
const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
|
|
2345
|
+
await assertNotSymlink(settingsPath);
|
|
2346
|
+
const { raw, parsed } = await readSettings(settingsPath);
|
|
2347
|
+
if (raw === null) {
|
|
2348
|
+
console.log("No settings.json; nothing to remove.");
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
const { settings, action } = removeStopHook(parsed);
|
|
2352
|
+
if (action === "absent") {
|
|
2353
|
+
console.log("No basou Stop hook found; nothing removed.");
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
const newBody = `${JSON.stringify(settings, null, 2)}
|
|
2357
|
+
`;
|
|
2358
|
+
if (options.dryRun === true) {
|
|
2359
|
+
console.log("[dry-run] Would remove the basou Stop hook from settings.json.");
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const recheck = await readSettings(settingsPath);
|
|
2363
|
+
if (recheck.raw !== raw) {
|
|
2364
|
+
throw new Error(
|
|
2365
|
+
"The settings.json changed during uninstall; aborting so a concurrent edit is not overwritten. Re-run 'basou hook uninstall'."
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
await backupSettingsOnce(settingsPath, raw);
|
|
2369
|
+
await writeFileDurable(settingsPath, newBody);
|
|
2370
|
+
console.log("Removed the basou Stop hook from settings.json.");
|
|
2371
|
+
}
|
|
2372
|
+
async function runHookStatus(options) {
|
|
2373
|
+
try {
|
|
2374
|
+
await doRunHookStatus(normalizeInstallOptions(options));
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2377
|
+
process.exitCode = 1;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
async function doRunHookStatus(options) {
|
|
2381
|
+
const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
|
|
2382
|
+
const { parsed } = await readSettings(settingsPath);
|
|
2383
|
+
const command = findBasouStopHookCommand(parsed);
|
|
2384
|
+
if (command === null) {
|
|
2385
|
+
console.log("basou Stop hook: not registered. Run 'basou hook install' to register it.");
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
const mode = / --block\b/.test(command) ? "blocking (opt-in enforcement)" : "advisory (non-blocking)";
|
|
2389
|
+
console.log(`basou Stop hook: registered, ${mode}.`);
|
|
2390
|
+
}
|
|
2163
2391
|
|
|
2164
2392
|
// src/commands/import.ts
|
|
2165
2393
|
import { createReadStream } from "fs";
|
|
2166
|
-
import { readdir, readFile as readFile3, rm, stat as
|
|
2167
|
-
import { homedir as
|
|
2168
|
-
import { basename as
|
|
2394
|
+
import { readdir, readFile as readFile3, rm, stat as stat4 } from "fs/promises";
|
|
2395
|
+
import { homedir as homedir5 } from "os";
|
|
2396
|
+
import { basename as basename3, dirname as dirname2, join as join7, resolve as resolve4 } from "path";
|
|
2169
2397
|
import { createInterface } from "readline";
|
|
2170
2398
|
import {
|
|
2171
2399
|
AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
|
|
@@ -2251,11 +2479,11 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
2251
2479
|
repoRoot: repositoryRoot,
|
|
2252
2480
|
cwd: ctx.cwd ?? process.cwd()
|
|
2253
2481
|
});
|
|
2254
|
-
const projectsRoot = ctx.claudeProjectsDir ??
|
|
2482
|
+
const projectsRoot = ctx.claudeProjectsDir ?? join7(homedir5(), ".claude", "projects");
|
|
2255
2483
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
2256
2484
|
const projectSet = new Set(projectPaths);
|
|
2257
2485
|
const candidates = files.map((file) => {
|
|
2258
|
-
const externalId =
|
|
2486
|
+
const externalId = basename3(file, ".jsonl");
|
|
2259
2487
|
return {
|
|
2260
2488
|
externalId,
|
|
2261
2489
|
sourcePath: file,
|
|
@@ -2290,7 +2518,7 @@ async function doRunImportCodex(options, ctx) {
|
|
|
2290
2518
|
repoRoot: repositoryRoot,
|
|
2291
2519
|
cwd: ctx.cwd ?? process.cwd()
|
|
2292
2520
|
});
|
|
2293
|
-
const sessionsRoot = ctx.codexSessionsDir ??
|
|
2521
|
+
const sessionsRoot = ctx.codexSessionsDir ?? join7(homedir5(), ".codex", "sessions");
|
|
2294
2522
|
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
2295
2523
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
2296
2524
|
externalId,
|
|
@@ -2345,7 +2573,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2345
2573
|
files: payload.session.related_files ?? [],
|
|
2346
2574
|
workingDirectory: payload.session.working_directory,
|
|
2347
2575
|
sourceRoots: projectPaths,
|
|
2348
|
-
masterRoot:
|
|
2576
|
+
masterRoot: dirname2(paths.root),
|
|
2349
2577
|
extraInRoot: AGENT_INFRA_DIRS2
|
|
2350
2578
|
});
|
|
2351
2579
|
if (scope.outOfRoot.length > 0) crossProject.push({ externalId, outOfRoot: scope.outOfRoot });
|
|
@@ -2419,7 +2647,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2419
2647
|
if (priors.length > 0 && options.force === true) {
|
|
2420
2648
|
if (options.dryRun !== true) {
|
|
2421
2649
|
for (const { sessionId } of priors) {
|
|
2422
|
-
await rm(
|
|
2650
|
+
await rm(join7(paths.sessions, sessionId), { recursive: true, force: true });
|
|
2423
2651
|
}
|
|
2424
2652
|
}
|
|
2425
2653
|
counts.replaced++;
|
|
@@ -2530,7 +2758,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2530
2758
|
if (options.session !== void 0) {
|
|
2531
2759
|
const matches = [];
|
|
2532
2760
|
for (const projectPath of projectPaths) {
|
|
2533
|
-
const file =
|
|
2761
|
+
const file = join7(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
2534
2762
|
if (await pathExists(file)) matches.push(file);
|
|
2535
2763
|
}
|
|
2536
2764
|
if (matches.length === 0) {
|
|
@@ -2541,7 +2769,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2541
2769
|
const files = [];
|
|
2542
2770
|
let anyDirFound = false;
|
|
2543
2771
|
for (const projectPath of projectPaths) {
|
|
2544
|
-
const transcriptDir =
|
|
2772
|
+
const transcriptDir = join7(projectsRoot, encodeProjectDir(projectPath));
|
|
2545
2773
|
let entries;
|
|
2546
2774
|
try {
|
|
2547
2775
|
entries = await readdir(transcriptDir);
|
|
@@ -2551,7 +2779,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2551
2779
|
}
|
|
2552
2780
|
anyDirFound = true;
|
|
2553
2781
|
for (const name of entries) {
|
|
2554
|
-
if (name.endsWith(".jsonl")) files.push(
|
|
2782
|
+
if (name.endsWith(".jsonl")) files.push(join7(transcriptDir, name));
|
|
2555
2783
|
}
|
|
2556
2784
|
}
|
|
2557
2785
|
if (!anyDirFound) {
|
|
@@ -2561,7 +2789,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2561
2789
|
}
|
|
2562
2790
|
async function pathExists(file) {
|
|
2563
2791
|
try {
|
|
2564
|
-
await
|
|
2792
|
+
await stat4(file);
|
|
2565
2793
|
return true;
|
|
2566
2794
|
} catch (error) {
|
|
2567
2795
|
if (findErrorCode5(error, "ENOENT")) return false;
|
|
@@ -2570,7 +2798,7 @@ async function pathExists(file) {
|
|
|
2570
2798
|
}
|
|
2571
2799
|
async function statSize(file) {
|
|
2572
2800
|
try {
|
|
2573
|
-
return (await
|
|
2801
|
+
return (await stat4(file)).size;
|
|
2574
2802
|
} catch (error) {
|
|
2575
2803
|
if (findErrorCode5(error, "ENOENT")) return void 0;
|
|
2576
2804
|
throw error;
|
|
@@ -2608,7 +2836,7 @@ async function findRolloutFiles(sessionsRoot) {
|
|
|
2608
2836
|
throw new Error("Failed to read Codex sessions directory", { cause: error });
|
|
2609
2837
|
}
|
|
2610
2838
|
for (const entry of entries) {
|
|
2611
|
-
const full =
|
|
2839
|
+
const full = join7(dir, entry.name);
|
|
2612
2840
|
if (entry.isDirectory()) {
|
|
2613
2841
|
await walk(full, false);
|
|
2614
2842
|
} else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
@@ -2786,7 +3014,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
|
|
|
2786
3014
|
}
|
|
2787
3015
|
|
|
2788
3016
|
// src/commands/init.ts
|
|
2789
|
-
import { basename as
|
|
3017
|
+
import { basename as basename4, relative, resolve as resolve5 } from "path";
|
|
2790
3018
|
import {
|
|
2791
3019
|
appendBasouGitignore,
|
|
2792
3020
|
createManifest,
|
|
@@ -2825,7 +3053,7 @@ async function runInit(options, ctx = {}) {
|
|
|
2825
3053
|
async function doRunInit(options, ctx) {
|
|
2826
3054
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2827
3055
|
const repositoryRoot = await resolveRepositoryRootForInit(cwd);
|
|
2828
|
-
const workspaceName = options.name ??
|
|
3056
|
+
const workspaceName = options.name ?? basename4(repositoryRoot);
|
|
2829
3057
|
let repositoryUrl;
|
|
2830
3058
|
if (options.repoUrl !== void 0) {
|
|
2831
3059
|
repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
|
|
@@ -3050,13 +3278,13 @@ import {
|
|
|
3050
3278
|
} from "@basou/core";
|
|
3051
3279
|
|
|
3052
3280
|
// src/lib/hosts-config.ts
|
|
3053
|
-
import { homedir as
|
|
3054
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3281
|
+
import { homedir as homedir6 } from "os";
|
|
3282
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve6 } from "path";
|
|
3055
3283
|
import { readYamlFile as readYamlFile4 } from "@basou/core";
|
|
3056
|
-
var DEFAULT_HOSTS_CONFIG_PATH =
|
|
3284
|
+
var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
|
|
3057
3285
|
function expandTilde2(p) {
|
|
3058
|
-
if (p === "~") return
|
|
3059
|
-
if (p.startsWith("~/")) return
|
|
3286
|
+
if (p === "~") return homedir6();
|
|
3287
|
+
if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
|
|
3060
3288
|
return p;
|
|
3061
3289
|
}
|
|
3062
3290
|
function isRecord2(value) {
|
|
@@ -3382,7 +3610,7 @@ import {
|
|
|
3382
3610
|
writeFileSync,
|
|
3383
3611
|
writeSync
|
|
3384
3612
|
} from "fs";
|
|
3385
|
-
import { basename as
|
|
3613
|
+
import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve7 } from "path";
|
|
3386
3614
|
import {
|
|
3387
3615
|
appendBasouGitignore as appendBasouGitignore2,
|
|
3388
3616
|
basouPaths as basouPaths10,
|
|
@@ -3391,6 +3619,7 @@ import {
|
|
|
3391
3619
|
ensureBasouDirectory as ensureBasouDirectory2,
|
|
3392
3620
|
GENERATED_END,
|
|
3393
3621
|
GENERATED_START,
|
|
3622
|
+
instructionMode,
|
|
3394
3623
|
isGitNotFound,
|
|
3395
3624
|
parseMarkers,
|
|
3396
3625
|
pathBasename,
|
|
@@ -3685,7 +3914,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
|
3685
3914
|
} catch {
|
|
3686
3915
|
return { path: declaredPath, kind: "unresolved" };
|
|
3687
3916
|
}
|
|
3688
|
-
return { path: declaredPath, kind: existsSync(
|
|
3917
|
+
return { path: declaredPath, kind: existsSync(join9(real, ".git")) ? "repo" : "non-repo" };
|
|
3689
3918
|
}
|
|
3690
3919
|
async function doRunProjectAdopt(options, ctx) {
|
|
3691
3920
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -3780,7 +4009,8 @@ async function isTrackedByGit(repoRoot, relPath) {
|
|
|
3780
4009
|
async function gatherRepoWiring(repositoryRoot, entry) {
|
|
3781
4010
|
const base = {
|
|
3782
4011
|
path: entry.path,
|
|
3783
|
-
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
|
|
4012
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
|
|
4013
|
+
...instructionMode(entry) === "self" ? { self: true } : {}
|
|
3784
4014
|
};
|
|
3785
4015
|
let real;
|
|
3786
4016
|
try {
|
|
@@ -3788,7 +4018,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
3788
4018
|
} catch {
|
|
3789
4019
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
3790
4020
|
}
|
|
3791
|
-
if (!existsSync(
|
|
4021
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
3792
4022
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
3793
4023
|
}
|
|
3794
4024
|
try {
|
|
@@ -3796,7 +4026,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
3796
4026
|
for (const name of INSTRUCTION_FILES) {
|
|
3797
4027
|
let present = true;
|
|
3798
4028
|
try {
|
|
3799
|
-
lstatSync(
|
|
4029
|
+
lstatSync(join9(real, name));
|
|
3800
4030
|
} catch {
|
|
3801
4031
|
present = false;
|
|
3802
4032
|
}
|
|
@@ -3863,6 +4093,13 @@ function renderProjectWiring(result) {
|
|
|
3863
4093
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3864
4094
|
lines.push("");
|
|
3865
4095
|
}
|
|
4096
|
+
if (result.self.length > 0) {
|
|
4097
|
+
lines.push(
|
|
4098
|
+
`## instructions: self (${result.self.length}) \u2014 committed instruction files are intentional (no leak risk)`
|
|
4099
|
+
);
|
|
4100
|
+
for (const p of result.self) lines.push(`- ${p}`);
|
|
4101
|
+
lines.push("");
|
|
4102
|
+
}
|
|
3866
4103
|
if (result.incomplete.length > 0) {
|
|
3867
4104
|
lines.push(
|
|
3868
4105
|
`## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
|
|
@@ -3891,7 +4128,8 @@ async function runProjectGitignore(options, ctx = {}) {
|
|
|
3891
4128
|
function gatherRepoGitignore(repositoryRoot, entry) {
|
|
3892
4129
|
const base = {
|
|
3893
4130
|
path: entry.path,
|
|
3894
|
-
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
|
|
4131
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
|
|
4132
|
+
...instructionMode(entry) === "self" ? { self: true } : {}
|
|
3895
4133
|
};
|
|
3896
4134
|
let real;
|
|
3897
4135
|
try {
|
|
@@ -3899,10 +4137,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
|
|
|
3899
4137
|
} catch {
|
|
3900
4138
|
return { ...base, reachable: false, currentLines: [] };
|
|
3901
4139
|
}
|
|
3902
|
-
if (!existsSync(
|
|
4140
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
3903
4141
|
return { ...base, reachable: false, currentLines: [] };
|
|
3904
4142
|
}
|
|
3905
|
-
return { ...base, reachable: true, currentLines: readGitignoreLines(
|
|
4143
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join9(real, ".gitignore")) };
|
|
3906
4144
|
}
|
|
3907
4145
|
function hasErrorCode(error) {
|
|
3908
4146
|
return error instanceof Error && typeof error.code === "string";
|
|
@@ -3916,7 +4154,7 @@ function readGitignoreLines(file) {
|
|
|
3916
4154
|
}
|
|
3917
4155
|
}
|
|
3918
4156
|
function applyGitignorePlan(repositoryRoot, plan) {
|
|
3919
|
-
const file =
|
|
4157
|
+
const file = join9(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
|
|
3920
4158
|
let existing = "";
|
|
3921
4159
|
try {
|
|
3922
4160
|
existing = readFileSync(file, "utf8");
|
|
@@ -3986,6 +4224,13 @@ function renderProjectGitignore(result) {
|
|
|
3986
4224
|
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
3987
4225
|
lines.push("");
|
|
3988
4226
|
}
|
|
4227
|
+
if (result.self.length > 0) {
|
|
4228
|
+
lines.push(
|
|
4229
|
+
`## instructions: self (${result.self.length}) \u2014 skipped by design; their committed instruction files are shared, never gitignored`
|
|
4230
|
+
);
|
|
4231
|
+
for (const p of result.self) lines.push(`- ${p}`);
|
|
4232
|
+
lines.push("");
|
|
4233
|
+
}
|
|
3989
4234
|
if (result.unreachable.length > 0) {
|
|
3990
4235
|
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
3991
4236
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
@@ -4007,12 +4252,13 @@ async function runProjectSymlinks(options, ctx = {}) {
|
|
|
4007
4252
|
process.exitCode = 1;
|
|
4008
4253
|
}
|
|
4009
4254
|
}
|
|
4010
|
-
function expectedSymlinkTargets(repoDirReal, canonicalFile) {
|
|
4011
|
-
|
|
4012
|
-
{ name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) },
|
|
4255
|
+
function expectedSymlinkTargets(repoDirReal, canonicalFile, mode = "hub") {
|
|
4256
|
+
const spokes = [
|
|
4013
4257
|
{ name: "CLAUDE.md", target: CANONICAL_FILE },
|
|
4014
4258
|
{ name: ".github/copilot-instructions.md", target: `../${CANONICAL_FILE}` }
|
|
4015
4259
|
];
|
|
4260
|
+
if (mode === "self") return spokes;
|
|
4261
|
+
return [{ name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) }, ...spokes];
|
|
4016
4262
|
}
|
|
4017
4263
|
function inspectSymlink(filePath, expectedTarget) {
|
|
4018
4264
|
let isLink;
|
|
@@ -4027,7 +4273,9 @@ function inspectSymlink(filePath, expectedTarget) {
|
|
|
4027
4273
|
return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
|
|
4028
4274
|
}
|
|
4029
4275
|
function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
4030
|
-
const
|
|
4276
|
+
const mode = instructionMode(entry);
|
|
4277
|
+
const isSelf = mode === "self";
|
|
4278
|
+
const base = { path: entry.path, ...isSelf ? { self: true } : {} };
|
|
4031
4279
|
let real;
|
|
4032
4280
|
try {
|
|
4033
4281
|
real = realpathSync(resolve7(repositoryRoot, entry.path));
|
|
@@ -4037,16 +4285,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4037
4285
|
if (real === anchorReal) {
|
|
4038
4286
|
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
4039
4287
|
}
|
|
4040
|
-
if (!existsSync(
|
|
4288
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
4041
4289
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4042
4290
|
}
|
|
4043
|
-
const canonicalFile =
|
|
4291
|
+
const canonicalFile = isSelf ? join9(real, CANONICAL_FILE) : join9(anchorReal, "agents", basename5(real), CANONICAL_FILE);
|
|
4044
4292
|
if (!existsSync(canonicalFile)) {
|
|
4045
4293
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
4046
4294
|
}
|
|
4047
|
-
const files = expectedSymlinkTargets(real, canonicalFile).map(
|
|
4295
|
+
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
4048
4296
|
(spec) => {
|
|
4049
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4297
|
+
const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
|
|
4050
4298
|
return {
|
|
4051
4299
|
name: spec.name,
|
|
4052
4300
|
expectedTarget: spec.target,
|
|
@@ -4060,7 +4308,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4060
4308
|
isAnchor: false,
|
|
4061
4309
|
reachable: true,
|
|
4062
4310
|
canonicalPresent: true,
|
|
4063
|
-
canonicalName:
|
|
4311
|
+
canonicalName: basename5(real),
|
|
4064
4312
|
files
|
|
4065
4313
|
};
|
|
4066
4314
|
}
|
|
@@ -4075,9 +4323,9 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
4075
4323
|
const created = [];
|
|
4076
4324
|
const failed = [];
|
|
4077
4325
|
for (const { name, target } of plan.toCreate) {
|
|
4078
|
-
const filePath =
|
|
4326
|
+
const filePath = join9(real, name);
|
|
4079
4327
|
try {
|
|
4080
|
-
mkdirSync(
|
|
4328
|
+
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4081
4329
|
symlinkSync(target, filePath);
|
|
4082
4330
|
created.push(name);
|
|
4083
4331
|
} catch (error) {
|
|
@@ -4197,6 +4445,13 @@ function renderProjectSymlinks(result) {
|
|
|
4197
4445
|
for (const p of result.missingCanonical) lines.push(`- ${p}`);
|
|
4198
4446
|
lines.push("");
|
|
4199
4447
|
}
|
|
4448
|
+
if (result.selfAgentsMissing.length > 0) {
|
|
4449
|
+
lines.push(
|
|
4450
|
+
`## AGENTS.md missing (${result.selfAgentsMissing.length}) \u2014 these \`instructions: self\` repos have no committed AGENTS.md yet; author it, then re-run to wire the spokes`
|
|
4451
|
+
);
|
|
4452
|
+
for (const p of result.selfAgentsMissing) lines.push(`- ${p}`);
|
|
4453
|
+
lines.push("");
|
|
4454
|
+
}
|
|
4200
4455
|
if (result.unreachable.length > 0) {
|
|
4201
4456
|
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4202
4457
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
@@ -4221,7 +4476,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
4221
4476
|
return realpathSync(abs);
|
|
4222
4477
|
} catch {
|
|
4223
4478
|
try {
|
|
4224
|
-
return
|
|
4479
|
+
return join9(realpathSync(dirname3(abs)), basename5(abs));
|
|
4225
4480
|
} catch {
|
|
4226
4481
|
return abs;
|
|
4227
4482
|
}
|
|
@@ -4238,8 +4493,8 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
4238
4493
|
if (expectedTarget === "" || expectedTarget === ".") {
|
|
4239
4494
|
return { path: entry.path, reachable: false };
|
|
4240
4495
|
}
|
|
4241
|
-
const linkName =
|
|
4242
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4496
|
+
const linkName = basename5(repoReal);
|
|
4497
|
+
const { state, actualTarget } = inspectSymlink(join9(viewDir, linkName), expectedTarget);
|
|
4243
4498
|
return {
|
|
4244
4499
|
path: entry.path,
|
|
4245
4500
|
reachable: true,
|
|
@@ -4253,9 +4508,9 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
4253
4508
|
const created = [];
|
|
4254
4509
|
const failed = [];
|
|
4255
4510
|
for (const { name, target } of toCreate) {
|
|
4256
|
-
const filePath =
|
|
4511
|
+
const filePath = join9(viewDir, name);
|
|
4257
4512
|
try {
|
|
4258
|
-
mkdirSync(
|
|
4513
|
+
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4259
4514
|
symlinkSync(target, filePath);
|
|
4260
4515
|
created.push(name);
|
|
4261
4516
|
} catch (error) {
|
|
@@ -4268,7 +4523,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
|
4268
4523
|
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
4269
4524
|
);
|
|
4270
4525
|
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
4271
|
-
const filePath =
|
|
4526
|
+
const filePath = join9(viewDir, name);
|
|
4272
4527
|
let isLink;
|
|
4273
4528
|
try {
|
|
4274
4529
|
isLink = lstatSync(filePath).isSymbolicLink();
|
|
@@ -4297,7 +4552,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
4297
4552
|
if (!isDir) {
|
|
4298
4553
|
return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
|
|
4299
4554
|
}
|
|
4300
|
-
return { target, kind: existsSync(
|
|
4555
|
+
return { target, kind: existsSync(join9(resolved, ".git")) ? "repo" : "non-repo" };
|
|
4301
4556
|
}
|
|
4302
4557
|
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
4303
4558
|
let names;
|
|
@@ -4322,7 +4577,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
4322
4577
|
const pruned = [];
|
|
4323
4578
|
const failed = [];
|
|
4324
4579
|
for (const { name } of toPrune) {
|
|
4325
|
-
const filePath =
|
|
4580
|
+
const filePath = join9(viewDir, name);
|
|
4326
4581
|
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
4327
4582
|
if (c === null || c.kind !== "repo") {
|
|
4328
4583
|
failed.push({
|
|
@@ -4368,7 +4623,7 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
4368
4623
|
} else {
|
|
4369
4624
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
4370
4625
|
const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
|
|
4371
|
-
const rosterNames = roster.map((entry) =>
|
|
4626
|
+
const rosterNames = roster.map((entry) => basename5(resolve7(repositoryRoot, entry.path)));
|
|
4372
4627
|
const rosterRealpaths = /* @__PURE__ */ new Set();
|
|
4373
4628
|
for (const entry of roster) {
|
|
4374
4629
|
try {
|
|
@@ -4535,10 +4790,10 @@ async function runProjectPreset(options, ctx = {}) {
|
|
|
4535
4790
|
}
|
|
4536
4791
|
}
|
|
4537
4792
|
function canonicalFileFor(anchorReal, canonicalName) {
|
|
4538
|
-
return
|
|
4793
|
+
return join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4539
4794
|
}
|
|
4540
4795
|
function canonicalLabelFor(canonicalName) {
|
|
4541
|
-
return
|
|
4796
|
+
return join9("agents", canonicalName, CANONICAL_FILE);
|
|
4542
4797
|
}
|
|
4543
4798
|
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
4544
4799
|
const declared = {
|
|
@@ -4547,6 +4802,9 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
4547
4802
|
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
4548
4803
|
...entry.publishes !== void 0 ? { publishes: entry.publishes } : {}
|
|
4549
4804
|
};
|
|
4805
|
+
if (instructionMode(entry) === "self") {
|
|
4806
|
+
return { ...declared, self: true, isAnchor: false, reachable: true, canonicalPresent: false };
|
|
4807
|
+
}
|
|
4550
4808
|
let real;
|
|
4551
4809
|
try {
|
|
4552
4810
|
real = realpathSync(resolve7(repositoryRoot, entry.path));
|
|
@@ -4556,10 +4814,10 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
4556
4814
|
if (real === anchorReal) {
|
|
4557
4815
|
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
4558
4816
|
}
|
|
4559
|
-
if (!existsSync(
|
|
4817
|
+
if (!existsSync(join9(real, ".git"))) {
|
|
4560
4818
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
4561
4819
|
}
|
|
4562
|
-
const canonicalName =
|
|
4820
|
+
const canonicalName = basename5(real);
|
|
4563
4821
|
let content;
|
|
4564
4822
|
try {
|
|
4565
4823
|
content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
|
|
@@ -4604,7 +4862,7 @@ async function applyPresetPlan(anchorReal, plan) {
|
|
|
4604
4862
|
isLink = false;
|
|
4605
4863
|
}
|
|
4606
4864
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
4607
|
-
if (plan.action === "create") mkdirSync(
|
|
4865
|
+
if (plan.action === "create") mkdirSync(dirname3(file), { recursive: true });
|
|
4608
4866
|
const existing = await readMarkdownFile4(file);
|
|
4609
4867
|
await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
|
|
4610
4868
|
}
|
|
@@ -4754,6 +5012,13 @@ function renderProjectPreset(result) {
|
|
|
4754
5012
|
for (const p of result.anchors) lines.push(`- ${p}`);
|
|
4755
5013
|
lines.push("");
|
|
4756
5014
|
}
|
|
5015
|
+
if (result.self.length > 0) {
|
|
5016
|
+
lines.push(
|
|
5017
|
+
`## instructions: self (${result.self.length}) \u2014 hands-off; their hand-authored AGENTS.md is never written by basou`
|
|
5018
|
+
);
|
|
5019
|
+
for (const p of result.self) lines.push(`- ${p}`);
|
|
5020
|
+
lines.push("");
|
|
5021
|
+
}
|
|
4757
5022
|
if (result.unreachable.length > 0) {
|
|
4758
5023
|
lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
|
|
4759
5024
|
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
@@ -4787,28 +5052,28 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
4787
5052
|
return empty;
|
|
4788
5053
|
}
|
|
4789
5054
|
const anchorReal = realpathSync(repositoryRoot);
|
|
4790
|
-
const canonicalName =
|
|
5055
|
+
const canonicalName = basename5(real);
|
|
4791
5056
|
const instructionFiles = [];
|
|
4792
5057
|
for (const name of INSTRUCTION_FILES) {
|
|
4793
5058
|
try {
|
|
4794
|
-
lstatSync(
|
|
5059
|
+
lstatSync(join9(real, name));
|
|
4795
5060
|
instructionFiles.push(name);
|
|
4796
5061
|
} catch {
|
|
4797
5062
|
}
|
|
4798
5063
|
}
|
|
4799
5064
|
let ignored;
|
|
4800
5065
|
try {
|
|
4801
|
-
ignored = new Set(readGitignoreLines(
|
|
5066
|
+
ignored = new Set(readGitignoreLines(join9(real, ".gitignore")).map((l) => l.trim()));
|
|
4802
5067
|
} catch {
|
|
4803
5068
|
ignored = /* @__PURE__ */ new Set();
|
|
4804
5069
|
}
|
|
4805
5070
|
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
4806
|
-
const canonical2 = existsSync(
|
|
5071
|
+
const canonical2 = existsSync(join9(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
4807
5072
|
let viewLink = false;
|
|
4808
5073
|
const viewPath = manifest.workspace.view;
|
|
4809
5074
|
if (viewPath !== void 0) {
|
|
4810
5075
|
try {
|
|
4811
|
-
lstatSync(
|
|
5076
|
+
lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
4812
5077
|
viewLink = true;
|
|
4813
5078
|
} catch {
|
|
4814
5079
|
}
|
|
@@ -4822,11 +5087,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
4822
5087
|
};
|
|
4823
5088
|
}
|
|
4824
5089
|
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
4825
|
-
const canonicalFile =
|
|
5090
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
4826
5091
|
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
4827
5092
|
}
|
|
4828
5093
|
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
4829
|
-
const filePath =
|
|
5094
|
+
const filePath = join9(viewDir, name);
|
|
4830
5095
|
try {
|
|
4831
5096
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4832
5097
|
const target = readlinkSync(filePath);
|
|
@@ -4837,7 +5102,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
|
4837
5102
|
}
|
|
4838
5103
|
}
|
|
4839
5104
|
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
4840
|
-
const filePath =
|
|
5105
|
+
const filePath = join9(viewDir, name);
|
|
4841
5106
|
try {
|
|
4842
5107
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
4843
5108
|
const target = readlinkSync(filePath);
|
|
@@ -4857,15 +5122,17 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4857
5122
|
}
|
|
4858
5123
|
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
4859
5124
|
const targetAbs = resolve7(repositoryRoot, target);
|
|
4860
|
-
const canonicalName =
|
|
5125
|
+
const canonicalName = basename5(repoReal ?? targetAbs);
|
|
4861
5126
|
const roster = manifest.repos ?? [];
|
|
4862
|
-
const
|
|
5127
|
+
const declaredEntry = roster.find((r) => {
|
|
4863
5128
|
try {
|
|
4864
5129
|
return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
|
|
4865
5130
|
} catch {
|
|
4866
5131
|
return resolve7(repositoryRoot, r.path) === targetAbs;
|
|
4867
5132
|
}
|
|
4868
5133
|
});
|
|
5134
|
+
const inRoster = declaredEntry !== void 0;
|
|
5135
|
+
const isSelf = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
|
|
4869
5136
|
const cnFold = canonicalName.toLowerCase();
|
|
4870
5137
|
const canonicalShared = roster.some((r) => {
|
|
4871
5138
|
let rReal = null;
|
|
@@ -4876,17 +5143,27 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4876
5143
|
}
|
|
4877
5144
|
if (rReal !== null) {
|
|
4878
5145
|
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
4879
|
-
return
|
|
5146
|
+
return basename5(rReal).toLowerCase() === cnFold;
|
|
4880
5147
|
}
|
|
4881
5148
|
if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
|
|
4882
|
-
return
|
|
5149
|
+
return basename5(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
4883
5150
|
});
|
|
4884
5151
|
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
4885
5152
|
const items = [];
|
|
4886
5153
|
if (!isAnchor) {
|
|
4887
5154
|
if (repoReal !== void 0) {
|
|
4888
5155
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
4889
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5156
|
+
const { state, actualTarget } = inspectSymlink(join9(repoReal, spec.name), spec.target);
|
|
5157
|
+
if (isSelf) {
|
|
5158
|
+
if (state !== "missing")
|
|
5159
|
+
items.push({
|
|
5160
|
+
kind: "instruction-symlink",
|
|
5161
|
+
label: spec.name,
|
|
5162
|
+
state: "foreign",
|
|
5163
|
+
note: "instructions: self \u2014 committed, left untouched"
|
|
5164
|
+
});
|
|
5165
|
+
continue;
|
|
5166
|
+
}
|
|
4890
5167
|
if (state === "correct")
|
|
4891
5168
|
items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
|
|
4892
5169
|
else if (state === "mismatch")
|
|
@@ -4913,7 +5190,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4913
5190
|
}
|
|
4914
5191
|
let ignored;
|
|
4915
5192
|
try {
|
|
4916
|
-
ignored = new Set(readGitignoreLines(
|
|
5193
|
+
ignored = new Set(readGitignoreLines(join9(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
4917
5194
|
for (const p of INSTRUCTION_FILES) {
|
|
4918
5195
|
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
4919
5196
|
items.push({
|
|
@@ -4936,7 +5213,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4936
5213
|
const viewPath = manifest.workspace.view;
|
|
4937
5214
|
if (viewPath !== void 0) {
|
|
4938
5215
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
4939
|
-
const linkPath =
|
|
5216
|
+
const linkPath = join9(viewDir, canonicalName);
|
|
4940
5217
|
let isLink = false;
|
|
4941
5218
|
try {
|
|
4942
5219
|
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
@@ -4962,8 +5239,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
4962
5239
|
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
4963
5240
|
}
|
|
4964
5241
|
}
|
|
4965
|
-
const canonicalFile =
|
|
4966
|
-
const canonicalLabel =
|
|
5242
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5243
|
+
const canonicalLabel = join9("agents", canonicalName, CANONICAL_FILE);
|
|
4967
5244
|
let canonicalIsLink = false;
|
|
4968
5245
|
try {
|
|
4969
5246
|
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
@@ -5067,12 +5344,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5067
5344
|
);
|
|
5068
5345
|
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
5069
5346
|
const expected = expectedByName.get(item.label);
|
|
5070
|
-
if (repoReal === null || expected === void 0 || inspectSymlink(
|
|
5347
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join9(repoReal, item.label), expected).state !== "correct") {
|
|
5071
5348
|
changed(item.label);
|
|
5072
5349
|
continue;
|
|
5073
5350
|
}
|
|
5074
5351
|
try {
|
|
5075
|
-
unlinkSync(
|
|
5352
|
+
unlinkSync(join9(repoReal, item.label));
|
|
5076
5353
|
removed.push(item.label);
|
|
5077
5354
|
} catch (error) {
|
|
5078
5355
|
failed.push({ label: item.label, message: failureReason(error) });
|
|
@@ -5091,7 +5368,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5091
5368
|
continue;
|
|
5092
5369
|
}
|
|
5093
5370
|
try {
|
|
5094
|
-
unlinkSync(
|
|
5371
|
+
unlinkSync(join9(viewDir, item.label));
|
|
5095
5372
|
removed.push(`view/${item.label}`);
|
|
5096
5373
|
} catch (error) {
|
|
5097
5374
|
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
@@ -5099,7 +5376,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5099
5376
|
}
|
|
5100
5377
|
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
5101
5378
|
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
5102
|
-
const canonicalFile =
|
|
5379
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5103
5380
|
try {
|
|
5104
5381
|
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
5105
5382
|
changed(item.label);
|
|
@@ -5334,7 +5611,7 @@ function renderProjectArchive(result) {
|
|
|
5334
5611
|
if (t.gitignorePatterns.length > 0)
|
|
5335
5612
|
items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
|
|
5336
5613
|
if (t.canonical)
|
|
5337
|
-
items.push(`the anchor's canonical (agents/${
|
|
5614
|
+
items.push(`the anchor's canonical (agents/${basename5(result.target)}/AGENTS.md)`);
|
|
5338
5615
|
if (!t.inspected) {
|
|
5339
5616
|
lines.push(
|
|
5340
5617
|
"## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
|
|
@@ -5373,12 +5650,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
|
5373
5650
|
} catch {
|
|
5374
5651
|
return { canonicalDirOld: false, viewLinkOld: false };
|
|
5375
5652
|
}
|
|
5376
|
-
const canonicalDirOld = existsSync(
|
|
5653
|
+
const canonicalDirOld = existsSync(join9(anchorReal, "agents", oldBasename));
|
|
5377
5654
|
let viewLinkOld = false;
|
|
5378
5655
|
const viewPath = manifest.workspace.view;
|
|
5379
5656
|
if (viewPath !== void 0) {
|
|
5380
5657
|
try {
|
|
5381
|
-
lstatSync(
|
|
5658
|
+
lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
5382
5659
|
viewLinkOld = true;
|
|
5383
5660
|
} catch {
|
|
5384
5661
|
}
|
|
@@ -5537,7 +5814,7 @@ async function resolveRepositoryRootForNew(cwd) {
|
|
|
5537
5814
|
async function doRunProjectNew(repos, options, ctx) {
|
|
5538
5815
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5539
5816
|
const repositoryRoot = await resolveRepositoryRootForNew(cwd);
|
|
5540
|
-
const workspaceName =
|
|
5817
|
+
const workspaceName = basename5(repositoryRoot);
|
|
5541
5818
|
const declared = repos.map((p) => {
|
|
5542
5819
|
const abs = resolve7(cwd, p);
|
|
5543
5820
|
let real;
|
|
@@ -5719,7 +5996,7 @@ function regularFileSpokes(repoReal) {
|
|
|
5719
5996
|
const out = [];
|
|
5720
5997
|
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
5721
5998
|
try {
|
|
5722
|
-
const st = lstatSync(
|
|
5999
|
+
const st = lstatSync(join9(repoReal, spoke));
|
|
5723
6000
|
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
5724
6001
|
} catch {
|
|
5725
6002
|
}
|
|
@@ -5735,7 +6012,7 @@ function pathPresent(p) {
|
|
|
5735
6012
|
}
|
|
5736
6013
|
}
|
|
5737
6014
|
function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
|
|
5738
|
-
const
|
|
6015
|
+
const declaredEntry = roster.find((entry) => {
|
|
5739
6016
|
const entryAbs = resolve7(repositoryRoot, entry.path);
|
|
5740
6017
|
if (argReal !== void 0) {
|
|
5741
6018
|
try {
|
|
@@ -5745,13 +6022,16 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
5745
6022
|
}
|
|
5746
6023
|
return entryAbs === argAbs;
|
|
5747
6024
|
});
|
|
6025
|
+
const declared = declaredEntry !== void 0;
|
|
6026
|
+
const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
|
|
5748
6027
|
const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
|
|
5749
6028
|
const path = displayRel === "" ? "." : displayRel;
|
|
5750
|
-
const canonicalName =
|
|
6029
|
+
const canonicalName = basename5(argReal ?? argAbs);
|
|
5751
6030
|
if (argReal === void 0) {
|
|
5752
6031
|
return {
|
|
5753
6032
|
path,
|
|
5754
6033
|
declared,
|
|
6034
|
+
...self ? { self: true } : {},
|
|
5755
6035
|
isAnchor: false,
|
|
5756
6036
|
reachable: false,
|
|
5757
6037
|
canonicalName,
|
|
@@ -5761,23 +6041,24 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
5761
6041
|
};
|
|
5762
6042
|
}
|
|
5763
6043
|
const isAnchor = argReal === anchorReal;
|
|
5764
|
-
const reachable = existsSync(
|
|
5765
|
-
const canonicalFile =
|
|
6044
|
+
const reachable = existsSync(join9(argReal, ".git"));
|
|
6045
|
+
const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5766
6046
|
return {
|
|
5767
6047
|
path,
|
|
5768
6048
|
declared,
|
|
6049
|
+
...self ? { self: true } : {},
|
|
5769
6050
|
isAnchor,
|
|
5770
6051
|
reachable,
|
|
5771
6052
|
canonicalName,
|
|
5772
|
-
agentsState: inspectAgentsState(
|
|
6053
|
+
agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
|
|
5773
6054
|
canonicalExists: pathPresent(canonicalFile),
|
|
5774
6055
|
regularSpokes: regularFileSpokes(argReal)
|
|
5775
6056
|
};
|
|
5776
6057
|
}
|
|
5777
6058
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
5778
|
-
const agentsFile =
|
|
6059
|
+
const agentsFile = join9(repoReal, CANONICAL_FILE);
|
|
5779
6060
|
try {
|
|
5780
|
-
mkdirSync(
|
|
6061
|
+
mkdirSync(dirname3(canonicalFile), { recursive: true });
|
|
5781
6062
|
} catch (error) {
|
|
5782
6063
|
return { ok: false, message: failureReason(error), partial: false };
|
|
5783
6064
|
}
|
|
@@ -5815,7 +6096,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
5815
6096
|
let failure;
|
|
5816
6097
|
let partial = false;
|
|
5817
6098
|
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
5818
|
-
const canonicalFile =
|
|
6099
|
+
const canonicalFile = join9(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
5819
6100
|
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
5820
6101
|
if (res.ok) {
|
|
5821
6102
|
applied = true;
|
|
@@ -5868,6 +6149,10 @@ function renderProjectRetrofit(result) {
|
|
|
5868
6149
|
lines.push(
|
|
5869
6150
|
`\u2139\uFE0F \`${result.path}\` is not declared in the roster (manifest \`repos\`). Add it first with \`basou project new\` / \`basou project adopt\`, then re-run.`
|
|
5870
6151
|
);
|
|
6152
|
+
} else if (result.reason === "self") {
|
|
6153
|
+
lines.push(
|
|
6154
|
+
`\u2139\uFE0F \`${result.path}\` declares \`instructions: self\` \u2014 its \`${CANONICAL_FILE}\` is a hand-authored committed file that stays in the repo, so there is no anchor canonical to relocate it to. Retrofit does not apply (its CLAUDE.md / Copilot spokes are wired by \`basou project symlinks\`).`
|
|
6155
|
+
);
|
|
5871
6156
|
} else if (result.reason === "anchor") {
|
|
5872
6157
|
lines.push(
|
|
5873
6158
|
`\u26A0\uFE0F \`${result.path}\` is the anchor (the project root). It owns its canonical directly \u2014 there is nothing to relocate.`
|
|
@@ -5943,70 +6228,17 @@ import {
|
|
|
5943
6228
|
removeMarkerSection as removeMarkerSection2
|
|
5944
6229
|
} from "@basou/core";
|
|
5945
6230
|
|
|
5946
|
-
// src/lib/durable-write.ts
|
|
5947
|
-
import { randomUUID } from "crypto";
|
|
5948
|
-
import { lstat, open as open2, rename, stat as stat4, unlink as unlink2 } from "fs/promises";
|
|
5949
|
-
import { basename as basename5, dirname as dirname3, join as join8 } from "path";
|
|
5950
|
-
async function assertNotSymlink(targetPath) {
|
|
5951
|
-
try {
|
|
5952
|
-
const st = await lstat(targetPath);
|
|
5953
|
-
if (st.isSymbolicLink()) {
|
|
5954
|
-
throw new Error(
|
|
5955
|
-
"Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
|
|
5956
|
-
);
|
|
5957
|
-
}
|
|
5958
|
-
} catch (error) {
|
|
5959
|
-
if (error instanceof Error && error.code === "ENOENT") return;
|
|
5960
|
-
throw error;
|
|
5961
|
-
}
|
|
5962
|
-
}
|
|
5963
|
-
async function writeFileDurable(targetPath, content) {
|
|
5964
|
-
const dir = dirname3(targetPath);
|
|
5965
|
-
const tmpPath = join8(dir, `.${basename5(targetPath)}.tmp.${randomUUID()}`);
|
|
5966
|
-
let mode = 420;
|
|
5967
|
-
try {
|
|
5968
|
-
mode = (await stat4(targetPath)).mode & 511;
|
|
5969
|
-
} catch (error) {
|
|
5970
|
-
if (!(error instanceof Error && error.code === "ENOENT")) {
|
|
5971
|
-
throw error;
|
|
5972
|
-
}
|
|
5973
|
-
}
|
|
5974
|
-
let handle;
|
|
5975
|
-
try {
|
|
5976
|
-
handle = await open2(tmpPath, "wx", mode);
|
|
5977
|
-
await handle.writeFile(content, "utf8");
|
|
5978
|
-
await handle.chmod(mode);
|
|
5979
|
-
await handle.sync();
|
|
5980
|
-
await handle.close();
|
|
5981
|
-
handle = void 0;
|
|
5982
|
-
await rename(tmpPath, targetPath);
|
|
5983
|
-
} catch (error) {
|
|
5984
|
-
if (handle) await handle.close().catch(() => void 0);
|
|
5985
|
-
await unlink2(tmpPath).catch(() => void 0);
|
|
5986
|
-
throw error;
|
|
5987
|
-
}
|
|
5988
|
-
try {
|
|
5989
|
-
const dirHandle = await open2(dir, "r");
|
|
5990
|
-
try {
|
|
5991
|
-
await dirHandle.sync();
|
|
5992
|
-
} finally {
|
|
5993
|
-
await dirHandle.close();
|
|
5994
|
-
}
|
|
5995
|
-
} catch {
|
|
5996
|
-
}
|
|
5997
|
-
}
|
|
5998
|
-
|
|
5999
6231
|
// src/lib/protocols-config.ts
|
|
6000
|
-
import { homedir as
|
|
6001
|
-
import { isAbsolute as isAbsolute4, join as
|
|
6232
|
+
import { homedir as homedir7 } from "os";
|
|
6233
|
+
import { isAbsolute as isAbsolute4, join as join10, resolve as resolve8 } from "path";
|
|
6002
6234
|
import { readYamlFile as readYamlFile5 } from "@basou/core";
|
|
6003
|
-
var DEFAULT_PROTOCOLS_CONFIG_PATH =
|
|
6004
|
-
var DEFAULT_TARGET_PATH =
|
|
6235
|
+
var DEFAULT_PROTOCOLS_CONFIG_PATH = join10(homedir7(), ".basou", "protocols.yaml");
|
|
6236
|
+
var DEFAULT_TARGET_PATH = join10(homedir7(), ".claude", "CLAUDE.md");
|
|
6005
6237
|
var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
|
|
6006
6238
|
var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
|
|
6007
6239
|
function expandTilde3(p) {
|
|
6008
|
-
if (p === "~") return
|
|
6009
|
-
if (p.startsWith("~/")) return
|
|
6240
|
+
if (p === "~") return homedir7();
|
|
6241
|
+
if (p.startsWith("~/")) return join10(homedir7(), p.slice(2));
|
|
6010
6242
|
return p;
|
|
6011
6243
|
}
|
|
6012
6244
|
function isRecord3(value) {
|
|
@@ -6258,16 +6490,16 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
|
6258
6490
|
|
|
6259
6491
|
// src/commands/refresh-watch.ts
|
|
6260
6492
|
import { readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
6261
|
-
import { homedir as
|
|
6262
|
-
import { join as
|
|
6493
|
+
import { homedir as homedir8 } from "os";
|
|
6494
|
+
import { join as join11 } from "path";
|
|
6263
6495
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
6264
6496
|
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
6265
6497
|
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
6266
6498
|
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
6267
6499
|
function watchedRoots(ctx) {
|
|
6268
6500
|
return [
|
|
6269
|
-
ctx.codexSessionsDir ??
|
|
6270
|
-
ctx.claudeProjectsDir ??
|
|
6501
|
+
ctx.codexSessionsDir ?? join11(homedir8(), ".codex", "sessions"),
|
|
6502
|
+
ctx.claudeProjectsDir ?? join11(homedir8(), ".claude", "projects")
|
|
6271
6503
|
];
|
|
6272
6504
|
}
|
|
6273
6505
|
async function scanSourceLogs(roots) {
|
|
@@ -6281,7 +6513,7 @@ async function scanSourceLogs(roots) {
|
|
|
6281
6513
|
throw new Error("Failed to read a source log directory", { cause: error });
|
|
6282
6514
|
}
|
|
6283
6515
|
for (const entry of entries) {
|
|
6284
|
-
const full =
|
|
6516
|
+
const full = join11(dir, entry.name);
|
|
6285
6517
|
if (entry.isDirectory()) {
|
|
6286
6518
|
await walk(full);
|
|
6287
6519
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -6802,8 +7034,8 @@ function renderReviewGaps(summary) {
|
|
|
6802
7034
|
|
|
6803
7035
|
// src/commands/run.ts
|
|
6804
7036
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
6805
|
-
import { homedir as
|
|
6806
|
-
import { join as
|
|
7037
|
+
import { homedir as homedir9 } from "os";
|
|
7038
|
+
import { join as join12 } from "path";
|
|
6807
7039
|
import {
|
|
6808
7040
|
acquireLock as acquireLock5,
|
|
6809
7041
|
assertBasouRootSafe as assertBasouRootSafe11,
|
|
@@ -6856,13 +7088,13 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
6856
7088
|
await assertBasouRootSafe11(paths.root);
|
|
6857
7089
|
const manifest = await readManifest7(paths);
|
|
6858
7090
|
const sessionId = prefixedUlid4("ses");
|
|
6859
|
-
const sessionDir =
|
|
7091
|
+
const sessionDir = join12(paths.sessions, sessionId);
|
|
6860
7092
|
await mkdir2(sessionDir, { recursive: true });
|
|
6861
7093
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
6862
7094
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
6863
7095
|
});
|
|
6864
7096
|
const startedAt = now().toISOString();
|
|
6865
|
-
const sessionYamlPath =
|
|
7097
|
+
const sessionYamlPath = join12(sessionDir, "session.yaml");
|
|
6866
7098
|
const session = buildInitialSession2({
|
|
6867
7099
|
id: sessionId,
|
|
6868
7100
|
command,
|
|
@@ -6988,7 +7220,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
6988
7220
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
6989
7221
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
6990
7222
|
workingDirectory: repoRoot,
|
|
6991
|
-
homedir:
|
|
7223
|
+
homedir: homedir9()
|
|
6992
7224
|
}).sanitized;
|
|
6993
7225
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
6994
7226
|
await appendEvent(sessionDir, {
|
|
@@ -7132,7 +7364,7 @@ function buildInitialSession2(input) {
|
|
|
7132
7364
|
source: { ...claudeCodeAdapterMetadata },
|
|
7133
7365
|
started_at: input.startedAt,
|
|
7134
7366
|
status: "initialized",
|
|
7135
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
7367
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir9() }),
|
|
7136
7368
|
invocation: {
|
|
7137
7369
|
command: input.command,
|
|
7138
7370
|
args: [...input.args],
|
|
@@ -7205,7 +7437,7 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
7205
7437
|
|
|
7206
7438
|
// src/commands/session.ts
|
|
7207
7439
|
import { readFile as readFile5 } from "fs/promises";
|
|
7208
|
-
import { basename as basename6, isAbsolute as isAbsolute6, join as
|
|
7440
|
+
import { basename as basename6, isAbsolute as isAbsolute6, join as join13, relative as relative3 } from "path";
|
|
7209
7441
|
import {
|
|
7210
7442
|
acquireLock as acquireLock6,
|
|
7211
7443
|
appendEventToExistingSession as appendEventToExistingSession3,
|
|
@@ -7330,8 +7562,8 @@ async function doRunSessionShow(idInput, options, ctx) {
|
|
|
7330
7562
|
const paths = basouPaths15(repositoryRoot);
|
|
7331
7563
|
await assertWorkspaceInitialized10(paths.root);
|
|
7332
7564
|
const sessionId = await resolveSessionId3(paths, idInput);
|
|
7333
|
-
const sessionDir =
|
|
7334
|
-
const sessionYamlPath =
|
|
7565
|
+
const sessionDir = join13(paths.sessions, sessionId);
|
|
7566
|
+
const sessionYamlPath = join13(sessionDir, "session.yaml");
|
|
7335
7567
|
let session;
|
|
7336
7568
|
try {
|
|
7337
7569
|
const raw = await readYamlFile7(sessionYamlPath);
|
|
@@ -8074,7 +8306,7 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
8074
8306
|
|
|
8075
8307
|
// src/commands/task.ts
|
|
8076
8308
|
import { readFile as readFile6 } from "fs/promises";
|
|
8077
|
-
import { join as
|
|
8309
|
+
import { join as join14 } from "path";
|
|
8078
8310
|
import {
|
|
8079
8311
|
archiveTask,
|
|
8080
8312
|
assertBasouRootSafe as assertBasouRootSafe15,
|
|
@@ -8400,7 +8632,7 @@ async function doRunTaskShow(idInput, options, ctx) {
|
|
|
8400
8632
|
const events = [];
|
|
8401
8633
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
8402
8634
|
for (const s of sessions) {
|
|
8403
|
-
const sessionDir =
|
|
8635
|
+
const sessionDir = join14(paths.sessions, s.sessionId);
|
|
8404
8636
|
try {
|
|
8405
8637
|
for await (const ev of replayEvents3(sessionDir, {
|
|
8406
8638
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -9306,7 +9538,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
|
9306
9538
|
// src/lib/portfolio-safety.ts
|
|
9307
9539
|
import { execFile } from "child_process";
|
|
9308
9540
|
import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
|
|
9309
|
-
import { isAbsolute as isAbsolute7, join as
|
|
9541
|
+
import { isAbsolute as isAbsolute7, join as join15, relative as relative4, resolve as resolve10 } from "path";
|
|
9310
9542
|
import { promisify } from "util";
|
|
9311
9543
|
import { readManifest as readManifest11 } from "@basou/core";
|
|
9312
9544
|
var execFileAsync = promisify(execFile);
|
|
@@ -9330,7 +9562,7 @@ function isBasouPath(p) {
|
|
|
9330
9562
|
async function inspectRepo(repoPath) {
|
|
9331
9563
|
let hasEntry = false;
|
|
9332
9564
|
try {
|
|
9333
|
-
await lstat2(
|
|
9565
|
+
await lstat2(join15(repoPath, ".basou"));
|
|
9334
9566
|
hasEntry = true;
|
|
9335
9567
|
} catch (error) {
|
|
9336
9568
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -9431,7 +9663,7 @@ function formatSafetyReport(result) {
|
|
|
9431
9663
|
|
|
9432
9664
|
// src/lib/view-server.ts
|
|
9433
9665
|
import { createServer } from "http";
|
|
9434
|
-
import { join as
|
|
9666
|
+
import { join as join16 } from "path";
|
|
9435
9667
|
import {
|
|
9436
9668
|
computeWorkStats as computeWorkStats2,
|
|
9437
9669
|
enumerateApprovals as enumerateApprovals2,
|
|
@@ -10390,7 +10622,7 @@ async function sessionDetail(ws, sessionId) {
|
|
|
10390
10622
|
throw error;
|
|
10391
10623
|
}
|
|
10392
10624
|
try {
|
|
10393
|
-
const events = await readAllEvents2(
|
|
10625
|
+
const events = await readAllEvents2(join16(ws.paths.sessions, sessionId));
|
|
10394
10626
|
return { session, events };
|
|
10395
10627
|
} catch {
|
|
10396
10628
|
return { session, events: [], degraded: true };
|