@basou/cli 0.40.0 → 0.42.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 +360 -114
- package/dist/index.js.map +1 -1
- package/dist/program.js +360 -114
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -77,6 +77,18 @@ function printReplayWarning(warning, sessionId) {
|
|
|
77
77
|
`Warning: skipped invalid event at line ${warning.line} in ${short}/events.jsonl`
|
|
78
78
|
);
|
|
79
79
|
break;
|
|
80
|
+
case "retired_zero_duration":
|
|
81
|
+
console.error(
|
|
82
|
+
`Warning: kept an event with duration_ms: 0 at line ${warning.line} in ${short}/events.jsonl \u2014 no writer at that schema_version emits one; reading it as unobserved`
|
|
83
|
+
);
|
|
84
|
+
break;
|
|
85
|
+
default: {
|
|
86
|
+
const unhandled = warning;
|
|
87
|
+
console.error(
|
|
88
|
+
`Warning: unhandled replay warning in ${short}/events.jsonl: ${String(unhandled.kind)}`
|
|
89
|
+
);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
function printSessionSkip(sid, reason) {
|
|
@@ -119,6 +131,7 @@ import {
|
|
|
119
131
|
appendChainedEventLocked,
|
|
120
132
|
assertBasouRootSafe,
|
|
121
133
|
basouPaths,
|
|
134
|
+
EVENT_SCHEMA_VERSION,
|
|
122
135
|
enumerateApprovals,
|
|
123
136
|
findErrorCode,
|
|
124
137
|
isLazyExpired,
|
|
@@ -352,7 +365,7 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
|
|
|
352
365
|
if (decision === "approve") {
|
|
353
366
|
const note = options.note ?? null;
|
|
354
367
|
await appendChainedEventLocked(paths, approval.session_id, {
|
|
355
|
-
schema_version:
|
|
368
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
356
369
|
id: eventId,
|
|
357
370
|
session_id: approval.session_id,
|
|
358
371
|
occurred_at: occurredAt,
|
|
@@ -365,7 +378,7 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
|
|
|
365
378
|
} else {
|
|
366
379
|
const reason = options.reason;
|
|
367
380
|
await appendChainedEventLocked(paths, approval.session_id, {
|
|
368
|
-
schema_version:
|
|
381
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
369
382
|
id: eventId,
|
|
370
383
|
session_id: approval.session_id,
|
|
371
384
|
occurred_at: occurredAt,
|
|
@@ -710,8 +723,8 @@ ${block}${markers.end}
|
|
|
710
723
|
return `${section.before}${markers.start}
|
|
711
724
|
${block}${markers.end}${section.after}`;
|
|
712
725
|
case "no_markers": {
|
|
713
|
-
const
|
|
714
|
-
return `${existing}${
|
|
726
|
+
const sep2 = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
727
|
+
return `${existing}${sep2}${wrapped}`;
|
|
715
728
|
}
|
|
716
729
|
default:
|
|
717
730
|
throw new Error(
|
|
@@ -851,6 +864,7 @@ import {
|
|
|
851
864
|
basouPaths as basouPaths3,
|
|
852
865
|
classifyFilesBySourceRoot,
|
|
853
866
|
createAdHocSessionWithEvent,
|
|
867
|
+
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION2,
|
|
854
868
|
findErrorCode as findErrorCode2,
|
|
855
869
|
isValidPrefixedId,
|
|
856
870
|
loadSessionEntries,
|
|
@@ -1400,7 +1414,7 @@ async function decisionExists(paths, decisionId) {
|
|
|
1400
1414
|
}
|
|
1401
1415
|
function buildDecisionVoidedEvent(input) {
|
|
1402
1416
|
return {
|
|
1403
|
-
schema_version:
|
|
1417
|
+
schema_version: EVENT_SCHEMA_VERSION2,
|
|
1404
1418
|
id: input.eventId,
|
|
1405
1419
|
session_id: input.sessionId,
|
|
1406
1420
|
occurred_at: input.occurredAt,
|
|
@@ -1658,7 +1672,7 @@ function pickRichFields(options) {
|
|
|
1658
1672
|
}
|
|
1659
1673
|
function buildDecisionEvent(input) {
|
|
1660
1674
|
return {
|
|
1661
|
-
schema_version:
|
|
1675
|
+
schema_version: EVENT_SCHEMA_VERSION2,
|
|
1662
1676
|
id: input.eventId,
|
|
1663
1677
|
session_id: input.sessionId,
|
|
1664
1678
|
occurred_at: input.occurredAt,
|
|
@@ -1855,6 +1869,7 @@ import {
|
|
|
1855
1869
|
basouPaths as basouPaths5,
|
|
1856
1870
|
ChildProcessRunner,
|
|
1857
1871
|
appendChainedEvent as coreAppendChainedEvent,
|
|
1872
|
+
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION3,
|
|
1858
1873
|
finalizeSessionYaml,
|
|
1859
1874
|
getSnapshot,
|
|
1860
1875
|
overwriteYamlFile,
|
|
@@ -1865,6 +1880,7 @@ import {
|
|
|
1865
1880
|
resolveRepositoryRoot as resolveRepositoryRoot4,
|
|
1866
1881
|
SessionSchema,
|
|
1867
1882
|
sanitizeWorkingDirectory,
|
|
1883
|
+
writeObservedDuration,
|
|
1868
1884
|
writeYamlFile
|
|
1869
1885
|
} from "@basou/core";
|
|
1870
1886
|
function registerExecCommand(program2) {
|
|
@@ -1905,7 +1921,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1905
1921
|
});
|
|
1906
1922
|
await writeYamlFile(sessionYamlPath, session);
|
|
1907
1923
|
await appendEvent(sessionDir, {
|
|
1908
|
-
schema_version:
|
|
1924
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
1909
1925
|
type: "session_started",
|
|
1910
1926
|
id: prefixedUlid3("evt"),
|
|
1911
1927
|
session_id: sessionId,
|
|
@@ -1917,7 +1933,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1917
1933
|
}
|
|
1918
1934
|
const runningAt = now().toISOString();
|
|
1919
1935
|
await appendEvent(sessionDir, {
|
|
1920
|
-
schema_version:
|
|
1936
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
1921
1937
|
type: "session_status_changed",
|
|
1922
1938
|
id: prefixedUlid3("evt"),
|
|
1923
1939
|
session_id: sessionId,
|
|
@@ -1986,7 +2002,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1986
2002
|
}
|
|
1987
2003
|
const endedAt = now().toISOString();
|
|
1988
2004
|
await appendEvent(sessionDir, {
|
|
1989
|
-
schema_version:
|
|
2005
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
1990
2006
|
type: "command_executed",
|
|
1991
2007
|
id: prefixedUlid3("evt"),
|
|
1992
2008
|
session_id: sessionId,
|
|
@@ -1998,14 +2014,21 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1998
2014
|
exit_code: result.exit_code,
|
|
1999
2015
|
...result.signal !== null ? { signal: result.signal } : {},
|
|
2000
2016
|
...signalReceived !== null ? { received_signal: signalReceived } : {},
|
|
2001
|
-
|
|
2017
|
+
// Reached only when the child actually ran, and the runner times it on a
|
|
2018
|
+
// monotonic sub-millisecond clock, so the cheapest spawn on this host
|
|
2019
|
+
// (`/usr/bin/true`, median 0.83ms) still rounds to 1ms. A non-positive
|
|
2020
|
+
// value therefore means the measurement is unusable, not that the command
|
|
2021
|
+
// was instant, and is recorded as unobserved. A spawn that never ran
|
|
2022
|
+
// (ENOENT) does not reach this line at all: the runner rejects and
|
|
2023
|
+
// `finalizeSessionAsFailed` writes its own null.
|
|
2024
|
+
duration_ms: writeObservedDuration(result.duration_ms)
|
|
2002
2025
|
});
|
|
2003
2026
|
if (options.snapshot !== false) {
|
|
2004
2027
|
await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent);
|
|
2005
2028
|
}
|
|
2006
2029
|
const finalStatus = decideFinalStatus(result, signalReceived);
|
|
2007
2030
|
await appendEvent(sessionDir, {
|
|
2008
|
-
schema_version:
|
|
2031
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
2009
2032
|
type: "session_status_changed",
|
|
2010
2033
|
id: prefixedUlid3("evt"),
|
|
2011
2034
|
session_id: sessionId,
|
|
@@ -2015,7 +2038,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
2015
2038
|
to: finalStatus
|
|
2016
2039
|
});
|
|
2017
2040
|
await appendEvent(sessionDir, {
|
|
2018
|
-
schema_version:
|
|
2041
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
2019
2042
|
type: "session_ended",
|
|
2020
2043
|
id: prefixedUlid3("evt"),
|
|
2021
2044
|
session_id: sessionId,
|
|
@@ -2062,7 +2085,7 @@ async function tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, append
|
|
|
2062
2085
|
return;
|
|
2063
2086
|
}
|
|
2064
2087
|
await appendEvent(sessionDir, {
|
|
2065
|
-
schema_version:
|
|
2088
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
2066
2089
|
type: "git_snapshot",
|
|
2067
2090
|
id: prefixedUlid3("evt"),
|
|
2068
2091
|
session_id: sessionId,
|
|
@@ -2119,7 +2142,7 @@ async function mutateSessionYaml(filePath, mutator) {
|
|
|
2119
2142
|
}
|
|
2120
2143
|
async function finalizeSessionAsFailed(paths, sessionDir, sessionId, appendEvent, ctx) {
|
|
2121
2144
|
await appendEvent(sessionDir, {
|
|
2122
|
-
schema_version:
|
|
2145
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
2123
2146
|
type: "command_executed",
|
|
2124
2147
|
id: prefixedUlid3("evt"),
|
|
2125
2148
|
session_id: sessionId,
|
|
@@ -2131,10 +2154,12 @@ async function finalizeSessionAsFailed(paths, sessionDir, sessionId, appendEvent
|
|
|
2131
2154
|
exit_code: null,
|
|
2132
2155
|
signal: null,
|
|
2133
2156
|
...ctx.signalReceived !== null ? { received_signal: ctx.signalReceived } : {},
|
|
2134
|
-
|
|
2157
|
+
// Not observed: this event stands in for a run that ended before a duration
|
|
2158
|
+
// could be measured, so 0 would claim a measured zero.
|
|
2159
|
+
duration_ms: null
|
|
2135
2160
|
});
|
|
2136
2161
|
await appendEvent(sessionDir, {
|
|
2137
|
-
schema_version:
|
|
2162
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
2138
2163
|
type: "session_status_changed",
|
|
2139
2164
|
id: prefixedUlid3("evt"),
|
|
2140
2165
|
session_id: sessionId,
|
|
@@ -2144,7 +2169,7 @@ async function finalizeSessionAsFailed(paths, sessionDir, sessionId, appendEvent
|
|
|
2144
2169
|
to: "failed"
|
|
2145
2170
|
});
|
|
2146
2171
|
await appendEvent(sessionDir, {
|
|
2147
|
-
schema_version:
|
|
2172
|
+
schema_version: EVENT_SCHEMA_VERSION3,
|
|
2148
2173
|
type: "session_ended",
|
|
2149
2174
|
id: prefixedUlid3("evt"),
|
|
2150
2175
|
session_id: sessionId,
|
|
@@ -2241,7 +2266,7 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
2241
2266
|
}
|
|
2242
2267
|
|
|
2243
2268
|
// src/commands/hook.ts
|
|
2244
|
-
import { open as open2, readFile as readFile3, realpath as
|
|
2269
|
+
import { open as open2, readFile as readFile3, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
2245
2270
|
import { homedir as homedir7 } from "os";
|
|
2246
2271
|
import { join as join9 } from "path";
|
|
2247
2272
|
import { fileURLToPath } from "url";
|
|
@@ -2255,7 +2280,7 @@ import {
|
|
|
2255
2280
|
ORIENTATION_END as ORIENTATION_END2,
|
|
2256
2281
|
ORIENTATION_START as ORIENTATION_START2,
|
|
2257
2282
|
parseMarkers as parseMarkers2,
|
|
2258
|
-
readMarkdownFile as
|
|
2283
|
+
readMarkdownFile as readMarkdownFile6,
|
|
2259
2284
|
removeSessionStartHook,
|
|
2260
2285
|
removeStopHook,
|
|
2261
2286
|
upsertSessionStartHook,
|
|
@@ -2381,6 +2406,137 @@ function describeCodexHookTrust(trust) {
|
|
|
2381
2406
|
}
|
|
2382
2407
|
}
|
|
2383
2408
|
|
|
2409
|
+
// src/lib/foreign-workspace-warn.ts
|
|
2410
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
2411
|
+
import { dirname as dirname2 } from "path";
|
|
2412
|
+
import { readMarkdownFile as readMarkdownFile4 } from "@basou/core";
|
|
2413
|
+
|
|
2414
|
+
// src/lib/foreign-workspace-scan.ts
|
|
2415
|
+
import { homedir as osHomedir } from "os";
|
|
2416
|
+
import { basename as basename3, normalize, sep } from "path";
|
|
2417
|
+
var MIN_TOKEN_LENGTH = 4;
|
|
2418
|
+
function stripTrailingSep(p) {
|
|
2419
|
+
return p.length > 1 && p.endsWith(sep) ? p.slice(0, -sep.length) : p;
|
|
2420
|
+
}
|
|
2421
|
+
function toTildePath(absPath, home) {
|
|
2422
|
+
if (absPath === home) return "~";
|
|
2423
|
+
if (absPath.startsWith(home + sep)) return `~${absPath.slice(home.length)}`;
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
function encodedSpelling(name) {
|
|
2427
|
+
return name.replace(/[^a-zA-Z0-9]/g, "-");
|
|
2428
|
+
}
|
|
2429
|
+
function directoryNameSpellings(dir) {
|
|
2430
|
+
const paired = /^(.+)-(planning|workspace)$/.exec(dir);
|
|
2431
|
+
const plain = paired !== null ? [`${paired[1]}-planning`, `${paired[1]}-workspace`] : [dir, `${dir}-workspace`];
|
|
2432
|
+
return [...plain, ...plain.map(encodedSpelling)];
|
|
2433
|
+
}
|
|
2434
|
+
function nameTokensFor(workspacePath, home) {
|
|
2435
|
+
const abs = stripTrailingSep(normalize(workspacePath));
|
|
2436
|
+
const tokens = /* @__PURE__ */ new Set([abs]);
|
|
2437
|
+
const tilde = toTildePath(abs, home);
|
|
2438
|
+
if (tilde !== null) tokens.add(tilde);
|
|
2439
|
+
for (const name of directoryNameSpellings(basename3(abs))) tokens.add(name);
|
|
2440
|
+
return [...tokens].filter((t) => t.length >= MIN_TOKEN_LENGTH);
|
|
2441
|
+
}
|
|
2442
|
+
function isSpellingOfSelf(workspacePath, selfPath) {
|
|
2443
|
+
const own = new Set(directoryNameSpellings(basename3(stripTrailingSep(normalize(selfPath)))));
|
|
2444
|
+
return directoryNameSpellings(basename3(stripTrailingSep(normalize(workspacePath)))).some(
|
|
2445
|
+
(s) => own.has(s)
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
function scanForeignWorkspaceNames(input) {
|
|
2449
|
+
if (input.text.length === 0 || input.workspacePaths.length === 0) return [];
|
|
2450
|
+
const home = input.homedir ?? osHomedir();
|
|
2451
|
+
const own = input.selfPath === void 0 ? [] : nameTokensFor(input.selfPath, home);
|
|
2452
|
+
const lines = input.text.split("\n");
|
|
2453
|
+
const hits = [];
|
|
2454
|
+
for (const workspacePath of input.workspacePaths) {
|
|
2455
|
+
if (input.selfPath !== void 0 && isSpellingOfSelf(workspacePath, input.selfPath)) continue;
|
|
2456
|
+
const tokens = nameTokensFor(workspacePath, home).filter(
|
|
2457
|
+
(token) => !own.some((ownToken) => ownToken.includes(token))
|
|
2458
|
+
);
|
|
2459
|
+
if (tokens.length === 0) continue;
|
|
2460
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2461
|
+
const matchedLines = [];
|
|
2462
|
+
for (const [index, line] of lines.entries()) {
|
|
2463
|
+
let lineMatched = false;
|
|
2464
|
+
for (const token of tokens) {
|
|
2465
|
+
if (line.includes(token)) {
|
|
2466
|
+
matched.add(token);
|
|
2467
|
+
lineMatched = true;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (lineMatched) matchedLines.push(index + 1);
|
|
2471
|
+
}
|
|
2472
|
+
if (matchedLines.length === 0) continue;
|
|
2473
|
+
hits.push({
|
|
2474
|
+
workspacePath,
|
|
2475
|
+
tokens: [...matched].sort((a, b) => b.length - a.length || a.localeCompare(b)),
|
|
2476
|
+
lines: matchedLines
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
return hits;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// src/lib/foreign-workspace-warn.ts
|
|
2483
|
+
var MAX_LISTED_LINES = 5;
|
|
2484
|
+
async function canonicalize2(path) {
|
|
2485
|
+
try {
|
|
2486
|
+
return await realpath2(path);
|
|
2487
|
+
} catch {
|
|
2488
|
+
return path;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
async function findForeignWorkspaceNames(args) {
|
|
2492
|
+
let workspacePaths;
|
|
2493
|
+
let selfPath;
|
|
2494
|
+
try {
|
|
2495
|
+
const workspaces = await loadPortfolioConfig(args.configPath);
|
|
2496
|
+
workspacePaths = await Promise.all(workspaces.map((w) => canonicalize2(w.path)));
|
|
2497
|
+
selfPath = args.selfPath === void 0 ? void 0 : await canonicalize2(args.selfPath);
|
|
2498
|
+
} catch {
|
|
2499
|
+
return null;
|
|
2500
|
+
}
|
|
2501
|
+
const hits = scanForeignWorkspaceNames({
|
|
2502
|
+
text: args.text,
|
|
2503
|
+
workspacePaths,
|
|
2504
|
+
selfPath
|
|
2505
|
+
});
|
|
2506
|
+
if (hits.length === 0) return null;
|
|
2507
|
+
const lines = [...new Set(hits.flatMap((h) => h.lines))].sort((a, b) => a - b);
|
|
2508
|
+
return { workspaceCount: hits.length, lines };
|
|
2509
|
+
}
|
|
2510
|
+
function describeForeignWorkspaceLines(lines) {
|
|
2511
|
+
const listed = lines.slice(0, MAX_LISTED_LINES).join(", ");
|
|
2512
|
+
const rest = lines.length - MAX_LISTED_LINES;
|
|
2513
|
+
const numbers = rest > 0 ? `${listed} and ${rest} more` : listed;
|
|
2514
|
+
return `${lines.length === 1 ? "line" : "lines"} ${numbers}`;
|
|
2515
|
+
}
|
|
2516
|
+
function positionForeignWorkspaceWarning(report, where) {
|
|
2517
|
+
const subject = report.workspaceCount === 1 ? "names another registered workspace" : `names ${report.workspaceCount} other registered workspaces`;
|
|
2518
|
+
return `basou: this workspace's position ${subject} (${where}, ${describeForeignWorkspaceLines(report.lines)}). The position is handed to the agent session running in this workspace, so those names travel with it. Advisory only: nothing was withheld.`;
|
|
2519
|
+
}
|
|
2520
|
+
function protocolForeignWorkspaceWarning(report) {
|
|
2521
|
+
const subject = report.workspaceCount === 1 ? "names a registered workspace" : `names ${report.workspaceCount} registered workspaces`;
|
|
2522
|
+
return `basou: the protocol block ${subject} (block ${describeForeignWorkspaceLines(report.lines)}, counted from the block's first line, not the file's). Standing protocols are written to the user-global CLAUDE.md, which every project on this machine loads, so a workspace-specific name there reaches every workspace's sessions. Only the basou-managed block is checked; the rest of that file is not basou's to inspect. Advisory only: nothing was withheld.`;
|
|
2523
|
+
}
|
|
2524
|
+
async function warnIfPositionNamesOtherWorkspaces(args) {
|
|
2525
|
+
try {
|
|
2526
|
+
const body = await readMarkdownFile4(args.paths.files.orientation);
|
|
2527
|
+
if (body === null) return;
|
|
2528
|
+
const report = await findForeignWorkspaceNames({
|
|
2529
|
+
text: body,
|
|
2530
|
+
selfPath: dirname2(args.paths.root),
|
|
2531
|
+
configPath: args.configPath
|
|
2532
|
+
});
|
|
2533
|
+
if (report !== null) {
|
|
2534
|
+
console.error(positionForeignWorkspaceWarning(report, args.paths.files.orientation));
|
|
2535
|
+
}
|
|
2536
|
+
} catch {
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2384
2540
|
// src/commands/orient.ts
|
|
2385
2541
|
import {
|
|
2386
2542
|
assertBasouRootSafe as assertBasouRootSafe7,
|
|
@@ -2448,7 +2604,7 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
|
2448
2604
|
|
|
2449
2605
|
// src/lib/provenance-actions.ts
|
|
2450
2606
|
import {
|
|
2451
|
-
readMarkdownFile as
|
|
2607
|
+
readMarkdownFile as readMarkdownFile5,
|
|
2452
2608
|
renderDecisions as renderDecisions2,
|
|
2453
2609
|
renderHandoff as renderHandoff2,
|
|
2454
2610
|
renderOrientation,
|
|
@@ -2460,7 +2616,7 @@ import {
|
|
|
2460
2616
|
import { createReadStream } from "fs";
|
|
2461
2617
|
import { readdir, readFile as readFile2, rm, stat as stat3 } from "fs/promises";
|
|
2462
2618
|
import { homedir as homedir6 } from "os";
|
|
2463
|
-
import { basename as
|
|
2619
|
+
import { basename as basename4, dirname as dirname3, join as join8, resolve as resolve5 } from "path";
|
|
2464
2620
|
import { createInterface } from "readline";
|
|
2465
2621
|
import {
|
|
2466
2622
|
AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
|
|
@@ -2478,6 +2634,7 @@ import {
|
|
|
2478
2634
|
readSessionYaml as readSessionYaml2,
|
|
2479
2635
|
reimportPreservingId,
|
|
2480
2636
|
resolveRepositoryRoot as resolveRepositoryRoot6,
|
|
2637
|
+
SESSION_IMPORT_SCHEMA_VERSION,
|
|
2481
2638
|
SessionImportPayloadSchema
|
|
2482
2639
|
} from "@basou/core";
|
|
2483
2640
|
var SES_PREFIX2 = "ses_";
|
|
@@ -2550,7 +2707,7 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
2550
2707
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
2551
2708
|
const projectSet = new Set(projectPaths);
|
|
2552
2709
|
const candidates = files.map((file) => {
|
|
2553
|
-
const externalId =
|
|
2710
|
+
const externalId = basename4(file, ".jsonl");
|
|
2554
2711
|
return {
|
|
2555
2712
|
externalId,
|
|
2556
2713
|
sourcePath: file,
|
|
@@ -2640,7 +2797,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2640
2797
|
files: payload.session.related_files ?? [],
|
|
2641
2798
|
workingDirectory: payload.session.working_directory,
|
|
2642
2799
|
sourceRoots: projectPaths,
|
|
2643
|
-
masterRoot:
|
|
2800
|
+
masterRoot: dirname3(paths.root),
|
|
2644
2801
|
extraInRoot: AGENT_INFRA_DIRS2
|
|
2645
2802
|
});
|
|
2646
2803
|
if (scope.outOfRoot.length > 0) crossProject.push({ externalId, outOfRoot: scope.outOfRoot });
|
|
@@ -2665,7 +2822,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2665
2822
|
if (!parsed.success) {
|
|
2666
2823
|
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
2667
2824
|
}
|
|
2668
|
-
if (parsed.data.schema_version !==
|
|
2825
|
+
if (parsed.data.schema_version !== SESSION_IMPORT_SCHEMA_VERSION) {
|
|
2669
2826
|
throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
|
|
2670
2827
|
}
|
|
2671
2828
|
return parsed.data;
|
|
@@ -3158,7 +3315,7 @@ function importCodex(options, ctx) {
|
|
|
3158
3315
|
}
|
|
3159
3316
|
async function regenerateHandoff(paths, nowIso, callbacks) {
|
|
3160
3317
|
const result = await renderHandoff2({ paths, nowIso, ...callbacks });
|
|
3161
|
-
const existing = await
|
|
3318
|
+
const existing = await readMarkdownFile5(paths.files.handoff);
|
|
3162
3319
|
await writeMarkdownFile3(
|
|
3163
3320
|
paths.files.handoff,
|
|
3164
3321
|
renderWithMarkers3(existing, result.body, "handoff.md")
|
|
@@ -3172,7 +3329,7 @@ async function regenerateHandoff(paths, nowIso, callbacks) {
|
|
|
3172
3329
|
}
|
|
3173
3330
|
async function regenerateDecisions(paths, nowIso, callbacks) {
|
|
3174
3331
|
const result = await renderDecisions2({ paths, nowIso, ...callbacks });
|
|
3175
|
-
const existing = await
|
|
3332
|
+
const existing = await readMarkdownFile5(paths.files.decisions);
|
|
3176
3333
|
await writeMarkdownFile3(
|
|
3177
3334
|
paths.files.decisions,
|
|
3178
3335
|
renderWithMarkers3(existing, result.body, "decisions.md")
|
|
@@ -3282,6 +3439,19 @@ async function doRunOrient(options, ctx) {
|
|
|
3282
3439
|
} else {
|
|
3283
3440
|
console.log(result.body);
|
|
3284
3441
|
}
|
|
3442
|
+
await warnIfPositionNamesOtherWorkspaces2(result, ctx);
|
|
3443
|
+
}
|
|
3444
|
+
async function warnIfPositionNamesOtherWorkspaces2(result, ctx) {
|
|
3445
|
+
const report = await findForeignWorkspaceNames({
|
|
3446
|
+
text: result.body,
|
|
3447
|
+
selfPath: result.workspaceRoot,
|
|
3448
|
+
configPath: ctx.portfolioConfigPath
|
|
3449
|
+
});
|
|
3450
|
+
if (report !== null) {
|
|
3451
|
+
console.error(
|
|
3452
|
+
positionForeignWorkspaceWarning(report, basouPaths8(result.workspaceRoot).files.orientation)
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3285
3455
|
}
|
|
3286
3456
|
async function renderOrientationForCwd(options, ctx) {
|
|
3287
3457
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -3327,6 +3497,7 @@ async function renderOrientationForRoot(repositoryRoot, options, ctx, behaviour)
|
|
|
3327
3497
|
`);
|
|
3328
3498
|
return {
|
|
3329
3499
|
body: result.body,
|
|
3500
|
+
workspaceRoot: repositoryRoot,
|
|
3330
3501
|
sessionCount: result.sessionCount,
|
|
3331
3502
|
inFlightTaskCount: result.inFlightTaskCount,
|
|
3332
3503
|
pendingApprovalsCount: result.pendingApprovalsCount,
|
|
@@ -3351,7 +3522,7 @@ function registerHookCommand(program2) {
|
|
|
3351
3522
|
"Hook handlers for AI coding tools (Claude Code, Codex): read a hook payload on stdin, emit the tool's hook output on stdout"
|
|
3352
3523
|
);
|
|
3353
3524
|
hook.command("session-start").description(
|
|
3354
|
-
"Codex
|
|
3525
|
+
"SessionStart hook (Codex; Claude Code too): print the current position of the workspace the session was opened in (read from the payload's cwd) so the tool adds it to that session's context. Stays silent outside a registered basou workspace, and when the position names another registered workspace; never fails the session."
|
|
3355
3526
|
).addHelpText("after", HOOK_SESSION_START_HELP).action(async () => {
|
|
3356
3527
|
await runHookSessionStart();
|
|
3357
3528
|
});
|
|
@@ -3440,6 +3611,18 @@ desktop app has bound a folder, when cwd is '/') gets nothing. That is how one
|
|
|
3440
3611
|
user-global hook serves every workspace without any workspace's position ever
|
|
3441
3612
|
being written where another workspace's session would read it.
|
|
3442
3613
|
|
|
3614
|
+
The hook stays silent in one more case: when the position it would print names
|
|
3615
|
+
ANOTHER registered workspace (a recorded path under it, a captured decision that
|
|
3616
|
+
mentions it). 'basou orient' and 'basou refresh' report that as a stderr
|
|
3617
|
+
advisory the operator can read; a hook has no reader for stderr and its stdout
|
|
3618
|
+
becomes the session's trusted context, so it withholds the position instead.
|
|
3619
|
+
Run 'basou refresh' to see which lines are responsible.
|
|
3620
|
+
|
|
3621
|
+
Claude Code's SessionStart hook sends the same kind of payload (a JSON object
|
|
3622
|
+
with 'cwd') and adds stdout to context the same way, so a Claude Code user may
|
|
3623
|
+
register this command in ~/.claude/settings.json in place of 'basou orient' to
|
|
3624
|
+
get both gates. basou does not install that one.
|
|
3625
|
+
|
|
3443
3626
|
Codex trusts hooks by hash. A newly installed or changed hook is skipped until
|
|
3444
3627
|
you review it: the interactive CLI asks at startup ("Hooks need review"), the
|
|
3445
3628
|
desktop app lists it under Settings -> Hooks. Non-interactive 'codex exec' skips
|
|
@@ -3530,6 +3713,14 @@ async function renderRegisteredWorkspacePosition(cwd, portfolioConfigPath = DEFA
|
|
|
3530
3713
|
throw new Error("The workspace is not registered in the portfolio; the hook stays silent.");
|
|
3531
3714
|
}
|
|
3532
3715
|
const rendered = await renderOrientationForRoot(root, {}, { cwd }, { write: false });
|
|
3716
|
+
const foreign = await findForeignWorkspaceNames({
|
|
3717
|
+
text: rendered.body,
|
|
3718
|
+
selfPath: root,
|
|
3719
|
+
configPath: portfolioConfigPath
|
|
3720
|
+
});
|
|
3721
|
+
if (foreign !== null) {
|
|
3722
|
+
throw new Error("The position names another registered workspace; the hook stays silent.");
|
|
3723
|
+
}
|
|
3533
3724
|
return { body: rendered.body };
|
|
3534
3725
|
}
|
|
3535
3726
|
async function isRegisteredWorkspace(root, portfolioConfigPath) {
|
|
@@ -3539,9 +3730,9 @@ async function isRegisteredWorkspace(root, portfolioConfigPath) {
|
|
|
3539
3730
|
} catch {
|
|
3540
3731
|
return false;
|
|
3541
3732
|
}
|
|
3542
|
-
const rootReal = await
|
|
3733
|
+
const rootReal = await realpath3(root).catch(() => root);
|
|
3543
3734
|
for (const entry of entries) {
|
|
3544
|
-
const entryReal = await
|
|
3735
|
+
const entryReal = await realpath3(entry.path).catch(() => null);
|
|
3545
3736
|
if (entryReal !== null && entryReal === rootReal) return true;
|
|
3546
3737
|
}
|
|
3547
3738
|
return false;
|
|
@@ -3785,7 +3976,7 @@ var DEFAULT_CODEX_FACE_PATH = join9(homedir7(), ".codex", "AGENTS.md");
|
|
|
3785
3976
|
var LEFTOVER_FACE_NOTE = (label) => `${label} still carries an orientation block rendered by an earlier basou (0.39 or before); every Codex session on this machine reads it. \`basou channel clear codex\` removes it.`;
|
|
3786
3977
|
async function faceHasLeftoverOrientationBlock(facePath) {
|
|
3787
3978
|
try {
|
|
3788
|
-
const existing = await
|
|
3979
|
+
const existing = await readMarkdownFile6(facePath);
|
|
3789
3980
|
if (existing === null) return false;
|
|
3790
3981
|
const section = parseMarkers2(existing, { start: ORIENTATION_START2, end: ORIENTATION_END2 });
|
|
3791
3982
|
return section.kind !== "no_markers";
|
|
@@ -3965,7 +4156,7 @@ async function codexHookTrustFor(hooksPath, location, configPath) {
|
|
|
3965
4156
|
}
|
|
3966
4157
|
|
|
3967
4158
|
// src/commands/init.ts
|
|
3968
|
-
import { basename as
|
|
4159
|
+
import { basename as basename5, relative, resolve as resolve6 } from "path";
|
|
3969
4160
|
import {
|
|
3970
4161
|
appendBasouGitignore,
|
|
3971
4162
|
createManifest,
|
|
@@ -4003,7 +4194,7 @@ async function runInit(options, ctx = {}) {
|
|
|
4003
4194
|
async function doRunInit(options, ctx) {
|
|
4004
4195
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4005
4196
|
const repositoryRoot = await resolveRepositoryRootForInit(cwd);
|
|
4006
|
-
const workspaceName = options.name ??
|
|
4197
|
+
const workspaceName = options.name ?? basename5(repositoryRoot);
|
|
4007
4198
|
if (options.repoUrl !== void 0) {
|
|
4008
4199
|
console.error(
|
|
4009
4200
|
"Warning: --repo-url is deprecated and ignored (project.repository_url was removed); the flag will be removed at 1.0."
|
|
@@ -4058,6 +4249,7 @@ import {
|
|
|
4058
4249
|
assertBasouRootSafe as assertBasouRootSafe8,
|
|
4059
4250
|
basouPaths as basouPaths9,
|
|
4060
4251
|
createAdHocSessionWithEvent as createAdHocSessionWithEvent2,
|
|
4252
|
+
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION4,
|
|
4061
4253
|
findErrorCode as findErrorCode7,
|
|
4062
4254
|
readManifest as readManifest5,
|
|
4063
4255
|
resolveSessionId as resolveSessionId2
|
|
@@ -4162,7 +4354,7 @@ async function doRunNote(body, options, ctx) {
|
|
|
4162
4354
|
}
|
|
4163
4355
|
function buildNoteEvent(input) {
|
|
4164
4356
|
return {
|
|
4165
|
-
schema_version:
|
|
4357
|
+
schema_version: EVENT_SCHEMA_VERSION4,
|
|
4166
4358
|
id: input.eventId,
|
|
4167
4359
|
session_id: input.sessionId,
|
|
4168
4360
|
occurred_at: input.occurredAt,
|
|
@@ -4329,7 +4521,7 @@ import {
|
|
|
4329
4521
|
writeFileSync,
|
|
4330
4522
|
writeSync
|
|
4331
4523
|
} from "fs";
|
|
4332
|
-
import { basename as
|
|
4524
|
+
import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve7 } from "path";
|
|
4333
4525
|
import {
|
|
4334
4526
|
appendBasouGitignore as appendBasouGitignore2,
|
|
4335
4527
|
basouPaths as basouPaths10,
|
|
@@ -4348,7 +4540,7 @@ import {
|
|
|
4348
4540
|
planRosterAdoption,
|
|
4349
4541
|
planWorkspaceView,
|
|
4350
4542
|
readManifest as readManifest6,
|
|
4351
|
-
readMarkdownFile as
|
|
4543
|
+
readMarkdownFile as readMarkdownFile7,
|
|
4352
4544
|
reconcileSourceRoots,
|
|
4353
4545
|
removeMarkerSection as removeMarkerSection2,
|
|
4354
4546
|
renderAnchorStarter,
|
|
@@ -4960,9 +5152,9 @@ function applyGitignorePlan(repositoryRoot, plan) {
|
|
|
4960
5152
|
throw new Error("Failed to read .gitignore", { cause: error });
|
|
4961
5153
|
}
|
|
4962
5154
|
}
|
|
4963
|
-
const
|
|
5155
|
+
const sep2 = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
4964
5156
|
try {
|
|
4965
|
-
writeFileSync(file, `${existing}${
|
|
5157
|
+
writeFileSync(file, `${existing}${sep2}${plan.toAdd.join("\n")}
|
|
4966
5158
|
`);
|
|
4967
5159
|
} catch (error) {
|
|
4968
5160
|
throw new Error("Failed to write .gitignore", { cause: error });
|
|
@@ -5102,7 +5294,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5102
5294
|
isAnchor: true,
|
|
5103
5295
|
reachable: true,
|
|
5104
5296
|
canonicalPresent: true,
|
|
5105
|
-
canonicalName:
|
|
5297
|
+
canonicalName: basename6(real),
|
|
5106
5298
|
files: [{ name: CANONICAL_FILE, expectedTarget: CANONICAL_FILE, state: "blocked" }]
|
|
5107
5299
|
};
|
|
5108
5300
|
}
|
|
@@ -5124,14 +5316,14 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5124
5316
|
isAnchor: true,
|
|
5125
5317
|
reachable: true,
|
|
5126
5318
|
canonicalPresent: true,
|
|
5127
|
-
canonicalName:
|
|
5319
|
+
canonicalName: basename6(real),
|
|
5128
5320
|
files: anchorFiles
|
|
5129
5321
|
};
|
|
5130
5322
|
}
|
|
5131
5323
|
if (!existsSync2(join11(real, ".git"))) {
|
|
5132
5324
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
5133
5325
|
}
|
|
5134
|
-
const canonicalFile = isSelf ? join11(real, CANONICAL_FILE) : join11(anchorReal, "agents",
|
|
5326
|
+
const canonicalFile = isSelf ? join11(real, CANONICAL_FILE) : join11(anchorReal, "agents", basename6(real), CANONICAL_FILE);
|
|
5135
5327
|
if (!existsSync2(canonicalFile)) {
|
|
5136
5328
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
5137
5329
|
}
|
|
@@ -5151,7 +5343,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5151
5343
|
isAnchor: false,
|
|
5152
5344
|
reachable: true,
|
|
5153
5345
|
canonicalPresent: true,
|
|
5154
|
-
canonicalName:
|
|
5346
|
+
canonicalName: basename6(real),
|
|
5155
5347
|
files
|
|
5156
5348
|
};
|
|
5157
5349
|
}
|
|
@@ -5168,7 +5360,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
5168
5360
|
for (const { name, target } of plan.toCreate) {
|
|
5169
5361
|
const filePath = join11(real, name);
|
|
5170
5362
|
try {
|
|
5171
|
-
mkdirSync(
|
|
5363
|
+
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5172
5364
|
symlinkSync(target, filePath);
|
|
5173
5365
|
created.push(name);
|
|
5174
5366
|
} catch (error) {
|
|
@@ -5184,16 +5376,16 @@ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
|
|
|
5184
5376
|
for (const entry of roster) {
|
|
5185
5377
|
let name;
|
|
5186
5378
|
try {
|
|
5187
|
-
name =
|
|
5379
|
+
name = basename6(realpathSync(resolve7(repositoryRoot, entry.path)));
|
|
5188
5380
|
} catch {
|
|
5189
|
-
name =
|
|
5381
|
+
name = basename6(resolve7(repositoryRoot, entry.path));
|
|
5190
5382
|
}
|
|
5191
5383
|
if (name === viewName) return entry.path;
|
|
5192
5384
|
}
|
|
5193
5385
|
return void 0;
|
|
5194
5386
|
}
|
|
5195
5387
|
function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
|
|
5196
|
-
const viewName =
|
|
5388
|
+
const viewName = basename6(viewDir);
|
|
5197
5389
|
const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
|
|
5198
5390
|
if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
|
|
5199
5391
|
const canonicalFile = canonicalFileFor(anchorReal, viewName);
|
|
@@ -5218,7 +5410,7 @@ function applyViewSymlinks(viewDir, files) {
|
|
|
5218
5410
|
if (f.state !== "missing") continue;
|
|
5219
5411
|
const filePath = join11(viewDir, f.name);
|
|
5220
5412
|
try {
|
|
5221
|
-
mkdirSync(
|
|
5413
|
+
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5222
5414
|
symlinkSync(f.expectedTarget, filePath);
|
|
5223
5415
|
created.push(f.name);
|
|
5224
5416
|
} catch (error) {
|
|
@@ -5438,7 +5630,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
5438
5630
|
return realpathSync(abs);
|
|
5439
5631
|
} catch {
|
|
5440
5632
|
try {
|
|
5441
|
-
return join11(realpathSync(
|
|
5633
|
+
return join11(realpathSync(dirname4(abs)), basename6(abs));
|
|
5442
5634
|
} catch {
|
|
5443
5635
|
return abs;
|
|
5444
5636
|
}
|
|
@@ -5455,7 +5647,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
5455
5647
|
if (expectedTarget === "" || expectedTarget === ".") {
|
|
5456
5648
|
return { path: entry.path, reachable: false };
|
|
5457
5649
|
}
|
|
5458
|
-
const linkName =
|
|
5650
|
+
const linkName = basename6(repoReal);
|
|
5459
5651
|
const { state, actualTarget } = inspectSymlink(join11(viewDir, linkName), expectedTarget);
|
|
5460
5652
|
return {
|
|
5461
5653
|
path: entry.path,
|
|
@@ -5472,7 +5664,7 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
5472
5664
|
for (const { name, target } of toCreate) {
|
|
5473
5665
|
const filePath = join11(viewDir, name);
|
|
5474
5666
|
try {
|
|
5475
|
-
mkdirSync(
|
|
5667
|
+
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5476
5668
|
symlinkSync(target, filePath);
|
|
5477
5669
|
created.push(name);
|
|
5478
5670
|
} catch (error) {
|
|
@@ -5585,7 +5777,7 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
5585
5777
|
} else {
|
|
5586
5778
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
5587
5779
|
const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
|
|
5588
|
-
const rosterNames = roster.map((entry) =>
|
|
5780
|
+
const rosterNames = roster.map((entry) => basename6(resolve7(repositoryRoot, entry.path)));
|
|
5589
5781
|
const rosterRealpaths = /* @__PURE__ */ new Set();
|
|
5590
5782
|
for (const entry of roster) {
|
|
5591
5783
|
try {
|
|
@@ -5779,10 +5971,10 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
5779
5971
|
if (!existsSync2(join11(real, ".git"))) {
|
|
5780
5972
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
5781
5973
|
}
|
|
5782
|
-
const canonicalName =
|
|
5974
|
+
const canonicalName = basename6(real);
|
|
5783
5975
|
let content;
|
|
5784
5976
|
try {
|
|
5785
|
-
content = await
|
|
5977
|
+
content = await readMarkdownFile7(canonicalFileFor(anchorReal, canonicalName));
|
|
5786
5978
|
} catch {
|
|
5787
5979
|
return {
|
|
5788
5980
|
...declared,
|
|
@@ -5832,7 +6024,7 @@ function viewPresetReposFor(repositoryRoot, roster) {
|
|
|
5832
6024
|
} catch {
|
|
5833
6025
|
real = void 0;
|
|
5834
6026
|
}
|
|
5835
|
-
const name =
|
|
6027
|
+
const name = basename6(real ?? abs);
|
|
5836
6028
|
const isAnchor = real !== void 0 && anchorReal !== void 0 ? real === anchorReal : abs === repositoryRoot;
|
|
5837
6029
|
return {
|
|
5838
6030
|
name,
|
|
@@ -5890,8 +6082,8 @@ async function applyViewPreset(anchorReal, outcome) {
|
|
|
5890
6082
|
isLink = false;
|
|
5891
6083
|
}
|
|
5892
6084
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
5893
|
-
if (outcome.action === "create") mkdirSync(
|
|
5894
|
-
const existing = await
|
|
6085
|
+
if (outcome.action === "create") mkdirSync(dirname4(file), { recursive: true });
|
|
6086
|
+
const existing = await readMarkdownFile7(file);
|
|
5895
6087
|
await writeMarkdownFile5(file, renderWithMarkers4(existing, outcome.block, label));
|
|
5896
6088
|
}
|
|
5897
6089
|
async function applyPresetPlan(anchorReal, plan) {
|
|
@@ -5904,8 +6096,8 @@ async function applyPresetPlan(anchorReal, plan) {
|
|
|
5904
6096
|
isLink = false;
|
|
5905
6097
|
}
|
|
5906
6098
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
5907
|
-
if (plan.action === "create") mkdirSync(
|
|
5908
|
-
const existing = await
|
|
6099
|
+
if (plan.action === "create") mkdirSync(dirname4(file), { recursive: true });
|
|
6100
|
+
const existing = await readMarkdownFile7(file);
|
|
5909
6101
|
await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
|
|
5910
6102
|
}
|
|
5911
6103
|
function presetFailureReason(error) {
|
|
@@ -5927,7 +6119,7 @@ async function doRunProjectPreset(options, ctx) {
|
|
|
5927
6119
|
const facts = [];
|
|
5928
6120
|
for (const entry of roster) facts.push(await gatherRepoPreset(repositoryRoot, anchorReal, entry));
|
|
5929
6121
|
const viewPath = manifest.workspace.view;
|
|
5930
|
-
const viewCanonicalName = roster.length > 0 && viewPath !== void 0 ?
|
|
6122
|
+
const viewCanonicalName = roster.length > 0 && viewPath !== void 0 ? basename6(resolveViewDir(repositoryRoot, viewPath)) : void 0;
|
|
5931
6123
|
const summary = summarizePresetPlan(
|
|
5932
6124
|
facts,
|
|
5933
6125
|
viewCanonicalName !== void 0 ? { viewCanonicalName } : void 0
|
|
@@ -6154,7 +6346,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6154
6346
|
return empty;
|
|
6155
6347
|
}
|
|
6156
6348
|
const anchorReal = realpathSync(repositoryRoot);
|
|
6157
|
-
const canonicalName =
|
|
6349
|
+
const canonicalName = basename6(real);
|
|
6158
6350
|
const instructionFiles = [];
|
|
6159
6351
|
for (const name of INSTRUCTION_FILES) {
|
|
6160
6352
|
try {
|
|
@@ -6224,7 +6416,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6224
6416
|
}
|
|
6225
6417
|
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
6226
6418
|
const targetAbs = resolve7(repositoryRoot, target);
|
|
6227
|
-
const canonicalName =
|
|
6419
|
+
const canonicalName = basename6(repoReal ?? targetAbs);
|
|
6228
6420
|
const roster = manifest.repos ?? [];
|
|
6229
6421
|
const declaredEntry = roster.find((r) => {
|
|
6230
6422
|
try {
|
|
@@ -6245,10 +6437,10 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6245
6437
|
}
|
|
6246
6438
|
if (rReal !== null) {
|
|
6247
6439
|
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
6248
|
-
return
|
|
6440
|
+
return basename6(rReal).toLowerCase() === cnFold;
|
|
6249
6441
|
}
|
|
6250
6442
|
if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
|
|
6251
|
-
return
|
|
6443
|
+
return basename6(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
6252
6444
|
});
|
|
6253
6445
|
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
6254
6446
|
const items = [];
|
|
@@ -6713,7 +6905,7 @@ function renderProjectArchive(result) {
|
|
|
6713
6905
|
if (t.gitignorePatterns.length > 0)
|
|
6714
6906
|
items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
|
|
6715
6907
|
if (t.canonical)
|
|
6716
|
-
items.push(`the anchor's canonical (agents/${
|
|
6908
|
+
items.push(`the anchor's canonical (agents/${basename6(result.target)}/AGENTS.md)`);
|
|
6717
6909
|
if (!t.inspected) {
|
|
6718
6910
|
lines.push(
|
|
6719
6911
|
"## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
|
|
@@ -6924,7 +7116,7 @@ function validateProjectName(name) {
|
|
|
6924
7116
|
async function doRunProjectNew(repos, options, ctx) {
|
|
6925
7117
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6926
7118
|
const repositoryRoot = await resolveRepositoryRootForNew(cwd);
|
|
6927
|
-
const workspaceName =
|
|
7119
|
+
const workspaceName = basename6(repositoryRoot);
|
|
6928
7120
|
const productName = options.projectName !== void 0 ? validateProjectName(options.projectName) : void 0;
|
|
6929
7121
|
const viewStem = productName ?? workspaceName;
|
|
6930
7122
|
const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
|
|
@@ -7123,10 +7315,10 @@ async function doRunProjectSeedAnchor(options, ctx) {
|
|
|
7123
7315
|
return;
|
|
7124
7316
|
}
|
|
7125
7317
|
const viewPath = manifest.workspace.view;
|
|
7126
|
-
const viewName = viewPath !== void 0 ?
|
|
7318
|
+
const viewName = viewPath !== void 0 ? basename6(resolveViewDir(repositoryRoot, viewPath)) : void 0;
|
|
7127
7319
|
const repos = viewPresetReposFor(repositoryRoot, roster);
|
|
7128
7320
|
const content = renderAnchorStarter({
|
|
7129
|
-
anchorName:
|
|
7321
|
+
anchorName: basename6(repositoryRoot),
|
|
7130
7322
|
...manifest.project?.name !== void 0 ? { projectName: manifest.project.name } : {},
|
|
7131
7323
|
...viewName !== void 0 ? { viewName } : {},
|
|
7132
7324
|
repos
|
|
@@ -7207,7 +7399,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
7207
7399
|
const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
|
|
7208
7400
|
const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
|
|
7209
7401
|
const path = displayRel === "" ? "." : displayRel;
|
|
7210
|
-
const canonicalName =
|
|
7402
|
+
const canonicalName = basename6(argReal ?? argAbs);
|
|
7211
7403
|
if (argReal === void 0) {
|
|
7212
7404
|
return {
|
|
7213
7405
|
path,
|
|
@@ -7241,7 +7433,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
7241
7433
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
7242
7434
|
const agentsFile = join11(repoReal, CANONICAL_FILE);
|
|
7243
7435
|
try {
|
|
7244
|
-
mkdirSync(
|
|
7436
|
+
mkdirSync(dirname4(canonicalFile), { recursive: true });
|
|
7245
7437
|
} catch (error) {
|
|
7246
7438
|
return { ok: false, message: failureReason(error), partial: false };
|
|
7247
7439
|
}
|
|
@@ -7262,7 +7454,7 @@ function relocateAgentsFile(repoReal, canonicalFile) {
|
|
|
7262
7454
|
function gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster) {
|
|
7263
7455
|
if (viewPath === void 0) return { kind: "no-view" };
|
|
7264
7456
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
7265
|
-
const viewName =
|
|
7457
|
+
const viewName = basename6(viewDir);
|
|
7266
7458
|
const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
|
|
7267
7459
|
if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
|
|
7268
7460
|
const canonicalFile = canonicalFileFor(anchorReal, viewName);
|
|
@@ -7294,7 +7486,7 @@ async function applyViewRetrofit(anchorReal, outcome) {
|
|
|
7294
7486
|
isLink = false;
|
|
7295
7487
|
}
|
|
7296
7488
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
7297
|
-
const existing = await
|
|
7489
|
+
const existing = await readMarkdownFile7(file);
|
|
7298
7490
|
await writeMarkdownFile5(file, seedMarkers(existing, outcome.block, label));
|
|
7299
7491
|
}
|
|
7300
7492
|
async function doRunProjectRetrofit(repo, options, ctx) {
|
|
@@ -7341,7 +7533,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7341
7533
|
} catch {
|
|
7342
7534
|
argReal = void 0;
|
|
7343
7535
|
}
|
|
7344
|
-
const viewCanonicalName = viewPath !== void 0 ?
|
|
7536
|
+
const viewCanonicalName = viewPath !== void 0 ? basename6(resolveViewDir(repositoryRoot, viewPath)) : void 0;
|
|
7345
7537
|
const facts = gatherRetrofit(
|
|
7346
7538
|
repositoryRoot,
|
|
7347
7539
|
anchorReal,
|
|
@@ -7557,7 +7749,7 @@ function renderProjectRetrofit(result) {
|
|
|
7557
7749
|
|
|
7558
7750
|
// src/commands/protocol.ts
|
|
7559
7751
|
import { readFile as readFile4 } from "fs/promises";
|
|
7560
|
-
import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers4, readMarkdownFile as
|
|
7752
|
+
import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers4, readMarkdownFile as readMarkdownFile8 } from "@basou/core";
|
|
7561
7753
|
|
|
7562
7754
|
// src/lib/protocols-config.ts
|
|
7563
7755
|
import { homedir as homedir8 } from "os";
|
|
@@ -7709,18 +7901,23 @@ ${body}` : body;
|
|
|
7709
7901
|
${sections.join("\n\n")}
|
|
7710
7902
|
`;
|
|
7711
7903
|
}
|
|
7712
|
-
async function doRunProtocolSync(options) {
|
|
7904
|
+
async function doRunProtocolSync(options, ctx = {}) {
|
|
7713
7905
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
7714
7906
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
7715
7907
|
const entries = await loadProtocolsConfig(configPath);
|
|
7716
7908
|
const sources = await readProtocolSources(entries);
|
|
7717
7909
|
const block = buildBlock(sources);
|
|
7910
|
+
const foreign = await findForeignWorkspaceNames({
|
|
7911
|
+
text: block,
|
|
7912
|
+
configPath: ctx.portfolioConfigPath
|
|
7913
|
+
});
|
|
7718
7914
|
const result = await syncMarkerBlock({
|
|
7719
7915
|
target,
|
|
7720
7916
|
markers: PROTOCOL_MARKERS,
|
|
7721
7917
|
block,
|
|
7722
7918
|
...options.dryRun === true ? { dryRun: true } : {}
|
|
7723
7919
|
});
|
|
7920
|
+
if (foreign !== null) console.error(protocolForeignWorkspaceWarning(foreign));
|
|
7724
7921
|
if (result.action === "unchanged") {
|
|
7725
7922
|
console.log(`The basou:protocols block is already up to date (${entries.length} protocol(s)).`);
|
|
7726
7923
|
return;
|
|
@@ -7742,7 +7939,7 @@ async function doRunProtocolList(options) {
|
|
|
7742
7939
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
7743
7940
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
7744
7941
|
const entries = await loadProtocolsConfig(configPath);
|
|
7745
|
-
const existing = await
|
|
7942
|
+
const existing = await readMarkdownFile8(target);
|
|
7746
7943
|
const installed = existing !== null && parseMarkers4(existing, PROTOCOL_MARKERS).kind === "ok";
|
|
7747
7944
|
console.log(`Declared protocols (${entries.length}):`);
|
|
7748
7945
|
for (const entry of entries) {
|
|
@@ -7975,7 +8172,7 @@ async function doRunRefreshPortfolio(options, ctx) {
|
|
|
7975
8172
|
for (const ws of workspaces) {
|
|
7976
8173
|
const label = ws.label ?? ws.path;
|
|
7977
8174
|
try {
|
|
7978
|
-
const { result } = await computeRefresh(
|
|
8175
|
+
const { result, paths } = await computeRefresh(
|
|
7979
8176
|
{ ...options, portfolio: false },
|
|
7980
8177
|
{ ...ctx, cwd: ws.path }
|
|
7981
8178
|
);
|
|
@@ -7985,6 +8182,7 @@ async function doRunRefreshPortfolio(options, ctx) {
|
|
|
7985
8182
|
## ${label} (${ws.path})`);
|
|
7986
8183
|
printRefreshSummary(result);
|
|
7987
8184
|
}
|
|
8185
|
+
await warnPosition(paths, result, ctx);
|
|
7988
8186
|
} catch (error) {
|
|
7989
8187
|
const message = error instanceof Error ? error.message : String(error);
|
|
7990
8188
|
rollup.push({ label, path: ws.path, status: "failed", error: message });
|
|
@@ -8068,8 +8266,16 @@ async function doRunRefresh(options, ctx) {
|
|
|
8068
8266
|
const line = await retiredChannelNotice(paths);
|
|
8069
8267
|
if (line !== null) console.log(line);
|
|
8070
8268
|
}
|
|
8269
|
+
await warnPosition(paths, result, ctx);
|
|
8071
8270
|
return reported;
|
|
8072
8271
|
}
|
|
8272
|
+
async function warnPosition(paths, result, ctx) {
|
|
8273
|
+
if (result.orientation.status !== "generated") return;
|
|
8274
|
+
await warnIfPositionNamesOtherWorkspaces({
|
|
8275
|
+
paths,
|
|
8276
|
+
configPath: ctx.portfolioConfigPath
|
|
8277
|
+
});
|
|
8278
|
+
}
|
|
8073
8279
|
async function retiredChannelNotice(paths) {
|
|
8074
8280
|
try {
|
|
8075
8281
|
const manifest = await readManifest7(paths);
|
|
@@ -8665,6 +8871,7 @@ import {
|
|
|
8665
8871
|
claudeCodeAdapterMetadata,
|
|
8666
8872
|
codexAdapterMetadata,
|
|
8667
8873
|
appendChainedEvent as coreAppendChainedEvent2,
|
|
8874
|
+
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION5,
|
|
8668
8875
|
finalizeSessionYaml as finalizeSessionYaml2,
|
|
8669
8876
|
findBasouSessionStartHook as findBasouSessionStartHook2,
|
|
8670
8877
|
getDiff,
|
|
@@ -8679,6 +8886,7 @@ import {
|
|
|
8679
8886
|
SessionSchema as SessionSchema2,
|
|
8680
8887
|
sanitizeRelatedFiles,
|
|
8681
8888
|
sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
|
|
8889
|
+
writeObservedDuration as writeObservedDuration2,
|
|
8682
8890
|
writeYamlFile as writeYamlFile2
|
|
8683
8891
|
} from "@basou/core";
|
|
8684
8892
|
function registerRunCommand(program2, ctx = {}) {
|
|
@@ -8751,7 +8959,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8751
8959
|
});
|
|
8752
8960
|
await writeYamlFile2(sessionYamlPath, session);
|
|
8753
8961
|
await appendEvent(sessionDir, {
|
|
8754
|
-
schema_version:
|
|
8962
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
8755
8963
|
type: "session_started",
|
|
8756
8964
|
id: prefixedUlid4("evt"),
|
|
8757
8965
|
session_id: sessionId,
|
|
@@ -8764,7 +8972,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8764
8972
|
}
|
|
8765
8973
|
const runningAt = now().toISOString();
|
|
8766
8974
|
await appendEvent(sessionDir, {
|
|
8767
|
-
schema_version:
|
|
8975
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
8768
8976
|
type: "session_status_changed",
|
|
8769
8977
|
id: prefixedUlid4("evt"),
|
|
8770
8978
|
session_id: sessionId,
|
|
@@ -8837,7 +9045,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8837
9045
|
}
|
|
8838
9046
|
const endedAt = now().toISOString();
|
|
8839
9047
|
await appendEvent(sessionDir, {
|
|
8840
|
-
schema_version:
|
|
9048
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
8841
9049
|
type: "command_executed",
|
|
8842
9050
|
id: prefixedUlid4("evt"),
|
|
8843
9051
|
session_id: sessionId,
|
|
@@ -8849,7 +9057,14 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8849
9057
|
exit_code: result.exit_code,
|
|
8850
9058
|
...result.signal !== null ? { signal: result.signal } : {},
|
|
8851
9059
|
...signalReceived !== null ? { received_signal: signalReceived } : {},
|
|
8852
|
-
|
|
9060
|
+
// Reached only when the child actually ran, and the runner times it on a
|
|
9061
|
+
// monotonic sub-millisecond clock, so the cheapest spawn on this host
|
|
9062
|
+
// (`/usr/bin/true`, median 0.83ms) still rounds to 1ms. A non-positive
|
|
9063
|
+
// value therefore means the measurement is unusable, not that the command
|
|
9064
|
+
// was instant, and is recorded as unobserved. A spawn that never ran
|
|
9065
|
+
// (ENOENT) does not reach this line at all: the runner rejects and
|
|
9066
|
+
// `finalizeSessionAsFailed` writes its own null.
|
|
9067
|
+
duration_ms: writeObservedDuration2(result.duration_ms)
|
|
8853
9068
|
});
|
|
8854
9069
|
let postSnapshot = null;
|
|
8855
9070
|
if (options.snapshot !== false) {
|
|
@@ -8875,7 +9090,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8875
9090
|
}).sanitized;
|
|
8876
9091
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
8877
9092
|
await appendEvent(sessionDir, {
|
|
8878
|
-
schema_version:
|
|
9093
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
8879
9094
|
type: "session_status_changed",
|
|
8880
9095
|
id: prefixedUlid4("evt"),
|
|
8881
9096
|
session_id: sessionId,
|
|
@@ -8885,7 +9100,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8885
9100
|
to: finalStatus
|
|
8886
9101
|
});
|
|
8887
9102
|
await appendEvent(sessionDir, {
|
|
8888
|
-
schema_version:
|
|
9103
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
8889
9104
|
type: "session_ended",
|
|
8890
9105
|
id: prefixedUlid4("evt"),
|
|
8891
9106
|
session_id: sessionId,
|
|
@@ -8931,7 +9146,7 @@ async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appen
|
|
|
8931
9146
|
return null;
|
|
8932
9147
|
}
|
|
8933
9148
|
await appendEvent(sessionDir, {
|
|
8934
|
-
schema_version:
|
|
9149
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
8935
9150
|
type: "git_snapshot",
|
|
8936
9151
|
id: prefixedUlid4("evt"),
|
|
8937
9152
|
session_id: sessionId,
|
|
@@ -8951,7 +9166,7 @@ async function tryAppendFileChangedEvents(sessionDir, sessionId, repoRoot, baseR
|
|
|
8951
9166
|
}
|
|
8952
9167
|
for (const change of diff.changed_files) {
|
|
8953
9168
|
await appendEvent(sessionDir, {
|
|
8954
|
-
schema_version:
|
|
9169
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
8955
9170
|
type: "file_changed",
|
|
8956
9171
|
id: prefixedUlid4("evt"),
|
|
8957
9172
|
session_id: sessionId,
|
|
@@ -9035,7 +9250,7 @@ async function mutateSessionYaml2(filePath, mutator) {
|
|
|
9035
9250
|
}
|
|
9036
9251
|
async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, ctx) {
|
|
9037
9252
|
await appendEvent(sessionDir, {
|
|
9038
|
-
schema_version:
|
|
9253
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
9039
9254
|
type: "command_executed",
|
|
9040
9255
|
id: prefixedUlid4("evt"),
|
|
9041
9256
|
session_id: sessionId,
|
|
@@ -9047,10 +9262,12 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
9047
9262
|
exit_code: null,
|
|
9048
9263
|
signal: null,
|
|
9049
9264
|
...ctx.signalReceived !== null ? { received_signal: ctx.signalReceived } : {},
|
|
9050
|
-
|
|
9265
|
+
// Not observed: this event stands in for a run that ended before a duration
|
|
9266
|
+
// could be measured, so 0 would claim a measured zero.
|
|
9267
|
+
duration_ms: null
|
|
9051
9268
|
});
|
|
9052
9269
|
await appendEvent(sessionDir, {
|
|
9053
|
-
schema_version:
|
|
9270
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
9054
9271
|
type: "session_status_changed",
|
|
9055
9272
|
id: prefixedUlid4("evt"),
|
|
9056
9273
|
session_id: sessionId,
|
|
@@ -9060,7 +9277,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
9060
9277
|
to: "failed"
|
|
9061
9278
|
});
|
|
9062
9279
|
await appendEvent(sessionDir, {
|
|
9063
|
-
schema_version:
|
|
9280
|
+
schema_version: EVENT_SCHEMA_VERSION5,
|
|
9064
9281
|
type: "session_ended",
|
|
9065
9282
|
id: prefixedUlid4("evt"),
|
|
9066
9283
|
session_id: sessionId,
|
|
@@ -9104,22 +9321,25 @@ async function noteCodexHookStatusPreSpawn(_cwd, ctx) {
|
|
|
9104
9321
|
|
|
9105
9322
|
// src/commands/session.ts
|
|
9106
9323
|
import { readFile as readFile7 } from "fs/promises";
|
|
9107
|
-
import { basename as
|
|
9324
|
+
import { basename as basename7, isAbsolute as isAbsolute6, join as join15, relative as relative3 } from "path";
|
|
9108
9325
|
import {
|
|
9109
9326
|
acquireLock as acquireLock6,
|
|
9110
9327
|
appendEventToExistingSession as appendEventToExistingSession3,
|
|
9111
9328
|
assertBasouRootSafe as assertBasouRootSafe13,
|
|
9112
9329
|
basouPaths as basouPaths16,
|
|
9330
|
+
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION6,
|
|
9113
9331
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
9114
9332
|
findErrorCode as findErrorCode12,
|
|
9115
9333
|
importSessionFromJson as importSessionFromJson2,
|
|
9116
9334
|
loadSessionEntries as loadSessionEntries2,
|
|
9117
9335
|
readAllEvents,
|
|
9118
9336
|
readManifest as readManifest10,
|
|
9337
|
+
readObservedDuration,
|
|
9119
9338
|
readYamlFile as readYamlFile7,
|
|
9120
9339
|
rechainSessionInPlace,
|
|
9121
9340
|
resolveSessionId as resolveSessionId3,
|
|
9122
9341
|
resolveTaskId,
|
|
9342
|
+
SESSION_IMPORT_SCHEMA_VERSION as SESSION_IMPORT_SCHEMA_VERSION2,
|
|
9123
9343
|
SessionImportPayloadSchema as SessionImportPayloadSchema2,
|
|
9124
9344
|
SessionSchema as SessionSchema3,
|
|
9125
9345
|
SessionStatusSchema,
|
|
@@ -9241,15 +9461,19 @@ async function doRunSessionShow(idInput, options, ctx) {
|
|
|
9241
9461
|
}
|
|
9242
9462
|
throw new Error("Failed to read session", { cause: error });
|
|
9243
9463
|
}
|
|
9464
|
+
let eventsLostLines = 0;
|
|
9244
9465
|
const events = await readAllEvents(sessionDir, {
|
|
9245
|
-
onWarning: (w) =>
|
|
9466
|
+
onWarning: (w) => {
|
|
9467
|
+
if (w.kind === "malformed_json" || w.kind === "schema_violation") eventsLostLines++;
|
|
9468
|
+
printReplayWarning(w, sessionId);
|
|
9469
|
+
}
|
|
9246
9470
|
});
|
|
9247
9471
|
if (options.json === true) {
|
|
9248
9472
|
console.log(JSON.stringify({ session: session.session, events }, null, 2));
|
|
9249
9473
|
return;
|
|
9250
9474
|
}
|
|
9251
9475
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
9252
|
-
printSessionShowText(session, events, options, repositoryRoot, now);
|
|
9476
|
+
printSessionShowText(session, events, options, repositoryRoot, now, eventsLostLines);
|
|
9253
9477
|
}
|
|
9254
9478
|
function suspectLabel(reason) {
|
|
9255
9479
|
if (reason === "events_say_ended_but_yaml_running") return " \u26A0 ended (yaml stale)";
|
|
@@ -9295,7 +9519,7 @@ function printSessionListText(records) {
|
|
|
9295
9519
|
);
|
|
9296
9520
|
}
|
|
9297
9521
|
}
|
|
9298
|
-
function printSessionShowText(session, events, options, repositoryRoot, now) {
|
|
9522
|
+
function printSessionShowText(session, events, options, repositoryRoot, now, eventsLostLines) {
|
|
9299
9523
|
const s = session.session;
|
|
9300
9524
|
console.log(`Session: ${s.id} (status: ${s.status})`);
|
|
9301
9525
|
console.log(`Source: ${s.source.kind} (v${s.source.version})`);
|
|
@@ -9321,7 +9545,7 @@ function printSessionShowText(session, events, options, repositoryRoot, now) {
|
|
|
9321
9545
|
console.log(` ${pad2(`${type}:`, 24)} ${n}`);
|
|
9322
9546
|
}
|
|
9323
9547
|
console.log("");
|
|
9324
|
-
console.log(`Work: ${formatSessionWork(session, events, now)}`);
|
|
9548
|
+
console.log(`Work: ${formatSessionWork(session, events, now, eventsLostLines)}`);
|
|
9325
9549
|
if (events.length === 0) return;
|
|
9326
9550
|
const last = options.last ?? 5;
|
|
9327
9551
|
const showAll = options.events === true && options.last === void 0;
|
|
@@ -9333,8 +9557,15 @@ function printSessionShowText(session, events, options, repositoryRoot, now) {
|
|
|
9333
9557
|
console.log(` ${formatEventLine(ev)}`);
|
|
9334
9558
|
}
|
|
9335
9559
|
}
|
|
9336
|
-
function formatSessionWork(session, events, now) {
|
|
9337
|
-
const w = sessionWorkStatsFromEvents(
|
|
9560
|
+
function formatSessionWork(session, events, now, eventsLostLines) {
|
|
9561
|
+
const w = sessionWorkStatsFromEvents(
|
|
9562
|
+
session.session.id,
|
|
9563
|
+
session.session,
|
|
9564
|
+
events,
|
|
9565
|
+
now,
|
|
9566
|
+
false,
|
|
9567
|
+
eventsLostLines
|
|
9568
|
+
);
|
|
9338
9569
|
const parts = [];
|
|
9339
9570
|
if (w.tokens.output > 0) parts.push(`${w.tokens.output.toLocaleString("en-US")} output tokens`);
|
|
9340
9571
|
parts.push(`${w.commandCount} cmd / ${w.fileChangedCount} files / ${w.decisionCount} dec`);
|
|
@@ -9345,7 +9576,7 @@ function formatSessionWork(session, events, now) {
|
|
|
9345
9576
|
}
|
|
9346
9577
|
parts.push(`span ${formatDurationMs(w.sessionSpanMs)}${w.open ? " (open)" : ""}`);
|
|
9347
9578
|
parts.push(
|
|
9348
|
-
w.availability.commandTime ? `command ${formatDurationMs(w.commandTimeMs)}` : "command n/a (
|
|
9579
|
+
w.availability.commandTime ? `command ${formatDurationMs(w.commandTimeMs)}` : "command n/a (no duration observed, or the stream was not read in full)"
|
|
9349
9580
|
);
|
|
9350
9581
|
return parts.join(", ");
|
|
9351
9582
|
}
|
|
@@ -9384,7 +9615,9 @@ function eventVariantSummary(ev) {
|
|
|
9384
9615
|
const argsPart = ev.args.length > 0 ? ` ${ev.args.join(" ")}` : "";
|
|
9385
9616
|
const executorPart = ev.command ?? "(executor unrecorded)";
|
|
9386
9617
|
const exitPart = ev.exit_code === null ? "exit=unknown" : `exit=${ev.exit_code}`;
|
|
9387
|
-
|
|
9618
|
+
const observedDuration = readObservedDuration(ev);
|
|
9619
|
+
const durationPart = observedDuration === null ? "duration=unknown" : `${observedDuration}ms`;
|
|
9620
|
+
return `${executorPart}${argsPart} (${exitPart}, ${durationPart})`;
|
|
9388
9621
|
}
|
|
9389
9622
|
case "git_snapshot":
|
|
9390
9623
|
return `branch=${ev.branch} dirty=${ev.dirty}`;
|
|
@@ -9534,7 +9767,7 @@ async function doRunSessionImport(options, ctx) {
|
|
|
9534
9767
|
if (!parsed.success) {
|
|
9535
9768
|
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
9536
9769
|
}
|
|
9537
|
-
if (parsed.data.schema_version !==
|
|
9770
|
+
if (parsed.data.schema_version !== SESSION_IMPORT_SCHEMA_VERSION2) {
|
|
9538
9771
|
throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
|
|
9539
9772
|
}
|
|
9540
9773
|
const importOptions2 = { dryRun: options.dryRun === true };
|
|
@@ -9612,7 +9845,7 @@ function printSessionImportResult(options, result) {
|
|
|
9612
9845
|
return;
|
|
9613
9846
|
}
|
|
9614
9847
|
console.log(
|
|
9615
|
-
`Imported session ${sid} (${result.eventCount} events) from ${
|
|
9848
|
+
`Imported session ${sid} (${result.eventCount} events) from ${basename7(options.from)}`
|
|
9616
9849
|
);
|
|
9617
9850
|
}
|
|
9618
9851
|
var NOTE_BODY_PREVIEW_LIMIT = 80;
|
|
@@ -9655,7 +9888,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
9655
9888
|
paths,
|
|
9656
9889
|
sessionId: sesId,
|
|
9657
9890
|
eventBuilder: (eventId) => ({
|
|
9658
|
-
schema_version:
|
|
9891
|
+
schema_version: EVENT_SCHEMA_VERSION6,
|
|
9659
9892
|
id: eventId,
|
|
9660
9893
|
session_id: sesId,
|
|
9661
9894
|
occurred_at: occurredAt,
|
|
@@ -9849,7 +10082,7 @@ function printStatsText(result, bySource, byDay) {
|
|
|
9849
10082
|
console.log(
|
|
9850
10083
|
` Span: ${formatDurationMs(t.sessionSpanMs)} (total elapsed${openPart})`
|
|
9851
10084
|
);
|
|
9852
|
-
const cmdCaveat = t.commandTimeReliable ? "" : "; some sessions
|
|
10085
|
+
const cmdCaveat = t.commandTimeReliable ? "; at least, only durations the sources reported are counted" : "; some sessions ran commands with no duration observed, or could not be read in full, so this is a floor";
|
|
9853
10086
|
console.log(
|
|
9854
10087
|
` Command: ${formatDurationMs(t.commandTimeMs)} (real shell execution${cmdCaveat})`
|
|
9855
10088
|
);
|
|
@@ -9872,11 +10105,15 @@ function printStatsText(result, bySource, byDay) {
|
|
|
9872
10105
|
}
|
|
9873
10106
|
}
|
|
9874
10107
|
function describeSource(s) {
|
|
9875
|
-
const cmd = s.commandTimeReliable ? formatDurationMs(s.commandTimeMs) : "n/a";
|
|
10108
|
+
const cmd = s.commandTimeReliable ? formatDurationMs(s.commandTimeMs) : s.commandTimeMs > 0 ? `>=${formatFloorMs(s.commandTimeMs)}` : "n/a";
|
|
9876
10109
|
const tokens = s.tokensAvailable ? `${formatInt(s.tokens.output)} out tok` : "no tokens";
|
|
9877
10110
|
const machine = s.machineActiveAvailable ? `, model ${formatDurationMs(s.machineActiveTimeMs)}` : "";
|
|
9878
10111
|
return `${s.sessionCount} sessions, ${tokens}, active ${formatDurationMs(s.activeTimeMs)}${machine}, command ${cmd}`;
|
|
9879
10112
|
}
|
|
10113
|
+
function formatFloorMs(ms) {
|
|
10114
|
+
const coarse = formatDurationMs(ms);
|
|
10115
|
+
return coarse === "0s" ? `${Math.round(ms)}ms` : coarse;
|
|
10116
|
+
}
|
|
9880
10117
|
function formatInt(n) {
|
|
9881
10118
|
return n.toLocaleString("en-US");
|
|
9882
10119
|
}
|
|
@@ -11212,7 +11449,7 @@ async function assertWorkspaceInitialized14(basouRoot) {
|
|
|
11212
11449
|
// src/commands/view.ts
|
|
11213
11450
|
import { spawn } from "child_process";
|
|
11214
11451
|
import { createHash as createHash2 } from "crypto";
|
|
11215
|
-
import { basename as
|
|
11452
|
+
import { basename as basename10, resolve as resolve13 } from "path";
|
|
11216
11453
|
import {
|
|
11217
11454
|
assertBasouRootSafe as assertBasouRootSafe18,
|
|
11218
11455
|
basouPaths as basouPaths22,
|
|
@@ -11226,7 +11463,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
|
11226
11463
|
import { createReadStream as createReadStream2 } from "fs";
|
|
11227
11464
|
import { readdir as readdir3, stat as stat6 } from "fs/promises";
|
|
11228
11465
|
import { homedir as homedir12 } from "os";
|
|
11229
|
-
import { basename as
|
|
11466
|
+
import { basename as basename8, dirname as dirname5, join as join17 } from "path";
|
|
11230
11467
|
import { createInterface as createInterface2 } from "readline";
|
|
11231
11468
|
import { basouPaths as basouPaths21, readManifest as readManifest13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
|
|
11232
11469
|
function uncapturedTotal(result) {
|
|
@@ -11247,7 +11484,7 @@ async function checkPortfolioCoverage(workspaces, ctx = {}) {
|
|
|
11247
11484
|
file,
|
|
11248
11485
|
"claude-code",
|
|
11249
11486
|
claudeTranscriptCwd,
|
|
11250
|
-
listedDirs.has(
|
|
11487
|
+
listedDirs.has(basename8(dirname5(file)))
|
|
11251
11488
|
);
|
|
11252
11489
|
}
|
|
11253
11490
|
}
|
|
@@ -11536,7 +11773,7 @@ function inertLines(result) {
|
|
|
11536
11773
|
|
|
11537
11774
|
// src/lib/portfolio-safety.ts
|
|
11538
11775
|
import { execFile } from "child_process";
|
|
11539
|
-
import { lstat as lstat2, realpath as
|
|
11776
|
+
import { lstat as lstat2, realpath as realpath4 } from "fs/promises";
|
|
11540
11777
|
import { isAbsolute as isAbsolute7, join as join18, relative as relative4, resolve as resolve11 } from "path";
|
|
11541
11778
|
import { promisify } from "util";
|
|
11542
11779
|
import { readManifest as readManifest14 } from "@basou/core";
|
|
@@ -11546,7 +11783,7 @@ function errorCode(error) {
|
|
|
11546
11783
|
}
|
|
11547
11784
|
async function canonical(p) {
|
|
11548
11785
|
try {
|
|
11549
|
-
return await
|
|
11786
|
+
return await realpath4(p);
|
|
11550
11787
|
} catch {
|
|
11551
11788
|
return resolve11(p);
|
|
11552
11789
|
}
|
|
@@ -11715,7 +11952,7 @@ function formatSafetyReport(result) {
|
|
|
11715
11952
|
|
|
11716
11953
|
// src/lib/view-server.ts
|
|
11717
11954
|
import { createServer } from "http";
|
|
11718
|
-
import { basename as
|
|
11955
|
+
import { basename as basename9, join as join19, resolve as resolve12 } from "path";
|
|
11719
11956
|
import {
|
|
11720
11957
|
computeWorkStats as computeWorkStats2,
|
|
11721
11958
|
enumerateApprovals as enumerateApprovals2,
|
|
@@ -11726,7 +11963,7 @@ import {
|
|
|
11726
11963
|
loadTaskEntries as loadTaskEntries2,
|
|
11727
11964
|
readAllEvents as readAllEvents2,
|
|
11728
11965
|
readManifest as readManifest15,
|
|
11729
|
-
readMarkdownFile as
|
|
11966
|
+
readMarkdownFile as readMarkdownFile9,
|
|
11730
11967
|
readSessionYaml as readSessionYaml3,
|
|
11731
11968
|
readTaskFile as readTaskFile2,
|
|
11732
11969
|
renderDecisions as renderDecisions3,
|
|
@@ -12207,6 +12444,12 @@ var VIEW_HTML = `<!doctype html>
|
|
|
12207
12444
|
if (m > 0) return m + 'm ' + (sec < 10 ? '0' : '') + sec + 's';
|
|
12208
12445
|
return sec + 's';
|
|
12209
12446
|
}
|
|
12447
|
+
// A floor under half a second renders as '>=0s' -- true of any total, and
|
|
12448
|
+
// reading as a measured zero. Show those milliseconds instead.
|
|
12449
|
+
function fmtFloor(ms) {
|
|
12450
|
+
var coarse = fmtDur(ms);
|
|
12451
|
+
return coarse === '0s' ? Math.round(ms || 0) + 'ms' : coarse;
|
|
12452
|
+
}
|
|
12210
12453
|
function kvrow(k, v) {
|
|
12211
12454
|
return el('tr', {}, [el('td', { class: 'k', text: k }), el('td', { text: v })]);
|
|
12212
12455
|
}
|
|
@@ -12248,12 +12491,12 @@ var VIEW_HTML = `<!doctype html>
|
|
|
12248
12491
|
timeRows.push(kvrow('model working', fmtDur(t.machineActiveTimeMs) + ' (model compute, subset of active; Codex turn duration on ' + machineSessions + ' of ' + t.sessionCount + ' sessions; not wall-clock-deduped)'));
|
|
12249
12492
|
}
|
|
12250
12493
|
timeRows.push(kvrow('span', fmtDur(t.sessionSpanMs) + (t.openSessionCount > 0 ? ' (' + t.openSessionCount + ' open)' : '')));
|
|
12251
|
-
timeRows.push(kvrow('command', fmtDur(t.commandTimeMs) + (t.commandTimeReliable ? '' : ' (some sessions
|
|
12494
|
+
timeRows.push(kvrow('command', fmtDur(t.commandTimeMs) + (t.commandTimeReliable ? '' : ' (floor: some sessions had no duration observed, or could not be read in full)')));
|
|
12252
12495
|
detail.appendChild(el('table', { class: 'kv' }, [el('tbody', {}, timeRows)]));
|
|
12253
12496
|
if (d.bySource && d.bySource.length) {
|
|
12254
12497
|
detail.appendChild(el('h3', { text: 'By source' }));
|
|
12255
12498
|
d.bySource.forEach(function (s) {
|
|
12256
|
-
var cmd = s.commandTimeReliable ? fmtDur(s.commandTimeMs) : 'n/a';
|
|
12499
|
+
var cmd = s.commandTimeReliable ? fmtDur(s.commandTimeMs) : (s.commandTimeMs > 0 ? '>=' + fmtFloor(s.commandTimeMs) : 'n/a');
|
|
12257
12500
|
var machine = s.machineActiveAvailable ? ', model ' + fmtDur(s.machineActiveTimeMs) : '';
|
|
12258
12501
|
detail.appendChild(el('div', { class: 'row' }, [
|
|
12259
12502
|
el('span', { text: s.sourceKind + ': ' + s.sessionCount + ' sessions, ' + numfmt(s.tokens.output) + ' out tok, active ' + fmtDur(s.activeTimeMs) + machine + ', command ' + cmd })
|
|
@@ -12591,6 +12834,9 @@ async function handleWorkspacePost(res, sub, ws, body, deps, runExclusive) {
|
|
|
12591
12834
|
const result = await runExclusive(
|
|
12592
12835
|
() => refreshAll({ options: actionOptions, ctx: ws.importCtx, paths: ws.paths, nowIso })
|
|
12593
12836
|
);
|
|
12837
|
+
if (result.orientation.status === "generated") {
|
|
12838
|
+
await warnIfPositionNamesOtherWorkspaces({ paths: ws.paths });
|
|
12839
|
+
}
|
|
12594
12840
|
sendJson(res, 200, result);
|
|
12595
12841
|
return true;
|
|
12596
12842
|
}
|
|
@@ -12721,7 +12967,7 @@ async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
|
|
|
12721
12967
|
const remote = await resolveRemoteUrl(abs);
|
|
12722
12968
|
const url = remote !== void 0 ? toBrowserUrl(remote) : null;
|
|
12723
12969
|
return {
|
|
12724
|
-
name:
|
|
12970
|
+
name: basename9(abs),
|
|
12725
12971
|
path: repo.path,
|
|
12726
12972
|
...url !== null ? { url } : {},
|
|
12727
12973
|
...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
|
|
@@ -12778,7 +13024,7 @@ async function taskDetail(ws, taskId) {
|
|
|
12778
13024
|
}
|
|
12779
13025
|
}
|
|
12780
13026
|
async function decisionsView(ws, nowProvider) {
|
|
12781
|
-
const fromDisk = await
|
|
13027
|
+
const fromDisk = await readMarkdownFile9(ws.paths.files.decisions);
|
|
12782
13028
|
if (fromDisk !== null) {
|
|
12783
13029
|
return { body: fromDisk, fromDisk: true };
|
|
12784
13030
|
}
|
|
@@ -12801,7 +13047,7 @@ async function approvalsView(ws, nowProvider) {
|
|
|
12801
13047
|
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
12802
13048
|
}
|
|
12803
13049
|
async function handoffView(ws, nowProvider) {
|
|
12804
|
-
const fromDisk = await
|
|
13050
|
+
const fromDisk = await readMarkdownFile9(ws.paths.files.handoff);
|
|
12805
13051
|
if (fromDisk !== null) {
|
|
12806
13052
|
return { body: fromDisk, fromDisk: true };
|
|
12807
13053
|
}
|
|
@@ -13036,7 +13282,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
|
13036
13282
|
const notFound = error instanceof Error && error.message === "YAML file not found";
|
|
13037
13283
|
return {
|
|
13038
13284
|
key: `ws-${createHash2("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
|
|
13039
|
-
label: labelOverride ??
|
|
13285
|
+
label: labelOverride ?? basename10(repoRoot),
|
|
13040
13286
|
paths,
|
|
13041
13287
|
repoRoot,
|
|
13042
13288
|
importCtx,
|