@critiquedotsh/harness 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -71,6 +71,12 @@ Then:
71
71
  critique-code
72
72
  ```
73
73
 
74
+ Or skip the TTY and open a local browser UI (still the same Pi author kernel in this process, bound to `127.0.0.1` only):
75
+
76
+ ```bash
77
+ critique-code web
78
+ ```
79
+
74
80
  Describe the change. Slash lines (`/review`, `/ship`, …) are never sent to the model.
75
81
 
76
82
  ## What a session does
@@ -92,6 +98,7 @@ Interactive TTY sessions keep **stdout quiet**. Pass `--json` for the session en
92
98
  ```text
93
99
  critique-code
94
100
  critique-code chat [--intent <text>] [--models provider/model,...] [--cwd <dir>] [--store <dir>] [--voice] [--json]
101
+ critique-code web [--port <n>] [--cwd <dir>] [--store <dir>] [--models ...]
95
102
  critique-code login
96
103
  critique-code settings | keys | models
97
104
  critique-code review [--depth quick|standard|paranoid] [--focus general,security] [--models ...] [--base <ref>] [--intent <text>] [--cwd <dir>] [--store <dir>]
@@ -105,6 +112,7 @@ critique-code help
105
112
  | Command | Purpose |
106
113
  | --- | --- |
107
114
  | `critique-code` / `chat` | Interactive author session |
115
+ | `web` | Local browser UI on `127.0.0.1` (same author kernel; no TTY required) |
108
116
  | `login` | Browser device approval; connect Critique Inference |
109
117
  | `settings` / `keys` / `models` | TUI for route, stored keys, and model ids |
110
118
  | `review` | Evidence harness (JSON on stdout) |
@@ -126,6 +134,7 @@ critique-code help
126
134
  | `--base <ref>` | Review against a git base ref (not a checkout) |
127
135
  | `--voice` | Start the author session in voice mode |
128
136
  | `--json` | Print the session envelope on stdout even on a TTY |
137
+ | `--port <n>` | Loopback port for `critique-code web` (`0` = ephemeral) |
129
138
  | `-h` / `help` | Help |
130
139
 
131
140
  `critique-code review` is read-only. Use `critique-code repair <review-run-id>` for explicit repair. `--repair apply` on `review` is rejected.
package/dist/cli.js CHANGED
@@ -12884,6 +12884,16 @@ var init_critique_code_runtime_config = __esm({
12884
12884
  });
12885
12885
 
12886
12886
  // lib/finish/critique-code-model-defaults.ts
12887
+ function modelFromInferenceSelection(id4) {
12888
+ const raw = id4?.trim();
12889
+ if (!raw || raw === "auto") return DEFAULT_INFERENCE_MODEL;
12890
+ try {
12891
+ const parsed = parseCritiqueCodeModels(raw.startsWith("critique/") ? raw : `critique/${raw}`)[0];
12892
+ if (parsed?.provider === "critique") return parsed;
12893
+ } catch {
12894
+ }
12895
+ return DEFAULT_INFERENCE_MODEL;
12896
+ }
12887
12897
  function defaultModelForRoute(route) {
12888
12898
  if (route === "critique-inference") return DEFAULT_INFERENCE_MODEL;
12889
12899
  if (route === "anthropic") return DEFAULT_ANTHROPIC_MODEL;
@@ -20251,7 +20261,7 @@ function defaultOpenUrl(url) {
20251
20261
  } catch {
20252
20262
  }
20253
20263
  }
