@dev.fast/whiteboard 0.2.4 → 0.2.5-preview.20261013.109

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.
Files changed (77) hide show
  1. package/dist/{account-alias-_1_7vROx.js → account-alias-D0hF0sKk.js} +2 -3
  2. package/dist/{agent-cli-BrFqhnzM.js → agent-cli-BBPxwUOd.js} +6 -6
  3. package/dist/agent-cli-YSuhE5X8.js +2 -0
  4. package/dist/{agent-client-B_b1GtUc.js → agent-client-P3J4y8Zn.js} +3 -3
  5. package/dist/{authoring-tools-DvIO906l.js → authoring-tools-D8WXsm_g.js} +11 -10
  6. package/dist/build-info.json +1 -1
  7. package/dist/{cli-gRGxkwc0.js → cli-C0ziP6hg.js} +1 -1
  8. package/dist/{cli-runner-Cjl8h376.js → cli-runner-C1gY-HAI.js} +16 -17
  9. package/dist/cli.js +1 -1
  10. package/dist/{client-BFClfQLO.js → client-DzcSt6pV.js} +63 -13
  11. package/dist/{desktop-discovery-C-Qz54wO.js → desktop-discovery-Ba3dD5Cn.js} +1 -1
  12. package/dist/{headless-host-DcWaxx9P.js → headless-host-BHD_Vg5F.js} +4 -4
  13. package/dist/{local-data-Bm84Xdck.js → local-data-BlERtVm2.js} +281 -75
  14. package/dist/{mcp-BDy9Aybd.js → mcp-t7VN9R3k.js} +4 -4
  15. package/dist/{package-paths-B6-zxvIO.js → package-paths-B9PDoAl6.js} +1 -1
  16. package/dist/{process-error-telemetry-Dzh5a3s_.js → process-error-telemetry-C0swByfp.js} +158 -57
  17. package/dist/{profile-RDEioF8b.js → profile-BsYDBr7t.js} +2 -2
  18. package/dist/profile-j97aRTS7.js +2 -0
  19. package/dist/{review-agent-traces-DNJFYzc8.js → review-home-paths-Y9mYWkCI.js} +2600 -62
  20. package/dist/{review-telemetry-DBy3eVCo.js → review-telemetry-CmM6tCbj.js} +2 -2
  21. package/dist/server/desktop-host.js +35 -46
  22. package/dist/{server-discovery-Dbzn3w6Z.js → server-discovery-ZVi8Ay8J.js} +1 -1
  23. package/dist/sharing/index.d.ts +37 -49
  24. package/dist/sharing/index.js +1 -1
  25. package/dist/{src-CmdiBL20.js → src-DiHP11l3.js} +11 -1
  26. package/dist/{stored-document-migration-BxZWQQjv.js → stored-document-migration-u4L_1VZa.js} +15 -36
  27. package/dist/{tool-failure-DJmTieh5.js → tool-failure-CJHBCHMh.js} +2 -2
  28. package/instructions/scratchpad.md +1 -1
  29. package/package.json +5 -4
  30. package/src/ask/agents.ts +6 -0
  31. package/src/ask/thread-state.ts +17 -1
  32. package/src/ask/thread.ts +116 -22
  33. package/src/ask/threads.ts +8 -2
  34. package/src/error-telemetry.ts +13 -9
  35. package/src/fixtures/blocks/tutorial.json +3 -40
  36. package/src/review-api/README.md +10 -7
  37. package/src/review-api/ask-history.ts +37 -0
  38. package/src/review-api/authoring-tools.ts +8 -5
  39. package/src/review-api/blocks/database_lens.ts +5 -0
  40. package/src/review-api/blocks/tutorial.ts +2 -18
  41. package/src/review-api/comparison-coverage.ts +16 -9
  42. package/src/review-api/component-reference.ts +1 -1
  43. package/src/review-api/document.ts +24 -10
  44. package/src/review-api/http.ts +267 -189
  45. package/src/review-api/local-data.ts +126 -37
  46. package/src/review-api/store-schema.ts +25 -1
  47. package/src/review-api/store.ts +165 -33
  48. package/src/review-api/workspaces.ts +14 -14
  49. package/src/review-api/worktree-events.ts +46 -0
  50. package/src/review-app.ts +1 -1
  51. package/src/review-info.ts +1 -1
  52. package/src/review-stack.ts +2 -2
  53. package/src/server/review-server-core.ts +25 -1
  54. package/src/server/tutorial-service.ts +22 -46
  55. package/src/sharing/export.ts +1 -0
  56. package/src/sharing/import.ts +111 -26
  57. package/tutorial/document.json +27 -86
  58. package/tutorial/lenses.json +10 -0
  59. package/tutorial/runtime-manifest.json +2 -3
  60. package/tutorial/sample-service/src/database/drizzle-orm.d.ts +16 -0
  61. package/tutorial/sample-service/src/database/schema.ts +9 -10
  62. package/tutorial/sample-service/src/inventory/forklift-mood.ts +4 -0
  63. package/tutorial/sample-service/src/inventory/inventory-service.ts +18 -1
  64. package/tutorial/sample-service/src/inventory/warehouse-lore.ts +11 -0
  65. package/tutorial/sample-service/src/orders/order-service.ts +3 -2
  66. package/tutorial/sample-service/src/orders/order.ts +27 -5
  67. package/tutorial/sample-service/src/payments/fraud-vibes.ts +4 -0
  68. package/tutorial/sample-service/src/payments/payment-gateway.ts +19 -2
  69. package/tutorial/sample-service/src/payments/raccoon-ledger.ts +9 -0
  70. package/dist/agent-cli-BR2j-2n5.js +0 -2
  71. package/dist/input-error-OLgB_h31.js +0 -11
  72. package/dist/profile-BQGaX8aX.js +0 -2
  73. package/dist/review-home-paths-6zZH-1c9.js +0 -2560
  74. package/dist/tutorial-trace-DrBvkaeL.js +0 -43
  75. package/src/tutorial-conversation.ts +0 -18
  76. package/tutorial/software-map.json +0 -139
  77. package/tutorial/trace.json +0 -20
