@basou/cli 0.29.0 → 0.31.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 +716 -279
- package/dist/index.js.map +1 -1
- package/dist/program.js +716 -279
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2114,16 +2114,20 @@ function registerHookCommand(program2) {
|
|
|
2114
2114
|
).option(
|
|
2115
2115
|
"--block",
|
|
2116
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"
|
|
2117
2120
|
).addHelpText("after", HOOK_STOP_HELP).action(async (options) => {
|
|
2118
2121
|
const minEdits = parseMinEdits(options.minEdits);
|
|
2119
2122
|
await runHookStop({
|
|
2120
2123
|
...minEdits !== void 0 ? { minEdits } : {},
|
|
2121
|
-
...options.block === true ? { block: true } : {}
|
|
2124
|
+
...options.block === true ? { block: true } : {},
|
|
2125
|
+
...options.requireReview === true ? { requireReview: true } : {}
|
|
2122
2126
|
});
|
|
2123
2127
|
});
|
|
2124
2128
|
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) => {
|
|
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) => {
|
|
2127
2131
|
await runHookInstall(opts);
|
|
2128
2132
|
});
|
|
2129
2133
|
hook.command("uninstall").description(
|
|
@@ -2147,6 +2151,12 @@ Substantive = EITHER >= ${DEFAULT_STOP_HOOK_MIN_EDITS} file edits (default) OR a
|
|
|
2147
2151
|
answer (an uncaptured conversational decision). Read-only Bash (ls / grep /
|
|
2148
2152
|
git status) does NOT count.
|
|
2149
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
|
+
|
|
2150
2160
|
By default the reminder is non-blocking: Claude sees it and may act on it or
|
|
2151
2161
|
stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
|
|
2152
2162
|
returns decision:block, holding the agent in-turn to act on the reminder; the
|
|
@@ -2190,11 +2200,17 @@ async function doRunHookStop(options, ctx) {
|
|
|
2190
2200
|
stopHookActive: false,
|
|
2191
2201
|
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
2192
2202
|
});
|
|
2193
|
-
|
|
2194
|
-
|
|
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({
|
|
2195
2211
|
hookSpecificOutput: {
|
|
2196
2212
|
hookEventName: "Stop",
|
|
2197
|
-
additionalContext:
|
|
2213
|
+
additionalContext: reason
|
|
2198
2214
|
}
|
|
2199
2215
|
});
|
|
2200
2216
|
write(`${payloadJson}
|
|
@@ -2247,6 +2263,7 @@ function resolveCliEntry() {
|
|
|
2247
2263
|
function normalizeInstallOptions(raw) {
|
|
2248
2264
|
const out = {};
|
|
2249
2265
|
if (raw.block === true) out.block = true;
|
|
2266
|
+
if (raw.requireReview === true) out.requireReview = true;
|
|
2250
2267
|
if (raw.settings !== void 0) out.settings = raw.settings;
|
|
2251
2268
|
if (raw.dryRun === true) out.dryRun = true;
|
|
2252
2269
|
if (raw.verbose === true) out.verbose = true;
|
|
@@ -2306,9 +2323,13 @@ async function doRunHookInstall(options, ctx = {}) {
|
|
|
2306
2323
|
const command = buildStopHookCommand({
|
|
2307
2324
|
cliEntry,
|
|
2308
2325
|
...options.block === true ? { block: true } : {},
|
|
2326
|
+
...options.requireReview === true ? { requireReview: true } : {},
|
|
2309
2327
|
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
2310
2328
|
});
|
|
2311
|
-
const mode =
|
|
2329
|
+
const mode = describeHookMode({
|
|
2330
|
+
block: options.block === true,
|
|
2331
|
+
review: options.requireReview === true
|
|
2332
|
+
});
|
|
2312
2333
|
await assertNotSymlink(settingsPath);
|
|
2313
2334
|
const { raw, parsed } = await readSettings(settingsPath);
|
|
2314
2335
|
const { settings, action } = upsertStopHook(parsed, command);
|
|
@@ -2385,9 +2406,17 @@ async function doRunHookStatus(options) {
|
|
|
2385
2406
|
console.log("basou Stop hook: not registered. Run 'basou hook install' to register it.");
|
|
2386
2407
|
return;
|
|
2387
2408
|
}
|
|
2388
|
-
const mode =
|
|
2409
|
+
const mode = describeHookMode({
|
|
2410
|
+
block: / --block\b/.test(command),
|
|
2411
|
+
review: / --require-review\b/.test(command)
|
|
2412
|
+
});
|
|
2389
2413
|
console.log(`basou Stop hook: registered, ${mode}.`);
|
|
2390
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
|
+
}
|
|
2391
2420
|
|
|
2392
2421
|
// src/commands/import.ts
|
|
2393
2422
|
import { createReadStream } from "fs";
|
|
@@ -3020,7 +3049,6 @@ import {
|
|
|
3020
3049
|
createManifest,
|
|
3021
3050
|
ensureBasouDirectory,
|
|
3022
3051
|
resolveRepositoryRoot as resolveRepositoryRoot7,
|
|
3023
|
-
tryRemoteUrl,
|
|
3024
3052
|
writeManifest
|
|
3025
3053
|
} from "@basou/core";
|
|
3026
3054
|
function collectValue(value, previous) {
|
|
@@ -3029,7 +3057,7 @@ function collectValue(value, previous) {
|
|
|
3029
3057
|
function registerInitCommand(program2) {
|
|
3030
3058
|
program2.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
|
|
3031
3059
|
"--repo-url <url>",
|
|
3032
|
-
"
|
|
3060
|
+
"Deprecated and ignored (project.repository_url was removed); accepted for 0.x CLI stability, removed at 1.0"
|
|
3033
3061
|
).option(
|
|
3034
3062
|
"--source-root <path>",
|
|
3035
3063
|
"Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
|
|
@@ -3054,11 +3082,10 @@ async function doRunInit(options, ctx) {
|
|
|
3054
3082
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3055
3083
|
const repositoryRoot = await resolveRepositoryRootForInit(cwd);
|
|
3056
3084
|
const workspaceName = options.name ?? basename4(repositoryRoot);
|
|
3057
|
-
let repositoryUrl;
|
|
3058
3085
|
if (options.repoUrl !== void 0) {
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3086
|
+
console.error(
|
|
3087
|
+
"Warning: --repo-url is deprecated and ignored (project.repository_url was removed); the flag will be removed at 1.0."
|
|
3088
|
+
);
|
|
3062
3089
|
}
|
|
3063
3090
|
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
3064
3091
|
const rel = relative(repositoryRoot, resolve5(cwd, p));
|
|
@@ -3069,7 +3096,6 @@ async function doRunInit(options, ctx) {
|
|
|
3069
3096
|
workspaceName,
|
|
3070
3097
|
...options.projectName !== void 0 ? { projectName: options.projectName } : {},
|
|
3071
3098
|
...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
|
|
3072
|
-
...repositoryUrl !== void 0 ? { repositoryUrl } : {},
|
|
3073
3099
|
...sourceRoots.length > 0 ? { sourceRoots } : {}
|
|
3074
3100
|
});
|
|
3075
3101
|
await writeManifest(paths, manifest, { force: options.force === true });
|
|
@@ -6220,25 +6246,131 @@ function renderProjectRetrofit(result) {
|
|
|
6220
6246
|
|
|
6221
6247
|
// src/commands/protocol.ts
|
|
6222
6248
|
import { readFile as readFile4 } from "fs/promises";
|
|
6249
|
+
import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdownFile as readMarkdownFile6 } from "@basou/core";
|
|
6250
|
+
|
|
6251
|
+
// src/lib/context-channel.ts
|
|
6252
|
+
import { homedir as homedir7 } from "os";
|
|
6253
|
+
import { join as join10 } from "path";
|
|
6223
6254
|
import {
|
|
6224
|
-
|
|
6225
|
-
|
|
6255
|
+
ORIENTATION_END,
|
|
6256
|
+
ORIENTATION_START,
|
|
6226
6257
|
parseMarkers as parseMarkers2,
|
|
6227
6258
|
readMarkdownFile as readMarkdownFile5,
|
|
6228
6259
|
removeMarkerSection as removeMarkerSection2
|
|
6229
6260
|
} from "@basou/core";
|
|
6261
|
+
var CODEX_TARGET_PATH = join10(homedir7(), ".codex", "AGENTS.md");
|
|
6262
|
+
var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
|
|
6263
|
+
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. -->";
|
|
6264
|
+
function buildTargetBody(existing, block, markers) {
|
|
6265
|
+
const wrapped = `${markers.start}
|
|
6266
|
+
${block}${markers.end}
|
|
6267
|
+
`;
|
|
6268
|
+
if (existing === null || existing === "") return wrapped;
|
|
6269
|
+
const section = parseMarkers2(existing, markers);
|
|
6270
|
+
switch (section.kind) {
|
|
6271
|
+
case "ok":
|
|
6272
|
+
return `${section.before}${markers.start}
|
|
6273
|
+
${block}${markers.end}${section.after}`;
|
|
6274
|
+
case "no_markers": {
|
|
6275
|
+
const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
6276
|
+
return `${existing}${sep}${wrapped}`;
|
|
6277
|
+
}
|
|
6278
|
+
default:
|
|
6279
|
+
throw new Error(
|
|
6280
|
+
"The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
|
|
6281
|
+
);
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
async function backupOnce(target, existing) {
|
|
6285
|
+
if (existing === null) return;
|
|
6286
|
+
const bak = `${target}.basou-bak`;
|
|
6287
|
+
const already = await readMarkdownFile5(bak);
|
|
6288
|
+
if (already !== null) return;
|
|
6289
|
+
await writeFileDurable(bak, existing);
|
|
6290
|
+
}
|
|
6291
|
+
async function syncMarkerBlock(opts) {
|
|
6292
|
+
const { target, markers, block } = opts;
|
|
6293
|
+
await assertNotSymlink(target);
|
|
6294
|
+
const existing = await readMarkdownFile5(target);
|
|
6295
|
+
const newBody = buildTargetBody(existing, block, markers);
|
|
6296
|
+
if (newBody === existing) return { action: "unchanged" };
|
|
6297
|
+
const hadBlock = existing !== null && parseMarkers2(existing, markers).kind === "ok";
|
|
6298
|
+
const action = hadBlock ? "updated" : "installed";
|
|
6299
|
+
if (opts.dryRun === true) return { action };
|
|
6300
|
+
const recheck = await readMarkdownFile5(target);
|
|
6301
|
+
if (recheck !== existing) {
|
|
6302
|
+
throw new Error(
|
|
6303
|
+
"The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
6304
|
+
);
|
|
6305
|
+
}
|
|
6306
|
+
await backupOnce(target, existing);
|
|
6307
|
+
await writeFileDurable(target, newBody);
|
|
6308
|
+
return { action };
|
|
6309
|
+
}
|
|
6310
|
+
function assertNoMarkerLine(body, markers) {
|
|
6311
|
+
for (const line of body.split(/\r?\n/)) {
|
|
6312
|
+
if (line === markers.start || line === markers.end) {
|
|
6313
|
+
throw new Error(
|
|
6314
|
+
"The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
|
|
6315
|
+
);
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
async function removeMarkerBlock(opts) {
|
|
6320
|
+
const { target, markers, fileLabel } = opts;
|
|
6321
|
+
await assertNotSymlink(target);
|
|
6322
|
+
const existing = await readMarkdownFile5(target);
|
|
6323
|
+
if (existing === null) return { removed: false };
|
|
6324
|
+
const newBody = removeMarkerSection2(existing, fileLabel, markers);
|
|
6325
|
+
if (newBody === existing) return { removed: false };
|
|
6326
|
+
if (opts.dryRun === true) return { removed: true };
|
|
6327
|
+
const recheck = await readMarkdownFile5(target);
|
|
6328
|
+
if (recheck !== existing) {
|
|
6329
|
+
throw new Error(
|
|
6330
|
+
"The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
6331
|
+
);
|
|
6332
|
+
}
|
|
6333
|
+
await backupOnce(target, existing);
|
|
6334
|
+
await writeFileDurable(target, newBody);
|
|
6335
|
+
return { removed: true };
|
|
6336
|
+
}
|
|
6337
|
+
async function syncOrientationChannel(opts) {
|
|
6338
|
+
assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
|
|
6339
|
+
const block = `${ORIENTATION_MANAGED_NOTE}
|
|
6340
|
+
|
|
6341
|
+
${opts.body.replace(/\s+$/, "")}
|
|
6342
|
+
`;
|
|
6343
|
+
return syncMarkerBlock({
|
|
6344
|
+
target: opts.target ?? CODEX_TARGET_PATH,
|
|
6345
|
+
markers: ORIENTATION_MARKERS,
|
|
6346
|
+
block,
|
|
6347
|
+
...opts.dryRun === true ? { dryRun: true } : {}
|
|
6348
|
+
});
|
|
6349
|
+
}
|
|
6350
|
+
async function renderOrientationToCodexChannel(opts) {
|
|
6351
|
+
const body = await readMarkdownFile5(opts.orientationPath);
|
|
6352
|
+
if (body === null) return null;
|
|
6353
|
+
const { action } = await syncOrientationChannel({
|
|
6354
|
+
body,
|
|
6355
|
+
...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
|
|
6356
|
+
});
|
|
6357
|
+
return {
|
|
6358
|
+
action,
|
|
6359
|
+
line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6230
6362
|
|
|
6231
6363
|
// src/lib/protocols-config.ts
|
|
6232
|
-
import { homedir as
|
|
6233
|
-
import { isAbsolute as isAbsolute4, join as
|
|
6364
|
+
import { homedir as homedir8 } from "os";
|
|
6365
|
+
import { isAbsolute as isAbsolute4, join as join11, resolve as resolve8 } from "path";
|
|
6234
6366
|
import { readYamlFile as readYamlFile5 } from "@basou/core";
|
|
6235
|
-
var DEFAULT_PROTOCOLS_CONFIG_PATH =
|
|
6236
|
-
var DEFAULT_TARGET_PATH =
|
|
6367
|
+
var DEFAULT_PROTOCOLS_CONFIG_PATH = join11(homedir8(), ".basou", "protocols.yaml");
|
|
6368
|
+
var DEFAULT_TARGET_PATH = join11(homedir8(), ".claude", "CLAUDE.md");
|
|
6237
6369
|
var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
|
|
6238
6370
|
var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
|
|
6239
6371
|
function expandTilde3(p) {
|
|
6240
|
-
if (p === "~") return
|
|
6241
|
-
if (p.startsWith("~/")) return
|
|
6372
|
+
if (p === "~") return homedir8();
|
|
6373
|
+
if (p.startsWith("~/")) return join11(homedir8(), p.slice(2));
|
|
6242
6374
|
return p;
|
|
6243
6375
|
}
|
|
6244
6376
|
function isRecord3(value) {
|
|
@@ -6359,13 +6491,7 @@ async function readProtocolSources(entries) {
|
|
|
6359
6491
|
}
|
|
6360
6492
|
throw new Error("Failed to read a protocol source file.", { cause: error });
|
|
6361
6493
|
}
|
|
6362
|
-
|
|
6363
|
-
if (line === PROTOCOL_START || line === PROTOCOL_END) {
|
|
6364
|
-
throw new Error(
|
|
6365
|
-
"A protocol source contains a BASOU:PROTOCOLS marker line, which would corrupt the managed block. Remove that line from the source."
|
|
6366
|
-
);
|
|
6367
|
-
}
|
|
6368
|
-
}
|
|
6494
|
+
assertNoMarkerLine(content, PROTOCOL_MARKERS);
|
|
6369
6495
|
out.push({ entry, content });
|
|
6370
6496
|
}
|
|
6371
6497
|
return out;
|
|
@@ -6382,74 +6508,41 @@ ${body}` : body;
|
|
|
6382
6508
|
${sections.join("\n\n")}
|
|
6383
6509
|
`;
|
|
6384
6510
|
}
|
|
6385
|
-
function buildTargetBody(existing, block) {
|
|
6386
|
-
const wrapped = `${PROTOCOL_START}
|
|
6387
|
-
${block}${PROTOCOL_END}
|
|
6388
|
-
`;
|
|
6389
|
-
if (existing === null || existing === "") return wrapped;
|
|
6390
|
-
const section = parseMarkers2(existing, PROTOCOL_MARKERS);
|
|
6391
|
-
switch (section.kind) {
|
|
6392
|
-
case "ok":
|
|
6393
|
-
return `${section.before}${PROTOCOL_START}
|
|
6394
|
-
${block}${PROTOCOL_END}${section.after}`;
|
|
6395
|
-
case "no_markers": {
|
|
6396
|
-
const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
6397
|
-
return `${existing}${sep}${wrapped}`;
|
|
6398
|
-
}
|
|
6399
|
-
default:
|
|
6400
|
-
throw new Error(
|
|
6401
|
-
"The BASOU:PROTOCOLS markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
|
|
6402
|
-
);
|
|
6403
|
-
}
|
|
6404
|
-
}
|
|
6405
|
-
async function backupOnce(target, existing) {
|
|
6406
|
-
if (existing === null) return;
|
|
6407
|
-
const bak = `${target}.basou-bak`;
|
|
6408
|
-
const already = await readMarkdownFile5(bak);
|
|
6409
|
-
if (already !== null) return;
|
|
6410
|
-
await writeFileDurable(bak, existing);
|
|
6411
|
-
}
|
|
6412
6511
|
async function doRunProtocolSync(options) {
|
|
6413
6512
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
6414
6513
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
6415
6514
|
const entries = await loadProtocolsConfig(configPath);
|
|
6416
6515
|
const sources = await readProtocolSources(entries);
|
|
6417
6516
|
const block = buildBlock(sources);
|
|
6418
|
-
await
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6517
|
+
const result = await syncMarkerBlock({
|
|
6518
|
+
target,
|
|
6519
|
+
markers: PROTOCOL_MARKERS,
|
|
6520
|
+
block,
|
|
6521
|
+
...options.dryRun === true ? { dryRun: true } : {}
|
|
6522
|
+
});
|
|
6523
|
+
if (result.action === "unchanged") {
|
|
6422
6524
|
console.log(`The basou:protocols block is already up to date (${entries.length} protocol(s)).`);
|
|
6423
6525
|
return;
|
|
6424
6526
|
}
|
|
6425
|
-
const hadBlock = existing !== null && parseMarkers2(existing, PROTOCOL_MARKERS).kind === "ok";
|
|
6426
6527
|
if (options.dryRun === true) {
|
|
6427
6528
|
console.log(
|
|
6428
|
-
`[dry-run] Would ${
|
|
6529
|
+
`[dry-run] Would ${result.action === "updated" ? "update" : "install"} the basou:protocols block (${entries.length} protocol(s)).`
|
|
6429
6530
|
);
|
|
6430
6531
|
for (const { entry } of sources) {
|
|
6431
6532
|
console.log(` - ${entry.title ?? entry.source}`);
|
|
6432
6533
|
}
|
|
6433
6534
|
return;
|
|
6434
6535
|
}
|
|
6435
|
-
const recheck = await readMarkdownFile5(target);
|
|
6436
|
-
if (recheck !== existing) {
|
|
6437
|
-
throw new Error(
|
|
6438
|
-
"The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol sync'."
|
|
6439
|
-
);
|
|
6440
|
-
}
|
|
6441
|
-
await backupOnce(target, existing);
|
|
6442
|
-
await writeFileDurable(target, newBody);
|
|
6443
6536
|
console.log(
|
|
6444
|
-
`${
|
|
6537
|
+
`${result.action === "updated" ? "Updated" : "Installed"} the basou:protocols block in the global CLAUDE.md (${entries.length} protocol(s)).`
|
|
6445
6538
|
);
|
|
6446
6539
|
}
|
|
6447
6540
|
async function doRunProtocolList(options) {
|
|
6448
6541
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
6449
6542
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
6450
6543
|
const entries = await loadProtocolsConfig(configPath);
|
|
6451
|
-
const existing = await
|
|
6452
|
-
const installed = existing !== null &&
|
|
6544
|
+
const existing = await readMarkdownFile6(target);
|
|
6545
|
+
const installed = existing !== null && parseMarkers3(existing, PROTOCOL_MARKERS).kind === "ok";
|
|
6453
6546
|
console.log(`Declared protocols (${entries.length}):`);
|
|
6454
6547
|
for (const entry of entries) {
|
|
6455
6548
|
console.log(` - ${entry.title ?? entry.source}`);
|
|
@@ -6458,14 +6551,13 @@ async function doRunProtocolList(options) {
|
|
|
6458
6551
|
}
|
|
6459
6552
|
async function doRunProtocolUnsync(options) {
|
|
6460
6553
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
6461
|
-
await
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
}
|
|
6467
|
-
|
|
6468
|
-
if (newBody === existing) {
|
|
6554
|
+
const result = await removeMarkerBlock({
|
|
6555
|
+
target,
|
|
6556
|
+
markers: PROTOCOL_MARKERS,
|
|
6557
|
+
fileLabel: "CLAUDE.md",
|
|
6558
|
+
...options.dryRun === true ? { dryRun: true } : {}
|
|
6559
|
+
});
|
|
6560
|
+
if (!result.removed) {
|
|
6469
6561
|
console.log("No basou:protocols block found; nothing removed.");
|
|
6470
6562
|
return;
|
|
6471
6563
|
}
|
|
@@ -6473,14 +6565,6 @@ async function doRunProtocolUnsync(options) {
|
|
|
6473
6565
|
console.log("[dry-run] Would remove the basou:protocols block from the global CLAUDE.md.");
|
|
6474
6566
|
return;
|
|
6475
6567
|
}
|
|
6476
|
-
const recheck = await readMarkdownFile5(target);
|
|
6477
|
-
if (recheck !== existing) {
|
|
6478
|
-
throw new Error(
|
|
6479
|
-
"The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol unsync'."
|
|
6480
|
-
);
|
|
6481
|
-
}
|
|
6482
|
-
await backupOnce(target, existing);
|
|
6483
|
-
await writeFileDurable(target, newBody);
|
|
6484
6568
|
console.log("Removed the basou:protocols block from the global CLAUDE.md.");
|
|
6485
6569
|
}
|
|
6486
6570
|
|
|
@@ -6490,16 +6574,16 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
|
6490
6574
|
|
|
6491
6575
|
// src/commands/refresh-watch.ts
|
|
6492
6576
|
import { readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
6493
|
-
import { homedir as
|
|
6494
|
-
import { join as
|
|
6577
|
+
import { homedir as homedir9 } from "os";
|
|
6578
|
+
import { join as join12 } from "path";
|
|
6495
6579
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
6496
6580
|
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
6497
6581
|
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
6498
6582
|
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
6499
6583
|
function watchedRoots(ctx) {
|
|
6500
6584
|
return [
|
|
6501
|
-
ctx.codexSessionsDir ??
|
|
6502
|
-
ctx.claudeProjectsDir ??
|
|
6585
|
+
ctx.codexSessionsDir ?? join12(homedir9(), ".codex", "sessions"),
|
|
6586
|
+
ctx.claudeProjectsDir ?? join12(homedir9(), ".claude", "projects")
|
|
6503
6587
|
];
|
|
6504
6588
|
}
|
|
6505
6589
|
async function scanSourceLogs(roots) {
|
|
@@ -6513,7 +6597,7 @@ async function scanSourceLogs(roots) {
|
|
|
6513
6597
|
throw new Error("Failed to read a source log directory", { cause: error });
|
|
6514
6598
|
}
|
|
6515
6599
|
for (const entry of entries) {
|
|
6516
|
-
const full =
|
|
6600
|
+
const full = join12(dir, entry.name);
|
|
6517
6601
|
if (entry.isDirectory()) {
|
|
6518
6602
|
await walk(full);
|
|
6519
6603
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -6620,19 +6704,19 @@ function parseInterval(value) {
|
|
|
6620
6704
|
return seconds;
|
|
6621
6705
|
}
|
|
6622
6706
|
function abortableSleep(ms, signal) {
|
|
6623
|
-
return new Promise((
|
|
6707
|
+
return new Promise((resolve14) => {
|
|
6624
6708
|
if (signal.aborted) {
|
|
6625
|
-
|
|
6709
|
+
resolve14();
|
|
6626
6710
|
return;
|
|
6627
6711
|
}
|
|
6628
6712
|
let timer;
|
|
6629
6713
|
const onAbort = () => {
|
|
6630
6714
|
clearTimeout(timer);
|
|
6631
|
-
|
|
6715
|
+
resolve14();
|
|
6632
6716
|
};
|
|
6633
6717
|
timer = setTimeout(() => {
|
|
6634
6718
|
signal.removeEventListener("abort", onAbort);
|
|
6635
|
-
|
|
6719
|
+
resolve14();
|
|
6636
6720
|
}, ms);
|
|
6637
6721
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
6638
6722
|
});
|
|
@@ -6685,7 +6769,7 @@ async function doRunRefreshPortfolio(options, ctx) {
|
|
|
6685
6769
|
for (const ws of workspaces) {
|
|
6686
6770
|
const label = ws.label ?? ws.path;
|
|
6687
6771
|
try {
|
|
6688
|
-
const result = await computeRefresh(
|
|
6772
|
+
const { result } = await computeRefresh(
|
|
6689
6773
|
{ ...options, portfolio: false },
|
|
6690
6774
|
{ ...ctx, cwd: ws.path }
|
|
6691
6775
|
);
|
|
@@ -6754,7 +6838,7 @@ async function computeRefresh(options, ctx) {
|
|
|
6754
6838
|
const paths = basouPaths11(repositoryRoot);
|
|
6755
6839
|
await assertWorkspaceInitialized8(paths.root);
|
|
6756
6840
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
6757
|
-
|
|
6841
|
+
const result = await refreshAll({
|
|
6758
6842
|
options: {
|
|
6759
6843
|
...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
6760
6844
|
...options.force === true ? { force: true } : {},
|
|
@@ -6766,16 +6850,30 @@ async function computeRefresh(options, ctx) {
|
|
|
6766
6850
|
paths,
|
|
6767
6851
|
nowIso
|
|
6768
6852
|
});
|
|
6853
|
+
return { result, paths };
|
|
6769
6854
|
}
|
|
6770
6855
|
async function doRunRefresh(options, ctx) {
|
|
6771
|
-
const result = await computeRefresh(options, ctx);
|
|
6856
|
+
const { result, paths } = await computeRefresh(options, ctx);
|
|
6857
|
+
const channelLine = options.dryRun === true ? null : await syncCodexOrientationChannel(paths, ctx.codexChannelPath);
|
|
6772
6858
|
if (options.json === true) {
|
|
6773
6859
|
console.log(JSON.stringify(result));
|
|
6774
6860
|
} else {
|
|
6775
6861
|
printRefreshSummary(result);
|
|
6862
|
+
if (channelLine !== null) console.log(channelLine);
|
|
6776
6863
|
}
|
|
6777
6864
|
return result;
|
|
6778
6865
|
}
|
|
6866
|
+
async function syncCodexOrientationChannel(paths, channelPath) {
|
|
6867
|
+
try {
|
|
6868
|
+
const rendered = await renderOrientationToCodexChannel({
|
|
6869
|
+
orientationPath: paths.files.orientation,
|
|
6870
|
+
...channelPath !== void 0 ? { channelPath } : {}
|
|
6871
|
+
});
|
|
6872
|
+
return rendered === null ? null : rendered.line;
|
|
6873
|
+
} catch (error) {
|
|
6874
|
+
return `codex channel skipped: ${error instanceof Error ? error.message : String(error)}`;
|
|
6875
|
+
}
|
|
6876
|
+
}
|
|
6779
6877
|
function describeImport(outcome) {
|
|
6780
6878
|
if (outcome.status === "skipped") {
|
|
6781
6879
|
return `${outcome.adapter}: skipped (${outcome.reason})`;
|
|
@@ -6908,9 +7006,195 @@ async function assertWorkspaceInitialized9(basouRoot) {
|
|
|
6908
7006
|
}
|
|
6909
7007
|
}
|
|
6910
7008
|
|
|
6911
|
-
// src/commands/review
|
|
7009
|
+
// src/commands/review.ts
|
|
7010
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
7011
|
+
import { homedir as homedir10 } from "os";
|
|
7012
|
+
import { resolve as resolve10 } from "path";
|
|
6912
7013
|
import {
|
|
7014
|
+
assertBasouRootSafe as assertBasouRootSafe11,
|
|
6913
7015
|
basouPaths as basouPaths13,
|
|
7016
|
+
buildReviewRecordedEvent,
|
|
7017
|
+
buildReviewRecordLabel,
|
|
7018
|
+
createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
|
|
7019
|
+
findErrorCode as findErrorCode11,
|
|
7020
|
+
parseReviewRecordInput,
|
|
7021
|
+
readManifest as readManifest7,
|
|
7022
|
+
sanitizePath as sanitizePath2
|
|
7023
|
+
} from "@basou/core";
|
|
7024
|
+
function registerReviewCommand(program2) {
|
|
7025
|
+
const review = program2.command("review").description("Record reviews that ran (the durable signal a review happened)");
|
|
7026
|
+
review.command("record").description(
|
|
7027
|
+
"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."
|
|
7028
|
+
).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) => {
|
|
7029
|
+
await runReviewRecord(options);
|
|
7030
|
+
});
|
|
7031
|
+
}
|
|
7032
|
+
var REVIEW_RECORD_HELP = `
|
|
7033
|
+
Input format (a single JSON object describing one review):
|
|
7034
|
+
{
|
|
7035
|
+
"reviewer": "codex",
|
|
7036
|
+
"target": "working-tree",
|
|
7037
|
+
"verdict": "needs-attention",
|
|
7038
|
+
"findings": [
|
|
7039
|
+
{ "title": "Off-by-one in pager", "severity": "medium", "location": "src/page.ts:42", "summary": "..." }
|
|
7040
|
+
],
|
|
7041
|
+
"blocked": [
|
|
7042
|
+
{ "title": "Reviewer wanted to drop the singleton", "reason": "design-reversal", "why": "Settled in decision_X" }
|
|
7043
|
+
]
|
|
7044
|
+
}
|
|
7045
|
+
|
|
7046
|
+
Only "reviewer" and "target" are required; verdict / findings / blocked are
|
|
7047
|
+
optional. Record blocked findings (spec-deviation / design-reversal) here so the
|
|
7048
|
+
adversarial-review protocol's "always report what you blocked" becomes a durable
|
|
7049
|
+
trail artifact -- an explicit empty "blocked": [] is encouraged to record that
|
|
7050
|
+
you blocked nothing. The review is written into one ad-hoc session timestamped
|
|
7051
|
+
now. Run from a workspace-view directory and it resolves to the planning repo,
|
|
7052
|
+
like 'basou decision capture' / 'basou note'.
|
|
7053
|
+
|
|
7054
|
+
Example (heredoc on stdin):
|
|
7055
|
+
basou review record <<'JSON'
|
|
7056
|
+
{ "reviewer": "codex", "target": "working-tree", "verdict": "pass", "blocked": [] }
|
|
7057
|
+
JSON
|
|
7058
|
+
`;
|
|
7059
|
+
async function runReviewRecord(options, ctx = {}) {
|
|
7060
|
+
try {
|
|
7061
|
+
await doRunReviewRecord(options, ctx);
|
|
7062
|
+
} catch (error) {
|
|
7063
|
+
renderCliError(error, {
|
|
7064
|
+
verbose: isVerbose(options),
|
|
7065
|
+
classifiers: [failedToFinalizeClassifier]
|
|
7066
|
+
});
|
|
7067
|
+
process.exitCode = 1;
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
async function doRunReviewRecord(options, ctx) {
|
|
7071
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
7072
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "review record");
|
|
7073
|
+
const paths = basouPaths13(repositoryRoot);
|
|
7074
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
7075
|
+
const raw = await readReviewInput(options, ctx);
|
|
7076
|
+
const review = parseReviewRecordInput(raw);
|
|
7077
|
+
if (options.dryRun === true) {
|
|
7078
|
+
printReviewPreview(options, review);
|
|
7079
|
+
return;
|
|
7080
|
+
}
|
|
7081
|
+
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
7082
|
+
const occurredAt = now.toISOString();
|
|
7083
|
+
const manifest = await readManifest7(paths);
|
|
7084
|
+
const invocationArgs = options.file !== void 0 ? [
|
|
7085
|
+
"--file",
|
|
7086
|
+
sanitizePath2(resolve10(cwd, options.file), {
|
|
7087
|
+
workingDirectory: repositoryRoot,
|
|
7088
|
+
homedir: homedir10()
|
|
7089
|
+
})
|
|
7090
|
+
] : [];
|
|
7091
|
+
const adHoc = await createAdHocSessionWithEvent3({
|
|
7092
|
+
paths,
|
|
7093
|
+
manifest,
|
|
7094
|
+
label: buildReviewRecordLabel(review),
|
|
7095
|
+
occurredAt,
|
|
7096
|
+
sessionSource: "human",
|
|
7097
|
+
workingDirectory: repositoryRoot,
|
|
7098
|
+
invocation: { command: "basou review record", args: invocationArgs },
|
|
7099
|
+
targetEventBuilders: [
|
|
7100
|
+
(sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review })
|
|
7101
|
+
]
|
|
7102
|
+
});
|
|
7103
|
+
printReviewResult(options, {
|
|
7104
|
+
sessionId: adHoc.sessionId,
|
|
7105
|
+
eventId: adHoc.targetEventIds[0],
|
|
7106
|
+
review
|
|
7107
|
+
});
|
|
7108
|
+
}
|
|
7109
|
+
async function readReviewInput(options, ctx) {
|
|
7110
|
+
if (options.file !== void 0) {
|
|
7111
|
+
try {
|
|
7112
|
+
return await readFile5(options.file, "utf8");
|
|
7113
|
+
} catch (error) {
|
|
7114
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
7115
|
+
throw new Error(`Input file not found: ${options.file}`);
|
|
7116
|
+
}
|
|
7117
|
+
throw error;
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
if (ctx.readInput !== void 0) {
|
|
7121
|
+
return await ctx.readInput();
|
|
7122
|
+
}
|
|
7123
|
+
if (process.stdin.isTTY === true) {
|
|
7124
|
+
throw new Error(NO_INPUT_HINT2);
|
|
7125
|
+
}
|
|
7126
|
+
return await readStdinToEnd2();
|
|
7127
|
+
}
|
|
7128
|
+
async function readStdinToEnd2() {
|
|
7129
|
+
const chunks = [];
|
|
7130
|
+
for await (const chunk of process.stdin) {
|
|
7131
|
+
chunks.push(Buffer.from(chunk));
|
|
7132
|
+
}
|
|
7133
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
7134
|
+
}
|
|
7135
|
+
var NO_INPUT_HINT2 = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
|
|
7136
|
+
function reviewToPayload(review) {
|
|
7137
|
+
const payload = {
|
|
7138
|
+
reviewer: review.reviewer,
|
|
7139
|
+
target: review.target
|
|
7140
|
+
};
|
|
7141
|
+
if (review.verdict !== void 0) payload.verdict = review.verdict;
|
|
7142
|
+
if (review.findings !== void 0) payload.findings = review.findings;
|
|
7143
|
+
if (review.blocked !== void 0) payload.blocked = review.blocked;
|
|
7144
|
+
return payload;
|
|
7145
|
+
}
|
|
7146
|
+
function reviewSummaryLine(review) {
|
|
7147
|
+
const parts = [];
|
|
7148
|
+
if (review.verdict !== void 0) parts.push(`verdict: ${review.verdict}`);
|
|
7149
|
+
if (review.findings !== void 0) {
|
|
7150
|
+
parts.push(`${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}`);
|
|
7151
|
+
}
|
|
7152
|
+
if (review.blocked !== void 0) {
|
|
7153
|
+
parts.push(`${review.blocked.length} blocked`);
|
|
7154
|
+
}
|
|
7155
|
+
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
7156
|
+
}
|
|
7157
|
+
function printReviewPreview(options, review) {
|
|
7158
|
+
if (options.json === true) {
|
|
7159
|
+
console.log(JSON.stringify({ dry_run: true, review: reviewToPayload(review) }));
|
|
7160
|
+
return;
|
|
7161
|
+
}
|
|
7162
|
+
console.log(
|
|
7163
|
+
`Would record review by ${review.reviewer} of ${review.target}${reviewSummaryLine(review)} (dry run; nothing written).`
|
|
7164
|
+
);
|
|
7165
|
+
}
|
|
7166
|
+
function printReviewResult(options, result) {
|
|
7167
|
+
const sid = shortSessionId(result.sessionId);
|
|
7168
|
+
if (options.json === true) {
|
|
7169
|
+
console.log(
|
|
7170
|
+
JSON.stringify({
|
|
7171
|
+
mode: "ad-hoc",
|
|
7172
|
+
session_id: result.sessionId,
|
|
7173
|
+
session_status: "completed",
|
|
7174
|
+
event_id: result.eventId,
|
|
7175
|
+
review: reviewToPayload(result.review)
|
|
7176
|
+
})
|
|
7177
|
+
);
|
|
7178
|
+
return;
|
|
7179
|
+
}
|
|
7180
|
+
console.log(
|
|
7181
|
+
`Recorded review by ${result.review.reviewer} of ${result.review.target}${reviewSummaryLine(result.review)} in ad-hoc session ${sid}.`
|
|
7182
|
+
);
|
|
7183
|
+
}
|
|
7184
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
7185
|
+
try {
|
|
7186
|
+
await assertBasouRootSafe11(basouRoot);
|
|
7187
|
+
} catch (error) {
|
|
7188
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
7189
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
7190
|
+
}
|
|
7191
|
+
throw error;
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
|
|
7195
|
+
// src/commands/review-gaps.ts
|
|
7196
|
+
import {
|
|
7197
|
+
basouPaths as basouPaths14,
|
|
6914
7198
|
findReviewGaps
|
|
6915
7199
|
} from "@basou/core";
|
|
6916
7200
|
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
@@ -6951,7 +7235,7 @@ async function runReviewGaps(options, ctx = {}) {
|
|
|
6951
7235
|
async function doRunReviewGaps(options, ctx) {
|
|
6952
7236
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6953
7237
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
|
|
6954
|
-
const paths =
|
|
7238
|
+
const paths = basouPaths14(repositoryRoot);
|
|
6955
7239
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
6956
7240
|
const summary = await findReviewGaps({
|
|
6957
7241
|
paths,
|
|
@@ -7034,23 +7318,25 @@ function renderReviewGaps(summary) {
|
|
|
7034
7318
|
|
|
7035
7319
|
// src/commands/run.ts
|
|
7036
7320
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
7037
|
-
import { homedir as
|
|
7038
|
-
import { join as
|
|
7321
|
+
import { homedir as homedir11 } from "os";
|
|
7322
|
+
import { join as join13 } from "path";
|
|
7039
7323
|
import {
|
|
7040
7324
|
acquireLock as acquireLock5,
|
|
7041
|
-
assertBasouRootSafe as
|
|
7042
|
-
basouPaths as
|
|
7325
|
+
assertBasouRootSafe as assertBasouRootSafe12,
|
|
7326
|
+
basouPaths as basouPaths15,
|
|
7043
7327
|
ChildProcessRunner as ChildProcessRunner2,
|
|
7044
7328
|
claudeCodeAdapterMetadata,
|
|
7329
|
+
codexAdapterMetadata,
|
|
7045
7330
|
appendChainedEvent as coreAppendChainedEvent2,
|
|
7046
7331
|
finalizeSessionYaml as finalizeSessionYaml2,
|
|
7047
7332
|
getDiff,
|
|
7048
7333
|
getSnapshot as getSnapshot2,
|
|
7049
7334
|
overwriteYamlFile as overwriteYamlFile2,
|
|
7050
7335
|
prefixedUlid as prefixedUlid4,
|
|
7051
|
-
readManifest as
|
|
7336
|
+
readManifest as readManifest8,
|
|
7052
7337
|
readYamlFile as readYamlFile6,
|
|
7053
7338
|
resolveClaudeCodeCommand,
|
|
7339
|
+
resolveCodexCommand,
|
|
7054
7340
|
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
7055
7341
|
SessionSchema as SessionSchema2,
|
|
7056
7342
|
sanitizeRelatedFiles,
|
|
@@ -7058,50 +7344,72 @@ import {
|
|
|
7058
7344
|
writeYamlFile as writeYamlFile2
|
|
7059
7345
|
} from "@basou/core";
|
|
7060
7346
|
function registerRunCommand(program2, ctx = {}) {
|
|
7061
|
-
const runCommand =
|
|
7062
|
-
|
|
7347
|
+
const runCommand = addRunOptions(
|
|
7348
|
+
program2.command("run").description("Run an AI coding tool through Basou as a tracked session").enablePositionalOptions()
|
|
7349
|
+
);
|
|
7350
|
+
const dispatch = async (args, options, command, run) => {
|
|
7063
7351
|
const parentOptions = command.parent?.opts() ?? {};
|
|
7064
7352
|
const snapshotOn = parentOptions.snapshot !== false && options.snapshot !== false;
|
|
7065
|
-
const merged = {
|
|
7066
|
-
...parentOptions,
|
|
7067
|
-
...options,
|
|
7068
|
-
snapshot: snapshotOn
|
|
7069
|
-
};
|
|
7353
|
+
const merged = { ...parentOptions, ...options, snapshot: snapshotOn };
|
|
7070
7354
|
try {
|
|
7071
|
-
const exitCode = await
|
|
7355
|
+
const exitCode = await run(args, merged, ctx);
|
|
7072
7356
|
process.exit(exitCode);
|
|
7073
7357
|
} catch (error) {
|
|
7074
7358
|
renderCliError(error, { verbose: isVerbose(merged) });
|
|
7075
7359
|
process.exit(1);
|
|
7076
7360
|
}
|
|
7361
|
+
};
|
|
7362
|
+
addRunOptions(runCommand.command("claude-code [args...]")).description("Run Claude Code CLI as a Basou-tracked session").passThroughOptions().action(
|
|
7363
|
+
(args, options, command) => dispatch(args, options, command, runClaudeCode)
|
|
7364
|
+
);
|
|
7365
|
+
addRunOptions(runCommand.command("codex [args...]")).description("Run the Codex CLI as a Basou-tracked session").passThroughOptions().action(
|
|
7366
|
+
(args, options, command) => dispatch(args, options, command, runCodex)
|
|
7367
|
+
);
|
|
7368
|
+
}
|
|
7369
|
+
function addRunOptions(command) {
|
|
7370
|
+
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");
|
|
7371
|
+
}
|
|
7372
|
+
function runClaudeCode(args, options, ctx = {}) {
|
|
7373
|
+
return runTrackedTool(args, options, ctx, {
|
|
7374
|
+
resolveCommand: ctx.resolveCommand ?? resolveClaudeCodeCommand,
|
|
7375
|
+
metadata: claudeCodeAdapterMetadata
|
|
7077
7376
|
});
|
|
7078
7377
|
}
|
|
7079
|
-
|
|
7378
|
+
function runCodex(args, options, ctx = {}) {
|
|
7379
|
+
return runTrackedTool(args, options, ctx, {
|
|
7380
|
+
resolveCommand: ctx.resolveCodexCommand ?? resolveCodexCommand,
|
|
7381
|
+
metadata: codexAdapterMetadata,
|
|
7382
|
+
transformArgs: (a) => ["-c", "shell_environment_policy.inherit=all", ...a],
|
|
7383
|
+
preSpawn: syncCodexOrientationChannelPreSpawn
|
|
7384
|
+
});
|
|
7385
|
+
}
|
|
7386
|
+
async function runTrackedTool(args, options, ctx, adapter) {
|
|
7080
7387
|
const runner = ctx.runner ?? new ChildProcessRunner2();
|
|
7081
7388
|
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
7082
|
-
const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
|
|
7083
7389
|
const getDiffFn = ctx.getDiff ?? getDiff;
|
|
7084
|
-
const { command } = await resolveCommand();
|
|
7390
|
+
const { command } = await adapter.resolveCommand();
|
|
7391
|
+
const childArgs = adapter.transformArgs ? adapter.transformArgs(args) : args;
|
|
7085
7392
|
const cwd = options.cwd ?? process.cwd();
|
|
7086
7393
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
7087
|
-
const paths =
|
|
7088
|
-
await
|
|
7089
|
-
const manifest = await
|
|
7394
|
+
const paths = basouPaths15(repoRoot);
|
|
7395
|
+
await assertBasouRootSafe12(paths.root);
|
|
7396
|
+
const manifest = await readManifest8(paths);
|
|
7090
7397
|
const sessionId = prefixedUlid4("ses");
|
|
7091
|
-
const sessionDir =
|
|
7398
|
+
const sessionDir = join13(paths.sessions, sessionId);
|
|
7092
7399
|
await mkdir2(sessionDir, { recursive: true });
|
|
7093
7400
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
7094
7401
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
7095
7402
|
});
|
|
7096
7403
|
const startedAt = now().toISOString();
|
|
7097
|
-
const sessionYamlPath =
|
|
7404
|
+
const sessionYamlPath = join13(sessionDir, "session.yaml");
|
|
7098
7405
|
const session = buildInitialSession2({
|
|
7099
7406
|
id: sessionId,
|
|
7100
7407
|
command,
|
|
7101
|
-
args,
|
|
7408
|
+
args: childArgs,
|
|
7102
7409
|
cwd: repoRoot,
|
|
7103
7410
|
workspaceId: manifest.workspace.id,
|
|
7104
|
-
startedAt
|
|
7411
|
+
startedAt,
|
|
7412
|
+
source: adapter.metadata
|
|
7105
7413
|
});
|
|
7106
7414
|
await writeYamlFile2(sessionYamlPath, session);
|
|
7107
7415
|
await appendEvent(sessionDir, {
|
|
@@ -7110,7 +7418,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7110
7418
|
id: prefixedUlid4("evt"),
|
|
7111
7419
|
session_id: sessionId,
|
|
7112
7420
|
occurred_at: startedAt,
|
|
7113
|
-
source:
|
|
7421
|
+
source: adapter.metadata.kind
|
|
7114
7422
|
});
|
|
7115
7423
|
let preSnapshot = null;
|
|
7116
7424
|
if (options.snapshot !== false) {
|
|
@@ -7123,7 +7431,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7123
7431
|
id: prefixedUlid4("evt"),
|
|
7124
7432
|
session_id: sessionId,
|
|
7125
7433
|
occurred_at: runningAt,
|
|
7126
|
-
source:
|
|
7434
|
+
source: adapter.metadata.kind,
|
|
7127
7435
|
from: "initialized",
|
|
7128
7436
|
to: "running"
|
|
7129
7437
|
});
|
|
@@ -7157,10 +7465,14 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7157
7465
|
process.on("SIGTERM", onSigTerm);
|
|
7158
7466
|
process.on("exit", exitHandler);
|
|
7159
7467
|
ctx.onExitHookInstalled?.(exitHandler);
|
|
7468
|
+
if (adapter.preSpawn !== void 0) {
|
|
7469
|
+
const line = await adapter.preSpawn(cwd, ctx);
|
|
7470
|
+
if (line !== null) console.log(line);
|
|
7471
|
+
}
|
|
7160
7472
|
let result;
|
|
7161
7473
|
try {
|
|
7162
7474
|
try {
|
|
7163
|
-
result = await runner.run(command,
|
|
7475
|
+
result = await runner.run(command, childArgs, {
|
|
7164
7476
|
cwd: repoRoot,
|
|
7165
7477
|
capture: "none",
|
|
7166
7478
|
signal: controller.signal,
|
|
@@ -7171,10 +7483,11 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7171
7483
|
} catch (spawnError) {
|
|
7172
7484
|
await finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, {
|
|
7173
7485
|
command,
|
|
7174
|
-
args,
|
|
7486
|
+
args: childArgs,
|
|
7175
7487
|
cwd: repoRoot,
|
|
7176
7488
|
occurredAt: now().toISOString(),
|
|
7177
|
-
signalReceived
|
|
7489
|
+
signalReceived,
|
|
7490
|
+
sourceKind: adapter.metadata.kind
|
|
7178
7491
|
});
|
|
7179
7492
|
throw spawnError;
|
|
7180
7493
|
}
|
|
@@ -7193,7 +7506,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7193
7506
|
occurred_at: endedAt,
|
|
7194
7507
|
source: "terminal-recording",
|
|
7195
7508
|
command,
|
|
7196
|
-
args,
|
|
7509
|
+
args: childArgs,
|
|
7197
7510
|
cwd: repoRoot,
|
|
7198
7511
|
exit_code: result.exit_code,
|
|
7199
7512
|
...result.signal !== null ? { signal: result.signal } : {},
|
|
@@ -7220,7 +7533,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7220
7533
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
7221
7534
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
7222
7535
|
workingDirectory: repoRoot,
|
|
7223
|
-
homedir:
|
|
7536
|
+
homedir: homedir11()
|
|
7224
7537
|
}).sanitized;
|
|
7225
7538
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
7226
7539
|
await appendEvent(sessionDir, {
|
|
@@ -7229,7 +7542,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7229
7542
|
id: prefixedUlid4("evt"),
|
|
7230
7543
|
session_id: sessionId,
|
|
7231
7544
|
occurred_at: endedAt,
|
|
7232
|
-
source:
|
|
7545
|
+
source: adapter.metadata.kind,
|
|
7233
7546
|
from: "running",
|
|
7234
7547
|
to: finalStatus
|
|
7235
7548
|
});
|
|
@@ -7239,7 +7552,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
7239
7552
|
id: prefixedUlid4("evt"),
|
|
7240
7553
|
session_id: sessionId,
|
|
7241
7554
|
occurred_at: endedAt,
|
|
7242
|
-
source:
|
|
7555
|
+
source: adapter.metadata.kind,
|
|
7243
7556
|
...result.exit_code !== null ? { exit_code: result.exit_code } : {}
|
|
7244
7557
|
});
|
|
7245
7558
|
await finalizeSessionYaml2(paths, sessionId, (s) => {
|
|
@@ -7361,10 +7674,10 @@ function buildInitialSession2(input) {
|
|
|
7361
7674
|
label: `basou run ${cmdline} (${input.startedAt})`,
|
|
7362
7675
|
task_id: null,
|
|
7363
7676
|
workspace_id: input.workspaceId,
|
|
7364
|
-
source: { ...
|
|
7677
|
+
source: { ...input.source },
|
|
7365
7678
|
started_at: input.startedAt,
|
|
7366
7679
|
status: "initialized",
|
|
7367
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
7680
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir11() }),
|
|
7368
7681
|
invocation: {
|
|
7369
7682
|
command: input.command,
|
|
7370
7683
|
args: [...input.args],
|
|
@@ -7404,7 +7717,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
7404
7717
|
id: prefixedUlid4("evt"),
|
|
7405
7718
|
session_id: sessionId,
|
|
7406
7719
|
occurred_at: ctx.occurredAt,
|
|
7407
|
-
source:
|
|
7720
|
+
source: ctx.sourceKind,
|
|
7408
7721
|
from: "running",
|
|
7409
7722
|
to: "failed"
|
|
7410
7723
|
});
|
|
@@ -7414,7 +7727,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
7414
7727
|
id: prefixedUlid4("evt"),
|
|
7415
7728
|
session_id: sessionId,
|
|
7416
7729
|
occurred_at: ctx.occurredAt,
|
|
7417
|
-
source:
|
|
7730
|
+
source: ctx.sourceKind
|
|
7418
7731
|
});
|
|
7419
7732
|
await finalizeSessionYaml2(paths, sessionId, (s) => {
|
|
7420
7733
|
s.session.status = "failed";
|
|
@@ -7434,21 +7747,34 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
7434
7747
|
throw error;
|
|
7435
7748
|
}
|
|
7436
7749
|
}
|
|
7750
|
+
async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
|
|
7751
|
+
try {
|
|
7752
|
+
const root = await resolveBasouRootForCommand(cwd, "run");
|
|
7753
|
+
const paths = basouPaths15(root);
|
|
7754
|
+
const rendered = await renderOrientationToCodexChannel({
|
|
7755
|
+
orientationPath: paths.files.orientation,
|
|
7756
|
+
...ctx.codexChannelPath !== void 0 ? { channelPath: ctx.codexChannelPath } : {}
|
|
7757
|
+
});
|
|
7758
|
+
return rendered === null ? null : rendered.line;
|
|
7759
|
+
} catch {
|
|
7760
|
+
return null;
|
|
7761
|
+
}
|
|
7762
|
+
}
|
|
7437
7763
|
|
|
7438
7764
|
// src/commands/session.ts
|
|
7439
|
-
import { readFile as
|
|
7440
|
-
import { basename as basename6, isAbsolute as isAbsolute6, join as
|
|
7765
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
7766
|
+
import { basename as basename6, isAbsolute as isAbsolute6, join as join14, relative as relative3 } from "path";
|
|
7441
7767
|
import {
|
|
7442
7768
|
acquireLock as acquireLock6,
|
|
7443
7769
|
appendEventToExistingSession as appendEventToExistingSession3,
|
|
7444
|
-
assertBasouRootSafe as
|
|
7445
|
-
basouPaths as
|
|
7770
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
7771
|
+
basouPaths as basouPaths16,
|
|
7446
7772
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
7447
|
-
findErrorCode as
|
|
7773
|
+
findErrorCode as findErrorCode12,
|
|
7448
7774
|
importSessionFromJson as importSessionFromJson2,
|
|
7449
7775
|
loadSessionEntries as loadSessionEntries2,
|
|
7450
7776
|
readAllEvents,
|
|
7451
|
-
readManifest as
|
|
7777
|
+
readManifest as readManifest9,
|
|
7452
7778
|
readYamlFile as readYamlFile7,
|
|
7453
7779
|
rechainSessionInPlace,
|
|
7454
7780
|
resolveSessionId as resolveSessionId3,
|
|
@@ -7507,8 +7833,8 @@ async function runSessionList(options, ctx = {}) {
|
|
|
7507
7833
|
async function doRunSessionList(options, ctx) {
|
|
7508
7834
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7509
7835
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
|
|
7510
|
-
const paths =
|
|
7511
|
-
await
|
|
7836
|
+
const paths = basouPaths16(repositoryRoot);
|
|
7837
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
7512
7838
|
const now = /* @__PURE__ */ new Date();
|
|
7513
7839
|
const records = (await loadSessionEntries2(paths, {
|
|
7514
7840
|
now,
|
|
@@ -7559,17 +7885,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
|
|
|
7559
7885
|
async function doRunSessionShow(idInput, options, ctx) {
|
|
7560
7886
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7561
7887
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
|
|
7562
|
-
const paths =
|
|
7563
|
-
await
|
|
7888
|
+
const paths = basouPaths16(repositoryRoot);
|
|
7889
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
7564
7890
|
const sessionId = await resolveSessionId3(paths, idInput);
|
|
7565
|
-
const sessionDir =
|
|
7566
|
-
const sessionYamlPath =
|
|
7891
|
+
const sessionDir = join14(paths.sessions, sessionId);
|
|
7892
|
+
const sessionYamlPath = join14(sessionDir, "session.yaml");
|
|
7567
7893
|
let session;
|
|
7568
7894
|
try {
|
|
7569
7895
|
const raw = await readYamlFile7(sessionYamlPath);
|
|
7570
7896
|
session = SessionSchema3.parse(raw);
|
|
7571
7897
|
} catch (error) {
|
|
7572
|
-
if (
|
|
7898
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
7573
7899
|
throw new Error(`Session not found: ${idInput}`);
|
|
7574
7900
|
}
|
|
7575
7901
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -7763,6 +8089,11 @@ function eventVariantSummary(ev) {
|
|
|
7763
8089
|
return `task ${shortTaskId2(ev.task_id)}: ${ev.title} (archived)`;
|
|
7764
8090
|
case "note_added":
|
|
7765
8091
|
return ev.body.length > 80 ? `${ev.body.slice(0, 77)}...` : ev.body;
|
|
8092
|
+
case "review_recorded": {
|
|
8093
|
+
const verdict = ev.verdict !== void 0 ? ` (${ev.verdict})` : "";
|
|
8094
|
+
const blocked = ev.blocked !== void 0 && ev.blocked.length > 0 ? ` ${ev.blocked.length} blocked` : "";
|
|
8095
|
+
return `${ev.reviewer} -> ${ev.target}${verdict}${blocked}`;
|
|
8096
|
+
}
|
|
7766
8097
|
case "adapter_output":
|
|
7767
8098
|
return `${ev.stream} "${ev.summary}" raw_ref=${ev.raw_ref}`;
|
|
7768
8099
|
}
|
|
@@ -7810,11 +8141,11 @@ function maxLen2(values, floor) {
|
|
|
7810
8141
|
async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
7811
8142
|
return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
|
|
7812
8143
|
}
|
|
7813
|
-
async function
|
|
8144
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
7814
8145
|
try {
|
|
7815
|
-
await
|
|
8146
|
+
await assertBasouRootSafe13(basouRoot);
|
|
7816
8147
|
} catch (error) {
|
|
7817
|
-
if (
|
|
8148
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
7818
8149
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
7819
8150
|
}
|
|
7820
8151
|
throw error;
|
|
@@ -7852,9 +8183,9 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
7852
8183
|
async function doRunSessionImport(options, ctx) {
|
|
7853
8184
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7854
8185
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
7855
|
-
const paths =
|
|
7856
|
-
await
|
|
7857
|
-
const manifest = await
|
|
8186
|
+
const paths = basouPaths16(repositoryRoot);
|
|
8187
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
8188
|
+
const manifest = await readManifest9(paths);
|
|
7858
8189
|
const rawBody = await readInputFile(options.from);
|
|
7859
8190
|
const json = parseJsonStrict(rawBody);
|
|
7860
8191
|
const parsed = SessionImportPayloadSchema2.safeParse(json);
|
|
@@ -7881,12 +8212,12 @@ async function doRunSessionImport(options, ctx) {
|
|
|
7881
8212
|
}
|
|
7882
8213
|
async function readInputFile(path) {
|
|
7883
8214
|
try {
|
|
7884
|
-
return await
|
|
8215
|
+
return await readFile6(path, "utf8");
|
|
7885
8216
|
} catch (error) {
|
|
7886
|
-
if (
|
|
8217
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
7887
8218
|
throw new Error("Import source not found", { cause: error });
|
|
7888
8219
|
}
|
|
7889
|
-
if (
|
|
8220
|
+
if (findErrorCode12(error, "EISDIR")) {
|
|
7890
8221
|
throw new Error("Import source is not a file", { cause: error });
|
|
7891
8222
|
}
|
|
7892
8223
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -7966,8 +8297,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
7966
8297
|
}
|
|
7967
8298
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7968
8299
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
7969
|
-
const paths =
|
|
7970
|
-
await
|
|
8300
|
+
const paths = basouPaths16(repositoryRoot);
|
|
8301
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
7971
8302
|
const sessionId = await resolveSessionId3(paths, sessionIdInput);
|
|
7972
8303
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
7973
8304
|
if (body.length === 0) {
|
|
@@ -7998,12 +8329,12 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
7998
8329
|
}
|
|
7999
8330
|
async function readNoteFile(path) {
|
|
8000
8331
|
try {
|
|
8001
|
-
return await
|
|
8332
|
+
return await readFile6(path, "utf8");
|
|
8002
8333
|
} catch (error) {
|
|
8003
|
-
if (
|
|
8334
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
8004
8335
|
throw new Error("Note source not found", { cause: error });
|
|
8005
8336
|
}
|
|
8006
|
-
if (
|
|
8337
|
+
if (findErrorCode12(error, "EISDIR")) {
|
|
8007
8338
|
throw new Error("Note source is not a file", { cause: error });
|
|
8008
8339
|
}
|
|
8009
8340
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -8048,8 +8379,8 @@ async function doRunSessionRechain(options, ctx) {
|
|
|
8048
8379
|
}
|
|
8049
8380
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8050
8381
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
8051
|
-
const paths =
|
|
8052
|
-
await
|
|
8382
|
+
const paths = basouPaths16(repositoryRoot);
|
|
8383
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
8053
8384
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
8054
8385
|
const dryRun = options.dryRun === true;
|
|
8055
8386
|
const rows = [];
|
|
@@ -8102,10 +8433,10 @@ function renderRechainRow(row, dryRun) {
|
|
|
8102
8433
|
|
|
8103
8434
|
// src/commands/stats.ts
|
|
8104
8435
|
import {
|
|
8105
|
-
assertBasouRootSafe as
|
|
8106
|
-
basouPaths as
|
|
8436
|
+
assertBasouRootSafe as assertBasouRootSafe14,
|
|
8437
|
+
basouPaths as basouPaths17,
|
|
8107
8438
|
computeWorkStats,
|
|
8108
|
-
findErrorCode as
|
|
8439
|
+
findErrorCode as findErrorCode13,
|
|
8109
8440
|
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
8110
8441
|
} from "@basou/core";
|
|
8111
8442
|
function registerStatsCommand(program2) {
|
|
@@ -8124,8 +8455,8 @@ async function runStats(options, ctx = {}) {
|
|
|
8124
8455
|
async function doRunStats(options, ctx) {
|
|
8125
8456
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8126
8457
|
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
8127
|
-
const paths =
|
|
8128
|
-
await
|
|
8458
|
+
const paths = basouPaths17(repositoryRoot);
|
|
8459
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
8129
8460
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
8130
8461
|
const result = await computeWorkStats({
|
|
8131
8462
|
paths,
|
|
@@ -8219,11 +8550,11 @@ async function resolveRepositoryRootForStats(cwd) {
|
|
|
8219
8550
|
throw error;
|
|
8220
8551
|
}
|
|
8221
8552
|
}
|
|
8222
|
-
async function
|
|
8553
|
+
async function assertWorkspaceInitialized12(basouRoot) {
|
|
8223
8554
|
try {
|
|
8224
|
-
await
|
|
8555
|
+
await assertBasouRootSafe14(basouRoot);
|
|
8225
8556
|
} catch (error) {
|
|
8226
|
-
if (
|
|
8557
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
8227
8558
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
8228
8559
|
}
|
|
8229
8560
|
throw error;
|
|
@@ -8232,11 +8563,11 @@ async function assertWorkspaceInitialized11(basouRoot) {
|
|
|
8232
8563
|
|
|
8233
8564
|
// src/commands/status.ts
|
|
8234
8565
|
import {
|
|
8235
|
-
assertBasouRootSafe as
|
|
8236
|
-
basouPaths as
|
|
8566
|
+
assertBasouRootSafe as assertBasouRootSafe15,
|
|
8567
|
+
basouPaths as basouPaths18,
|
|
8237
8568
|
buildStatusSnapshot,
|
|
8238
|
-
findErrorCode as
|
|
8239
|
-
readManifest as
|
|
8569
|
+
findErrorCode as findErrorCode14,
|
|
8570
|
+
readManifest as readManifest10,
|
|
8240
8571
|
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
8241
8572
|
writeStatus
|
|
8242
8573
|
} from "@basou/core";
|
|
@@ -8256,22 +8587,24 @@ async function runStatus(options, ctx = {}) {
|
|
|
8256
8587
|
async function doRunStatus(options, ctx) {
|
|
8257
8588
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8258
8589
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
8259
|
-
const paths =
|
|
8590
|
+
const paths = basouPaths18(repositoryRoot);
|
|
8260
8591
|
try {
|
|
8261
|
-
await
|
|
8592
|
+
await assertBasouRootSafe15(paths.root);
|
|
8262
8593
|
} catch (error) {
|
|
8263
|
-
if (
|
|
8594
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
8264
8595
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
8265
8596
|
}
|
|
8266
8597
|
throw error;
|
|
8267
8598
|
}
|
|
8268
8599
|
let manifest;
|
|
8269
8600
|
try {
|
|
8270
|
-
manifest = await
|
|
8601
|
+
manifest = await readManifest10(paths);
|
|
8271
8602
|
} catch (error) {
|
|
8272
|
-
if (
|
|
8603
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
8273
8604
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
8274
8605
|
}
|
|
8606
|
+
const gateMessage = formatVersionGateMessage(error);
|
|
8607
|
+
if (gateMessage !== void 0) throw new Error(gateMessage, { cause: error });
|
|
8275
8608
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
8276
8609
|
}
|
|
8277
8610
|
const snapshot = await buildStatusSnapshot({ manifest, paths });
|
|
@@ -8303,23 +8636,35 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
8303
8636
|
throw error;
|
|
8304
8637
|
}
|
|
8305
8638
|
}
|
|
8639
|
+
function formatVersionGateMessage(error) {
|
|
8640
|
+
const issues = error.issues;
|
|
8641
|
+
if (!Array.isArray(issues)) return void 0;
|
|
8642
|
+
for (const issue of issues) {
|
|
8643
|
+
const path = issue.path;
|
|
8644
|
+
const message = issue.message;
|
|
8645
|
+
if (Array.isArray(path) && (path.includes("schema_version") || path.includes("basou_version")) && typeof message === "string" && message.startsWith("unsupported .basou format version")) {
|
|
8646
|
+
return message;
|
|
8647
|
+
}
|
|
8648
|
+
}
|
|
8649
|
+
return void 0;
|
|
8650
|
+
}
|
|
8306
8651
|
|
|
8307
8652
|
// src/commands/task.ts
|
|
8308
|
-
import { readFile as
|
|
8309
|
-
import { join as
|
|
8653
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
8654
|
+
import { join as join15 } from "path";
|
|
8310
8655
|
import {
|
|
8311
8656
|
archiveTask,
|
|
8312
|
-
assertBasouRootSafe as
|
|
8313
|
-
basouPaths as
|
|
8657
|
+
assertBasouRootSafe as assertBasouRootSafe16,
|
|
8658
|
+
basouPaths as basouPaths19,
|
|
8314
8659
|
createTaskWithEvent,
|
|
8315
8660
|
deleteTask,
|
|
8316
8661
|
editTask,
|
|
8317
8662
|
enumerateArchivedTaskIds,
|
|
8318
|
-
findErrorCode as
|
|
8663
|
+
findErrorCode as findErrorCode15,
|
|
8319
8664
|
loadSessionEntries as loadSessionEntries3,
|
|
8320
8665
|
loadTaskEntries,
|
|
8321
8666
|
prefixedUlid as prefixedUlid5,
|
|
8322
|
-
readManifest as
|
|
8667
|
+
readManifest as readManifest11,
|
|
8323
8668
|
readTaskFile,
|
|
8324
8669
|
readTaskFileWithArchiveFallback,
|
|
8325
8670
|
reconcileAllTasks,
|
|
@@ -8411,8 +8756,8 @@ async function doRunTaskNew(options, ctx) {
|
|
|
8411
8756
|
}
|
|
8412
8757
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8413
8758
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
8414
|
-
const paths =
|
|
8415
|
-
await
|
|
8759
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8760
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8416
8761
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
8417
8762
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
8418
8763
|
const occurredAt = now.toISOString();
|
|
@@ -8446,7 +8791,7 @@ async function doRunTaskNew(options, ctx) {
|
|
|
8446
8791
|
});
|
|
8447
8792
|
return;
|
|
8448
8793
|
}
|
|
8449
|
-
const manifest = await
|
|
8794
|
+
const manifest = await readManifest11(paths);
|
|
8450
8795
|
const result = await createTaskWithEvent({
|
|
8451
8796
|
mode: "ad-hoc",
|
|
8452
8797
|
paths,
|
|
@@ -8520,8 +8865,8 @@ async function runTaskList(options, ctx = {}) {
|
|
|
8520
8865
|
async function doRunTaskList(options, ctx) {
|
|
8521
8866
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8522
8867
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
8523
|
-
const paths =
|
|
8524
|
-
await
|
|
8868
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8869
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8525
8870
|
const entries = await loadTaskEntries(paths, {
|
|
8526
8871
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
8527
8872
|
});
|
|
@@ -8624,15 +8969,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
8624
8969
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
8625
8970
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8626
8971
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
8627
|
-
const paths =
|
|
8628
|
-
await
|
|
8972
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8973
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8629
8974
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
8630
8975
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
8631
8976
|
const sessions = await loadSessionEntries3(paths, { now: /* @__PURE__ */ new Date() });
|
|
8632
8977
|
const events = [];
|
|
8633
8978
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
8634
8979
|
for (const s of sessions) {
|
|
8635
|
-
const sessionDir =
|
|
8980
|
+
const sessionDir = join15(paths.sessions, s.sessionId);
|
|
8636
8981
|
try {
|
|
8637
8982
|
for await (const ev of replayEvents3(sessionDir, {
|
|
8638
8983
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -8768,8 +9113,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
8768
9113
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
8769
9114
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8770
9115
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
8771
|
-
const paths =
|
|
8772
|
-
await
|
|
9116
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9117
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
8773
9118
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
8774
9119
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
8775
9120
|
const occurredAt = now.toISOString();
|
|
@@ -8794,7 +9139,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
8794
9139
|
});
|
|
8795
9140
|
return;
|
|
8796
9141
|
}
|
|
8797
|
-
const manifest = await
|
|
9142
|
+
const manifest = await readManifest11(paths);
|
|
8798
9143
|
const result = await updateTaskStatusWithEvent({
|
|
8799
9144
|
mode: "ad-hoc",
|
|
8800
9145
|
paths,
|
|
@@ -8845,9 +9190,9 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
8845
9190
|
async function doRunTaskReconcile(options, ctx) {
|
|
8846
9191
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8847
9192
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
8848
|
-
const paths =
|
|
8849
|
-
await
|
|
8850
|
-
const manifest = await
|
|
9193
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9194
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9195
|
+
const manifest = await readManifest11(paths);
|
|
8851
9196
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
8852
9197
|
const write = options.write === true;
|
|
8853
9198
|
const verbose = isVerbose(options);
|
|
@@ -9025,9 +9370,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
9025
9370
|
}
|
|
9026
9371
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9027
9372
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
9028
|
-
const paths =
|
|
9029
|
-
await
|
|
9030
|
-
const manifest = await
|
|
9373
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9374
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9375
|
+
const manifest = await readManifest11(paths);
|
|
9031
9376
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
9032
9377
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
9033
9378
|
const write = options.write === true;
|
|
@@ -9105,9 +9450,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
9105
9450
|
}
|
|
9106
9451
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9107
9452
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
9108
|
-
const paths =
|
|
9109
|
-
await
|
|
9110
|
-
const manifest = await
|
|
9453
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9454
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9455
|
+
const manifest = await readManifest11(paths);
|
|
9111
9456
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
9112
9457
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
9113
9458
|
const occurredAt = now.toISOString();
|
|
@@ -9161,9 +9506,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
9161
9506
|
}
|
|
9162
9507
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9163
9508
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
9164
|
-
const paths =
|
|
9165
|
-
await
|
|
9166
|
-
const manifest = await
|
|
9509
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9510
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9511
|
+
const manifest = await readManifest11(paths);
|
|
9167
9512
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
9168
9513
|
if (options.yes !== true) {
|
|
9169
9514
|
await confirmDestructiveAction("delete", taskId);
|
|
@@ -9206,9 +9551,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
9206
9551
|
}
|
|
9207
9552
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9208
9553
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
9209
|
-
const paths =
|
|
9210
|
-
await
|
|
9211
|
-
const manifest = await
|
|
9554
|
+
const paths = basouPaths19(repositoryRoot);
|
|
9555
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
9556
|
+
const manifest = await readManifest11(paths);
|
|
9212
9557
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
9213
9558
|
if (options.yes !== true) {
|
|
9214
9559
|
await confirmDestructiveAction("archive", taskId);
|
|
@@ -9320,12 +9665,12 @@ function parsePositiveInt2(raw) {
|
|
|
9320
9665
|
}
|
|
9321
9666
|
async function readDescriptionFile(path) {
|
|
9322
9667
|
try {
|
|
9323
|
-
return await
|
|
9668
|
+
return await readFile7(path, "utf8");
|
|
9324
9669
|
} catch (error) {
|
|
9325
|
-
if (
|
|
9670
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
9326
9671
|
throw new Error("Description source not found", { cause: error });
|
|
9327
9672
|
}
|
|
9328
|
-
if (
|
|
9673
|
+
if (findErrorCode15(error, "EISDIR")) {
|
|
9329
9674
|
throw new Error("Description source is not a file", { cause: error });
|
|
9330
9675
|
}
|
|
9331
9676
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -9334,11 +9679,11 @@ async function readDescriptionFile(path) {
|
|
|
9334
9679
|
async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
9335
9680
|
return resolveBasouRootForCommand(cwd, `task ${subcmd}`);
|
|
9336
9681
|
}
|
|
9337
|
-
async function
|
|
9682
|
+
async function assertWorkspaceInitialized13(basouRoot) {
|
|
9338
9683
|
try {
|
|
9339
|
-
await
|
|
9684
|
+
await assertBasouRootSafe16(basouRoot);
|
|
9340
9685
|
} catch (error) {
|
|
9341
|
-
if (
|
|
9686
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
9342
9687
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
9343
9688
|
}
|
|
9344
9689
|
throw error;
|
|
@@ -9426,10 +9771,10 @@ function maxLen3(values, floor) {
|
|
|
9426
9771
|
|
|
9427
9772
|
// src/commands/verify.ts
|
|
9428
9773
|
import {
|
|
9429
|
-
assertBasouRootSafe as
|
|
9430
|
-
basouPaths as
|
|
9774
|
+
assertBasouRootSafe as assertBasouRootSafe17,
|
|
9775
|
+
basouPaths as basouPaths20,
|
|
9431
9776
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
9432
|
-
findErrorCode as
|
|
9777
|
+
findErrorCode as findErrorCode16,
|
|
9433
9778
|
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
9434
9779
|
resolveSessionId as resolveSessionId5,
|
|
9435
9780
|
verifyEventsChain
|
|
@@ -9453,8 +9798,8 @@ async function doRunVerify(options, ctx) {
|
|
|
9453
9798
|
}
|
|
9454
9799
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9455
9800
|
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
9456
|
-
const paths =
|
|
9457
|
-
await
|
|
9801
|
+
const paths = basouPaths20(repositoryRoot);
|
|
9802
|
+
await assertWorkspaceInitialized14(paths.root);
|
|
9458
9803
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
9459
9804
|
const rows = [];
|
|
9460
9805
|
for (const sessionId of sessionIds) {
|
|
@@ -9511,11 +9856,11 @@ async function resolveRepositoryRootForVerify(cwd) {
|
|
|
9511
9856
|
throw error;
|
|
9512
9857
|
}
|
|
9513
9858
|
}
|
|
9514
|
-
async function
|
|
9859
|
+
async function assertWorkspaceInitialized14(basouRoot) {
|
|
9515
9860
|
try {
|
|
9516
|
-
await
|
|
9861
|
+
await assertBasouRootSafe17(basouRoot);
|
|
9517
9862
|
} catch (error) {
|
|
9518
|
-
if (
|
|
9863
|
+
if (findErrorCode16(error, "ENOENT")) {
|
|
9519
9864
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
9520
9865
|
}
|
|
9521
9866
|
throw error;
|
|
@@ -9525,12 +9870,12 @@ async function assertWorkspaceInitialized13(basouRoot) {
|
|
|
9525
9870
|
// src/commands/view.ts
|
|
9526
9871
|
import { spawn } from "child_process";
|
|
9527
9872
|
import { createHash } from "crypto";
|
|
9528
|
-
import { basename as
|
|
9873
|
+
import { basename as basename8, resolve as resolve13 } from "path";
|
|
9529
9874
|
import {
|
|
9530
|
-
assertBasouRootSafe as
|
|
9531
|
-
basouPaths as
|
|
9532
|
-
findErrorCode as
|
|
9533
|
-
readManifest as
|
|
9875
|
+
assertBasouRootSafe as assertBasouRootSafe18,
|
|
9876
|
+
basouPaths as basouPaths21,
|
|
9877
|
+
findErrorCode as findErrorCode18,
|
|
9878
|
+
readManifest as readManifest14,
|
|
9534
9879
|
resolveRepositoryRoot as resolveRepositoryRoot14
|
|
9535
9880
|
} from "@basou/core";
|
|
9536
9881
|
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
@@ -9538,9 +9883,9 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
|
9538
9883
|
// src/lib/portfolio-safety.ts
|
|
9539
9884
|
import { execFile } from "child_process";
|
|
9540
9885
|
import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
|
|
9541
|
-
import { isAbsolute as isAbsolute7, join as
|
|
9886
|
+
import { isAbsolute as isAbsolute7, join as join16, relative as relative4, resolve as resolve11 } from "path";
|
|
9542
9887
|
import { promisify } from "util";
|
|
9543
|
-
import { readManifest as
|
|
9888
|
+
import { readManifest as readManifest12 } from "@basou/core";
|
|
9544
9889
|
var execFileAsync = promisify(execFile);
|
|
9545
9890
|
function errorCode(error) {
|
|
9546
9891
|
return error instanceof Error ? error.code : void 0;
|
|
@@ -9549,7 +9894,7 @@ async function canonical(p) {
|
|
|
9549
9894
|
try {
|
|
9550
9895
|
return await realpath2(p);
|
|
9551
9896
|
} catch {
|
|
9552
|
-
return
|
|
9897
|
+
return resolve11(p);
|
|
9553
9898
|
}
|
|
9554
9899
|
}
|
|
9555
9900
|
function isInside(child, parent) {
|
|
@@ -9562,7 +9907,7 @@ function isBasouPath(p) {
|
|
|
9562
9907
|
async function inspectRepo(repoPath) {
|
|
9563
9908
|
let hasEntry = false;
|
|
9564
9909
|
try {
|
|
9565
|
-
await lstat2(
|
|
9910
|
+
await lstat2(join16(repoPath, ".basou"));
|
|
9566
9911
|
hasEntry = true;
|
|
9567
9912
|
} catch (error) {
|
|
9568
9913
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -9593,7 +9938,7 @@ async function checkPortfolioSafety(workspaces) {
|
|
|
9593
9938
|
const wsReal = await canonical(ws.repoRoot);
|
|
9594
9939
|
let sourceRoots = [];
|
|
9595
9940
|
try {
|
|
9596
|
-
const manifest = await
|
|
9941
|
+
const manifest = await readManifest12(ws.paths);
|
|
9597
9942
|
sourceRoots = manifest.import?.source_roots ?? [];
|
|
9598
9943
|
} catch (error) {
|
|
9599
9944
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
@@ -9611,7 +9956,7 @@ async function checkPortfolioSafety(workspaces) {
|
|
|
9611
9956
|
}
|
|
9612
9957
|
const monitored = /* @__PURE__ */ new Map();
|
|
9613
9958
|
for (const root of sourceRoots) {
|
|
9614
|
-
const display =
|
|
9959
|
+
const display = resolve11(ws.repoRoot, root);
|
|
9615
9960
|
const real = await canonical(display);
|
|
9616
9961
|
if (real !== wsReal) monitored.set(real, display);
|
|
9617
9962
|
}
|
|
@@ -9663,25 +10008,58 @@ function formatSafetyReport(result) {
|
|
|
9663
10008
|
|
|
9664
10009
|
// src/lib/view-server.ts
|
|
9665
10010
|
import { createServer } from "http";
|
|
9666
|
-
import { join as
|
|
10011
|
+
import { basename as basename7, join as join17, resolve as resolve12 } from "path";
|
|
9667
10012
|
import {
|
|
9668
10013
|
computeWorkStats as computeWorkStats2,
|
|
9669
10014
|
enumerateApprovals as enumerateApprovals2,
|
|
9670
|
-
findErrorCode as
|
|
10015
|
+
findErrorCode as findErrorCode17,
|
|
9671
10016
|
isLazyExpired as isLazyExpired2,
|
|
9672
10017
|
loadApproval as loadApproval2,
|
|
9673
10018
|
loadSessionEntries as loadSessionEntries4,
|
|
9674
10019
|
loadTaskEntries as loadTaskEntries2,
|
|
9675
10020
|
readAllEvents as readAllEvents2,
|
|
9676
|
-
readManifest as
|
|
9677
|
-
readMarkdownFile as
|
|
10021
|
+
readManifest as readManifest13,
|
|
10022
|
+
readMarkdownFile as readMarkdownFile7,
|
|
9678
10023
|
readSessionYaml as readSessionYaml3,
|
|
9679
10024
|
readTaskFile as readTaskFile2,
|
|
9680
10025
|
renderDecisions as renderDecisions3,
|
|
9681
10026
|
renderHandoff as renderHandoff3,
|
|
9682
|
-
summarizeOrientation
|
|
10027
|
+
summarizeOrientation,
|
|
10028
|
+
tryRemoteUrl
|
|
9683
10029
|
} from "@basou/core";
|
|
9684
10030
|
|
|
10031
|
+
// src/lib/repo-url.ts
|
|
10032
|
+
function toBrowserUrl(remote) {
|
|
10033
|
+
const raw = remote.trim();
|
|
10034
|
+
if (raw.length === 0) return null;
|
|
10035
|
+
let host;
|
|
10036
|
+
let path;
|
|
10037
|
+
if (raw.includes("://")) {
|
|
10038
|
+
let parsed;
|
|
10039
|
+
try {
|
|
10040
|
+
parsed = new URL(raw);
|
|
10041
|
+
} catch {
|
|
10042
|
+
return null;
|
|
10043
|
+
}
|
|
10044
|
+
const scheme = parsed.protocol;
|
|
10045
|
+
if (scheme !== "ssh:" && scheme !== "git:" && scheme !== "http:" && scheme !== "https:") {
|
|
10046
|
+
return null;
|
|
10047
|
+
}
|
|
10048
|
+
host = scheme === "http:" || scheme === "https:" ? parsed.host : parsed.hostname;
|
|
10049
|
+
path = parsed.pathname;
|
|
10050
|
+
} else {
|
|
10051
|
+
const match = /^[^@/\s]+@([^:/\s@]+):(.+)$/.exec(raw);
|
|
10052
|
+
if (match === null || match[1] === void 0 || match[2] === void 0) return null;
|
|
10053
|
+
host = match[1];
|
|
10054
|
+
path = match[2];
|
|
10055
|
+
}
|
|
10056
|
+
const cleanPath = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "");
|
|
10057
|
+
if (host.length === 0 || cleanPath.length === 0) return null;
|
|
10058
|
+
if (/\s/.test(host) || /\s/.test(cleanPath)) return null;
|
|
10059
|
+
if (cleanPath.split("/").some((seg) => seg === "." || seg === "..")) return null;
|
|
10060
|
+
return `https://${host}/${cleanPath}`;
|
|
10061
|
+
}
|
|
10062
|
+
|
|
9685
10063
|
// src/lib/view-ui.ts
|
|
9686
10064
|
var VIEW_HTML = `<!doctype html>
|
|
9687
10065
|
<html lang="en">
|
|
@@ -10083,9 +10461,30 @@ var VIEW_HTML = `<!doctype html>
|
|
|
10083
10461
|
card(c.approvalsPending, 'approvals pending')
|
|
10084
10462
|
]);
|
|
10085
10463
|
detail.appendChild(cards);
|
|
10464
|
+
renderRepos(detail, d.repos || []);
|
|
10086
10465
|
detail.appendChild(el('p', { class: 'muted', text: 'repo: ' + d.repoRoot }));
|
|
10087
10466
|
}).catch(fail);
|
|
10088
10467
|
}
|
|
10468
|
+
// The declared roster repos, each with a LIVE git link (derived server-side
|
|
10469
|
+
// from the repo's local git config at request time, never stored). A repo
|
|
10470
|
+
// with no remote renders as "local only". Links open in a new tab.
|
|
10471
|
+
function renderRepos(detail, repos) {
|
|
10472
|
+
if (!repos.length) return;
|
|
10473
|
+
detail.appendChild(el('h3', { text: 'Repos' }));
|
|
10474
|
+
var rows = el('div', { class: 'repos' }, []);
|
|
10475
|
+
repos.forEach(function (r) {
|
|
10476
|
+
var vis = r.visibility ? (' (' + r.visibility + ')') : '';
|
|
10477
|
+
var link = r.url
|
|
10478
|
+
? el('a', { href: r.url, target: '_blank', rel: 'noopener noreferrer', text: r.url })
|
|
10479
|
+
: el('span', { class: 'muted', text: 'local only' });
|
|
10480
|
+
rows.appendChild(el('div', { class: 'f' }, [
|
|
10481
|
+
el('strong', { text: r.name }),
|
|
10482
|
+
el('span', { class: 'muted', text: vis + ' ' }),
|
|
10483
|
+
link
|
|
10484
|
+
]));
|
|
10485
|
+
});
|
|
10486
|
+
detail.appendChild(rows);
|
|
10487
|
+
}
|
|
10089
10488
|
function card(n, label) {
|
|
10090
10489
|
return el('div', { class: 'card' }, [
|
|
10091
10490
|
el('div', { class: 'n', text: String(n) }),
|
|
@@ -10324,7 +10723,7 @@ function startViewServer(opts) {
|
|
|
10324
10723
|
};
|
|
10325
10724
|
let boundPort = port;
|
|
10326
10725
|
const getPort = () => boundPort;
|
|
10327
|
-
return new Promise((
|
|
10726
|
+
return new Promise((resolve14, reject) => {
|
|
10328
10727
|
const server = createServer((req, res) => {
|
|
10329
10728
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
10330
10729
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -10335,7 +10734,7 @@ function startViewServer(opts) {
|
|
|
10335
10734
|
const address = server.address();
|
|
10336
10735
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
10337
10736
|
server.off("error", reject);
|
|
10338
|
-
|
|
10737
|
+
resolve14({
|
|
10339
10738
|
url: `http://${host}:${boundPort}`,
|
|
10340
10739
|
port: boundPort,
|
|
10341
10740
|
close: () => closeServer(server)
|
|
@@ -10347,8 +10746,8 @@ function isAddressInfo(value) {
|
|
|
10347
10746
|
return value !== null && typeof value === "object";
|
|
10348
10747
|
}
|
|
10349
10748
|
function closeServer(server) {
|
|
10350
|
-
return new Promise((
|
|
10351
|
-
server.close(() =>
|
|
10749
|
+
return new Promise((resolve14) => {
|
|
10750
|
+
server.close(() => resolve14());
|
|
10352
10751
|
server.closeAllConnections();
|
|
10353
10752
|
});
|
|
10354
10753
|
}
|
|
@@ -10391,20 +10790,29 @@ async function handleGet(res, pathname, deps) {
|
|
|
10391
10790
|
sendError(res, 404, "Unknown workspace");
|
|
10392
10791
|
return;
|
|
10393
10792
|
}
|
|
10394
|
-
if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
|
|
10793
|
+
if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider, remoteUrlOf(deps))) {
|
|
10395
10794
|
sendError(res, 404, "Not found");
|
|
10396
10795
|
}
|
|
10397
10796
|
return;
|
|
10398
10797
|
}
|
|
10399
10798
|
if (pathname.startsWith(API_PREFIX)) {
|
|
10400
10799
|
const sub = pathname.slice(API_PREFIX.length);
|
|
10401
|
-
if (!await handleWorkspaceGet(
|
|
10800
|
+
if (!await handleWorkspaceGet(
|
|
10801
|
+
res,
|
|
10802
|
+
sub,
|
|
10803
|
+
primaryWorkspace(deps),
|
|
10804
|
+
deps.nowProvider,
|
|
10805
|
+
remoteUrlOf(deps)
|
|
10806
|
+
)) {
|
|
10402
10807
|
sendError(res, 404, "Not found");
|
|
10403
10808
|
}
|
|
10404
10809
|
return;
|
|
10405
10810
|
}
|
|
10406
10811
|
sendError(res, 404, "Not found");
|
|
10407
10812
|
}
|
|
10813
|
+
function remoteUrlOf(deps) {
|
|
10814
|
+
return deps.remoteUrlOf ?? tryRemoteUrl;
|
|
10815
|
+
}
|
|
10408
10816
|
async function handlePost(res, pathname, body, deps, runExclusive) {
|
|
10409
10817
|
const scoped = matchWsRoute(pathname);
|
|
10410
10818
|
if (scoped !== null) {
|
|
@@ -10427,9 +10835,9 @@ async function handlePost(res, pathname, body, deps, runExclusive) {
|
|
|
10427
10835
|
}
|
|
10428
10836
|
sendError(res, 404, "Not found");
|
|
10429
10837
|
}
|
|
10430
|
-
async function handleWorkspaceGet(res, sub, ws, nowProvider) {
|
|
10838
|
+
async function handleWorkspaceGet(res, sub, ws, nowProvider, resolveRemoteUrl) {
|
|
10431
10839
|
if (sub === "overview") {
|
|
10432
|
-
sendJson(res, 200, await overview(ws, nowProvider));
|
|
10840
|
+
sendJson(res, 200, await overview(ws, nowProvider, resolveRemoteUrl));
|
|
10433
10841
|
return true;
|
|
10434
10842
|
}
|
|
10435
10843
|
if (sub === "sessions") {
|
|
@@ -10562,12 +10970,12 @@ async function captureStaleness(ws, nowIso) {
|
|
|
10562
10970
|
const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
|
|
10563
10971
|
return probe === null ? { checked: false } : { checked: true, ...probe };
|
|
10564
10972
|
}
|
|
10565
|
-
async function overview(ws, nowProvider) {
|
|
10973
|
+
async function overview(ws, nowProvider, resolveRemoteUrl) {
|
|
10566
10974
|
let manifest;
|
|
10567
10975
|
try {
|
|
10568
|
-
manifest = await
|
|
10976
|
+
manifest = await readManifest13(ws.paths);
|
|
10569
10977
|
} catch (error) {
|
|
10570
|
-
if (
|
|
10978
|
+
if (findErrorCode17(error, "ENOENT")) {
|
|
10571
10979
|
return { initialized: false, repoRoot: ws.repoRoot };
|
|
10572
10980
|
}
|
|
10573
10981
|
throw error;
|
|
@@ -10575,6 +10983,7 @@ async function overview(ws, nowProvider) {
|
|
|
10575
10983
|
const nowIso = nowProvider().toISOString();
|
|
10576
10984
|
const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
|
|
10577
10985
|
const approvals = await enumerateApprovals2(ws.paths);
|
|
10986
|
+
const repos = await rosterRepos(ws.repoRoot, manifest, resolveRemoteUrl);
|
|
10578
10987
|
return {
|
|
10579
10988
|
initialized: true,
|
|
10580
10989
|
repoRoot: ws.repoRoot,
|
|
@@ -10592,9 +11001,26 @@ async function overview(ws, nowProvider) {
|
|
|
10592
11001
|
approvalsPending: approvals.pending.length,
|
|
10593
11002
|
approvalsResolved: approvals.resolved.length
|
|
10594
11003
|
},
|
|
11004
|
+
repos,
|
|
10595
11005
|
generatedAt: nowIso
|
|
10596
11006
|
};
|
|
10597
11007
|
}
|
|
11008
|
+
async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
|
|
11009
|
+
const roster = manifest.repos ?? [];
|
|
11010
|
+
return Promise.all(
|
|
11011
|
+
roster.map(async (repo) => {
|
|
11012
|
+
const abs = resolve12(repoRoot, repo.path);
|
|
11013
|
+
const remote = await resolveRemoteUrl(abs);
|
|
11014
|
+
const url = remote !== void 0 ? toBrowserUrl(remote) : null;
|
|
11015
|
+
return {
|
|
11016
|
+
name: basename7(abs),
|
|
11017
|
+
path: repo.path,
|
|
11018
|
+
...url !== null ? { url } : {},
|
|
11019
|
+
...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
|
|
11020
|
+
};
|
|
11021
|
+
})
|
|
11022
|
+
);
|
|
11023
|
+
}
|
|
10598
11024
|
async function sessionsList(ws, nowProvider) {
|
|
10599
11025
|
const entries = await loadSessionEntries4(ws.paths, { now: nowProvider() });
|
|
10600
11026
|
const sessions = entries.map((entry) => ({
|
|
@@ -10622,7 +11048,7 @@ async function sessionDetail(ws, sessionId) {
|
|
|
10622
11048
|
throw error;
|
|
10623
11049
|
}
|
|
10624
11050
|
try {
|
|
10625
|
-
const events = await readAllEvents2(
|
|
11051
|
+
const events = await readAllEvents2(join17(ws.paths.sessions, sessionId));
|
|
10626
11052
|
return { session, events };
|
|
10627
11053
|
} catch {
|
|
10628
11054
|
return { session, events: [], degraded: true };
|
|
@@ -10644,7 +11070,7 @@ async function taskDetail(ws, taskId) {
|
|
|
10644
11070
|
}
|
|
10645
11071
|
}
|
|
10646
11072
|
async function decisionsView(ws, nowProvider) {
|
|
10647
|
-
const fromDisk = await
|
|
11073
|
+
const fromDisk = await readMarkdownFile7(ws.paths.files.decisions);
|
|
10648
11074
|
if (fromDisk !== null) {
|
|
10649
11075
|
return { body: fromDisk, fromDisk: true };
|
|
10650
11076
|
}
|
|
@@ -10667,7 +11093,7 @@ async function approvalsView(ws, nowProvider) {
|
|
|
10667
11093
|
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
10668
11094
|
}
|
|
10669
11095
|
async function handoffView(ws, nowProvider) {
|
|
10670
|
-
const fromDisk = await
|
|
11096
|
+
const fromDisk = await readMarkdownFile7(ws.paths.files.handoff);
|
|
10671
11097
|
if (fromDisk !== null) {
|
|
10672
11098
|
return { body: fromDisk, fromDisk: true };
|
|
10673
11099
|
}
|
|
@@ -10836,18 +11262,23 @@ async function doRunView(options, ctx) {
|
|
|
10836
11262
|
}
|
|
10837
11263
|
async function buildSingleDeps(ctx, cwd) {
|
|
10838
11264
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
10839
|
-
const paths =
|
|
10840
|
-
await
|
|
11265
|
+
const paths = basouPaths21(repositoryRoot);
|
|
11266
|
+
await assertWorkspaceInitialized15(paths.root);
|
|
10841
11267
|
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
10842
|
-
return {
|
|
11268
|
+
return {
|
|
11269
|
+
workspaces: [entry],
|
|
11270
|
+
mode: "single",
|
|
11271
|
+
nowProvider: nowProviderOf(ctx),
|
|
11272
|
+
...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
|
|
11273
|
+
};
|
|
10843
11274
|
}
|
|
10844
11275
|
async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
10845
|
-
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path:
|
|
11276
|
+
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve13(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
10846
11277
|
const entries = [];
|
|
10847
11278
|
const seenPath = /* @__PURE__ */ new Set();
|
|
10848
11279
|
const seenKey = /* @__PURE__ */ new Set();
|
|
10849
11280
|
for (const spec of specs) {
|
|
10850
|
-
const repoRoot =
|
|
11281
|
+
const repoRoot = resolve13(spec.path);
|
|
10851
11282
|
if (seenPath.has(repoRoot)) continue;
|
|
10852
11283
|
seenPath.add(repoRoot);
|
|
10853
11284
|
const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
|
|
@@ -10857,17 +11288,22 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
|
10857
11288
|
entries.push({ ...entry, key });
|
|
10858
11289
|
}
|
|
10859
11290
|
if (entries.length === 0) throw new Error("No workspaces to show.");
|
|
10860
|
-
return {
|
|
11291
|
+
return {
|
|
11292
|
+
workspaces: entries,
|
|
11293
|
+
mode: "portfolio",
|
|
11294
|
+
nowProvider: nowProviderOf(ctx),
|
|
11295
|
+
...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
|
|
11296
|
+
};
|
|
10861
11297
|
}
|
|
10862
11298
|
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
10863
|
-
const paths =
|
|
11299
|
+
const paths = basouPaths21(repoRoot);
|
|
10864
11300
|
const importCtx = {
|
|
10865
11301
|
cwd: repoRoot,
|
|
10866
11302
|
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
10867
11303
|
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
10868
11304
|
};
|
|
10869
11305
|
try {
|
|
10870
|
-
const manifest = await
|
|
11306
|
+
const manifest = await readManifest14(paths);
|
|
10871
11307
|
return {
|
|
10872
11308
|
key: manifest.workspace.id,
|
|
10873
11309
|
label: labelOverride ?? manifest.workspace.name,
|
|
@@ -10880,7 +11316,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
|
10880
11316
|
const notFound = error instanceof Error && error.message === "YAML file not found";
|
|
10881
11317
|
return {
|
|
10882
11318
|
key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
|
|
10883
|
-
label: labelOverride ??
|
|
11319
|
+
label: labelOverride ?? basename8(repoRoot),
|
|
10884
11320
|
paths,
|
|
10885
11321
|
repoRoot,
|
|
10886
11322
|
importCtx,
|
|
@@ -10896,7 +11332,7 @@ async function startListening(port, deps) {
|
|
|
10896
11332
|
try {
|
|
10897
11333
|
return await startViewServer({ port, deps });
|
|
10898
11334
|
} catch (error) {
|
|
10899
|
-
if (
|
|
11335
|
+
if (findErrorCode18(error, "EADDRINUSE")) {
|
|
10900
11336
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
10901
11337
|
cause: error
|
|
10902
11338
|
});
|
|
@@ -10919,7 +11355,7 @@ function openInBrowser(url, override) {
|
|
|
10919
11355
|
}
|
|
10920
11356
|
}
|
|
10921
11357
|
function waitForShutdown(signal) {
|
|
10922
|
-
return new Promise((
|
|
11358
|
+
return new Promise((resolve14) => {
|
|
10923
11359
|
const cleanup = () => {
|
|
10924
11360
|
process.off("SIGINT", onSignal);
|
|
10925
11361
|
process.off("SIGTERM", onSignal);
|
|
@@ -10927,18 +11363,18 @@ function waitForShutdown(signal) {
|
|
|
10927
11363
|
};
|
|
10928
11364
|
const onSignal = () => {
|
|
10929
11365
|
cleanup();
|
|
10930
|
-
|
|
11366
|
+
resolve14();
|
|
10931
11367
|
};
|
|
10932
11368
|
const onAbort = () => {
|
|
10933
11369
|
cleanup();
|
|
10934
|
-
|
|
11370
|
+
resolve14();
|
|
10935
11371
|
};
|
|
10936
11372
|
process.on("SIGINT", onSignal);
|
|
10937
11373
|
process.on("SIGTERM", onSignal);
|
|
10938
11374
|
if (signal !== void 0) {
|
|
10939
11375
|
if (signal.aborted) {
|
|
10940
11376
|
cleanup();
|
|
10941
|
-
|
|
11377
|
+
resolve14();
|
|
10942
11378
|
return;
|
|
10943
11379
|
}
|
|
10944
11380
|
signal.addEventListener("abort", onAbort);
|
|
@@ -10957,11 +11393,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
10957
11393
|
throw error;
|
|
10958
11394
|
}
|
|
10959
11395
|
}
|
|
10960
|
-
async function
|
|
11396
|
+
async function assertWorkspaceInitialized15(basouRoot) {
|
|
10961
11397
|
try {
|
|
10962
|
-
await
|
|
11398
|
+
await assertBasouRootSafe18(basouRoot);
|
|
10963
11399
|
} catch (error) {
|
|
10964
|
-
if (
|
|
11400
|
+
if (findErrorCode18(error, "ENOENT")) {
|
|
10965
11401
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
10966
11402
|
}
|
|
10967
11403
|
throw error;
|
|
@@ -10993,6 +11429,7 @@ function buildProgram() {
|
|
|
10993
11429
|
registerDecisionsCommand(program2);
|
|
10994
11430
|
registerReportCommand(program2);
|
|
10995
11431
|
registerOrientCommand(program2);
|
|
11432
|
+
registerReviewCommand(program2);
|
|
10996
11433
|
registerReviewGapsCommand(program2);
|
|
10997
11434
|
registerProjectCommand(program2);
|
|
10998
11435
|
registerProtocolCommand(program2);
|