20254
- async function connectCritiqueInference(input) {
20264
+ async function beginCritiqueInferenceLogin(input) {
20255
20265
  const env = input.env ?? process.env;
20256
20266
  const origin = critiqueSiteOrigin(env);
20257
20267
  const fetchImpl = input.io.fetch ?? fetch;
@@ -20273,35 +20283,48 @@ async function connectCritiqueInference(input) {
20273
20283
  (input.io.openUrl ?? defaultOpenUrl)(verifyUrl);
20274
20284
  const intervalMs = Math.max(2, authorization.interval ?? 3) * 1e3;
20275
20285
  const deadline = Date.now() + Math.max(30, authorization.expires_in ?? 600) * 1e3;
20276
- while (Date.now() < deadline) {
20277
- if (input.io.signal?.aborted) throw new Error("Browser sign-in cancelled.");
20278
- await sleep(intervalMs);
20279
- const polled = await fetchImpl(`${origin}/api/new/v1/device/token`, {
20280
- method: "POST",
20281
- headers: { "content-type": "application/json" },
20282
- body: JSON.stringify({ device_code: authorization.device_code }),
20283
- signal: input.io.signal
20284
- });
20285
- const token = await polled.json().catch(() => null);
20286
- if (token?.status === "authorization_pending") continue;
20287
- if (token?.status === "expired") throw new Error("The website approval code expired. Run /login again.");
20288
- if (token?.status === "denied") throw new Error("Website approval was denied.");
20289
- const apiKey = token?.api_key?.trim() || token?.apiKey?.trim();
20290
- if (token?.status === "authorized" && apiKey) {
20291
- await saveCritiqueCodeCredential({ home: input.home, kind: "critique", key: apiKey });
20292
- await saveCritiqueCodeRuntimeSettings({
20293
- home: input.home,
20294
- settings: settingsFromSelection({
20295
- route: "critique-inference",
20296
- author: DEFAULT_INFERENCE_MODEL,
20297
- review: [DEFAULT_INFERENCE_MODEL]
20298
- })
20286
+ const deviceCode = authorization.device_code;
20287
+ const finished = (async () => {
20288
+ while (Date.now() < deadline) {
20289
+ if (input.io.signal?.aborted) throw new Error("Browser sign-in cancelled.");
20290
+ await sleep(intervalMs);
20291
+ const polled = await fetchImpl(`${origin}/api/new/v1/device/token`, {
20292
+ method: "POST",
20293
+ headers: { "content-type": "application/json" },
20294
+ body: JSON.stringify({ device_code: deviceCode }),
20295
+ signal: input.io.signal
20299
20296
  });
20300
- input.io.note("Critique Inference is connected. Default model is critique/auto.");
20301
- return { apiKey, verificationUri: verifyUrl };
20297
+ const token = await polled.json().catch(() => null);
20298
+ if (token?.status === "authorization_pending") continue;
20299
+ if (token?.status === "expired") throw new Error("The website approval code expired. Run /login again.");
20300
+ if (token?.status === "denied") throw new Error("Website approval was denied.");
20301
+ const apiKey = token?.api_key?.trim() || token?.apiKey?.trim();
20302
+ if (token?.status === "authorized" && apiKey) {
20303
+ const author = modelFromInferenceSelection(token.model);
20304
+ await saveCritiqueCodeCredential({ home: input.home, kind: "critique", key: apiKey });
20305
+ await saveCritiqueCodeRuntimeSettings({
20306
+ home: input.home,
20307
+ settings: settingsFromSelection({
20308
+ route: "critique-inference",
20309
+ author,
20310
+ review: [author]
20311
+ })
20312
+ });
20313
+ input.io.note(`Critique Inference is connected. Default model is ${piModelId(author)}.`);
20314
+ return { apiKey, verificationUri: verifyUrl };
20315
+ }
20302
20316
  }
20303
- }
20304
- throw new Error("Timed out waiting for website approval.");
20317
+ throw new Error("Timed out waiting for website approval.");
20318
+ })();
20319
+ return {
20320
+ verificationUri: verifyUrl,
20321
+ userCode: authorization.user_code,
20322
+ finished
20323
+ };
20324
+ }
20325
+ async function connectCritiqueInference(input) {
20326
+ const started = await beginCritiqueInferenceLogin(input);
20327
+ return started.finished;
20305
20328
  }
20306
20329
 
20307
20330
  // lib/finish/critique-code-settings-tui.ts
@@ -20757,14 +20780,16 @@ async function runCritiqueCodeSettingsSession(input) {
20757
20780
  }
20758
20781
  });
20759
20782
  config = await loadCritiqueCodeRuntimeConfig(env);
20760
- route = "critique-inference";
20761
- author = DEFAULT_INFERENCE_MODEL;
20762
- review = [author];
20783
+ route = config.settings?.route ?? "critique-inference";
20784
+ author = config.settings?.author_model ?? DEFAULT_INFERENCE_MODEL;
20785
+ review = config.settings?.review_models ?? [author];
20786
+ screen = "models";
20787
+ modelRole = "author";
20763
20788
  } catch (error) {
20764
20789
  input.ui.note(error instanceof Error ? error.message : String(error));
20790
+ if (exitNested()) break;
20791
+ screen = "home";
20765
20792
  }
20766
- if (input.start === "login") break;
20767
- screen = "home";
20768
20793
  continue;
20769
20794
  }
20770
20795
  if (screen === "keys") {
@@ -20840,8 +20865,12 @@ async function runCritiqueCodeSettingsSession(input) {
20840
20865
  continue;
20841
20866
  }
20842
20867
  if (screen === "models") {
20868
+ const current = piModelId(modelRole === "author" ? author : review[0] ?? author);
20843
20869
  const items = [
20844
- ...modelsForRoute(route),
20870
+ ...modelsForRoute(route).map((item) => ({
20871
+ ...item,
20872
+ description: item.value === current || item.value === current.replace(/^critique\/critique\//, "critique/") ? "current" : item.description
20873
+ })),
20845
20874
  { value: "custom", label: "Enter a model id" },
20846
20875
  { value: "back", label: "Back" }
20847
20876
  ];
@@ -20862,7 +20891,7 @@ async function runCritiqueCodeSettingsSession(input) {
20862
20891
  await persistSettings();
20863
20892
  input.ui.note(`Using ${piModelId(parsed)}.`);
20864
20893
  }
20865
- if (input.start === "models") break;
20894
+ if (exitNested()) break;
20866
20895
  screen = "home";
20867
20896
  continue;
20868
20897
  }
@@ -21119,7 +21148,9 @@ function parseAuthorCommand(line) {
21119
21148
  if (lower === "login" || lower === "/login") return { kind: "settings", start: "login" };
21120
21149
  if (lower === "settings" || lower === "/settings") return { kind: "settings", start: "home" };
21121
21150
  if (lower === "keys" || lower === "/keys") return { kind: "settings", start: "keys" };
21122
- if (lower === "models" || lower === "/models") return { kind: "settings", start: "models" };
21151
+ if (lower === "models" || lower === "/models" || lower === "model" || lower === "/model") {
21152
+ return { kind: "settings", start: "models" };
21153
+ }
21123
21154
  if (lower === "skills" || lower === "/skills") return { kind: "skills" };
21124
21155
  if (lower === "review" || lower === "/review" || lower === "review since" || lower === "/review since") {
21125
21156
  return { kind: "review", mode: "since" };
@@ -21979,8 +22010,9 @@ function authorModelId(model) {
21979
22010
  return piModelId(model);
21980
22011
  }
21981
22012
  function createAuthorLineReader(input) {
21982
- if (Array.isArray(input.lines)) {
21983
- const rows = input.lines;
22013
+ const lines = input.lines;
22014
+ if (Array.isArray(lines)) {
22015
+ const rows = lines;
21984
22016
  let index = 0;
21985
22017
  return {
21986
22018
  async readLine() {
@@ -21995,8 +22027,8 @@ function createAuthorLineReader(input) {
21995
22027
  }
21996
22028
  };
21997
22029
  }
21998
- if (input.lines) {
21999
- const iterator = input.lines[Symbol.asyncIterator]();
22030
+ if (lines != null && !Array.isArray(lines)) {
22031
+ const iterator = lines[Symbol.asyncIterator]();
22000
22032
  return {
22001
22033
  async readLine() {
22002
22034
  const next = await iterator.next();
@@ -22444,6 +22476,10 @@ ${task.prompt}`,
22444
22476
  if (coordinator.session().review_required) {
22445
22477
  await runReviewGate("write_budget");
22446
22478
  }
22479
+ } catch (error) {
22480
+ const text = error instanceof Error ? error.message : String(error);
22481
+ input.stderr.write(`${text.endsWith("\n") ? text : `${text}
22482
+ `}`);
22447
22483
  } finally {
22448
22484
  activity.endTurn();
22449
22485
  reader.resume();
@@ -22626,6 +22662,925 @@ ${task.prompt}`,
22626
22662
  };
22627
22663
  }
22628
22664
 
22665
+ // lib/finish/critique-code-web-server.ts
22666
+ init_change_capsule();
22667
+ import { randomBytes } from "node:crypto";
22668
+ import { createServer } from "node:http";
22669
+ import { basename as basename3 } from "node:path";
22670
+ import { execFileSync, spawn as spawn5 } from "node:child_process";
22671
+
22672
+ // lib/finish/critique-code-line-hub.ts
22673
+ var CritiqueCodeLineHub = class {
22674
+ #queued = [];
22675
+ #wait;
22676
+ #closed = false;
22677
+ get closed() {
22678
+ return this.#closed;
22679
+ }
22680
+ push(line) {
22681
+ if (this.#closed) return false;
22682
+ if (this.#wait) {
22683
+ this.onState?.(true);
22684
+ const waiting = this.#wait;
22685
+ this.#wait = void 0;
22686
+ waiting(line);
22687
+ return true;
22688
+ }
22689
+ this.#queued.push(line);
22690
+ return true;
22691
+ }
22692
+ close() {
22693
+ this.#closed = true;
22694
+ this.#wait?.(void 0);
22695
+ this.#wait = void 0;
22696
+ }
22697
+ async *[Symbol.asyncIterator]() {
22698
+ for (; ; ) {
22699
+ if (this.#queued.length > 0) {
22700
+ this.onState?.(true);
22701
+ yield this.#queued.shift();
22702
+ continue;
22703
+ }
22704
+ if (this.#closed) return;
22705
+ this.onState?.(false);
22706
+ const line = await new Promise((resolve13) => {
22707
+ this.#wait = resolve13;
22708
+ });
22709
+ if (line === void 0) return;
22710
+ yield line;
22711
+ }
22712
+ }
22713
+ };
22714
+
22715
+ // lib/finish/critique-code-web-server.ts
22716
+ init_critique_code_kernel();
22717
+ init_critique_code_model_defaults();
22718
+ init_critique_code_runtime_config();
22719
+
22720
+ // lib/finish/critique-code-web-ui.ts
22721
+ function escapeHtml(value) {
22722
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
22723
+ }
22724
+ function humanizeCritiqueCodeWebError(text) {
22725
+ const raw = text.replace(/\u001b\[[0-9;]*m/g, "").trim();
22726
+ const missing = raw.match(/No API key found for ([a-z0-9._-]+)/i);
22727
+ if (missing) {
22728
+ const provider = missing[1] ?? "this provider";
22729
+ return `No API key for ${provider}. Use Login to connect Critique Inference, or run critique-code keys in the TTY for BYOK, then restart critique-code web.`;
22730
+ }
22731
+ return raw.replace(/(?:file:\/\/)?\S+\.vendor\/node_modules\S+/g, "").replace(/https?:\/\/\S+\/node_modules\/\S+/g, "").replace(/\n[ \t]*\n[ \t]*\n+/g, "\n\n").trim().slice(0, 1200);
22732
+ }
22733
+ function critiqueCodeWebPage(input) {
22734
+ const repositoryName = escapeHtml(input.repository_name);
22735
+ const repositoryRoot = escapeHtml(input.repository_root);
22736
+ const branch = escapeHtml(input.branch);
22737
+ const model = escapeHtml(input.model);
22738
+ const listen = escapeHtml(input.listen);
22739
+ const modelShort = escapeHtml(input.model.replace(/^openrouter\//, "").replace(/^critique\/critique\//, "critique/").replace(/^critique\//, "critique/"));
22740
+ const models = input.models && input.models.length > 0 ? input.models : [{ id: input.model, label: modelShort }];
22741
+ const currentId = input.model.replace(/^critique\/critique\//, "critique/");
22742
+ const options = models.map((entry) => {
22743
+ const selected = entry.id === input.model || entry.id === currentId ? " selected" : "";
22744
+ return `<option value="${escapeHtml(entry.id)}"${selected}>${escapeHtml(entry.label)}</option>`;
22745
+ }).join("");
22746
+ return `<!DOCTYPE html>
22747
+ <html lang="en">
22748
+ <head>
22749
+ <meta charset="utf-8">
22750
+ <meta name="viewport" content="width=device-width, initial-scale=1">
22751
+ <title>CritiqueCode</title>
22752
+ <style>
22753
+ :root {
22754
+ --bg: #0e0e0e;
22755
+ --rail: #111111;
22756
+ --panel: #161616;
22757
+ --prompt: #1c1c1c;
22758
+ --line: rgba(255,255,255,.08);
22759
+ --fg: #ececec;
22760
+ --muted: #8d8d8d;
22761
+ --faint: #5c5c5c;
22762
+ --live: #3dd68c;
22763
+ --warn: #d4a017;
22764
+ --err: #e36d6d;
22765
+ --ease: cubic-bezier(.32,.72,0,1);
22766
+ }
22767
+ * { box-sizing: border-box; }
22768
+ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg);
22769
+ font-family: "SF Pro Text", "Segoe UI", ui-sans-serif, system-ui, sans-serif;
22770
+ font-size: 14px; letter-spacing: -0.011em; }
22771
+ button, textarea { font: inherit; color: inherit; }
22772
+ button { cursor: pointer; }
22773
+ .shell { display: grid; grid-template-columns: 56px 1fr; min-height: 100dvh; }
22774
+ .rail { background: var(--rail); border-right: 1px solid var(--line); display: flex; flex-direction: column; align-items: center; padding: 14px 0; gap: 12px; }
22775
+ .mark { width: 30px; height: 30px; border-radius: 8px; background: #1b1b1b; border: 1px solid var(--line); display: grid; place-items: center; font-size: 12px; font-weight: 600; letter-spacing: .04em; }
22776
+ .rail .plus { width: 30px; height: 30px; border-radius: 8px; background: transparent; border: 1px solid var(--line); color: var(--muted); display: grid; place-items: center; transition: border-color .2s var(--ease), color .2s var(--ease); }
22777
+ .rail .plus:hover { color: var(--fg); border-color: rgba(255,255,255,.2); }
22778
+ .stage { display: grid; grid-template-rows: auto 1fr auto; min-width: 0; min-height: 100dvh; }
22779
+ header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 22px 8px; }
22780
+ .session { display: flex; align-items: center; gap: 10px; color: var(--fg); font-size: 13px; font-weight: 500; }
22781
+ .session svg { opacity: .55; }
22782
+ .badges { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
22783
+ .badge { display: inline-flex; align-items: center; gap: 7px; padding: 5px 10px; border-radius: 999px; background: #181818; border: 1px solid var(--line); color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
22784
+ .badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--live); box-shadow: 0 0 0 3px rgba(61,214,140,.12); }
22785
+ .badge.model { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22786
+ main { min-height: 0; overflow: auto; }
22787
+ #empty { max-width: 560px; margin: 12vh auto 0; padding: 0 24px 40px; }
22788
+ #empty h1 { margin: 0 0 28px; font-size: 28px; font-weight: 500; color: #6e6e6e; letter-spacing: -.03em; }
22789
+ .meta { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; color: var(--muted); font-size: 13px; }
22790
+ .meta li { display: grid; grid-template-columns: 18px 1fr; gap: 12px; align-items: start; }
22791
+ .meta svg { margin-top: 2px; opacity: .7; }
22792
+ .meta strong { color: var(--fg); font-weight: 500; word-break: break-all; }
22793
+ #feed { max-width: 720px; margin: 0 auto; padding: 12px 24px 28px; display: none; }
22794
+ #feed.on { display: block; }
22795
+ .turn { margin: 0 0 28px; }
22796
+ .prompt { background: var(--prompt); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
22797
+ .steps { margin: 10px 2px 0; color: var(--faint); font-size: 12px; }
22798
+ .steps summary { cursor: pointer; list-style: none; display: flex; justify-content: space-between; gap: 12px; }
22799
+ .steps summary::-webkit-details-marker { display: none; }
22800
+ .steps[open] summary { color: var(--muted); }
22801
+ .step-list { margin: 8px 0 0; padding: 10px 12px; background: #141414; border-radius: 10px; border: 1px solid var(--line); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); white-space: pre-wrap; word-break: break-word; }
22802
+ .reply { margin: 14px 2px 0; line-height: 1.6; white-space: pre-wrap; word-break: break-word; color: #dedede; }
22803
+ .reply h { display: block; font-weight: 600; color: #fff; margin: 0 0 8px; font-size: 15px; }
22804
+ .note, .alert, .review-card { margin: 14px 0; padding: 12px 14px; border-radius: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
22805
+ .note { color: var(--muted); }
22806
+ .alert { background: rgba(227,109,109,.08); color: #f0c7c7; border: 1px solid rgba(227,109,109,.18); }
22807
+ .review-card { background: #141414; border: 1px solid var(--line); color: var(--muted); font-size: 13px; }
22808
+ .dock { padding: 0 20px 22px; }
22809
+ .composer { max-width: 720px; margin: 0 auto; background: var(--panel); border: 1px solid var(--line); border-radius: 18px; padding: 6px 6px 8px; box-shadow: 0 18px 40px rgba(0,0,0,.28); }
22810
+ textarea { width: 100%; min-height: 56px; max-height: 200px; resize: none; background: transparent; border: 0; padding: 12px 14px 4px; outline: none; line-height: 1.45; }
22811
+ textarea::placeholder { color: #6a6a6a; }
22812
+ .bar { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 4px 6px 4px 8px; }
22813
+ .cmds { display: flex; flex-wrap: wrap; gap: 2px; min-width: 0; }
22814
+ .cmds button { background: transparent; border: 0; color: var(--muted); padding: 5px 8px; border-radius: 8px; font-size: 12px; }
22815
+ .cmds button:hover { background: rgba(255,255,255,.05); color: var(--fg); }
22816
+ .bar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
22817
+ .model-chip { color: var(--muted); font-size: 12px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22818
+ #model { max-width: 220px; background: transparent; border: 0; color: var(--muted); font-size: 12px; padding: 4px 0; outline: none; }
22819
+ #model:hover, #model:focus { color: var(--fg); }
22820
+ #send { width: 34px; height: 34px; border: 0; border-radius: 10px; background: #ececec; color: #111; display: grid; place-items: center; transition: transform .18s var(--ease), opacity .18s var(--ease); }
22821
+ #send:hover { transform: translateY(-1px); }
22822
+ #send:disabled, textarea:disabled { opacity: .4; cursor: not-allowed; }
22823
+ #modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); align-items: center; justify-content: center; padding: 24px; z-index: 4; }
22824
+ #modal.open { display: flex; }
22825
+ .dialog { width: min(520px, 100%); background: #161616; border: 1px solid var(--line); border-radius: 16px; padding: 18px; }
22826
+ .dialog h3 { margin: 0 0 8px; font-size: 15px; font-weight: 600; }
22827
+ .dialog .purpose { color: var(--muted); font-size: 13px; }
22828
+ .preview { max-height: 240px; overflow: auto; white-space: pre-wrap; word-break: break-word; margin: 12px 0 16px; padding: 12px; background: #101010; border-radius: 10px; font-family: ui-monospace, Menlo, monospace; font-size: 12px; color: var(--muted); }
22829
+ .modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
22830
+ .modal-actions button { border: 1px solid var(--line); background: #1c1c1c; border-radius: 10px; padding: 8px 12px; }
22831
+ .modal-actions .deny { color: var(--err); }
22832
+ .modal-actions #approve { background: #ececec; color: #111; border-color: transparent; }
22833
+ @media (max-width: 720px) {
22834
+ .shell { grid-template-columns: 1fr; }
22835
+ .rail { display: none; }
22836
+ header { padding: 12px 16px 4px; }
22837
+ .dock { padding: 0 12px 16px; }
22838
+ }
22839
+ </style>
22840
+ </head>
22841
+ <body>
22842
+ <div class="shell">
22843
+ <aside class="rail">
22844
+ <div class="mark" title="CritiqueCode">C</div>
22845
+ <button type="button" class="plus" id="focus-composer" title="Focus composer" aria-label="Focus composer">
22846
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2v10M2 7h10" stroke="currentColor" stroke-width="1.4"/></svg>
22847
+ </button>
22848
+ </aside>
22849
+ <div class="stage">
22850
+ <header>
22851
+ <div class="session">
22852
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 4h10v8H2zM4 4V3a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="1.2"/></svg>
22853
+ New session
22854
+ </div>
22855
+ <div class="badges">
22856
+ <span class="badge"><span class="dot"></span>${listen}</span>
22857
+ <span class="badge" id="files-badge" hidden></span>
22858
+ <span class="badge model" id="model-badge" title="${model}">${modelShort}</span>
22859
+ </div>
22860
+ </header>
22861
+ <main>
22862
+ <div id="empty">
22863
+ <h1>New session</h1>
22864
+ <ul class="meta">
22865
+ <li>
22866
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2.5 4.5h4l1.5 1.5H13.5v7H2.5z" stroke="currentColor" stroke-width="1.2"/></svg>
22867
+ <span><strong>${repositoryRoot}</strong></span>
22868
+ </li>
22869
+ <li>
22870
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 12V8m0 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm8-4v4m0 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM6 8h4" stroke="currentColor" stroke-width="1.2"/></svg>
22871
+ <span>Main branch (<strong>${branch}</strong>)</span>
22872
+ </li>
22873
+ <li>
22874
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M8 5.5V8l2 1.5" stroke="currentColor" stroke-width="1.2"/></svg>
22875
+ <span>Loopback only \xB7 ${repositoryName}</span>
22876
+ </li>
22877
+ </ul>
22878
+ </div>
22879
+ <div id="feed"></div>
22880
+ </main>
22881
+ <div class="dock">
22882
+ <div class="composer">
22883
+ <textarea id="input" rows="2" placeholder="Ask anything\u2026"></textarea>
22884
+ <div class="bar">
22885
+ <div class="cmds">
22886
+ <button type="button" data-cmd="/review">Review</button>
22887
+ <button type="button" data-cmd="/review all">Review all</button>
22888
+ <button type="button" data-cmd="/repair">Repair</button>
22889
+ <button type="button" data-cmd="/ship">Ship</button>
22890
+ <button type="button" data-cmd="/skills">Skills</button>
22891
+ <button type="button" data-cmd="/login" id="login">Login</button>
22892
+ <button type="button" data-cmd="/exit">Exit</button>
22893
+ </div>
22894
+ <div class="bar-right">
22895
+ <select id="model" aria-label="Author model">${options}</select>
22896
+ <button type="button" id="send" aria-label="Send">
22897
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 11V3M3.5 6.5 7 3l3.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
22898
+ </button>
22899
+ </div>
22900
+ </div>
22901
+ </div>
22902
+ </div>
22903
+ </div>
22904
+ </div>
22905
+ <div id="modal">
22906
+ <div class="dialog">
22907
+ <h3>Run this command?</h3>
22908
+ <div class="purpose" id="exec-purpose"></div>
22909
+ <div class="preview" id="exec-preview"></div>
22910
+ <div class="modal-actions">
22911
+ <button type="button" class="deny" id="deny">Deny</button>
22912
+ <button type="button" id="approve">Approve</button>
22913
+ </div>
22914
+ </div>
22915
+ </div>
22916
+ <script>
22917
+ window.CRITIQUE_CODE_TOKEN = "__CRITIQUE_CODE_TOKEN__";
22918
+ (function () {
22919
+ var TOKEN = window.CRITIQUE_CODE_TOKEN;
22920
+ var emptyEl = document.getElementById("empty");
22921
+ var feedEl = document.getElementById("feed");
22922
+ var filesBadge = document.getElementById("files-badge");
22923
+ var modelEl = document.getElementById("model");
22924
+ var modelBadge = document.getElementById("model-badge");
22925
+ var inputEl = document.getElementById("input");
22926
+ var sendEl = document.getElementById("send");
22927
+ var modalEl = document.getElementById("modal");
22928
+ var purposeEl = document.getElementById("exec-purpose");
22929
+ var previewEl = document.getElementById("exec-preview");
22930
+ var busy = false;
22931
+ var turnEl = null;
22932
+ var assistantEl = null;
22933
+ var pendingExecId = null;
22934
+ var es = null;
22935
+ var reconnectTimer = null;
22936
+
22937
+ function headers() {
22938
+ return { "content-type": "application/json", "x-critique-code-token": TOKEN };
22939
+ }
22940
+
22941
+ function api(method, path, body) {
22942
+ return fetch(path, {
22943
+ method: method,
22944
+ headers: headers(),
22945
+ body: body ? JSON.stringify(body) : undefined
22946
+ });
22947
+ }
22948
+
22949
+ function showFeed() {
22950
+ emptyEl.style.display = "none";
22951
+ feedEl.className = "on";
22952
+ }
22953
+
22954
+ function setModel(id) {
22955
+ if (!id) return;
22956
+ if (modelEl) {
22957
+ modelEl.value = id;
22958
+ if (modelEl.value !== id) {
22959
+ for (var i = 0; i < modelEl.options.length; i++) {
22960
+ if (modelEl.options[i].value === id || "critique/" + modelEl.options[i].value === id) {
22961
+ modelEl.selectedIndex = i;
22962
+ break;
22963
+ }
22964
+ }
22965
+ }
22966
+ }
22967
+ if (modelBadge) {
22968
+ var option = modelEl && modelEl.options[modelEl.selectedIndex];
22969
+ var label = option && option.text ? option.text : String(id).replace(/^openrouter//, "").replace(/^critique/critique//, "critique/");
22970
+ modelBadge.textContent = label;
22971
+ modelBadge.title = id;
22972
+ }
22973
+ }
22974
+
22975
+ function setBusy(next) {
22976
+ busy = !!next;
22977
+ sendEl.disabled = busy;
22978
+ inputEl.disabled = busy;
22979
+ if (modelEl) modelEl.disabled = busy;
22980
+ }
22981
+
22982
+ function scrollMain() {
22983
+ var main = document.querySelector("main");
22984
+ if (main) main.scrollTop = main.scrollHeight;
22985
+ }
22986
+
22987
+ function addBlock(className, text) {
22988
+ showFeed();
22989
+ turnEl = null;
22990
+ assistantEl = null;
22991
+ var node = document.createElement("div");
22992
+ node.className = className;
22993
+ node.textContent = text == null ? "" : String(text);
22994
+ feedEl.appendChild(node);
22995
+ scrollMain();
22996
+ return node;
22997
+ }
22998
+
22999
+ function startTurn(text) {
23000
+ showFeed();
23001
+ assistantEl = null;
23002
+ turnEl = document.createElement("div");
23003
+ turnEl.className = "turn";
23004
+ var prompt = document.createElement("div");
23005
+ prompt.className = "prompt";
23006
+ prompt.textContent = text;
23007
+ turnEl.appendChild(prompt);
23008
+ feedEl.appendChild(turnEl);
23009
+ scrollMain();
23010
+ }
23011
+
23012
+ function appendAssistant(text) {
23013
+ if (!turnEl) {
23014
+ showFeed();
23015
+ turnEl = document.createElement("div");
23016
+ turnEl.className = "turn";
23017
+ feedEl.appendChild(turnEl);
23018
+ }
23019
+ if (!assistantEl) {
23020
+ assistantEl = document.createElement("div");
23021
+ assistantEl.className = "reply";
23022
+ turnEl.appendChild(assistantEl);
23023
+ }
23024
+ assistantEl.textContent = (assistantEl.textContent || "") + (text || "");
23025
+ scrollMain();
23026
+ }
23027
+
23028
+ function addTool(name, phase, detail) {
23029
+ if (!turnEl) {
23030
+ showFeed();
23031
+ turnEl = document.createElement("div");
23032
+ turnEl.className = "turn";
23033
+ feedEl.appendChild(turnEl);
23034
+ }
23035
+ var steps = turnEl.querySelector(".steps");
23036
+ if (!steps) {
23037
+ steps = document.createElement("details");
23038
+ steps.className = "steps";
23039
+ var summary = document.createElement("summary");
23040
+ summary.innerHTML = "<span>Show steps</span><span class=\\"dur\\"></span>";
23041
+ var list = document.createElement("div");
23042
+ list.className = "step-list";
23043
+ steps.appendChild(summary);
23044
+ steps.appendChild(list);
23045
+ if (assistantEl) turnEl.insertBefore(steps, assistantEl);
23046
+ else turnEl.appendChild(steps);
23047
+ }
23048
+ var listEl = steps.querySelector(".step-list");
23049
+ var line = (name || "tool") + " " + (phase || "");
23050
+ if (detail) line += "\\n" + detail;
23051
+ listEl.textContent = (listEl.textContent ? listEl.textContent + "\\n" : "") + line;
23052
+ scrollMain();
23053
+ }
23054
+
23055
+ function setPaths(paths) {
23056
+ if (!paths || !paths.length) {
23057
+ filesBadge.hidden = true;
23058
+ filesBadge.textContent = "";
23059
+ return;
23060
+ }
23061
+ filesBadge.hidden = false;
23062
+ filesBadge.textContent = paths.length === 1 ? "1 file" : String(paths.length) + " files";
23063
+ filesBadge.title = paths.join("\\n");
23064
+ }
23065
+
23066
+ function setReview(text) {
23067
+ if (!text) return;
23068
+ addBlock("review-card", text);
23069
+ }
23070
+
23071
+ function showExec(req) {
23072
+ if (!req) {
23073
+ modalEl.className = "";
23074
+ pendingExecId = null;
23075
+ return;
23076
+ }
23077
+ pendingExecId = req.id;
23078
+ purposeEl.textContent = req.purpose || "";
23079
+ previewEl.textContent = req.preview || "";
23080
+ modalEl.className = "open";
23081
+ }
23082
+
23083
+ function handleEvent(ev) {
23084
+ if (!ev || !ev.type) return;
23085
+ switch (ev.type) {
23086
+ case "note":
23087
+ addBlock("note", ev.text);
23088
+ break;
23089
+ case "user":
23090
+ startTurn(ev.text);
23091
+ break;
23092
+ case "assistant_delta":
23093
+ appendAssistant(ev.text);
23094
+ break;
23095
+ case "tool":
23096
+ addTool(ev.name, ev.phase, ev.detail);
23097
+ break;
23098
+ case "exec_request":
23099
+ showExec({ id: ev.id, purpose: ev.purpose, preview: ev.preview });
23100
+ break;
23101
+ case "exec_resolved":
23102
+ if (pendingExecId === ev.id) showExec(null);
23103
+ addBlock("note", ev.decision === "approve" ? "Run approved. Output is not Evidence." : "Run denied.");
23104
+ break;
23105
+ case "review":
23106
+ setReview((ev.text || "") + (ev.conclusion ? "\\n" + ev.conclusion : ""));
23107
+ break;
23108
+ case "model":
23109
+ setModel(ev.text);
23110
+ break;
23111
+ case "busy":
23112
+ setBusy(ev.busy);
23113
+ break;
23114
+ case "ended":
23115
+ setBusy(false);
23116
+ addBlock("note", ev.reason ? "Session ended \xB7 " + ev.reason : "Session ended");
23117
+ break;
23118
+ case "error":
23119
+ addBlock("alert", ev.text);
23120
+ break;
23121
+ }
23122
+ }
23123
+
23124
+ function applySnapshot(s) {
23125
+ if (!s) return;
23126
+ if (s.model) setModel(s.model);
23127
+ setBusy(s.busy);
23128
+ setPaths(s.changed_paths);
23129
+ if (s.last_review_text) setReview(s.last_review_text);
23130
+ if (s.pending_exec) showExec(s.pending_exec);
23131
+ }
23132
+
23133
+ function connect() {
23134
+ if (es) {
23135
+ es.close();
23136
+ es = null;
23137
+ }
23138
+ es = new EventSource("/api/events?token=" + encodeURIComponent(TOKEN));
23139
+ es.onmessage = function (msg) {
23140
+ try { handleEvent(JSON.parse(msg.data)); } catch (e) {}
23141
+ };
23142
+ es.onerror = function () {
23143
+ if (es) { es.close(); es = null; }
23144
+ if (reconnectTimer) clearTimeout(reconnectTimer);
23145
+ reconnectTimer = setTimeout(connect, 1200);
23146
+ };
23147
+ }
23148
+
23149
+ function startLogin() {
23150
+ api("POST", "/api/login", {}).then(function (res) {
23151
+ return res.json().then(function (body) {
23152
+ if (!res.ok) addBlock("alert", body.error || "Could not start Login.");
23153
+ });
23154
+ }).catch(function () {
23155
+ addBlock("alert", "Could not start Login.");
23156
+ });
23157
+ }
23158
+
23159
+ function submitText(raw) {
23160
+ var text = (raw == null ? inputEl.value : raw).trim();
23161
+ if (!text || busy) return;
23162
+ if (text === "/login" || text === "login") {
23163
+ if (raw == null) inputEl.value = "";
23164
+ startLogin();
23165
+ return;
23166
+ }
23167
+ var isCmd = text.charAt(0) === "/";
23168
+ if (raw == null) inputEl.value = "";
23169
+ api("POST", isCmd ? "/api/command" : "/api/prompt", { text: text }).then(function (res) {
23170
+ if (!res.ok) addBlock("alert", "Request failed");
23171
+ }).catch(function () {
23172
+ addBlock("alert", "Request failed");
23173
+ });
23174
+ }
23175
+
23176
+ function decide(decision) {
23177
+ if (!pendingExecId) return;
23178
+ var id = pendingExecId;
23179
+ api("POST", "/api/exec", { id: id, decision: decision }).then(function () {
23180
+ showExec(null);
23181
+ }).catch(function () {
23182
+ addBlock("alert", "Could not resolve the run.");
23183
+ });
23184
+ }
23185
+
23186
+ sendEl.addEventListener("click", function () { submitText(null); });
23187
+ document.getElementById("focus-composer").addEventListener("click", function () {
23188
+ inputEl.focus();
23189
+ });
23190
+ inputEl.addEventListener("keydown", function (e) {
23191
+ if (e.key === "Enter" && !e.shiftKey) {
23192
+ e.preventDefault();
23193
+ submitText(null);
23194
+ }
23195
+ });
23196
+ document.querySelectorAll(".cmds button").forEach(function (btn) {
23197
+ btn.addEventListener("click", function () {
23198
+ var cmd = btn.getAttribute("data-cmd");
23199
+ if (cmd === "/login") {
23200
+ startLogin();
23201
+ return;
23202
+ }
23203
+ submitText(cmd);
23204
+ });
23205
+ });
23206
+ document.getElementById("approve").addEventListener("click", function () { decide("approve"); });
23207
+ document.getElementById("deny").addEventListener("click", function () { decide("deny"); });
23208
+ if (modelEl) {
23209
+ modelEl.addEventListener("change", function () {
23210
+ var id = modelEl.value;
23211
+ if (!id || busy) return;
23212
+ api("POST", "/api/model", { id: id }).then(function (res) {
23213
+ return res.json().then(function (body) {
23214
+ if (!res.ok) addBlock("alert", body.error || "Could not change model.");
23215
+ else setModel(body.model || id);
23216
+ });
23217
+ }).catch(function () {
23218
+ addBlock("alert", "Could not change model.");
23219
+ });
23220
+ });
23221
+ }
23222
+
23223
+ api("GET", "/api/snapshot").then(function (res) { return res.json(); }).then(applySnapshot).catch(function () {});
23224
+ connect();
23225
+ })();
23226
+ </script>
23227
+ </body>
23228
+ </html>
23229
+ `;
23230
+ }
23231
+
23232
+ // lib/finish/critique-code-web-server.ts
23233
+ var KERNEL_REQUIRED3 = "Interactive author requires a live driver and at least one model. Run /login to connect Critique Inference in the browser, or add a BYOK key with /keys. Default is Critique Inference critique/auto. This is not a fake chat.";
23234
+ function gitBranch(root) {
23235
+ try {
23236
+ return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
23237
+ cwd: root,
23238
+ encoding: "utf8",
23239
+ timeout: 2e3
23240
+ }).trim() || "unknown";
23241
+ } catch {
23242
+ return "unknown";
23243
+ }
23244
+ }
23245
+ async function startCritiqueCodeWebServer(input) {
23246
+ const host = input.host ?? "127.0.0.1";
23247
+ if (host !== "127.0.0.1" && host !== "localhost") {
23248
+ throw new Error("CritiqueCode web UI binds only to 127.0.0.1. LAN listen is not allowed.");
23249
+ }
23250
+ const bindHost = "127.0.0.1";
23251
+ const env = input.env ?? process.env;
23252
+ let pendingModelChoice;
23253
+ let model = "critique/auto";
23254
+ let modelChoices = [];
23255
+ try {
23256
+ const config = await loadCritiqueCodeRuntimeConfig(env);
23257
+ const authorModels = input.author_models && input.author_models.length > 0 ? [...input.author_models] : await resolveHarnessModels({ role: "author", env, config });
23258
+ const driver = await resolveOptionalPiDriver({ driver: input.driver, env });
23259
+ if (authorModels.length === 0 || !driver) {
23260
+ return { kind: "kernel_required", limitation: KERNEL_REQUIRED3 };
23261
+ }
23262
+ model = piModelId(authorModels[0]);
23263
+ const route = config.settings?.route ?? "critique-inference";
23264
+ modelChoices = modelsForRoute(route).map((item) => ({ id: item.value, label: item.label }));
23265
+ } catch (error) {
23266
+ return { kind: "kernel_required", limitation: error instanceof Error ? error.message : String(error) };
23267
+ }
23268
+ const repositoryRoot = await findRepositoryRoot(input.cwd);
23269
+ const token = randomBytes(18).toString("base64url");
23270
+ const hub = new CritiqueCodeLineHub();
23271
+ const clients = /* @__PURE__ */ new Set();
23272
+ const execs = /* @__PURE__ */ new Map();
23273
+ let busy = false;
23274
+ let ended;
23275
+ const changedPaths = [];
23276
+ let lastReviewText = "";
23277
+ let pendingExec;
23278
+ const emit = (event) => {
23279
+ const payload = `data: ${JSON.stringify(event)}
23280
+
23281
+ `;
23282
+ for (const client of clients) client.write(payload);
23283
+ };
23284
+ hub.onState = (nextBusy) => {
23285
+ busy = nextBusy;
23286
+ emit({ type: "busy", busy });
23287
+ };
23288
+ const mapKernel = (event) => {
23289
+ if (event.type === "text_delta" && event.text) {
23290
+ emit({ type: "assistant_delta", text: event.text });
23291
+ return;
23292
+ }
23293
+ if (event.type === "tool_started" || event.type === "tool_completed") {
23294
+ if (event.tool_name === "critique_write_file" && event.detail && event.type === "tool_completed") {
23295
+ if (!changedPaths.includes(event.detail)) changedPaths.push(event.detail);
23296
+ }
23297
+ emit({
23298
+ type: "tool",
23299
+ name: event.tool_name,
23300
+ detail: event.detail,
23301
+ phase: event.type === "tool_started" ? "started" : "completed"
23302
+ });
23303
+ return;
23304
+ }
23305
+ if (event.type === "session_failed") {
23306
+ emit({ type: "error", text: humanizeCritiqueCodeWebError(event.error ?? event.detail ?? "session failed") });
23307
+ }
23308
+ };
23309
+ const approveExec = (request) => new Promise((resolve13) => {
23310
+ const id4 = randomBytes(9).toString("hex");
23311
+ const pending = { id: id4, purpose: request.purpose, preview: request.preview, resolve: resolve13 };
23312
+ execs.set(id4, pending);
23313
+ pendingExec = { id: id4, purpose: request.purpose, preview: request.preview };
23314
+ emit({ type: "exec_request", id: id4, purpose: request.purpose, preview: request.preview });
23315
+ });
23316
+ const snapshot = () => ({
23317
+ repository_root: repositoryRoot,
23318
+ model,
23319
+ models: modelChoices,
23320
+ busy,
23321
+ changed_paths: [...changedPaths],
23322
+ last_review_text: lastReviewText,
23323
+ ...pendingExec ? { pending_exec: pendingExec } : {},
23324
+ ...ended ? { ended } : {}
23325
+ });
23326
+ const originOk = (req, port) => {
23327
+ const origin = req.headers.origin;
23328
+ if (!origin) return true;
23329
+ return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
23330
+ };
23331
+ const tokenOk = (req) => {
23332
+ const header = req.headers["x-critique-code-token"];
23333
+ if (typeof header === "string" && header === token) return true;
23334
+ const url2 = new URL(req.url ?? "/", "http://127.0.0.1");
23335
+ return url2.searchParams.get("token") === token;
23336
+ };
23337
+ const readJson = async (req) => {
23338
+ const chunks = [];
23339
+ for await (const chunk of req) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
23340
+ if (chunks.length === 0) return {};
23341
+ try {
23342
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
23343
+ return parsed && typeof parsed === "object" ? parsed : {};
23344
+ } catch {
23345
+ return {};
23346
+ }
23347
+ };
23348
+ const send = (res, status, body, contentType = "application/json; charset=utf-8") => {
23349
+ const text = typeof body === "string" ? body : JSON.stringify(body);
23350
+ res.writeHead(status, {
23351
+ "content-type": contentType,
23352
+ "cache-control": "no-store"
23353
+ });
23354
+ res.end(text);
23355
+ };
23356
+ const server = createServer(async (req, res) => {
23357
+ const port = listeningPort();
23358
+ const url2 = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
23359
+ const path2 = url2.pathname;
23360
+ if (req.method === "GET" && path2 === "/") {
23361
+ const html = critiqueCodeWebPage({
23362
+ repository_name: basename3(repositoryRoot),
23363
+ repository_root: repositoryRoot,
23364
+ branch: gitBranch(repositoryRoot),
23365
+ model,
23366
+ models: modelChoices,
23367
+ listen: `127.0.0.1:${port}`
23368
+ }).replaceAll("__CRITIQUE_CODE_TOKEN__", token);
23369
+ send(res, 200, html, "text/html; charset=utf-8");
23370
+ return;
23371
+ }
23372
+ if (req.method === "GET" && path2 === "/api/events") {
23373
+ if (!tokenOk(req)) {
23374
+ send(res, 401, { error: "unauthorized" });
23375
+ return;
23376
+ }
23377
+ res.writeHead(200, {
23378
+ "content-type": "text/event-stream; charset=utf-8",
23379
+ "cache-control": "no-store",
23380
+ connection: "keep-alive"
23381
+ });
23382
+ res.write(`data: ${JSON.stringify({ type: "busy", busy })}
23383
+
23384
+ `);
23385
+ clients.add(res);
23386
+ req.on("close", () => {
23387
+ clients.delete(res);
23388
+ });
23389
+ return;
23390
+ }
23391
+ if (!tokenOk(req)) {
23392
+ send(res, 401, { error: "unauthorized" });
23393
+ return;
23394
+ }
23395
+ if (!originOk(req, port)) {
23396
+ send(res, 403, { error: "origin refused" });
23397
+ return;
23398
+ }
23399
+ if (req.method === "GET" && path2 === "/api/snapshot") {
23400
+ send(res, 200, snapshot());
23401
+ return;
23402
+ }
23403
+ if (req.method === "POST" && (path2 === "/api/prompt" || path2 === "/api/command")) {
23404
+ const body = await readJson(req);
23405
+ const text = String(body.text ?? body.prompt ?? "").trim();
23406
+ if (!text) {
23407
+ send(res, 400, { error: "text required" });
23408
+ return;
23409
+ }
23410
+ if (ended) {
23411
+ send(res, 409, { error: "session ended" });
23412
+ return;
23413
+ }
23414
+ emit({ type: "user", text });
23415
+ const queued = hub.push(text);
23416
+ send(res, queued ? 202 : 409, { ok: queued });
23417
+ return;
23418
+ }
23419
+ if (req.method === "POST" && path2 === "/api/exec") {
23420
+ const body = await readJson(req);
23421
+ const id4 = String(body.id ?? "");
23422
+ const decision = body.decision === "approve" ? "approve" : "deny";
23423
+ const pending = execs.get(id4);
23424
+ if (!pending) {
23425
+ send(res, 404, { error: "unknown exec" });
23426
+ return;
23427
+ }
23428
+ execs.delete(id4);
23429
+ pendingExec = void 0;
23430
+ pending.resolve(decision);
23431
+ emit({ type: "exec_resolved", id: id4, decision });
23432
+ send(res, 200, { ok: true });
23433
+ return;
23434
+ }
23435
+ if (req.method === "POST" && path2 === "/api/login") {
23436
+ try {
23437
+ const started = await beginCritiqueInferenceLogin({
23438
+ home: critiqueCodeHome(env),
23439
+ env,
23440
+ io: {
23441
+ note: (message) => emit({ type: "note", text: message })
23442
+ }
23443
+ });
23444
+ send(res, 202, { ok: true, verification_uri: started.verificationUri });
23445
+ void started.finished.then(() => {
23446
+ emit({ type: "note", text: "Inference is connected. The author model is the one you picked on the website. Restart critique-code web if this session still shows the previous model." });
23447
+ }).catch((error) => {
23448
+ const text = error instanceof Error ? error.message : String(error);
23449
+ emit({ type: "error", text: humanizeCritiqueCodeWebError(text) });
23450
+ });
23451
+ } catch (error) {
23452
+ const text = error instanceof Error ? error.message : String(error);
23453
+ emit({ type: "error", text: humanizeCritiqueCodeWebError(text) });
23454
+ send(res, 400, { error: humanizeCritiqueCodeWebError(text) });
23455
+ }
23456
+ return;
23457
+ }
23458
+ if (req.method === "GET" && path2 === "/api/models") {
23459
+ send(res, 200, { model, models: modelChoices });
23460
+ return;
23461
+ }
23462
+ if (req.method === "POST" && path2 === "/api/model") {
23463
+ const body = await readJson(req);
23464
+ const id4 = String(body.id ?? body.model ?? "").trim();
23465
+ const parsed = parseCritiqueCodeModels(id4)[0];
23466
+ if (!parsed) {
23467
+ send(res, 400, { error: "Unknown model." });
23468
+ return;
23469
+ }
23470
+ const config = await loadCritiqueCodeRuntimeConfig(env);
23471
+ const route = config.settings?.route ?? "critique-inference";
23472
+ await saveCritiqueCodeRuntimeSettings({
23473
+ home: critiqueCodeHome(env),
23474
+ settings: settingsFromSelection({
23475
+ route,
23476
+ author: parsed,
23477
+ review: [parsed],
23478
+ ...config.settings?.custom_openai ? { custom_openai: config.settings.custom_openai } : {}
23479
+ })
23480
+ });
23481
+ model = piModelId(parsed);
23482
+ emit({ type: "model", text: model });
23483
+ if (!busy && !ended) {
23484
+ pendingModelChoice = id4;
23485
+ hub.push("/models");
23486
+ }
23487
+ send(res, 200, { ok: true, model });
23488
+ return;
23489
+ }
23490
+ send(res, 404, { error: "not found" });
23491
+ });
23492
+ let portValue = 0;
23493
+ const listeningPort = () => portValue;
23494
+ await new Promise((resolve13, reject) => {
23495
+ server.once("error", reject);
23496
+ server.listen(input.port ?? 0, bindHost, () => {
23497
+ const address = server.address();
23498
+ if (!address || typeof address === "string") {
23499
+ reject(new Error("CritiqueCode web UI failed to bind loopback."));
23500
+ return;
23501
+ }
23502
+ portValue = address.port;
23503
+ resolve13();
23504
+ });
23505
+ });
23506
+ const url = `http://127.0.0.1:${portValue}`;
23507
+ input.stderr.write(`CritiqueCode web UI ${url}
23508
+ Loopback only. The author session stays in this process.
23509
+ `);
23510
+ const session = runLocalCritiqueCodeAuthor({
23511
+ cwd: repositoryRoot,
23512
+ env,
23513
+ stdout: input.stdout,
23514
+ stderr: {
23515
+ write(chunk) {
23516
+ input.stderr.write(chunk);
23517
+ const text = chunk.trim();
23518
+ if (!text) return;
23519
+ if (/evidence harness|checkpoint|Repair Portfolio|none_promoted|not a correctness proof|transcript is withheld/i.test(text)) {
23520
+ lastReviewText = text.slice(0, 4e3);
23521
+ emit({ type: "review", text: lastReviewText });
23522
+ }
23523
+ }
23524
+ },
23525
+ lines: hub,
23526
+ driver: input.driver,
23527
+ ...input.review ? { review: input.review } : {},
23528
+ ...input.now ? { now: input.now } : {},
23529
+ ...input.checks ? { checks: input.checks } : {},
23530
+ ...input.manifest ? { manifest: input.manifest } : {},
23531
+ host_tools: input.host_tools ?? "discover",
23532
+ ...input.store_root ? { store_root: input.store_root } : {},
23533
+ ...input.specialist_models ? { specialist_models: input.specialist_models } : {},
23534
+ ...input.author_models ? { author_models: input.author_models } : {},
23535
+ approve_exec: approveExec,
23536
+ on_kernel_event: mapKernel,
23537
+ settings_ui: {
23538
+ async select(_title, items) {
23539
+ if (pendingModelChoice) {
23540
+ const choice = pendingModelChoice;
23541
+ pendingModelChoice = void 0;
23542
+ if (items.some((item) => item.value === choice)) return choice;
23543
+ }
23544
+ return "back";
23545
+ },
23546
+ async prompt() {
23547
+ return void 0;
23548
+ },
23549
+ note(message) {
23550
+ emit({ type: "note", text: message });
23551
+ }
23552
+ }
23553
+ }).then((result) => {
23554
+ ended = result.kind === "kernel_required" ? "kernel_required" : result.reason;
23555
+ emit({ type: "ended", reason: ended });
23556
+ hub.close();
23557
+ return result;
23558
+ });
23559
+ if (input.open !== false) {
23560
+ try {
23561
+ spawn5("open", [url], { stdio: "ignore", detached: true }).unref();
23562
+ } catch {
23563
+ }
23564
+ }
23565
+ return {
23566
+ url,
23567
+ token,
23568
+ async close() {
23569
+ hub.close();
23570
+ for (const pending of execs.values()) pending.resolve("deny");
23571
+ execs.clear();
23572
+ for (const client of clients) client.end();
23573
+ clients.clear();
23574
+ await new Promise((resolve13, reject) => {
23575
+ server.close((error) => {
23576
+ error ? reject(error) : resolve13();
23577
+ });
23578
+ });
23579
+ await session.catch(() => void 0);
23580
+ }
23581
+ };
23582
+ }
23583
+
22629
23584
  // lib/finish/critique-code-program.ts
22630
23585
  var CRITIQUE_CODE_EXIT = {
22631
23586
  ok: 0,
@@ -22643,6 +23598,7 @@ Usage:
22643
23598
  pnpm critique-code
22644
23599
  pnpm --filter @critiquedotsh/harness start
22645
23600
  critique-code | critique-code chat [--intent <text>] [--models provider/model,...] [--cwd <dir>] [--store <dir>] [--voice] [--json]
23601
+ critique-code web [--port <n>] [--cwd <dir>] [--store <dir>] [--models ...]
22646
23602
  critique-code login
22647
23603
  critique-code settings | keys | models
22648
23604
  critique-code review [--depth quick|standard|paranoid] [--focus general,security] [--models provider/model,...] [--base <ref>] [--intent <text>] [--cwd <dir>] [--store <dir>]
@@ -22669,6 +23625,7 @@ Interactive author commands:
22669
23625
  /exit
22670
23626
 
22671
23627
  Interactive TTY author sessions keep stdout quiet; pass --json for the session envelope.
23628
+ \`critique-code web\` serves a loopback browser UI on 127.0.0.1. The author kernel still runs in this process.
22672
23629
  Other commands print JSON on stdout. Live chrome goes to stderr.
22673
23630
  Author implements, then the controller forces review + verified repair.
22674
23631
  none_promoted is not a correctness proof.
@@ -22704,7 +23661,8 @@ function parseInvocation(argv) {
22704
23661
  help: { type: "boolean", short: "h" },
22705
23662
  json: { type: "boolean" },
22706
23663
  voice: { type: "boolean" },
22707
- repair: { type: "string" }
23664
+ repair: { type: "string" },
23665
+ port: { type: "string" }
22708
23666
  }
22709
23667
  });
22710
23668
  } catch (error) {
@@ -22816,6 +23774,34 @@ function parseInvocation(argv) {
22816
23774
  if (command === "import-skills") {
22817
23775
  return { command: "skills", import: true, ...cwd ? { cwd } : {} };
22818
23776
  }
23777
+ if (command === "web") {
23778
+ const portRaw = stringFlag(parsed.values.port);
23779
+ let port;
23780
+ if (portRaw) {
23781
+ port = Number(portRaw);
23782
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
23783
+ return { command: "usage", error: "Port must be an integer from 0 to 65535. 0 means an ephemeral loopback port." };
23784
+ }
23785
+ }
23786
+ let specialist_models;
23787
+ if (models) {
23788
+ try {
23789
+ specialist_models = parseCritiqueCodeModels(models);
23790
+ if (specialist_models.length === 0) {
23791
+ return { command: "usage", error: "Models must be a comma-separated provider:family:model_id or provider/model list, or a JSON array." };
23792
+ }
23793
+ } catch {
23794
+ return { command: "usage", error: "Models must be a comma-separated provider:family:model_id or provider/model list, or a JSON array." };
23795
+ }
23796
+ }
23797
+ return {
23798
+ command: "web",
23799
+ ...cwd ? { cwd } : {},
23800
+ ...storeRoot ? { store_root: storeRoot } : {},
23801
+ ...specialist_models ? { specialist_models, author_models: specialist_models } : {},
23802
+ ...port !== void 0 ? { port } : {}
23803
+ };
23804
+ }
22819
23805
  if (command === "settings" || command === "models" || command === "keys" || command === "login") {
22820
23806
  return {
22821
23807
  command: "settings",
@@ -22829,7 +23815,7 @@ function stringFlag(value) {
22829
23815
  }
22830
23816
  function interactiveCommand(argv) {
22831
23817
  const command = argv.find((arg) => arg !== "--" && !arg.startsWith("-"));
22832
- return command === void 0 || command === "chat" || command === "settings" || command === "keys" || command === "models" || command === "login";
23818
+ return command === void 0 || command === "chat" || command === "web" || command === "settings" || command === "keys" || command === "models" || command === "login";
22833
23819
  }
22834
23820
  function writeJson(io, value) {
22835
23821
  if (io.stdin?.isTTY === true && !io.argv.includes("--json") && interactiveCommand(io.argv)) return;
@@ -22894,6 +23880,45 @@ ${helpText}`);
22894
23880
  });
22895
23881
  return CRITIQUE_CODE_EXIT.ok;
22896
23882
  }
23883
+ if (invocation.command === "web") {
23884
+ const started = await (io.web ?? startCritiqueCodeWebServer)({
23885
+ cwd: invocation.cwd ?? io.cwd,
23886
+ env: io.env ?? process.env,
23887
+ stdout: io.stdout,
23888
+ stderr: io.stderr,
23889
+ open: !io.web,
23890
+ ...io.driver ? { driver: io.driver } : {},
23891
+ ...io.review ? { review: io.review } : {},
23892
+ ...invocation.store_root ? { store_root: invocation.store_root } : {},
23893
+ ...invocation.specialist_models ? { specialist_models: invocation.specialist_models } : {},
23894
+ ...invocation.author_models ? { author_models: invocation.author_models } : {},
23895
+ ...invocation.port !== void 0 ? { port: invocation.port } : {}
23896
+ });
23897
+ if ("kind" in started && started.kind === "kernel_required") {
23898
+ writeJson(io, { schema_version: "critique.code-cli.v1", command: "web", ...started });
23899
+ io.stderr.write("Interactive author is kernel_required without a live Pi driver.\nnone_promoted is not a correctness proof.\n");
23900
+ return CRITIQUE_CODE_EXIT.kernel_required;
23901
+ }
23902
+ const server = started;
23903
+ writeJson(io, {
23904
+ schema_version: "critique.code-cli.v1",
23905
+ command: "web",
23906
+ url: server.url,
23907
+ bind: "127.0.0.1"
23908
+ });
23909
+ if (io.web) {
23910
+ await server.close();
23911
+ return CRITIQUE_CODE_EXIT.ok;
23912
+ }
23913
+ await new Promise((resolve13) => {
23914
+ const stop = () => {
23915
+ void server.close().finally(resolve13);
23916
+ };
23917
+ process.once("SIGINT", stop);
23918
+ process.once("SIGTERM", stop);
23919
+ });
23920
+ return CRITIQUE_CODE_EXIT.ok;
23921
+ }
22897
23922
  if (invocation.command === "chat") {
22898
23923
  const result2 = await (io.chat ?? runLocalCritiqueCodeAuthor)({
22899
23924
  cwd: invocation.cwd ?? io.cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critiquedotsh/harness",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CritiqueCode author agent — implement, then forced review and verified repair. Same org as @critiquedotsh/cli; not the sidecar.",
5
5
  "homepage": "https://critique.sh/docs/platform/critique-code",
6
6
  "repository": {
@@ -27,14 +27,16 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "README.md"
30
+ "README.md",
31
+ "scripts/postinstall.mjs"
31
32
  ],
32
33
  "scripts": {
33
34
  "build": "node scripts/build-cli.mjs",
35
+ "postinstall": "node scripts/postinstall.mjs",
34
36
  "start": "node --import ../../scripts/register-test-path-alias.mjs --experimental-strip-types src/cli.ts",
35
37
  "install:pi": "npm install --prefix .vendor",
36
38
  "prepublishOnly": "npm run build",
37
- "test": "node --import ../../scripts/register-test-path-alias.mjs --experimental-strip-types --test ../../lib/finish/critique-code-program.test.ts ../../lib/finish/critique-code-local-session.test.ts ../../lib/finish/critique-code-local-author.test.ts ../../lib/finish/critique-code-author-apply.test.ts ../../lib/finish/critique-code-local-repair.test.ts ../../lib/finish/critique-code-author-coordinator.test.ts ../../lib/finish/critique-code-repair-kernel.test.ts ../../lib/finish/critique-code-review-checkpoint.test.ts ../../lib/finish/critique-code-runtime-config.test.ts ../../lib/finish/critique-code-model-defaults.test.ts ../../lib/finish/critique-code-pi-providers.test.ts ../../lib/finish/critique-code-settings-tui.test.ts ../../lib/finish/critique-code-tui-theme.test.ts ../../lib/finish/earendil-pi-driver.test.ts ../../lib/finish/critique-code-device-login.test.ts ../../lib/finish/critique-code-author-commands.test.ts ../../lib/finish/critique-code-skills.test.ts ../../lib/finish/pi-author-task.test.ts ../../lib/finish/pi-session-harness.test.ts"
39
+ "test": "node --import ../../scripts/register-test-path-alias.mjs --experimental-strip-types --test ../../lib/finish/critique-code-program.test.ts ../../lib/finish/critique-code-local-session.test.ts ../../lib/finish/critique-code-local-author.test.ts ../../lib/finish/critique-code-author-apply.test.ts ../../lib/finish/critique-code-local-repair.test.ts ../../lib/finish/critique-code-author-coordinator.test.ts ../../lib/finish/critique-code-repair-kernel.test.ts ../../lib/finish/critique-code-review-checkpoint.test.ts ../../lib/finish/critique-code-runtime-config.test.ts ../../lib/finish/critique-code-model-defaults.test.ts ../../lib/finish/critique-code-pi-providers.test.ts ../../lib/finish/critique-code-settings-tui.test.ts ../../lib/finish/critique-code-tui-theme.test.ts ../../lib/finish/earendil-pi-driver.test.ts ../../lib/finish/critique-code-device-login.test.ts ../../lib/finish/critique-code-author-commands.test.ts ../../lib/finish/critique-code-skills.test.ts ../../lib/finish/pi-author-task.test.ts ../../lib/finish/pi-session-harness.test.ts ../../lib/finish/critique-code-web-server.test.ts"
38
40
  },
39
41
  "dependencies": {
40
42
  "typescript": "^5.9.3"
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ const silent =
4
+ process.env.CI === 'true'
5
+ || process.env.npm_config_loglevel === 'silent'
6
+ || process.env.npm_config_loglevel === 'error'
7
+
8
+ if (silent) process.exit(0)
9
+
10
+ process.stderr.write(`
11
+ CritiqueCode installed. Binary: critique-code
12
+
13
+ cd into a git repo, then:
14
+
15
+ critique-code login connect Critique Inference in the browser (no key paste)
16
+ critique-code terminal author session
17
+ critique-code web local browser UI on 127.0.0.1
18
+ critique-code help commands, /review /repair /ship, voice
19
+
20
+ Voice needs the ffmpeg-static install script:
21
+ npm install -g @critiquedotsh/harness --allow-scripts=ffmpeg-static
22
+
23
+ Docs: https://critique.sh/docs/platform/critique-code
24
+
25
+ `)