@@ -1,19 +1,21 @@
1
- import { d as gitAt, u as git, v as resolveRepoContext } from "./src-CmdiBL20.js";
1
+ import { d as gitAt, u as git, v as resolveRepoContext } from "./src-DiHP11l3.js";
2
2
  import { a as jsonNumber, c as jsonString, i as jsonBoolean, m as isStringValue, n as isJsonObject, o as jsonObject, r as jsonArray, t as isJsonArray, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { a as TRACE_STORE_CLIENT_ID, b as commitShaSchema, c as deleteStoreResponseSchema, d as listUploadsResponseSchema, f as storeErrorCodeSchema, g as uploadManifestMismatch, h as traceObjectNameSchema, i as storeRoutes, l as listSessionsResponseSchema, m as storeResponseSchema, n as DEVICE_TOKEN_PATH, o as beginUploadResponseSchema, p as storeErrorEnvelopeSchema, r as SESSION_PATH, s as completeUploadResponseSchema, t as DEVICE_CODE_PATH, x as sessionIdSchema } from "./src-CGP5ytbV.js";
4
- import { A as devReviewHome, E as writeTraceConfigFile, M as writeFileAtomic, P as writePrivateJsonAtomic, S as readTraceConfigFile, _ as emptyTraceConfig, c as resolveS3Setup, f as traceEnvValue$1, g as currentStore, h as TraceConfigurationError, i as isS3MockMode, j as normalizeStoreOrigin, l as s3CacheIdentity, m as DEFAULT_HOSTED_ORIGIN, n as clearTraceEnvCache$1, u as s3MockRoot, v as enabledOriginsOf, w as s3Store, x as hostedOrigin, y as hasHostedStore } from "./s3-config-CAxhO9u_.js";
3
+ import { a as TRACE_STORE_CLIENT_ID, b as commitShaSchema, c as deleteStoreResponseSchema, d as listUploadsResponseSchema, f as storeErrorCodeSchema, g as uploadManifestMismatch, h as traceObjectNameSchema, i as storeRoutes, l as listSessionsResponseSchema, m as storeResponseSchema, n as DEVICE_TOKEN_PATH, o as beginUploadResponseSchema, p as storeErrorEnvelopeSchema, r as SESSION_PATH, s as completeUploadResponseSchema, t as DEVICE_CODE_PATH, u as listUploadsQuerySchema, x as sessionIdSchema } from "./src-CGP5ytbV.js";
4
+ import { A as devReviewHome, E as writeTraceConfigFile, M as writeFileAtomic, N as writeFileAtomicAsync, P as writePrivateJsonAtomic, S as readTraceConfigFile, _ as emptyTraceConfig, b as hostedCaptureEnabled, c as resolveS3Setup, d as traceEnvPath, f as traceEnvValue$1, g as currentStore, h as TraceConfigurationError, i as isS3MockMode, j as normalizeStoreOrigin, l as s3CacheIdentity, m as DEFAULT_HOSTED_ORIGIN, n as clearTraceEnvCache$1, p as traceSettingsPath, u as s3MockRoot, v as enabledOriginsOf, w as s3Store, x as hostedOrigin, y as hasHostedStore } from "./s3-config-CAxhO9u_.js";
5
5
  import { t as errorMessage } from "./error-message-OtiDonty.js";
6
- import { createHash, randomBytes } from "node:crypto";
6
+ import crypto, { createHash, randomBytes } from "node:crypto";
7
7
  import { z } from "zod";
8
- import { closeSync, createReadStream, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
9
- import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, stat } from "node:fs/promises";
8
+ import { accessSync, closeSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
9
+ import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
10
10
  import path from "node:path";
11
11
  import os, { homedir, tmpdir } from "node:os";
12
- import { spawn } from "node:child_process";
12
+ import { execFile, spawn } from "node:child_process";
13
+ import { promisify } from "node:util";
13
14
  import { Readable, Transform } from "node:stream";
14
15
  import { AsyncLocalStorage } from "node:async_hooks";
15
16
  import { pipeline } from "node:stream/promises";
16
17
  import { createGunzip, createGzip } from "node:zlib";
18
+ import { InvalidArgumentError, Option } from "commander";
17
19
  //#region ../trace-protocol/src/event-text.ts
18
20
  function extractTraceEventText$1(event) {
19
21
  if (!event) return "";
@@ -488,6 +490,638 @@ async function runStoreWhoami(input) {
488
490
  return 0;
489
491
  }
490
492
  //#endregion
493
+ //#region ../trace-core/src/trace-command.ts
494
+ /** Resolves the machine paths and environment shared by one CLI run. */
495
+ function traceScope(input = {}) {
496
+ const env = input.env ?? process.env;
497
+ const homeDir = input.homeDir ?? os.homedir();
498
+ return {
499
+ homeDir,
500
+ env,
501
+ devHome: devReviewHome(env, homeDir)
502
+ };
503
+ }
504
+ /** The executable name used in trace hooks. */
505
+ function traceCliName() {
506
+ return "whiteboard";
507
+ }
508
+ /** Returns the configured trace home, then the operating-system home. */
509
+ function traceHomeDir(env = process.env) {
510
+ return env.TRACE_HOME_DIR ?? os.homedir();
511
+ }
512
+ /**
513
+ * Resolves an explicit command, the environment, an installed command, or
514
+ * the configured CLI name on PATH, in that order.
515
+ */
516
+ function resolveTraceCommand(input = {}) {
517
+ if (isStringValue(input.explicit)) return { file: input.explicit };
518
+ if (input.explicit) return input.explicit;
519
+ const env = input.env ?? process.env;
520
+ if (env.REVIEW_TRACE_COMMAND) return { file: env.REVIEW_TRACE_COMMAND };
521
+ const installed = path.join(input.homeDir ?? traceHomeDir(env), ".local", "bin", "whiteboard");
522
+ if (existsSync(installed)) return { file: installed };
523
+ return { file: (env.PATH ?? "").split(path.delimiter).filter((directory) => path.isAbsolute(directory)).map((directory) => path.join(directory, "whiteboard")).find(isLiveTraceExecutable) ?? "whiteboard" };
524
+ }
525
+ /** Quotes one value for a POSIX shell command. */
526
+ function shellQuote(value) {
527
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
528
+ }
529
+ /** Renders the executable and each leading argument as quoted shell words. */
530
+ function renderTraceCommand(command) {
531
+ return [command.file, ...command.args ?? []].map(shellQuote).join(" ");
532
+ }
533
+ /** Only stable absolute commands can retain ownership across installations. */
534
+ function isLiveTraceExecutable(file) {
535
+ if (!file || !path.isAbsolute(file)) return false;
536
+ try {
537
+ accessSync(file, constants.X_OK);
538
+ return true;
539
+ } catch {
540
+ return false;
541
+ }
542
+ }
543
+ /** Keep an existing working installation; a missing executable can be repaired. */
544
+ function keepTraceExecutable(existing, wanted) {
545
+ return existing !== wanted && isLiveTraceExecutable(existing);
546
+ }
547
+ /** The first shell word from our rendered Git hook command. */
548
+ function traceCommandExecutable(command) {
549
+ if (!command) return void 0;
550
+ const quoted = /^'((?:[^']|'"'"')*)'(?:\s|$)/.exec(command);
551
+ const bare = /^([^\s']+)(?:\s|$)/.exec(command);
552
+ return quoted ? quoted[1].replaceAll(`'"'"'`, "'") : bare?.[1];
553
+ }
554
+ //#endregion
555
+ //#region ../trace-core/src/agent-trace-hooks.ts
556
+ /** Every harness whose trace hook this package writes, in report order. */
557
+ const AGENT_TRACE_HOOK_AGENTS = [
558
+ "claude",
559
+ "codex",
560
+ "opencode",
561
+ "pi"
562
+ ];
563
+ const PI_EXTENSION_MARKER = "Managed by Review Desktop trace setup";
564
+ const OPENCODE_TRACE_PLUGIN_MARKER = PI_EXTENSION_MARKER;
565
+ function claudeSettingsPath(homeDir) {
566
+ return path.join(homeDir, ".claude", "settings.json");
567
+ }
568
+ function codexConfigPath(homeDir) {
569
+ return path.join(homeDir, ".codex", "config.toml");
570
+ }
571
+ function piExtensionPath(homeDir) {
572
+ return path.join(homeDir, ".pi", "agent", "extensions", "review-trace.ts");
573
+ }
574
+ /** The configuration base OpenCode reads: `$XDG_CONFIG_HOME`, else `~/.config`. */
575
+ function configHome(homeDir, env = process.env) {
576
+ return env.XDG_CONFIG_HOME?.trim() || path.join(homeDir, ".config");
577
+ }
578
+ function openCodeDirectory(homeDir, env = process.env) {
579
+ return path.join(configHome(homeDir, env), "opencode");
580
+ }
581
+ function openCodePluginPath(homeDir, env = process.env) {
582
+ return path.join(openCodeDirectory(homeDir, env), "plugins", "review-trace.ts");
583
+ }
584
+ function executableOwner(file) {
585
+ const base = path.basename(file);
586
+ return base === "review" || base === "whiteboard" ? "review" : null;
587
+ }
588
+ /** The executable of a single lifecycle command, or undefined for a shell compound. */
589
+ function traceHookCommandFile(command) {
590
+ const text = jsonString(command);
591
+ if (text === void 0) return void 0;
592
+ const match = /^(.*) trace hook (SessionStart|UserPromptSubmit|SessionEnd)$/.exec(text);
593
+ if (!match) return void 0;
594
+ const prefix = match[1];
595
+ if (/^[a-zA-Z0-9_./-]+$/.test(prefix)) return prefix;
596
+ const decoded = prefix.slice(1, -1).replaceAll(`'"'"'`, "'");
597
+ return shellQuote(decoded) === prefix ? decoded : void 0;
598
+ }
599
+ /** Identifies a single executable lifecycle command; never accepts shell compounds. */
600
+ function traceHookCommandOwner(command) {
601
+ const file = traceHookCommandFile(command);
602
+ return file === void 0 ? null : executableOwner(file);
603
+ }
604
+ function extensionCommandFile(source) {
605
+ if (!source.trimStart().startsWith(`// ${PI_EXTENSION_MARKER}`)) return void 0;
606
+ const match = /spawn\(("(?:[^"\\]|\\.)*"), \["trace", "hook"/.exec(source);
607
+ if (!match) return void 0;
608
+ return jsonString(parseJsonText(match[1]));
609
+ }
610
+ function extensionOwner(source) {
611
+ const file = extensionCommandFile(source);
612
+ return file === void 0 ? null : executableOwner(file);
613
+ }
614
+ function piExtensionSource(reviewCommand) {
615
+ return `// ${PI_EXTENSION_MARKER}
616
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
617
+ import { spawn } from "node:child_process";
618
+
619
+ export default function (pi: ExtensionAPI) {
620
+ pi.on("session_start", async (_event, ctx) => {
621
+ const sessionId = ctx.sessionManager.getSessionId();
622
+ if (!sessionId) return;
623
+ runTraceHook("SessionStart", sessionId, ctx.cwd);
624
+ });
625
+
626
+ pi.on("turn_start", async (_event, ctx) => {
627
+ const sessionId = ctx.sessionManager.getSessionId();
628
+ if (!sessionId) return;
629
+ runTraceHook("TurnStart", sessionId, ctx.cwd);
630
+ });
631
+
632
+ pi.on("session_shutdown", async (_event, ctx) => {
633
+ const sessionId = ctx.sessionManager.getSessionId();
634
+ if (!sessionId) return;
635
+ runTraceHook("SessionEnd", sessionId, ctx.cwd);
636
+ });
637
+ }
638
+
639
+ function runTraceHook(eventName: string, sessionId: string, cwd: string) {
640
+ const payload = JSON.stringify({
641
+ hook_event_name: eventName,
642
+ session_id: sessionId,
643
+ });
644
+
645
+ const proc = spawn(${JSON.stringify(reviewCommand)}, ["trace", "hook", eventName], {
646
+ cwd,
647
+ stdio: ["pipe", "ignore", "ignore"],
648
+ });
649
+
650
+ // Missing binaries and closed pipes must never crash the harness.
651
+ proc.on("error", () => {});
652
+ proc.stdin.on("error", () => {});
653
+ proc.stdin.end(payload);
654
+ }
655
+ `;
656
+ }
657
+ function openCodeTracePluginSource(reviewCommand) {
658
+ return `// ${OPENCODE_TRACE_PLUGIN_MARKER}
659
+ import { spawn } from "node:child_process";
660
+
661
+ interface OpenCodeEvent {
662
+ type: string;
663
+ properties: Record<string, unknown>;
664
+ }
665
+
666
+ // OpenCode has no session-end event. A session goes idle after each turn,
667
+ // so every turn is one SessionStart/UserPromptSubmit .. SessionEnd cycle:
668
+ // the prompt registers the session for commit stamping and idle syncs the
669
+ // trace. Child sessions spawned by the task tool stay attached to their
670
+ // parent's turn and are never registered on their own.
671
+ export default async function reviewTracePlugin(input: { directory: string }) {
672
+ const directories = new Map<string, string>();
673
+ const childSessions = new Set<string>();
674
+
675
+ return {
676
+ event: async ({ event }: { event: OpenCodeEvent }) => {
677
+ const properties = event.properties;
678
+ if (event.type === "session.created") {
679
+ const info = record(properties.info);
680
+ const sessionId = text(info.id);
681
+ if (!sessionId) return;
682
+ if (typeof info.parentID === "string") {
683
+ childSessions.add(sessionId);
684
+ return;
685
+ }
686
+ const directory = text(info.directory) ?? input.directory;
687
+ directories.set(sessionId, directory);
688
+ runTraceHook("SessionStart", sessionId, directory);
689
+ return;
690
+ }
691
+ if (event.type === "message.updated") {
692
+ const info = record(properties.info);
693
+ if (info.role !== "user") return;
694
+ const sessionId = text(info.sessionID);
695
+ if (!sessionId || childSessions.has(sessionId)) return;
696
+ runTraceHook(
697
+ "UserPromptSubmit",
698
+ sessionId,
699
+ directories.get(sessionId) ?? input.directory,
700
+ );
701
+ return;
702
+ }
703
+ if (event.type === "session.idle") {
704
+ const sessionId = text(properties.sessionID);
705
+ if (!sessionId || childSessions.has(sessionId)) return;
706
+ runTraceHook(
707
+ "SessionEnd",
708
+ sessionId,
709
+ directories.get(sessionId) ?? input.directory,
710
+ );
711
+ }
712
+ },
713
+ };
714
+ }
715
+
716
+ function record(value: unknown): Record<string, unknown> {
717
+ return value && typeof value === "object"
718
+ ? (value as Record<string, unknown>)
719
+ : {};
720
+ }
721
+
722
+ function text(value: unknown): string | undefined {
723
+ return typeof value === "string" && value ? value : undefined;
724
+ }
725
+
726
+ function runTraceHook(eventName: string, sessionId: string, cwd: string) {
727
+ const payload = JSON.stringify({
728
+ hook_event_name: eventName,
729
+ session_id: sessionId,
730
+ });
731
+
732
+ const proc = spawn(${JSON.stringify(reviewCommand)}, ["trace", "hook", eventName], {
733
+ cwd,
734
+ stdio: ["pipe", "ignore", "ignore"],
735
+ });
736
+
737
+ // Missing binaries and closed pipes must never crash the harness.
738
+ proc.on("error", () => {});
739
+ proc.stdin.on("error", () => {});
740
+ proc.stdin.end(payload);
741
+ }
742
+ `;
743
+ }
744
+ /**
745
+ * Installs the session lifecycle hook of every harness this package owns.
746
+ *
747
+ * `harnessHooks: false` installs none, so one caller can keep the option its
748
+ * command registers. The result names each file that was written, in the
749
+ * order the installers ran.
750
+ */
751
+ async function installHarnessHooks(input) {
752
+ const installed = [];
753
+ const skipped = [];
754
+ if (input.harnessHooks === false) return {
755
+ installed,
756
+ skipped
757
+ };
758
+ const env = input.env ?? process.env;
759
+ for (const agent of AGENT_TRACE_HOOK_AGENTS) {
760
+ const present = existsSync(agentTraceHomeDirectory(agent, input.homeDir, env));
761
+ if (input.allHarnesses !== true && !present) {
762
+ skipped.push(agent);
763
+ continue;
764
+ }
765
+ const result = await HARNESS_HOOK_INSTALLERS[agent](input.homeDir, input.executable, env);
766
+ installed.push(result);
767
+ }
768
+ return {
769
+ installed,
770
+ skipped
771
+ };
772
+ }
773
+ /** The installer of one harness hook, keyed by the harness. */
774
+ const HARNESS_HOOK_INSTALLERS = {
775
+ claude: installClaudeTraceHook,
776
+ codex: installCodexTraceHook,
777
+ opencode: installOpenCodeTraceExtension,
778
+ pi: installPiTraceExtension
779
+ };
780
+ /** One line that names the harnesses an install left alone. */
781
+ function skippedHarnessesLine(skipped) {
782
+ return `Skipped the ${skipped.join(", ")} hook${skipped.length === 1 ? "" : "s"}: this machine has no such harness. Use --all-harnesses to write them anyway.\n`;
783
+ }
784
+ /**
785
+ * Idempotently configures Claude Code session lifecycle hooks in ~/.claude/settings.json.
786
+ */
787
+ async function installClaudeTraceHook(homeDir = os.homedir(), reviewCommand = traceCliName()) {
788
+ const settingsDir = path.join(homeDir, ".claude");
789
+ const settingsPath = claudeSettingsPath(homeDir);
790
+ let parsed = {};
791
+ if (existsSync(settingsPath)) try {
792
+ const content = parseJsonText(await readFile(settingsPath, "utf8"));
793
+ if (isJsonObject(content)) parsed = content;
794
+ } catch {
795
+ parsed = {};
796
+ }
797
+ const hooks = isJsonObject(parsed.hooks) ? parsed.hooks : {};
798
+ let modified = false;
799
+ let keptCommand;
800
+ const hookCommand = (eventName) => ({
801
+ type: "command",
802
+ command: `${shellCommand(reviewCommand)} trace hook ${eventName}`
803
+ });
804
+ for (const eventName of [
805
+ "SessionStart",
806
+ "UserPromptSubmit",
807
+ "SessionEnd"
808
+ ]) {
809
+ const existing = hooks[eventName];
810
+ const group = isJsonArray(existing) ? existing : [];
811
+ const wanted = hookCommand(eventName);
812
+ let found = false;
813
+ for (const entry of group) {
814
+ if (!isJsonObject(entry) || !isJsonArray(entry.hooks)) continue;
815
+ for (const hook of entry.hooks) {
816
+ if (!isJsonObject(hook)) continue;
817
+ const file = traceHookCommandFile(hook.command);
818
+ if (file === void 0 || executableOwner(file) === null) continue;
819
+ found = true;
820
+ if (keepTraceExecutable(file, reviewCommand)) {
821
+ keptCommand = file;
822
+ continue;
823
+ }
824
+ if (hook.command !== wanted.command) {
825
+ hook.command = wanted.command;
826
+ modified = true;
827
+ }
828
+ }
829
+ }
830
+ if (!found) {
831
+ group.push({ hooks: [wanted] });
832
+ modified = true;
833
+ }
834
+ hooks[eventName] = group;
835
+ }
836
+ if (modified || !existsSync(settingsPath)) {
837
+ parsed.hooks = hooks;
838
+ await mkdir(settingsDir, { recursive: true });
839
+ await writeFile(settingsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
840
+ }
841
+ const result = {
842
+ agent: "claude",
843
+ path: settingsPath,
844
+ modified
845
+ };
846
+ if (keptCommand) result.keptCommand = keptCommand;
847
+ return result;
848
+ }
849
+ /**
850
+ * Idempotently configures Codex session lifecycle hooks in ~/.codex/config.toml.
851
+ */
852
+ async function installCodexTraceHook(homeDir = os.homedir(), reviewCommand = traceCliName()) {
853
+ const codexDir = path.join(homeDir, ".codex");
854
+ const configPath = codexConfigPath(homeDir);
855
+ let existing = "";
856
+ if (existsSync(configPath)) existing = await readFile(configPath, "utf8");
857
+ const hookEvents = [
858
+ "SessionStart",
859
+ "UserPromptSubmit",
860
+ "SessionEnd"
861
+ ];
862
+ const found = /* @__PURE__ */ new Set();
863
+ let keptCommand;
864
+ let next = transformCodexHooks(existing, (block, command, event) => {
865
+ const file = traceHookCommandFile(command);
866
+ if (file === void 0 || executableOwner(file) === null) return block;
867
+ found.add(event);
868
+ if (keepTraceExecutable(file, reviewCommand)) {
869
+ keptCommand = file;
870
+ return block;
871
+ }
872
+ return block.replace(/^command = .*$/m, () => `command = ${JSON.stringify(`${shellCommand(reviewCommand)} trace hook ${event}`)}`);
873
+ });
874
+ const missing = hookEvents.filter((event) => !found.has(event));
875
+ if (missing.length > 0) next = existing ? `${next.trimEnd()}\n\n${missing.map((event) => codexTraceHookToml(event, reviewCommand)).join("\n\n")}\n` : codexHookBlock(reviewCommand).trimStart();
876
+ const result = {
877
+ agent: "codex",
878
+ path: configPath,
879
+ modified: next !== existing
880
+ };
881
+ if (keptCommand) result.keptCommand = keptCommand;
882
+ if (!result.modified) return result;
883
+ await mkdir(codexDir, { recursive: true });
884
+ await writeFile(configPath, next, "utf8");
885
+ return result;
886
+ }
887
+ /**
888
+ * Idempotently writes the Pi session lifecycle extension into ~/.pi/agent/extensions/review-trace.ts.
889
+ */
890
+ async function installPiTraceExtension(homeDir = os.homedir(), reviewCommand = traceCliName()) {
891
+ const extensionsDir = path.join(homeDir, ".pi", "agent", "extensions");
892
+ const extensionPath = piExtensionPath(homeDir);
893
+ let existing = "";
894
+ if (existsSync(extensionPath)) existing = await readFile(extensionPath, "utf8");
895
+ const existingCommand = extensionCommandFile(existing);
896
+ if (keepTraceExecutable(existingCommand, reviewCommand)) return {
897
+ agent: "pi",
898
+ path: extensionPath,
899
+ modified: false,
900
+ keptCommand: existingCommand
901
+ };
902
+ const source = piExtensionSource(reviewCommand);
903
+ if (existing.trim() === source.trim()) return {
904
+ agent: "pi",
905
+ path: extensionPath,
906
+ modified: false
907
+ };
908
+ await mkdir(extensionsDir, { recursive: true });
909
+ await writeFile(extensionPath, source, "utf8");
910
+ return {
911
+ agent: "pi",
912
+ path: extensionPath,
913
+ modified: true
914
+ };
915
+ }
916
+ /**
917
+ * Idempotently writes the OpenCode trace plugin into ~/.config/opencode/plugins/review-trace.ts.
918
+ */
919
+ async function installOpenCodeTraceExtension(homeDir = os.homedir(), reviewCommand = traceCliName(), env = process.env) {
920
+ const pluginPath = openCodePluginPath(homeDir, env);
921
+ const pluginsDir = path.dirname(pluginPath);
922
+ let existing = "";
923
+ if (existsSync(pluginPath)) existing = await readFile(pluginPath, "utf8");
924
+ const existingCommand = extensionCommandFile(existing);
925
+ if (keepTraceExecutable(existingCommand, reviewCommand)) return {
926
+ agent: "opencode",
927
+ path: pluginPath,
928
+ modified: false,
929
+ keptCommand: existingCommand
930
+ };
931
+ const source = openCodeTracePluginSource(reviewCommand);
932
+ if (existing.trim() === source.trim()) return {
933
+ agent: "opencode",
934
+ path: pluginPath,
935
+ modified: false
936
+ };
937
+ await mkdir(pluginsDir, { recursive: true });
938
+ await writeFile(pluginPath, source, "utf8");
939
+ return {
940
+ agent: "opencode",
941
+ path: pluginPath,
942
+ modified: true
943
+ };
944
+ }
945
+ /** Removes Review hooks; a scoped removal preserves another live executable. */
946
+ async function removeAgentTraceHook(agent, homeDir = os.homedir(), env = process.env, expectedCommand) {
947
+ const removable = (file) => expectedCommand === void 0 || !keepTraceExecutable(file, expectedCommand);
948
+ if (agent === "claude") {
949
+ const settingsPath = claudeSettingsPath(homeDir);
950
+ if (!existsSync(settingsPath)) return false;
951
+ let parsed;
952
+ try {
953
+ parsed = parseJsonText(await readFile(settingsPath, "utf8"));
954
+ } catch {
955
+ return false;
956
+ }
957
+ if (!isJsonObject(parsed)) return false;
958
+ const hooks = parsed.hooks;
959
+ if (!isJsonObject(hooks)) return false;
960
+ let changed = false;
961
+ for (const eventName of [
962
+ "SessionStart",
963
+ "UserPromptSubmit",
964
+ "SessionEnd"
965
+ ]) {
966
+ const existing = hooks[eventName];
967
+ const keptGroups = (isJsonArray(existing) ? existing : []).flatMap((group) => {
968
+ if (!isJsonObject(group)) return [group];
969
+ if (!isJsonArray(group.hooks)) return [group];
970
+ const keptHooks = group.hooks.filter((hook) => {
971
+ const command = isJsonObject(hook) ? hook.command : void 0;
972
+ const owned = traceHookCommandOwner(command) === "review" && removable(traceHookCommandFile(command));
973
+ if (owned) changed = true;
974
+ return !owned;
975
+ });
976
+ return keptHooks.length > 0 ? [{
977
+ ...group,
978
+ hooks: keptHooks
979
+ }] : [];
980
+ });
981
+ if (keptGroups.length > 0) hooks[eventName] = keptGroups;
982
+ else delete hooks[eventName];
983
+ }
984
+ if (!changed) return false;
985
+ parsed.hooks = hooks;
986
+ await writeFile(settingsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
987
+ return true;
988
+ }
989
+ if (agent === "codex") {
990
+ const configPath = codexConfigPath(homeDir);
991
+ if (!existsSync(configPath)) return false;
992
+ const existing = await readFile(configPath, "utf8");
993
+ const removed = transformCodexHooks(existing, (block, command) => traceHookCommandOwner(command) === "review" && removable(traceHookCommandFile(command)) ? "" : block).replace(/# review-trace-hooks:start\n\s*# review-trace-hooks:end\n?/, "");
994
+ if (removed === existing) return false;
995
+ const next = removed.replace(/\n{3,}/g, "\n\n");
996
+ await writeFile(configPath, next.trim() ? `${next.trimEnd()}\n` : "", "utf8");
997
+ return true;
998
+ }
999
+ const extensionPath = agent === "pi" ? piExtensionPath(homeDir) : openCodePluginPath(homeDir, env);
1000
+ if (!existsSync(extensionPath)) return false;
1001
+ const existing = await readFile(extensionPath, "utf8");
1002
+ if (extensionOwner(existing) !== "review" || !removable(extensionCommandFile(existing))) return false;
1003
+ await rm(extensionPath, { force: true });
1004
+ return true;
1005
+ }
1006
+ function shellCommand(command) {
1007
+ return command === traceCliName() ? command : shellQuote(command);
1008
+ }
1009
+ function codexTraceHookToml(eventName, reviewCommand) {
1010
+ const status = eventName === "SessionStart" ? "\nstatusMessage = \"Recording agent session id for trace stamping\"" : "";
1011
+ return `[[hooks.${eventName}]]
1012
+ [[hooks.${eventName}.hooks]]
1013
+ type = "command"
1014
+ command = ${JSON.stringify(`${shellCommand(reviewCommand)} trace hook ${eventName}`)}${status}`;
1015
+ }
1016
+ /** The whole marked block, written when the Codex config is empty. */
1017
+ function codexHookBlock(reviewCommand) {
1018
+ return `\n# review-trace-hooks:start\n${[
1019
+ "SessionStart",
1020
+ "UserPromptSubmit",
1021
+ "SessionEnd"
1022
+ ].map((eventName) => codexTraceHookToml(eventName, reviewCommand)).join("\n\n")}\n# review-trace-hooks:end\n`;
1023
+ }
1024
+ function transformCodexHooks(content, transform) {
1025
+ return content.replace(/^\[\[hooks\.(SessionStart|UserPromptSubmit|SessionEnd)\]\]\n\[\[hooks\.\1\.hooks\]\]\ntype = "command"\ncommand = ("(?:[^"\\\n]|\\.)*")(?:\n|$)(?:statusMessage = "[^"\n]*"(?:\n|$))?/gm, (block, event, encoded, offset) => {
1026
+ let reachedOtherTable = false;
1027
+ for (const line of content.slice(offset + block.length).split("\n")) {
1028
+ const trimmed = line.trim();
1029
+ if (!trimmed || trimmed.startsWith("#")) continue;
1030
+ if (!trimmed.startsWith("[")) {
1031
+ if (!reachedOtherTable) return block;
1032
+ continue;
1033
+ }
1034
+ const header = /^\[{1,2}([A-Za-z0-9_.-]+)\]{1,2}(?:\s*#.*)?$/.exec(trimmed);
1035
+ if (!header) return block;
1036
+ if (trimmed === `[[hooks.${event}]]`) break;
1037
+ if (header[1] === `hooks.${event}` || header[1].startsWith(`hooks.${event}.`)) return block;
1038
+ reachedOtherTable = true;
1039
+ }
1040
+ try {
1041
+ const command = jsonString(parseJsonText(encoded));
1042
+ if (!command?.endsWith(` trace hook ${event}`)) return block;
1043
+ return transform(block, command, event);
1044
+ } catch {
1045
+ return block;
1046
+ }
1047
+ });
1048
+ }
1049
+ async function readTextOrEmpty(filePath) {
1050
+ return existsSync(filePath) ? readFile(filePath, "utf8") : "";
1051
+ }
1052
+ /**
1053
+ * The directory one harness keeps its own configuration in. A machine without
1054
+ * that directory does not run the harness, so an install writes it no hook.
1055
+ */
1056
+ function agentTraceHomeDirectory(agent, homeDir = os.homedir(), env = process.env) {
1057
+ if (agent === "claude") return path.join(homeDir, ".claude");
1058
+ if (agent === "codex") return path.join(homeDir, ".codex");
1059
+ if (agent === "opencode") return openCodeDirectory(homeDir, env);
1060
+ if (agent === "pi") return path.join(homeDir, ".pi");
1061
+ throw new Error(`Unknown trace hook agent: ${String(agent)}`);
1062
+ }
1063
+ /** Reports recognized SessionStart owners and extension owners without changing files. */
1064
+ async function describeTraceHookOwners(homeDir = os.homedir(), env = process.env) {
1065
+ let claude = null;
1066
+ try {
1067
+ const parsed = parseJsonText(await readTextOrEmpty(claudeSettingsPath(homeDir)));
1068
+ const hooks = isJsonObject(parsed) ? parsed.hooks : void 0;
1069
+ const groups = isJsonObject(hooks) ? hooks.SessionStart : void 0;
1070
+ for (const group of isJsonArray(groups) ? groups : []) {
1071
+ if (!isJsonObject(group) || !isJsonArray(group.hooks)) continue;
1072
+ for (const hook of group.hooks) {
1073
+ const owner = isJsonObject(hook) ? traceHookCommandOwner(hook.command) : null;
1074
+ if (owner) claude = owner;
1075
+ }
1076
+ }
1077
+ } catch {
1078
+ claude = null;
1079
+ }
1080
+ let codex = null;
1081
+ transformCodexHooks(await readTextOrEmpty(codexConfigPath(homeDir)), (block, command, event) => {
1082
+ if (event === "SessionStart") codex = traceHookCommandOwner(command) ?? codex;
1083
+ return block;
1084
+ });
1085
+ return {
1086
+ claude,
1087
+ codex,
1088
+ opencode: extensionOwner(await readTextOrEmpty(openCodePluginPath(homeDir, env))),
1089
+ pi: extensionOwner(await readTextOrEmpty(piExtensionPath(homeDir)))
1090
+ };
1091
+ }
1092
+ async function readActiveTraceSessions(filePath, now = Date.now()) {
1093
+ const [content, fileStat] = await Promise.all([readFile(filePath, "utf8").catch(() => ""), stat(filePath).catch(() => null)]);
1094
+ const legacyTimestamp = Math.floor(fileStat?.mtimeMs ?? now);
1095
+ const sessions = /* @__PURE__ */ new Map();
1096
+ for (const rawLine of content.split("\n")) {
1097
+ const line = rawLine.trim();
1098
+ if (!line) continue;
1099
+ const fields = line.split(" ");
1100
+ const parsedSessionId = sessionIdSchema.safeParse(fields[0]);
1101
+ if (!parsedSessionId.success) continue;
1102
+ const sessionId = parsedSessionId.data;
1103
+ let lastActiveAt = legacyTimestamp;
1104
+ if (fields.length > 1) {
1105
+ if (fields.length !== 2 || !/^\d+$/.test(fields[1] ?? "")) continue;
1106
+ lastActiveAt = Number(fields[1]);
1107
+ if (!Number.isSafeInteger(lastActiveAt) || lastActiveAt <= 0) continue;
1108
+ }
1109
+ if (now - lastActiveAt <= 72e5) sessions.set(sessionId, lastActiveAt);
1110
+ }
1111
+ return sessions;
1112
+ }
1113
+ async function writeTraceSessions(filePath, sessions) {
1114
+ if (sessions.size === 0) {
1115
+ await rm(filePath, { force: true });
1116
+ return;
1117
+ }
1118
+ const content = [...sessions].map(([sessionId, lastActiveAt]) => `${sessionId}\t${lastActiveAt}`).join("\n");
1119
+ await writeFile(filePath, `${content}\n`, "utf8");
1120
+ }
1121
+ //#endregion
1122
+ //#region ../trace-core/src/trace-capture-scope.ts
1123
+ const HOSTED_CAPTURE_SCOPE_DESCRIPTION = "Hosted capture is limited to repositories allowed at this origin. Enable another repository only when the user explicitly requests publication.\n";
1124
+ //#endregion
491
1125
  //#region ../trace-core/src/trace-consent.ts
492
1126
  async function readTraceUserConfig(devHome) {
493
1127
  return (await readTraceUserConfigFile(devHome)).consent;
@@ -598,6 +1232,237 @@ async function inferRepoFromGit(cwd) {
598
1232
  throw new Error("Could not infer GitHub repository from origin remote.");
599
1233
  }
600
1234
  //#endregion
1235
+ //#region ../trace-core/src/trace-repository-hooks.ts
1236
+ const repositoryHookStateSchema = z.object({
1237
+ version: z.literal(1),
1238
+ root: z.string(),
1239
+ managedHooksPath: z.string(),
1240
+ previousHooksPath: z.string(),
1241
+ previousHookDirectory: z.string(),
1242
+ previousWasConfigured: z.boolean(),
1243
+ command: z.string().optional()
1244
+ });
1245
+ async function enableTraceRepository(input) {
1246
+ const resolved = await resolveRepository(input.cwd);
1247
+ if (!resolved) return {
1248
+ repository: false,
1249
+ enabled: false,
1250
+ message: "Not inside a Git repository."
1251
+ };
1252
+ const statePath = path.join(resolved.commonDir, "dev-fast", "trace-hooks", "state.json");
1253
+ const hooksPath = path.join(resolved.commonDir, "dev-fast", "trace-hooks", "hooks");
1254
+ const current = await configuredHooksPath(resolved.root);
1255
+ const oldState = await readState(statePath);
1256
+ const alreadyManaged = path.resolve(resolved.root, current.value || ".") === path.resolve(hooksPath);
1257
+ const previousWasConfigured = alreadyManaged ? oldState?.previousWasConfigured ?? false : current.configured;
1258
+ const previousHooksPath = alreadyManaged ? oldState?.previousHooksPath ?? path.join(resolved.commonDir, "hooks") : current.configured ? current.value : path.join(resolved.commonDir, "hooks");
1259
+ const previousHookDirectory = alreadyManaged ? oldState?.previousHookDirectory ?? resolveHooksPath(resolved.root, previousHooksPath) : previousHooksPath;
1260
+ const homeDir = input.scope.homeDir;
1261
+ let reviewCommand = renderTraceCommand(resolveTraceCommand({
1262
+ explicit: input.reviewCommand,
1263
+ env: input.scope.env,
1264
+ homeDir
1265
+ }));
1266
+ if (alreadyManaged && !input.replaceCommand && oldState?.command && keepTraceExecutable(traceCommandExecutable(oldState.command), traceCommandExecutable(reviewCommand) ?? "")) reviewCommand = oldState.command;
1267
+ const state = {
1268
+ version: 1,
1269
+ root: resolved.root,
1270
+ managedHooksPath: hooksPath,
1271
+ previousHooksPath,
1272
+ previousHookDirectory,
1273
+ previousWasConfigured,
1274
+ command: reviewCommand
1275
+ };
1276
+ await mkdir(hooksPath, { recursive: true });
1277
+ await writeHook(path.join(hooksPath, "prepare-commit-msg"), prepareCommitMessageHook(previousHookDirectory, reviewCommand));
1278
+ await writeHook(path.join(hooksPath, "pre-push"), prePushHook(previousHookDirectory, reviewCommand));
1279
+ await writePrivateJson(statePath, state);
1280
+ await gitAt(resolved.root, [
1281
+ "config",
1282
+ "--local",
1283
+ "core.hooksPath",
1284
+ hooksPath
1285
+ ]);
1286
+ await registerRepository(homeDir, resolved.root);
1287
+ return {
1288
+ repository: true,
1289
+ enabled: true,
1290
+ root: resolved.root,
1291
+ managedHooksPath: hooksPath,
1292
+ previousHooksPath,
1293
+ command: reviewCommand,
1294
+ message: "Review trace hooks are enabled for this repository."
1295
+ };
1296
+ }
1297
+ async function repairTraceRepository(input) {
1298
+ return enableTraceRepository(input);
1299
+ }
1300
+ async function disableTraceRepository(input) {
1301
+ const resolved = await resolveRepository(input.cwd);
1302
+ if (!resolved) return {
1303
+ repository: false,
1304
+ enabled: false,
1305
+ message: "Not inside a Git repository."
1306
+ };
1307
+ const stateDir = path.join(resolved.commonDir, "dev-fast", "trace-hooks");
1308
+ const state = await readState(path.join(stateDir, "state.json"));
1309
+ if (!state) return {
1310
+ repository: true,
1311
+ enabled: false,
1312
+ root: resolved.root,
1313
+ message: "Review trace hooks are not enabled for this repository."
1314
+ };
1315
+ const current = await configuredHooksPath(resolved.root);
1316
+ if (path.resolve(resolved.root, current.value || ".") === path.resolve(state.managedHooksPath)) {
1317
+ if (state.previousWasConfigured) await gitAt(resolved.root, [
1318
+ "config",
1319
+ "--local",
1320
+ "core.hooksPath",
1321
+ state.previousHooksPath
1322
+ ]);
1323
+ else await gitAt(resolved.root, [
1324
+ "config",
1325
+ "--local",
1326
+ "--unset",
1327
+ "core.hooksPath"
1328
+ ], { allowFailure: true });
1329
+ }
1330
+ await rm(stateDir, {
1331
+ recursive: true,
1332
+ force: true
1333
+ });
1334
+ await unregisterRepository(input.scope.homeDir, resolved.root);
1335
+ return {
1336
+ repository: true,
1337
+ enabled: false,
1338
+ root: resolved.root,
1339
+ message: "Review trace hooks are disabled for this repository."
1340
+ };
1341
+ }
1342
+ async function traceRepositoryStatus(cwd) {
1343
+ const resolved = await resolveRepository(cwd);
1344
+ if (!resolved) return {
1345
+ repository: false,
1346
+ enabled: false,
1347
+ message: "Not inside a Git repository."
1348
+ };
1349
+ const state = await readState(path.join(resolved.commonDir, "dev-fast", "trace-hooks", "state.json"));
1350
+ const current = await configuredHooksPath(resolved.root);
1351
+ const enabled = Boolean(state && path.resolve(resolved.root, current.value || ".") === path.resolve(state.managedHooksPath));
1352
+ const status = {
1353
+ repository: true,
1354
+ enabled,
1355
+ root: resolved.root,
1356
+ message: enabled ? "Review trace hooks are enabled for this repository." : "Review trace hooks are not enabled for this repository."
1357
+ };
1358
+ if (state) {
1359
+ status.managedHooksPath = state.managedHooksPath;
1360
+ status.previousHooksPath = state.previousHooksPath;
1361
+ if (state.command !== void 0) status.command = state.command;
1362
+ }
1363
+ return status;
1364
+ }
1365
+ /** Disables registered hooks, preserving other live executables when scoped. */
1366
+ async function disableAllTraceRepositories(scope, expectedCommand) {
1367
+ const registry = await readRegistry(scope.homeDir);
1368
+ const disabled = [];
1369
+ const kept = [];
1370
+ for (const root of registry) {
1371
+ if (expectedCommand !== void 0) {
1372
+ if (keepTraceExecutable(traceCommandExecutable((await traceRepositoryStatus(root).catch(() => null))?.command), expectedCommand)) {
1373
+ kept.push(root);
1374
+ continue;
1375
+ }
1376
+ }
1377
+ if ((await disableTraceRepository({
1378
+ cwd: root,
1379
+ scope
1380
+ }).catch(() => null))?.repository) disabled.push(root);
1381
+ }
1382
+ return {
1383
+ disabled,
1384
+ kept
1385
+ };
1386
+ }
1387
+ async function resolveRepository(cwd) {
1388
+ const rootResult = await gitAt(cwd, ["rev-parse", "--show-toplevel"], { allowFailure: true });
1389
+ const commonResult = await gitAt(cwd, [
1390
+ "rev-parse",
1391
+ "--path-format=absolute",
1392
+ "--git-common-dir"
1393
+ ], { allowFailure: true });
1394
+ if (!rootResult.ok || !commonResult.ok) return null;
1395
+ return {
1396
+ root: rootResult.stdout.trim(),
1397
+ commonDir: commonResult.stdout.trim()
1398
+ };
1399
+ }
1400
+ async function configuredHooksPath(root) {
1401
+ const result = await gitAt(root, [
1402
+ "config",
1403
+ "--local",
1404
+ "--get",
1405
+ "core.hooksPath"
1406
+ ], { allowFailure: true });
1407
+ return {
1408
+ configured: result.ok && Boolean(result.stdout.trim()),
1409
+ value: result.stdout.trim()
1410
+ };
1411
+ }
1412
+ function resolveHooksPath(root, hooksPath) {
1413
+ return path.isAbsolute(hooksPath) ? hooksPath : path.resolve(root, hooksPath);
1414
+ }
1415
+ function previousHookSetup(pathValue, name) {
1416
+ if (path.isAbsolute(pathValue)) return `previous=${shellQuote(path.join(pathValue, name))}`;
1417
+ return ["root=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"", `previous="$root"/${shellQuote(path.join(pathValue, name))}`].join("\n");
1418
+ }
1419
+ function prepareCommitMessageHook(previousPath, reviewCommand) {
1420
+ return `#!/bin/sh\n${previousHookSetup(previousPath, "prepare-commit-msg")}\nif [ -x "$previous" ]; then\n "$previous" "$@" || exit $?\nfi\n${reviewCommand} trace git-hook prepare-commit-msg "$@" || true\nexit 0\n`;
1421
+ }
1422
+ function prePushHook(previousPath, reviewCommand) {
1423
+ return `#!/bin/sh\n${previousHookSetup(previousPath, "pre-push")}\ntmp="$(mktemp "\${TMPDIR:-/tmp}/review-pre-push.XXXXXX")" || exit 0\ntrap 'rm -f "$tmp"' EXIT HUP INT TERM\ncat > "$tmp"\nif [ -x "$previous" ]; then\n "$previous" "$@" < "$tmp" || exit $?\nfi\n${reviewCommand} trace git-hook pre-push "$@" < "$tmp" || true\nexit 0\n`;
1424
+ }
1425
+ async function writeHook(filePath, contents) {
1426
+ await writeFile(filePath, contents, { mode: 493 });
1427
+ await chmod(filePath, 493);
1428
+ }
1429
+ async function readState(filePath) {
1430
+ try {
1431
+ const parsed = repositoryHookStateSchema.safeParse(parseJsonText(await readFile(filePath, "utf8")));
1432
+ return parsed.success ? parsed.data : null;
1433
+ } catch {
1434
+ return null;
1435
+ }
1436
+ }
1437
+ async function writePrivateJson(filePath, value) {
1438
+ await writeFileAtomicAsync(filePath, `${JSON.stringify(value, null, 2)}\n`, {
1439
+ encoding: "utf8",
1440
+ mode: 384
1441
+ });
1442
+ }
1443
+ function registryPath(homeDir) {
1444
+ return path.join(homeDir, ".config", "dev-trace", "repositories.json");
1445
+ }
1446
+ /** Returns the registered repository roots; never scans repositories or changes hooks. */
1447
+ function listTraceRepositoryRoots(homeDir) {
1448
+ return readRegistry(homeDir ?? traceHomeDir());
1449
+ }
1450
+ async function readRegistry(homeDir) {
1451
+ try {
1452
+ const value = parseJsonText(await readFile(registryPath(homeDir), "utf8"));
1453
+ return (jsonArray(value) ?? []).map(jsonString).filter((item) => item !== void 0);
1454
+ } catch {
1455
+ return [];
1456
+ }
1457
+ }
1458
+ async function registerRepository(homeDir, root) {
1459
+ const current = await readRegistry(homeDir);
1460
+ await writePrivateJson(registryPath(homeDir), [.../* @__PURE__ */ new Set([...current, root])]);
1461
+ }
1462
+ async function unregisterRepository(homeDir, root) {
1463
+ await writePrivateJson(registryPath(homeDir), (await readRegistry(homeDir)).filter((entry) => entry !== root));
1464
+ }
1465
+ //#endregion
601
1466
  //#region ../trace-core/src/trace-repository-target.ts
602
1467
  /**
603
1468
  * The identity of one target on this machine: a short digest of the origin
@@ -1675,62 +2540,467 @@ async function loadS3TraceStorage() {
1675
2540
  return (await import("./s3-SzLpeCG6.js")).S3TraceStorage;
1676
2541
  }
1677
2542
  //#endregion
1678
- //#region ../trace-core/src/agent-trace-parser.ts
1679
- function extractTraceEventText(event) {
1680
- return extractTraceEventText$1(event);
2543
+ //#region ../trace-core/src/trace-upload-status.ts
2544
+ /** A live, writer-authorized check of recorded uploads. */
2545
+ async function writeOwnUploadStatus(input) {
2546
+ const query = listUploadsQuerySchema.safeParse({
2547
+ session: input.session,
2548
+ cursor: input.cursor,
2549
+ limit: input.limit ?? 10
2550
+ });
2551
+ if (!query.success) {
2552
+ input.stdout.write("Upload status: invalid session, cursor, or limit.\n");
2553
+ return 1;
2554
+ }
2555
+ if (!input.client) {
2556
+ input.stdout.write("Upload status: not checked. Log in to the selected store.\n");
2557
+ return 1;
2558
+ }
2559
+ const client = input.client;
2560
+ try {
2561
+ const store = await client.findStore({
2562
+ owner: input.repo.owner,
2563
+ name: input.repo.repo
2564
+ });
2565
+ if (!store) throw new StoreApiError("not_found", 404, "This repository has no hosted trace store.");
2566
+ if (store.status !== "active") throw new StoreApiError("store_deleted", 410, "This trace store was deleted.");
2567
+ if (store.bytesStored !== void 0) input.stdout.write(`Stored bytes: ${store.bytesStored}\n`);
2568
+ const page = await client.listOwnUploads(store.repositoryId, query.data);
2569
+ if (page.storeId !== store.storeId) throw new StoreApiError("store_deleted", 410, "The trace store changed. Check status again.");
2570
+ input.stdout.write("Your uploads (checked with the store):\n");
2571
+ if (page.uploads.length === 0) input.stdout.write("No upload found for this account.\n");
2572
+ for (const upload of page.uploads) {
2573
+ const label = upload.status === "pending" ? "Not completed" : upload.current ? "Uploaded" : "Uploaded, later replaced";
2574
+ input.stdout.write(`${upload.sessionId}: ${label} at ${upload.completedAt ?? upload.createdAt} (upload ${upload.uploadId}).\n`);
2575
+ }
2576
+ if (page.nextCursor) input.stdout.write(`More uploads: run \`whiteboard trace status${input.session ? ` --agent-session ${input.session}` : ""} --limit ${query.data.limit} --cursor ${page.nextCursor}\`.\n`);
2577
+ return 0;
2578
+ } catch (error) {
2579
+ const cause = error instanceof Error ? error : new Error(String(error));
2580
+ input.stdout.write(`Upload status: not checked. ${cause.message}\n`);
2581
+ return 1;
2582
+ }
1681
2583
  }
1682
- const ACTIVE_GAP_LIMIT_MS = 6e5;
1683
- function computeActiveMs(events) {
1684
- let total = 0;
1685
- let previous = null;
1686
- let sawTimestamp = false;
1687
- for (const event of events) {
1688
- const at = timestampOf(event);
1689
- if (at === null) continue;
1690
- sawTimestamp = true;
1691
- if (previous !== null && at > previous) total += Math.min(at - previous, ACTIVE_GAP_LIMIT_MS);
1692
- previous = at;
2584
+ //#endregion
2585
+ //#region ../trace-core/src/trace-hosted-cli.ts
2586
+ /** A store failure as one sentence the user can act on. */
2587
+ function describeStoreFailure(error, origin, repository) {
2588
+ if (!(error instanceof StoreApiError)) return `Could not reach the trace store at ${origin}: ${error.message}`;
2589
+ switch (error.code) {
2590
+ case "unauthorized": return `The trace store at ${origin} rejected the login. Run \`whiteboard login --origin ${origin}\`.`;
2591
+ case "forbidden": return `You cannot read the traces of ${repository}: ${error.message}`;
2592
+ case "store_deleted": return `The trace store of ${repository} was deleted. Run \`whiteboard trace store create\` to create a new one.`;
2593
+ case "not_found": return `${repository} has no trace store. Run \`whiteboard trace store create\` first.`;
2594
+ default: return `The trace store at ${origin} answered ${error.code}: ${error.message}`;
1693
2595
  }
1694
- return sawTimestamp ? total : null;
1695
2596
  }
1696
- const OUTPUT_LIMIT = 2e4;
1697
- const INPUT_LIMIT = 4e3;
1698
- const TITLE_LIMIT = 160;
1699
- function parseAgentTraceJsonl(jsonl, options) {
1700
- const records = [];
1701
- for (const line of jsonl.split("\n")) {
1702
- const trimmed = line.trim();
1703
- if (!trimmed) continue;
1704
- try {
1705
- records.push(parseJsonText(trimmed));
1706
- } catch {}
2597
+ /** A failure a hosted command reports on both channels, then exits 1. */
2598
+ var HostedCommandFailure = class extends Error {};
2599
+ /** Shares repository inference without changing each command's login policy. */
2600
+ async function withHostedRepository(input, stage, fn) {
2601
+ const fail = (message) => failWithJsonError(input, stage, message);
2602
+ let name;
2603
+ try {
2604
+ name = await inferRepoFromGit(input.cwd);
2605
+ } catch (error) {
2606
+ return fail(errorMessage(error));
2607
+ }
2608
+ let origin = input.origin ?? "https://app.dev.fast";
2609
+ let client = input.client;
2610
+ if (stage === "store.create") try {
2611
+ client ??= await requireStoreClient(input.scope.env);
2612
+ } catch (error) {
2613
+ return fail(errorMessage(error));
2614
+ }
2615
+ else if (stage === "allow" || !client) {
2616
+ const auth = await readStoreAuth(input.scope.env);
2617
+ if (!auth) return fail(stage === "allow" ? `Run \`whiteboard login\` first.` : `The trace store login is missing. Run \`whiteboard login --origin ${origin}\`.`);
2618
+ origin = input.origin ?? auth.origin;
2619
+ if (auth.origin !== origin) return fail(`You are logged in to ${auth.origin}, not the selected store ${origin}. Run \`whiteboard login --origin ${origin}\`.`);
2620
+ client ??= new StoreClient({
2621
+ origin,
2622
+ token: auth.token
2623
+ });
2624
+ }
2625
+ try {
2626
+ return await fn({
2627
+ name,
2628
+ repository: traceRepoName(name),
2629
+ origin,
2630
+ client,
2631
+ stage
2632
+ });
2633
+ } catch (error) {
2634
+ if (error instanceof HostedCommandFailure) return fail(error.message);
2635
+ throw error;
1707
2636
  }
1708
- const firstType = jsonObject(records[0])?.type;
1709
- const parsed = firstType === "session_meta" ? parseCodexRecords(records) : firstType === "session" ? parsePiRecords(records) : firstType === "opencode_session" ? parseOpenCodeRecords(records) : parseClaudeRecords(records, options);
1710
- parsed.activeMs = computeActiveMs(parsed.events);
1711
- return parsed;
1712
2637
  }
1713
- function truncate(text, limit) {
1714
- if (text.length <= limit) return text;
1715
- const half = Math.floor(limit / 2);
1716
- return `${text.slice(0, half)}\n… (+${text.length - limit} characters omitted) …\n${text.slice(-half)}`;
2638
+ /** Requires an active store, preserving the command's store-error wording. */
2639
+ async function requireActiveStore(ctx) {
2640
+ let store;
2641
+ try {
2642
+ store = await ctx.client.findStore({
2643
+ owner: ctx.name.owner,
2644
+ name: ctx.name.repo
2645
+ });
2646
+ } catch (error) {
2647
+ const failure = error instanceof Error ? error : new Error(String(error));
2648
+ throw new HostedCommandFailure(ctx.stage === "sessions" ? describeStoreFailure(failure, ctx.origin, ctx.repository) : failure.message);
2649
+ }
2650
+ if (!store) throw new HostedCommandFailure(`${ctx.repository} has no trace store. Run \`whiteboard trace store create\` first.`);
2651
+ if (store.status !== "active") throw new HostedCommandFailure(`The trace store of ${store.displayName} was deleted. Run \`whiteboard trace store create\` to create a new one.`);
2652
+ return store;
1717
2653
  }
1718
- function compactLine(text, limit = TITLE_LIMIT) {
1719
- const collapsed = text.replace(/\s+/g, " ").trim();
1720
- if (collapsed.length <= limit) return collapsed;
1721
- return `${collapsed.slice(0, limit - 1)}…`;
2654
+ /**
2655
+ * Hosted consent commands: onboarding creates a repository store, allow
2656
+ * records publication consent bound to the login's origin, deny withdraws
2657
+ * it. None of them selects hosted storage; the trace storage use
2658
+ * hosted` does that explicitly.
2659
+ */
2660
+ async function runTraceOnboard(input) {
2661
+ return withHostedRepository(input, "store.create", async (ctx) => {
2662
+ let store;
2663
+ try {
2664
+ store = await ctx.client.createStore({
2665
+ owner: ctx.name.owner,
2666
+ name: ctx.name.repo
2667
+ });
2668
+ } catch (error) {
2669
+ if (error instanceof StoreApiError && error.code === "forbidden") throw new HostedCommandFailure(`You need push access to ${ctx.repository} to create its trace store.`);
2670
+ throw new HostedCommandFailure(errorMessage(error));
2671
+ }
2672
+ emitJsonEvent(input, {
2673
+ event: "trace.store.create",
2674
+ repositoryId: store.repositoryId,
2675
+ displayName: store.displayName,
2676
+ created: store.created === true
2677
+ });
2678
+ const stream = humanStream(input);
2679
+ stream.write(`Created the trace store of ${store.displayName} (id ${store.repositoryId}).\n`);
2680
+ stream.write(`Run \`whiteboard trace allow .\` to send traces from this repository.\n`);
2681
+ return 0;
2682
+ });
1722
2683
  }
1723
- /** File titles keep their tail: the filename matters more than the prefix. */
1724
- function compactFileLine(text, limit = TITLE_LIMIT) {
1725
- const collapsed = text.replace(/\s+/g, " ").trim();
1726
- if (collapsed.length <= limit) return collapsed;
1727
- return `…${collapsed.slice(-(limit - 1))}`;
2684
+ /** Reports the hosted store of one repository, or exits 1 without one. */
2685
+ async function runTraceStoreInfo(input) {
2686
+ return withHostedRepository(input, "store.info", async (ctx) => {
2687
+ let store;
2688
+ try {
2689
+ store = await ctx.client.findStore({
2690
+ owner: ctx.name.owner,
2691
+ name: ctx.name.repo
2692
+ });
2693
+ } catch (error) {
2694
+ throw new HostedCommandFailure(describeStoreFailure(error instanceof Error ? error : new Error(String(error)), ctx.origin, ctx.repository));
2695
+ }
2696
+ if (!store) throw new HostedCommandFailure(`${ctx.repository} has no trace store. Run \`whiteboard trace store create\` first.`);
2697
+ emitJsonEvent(input, {
2698
+ event: "trace.store",
2699
+ repository: store.displayName,
2700
+ repositoryId: store.repositoryId,
2701
+ storeId: store.storeId,
2702
+ status: store.status,
2703
+ bytesStored: store.bytesStored ?? null
2704
+ });
2705
+ const stream = humanStream(input);
2706
+ stream.write(`Repository: ${store.displayName} (id ${store.repositoryId})\n`);
2707
+ stream.write(`Store: ${store.storeId} (${store.status})\n`);
2708
+ if (store.bytesStored !== void 0) stream.write(`Stored bytes: ${store.bytesStored}\n`);
2709
+ return 0;
2710
+ });
1728
2711
  }
1729
- function relativizePath(filePath, cwd) {
1730
- if (!cwd) return filePath;
1731
- const relative = path.relative(cwd, filePath);
1732
- if (!relative || relative.startsWith("..")) return filePath;
1733
- return relative;
2712
+ /**
2713
+ * Deletes the hosted store of one repository. The deletion is logical: the
2714
+ * store stops every read and write at once, and an operator removes the
2715
+ * objects later. The consent of this machine stays; `deny` removes that.
2716
+ */
2717
+ async function runTraceStoreDelete(input) {
2718
+ return withHostedRepository(input, "store.delete", async (ctx) => {
2719
+ const store = await requireActiveStore(ctx);
2720
+ let deletion;
2721
+ try {
2722
+ deletion = await ctx.client.deleteStore(store.repositoryId);
2723
+ } catch (error) {
2724
+ if (error instanceof StoreApiError && error.code === "forbidden") throw new HostedCommandFailure(`Deleting the store of ${ctx.repository} needs admin access to the repository.`);
2725
+ throw new HostedCommandFailure(errorMessage(error));
2726
+ }
2727
+ emitJsonEvent(input, {
2728
+ event: "trace.store.delete",
2729
+ repository: store.displayName,
2730
+ repositoryId: deletion.repositoryId,
2731
+ storeId: deletion.storeId,
2732
+ status: deletion.status
2733
+ });
2734
+ humanStream(input).write(`Store deletion requested for ${store.displayName} (store ${deletion.storeId}). Uploaded objects are removed by a later operator cleanup.\n`);
2735
+ return 0;
2736
+ });
2737
+ }
2738
+ async function runTraceAllow(input) {
2739
+ return withHostedRepository(input, "allow", async (ctx) => {
2740
+ const selection = selectTraceStorage(input.scope);
2741
+ if (selection.error) return failWithJsonError(input, "allow", selection.error);
2742
+ if (selection.mode === "s3") return failWithJsonError(input, "allow", `This machine sends traces to a bucket. Run \`whiteboard trace storage use hosted\` first.`);
2743
+ const storeOrigin = ctx.origin;
2744
+ const store = await requireActiveStore(ctx);
2745
+ const { skipped } = await installHarnessHooks({
2746
+ homeDir: input.scope.homeDir,
2747
+ env: input.scope.env,
2748
+ executable: input.traceCommand?.file,
2749
+ harnessHooks: input.harnessHooks,
2750
+ allHarnesses: input.allHarnesses
2751
+ });
2752
+ if (skipped.length > 0) humanStream(input).write(skippedHarnessesLine(skipped));
2753
+ await enableTraceRepository({
2754
+ cwd: input.cwd,
2755
+ scope: input.scope,
2756
+ reviewCommand: input.traceCommand
2757
+ });
2758
+ await enableHostedCapture(input.scope.devHome, storeOrigin);
2759
+ await allowTraceRepository({
2760
+ repositoryId: store.repositoryId,
2761
+ name: store.displayName,
2762
+ origin: storeOrigin
2763
+ }, input.scope.devHome);
2764
+ emitJsonEvent(input, {
2765
+ event: "trace.allow",
2766
+ repositoryId: store.repositoryId,
2767
+ name: store.displayName,
2768
+ store: storeOrigin
2769
+ });
2770
+ const verifyCommand = input.verifyCommand ?? `whiteboard trace status`;
2771
+ humanStream(input).write(`Traces from ${store.displayName} may be published to ${storeOrigin}. Run \`${verifyCommand}\` to verify.\n`);
2772
+ return 0;
2773
+ });
2774
+ }
2775
+ /**
2776
+ * Switches hosted capture on. An uninstall leaves `capture.enabled: false`
2777
+ * behind; a later allow means the user wants capture back. A file with no
2778
+ * hosted entry gets one that names the login's origin.
2779
+ */
2780
+ async function enableHostedCapture(devHome, origin) {
2781
+ const file = readTraceConfigFile({ devHome });
2782
+ const current = file.config ?? emptyTraceConfig();
2783
+ const hosted = current.stores?.hosted;
2784
+ if (hosted?.capture?.enabled === true) return;
2785
+ await writeTraceConfigFile(file, {
2786
+ ...current,
2787
+ stores: {
2788
+ ...current.stores,
2789
+ hosted: hosted ? {
2790
+ ...hosted,
2791
+ capture: { enabled: true }
2792
+ } : {
2793
+ origin,
2794
+ capture: { enabled: true }
2795
+ }
2796
+ }
2797
+ });
2798
+ }
2799
+ async function runTraceDeny(input) {
2800
+ let name;
2801
+ try {
2802
+ name = traceRepoName(await inferRepoFromGit(input.cwd));
2803
+ } catch (error) {
2804
+ return failWithJsonError(input, "deny", errorMessage(error));
2805
+ }
2806
+ const devHome = input.scope.devHome;
2807
+ const cached = await readCachedTraceRepositoryTarget({
2808
+ cwd: input.cwd,
2809
+ origin: hostedOrigin(readTraceConfigFile({ devHome }).config),
2810
+ devHome
2811
+ }).catch(() => null);
2812
+ const removed = await denyTraceRepository({
2813
+ name,
2814
+ repositoryId: cached?.repositoryId ?? null
2815
+ }, devHome);
2816
+ emitJsonEvent(input, {
2817
+ event: "trace.deny",
2818
+ name,
2819
+ removed
2820
+ });
2821
+ humanStream(input).write(removed ? `${name} will no longer publish traces.\n` : `${name} was not allowed to publish traces.\n`);
2822
+ return 0;
2823
+ }
2824
+ function listedSession(session) {
2825
+ let bytes = 0;
2826
+ for (const object of session.objects) bytes += object.size;
2827
+ return {
2828
+ id: session.sessionId,
2829
+ harness: session.harness,
2830
+ updatedAt: session.updatedAt,
2831
+ branch: session.branch ?? null,
2832
+ author: session.author ?? null,
2833
+ generation: session.generation,
2834
+ commits: session.commits,
2835
+ traces: session.objects.map((object) => traceNameFromObject(object.name)),
2836
+ bytes
2837
+ };
2838
+ }
2839
+ /**
2840
+ * Lists every published session of this checkout's hosted store, one page
2841
+ * at a time. The command reads the store live: a missing login, a store
2842
+ * that does not answer, or a refusal is a failure, never a saved copy.
2843
+ * Reading needs no local publication consent; the store checks GitHub
2844
+ * access itself.
2845
+ */
2846
+ async function runTraceSessions(input) {
2847
+ const fail = (message) => failWithJsonError(input, "sessions", message);
2848
+ if (input.limit !== void 0 && (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 200)) return fail(`--limit must be a whole number from 1 to 200.`);
2849
+ if (input.cursor !== void 0 && !sessionIdSchema.safeParse(input.cursor).success) return fail("--cursor must be a session id from a previous page.");
2850
+ const selection = selectTraceStorage(input.scope);
2851
+ const mode = input.storage ?? selection.mode;
2852
+ if (mode === "s3") return fail(`\`whiteboard trace sessions\` lists the hosted store only. Run \`whiteboard trace storage use hosted\`, or pass \`--storage hosted\`.`);
2853
+ if (selection.error && (mode !== "hosted" || !selection.hosted)) return fail(selection.error);
2854
+ if (!selection.hosted) return fail(`Hosted trace storage is not configured. Run \`whiteboard trace allow .\` or \`whiteboard trace storage use hosted\`.`);
2855
+ return withHostedRepository({
2856
+ ...input,
2857
+ origin: selection.hosted.origin
2858
+ }, "sessions", async (ctx) => {
2859
+ const store = await requireActiveStore(ctx);
2860
+ const origin = ctx.origin;
2861
+ const query = input.cursor === void 0 ? { limit: input.limit ?? 50 } : {
2862
+ limit: input.limit ?? 50,
2863
+ cursor: input.cursor
2864
+ };
2865
+ let page;
2866
+ try {
2867
+ page = await ctx.client.listSessions(store.repositoryId, query);
2868
+ } catch (error) {
2869
+ if (error instanceof StoreApiError && error.code === "invalid_request") throw new HostedCommandFailure(`The trace store at ${origin} does not support listing every session yet. Update the store, or use \`whiteboard trace list --commit <sha>\`.`);
2870
+ throw new HostedCommandFailure(describeStoreFailure(error instanceof Error ? error : new Error(String(error)), origin, store.displayName));
2871
+ }
2872
+ const sessions = page.sessions.map(listedSession);
2873
+ emitJsonEvent(input, {
2874
+ event: "trace.sessions",
2875
+ repository: store.displayName,
2876
+ repositoryId: store.repositoryId,
2877
+ store: origin,
2878
+ sessions,
2879
+ nextCursor: page.nextCursor ?? null
2880
+ });
2881
+ const stream = humanStream(input);
2882
+ if (sessions.length === 0) {
2883
+ stream.write(`No published sessions in the trace store of ${store.displayName} at ${origin}.\n`);
2884
+ return 0;
2885
+ }
2886
+ for (const session of sessions) stream.write(`${session.id} ${session.harness} ${session.updatedAt} ${session.branch ?? "-"} ${session.bytes} bytes\n`);
2887
+ const nextPageFlags = (input.limit === void 0 ? "" : ` --limit ${input.limit}`) + (input.storage === void 0 ? "" : ` --storage ${input.storage}`);
2888
+ stream.write(page.nextCursor ? `Sessions are ordered by id. More follow: run \`whiteboard trace sessions${nextPageFlags} --cursor ${page.nextCursor}\`.\n` : "Sessions are ordered by id. This is the last page.\n");
2889
+ return 0;
2890
+ });
2891
+ }
2892
+ /** The hosted trace status lines: login, consent, and pending work. */
2893
+ async function writeHostedTraceStatus(input) {
2894
+ const stream = input.stdout;
2895
+ stream.write(HOSTED_CAPTURE_SCOPE_DESCRIPTION);
2896
+ const devHome = input.scope.devHome;
2897
+ const auth = await readStoreAuth(input.scope.env);
2898
+ const config = await readTraceUserConfig(devHome);
2899
+ stream.write(auth ? `Login: ${auth.login} at ${auth.origin}${auth.origin === input.origin ? "" : ` (selected store is ${input.origin}; run \`whiteboard login --origin ${input.origin}\`)`}\n` : `Login: none. Run \`whiteboard login --origin ${input.origin}\`.\n`);
2900
+ stream.write(`Capture switch: ${hostedCaptureEnabled(readTraceConfigFile({ devHome }).config) ? "on" : "off"}\n`);
2901
+ const owners = await describeTraceHookOwners(input.scope.homeDir, input.scope.env);
2902
+ stream.write(`Harness hooks: claude -> ${owners.claude ?? "none"}, codex -> ${owners.codex ?? "none"}, opencode -> ${owners.opencode ?? "none"}, pi -> ${owners.pi ?? "none"}\n`);
2903
+ const repositoryHooks = await traceRepositoryStatus(input.cwd);
2904
+ stream.write(`Git hooks: ${repositoryHooks.enabled ? repositoryHooks.command ?? "enabled" : "not enabled"}\n`);
2905
+ if (config.repositories.length === 0) stream.write(`Allowed repositories: none. Run \`whiteboard trace allow .\`.\n`);
2906
+ else for (const repository of config.repositories) stream.write(`Allowed repository: ${repository.name} (id ${repository.repositoryId}) -> ${repository.enabledOrigins.join(", ")}\n`);
2907
+ let repo = null;
2908
+ try {
2909
+ repo = await inferRepoFromGit(input.cwd);
2910
+ } catch {
2911
+ repo = null;
2912
+ }
2913
+ if (repo === null) stream.write("This directory has no GitHub remote to check.\n");
2914
+ else {
2915
+ const name = traceRepoName(repo);
2916
+ const entry = findTraceRepository(config, name);
2917
+ if (!entry) stream.write(`This repository (${name}) is not allowed. Run \`whiteboard trace allow .\`.\n`);
2918
+ else if (!entry.enabledOrigins.includes(input.origin)) stream.write(`This repository (${name}) is allowed at ${entry.enabledOrigins.join(", ")}, not the selected ${input.origin}. Run \`whiteboard trace allow .\` while logged in there.\n`);
2919
+ else stream.write(`This repository (${name}) is allowed to publish traces to ${input.origin}.\n`);
2920
+ }
2921
+ for (const sessionId of await pendingTraceSessions(input.cwd)) {
2922
+ if (input.session !== void 0 && input.session !== sessionId) continue;
2923
+ stream.write(`Pending agent session: ${sessionId}\n`);
2924
+ }
2925
+ for (const failure of await listTraceSyncFailures(devHome)) if (input.session === void 0 || input.session === failure.session) stream.write(describeTraceSyncFailure(failure));
2926
+ if (repo === null) return 1;
2927
+ return writeOwnUploadStatus({
2928
+ ...input,
2929
+ repo,
2930
+ client: input.client ?? (auth && auth.origin === input.origin ? new StoreClient({
2931
+ origin: auth.origin,
2932
+ token: auth.token
2933
+ }) : null)
2934
+ });
2935
+ }
2936
+ /** The agent sessions still marked active in this checkout. */
2937
+ async function pendingTraceSessions(cwd) {
2938
+ const gitPathResult = await git(cwd, [
2939
+ "rev-parse",
2940
+ "--git-path",
2941
+ "agent-session"
2942
+ ], { allowFailure: true });
2943
+ const sessionFilePath = gitPathResult.ok ? gitPathResult.stdout.trim() : "";
2944
+ if (!sessionFilePath) return [];
2945
+ return [...(await readActiveTraceSessions(sessionFilePath)).keys()];
2946
+ }
2947
+ //#endregion
2948
+ //#region ../trace-core/src/agent-trace-parser.ts
2949
+ function extractTraceEventText(event) {
2950
+ return extractTraceEventText$1(event);
2951
+ }
2952
+ const ACTIVE_GAP_LIMIT_MS = 6e5;
2953
+ function computeActiveMs(events) {
2954
+ let total = 0;
2955
+ let previous = null;
2956
+ let sawTimestamp = false;
2957
+ for (const event of events) {
2958
+ const at = timestampOf(event);
2959
+ if (at === null) continue;
2960
+ sawTimestamp = true;
2961
+ if (previous !== null && at > previous) total += Math.min(at - previous, ACTIVE_GAP_LIMIT_MS);
2962
+ previous = at;
2963
+ }
2964
+ return sawTimestamp ? total : null;
2965
+ }
2966
+ const OUTPUT_LIMIT = 2e4;
2967
+ const INPUT_LIMIT = 4e3;
2968
+ const TITLE_LIMIT = 160;
2969
+ function parseAgentTraceJsonl(jsonl, options) {
2970
+ const records = [];
2971
+ for (const line of jsonl.split("\n")) {
2972
+ const trimmed = line.trim();
2973
+ if (!trimmed) continue;
2974
+ try {
2975
+ records.push(parseJsonText(trimmed));
2976
+ } catch {}
2977
+ }
2978
+ const firstType = jsonObject(records[0])?.type;
2979
+ const parsed = firstType === "session_meta" ? parseCodexRecords(records) : firstType === "session" ? parsePiRecords(records) : firstType === "opencode_session" ? parseOpenCodeRecords(records) : parseClaudeRecords(records, options);
2980
+ parsed.activeMs = computeActiveMs(parsed.events);
2981
+ return parsed;
2982
+ }
2983
+ function truncate(text, limit) {
2984
+ if (text.length <= limit) return text;
2985
+ const half = Math.floor(limit / 2);
2986
+ return `${text.slice(0, half)}\n… (+${text.length - limit} characters omitted) …\n${text.slice(-half)}`;
2987
+ }
2988
+ function compactLine(text, limit = TITLE_LIMIT) {
2989
+ const collapsed = text.replace(/\s+/g, " ").trim();
2990
+ if (collapsed.length <= limit) return collapsed;
2991
+ return `${collapsed.slice(0, limit - 1)}…`;
2992
+ }
2993
+ /** File titles keep their tail: the filename matters more than the prefix. */
2994
+ function compactFileLine(text, limit = TITLE_LIMIT) {
2995
+ const collapsed = text.replace(/\s+/g, " ").trim();
2996
+ if (collapsed.length <= limit) return collapsed;
2997
+ return `…${collapsed.slice(-(limit - 1))}`;
2998
+ }
2999
+ function relativizePath(filePath, cwd) {
3000
+ if (!cwd) return filePath;
3001
+ const relative = path.relative(cwd, filePath);
3002
+ if (!relative || relative.startsWith("..")) return filePath;
3003
+ return relative;
1734
3004
  }
1735
3005
  function timestampOf(event) {
1736
3006
  if (!event || event.kind === "separator" || !event.at) return null;
@@ -3344,7 +4614,6 @@ function codexSessionsRoot() {
3344
4614
  //#region ../trace-core/src/trace-read.ts
3345
4615
  const STORE_COMMIT_LOOKUP_LIMIT = 30;
3346
4616
  const REMOTE_HEAD_TTL_MS = 15e3;
3347
- const TUTORIAL_TRACE_SESSION_ID = "review-tutorial-checkout";
3348
4617
  const lastCheckedTimes = /* @__PURE__ */ new Map();
3349
4618
  /**
3350
4619
  * The store to read from or publish to: an explicit override, or the
@@ -3418,11 +4687,6 @@ async function describeTraceSession(ref, storage) {
3418
4687
  /** Loads the offline tutorial or a store-compatible normalized trace, preserving freshness and offline fallback status; store refusals propagate. */
3419
4688
  async function loadReviewAgentTrace(input) {
3420
4689
  const { sessionId, trace } = input;
3421
- if (sessionId === "review-tutorial-checkout") {
3422
- if (trace && trace !== "main") return null;
3423
- const { loadTutorialTrace } = await import("./tutorial-trace-DrBvkaeL.js");
3424
- return loadTutorialTrace();
3425
- }
3426
4690
  if (!sessionIdSchema.safeParse(sessionId).success) return null;
3427
4691
  const traceName = trace ?? "main";
3428
4692
  const storage = await storageFor(input.storage, input.cwd);
@@ -3863,4 +5127,1278 @@ async function writeReviewTraceCommitMapping(input) {
3863
5127
  });
3864
5128
  }
3865
5129
  //#endregion
3866
- export { emitJsonEvent as $, traceCaptureIdentity as A, findTraceRepository as B, TraceStorageDeniedError as C, recordTraceSyncFailure as D, listTraceSyncFailures as E, inferRepoFromGit as F, readStoreAuth as G, DEFAULT_STORE_ORIGIN as H, parseRepo as I, runStoreLogout as J, requireStoreClient as K, traceRepoName as L, readCachedTraceRepositoryTarget as M, requireTraceConsent as N, TraceProvenanceError as O, resolveTraceRepositoryTarget as P, withStoreAuthorization as Q, allowTraceRepository as R, traceNameFromObject as S, describeTraceSyncFailure as T, clearStoreAuth as U, readTraceUserConfig as V, openUrlInBrowser as W, StoreApiError as X, runStoreWhoami as Y, StoreClient as Z, isTraceStorageConfigured as _, describeTraceSession as a, selectTraceStorage as b, lookupReviewTraceBlame as c, findLocalTrace as d, failWithJsonError as et, isOpenCodeSessionId as f, describeSelection as g, extractTraceEventText as h, clearTraceEnvCache as i, gitCommonDirectory as j, recordTraceSessionProvenance as k, lookupReviewTraceCommit as l, readTrailerSessions as m, writeReviewTraceCommitMapping as n, jsonRequestedInArgv as nt, listReviewTraceSessions as o, listRepositoryTraceSessionIds as p, runStoreLogin as q, TUTORIAL_TRACE_SESSION_ID as r, loadReviewAgentTrace as s, syncReviewTrace as t, humanStream as tt, pullReviewTraceCorpus as u, loadS3TraceStorage as v, clearTraceSyncFailure as w, traceStorageExpectation as x, resolveTraceStorage as y, denyTraceRepository as z };
5130
+ //#region ../trace-core/src/trace-machine-setup.ts
5131
+ const traceMachineSettingsSchema = z.object({
5132
+ version: z.literal(1),
5133
+ enabled: z.boolean(),
5134
+ autoActivateRepositories: z.literal(true),
5135
+ verifiedAt: z.string().optional(),
5136
+ error: z.string().optional()
5137
+ });
5138
+ /** The bucket credentials in the setup flow's field names. */
5139
+ async function readTraceCredentials(homeDir = os.homedir(), env = process.env) {
5140
+ const credentials = resolveS3Setup({
5141
+ homeDir,
5142
+ env
5143
+ }).credentials;
5144
+ if (!credentials) return null;
5145
+ return {
5146
+ endpoint: credentials.endpoint,
5147
+ bucket: credentials.bucket,
5148
+ key: credentials.accessKeyId,
5149
+ secret: credentials.secretAccessKey,
5150
+ region: credentials.region
5151
+ };
5152
+ }
5153
+ /**
5154
+ * The capture settings in effect: the profile's when the version-2 profile
5155
+ * carries them, otherwise the legacy settings file.
5156
+ */
5157
+ async function readCaptureSettings(setup, settingsPath) {
5158
+ if (setup.profile?.capture) return {
5159
+ settings: setup.profile.capture,
5160
+ source: "profile"
5161
+ };
5162
+ const legacy = await readSettings(settingsPath);
5163
+ return {
5164
+ settings: legacy ? {
5165
+ enabled: legacy.enabled,
5166
+ autoActivateRepositories: legacy.autoActivateRepositories,
5167
+ verifiedAt: legacy.verifiedAt,
5168
+ error: legacy.error
5169
+ } : null,
5170
+ source: "settings"
5171
+ };
5172
+ }
5173
+ async function traceMachineStatus(input = {}) {
5174
+ const homeDir = input.homeDir ?? os.homedir();
5175
+ const env = input.env ?? process.env;
5176
+ const envPath = traceEnvPath(homeDir, env);
5177
+ const settingsPath = traceSettingsPath(homeDir, env);
5178
+ const selection = selectTraceStorage({
5179
+ homeDir,
5180
+ env
5181
+ });
5182
+ if (!selection.s3) return {
5183
+ enabled: false,
5184
+ configured: false,
5185
+ autoActivateRepositories: false,
5186
+ envPath,
5187
+ settingsPath,
5188
+ configPath: selection.config.path,
5189
+ storageMode: selection.mode,
5190
+ error: selection.error
5191
+ };
5192
+ const setup = selection.s3;
5193
+ const credentials = setup.credentials;
5194
+ const { settings, source } = await readCaptureSettings(setup, settingsPath);
5195
+ const hostedSelected = selection.mode === "hosted";
5196
+ const hosted = hostedSelected && hostedCaptureEnabled(selection.config.config);
5197
+ const s3Enabled = !hostedSelected && settings?.enabled === true;
5198
+ const status = {
5199
+ enabled: hosted || s3Enabled,
5200
+ configured: hostedSelected || credentials !== null,
5201
+ autoActivateRepositories: hosted || s3Enabled && settings?.autoActivateRepositories === true,
5202
+ envPath,
5203
+ settingsPath,
5204
+ configPath: setup.configPath,
5205
+ storageMode: selection.mode,
5206
+ credentialsSource: setup.source,
5207
+ captureSource: source
5208
+ };
5209
+ if (credentials) {
5210
+ status.endpoint = credentials.endpoint;
5211
+ status.bucket = credentials.bucket;
5212
+ status.region = credentials.region;
5213
+ status.accessKeyIdPrefix = credentials.accessKeyId.slice(0, 6);
5214
+ }
5215
+ if (settings?.verifiedAt) status.verifiedAt = settings.verifiedAt;
5216
+ if (settings?.error) status.error = settings.error;
5217
+ if (selection.error) status.error = selection.error;
5218
+ return status;
5219
+ }
5220
+ /**
5221
+ * Whether this machine captures agent sessions at all. Every gate (session
5222
+ * hook, scaffold discovery, hook installation, repository enable/repair)
5223
+ * asks this instead of reading the settings file itself.
5224
+ */
5225
+ async function traceMachineEnabled(input = {}) {
5226
+ return (await traceMachineStatus(input)).enabled;
5227
+ }
5228
+ async function configureTraceMachine(input) {
5229
+ const homeDir = input.homeDir ?? os.homedir();
5230
+ const env = input.env ?? process.env;
5231
+ const existing = await readTraceCredentials(homeDir, env);
5232
+ const credentials = {
5233
+ endpoint: input.credentials?.endpoint ?? existing?.endpoint ?? "",
5234
+ bucket: input.credentials?.bucket ?? existing?.bucket ?? "",
5235
+ key: input.credentials?.key ?? existing?.key ?? "",
5236
+ secret: input.credentials?.secret ?? existing?.secret ?? ""
5237
+ };
5238
+ if (!Object.values(credentials).every(Boolean)) throw new Error("Trace setup needs an S3/R2 endpoint, bucket, access key ID, and secret access key.");
5239
+ const region = input.credentials?.region?.trim() || existing?.region || "auto";
5240
+ const configFile = readTraceConfigFile({
5241
+ homeDir,
5242
+ env
5243
+ });
5244
+ if (configFile.error) throw new TraceConfigurationError(configFile.error);
5245
+ const envPath = traceEnvPath(homeDir, env);
5246
+ const legacyPathsRequested = env.TRACE_ENV_FILE !== void 0 || env.TRACE_SETTINGS_FILE !== void 0;
5247
+ const target = configFile.config?.stores?.s3 || !existsSync(envPath) && !legacyPathsRequested ? "profile" : "legacy";
5248
+ if (target === "legacy") {
5249
+ await mkdir(path.dirname(envPath), { recursive: true });
5250
+ await writeFile(envPath, [
5251
+ `export TRACE_R2_ENDPOINT=${JSON.stringify(credentials.endpoint)}`,
5252
+ `export TRACE_R2_BUCKET=${JSON.stringify(credentials.bucket)}`,
5253
+ `export TRACE_R2_ACCESS_KEY_ID=${JSON.stringify(credentials.key)}`,
5254
+ `export TRACE_R2_SECRET_ACCESS_KEY=${JSON.stringify(credentials.secret)}`,
5255
+ `export TRACE_R2_REGION=${JSON.stringify(region)}`,
5256
+ ""
5257
+ ].join("\n"), { mode: 384 });
5258
+ await chmod(envPath, 384);
5259
+ clearTraceEnvCache();
5260
+ }
5261
+ let error;
5262
+ let verifiedAt;
5263
+ if (input.verify !== false) {
5264
+ if (env.TRACE_R2_MODE === "mock") verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
5265
+ else {
5266
+ const { S3TraceStorage } = await import("./s3-SzLpeCG6.js");
5267
+ const readiness = await S3TraceStorage.fromCredentials({
5268
+ endpoint: credentials.endpoint,
5269
+ bucket: credentials.bucket,
5270
+ accessKeyId: credentials.key,
5271
+ secretAccessKey: credentials.secret,
5272
+ region
5273
+ }, env).readiness();
5274
+ if (readiness.ready) verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
5275
+ else error = readiness.reason;
5276
+ }
5277
+ }
5278
+ if (target === "legacy") {
5279
+ const settings = {
5280
+ version: 1,
5281
+ enabled: true,
5282
+ autoActivateRepositories: true
5283
+ };
5284
+ if (verifiedAt) settings.verifiedAt = verifiedAt;
5285
+ if (error) settings.error = error;
5286
+ await writeSettings(traceSettingsPath(homeDir, env), settings);
5287
+ } else {
5288
+ const capture = {
5289
+ enabled: true,
5290
+ autoActivateRepositories: true
5291
+ };
5292
+ if (verifiedAt) capture.verifiedAt = verifiedAt;
5293
+ if (error) capture.error = error;
5294
+ await writeS3Profile(configFile, {
5295
+ endpoint: credentials.endpoint,
5296
+ bucket: credentials.bucket,
5297
+ accessKeyId: credentials.key,
5298
+ secretAccessKey: credentials.secret,
5299
+ region,
5300
+ capture
5301
+ });
5302
+ clearTraceEnvCache();
5303
+ }
5304
+ return traceMachineStatus({
5305
+ homeDir,
5306
+ env
5307
+ });
5308
+ }
5309
+ /**
5310
+ * Stores the profile and, when nothing is selected yet, selects direct
5311
+ * storage explicitly. An explicit hosted selection is left alone: setup
5312
+ * never silently redirects uploads.
5313
+ */
5314
+ async function writeS3Profile(configFile, profile) {
5315
+ const current = configFile.config ?? emptyTraceConfig();
5316
+ await writeTraceConfigFile(configFile, {
5317
+ ...current,
5318
+ "current-store": current["current-store"] ?? "s3",
5319
+ stores: {
5320
+ ...current.stores,
5321
+ s3: profile
5322
+ }
5323
+ });
5324
+ }
5325
+ async function disableTraceMachine(input = {}) {
5326
+ const homeDir = input.homeDir ?? os.homedir();
5327
+ const env = input.env ?? process.env;
5328
+ const configFile = readTraceConfigFile({
5329
+ homeDir,
5330
+ env
5331
+ });
5332
+ if (configFile.error) throw new TraceConfigurationError(configFile.error);
5333
+ if (selectTraceStorage({
5334
+ homeDir,
5335
+ env
5336
+ }).mode === "hosted") {
5337
+ const current = configFile.config ?? emptyTraceConfig();
5338
+ await writeTraceConfigFile(configFile, {
5339
+ ...current,
5340
+ stores: {
5341
+ ...current.stores,
5342
+ hosted: {
5343
+ ...current.stores?.hosted,
5344
+ capture: { enabled: false }
5345
+ }
5346
+ }
5347
+ });
5348
+ return;
5349
+ }
5350
+ const profile = configFile.config?.stores?.s3;
5351
+ if (profile?.capture) {
5352
+ await writeS3Profile(configFile, {
5353
+ ...profile,
5354
+ capture: {
5355
+ ...profile.capture,
5356
+ enabled: false
5357
+ }
5358
+ });
5359
+ return;
5360
+ }
5361
+ const settingsPath = traceSettingsPath(homeDir, env);
5362
+ if (input.removeSettings) {
5363
+ await rm(settingsPath, { force: true });
5364
+ return;
5365
+ }
5366
+ await writeSettings(settingsPath, {
5367
+ version: 1,
5368
+ enabled: false,
5369
+ autoActivateRepositories: true
5370
+ });
5371
+ }
5372
+ /** The legacy settings file alone, as `config migrate` reports it. */
5373
+ async function readLegacyCaptureSettings(filePath) {
5374
+ const legacy = await readSettings(filePath);
5375
+ if (!legacy) return null;
5376
+ const capture = {
5377
+ enabled: legacy.enabled,
5378
+ autoActivateRepositories: legacy.autoActivateRepositories
5379
+ };
5380
+ if (legacy.verifiedAt) capture.verifiedAt = legacy.verifiedAt;
5381
+ if (legacy.error) capture.error = legacy.error;
5382
+ return capture;
5383
+ }
5384
+ async function readSettings(filePath) {
5385
+ try {
5386
+ const parsed = traceMachineSettingsSchema.safeParse(parseJsonText(await readFile(filePath, "utf8")));
5387
+ return parsed.success ? parsed.data : null;
5388
+ } catch {
5389
+ return null;
5390
+ }
5391
+ }
5392
+ async function writeSettings(filePath, settings) {
5393
+ await writeFileAtomicAsync(filePath, `${JSON.stringify(settings, null, 2)}\n`, {
5394
+ encoding: "utf8",
5395
+ mode: 384
5396
+ });
5397
+ }
5398
+ //#endregion
5399
+ //#region ../trace-core/src/trace-hook-runner.ts
5400
+ const execFileAsync = promisify(execFile);
5401
+ const SESSION_ID_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/;
5402
+ /**
5403
+ * Starts `whiteboard trace sync` detached for one session. The attempt names
5404
+ * the destination it was started for; the sync rechecks the selection and
5405
+ * consent before any transfer. A missing CLI reports asynchronously and
5406
+ * never fails the caller.
5407
+ */
5408
+ function spawnDetachedTraceSync(input) {
5409
+ try {
5410
+ const command = resolveTraceCommand({
5411
+ explicit: input.command,
5412
+ env: input.scope.env,
5413
+ homeDir: input.scope.homeDir
5414
+ });
5415
+ const expectation = traceStorageExpectation(input.scope);
5416
+ const child = spawn(command.file, [
5417
+ ...command.args ?? [],
5418
+ "trace",
5419
+ "sync",
5420
+ input.sessionId,
5421
+ "--expect-storage",
5422
+ expectation
5423
+ ], {
5424
+ cwd: input.cwd,
5425
+ detached: true,
5426
+ stdio: "ignore"
5427
+ });
5428
+ child.on("error", () => {});
5429
+ child.unref();
5430
+ } catch {}
5431
+ }
5432
+ async function runTraceHook(input) {
5433
+ if (input.scope.env.TRACE_DISABLE === "1") return 0;
5434
+ const selection = selectTraceStorage(input.scope);
5435
+ if (selection.error || selection.mode === "none") return 0;
5436
+ if (!await traceMachineEnabled(input.scope)) return 0;
5437
+ let event = input.event;
5438
+ let sessionId = input.sessionId;
5439
+ if (input.stdin && !input.stdin.isTTY) try {
5440
+ const chunks = [];
5441
+ for await (const chunk of input.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5442
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
5443
+ if (raw) {
5444
+ const parsed = jsonObject(parseJsonText(raw));
5445
+ const hookEventName = jsonString(parsed?.hook_event_name);
5446
+ const parsedSessionId = jsonString(parsed?.session_id);
5447
+ if (hookEventName) event = hookEventName;
5448
+ if (parsedSessionId) sessionId = parsedSessionId;
5449
+ }
5450
+ } catch {}
5451
+ sessionId = (sessionId || input.scope.env.AGENT_SESSION_ID || "").trim();
5452
+ if (!sessionId || !SESSION_ID_REGEX.test(sessionId)) return 0;
5453
+ const normalizedEvent = event.toLowerCase().replaceAll(/[_-]/g, "");
5454
+ const isStart = normalizedEvent === "sessionstart";
5455
+ const isEnd = normalizedEvent === "sessionend";
5456
+ const isHeartbeat = normalizedEvent === "userpromptsubmit" || normalizedEvent === "turnstart";
5457
+ if (!isStart && !isEnd && !isHeartbeat) return 0;
5458
+ if (selection.mode === "hosted") {
5459
+ const origin = selection.hosted?.origin ?? "";
5460
+ const entry = await resolveAllowedTraceRepository(input.cwd, input.scope);
5461
+ await recordCaptureProvenance({
5462
+ cwd: input.cwd,
5463
+ sessionId,
5464
+ entry,
5465
+ origin,
5466
+ scope: input.scope
5467
+ }).catch(() => void 0);
5468
+ if (!entry || !entry.enabledOrigins.includes(origin)) return 0;
5469
+ }
5470
+ if (isStart) await enableTraceRepository({
5471
+ cwd: input.cwd,
5472
+ scope: input.scope,
5473
+ reviewCommand: input.traceCommand
5474
+ }).catch(() => void 0);
5475
+ const gitPathResult = await git(input.cwd, [
5476
+ "rev-parse",
5477
+ "--git-path",
5478
+ "agent-session"
5479
+ ], { allowFailure: true });
5480
+ let sessionFilePath = null;
5481
+ if (gitPathResult.ok && gitPathResult.stdout.trim()) sessionFilePath = gitPathResult.stdout.trim();
5482
+ const jjRootResult = await execFileAsync("jj", ["root"], { cwd: input.cwd }).catch(() => null);
5483
+ if (!sessionFilePath && jjRootResult?.stdout.trim()) sessionFilePath = path.join(jjRootResult.stdout.trim(), ".jj", "agent-session");
5484
+ let remainingSessions = [];
5485
+ if (sessionFilePath) {
5486
+ const now = Date.now();
5487
+ const currentSessions = await readActiveTraceSessions(sessionFilePath, now);
5488
+ if (isStart || isHeartbeat) {
5489
+ currentSessions.set(sessionId, now);
5490
+ remainingSessions = [...currentSessions.keys()];
5491
+ await writeTraceSessions(sessionFilePath, currentSessions).catch(() => void 0);
5492
+ } else if (isEnd) {
5493
+ currentSessions.delete(sessionId);
5494
+ remainingSessions = [...currentSessions.keys()];
5495
+ await writeTraceSessions(sessionFilePath, currentSessions).catch(() => void 0);
5496
+ }
5497
+ }
5498
+ if (jjRootResult?.stdout.trim()) {
5499
+ if ((isStart || isHeartbeat) && remainingSessions.length > 0) {
5500
+ const templateVal = jjCommitTrailersConfigValue(remainingSessions);
5501
+ await execFileAsync("jj", [
5502
+ "config",
5503
+ "set",
5504
+ "--repo",
5505
+ "templates.commit_trailers",
5506
+ templateVal
5507
+ ], { cwd: input.cwd }).catch(() => void 0);
5508
+ } else if (isEnd) {
5509
+ if (remainingSessions.length > 0) {
5510
+ const templateVal = jjCommitTrailersConfigValue(remainingSessions);
5511
+ await execFileAsync("jj", [
5512
+ "config",
5513
+ "set",
5514
+ "--repo",
5515
+ "templates.commit_trailers",
5516
+ templateVal
5517
+ ], { cwd: input.cwd }).catch(() => void 0);
5518
+ } else await execFileAsync("jj", [
5519
+ "config",
5520
+ "unset",
5521
+ "--repo",
5522
+ "templates.commit_trailers"
5523
+ ], { cwd: input.cwd }).catch(() => void 0);
5524
+ }
5525
+ }
5526
+ if (isEnd) spawnDetachedTraceSync({
5527
+ sessionId,
5528
+ cwd: input.cwd,
5529
+ scope: input.scope,
5530
+ command: input.traceCommand
5531
+ });
5532
+ return 0;
5533
+ }
5534
+ /**
5535
+ * The consent entry for this checkout's repository, or null when the
5536
+ * directory is not a GitHub repository or has no entry.
5537
+ */
5538
+ async function resolveAllowedTraceRepository(cwd, scope) {
5539
+ let name;
5540
+ try {
5541
+ name = traceRepoName(await inferRepoFromGit(cwd));
5542
+ } catch {
5543
+ return null;
5544
+ }
5545
+ return findTraceRepository(await readTraceUserConfig(scope.devHome), name);
5546
+ }
5547
+ /**
5548
+ * Records where this session ran. The identity is the allowed target when
5549
+ * the entry names the selected store and the login matches it; otherwise
5550
+ * the repository, or the Git directory of a checkout without a remote.
5551
+ */
5552
+ async function recordCaptureProvenance(input) {
5553
+ const auth = await readStoreAuth(input.scope.env);
5554
+ let identity;
5555
+ if (input.entry && input.entry.enabledOrigins.includes(input.origin) && auth && auth.origin === input.origin) identity = traceCaptureIdentity({ target: {
5556
+ origin: input.origin,
5557
+ repositoryId: input.entry.repositoryId
5558
+ } });
5559
+ else {
5560
+ const repo = await inferRepoFromGit(input.cwd).catch(() => null);
5561
+ if (repo) identity = traceCaptureIdentity({ repositoryName: traceRepoName(repo) });
5562
+ else {
5563
+ const gitDir = await gitCommonDirectory(input.cwd);
5564
+ if (!gitDir) return;
5565
+ identity = traceCaptureIdentity({ gitDir });
5566
+ }
5567
+ }
5568
+ await recordTraceSessionProvenance({
5569
+ sessionId: input.sessionId,
5570
+ ...identity,
5571
+ devHome: input.scope.devHome
5572
+ });
5573
+ }
5574
+ function jjCommitTrailersConfigValue(sessionIds) {
5575
+ const trailers = `${sessionIds.map((id) => `Agent-Session: ${id}`).join("\n")}\n`;
5576
+ const templateExpression = JSON.stringify(trailers);
5577
+ return JSON.stringify(templateExpression);
5578
+ }
5579
+ //#endregion
5580
+ //#region ../trace-core/src/trace-git-hook-runner.ts
5581
+ const ZERO_OID = /^0+$/;
5582
+ async function runTraceGitHook(input) {
5583
+ if (input.scope.env.TRACE_DISABLE === "1") return 0;
5584
+ if (!await traceMachineEnabled(input.scope)) return 0;
5585
+ try {
5586
+ if (input.hook === "prepare-commit-msg") return runPrepareCommitMessage(input.cwd, input.args[0]);
5587
+ if (input.hook === "pre-push") {
5588
+ await runPrePush(input);
5589
+ return 0;
5590
+ }
5591
+ } catch (cause) {
5592
+ input.stderr.write(`trace-sync: warning: ${errorMessage(cause)}\n`);
5593
+ }
5594
+ return 0;
5595
+ }
5596
+ async function runPrepareCommitMessage(cwd, messagePath) {
5597
+ if (!messagePath) return 0;
5598
+ const sessions = await activeSessions(cwd);
5599
+ for (const session of sessions) await git(cwd, [
5600
+ "interpret-trailers",
5601
+ "--in-place",
5602
+ "--if-exists",
5603
+ "addIfDifferent",
5604
+ "--trailer",
5605
+ `Agent-Session: ${session}`,
5606
+ messagePath
5607
+ ], { allowFailure: true });
5608
+ return 0;
5609
+ }
5610
+ async function activeSessions(cwd) {
5611
+ const fromEnv = (process.env.AGENT_SESSION_ID ?? "").trim();
5612
+ if (sessionIdSchema.safeParse(fromEnv).success) return [fromEnv];
5613
+ const fileResult = await git(cwd, [
5614
+ "rev-parse",
5615
+ "--git-path",
5616
+ "agent-session"
5617
+ ], { allowFailure: true });
5618
+ if (!fileResult.ok) return [];
5619
+ const filePath = fileResult.stdout.trim();
5620
+ if (!filePath) return [];
5621
+ const sessions = await readActiveTraceSessions(filePath);
5622
+ await writeTraceSessions(filePath, sessions).catch(() => void 0);
5623
+ return [...sessions.keys()];
5624
+ }
5625
+ async function runPrePush(input) {
5626
+ const raw = await readStdin(input.stdin);
5627
+ const commits = /* @__PURE__ */ new Map();
5628
+ for (const line of raw.split("\n")) {
5629
+ const [localRef, localSha, remoteRef, remoteSha] = line.trim().split(/\s+/);
5630
+ if (!localRef || !localSha || !remoteRef || !remoteSha || ZERO_OID.test(localSha)) continue;
5631
+ const branch = remoteRef.startsWith("refs/heads/") ? remoteRef.slice(11) : null;
5632
+ const revisionArgs = await revisionRange(input.cwd, localSha, remoteSha);
5633
+ const listed = await git(input.cwd, [
5634
+ "rev-list",
5635
+ "--max-count=500",
5636
+ ...revisionArgs
5637
+ ], { allowFailure: true });
5638
+ if (!listed.ok) continue;
5639
+ for (const commit of listed.stdout.split("\n").map((value) => value.trim()).filter(Boolean)) {
5640
+ const sessions = await readTrailerSessions(input.cwd, commit);
5641
+ if (sessions.length > 0) commits.set(commit, {
5642
+ branch,
5643
+ sessions
5644
+ });
5645
+ }
5646
+ }
5647
+ if (commits.size === 0) return;
5648
+ const sessionCommits = /* @__PURE__ */ new Map();
5649
+ for (const [commit, value] of commits) {
5650
+ await writeReviewTraceCommitMapping({
5651
+ cwd: input.cwd,
5652
+ commit,
5653
+ sessions: value.sessions,
5654
+ branch: value.branch
5655
+ }).catch((cause) => warn(input.stderr, cause));
5656
+ for (const session of value.sessions) sessionCommits.set(session, [...sessionCommits.get(session) ?? [], commit]);
5657
+ }
5658
+ const selection = selectTraceStorage(input.scope);
5659
+ for (const [sessionId, values] of sessionCommits) {
5660
+ if (!isOpenCodeSessionId(sessionId)) try {
5661
+ if (!await findLocalTrace(sessionId)) {
5662
+ input.stderr.write(`trace-sync: skipping ${sessionId}: no local transcript.\n`);
5663
+ continue;
5664
+ }
5665
+ } catch (cause) {
5666
+ warn(input.stderr, cause);
5667
+ continue;
5668
+ }
5669
+ if (selection.mode === "hosted") {
5670
+ spawnDetachedTraceSync({
5671
+ sessionId,
5672
+ cwd: input.cwd,
5673
+ scope: input.scope,
5674
+ command: input.traceCommand
5675
+ });
5676
+ continue;
5677
+ }
5678
+ await syncReviewTrace({
5679
+ sessionId,
5680
+ cwd: input.cwd,
5681
+ commits: values
5682
+ }).catch((cause) => warn(input.stderr, cause));
5683
+ }
5684
+ }
5685
+ async function revisionRange(cwd, localSha, remoteSha) {
5686
+ if (ZERO_OID.test(remoteSha)) return [
5687
+ localSha,
5688
+ "--not",
5689
+ "--remotes"
5690
+ ];
5691
+ return (await git(cwd, [
5692
+ "cat-file",
5693
+ "-e",
5694
+ remoteSha
5695
+ ], { allowFailure: true })).ok ? [`${remoteSha}..${localSha}`] : [
5696
+ localSha,
5697
+ "--not",
5698
+ "--remotes"
5699
+ ];
5700
+ }
5701
+ async function readStdin(stdin) {
5702
+ if (!stdin) return "";
5703
+ const chunks = [];
5704
+ for await (const chunk of stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5705
+ return Buffer.concat(chunks).toString("utf8");
5706
+ }
5707
+ function warn(stderr, cause) {
5708
+ stderr.write(`trace-sync: warning: ${errorMessage(cause)}\n`);
5709
+ }
5710
+ //#endregion
5711
+ //#region ../trace-core/src/trace-capture-cli.ts
5712
+ async function runTraceStatus(input) {
5713
+ const machine = await traceMachineStatus(input.scope);
5714
+ const repository = await traceRepositoryStatus(input.cwd);
5715
+ const selection = selectTraceStorage(input.scope);
5716
+ input.stdout.write(`Trace capture: ${machine.enabled ? "enabled" : "disabled"}\n`);
5717
+ input.stdout.write(`Repository: ${repository.message}\n`);
5718
+ input.stdout.write(`Storage: ${describeSelection(selection)}\n`);
5719
+ input.stdout.write(`Config: ${selection.config.path} (${selection.config.source === "absent" ? "not present" : `version ${selection.config.source === "v1" ? "1, consent only" : "2"}`})\n`);
5720
+ if (selection.error) {
5721
+ input.stderr.write(`trace status: ${selection.error}\n`);
5722
+ return 1;
5723
+ }
5724
+ if (selection.mode === "hosted") return writeHostedTraceStatus({
5725
+ scope: input.scope,
5726
+ cwd: input.cwd,
5727
+ origin: selection.hosted?.origin ?? "",
5728
+ stdout: input.stdout,
5729
+ session: input.session,
5730
+ cursor: input.cursor,
5731
+ limit: input.limit
5732
+ });
5733
+ if (input.session !== void 0 || input.cursor !== void 0 || input.limit !== void 0) {
5734
+ input.stderr.write("Upload status filters require hosted storage.\n");
5735
+ return 1;
5736
+ }
5737
+ const [{ S3TraceStorage }, { describeS3Setup, noTraceConfigurationMessage }] = await Promise.all([import("./s3-SzLpeCG6.js"), import("./s3-config-BgxkSoOy.js")]);
5738
+ const setup = describeS3Setup(input.scope.env);
5739
+ input.stdout.write(`Checking trace configuration (${setup.envPath})…\n`);
5740
+ for (const failure of await listTraceSyncFailures(input.scope.devHome)) input.stdout.write(describeTraceSyncFailure(failure));
5741
+ if (!setup.config) {
5742
+ input.stderr.write(`trace status: ${setup.error ?? noTraceConfigurationMessage()}\n`);
5743
+ return 1;
5744
+ }
5745
+ input.stdout.write(` Endpoint: ${setup.config.endpoint}\n`);
5746
+ input.stdout.write(` Bucket: ${setup.config.bucket}\n`);
5747
+ input.stdout.write(` Key: ${setup.config.accessKeyId.slice(0, 6)}…\n`);
5748
+ const readiness = await S3TraceStorage.fromEnvironment(input.scope)?.readiness() ?? {
5749
+ ready: false,
5750
+ reason: "unknown error"
5751
+ };
5752
+ if (readiness.ready) {
5753
+ input.stdout.write(`✓ S3/R2 bucket "${setup.config.bucket}" is reachable.\n`);
5754
+ return 0;
5755
+ }
5756
+ input.stderr.write(`✗ Cannot reach S3/R2 bucket "${setup.config.bucket}": ${readiness.reason ?? "unknown error"}\n`);
5757
+ return 1;
5758
+ }
5759
+ async function runTraceEnable(input) {
5760
+ if (!(await traceMachineStatus(input.scope)).enabled) {
5761
+ input.stderr.write(`trace enable: Trace capture is not enabled. Run \`whiteboard trace allow .\`\n`);
5762
+ return 1;
5763
+ }
5764
+ const result = await enableTraceRepository({
5765
+ cwd: input.cwd,
5766
+ scope: input.scope,
5767
+ reviewCommand: input.traceCommand
5768
+ });
5769
+ (result.enabled ? input.stdout : input.stderr).write(`${result.message}\n`);
5770
+ return result.enabled ? 0 : 1;
5771
+ }
5772
+ /**
5773
+ * Installs the machine parts of trace capture: the harness hooks of every
5774
+ * agent, and the CLI itself when the CLI passes an installer. The command
5775
+ * touches no repository; `allow` keeps the consent and the Git hooks.
5776
+ *
5777
+ * The CLI install runs first, because it reports the command file the
5778
+ * harness hooks must call.
5779
+ */
5780
+ async function runTraceInstallMachine(input) {
5781
+ const output = {
5782
+ json: input.json,
5783
+ stdout: input.stdout,
5784
+ stderr: input.stderr
5785
+ };
5786
+ const traceCommand = input.traceCommand;
5787
+ const { installed, skipped } = await installHarnessHooks({
5788
+ homeDir: input.scope.homeDir,
5789
+ env: input.scope.env,
5790
+ executable: traceCommand?.file,
5791
+ harnessHooks: input.harnessHooks,
5792
+ allHarnesses: input.allHarnesses
5793
+ });
5794
+ emitJsonEvent(output, {
5795
+ event: "trace.install",
5796
+ hooks: installed,
5797
+ skipped
5798
+ });
5799
+ const stream = humanStream(output);
5800
+ if (input.harnessHooks === false) {
5801
+ stream.write("Harness hooks: skipped.\n");
5802
+ return 0;
5803
+ }
5804
+ for (const hook of installed) stream.write(`Harness hook: ${hook.agent} -> ${hook.path}\n`);
5805
+ if (skipped.length > 0) stream.write(skippedHarnessesLine(skipped));
5806
+ return 0;
5807
+ }
5808
+ async function runTraceDisable(input) {
5809
+ const result = await disableTraceRepository({
5810
+ cwd: input.cwd,
5811
+ scope: input.scope
5812
+ });
5813
+ input.stdout.write(`${result.message}\n`);
5814
+ return result.repository ? 0 : 1;
5815
+ }
5816
+ async function runTraceRepair(input) {
5817
+ if (!(await traceMachineStatus(input.scope)).enabled) {
5818
+ input.stderr.write(`trace repair: Trace capture is not enabled. Run \`whiteboard trace allow .\`\n`);
5819
+ return 1;
5820
+ }
5821
+ const result = await repairTraceRepository({
5822
+ cwd: input.cwd,
5823
+ scope: input.scope,
5824
+ reviewCommand: input.traceCommand
5825
+ });
5826
+ (result.enabled ? input.stdout : input.stderr).write(`${result.message}\n`);
5827
+ return result.enabled ? 0 : 1;
5828
+ }
5829
+ async function runTraceSync(input) {
5830
+ let result;
5831
+ try {
5832
+ if (input.expectStorage !== void 0) {
5833
+ const current = traceStorageExpectation(input.scope);
5834
+ if (current !== input.expectStorage) throw new Error(`The trace storage selection changed since this capture started (expected ${input.expectStorage}, now ${current}). Run \`whiteboard trace sync ${input.sessionId}\` to publish to the current selection.`);
5835
+ }
5836
+ result = await syncReviewTrace({
5837
+ sessionId: input.sessionId,
5838
+ cwd: input.cwd,
5839
+ repo: input.repo
5840
+ });
5841
+ } catch (error) {
5842
+ const message = errorMessage(error);
5843
+ await recordTraceSyncFailure({
5844
+ sessionId: input.sessionId.trim(),
5845
+ repository: await inferRepoFromGit(input.cwd).then((repo) => `${repo.owner}/${repo.repo}`).catch(() => null),
5846
+ error: message,
5847
+ reason: error instanceof TraceProvenanceError ? error.reason : void 0,
5848
+ devHome: input.scope.devHome
5849
+ }).catch(() => void 0);
5850
+ throw error;
5851
+ }
5852
+ await clearTraceSyncFailure(input.sessionId.trim(), input.scope.devHome).catch(() => void 0);
5853
+ if (input.json) {
5854
+ input.stdout.write(`${JSON.stringify(result)}\n`);
5855
+ return 0;
5856
+ }
5857
+ for (const upload of result.uploads) input.stdout.write(`${upload.blob} ${upload.bytes_stored} bytes ${upload.status}\n`);
5858
+ if (result.hosted) {
5859
+ for (const name of result.hosted.omitted.subagents) input.stdout.write(`${name} omitted (over the object limit or not a store name)\n`);
5860
+ if (result.hosted.omitted.commits > 0) input.stdout.write(`${result.hosted.omitted.commits} commit link(s) omitted (over the commit limit).\n`);
5861
+ input.stdout.write(result.hosted.complete ? `Published session ${result.session} of ${result.repo} to the trace store (generation ${result.hosted.generation}).\n` : `Published part of session ${result.session} of ${result.repo} to the trace store (generation ${result.hosted.generation}).\n`);
5862
+ return 0;
5863
+ }
5864
+ input.stdout.write(`Updated meta for session ${result.session} in ${result.repo}.\n`);
5865
+ return 0;
5866
+ }
5867
+ //#endregion
5868
+ //#region ../trace-core/src/trace-read-cli.ts
5869
+ /**
5870
+ * The store a read command uses: the explicit `--storage` override for this
5871
+ * one operation, or the machine's selection when none is given. An override
5872
+ * never changes the selection, capture settings, or consent.
5873
+ */
5874
+ async function resolveTraceReadStorage(override, cwd) {
5875
+ if (!override) return void 0;
5876
+ return resolveTraceStorage({
5877
+ cwd,
5878
+ override
5879
+ });
5880
+ }
5881
+ async function listSessionsForReviewScope(review, storage) {
5882
+ return listReviewTraceSessions({
5883
+ rootPath: review.repoRoot,
5884
+ baseCommit: review.baseCommit,
5885
+ headCommit: review.headCommit,
5886
+ storage
5887
+ });
5888
+ }
5889
+ async function runTraceList(input) {
5890
+ const storage = "resolvedStorage" in input ? input.resolvedStorage : await resolveTraceReadStorage(input.storage, input.cwd);
5891
+ let scope;
5892
+ let sessions;
5893
+ if ("commit" in input.scope) {
5894
+ const resolution = await lookupReviewTraceCommit({
5895
+ cwd: input.cwd,
5896
+ sha: input.scope.commit,
5897
+ storage
5898
+ });
5899
+ scope = { commit: resolution.commit };
5900
+ sessions = await Promise.all(resolution.sessions.map((sessionId) => describeTraceSession({
5901
+ sessionId,
5902
+ commits: [{
5903
+ sha: resolution.commit,
5904
+ subject: ""
5905
+ }]
5906
+ }, storage)));
5907
+ } else {
5908
+ scope = { review: input.scope.review.uuid };
5909
+ sessions = await listSessionsForReviewScope(input.scope.review, storage);
5910
+ }
5911
+ const publicSessions = sessions.map((session) => ({
5912
+ id: session.sessionId,
5913
+ harness: session.harness,
5914
+ available: session.available,
5915
+ traces: ["main", ...session.subagents ?? []],
5916
+ commits: session.commits
5917
+ }));
5918
+ if (input.json) {
5919
+ input.stdout.write(`${JSON.stringify({
5920
+ ...scope,
5921
+ sessions: publicSessions
5922
+ })}\n`);
5923
+ return 0;
5924
+ }
5925
+ if (sessions.length === 0) {
5926
+ const label = "review" in scope ? `review ${scope.review}` : `commit ${scope.commit}`;
5927
+ input.stdout.write(`No agent sessions recorded for ${label}.\n`);
5928
+ return 0;
5929
+ }
5930
+ for (const session of publicSessions) {
5931
+ input.stdout.write(`${session.id} (${session.harness}, ${session.available ? "S3/R2 synced" : "not synced"})\n`);
5932
+ for (const commit of session.commits) input.stdout.write(` commit ${commit.sha.slice(0, 9)} ${commit.subject}\n`);
5933
+ for (const name of session.traces.slice(1)) input.stdout.write(` trace ${name}\n`);
5934
+ }
5935
+ return 0;
5936
+ }
5937
+ async function runTraceShow(input) {
5938
+ const traceName = input.trace === "main" ? void 0 : input.trace;
5939
+ const loaded = await loadReviewAgentTrace({
5940
+ sessionId: input.sessionId,
5941
+ trace: traceName,
5942
+ cwd: input.cwd,
5943
+ storage: await resolveTraceReadStorage(input.storage, input.cwd)
5944
+ });
5945
+ if (!loaded) throw new Error(`No transcript is available for session ${input.sessionId}${traceName ? ` (trace ${traceName})` : ""}.`);
5946
+ const { trace } = loaded;
5947
+ if (input.eventIndex !== void 0) {
5948
+ const event = trace.events[input.eventIndex];
5949
+ if (!event) throw new Error(`Event ${input.eventIndex} is out of range. Session ${input.sessionId} has ${trace.events.length} events.`);
5950
+ const text = extractTraceEventText(event);
5951
+ if (input.json) {
5952
+ const traceQuoteProps = {
5953
+ sessionId: input.sessionId,
5954
+ event: input.eventIndex,
5955
+ trace: traceName || void 0
5956
+ };
5957
+ input.stdout.write(`${JSON.stringify({
5958
+ session: input.sessionId,
5959
+ trace: traceName ?? "main",
5960
+ event: input.eventIndex,
5961
+ kind: event.kind,
5962
+ text,
5963
+ trace_quote_props: traceQuoteProps
5964
+ })}\n`);
5965
+ return 0;
5966
+ }
5967
+ input.stdout.write(`${text}\n`);
5968
+ return 0;
5969
+ }
5970
+ const rows = trace.events.map((event, index) => ({
5971
+ event,
5972
+ index
5973
+ })).filter((row) => !input.kind || row.event.kind === input.kind);
5974
+ if (input.json) {
5975
+ input.stdout.write(`${JSON.stringify({
5976
+ session: input.sessionId,
5977
+ trace: traceName ?? "main",
5978
+ harness: trace.harness,
5979
+ title: trace.title,
5980
+ cache: loaded.cacheStatus,
5981
+ events: rows.map(({ event, index }) => ({
5982
+ event: index,
5983
+ kind: event.kind,
5984
+ summary: compactEventLine(event)
5985
+ }))
5986
+ })}\n`);
5987
+ return 0;
5988
+ }
5989
+ input.stdout.write(`# session ${input.sessionId}${traceName ? ` (trace ${traceName})` : ""} (${trace.harness}) — ${trace.title ?? "untitled"}\n# ${trace.events.length} events${input.kind ? ` (${rows.length} shown, kind=${input.kind})` : ""}${loaded.cacheStatus === "current" ? "" : ` (${loaded.cacheStatus} copy)`}\n`);
5990
+ for (const { event, index } of rows) input.stdout.write(`${String(index).padStart(4, " ")} ${compactEventLine(event)}\n`);
5991
+ return 0;
5992
+ }
5993
+ async function runTracePull(input) {
5994
+ try {
5995
+ const storage = "resolvedStorage" in input ? input.resolvedStorage : await resolveTraceReadStorage(input.storage, input.cwd);
5996
+ let scope;
5997
+ let sessions;
5998
+ let repoRoot = input.cwd;
5999
+ if ("review" in input.scope) {
6000
+ const review = input.scope.review;
6001
+ repoRoot = review.repoRoot;
6002
+ const refs = await listSessionsForReviewScope(review, storage);
6003
+ scope = { review: review.uuid };
6004
+ sessions = refs.map((ref) => ({
6005
+ id: ref.sessionId,
6006
+ traces: ref.subagents
6007
+ }));
6008
+ } else if ("commit" in input.scope) {
6009
+ const resolution = await lookupReviewTraceCommit({
6010
+ cwd: input.cwd,
6011
+ sha: input.scope.commit,
6012
+ storage
6013
+ });
6014
+ scope = { commit: resolution.commit };
6015
+ sessions = resolution.sessions.map((id) => ({ id }));
6016
+ } else if ("session" in input.scope) {
6017
+ scope = { session: input.scope.session };
6018
+ sessions = [{ id: input.scope.session }];
6019
+ } else {
6020
+ sessions = (await listRepositoryTraceSessionIds(input.cwd)).map((id) => ({ id }));
6021
+ scope = { repository: input.repo ?? "current" };
6022
+ }
6023
+ const result = await pullReviewTraceCorpus({
6024
+ repo: input.repo ? parseRepo(input.repo) : await inferRepoFromGit(repoRoot),
6025
+ sessions,
6026
+ mainOnly: input.mainOnly,
6027
+ cwd: repoRoot,
6028
+ storage
6029
+ });
6030
+ const output = {
6031
+ scope,
6032
+ corpus_root: result.corpusRoot,
6033
+ repository: result.repository,
6034
+ sessions: result.sessions,
6035
+ unavailable_sessions: result.unavailableSessions,
6036
+ events: result.events,
6037
+ files: result.files,
6038
+ paths: result.paths
6039
+ };
6040
+ if (input.json) input.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
6041
+ else {
6042
+ input.stdout.write(`Pulled ${result.sessions.length} session(s) into ${result.corpusRoot}.\n`);
6043
+ input.stdout.write(`Materialized ${result.files} normalized trace file(s) with ${result.events} event(s) for ${result.repository}.\n`);
6044
+ for (const filePath of result.paths) input.stdout.write(` ${filePath}\n`);
6045
+ if (result.unavailableSessions.length > 0) input.stderr.write(`Unavailable sessions: ${result.unavailableSessions.join(", ")}\n`);
6046
+ }
6047
+ return sessions.length > 0 && result.sessions.length === 0 ? 1 : 0;
6048
+ } catch (error) {
6049
+ input.stderr.write(`trace pull error: ${errorMessage(error)}\n`);
6050
+ return 1;
6051
+ }
6052
+ }
6053
+ async function runTraceBlame(input) {
6054
+ let result;
6055
+ try {
6056
+ result = await lookupReviewTraceBlame({
6057
+ cwd: input.cwd,
6058
+ file: input.file,
6059
+ lines: input.lines,
6060
+ history: input.history,
6061
+ storage: await resolveTraceReadStorage(input.storage, input.cwd)
6062
+ });
6063
+ } catch (err) {
6064
+ input.stderr.write(`trace blame error: ${errorMessage(err)}\n`);
6065
+ return 1;
6066
+ }
6067
+ if (input.json) {
6068
+ input.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
6069
+ return result.resolutions.some((r) => r.sessions.length > 0) ? 0 : 1;
6070
+ }
6071
+ if (result.resolutions.length === 0) {
6072
+ input.stderr.write(`no commits found for ${input.file}\n`);
6073
+ return 1;
6074
+ }
6075
+ for (const resolution of result.resolutions) printCommitResolution(resolution, input.stdout);
6076
+ return result.resolutions.some((r) => r.sessions.length > 0) ? 0 : 1;
6077
+ }
6078
+ function printCommitResolution(resolution, stdout) {
6079
+ const shortCommit = resolution.commit.slice(0, 12);
6080
+ if (resolution.sessions.length === 0) {
6081
+ stdout.write(`${shortCommit} no agent sessions found (source checked: trailer, index, pr-scan)\n`);
6082
+ return;
6083
+ }
6084
+ const prSuffix = resolution.pr !== null ? ` PR #${resolution.pr}` : "";
6085
+ stdout.write(`${shortCommit} → ${resolution.sessions.length} session(s) via ${resolution.source}${prSuffix}\n`);
6086
+ for (const session of resolution.sessions) {
6087
+ const meta = resolution.session_meta?.[session];
6088
+ const metaSuffix = meta ? ` (${meta.branch || "?"}, ${meta.author || "?"})` : "";
6089
+ stdout.write(` ${session}${metaSuffix}\n`);
6090
+ stdout.write(` trace: by-session/${session}/trace.jsonl\n`);
6091
+ stdout.write(` pull for FFF: whiteboard trace pull --agent-session ${session}\n`);
6092
+ }
6093
+ }
6094
+ function compactEventLine(event) {
6095
+ const oneLine = (text, limit) => {
6096
+ const collapsed = text.replace(/\s+/g, " ").trim();
6097
+ return collapsed.length > limit ? `${collapsed.slice(0, limit - 1)}…` : collapsed;
6098
+ };
6099
+ if (event.kind === "user") return `user ${oneLine(event.text, 160)}`;
6100
+ if (event.kind === "assistant") return `${event.thinking ? "thinking " : "assistant "} ${oneLine(event.markdown, 160)}`;
6101
+ if (event.kind === "separator") return `separator ${event.label}`;
6102
+ const counts = [event.additions ? `+${event.additions}` : null, event.deletions ? `−${event.deletions}` : null].filter(Boolean).join(" ");
6103
+ return `tool ${event.verb} ${oneLine(event.title, 120)}${counts ? ` ${counts}` : ""}${event.filePath ? ` [${event.filePath}]` : ""}`;
6104
+ }
6105
+ //#endregion
6106
+ //#region ../trace-core/src/trace-command-storage-option.ts
6107
+ /** Adds a read-only store override ; never changes persisted selection. */
6108
+ function addTraceStorageOption(command) {
6109
+ return command.addOption(new Option("--storage <mode>", "read from the s3 or hosted store instead of the selected one").choices(["s3", "hosted"]));
6110
+ }
6111
+ //#endregion
6112
+ //#region ../trace-core/src/trace-uninstall-hooks.ts
6113
+ /** Release Review hooks; keep its install, login, consent and data. */
6114
+ async function runTraceUninstallHooks(input) {
6115
+ const removed = [];
6116
+ for (const agent of AGENT_TRACE_HOOK_AGENTS) if (await removeAgentTraceHook(agent, input.scope.homeDir, input.scope.env)) removed.push(agent);
6117
+ const { disabled } = await disableAllTraceRepositories(input.scope);
6118
+ emitJsonEvent(input, {
6119
+ event: "trace.uninstall-hooks",
6120
+ owner: "review",
6121
+ removed,
6122
+ repositories: disabled
6123
+ });
6124
+ const output = humanStream(input);
6125
+ output.write(`Removed Whiteboard trace hooks. Kept the CLI, login, consent and captured traces.\n`);
6126
+ output.write("Run `whiteboard trace install` to restore trace hooks.\n");
6127
+ return 0;
6128
+ }
6129
+ //#endregion
6130
+ //#region ../trace-core/src/trace-capture-commands.ts
6131
+ /** Registers storage inspection and repository capture actions; never owns app commands. */
6132
+ function registerTraceCaptureCommands(trace, settings) {
6133
+ const { runtime, cwd, scope, traceCommand, configureOutput, configureJsonOutput } = settings;
6134
+ configureJsonOutput(trace.command("uninstall-hooks").description("Remove this CLI's agent and Git trace hooks; keep login, consent and traces")).action(async (options) => {
6135
+ settings.setExitCode(await runTraceUninstallHooks({
6136
+ scope,
6137
+ json: options.json,
6138
+ stdout: settings.stdout,
6139
+ stderr: settings.stderr
6140
+ }));
6141
+ });
6142
+ const withStorage = (command) => addTraceStorageOption(command);
6143
+ configureOutput(trace.command("status").description("Check trace storage and your hosted uploads").option("--agent-session <id>", "Check uploads of one session").option("--cursor <cursor>", "Continue an upload status page").option("--limit <count>", "Uploads per page", Number)).action(async (options) => {
6144
+ settings.setExitCode(await runtime.runTraceStatus({
6145
+ scope,
6146
+ cwd,
6147
+ session: options.agentSession,
6148
+ cursor: options.cursor,
6149
+ limit: options.limit,
6150
+ stdout: settings.stdout,
6151
+ stderr: settings.stderr
6152
+ }));
6153
+ });
6154
+ configureJsonOutput(withStorage(trace.command("sessions").description("List every published session of this repository's hosted trace store").option("--limit <n>", `sessions per page (1-200, default 50)`, (value) => {
6155
+ if (!/^\d+$/.test(value)) throw new InvalidArgumentError(`--limit must be a whole number from 1 to 200.`);
6156
+ return Number(value);
6157
+ }).option("--cursor <session-id>", "continue after this session id"))).action(async (options) => {
6158
+ settings.setExitCode(await runtime.runTraceSessions({
6159
+ scope,
6160
+ cwd,
6161
+ limit: options.limit,
6162
+ cursor: options.cursor,
6163
+ storage: options.storage,
6164
+ json: options.json,
6165
+ stdout: settings.stdout,
6166
+ stderr: settings.stderr
6167
+ }));
6168
+ });
6169
+ const repositoryCwd = (repoPath) => repoPath ? path.resolve(cwd, repoPath) : cwd;
6170
+ const createStore = async (repoPath, options) => {
6171
+ settings.setExitCode(await runtime.runTraceOnboard({
6172
+ scope,
6173
+ cwd: repositoryCwd(repoPath),
6174
+ json: options.json,
6175
+ stdout: settings.stdout,
6176
+ stderr: settings.stderr
6177
+ }));
6178
+ };
6179
+ const store = configureOutput(trace.command("store").description("Manage this repository's hosted trace store"));
6180
+ configureJsonOutput(store.command("create [path]").description("Create the hosted trace store for one repository (needs push access)")).action(createStore);
6181
+ configureJsonOutput(store.command("delete [path]").description("Delete the hosted trace store of one repository (admins only)")).action(async (repoPath, options) => {
6182
+ settings.setExitCode(await runtime.runTraceStoreDelete({
6183
+ scope,
6184
+ cwd: repositoryCwd(repoPath),
6185
+ json: options.json,
6186
+ stdout: settings.stdout,
6187
+ stderr: settings.stderr
6188
+ }));
6189
+ });
6190
+ configureJsonOutput(store.command("info [path]").description("Show the hosted trace store of one repository")).action(async (repoPath, options) => {
6191
+ settings.setExitCode(await runtime.runTraceStoreInfo({
6192
+ scope,
6193
+ cwd: repositoryCwd(repoPath),
6194
+ json: options.json,
6195
+ stdout: settings.stdout,
6196
+ stderr: settings.stderr
6197
+ }));
6198
+ });
6199
+ configureJsonOutput(trace.command("install").description("Install the agent hooks on this machine").option("--no-harness-hooks", "skip the Claude, Codex, OpenCode, and pi hook installers").option("--all-harnesses", "write every harness hook, even for a harness this machine lacks")).action(async (options) => {
6200
+ settings.setExitCode(await runtime.runTraceInstallMachine({
6201
+ scope,
6202
+ json: options.json,
6203
+ harnessHooks: options.harnessHooks,
6204
+ allHarnesses: options.allHarnesses,
6205
+ traceCommand,
6206
+ stdout: settings.stdout,
6207
+ stderr: settings.stderr
6208
+ }));
6209
+ });
6210
+ configureJsonOutput(trace.command("allow [path]").description("Allow one repository to publish traces to the hosted store").option("--no-harness-hooks", "skip the Claude, Codex, OpenCode, and pi hook installers").option("--all-harnesses", "write every harness hook, even for a harness this machine lacks")).action(async (repoPath, options) => {
6211
+ settings.setExitCode(await runtime.runTraceAllow({
6212
+ scope,
6213
+ cwd: repositoryCwd(repoPath),
6214
+ json: options.json,
6215
+ harnessHooks: options.harnessHooks,
6216
+ allHarnesses: options.allHarnesses,
6217
+ traceCommand,
6218
+ verifyCommand: settings.verifyCommand,
6219
+ stdout: settings.stdout,
6220
+ stderr: settings.stderr
6221
+ }));
6222
+ });
6223
+ configureJsonOutput(trace.command("deny [path]").description("Stop publishing traces from one repository to the hosted store")).action(async (repoPath, options) => {
6224
+ settings.setExitCode(await runtime.runTraceDeny({
6225
+ scope,
6226
+ cwd: repositoryCwd(repoPath),
6227
+ json: options.json,
6228
+ stdout: settings.stdout,
6229
+ stderr: settings.stderr
6230
+ }));
6231
+ });
6232
+ configureOutput(trace.command("enable [path]").description("Enable trace hooks for one Git repository")).action(async (repoPath) => {
6233
+ settings.setExitCode(await runtime.runTraceEnable({
6234
+ scope,
6235
+ cwd: repositoryCwd(repoPath),
6236
+ stdout: settings.stdout,
6237
+ stderr: settings.stderr,
6238
+ traceCommand
6239
+ }));
6240
+ });
6241
+ configureOutput(trace.command("disable [path]").description("Disable the trace hooks of one Git repository")).action(async (repoPath) => {
6242
+ settings.setExitCode(await runtime.runTraceDisable({
6243
+ scope,
6244
+ cwd: repositoryCwd(repoPath),
6245
+ stdout: settings.stdout
6246
+ }));
6247
+ });
6248
+ configureOutput(trace.command("repair [path]").description("Repair the trace hooks of one Git repository")).action(async (repoPath) => {
6249
+ settings.setExitCode(await runtime.runTraceRepair({
6250
+ scope,
6251
+ cwd: repositoryCwd(repoPath),
6252
+ stdout: settings.stdout,
6253
+ stderr: settings.stderr,
6254
+ traceCommand
6255
+ }));
6256
+ });
6257
+ }
6258
+ //#endregion
6259
+ //#region ../trace-core/src/trace-hook-commands.ts
6260
+ /** Registers upload and hidden hook actions; never resolves the machine scope. */
6261
+ function registerTraceHookCommands(trace, settings) {
6262
+ const { runtime, cwd, scope, traceCommand, configureOutput, configureJsonOutput } = settings;
6263
+ configureJsonOutput(trace.command("sync <session-id>").description("Upload a local session trace and its metadata").option("--repo <repo>", "GitHub owner/repo").addOption(new Option("--expect-storage <selection>", "abort when the storage selection changed since capture").hideHelp())).action(async (sessionId, options) => {
6264
+ settings.setExitCode(await runtime.runTraceSync({
6265
+ scope,
6266
+ cwd,
6267
+ sessionId,
6268
+ repo: options.repo,
6269
+ json: options.json,
6270
+ expectStorage: options.expectStorage,
6271
+ stdout: settings.stdout,
6272
+ stderr: settings.stderr
6273
+ }));
6274
+ });
6275
+ configureOutput(trace.command("hook <event>", { hidden: true }).description("Handle agent session lifecycle hooks").option("--session <id>", "Agent session ID")).action(async (event, options) => {
6276
+ settings.setExitCode(await runtime.runTraceHook({
6277
+ scope,
6278
+ cwd,
6279
+ event,
6280
+ sessionId: options.session,
6281
+ stdin: settings.stdin,
6282
+ traceCommand
6283
+ }));
6284
+ });
6285
+ configureOutput(trace.command("git-hook <hook> [args...]", { hidden: true }).description("Run a package-owned Git trace hook")).action(async (hook, args) => {
6286
+ settings.setExitCode(await runtime.runTraceGitHook({
6287
+ scope,
6288
+ cwd,
6289
+ hook,
6290
+ args,
6291
+ stdin: settings.stdin,
6292
+ stderr: settings.stderr,
6293
+ traceCommand
6294
+ }));
6295
+ });
6296
+ }
6297
+ //#endregion
6298
+ //#region ../trace-core/src/trace-read-commands.ts
6299
+ /** Registers Review trace reads; never resolves Review state. */
6300
+ function registerTraceReadCommands(trace, settings) {
6301
+ const { runtime, cwd, configureJsonOutput } = settings;
6302
+ const withStorage = (command) => addTraceStorageOption(command);
6303
+ const listOptions = (command) => command.option("--session <uuid>", "Whiteboard session UUID").option("--commit <sha>", "commit or revision");
6304
+ const pullOptions = (command) => command.option("--session <uuid>", "pull sessions for one Whiteboard");
6305
+ configureJsonOutput(withStorage(listOptions(trace.command("list").description("List agent sessions for a Whiteboard or commit")))).action(async (options) => {
6306
+ if (options.session && options.commit) throw new Error("Use either --session or --commit, not both.");
6307
+ settings.setExitCode(await runtime.runTraceList({
6308
+ cwd,
6309
+ reviewUuid: options.session,
6310
+ commitSha: options.commit,
6311
+ storage: options.storage,
6312
+ json: options.json,
6313
+ stdout: settings.stdout
6314
+ }));
6315
+ });
6316
+ configureJsonOutput(withStorage(trace.command("show <session-id>").description("Survey a trace or show an exact event").option("--trace <name>", "trace name; omit for the main trace").option("--event <index>", "print the complete text of one event", (value) => Number.parseInt(value, 10)).option("--kind <kind>", "only list user|assistant|tool|separator rows"))).action(async (sessionId, options) => {
6317
+ settings.setExitCode(await runtime.runTraceShow({
6318
+ cwd,
6319
+ sessionId,
6320
+ trace: options.trace,
6321
+ eventIndex: options.event,
6322
+ kind: options.kind,
6323
+ storage: options.storage,
6324
+ json: options.json,
6325
+ stdout: settings.stdout,
6326
+ stderr: settings.stderr
6327
+ }));
6328
+ });
6329
+ configureJsonOutput(withStorage(pullOptions(trace.command("pull").description("Pull traces into the local FFF search corpus").option("--repo <owner/repo>", "repository for the corpus path")).option("--commit <sha>", "pull sessions for one commit or revision").option("--agent-session <id>", "pull one session").option("--main-only", "exclude subagent traces"))).action(async (options) => {
6330
+ if ([
6331
+ options.session,
6332
+ options.commit,
6333
+ options.agentSession
6334
+ ].filter(Boolean).length > 1) throw new Error("Use only one of --session, --commit, or --agent-session.");
6335
+ settings.setExitCode(await runtime.runTracePull({
6336
+ cwd,
6337
+ repo: options.repo,
6338
+ reviewUuid: options.session,
6339
+ commitSha: options.commit,
6340
+ session: options.agentSession,
6341
+ mainOnly: options.mainOnly,
6342
+ storage: options.storage,
6343
+ json: options.json,
6344
+ stdout: settings.stdout,
6345
+ stderr: settings.stderr
6346
+ }));
6347
+ });
6348
+ configureJsonOutput(withStorage(trace.command("blame <file>").description("Blame lines in a file to agent sessions").option("-L, --lines <range>", "start,end line range").option("--history", "use git log -L to include every commit that shaped the lines"))).action(async (file, options) => {
6349
+ settings.setExitCode(await runtime.runTraceBlame({
6350
+ cwd,
6351
+ file,
6352
+ lines: options.lines,
6353
+ history: options.history,
6354
+ storage: options.storage,
6355
+ json: options.json,
6356
+ stdout: settings.stdout,
6357
+ stderr: settings.stderr
6358
+ }));
6359
+ });
6360
+ }
6361
+ //#endregion
6362
+ //#region ../trace-core/src/trace-commands.ts
6363
+ /** Registers the shared trace surface with explicit runtime and scope; never imports Review app state. */
6364
+ function registerTraceCommands(parent, options) {
6365
+ registerTraceCaptureCommands(parent, options);
6366
+ registerTraceReadCommands(parent, options);
6367
+ registerTraceHookCommands(parent, options);
6368
+ }
6369
+ //#endregion
6370
+ //#region src/review-home-paths.ts
6371
+ /** `<basename>-<12 hex>` of a resolved path, one safe storage segment. */
6372
+ function hashedSegment(target, fallback) {
6373
+ const resolved = path.resolve(target);
6374
+ return `${safeStorageSegment(path.basename(resolved) || fallback)}-${crypto.createHash("sha256").update(resolved).digest("hex").slice(0, 12)}`;
6375
+ }
6376
+ /** Encode a value as one filesystem-safe storage path segment. */
6377
+ function safeStorageSegment(value) {
6378
+ return value.replace(/[^A-Za-z0-9_.-]+/g, "__");
6379
+ }
6380
+ function reviewDesktopRoot(env = process.env) {
6381
+ return path.join(devReviewHome(env), "review-desktop");
6382
+ }
6383
+ /** Written by Desktops that predate instance selection. */
6384
+ function reviewLegacyDiscoveryPath(env = process.env) {
6385
+ return path.join(reviewDesktopRoot(env), "server.json");
6386
+ }
6387
+ function reviewInstancesDir(env = process.env) {
6388
+ return path.join(reviewDesktopRoot(env), "instances");
6389
+ }
6390
+ function reviewInstanceDiscoveryPath(key, env = process.env) {
6391
+ return path.join(reviewInstancesDir(env), `${key}.json`);
6392
+ }
6393
+ function reviewDefaultInstancePath(env = process.env) {
6394
+ return path.join(reviewDesktopRoot(env), "default-instance");
6395
+ }
6396
+ /** `dev-<name>-<hash>` for a source checkout, keyed like reviewRepoStorageRoot. */
6397
+ function reviewDevInstanceKey(checkout) {
6398
+ return `dev-${hashedSegment(checkout, "checkout")}`;
6399
+ }
6400
+ function reviewDesktopStateDir(env = process.env) {
6401
+ return path.join(reviewDesktopRoot(env), "state");
6402
+ }
6403
+ //#endregion
6404
+ export { resolveTraceCommand as $, runTraceDeny as A, TraceStorageDeniedError as B, disableTraceMachine as C, listReviewTraceSessions as D, traceMachineStatus as E, describeSelection as F, listTraceRepositoryRoots as G, resolveTraceRepositoryTarget as H, isTraceStorageConfigured as I, HOSTED_CAPTURE_SCOPE_DESCRIPTION as J, traceRepositoryStatus as K, loadS3TraceStorage as L, runTraceSessions as M, runTraceStoreDelete as N, loadReviewAgentTrace as O, runTraceStoreInfo as P, removeAgentTraceHook as Q, resolveTraceStorage as R, configureTraceMachine as S, traceMachineEnabled as T, disableAllTraceRepositories as U, requireTraceConsent as V, enableTraceRepository as W, describeTraceHookOwners as X, AGENT_TRACE_HOOK_AGENTS as Y, installHarnessHooks as Z, runTraceRepair as _, reviewInstancesDir as a, openUrlInBrowser as at, runTraceGitHook as b, registerTraceCommands as c, runStoreLogout as ct, runTraceList as d, StoreClient as dt, shellQuote as et, runTracePull as f, withStoreAuthorization as ft, runTraceInstallMachine as g, jsonRequestedInArgv as gt, runTraceEnable as h, humanStream as ht, reviewInstanceDiscoveryPath as i, clearStoreAuth as it, runTraceOnboard as j, runTraceAllow as k, resolveTraceReadStorage as l, runStoreWhoami as lt, runTraceDisable as m, failWithJsonError as mt, reviewDesktopStateDir as n, traceScope as nt, reviewLegacyDiscoveryPath as o, readStoreAuth as ot, runTraceShow as p, emitJsonEvent as pt, readTraceUserConfig as q, reviewDevInstanceKey as r, DEFAULT_STORE_ORIGIN as rt, safeStorageSegment as s, runStoreLogin as st, reviewDefaultInstancePath as t, traceHomeDir as tt, runTraceBlame as u, StoreApiError as ut, runTraceStatus as v, readLegacyCaptureSettings as w, runTraceHook as x, runTraceSync as y, selectTraceStorage as z };