glassbox 1.0.0 → 1.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -49,148 +49,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
49
49
  mod
50
50
  ));
51
51
 
52
- // src/review-notes/types.ts
53
- function isNoteKind(value) {
54
- return NOTE_KINDS.includes(value);
55
- }
56
- var NOTE_KINDS, CONFIDENCE_PROPERTY_KEY, ANCHOR_FINGERPRINT_KEY, DEFAULT_PRODUCER, DEFAULT_SHARD_CAP;
57
- var init_types = __esm({
58
- "src/review-notes/types.ts"() {
59
- "use strict";
60
- NOTE_KINDS = [
61
- "rationale",
62
- "proof",
63
- "assumption",
64
- "alternative-considered",
65
- "risk",
66
- "test-evidence"
67
- ];
68
- CONFIDENCE_PROPERTY_KEY = "ext-ai-tool-confidence";
69
- ANCHOR_FINGERPRINT_KEY = "prNoteAnchor/v1";
70
- DEFAULT_PRODUCER = "unknown-ai-tool";
71
- DEFAULT_SHARD_CAP = 1e4;
72
- }
73
- });
74
-
75
- // src/review-notes/instructions.ts
76
- function reviewNoteInstructions() {
77
- return [
78
- "# Emitting AI review notes (`.pr-notes/`)",
79
- "",
80
- "As you write or change code, leave **line-anchored review notes** that explain",
81
- "your reasoning and prove your work \u2014 the rationale a careful reviewer (human or",
82
- "the next AI session) would want, anchored to the exact lines it concerns. Notes",
83
- "are committed to the repo as tool-neutral SARIF under `.pr-notes/` and shown",
84
- "review-comment-style in Glassbox. Write them **while editing**, at the moment of",
85
- "richest context \u2014 not reconstructed afterward.",
86
- "",
87
- "## When to emit a note",
88
- "",
89
- 'Emit one when a future reader would otherwise have to ask "why?". Good triggers:',
90
- "",
91
- "- A **non-obvious decision** \u2014 why this approach over the obvious one.",
92
- "- **Proof** a change is correct \u2014 the reasoning, test output, or invariant that backs it.",
93
- "- An **assumption** you relied on that could later be wrong.",
94
- "- An **alternative you considered and rejected**, and why.",
95
- "- A **risk** or sharp edge a reviewer should scrutinize.",
96
- "- **Test evidence** demonstrating the change works.",
97
- "",
98
- "Do **not** narrate the obvious or restate what the diff already says. One note per",
99
- "genuine decision or claim \u2014 not per line.",
100
- "",
101
- "## How to emit a note",
102
- "",
103
- "Shell out to the `glassbox note` CLI (it owns the on-disk format, fingerprint,",
104
- "and commit provenance \u2014 you never write the SARIF by hand):",
105
- "",
106
- "```",
107
- "glassbox note add \\",
108
- " --file <repo-relative path> \\",
109
- " --lines <A[-B]> \\",
110
- ` --kind <${NOTE_KINDS.join("|")}> \\`,
111
- ' --body - \\ # body read from stdin (use --body "text" for short notes)',
112
- " [--confidence 0..1] [--rank 0..100] [--ticket <id|url>] \\",
113
- " [--artifact <path>]\u2026 \\ # attach committed proof artifacts (repeatable)",
114
- ' [--producer "<your tool/agent name>"] [--producer-version <v>]',
115
- "```",
116
- "",
117
- "Always set `--producer` so the note records who wrote it, and `--ticket` when the",
118
- "work traces to a tracked task. Keep the body focused: the claim and its support.",
119
- "",
120
- "## Proof artifacts \u2014 diagrams, test output, screenshots",
121
- "",
122
- "When a claim is clearer shown than told, save the evidence under",
123
- "`.pr-notes/artifacts/` and attach it with `--artifact <path>` (repeatable):",
124
- "",
125
- "- **When a diagram is the clearest proof or rationale** \u2014 a state machine, a",
126
- " data/control flow, a sequence of interactions, an architecture relationship \u2014",
127
- " **write it as Mermaid SOURCE** (a `.mmd` file) and attach that. Diagram source",
128
- " stays diffable and reviewable; never attach a rendered diagram image, and",
129
- " never fall back to ASCII art in the note body.",
130
- "- **Test output, logs, and measurements** are attached the same way as plain",
131
- " text files.",
132
- "- **Screenshots** are for genuinely visual claims only \u2014 downscale and compress",
133
- " (WebP/AVIF; stored via Git LFS automatically).",
134
- "",
135
- "Prefer text/diagram-source over images, and attach one artifact per *claim*,",
136
- "not per step.",
137
- "",
138
- "## Revise and consolidate",
139
- "",
140
- "Notes should reflect the **final** state of your change, not an obsolete",
141
- "intermediate step:",
142
- "",
143
- "- `glassbox note add` prints a stable note id (guid).",
144
- "- `glassbox note update --id <guid> [--body -|--kind \u2026|--confidence \u2026|--rank \u2026|--ticket \u2026]`",
145
- " to correct a note as the work evolves.",
146
- "- `glassbox note remove --id <guid>` to drop one that no longer applies.",
147
- "- When the task is done, run `glassbox note coalesce` to drop redundant notes",
148
- " (identical anchor + kind + body, keeping the most recent).",
149
- "",
150
- "## Final consolidation pass",
151
- "",
152
- "After the mechanical `coalesce`, do one **cross-cutting pass over all the notes",
153
- "you wrote this session** \u2014 the relationships that only become visible once the",
154
- "whole change is in front of you, not while editing a single file. `coalesce`",
155
- "only catches byte-identical duplicates; this pass is the judgment it cannot make:",
156
- "",
157
- "- **Merge near-duplicates.** Two notes that make the *same* point in different",
158
- " words (e.g. the same rationale restated on two files) aren't caught by",
159
- " `coalesce`. Keep the clearest one \u2014 `update` it to the best wording, widen its",
160
- " anchor or `--ticket` if helpful \u2014 and `remove` the rest.",
161
- "- **Link related notes across files.** When several notes are facets of one",
162
- " decision spanning multiple files, make that explicit: name the related file(s)",
163
- ' and the connecting idea in each note body (a short "see also `path/to/file`"',
164
- " line), and give them a shared `--ticket` so a reader can pivot between them.",
165
- " Bodies render as Markdown, so an inline `` `path` `` reference reads cleanly.",
166
- "- **Prune what the finished change made obvious.** A note that justified an",
167
- " intermediate step the final diff no longer shows is noise \u2014 `remove` it.",
168
- "",
169
- "The goal is the smallest set of notes that still proves the work: no restated",
170
- "point twice, and every cross-file relationship spelled out where a reviewer will",
171
- "see it.",
172
- "",
173
- "If your tool can't shell out, write the SARIF directly under `.pr-notes/` per the",
174
- "format in `docs/20-ai-review-notes.md` \u2014 but prefer the CLI."
175
- ].join("\n");
176
- }
177
- var init_instructions = __esm({
178
- "src/review-notes/instructions.ts"() {
179
- "use strict";
180
- init_types();
181
- }
182
- });
183
-
184
- // src/db/ids.ts
185
- function generateId() {
186
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
187
- }
188
- var init_ids = __esm({
189
- "src/db/ids.ts"() {
190
- "use strict";
191
- }
192
- });
193
-
194
52
  // node_modules/zod/v4/core/core.js
195
53
  // @__NO_SIDE_EFFECTS__
196
54
  function $constructor(name, initializer3, params) {
@@ -15351,6 +15209,247 @@ var init_zod = __esm({
15351
15209
  }
15352
15210
  });
15353
15211
 
