@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 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: "0.1.0",
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: "0.1.0",
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 sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
714
- return `${existing}${sep}${wrapped}`;
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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
- duration_ms: result.duration_ms
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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
- duration_ms: 0
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: "0.1.0",
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: "0.1.0",
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 realpath2, stat as stat4 } from "fs/promises";
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 readMarkdownFile5,
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 readMarkdownFile4,
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 basename3, dirname as dirname2, join as join8, resolve as resolve5 } from "path";
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 = basename3(file, ".jsonl");
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: dirname2(paths.root),
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 !== "0.1.0") {
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 readMarkdownFile4(paths.files.handoff);
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 readMarkdownFile4(paths.files.decisions);
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 SessionStart hook: print the current position of the workspace Codex was opened in (read from the payload's cwd) so Codex adds it to that session's context. Stays silent outside a basou workspace; never fails the session."
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 realpath2(root).catch(() => root);
3733
+ const rootReal = await realpath3(root).catch(() => root);
3543
3734
  for (const entry of entries) {
3544
- const entryReal = await realpath2(entry.path).catch(() => null);
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 readMarkdownFile5(facePath);
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 basename4, relative, resolve as resolve6 } from "path";
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 ?? basename4(repositoryRoot);
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: "0.1.0",
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 basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve7 } from "path";
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 readMarkdownFile6,
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 sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
5155
+ const sep2 = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
4964
5156
  try {
4965
- writeFileSync(file, `${existing}${sep}${plan.toAdd.join("\n")}
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: basename5(real),
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: basename5(real),
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", basename5(real), CANONICAL_FILE);
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: basename5(real),
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(dirname3(filePath), { recursive: true });
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 = basename5(realpathSync(resolve7(repositoryRoot, entry.path)));
5379
+ name = basename6(realpathSync(resolve7(repositoryRoot, entry.path)));
5188
5380
  } catch {
5189
- name = basename5(resolve7(repositoryRoot, entry.path));
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 = basename5(viewDir);
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(dirname3(filePath), { recursive: true });
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(dirname3(abs)), basename5(abs));
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 = basename5(repoReal);
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(dirname3(filePath), { recursive: true });
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) => basename5(resolve7(repositoryRoot, entry.path)));
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 = basename5(real);
5974
+ const canonicalName = basename6(real);
5783
5975
  let content;
5784
5976
  try {
5785
- content = await readMarkdownFile6(canonicalFileFor(anchorReal, canonicalName));
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 = basename5(real ?? abs);
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(dirname3(file), { recursive: true });
5894
- const existing = await readMarkdownFile6(file);
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(dirname3(file), { recursive: true });
5908
- const existing = await readMarkdownFile6(file);
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 ? basename5(resolveViewDir(repositoryRoot, 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 = basename5(real);
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 = basename5(repoReal ?? targetAbs);
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 basename5(rReal).toLowerCase() === cnFold;
6440
+ return basename6(rReal).toLowerCase() === cnFold;
6249
6441
  }
6250
6442
  if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
6251
- return basename5(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
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/${basename5(result.target)}/AGENTS.md)`);
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 = basename5(repositoryRoot);
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 ? basename5(resolveViewDir(repositoryRoot, 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: basename5(repositoryRoot),
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 = basename5(argReal ?? argAbs);
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(dirname3(canonicalFile), { recursive: true });
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 = basename5(viewDir);
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 readMarkdownFile6(file);
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 ? basename5(resolveViewDir(repositoryRoot, 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 readMarkdownFile7 } from "@basou/core";
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 readMarkdownFile7(target);
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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
- duration_ms: result.duration_ms
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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: "0.1.0",
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
- duration_ms: 0
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: "0.1.0",
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: "0.1.0",
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 basename6, isAbsolute as isAbsolute6, join as join15, relative as relative3 } from "path";
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) => printReplayWarning(w, sessionId)
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(session.session.id, session.session, events, now);
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 (import)"
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
- return `${executorPart}${argsPart} (${exitPart}, ${ev.duration_ms}ms)`;
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 !== "0.1.0") {
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 ${basename6(options.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: "0.1.0",
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 (e.g. claude-code-import) report 0 shell time";
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 basename9, resolve as resolve13 } from "path";
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 basename7, dirname as dirname4, join as join17 } from "path";
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(basename7(dirname4(file)))
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 realpath3 } from "fs/promises";
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 realpath3(p);
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 basename8, join as join19, resolve as resolve12 } from "path";
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 readMarkdownFile8,
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 report 0)')));
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: basename8(abs),
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 readMarkdownFile8(ws.paths.files.decisions);
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 readMarkdownFile8(ws.paths.files.handoff);
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 ?? basename9(repoRoot),
13285
+ label: labelOverride ?? basename10(repoRoot),
13040
13286
  paths,
13041
13287
  repoRoot,
13042
13288
  importCtx,