glassbox 0.14.0 → 0.15.0-beta.6

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
@@ -52,6 +52,7 @@ var init_schema = __esm({
52
52
  content TEXT NOT NULL,
53
53
  is_stale BOOLEAN NOT NULL DEFAULT FALSE,
54
54
  original_content TEXT,
55
+ reply_to_note_id TEXT,
55
56
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
56
57
  updated_at TIMESTAMP NOT NULL DEFAULT NOW()
57
58
  );
@@ -146,6 +147,7 @@ async function initSchema(db2) {
146
147
  await addColumnIfMissing(db2, "reviews", "head_commit", "TEXT");
147
148
  await addColumnIfMissing(db2, "annotations", "is_stale", "BOOLEAN NOT NULL DEFAULT FALSE");
148
149
  await addColumnIfMissing(db2, "annotations", "original_content", "TEXT");
150
+ await addColumnIfMissing(db2, "annotations", "reply_to_note_id", "TEXT");
149
151
  await addColumnIfMissing(db2, "ai_file_scores", "notes", "TEXT");
150
152
  await addColumnIfMissing(db2, "ai_analyses", "progress_completed", "INTEGER NOT NULL DEFAULT 0");
151
153
  await addColumnIfMissing(db2, "ai_analyses", "progress_total", "INTEGER NOT NULL DEFAULT 0");
@@ -15369,11 +15371,11 @@ function parseRow(schema, row) {
15369
15371
  }
15370
15372
  return result.data;
15371
15373
  }
15372
- function parseJsonColumn(schema, raw) {
15373
- if (raw === null || raw === void 0) return null;
15374
+ function parseJsonColumn(schema, raw2) {
15375
+ if (raw2 === null || raw2 === void 0) return null;
15374
15376
  let parsed;
15375
15377
  try {
15376
- parsed = JSON.parse(raw);
15378
+ parsed = JSON.parse(raw2);
15377
15379
  } catch {
15378
15380
  return null;
15379
15381
  }
@@ -15421,6 +15423,10 @@ var init_schemas3 = __esm({
15421
15423
  content: external_exports.string(),
15422
15424
  is_stale: external_exports.boolean(),
15423
15425
  original_content: external_exports.string().nullable(),
15426
+ // The SARIF guid of the AI review note this annotation replies to (doc 20
15427
+ // threading), or null for a normal annotation. `.default(null)` tolerates
15428
+ // rows written before the column existed.
15429
+ reply_to_note_id: external_exports.string().nullable().default(null),
15424
15430
  created_at: TimestampSchema,
15425
15431
  updated_at: TimestampSchema
15426
15432
  });
@@ -15592,13 +15598,13 @@ async function deleteReviewFile(id) {
15592
15598
  await db2.query("DELETE FROM annotations WHERE review_file_id = $1", [id]);
15593
15599
  await db2.query("DELETE FROM review_files WHERE id = $1", [id]);
15594
15600
  }
15595
- async function addAnnotation(reviewFileId, lineNumber, side, category, content) {
15601
+ async function addAnnotation(reviewFileId, lineNumber, side, category, content, replyToNoteId) {
15596
15602
  const db2 = await getDb();
15597
15603
  const id = generateId();
15598
15604
  const result = await db2.query(
15599
- `INSERT INTO annotations (id, review_file_id, line_number, side, category, content)
15600
- VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
15601
- [id, reviewFileId, lineNumber, side, category, content]
15605
+ `INSERT INTO annotations (id, review_file_id, line_number, side, category, content, reply_to_note_id)
15606
+ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
15607
+ [id, reviewFileId, lineNumber, side, category, content, replyToNoteId ?? null]
15602
15608
  );
15603
15609
  const annotation = parseRow(AnnotationSchema, result.rows[0]);
15604
15610
  if (annotation === void 0) throw new Error("addAnnotation: INSERT did not return a row");
@@ -15719,6 +15725,450 @@ var init_queries = __esm({
15719
15725
  }
15720
15726
  });
15721
15727
 
15728
+ // src/review-notes/types.ts
15729
+ function isNoteKind(value) {
15730
+ return NOTE_KINDS.includes(value);
15731
+ }
15732
+ var NOTE_KINDS, CONFIDENCE_PROPERTY_KEY, ANCHOR_FINGERPRINT_KEY, DEFAULT_PRODUCER, DEFAULT_SHARD_CAP;
15733
+ var init_types = __esm({
15734
+ "src/review-notes/types.ts"() {
15735
+ "use strict";
15736
+ NOTE_KINDS = [
15737
+ "rationale",
15738
+ "proof",
15739
+ "assumption",
15740
+ "alternative-considered",
15741
+ "risk",
15742
+ "test-evidence"
15743
+ ];
15744
+ CONFIDENCE_PROPERTY_KEY = "ext-ai-tool-confidence";
15745
+ ANCHOR_FINGERPRINT_KEY = "prNoteAnchor/v1";
15746
+ DEFAULT_PRODUCER = "unknown-ai-tool";
15747
+ DEFAULT_SHARD_CAP = 1e4;
15748
+ }
15749
+ });
15750
+
15751
+ // src/review-notes/sarif.ts
15752
+ function emptyLog(producer, opts = {}) {
15753
+ return { $schema: SARIF_SCHEMA_URL, version: "2.1.0", runs: [newRun(producer, opts)] };
15754
+ }
15755
+ function newRun(producer, opts = {}) {
15756
+ const run = {
15757
+ tool: {
15758
+ driver: {
15759
+ name: producer,
15760
+ ...opts.producerVersion !== void 0 ? { version: opts.producerVersion } : {},
15761
+ rules: [{
15762
+ id: REVIEW_NOTE_RULE_ID,
15763
+ name: "ReviewNote",
15764
+ shortDescription: { text: "AI-authored, line-anchored review note." }
15765
+ }]
15766
+ }
15767
+ },
15768
+ results: []
15769
+ };
15770
+ if (opts.revisionId !== void 0 || opts.branch !== void 0 || opts.repositoryUri !== void 0) {
15771
+ const vcs = {};
15772
+ if (opts.repositoryUri !== void 0) vcs.repositoryUri = opts.repositoryUri;
15773
+ if (opts.revisionId !== void 0) vcs.revisionId = opts.revisionId;
15774
+ if (opts.branch !== void 0) vcs.branch = opts.branch;
15775
+ run.versionControlProvenance = [vcs];
15776
+ }
15777
+ return run;
15778
+ }
15779
+ function buildResult(input, meta3) {
15780
+ const region = { startLine: input.startLine, endLine: input.endLine };
15781
+ if (meta3.snippet !== void 0) region.snippet = { text: meta3.snippet };
15782
+ const properties = { tags: [input.kind] };
15783
+ if (input.confidence !== void 0) properties[CONFIDENCE_PROPERTY_KEY] = input.confidence;
15784
+ const result = {
15785
+ ruleId: REVIEW_NOTE_RULE_ID,
15786
+ ruleIndex: 0,
15787
+ kind: "informational",
15788
+ level: input.kind === "risk" ? "warning" : "none",
15789
+ guid: meta3.guid,
15790
+ message: { text: input.body, markdown: input.body },
15791
+ locations: [{
15792
+ physicalLocation: {
15793
+ artifactLocation: { uri: input.file },
15794
+ region
15795
+ }
15796
+ }],
15797
+ properties
15798
+ };
15799
+ if (input.rank !== void 0) result.rank = input.rank;
15800
+ if (input.ticket !== void 0 && input.ticket !== "") result.workItemUris = [input.ticket];
15801
+ if (meta3.fingerprint !== void 0) result.partialFingerprints = { [ANCHOR_FINGERPRINT_KEY]: meta3.fingerprint };
15802
+ if (input.artifacts !== void 0 && input.artifacts.length > 0) {
15803
+ result.attachments = input.artifacts.map((uri) => {
15804
+ const artifactLocation = { uri };
15805
+ const hash2 = meta3.artifactHashes?.[uri];
15806
+ if (hash2 !== void 0) artifactLocation.properties = { "ext-sha256": hash2 };
15807
+ return { artifactLocation };
15808
+ });
15809
+ }
15810
+ return result;
15811
+ }
15812
+ var SARIF_SCHEMA_URL, REVIEW_NOTE_RULE_ID, SarifLogShapeSchema;
15813
+ var init_sarif = __esm({
15814
+ "src/review-notes/sarif.ts"() {
15815
+ "use strict";
15816
+ init_zod();
15817
+ init_types();
15818
+ SARIF_SCHEMA_URL = "https://json.schemastore.org/sarif-2.1.0.json";
15819
+ REVIEW_NOTE_RULE_ID = "review-note";
15820
+ SarifLogShapeSchema = external_exports.object({
15821
+ version: external_exports.string(),
15822
+ runs: external_exports.array(external_exports.object({
15823
+ tool: external_exports.object({ driver: external_exports.object({ name: external_exports.string() }).loose() }).loose(),
15824
+ versionControlProvenance: external_exports.array(external_exports.object({ revisionId: external_exports.string().optional() }).loose()).optional(),
15825
+ results: external_exports.array(external_exports.unknown())
15826
+ }).loose())
15827
+ }).loose();
15828
+ }
15829
+ });
15830
+
15831
+ // src/review-notes/view.ts
15832
+ var REVIEW_NOTE_LABELS, IMAGE_ARTIFACT_RE;
15833
+ var init_view = __esm({
15834
+ "src/review-notes/view.ts"() {
15835
+ "use strict";
15836
+ REVIEW_NOTE_LABELS = {
15837
+ rationale: "Rationale",
15838
+ proof: "Proof",
15839
+ assumption: "Assumption",
15840
+ "alternative-considered": "Alternative",
15841
+ risk: "Risk",
15842
+ "test-evidence": "Test"
15843
+ };
15844
+ IMAGE_ARTIFACT_RE = /\.(png|webp|avif|gif|jpe?g|svg)$/i;
15845
+ }
15846
+ });
15847
+
15848
+ // src/review-notes/store.ts
15849
+ import { spawnSync as spawnSync5 } from "child_process";
15850
+ import { createHash } from "crypto";
15851
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
15852
+ import { dirname as dirname3, join as join6 } from "path";
15853
+ function sanitizeRel(file2) {
15854
+ const rel = file2.replace(/\\/g, "/").replace(/^\/+/, "").replace(/(^|\/)\.\.(?=\/|$)/g, "$1_");
15855
+ return rel === "" ? "file" : rel;
15856
+ }
15857
+ function shardPath(repoRoot, safeRel, index) {
15858
+ return join6(repoRoot, NOTES_SUBDIR, `${safeRel}.${String(index).padStart(6, "0")}.sarif`);
15859
+ }
15860
+ function listShardIndices(repoRoot, safeRel) {
15861
+ const dir = join6(repoRoot, NOTES_SUBDIR, dirname3(safeRel));
15862
+ if (!existsSync5(dir)) return [];
15863
+ const base = safeRel.split("/").pop() ?? safeRel;
15864
+ const indices = [];
15865
+ for (const entry of readdirSync(dir)) {
15866
+ if (!entry.startsWith(base + ".")) continue;
15867
+ const m = SHARD_RE.exec(entry);
15868
+ if (m !== null && entry === `${base}.${m[1]}.sarif`) indices.push(parseInt(m[1], 10));
15869
+ }
15870
+ return indices.sort((a, b) => a - b);
15871
+ }
15872
+ function totalResults(log) {
15873
+ return log.runs.reduce((sum, run) => sum + run.results.length, 0);
15874
+ }
15875
+ function readLog(path) {
15876
+ const raw2 = JSON.parse(readFileSync5(path, "utf-8"));
15877
+ const parsed = SarifLogShapeSchema.safeParse(raw2);
15878
+ if (!parsed.success) {
15879
+ throw new Error(`existing notes shard is not a SARIF log we recognize, refusing to overwrite: ${path}`);
15880
+ }
15881
+ return raw2;
15882
+ }
15883
+ function findOrAddRun(log, producer, producerVersion, vcs) {
15884
+ const existing = log.runs.find((run2) => run2.tool.driver.name === producer && run2.tool.driver.version === producerVersion && (run2.versionControlProvenance?.[0]?.revisionId ?? void 0) === vcs.revisionId);
15885
+ if (existing !== void 0) return existing;
15886
+ const run = newRun(producer, { producerVersion, ...vcs });
15887
+ log.runs.push(run);
15888
+ return run;
15889
+ }
15890
+ function gitValue(repoRoot, args) {
15891
+ try {
15892
+ const res = spawnSync5("git", args, { cwd: repoRoot, encoding: "utf-8" });
15893
+ if (res.status !== 0) return void 0;
15894
+ const out = res.stdout.trim();
15895
+ return out === "" ? void 0 : out;
15896
+ } catch {
15897
+ return void 0;
15898
+ }
15899
+ }
15900
+ function anchorSnippet(repoRoot, safeRel, startLine, endLine) {
15901
+ try {
15902
+ const lines = readFileSync5(join6(repoRoot, safeRel), "utf-8").split("\n");
15903
+ const slice = lines.slice(Math.max(0, startLine - 1), Math.max(startLine, endLine));
15904
+ if (slice.length === 0) return {};
15905
+ const snippet = slice.join("\n");
15906
+ const normalized = slice.map((l) => l.trim().replace(/\s+/g, " ")).join("\n");
15907
+ const fingerprint = createHash("sha256").update(normalized).digest("hex").slice(0, 32);
15908
+ return { snippet, fingerprint };
15909
+ } catch {
15910
+ return {};
15911
+ }
15912
+ }
15913
+ function warnIfPrNotesIgnored(repoRoot) {
15914
+ try {
15915
+ const res = spawnSync5("git", ["check-ignore", ".pr-notes"], { cwd: repoRoot, encoding: "utf-8" });
15916
+ if (res.status === 0) {
15917
+ console.error("Warning: .pr-notes/ appears to be gitignored. Review notes must be committed to be useful \u2014 remove it from .gitignore.");
15918
+ }
15919
+ } catch {
15920
+ }
15921
+ }
15922
+ function writeReviewNote(repoRoot, input, opts = {}) {
15923
+ const cap = opts.cap ?? DEFAULT_SHARD_CAP;
15924
+ const safeRel = sanitizeRel(input.file);
15925
+ const producer = input.producer ?? DEFAULT_PRODUCER;
15926
+ const vcs = {
15927
+ revisionId: gitValue(repoRoot, ["rev-parse", "HEAD"]),
15928
+ branch: gitValue(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]),
15929
+ repositoryUri: gitValue(repoRoot, ["config", "--get", "remote.origin.url"])
15930
+ };
15931
+ const { snippet, fingerprint } = anchorSnippet(repoRoot, safeRel, input.startLine, input.endLine);
15932
+ const guid3 = generateId();
15933
+ const artifactHashes = hashArtifacts(repoRoot, input.artifacts ?? []);
15934
+ const result = buildResult({ ...input, file: safeRel }, { guid: guid3, snippet, fingerprint, artifactHashes });
15935
+ if ((input.artifacts ?? []).some((a) => IMAGE_ARTIFACT_RE.test(a))) ensureArtifactLfsFilter(repoRoot);
15936
+ const indices = listShardIndices(repoRoot, safeRel);
15937
+ let index = indices.length > 0 ? indices[indices.length - 1] : 0;
15938
+ let path = shardPath(repoRoot, safeRel, index);
15939
+ let log;
15940
+ if (existsSync5(path)) {
15941
+ log = readLog(path);
15942
+ if (totalResults(log) >= cap) {
15943
+ index += 1;
15944
+ path = shardPath(repoRoot, safeRel, index);
15945
+ log = emptyLog(producer, { producerVersion: input.producerVersion, ...vcs });
15946
+ }
15947
+ } else {
15948
+ log = emptyLog(producer, { producerVersion: input.producerVersion, ...vcs });
15949
+ }
15950
+ findOrAddRun(log, producer, input.producerVersion, vcs).results.push(result);
15951
+ mkdirSync4(dirname3(path), { recursive: true });
15952
+ writeFileSync5(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15953
+ return { path, guid: guid3 };
15954
+ }
15955
+ function writeLog(path, log) {
15956
+ writeFileSync5(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15957
+ }
15958
+ function hashArtifacts(repoRoot, artifacts) {
15959
+ const out = {};
15960
+ for (const uri of artifacts) {
15961
+ const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
15962
+ if (/(^|\/)\.\.(\/|$)/.test(safe)) continue;
15963
+ try {
15964
+ const abs = join6(repoRoot, safe);
15965
+ const stat = statSync2(abs);
15966
+ if (!stat.isFile() || stat.size > ARTIFACT_HASH_MAX_BYTES) continue;
15967
+ out[uri] = createHash("sha256").update(readFileSync5(abs)).digest("hex");
15968
+ } catch {
15969
+ }
15970
+ }
15971
+ return out;
15972
+ }
15973
+ function ensureArtifactLfsFilter(repoRoot) {
15974
+ const path = join6(repoRoot, ".gitattributes");
15975
+ try {
15976
+ const existing = existsSync5(path) ? readFileSync5(path, "utf-8") : "";
15977
+ if (existing.includes(".pr-notes/artifacts/**")) return;
15978
+ const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
15979
+ writeFileSync5(path, `${existing}${prefix}${LFS_FILTER_LINE}
15980
+ `, "utf-8");
15981
+ } catch {
15982
+ }
15983
+ }
15984
+ function listShardPaths(repoRoot, safeRel) {
15985
+ return listShardIndices(repoRoot, safeRel).map((i) => shardPath(repoRoot, safeRel, i));
15986
+ }
15987
+ function allShardPaths(repoRoot) {
15988
+ const root = join6(repoRoot, NOTES_SUBDIR);
15989
+ if (!existsSync5(root)) return [];
15990
+ const entries = readdirSync(root, { recursive: true });
15991
+ return entries.filter((e) => SHARD_RE.test(e)).map((e) => join6(root, e));
15992
+ }
15993
+ function persistOrDelete(path, log) {
15994
+ log.runs = log.runs.filter((run) => run.results.length > 0);
15995
+ if (log.runs.length === 0) {
15996
+ if (existsSync5(path)) unlinkSync(path);
15997
+ } else {
15998
+ writeLog(path, log);
15999
+ }
16000
+ }
16001
+ function removeNote(repoRoot, guid3, file2) {
16002
+ const paths = file2 !== void 0 ? listShardPaths(repoRoot, sanitizeRel(file2)) : allShardPaths(repoRoot);
16003
+ for (const path of paths) {
16004
+ const log = readLog(path);
16005
+ for (const run of log.runs) {
16006
+ const idx = run.results.findIndex((r) => r.guid === guid3);
16007
+ if (idx !== -1) {
16008
+ run.results.splice(idx, 1);
16009
+ persistOrDelete(path, log);
16010
+ return true;
16011
+ }
16012
+ }
16013
+ }
16014
+ return false;
16015
+ }
16016
+ function updateNote(repoRoot, guid3, patch, file2) {
16017
+ const paths = file2 !== void 0 ? listShardPaths(repoRoot, sanitizeRel(file2)) : allShardPaths(repoRoot);
16018
+ for (const path of paths) {
16019
+ const log = readLog(path);
16020
+ for (const run of log.runs) {
16021
+ const result = run.results.find((r) => r.guid === guid3);
16022
+ if (result !== void 0) {
16023
+ if (patch.body !== void 0) result.message = { text: patch.body, markdown: patch.body };
16024
+ if (patch.kind !== void 0) {
16025
+ result.properties = { ...result.properties, tags: [patch.kind] };
16026
+ result.level = patch.kind === "risk" ? "warning" : "none";
16027
+ }
16028
+ if (patch.confidence !== void 0) {
16029
+ result.properties = { ...result.properties, [CONFIDENCE_PROPERTY_KEY]: patch.confidence };
16030
+ }
16031
+ if (patch.rank !== void 0) result.rank = patch.rank;
16032
+ if (patch.ticket !== void 0) result.workItemUris = patch.ticket === "" ? void 0 : [patch.ticket];
16033
+ writeLog(path, log);
16034
+ return true;
16035
+ }
16036
+ }
16037
+ }
16038
+ return false;
16039
+ }
16040
+ function noteKey(r) {
16041
+ const loc = r.locations?.[0]?.physicalLocation;
16042
+ return JSON.stringify([
16043
+ loc?.artifactLocation?.uri,
16044
+ loc?.region?.startLine,
16045
+ loc?.region?.endLine,
16046
+ r.properties?.tags,
16047
+ r.message?.text
16048
+ ]);
16049
+ }
16050
+ function coalesceFile(repoRoot, file2) {
16051
+ const paths = listShardPaths(repoRoot, sanitizeRel(file2));
16052
+ const logs = paths.map((path) => ({ path, log: readLog(path) }));
16053
+ const refs = [];
16054
+ logs.forEach((entry, logIdx) => {
16055
+ entry.log.runs.forEach((run, runIdx) => {
16056
+ run.results.forEach((r, resultIdx) => {
16057
+ refs.push({ logIdx, runIdx, resultIdx, key: noteKey(r) });
16058
+ });
16059
+ });
16060
+ });
16061
+ const lastIndexForKey = /* @__PURE__ */ new Map();
16062
+ refs.forEach((ref, i) => lastIndexForKey.set(ref.key, i));
16063
+ const toRemove = refs.filter((ref, i) => lastIndexForKey.get(ref.key) !== i);
16064
+ if (toRemove.length === 0) return 0;
16065
+ const touched = /* @__PURE__ */ new Set();
16066
+ const byRun = /* @__PURE__ */ new Map();
16067
+ for (const ref of toRemove) {
16068
+ const k = `${String(ref.logIdx)}:${String(ref.runIdx)}`;
16069
+ const arr = byRun.get(k) ?? [];
16070
+ arr.push(ref.resultIdx);
16071
+ byRun.set(k, arr);
16072
+ touched.add(ref.logIdx);
16073
+ }
16074
+ for (const [k, idxs] of byRun) {
16075
+ const [logIdx, runIdx] = k.split(":").map(Number);
16076
+ const results = logs[logIdx].log.runs[runIdx].results;
16077
+ for (const idx of idxs.sort((a, b) => b - a)) results.splice(idx, 1);
16078
+ }
16079
+ for (const logIdx of touched) persistOrDelete(logs[logIdx].path, logs[logIdx].log);
16080
+ return toRemove.length;
16081
+ }
16082
+ function coalesceAll(repoRoot) {
16083
+ const root = join6(repoRoot, NOTES_SUBDIR);
16084
+ if (!existsSync5(root)) return 0;
16085
+ const sources = /* @__PURE__ */ new Set();
16086
+ for (const entry of readdirSync(root, { recursive: true })) {
16087
+ const m = SHARD_RE.exec(entry);
16088
+ if (m !== null) sources.add(entry.replace(SHARD_RE, "").replace(/\\/g, "/"));
16089
+ }
16090
+ let total = 0;
16091
+ for (const src of sources) total += coalesceFile(repoRoot, src);
16092
+ return total;
16093
+ }
16094
+ function readArtifactText(repoRoot, uri) {
16095
+ const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
16096
+ if (/(^|\/)\.\.(\/|$)/.test(safe)) return void 0;
16097
+ try {
16098
+ const abs = join6(repoRoot, safe);
16099
+ const stat = statSync2(abs);
16100
+ if (!stat.isFile() || stat.size > ARTIFACT_MAX_BYTES) return void 0;
16101
+ const buf = readFileSync5(abs);
16102
+ if (buf.includes(0)) return void 0;
16103
+ return buf.toString("utf-8");
16104
+ } catch {
16105
+ return void 0;
16106
+ }
16107
+ }
16108
+ function readArtifacts(repoRoot, result) {
16109
+ const out = [];
16110
+ for (const att of result.attachments ?? []) {
16111
+ const uri = att.artifactLocation?.uri;
16112
+ if (typeof uri !== "string" || uri === "") continue;
16113
+ if (IMAGE_ARTIFACT_RE.test(uri)) {
16114
+ out.push({ uri, isImage: true });
16115
+ } else {
16116
+ out.push({ uri, content: readArtifactText(repoRoot, uri) });
16117
+ }
16118
+ }
16119
+ return out.length > 0 ? out : void 0;
16120
+ }
16121
+ function loadReviewNotesForFile(repoRoot, file2) {
16122
+ const safeRel = sanitizeRel(file2);
16123
+ const out = [];
16124
+ for (const path of listShardPaths(repoRoot, safeRel)) {
16125
+ let log;
16126
+ try {
16127
+ log = readLog(path);
16128
+ } catch {
16129
+ continue;
16130
+ }
16131
+ for (const run of log.runs) {
16132
+ const producer = run.tool.driver.name;
16133
+ for (const raw2 of run.results) {
16134
+ const r = raw2;
16135
+ const region = r.locations?.[0]?.physicalLocation?.region;
16136
+ const startLine = region?.startLine;
16137
+ const kind = r.properties?.tags?.[0];
16138
+ if (startLine === void 0 || kind === void 0 || !isNoteKind(kind)) continue;
16139
+ const confidence = r.properties?.[CONFIDENCE_PROPERTY_KEY];
16140
+ out.push({
16141
+ guid: r.guid,
16142
+ line: startLine,
16143
+ side: "new",
16144
+ kind,
16145
+ body: r.message?.text ?? "",
16146
+ confidence: typeof confidence === "number" ? confidence : void 0,
16147
+ producer: producer === "" ? void 0 : producer,
16148
+ snippet: region?.snippet?.text,
16149
+ artifacts: readArtifacts(repoRoot, r)
16150
+ });
16151
+ }
16152
+ }
16153
+ }
16154
+ return out;
16155
+ }
16156
+ var NOTES_SUBDIR, SHARD_RE, ARTIFACT_HASH_MAX_BYTES, LFS_FILTER_LINE, ARTIFACT_MAX_BYTES;
16157
+ var init_store = __esm({
16158
+ "src/review-notes/store.ts"() {
16159
+ "use strict";
16160
+ init_ids();
16161
+ init_sarif();
16162
+ init_types();
16163
+ init_view();
16164
+ NOTES_SUBDIR = join6(".pr-notes", "notes");
16165
+ SHARD_RE = /\.(\d{6})\.sarif$/;
16166
+ ARTIFACT_HASH_MAX_BYTES = 5e7;
16167
+ LFS_FILTER_LINE = ".pr-notes/artifacts/** filter=lfs diff=lfs merge=lfs -text";
16168
+ ARTIFACT_MAX_BYTES = 2e4;
16169
+ }
16170
+ });
16171
+
15722
16172
  // src/difftool/blob-store.ts
15723
16173
  var blob_store_exports = {};
15724
16174
  __export(blob_store_exports, {
@@ -15726,10 +16176,10 @@ __export(blob_store_exports, {
15726
16176
  readDifftoolBlob: () => readDifftoolBlob,
15727
16177
  writeDifftoolBlob: () => writeDifftoolBlob
15728
16178
  });
15729
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
15730
- import { join as join7 } from "path";
16179
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
16180
+ import { join as join9 } from "path";
15731
16181
  function blobDir(dataDir) {
15732
- return join7(dataDir, "difftool-blobs");
16182
+ return join9(dataDir, "difftool-blobs");
15733
16183
  }
15734
16184
  function blobName(fileId, side) {
15735
16185
  return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
@@ -15737,14 +16187,14 @@ function blobName(fileId, side) {
15737
16187
  function writeDifftoolBlob(dataDir, fileId, side, bytes) {
15738
16188
  if (bytes.length === 0) return;
15739
16189
  const dir = blobDir(dataDir);
15740
- mkdirSync5(dir, { recursive: true });
15741
- writeFileSync6(join7(dir, blobName(fileId, side)), bytes);
16190
+ mkdirSync6(dir, { recursive: true });
16191
+ writeFileSync7(join9(dir, blobName(fileId, side)), bytes);
15742
16192
  }
15743
16193
  function readDifftoolBlob(dataDir, fileId, side) {
15744
- const path = join7(blobDir(dataDir), blobName(fileId, side));
15745
- if (!existsSync6(path)) return null;
16194
+ const path = join9(blobDir(dataDir), blobName(fileId, side));
16195
+ if (!existsSync8(path)) return null;
15746
16196
  try {
15747
- return readFileSync6(path);
16197
+ return readFileSync7(path);
15748
16198
  } catch {
15749
16199
  return null;
15750
16200
  }
@@ -15850,9 +16300,9 @@ __export(difftool_exports, {
15850
16300
  registerDifftool: () => registerDifftool,
15851
16301
  unregisterDifftool: () => unregisterDifftool
15852
16302
  });
15853
- import { spawnSync as spawnSync9 } from "child_process";
16303
+ import { spawnSync as spawnSync10 } from "child_process";
15854
16304
  function git2(args, cwd) {
15855
- const r = spawnSync9("git", args, { encoding: "utf-8", cwd });
16305
+ const r = spawnSync10("git", args, { encoding: "utf-8", cwd });
15856
16306
  if (r.error !== void 0) {
15857
16307
  const code = r.error.code ?? "UNKNOWN";
15858
16308
  return { status: -1, stdout: "", stderr: `spawn git failed (${code}): ${r.error.message}` };
@@ -15918,6 +16368,340 @@ var init_difftool = __esm({
15918
16368
  }
15919
16369
  });
15920
16370
 
16371
+ // src/review-notes/instructions.ts
16372
+ function reviewNoteInstructions() {
16373
+ return [
16374
+ "# Emitting AI review notes (`.pr-notes/`)",
16375
+ "",
16376
+ "As you write or change code, leave **line-anchored review notes** that explain",
16377
+ "your reasoning and prove your work \u2014 the rationale a careful reviewer (human or",
16378
+ "the next AI session) would want, anchored to the exact lines it concerns. Notes",
16379
+ "are committed to the repo as tool-neutral SARIF under `.pr-notes/` and shown",
16380
+ "review-comment-style in Glassbox. Write them **while editing**, at the moment of",
16381
+ "richest context \u2014 not reconstructed afterward.",
16382
+ "",
16383
+ "## When to emit a note",
16384
+ "",
16385
+ 'Emit one when a future reader would otherwise have to ask "why?". Good triggers:',
16386
+ "",
16387
+ "- A **non-obvious decision** \u2014 why this approach over the obvious one.",
16388
+ "- **Proof** a change is correct \u2014 the reasoning, test output, or invariant that backs it.",
16389
+ "- An **assumption** you relied on that could later be wrong.",
16390
+ "- An **alternative you considered and rejected**, and why.",
16391
+ "- A **risk** or sharp edge a reviewer should scrutinize.",
16392
+ "- **Test evidence** demonstrating the change works.",
16393
+ "",
16394
+ "Do **not** narrate the obvious or restate what the diff already says. One note per",
16395
+ "genuine decision or claim \u2014 not per line.",
16396
+ "",
16397
+ "## How to emit a note",
16398
+ "",
16399
+ "Shell out to the `glassbox note` CLI (it owns the on-disk format, fingerprint,",
16400
+ "and commit provenance \u2014 you never write the SARIF by hand):",
16401
+ "",
16402
+ "```",
16403
+ "glassbox note add \\",
16404
+ " --file <repo-relative path> \\",
16405
+ " --lines <A[-B]> \\",
16406
+ ` --kind <${NOTE_KINDS.join("|")}> \\`,
16407
+ ' --body - \\ # body read from stdin (use --body "text" for short notes)',
16408
+ " [--confidence 0..1] [--rank 0..100] [--ticket <id|url>] \\",
16409
+ ' [--producer "<your tool/agent name>"] [--producer-version <v>]',
16410
+ "```",
16411
+ "",
16412
+ "Always set `--producer` so the note records who wrote it, and `--ticket` when the",
16413
+ "work traces to a tracked task. Keep the body focused: the claim and its support.",
16414
+ "",
16415
+ "## Revise and consolidate",
16416
+ "",
16417
+ "Notes should reflect the **final** state of your change, not an obsolete",
16418
+ "intermediate step:",
16419
+ "",
16420
+ "- `glassbox note add` prints a stable note id (guid).",
16421
+ "- `glassbox note update --id <guid> [--body -|--kind \u2026|--confidence \u2026|--rank \u2026|--ticket \u2026]`",
16422
+ " to correct a note as the work evolves.",
16423
+ "- `glassbox note remove --id <guid>` to drop one that no longer applies.",
16424
+ "- When the task is done, run `glassbox note coalesce` to drop redundant notes",
16425
+ " (identical anchor + kind + body, keeping the most recent).",
16426
+ "",
16427
+ "## Final consolidation pass",
16428
+ "",
16429
+ "After the mechanical `coalesce`, do one **cross-cutting pass over all the notes",
16430
+ "you wrote this session** \u2014 the relationships that only become visible once the",
16431
+ "whole change is in front of you, not while editing a single file. `coalesce`",
16432
+ "only catches byte-identical duplicates; this pass is the judgment it cannot make:",
16433
+ "",
16434
+ "- **Merge near-duplicates.** Two notes that make the *same* point in different",
16435
+ " words (e.g. the same rationale restated on two files) aren't caught by",
16436
+ " `coalesce`. Keep the clearest one \u2014 `update` it to the best wording, widen its",
16437
+ " anchor or `--ticket` if helpful \u2014 and `remove` the rest.",
16438
+ "- **Link related notes across files.** When several notes are facets of one",
16439
+ " decision spanning multiple files, make that explicit: name the related file(s)",
16440
+ ' and the connecting idea in each note body (a short "see also `path/to/file`"',
16441
+ " line), and give them a shared `--ticket` so a reader can pivot between them.",
16442
+ " Bodies render as Markdown, so an inline `` `path` `` reference reads cleanly.",
16443
+ "- **Prune what the finished change made obvious.** A note that justified an",
16444
+ " intermediate step the final diff no longer shows is noise \u2014 `remove` it.",
16445
+ "",
16446
+ "The goal is the smallest set of notes that still proves the work: no restated",
16447
+ "point twice, and every cross-file relationship spelled out where a reviewer will",
16448
+ "see it.",
16449
+ "",
16450
+ "If your tool can't shell out, write the SARIF directly under `.pr-notes/` per the",
16451
+ "format in `docs/20-ai-review-notes.md` \u2014 but prefer the CLI."
16452
+ ].join("\n");
16453
+ }
16454
+ var init_instructions = __esm({
16455
+ "src/review-notes/instructions.ts"() {
16456
+ "use strict";
16457
+ init_types();
16458
+ }
16459
+ });
16460
+
16461
+ // src/review-notes/cli.ts
16462
+ var cli_exports = {};
16463
+ __export(cli_exports, {
16464
+ parseNoteAdd: () => parseNoteAdd,
16465
+ runNoteCli: () => runNoteCli
16466
+ });
16467
+ import { spawnSync as spawnSync11 } from "child_process";
16468
+ import { relative as relative2, resolve as resolve9 } from "path";
16469
+ function parseFlags(args) {
16470
+ const flags = /* @__PURE__ */ new Map();
16471
+ for (let i = 0; i < args.length; i++) {
16472
+ const a = args[i];
16473
+ if (!a.startsWith("--")) throw new Error(`unexpected argument: ${a}`);
16474
+ const key = a.slice(2);
16475
+ const next = args.at(i + 1);
16476
+ if (next === void 0 || next.startsWith("--")) throw new Error(`missing value for --${key}`);
16477
+ flags.set(key, next);
16478
+ i++;
16479
+ }
16480
+ return flags;
16481
+ }
16482
+ function collectRepeatable(args, name) {
16483
+ const out = [];
16484
+ for (let i = 0; i < args.length; i++) {
16485
+ if (args[i] === `--${name}`) {
16486
+ const v = args.at(i + 1);
16487
+ if (v !== void 0 && !v.startsWith("--")) out.push(v);
16488
+ }
16489
+ }
16490
+ return out;
16491
+ }
16492
+ function noteUsage() {
16493
+ return `glassbox note \u2014 AI-authored, line-anchored review notes (docs/20)
16494
+
16495
+ Usage:
16496
+ glassbox note add --file <path> --lines <A[-B]> --kind <kind> --body <text|-> [options]
16497
+ glassbox note update --id <guid> [--file <path>] [--body <text|->] [--kind <kind>] [--confidence <0..1>] [--rank <0..100>] [--ticket <id>]
16498
+ glassbox note remove --id <guid> [--file <path>]
16499
+ glassbox note coalesce [--file <path>]
16500
+ glassbox note instructions Print the inbound AI-instructions contract (for orchestrators to inject)
16501
+
16502
+ add \u2014 required:
16503
+ --file <path> Source file the note anchors to (relative to cwd or absolute)
16504
+ --lines <A[-B]> 1-based line or line range (e.g. 42 or 42-50)
16505
+ --kind <kind> One of: ${NOTE_KINDS.join(", ")}
16506
+ --body <text|-> Markdown body; pass - to read the body from stdin
16507
+
16508
+ add \u2014 options:
16509
+ --confidence <0..1> Author confidence
16510
+ --rank <0..100> Importance
16511
+ --ticket <id|url> Linked ticket
16512
+ --producer <name> Producing tool/agent (e.g. "Claude Code", "Hot Sheet")
16513
+ --producer-version <v>
16514
+ --artifact <path> Attach a committed proof artifact (test output, log,
16515
+ diagram source); repeatable, repo-relative path
16516
+
16517
+ update/remove use the guid returned by 'add' (--file scopes the search; omit to search all notes).
16518
+ coalesce drops redundant notes (identical anchor + kind + body), keeping the most recent.
16519
+ `;
16520
+ }
16521
+ function parseInteger(label, value) {
16522
+ const n = Number(value);
16523
+ if (!Number.isInteger(n)) throw new Error(`${label} must be an integer, got "${value}"`);
16524
+ return n;
16525
+ }
16526
+ function parseNoteAdd(args) {
16527
+ const flags = parseFlags(args);
16528
+ const file2 = flags.get("file");
16529
+ const lines = flags.get("lines");
16530
+ const kindRaw = flags.get("kind");
16531
+ if (file2 === void 0) throw new Error("--file is required");
16532
+ if (lines === void 0) throw new Error("--lines is required");
16533
+ if (kindRaw === void 0) throw new Error("--kind is required");
16534
+ if (!isNoteKind(kindRaw)) throw new Error(`--kind must be one of: ${NOTE_KINDS.join(", ")}`);
16535
+ const lineMatch = /^(\d+)(?:-(\d+))?$/.exec(lines);
16536
+ if (lineMatch === null) throw new Error(`--lines must be A or A-B (e.g. 42 or 42-50), got "${lines}"`);
16537
+ const startLine = parseInteger("--lines start", lineMatch[1]);
16538
+ const endRaw = lineMatch.at(2);
16539
+ const endLine = endRaw !== void 0 ? parseInteger("--lines end", endRaw) : startLine;
16540
+ if (startLine < 1 || endLine < startLine) throw new Error("--lines must be 1-based with start <= end");
16541
+ const parsed = { file: file2, startLine, endLine, kind: kindRaw, artifacts: collectRepeatable(args, "artifact"), bodyStdin: false };
16542
+ const confidence = flags.get("confidence");
16543
+ if (confidence !== void 0) {
16544
+ const c = Number(confidence);
16545
+ if (Number.isNaN(c) || c < 0 || c > 1) throw new Error("--confidence must be between 0 and 1");
16546
+ parsed.confidence = c;
16547
+ }
16548
+ const rank = flags.get("rank");
16549
+ if (rank !== void 0) {
16550
+ const r = Number(rank);
16551
+ if (!Number.isInteger(r) || r < 0 || r > 100) throw new Error("--rank must be an integer 0..100");
16552
+ parsed.rank = r;
16553
+ }
16554
+ if (flags.has("ticket")) parsed.ticket = flags.get("ticket");
16555
+ if (flags.has("producer")) parsed.producer = flags.get("producer");
16556
+ if (flags.has("producer-version")) parsed.producerVersion = flags.get("producer-version");
16557
+ const body = flags.get("body");
16558
+ if (body === void 0) throw new Error("--body is required (text, or - to read stdin)");
16559
+ if (body === "-") parsed.bodyStdin = true;
16560
+ else parsed.body = body;
16561
+ return parsed;
16562
+ }
16563
+ function readStdin() {
16564
+ return new Promise((res, rej) => {
16565
+ let data = "";
16566
+ process.stdin.setEncoding("utf-8");
16567
+ process.stdin.on("data", (chunk) => {
16568
+ data += String(chunk);
16569
+ });
16570
+ process.stdin.on("end", () => {
16571
+ res(data);
16572
+ });
16573
+ process.stdin.on("error", rej);
16574
+ });
16575
+ }
16576
+ function findRepoRoot(cwd) {
16577
+ try {
16578
+ const res = spawnSync11("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" });
16579
+ if (res.status === 0) {
16580
+ const top = res.stdout.trim();
16581
+ if (top !== "") return top;
16582
+ }
16583
+ } catch {
16584
+ }
16585
+ return cwd;
16586
+ }
16587
+ function toRepoRelative(repoRoot, cwd, file2) {
16588
+ const abs = resolve9(cwd, file2);
16589
+ const rel = relative2(repoRoot, abs).replace(/\\/g, "/");
16590
+ if (rel === "" || rel.startsWith("../")) {
16591
+ throw new Error(`--file must be inside the repository: ${file2}`);
16592
+ }
16593
+ return rel;
16594
+ }
16595
+ async function runAdd(args, cwd) {
16596
+ const parsed = parseNoteAdd(args);
16597
+ const body = parsed.bodyStdin ? await readStdin() : parsed.body;
16598
+ if (body === void 0 || body.trim() === "") throw new Error("note body is empty");
16599
+ const repoRoot = findRepoRoot(cwd);
16600
+ const input = {
16601
+ file: toRepoRelative(repoRoot, cwd, parsed.file),
16602
+ startLine: parsed.startLine,
16603
+ endLine: parsed.endLine,
16604
+ body,
16605
+ kind: parsed.kind,
16606
+ confidence: parsed.confidence,
16607
+ rank: parsed.rank,
16608
+ ticket: parsed.ticket,
16609
+ producer: parsed.producer,
16610
+ producerVersion: parsed.producerVersion,
16611
+ artifacts: parsed.artifacts.length > 0 ? parsed.artifacts : void 0
16612
+ };
16613
+ warnIfPrNotesIgnored(repoRoot);
16614
+ const { path, guid: guid3 } = writeReviewNote(repoRoot, input);
16615
+ console.log(`Wrote review note ${guid3} -> ${relative2(repoRoot, path)}`);
16616
+ }
16617
+ function scopedFile(flags, repoRoot, cwd) {
16618
+ const file2 = flags.get("file");
16619
+ return file2 === void 0 ? void 0 : toRepoRelative(repoRoot, cwd, file2);
16620
+ }
16621
+ function runRemove(args, cwd) {
16622
+ const flags = parseFlags(args);
16623
+ const id = flags.get("id");
16624
+ if (id === void 0) throw new Error("--id <guid> is required");
16625
+ const repoRoot = findRepoRoot(cwd);
16626
+ const removed = removeNote(repoRoot, id, scopedFile(flags, repoRoot, cwd));
16627
+ if (!removed) throw new Error(`no review note found with id ${id}`);
16628
+ console.log(`Removed review note ${id}`);
16629
+ }
16630
+ async function runUpdate(args, cwd) {
16631
+ const flags = parseFlags(args);
16632
+ const id = flags.get("id");
16633
+ if (id === void 0) throw new Error("--id <guid> is required");
16634
+ const patch = {};
16635
+ const bodyFlag = flags.get("body");
16636
+ if (bodyFlag !== void 0) patch.body = bodyFlag === "-" ? await readStdin() : bodyFlag;
16637
+ const kind = flags.get("kind");
16638
+ if (kind !== void 0) {
16639
+ if (!isNoteKind(kind)) throw new Error(`--kind must be one of: ${NOTE_KINDS.join(", ")}`);
16640
+ patch.kind = kind;
16641
+ }
16642
+ const confidence = flags.get("confidence");
16643
+ if (confidence !== void 0) {
16644
+ const c = Number(confidence);
16645
+ if (Number.isNaN(c) || c < 0 || c > 1) throw new Error("--confidence must be between 0 and 1");
16646
+ patch.confidence = c;
16647
+ }
16648
+ const rank = flags.get("rank");
16649
+ if (rank !== void 0) {
16650
+ const r = Number(rank);
16651
+ if (!Number.isInteger(r) || r < 0 || r > 100) throw new Error("--rank must be an integer 0..100");
16652
+ patch.rank = r;
16653
+ }
16654
+ if (flags.has("ticket")) patch.ticket = flags.get("ticket");
16655
+ if (Object.keys(patch).length === 0) throw new Error("nothing to update \u2014 pass at least one of --body/--kind/--confidence/--rank/--ticket");
16656
+ const repoRoot = findRepoRoot(cwd);
16657
+ const updated = updateNote(repoRoot, id, patch, scopedFile(flags, repoRoot, cwd));
16658
+ if (!updated) throw new Error(`no review note found with id ${id}`);
16659
+ console.log(`Updated review note ${id}`);
16660
+ }
16661
+ function runCoalesce(args, cwd) {
16662
+ const flags = parseFlags(args);
16663
+ const repoRoot = findRepoRoot(cwd);
16664
+ const file2 = flags.get("file");
16665
+ const removed = file2 !== void 0 ? coalesceFile(repoRoot, toRepoRelative(repoRoot, cwd, file2)) : coalesceAll(repoRoot);
16666
+ console.log(`Coalesced review notes \u2014 removed ${String(removed)} redundant note(s)`);
16667
+ }
16668
+ async function runNoteCli(args, ctx = {}) {
16669
+ const cwd = ctx.cwd ?? process.cwd();
16670
+ const sub = args.at(0);
16671
+ if (sub === void 0 || sub === "help" || sub === "--help" || sub === "-h") {
16672
+ console.log(noteUsage());
16673
+ return;
16674
+ }
16675
+ const rest = args.slice(1);
16676
+ switch (sub) {
16677
+ case "add":
16678
+ await runAdd(rest, cwd);
16679
+ return;
16680
+ case "remove":
16681
+ runRemove(rest, cwd);
16682
+ return;
16683
+ case "update":
16684
+ await runUpdate(rest, cwd);
16685
+ return;
16686
+ case "coalesce":
16687
+ runCoalesce(rest, cwd);
16688
+ return;
16689
+ case "instructions":
16690
+ console.log(reviewNoteInstructions());
16691
+ return;
16692
+ default:
16693
+ throw new Error(`unknown 'note' subcommand: ${sub} (expected add/update/remove/coalesce/instructions)`);
16694
+ }
16695
+ }
16696
+ var init_cli = __esm({
16697
+ "src/review-notes/cli.ts"() {
16698
+ "use strict";
16699
+ init_instructions();
16700
+ init_store();
16701
+ init_types();
16702
+ }
16703
+ });
16704
+
15921
16705
  // src/git/difftool-discovery.ts
15922
16706
  var difftool_discovery_exports = {};
15923
16707
  __export(difftool_discovery_exports, {
@@ -15931,22 +16715,22 @@ __export(difftool_discovery_exports, {
15931
16715
  tryAcquireStartingLock: () => tryAcquireStartingLock,
15932
16716
  writeDiscovery: () => writeDiscovery
15933
16717
  });
15934
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync16, rmSync as rmSync5, statSync as statSync2, writeFileSync as writeFileSync11 } from "fs";
16718
+ import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync18, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
15935
16719
  import { homedir as homedir4 } from "os";
15936
- import { join as join16 } from "path";
16720
+ import { join as join18 } from "path";
15937
16721
  function difftoolHome() {
15938
- return join16(homedir4(), ".glassbox");
16722
+ return join18(homedir4(), ".glassbox");
15939
16723
  }
15940
16724
  function discoveryPath(home = difftoolHome()) {
15941
- return join16(home, "difftool.lock");
16725
+ return join18(home, "difftool.lock");
15942
16726
  }
15943
16727
  function startingLockPath(home = difftoolHome()) {
15944
- return join16(home, "difftool-starting.lock");
16728
+ return join18(home, "difftool-starting.lock");
15945
16729
  }
15946
- function parseDiscovery(raw) {
16730
+ function parseDiscovery(raw2) {
15947
16731
  let parsed;
15948
16732
  try {
15949
- parsed = JSON.parse(raw);
16733
+ parsed = JSON.parse(raw2);
15950
16734
  } catch {
15951
16735
  return null;
15952
16736
  }
@@ -15955,16 +16739,16 @@ function parseDiscovery(raw) {
15955
16739
  }
15956
16740
  function readDiscovery(home = difftoolHome()) {
15957
16741
  const path = discoveryPath(home);
15958
- if (!existsSync13(path)) return null;
16742
+ if (!existsSync15(path)) return null;
15959
16743
  try {
15960
- return parseDiscovery(readFileSync16(path, "utf-8"));
16744
+ return parseDiscovery(readFileSync18(path, "utf-8"));
15961
16745
  } catch {
15962
16746
  return null;
15963
16747
  }
15964
16748
  }
15965
16749
  function writeDiscovery(port, home = difftoolHome()) {
15966
- mkdirSync11(home, { recursive: true });
15967
- writeFileSync11(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
16750
+ mkdirSync12(home, { recursive: true });
16751
+ writeFileSync12(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
15968
16752
  }
15969
16753
  function clearDiscovery(home = difftoolHome()) {
15970
16754
  try {
@@ -15973,17 +16757,17 @@ function clearDiscovery(home = difftoolHome()) {
15973
16757
  }
15974
16758
  }
15975
16759
  function tryAcquireStartingLock(home = difftoolHome()) {
15976
- mkdirSync11(home, { recursive: true });
16760
+ mkdirSync12(home, { recursive: true });
15977
16761
  const path = startingLockPath(home);
15978
16762
  try {
15979
- writeFileSync11(path, String(process.pid), { flag: "wx" });
16763
+ writeFileSync12(path, String(process.pid), { flag: "wx" });
15980
16764
  return true;
15981
16765
  } catch {
15982
16766
  try {
15983
- const ageMs = Date.now() - statSync2(path).mtimeMs;
16767
+ const ageMs = Date.now() - statSync4(path).mtimeMs;
15984
16768
  if (ageMs > STARTING_LOCK_STALE_MS) {
15985
16769
  rmSync5(path, { force: true });
15986
- writeFileSync11(path, String(process.pid), { flag: "wx" });
16770
+ writeFileSync12(path, String(process.pid), { flag: "wx" });
15987
16771
  return true;
15988
16772
  }
15989
16773
  } catch {
@@ -16013,9 +16797,9 @@ var init_difftool_discovery = __esm({
16013
16797
  // src/cli.ts
16014
16798
  init_connection();
16015
16799
  init_queries();
16016
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, realpathSync, statSync as statSync3 } from "fs";
16800
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, realpathSync, statSync as statSync5 } from "fs";
16017
16801
  import { tmpdir as tmpdir2 } from "os";
16018
- import { basename as basename2, join as join17, resolve as resolve8 } from "path";
16802
+ import { basename as basename2, join as join19, resolve as resolve10 } from "path";
16019
16803
 
16020
16804
  // src/debug.ts
16021
16805
  var debugEnabled = false;
@@ -16054,13 +16838,18 @@ import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync, writeFile
16054
16838
  import { homedir } from "os";
16055
16839
  import { join as join2 } from "path";
16056
16840
  var GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
16057
- var GLOBAL_CONFIG_DIR = join2(homedir(), ".glassbox");
16841
+ function resolveGlobalConfigDir() {
16842
+ const override = process.env.GLASSBOX_CONFIG_DIR;
16843
+ if (override !== void 0 && override.trim() !== "") return override;
16844
+ return join2(homedir(), ".glassbox");
16845
+ }
16846
+ var GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
16058
16847
  var GLOBAL_CONFIG_PATH = join2(GLOBAL_CONFIG_DIR, "config.json");
16059
16848
  function readGlobalConfig() {
16060
16849
  try {
16061
16850
  if (existsSync(GLOBAL_CONFIG_PATH)) {
16062
- const raw = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
16063
- const parsed = GlobalConfigSchema.safeParse(raw);
16851
+ const raw2 = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
16852
+ const parsed = GlobalConfigSchema.safeParse(raw2);
16064
16853
  if (parsed.success) return parsed.data;
16065
16854
  }
16066
16855
  } catch {
@@ -16187,12 +16976,17 @@ var AIModelSchema = external_exports.object({
16187
16976
  contextWindow: external_exports.number(),
16188
16977
  isDefault: external_exports.boolean()
16189
16978
  });
16190
- var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google"]);
16979
+ var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google", "local", "apple"]);
16980
+ var KEYLESS_PLATFORMS = /* @__PURE__ */ new Set(["local", "apple"]);
16191
16981
  var PLATFORMS = {
16192
16982
  anthropic: "Anthropic",
16193
16983
  openai: "OpenAI",
16194
- google: "Google"
16984
+ google: "Google",
16985
+ local: "Local",
16986
+ apple: "Apple"
16195
16987
  };
16988
+ var APPLE_ON_DEVICE_MODEL_ID = "apple-on-device";
16989
+ var APPLE_FM_ANALYSIS_ENABLED = true;
16196
16990
  var MODELS = {
16197
16991
  anthropic: [
16198
16992
  { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 1e6, isDefault: false },
@@ -16206,12 +17000,28 @@ var MODELS = {
16206
17000
  google: [
16207
17001
  { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1e6, isDefault: true },
16208
17002
  { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1e6, isDefault: false }
17003
+ ],
17004
+ // Local models are server-specific and discovered live from the configured
17005
+ // endpoint; this is only the fallback default when discovery is unavailable.
17006
+ local: [
17007
+ { id: "llama3.1", name: "Llama 3.1", contextWindow: 8192, isDefault: true }
17008
+ ],
17009
+ // On-device Apple Foundation Models — a single fixed entry (no discovery).
17010
+ // The on-device model has a small context window, so batch conservatively.
17011
+ apple: [
17012
+ { id: APPLE_ON_DEVICE_MODEL_ID, name: "Apple On-Device", contextWindow: 4096, isDefault: true }
16209
17013
  ]
16210
17014
  };
16211
17015
  var ENV_KEY_NAMES = {
16212
17016
  anthropic: "ANTHROPIC_API_KEY",
16213
17017
  openai: "OPENAI_API_KEY",
16214
- google: "GEMINI_API_KEY"
17018
+ google: "GEMINI_API_KEY",
17019
+ // Optional — most local servers (Ollama) need no key; some do.
17020
+ local: "GLASSBOX_LOCAL_API_KEY",
17021
+ // Apple Foundation Models are keyless (on-device). This name is never read
17022
+ // for auth — `apple` is in KEYLESS_PLATFORMS — but the record requires an
17023
+ // entry per platform.
17024
+ apple: "GLASSBOX_APPLE_API_KEY"
16215
17025
  };
16216
17026
  function getDefaultModel(platform) {
16217
17027
  const models = MODELS[platform];
@@ -16220,7 +17030,10 @@ function getDefaultModel(platform) {
16220
17030
  }
16221
17031
  function getModelContextWindow(platform, modelId) {
16222
17032
  const model = MODELS[platform].find((m) => m.id === modelId);
16223
- return model ? model.contextWindow : 128e3;
17033
+ if (model) return model.contextWindow;
17034
+ if (platform === "local") return 8192;
17035
+ if (platform === "apple") return 4096;
17036
+ return 128e3;
16224
17037
  }
16225
17038
  function modelFamily(id) {
16226
17039
  const lower = id.toLowerCase();
@@ -16273,8 +17086,8 @@ function saveAPIKey(platform, key, storage) {
16273
17086
  if (storage === "keychain") {
16274
17087
  saveKeyToKeychain(platform, key);
16275
17088
  } else {
16276
- updateGlobalConfig((raw) => {
16277
- const parsed = ConfigFileSchema.safeParse(raw);
17089
+ updateGlobalConfig((raw2) => {
17090
+ const parsed = ConfigFileSchema.safeParse(raw2);
16278
17091
  const cfg = parsed.success ? parsed.data : {};
16279
17092
  cfg.ai ??= {};
16280
17093
  cfg.ai.keys ??= {};
@@ -16298,8 +17111,8 @@ function deleteAPIKey(platform) {
16298
17111
  } catch {
16299
17112
  }
16300
17113
  if (readConfigFile().ai?.keys === void 0) return;
16301
- updateGlobalConfig((raw) => {
16302
- const parsed = ConfigFileSchema.safeParse(raw);
17114
+ updateGlobalConfig((raw2) => {
17115
+ const parsed = ConfigFileSchema.safeParse(raw2);
16303
17116
  const cfg = parsed.success ? parsed.data : {};
16304
17117
  if (cfg.ai?.keys !== void 0) {
16305
17118
  cfg.ai.keys[platform] = "";
@@ -16320,11 +17133,21 @@ function detectAvailablePlatforms() {
16320
17133
  }
16321
17134
 
16322
17135
  // src/ai/config.ts
17136
+ var DEFAULT_LOCAL_ENDPOINT = "http://localhost:11434/v1";
17137
+ function resolveLocalEndpoint() {
17138
+ const configured = readConfigFile().ai?.localEndpoint?.trim();
17139
+ const base = configured !== void 0 && configured !== "" ? configured : DEFAULT_LOCAL_ENDPOINT;
17140
+ return base.replace(/\/+$/, "");
17141
+ }
16323
17142
  var ConfigFileSchema = external_exports.object({
16324
17143
  ai: external_exports.object({
16325
17144
  platform: external_exports.string().optional(),
16326
17145
  model: external_exports.string().optional(),
16327
- keys: external_exports.record(external_exports.string(), external_exports.string()).optional()
17146
+ keys: external_exports.record(external_exports.string(), external_exports.string()).optional(),
17147
+ localEndpoint: external_exports.string().optional(),
17148
+ // Secondary model used when the primary (Apple FM) fails a batch.
17149
+ fallbackPlatform: external_exports.string().optional(),
17150
+ fallbackModel: external_exports.string().optional()
16328
17151
  }).optional(),
16329
17152
  guidedReview: external_exports.object({
16330
17153
  enabled: external_exports.boolean().optional(),
@@ -16332,25 +17155,60 @@ var ConfigFileSchema = external_exports.object({
16332
17155
  }).optional()
16333
17156
  }).loose();
16334
17157
  function readConfigFile() {
16335
- const raw = readGlobalConfig();
16336
- const parsed = ConfigFileSchema.safeParse(raw);
17158
+ const raw2 = readGlobalConfig();
17159
+ const parsed = ConfigFileSchema.safeParse(raw2);
16337
17160
  return parsed.success ? parsed.data : {};
16338
17161
  }
17162
+ function resolvePlatformConfig(platform, rawModelOrUndefined) {
17163
+ const rawModel = rawModelOrUndefined ?? getDefaultModel(platform);
17164
+ const model = KEYLESS_PLATFORMS.has(platform) ? rawModel : resolveModelId(platform, rawModel);
17165
+ const { key, source } = resolveAPIKey(platform);
17166
+ const baseUrl = platform === "local" ? resolveLocalEndpoint() : void 0;
17167
+ return { platform, model, apiKey: key, keySource: source, baseUrl };
17168
+ }
16339
17169
  function loadAIConfig() {
16340
17170
  const config2 = readConfigFile();
16341
17171
  const platformRaw = config2.ai?.platform ?? "anthropic";
16342
- const platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
16343
- const model = resolveModelId(platform, config2.ai?.model ?? getDefaultModel(platform));
16344
- const { key, source } = resolveAPIKey(platform);
16345
- return { platform, model, apiKey: key, keySource: source };
16346
- }
16347
- function saveAIConfigPreferences(platform, model) {
17172
+ let platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
17173
+ if (platform === "apple" && !APPLE_FM_ANALYSIS_ENABLED) platform = "anthropic";
17174
+ const primary = resolvePlatformConfig(platform, config2.ai?.model);
17175
+ if (platform === "apple") {
17176
+ const fallback = resolveFallbackConfig(config2);
17177
+ if (fallback !== null) primary.fallback = fallback;
17178
+ }
17179
+ return primary;
17180
+ }
17181
+ function fallbackSelectionFrom(config2) {
17182
+ const raw2 = config2.ai?.fallbackPlatform;
17183
+ if (raw2 === void 0 || raw2 === "") return null;
17184
+ const parsed = AIPlatformSchema.safeParse(raw2);
17185
+ if (!parsed.success || parsed.data === "apple") return null;
17186
+ return { platform: parsed.data, model: config2.ai?.fallbackModel ?? getDefaultModel(parsed.data) };
17187
+ }
17188
+ function loadFallbackSelection() {
17189
+ return fallbackSelectionFrom(readConfigFile());
17190
+ }
17191
+ function resolveFallbackConfig(config2) {
17192
+ const sel = fallbackSelectionFrom(config2);
17193
+ return sel === null ? null : resolvePlatformConfig(sel.platform, sel.model);
17194
+ }
17195
+ function saveAIConfigPreferences(platform, model, opts = {}) {
16348
17196
  updateGlobalConfig((config2) => {
16349
17197
  const parsed = ConfigFileSchema.safeParse(config2);
16350
17198
  const cfg = parsed.success ? parsed.data : {};
16351
17199
  cfg.ai ??= {};
16352
17200
  cfg.ai.platform = platform;
16353
17201
  cfg.ai.model = model;
17202
+ if (opts.localEndpoint !== void 0) {
17203
+ const trimmed = opts.localEndpoint.trim();
17204
+ cfg.ai.localEndpoint = trimmed === "" ? void 0 : trimmed;
17205
+ }
17206
+ if (opts.fallbackPlatform !== void 0) {
17207
+ const fp = opts.fallbackPlatform.trim();
17208
+ const fm = opts.fallbackModel?.trim();
17209
+ cfg.ai.fallbackPlatform = fp === "" ? void 0 : fp;
17210
+ cfg.ai.fallbackModel = fp === "" || fm === void 0 || fm === "" ? void 0 : fm;
17211
+ }
16354
17212
  return cfg;
16355
17213
  });
16356
17214
  }
@@ -16495,14 +17353,14 @@ async function getUserPreferences() {
16495
17353
  last_image_mode: "metadata"
16496
17354
  };
16497
17355
  if (result.rows.length === 0) return defaults;
16498
- const raw = result.rows[0];
17356
+ const raw2 = result.rows[0];
16499
17357
  const merged = {
16500
- sort_mode: typeof raw.sort_mode === "string" ? raw.sort_mode : defaults.sort_mode,
16501
- risk_sort_dimension: typeof raw.risk_sort_dimension === "string" ? raw.risk_sort_dimension : defaults.risk_sort_dimension,
16502
- show_risk_scores: typeof raw.show_risk_scores === "boolean" ? raw.show_risk_scores : defaults.show_risk_scores,
16503
- ignore_whitespace: typeof raw.ignore_whitespace === "boolean" ? raw.ignore_whitespace : defaults.ignore_whitespace,
16504
- svg_view_mode: typeof raw.svg_view_mode === "string" ? raw.svg_view_mode : defaults.svg_view_mode,
16505
- last_image_mode: typeof raw.last_image_mode === "string" ? raw.last_image_mode : defaults.last_image_mode
17358
+ sort_mode: typeof raw2.sort_mode === "string" ? raw2.sort_mode : defaults.sort_mode,
17359
+ risk_sort_dimension: typeof raw2.risk_sort_dimension === "string" ? raw2.risk_sort_dimension : defaults.risk_sort_dimension,
17360
+ show_risk_scores: typeof raw2.show_risk_scores === "boolean" ? raw2.show_risk_scores : defaults.show_risk_scores,
17361
+ ignore_whitespace: typeof raw2.ignore_whitespace === "boolean" ? raw2.ignore_whitespace : defaults.ignore_whitespace,
17362
+ svg_view_mode: typeof raw2.svg_view_mode === "string" ? raw2.svg_view_mode : defaults.svg_view_mode,
17363
+ last_image_mode: typeof raw2.last_image_mode === "string" ? raw2.last_image_mode : defaults.last_image_mode
16506
17364
  };
16507
17365
  return UserPreferencesSchema.parse(merged);
16508
17366
  }
@@ -16526,6 +17384,17 @@ async function saveUserPreferences(prefs) {
16526
17384
 
16527
17385
  // src/demo.ts
16528
17386
  init_queries();
17387
+ function demoReviewNotes(filePath) {
17388
+ if (filePath !== "src/auth/session.ts") return [];
17389
+ return [
17390
+ { 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" },
17391
+ { 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: [{ 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" }] },
17392
+ { guid: "demo-note-risk", line: 31, side: "new", kind: "risk", body: "expiresAt round-trips through JSON as a string and is re-wrapped in Date() \u2014 verify the comparison holds in your runtime.", confidence: 0.6, producer: "Claude Code", artifacts: [{ uri: "assets/demo-annotations.png", isImage: true }] },
17393
+ // Re-anchoring showcase (P3): authored against a 16-byte id, but the code
17394
+ // now uses 32 bytes, so the note no longer matches and renders as stale.
17395
+ { guid: "demo-note-stale", line: 15, side: "new", kind: "assumption", body: "Assumed a 16-byte token id here \u2014 the implementation has since changed, so this note is out of date.", producer: "Claude Code", snippet: " const id = randomBytes(16).toString('hex');" }
17396
+ ];
17397
+ }
16529
17398
  var DEMO_SCENARIOS = [
16530
17399
  { id: 1, label: "Main UI with guided review notes" },
16531
17400
  { id: 2, label: "Risk mode with inline risk notes" },
@@ -16922,6 +17791,9 @@ var NARRATIVE_ORDER = [
16922
17791
  { path: "tests/auth.test.ts", position: 7, rationale: "Tests \u2014 read last to verify the changes work correctly.", notes: { overview: "Tests for the session management changes. Read these last to confirm the new async API works as expected.", lines: [] } }
16923
17792
  ];
16924
17793
  var ANNOTATIONS = [
17794
+ // A reviewer reply to the line-31 risk review note (doc 20 threading) —
17795
+ // renders nested beneath that note.
17796
+ { filePath: "src/auth/session.ts", line: 31, side: "new", category: "note", content: "Confirmed \u2014 the JSON value is an ISO string, so new Date() parses it and the comparison holds.", replyToNoteId: "demo-note-risk" },
16925
17797
  { filePath: "src/auth/session.ts", line: 23, side: "new", category: "bug", content: "Redis key should be sanitized \u2014 if a session ID contains a colon, it will conflict with the key namespace." },
16926
17798
  { filePath: "src/auth/session.ts", line: 30, side: "new", category: "fix", content: "Wrap JSON.parse in try/catch to handle corrupted Redis data gracefully instead of crashing." },
16927
17799
  { filePath: "src/auth/session.ts", line: 12, side: "new", category: "pattern-follow", content: "Good use of a named constant instead of a magic number. This makes the TTL self-documenting." },
@@ -17034,7 +17906,7 @@ async function setupAnnotations(fileIdMap) {
17034
17906
  for (const ann of ANNOTATIONS) {
17035
17907
  const fileId = fileIdMap.get(ann.filePath);
17036
17908
  if (fileId !== void 0) {
17037
- await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content);
17909
+ await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content, ann.replyToNoteId);
17038
17910
  }
17039
17911
  }
17040
17912
  }
@@ -17120,11 +17992,11 @@ function emptyFileDiff(filePath = "") {
17120
17992
  isBinary: false
17121
17993
  };
17122
17994
  }
17123
- function parseDiffData(raw) {
17124
- if (raw === null || raw === void 0 || raw === "") return null;
17995
+ function parseDiffData(raw2) {
17996
+ if (raw2 === null || raw2 === void 0 || raw2 === "") return null;
17125
17997
  let parsed;
17126
17998
  try {
17127
- parsed = JSON.parse(raw);
17999
+ parsed = JSON.parse(raw2);
17128
18000
  } catch {
17129
18001
  return null;
17130
18002
  }
@@ -17286,9 +18158,9 @@ function createNewFileDiff(filePath, repoRoot) {
17286
18158
  isBinary: false
17287
18159
  };
17288
18160
  }
17289
- function parseDiff(raw) {
18161
+ function parseDiff(raw2) {
17290
18162
  const files = [];
17291
- const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
18163
+ const fileChunks = raw2.split(/^diff --git /m).filter(Boolean);
17292
18164
  for (const chunk of fileChunks) {
17293
18165
  const headerEnd = chunk.indexOf("@@");
17294
18166
  const header = headerEnd === -1 ? chunk : chunk.slice(0, headerEnd);
@@ -17323,12 +18195,12 @@ function parseDiff(raw) {
17323
18195
  }
17324
18196
  return files;
17325
18197
  }
17326
- function parseHunks(raw) {
18198
+ function parseHunks(raw2) {
17327
18199
  const hunks = [];
17328
18200
  const hunkRegex = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)/gm;
17329
18201
  let match;
17330
18202
  const hunkStarts = [];
17331
- while ((match = hunkRegex.exec(raw)) !== null) {
18203
+ while ((match = hunkRegex.exec(raw2)) !== null) {
17332
18204
  const groups = match;
17333
18205
  hunkStarts.push({
17334
18206
  index: match.index + match[0].length,
@@ -17340,8 +18212,8 @@ function parseHunks(raw) {
17340
18212
  }
17341
18213
  for (let i = 0; i < hunkStarts.length; i++) {
17342
18214
  const start = hunkStarts[i];
17343
- const end = i + 1 < hunkStarts.length ? raw.lastIndexOf("\n@@", hunkStarts[i + 1].index) : raw.length;
17344
- const body = raw.slice(start.index, end);
18215
+ const end = i + 1 < hunkStarts.length ? raw2.lastIndexOf("\n@@", hunkStarts[i + 1].index) : raw2.length;
18216
+ const body = raw2.slice(start.index, end);
17345
18217
  const lines = [];
17346
18218
  let oldNum = start.oldStart;
17347
18219
  let newNum = start.newStart;
@@ -17494,8 +18366,8 @@ function acquireLock(dataDir) {
17494
18366
  lockPath = join4(dataDir, "glassbox.lock");
17495
18367
  if (existsSync3(lockPath)) {
17496
18368
  try {
17497
- const raw = JSON.parse(readFileSync3(lockPath, "utf-8"));
17498
- const contents = LockFileSchema.parse(raw);
18369
+ const raw2 = JSON.parse(readFileSync3(lockPath, "utf-8"));
18370
+ const contents = LockFileSchema.parse(raw2);
17499
18371
  const pid = contents.pid;
17500
18372
  try {
17501
18373
  process.kill(pid, 0);
@@ -17643,9 +18515,9 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
17643
18515
 
17644
18516
  // src/server.ts
17645
18517
  import { serve } from "@hono/node-server";
17646
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
17647
- import { Hono as Hono18 } from "hono";
17648
- import { dirname as dirname3, join as join13 } from "path";
18518
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
18519
+ import { Hono as Hono19 } from "hono";
18520
+ import { dirname as dirname4, join as join15 } from "path";
17649
18521
  import { fileURLToPath as fileURLToPath2 } from "url";
17650
18522
 
17651
18523
  // src/channel-config.ts
@@ -17680,8 +18552,8 @@ function registerChannel(dataDir) {
17680
18552
  let config2 = {};
17681
18553
  if (existsSync4(mcpPath)) {
17682
18554
  try {
17683
- const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
17684
- const parsed = McpConfigSchema.safeParse(raw);
18555
+ const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
18556
+ const parsed = McpConfigSchema.safeParse(raw2);
17685
18557
  if (parsed.success) config2 = parsed.data;
17686
18558
  } catch {
17687
18559
  }
@@ -17698,8 +18570,8 @@ function unregisterChannel(dataDir) {
17698
18570
  const mcpPath = join5(root, ".mcp.json");
17699
18571
  if (!existsSync4(mcpPath)) return;
17700
18572
  try {
17701
- const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
17702
- const parsed = McpConfigSchema.safeParse(raw);
18573
+ const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
18574
+ const parsed = McpConfigSchema.safeParse(raw2);
17703
18575
  if (!parsed.success) return;
17704
18576
  const config2 = parsed.data;
17705
18577
  if (config2.mcpServers?.[MCP_SERVER_KEY] !== void 0) {
@@ -17725,8 +18597,8 @@ async function isChannelAlive(dataDir) {
17725
18597
  if (port === null) return false;
17726
18598
  try {
17727
18599
  const res = await fetch(`http://127.0.0.1:${port}/health`);
17728
- const raw = await res.json();
17729
- const parsed = HealthResponseSchema.safeParse(raw);
18600
+ const raw2 = await res.json();
18601
+ const parsed = HealthResponseSchema.safeParse(raw2);
17730
18602
  return parsed.success && parsed.data.ok;
17731
18603
  } catch {
17732
18604
  return false;
@@ -17759,8 +18631,120 @@ init_zod();
17759
18631
  // src/ai/shared.ts
17760
18632
  init_zod();
17761
18633
 
18634
+ // src/review-notes/format.ts
18635
+ init_store();
18636
+ init_view();
18637
+ function notesByFile(repoRoot, filePaths) {
18638
+ const out = [];
18639
+ for (const file2 of filePaths) {
18640
+ const notes = loadReviewNotesForFile(repoRoot, file2);
18641
+ if (notes.length > 0) out.push({ file: file2, notes });
18642
+ }
18643
+ return out;
18644
+ }
18645
+ function reviewNotesPromptSection(repoRoot, filePaths) {
18646
+ const grouped = notesByFile(repoRoot, filePaths);
18647
+ if (grouped.length === 0) return "";
18648
+ const lines = [
18649
+ "=== Author review notes ===",
18650
+ "The author (an AI tool) left line-anchored notes explaining these changes. Use them to inform your analysis \u2014 weight their stated risks and assumptions.",
18651
+ ""
18652
+ ];
18653
+ for (const { file: file2, notes } of grouped) {
18654
+ lines.push(`${file2}:`);
18655
+ for (const n of notes) {
18656
+ lines.push(`- [${REVIEW_NOTE_LABELS[n.kind] ?? n.kind}, L${String(n.line)}] ${n.body}`);
18657
+ }
18658
+ lines.push("");
18659
+ }
18660
+ return lines.join("\n").trimEnd();
18661
+ }
18662
+ function reviewNotesExportSection(repoRoot, filePaths) {
18663
+ const grouped = notesByFile(repoRoot, filePaths);
18664
+ if (grouped.length === 0) return [];
18665
+ const lines = [
18666
+ "## AI Review Notes",
18667
+ "",
18668
+ "> Line-anchored notes the generating AI left explaining its changes (from `.pr-notes/`). Read these for the rationale and proof behind the code.",
18669
+ ""
18670
+ ];
18671
+ for (const { file: file2, notes } of grouped) {
18672
+ lines.push(`### ${file2}`);
18673
+ lines.push("");
18674
+ for (const n of notes) {
18675
+ const who = n.producer !== void 0 ? ` _(${n.producer})_` : "";
18676
+ lines.push(`- **Line ${String(n.line)}** [${n.kind}]: ${n.body}${who}`);
18677
+ }
18678
+ lines.push("");
18679
+ }
18680
+ return lines;
18681
+ }
18682
+
17762
18683
  // src/ai/client.ts
17763
18684
  init_zod();
18685
+
18686
+ // src/ai/apple-foundation.ts
18687
+ init_zod();
18688
+ import { spawn } from "child_process";
18689
+ import { existsSync as existsSync6 } from "fs";
18690
+ import { join as join7 } from "path";
18691
+ var defaultRunner = (bin, args, stdin) => new Promise((resolve11, reject) => {
18692
+ const child = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
18693
+ let stdout = "";
18694
+ child.stdout.setEncoding("utf-8");
18695
+ child.stdout.on("data", (chunk) => {
18696
+ stdout += chunk;
18697
+ });
18698
+ child.on("error", reject);
18699
+ child.on("close", (code) => {
18700
+ resolve11({ stdout, code: code ?? 0 });
18701
+ });
18702
+ child.stdin.end(stdin);
18703
+ });
18704
+ var runner = defaultRunner;
18705
+ var isDarwin = process.platform === "darwin";
18706
+ var availabilityCache = null;
18707
+ function appleFmBinPath() {
18708
+ const env = process.env.GLASSBOX_APPLE_FM_BIN;
18709
+ if (env !== void 0 && env !== "" && existsSync6(env)) return env;
18710
+ const fallback = join7(process.cwd(), "apple-fm-helper");
18711
+ if (existsSync6(fallback)) return fallback;
18712
+ return null;
18713
+ }
18714
+ async function isAppleFoundationAvailable() {
18715
+ if (availabilityCache !== null) return availabilityCache;
18716
+ availabilityCache = await probeAvailability();
18717
+ return availabilityCache;
18718
+ }
18719
+ async function probeAvailability() {
18720
+ if (!isDarwin) return false;
18721
+ const bin = appleFmBinPath();
18722
+ if (bin === null) return false;
18723
+ try {
18724
+ const { stdout, code } = await runner(bin, ["--probe"], "");
18725
+ return code === 0 && stdout.trim().toLowerCase().startsWith("available");
18726
+ } catch {
18727
+ return false;
18728
+ }
18729
+ }
18730
+ var InferOutputSchema = external_exports.object({ content: external_exports.string() });
18731
+ async function runAppleFoundationInfer(system, messages) {
18732
+ const bin = appleFmBinPath();
18733
+ if (bin === null) throw new Error("Apple Foundation Models helper not found");
18734
+ const { stdout, code } = await runner(bin, ["--infer"], JSON.stringify({ system, messages }));
18735
+ if (code !== 0) throw new Error(`Apple Foundation Models helper exited with code ${String(code)}`);
18736
+ let raw2;
18737
+ try {
18738
+ raw2 = JSON.parse(stdout);
18739
+ } catch {
18740
+ throw new Error("Apple Foundation Models helper returned non-JSON output");
18741
+ }
18742
+ const parsed = InferOutputSchema.safeParse(raw2);
18743
+ if (!parsed.success) throw new Error("Apple Foundation Models helper returned an unexpected payload");
18744
+ return parsed.data.content;
18745
+ }
18746
+
18747
+ // src/ai/client.ts
17764
18748
  var AnthropicResponseSchema = external_exports.object({
17765
18749
  content: external_exports.array(external_exports.object({ type: external_exports.string(), text: external_exports.string().optional() }).loose()),
17766
18750
  usage: external_exports.object({ input_tokens: external_exports.number(), output_tokens: external_exports.number() }).loose()
@@ -17771,6 +18755,12 @@ var OpenAIResponseSchema = external_exports.object({
17771
18755
  }).loose()).min(1),
17772
18756
  usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose()
17773
18757
  }).loose();
18758
+ var LocalResponseSchema = external_exports.object({
18759
+ choices: external_exports.array(external_exports.object({
18760
+ message: external_exports.object({ content: external_exports.string() }).loose()
18761
+ }).loose()).min(1),
18762
+ usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose().optional()
18763
+ }).loose();
17774
18764
  var GoogleResponseSchema = external_exports.object({
17775
18765
  candidates: external_exports.array(external_exports.object({
17776
18766
  content: external_exports.object({
@@ -17782,23 +18772,35 @@ var GoogleResponseSchema = external_exports.object({
17782
18772
  candidatesTokenCount: external_exports.number()
17783
18773
  }).loose().optional()
17784
18774
  }).loose();
17785
- async function sendAIRequest(config2, systemPrompt, messages) {
18775
+ function requireKey(config2) {
17786
18776
  if (config2.apiKey === null) {
17787
18777
  throw new Error(`No API key configured for ${config2.platform}`);
17788
18778
  }
18779
+ return config2.apiKey;
18780
+ }
18781
+ async function sendAIRequest(config2, systemPrompt, messages) {
18782
+ if (config2.apiKey === null && !KEYLESS_PLATFORMS.has(config2.platform)) {
18783
+ throw new Error(`No API key configured for ${config2.platform}`);
18784
+ }
17789
18785
  const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0) + systemPrompt.length;
17790
18786
  debugLog(`AI request \u2192 ${config2.platform}/${config2.model} | ${String(messages.length)} message(s) | ~${String(Math.ceil(totalChars / 3))} estimated tokens`);
17791
18787
  const start = Date.now();
17792
18788
  let response;
17793
18789
  switch (config2.platform) {
17794
18790
  case "anthropic":
17795
- response = await sendAnthropicRequest(config2.apiKey, config2.model, systemPrompt, messages);
18791
+ response = await sendAnthropicRequest(requireKey(config2), config2.model, systemPrompt, messages);
17796
18792
  break;
17797
18793
  case "openai":
17798
- response = await sendOpenAIRequest(config2.apiKey, config2.model, systemPrompt, messages);
18794
+ response = await sendOpenAIRequest(requireKey(config2), config2.model, systemPrompt, messages);
17799
18795
  break;
17800
18796
  case "google":
17801
- response = await sendGoogleRequest(config2.apiKey, config2.model, systemPrompt, messages);
18797
+ response = await sendGoogleRequest(requireKey(config2), config2.model, systemPrompt, messages);
18798
+ break;
18799
+ case "local":
18800
+ response = await sendLocalRequest(config2.baseUrl ?? DEFAULT_LOCAL_ENDPOINT, config2.apiKey, config2.model, systemPrompt, messages);
18801
+ break;
18802
+ case "apple":
18803
+ response = await sendAppleRequest(systemPrompt, messages);
17802
18804
  break;
17803
18805
  }
17804
18806
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
@@ -17827,8 +18829,8 @@ async function sendAnthropicRequest(apiKey, model, systemPrompt, messages) {
17827
18829
  const errorText = await response.text();
17828
18830
  throw new Error(`Anthropic API error (${String(response.status)}): ${errorText}`);
17829
18831
  }
17830
- const raw = await response.json();
17831
- const data = AnthropicResponseSchema.parse(raw);
18832
+ const raw2 = await response.json();
18833
+ const data = AnthropicResponseSchema.parse(raw2);
17832
18834
  const text = data.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("");
17833
18835
  return {
17834
18836
  content: text,
@@ -17857,14 +18859,42 @@ async function sendOpenAIRequest(apiKey, model, systemPrompt, messages) {
17857
18859
  const errorText = await response.text();
17858
18860
  throw new Error(`OpenAI API error (${String(response.status)}): ${errorText}`);
17859
18861
  }
17860
- const raw = await response.json();
17861
- const data = OpenAIResponseSchema.parse(raw);
18862
+ const raw2 = await response.json();
18863
+ const data = OpenAIResponseSchema.parse(raw2);
17862
18864
  return {
17863
18865
  content: data.choices[0].message.content,
17864
18866
  inputTokens: data.usage.prompt_tokens,
17865
18867
  outputTokens: data.usage.completion_tokens
17866
18868
  };
17867
18869
  }
18870
+ async function sendLocalRequest(baseUrl, apiKey, model, systemPrompt, messages) {
18871
+ const oaiMessages = [
18872
+ { role: "system", content: systemPrompt },
18873
+ ...messages.map((m) => ({ role: m.role, content: m.content }))
18874
+ ];
18875
+ const headers = { "Content-Type": "application/json" };
18876
+ if (apiKey !== null && apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
18877
+ const response = await fetch(`${baseUrl}/chat/completions`, {
18878
+ method: "POST",
18879
+ headers,
18880
+ body: JSON.stringify({ model, messages: oaiMessages, max_tokens: 8192, stream: false })
18881
+ });
18882
+ if (!response.ok) {
18883
+ const errorText = await response.text();
18884
+ throw new Error(`Local model error (${String(response.status)}): ${errorText}`);
18885
+ }
18886
+ const raw2 = await response.json();
18887
+ const data = LocalResponseSchema.parse(raw2);
18888
+ return {
18889
+ content: data.choices[0].message.content,
18890
+ inputTokens: data.usage?.prompt_tokens ?? 0,
18891
+ outputTokens: data.usage?.completion_tokens ?? 0
18892
+ };
18893
+ }
18894
+ async function sendAppleRequest(systemPrompt, messages) {
18895
+ const content = await runAppleFoundationInfer(systemPrompt, messages);
18896
+ return { content, inputTokens: 0, outputTokens: 0 };
18897
+ }
17868
18898
  async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
17869
18899
  const contents = messages.map((m) => ({
17870
18900
  role: m.role === "assistant" ? "model" : "user",
@@ -17889,8 +18919,8 @@ async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
17889
18919
  const errorText = await response.text();
17890
18920
  throw new Error(`Google AI API error (${String(response.status)}): ${errorText}`);
17891
18921
  }
17892
- const raw = await response.json();
17893
- const data = GoogleResponseSchema.parse(raw);
18922
+ const raw2 = await response.json();
18923
+ const data = GoogleResponseSchema.parse(raw2);
17894
18924
  const text = data.candidates[0].content.parts.map((p) => p.text).join("");
17895
18925
  return {
17896
18926
  content: text,
@@ -18014,14 +19044,26 @@ function extractJSON(text) {
18014
19044
  throw new Error(`Could not extract JSON from AI response: ${text.slice(0, 300)}`);
18015
19045
  }
18016
19046
  async function runAnalysisBatch(files, config2, repoRoot, options) {
19047
+ try {
19048
+ return await runAnalysisBatchOnce(files, config2, repoRoot, options);
19049
+ } catch (err) {
19050
+ if (config2.fallback === void 0) throw err;
19051
+ const msg = err instanceof Error ? err.message : String(err);
19052
+ debugLog(`[${options.analysisName}] primary platform '${config2.platform}' failed for batch (${msg.slice(0, 160)}); retrying with fallback '${config2.fallback.platform}'`);
19053
+ return runAnalysisBatchOnce(files, config2.fallback, repoRoot, options);
19054
+ }
19055
+ }
19056
+ async function runAnalysisBatchOnce(files, config2, repoRoot, options) {
18017
19057
  const contextWindow = getModelContextWindow(config2.platform, config2.model);
18018
19058
  const charBudget = Math.floor(contextWindow * 0.7 * 3);
18019
19059
  const contexts = buildFileContexts(files, charBudget);
18020
19060
  const validPaths = new Set(files.map((f) => f.file_path));
19061
+ const notesSection = reviewNotesPromptSection(repoRoot, files.map((f) => f.file_path));
18021
19062
  const initialPrompt = [
18022
19063
  options.initialPromptHeader(files.length),
18023
19064
  "",
18024
- formatContextsForPrompt(contexts)
19065
+ formatContextsForPrompt(contexts),
19066
+ ...notesSection === "" ? [] : ["", notesSection]
18025
19067
  ].join("\n");
18026
19068
  const messages = [{ role: "user", content: initialPrompt }];
18027
19069
  for (let round = 0; round < 3; round++) {
@@ -18045,7 +19087,8 @@ ${formatAdditionalContext(fileContents)}`
18045
19087
  });
18046
19088
  continue;
18047
19089
  }
18048
- const arrayResult = external_exports.array(options.itemSchema).safeParse(parsed);
19090
+ const candidate = Array.isArray(parsed) ? parsed : [parsed];
19091
+ const arrayResult = external_exports.array(options.itemSchema).safeParse(candidate);
18049
19092
  if (!arrayResult.success) {
18050
19093
  const summary = arrayResult.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
18051
19094
  throw new Error(`Expected an array of ${options.resultLabel} from AI \u2014 ${summary}`);
@@ -18486,8 +19529,8 @@ function isRetriable(err) {
18486
19529
  return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
18487
19530
  }
18488
19531
  function sleep(ms) {
18489
- return new Promise((resolve9) => {
18490
- setTimeout(resolve9, ms);
19532
+ return new Promise((resolve11) => {
19533
+ setTimeout(resolve11, ms);
18491
19534
  });
18492
19535
  }
18493
19536
 
@@ -18531,8 +19574,8 @@ function randomLines(count) {
18531
19574
  return lines.sort((a, b) => a.line - b.line);
18532
19575
  }
18533
19576
  function sleep2(ms) {
18534
- return new Promise((resolve9) => {
18535
- setTimeout(resolve9, ms);
19577
+ return new Promise((resolve11) => {
19578
+ setTimeout(resolve11, ms);
18536
19579
  });
18537
19580
  }
18538
19581
  async function mockRiskAnalysisBatch(files) {
@@ -18699,17 +19742,32 @@ var AIConfigRespSchema = external_exports.object({
18699
19742
  model: external_exports.string(),
18700
19743
  keyConfigured: external_exports.boolean(),
18701
19744
  keySource: KeySourceSchema,
18702
- guidedReview: GuidedReviewConfigShapeSchema
19745
+ /** Base URL for the `local` (OpenAI-compatible) platform. */
19746
+ localEndpoint: external_exports.string(),
19747
+ guidedReview: GuidedReviewConfigShapeSchema,
19748
+ /** Secondary model used when the primary (Apple FM) fails a batch. `null`
19749
+ * when none is configured. */
19750
+ fallbackPlatform: AIPlatformSchema.nullable(),
19751
+ fallbackModel: external_exports.string().nullable()
18703
19752
  });
18704
19753
  var SaveAIConfigReqSchema = external_exports.object({
18705
19754
  platform: AIPlatformSchema,
18706
19755
  model: external_exports.string().min(1),
18707
- guidedReview: GuidedReviewConfigShapeSchema.optional()
19756
+ localEndpoint: external_exports.string().optional(),
19757
+ guidedReview: GuidedReviewConfigShapeSchema.optional(),
19758
+ /** Apple-FM fallback selection. An empty `fallbackPlatform` clears it. */
19759
+ fallbackPlatform: external_exports.string().optional(),
19760
+ fallbackModel: external_exports.string().optional()
18708
19761
  });
18709
19762
  var SaveAIConfigRespSchema = OkResponseSchema;
18710
19763
  var ListAIModelsRespSchema = external_exports.object({
18711
19764
  platforms: external_exports.record(AIPlatformSchema, external_exports.string()),
18712
- models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema))
19765
+ models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema)),
19766
+ // Whether the on-device Apple Foundation Models helper is available right now
19767
+ // (macOS 26 + Apple Intelligence + bundled helper). The picker uses this to
19768
+ // show/hide the Apple platform; the `platforms`/`models` records always carry
19769
+ // every platform key (the record schema over the platform enum is exhaustive).
19770
+ appleAvailable: external_exports.boolean()
18713
19771
  });
18714
19772
  var AIKeyStatusEntrySchema = external_exports.object({
18715
19773
  configured: external_exports.boolean(),
@@ -18892,7 +19950,9 @@ var CreateAnnotationReqSchema = external_exports.object({
18892
19950
  lineNumber: external_exports.number().int().min(1),
18893
19951
  side: AnnotationSideSchema,
18894
19952
  category: AnnotationCategorySchema,
18895
- content: external_exports.string().min(1)
19953
+ content: external_exports.string().min(1),
19954
+ /** SARIF guid of the AI review note this annotation replies to (doc 20 threading). */
19955
+ replyToNoteId: external_exports.string().optional()
18896
19956
  });
18897
19957
  var CreateAnnotationRespSchema = AnnotationSchema;
18898
19958
  var UpdateAnnotationReqSchema = external_exports.object({
@@ -19954,6 +21014,19 @@ var DifftoolPollRespSchema = external_exports.object({
19954
21014
  });
19955
21015
  var DifftoolEndRespSchema = external_exports.object({ ok: external_exports.literal(true) });
19956
21016
 
21017
+ // src/api/review-notes.ts
21018
+ init_zod();
21019
+ var DiscardReviewNoteReqSchema = external_exports.object({
21020
+ guid: external_exports.string().min(1),
21021
+ /** Repo-relative source file the note is on (scopes the shard search). */
21022
+ file: external_exports.string().min(1)
21023
+ });
21024
+ var DiscardReviewNoteRespSchema = external_exports.object({
21025
+ ok: external_exports.boolean(),
21026
+ /** Whether a note was actually removed (false if it wasn't on disk, e.g. demo). */
21027
+ removed: external_exports.boolean()
21028
+ });
21029
+
19957
21030
  // src/api/index.ts
19958
21031
  var apis = {
19959
21032
  ...ai_exports,
@@ -19977,13 +21050,13 @@ init_schemas3();
19977
21050
 
19978
21051
  // src/utils/parseBody.ts
19979
21052
  async function parseBody(c, schema) {
19980
- let raw;
21053
+ let raw2;
19981
21054
  try {
19982
- raw = await c.req.json();
21055
+ raw2 = await c.req.json();
19983
21056
  } catch {
19984
21057
  return { ok: false, response: c.json({ error: "Body must be valid JSON" }, 400) };
19985
21058
  }
19986
- const result = schema.safeParse(raw);
21059
+ const result = schema.safeParse(raw2);
19987
21060
  if (!result.success) {
19988
21061
  const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
19989
21062
  return { ok: false, response: c.json({ error: summary }, 400) };
@@ -19991,8 +21064,8 @@ async function parseBody(c, schema) {
19991
21064
  return { ok: true, data: result.data };
19992
21065
  }
19993
21066
  function parseQuery(c, schema) {
19994
- const raw = c.req.query();
19995
- const result = schema.safeParse(raw);
21067
+ const raw2 = c.req.query();
21068
+ const result = schema.safeParse(raw2);
19996
21069
  if (!result.success) {
19997
21070
  const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
19998
21071
  return { ok: false, response: c.json({ error: summary }, 400) };
@@ -20042,7 +21115,7 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
20042
21115
  debugLog(`POST /analyze: type=${analysisType}, reviewId=${reviewId}`);
20043
21116
  const testMode = isAIServiceTest();
20044
21117
  const config2 = loadAIConfig();
20045
- if (config2.apiKey === null && !testMode) {
21118
+ if (config2.apiKey === null && !testMode && !KEYLESS_PLATFORMS.has(config2.platform)) {
20046
21119
  debugLog("POST /analyze: no API key configured");
20047
21120
  return c.json({ error: "No API key configured" }, 400);
20048
21121
  }
@@ -20443,11 +21516,11 @@ function anthropicContextWindow(id) {
20443
21516
  return id.toLowerCase().includes("haiku") ? 2e5 : 1e6;
20444
21517
  }
20445
21518
  async function fetchAnthropic(apiKey) {
20446
- const raw = await getJson("https://api.anthropic.com/v1/models?limit=1000", {
21519
+ const raw2 = await getJson("https://api.anthropic.com/v1/models?limit=1000", {
20447
21520
  "x-api-key": apiKey,
20448
21521
  "anthropic-version": "2023-06-01"
20449
21522
  });
20450
- const parsed = AnthropicListSchema.safeParse(raw);
21523
+ const parsed = AnthropicListSchema.safeParse(raw2);
20451
21524
  if (!parsed.success) return null;
20452
21525
  return parsed.data.data.map((m) => ({
20453
21526
  id: m.id,
@@ -20466,13 +21539,21 @@ function isOpenAIChatModel(id) {
20466
21539
  return isChat && !isNonChat;
20467
21540
  }
20468
21541
  async function fetchOpenAI(apiKey) {
20469
- const raw = await getJson("https://api.openai.com/v1/models", {
21542
+ const raw2 = await getJson("https://api.openai.com/v1/models", {
20470
21543
  Authorization: `Bearer ${apiKey}`
20471
21544
  });
20472
- const parsed = OpenAIListSchema.safeParse(raw);
21545
+ const parsed = OpenAIListSchema.safeParse(raw2);
20473
21546
  if (!parsed.success) return null;
20474
21547
  return parsed.data.data.filter((m) => isOpenAIChatModel(m.id)).map((m) => ({ id: m.id, name: m.id, contextWindow: 128e3, isDefault: false }));
20475
21548
  }
21549
+ async function fetchLocal(baseUrl, apiKey) {
21550
+ const headers = {};
21551
+ if (apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
21552
+ const raw2 = await getJson(`${baseUrl}/models`, headers);
21553
+ const parsed = OpenAIListSchema.safeParse(raw2);
21554
+ if (!parsed.success) return null;
21555
+ return parsed.data.data.map((m) => ({ id: m.id, name: m.id, contextWindow: 8192, isDefault: false }));
21556
+ }
20476
21557
  var GoogleListSchema = external_exports.object({
20477
21558
  models: external_exports.array(external_exports.object({
20478
21559
  name: external_exports.string(),
@@ -20482,11 +21563,11 @@ var GoogleListSchema = external_exports.object({
20482
21563
  }))
20483
21564
  });
20484
21565
  async function fetchGoogle(apiKey) {
20485
- const raw = await getJson(
21566
+ const raw2 = await getJson(
20486
21567
  `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(apiKey)}`,
20487
21568
  {}
20488
21569
  );
20489
- const parsed = GoogleListSchema.safeParse(raw);
21570
+ const parsed = GoogleListSchema.safeParse(raw2);
20490
21571
  if (!parsed.success) return null;
20491
21572
  return parsed.data.models.filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent")).map((m) => ({
20492
21573
  id: m.name.replace(/^models\//, ""),
@@ -20495,10 +21576,11 @@ async function fetchGoogle(apiKey) {
20495
21576
  isDefault: false
20496
21577
  }));
20497
21578
  }
20498
- async function fetchAvailableModels(platform, apiKey) {
21579
+ async function fetchAvailableModels(platform, apiKey, opts = {}) {
20499
21580
  let models;
20500
21581
  if (platform === "anthropic") models = await fetchAnthropic(apiKey);
20501
21582
  else if (platform === "openai") models = await fetchOpenAI(apiKey);
21583
+ else if (platform === "local") models = await fetchLocal(opts.baseUrl ?? "", apiKey);
20502
21584
  else models = await fetchGoogle(apiKey);
20503
21585
  if (models === null || models.length === 0) return null;
20504
21586
  const defaultId = getDefaultModel(platform);
@@ -20511,21 +21593,32 @@ async function fetchAvailableModels(platform, apiKey) {
20511
21593
 
20512
21594
  // src/routes/ai-config.ts
20513
21595
  var aiConfigRoutes = new Hono2();
20514
- aiConfigRoutes.get("/config", (c) => {
21596
+ aiConfigRoutes.get("/config", async (c) => {
20515
21597
  const config2 = loadAIConfig();
21598
+ const appleReady = APPLE_FM_ANALYSIS_ENABLED && config2.platform === "apple" && await isAppleFoundationAvailable();
21599
+ const fallbackSelection = loadFallbackSelection();
20516
21600
  return c.json({
20517
21601
  platform: config2.platform,
20518
21602
  model: config2.model,
20519
- keyConfigured: config2.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
21603
+ keyConfigured: config2.apiKey !== null || config2.platform === "local" || appleReady || isAIServiceTest() || getDemoMode() !== null,
20520
21604
  keySource: config2.keySource,
20521
- guidedReview: loadGuidedReviewConfig()
21605
+ localEndpoint: resolveLocalEndpoint(),
21606
+ guidedReview: loadGuidedReviewConfig(),
21607
+ // Apple-FM fallback selection as stored, regardless of the current primary
21608
+ // platform, so the settings dialog can show/preserve it; `null` when unset.
21609
+ fallbackPlatform: fallbackSelection?.platform ?? null,
21610
+ fallbackModel: fallbackSelection?.model ?? null
20522
21611
  });
20523
21612
  });
20524
21613
  aiConfigRoutes.post("/config", async (c) => {
20525
21614
  const parsed = await parseBody(c, SaveAIConfigReqSchema);
20526
21615
  if (!parsed.ok) return parsed.response;
20527
21616
  const body = parsed.data;
20528
- saveAIConfigPreferences(body.platform, body.model);
21617
+ saveAIConfigPreferences(body.platform, body.model, {
21618
+ localEndpoint: body.localEndpoint,
21619
+ fallbackPlatform: body.fallbackPlatform,
21620
+ fallbackModel: body.fallbackModel
21621
+ });
20529
21622
  if (body.guidedReview !== void 0) {
20530
21623
  saveGuidedReviewConfig(body.guidedReview);
20531
21624
  }
@@ -20533,7 +21626,13 @@ aiConfigRoutes.post("/config", async (c) => {
20533
21626
  });
20534
21627
  aiConfigRoutes.get("/models", async (c) => {
20535
21628
  const platforms = ["anthropic", "openai", "google"];
20536
- const models = { anthropic: MODELS.anthropic, openai: MODELS.openai, google: MODELS.google };
21629
+ const models = {
21630
+ anthropic: MODELS.anthropic,
21631
+ openai: MODELS.openai,
21632
+ google: MODELS.google,
21633
+ local: MODELS.local,
21634
+ apple: MODELS.apple
21635
+ };
20537
21636
  if (!isAIServiceTest() && getDemoMode() === null) {
20538
21637
  await Promise.all(platforms.map(async (platform) => {
20539
21638
  const { key } = resolveAPIKey(platform);
@@ -20541,11 +21640,15 @@ aiConfigRoutes.get("/models", async (c) => {
20541
21640
  const live = await fetchAvailableModels(platform, key);
20542
21641
  if (live !== null && live.length > 0) models[platform] = live;
20543
21642
  }));
21643
+ const { key: localKey } = resolveAPIKey("local");
21644
+ const localLive = await fetchAvailableModels("local", localKey ?? "", { baseUrl: resolveLocalEndpoint() });
21645
+ if (localLive !== null && localLive.length > 0) models.local = localLive;
20544
21646
  }
20545
- return c.json({ platforms: PLATFORMS, models });
21647
+ const appleAvailable = APPLE_FM_ANALYSIS_ENABLED && (isAIServiceTest() || await isAppleFoundationAvailable());
21648
+ return c.json({ platforms: PLATFORMS, models, appleAvailable });
20546
21649
  });
20547
21650
  aiConfigRoutes.get("/key-status", (c) => {
20548
- const platforms = ["anthropic", "openai", "google"];
21651
+ const platforms = ["anthropic", "openai", "google", "local", "apple"];
20549
21652
  const status = {};
20550
21653
  for (const platform of platforms) {
20551
21654
  const { source } = resolveAPIKey(platform);
@@ -20569,7 +21672,7 @@ aiConfigRoutes.delete("/key", (c) => {
20569
21672
  const platform = c.req.query("platform") ?? "anthropic";
20570
21673
  const parsed = AIPlatformSchema.safeParse(platform);
20571
21674
  if (!parsed.success) {
20572
- return errorResponse(c, `platform must be one of: anthropic, openai, google`);
21675
+ return errorResponse(c, `platform must be one of: ${Object.keys(PLATFORMS).join(", ")}`);
20573
21676
  }
20574
21677
  deleteAPIKey(parsed.data);
20575
21678
  return c.json({ ok: true });
@@ -20581,7 +21684,7 @@ aiApiRoutes.route("/", aiConfigRoutes);
20581
21684
  aiApiRoutes.route("/", aiAnalysisRoutes);
20582
21685
 
20583
21686
  // src/routes/api.ts
20584
- import { Hono as Hono13 } from "hono";
21687
+ import { Hono as Hono14 } from "hono";
20585
21688
 
20586
21689
  // src/routes/api/annotations.ts
20587
21690
  import { Hono as Hono4 } from "hono";
@@ -20590,28 +21693,28 @@ init_queries();
20590
21693
  // src/export/generate.ts
20591
21694
  init_zod();
20592
21695
  init_queries();
20593
- import { spawnSync as spawnSync5 } from "child_process";
20594
- import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
21696
+ import { spawnSync as spawnSync6 } from "child_process";
21697
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
20595
21698
  import { homedir as homedir2 } from "os";
20596
- import { join as join6 } from "path";
20597
- var DISMISS_FILE = join6(homedir2(), ".glassbox", "gitignore-dismissed.json");
21699
+ import { join as join8 } from "path";
21700
+ var DISMISS_FILE = join8(homedir2(), ".glassbox", "gitignore-dismissed.json");
20598
21701
  var DISMISS_DAYS = 30;
20599
21702
  var DismissalsSchema = external_exports.record(external_exports.string(), external_exports.number());
20600
21703
  function loadDismissals() {
20601
21704
  try {
20602
- const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync5(DISMISS_FILE, "utf-8")));
21705
+ const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync6(DISMISS_FILE, "utf-8")));
20603
21706
  return parsed.success ? parsed.data : {};
20604
21707
  } catch {
20605
21708
  return {};
20606
21709
  }
20607
21710
  }
20608
21711
  function saveDismissals(data) {
20609
- const dir = join6(homedir2(), ".glassbox");
20610
- mkdirSync4(dir, { recursive: true });
20611
- writeFileSync5(DISMISS_FILE, JSON.stringify(data), "utf-8");
21712
+ const dir = join8(homedir2(), ".glassbox");
21713
+ mkdirSync5(dir, { recursive: true });
21714
+ writeFileSync6(DISMISS_FILE, JSON.stringify(data), "utf-8");
20612
21715
  }
20613
21716
  function isGlassboxGitignored(repoRoot) {
20614
- const result = spawnSync5("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
21717
+ const result = spawnSync6("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
20615
21718
  return result.status === 0;
20616
21719
  }
20617
21720
  function shouldPromptGitignore(repoRoot) {
@@ -20626,16 +21729,16 @@ function shouldPromptGitignore(repoRoot) {
20626
21729
  return true;
20627
21730
  }
20628
21731
  function addGlassboxToGitignore(repoRoot) {
20629
- const gitignorePath = join6(repoRoot, ".gitignore");
20630
- if (existsSync5(gitignorePath)) {
20631
- const content = readFileSync5(gitignorePath, "utf-8");
21732
+ const gitignorePath = join8(repoRoot, ".gitignore");
21733
+ if (existsSync7(gitignorePath)) {
21734
+ const content = readFileSync6(gitignorePath, "utf-8");
20632
21735
  if (!content.endsWith("\n")) {
20633
21736
  appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
20634
21737
  } else {
20635
21738
  appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
20636
21739
  }
20637
21740
  } else {
20638
- writeFileSync5(gitignorePath, ".glassbox/\n", "utf-8");
21741
+ writeFileSync6(gitignorePath, ".glassbox/\n", "utf-8");
20639
21742
  }
20640
21743
  }
20641
21744
  function dismissGitignorePrompt2(repoRoot) {
@@ -20644,17 +21747,17 @@ function dismissGitignorePrompt2(repoRoot) {
20644
21747
  saveDismissals(dismissals);
20645
21748
  }
20646
21749
  function deleteReviewExport(reviewId, repoRoot) {
20647
- const exportDir = join6(repoRoot, ".glassbox");
20648
- const archivePath = join6(exportDir, `review-${reviewId}.md`);
20649
- if (existsSync5(archivePath)) unlinkSync(archivePath);
21750
+ const exportDir = join8(repoRoot, ".glassbox");
21751
+ const archivePath = join8(exportDir, `review-${reviewId}.md`);
21752
+ if (existsSync7(archivePath)) unlinkSync2(archivePath);
20650
21753
  }
20651
21754
  async function generateReviewExport(reviewId, repoRoot, isCurrent) {
20652
21755
  const review = await getReview(reviewId);
20653
21756
  if (!review) throw new Error("Review not found");
20654
21757
  const files = await getReviewFiles(reviewId);
20655
21758
  const annotations = await getAnnotationsForReview(reviewId);
20656
- const exportDir = join6(repoRoot, ".glassbox");
20657
- mkdirSync4(exportDir, { recursive: true });
21759
+ const exportDir = join8(repoRoot, ".glassbox");
21760
+ mkdirSync5(exportDir, { recursive: true });
20658
21761
  const byFile = {};
20659
21762
  for (const a of annotations) {
20660
21763
  if (!(a.file_path in byFile)) byFile[a.file_path] = [];
@@ -20705,6 +21808,8 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
20705
21808
  }
20706
21809
  lines.push("");
20707
21810
  }
21811
+ const reviewNoteLines = reviewNotesExportSection(repoRoot, files.map((f) => f.file_path));
21812
+ if (reviewNoteLines.length > 0) lines.push(...reviewNoteLines);
20708
21813
  lines.push("---");
20709
21814
  lines.push("");
20710
21815
  lines.push("## Instructions for AI Tools");
@@ -20719,11 +21824,11 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
20719
21824
  lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
20720
21825
  lines.push("");
20721
21826
  const content = lines.join("\n");
20722
- const archivePath = join6(exportDir, `review-${review.id}.md`);
20723
- writeFileSync5(archivePath, content, "utf-8");
21827
+ const archivePath = join8(exportDir, `review-${review.id}.md`);
21828
+ writeFileSync6(archivePath, content, "utf-8");
20724
21829
  if (isCurrent) {
20725
- const latestPath = join6(exportDir, "latest-review.md");
20726
- writeFileSync5(latestPath, content, "utf-8");
21830
+ const latestPath = join8(exportDir, "latest-review.md");
21831
+ writeFileSync6(latestPath, content, "utf-8");
20727
21832
  return latestPath;
20728
21833
  }
20729
21834
  return archivePath;
@@ -20754,7 +21859,8 @@ annotationsRoutes.post("/annotations", async (c) => {
20754
21859
  body.lineNumber,
20755
21860
  body.side,
20756
21861
  body.category,
20757
- body.content
21862
+ body.content,
21863
+ body.replyToNoteId
20758
21864
  );
20759
21865
  autoExport(c);
20760
21866
  return c.json(annotation, 201);
@@ -20841,7 +21947,7 @@ import { resolve as resolve4 } from "path";
20841
21947
  init_queries();
20842
21948
 
20843
21949
  // src/utils/openOS.ts
20844
- import { execFileSync, spawn } from "child_process";
21950
+ import { execFileSync, spawn as spawn2 } from "child_process";
20845
21951
  import { resolve as resolve3 } from "path";
20846
21952
  function openOS(target, mode) {
20847
21953
  if (mode === "edit") {
@@ -20873,7 +21979,7 @@ function openOS(target, mode) {
20873
21979
  }
20874
21980
  }
20875
21981
  function launchDetached(command, args) {
20876
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
21982
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
20877
21983
  child.on("error", (err) => {
20878
21984
  debugLog(`launchDetached(${command}) failed: ${err.message}`);
20879
21985
  });
@@ -20951,9 +22057,9 @@ init_blob_store();
20951
22057
  import { Hono as Hono7 } from "hono";
20952
22058
 
20953
22059
  // src/git/image.ts
20954
- import { spawnSync as spawnSync6 } from "child_process";
20955
- import { readFileSync as readFileSync7 } from "fs";
20956
- import { join as join8, resolve as resolve5 } from "path";
22060
+ import { spawnSync as spawnSync7 } from "child_process";
22061
+ import { readFileSync as readFileSync8 } from "fs";
22062
+ import { join as join10, resolve as resolve5 } from "path";
20957
22063
 
20958
22064
  // src/git/image-metadata.ts
20959
22065
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
@@ -21221,20 +22327,20 @@ function getNewRef(mode) {
21221
22327
  }
21222
22328
  function gitShowFile(ref, filePath, repoRoot) {
21223
22329
  const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
21224
- const result = spawnSync6("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
22330
+ const result = spawnSync7("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
21225
22331
  if (result.status !== 0 || result.stdout.length === 0) return null;
21226
22332
  return result.stdout;
21227
22333
  }
21228
22334
  function readWorkingFile(filePath, repoRoot) {
21229
22335
  try {
21230
- return readFileSync7(resolve5(repoRoot, filePath));
22336
+ return readFileSync8(resolve5(repoRoot, filePath));
21231
22337
  } catch {
21232
22338
  return null;
21233
22339
  }
21234
22340
  }
21235
22341
  function readDiskImage(absPath) {
21236
22342
  try {
21237
- const data = readFileSync7(absPath);
22343
+ const data = readFileSync8(absPath);
21238
22344
  return { data, size: data.length };
21239
22345
  } catch {
21240
22346
  return null;
@@ -21242,7 +22348,7 @@ function readDiskImage(absPath) {
21242
22348
  }
21243
22349
  function getOldImage(mode, filePath, oldPath, repoRoot) {
21244
22350
  if (mode.type === "diff") {
21245
- return readDiskImage(join8(directComparisonRoots(mode).rootA, oldPath ?? filePath));
22351
+ return readDiskImage(join10(directComparisonRoots(mode).rootA, oldPath ?? filePath));
21246
22352
  }
21247
22353
  const ref = getOldRef(mode);
21248
22354
  const path = oldPath ?? filePath;
@@ -21258,7 +22364,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
21258
22364
  }
21259
22365
  function getNewImage(mode, filePath, repoRoot) {
21260
22366
  if (mode.type === "diff") {
21261
- return readDiskImage(join8(directComparisonRoots(mode).rootB, filePath));
22367
+ return readDiskImage(join10(directComparisonRoots(mode).rootB, filePath));
21262
22368
  }
21263
22369
  const ref = getNewRef(mode);
21264
22370
  if (ref === null) {
@@ -21280,9 +22386,9 @@ function getNewImage(mode, filePath, repoRoot) {
21280
22386
  import { Worker } from "worker_threads";
21281
22387
 
21282
22388
  // src/git/svg-rasterize-render.ts
21283
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
22389
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
21284
22390
  import { createRequire } from "module";
21285
- import { join as join9 } from "path";
22391
+ import { join as join11 } from "path";
21286
22392
  var initialized = false;
21287
22393
  var ResvgClass;
21288
22394
  var fontBuffers = [];
@@ -21291,7 +22397,7 @@ async function ensureRenderInit() {
21291
22397
  const require2 = createRequire(import.meta.url);
21292
22398
  const resvgPath = require2.resolve("@resvg/resvg-wasm");
21293
22399
  const wasmPath = resvgPath.replace(/index\.(js|mjs)$/, "index_bg.wasm");
21294
- const wasmBuffer = readFileSync8(wasmPath);
22400
+ const wasmBuffer = readFileSync9(wasmPath);
21295
22401
  const mod = await import("@resvg/resvg-wasm");
21296
22402
  await mod.initWasm(wasmBuffer);
21297
22403
  ResvgClass = mod.Resvg;
@@ -21302,9 +22408,9 @@ function loadSystemFonts() {
21302
22408
  const buffers = [];
21303
22409
  const candidates = getFontCandidates();
21304
22410
  for (const path of candidates) {
21305
- if (!existsSync7(path)) continue;
22411
+ if (!existsSync9(path)) continue;
21306
22412
  try {
21307
- buffers.push(readFileSync8(path));
22413
+ buffers.push(readFileSync9(path));
21308
22414
  } catch {
21309
22415
  }
21310
22416
  }
@@ -21317,24 +22423,24 @@ function getFontCandidates() {
21317
22423
  const sup = "/System/Library/Fonts/Supplemental";
21318
22424
  return [
21319
22425
  // Core system fonts (serif, sans-serif, monospace)
21320
- join9(sys, "Helvetica.ttc"),
21321
- join9(sys, "Times.ttc"),
21322
- join9(sys, "Courier.ttc"),
21323
- join9(sys, "Menlo.ttc"),
21324
- join9(sys, "SFPro.ttf"),
21325
- join9(sys, "SFNS.ttf"),
21326
- join9(sys, "SFNSMono.ttf"),
22426
+ join11(sys, "Helvetica.ttc"),
22427
+ join11(sys, "Times.ttc"),
22428
+ join11(sys, "Courier.ttc"),
22429
+ join11(sys, "Menlo.ttc"),
22430
+ join11(sys, "SFPro.ttf"),
22431
+ join11(sys, "SFNS.ttf"),
22432
+ join11(sys, "SFNSMono.ttf"),
21327
22433
  // Supplemental (common named fonts in SVGs)
21328
- join9(sup, "Arial.ttf"),
21329
- join9(sup, "Arial Bold.ttf"),
21330
- join9(sup, "Georgia.ttf"),
21331
- join9(sup, "Verdana.ttf"),
21332
- join9(sup, "Tahoma.ttf"),
21333
- join9(sup, "Trebuchet MS.ttf"),
21334
- join9(sup, "Impact.ttf"),
21335
- join9(sup, "Comic Sans MS.ttf"),
21336
- join9(sup, "Courier New.ttf"),
21337
- join9(sup, "Times New Roman.ttf")
22434
+ join11(sup, "Arial.ttf"),
22435
+ join11(sup, "Arial Bold.ttf"),
22436
+ join11(sup, "Georgia.ttf"),
22437
+ join11(sup, "Verdana.ttf"),
22438
+ join11(sup, "Tahoma.ttf"),
22439
+ join11(sup, "Trebuchet MS.ttf"),
22440
+ join11(sup, "Impact.ttf"),
22441
+ join11(sup, "Comic Sans MS.ttf"),
22442
+ join11(sup, "Courier New.ttf"),
22443
+ join11(sup, "Times New Roman.ttf")
21338
22444
  ];
21339
22445
  }
21340
22446
  if (os === "linux") {
@@ -21353,17 +22459,17 @@ function getFontCandidates() {
21353
22459
  ];
21354
22460
  }
21355
22461
  if (os === "win32") {
21356
- const winFonts = join9(process.env.WINDIR ?? "C:\\Windows", "Fonts");
22462
+ const winFonts = join11(process.env.WINDIR ?? "C:\\Windows", "Fonts");
21357
22463
  return [
21358
- join9(winFonts, "arial.ttf"),
21359
- join9(winFonts, "arialbd.ttf"),
21360
- join9(winFonts, "times.ttf"),
21361
- join9(winFonts, "cour.ttf"),
21362
- join9(winFonts, "verdana.ttf"),
21363
- join9(winFonts, "tahoma.ttf"),
21364
- join9(winFonts, "georgia.ttf"),
21365
- join9(winFonts, "consola.ttf"),
21366
- join9(winFonts, "segoeui.ttf")
22464
+ join11(winFonts, "arial.ttf"),
22465
+ join11(winFonts, "arialbd.ttf"),
22466
+ join11(winFonts, "times.ttf"),
22467
+ join11(winFonts, "cour.ttf"),
22468
+ join11(winFonts, "verdana.ttf"),
22469
+ join11(winFonts, "tahoma.ttf"),
22470
+ join11(winFonts, "georgia.ttf"),
22471
+ join11(winFonts, "consola.ttf"),
22472
+ join11(winFonts, "segoeui.ttf")
21367
22473
  ];
21368
22474
  }
21369
22475
  return [];
@@ -21512,8 +22618,8 @@ function submit(job) {
21512
22618
  }
21513
22619
  async function rasterizeSvg(svgData) {
21514
22620
  const svg = svgData.toString("utf-8");
21515
- return new Promise((resolve9, reject) => {
21516
- submit({ svg, resolve: resolve9, reject });
22621
+ return new Promise((resolve11, reject) => {
22622
+ submit({ svg, resolve: resolve11, reject });
21517
22623
  });
21518
22624
  }
21519
22625
 
@@ -21581,8 +22687,8 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
21581
22687
 
21582
22688
  // src/routes/api/outline.ts
21583
22689
  init_queries();
21584
- import { spawnSync as spawnSync7 } from "child_process";
21585
- import { readFileSync as readFileSync9 } from "fs";
22690
+ import { spawnSync as spawnSync8 } from "child_process";
22691
+ import { readFileSync as readFileSync10 } from "fs";
21586
22692
  import { Hono as Hono8 } from "hono";
21587
22693
  import { resolve as resolve6 } from "path";
21588
22694
 
@@ -21948,7 +23054,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
21948
23054
  }
21949
23055
  if (definitions.length === 0) {
21950
23056
  try {
21951
- const allFiles = spawnSync7("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
23057
+ const allFiles = spawnSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
21952
23058
  let scanned = 0;
21953
23059
  for (const filePath of allFiles) {
21954
23060
  if (searchedPaths.has(filePath)) continue;
@@ -21960,7 +23066,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
21960
23066
  }
21961
23067
  let content = "";
21962
23068
  try {
21963
- content = readFileSync9(resolve6(repoRoot, filePath), "utf-8");
23069
+ content = readFileSync10(resolve6(repoRoot, filePath), "utf-8");
21964
23070
  } catch {
21965
23071
  continue;
21966
23072
  }
@@ -21996,16 +23102,16 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
21996
23102
  }
21997
23103
 
21998
23104
  // src/routes/api/project-settings.ts
21999
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
23105
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
22000
23106
  import { Hono as Hono9 } from "hono";
22001
- import { join as join10 } from "path";
23107
+ import { join as join12 } from "path";
22002
23108
  var projectSettingsRoutes = new Hono9();
22003
23109
  function readProjectSettings(repoRoot) {
22004
- const settingsPath = join10(repoRoot, ".glassbox", "settings.json");
23110
+ const settingsPath = join12(repoRoot, ".glassbox", "settings.json");
22005
23111
  try {
22006
- if (existsSync8(settingsPath)) {
22007
- const raw = JSON.parse(readFileSync10(settingsPath, "utf-8"));
22008
- const parsed = ProjectSettingsSchema.safeParse(raw);
23112
+ if (existsSync10(settingsPath)) {
23113
+ const raw2 = JSON.parse(readFileSync11(settingsPath, "utf-8"));
23114
+ const parsed = ProjectSettingsSchema.safeParse(raw2);
22009
23115
  if (parsed.success) return parsed.data;
22010
23116
  }
22011
23117
  } catch {
@@ -22013,9 +23119,9 @@ function readProjectSettings(repoRoot) {
22013
23119
  return {};
22014
23120
  }
22015
23121
  function writeProjectSettings(repoRoot, settings) {
22016
- const dir = join10(repoRoot, ".glassbox");
22017
- mkdirSync6(dir, { recursive: true });
22018
- writeFileSync7(join10(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
23122
+ const dir = join12(repoRoot, ".glassbox");
23123
+ mkdirSync7(dir, { recursive: true });
23124
+ writeFileSync8(join12(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
22019
23125
  }
22020
23126
  projectSettingsRoutes.get("/project-settings", (c) => {
22021
23127
  const repoRoot = c.get("repoRoot");
@@ -22031,10 +23137,54 @@ projectSettingsRoutes.patch("/project-settings", async (c) => {
22031
23137
  return c.json(current);
22032
23138
  });
22033
23139
 
23140
+ // src/routes/api/review-notes.ts
23141
+ init_store();
23142
+ import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
23143
+ import { Hono as Hono10 } from "hono";
23144
+ import { extname, relative, resolve as resolve7 } from "path";
23145
+ var reviewNotesRoutes = new Hono10();
23146
+ var ARTIFACT_SERVE_MAX_BYTES = 1e7;
23147
+ var IMAGE_CONTENT_TYPES = {
23148
+ ".png": "image/png",
23149
+ ".webp": "image/webp",
23150
+ ".avif": "image/avif",
23151
+ ".gif": "image/gif",
23152
+ ".jpg": "image/jpeg",
23153
+ ".jpeg": "image/jpeg",
23154
+ ".svg": "image/svg+xml"
23155
+ };
23156
+ reviewNotesRoutes.get("/review-notes/artifact", (c) => {
23157
+ const file2 = c.req.query("file");
23158
+ if (file2 === void 0 || file2 === "") return c.text("Missing file", 400);
23159
+ const repoRoot = c.get("repoRoot");
23160
+ const abs = resolve7(repoRoot, file2);
23161
+ const rel = relative(repoRoot, abs);
23162
+ if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
23163
+ const ext = extname(abs).toLowerCase();
23164
+ if (!(ext in IMAGE_CONTENT_TYPES)) return c.text("Unsupported artifact type", 415);
23165
+ const contentType = IMAGE_CONTENT_TYPES[ext];
23166
+ try {
23167
+ const stat = statSync3(abs);
23168
+ if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
23169
+ const body = readFileSync12(abs);
23170
+ return c.body(body, 200, { "Content-Type": contentType });
23171
+ } catch {
23172
+ return c.text("Not found", 404);
23173
+ }
23174
+ });
23175
+ reviewNotesRoutes.delete("/review-notes/:guid", (c) => {
23176
+ const guid3 = requirePathParam(c, "guid");
23177
+ if (!guid3.ok) return guid3.response;
23178
+ const repoRoot = c.get("repoRoot");
23179
+ const file2 = c.req.query("file");
23180
+ const removed = removeNote(repoRoot, guid3.data, file2);
23181
+ return c.json({ ok: true, removed });
23182
+ });
23183
+
22034
23184
  // src/routes/api/reviews.ts
22035
23185
  init_queries();
22036
- import { Hono as Hono10 } from "hono";
22037
- var reviewsRoutes = new Hono10();
23186
+ import { Hono as Hono11 } from "hono";
23187
+ var reviewsRoutes = new Hono11();
22038
23188
  reviewsRoutes.get("/reviews", async (c) => {
22039
23189
  const repoRoot = c.get("repoRoot");
22040
23190
  const reviews = await listReviews(repoRoot);
@@ -22124,8 +23274,8 @@ reviewsRoutes.post("/reviews/delete-all", async (c) => {
22124
23274
 
22125
23275
  // src/routes/api/share-prompt.ts
22126
23276
  init_zod();
22127
- import { Hono as Hono11 } from "hono";
22128
- var sharePromptRoutes = new Hono11();
23277
+ import { Hono as Hono12 } from "hono";
23278
+ var sharePromptRoutes = new Hono12();
22129
23279
  var SharePromptShapeSchema = external_exports.object({
22130
23280
  dismissedAt: external_exports.number().nullable().optional(),
22131
23281
  totalOpenMs: external_exports.number().optional()
@@ -22164,8 +23314,8 @@ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
22164
23314
  });
22165
23315
 
22166
23316
  // src/routes/api/system.ts
22167
- import { Hono as Hono12 } from "hono";
22168
- var systemRoutes = new Hono12();
23317
+ import { Hono as Hono13 } from "hono";
23318
+ var systemRoutes = new Hono13();
22169
23319
  systemRoutes.post("/open-external", async (c) => {
22170
23320
  const parsed = await parseBody(c, OpenExternalReqSchema);
22171
23321
  if (!parsed.ok) return parsed.response;
@@ -22177,7 +23327,7 @@ systemRoutes.post("/open-external", async (c) => {
22177
23327
  });
22178
23328
 
22179
23329
  // src/routes/api.ts
22180
- var apiRoutes = new Hono13();
23330
+ var apiRoutes = new Hono14();
22181
23331
  apiRoutes.route("/", reviewsRoutes);
22182
23332
  apiRoutes.route("/", filesRoutes);
22183
23333
  apiRoutes.route("/", annotationsRoutes);
@@ -22185,20 +23335,21 @@ apiRoutes.route("/", outlineRoutes);
22185
23335
  apiRoutes.route("/", contextRoutes);
22186
23336
  apiRoutes.route("/", projectSettingsRoutes);
22187
23337
  apiRoutes.route("/", imageRoutes);
23338
+ apiRoutes.route("/", reviewNotesRoutes);
22188
23339
  apiRoutes.route("/", sharePromptRoutes);
22189
23340
  apiRoutes.route("/", systemRoutes);
22190
23341
 
22191
23342
  // src/routes/channel-api.ts
22192
- import { spawnSync as spawnSync8 } from "child_process";
22193
- import { mkdirSync as mkdirSync7 } from "fs";
22194
- import { Hono as Hono14 } from "hono";
22195
- import { join as join11 } from "path";
22196
- var channelApiRoutes = new Hono14();
23343
+ import { spawnSync as spawnSync9 } from "child_process";
23344
+ import { mkdirSync as mkdirSync8 } from "fs";
23345
+ import { Hono as Hono15 } from "hono";
23346
+ import { join as join13 } from "path";
23347
+ var channelApiRoutes = new Hono15();
22197
23348
  channelApiRoutes.get("/status", async (c) => {
22198
23349
  const config2 = readGlobalConfig();
22199
23350
  const enabled = config2.channelEnabled === true;
22200
23351
  const repoRoot = c.get("repoRoot");
22201
- const dataDir = join11(repoRoot, ".glassbox");
23352
+ const dataDir = join13(repoRoot, ".glassbox");
22202
23353
  const connected = enabled ? await isChannelAlive(dataDir) : false;
22203
23354
  return c.json({ enabled, connected });
22204
23355
  });
@@ -22207,8 +23358,8 @@ channelApiRoutes.post("/enable", (c) => {
22207
23358
  config2.channelEnabled = true;
22208
23359
  });
22209
23360
  const repoRoot = c.get("repoRoot");
22210
- const dataDir = join11(repoRoot, ".glassbox");
22211
- mkdirSync7(dataDir, { recursive: true });
23361
+ const dataDir = join13(repoRoot, ".glassbox");
23362
+ mkdirSync8(dataDir, { recursive: true });
22212
23363
  registerChannel(dataDir);
22213
23364
  return c.json({ ok: true });
22214
23365
  });
@@ -22217,7 +23368,7 @@ channelApiRoutes.post("/disable", (c) => {
22217
23368
  config2.channelEnabled = false;
22218
23369
  });
22219
23370
  const repoRoot = c.get("repoRoot");
22220
- const dataDir = join11(repoRoot, ".glassbox");
23371
+ const dataDir = join13(repoRoot, ".glassbox");
22221
23372
  unregisterChannel(dataDir);
22222
23373
  return c.json({ ok: true });
22223
23374
  });
@@ -22225,7 +23376,7 @@ channelApiRoutes.post("/trigger", async (c) => {
22225
23376
  const parsed = await parseBody(c, TriggerChannelReqSchema);
22226
23377
  if (!parsed.ok) return parsed.response;
22227
23378
  const repoRoot = c.get("repoRoot");
22228
- const dataDir = join11(repoRoot, ".glassbox");
23379
+ const dataDir = join13(repoRoot, ".glassbox");
22229
23380
  const sent = await triggerChannel(dataDir, parsed.data.message);
22230
23381
  if (!sent) {
22231
23382
  return c.json({ error: "Channel not connected" }, 503);
@@ -22234,7 +23385,7 @@ channelApiRoutes.post("/trigger", async (c) => {
22234
23385
  });
22235
23386
  channelApiRoutes.get("/claude-check", (c) => {
22236
23387
  try {
22237
- const result = spawnSync8("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
23388
+ const result = spawnSync9("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
22238
23389
  if (result.status !== 0) {
22239
23390
  return c.json({ installed: false, version: null, meetsMinimum: false });
22240
23391
  }
@@ -22253,13 +23404,13 @@ channelApiRoutes.get("/claude-check", (c) => {
22253
23404
  });
22254
23405
 
22255
23406
  // src/routes/difftool-api.ts
22256
- import { Hono as Hono15 } from "hono";
23407
+ import { Hono as Hono16 } from "hono";
22257
23408
  init_connection();
22258
23409
  init_queries();
22259
23410
  init_blob_store();
22260
23411
  init_session();
22261
23412
  init_difftool();
22262
- var difftoolApiRoutes = new Hono15();
23413
+ var difftoolApiRoutes = new Hono16();
22263
23414
  difftoolApiRoutes.get("/status", (c) => {
22264
23415
  return c.json(getDifftoolStatus("global"));
22265
23416
  });
@@ -22321,9 +23472,9 @@ difftoolApiRoutes.get("/hold", (c) => {
22321
23472
  const session2 = getDifftoolSession();
22322
23473
  if (session2 === null) return c.json({ ended: true });
22323
23474
  noteDifftoolActivity();
22324
- return new Promise((resolve9) => {
23475
+ return new Promise((resolve11) => {
22325
23476
  addDifftoolHold(() => {
22326
- resolve9(c.json({ ended: true }));
23477
+ resolve11(c.json({ ended: true }));
22327
23478
  });
22328
23479
  c.req.raw.signal.addEventListener("abort", () => {
22329
23480
  endDifftoolSession();
@@ -22336,9 +23487,12 @@ difftoolApiRoutes.post("/end", (c) => {
22336
23487
  });
22337
23488
 
22338
23489
  // src/routes/pages.tsx
22339
- import { readFileSync as readFileSync12 } from "fs";
22340
- import { Hono as Hono16 } from "hono";
22341
- import { resolve as resolve7 } from "path";
23490
+ import { readFileSync as readFileSync14 } from "fs";
23491
+ import { Hono as Hono17 } from "hono";
23492
+ import { resolve as resolve8 } from "path";
23493
+
23494
+ // src/components/diffView.tsx
23495
+ import { raw } from "kerfjs";
22342
23496
 
22343
23497
  // src/icons.tsx
22344
23498
  import { jsx, jsxs } from "kerfjs/jsx-runtime";
@@ -22400,6 +23554,9 @@ function IconActualSize() {
22400
23554
  ] });
22401
23555
  }
22402
23556
 
23557
+ // src/components/diffView.tsx
23558
+ init_view();
23559
+
22403
23560
  // src/utils/charDiff.ts
22404
23561
  var MAX_LINE_LENGTH = 5e3;
22405
23562
  function charDiff(oldStr, newStr) {
@@ -22471,6 +23628,26 @@ function truncateDiffLine(content) {
22471
23628
  };
22472
23629
  }
22473
23630
 
23631
+ // src/utils/noteMarkdown.ts
23632
+ function escapeHtml(text) {
23633
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
23634
+ }
23635
+ var SAFE_URL = /^(https?:\/\/|mailto:)/i;
23636
+ function renderInline(escaped) {
23637
+ let out = escaped.replace(/`([^`]+)`/g, "<code>$1</code>");
23638
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, text, url2) => {
23639
+ if (!SAFE_URL.test(url2)) return match;
23640
+ return `<a href="${url2}" target="_blank" rel="noopener noreferrer">${text}</a>`;
23641
+ });
23642
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
23643
+ out = out.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
23644
+ out = out.replace(/(^|[^_])_([^_\s][^_]*)_/g, "$1<em>$2</em>");
23645
+ return out;
23646
+ }
23647
+ function renderNoteMarkdown(text) {
23648
+ return escapeHtml(text).split("\n").map(renderInline).join("<br>");
23649
+ }
23650
+
22474
23651
  // src/components/imageDiff.tsx
22475
23652
  import { jsx as jsx2, jsxs as jsxs2 } from "kerfjs/jsx-runtime";
22476
23653
  function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
@@ -22521,12 +23698,23 @@ function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
22521
23698
 
22522
23699
  // src/components/diffView.tsx
22523
23700
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "kerfjs/jsx-runtime";
22524
- function DiffView({ file: file2, diff, annotations, mode }) {
23701
+ function DiffView({ file: file2, diff, annotations, mode, reviewNotes = [] }) {
23702
+ const loadedNoteGuids = new Set(reviewNotes.map((n) => n.guid).filter((g) => g !== void 0));
23703
+ const repliesByNote = {};
22525
23704
  const annotationsByLine = {};
22526
23705
  for (const a of annotations) {
23706
+ if (a.reply_to_note_id !== null && loadedNoteGuids.has(a.reply_to_note_id)) {
23707
+ (repliesByNote[a.reply_to_note_id] ??= []).push(a);
23708
+ continue;
23709
+ }
22527
23710
  const key = `${a.line_number}:${a.side}`;
22528
- if (!(key in annotationsByLine)) annotationsByLine[key] = [];
22529
- annotationsByLine[key].push(a);
23711
+ (annotationsByLine[key] ??= []).push(a);
23712
+ }
23713
+ const reviewNotesByLine = {};
23714
+ for (const n of reviewNotes) {
23715
+ const key = `${n.line}:${n.side}`;
23716
+ if (!(key in reviewNotesByLine)) reviewNotesByLine[key] = [];
23717
+ reviewNotesByLine[key].push(n);
22530
23718
  }
22531
23719
  return /* @__PURE__ */ jsxs3(
22532
23720
  "div",
@@ -22543,7 +23731,7 @@ function DiffView({ file: file2, diff, annotations, mode }) {
22543
23731
  ] }),
22544
23732
  /* @__PURE__ */ jsx3("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx3("span", { className: `file-status ${diff.status}`, children: diff.status }) })
22545
23733
  ] }),
22546
- diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx3(ImageDiff, { file: file2, diff }) : diff.isBinary ? /* @__PURE__ */ jsx3("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx3(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx3(SplitDiff, { hunks: diff.hunks, annotationsByLine })
23734
+ diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx3(ImageDiff, { file: file2, diff }) : diff.isBinary ? /* @__PURE__ */ jsx3("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx3(UnifiedDiff, { hunks: diff.hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) : /* @__PURE__ */ jsx3(SplitDiff, { hunks: diff.hunks, annotationsByLine, reviewNotesByLine, repliesByNote })
22547
23735
  ]
22548
23736
  }
22549
23737
  );
@@ -22553,7 +23741,10 @@ function getAnnotations(pair, annotationsByLine) {
22553
23741
  const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] ?? [] : [];
22554
23742
  return [...leftAnns, ...rightAnns];
22555
23743
  }
22556
- function SplitDiff({ hunks, annotationsByLine }) {
23744
+ function getReviewNotes(pair, reviewNotesByLine) {
23745
+ return pair.right ? reviewNotesByLine[`${pair.right.newNum}:new`] ?? [] : [];
23746
+ }
23747
+ function SplitDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
22557
23748
  const lastHunk = hunks[hunks.length - 1];
22558
23749
  const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
22559
23750
  const items = [];
@@ -22565,8 +23756,9 @@ function SplitDiff({ hunks, annotationsByLine }) {
22565
23756
  items.push({ kind: "separator", hunkIdx, hunk, gapStart, gapEnd });
22566
23757
  for (const pair of pairLines(hunk.lines)) {
22567
23758
  const anns = getAnnotations(pair, annotationsByLine);
22568
- if (anns.length > 0) {
22569
- items.push({ kind: "annotated", pair, annotations: anns });
23759
+ const notes = getReviewNotes(pair, reviewNotesByLine);
23760
+ if (anns.length > 0 || notes.length > 0) {
23761
+ items.push({ kind: "annotated", pair, annotations: anns, reviewNotes: notes });
22570
23762
  } else {
22571
23763
  items.push({ kind: "pair", pair });
22572
23764
  }
@@ -22581,7 +23773,7 @@ function SplitDiff({ hunks, annotationsByLine }) {
22581
23773
  groups.push({ type: "columns", items: run });
22582
23774
  run = [];
22583
23775
  }
22584
- groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations });
23776
+ groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations, reviewNotes: item.reviewNotes });
22585
23777
  } else {
22586
23778
  run.push(item);
22587
23779
  }
@@ -22617,7 +23809,8 @@ function SplitDiff({ hunks, annotationsByLine }) {
22617
23809
  }
22618
23810
  )
22619
23811
  ] }),
22620
- /* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations })
23812
+ group.annotations.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations }) : null,
23813
+ group.reviewNotes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes: group.reviewNotes, repliesByNote }) : null
22621
23814
  ] });
22622
23815
  }
22623
23816
  return /* @__PURE__ */ jsxs3("div", { className: "split-columns", children: [
@@ -22783,7 +23976,7 @@ function buildUnifiedCharDiffs(lines) {
22783
23976
  }
22784
23977
  return result;
22785
23978
  }
22786
- function UnifiedDiff({ hunks, annotationsByLine }) {
23979
+ function UnifiedDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
22787
23980
  const lastHunk = hunks[hunks.length - 1];
22788
23981
  const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
22789
23982
  return /* @__PURE__ */ jsxs3("div", { className: "diff-table-unified", children: [
@@ -22816,6 +24009,7 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
22816
24009
  const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
22817
24010
  const side = line.type === "remove" ? "old" : "new";
22818
24011
  const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
24012
+ const notes = reviewNotesByLine[`${lineNum}:${side}`] ?? [];
22819
24013
  const segments = charDiffs.get(line);
22820
24014
  return /* @__PURE__ */ jsxs3("div", { children: [
22821
24015
  /* @__PURE__ */ jsxs3(
@@ -22831,7 +24025,8 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
22831
24025
  ]
22832
24026
  }
22833
24027
  ),
22834
- anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null
24028
+ anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null,
24029
+ notes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes, repliesByNote }) : null
22835
24030
  ] });
22836
24031
  })
22837
24032
  ] });
@@ -22839,8 +24034,61 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
22839
24034
  /* @__PURE__ */ jsx3("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
22840
24035
  ] });
22841
24036
  }
22842
- function AnnotationRows({ annotations }) {
22843
- return /* @__PURE__ */ jsx3("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsxs3(
24037
+ function ReviewNoteRows({ notes, repliesByNote }) {
24038
+ return /* @__PURE__ */ jsx3(Fragment, { children: notes.map((n) => {
24039
+ const replies = n.guid !== void 0 ? repliesByNote[n.guid] ?? [] : [];
24040
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
24041
+ /* @__PURE__ */ jsxs3(
24042
+ "div",
24043
+ {
24044
+ className: `ai-note-row ai-note-review${n.stale === true ? " ai-note-stale" : ""}`,
24045
+ "data-kind": n.kind,
24046
+ "data-note-id": n.guid,
24047
+ children: [
24048
+ /* @__PURE__ */ jsxs3("div", { className: "ai-note-item", children: [
24049
+ /* @__PURE__ */ jsx3("span", { className: `ai-note-label ai-note-label-${n.kind}`, children: REVIEW_NOTE_LABELS[n.kind] ?? n.kind }),
24050
+ n.stale === true ? /* @__PURE__ */ jsx3("span", { className: "ai-note-stale-tag", title: "The code this note referred to has changed", children: "outdated" }) : null,
24051
+ /* @__PURE__ */ jsx3("span", { className: "ai-note-text", children: raw(renderNoteMarkdown(n.body)) }),
24052
+ n.producer !== void 0 ? /* @__PURE__ */ jsx3("span", { className: "ai-note-producer", children: n.producer }) : null,
24053
+ n.guid !== void 0 ? /* @__PURE__ */ jsx3("button", { className: "ai-note-reply-btn", "data-line": String(n.line), children: "Reply" }) : null,
24054
+ n.stale === true && n.guid !== void 0 ? /* @__PURE__ */ jsxs3("span", { className: "ai-note-stale-actions", children: [
24055
+ /* @__PURE__ */ jsx3("button", { className: "ai-note-keep-btn", title: "Dismiss the outdated flag for now", children: "Keep" }),
24056
+ /* @__PURE__ */ jsx3("button", { className: "ai-note-discard-btn", title: "Remove this note from .pr-notes/", children: "Discard" })
24057
+ ] }) : null
24058
+ ] }),
24059
+ n.artifacts !== void 0 && n.artifacts.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "ai-note-artifacts", children: n.artifacts.map((a) => a.content !== void 0 ? /* @__PURE__ */ jsxs3("details", { className: "ai-note-artifact", children: [
24060
+ /* @__PURE__ */ jsxs3("summary", { children: [
24061
+ "\u{1F4CE} ",
24062
+ a.uri
24063
+ ] }),
24064
+ /* @__PURE__ */ jsx3("pre", { className: "ai-note-artifact-content", children: /* @__PURE__ */ jsx3("code", { children: a.content }) })
24065
+ ] }) : a.isImage === true ? /* @__PURE__ */ jsxs3("details", { className: "ai-note-artifact", children: [
24066
+ /* @__PURE__ */ jsxs3("summary", { children: [
24067
+ "\u{1F4CE} ",
24068
+ a.uri
24069
+ ] }),
24070
+ /* @__PURE__ */ jsx3(
24071
+ "img",
24072
+ {
24073
+ className: "ai-note-artifact-img",
24074
+ loading: "lazy",
24075
+ alt: a.uri,
24076
+ src: `/api/review-notes/artifact?file=${encodeURIComponent(a.uri)}`
24077
+ }
24078
+ )
24079
+ ] }) : /* @__PURE__ */ jsxs3("div", { className: "ai-note-artifact ai-note-artifact-ref", children: [
24080
+ "\u{1F4CE} ",
24081
+ a.uri
24082
+ ] })) }) : null
24083
+ ]
24084
+ }
24085
+ ),
24086
+ replies.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "annotation-row ai-note-replies", children: replies.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) }) : null
24087
+ ] });
24088
+ }) });
24089
+ }
24090
+ function AnnotationItem({ annotation: a }) {
24091
+ return /* @__PURE__ */ jsxs3(
22844
24092
  "div",
22845
24093
  {
22846
24094
  className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
@@ -22850,6 +24098,7 @@ function AnnotationRows({ annotations }) {
22850
24098
  children: [
22851
24099
  /* @__PURE__ */ jsx3("span", { className: "annotation-drag-handle", draggable: true, title: "Drag to move", children: "\u283F" }),
22852
24100
  /* @__PURE__ */ jsx3("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
24101
+ a.reply_to_note_id !== null ? /* @__PURE__ */ jsx3("span", { className: "annotation-reply-tag", title: "Reply to an AI review note", children: "\u21B3 reply" }) : null,
22853
24102
  /* @__PURE__ */ jsx3("span", { className: "annotation-text", children: a.content }),
22854
24103
  /* @__PURE__ */ jsxs3("div", { className: "annotation-actions", children: [
22855
24104
  a.is_stale ? /* @__PURE__ */ jsx3("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
@@ -22858,13 +24107,16 @@ function AnnotationRows({ annotations }) {
22858
24107
  ] })
22859
24108
  ]
22860
24109
  }
22861
- )) });
24110
+ );
24111
+ }
24112
+ function AnnotationRows({ annotations }) {
24113
+ return /* @__PURE__ */ jsx3("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) });
22862
24114
  }
22863
24115
 
22864
24116
  // src/themes/config.ts
22865
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readdirSync, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "fs";
22866
- import { join as join12 } from "path";
22867
- var THEMES_DIR = join12(GLOBAL_CONFIG_DIR, "themes");
24117
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "fs";
24118
+ import { join as join14 } from "path";
24119
+ var THEMES_DIR = join14(GLOBAL_CONFIG_DIR, "themes");
22868
24120
  function getActiveThemeId() {
22869
24121
  const config2 = readGlobalConfig();
22870
24122
  const theme = config2.theme;
@@ -22878,13 +24130,13 @@ function setActiveThemeId(id) {
22878
24130
  });
22879
24131
  }
22880
24132
  function loadCustomThemes() {
22881
- if (!existsSync9(THEMES_DIR)) return [];
24133
+ if (!existsSync11(THEMES_DIR)) return [];
22882
24134
  const themes = [];
22883
24135
  try {
22884
- const files = readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json"));
24136
+ const files = readdirSync2(THEMES_DIR).filter((f) => f.endsWith(".json"));
22885
24137
  for (const file2 of files) {
22886
24138
  try {
22887
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync11(join12(THEMES_DIR, file2), "utf-8")));
24139
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(join14(THEMES_DIR, file2), "utf-8")));
22888
24140
  if (!parsed.success) continue;
22889
24141
  const d = parsed.data;
22890
24142
  themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
@@ -22896,21 +24148,21 @@ function loadCustomThemes() {
22896
24148
  return themes;
22897
24149
  }
22898
24150
  function saveCustomTheme(theme) {
22899
- mkdirSync8(THEMES_DIR, { recursive: true });
22900
- const filePath = join12(THEMES_DIR, `${theme.id}.json`);
22901
- writeFileSync8(filePath, JSON.stringify(theme, null, 2), "utf-8");
24151
+ mkdirSync9(THEMES_DIR, { recursive: true });
24152
+ const filePath = join14(THEMES_DIR, `${theme.id}.json`);
24153
+ writeFileSync9(filePath, JSON.stringify(theme, null, 2), "utf-8");
22902
24154
  }
22903
24155
  function deleteCustomTheme(id) {
22904
- const filePath = join12(THEMES_DIR, `${id}.json`);
22905
- if (existsSync9(filePath)) {
22906
- unlinkSync2(filePath);
24156
+ const filePath = join14(THEMES_DIR, `${id}.json`);
24157
+ if (existsSync11(filePath)) {
24158
+ unlinkSync3(filePath);
22907
24159
  }
22908
24160
  }
22909
24161
  function getCustomTheme(id) {
22910
- const filePath = join12(THEMES_DIR, `${id}.json`);
22911
- if (!existsSync9(filePath)) return void 0;
24162
+ const filePath = join14(THEMES_DIR, `${id}.json`);
24163
+ if (!existsSync11(filePath)) return void 0;
22912
24164
  try {
22913
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync11(filePath, "utf-8")));
24165
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(filePath, "utf-8")));
22914
24166
  if (!parsed.success) return void 0;
22915
24167
  const d = parsed.data;
22916
24168
  return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
@@ -22972,8 +24224,8 @@ function formatReviewMode(mode, modeArgs) {
22972
24224
  return `commit: ${shortenIfSha(argsFor(mode, "commit:", modeArgs))}`;
22973
24225
  }
22974
24226
  if (mode === "range" || mode.startsWith("range:")) {
22975
- const raw = argsFor(mode, "range:", modeArgs);
22976
- const [from = "", to = ""] = raw.split("..");
24227
+ const raw2 = argsFor(mode, "range:", modeArgs);
24228
+ const [from = "", to = ""] = raw2.split("..");
22977
24229
  return `range: ${shortenIfSha(from)}..${shortenIfSha(to)}`;
22978
24230
  }
22979
24231
  if (mode === "branch" || mode.startsWith("branch:")) {
@@ -23209,8 +24461,41 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
23209
24461
 
23210
24462
  // src/routes/pages.tsx
23211
24463
  init_queries();
24464
+
24465
+ // src/review-notes/reanchor.ts
24466
+ var MATCH_RADIUS = 50;
24467
+ function reanchorReviewNotes(notes, diff) {
24468
+ const byLine = /* @__PURE__ */ new Map();
24469
+ for (const hunk of diff.hunks) {
24470
+ for (const line of hunk.lines) {
24471
+ if (line.newNum !== null) byLine.set(line.newNum, line.content);
24472
+ }
24473
+ }
24474
+ return notes.map((note) => {
24475
+ if (note.snippet === void 0 || note.snippet === "") return note;
24476
+ const anchor = note.snippet.split("\n")[0];
24477
+ const current = byLine.get(note.line);
24478
+ if (current === anchor) return note;
24479
+ let best = null;
24480
+ let bestDistance = Infinity;
24481
+ for (const [lineNum, content] of byLine) {
24482
+ if (content !== anchor) continue;
24483
+ const distance = Math.abs(lineNum - note.line);
24484
+ if (distance < bestDistance && distance <= MATCH_RADIUS) {
24485
+ bestDistance = distance;
24486
+ best = lineNum;
24487
+ }
24488
+ }
24489
+ if (best !== null) return { ...note, line: best, stale: false };
24490
+ if (current !== void 0) return { ...note, stale: true };
24491
+ return note;
24492
+ });
24493
+ }
24494
+
24495
+ // src/routes/pages.tsx
24496
+ init_store();
23212
24497
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "kerfjs/jsx-runtime";
23213
- var pageRoutes = new Hono16();
24498
+ var pageRoutes = new Hono17();
23214
24499
  pageRoutes.get("/", async (c) => {
23215
24500
  const reviewId = c.get("reviewId");
23216
24501
  const review = await getReview(reviewId);
@@ -23289,7 +24574,9 @@ pageRoutes.get("/file/:fileId", async (c) => {
23289
24574
  }
23290
24575
  }
23291
24576
  }
23292
- const html = /* @__PURE__ */ jsx8(DiffView, { file: file2, diff: finalDiff, annotations, mode });
24577
+ const rawNotes = getDemoMode() !== null ? demoReviewNotes(file2.file_path) : loadReviewNotesForFile(c.get("repoRoot"), file2.file_path);
24578
+ const reviewNotes = reanchorReviewNotes(rawNotes, finalDiff);
24579
+ const html = /* @__PURE__ */ jsx8(DiffView, { file: file2, diff: finalDiff, annotations, mode, reviewNotes });
23293
24580
  return c.html(html.toString());
23294
24581
  });
23295
24582
  pageRoutes.get("/file-raw", (c) => {
@@ -23298,7 +24585,7 @@ pageRoutes.get("/file-raw", (c) => {
23298
24585
  const repoRoot = c.get("repoRoot");
23299
24586
  let content;
23300
24587
  try {
23301
- content = readFileSync12(resolve7(repoRoot, filePath), "utf-8");
24588
+ content = readFileSync14(resolve8(repoRoot, filePath), "utf-8");
23302
24589
  } catch {
23303
24590
  return c.text("File not found", 404);
23304
24591
  }
@@ -23354,8 +24641,8 @@ pageRoutes.get("/history", async (c) => {
23354
24641
  });
23355
24642
 
23356
24643
  // src/routes/theme-api.ts
23357
- import { Hono as Hono17 } from "hono";
23358
- var themeApiRoutes = new Hono17();
24644
+ import { Hono as Hono18 } from "hono";
24645
+ var themeApiRoutes = new Hono18();
23359
24646
  themeApiRoutes.get("/", (c) => {
23360
24647
  const themes = getAllThemes();
23361
24648
  const activeId = getActiveThemeId();
@@ -23469,10 +24756,10 @@ themeApiRoutes.delete("/:id", (c) => {
23469
24756
 
23470
24757
  // src/server.ts
23471
24758
  function tryServe(appFetch, port) {
23472
- return new Promise((resolve9, reject) => {
24759
+ return new Promise((resolve11, reject) => {
23473
24760
  const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
23474
24761
  server.on("listening", () => {
23475
- resolve9({ port, server });
24762
+ resolve11({ port, server });
23476
24763
  });
23477
24764
  server.on("error", (err) => {
23478
24765
  reject(err);
@@ -23480,25 +24767,25 @@ function tryServe(appFetch, port) {
23480
24767
  });
23481
24768
  }
23482
24769
  async function startServer(port, reviewId, repoRoot, options) {
23483
- const app = new Hono18();
24770
+ const app = new Hono19();
23484
24771
  app.use("*", async (c, next) => {
23485
24772
  c.set("reviewId", reviewId);
23486
24773
  c.set("currentReviewId", reviewId);
23487
24774
  c.set("repoRoot", repoRoot);
23488
24775
  await next();
23489
24776
  });
23490
- const selfDir = dirname3(fileURLToPath2(import.meta.url));
23491
- const distDir = existsSync10(join13(selfDir, "client", "styles.css")) ? join13(selfDir, "client") : join13(selfDir, "..", "dist", "client");
24777
+ const selfDir = dirname4(fileURLToPath2(import.meta.url));
24778
+ const distDir = existsSync12(join15(selfDir, "client", "styles.css")) ? join15(selfDir, "client") : join15(selfDir, "..", "dist", "client");
23492
24779
  app.get("/static/styles.css", (c) => {
23493
- const css = readFileSync13(join13(distDir, "styles.css"), "utf-8");
24780
+ const css = readFileSync15(join15(distDir, "styles.css"), "utf-8");
23494
24781
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
23495
24782
  });
23496
24783
  app.get("/static/app.js", (c) => {
23497
- const js = readFileSync13(join13(distDir, "app.global.js"), "utf-8");
24784
+ const js = readFileSync15(join15(distDir, "app.global.js"), "utf-8");
23498
24785
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
23499
24786
  });
23500
24787
  app.get("/static/history.js", (c) => {
23501
- const js = readFileSync13(join13(distDir, "history.global.js"), "utf-8");
24788
+ const js = readFileSync15(join15(distDir, "history.global.js"), "utf-8");
23502
24789
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
23503
24790
  });
23504
24791
  app.get("/favicon.ico", (c) => c.body(null, 204));
@@ -23536,7 +24823,7 @@ async function startServer(port, reviewId, repoRoot, options) {
23536
24823
  try {
23537
24824
  const globalConfig2 = readGlobalConfig();
23538
24825
  if (globalConfig2.channelEnabled === true) {
23539
- const dataDir = join13(repoRoot, ".glassbox");
24826
+ const dataDir = join15(repoRoot, ".glassbox");
23540
24827
  registerChannel(dataDir);
23541
24828
  }
23542
24829
  } catch {
@@ -23551,8 +24838,8 @@ async function startServer(port, reviewId, repoRoot, options) {
23551
24838
  }
23552
24839
 
23553
24840
  // src/skills.ts
23554
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
23555
- import { join as join14 } from "path";
24841
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
24842
+ import { join as join16 } from "path";
23556
24843
  var SKILL_VERSION = 1;
23557
24844
  function versionHeader() {
23558
24845
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -23563,14 +24850,14 @@ function parseVersionHeader(content) {
23563
24850
  return parseInt(match[1], 10);
23564
24851
  }
23565
24852
  function updateFile(path, content) {
23566
- if (existsSync11(path)) {
23567
- const existing = readFileSync14(path, "utf-8");
24853
+ if (existsSync13(path)) {
24854
+ const existing = readFileSync16(path, "utf-8");
23568
24855
  const version2 = parseVersionHeader(existing);
23569
24856
  if (version2 !== null && version2 >= SKILL_VERSION) {
23570
24857
  return false;
23571
24858
  }
23572
24859
  }
23573
- writeFileSync9(path, content, "utf-8");
24860
+ writeFileSync10(path, content, "utf-8");
23574
24861
  return true;
23575
24862
  }
23576
24863
  function skillBody() {
@@ -23590,8 +24877,8 @@ function skillBody() {
23590
24877
  ].join("\n");
23591
24878
  }
23592
24879
  function ensureClaudeSkills(cwd) {
23593
- const dir = join14(cwd, ".claude", "skills", "glassbox");
23594
- mkdirSync9(dir, { recursive: true });
24880
+ const dir = join16(cwd, ".claude", "skills", "glassbox");
24881
+ mkdirSync10(dir, { recursive: true });
23595
24882
  const content = [
23596
24883
  "---",
23597
24884
  "name: glassbox",
@@ -23603,11 +24890,11 @@ function ensureClaudeSkills(cwd) {
23603
24890
  skillBody(),
23604
24891
  ""
23605
24892
  ].join("\n");
23606
- return updateFile(join14(dir, "SKILL.md"), content);
24893
+ return updateFile(join16(dir, "SKILL.md"), content);
23607
24894
  }
23608
24895
  function ensureCursorRules(cwd) {
23609
- const rulesDir = join14(cwd, ".cursor", "rules");
23610
- mkdirSync9(rulesDir, { recursive: true });
24896
+ const rulesDir = join16(cwd, ".cursor", "rules");
24897
+ mkdirSync10(rulesDir, { recursive: true });
23611
24898
  const content = [
23612
24899
  "---",
23613
24900
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -23618,11 +24905,11 @@ function ensureCursorRules(cwd) {
23618
24905
  skillBody(),
23619
24906
  ""
23620
24907
  ].join("\n");
23621
- return updateFile(join14(rulesDir, "glassbox.mdc"), content);
24908
+ return updateFile(join16(rulesDir, "glassbox.mdc"), content);
23622
24909
  }
23623
24910
  function ensureCopilotPrompts(cwd) {
23624
- const promptsDir = join14(cwd, ".github", "prompts");
23625
- mkdirSync9(promptsDir, { recursive: true });
24911
+ const promptsDir = join16(cwd, ".github", "prompts");
24912
+ mkdirSync10(promptsDir, { recursive: true });
23626
24913
  const content = [
23627
24914
  "---",
23628
24915
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -23632,11 +24919,11 @@ function ensureCopilotPrompts(cwd) {
23632
24919
  skillBody(),
23633
24920
  ""
23634
24921
  ].join("\n");
23635
- return updateFile(join14(promptsDir, "glassbox.prompt.md"), content);
24922
+ return updateFile(join16(promptsDir, "glassbox.prompt.md"), content);
23636
24923
  }
23637
24924
  function ensureWindsurfRules(cwd) {
23638
- const rulesDir = join14(cwd, ".windsurf", "rules");
23639
- mkdirSync9(rulesDir, { recursive: true });
24925
+ const rulesDir = join16(cwd, ".windsurf", "rules");
24926
+ mkdirSync10(rulesDir, { recursive: true });
23640
24927
  const content = [
23641
24928
  "---",
23642
24929
  "trigger: manual",
@@ -23647,21 +24934,21 @@ function ensureWindsurfRules(cwd) {
23647
24934
  skillBody(),
23648
24935
  ""
23649
24936
  ].join("\n");
23650
- return updateFile(join14(rulesDir, "glassbox.md"), content);
24937
+ return updateFile(join16(rulesDir, "glassbox.md"), content);
23651
24938
  }
23652
24939
  function ensureSkills() {
23653
24940
  const cwd = process.cwd();
23654
24941
  const platforms = [];
23655
- if (existsSync11(join14(cwd, ".claude"))) {
24942
+ if (existsSync13(join16(cwd, ".claude"))) {
23656
24943
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
23657
24944
  }
23658
- if (existsSync11(join14(cwd, ".cursor"))) {
24945
+ if (existsSync13(join16(cwd, ".cursor"))) {
23659
24946
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
23660
24947
  }
23661
- if (existsSync11(join14(cwd, ".github", "prompts")) || existsSync11(join14(cwd, ".github", "copilot-instructions.md"))) {
24948
+ if (existsSync13(join16(cwd, ".github", "prompts")) || existsSync13(join16(cwd, ".github", "copilot-instructions.md"))) {
23662
24949
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
23663
24950
  }
23664
- if (existsSync11(join14(cwd, ".windsurf"))) {
24951
+ if (existsSync13(join16(cwd, ".windsurf"))) {
23665
24952
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
23666
24953
  }
23667
24954
  return platforms;
@@ -23669,36 +24956,36 @@ function ensureSkills() {
23669
24956
 
23670
24957
  // src/update-check.ts
23671
24958
  init_zod();
23672
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
24959
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
23673
24960
  import { get } from "https";
23674
24961
  import { homedir as homedir3 } from "os";
23675
- import { dirname as dirname4, join as join15 } from "path";
24962
+ import { dirname as dirname5, join as join17 } from "path";
23676
24963
  import { fileURLToPath as fileURLToPath3 } from "url";
23677
- var DATA_DIR = join15(homedir3(), ".glassbox");
23678
- var CHECK_FILE = join15(DATA_DIR, "last-update-check");
24964
+ var DATA_DIR = join17(homedir3(), ".glassbox");
24965
+ var CHECK_FILE = join17(DATA_DIR, "last-update-check");
23679
24966
  var PACKAGE_NAME = "glassbox";
23680
24967
  var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
23681
24968
  function getCurrentVersion() {
23682
24969
  try {
23683
- const dir = dirname4(fileURLToPath3(import.meta.url));
23684
- const raw = JSON.parse(readFileSync15(join15(dir, "..", "package.json"), "utf-8"));
23685
- return VersionPayloadSchema.parse(raw).version;
24970
+ const dir = dirname5(fileURLToPath3(import.meta.url));
24971
+ const raw2 = JSON.parse(readFileSync17(join17(dir, "..", "package.json"), "utf-8"));
24972
+ return VersionPayloadSchema.parse(raw2).version;
23686
24973
  } catch {
23687
24974
  return "0.0.0";
23688
24975
  }
23689
24976
  }
23690
24977
  function getLastCheckDate() {
23691
24978
  try {
23692
- if (existsSync12(CHECK_FILE)) {
23693
- return readFileSync15(CHECK_FILE, "utf-8").trim();
24979
+ if (existsSync14(CHECK_FILE)) {
24980
+ return readFileSync17(CHECK_FILE, "utf-8").trim();
23694
24981
  }
23695
24982
  } catch {
23696
24983
  }
23697
24984
  return null;
23698
24985
  }
23699
24986
  function saveCheckDate() {
23700
- mkdirSync10(DATA_DIR, { recursive: true });
23701
- writeFileSync10(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
24987
+ mkdirSync11(DATA_DIR, { recursive: true });
24988
+ writeFileSync11(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
23702
24989
  }
23703
24990
  function isFirstUseToday() {
23704
24991
  const last = getLastCheckDate();
@@ -23707,10 +24994,10 @@ function isFirstUseToday() {
23707
24994
  return last !== today;
23708
24995
  }
23709
24996
  function fetchLatestVersion() {
23710
- return new Promise((resolve9) => {
24997
+ return new Promise((resolve11) => {
23711
24998
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
23712
24999
  if (res.statusCode !== 200) {
23713
- resolve9(null);
25000
+ resolve11(null);
23714
25001
  return;
23715
25002
  }
23716
25003
  let data = "";
@@ -23719,19 +25006,19 @@ function fetchLatestVersion() {
23719
25006
  });
23720
25007
  res.on("end", () => {
23721
25008
  try {
23722
- const raw = JSON.parse(data);
23723
- resolve9(VersionPayloadSchema.parse(raw).version);
25009
+ const raw2 = JSON.parse(data);
25010
+ resolve11(VersionPayloadSchema.parse(raw2).version);
23724
25011
  } catch {
23725
- resolve9(null);
25012
+ resolve11(null);
23726
25013
  }
23727
25014
  });
23728
25015
  });
23729
25016
  req.on("error", () => {
23730
- resolve9(null);
25017
+ resolve11(null);
23731
25018
  });
23732
25019
  req.on("timeout", () => {
23733
25020
  req.destroy();
23734
- resolve9(null);
25021
+ resolve11(null);
23735
25022
  });
23736
25023
  });
23737
25024
  }
@@ -23881,14 +25168,14 @@ function parseArgs(argv) {
23881
25168
  console.error("--diff requires two paths: --diff <pathA> <pathB>");
23882
25169
  process.exit(1);
23883
25170
  }
23884
- mode = { type: "diff", pathA: resolve8(args[++i]), pathB: resolve8(args[++i]) };
25171
+ mode = { type: "diff", pathA: resolve10(args[++i]), pathB: resolve10(args[++i]) };
23885
25172
  break;
23886
25173
  }
23887
25174
  case "--port":
23888
25175
  port = parseInt(args[++i], 10);
23889
25176
  break;
23890
25177
  case "--data-dir":
23891
- dataDir = resolve8(args[++i]);
25178
+ dataDir = resolve10(args[++i]);
23892
25179
  break;
23893
25180
  case "--resume":
23894
25181
  resume = true;
@@ -23946,6 +25233,17 @@ function parseArgs(argv) {
23946
25233
  return { mode, port, dataDir, resume, forceUpdateCheck, debug, aiServiceTest, demo, noOpen, strictPort, projectDir, difftoolAction, difftoolLocal, difftoolForce, difftoolServe };
23947
25234
  }
23948
25235
  async function main() {
25236
+ const rawArgs = process.argv.slice(2);
25237
+ if (rawArgs[0] === "note") {
25238
+ const { runNoteCli: runNoteCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
25239
+ try {
25240
+ await runNoteCli2(rawArgs.slice(1));
25241
+ process.exit(0);
25242
+ } catch (err) {
25243
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
25244
+ process.exit(1);
25245
+ }
25246
+ }
23949
25247
  const parsed = parseArgs(process.argv);
23950
25248
  if (!parsed) {
23951
25249
  printUsage();
@@ -23987,19 +25285,19 @@ async function main() {
23987
25285
  console.log("AI service test mode enabled \u2014 using mock AI responses");
23988
25286
  }
23989
25287
  if (debug) {
23990
- console.log(`[debug] Build timestamp: ${"2026-06-17T13:45:47.895Z"}`);
25288
+ console.log(`[debug] Build timestamp: ${"2026-06-19T07:12:20.944Z"}`);
23991
25289
  }
23992
25290
  if (projectDir !== null) {
23993
25291
  process.chdir(projectDir);
23994
25292
  }
23995
25293
  if (dataDir === null) {
23996
- dataDir = join17(process.cwd(), ".glassbox");
25294
+ dataDir = join19(process.cwd(), ".glassbox");
23997
25295
  }
23998
25296
  if (difftoolServe) {
23999
25297
  const { initDifftoolSession: initDifftoolSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24000
25298
  const { writeDiscovery: writeDiscovery2, clearDiscovery: clearDiscovery2, releaseStartingLock: releaseStartingLock2 } = await Promise.resolve().then(() => (init_difftool_discovery(), difftool_discovery_exports));
24001
25299
  const { clearDifftoolBlobs: clearDifftoolBlobs2 } = await Promise.resolve().then(() => (init_blob_store(), blob_store_exports));
24002
- mkdirSync12(dataDir, { recursive: true });
25300
+ mkdirSync13(dataDir, { recursive: true });
24003
25301
  setDataDir(dataDir);
24004
25302
  const sessionDataDir = dataDir;
24005
25303
  clearDifftoolBlobs2(sessionDataDir);
@@ -24034,13 +25332,13 @@ async function main() {
24034
25332
  }
24035
25333
  process.exit(1);
24036
25334
  }
24037
- dataDir = join17(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
25335
+ dataDir = join19(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
24038
25336
  setDemoMode(demo);
24039
25337
  console.log(`
24040
25338
  DEMO MODE: ${scenario.label}
24041
25339
  `);
24042
25340
  }
24043
- mkdirSync12(dataDir, { recursive: true });
25341
+ mkdirSync13(dataDir, { recursive: true });
24044
25342
  if (demo === null) {
24045
25343
  acquireLock(dataDir);
24046
25344
  }
@@ -24062,12 +25360,12 @@ async function main() {
24062
25360
  if (mode.type === "diff") {
24063
25361
  const { pathA, pathB } = mode;
24064
25362
  for (const p of [pathA, pathB]) {
24065
- if (!existsSync14(p)) {
25363
+ if (!existsSync16(p)) {
24066
25364
  console.error(`Error: path does not exist: ${p}`);
24067
25365
  process.exit(1);
24068
25366
  }
24069
25367
  }
24070
- if (statSync3(pathA).isDirectory() !== statSync3(pathB).isDirectory()) {
25368
+ if (statSync5(pathA).isDirectory() !== statSync5(pathB).isDirectory()) {
24071
25369
  console.error("Error: --diff requires two files or two folders, not a mix of both.");
24072
25370
  process.exit(1);
24073
25371
  }