15212
+ // src/app-version.ts
15213
+ import { readFileSync } from "fs";
15214
+ import { dirname, join } from "path";
15215
+ import { fileURLToPath } from "url";
15216
+ function getCurrentVersion() {
15217
+ try {
15218
+ const dir = dirname(fileURLToPath(import.meta.url));
15219
+ const raw2 = JSON.parse(readFileSync(join(dir, "..", "package.json"), "utf-8"));
15220
+ return VersionPayloadSchema.parse(raw2).version;
15221
+ } catch {
15222
+ return "0.0.0";
15223
+ }
15224
+ }
15225
+ var VersionPayloadSchema;
15226
+ var init_app_version = __esm({
15227
+ "src/app-version.ts"() {
15228
+ "use strict";
15229
+ init_zod();
15230
+ VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
15231
+ }
15232
+ });
15233
+
15234
+ // src/review-notes/types.ts
15235
+ function isNoteKind(value) {
15236
+ return NOTE_KINDS.includes(value);
15237
+ }
15238
+ var NOTE_KINDS, CONFIDENCE_PROPERTY_KEY, ANCHOR_FINGERPRINT_KEY, DEFAULT_PRODUCER, DEFAULT_SHARD_CAP;
15239
+ var init_types = __esm({
15240
+ "src/review-notes/types.ts"() {
15241
+ "use strict";
15242
+ NOTE_KINDS = [
15243
+ "rationale",
15244
+ "proof",
15245
+ "assumption",
15246
+ "alternative-considered",
15247
+ "risk",
15248
+ "test-evidence"
15249
+ ];
15250
+ CONFIDENCE_PROPERTY_KEY = "ext-ai-tool-confidence";
15251
+ ANCHOR_FINGERPRINT_KEY = "prNoteAnchor/v1";
15252
+ DEFAULT_PRODUCER = "unknown-ai-tool";
15253
+ DEFAULT_SHARD_CAP = 1e4;
15254
+ }
15255
+ });
15256
+
15257
+ // src/review-notes/instructions.ts
15258
+ function reviewNoteInstructions() {
15259
+ return [
15260
+ "# Emitting AI review notes (`.pr-notes/`)",
15261
+ "",
15262
+ "As you write or change code, leave **line-anchored review notes** that explain",
15263
+ "your reasoning and prove your work \u2014 the rationale a careful reviewer (human or",
15264
+ "the next AI session) would want, anchored to the exact lines it concerns. Notes",
15265
+ "are committed to the repo as tool-neutral SARIF under `.pr-notes/` and shown",
15266
+ "review-comment-style in Glassbox. Write them **while editing**, at the moment of",
15267
+ "richest context \u2014 not reconstructed afterward.",
15268
+ "",
15269
+ "## When to emit a note",
15270
+ "",
15271
+ 'Emit one when a future reader would otherwise have to ask "why?". Good triggers:',
15272
+ "",
15273
+ "- A **non-obvious decision** \u2014 why this approach over the obvious one.",
15274
+ "- **Proof** a change is correct \u2014 the reasoning, test output, or invariant that backs it.",
15275
+ "- An **assumption** you relied on that could later be wrong.",
15276
+ "- An **alternative you considered and rejected**, and why.",
15277
+ "- A **risk** or sharp edge a reviewer should scrutinize.",
15278
+ "- **Test evidence** demonstrating the change works.",
15279
+ "",
15280
+ "Do **not** narrate the obvious or restate what the diff already says. One note per",
15281
+ "genuine decision or claim \u2014 not per line.",
15282
+ "",
15283
+ "## How to emit a note",
15284
+ "",
15285
+ "Shell out to the `glassbox note` CLI (it owns the on-disk format, fingerprint,",
15286
+ "and commit provenance \u2014 you never write the SARIF by hand):",
15287
+ "",
15288
+ "```",
15289
+ "glassbox note add \\",
15290
+ " --file <repo-relative path> \\",
15291
+ " --lines <A[-B]> \\",
15292
+ ` --kind <${NOTE_KINDS.join("|")}> \\`,
15293
+ ' --body - \\ # body read from stdin (use --body "text" for short notes)',
15294
+ " [--confidence 0..1] [--rank 0..100] [--ticket <id|url>] \\",
15295
+ " [--artifact <path>]\u2026 \\ # attach committed proof artifacts (repeatable)",
15296
+ " [--related <file:line>]\u2026 \\ # code locations the body links to (repeatable)",
15297
+ ' [--producer "<your tool/agent name>"] [--producer-version <v>]',
15298
+ "```",
15299
+ "",
15300
+ "Always set `--producer` so the note records who wrote it, and `--ticket` when the",
15301
+ "work traces to a tracked task. Keep the body focused: the claim and its support.",
15302
+ "",
15303
+ "## Writing the body",
15304
+ "",
15305
+ "**Lead with a one-sentence summary.** The body's first sentence is what a reader",
15306
+ "sees where space is tight \u2014 the export folds each note into a list item, and a",
15307
+ "viewer may show only the opening. Make it stand alone, then expand beneath it.",
15308
+ "",
15309
+ "Bodies are **markdown** and may span multiple lines. Glassbox renders:",
15310
+ "",
15311
+ "- Headings (`### Root cause`), paragraphs, and thematic breaks (`---`)",
15312
+ "- Unordered and ordered lists, including nested ones",
15313
+ "- Fenced code blocks (```` ``` ```` or `~~~`) \u2014 use these for snippets and",
15314
+ " command output; indented code blocks are **not** rendered as code",
15315
+ "- Blockquotes (`> \u2026`)",
15316
+ "- Inline `` `code` ``, **bold**, _italic_, and http(s)/mailto links",
15317
+ "",
15318
+ "**Link to other code instead of describing it.** Pass `--related <file:line>`",
15319
+ "(repeatable) and reference the Nth one from the body as `[text](N)`, 0-based \u2014",
15320
+ "it renders as a click-to-jump link to that line. Prefer this over spelling out",
15321
+ '"see src/foo.ts line 120" in prose.',
15322
+ "",
15323
+ "Not rendered: tables, reference-style links, and raw HTML (a body is escaped",
15324
+ "before formatting, so any HTML shows as literal text). A single newline inside a",
15325
+ "paragraph renders as a visible line break, so short line-oriented prose reads the",
15326
+ "way you wrote it.",
15327
+ "",
15328
+ "## Proof artifacts \u2014 diagrams, test output, screenshots",
15329
+ "",
15330
+ "When a claim is clearer shown than told, save the evidence under",
15331
+ "`.pr-notes/artifacts/` and attach it with `--artifact <path>` (repeatable):",
15332
+ "",
15333
+ "- **When a diagram is the clearest proof or rationale** \u2014 a state machine, a",
15334
+ " data/control flow, a sequence of interactions, an architecture relationship \u2014",
15335
+ " **write it as Mermaid SOURCE** (a `.mmd` file) and attach that. Diagram source",
15336
+ " stays diffable and reviewable; never attach a rendered diagram image, and",
15337
+ " never fall back to ASCII art in the note body.",
15338
+ "- **Test output, logs, and measurements** are attached the same way as plain",
15339
+ " text files.",
15340
+ "- **Screenshots** are for genuinely visual claims only \u2014 downscale and compress",
15341
+ " (WebP/AVIF; stored via Git LFS automatically).",
15342
+ "",
15343
+ "Prefer text/diagram-source over images, and attach one artifact per *claim*,",
15344
+ "not per step.",
15345
+ "",
15346
+ "## Revise and consolidate",
15347
+ "",
15348
+ "Notes should reflect the **final** state of your change, not an obsolete",
15349
+ "intermediate step:",
15350
+ "",
15351
+ "- `glassbox note add` prints a stable note id (guid).",
15352
+ "- `glassbox note update --id <guid> [--body -|--kind \u2026|--confidence \u2026|--rank \u2026|--ticket \u2026]`",
15353
+ " to correct a note as the work evolves.",
15354
+ "- `glassbox note remove --id <guid>` to drop one that no longer applies.",
15355
+ "- When the task is done, run `glassbox note coalesce` to drop redundant notes",
15356
+ " (identical anchor + kind + body, keeping the most recent).",
15357
+ "",
15358
+ "## Final consolidation pass",
15359
+ "",
15360
+ "After the mechanical `coalesce`, do one **cross-cutting pass over all the notes",
15361
+ "you wrote this session** \u2014 the relationships that only become visible once the",
15362
+ "whole change is in front of you, not while editing a single file. `coalesce`",
15363
+ "only catches byte-identical duplicates; this pass is the judgment it cannot make:",
15364
+ "",
15365
+ "- **Merge near-duplicates.** Two notes that make the *same* point in different",
15366
+ " words (e.g. the same rationale restated on two files) aren't caught by",
15367
+ " `coalesce`. Keep the clearest one \u2014 `update` it to the best wording, widen its",
15368
+ " anchor or `--ticket` if helpful \u2014 and `remove` the rest.",
15369
+ "- **Link related notes across files.** When several notes are facets of one",
15370
+ " decision spanning multiple files, make that explicit: name the related file(s)",
15371
+ ' and the connecting idea in each note body (a short "see also `path/to/file`"',
15372
+ " line), and give them a shared `--ticket` so a reader can pivot between them.",
15373
+ " Bodies render as Markdown, so an inline `` `path` `` reference reads cleanly.",
15374
+ "- **Prune what the finished change made obvious.** A note that justified an",
15375
+ " intermediate step the final diff no longer shows is noise \u2014 `remove` it.",
15376
+ "",
15377
+ "The goal is the smallest set of notes that still proves the work: no restated",
15378
+ "point twice, and every cross-file relationship spelled out where a reviewer will",
15379
+ "see it.",
15380
+ "",
15381
+ "If your tool can't shell out, write the SARIF directly under `.pr-notes/` per the",
15382
+ "format in `docs/20-ai-review-notes.md` \u2014 but prefer the CLI."
15383
+ ].join("\n");
15384
+ }
15385
+ var init_instructions = __esm({
15386
+ "src/review-notes/instructions.ts"() {
15387
+ "use strict";
15388
+ init_types();
15389
+ }
15390
+ });
15391
+
15392
+ // src/db/ids.ts
15393
+ function generateId() {
15394
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
15395
+ }
15396
+ var init_ids = __esm({
15397
+ "src/db/ids.ts"() {
15398
+ "use strict";
15399
+ }
15400
+ });
15401
+
15402
+ // src/utils/lfs.ts
15403
+ function isLfsPointer(data) {
15404
+ const length = typeof data === "string" ? data.length : data.byteLength;
15405
+ if (length === 0 || length > MAX_POINTER_BYTES) return false;
15406
+ const head = typeof data === "string" ? data.slice(0, VERSION_LINE.length) : data.subarray(0, VERSION_LINE.length).toString("latin1");
15407
+ if (head !== VERSION_LINE) return false;
15408
+ const text = typeof data === "string" ? data : data.toString("utf-8");
15409
+ return OID_RE.test(text) && SIZE_RE.test(text);
15410
+ }
15411
+ var MAX_POINTER_BYTES, VERSION_LINE, OID_RE, SIZE_RE;
15412
+ var init_lfs = __esm({
15413
+ "src/utils/lfs.ts"() {
15414
+ "use strict";
15415
+ MAX_POINTER_BYTES = 1024;
15416
+ VERSION_LINE = "version https://git-lfs.github.com/spec/v1";
15417
+ OID_RE = /(^|\n)oid sha256:[0-9a-f]{64}(\r?\n|$)/;
15418
+ SIZE_RE = /(^|\n)size \d+(\r?\n|$)/;
15419
+ }
15420
+ });
15421
+
15422
+ // src/utils/flattenMarkdown.ts
15423
+ function stripInline(line) {
15424
+ return line.replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, "$1 ($2)").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1$2").replace(/(^|[^_])_([^_\s][^_]*)_/g, "$1$2");
15425
+ }
15426
+ function flattenMarkdown(body) {
15427
+ const out = [];
15428
+ let inFence = false;
15429
+ for (const line of body.split("\n")) {
15430
+ if (FENCE_RE.test(line)) {
15431
+ inFence = !inFence;
15432
+ continue;
15433
+ }
15434
+ if (inFence) {
15435
+ out.push(line);
15436
+ continue;
15437
+ }
15438
+ const heading = HEADING_RE.exec(line);
15439
+ const text = heading !== null ? heading[1].replace(/\s+#+\s*$/, "") : line;
15440
+ out.push(stripInline(text));
15441
+ }
15442
+ return out.join("\n");
15443
+ }
15444
+ var FENCE_RE, HEADING_RE;
15445
+ var init_flattenMarkdown = __esm({
15446
+ "src/utils/flattenMarkdown.ts"() {
15447
+ "use strict";
15448
+ FENCE_RE = /^ {0,3}(?:`{3,}|~{3,})/;
15449
+ HEADING_RE = /^ {0,3}#{1,6}\s+(.*)$/;
15450
+ }
15451
+ });
15452
+
15354
15453
  // src/review-notes/sarif.ts
15355
15454
  function emptyLog(producer, opts = {}) {
15356
15455
  return { $schema: SARIF_SCHEMA_URL, version: "2.1.0", runs: [newRun(producer, opts)] };
@@ -15379,6 +15478,10 @@ function newRun(producer, opts = {}) {
15379
15478
  }
15380
15479
  return run;
15381
15480
  }
15481
+ function noteMessage(body) {
15482
+ const flat = flattenMarkdown(body);
15483
+ return { text: flat === "" ? body : flat, markdown: body };
15484
+ }
15382
15485
  function buildResult(input, meta3) {
15383
15486
  const region = { startLine: input.startLine, endLine: input.endLine };
15384
15487
  if (meta3.snippet !== void 0) region.snippet = { text: meta3.snippet };
@@ -15390,7 +15493,7 @@ function buildResult(input, meta3) {
15390
15493
  kind: "informational",
15391
15494
  level: input.kind === "risk" ? "warning" : "none",
15392
15495
  guid: meta3.guid,
15393
- message: { text: input.body, markdown: input.body },
15496
+ message: noteMessage(input.body),
15394
15497
  locations: [{
15395
15498
  physicalLocation: {
15396
15499
  artifactLocation: { uri: input.file },
@@ -15399,6 +15502,15 @@ function buildResult(input, meta3) {
15399
15502
  }],
15400
15503
  properties
15401
15504
  };
15505
+ if (input.related !== void 0 && input.related.length > 0) {
15506
+ result.relatedLocations = input.related.map((loc, index) => ({
15507
+ id: index,
15508
+ physicalLocation: {
15509
+ artifactLocation: { uri: loc.uri },
15510
+ region: { startLine: loc.line }
15511
+ }
15512
+ }));
15513
+ }
15402
15514
  if (input.rank !== void 0) result.rank = input.rank;
15403
15515
  if (input.ticket !== void 0 && input.ticket !== "") result.workItemUris = [input.ticket];
15404
15516
  if (meta3.fingerprint !== void 0) result.partialFingerprints = { [ANCHOR_FINGERPRINT_KEY]: meta3.fingerprint };
@@ -15417,6 +15529,7 @@ var init_sarif = __esm({
15417
15529
  "src/review-notes/sarif.ts"() {
15418
15530
  "use strict";
15419
15531
  init_zod();
15532
+ init_flattenMarkdown();
15420
15533
  init_types();
15421
15534
  SARIF_SCHEMA_URL = "https://json.schemastore.org/sarif-2.1.0.json";
15422
15535
  REVIEW_NOTE_RULE_ID = "review-note";
@@ -15451,17 +15564,17 @@ var init_view = __esm({
15451
15564
  // src/review-notes/store.ts
15452
15565
  import { spawnSync } from "child_process";
15453
15566
  import { createHash } from "crypto";
15454
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
15455
- import { dirname, join } from "path";
15567
+ import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, unlinkSync, writeFileSync } from "fs";
15568
+ import { dirname as dirname2, join as join2 } from "path";
15456
15569
  function sanitizeRel(file2) {
15457
15570
  const rel = file2.replace(/\\/g, "/").replace(/^\/+/, "").replace(/(^|\/)\.\.(?=\/|$)/g, "$1_");
15458
15571
  return rel === "" ? "file" : rel;
15459
15572
  }
15460
15573
  function shardPath(repoRoot, safeRel, index) {
15461
- return join(repoRoot, NOTES_SUBDIR, `${safeRel}.${String(index).padStart(6, "0")}.sarif`);
15574
+ return join2(repoRoot, NOTES_SUBDIR, `${safeRel}.${String(index).padStart(6, "0")}.sarif`);
15462
15575
  }
15463
15576
  function listShardIndices(repoRoot, safeRel) {
15464
- const dir = join(repoRoot, NOTES_SUBDIR, dirname(safeRel));
15577
+ const dir = join2(repoRoot, NOTES_SUBDIR, dirname2(safeRel));
15465
15578
  if (!existsSync(dir)) return [];
15466
15579
  const base = safeRel.split("/").pop() ?? safeRel;
15467
15580
  const indices = [];
@@ -15476,7 +15589,7 @@ function totalResults(log) {
15476
15589
  return log.runs.reduce((sum, run) => sum + run.results.length, 0);
15477
15590
  }
15478
15591
  function readLog(path) {
15479
- const raw2 = JSON.parse(readFileSync(path, "utf-8"));
15592
+ const raw2 = JSON.parse(readFileSync2(path, "utf-8"));
15480
15593
  const parsed = SarifLogShapeSchema.safeParse(raw2);
15481
15594
  if (!parsed.success) {
15482
15595
  throw new Error(`existing notes shard is not a SARIF log we recognize, refusing to overwrite: ${path}`);
@@ -15502,7 +15615,7 @@ function gitValue(repoRoot, args) {
15502
15615
  }
15503
15616
  function anchorSnippet(repoRoot, safeRel, startLine, endLine) {
15504
15617
  try {
15505
- const lines = readFileSync(join(repoRoot, safeRel), "utf-8").split("\n");
15618
+ const lines = readFileSync2(join2(repoRoot, safeRel), "utf-8").split("\n");
15506
15619
  const slice = lines.slice(Math.max(0, startLine - 1), Math.max(startLine, endLine));
15507
15620
  if (slice.length === 0) return {};
15508
15621
  const snippet = slice.join("\n");
@@ -15551,10 +15664,13 @@ function writeReviewNote(repoRoot, input, opts = {}) {
15551
15664
  log = emptyLog(producer, { producerVersion: input.producerVersion, ...vcs });
15552
15665
  }
15553
15666
  findOrAddRun(log, producer, input.producerVersion, vcs).results.push(result);
15554
- mkdirSync(dirname(path), { recursive: true });
15667
+ mkdirSync(dirname2(path), { recursive: true });
15555
15668
  writeFileSync(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15556
15669
  return { path, guid: guid3 };
15557
15670
  }
15671
+ function noteBody(r) {
15672
+ return r.message?.markdown ?? r.message?.text ?? "";
15673
+ }
15558
15674
  function writeLog(path, log) {
15559
15675
  writeFileSync(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15560
15676
  }
@@ -15564,19 +15680,19 @@ function hashArtifacts(repoRoot, artifacts) {
15564
15680
  const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
15565
15681
  if (/(^|\/)\.\.(\/|$)/.test(safe)) continue;
15566
15682
  try {
15567
- const abs = join(repoRoot, safe);
15683
+ const abs = join2(repoRoot, safe);
15568
15684
  const stat = statSync(abs);
15569
15685
  if (!stat.isFile() || stat.size > ARTIFACT_HASH_MAX_BYTES) continue;
15570
- out[uri] = createHash("sha256").update(readFileSync(abs)).digest("hex");
15686
+ out[uri] = createHash("sha256").update(readFileSync2(abs)).digest("hex");
15571
15687
  } catch {
15572
15688
  }
15573
15689
  }
15574
15690
  return out;
15575
15691
  }
15576
15692
  function ensureArtifactLfsFilter(repoRoot) {
15577
- const path = join(repoRoot, ".gitattributes");
15693
+ const path = join2(repoRoot, ".gitattributes");
15578
15694
  try {
15579
- const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
15695
+ const existing = existsSync(path) ? readFileSync2(path, "utf-8") : "";
15580
15696
  if (existing.includes(".pr-notes/artifacts/**")) return;
15581
15697
  const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
15582
15698
  writeFileSync(path, `${existing}${prefix}${LFS_FILTER_LINE}
@@ -15588,10 +15704,10 @@ function listShardPaths(repoRoot, safeRel) {
15588
15704
  return listShardIndices(repoRoot, safeRel).map((i) => shardPath(repoRoot, safeRel, i));
15589
15705
  }
15590
15706
  function allShardPaths(repoRoot) {
15591
- const root = join(repoRoot, NOTES_SUBDIR);
15707
+ const root = join2(repoRoot, NOTES_SUBDIR);
15592
15708
  if (!existsSync(root)) return [];
15593
15709
  const entries = readdirSync(root, { recursive: true });
15594
- return entries.filter((e) => SHARD_RE.test(e)).map((e) => join(root, e));
15710
+ return entries.filter((e) => SHARD_RE.test(e)).map((e) => join2(root, e));
15595
15711
  }
15596
15712
  function persistOrDelete(path, log) {
15597
15713
  log.runs = log.runs.filter((run) => run.results.length > 0);
@@ -15623,7 +15739,7 @@ function updateNote(repoRoot, guid3, patch, file2) {
15623
15739
  for (const run of log.runs) {
15624
15740
  const result = run.results.find((r) => r.guid === guid3);
15625
15741
  if (result !== void 0) {
15626
- if (patch.body !== void 0) result.message = { text: patch.body, markdown: patch.body };
15742
+ if (patch.body !== void 0) result.message = noteMessage(patch.body);
15627
15743
  if (patch.kind !== void 0) {
15628
15744
  result.properties = { ...result.properties, tags: [patch.kind] };
15629
15745
  result.level = patch.kind === "risk" ? "warning" : "none";
@@ -15647,7 +15763,7 @@ function noteKey(r) {
15647
15763
  loc?.region?.startLine,
15648
15764
  loc?.region?.endLine,
15649
15765
  r.properties?.tags,
15650
- r.message?.text
15766
+ noteBody(r)
15651
15767
  ]);
15652
15768
  }
15653
15769
  function coalesceFile(repoRoot, file2) {
@@ -15683,7 +15799,7 @@ function coalesceFile(repoRoot, file2) {
15683
15799
  return toRemove.length;
15684
15800
  }
15685
15801
  function coalesceAll(repoRoot) {
15686
- const root = join(repoRoot, NOTES_SUBDIR);
15802
+ const root = join2(repoRoot, NOTES_SUBDIR);
15687
15803
  if (!existsSync(root)) return 0;
15688
15804
  const sources = /* @__PURE__ */ new Set();
15689
15805
  for (const entry of readdirSync(root, { recursive: true })) {
@@ -15698,16 +15814,27 @@ function readArtifactText(repoRoot, uri) {
15698
15814
  const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
15699
15815
  if (/(^|\/)\.\.(\/|$)/.test(safe)) return void 0;
15700
15816
  try {
15701
- const abs = join(repoRoot, safe);
15817
+ const abs = join2(repoRoot, safe);
15702
15818
  const stat = statSync(abs);
15703
15819
  if (!stat.isFile() || stat.size > ARTIFACT_MAX_BYTES) return void 0;
15704
- const buf = readFileSync(abs);
15820
+ const buf = readFileSync2(abs);
15705
15821
  if (buf.includes(0)) return void 0;
15822
+ if (isLfsPointer(buf)) return void 0;
15706
15823
  return buf.toString("utf-8");
15707
15824
  } catch {
15708
15825
  return void 0;
15709
15826
  }
15710
15827
  }
15828
+ function readRelated(result) {
15829
+ const raw2 = result.relatedLocations;
15830
+ if (raw2 === void 0 || raw2.length === 0) return void 0;
15831
+ return raw2.map((entry) => {
15832
+ const uri = entry.physicalLocation?.artifactLocation?.uri;
15833
+ const line = entry.physicalLocation?.region?.startLine;
15834
+ if (typeof uri !== "string" || uri === "" || typeof line !== "number") return { uri: "", line: 0 };
15835
+ return { uri, line };
15836
+ });
15837
+ }
15711
15838
  function readArtifacts(repoRoot, result) {
15712
15839
  const out = [];
15713
15840
  for (const att of result.attachments ?? []) {
@@ -15745,10 +15872,11 @@ function loadReviewNotesForFile(repoRoot, file2) {
15745
15872
  line: startLine,
15746
15873
  side: "new",
15747
15874
  kind,
15748
- body: r.message?.text ?? "",
15875
+ body: noteBody(r),
15749
15876
  confidence: typeof confidence === "number" ? confidence : void 0,
15750
15877
  producer: producer === "" ? void 0 : producer,
15751
15878
  snippet: region?.snippet?.text,
15879
+ related: readRelated(r),
15752
15880
  artifacts: readArtifacts(repoRoot, r)
15753
15881
  });
15754
15882
  }
@@ -15761,10 +15889,11 @@ var init_store = __esm({
15761
15889
  "src/review-notes/store.ts"() {
15762
15890
  "use strict";
15763
15891
  init_ids();
15892
+ init_lfs();
15764
15893
  init_sarif();
15765
15894
  init_types();
15766
15895
  init_view();
15767
- NOTES_SUBDIR = join(".pr-notes", "notes");
15896
+ NOTES_SUBDIR = join2(".pr-notes", "notes");
15768
15897
  SHARD_RE = /\.(\d{6})\.sarif$/;
15769
15898
  ARTIFACT_HASH_MAX_BYTES = 5e7;
15770
15899
  LFS_FILTER_LINE = ".pr-notes/artifacts/** filter=lfs diff=lfs merge=lfs -text";
@@ -15775,17 +15904,22 @@ var init_store = __esm({
15775
15904
  // src/review-notes/cli.ts
15776
15905
  var cli_exports = {};
15777
15906
  __export(cli_exports, {
15907
+ UnknownFlagError: () => UnknownFlagError,
15778
15908
  parseNoteAdd: () => parseNoteAdd,
15779
15909
  runNoteCli: () => runNoteCli
15780
15910
  });
15781
15911
  import { spawnSync as spawnSync2 } from "child_process";
15782
15912
  import { relative, resolve } from "path";
15783
- function parseFlags(args) {
15913
+ function parseFlags(args, sub) {
15914
+ const known = KNOWN_FLAGS[sub];
15784
15915
  const flags = /* @__PURE__ */ new Map();
15785
15916
  for (let i = 0; i < args.length; i++) {
15786
15917
  const a = args[i];
15787
15918
  if (!a.startsWith("--")) throw new Error(`unexpected argument: ${a}`);
15788
15919
  const key = a.slice(2);
15920
+ if (!known.includes(key)) {
15921
+ throw new UnknownFlagError(`unknown flag --${key} for 'glassbox note ${sub}' (accepted: ${known.map((f) => `--${f}`).join(", ")})`);
15922
+ }
15789
15923
  const next = args.at(i + 1);
15790
15924
  if (next === void 0 || next.startsWith("--")) throw new Error(`missing value for --${key}`);
15791
15925
  flags.set(key, next);
@@ -15827,11 +15961,21 @@ add \u2014 options:
15827
15961
  --producer-version <v>
15828
15962
  --artifact <path> Attach a committed proof artifact (test output, log,
15829
15963
  diagram source); repeatable, repo-relative path
15964
+ --related <file:line> A code location the body links to; repeatable. Reference
15965
+ the Nth one from the body as [text](N), 0-based \u2014 SARIF's
15966
+ embedded-link syntax, rendered as a jump-to-line link
15830
15967
 
15831
15968
  update/remove use the guid returned by 'add' (--file scopes the search; omit to search all notes).
15832
15969
  coalesce drops redundant notes (identical anchor + kind + body), keeping the most recent.
15833
15970
  `;
15834
15971
  }
15972
+ function parseRelated(value) {
15973
+ const match = /^(.+):(\d+)$/.exec(value);
15974
+ if (match === null) throw new Error(`--related must be <file:line> (e.g. src/a.ts:42), got "${value}"`);
15975
+ const line = parseInteger("--related line", match[2]);
15976
+ if (line < 1) throw new Error("--related line must be 1-based");
15977
+ return { uri: match[1], line };
15978
+ }
15835
15979
  function parseInteger(label, value) {
15836
15980
  const n = Number(value);
15837
15981
  if (!Number.isInteger(n)) throw new Error(`${label} must be an integer, got "${value}"`);
@@ -15854,7 +15998,7 @@ function parseConfidenceRank(flags) {
15854
15998
  return out;
15855
15999
  }
15856
16000
  function parseNoteAdd(args) {
15857
- const flags = parseFlags(args);
16001
+ const flags = parseFlags(args, "add");
15858
16002
  const file2 = flags.get("file");
15859
16003
  const lines = flags.get("lines");
15860
16004
  const kindRaw = flags.get("kind");
@@ -15868,7 +16012,15 @@ function parseNoteAdd(args) {
15868
16012
  const endRaw = lineMatch.at(2);
15869
16013
  const endLine = endRaw !== void 0 ? parseInteger("--lines end", endRaw) : startLine;
15870
16014
  if (startLine < 1 || endLine < startLine) throw new Error("--lines must be 1-based with start <= end");
15871
- const parsed = { file: file2, startLine, endLine, kind: kindRaw, artifacts: collectRepeatable(args, "artifact"), bodyStdin: false };
16015
+ const parsed = {
16016
+ file: file2,
16017
+ startLine,
16018
+ endLine,
16019
+ kind: kindRaw,
16020
+ artifacts: collectRepeatable(args, "artifact"),
16021
+ related: collectRepeatable(args, "related").map(parseRelated),
16022
+ bodyStdin: false
16023
+ };
15872
16024
  Object.assign(parsed, parseConfidenceRank(flags));
15873
16025
  if (flags.has("ticket")) parsed.ticket = flags.get("ticket");
15874
16026
  if (flags.has("producer")) parsed.producer = flags.get("producer");
@@ -15927,7 +16079,8 @@ async function runAdd(args, cwd) {
15927
16079
  ticket: parsed.ticket,
15928
16080
  producer: parsed.producer,
15929
16081
  producerVersion: parsed.producerVersion,
15930
- artifacts: parsed.artifacts.length > 0 ? parsed.artifacts : void 0
16082
+ artifacts: parsed.artifacts.length > 0 ? parsed.artifacts : void 0,
16083
+ related: parsed.related.length > 0 ? parsed.related : void 0
15931
16084
  };
15932
16085
  warnIfPrNotesIgnored(repoRoot);
15933
16086
  const { path, guid: guid3 } = writeReviewNote(repoRoot, input);
@@ -15938,7 +16091,7 @@ function scopedFile(flags, repoRoot, cwd) {
15938
16091
  return file2 === void 0 ? void 0 : toRepoRelative(repoRoot, cwd, file2);
15939
16092
  }
15940
16093
  function runRemove(args, cwd) {
15941
- const flags = parseFlags(args);
16094
+ const flags = parseFlags(args, "remove");
15942
16095
  const id = flags.get("id");
15943
16096
  if (id === void 0) throw new Error("--id <guid> is required");
15944
16097
  const repoRoot = findRepoRoot(cwd);
@@ -15947,7 +16100,7 @@ function runRemove(args, cwd) {
15947
16100
  console.log(`Removed review note ${id}`);
15948
16101
  }
15949
16102
  async function runUpdate(args, cwd) {
15950
- const flags = parseFlags(args);
16103
+ const flags = parseFlags(args, "update");
15951
16104
  const id = flags.get("id");
15952
16105
  if (id === void 0) throw new Error("--id <guid> is required");
15953
16106
  const patch = {};
@@ -15967,7 +16120,7 @@ async function runUpdate(args, cwd) {
15967
16120
  console.log(`Updated review note ${id}`);
15968
16121
  }
15969
16122
  function runCoalesce(args, cwd) {
15970
- const flags = parseFlags(args);
16123
+ const flags = parseFlags(args, "coalesce");
15971
16124
  const repoRoot = findRepoRoot(cwd);
15972
16125
  const file2 = flags.get("file");
15973
16126
  const removed = file2 !== void 0 ? coalesceFile(repoRoot, toRepoRelative(repoRoot, cwd, file2)) : coalesceAll(repoRoot);
@@ -15981,32 +16134,50 @@ async function runNoteCli(args, ctx = {}) {
15981
16134
  return;
15982
16135
  }
15983
16136
  const rest = args.slice(1);
15984
- switch (sub) {
15985
- case "add":
15986
- await runAdd(rest, cwd);
15987
- return;
15988
- case "remove":
15989
- runRemove(rest, cwd);
15990
- return;
15991
- case "update":
15992
- await runUpdate(rest, cwd);
15993
- return;
15994
- case "coalesce":
15995
- runCoalesce(rest, cwd);
15996
- return;
15997
- case "instructions":
15998
- console.log(reviewNoteInstructions());
15999
- return;
16000
- default:
16001
- throw new Error(`unknown 'note' subcommand: ${sub} (expected add/update/remove/coalesce/instructions)`);
16137
+ try {
16138
+ switch (sub) {
16139
+ case "add":
16140
+ await runAdd(rest, cwd);
16141
+ return;
16142
+ case "remove":
16143
+ runRemove(rest, cwd);
16144
+ return;
16145
+ case "update":
16146
+ await runUpdate(rest, cwd);
16147
+ return;
16148
+ case "coalesce":
16149
+ runCoalesce(rest, cwd);
16150
+ return;
16151
+ case "instructions":
16152
+ console.log(reviewNoteInstructions());
16153
+ return;
16154
+ default:
16155
+ throw new Error(`unknown 'note' subcommand: ${sub} (expected add/update/remove/coalesce/instructions)`);
16156
+ }
16157
+ } catch (err) {
16158
+ if (err instanceof UnknownFlagError) {
16159
+ throw new UnknownFlagError(`${err.message}
16160
+ This is glassbox ${getCurrentVersion()} \u2014 a flag added in a newer release is not recognized here.`);
16161
+ }
16162
+ throw err;
16002
16163
  }
16003
16164
  }
16165
+ var UnknownFlagError, KNOWN_FLAGS;
16004
16166
  var init_cli = __esm({
16005
16167
  "src/review-notes/cli.ts"() {
16006
16168
  "use strict";
16169
+ init_app_version();
16007
16170
  init_instructions();
16008
16171
  init_store();
16009
16172
  init_types();
16173
+ UnknownFlagError = class extends Error {
16174
+ };
16175
+ KNOWN_FLAGS = {
16176
+ add: ["file", "lines", "kind", "body", "confidence", "rank", "ticket", "producer", "producer-version", "artifact", "related"],
16177
+ update: ["id", "file", "body", "kind", "confidence", "rank", "ticket"],
16178
+ remove: ["id", "file"],
16179
+ coalesce: ["file"]
16180
+ };
16010
16181
  }
16011
16182
  });
16012
16183
 
@@ -16240,12 +16411,12 @@ __export(manifest_exports, {
16240
16411
  GroundTruthSetSchema: () => GroundTruthSetSchema,
16241
16412
  loadGroundTruthManifest: () => loadGroundTruthManifest
16242
16413
  });
16243
- import { readFileSync as readFileSync2 } from "fs";
16244
- import { basename, dirname as dirname2, isAbsolute, resolve as resolve2 } from "path";
16414
+ import { readFileSync as readFileSync3 } from "fs";
16415
+ import { basename, dirname as dirname3, isAbsolute, resolve as resolve2 } from "path";
16245
16416
  function loadGroundTruthManifest(manifestPath) {
16246
16417
  let raw2;
16247
16418
  try {
16248
- raw2 = readFileSync2(manifestPath, "utf-8");
16419
+ raw2 = readFileSync3(manifestPath, "utf-8");
16249
16420
  } catch {
16250
16421
  throw new Error(`Cannot read ground-truth manifest: ${manifestPath}`);
16251
16422
  }
@@ -16268,7 +16439,7 @@ function loadGroundTruthManifest(manifestPath) {
16268
16439
  const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
16269
16440
  throw new Error(`Ground-truth manifest is invalid (${manifestPath}): ${detail}`);
16270
16441
  }
16271
- const baseDir = dirname2(resolve2(manifestPath));
16442
+ const baseDir = dirname3(resolve2(manifestPath));
16272
16443
  const resolvePath = (p) => isAbsolute(p) ? p : resolve2(baseDir, p);
16273
16444
  const used = /* @__PURE__ */ new Set();
16274
16445
  const dedupe = (key) => {
@@ -16363,7 +16534,7 @@ __export(promote_exports, {
16363
16534
  promoteGroundTruthBaselines: () => promoteGroundTruthBaselines
16364
16535
  });
16365
16536
  import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
16366
- import { dirname as dirname3 } from "path";
16537
+ import { dirname as dirname4 } from "path";
16367
16538
  function promoteGroundTruthBaselines(manifestPath) {
16368
16539
  const entries = loadGroundTruthManifest(manifestPath);
16369
16540
  const result = { promoted: [], skipped: [] };
@@ -16379,7 +16550,7 @@ function promoteGroundTruthBaselines(manifestPath) {
16379
16550
  result.skipped.push({ key: entry.key, reason: `actual image not found: ${entry.actualPath}` });
16380
16551
  continue;
16381
16552
  }
16382
- mkdirSync2(dirname3(entry.expectedPath), { recursive: true });
16553
+ mkdirSync2(dirname4(entry.expectedPath), { recursive: true });
16383
16554
  copyFileSync(entry.actualPath, entry.expectedPath);
16384
16555
  result.promoted.push({ key: entry.key, from: entry.actualPath, to: entry.expectedPath });
16385
16556
  }
@@ -16612,12 +16783,12 @@ var init_ddl = __esm({
16612
16783
  // src/db/connection.ts
16613
16784
  import { PGlite } from "@electric-sql/pglite";
16614
16785
  import { mkdirSync as mkdirSync3, rmSync } from "fs";
16615
- import { join as join2 } from "path";
16786
+ import { join as join3 } from "path";
16616
16787
  function setDataDir(dataDir) {
16617
16788
  currentDataDir = dataDir;
16618
- const dbDir = join2(dataDir, "data");
16789
+ const dbDir = join3(dataDir, "data");
16619
16790
  mkdirSync3(dbDir, { recursive: true });
16620
- currentDbPath = join2(dbDir, "reviews");
16791
+ currentDbPath = join3(dbDir, "reviews");
16621
16792
  }
16622
16793
  function getDataDir() {
16623
16794
  return currentDataDir;
@@ -16692,10 +16863,10 @@ var init_connection = __esm({
16692
16863
  // src/attachments/store.ts
16693
16864
  import { createHash as createHash2 } from "crypto";
16694
16865
  import { existsSync as existsSync3, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
16695
- import { basename as basename2, join as join3 } from "path";
16866
+ import { basename as basename2, join as join4 } from "path";
16696
16867
  function attachmentsDir() {
16697
16868
  const dataDir = getDataDir();
16698
- return dataDir === null ? null : join3(dataDir, "attachments");
16869
+ return dataDir === null ? null : join4(dataDir, "attachments");
16699
16870
  }
16700
16871
  function sanitizeFilename(name) {
16701
16872
  const base = basename2(name.replace(/\\/g, "/"));
@@ -16706,7 +16877,7 @@ function writeAttachmentFile(id, originalFilename, bytes) {
16706
16877
  const dir = attachmentsDir();
16707
16878
  if (dir === null) throw new Error("No data directory for attachments");
16708
16879
  mkdirSync4(dir, { recursive: true });
16709
- const storedPath = join3(dir, `${id}-${sanitizeFilename(originalFilename)}`);
16880
+ const storedPath = join4(dir, `${id}-${sanitizeFilename(originalFilename)}`);
16710
16881
  writeFileSync2(storedPath, bytes);
16711
16882
  const sha256 = createHash2("sha256").update(bytes).digest("hex");
16712
16883
  return { storedPath, size: bytes.length, sha256 };
@@ -17213,18 +17384,18 @@ var init_queries = __esm({
17213
17384
  });
17214
17385
 
17215
17386
  // src/global-config.ts
17216
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
17387
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
17217
17388
  import { homedir } from "os";
17218
- import { join as join4 } from "path";
17389
+ import { join as join5 } from "path";
17219
17390
  function resolveGlobalConfigDir() {
17220
17391
  const override = process.env.GLASSBOX_CONFIG_DIR;
17221
17392
  if (override !== void 0 && override.trim() !== "") return override;
17222
- return join4(homedir(), ".glassbox");
17393
+ return join5(homedir(), ".glassbox");
17223
17394
  }
17224
17395
  function readGlobalConfig() {
17225
17396
  try {
17226
17397
  if (existsSync4(GLOBAL_CONFIG_PATH)) {
17227
- const raw2 = JSON.parse(readFileSync3(GLOBAL_CONFIG_PATH, "utf-8"));
17398
+ const raw2 = JSON.parse(readFileSync4(GLOBAL_CONFIG_PATH, "utf-8"));
17228
17399
  const parsed = GlobalConfigSchema.safeParse(raw2);
17229
17400
  if (parsed.success) return parsed.data;
17230
17401
  }
@@ -17252,7 +17423,7 @@ var init_global_config = __esm({
17252
17423
  init_zod();
17253
17424
  GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
17254
17425
  GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
17255
- GLOBAL_CONFIG_PATH = join4(GLOBAL_CONFIG_DIR, "config.json");
17426
+ GLOBAL_CONFIG_PATH = join5(GLOBAL_CONFIG_DIR, "config.json");
17256
17427
  }
17257
17428
  });
17258
17429
 
@@ -17391,10 +17562,10 @@ __export(image_blobs_exports, {
17391
17562
  readImageBlob: () => readImageBlob,
17392
17563
  writeImageBlob: () => writeImageBlob
17393
17564
  });
17394
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
17395
- import { join as join5 } from "path";
17565
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
17566
+ import { join as join6 } from "path";
17396
17567
  function blobDir(dataDir) {
17397
- return join5(dataDir, "image-blobs");
17568
+ return join6(dataDir, "image-blobs");
17398
17569
  }
17399
17570
  function blobName(fileId, side) {
17400
17571
  return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
@@ -17403,13 +17574,13 @@ function writeImageBlob(dataDir, fileId, side, bytes) {
17403
17574
  if (bytes.length === 0) return;
17404
17575
  const dir = blobDir(dataDir);
17405
17576
  mkdirSync6(dir, { recursive: true });
17406
- writeFileSync4(join5(dir, blobName(fileId, side)), bytes);
17577
+ writeFileSync4(join6(dir, blobName(fileId, side)), bytes);
17407
17578
  }
17408
17579
  function readImageBlob(dataDir, fileId, side) {
17409
- const path = join5(blobDir(dataDir), blobName(fileId, side));
17580
+ const path = join6(blobDir(dataDir), blobName(fileId, side));
17410
17581
  if (!existsSync5(path)) return null;
17411
17582
  try {
17412
- return readFileSync4(path);
17583
+ return readFileSync5(path);
17413
17584
  } catch {
17414
17585
  return null;
17415
17586
  }
@@ -17509,16 +17680,16 @@ var init_project_settings = __esm({
17509
17680
  });
17510
17681
 
17511
17682
  // src/project-settings-store.ts
17512
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
17513
- import { join as join10 } from "path";
17683
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
17684
+ import { join as join11 } from "path";
17514
17685
  function settingsPath(repoRoot) {
17515
- return join10(repoRoot, ".glassbox", "settings.json");
17686
+ return join11(repoRoot, ".glassbox", "settings.json");
17516
17687
  }
17517
17688
  function readProjectSettings(repoRoot) {
17518
17689
  try {
17519
17690
  const path = settingsPath(repoRoot);
17520
17691
  if (existsSync10(path)) {
17521
- const raw2 = JSON.parse(readFileSync9(path, "utf-8"));
17692
+ const raw2 = JSON.parse(readFileSync10(path, "utf-8"));
17522
17693
  const parsed = ProjectSettingsSchema.safeParse(raw2);
17523
17694
  if (parsed.success) return parsed.data;
17524
17695
  }
@@ -17530,9 +17701,9 @@ function updateProjectSettings2(repoRoot, mutator) {
17530
17701
  const current = readProjectSettings(repoRoot);
17531
17702
  const result = mutator(current);
17532
17703
  const next = result === void 0 ? current : result;
17533
- const dir = join10(repoRoot, ".glassbox");
17704
+ const dir = join11(repoRoot, ".glassbox");
17534
17705
  mkdirSync8(dir, { recursive: true });
17535
- writeFileSync9(join10(dir, "settings.json"), JSON.stringify(next, null, 2), "utf-8");
17706
+ writeFileSync9(join11(dir, "settings.json"), JSON.stringify(next, null, 2), "utf-8");
17536
17707
  }
17537
17708
  var init_project_settings_store = __esm({
17538
17709
  "src/project-settings-store.ts"() {
@@ -17886,8 +18057,8 @@ var init_settings = __esm({
17886
18057
  });
17887
18058
 
17888
18059
  // src/plugins/loader.ts
17889
- import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
17890
- import { join as join11 } from "path";
18060
+ import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
18061
+ import { join as join12 } from "path";
17891
18062
  import { pathToFileURL } from "url";
17892
18063
  function getConfigLabelOverride(pluginId, labelId) {
17893
18064
  return configLabelOverrides.get(`${pluginId}:${labelId}`);
@@ -17908,7 +18079,7 @@ function clearAllPluginUIElements() {
17908
18079
  pluginUIElements.clear();
17909
18080
  }
17910
18081
  function pluginsDir() {
17911
- return join11(GLOBAL_CONFIG_DIR, "plugins");
18082
+ return join12(GLOBAL_CONFIG_DIR, "plugins");
17912
18083
  }
17913
18084
  function discoverPluginDirs(root = pluginsDir()) {
17914
18085
  if (!existsSync11(root)) return [];
@@ -17921,7 +18092,7 @@ function discoverPluginDirs(root = pluginsDir()) {
17921
18092
  const out = [];
17922
18093
  for (const name of entries) {
17923
18094
  if (name.startsWith(".")) continue;
17924
- const dir = join11(root, name);
18095
+ const dir = join12(root, name);
17925
18096
  try {
17926
18097
  if (statSync3(dir).isDirectory()) out.push(dir);
17927
18098
  } catch {
@@ -17930,18 +18101,18 @@ function discoverPluginDirs(root = pluginsDir()) {
17930
18101
  return out.sort();
17931
18102
  }
17932
18103
  function readManifest(dir) {
17933
- const manifestPath = join11(dir, "manifest.json");
18104
+ const manifestPath = join12(dir, "manifest.json");
17934
18105
  if (existsSync11(manifestPath)) {
17935
18106
  try {
17936
- return parseManifest(JSON.parse(readFileSync10(manifestPath, "utf-8")));
18107
+ return parseManifest(JSON.parse(readFileSync11(manifestPath, "utf-8")));
17937
18108
  } catch {
17938
18109
  return null;
17939
18110
  }
17940
18111
  }
17941
- const pkgPath = join11(dir, "package.json");
18112
+ const pkgPath = join12(dir, "package.json");
17942
18113
  if (existsSync11(pkgPath)) {
17943
18114
  try {
17944
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
18115
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
17945
18116
  const gb = pkg.glassbox;
17946
18117
  if (gb !== null && typeof gb === "object") {
17947
18118
  return parseManifest({ id: pkg.name, version: pkg.version, entry: pkg.main, ...gb });
@@ -17984,7 +18155,7 @@ async function loadPluginDir(dir, registry3, isEnabled = ALWAYS_ENABLED, repoRoo
17984
18155
  return { id: manifest.id, dir, manifest, status: "disabled", disabledScope: enablement.scope };
17985
18156
  }
17986
18157
  try {
17987
- const entry = join11(dir, manifest.entry ?? "index.js");
18158
+ const entry = join12(dir, manifest.entry ?? "index.js");
17988
18159
  if (!existsSync11(entry)) throw new Error(`entry not found: ${manifest.entry ?? "index.js"}`);
17989
18160
  const mod = await import(pathToFileURL(entry).href);
17990
18161
  const plugin = mod.default ?? (typeof mod.activate === "function" ? { activate: mod.activate, onAction: mod.onAction } : void 0);
@@ -18029,18 +18200,18 @@ var init_loader = __esm({
18029
18200
 
18030
18201
  // src/plugins/install.ts
18031
18202
  import { createHash as createHash3 } from "crypto";
18032
- import { cpSync, existsSync as existsSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync11, rmSync as rmSync6, statSync as statSync4, symlinkSync, writeFileSync as writeFileSync10 } from "fs";
18033
- import { dirname as dirname7, join as join12 } from "path";
18034
- import { fileURLToPath as fileURLToPath2 } from "url";
18203
+ import { cpSync, existsSync as existsSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync12, rmSync as rmSync6, statSync as statSync4, symlinkSync, writeFileSync as writeFileSync10 } from "fs";
18204
+ import { dirname as dirname8, join as join13 } from "path";
18205
+ import { fileURLToPath as fileURLToPath3 } from "url";
18035
18206
  function dismissedPathFor(userDir) {
18036
- return join12(dirname7(userDir), DISMISSED_FILE);
18207
+ return join13(dirname8(userDir), DISMISSED_FILE);
18037
18208
  }
18038
18209
  function bundledPluginsDir() {
18039
18210
  const override = process.env.GLASSBOX_BUNDLED_PLUGINS_DIR;
18040
18211
  if (override !== void 0 && override.trim() !== "") return override;
18041
- const sibling = join12(dirname7(fileURLToPath2(import.meta.url)), "plugins");
18212
+ const sibling = join13(dirname8(fileURLToPath3(import.meta.url)), "plugins");
18042
18213
  if (existsSync12(sibling)) return sibling;
18043
- return join12(process.cwd(), "dist", "plugins");
18214
+ return join13(process.cwd(), "dist", "plugins");
18044
18215
  }
18045
18216
  function discoverBundledPlugins(bundledDir = bundledPluginsDir()) {
18046
18217
  if (!existsSync12(bundledDir)) return [];
@@ -18052,7 +18223,7 @@ function discoverBundledPlugins(bundledDir = bundledPluginsDir()) {
18052
18223
  }
18053
18224
  const out = [];
18054
18225
  for (const name of entries.sort()) {
18055
- const dir = join12(bundledDir, name);
18226
+ const dir = join13(bundledDir, name);
18056
18227
  try {
18057
18228
  if (!statSync4(dir).isDirectory()) continue;
18058
18229
  const manifest = readManifest(dir);
@@ -18064,7 +18235,7 @@ function discoverBundledPlugins(bundledDir = bundledPluginsDir()) {
18064
18235
  }
18065
18236
  function readDismissed(userDir = pluginsDir()) {
18066
18237
  try {
18067
- const raw2 = JSON.parse(readFileSync11(dismissedPathFor(userDir), "utf-8"));
18238
+ const raw2 = JSON.parse(readFileSync12(dismissedPathFor(userDir), "utf-8"));
18068
18239
  return Array.isArray(raw2) ? raw2.filter((x) => typeof x === "string") : [];
18069
18240
  } catch {
18070
18241
  return [];
@@ -18072,19 +18243,19 @@ function readDismissed(userDir = pluginsDir()) {
18072
18243
  }
18073
18244
  function writeDismissed(ids, userDir) {
18074
18245
  const path = dismissedPathFor(userDir);
18075
- mkdirSync9(dirname7(path), { recursive: true });
18246
+ mkdirSync9(dirname8(path), { recursive: true });
18076
18247
  writeFileSync10(path, JSON.stringify([...new Set(ids)], null, 2), "utf-8");
18077
18248
  }
18078
18249
  function hashPluginDir(dir) {
18079
18250
  const h = createHash3("sha1");
18080
18251
  const walk = (d, rel) => {
18081
18252
  for (const name of readdirSync3(d).sort()) {
18082
- const abs = join12(d, name);
18253
+ const abs = join13(d, name);
18083
18254
  const r = rel === "" ? name : `${rel}/${name}`;
18084
18255
  if (statSync4(abs).isDirectory()) walk(abs, r);
18085
18256
  else {
18086
18257
  h.update(r);
18087
- h.update(readFileSync11(abs));
18258
+ h.update(readFileSync12(abs));
18088
18259
  }
18089
18260
  }
18090
18261
  };
@@ -18116,13 +18287,13 @@ function installBundledPlugins(opts) {
18116
18287
  return;
18117
18288
  }
18118
18289
  for (const name of entries) {
18119
- const src = join12(bundledDir, name);
18290
+ const src = join13(bundledDir, name);
18120
18291
  try {
18121
18292
  if (!statSync4(src).isDirectory()) continue;
18122
18293
  const manifest = readManifest(src);
18123
18294
  if (manifest === null || dismissed.has(manifest.id)) continue;
18124
18295
  if (manifest.autoInstall === false) continue;
18125
- const dest = join12(userDir, manifest.id);
18296
+ const dest = join13(userDir, manifest.id);
18126
18297
  if (!shouldInstall(src, dest, manifest.version)) continue;
18127
18298
  mkdirSync9(userDir, { recursive: true });
18128
18299
  rmSync6(dest, { recursive: true, force: true });
@@ -18136,7 +18307,7 @@ function installPluginFromDisk(sourceDir, opts) {
18136
18307
  if (manifest === null) throw new Error("The selected folder is not a Glassbox plugin (no manifest.json).");
18137
18308
  const userDir = opts?.userDir ?? pluginsDir();
18138
18309
  mkdirSync9(userDir, { recursive: true });
18139
- const dest = join12(userDir, manifest.id);
18310
+ const dest = join13(userDir, manifest.id);
18140
18311
  rmSync6(dest, { recursive: true, force: true });
18141
18312
  try {
18142
18313
  symlinkSync(sourceDir, dest, "dir");
@@ -18151,7 +18322,7 @@ function undismissPlugin(id, userDir = pluginsDir()) {
18151
18322
  }
18152
18323
  function uninstallPlugin(id, opts) {
18153
18324
  const userDir = opts?.userDir ?? pluginsDir();
18154
- rmSync6(join12(userDir, id), { recursive: true, force: true });
18325
+ rmSync6(join13(userDir, id), { recursive: true, force: true });
18155
18326
  writeDismissed([...readDismissed(userDir), id], userDir);
18156
18327
  }
18157
18328
  var DISMISSED_FILE;
@@ -18566,17 +18737,17 @@ __export(difftool_discovery_exports, {
18566
18737
  tryAcquireStartingLock: () => tryAcquireStartingLock,
18567
18738
  writeDiscovery: () => writeDiscovery
18568
18739
  });
18569
- import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync22, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "fs";
18740
+ import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync23, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "fs";
18570
18741
  import { homedir as homedir3 } from "os";
18571
- import { join as join23 } from "path";
18742
+ import { join as join24 } from "path";
18572
18743
  function difftoolHome() {
18573
- return join23(homedir3(), ".glassbox");
18744
+ return join24(homedir3(), ".glassbox");
18574
18745
  }
18575
18746
  function discoveryPath(home = difftoolHome()) {
18576
- return join23(home, "difftool.lock");
18747
+ return join24(home, "difftool.lock");
18577
18748
  }
18578
18749
  function startingLockPath(home = difftoolHome()) {
18579
- return join23(home, "difftool-starting.lock");
18750
+ return join24(home, "difftool-starting.lock");
18580
18751
  }
18581
18752
  function parseDiscovery(raw2) {
18582
18753
  let parsed;
@@ -18592,7 +18763,7 @@ function readDiscovery(home = difftoolHome()) {
18592
18763
  const path = discoveryPath(home);
18593
18764
  if (!existsSync19(path)) return null;
18594
18765
  try {
18595
- return parseDiscovery(readFileSync22(path, "utf-8"));
18766
+ return parseDiscovery(readFileSync23(path, "utf-8"));
18596
18767
  } catch {
18597
18768
  return null;
18598
18769
  }
@@ -23083,12 +23254,12 @@ __export(perceptual_diff_exports, {
23083
23254
  decodeImage: () => decodeImage,
23084
23255
  isIdentical: () => isIdentical
23085
23256
  });
23086
- import { readFileSync as readFileSync23 } from "fs";
23257
+ import { readFileSync as readFileSync24 } from "fs";
23087
23258
  import { extname as extname3 } from "path";
23088
23259
  function decodeImage(path) {
23089
23260
  let buf;
23090
23261
  try {
23091
- buf = readFileSync23(path);
23262
+ buf = readFileSync24(path);
23092
23263
  } catch {
23093
23264
  return null;
23094
23265
  }
@@ -23112,7 +23283,7 @@ async function decodeWithFallback(path, pluginDecode) {
23112
23283
  if (core !== null) return core;
23113
23284
  let buf;
23114
23285
  try {
23115
- buf = readFileSync23(path);
23286
+ buf = readFileSync24(path);
23116
23287
  } catch {
23117
23288
  return null;
23118
23289
  }
@@ -23163,7 +23334,7 @@ var init_perceptual_diff = __esm({
23163
23334
  // src/cli.ts
23164
23335
  import { existsSync as existsSync20, mkdirSync as mkdirSync17, realpathSync, statSync as statSync8 } from "fs";
23165
23336
  import { tmpdir as tmpdir2 } from "os";
23166
- import { basename as basename5, join as join24, resolve as resolve12 } from "path";
23337
+ import { basename as basename5, join as join25, resolve as resolve12 } from "path";
23167
23338
 
23168
23339
  // src/cli-subcommands.ts
23169
23340
  import { resolve as resolve3 } from "path";
@@ -24133,12 +24304,17 @@ function seedSvgBlobs(fileId, diff) {
24133
24304
  function demoReviewNotes(filePath) {
24134
24305
  if (filePath !== "src/auth/session.ts") return [];
24135
24306
  return [
24136
- { guid: "demo-note-rationale", line: 14, side: "new", kind: "rationale", body: "`createSession` is **async** now because session state moved from an in-process `Map` to Redis; callers must `await` it.", confidence: 0.9, producer: "Claude Code" },
24307
+ // The body carries a SARIF embedded link `[text](0)` indexes `related`
24308
+ // which renders as a jump-to-line link into the other changed file (doc 20
24309
+ // §20.6).
24310
+ { guid: "demo-note-rationale", line: 14, side: "new", kind: "rationale", body: "`createSession` is **async** now because session state moved from an in-process `Map` to [the Redis client](0); callers must `await` it.", confidence: 0.9, producer: "Claude Code", related: [{ uri: "src/db/redis.ts", line: 1 }] },
24137
24311
  // The proof note carries two artifacts: mocked test output (always a code
24138
24312
  // block) and a Mermaid sequence diagram — with the mermaid content plugin
24139
24313
  // installed it renders inline as a diagram (doc 29); without it, the
24140
24314
  // fail-soft code block still tells the story.
24141
- { guid: "demo-note-proof", line: 23, side: "new", kind: "proof", body: "The TTL is written atomically with the value via the EX option, so a session can never be stored without an expiry.", producer: "Claude Code", artifacts: [
24315
+ // Body deliberately uses block markdown (paragraph + list) to showcase the
24316
+ // renderer's block support (doc 20 §20.6).
24317
+ { guid: "demo-note-proof", line: 23, side: "new", kind: "proof", body: "The TTL is written atomically with the value via the EX option, so a session can never be stored without an expiry.\n\n- `SET` carries `EX 3600` in a single round trip\n- No window exists where a session is stored without one", producer: "Claude Code", artifacts: [
24142
24318
  { uri: ".pr-notes/artifacts/session-ttl.test.txt", content: "PASS session.test.ts\n \u2713 createSession writes value and TTL atomically (4 ms)\n \u2713 a session always has an expiry (2 ms)\n\nTests: 2 passed, 2 total" },
24143
24319
  { uri: ".pr-notes/artifacts/session-flow.mmd", content: "sequenceDiagram\n participant C as Caller\n participant S as createSession\n participant R as Redis\n C->>S: createSession(userId)\n S->>S: id = randomUUID()\n S->>R: SET session:id {userId} EX 3600\n Note over R: value + TTL written atomically\n R-->>S: OK\n S-->>C: id" }
24144
24320
  ] },
@@ -24279,11 +24455,12 @@ async function setupReviewNoteReply(fileIdMap) {
24279
24455
 
24280
24456
  // src/git/diff.ts
24281
24457
  init_zod();
24282
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
24458
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
24283
24459
  import { tmpdir } from "os";
24284
- import { basename as basename3, dirname as dirname4, join as join6, resolve as resolve4 } from "path";
24460
+ import { basename as basename3, dirname as dirname5, join as join7, resolve as resolve4 } from "path";
24285
24461
 
24286
24462
  // src/git/parseDiff.ts
24463
+ init_lfs();
24287
24464
  function parseDiff(raw2) {
24288
24465
  const files = [];
24289
24466
  const fileChunks = raw2.split(/^diff --git /m).filter(Boolean);
@@ -24317,10 +24494,20 @@ function parseDiff(raw2) {
24317
24494
  continue;
24318
24495
  }
24319
24496
  const hunks = parseHunks(chunk.slice(headerEnd));
24497
+ if (hunksAreLfsPointer(hunks)) {
24498
+ files.push({ filePath, oldPath, status, hunks: [], isBinary: true });
24499
+ continue;
24500
+ }
24320
24501
  files.push({ filePath, oldPath, status, hunks, isBinary: false });
24321
24502
  }
24322
24503
  return files;
24323
24504
  }
24505
+ function hunksAreLfsPointer(hunks) {
24506
+ if (hunks.length !== 1) return false;
24507
+ const lines = hunks[0].lines;
24508
+ const side = (types) => lines.filter((l) => types.includes(l.type)).map((l) => l.content).join("\n") + "\n";
24509
+ return isLfsPointer(side(["context", "add"])) || isLfsPointer(side(["context", "remove"]));
24510
+ }
24324
24511
  function parseHunks(raw2) {
24325
24512
  const hunks = [];
24326
24513
  const hunkRegex = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)/gm;
@@ -24501,9 +24688,9 @@ function getDiffArgs(mode) {
24501
24688
  function directComparisonRoots(mode) {
24502
24689
  const rootOf = (p) => {
24503
24690
  try {
24504
- return statSync2(p).isDirectory() ? p : dirname4(p);
24691
+ return statSync2(p).isDirectory() ? p : dirname5(p);
24505
24692
  } catch {
24506
- return dirname4(p);
24693
+ return dirname5(p);
24507
24694
  }
24508
24695
  };
24509
24696
  return { rootA: rootOf(mode.pathA), rootB: rootOf(mode.pathB) };
@@ -24531,14 +24718,14 @@ function getDirectComparisonFiles(mode, cwd) {
24531
24718
  function diffRawContent(displayPath, oldContent, newContent) {
24532
24719
  const rel = displayPath.replace(/\\/g, "/").replace(/^\/+/, "").replace(/(^|\/)\.\.(?=\/|$)/g, "$1_");
24533
24720
  const safeRel = rel === "" ? "file" : rel;
24534
- const work = mkdtempSync(join6(tmpdir(), "glassbox-difftool-append-"));
24535
- const rootA = join6(work, "a");
24536
- const rootB = join6(work, "b");
24537
- const oldAbs = join6(rootA, safeRel);
24538
- const newAbs = join6(rootB, safeRel);
24721
+ const work = mkdtempSync(join7(tmpdir(), "glassbox-difftool-append-"));
24722
+ const rootA = join7(work, "a");
24723
+ const rootB = join7(work, "b");
24724
+ const oldAbs = join7(rootA, safeRel);
24725
+ const newAbs = join7(rootB, safeRel);
24539
24726
  try {
24540
- mkdirSync7(dirname4(oldAbs), { recursive: true });
24541
- mkdirSync7(dirname4(newAbs), { recursive: true });
24727
+ mkdirSync7(dirname5(oldAbs), { recursive: true });
24728
+ mkdirSync7(dirname5(newAbs), { recursive: true });
24542
24729
  writeFileSync5(oldAbs, oldContent);
24543
24730
  writeFileSync5(newAbs, newContent);
24544
24731
  const rawDiff = gitOrEmpty(["diff", "--no-index", "-U3", toGitArg(oldAbs), toGitArg(newAbs)], work);
@@ -24602,7 +24789,7 @@ function getAllFiles(repoRoot) {
24602
24789
  function createNewFileDiff(filePath, repoRoot) {
24603
24790
  let content;
24604
24791
  try {
24605
- const buf = readFileSync5(resolve4(repoRoot, filePath));
24792
+ const buf = readFileSync6(resolve4(repoRoot, filePath));
24606
24793
  const checkLen = Math.min(buf.length, 8192);
24607
24794
  for (let i = 0; i < checkLen; i++) {
24608
24795
  if (buf[i] === 0) {
@@ -24638,7 +24825,7 @@ function getFileContent(filePath, ref, cwd) {
24638
24825
  const repoRoot = getRepoRoot(cwd);
24639
24826
  try {
24640
24827
  if (ref === "working") {
24641
- return readFileSync5(resolve4(repoRoot, filePath), "utf-8");
24828
+ return readFileSync6(resolve4(repoRoot, filePath), "utf-8");
24642
24829
  }
24643
24830
  return git(["show", `${ref}:${filePath}`], repoRoot);
24644
24831
  } catch {
@@ -24648,9 +24835,9 @@ function getFileContent(filePath, ref, cwd) {
24648
24835
  function getModeFileContent(mode, filePath, side, cwd) {
24649
24836
  if (mode.type === "diff") {
24650
24837
  const { rootA, rootB } = directComparisonRoots(mode);
24651
- const abs = join6(side === "old" ? rootA : rootB, filePath);
24838
+ const abs = join7(side === "old" ? rootA : rootB, filePath);
24652
24839
  try {
24653
- return readFileSync5(abs, "utf-8");
24840
+ return readFileSync6(abs, "utf-8");
24654
24841
  } catch {
24655
24842
  return "";
24656
24843
  }
@@ -24695,8 +24882,8 @@ function getSingleFileDiff(mode, filePath, repoRoot, extraFlags = "") {
24695
24882
  if (mode.type === "ground-truth") return null;
24696
24883
  if (mode.type === "diff") {
24697
24884
  const { rootA, rootB } = directComparisonRoots(mode);
24698
- const oldAbs = join6(rootA, filePath);
24699
- const newAbs = join6(rootB, filePath);
24885
+ const oldAbs = join7(rootA, filePath);
24886
+ const newAbs = join7(rootB, filePath);
24700
24887
  if (!existsSync6(oldAbs) || !existsSync6(newAbs)) return null;
24701
24888
  const args2 = ["diff", "--no-index", "-U3"];
24702
24889
  if (extraFlags) args2.push(...extraFlags.split(" ").filter(Boolean));
@@ -24762,8 +24949,8 @@ function getModeArgs(mode) {
24762
24949
  }
24763
24950
 
24764
24951
  // src/git/gitignore.ts
24765
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
24766
- import { basename as basename4, dirname as dirname5, join as join7 } from "path";
24952
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
24953
+ import { basename as basename4, dirname as dirname6, join as join8 } from "path";
24767
24954
  var GLASSBOX_GITIGNORE_LINES = ["/.glassbox/*", "!/.glassbox/settings.json"];
24768
24955
  function isGlassboxPattern(token) {
24769
24956
  const core = token.replace(/^!/, "").replace(/^\//, "").trim();
@@ -24814,10 +25001,10 @@ function computeGitignore(existing) {
24814
25001
  }
24815
25002
  function ensureGlassboxGitignored(glassboxDir) {
24816
25003
  if (basename4(glassboxDir) !== ".glassbox") return { changed: false };
24817
- const targetDir = dirname5(glassboxDir);
25004
+ const targetDir = dirname6(glassboxDir);
24818
25005
  if (!isGitRepo(targetDir)) return { changed: false };
24819
- const gitignorePath = join7(targetDir, ".gitignore");
24820
- const existing = existsSync7(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : null;
25006
+ const gitignorePath = join8(targetDir, ".gitignore");
25007
+ const existing = existsSync7(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : null;
24821
25008
  const { changed, content } = computeGitignore(existing);
24822
25009
  if (changed) writeFileSync6(gitignorePath, content, "utf-8");
24823
25010
  return { changed };
@@ -24825,15 +25012,15 @@ function ensureGlassboxGitignored(glassboxDir) {
24825
25012
 
24826
25013
  // src/lock.ts
24827
25014
  init_zod();
24828
- import { existsSync as existsSync8, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
24829
- import { join as join8 } from "path";
25015
+ import { existsSync as existsSync8, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
25016
+ import { join as join9 } from "path";
24830
25017
  var lockPath = null;
24831
25018
  var LockFileSchema = external_exports.object({ pid: external_exports.number().int() });
24832
25019
  function acquireLock(dataDir) {
24833
- lockPath = join8(dataDir, "glassbox.lock");
25020
+ lockPath = join9(dataDir, "glassbox.lock");
24834
25021
  if (existsSync8(lockPath)) {
24835
25022
  try {
24836
- const raw2 = JSON.parse(readFileSync7(lockPath, "utf-8"));
25023
+ const raw2 = JSON.parse(readFileSync8(lockPath, "utf-8"));
24837
25024
  const contents = LockFileSchema.parse(raw2);
24838
25025
  const pid = contents.pid;
24839
25026
  try {
@@ -25003,23 +25190,23 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
25003
25190
 
25004
25191
  // src/server.ts
25005
25192
  import { serve } from "@hono/node-server";
25006
- import { existsSync as existsSync16, readFileSync as readFileSync19 } from "fs";
25193
+ import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
25007
25194
  import { Hono as Hono21 } from "hono";
25008
- import { dirname as dirname8, join as join20 } from "path";
25009
- import { fileURLToPath as fileURLToPath3 } from "url";
25195
+ import { dirname as dirname9, join as join21 } from "path";
25196
+ import { fileURLToPath as fileURLToPath4 } from "url";
25010
25197
 
25011
25198
  // src/channel-config.ts
25012
25199
  init_zod();
25013
- import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
25014
- import { dirname as dirname6, join as join9, resolve as resolve5 } from "path";
25015
- import { fileURLToPath } from "url";
25200
+ import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
25201
+ import { dirname as dirname7, join as join10, resolve as resolve5 } from "path";
25202
+ import { fileURLToPath as fileURLToPath2 } from "url";
25016
25203
  var MCP_SERVER_KEY = "glassbox-channel";
25017
25204
  var McpConfigSchema = external_exports.object({
25018
25205
  mcpServers: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
25019
25206
  }).loose();
25020
25207
  var HealthResponseSchema = external_exports.object({ ok: external_exports.boolean() });
25021
25208
  function getChannelServerPath() {
25022
- const thisDir = dirname6(fileURLToPath(import.meta.url));
25209
+ const thisDir = dirname7(fileURLToPath2(import.meta.url));
25023
25210
  const distPath = resolve5(thisDir, "channel.js");
25024
25211
  if (existsSync9(distPath)) {
25025
25212
  return { command: process.execPath, args: [distPath] };
@@ -25035,12 +25222,12 @@ function projectRoot(dataDir) {
25035
25222
  }
25036
25223
  function registerChannel(dataDir) {
25037
25224
  const root = projectRoot(dataDir);
25038
- const mcpPath = join9(root, ".mcp.json");
25225
+ const mcpPath = join10(root, ".mcp.json");
25039
25226
  const { command, args } = getChannelServerPath();
25040
25227
  let config2 = {};
25041
25228
  if (existsSync9(mcpPath)) {
25042
25229
  try {
25043
- const raw2 = JSON.parse(readFileSync8(mcpPath, "utf-8"));
25230
+ const raw2 = JSON.parse(readFileSync9(mcpPath, "utf-8"));
25044
25231
  const parsed = McpConfigSchema.safeParse(raw2);
25045
25232
  if (parsed.success) config2 = parsed.data;
25046
25233
  } catch {
@@ -25055,10 +25242,10 @@ function registerChannel(dataDir) {
25055
25242
  }
25056
25243
  function unregisterChannel(dataDir) {
25057
25244
  const root = projectRoot(dataDir);
25058
- const mcpPath = join9(root, ".mcp.json");
25245
+ const mcpPath = join10(root, ".mcp.json");
25059
25246
  if (!existsSync9(mcpPath)) return;
25060
25247
  try {
25061
- const raw2 = JSON.parse(readFileSync8(mcpPath, "utf-8"));
25248
+ const raw2 = JSON.parse(readFileSync9(mcpPath, "utf-8"));
25062
25249
  const parsed = McpConfigSchema.safeParse(raw2);
25063
25250
  if (!parsed.success) return;
25064
25251
  const config2 = parsed.data;
@@ -25073,7 +25260,7 @@ function unregisterChannel(dataDir) {
25073
25260
  }
25074
25261
  function getChannelPort(dataDir) {
25075
25262
  try {
25076
- const portStr = readFileSync8(join9(dataDir, "channel-port"), "utf-8").trim();
25263
+ const portStr = readFileSync9(join10(dataDir, "channel-port"), "utf-8").trim();
25077
25264
  const port = parseInt(portStr, 10);
25078
25265
  return isNaN(port) ? null : port;
25079
25266
  } catch {
@@ -25094,7 +25281,7 @@ async function isChannelAlive(dataDir) {
25094
25281
  }
25095
25282
  function getChannelSecret(dataDir) {
25096
25283
  try {
25097
- const secret = readFileSync8(join9(dataDir, "channel-secret"), "utf-8").trim();
25284
+ const secret = readFileSync9(join10(dataDir, "channel-secret"), "utf-8").trim();
25098
25285
  return secret === "" ? null : secret;
25099
25286
  } catch {
25100
25287
  return null;
@@ -25203,6 +25390,42 @@ init_zod();
25203
25390
  // src/review-notes/format.ts
25204
25391
  init_store();
25205
25392
  init_view();
25393
+ var EXPORT_MIN_HEADING = 4;
25394
+ var ITEM_INDENT = " ";
25395
+ var FENCE_RE2 = /^\s*(?:```|~~~)/;
25396
+ var ATX_RE = /^( {0,3})(#{1,6})(\s)/;
25397
+ function indentLines(text, indent) {
25398
+ return text.split("\n").map((line) => line.trim() === "" ? "" : indent + line);
25399
+ }
25400
+ function demoteHeadings(body, minLevel) {
25401
+ const lines = body.split("\n");
25402
+ const levels = [];
25403
+ let inFence = false;
25404
+ for (const line of lines) {
25405
+ if (FENCE_RE2.test(line)) inFence = !inFence;
25406
+ else if (!inFence) {
25407
+ const m = ATX_RE.exec(line);
25408
+ if (m !== null) levels.push(m[2].length);
25409
+ }
25410
+ }
25411
+ if (levels.length === 0) return body;
25412
+ const shift = minLevel - Math.min(...levels);
25413
+ if (shift <= 0) return body;
25414
+ inFence = false;
25415
+ return lines.map((line) => {
25416
+ if (FENCE_RE2.test(line)) {
25417
+ inFence = !inFence;
25418
+ return line;
25419
+ }
25420
+ if (inFence) return line;
25421
+ return line.replace(ATX_RE, (_m, pad, hashes, space) => `${pad}${"#".repeat(Math.min(6, hashes.length + shift))}${space}`);
25422
+ }).join("\n");
25423
+ }
25424
+ function noteListItem(label, body) {
25425
+ const trimmed = body.replace(/\s+$/, "");
25426
+ if (!trimmed.includes("\n")) return [`- ${label}: ${trimmed}`];
25427
+ return [`- ${label}`, "", ...indentLines(trimmed, ITEM_INDENT), ""];
25428
+ }
25206
25429
  function notesByFile(repoRoot, filePaths) {
25207
25430
  const out = [];
25208
25431
  for (const file2 of filePaths) {
@@ -25222,7 +25445,7 @@ function reviewNotesPromptSection(repoRoot, filePaths) {
25222
25445
  for (const { file: file2, notes } of grouped) {
25223
25446
  lines.push(`${file2}:`);
25224
25447
  for (const n of notes) {
25225
- lines.push(`- [${REVIEW_NOTE_LABELS[n.kind] ?? n.kind}, L${String(n.line)}] ${n.body}`);
25448
+ lines.push(...noteListItem(`[${REVIEW_NOTE_LABELS[n.kind] ?? n.kind}, L${String(n.line)}]`, n.body));
25226
25449
  }
25227
25450
  lines.push("");
25228
25451
  }
@@ -25242,7 +25465,7 @@ function reviewNotesExportSection(repoRoot, filePaths) {
25242
25465
  lines.push("");
25243
25466
  for (const n of notes) {
25244
25467
  const who = n.producer !== void 0 ? ` _(${n.producer})_` : "";
25245
- lines.push(`- **Line ${String(n.line)}** [${n.kind}]: ${n.body}${who}`);
25468
+ lines.push(...noteListItem(`**Line ${String(n.line)}** [${n.kind}]${who}`, demoteHeadings(n.body, EXPORT_MIN_HEADING)));
25246
25469
  }
25247
25470
  lines.push("");
25248
25471
  }
@@ -28295,8 +28518,8 @@ init_queries();
28295
28518
  init_attachment_queries();
28296
28519
  init_queries();
28297
28520
  init_schemas3();
28298
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
28299
- import { join as join13 } from "path";
28521
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
28522
+ import { join as join14 } from "path";
28300
28523
  init_image_metadata();
28301
28524
 
28302
28525
  // src/utils/formatReviewMode.ts
@@ -28591,9 +28814,9 @@ function buildReviewMarkdown(args) {
28591
28814
 
28592
28815
  // src/export/generate.ts
28593
28816
  function deleteReviewExport(reviewId, repoRoot) {
28594
- const exportDir = join13(repoRoot, ".glassbox");
28817
+ const exportDir = join14(repoRoot, ".glassbox");
28595
28818
  for (const ext of ["md", "json"]) {
28596
- const archivePath = join13(exportDir, `review-${reviewId}.${ext}`);
28819
+ const archivePath = join14(exportDir, `review-${reviewId}.${ext}`);
28597
28820
  if (existsSync13(archivePath)) unlinkSync2(archivePath);
28598
28821
  }
28599
28822
  }
@@ -28621,7 +28844,7 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
28621
28844
  if (!review) throw new Error("Review not found");
28622
28845
  const files = await getReviewFiles(reviewId);
28623
28846
  const annotations = await getAnnotationsForReview(reviewId);
28624
- const exportDir = join13(repoRoot, ".glassbox");
28847
+ const exportDir = join14(repoRoot, ".glassbox");
28625
28848
  mkdirSync10(exportDir, { recursive: true });
28626
28849
  const modeLabel = formatReviewMode(review.mode, review.mode_args);
28627
28850
  const exportDate = (/* @__PURE__ */ new Date()).toISOString();
@@ -28654,20 +28877,20 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
28654
28877
  resolveDims: resolveImageDims
28655
28878
  });
28656
28879
  const json2 = JSON.stringify(exportData, null, 2);
28657
- const archivePath = join13(exportDir, `review-${review.id}.md`);
28880
+ const archivePath = join14(exportDir, `review-${review.id}.md`);
28658
28881
  writeFileSync11(archivePath, content, "utf-8");
28659
- writeFileSync11(join13(exportDir, `review-${review.id}.json`), json2, "utf-8");
28882
+ writeFileSync11(join14(exportDir, `review-${review.id}.json`), json2, "utf-8");
28660
28883
  if (isCurrent) {
28661
- const latestPath = join13(exportDir, "latest-review.md");
28884
+ const latestPath = join14(exportDir, "latest-review.md");
28662
28885
  writeFileSync11(latestPath, content, "utf-8");
28663
- writeFileSync11(join13(exportDir, "latest-review.json"), json2, "utf-8");
28886
+ writeFileSync11(join14(exportDir, "latest-review.json"), json2, "utf-8");
28664
28887
  return latestPath;
28665
28888
  }
28666
28889
  return archivePath;
28667
28890
  }
28668
28891
  function resolveImageDims(absPath) {
28669
28892
  try {
28670
- const meta3 = extractMetadata(readFileSync12(absPath), absPath);
28893
+ const meta3 = extractMetadata(readFileSync13(absPath), absPath);
28671
28894
  if (meta3.width !== null && meta3.height !== null) {
28672
28895
  return { width: meta3.width, height: meta3.height };
28673
28896
  }
@@ -28775,7 +28998,7 @@ init_store2();
28775
28998
  init_attachment_queries();
28776
28999
  init_ids();
28777
29000
  init_queries();
28778
- import { readFileSync as readFileSync13, statSync as statSync5 } from "fs";
29001
+ import { readFileSync as readFileSync14, statSync as statSync5 } from "fs";
28779
29002
  import { Hono as Hono5 } from "hono";
28780
29003
 
28781
29004
  // src/utils/mime.ts
@@ -28940,7 +29163,7 @@ attachmentsRoutes.get("/attachments/:id/raw", async (c) => {
28940
29163
  try {
28941
29164
  const stat = statSync5(attachment.stored_path);
28942
29165
  if (!stat.isFile()) return c.text("Not found", 404);
28943
- const bytes = readFileSync13(attachment.stored_path);
29166
+ const bytes = readFileSync14(attachment.stored_path);
28944
29167
  const asciiName = attachment.original_filename.replace(/[^\x20-\x7E]/g, "_").replace(/["\\]/g, "_");
28945
29168
  const utf8Name = encodeURIComponent(attachment.original_filename);
28946
29169
  return new Response(new Uint8Array(bytes), {
@@ -29151,9 +29374,10 @@ init_queries();
29151
29374
  import { Hono as Hono8 } from "hono";
29152
29375
 
29153
29376
  // src/git/image.ts
29377
+ init_lfs();
29154
29378
  import { spawnSync as spawnSync8 } from "child_process";
29155
- import { readFileSync as readFileSync14 } from "fs";
29156
- import { join as join14, resolve as resolve8 } from "path";
29379
+ import { readFileSync as readFileSync15 } from "fs";
29380
+ import { join as join15, resolve as resolve8 } from "path";
29157
29381
  init_image_metadata();
29158
29382
  function getOldRef(mode) {
29159
29383
  switch (mode.type) {
@@ -29213,18 +29437,26 @@ function gitShowFile(ref, filePath, repoRoot) {
29213
29437
  const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
29214
29438
  const result = spawnSync8("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
29215
29439
  if (result.status !== 0 || result.stdout.length === 0) return null;
29440
+ if (isLfsPointer(result.stdout)) {
29441
+ const smudged = spawnSync8("git", ["cat-file", "--filters", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
29442
+ if (smudged.status === 0 && smudged.stdout.length > 0 && !isLfsPointer(smudged.stdout)) {
29443
+ return smudged.stdout;
29444
+ }
29445
+ return null;
29446
+ }
29216
29447
  return result.stdout;
29217
29448
  }
29218
29449
  function readWorkingFile(filePath, repoRoot) {
29219
29450
  try {
29220
- return readFileSync14(resolve8(repoRoot, filePath));
29451
+ const data = readFileSync15(resolve8(repoRoot, filePath));
29452
+ return isLfsPointer(data) ? null : data;
29221
29453
  } catch {
29222
29454
  return null;
29223
29455
  }
29224
29456
  }
29225
29457
  function readDiskImage(absPath) {
29226
29458
  try {
29227
- const data = readFileSync14(absPath);
29459
+ const data = readFileSync15(absPath);
29228
29460
  return { data, size: data.length };
29229
29461
  } catch {
29230
29462
  return null;
@@ -29235,7 +29467,7 @@ function groundTruthEntry(mode, filePath) {
29235
29467
  }
29236
29468
  function getOldImage(mode, filePath, oldPath, repoRoot) {
29237
29469
  if (mode.type === "diff") {
29238
- return readDiskImage(join14(directComparisonRoots(mode).rootA, oldPath ?? filePath));
29470
+ return readDiskImage(join15(directComparisonRoots(mode).rootA, oldPath ?? filePath));
29239
29471
  }
29240
29472
  if (mode.type === "ground-truth") {
29241
29473
  const entry = groundTruthEntry(mode, filePath);
@@ -29255,7 +29487,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
29255
29487
  }
29256
29488
  function getNewImage(mode, filePath, repoRoot) {
29257
29489
  if (mode.type === "diff") {
29258
- return readDiskImage(join14(directComparisonRoots(mode).rootB, filePath));
29490
+ return readDiskImage(join15(directComparisonRoots(mode).rootB, filePath));
29259
29491
  }
29260
29492
  if (mode.type === "ground-truth") {
29261
29493
  const entry = groundTruthEntry(mode, filePath);
@@ -29356,7 +29588,7 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
29356
29588
  // src/routes/api/outline.ts
29357
29589
  init_queries();
29358
29590
  import { spawnSync as spawnSync9 } from "child_process";
29359
- import { readFileSync as readFileSync15 } from "fs";
29591
+ import { readFileSync as readFileSync16 } from "fs";
29360
29592
  import { Hono as Hono9 } from "hono";
29361
29593
  import { resolve as resolve9 } from "path";
29362
29594
 
@@ -29734,7 +29966,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
29734
29966
  }
29735
29967
  let content = "";
29736
29968
  try {
29737
- content = readFileSync15(resolve9(repoRoot, filePath), "utf-8");
29969
+ content = readFileSync16(resolve9(repoRoot, filePath), "utf-8");
29738
29970
  } catch {
29739
29971
  continue;
29740
29972
  }
@@ -29776,7 +30008,7 @@ import { Hono as Hono10 } from "hono";
29776
30008
  init_install();
29777
30009
  init_loader();
29778
30010
  import { existsSync as existsSync14 } from "fs";
29779
- import { join as join15 } from "path";
30011
+ import { join as join16 } from "path";
29780
30012
 
29781
30013
  // src/plugins/readiness.ts
29782
30014
  import { spawnSync as spawnSync10 } from "child_process";
@@ -29832,7 +30064,7 @@ function listAvailablePlugins(opts) {
29832
30064
  const out = [];
29833
30065
  for (const { manifest } of discoverBundledPlugins(bundledDir)) {
29834
30066
  if (manifest.autoInstall !== false) continue;
29835
- if (existsSync14(join15(userDir, manifest.id))) continue;
30067
+ if (existsSync14(join16(userDir, manifest.id))) continue;
29836
30068
  out.push(describeAvailablePlugin(manifest, opts?.runProbe));
29837
30069
  }
29838
30070
  return out.sort((a, b) => a.name.localeCompare(b.name));
@@ -29849,7 +30081,7 @@ init_loader();
29849
30081
  import { spawnSync as spawnSync11 } from "child_process";
29850
30082
  import { createHash as createHash4 } from "crypto";
29851
30083
  import { cpSync as cpSync2, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
29852
- import { join as join16 } from "path";
30084
+ import { join as join17 } from "path";
29853
30085
  var defaultFetchBytes = async (url2) => {
29854
30086
  const res = await fetch(url2);
29855
30087
  if (!res.ok) throw new Error(`HTTP ${String(res.status)}`);
@@ -29872,7 +30104,7 @@ async function runProvisionStep(step, installDir, requirements, deps) {
29872
30104
  return { step: label2, ok: false, skipped: false, detail: `checksum mismatch (expected ${step.sha256})` };
29873
30105
  }
29874
30106
  }
29875
- writeFileSync12(join16(installDir, step.dest), bytes);
30107
+ writeFileSync12(join17(installDir, step.dest), bytes);
29876
30108
  return { step: label2, ok: true, skipped: false, detail: `saved ${step.dest}` };
29877
30109
  } catch (e) {
29878
30110
  return { step: label2, ok: false, skipped: false, detail: e instanceof Error ? e.message : "download failed" };
@@ -29907,7 +30139,7 @@ async function installAvailablePlugin(id, deps = {}) {
29907
30139
  return { id, installed: false, status: "error", requirements: [], provisioned: [], instructions: [], error: "Not a bundled plugin available to install." };
29908
30140
  }
29909
30141
  const manifest = bundled.manifest;
29910
- const dest = join16(userDir, id);
30142
+ const dest = join17(userDir, id);
29911
30143
  try {
29912
30144
  mkdirSync11(userDir, { recursive: true });
29913
30145
  cpSync2(bundled.dir, dest, { recursive: true });
@@ -30027,7 +30259,8 @@ projectSettingsRoutes.patch("/project-settings", async (c) => {
30027
30259
 
30028
30260
  // src/routes/api/review-notes.ts
30029
30261
  init_store();
30030
- import { readFileSync as readFileSync16, statSync as statSync6 } from "fs";
30262
+ init_lfs();
30263
+ import { readFileSync as readFileSync17, statSync as statSync6 } from "fs";
30031
30264
  import { Hono as Hono12 } from "hono";
30032
30265
  import { extname as extname2, relative as relative2, resolve as resolve10 } from "path";
30033
30266
  var reviewNotesRoutes = new Hono12();
@@ -30054,7 +30287,8 @@ reviewNotesRoutes.get("/review-notes/artifact", (c) => {
30054
30287
  try {
30055
30288
  const stat = statSync6(abs);
30056
30289
  if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
30057
- const body = readFileSync16(abs);
30290
+ const body = readFileSync17(abs);
30291
+ if (isLfsPointer(body)) return c.text("Not found", 404);
30058
30292
  return c.body(body, 200, { "Content-Type": contentType });
30059
30293
  } catch {
30060
30294
  return c.text("Not found", 404);
@@ -30076,12 +30310,12 @@ import { Hono as Hono13 } from "hono";
30076
30310
  // src/export/on-complete-hook.ts
30077
30311
  import { spawn as spawn2 } from "child_process";
30078
30312
  import { appendFileSync } from "fs";
30079
- import { join as join17 } from "path";
30313
+ import { join as join18 } from "path";
30080
30314
  function runOnCompleteHook(command, ctx) {
30081
30315
  if (command === null || command.trim() === "") {
30082
30316
  return Promise.resolve({ ran: false, ok: true, exitCode: 0 });
30083
30317
  }
30084
- const logPath = join17(ctx.repoRoot, ".glassbox", "on-complete.log");
30318
+ const logPath = join18(ctx.repoRoot, ".glassbox", "on-complete.log");
30085
30319
  const log = (s) => {
30086
30320
  try {
30087
30321
  appendFileSync(logPath, s);
@@ -30298,7 +30532,7 @@ apiRoutes.route("/", systemRoutes);
30298
30532
  import { spawnSync as spawnSync12 } from "child_process";
30299
30533
  import { mkdirSync as mkdirSync12 } from "fs";
30300
30534
  import { Hono as Hono17 } from "hono";
30301
- import { join as join18 } from "path";
30535
+ import { join as join19 } from "path";
30302
30536
  init_global_config();
30303
30537
  init_compareVersions();
30304
30538
  var MIN_CLAUDE_VERSION = "2.1.80";
@@ -30307,7 +30541,7 @@ channelApiRoutes.get("/status", async (c) => {
30307
30541
  const config2 = readGlobalConfig();
30308
30542
  const enabled = config2.channelEnabled === true;
30309
30543
  const repoRoot = c.get("repoRoot");
30310
- const dataDir = join18(repoRoot, ".glassbox");
30544
+ const dataDir = join19(repoRoot, ".glassbox");
30311
30545
  const connected = enabled ? await isChannelAlive(dataDir) : false;
30312
30546
  return c.json({ enabled, connected });
30313
30547
  });
@@ -30316,7 +30550,7 @@ channelApiRoutes.post("/enable", (c) => {
30316
30550
  config2.channelEnabled = true;
30317
30551
  });
30318
30552
  const repoRoot = c.get("repoRoot");
30319
- const dataDir = join18(repoRoot, ".glassbox");
30553
+ const dataDir = join19(repoRoot, ".glassbox");
30320
30554
  mkdirSync12(dataDir, { recursive: true });
30321
30555
  registerChannel(dataDir);
30322
30556
  return c.json({ ok: true });
@@ -30326,7 +30560,7 @@ channelApiRoutes.post("/disable", (c) => {
30326
30560
  config2.channelEnabled = false;
30327
30561
  });
30328
30562
  const repoRoot = c.get("repoRoot");
30329
- const dataDir = join18(repoRoot, ".glassbox");
30563
+ const dataDir = join19(repoRoot, ".glassbox");
30330
30564
  unregisterChannel(dataDir);
30331
30565
  return c.json({ ok: true });
30332
30566
  });
@@ -30334,7 +30568,7 @@ channelApiRoutes.post("/trigger", async (c) => {
30334
30568
  const parsed = await parseBody(c, TriggerChannelReqSchema);
30335
30569
  if (!parsed.ok) return parsed.response;
30336
30570
  const repoRoot = c.get("repoRoot");
30337
- const dataDir = join18(repoRoot, ".glassbox");
30571
+ const dataDir = join19(repoRoot, ".glassbox");
30338
30572
  const sent = await triggerChannel(dataDir, parsed.data.message);
30339
30573
  if (!sent) {
30340
30574
  return errorResponse(c, "Channel not connected", 503);
@@ -30441,7 +30675,7 @@ difftoolApiRoutes.post("/end", (c) => {
30441
30675
  });
30442
30676
 
30443
30677
  // src/routes/pages.tsx
30444
- import { readFileSync as readFileSync18 } from "fs";
30678
+ import { readFileSync as readFileSync19 } from "fs";
30445
30679
  import { Hono as Hono19 } from "hono";
30446
30680
  import { relative as relative3, resolve as resolve11 } from "path";
30447
30681
 
@@ -30636,12 +30870,23 @@ function truncateDiffLine(content) {
30636
30870
  function escapeHtml(str) {
30637
30871
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
30638
30872
  }
30873
+ function escapeAttr(str) {
30874
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30875
+ }
30639
30876
 
30640
30877
  // src/utils/noteMarkdown.ts
30641
30878
  var SAFE_URL = /^(https?:\/\/|mailto:)/i;
30642
- function renderInline(escaped) {
30879
+ function renderEmbeddedLink(text, destination, ctx) {
30880
+ if (!/^\d+$/.test(destination)) return null;
30881
+ const target = ctx.related.at(Number(destination));
30882
+ if (target === void 0 || target.uri === "" || target.line < 1) return null;
30883
+ return `<a class="ai-note-loclink" data-loc-file="${escapeAttr(target.uri)}" data-loc-line="${String(target.line)}" title="${escapeAttr(`${target.uri}:${String(target.line)}`)}">${text}</a>`;
30884
+ }
30885
+ function renderInline(escaped, ctx) {
30643
30886
  let out = escaped.replace(/`([^`]+)`/g, "<code>$1</code>");
30644
30887
  out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, text, url2) => {
30888
+ const embedded = renderEmbeddedLink(text, url2, ctx);
30889
+ if (embedded !== null) return embedded;
30645
30890
  if (!SAFE_URL.test(url2)) return match;
30646
30891
  return `<a href="${url2}" target="_blank" rel="noopener noreferrer">${text}</a>`;
30647
30892
  });
@@ -30650,8 +30895,155 @@ function renderInline(escaped) {
30650
30895
  out = out.replace(/(^|[^_])_([^_\s][^_]*)_/g, "$1<em>$2</em>");
30651
30896
  return out;
30652
30897
  }
30653
- function renderNoteMarkdown(text) {
30654
- return escapeHtml(text).split("\n").map(renderInline).join("<br>");
30898
+ var MAX_BLOCK_DEPTH = 6;
30899
+ var HEADING_BASE = 3;
30900
+ var FENCE_RE3 = /^ {0,3}(`{3,}|~{3,})\s*(.*)$/;
30901
+ var HEADING_RE2 = /^ {0,3}(#{1,6})\s+(.*)$/;
30902
+ var HR_RE = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/;
30903
+ var QUOTE_RE = /^ {0,3}&gt;[ \t]?(.*)$/;
30904
+ var ITEM_RE = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
30905
+ var CONTINUATION_RE = /^\s{2,}\S/;
30906
+ function matchItem(line) {
30907
+ const m = ITEM_RE.exec(line);
30908
+ if (m === null) return null;
30909
+ if (HR_RE.test(line)) return null;
30910
+ const ordered = /^\d/.test(m[2]);
30911
+ return {
30912
+ indent: m[1].length,
30913
+ ordered,
30914
+ number: ordered ? parseInt(m[2], 10) : 1,
30915
+ text: m[3]
30916
+ };
30917
+ }
30918
+ function isBlockStart(line) {
30919
+ return FENCE_RE3.test(line) || HEADING_RE2.test(line) || HR_RE.test(line) || QUOTE_RE.test(line) || matchItem(line) !== null;
30920
+ }
30921
+ function dedent(lines) {
30922
+ let min = Infinity;
30923
+ for (const line of lines) {
30924
+ if (line.trim() === "") continue;
30925
+ min = Math.min(min, line.length - line.trimStart().length);
30926
+ }
30927
+ if (min === Infinity || min === 0) return lines;
30928
+ return lines.map((line) => line.trim() === "" ? "" : line.slice(min));
30929
+ }
30930
+ function renderParagraph(lines, ctx) {
30931
+ return `<p>${lines.map((line) => renderInline(line, ctx)).join("<br>")}</p>`;
30932
+ }
30933
+ function renderCodeBlock(lines) {
30934
+ return `<pre><code>${lines.join("\n")}</code></pre>`;
30935
+ }
30936
+ function renderList(run, first, depth, ctx) {
30937
+ const base = first.indent;
30938
+ const items = [];
30939
+ for (const line of run) {
30940
+ const m = matchItem(line);
30941
+ if (m !== null && m.indent <= base) {
30942
+ items.push({ lines: [m.text], nested: [] });
30943
+ continue;
30944
+ }
30945
+ const item = items[items.length - 1];
30946
+ if (item.nested.length > 0 || m !== null) item.nested.push(line);
30947
+ else if (line.trim() === "") item.nested.push(line);
30948
+ else item.lines.push(line.trim());
30949
+ }
30950
+ const tag = first.ordered ? "ol" : "ul";
30951
+ const start = first.ordered && first.number !== 1 ? ` start="${String(first.number)}"` : "";
30952
+ const body = items.map((item) => {
30953
+ const text = item.lines.map((line) => renderInline(line, ctx)).join("<br>");
30954
+ const nested = dedent(item.nested).filter((l, i, a) => !(l === "" && i === a.length - 1));
30955
+ const inner = nested.length > 0 ? renderBlocks(nested, depth + 1, ctx) : "";
30956
+ return `<li>${text}${inner}</li>`;
30957
+ }).join("");
30958
+ return `<${tag}${start}>${body}</${tag}>`;
30959
+ }
30960
+ function renderBlocks(lines, depth, ctx) {
30961
+ if (depth > MAX_BLOCK_DEPTH) {
30962
+ const text = lines.filter((l) => l.trim() !== "");
30963
+ return text.length > 0 ? renderParagraph(text, ctx) : "";
30964
+ }
30965
+ const out = [];
30966
+ let i = 0;
30967
+ while (i < lines.length) {
30968
+ const line = lines[i];
30969
+ if (line.trim() === "") {
30970
+ i++;
30971
+ continue;
30972
+ }
30973
+ const fence = FENCE_RE3.exec(line);
30974
+ if (fence !== null) {
30975
+ const marker = fence[1][0];
30976
+ const body = [];
30977
+ i++;
30978
+ while (i < lines.length) {
30979
+ const close = FENCE_RE3.exec(lines[i]);
30980
+ if (close !== null && close[1][0] === marker && close[2] === "") {
30981
+ i++;
30982
+ break;
30983
+ }
30984
+ body.push(lines[i]);
30985
+ i++;
30986
+ }
30987
+ out.push(renderCodeBlock(body));
30988
+ continue;
30989
+ }
30990
+ const heading = HEADING_RE2.exec(line);
30991
+ if (heading !== null) {
30992
+ const level = Math.min(6, heading[1].length + HEADING_BASE);
30993
+ out.push(`<h${String(level)}>${renderInline(heading[2].replace(/\s+#+\s*$/, ""), ctx)}</h${String(level)}>`);
30994
+ i++;
30995
+ continue;
30996
+ }
30997
+ if (HR_RE.test(line)) {
30998
+ out.push("<hr>");
30999
+ i++;
31000
+ continue;
31001
+ }
31002
+ const quote = QUOTE_RE.exec(line);
31003
+ if (quote !== null) {
31004
+ const inner = [];
31005
+ while (i < lines.length) {
31006
+ const m = QUOTE_RE.exec(lines[i]);
31007
+ if (m === null) break;
31008
+ inner.push(m[1]);
31009
+ i++;
31010
+ }
31011
+ out.push(`<blockquote>${renderBlocks(inner, depth + 1, ctx)}</blockquote>`);
31012
+ continue;
31013
+ }
31014
+ const item = matchItem(line);
31015
+ if (item !== null) {
31016
+ const run = [];
31017
+ while (i < lines.length) {
31018
+ const current = lines[i];
31019
+ if (current.trim() === "") {
31020
+ let k = i;
31021
+ while (k < lines.length && lines[k].trim() === "") k++;
31022
+ if (k < lines.length && (matchItem(lines[k]) !== null || CONTINUATION_RE.test(lines[k]))) {
31023
+ run.push(...lines.slice(i, k));
31024
+ i = k;
31025
+ continue;
31026
+ }
31027
+ break;
31028
+ }
31029
+ if (matchItem(current) === null && !CONTINUATION_RE.test(current)) break;
31030
+ run.push(current);
31031
+ i++;
31032
+ }
31033
+ out.push(renderList(run, item, depth, ctx));
31034
+ continue;
31035
+ }
31036
+ const para = [];
31037
+ while (i < lines.length && lines[i].trim() !== "" && !(para.length > 0 && isBlockStart(lines[i]))) {
31038
+ para.push(lines[i]);
31039
+ i++;
31040
+ }
31041
+ out.push(renderParagraph(para, ctx));
31042
+ }
31043
+ return out.join("");
31044
+ }
31045
+ function renderNoteMarkdown(text, related = []) {
31046
+ return renderBlocks(escapeHtml(text).split("\n"), 0, { related });
30655
31047
  }
30656
31048
 
30657
31049
  // src/components/imageDiff.tsx
@@ -31225,7 +31617,7 @@ function ReviewNoteRows({ notes, repliesByNote }) {
31225
31617
  /* @__PURE__ */ jsxs4("div", { className: "ai-note-item", children: [
31226
31618
  /* @__PURE__ */ jsx4("span", { className: `ai-note-label ai-note-label-${n.kind}`, children: REVIEW_NOTE_LABELS[n.kind] ?? n.kind }),
31227
31619
  n.stale === true ? /* @__PURE__ */ jsx4("span", { className: "ai-note-stale-tag", title: "The code this note referred to has changed", children: "outdated" }) : null,
31228
- /* @__PURE__ */ jsx4("span", { className: "ai-note-text", children: raw(renderNoteMarkdown(n.body)) }),
31620
+ /* @__PURE__ */ jsx4("div", { className: "ai-note-text", children: raw(renderNoteMarkdown(n.body, n.related)) }),
31229
31621
  n.producer !== void 0 ? /* @__PURE__ */ jsx4("span", { className: "ai-note-producer", children: n.producer }) : null,
31230
31622
  n.guid !== void 0 ? /* @__PURE__ */ jsx4("button", { className: "ai-note-reply-btn", "data-line": String(n.line), children: "Reply" }) : null,
31231
31623
  n.stale === true && n.guid !== void 0 ? /* @__PURE__ */ jsxs4("span", { className: "ai-note-stale-actions", children: [
@@ -31326,10 +31718,10 @@ function AnnotationRows({ annotations }) {
31326
31718
 
31327
31719
  // src/themes/config.ts
31328
31720
  init_zod();
31329
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync17, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "fs";
31330
- import { join as join19 } from "path";
31721
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync18, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "fs";
31722
+ import { join as join20 } from "path";
31331
31723
  init_global_config();
31332
- var THEMES_DIR = join19(GLOBAL_CONFIG_DIR, "themes");
31724
+ var THEMES_DIR = join20(GLOBAL_CONFIG_DIR, "themes");
31333
31725
  var ThemeConfigSliceSchema = external_exports.object({ active: external_exports.string().optional() }).loose();
31334
31726
  function getActiveThemeId() {
31335
31727
  const config2 = readGlobalConfig();
@@ -31344,7 +31736,7 @@ function setActiveThemeId(id) {
31344
31736
  }
31345
31737
  function readStoredTheme(filePath) {
31346
31738
  try {
31347
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync17(filePath, "utf-8")));
31739
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync18(filePath, "utf-8")));
31348
31740
  if (!parsed.success) return void 0;
31349
31741
  const d = parsed.data;
31350
31742
  return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
@@ -31357,7 +31749,7 @@ function loadCustomThemes() {
31357
31749
  const themes = [];
31358
31750
  try {
31359
31751
  for (const file2 of readdirSync4(THEMES_DIR).filter((f) => f.endsWith(".json"))) {
31360
- const theme = readStoredTheme(join19(THEMES_DIR, file2));
31752
+ const theme = readStoredTheme(join20(THEMES_DIR, file2));
31361
31753
  if (theme !== void 0) themes.push(theme);
31362
31754
  }
31363
31755
  } catch {
@@ -31366,17 +31758,17 @@ function loadCustomThemes() {
31366
31758
  }
31367
31759
  function saveCustomTheme(theme) {
31368
31760
  mkdirSync13(THEMES_DIR, { recursive: true });
31369
- const filePath = join19(THEMES_DIR, `${theme.id}.json`);
31761
+ const filePath = join20(THEMES_DIR, `${theme.id}.json`);
31370
31762
  writeFileSync13(filePath, JSON.stringify(theme, null, 2), "utf-8");
31371
31763
  }
31372
31764
  function deleteCustomTheme(id) {
31373
- const filePath = join19(THEMES_DIR, `${id}.json`);
31765
+ const filePath = join20(THEMES_DIR, `${id}.json`);
31374
31766
  if (existsSync15(filePath)) {
31375
31767
  unlinkSync3(filePath);
31376
31768
  }
31377
31769
  }
31378
31770
  function getCustomTheme(id) {
31379
- const filePath = join19(THEMES_DIR, `${id}.json`);
31771
+ const filePath = join20(THEMES_DIR, `${id}.json`);
31380
31772
  if (!existsSync15(filePath)) return void 0;
31381
31773
  return readStoredTheme(filePath);
31382
31774
  }
@@ -31855,7 +32247,7 @@ pageRoutes.get("/file-raw", (c) => {
31855
32247
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
31856
32248
  let content;
31857
32249
  try {
31858
- content = readFileSync18(abs, "utf-8");
32250
+ content = readFileSync19(abs, "utf-8");
31859
32251
  } catch {
31860
32252
  return c.text("File not found", 404);
31861
32253
  }
@@ -32043,22 +32435,22 @@ async function startServer(port, reviewId, repoRoot, options) {
32043
32435
  c.set("onCompleteCommand", onCompleteCommand);
32044
32436
  await next();
32045
32437
  });
32046
- const selfDir = dirname8(fileURLToPath3(import.meta.url));
32047
- const distDir = existsSync16(join20(selfDir, "client", "styles.css")) ? join20(selfDir, "client") : join20(selfDir, "..", "dist", "client");
32438
+ const selfDir = dirname9(fileURLToPath4(import.meta.url));
32439
+ const distDir = existsSync16(join21(selfDir, "client", "styles.css")) ? join21(selfDir, "client") : join21(selfDir, "..", "dist", "client");
32048
32440
  app.get("/static/styles.css", (c) => {
32049
- const css = readFileSync19(join20(distDir, "styles.css"), "utf-8");
32441
+ const css = readFileSync20(join21(distDir, "styles.css"), "utf-8");
32050
32442
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
32051
32443
  });
32052
32444
  app.get("/static/app.js", (c) => {
32053
- const js = readFileSync19(join20(distDir, "app.global.js"), "utf-8");
32445
+ const js = readFileSync20(join21(distDir, "app.global.js"), "utf-8");
32054
32446
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
32055
32447
  });
32056
32448
  app.get("/static/history.js", (c) => {
32057
- const js = readFileSync19(join20(distDir, "history.global.js"), "utf-8");
32449
+ const js = readFileSync20(join21(distDir, "history.global.js"), "utf-8");
32058
32450
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
32059
32451
  });
32060
32452
  app.get("/favicon.svg", (c) => {
32061
- const svg = readFileSync19(join20(distDir, "favicon.svg"), "utf-8");
32453
+ const svg = readFileSync20(join21(distDir, "favicon.svg"), "utf-8");
32062
32454
  return c.body(svg, 200, { "Content-Type": "image/svg+xml", "Cache-Control": "no-cache" });
32063
32455
  });
32064
32456
  app.get("/favicon.ico", (c) => c.body(null, 204));
@@ -32102,7 +32494,7 @@ async function startServer(port, reviewId, repoRoot, options) {
32102
32494
  try {
32103
32495
  const globalConfig2 = readGlobalConfig();
32104
32496
  if (globalConfig2.channelEnabled === true) {
32105
- const dataDir = join20(repoRoot, ".glassbox");
32497
+ const dataDir = join21(repoRoot, ".glassbox");
32106
32498
  registerChannel(dataDir);
32107
32499
  }
32108
32500
  } catch {
@@ -32117,8 +32509,8 @@ async function startServer(port, reviewId, repoRoot, options) {
32117
32509
  }
32118
32510
 
32119
32511
  // src/skills.ts
32120
- import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
32121
- import { join as join21 } from "path";
32512
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
32513
+ import { join as join22 } from "path";
32122
32514
  var SKILL_VERSION = 1;
32123
32515
  function versionHeader() {
32124
32516
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -32130,7 +32522,7 @@ function parseVersionHeader(content) {
32130
32522
  }
32131
32523
  function updateFile(path, content) {
32132
32524
  if (existsSync17(path)) {
32133
- const existing = readFileSync20(path, "utf-8");
32525
+ const existing = readFileSync21(path, "utf-8");
32134
32526
  const version2 = parseVersionHeader(existing);
32135
32527
  if (version2 !== null && version2 >= SKILL_VERSION) {
32136
32528
  return false;
@@ -32156,7 +32548,7 @@ function skillBody() {
32156
32548
  ].join("\n");
32157
32549
  }
32158
32550
  function ensureClaudeSkills(cwd) {
32159
- const dir = join21(cwd, ".claude", "skills", "glassbox");
32551
+ const dir = join22(cwd, ".claude", "skills", "glassbox");
32160
32552
  mkdirSync14(dir, { recursive: true });
32161
32553
  const content = [
32162
32554
  "---",
@@ -32169,10 +32561,10 @@ function ensureClaudeSkills(cwd) {
32169
32561
  skillBody(),
32170
32562
  ""
32171
32563
  ].join("\n");
32172
- return updateFile(join21(dir, "SKILL.md"), content);
32564
+ return updateFile(join22(dir, "SKILL.md"), content);
32173
32565
  }
32174
32566
  function ensureCursorRules(cwd) {
32175
- const rulesDir = join21(cwd, ".cursor", "rules");
32567
+ const rulesDir = join22(cwd, ".cursor", "rules");
32176
32568
  mkdirSync14(rulesDir, { recursive: true });
32177
32569
  const content = [
32178
32570
  "---",
@@ -32184,10 +32576,10 @@ function ensureCursorRules(cwd) {
32184
32576
  skillBody(),
32185
32577
  ""
32186
32578
  ].join("\n");
32187
- return updateFile(join21(rulesDir, "glassbox.mdc"), content);
32579
+ return updateFile(join22(rulesDir, "glassbox.mdc"), content);
32188
32580
  }
32189
32581
  function ensureCopilotPrompts(cwd) {
32190
- const promptsDir = join21(cwd, ".github", "prompts");
32582
+ const promptsDir = join22(cwd, ".github", "prompts");
32191
32583
  mkdirSync14(promptsDir, { recursive: true });
32192
32584
  const content = [
32193
32585
  "---",
@@ -32198,10 +32590,10 @@ function ensureCopilotPrompts(cwd) {
32198
32590
  skillBody(),
32199
32591
  ""
32200
32592
  ].join("\n");
32201
- return updateFile(join21(promptsDir, "glassbox.prompt.md"), content);
32593
+ return updateFile(join22(promptsDir, "glassbox.prompt.md"), content);
32202
32594
  }
32203
32595
  function ensureWindsurfRules(cwd) {
32204
- const rulesDir = join21(cwd, ".windsurf", "rules");
32596
+ const rulesDir = join22(cwd, ".windsurf", "rules");
32205
32597
  mkdirSync14(rulesDir, { recursive: true });
32206
32598
  const content = [
32207
32599
  "---",
@@ -32213,21 +32605,21 @@ function ensureWindsurfRules(cwd) {
32213
32605
  skillBody(),
32214
32606
  ""
32215
32607
  ].join("\n");
32216
- return updateFile(join21(rulesDir, "glassbox.md"), content);
32608
+ return updateFile(join22(rulesDir, "glassbox.md"), content);
32217
32609
  }
32218
32610
  function ensureSkills() {
32219
32611
  const cwd = process.cwd();
32220
32612
  const platforms = [];
32221
- if (existsSync17(join21(cwd, ".claude"))) {
32613
+ if (existsSync17(join22(cwd, ".claude"))) {
32222
32614
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
32223
32615
  }
32224
- if (existsSync17(join21(cwd, ".cursor"))) {
32616
+ if (existsSync17(join22(cwd, ".cursor"))) {
32225
32617
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
32226
32618
  }
32227
- if (existsSync17(join21(cwd, ".github", "prompts")) || existsSync17(join21(cwd, ".github", "copilot-instructions.md"))) {
32619
+ if (existsSync17(join22(cwd, ".github", "prompts")) || existsSync17(join22(cwd, ".github", "copilot-instructions.md"))) {
32228
32620
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
32229
32621
  }
32230
- if (existsSync17(join21(cwd, ".windsurf"))) {
32622
+ if (existsSync17(join22(cwd, ".windsurf"))) {
32231
32623
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
32232
32624
  }
32233
32625
  return platforms;
@@ -32235,29 +32627,20 @@ function ensureSkills() {
32235
32627
 
32236
32628
  // src/update-check.ts
32237
32629
  init_zod();
32630
+ init_app_version();
32238
32631
  init_compareVersions();
32239
- import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
32632
+ import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
32240
32633
  import { get } from "https";
32241
32634
  import { homedir as homedir2 } from "os";
32242
- import { dirname as dirname9, join as join22 } from "path";
32243
- import { fileURLToPath as fileURLToPath4 } from "url";
32244
- var DATA_DIR = join22(homedir2(), ".glassbox");
32245
- var CHECK_FILE = join22(DATA_DIR, "last-update-check");
32635
+ import { join as join23 } from "path";
32636
+ var DATA_DIR = join23(homedir2(), ".glassbox");
32637
+ var CHECK_FILE = join23(DATA_DIR, "last-update-check");
32246
32638
  var PACKAGE_NAME = "glassbox";
32247
- var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
32248
- function getCurrentVersion() {
32249
- try {
32250
- const dir = dirname9(fileURLToPath4(import.meta.url));
32251
- const raw2 = JSON.parse(readFileSync21(join22(dir, "..", "package.json"), "utf-8"));
32252
- return VersionPayloadSchema.parse(raw2).version;
32253
- } catch {
32254
- return "0.0.0";
32255
- }
32256
- }
32639
+ var VersionPayloadSchema2 = external_exports.object({ version: external_exports.string() });
32257
32640
  function getLastCheckDate() {
32258
32641
  try {
32259
32642
  if (existsSync18(CHECK_FILE)) {
32260
- return readFileSync21(CHECK_FILE, "utf-8").trim();
32643
+ return readFileSync22(CHECK_FILE, "utf-8").trim();
32261
32644
  }
32262
32645
  } catch {
32263
32646
  }
@@ -32287,7 +32670,7 @@ function fetchLatestVersion() {
32287
32670
  res.on("end", () => {
32288
32671
  try {
32289
32672
  const raw2 = JSON.parse(data);
32290
- resolve13(VersionPayloadSchema.parse(raw2).version);
32673
+ resolve13(VersionPayloadSchema2.parse(raw2).version);
32291
32674
  } catch {
32292
32675
  resolve13(null);
32293
32676
  }
@@ -32626,7 +33009,7 @@ async function main() {
32626
33009
  console.log("AI service test mode enabled \u2014 using mock AI responses");
32627
33010
  }
32628
33011
  if (debug) {
32629
- console.log(`[debug] Build timestamp: ${"2026-07-23T02:30:34.669Z"}`);
33012
+ console.log(`[debug] Build timestamp: ${"2026-07-24T02:30:22.046Z"}`);
32630
33013
  }
32631
33014
  if (projectDir !== null) {
32632
33015
  if (!existsSync20(projectDir) || !statSync8(projectDir).isDirectory()) {
@@ -32636,7 +33019,7 @@ async function main() {
32636
33019
  process.chdir(projectDir);
32637
33020
  }
32638
33021
  if (dataDir === null) {
32639
- dataDir = join24(process.cwd(), ".glassbox");
33022
+ dataDir = join25(process.cwd(), ".glassbox");
32640
33023
  }
32641
33024
  if (difftoolServe) {
32642
33025
  await runDifftoolServe(dataDir, port, { noOpen, strictPort, onComplete });
@@ -32652,7 +33035,7 @@ async function main() {
32652
33035
  }
32653
33036
  process.exit(1);
32654
33037
  }
32655
- dataDir = join24(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
33038
+ dataDir = join25(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
32656
33039
  setDemoMode(demo);
32657
33040
  console.log(`
32658
33041
  DEMO MODE: ${scenario.label}