@critiquedotsh/harness 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/dist/cli.js +946 -34
  3. package/package.json +2 -2
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
@@ -20251,7 +20251,7 @@ function defaultOpenUrl(url) {
20251
20251
  } catch {
20252
20252
  }
20253
20253
  }
20254
- async function connectCritiqueInference(input) {
20254
+ async function beginCritiqueInferenceLogin(input) {
20255
20255
  const env = input.env ?? process.env;
20256
20256
  const origin = critiqueSiteOrigin(env);
20257
20257
  const fetchImpl = input.io.fetch ?? fetch;
@@ -20273,35 +20273,47 @@ async function connectCritiqueInference(input) {
20273
20273
  (input.io.openUrl ?? defaultOpenUrl)(verifyUrl);
20274
20274
  const intervalMs = Math.max(2, authorization.interval ?? 3) * 1e3;
20275
20275
  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
- })
20276
+ const deviceCode = authorization.device_code;
20277
+ const finished = (async () => {
20278
+ while (Date.now() < deadline) {
20279
+ if (input.io.signal?.aborted) throw new Error("Browser sign-in cancelled.");
20280
+ await sleep(intervalMs);
20281
+ const polled = await fetchImpl(`${origin}/api/new/v1/device/token`, {
20282
+ method: "POST",
20283
+ headers: { "content-type": "application/json" },
20284
+ body: JSON.stringify({ device_code: deviceCode }),
20285
+ signal: input.io.signal
20299
20286
  });
20300
- input.io.note("Critique Inference is connected. Default model is critique/auto.");
20301
- return { apiKey, verificationUri: verifyUrl };
20287
+ const token = await polled.json().catch(() => null);
20288
+ if (token?.status === "authorization_pending") continue;
20289
+ if (token?.status === "expired") throw new Error("The website approval code expired. Run /login again.");
20290
+ if (token?.status === "denied") throw new Error("Website approval was denied.");
20291
+ const apiKey = token?.api_key?.trim() || token?.apiKey?.trim();
20292
+ if (token?.status === "authorized" && apiKey) {
20293
+ await saveCritiqueCodeCredential({ home: input.home, kind: "critique", key: apiKey });
20294
+ await saveCritiqueCodeRuntimeSettings({
20295
+ home: input.home,
20296
+ settings: settingsFromSelection({
20297
+ route: "critique-inference",
20298
+ author: DEFAULT_INFERENCE_MODEL,
20299
+ review: [DEFAULT_INFERENCE_MODEL]
20300
+ })
20301
+ });
20302
+ input.io.note("Critique Inference is connected. Default model is critique/auto.");
20303
+ return { apiKey, verificationUri: verifyUrl };
20304
+ }
20302
20305
  }
20303
- }
20304
- throw new Error("Timed out waiting for website approval.");
20306
+ throw new Error("Timed out waiting for website approval.");
20307
+ })();
20308
+ return {
20309
+ verificationUri: verifyUrl,
20310
+ userCode: authorization.user_code,
20311
+ finished
20312
+ };
20313
+ }
20314
+ async function connectCritiqueInference(input) {
20315
+ const started = await beginCritiqueInferenceLogin(input);
20316
+ return started.finished;
20305
20317
  }
20306
20318
 
20307
20319
  // lib/finish/critique-code-settings-tui.ts
@@ -21979,8 +21991,9 @@ function authorModelId(model) {
21979
21991
  return piModelId(model);
21980
21992
  }
21981
21993
  function createAuthorLineReader(input) {
21982
- if (Array.isArray(input.lines)) {
21983
- const rows = input.lines;
21994
+ const lines = input.lines;
21995
+ if (Array.isArray(lines)) {
21996
+ const rows = lines;
21984
21997
  let index = 0;
21985
21998
  return {
21986
21999
  async readLine() {
@@ -21995,8 +22008,8 @@ function createAuthorLineReader(input) {
21995
22008
  }
21996
22009
  };
21997
22010
  }
21998
- if (input.lines) {
21999
- const iterator = input.lines[Symbol.asyncIterator]();
22011
+ if (lines != null && !Array.isArray(lines)) {
22012
+ const iterator = lines[Symbol.asyncIterator]();
22000
22013
  return {
22001
22014
  async readLine() {
22002
22015
  const next = await iterator.next();
@@ -22444,6 +22457,10 @@ ${task.prompt}`,
22444
22457
  if (coordinator.session().review_required) {
22445
22458
  await runReviewGate("write_budget");
22446
22459
  }
22460
+ } catch (error) {
22461
+ const text = error instanceof Error ? error.message : String(error);
22462
+ input.stderr.write(`${text.endsWith("\n") ? text : `${text}
22463
+ `}`);
22447
22464
  } finally {
22448
22465
  activity.endTurn();
22449
22466
  reader.resume();
@@ -22626,6 +22643,831 @@ ${task.prompt}`,
22626
22643
  };
22627
22644
  }
22628
22645
 
22646
+ // lib/finish/critique-code-web-server.ts
22647
+ init_change_capsule();
22648
+ import { randomBytes } from "node:crypto";
22649
+ import { createServer } from "node:http";
22650
+ import { basename as basename3 } from "node:path";
22651
+ import { execFileSync, spawn as spawn5 } from "node:child_process";
22652
+
22653
+ // lib/finish/critique-code-line-hub.ts
22654
+ var CritiqueCodeLineHub = class {
22655
+ #queued = [];
22656
+ #wait;
22657
+ #closed = false;
22658
+ get closed() {
22659
+ return this.#closed;
22660
+ }
22661
+ push(line) {
22662
+ if (this.#closed) return false;
22663
+ if (this.#wait) {
22664
+ this.onState?.(true);
22665
+ const waiting = this.#wait;
22666
+ this.#wait = void 0;
22667
+ waiting(line);
22668
+ return true;
22669
+ }
22670
+ this.#queued.push(line);
22671
+ return true;
22672
+ }
22673
+ close() {
22674
+ this.#closed = true;
22675
+ this.#wait?.(void 0);
22676
+ this.#wait = void 0;
22677
+ }
22678
+ async *[Symbol.asyncIterator]() {
22679
+ for (; ; ) {
22680
+ if (this.#queued.length > 0) {
22681
+ this.onState?.(true);
22682
+ yield this.#queued.shift();
22683
+ continue;
22684
+ }
22685
+ if (this.#closed) return;
22686
+ this.onState?.(false);
22687
+ const line = await new Promise((resolve13) => {
22688
+ this.#wait = resolve13;
22689
+ });
22690
+ if (line === void 0) return;
22691
+ yield line;
22692
+ }
22693
+ }
22694
+ };
22695
+
22696
+ // lib/finish/critique-code-web-server.ts
22697
+ init_critique_code_model_defaults();
22698
+ init_critique_code_runtime_config();
22699
+
22700
+ // lib/finish/critique-code-web-ui.ts
22701
+ function escapeHtml(value) {
22702
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
22703
+ }
22704
+ function humanizeCritiqueCodeWebError(text) {
22705
+ const raw = text.replace(/\u001b\[[0-9;]*m/g, "").trim();
22706
+ const missing = raw.match(/No API key found for ([a-z0-9._-]+)/i);
22707
+ if (missing) {
22708
+ const provider = missing[1] ?? "this provider";
22709
+ 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.`;
22710
+ }
22711
+ 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);
22712
+ }
22713
+ function critiqueCodeWebPage(input) {
22714
+ const repositoryName = escapeHtml(input.repository_name);
22715
+ const repositoryRoot = escapeHtml(input.repository_root);
22716
+ const branch = escapeHtml(input.branch);
22717
+ const model = escapeHtml(input.model);
22718
+ const listen = escapeHtml(input.listen);
22719
+ const modelShort = escapeHtml(input.model.replace(/^openrouter\//, "").replace(/^critique\//, "critique/"));
22720
+ return `<!DOCTYPE html>
22721
+ <html lang="en">
22722
+ <head>
22723
+ <meta charset="utf-8">
22724
+ <meta name="viewport" content="width=device-width, initial-scale=1">
22725
+ <title>CritiqueCode</title>
22726
+ <style>
22727
+ :root {
22728
+ --bg: #0e0e0e;
22729
+ --rail: #111111;
22730
+ --panel: #161616;
22731
+ --prompt: #1c1c1c;
22732
+ --line: rgba(255,255,255,.08);
22733
+ --fg: #ececec;
22734
+ --muted: #8d8d8d;
22735
+ --faint: #5c5c5c;
22736
+ --live: #3dd68c;
22737
+ --warn: #d4a017;
22738
+ --err: #e36d6d;
22739
+ --ease: cubic-bezier(.32,.72,0,1);
22740
+ }
22741
+ * { box-sizing: border-box; }
22742
+ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg);
22743
+ font-family: "SF Pro Text", "Segoe UI", ui-sans-serif, system-ui, sans-serif;
22744
+ font-size: 14px; letter-spacing: -0.011em; }
22745
+ button, textarea { font: inherit; color: inherit; }
22746
+ button { cursor: pointer; }
22747
+ .shell { display: grid; grid-template-columns: 56px 1fr; min-height: 100dvh; }
22748
+ .rail { background: var(--rail); border-right: 1px solid var(--line); display: flex; flex-direction: column; align-items: center; padding: 14px 0; gap: 12px; }
22749
+ .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; }
22750
+ .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); }
22751
+ .rail .plus:hover { color: var(--fg); border-color: rgba(255,255,255,.2); }
22752
+ .stage { display: grid; grid-template-rows: auto 1fr auto; min-width: 0; min-height: 100dvh; }
22753
+ header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 22px 8px; }
22754
+ .session { display: flex; align-items: center; gap: 10px; color: var(--fg); font-size: 13px; font-weight: 500; }
22755
+ .session svg { opacity: .55; }
22756
+ .badges { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
22757
+ .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; }
22758
+ .badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--live); box-shadow: 0 0 0 3px rgba(61,214,140,.12); }
22759
+ .badge.model { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22760
+ main { min-height: 0; overflow: auto; }
22761
+ #empty { max-width: 560px; margin: 12vh auto 0; padding: 0 24px 40px; }
22762
+ #empty h1 { margin: 0 0 28px; font-size: 28px; font-weight: 500; color: #6e6e6e; letter-spacing: -.03em; }
22763
+ .meta { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; color: var(--muted); font-size: 13px; }
22764
+ .meta li { display: grid; grid-template-columns: 18px 1fr; gap: 12px; align-items: start; }
22765
+ .meta svg { margin-top: 2px; opacity: .7; }
22766
+ .meta strong { color: var(--fg); font-weight: 500; word-break: break-all; }
22767
+ #feed { max-width: 720px; margin: 0 auto; padding: 12px 24px 28px; display: none; }
22768
+ #feed.on { display: block; }
22769
+ .turn { margin: 0 0 28px; }
22770
+ .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; }
22771
+ .steps { margin: 10px 2px 0; color: var(--faint); font-size: 12px; }
22772
+ .steps summary { cursor: pointer; list-style: none; display: flex; justify-content: space-between; gap: 12px; }
22773
+ .steps summary::-webkit-details-marker { display: none; }
22774
+ .steps[open] summary { color: var(--muted); }
22775
+ .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; }
22776
+ .reply { margin: 14px 2px 0; line-height: 1.6; white-space: pre-wrap; word-break: break-word; color: #dedede; }
22777
+ .reply h { display: block; font-weight: 600; color: #fff; margin: 0 0 8px; font-size: 15px; }
22778
+ .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; }
22779
+ .note { color: var(--muted); }
22780
+ .alert { background: rgba(227,109,109,.08); color: #f0c7c7; border: 1px solid rgba(227,109,109,.18); }
22781
+ .review-card { background: #141414; border: 1px solid var(--line); color: var(--muted); font-size: 13px; }
22782
+ .dock { padding: 0 20px 22px; }
22783
+ .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); }
22784
+ 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; }
22785
+ textarea::placeholder { color: #6a6a6a; }
22786
+ .bar { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 4px 6px 4px 8px; }
22787
+ .cmds { display: flex; flex-wrap: wrap; gap: 2px; min-width: 0; }
22788
+ .cmds button { background: transparent; border: 0; color: var(--muted); padding: 5px 8px; border-radius: 8px; font-size: 12px; }
22789
+ .cmds button:hover { background: rgba(255,255,255,.05); color: var(--fg); }
22790
+ .bar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
22791
+ .model-chip { color: var(--muted); font-size: 12px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22792
+ #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); }
22793
+ #send:hover { transform: translateY(-1px); }
22794
+ #send:disabled, textarea:disabled { opacity: .4; cursor: not-allowed; }
22795
+ #modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); align-items: center; justify-content: center; padding: 24px; z-index: 4; }
22796
+ #modal.open { display: flex; }
22797
+ .dialog { width: min(520px, 100%); background: #161616; border: 1px solid var(--line); border-radius: 16px; padding: 18px; }
22798
+ .dialog h3 { margin: 0 0 8px; font-size: 15px; font-weight: 600; }
22799
+ .dialog .purpose { color: var(--muted); font-size: 13px; }
22800
+ .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); }
22801
+ .modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
22802
+ .modal-actions button { border: 1px solid var(--line); background: #1c1c1c; border-radius: 10px; padding: 8px 12px; }
22803
+ .modal-actions .deny { color: var(--err); }
22804
+ .modal-actions #approve { background: #ececec; color: #111; border-color: transparent; }
22805
+ @media (max-width: 720px) {
22806
+ .shell { grid-template-columns: 1fr; }
22807
+ .rail { display: none; }
22808
+ header { padding: 12px 16px 4px; }
22809
+ .dock { padding: 0 12px 16px; }
22810
+ }
22811
+ </style>
22812
+ </head>
22813
+ <body>
22814
+ <div class="shell">
22815
+ <aside class="rail">
22816
+ <div class="mark" title="CritiqueCode">C</div>
22817
+ <button type="button" class="plus" id="focus-composer" title="Focus composer" aria-label="Focus composer">
22818
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2v10M2 7h10" stroke="currentColor" stroke-width="1.4"/></svg>
22819
+ </button>
22820
+ </aside>
22821
+ <div class="stage">
22822
+ <header>
22823
+ <div class="session">
22824
+ <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>
22825
+ New session
22826
+ </div>
22827
+ <div class="badges">
22828
+ <span class="badge"><span class="dot"></span>${listen}</span>
22829
+ <span class="badge" id="files-badge" hidden></span>
22830
+ <span class="badge model" title="${model}">${modelShort}</span>
22831
+ </div>
22832
+ </header>
22833
+ <main>
22834
+ <div id="empty">
22835
+ <h1>New session</h1>
22836
+ <ul class="meta">
22837
+ <li>
22838
+ <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>
22839
+ <span><strong>${repositoryRoot}</strong></span>
22840
+ </li>
22841
+ <li>
22842
+ <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>
22843
+ <span>Main branch (<strong>${branch}</strong>)</span>
22844
+ </li>
22845
+ <li>
22846
+ <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>
22847
+ <span>Loopback only \xB7 ${repositoryName}</span>
22848
+ </li>
22849
+ </ul>
22850
+ </div>
22851
+ <div id="feed"></div>
22852
+ </main>
22853
+ <div class="dock">
22854
+ <div class="composer">
22855
+ <textarea id="input" rows="2" placeholder="Ask anything\u2026"></textarea>
22856
+ <div class="bar">
22857
+ <div class="cmds">
22858
+ <button type="button" data-cmd="/review">Review</button>
22859
+ <button type="button" data-cmd="/review all">Review all</button>
22860
+ <button type="button" data-cmd="/repair">Repair</button>
22861
+ <button type="button" data-cmd="/ship">Ship</button>
22862
+ <button type="button" data-cmd="/skills">Skills</button>
22863
+ <button type="button" data-cmd="/login" id="login">Login</button>
22864
+ <button type="button" data-cmd="/exit">Exit</button>
22865
+ </div>
22866
+ <div class="bar-right">
22867
+ <span class="model-chip">${modelShort}</span>
22868
+ <button type="button" id="send" aria-label="Send">
22869
+ <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>
22870
+ </button>
22871
+ </div>
22872
+ </div>
22873
+ </div>
22874
+ </div>
22875
+ </div>
22876
+ </div>
22877
+ <div id="modal">
22878
+ <div class="dialog">
22879
+ <h3>Run this command?</h3>
22880
+ <div class="purpose" id="exec-purpose"></div>
22881
+ <div class="preview" id="exec-preview"></div>
22882
+ <div class="modal-actions">
22883
+ <button type="button" class="deny" id="deny">Deny</button>
22884
+ <button type="button" id="approve">Approve</button>
22885
+ </div>
22886
+ </div>
22887
+ </div>
22888
+ <script>
22889
+ window.CRITIQUE_CODE_TOKEN = "__CRITIQUE_CODE_TOKEN__";
22890
+ (function () {
22891
+ var TOKEN = window.CRITIQUE_CODE_TOKEN;
22892
+ var emptyEl = document.getElementById("empty");
22893
+ var feedEl = document.getElementById("feed");
22894
+ var filesBadge = document.getElementById("files-badge");
22895
+ var inputEl = document.getElementById("input");
22896
+ var sendEl = document.getElementById("send");
22897
+ var modalEl = document.getElementById("modal");
22898
+ var purposeEl = document.getElementById("exec-purpose");
22899
+ var previewEl = document.getElementById("exec-preview");
22900
+ var busy = false;
22901
+ var turnEl = null;
22902
+ var assistantEl = null;
22903
+ var pendingExecId = null;
22904
+ var es = null;
22905
+ var reconnectTimer = null;
22906
+
22907
+ function headers() {
22908
+ return { "content-type": "application/json", "x-critique-code-token": TOKEN };
22909
+ }
22910
+
22911
+ function api(method, path, body) {
22912
+ return fetch(path, {
22913
+ method: method,
22914
+ headers: headers(),
22915
+ body: body ? JSON.stringify(body) : undefined
22916
+ });
22917
+ }
22918
+
22919
+ function showFeed() {
22920
+ emptyEl.style.display = "none";
22921
+ feedEl.className = "on";
22922
+ }
22923
+
22924
+ function setBusy(next) {
22925
+ busy = !!next;
22926
+ sendEl.disabled = busy;
22927
+ inputEl.disabled = busy;
22928
+ }
22929
+
22930
+ function scrollMain() {
22931
+ var main = document.querySelector("main");
22932
+ if (main) main.scrollTop = main.scrollHeight;
22933
+ }
22934
+
22935
+ function addBlock(className, text) {
22936
+ showFeed();
22937
+ turnEl = null;
22938
+ assistantEl = null;
22939
+ var node = document.createElement("div");
22940
+ node.className = className;
22941
+ node.textContent = text == null ? "" : String(text);
22942
+ feedEl.appendChild(node);
22943
+ scrollMain();
22944
+ return node;
22945
+ }
22946
+
22947
+ function startTurn(text) {
22948
+ showFeed();
22949
+ assistantEl = null;
22950
+ turnEl = document.createElement("div");
22951
+ turnEl.className = "turn";
22952
+ var prompt = document.createElement("div");
22953
+ prompt.className = "prompt";
22954
+ prompt.textContent = text;
22955
+ turnEl.appendChild(prompt);
22956
+ feedEl.appendChild(turnEl);
22957
+ scrollMain();
22958
+ }
22959
+
22960
+ function appendAssistant(text) {
22961
+ if (!turnEl) {
22962
+ showFeed();
22963
+ turnEl = document.createElement("div");
22964
+ turnEl.className = "turn";
22965
+ feedEl.appendChild(turnEl);
22966
+ }
22967
+ if (!assistantEl) {
22968
+ assistantEl = document.createElement("div");
22969
+ assistantEl.className = "reply";
22970
+ turnEl.appendChild(assistantEl);
22971
+ }
22972
+ assistantEl.textContent = (assistantEl.textContent || "") + (text || "");
22973
+ scrollMain();
22974
+ }
22975
+
22976
+ function addTool(name, phase, detail) {
22977
+ if (!turnEl) {
22978
+ showFeed();
22979
+ turnEl = document.createElement("div");
22980
+ turnEl.className = "turn";
22981
+ feedEl.appendChild(turnEl);
22982
+ }
22983
+ var steps = turnEl.querySelector(".steps");
22984
+ if (!steps) {
22985
+ steps = document.createElement("details");
22986
+ steps.className = "steps";
22987
+ var summary = document.createElement("summary");
22988
+ summary.innerHTML = "<span>Show steps</span><span class=\\"dur\\"></span>";
22989
+ var list = document.createElement("div");
22990
+ list.className = "step-list";
22991
+ steps.appendChild(summary);
22992
+ steps.appendChild(list);
22993
+ if (assistantEl) turnEl.insertBefore(steps, assistantEl);
22994
+ else turnEl.appendChild(steps);
22995
+ }
22996
+ var listEl = steps.querySelector(".step-list");
22997
+ var line = (name || "tool") + " " + (phase || "");
22998
+ if (detail) line += "\\n" + detail;
22999
+ listEl.textContent = (listEl.textContent ? listEl.textContent + "\\n" : "") + line;
23000
+ scrollMain();
23001
+ }
23002
+
23003
+ function setPaths(paths) {
23004
+ if (!paths || !paths.length) {
23005
+ filesBadge.hidden = true;
23006
+ filesBadge.textContent = "";
23007
+ return;
23008
+ }
23009
+ filesBadge.hidden = false;
23010
+ filesBadge.textContent = paths.length === 1 ? "1 file" : String(paths.length) + " files";
23011
+ filesBadge.title = paths.join("\\n");
23012
+ }
23013
+
23014
+ function setReview(text) {
23015
+ if (!text) return;
23016
+ addBlock("review-card", text);
23017
+ }
23018
+
23019
+ function showExec(req) {
23020
+ if (!req) {
23021
+ modalEl.className = "";
23022
+ pendingExecId = null;
23023
+ return;
23024
+ }
23025
+ pendingExecId = req.id;
23026
+ purposeEl.textContent = req.purpose || "";
23027
+ previewEl.textContent = req.preview || "";
23028
+ modalEl.className = "open";
23029
+ }
23030
+
23031
+ function handleEvent(ev) {
23032
+ if (!ev || !ev.type) return;
23033
+ switch (ev.type) {
23034
+ case "note":
23035
+ addBlock("note", ev.text);
23036
+ break;
23037
+ case "user":
23038
+ startTurn(ev.text);
23039
+ break;
23040
+ case "assistant_delta":
23041
+ appendAssistant(ev.text);
23042
+ break;
23043
+ case "tool":
23044
+ addTool(ev.name, ev.phase, ev.detail);
23045
+ break;
23046
+ case "exec_request":
23047
+ showExec({ id: ev.id, purpose: ev.purpose, preview: ev.preview });
23048
+ break;
23049
+ case "exec_resolved":
23050
+ if (pendingExecId === ev.id) showExec(null);
23051
+ addBlock("note", ev.decision === "approve" ? "Run approved. Output is not Evidence." : "Run denied.");
23052
+ break;
23053
+ case "review":
23054
+ setReview((ev.text || "") + (ev.conclusion ? "\\n" + ev.conclusion : ""));
23055
+ break;
23056
+ case "busy":
23057
+ setBusy(ev.busy);
23058
+ break;
23059
+ case "ended":
23060
+ setBusy(false);
23061
+ addBlock("note", ev.reason ? "Session ended \xB7 " + ev.reason : "Session ended");
23062
+ break;
23063
+ case "error":
23064
+ addBlock("alert", ev.text);
23065
+ break;
23066
+ }
23067
+ }
23068
+
23069
+ function applySnapshot(s) {
23070
+ if (!s) return;
23071
+ setBusy(s.busy);
23072
+ setPaths(s.changed_paths);
23073
+ if (s.last_review_text) setReview(s.last_review_text);
23074
+ if (s.pending_exec) showExec(s.pending_exec);
23075
+ }
23076
+
23077
+ function connect() {
23078
+ if (es) {
23079
+ es.close();
23080
+ es = null;
23081
+ }
23082
+ es = new EventSource("/api/events?token=" + encodeURIComponent(TOKEN));
23083
+ es.onmessage = function (msg) {
23084
+ try { handleEvent(JSON.parse(msg.data)); } catch (e) {}
23085
+ };
23086
+ es.onerror = function () {
23087
+ if (es) { es.close(); es = null; }
23088
+ if (reconnectTimer) clearTimeout(reconnectTimer);
23089
+ reconnectTimer = setTimeout(connect, 1200);
23090
+ };
23091
+ }
23092
+
23093
+ function startLogin() {
23094
+ api("POST", "/api/login", {}).then(function (res) {
23095
+ return res.json().then(function (body) {
23096
+ if (!res.ok) addBlock("alert", body.error || "Could not start Login.");
23097
+ });
23098
+ }).catch(function () {
23099
+ addBlock("alert", "Could not start Login.");
23100
+ });
23101
+ }
23102
+
23103
+ function submitText(raw) {
23104
+ var text = (raw == null ? inputEl.value : raw).trim();
23105
+ if (!text || busy) return;
23106
+ if (text === "/login" || text === "login") {
23107
+ if (raw == null) inputEl.value = "";
23108
+ startLogin();
23109
+ return;
23110
+ }
23111
+ var isCmd = text.charAt(0) === "/";
23112
+ if (raw == null) inputEl.value = "";
23113
+ api("POST", isCmd ? "/api/command" : "/api/prompt", { text: text }).then(function (res) {
23114
+ if (!res.ok) addBlock("alert", "Request failed");
23115
+ }).catch(function () {
23116
+ addBlock("alert", "Request failed");
23117
+ });
23118
+ }
23119
+
23120
+ function decide(decision) {
23121
+ if (!pendingExecId) return;
23122
+ var id = pendingExecId;
23123
+ api("POST", "/api/exec", { id: id, decision: decision }).then(function () {
23124
+ showExec(null);
23125
+ }).catch(function () {
23126
+ addBlock("alert", "Could not resolve the run.");
23127
+ });
23128
+ }
23129
+
23130
+ sendEl.addEventListener("click", function () { submitText(null); });
23131
+ document.getElementById("focus-composer").addEventListener("click", function () {
23132
+ inputEl.focus();
23133
+ });
23134
+ inputEl.addEventListener("keydown", function (e) {
23135
+ if (e.key === "Enter" && !e.shiftKey) {
23136
+ e.preventDefault();
23137
+ submitText(null);
23138
+ }
23139
+ });
23140
+ document.querySelectorAll(".cmds button").forEach(function (btn) {
23141
+ btn.addEventListener("click", function () {
23142
+ var cmd = btn.getAttribute("data-cmd");
23143
+ if (cmd === "/login") {
23144
+ startLogin();
23145
+ return;
23146
+ }
23147
+ submitText(cmd);
23148
+ });
23149
+ });
23150
+ document.getElementById("approve").addEventListener("click", function () { decide("approve"); });
23151
+ document.getElementById("deny").addEventListener("click", function () { decide("deny"); });
23152
+
23153
+ api("GET", "/api/snapshot").then(function (res) { return res.json(); }).then(applySnapshot).catch(function () {});
23154
+ connect();
23155
+ })();
23156
+ </script>
23157
+ </body>
23158
+ </html>
23159
+ `;
23160
+ }
23161
+
23162
+ // lib/finish/critique-code-web-server.ts
23163
+ 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.";
23164
+ function gitBranch(root) {
23165
+ try {
23166
+ return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
23167
+ cwd: root,
23168
+ encoding: "utf8",
23169
+ timeout: 2e3
23170
+ }).trim() || "unknown";
23171
+ } catch {
23172
+ return "unknown";
23173
+ }
23174
+ }
23175
+ async function startCritiqueCodeWebServer(input) {
23176
+ const host = input.host ?? "127.0.0.1";
23177
+ if (host !== "127.0.0.1" && host !== "localhost") {
23178
+ throw new Error("CritiqueCode web UI binds only to 127.0.0.1. LAN listen is not allowed.");
23179
+ }
23180
+ const bindHost = "127.0.0.1";
23181
+ const env = input.env ?? process.env;
23182
+ let model = "critique/auto";
23183
+ try {
23184
+ const config = await loadCritiqueCodeRuntimeConfig(env);
23185
+ const authorModels = input.author_models && input.author_models.length > 0 ? [...input.author_models] : await resolveHarnessModels({ role: "author", env, config });
23186
+ const driver = await resolveOptionalPiDriver({ driver: input.driver, env });
23187
+ if (authorModels.length === 0 || !driver) {
23188
+ return { kind: "kernel_required", limitation: KERNEL_REQUIRED3 };
23189
+ }
23190
+ model = piModelId(authorModels[0]);
23191
+ } catch (error) {
23192
+ return { kind: "kernel_required", limitation: error instanceof Error ? error.message : String(error) };
23193
+ }
23194
+ const repositoryRoot = await findRepositoryRoot(input.cwd);
23195
+ const token = randomBytes(18).toString("base64url");
23196
+ const hub = new CritiqueCodeLineHub();
23197
+ const clients = /* @__PURE__ */ new Set();
23198
+ const execs = /* @__PURE__ */ new Map();
23199
+ let busy = false;
23200
+ let ended;
23201
+ const changedPaths = [];
23202
+ let lastReviewText = "";
23203
+ let pendingExec;
23204
+ const emit = (event) => {
23205
+ const payload = `data: ${JSON.stringify(event)}
23206
+
23207
+ `;
23208
+ for (const client of clients) client.write(payload);
23209
+ };
23210
+ hub.onState = (nextBusy) => {
23211
+ busy = nextBusy;
23212
+ emit({ type: "busy", busy });
23213
+ };
23214
+ const mapKernel = (event) => {
23215
+ if (event.type === "text_delta" && event.text) {
23216
+ emit({ type: "assistant_delta", text: event.text });
23217
+ return;
23218
+ }
23219
+ if (event.type === "tool_started" || event.type === "tool_completed") {
23220
+ if (event.tool_name === "critique_write_file" && event.detail && event.type === "tool_completed") {
23221
+ if (!changedPaths.includes(event.detail)) changedPaths.push(event.detail);
23222
+ }
23223
+ emit({
23224
+ type: "tool",
23225
+ name: event.tool_name,
23226
+ detail: event.detail,
23227
+ phase: event.type === "tool_started" ? "started" : "completed"
23228
+ });
23229
+ return;
23230
+ }
23231
+ if (event.type === "session_failed") {
23232
+ emit({ type: "error", text: humanizeCritiqueCodeWebError(event.error ?? event.detail ?? "session failed") });
23233
+ }
23234
+ };
23235
+ const approveExec = (request) => new Promise((resolve13) => {
23236
+ const id4 = randomBytes(9).toString("hex");
23237
+ const pending = { id: id4, purpose: request.purpose, preview: request.preview, resolve: resolve13 };
23238
+ execs.set(id4, pending);
23239
+ pendingExec = { id: id4, purpose: request.purpose, preview: request.preview };
23240
+ emit({ type: "exec_request", id: id4, purpose: request.purpose, preview: request.preview });
23241
+ });
23242
+ const snapshot = () => ({
23243
+ repository_root: repositoryRoot,
23244
+ model,
23245
+ busy,
23246
+ changed_paths: [...changedPaths],
23247
+ last_review_text: lastReviewText,
23248
+ ...pendingExec ? { pending_exec: pendingExec } : {},
23249
+ ...ended ? { ended } : {}
23250
+ });
23251
+ const originOk = (req, port) => {
23252
+ const origin = req.headers.origin;
23253
+ if (!origin) return true;
23254
+ return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
23255
+ };
23256
+ const tokenOk = (req) => {
23257
+ const header = req.headers["x-critique-code-token"];
23258
+ if (typeof header === "string" && header === token) return true;
23259
+ const url2 = new URL(req.url ?? "/", "http://127.0.0.1");
23260
+ return url2.searchParams.get("token") === token;
23261
+ };
23262
+ const readJson = async (req) => {
23263
+ const chunks = [];
23264
+ for await (const chunk of req) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
23265
+ if (chunks.length === 0) return {};
23266
+ try {
23267
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
23268
+ return parsed && typeof parsed === "object" ? parsed : {};
23269
+ } catch {
23270
+ return {};
23271
+ }
23272
+ };
23273
+ const send = (res, status, body, contentType = "application/json; charset=utf-8") => {
23274
+ const text = typeof body === "string" ? body : JSON.stringify(body);
23275
+ res.writeHead(status, {
23276
+ "content-type": contentType,
23277
+ "cache-control": "no-store"
23278
+ });
23279
+ res.end(text);
23280
+ };
23281
+ const server = createServer(async (req, res) => {
23282
+ const port = listeningPort();
23283
+ const url2 = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
23284
+ const path2 = url2.pathname;
23285
+ if (req.method === "GET" && path2 === "/") {
23286
+ const html = critiqueCodeWebPage({
23287
+ repository_name: basename3(repositoryRoot),
23288
+ repository_root: repositoryRoot,
23289
+ branch: gitBranch(repositoryRoot),
23290
+ model,
23291
+ listen: `127.0.0.1:${port}`
23292
+ }).replaceAll("__CRITIQUE_CODE_TOKEN__", token);
23293
+ send(res, 200, html, "text/html; charset=utf-8");
23294
+ return;
23295
+ }
23296
+ if (req.method === "GET" && path2 === "/api/events") {
23297
+ if (!tokenOk(req)) {
23298
+ send(res, 401, { error: "unauthorized" });
23299
+ return;
23300
+ }
23301
+ res.writeHead(200, {
23302
+ "content-type": "text/event-stream; charset=utf-8",
23303
+ "cache-control": "no-store",
23304
+ connection: "keep-alive"
23305
+ });
23306
+ res.write(`data: ${JSON.stringify({ type: "busy", busy })}
23307
+
23308
+ `);
23309
+ clients.add(res);
23310
+ req.on("close", () => {
23311
+ clients.delete(res);
23312
+ });
23313
+ return;
23314
+ }
23315
+ if (!tokenOk(req)) {
23316
+ send(res, 401, { error: "unauthorized" });
23317
+ return;
23318
+ }
23319
+ if (!originOk(req, port)) {
23320
+ send(res, 403, { error: "origin refused" });
23321
+ return;
23322
+ }
23323
+ if (req.method === "GET" && path2 === "/api/snapshot") {
23324
+ send(res, 200, snapshot());
23325
+ return;
23326
+ }
23327
+ if (req.method === "POST" && (path2 === "/api/prompt" || path2 === "/api/command")) {
23328
+ const body = await readJson(req);
23329
+ const text = String(body.text ?? body.prompt ?? "").trim();
23330
+ if (!text) {
23331
+ send(res, 400, { error: "text required" });
23332
+ return;
23333
+ }
23334
+ if (ended) {
23335
+ send(res, 409, { error: "session ended" });
23336
+ return;
23337
+ }
23338
+ emit({ type: "user", text });
23339
+ const queued = hub.push(text);
23340
+ send(res, queued ? 202 : 409, { ok: queued });
23341
+ return;
23342
+ }
23343
+ if (req.method === "POST" && path2 === "/api/exec") {
23344
+ const body = await readJson(req);
23345
+ const id4 = String(body.id ?? "");
23346
+ const decision = body.decision === "approve" ? "approve" : "deny";
23347
+ const pending = execs.get(id4);
23348
+ if (!pending) {
23349
+ send(res, 404, { error: "unknown exec" });
23350
+ return;
23351
+ }
23352
+ execs.delete(id4);
23353
+ pendingExec = void 0;
23354
+ pending.resolve(decision);
23355
+ emit({ type: "exec_resolved", id: id4, decision });
23356
+ send(res, 200, { ok: true });
23357
+ return;
23358
+ }
23359
+ if (req.method === "POST" && path2 === "/api/login") {
23360
+ try {
23361
+ const started = await beginCritiqueInferenceLogin({
23362
+ home: critiqueCodeHome(env),
23363
+ env,
23364
+ io: {
23365
+ note: (message) => emit({ type: "note", text: message })
23366
+ }
23367
+ });
23368
+ send(res, 202, { ok: true, verification_uri: started.verificationUri });
23369
+ void started.finished.then(() => {
23370
+ emit({ type: "note", text: "Inference is connected. Restart critique-code web if you want this session on critique/auto." });
23371
+ }).catch((error) => {
23372
+ const text = error instanceof Error ? error.message : String(error);
23373
+ emit({ type: "error", text: humanizeCritiqueCodeWebError(text) });
23374
+ });
23375
+ } catch (error) {
23376
+ const text = error instanceof Error ? error.message : String(error);
23377
+ emit({ type: "error", text: humanizeCritiqueCodeWebError(text) });
23378
+ send(res, 400, { error: humanizeCritiqueCodeWebError(text) });
23379
+ }
23380
+ return;
23381
+ }
23382
+ send(res, 404, { error: "not found" });
23383
+ });
23384
+ let portValue = 0;
23385
+ const listeningPort = () => portValue;
23386
+ await new Promise((resolve13, reject) => {
23387
+ server.once("error", reject);
23388
+ server.listen(input.port ?? 0, bindHost, () => {
23389
+ const address = server.address();
23390
+ if (!address || typeof address === "string") {
23391
+ reject(new Error("CritiqueCode web UI failed to bind loopback."));
23392
+ return;
23393
+ }
23394
+ portValue = address.port;
23395
+ resolve13();
23396
+ });
23397
+ });
23398
+ const url = `http://127.0.0.1:${portValue}`;
23399
+ input.stderr.write(`CritiqueCode web UI ${url}
23400
+ Loopback only. The author session stays in this process.
23401
+ `);
23402
+ const session = runLocalCritiqueCodeAuthor({
23403
+ cwd: repositoryRoot,
23404
+ env,
23405
+ stdout: input.stdout,
23406
+ stderr: {
23407
+ write(chunk) {
23408
+ input.stderr.write(chunk);
23409
+ const text = chunk.trim();
23410
+ if (!text) return;
23411
+ if (/evidence harness|checkpoint|Repair Portfolio|none_promoted|not a correctness proof|transcript is withheld/i.test(text)) {
23412
+ lastReviewText = text.slice(0, 4e3);
23413
+ emit({ type: "review", text: lastReviewText });
23414
+ }
23415
+ }
23416
+ },
23417
+ lines: hub,
23418
+ driver: input.driver,
23419
+ ...input.review ? { review: input.review } : {},
23420
+ ...input.now ? { now: input.now } : {},
23421
+ ...input.checks ? { checks: input.checks } : {},
23422
+ ...input.manifest ? { manifest: input.manifest } : {},
23423
+ host_tools: input.host_tools ?? "discover",
23424
+ ...input.store_root ? { store_root: input.store_root } : {},
23425
+ ...input.specialist_models ? { specialist_models: input.specialist_models } : {},
23426
+ ...input.author_models ? { author_models: input.author_models } : {},
23427
+ approve_exec: approveExec,
23428
+ on_kernel_event: mapKernel,
23429
+ settings_ui: {
23430
+ async select() {
23431
+ return "back";
23432
+ },
23433
+ async prompt() {
23434
+ return void 0;
23435
+ },
23436
+ note(message) {
23437
+ emit({ type: "note", text: message });
23438
+ }
23439
+ }
23440
+ }).then((result) => {
23441
+ ended = result.kind === "kernel_required" ? "kernel_required" : result.reason;
23442
+ emit({ type: "ended", reason: ended });
23443
+ hub.close();
23444
+ return result;
23445
+ });
23446
+ if (input.open !== false) {
23447
+ try {
23448
+ spawn5("open", [url], { stdio: "ignore", detached: true }).unref();
23449
+ } catch {
23450
+ }
23451
+ }
23452
+ return {
23453
+ url,
23454
+ token,
23455
+ async close() {
23456
+ hub.close();
23457
+ for (const pending of execs.values()) pending.resolve("deny");
23458
+ execs.clear();
23459
+ for (const client of clients) client.end();
23460
+ clients.clear();
23461
+ await new Promise((resolve13, reject) => {
23462
+ server.close((error) => {
23463
+ error ? reject(error) : resolve13();
23464
+ });
23465
+ });
23466
+ await session.catch(() => void 0);
23467
+ }
23468
+ };
23469
+ }
23470
+
22629
23471
  // lib/finish/critique-code-program.ts
22630
23472
  var CRITIQUE_CODE_EXIT = {
22631
23473
  ok: 0,
@@ -22643,6 +23485,7 @@ Usage:
22643
23485
  pnpm critique-code
22644
23486
  pnpm --filter @critiquedotsh/harness start
22645
23487
  critique-code | critique-code chat [--intent <text>] [--models provider/model,...] [--cwd <dir>] [--store <dir>] [--voice] [--json]
23488
+ critique-code web [--port <n>] [--cwd <dir>] [--store <dir>] [--models ...]
22646
23489
  critique-code login
22647
23490
  critique-code settings | keys | models
22648
23491
  critique-code review [--depth quick|standard|paranoid] [--focus general,security] [--models provider/model,...] [--base <ref>] [--intent <text>] [--cwd <dir>] [--store <dir>]
@@ -22669,6 +23512,7 @@ Interactive author commands:
22669
23512
  /exit
22670
23513
 
22671
23514
  Interactive TTY author sessions keep stdout quiet; pass --json for the session envelope.
23515
+ \`critique-code web\` serves a loopback browser UI on 127.0.0.1. The author kernel still runs in this process.
22672
23516
  Other commands print JSON on stdout. Live chrome goes to stderr.
22673
23517
  Author implements, then the controller forces review + verified repair.
22674
23518
  none_promoted is not a correctness proof.
@@ -22704,7 +23548,8 @@ function parseInvocation(argv) {
22704
23548
  help: { type: "boolean", short: "h" },
22705
23549
  json: { type: "boolean" },
22706
23550
  voice: { type: "boolean" },
22707
- repair: { type: "string" }
23551
+ repair: { type: "string" },
23552
+ port: { type: "string" }
22708
23553
  }
22709
23554
  });
22710
23555
  } catch (error) {
@@ -22816,6 +23661,34 @@ function parseInvocation(argv) {
22816
23661
  if (command === "import-skills") {
22817
23662
  return { command: "skills", import: true, ...cwd ? { cwd } : {} };
22818
23663
  }
23664
+ if (command === "web") {
23665
+ const portRaw = stringFlag(parsed.values.port);
23666
+ let port;
23667
+ if (portRaw) {
23668
+ port = Number(portRaw);
23669
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
23670
+ return { command: "usage", error: "Port must be an integer from 0 to 65535. 0 means an ephemeral loopback port." };
23671
+ }
23672
+ }
23673
+ let specialist_models;
23674
+ if (models) {
23675
+ try {
23676
+ specialist_models = parseCritiqueCodeModels(models);
23677
+ if (specialist_models.length === 0) {
23678
+ return { command: "usage", error: "Models must be a comma-separated provider:family:model_id or provider/model list, or a JSON array." };
23679
+ }
23680
+ } catch {
23681
+ return { command: "usage", error: "Models must be a comma-separated provider:family:model_id or provider/model list, or a JSON array." };
23682
+ }
23683
+ }
23684
+ return {
23685
+ command: "web",
23686
+ ...cwd ? { cwd } : {},
23687
+ ...storeRoot ? { store_root: storeRoot } : {},
23688
+ ...specialist_models ? { specialist_models, author_models: specialist_models } : {},
23689
+ ...port !== void 0 ? { port } : {}
23690
+ };
23691
+ }
22819
23692
  if (command === "settings" || command === "models" || command === "keys" || command === "login") {
22820
23693
  return {
22821
23694
  command: "settings",
@@ -22829,7 +23702,7 @@ function stringFlag(value) {
22829
23702
  }
22830
23703
  function interactiveCommand(argv) {
22831
23704
  const command = argv.find((arg) => arg !== "--" && !arg.startsWith("-"));
22832
- return command === void 0 || command === "chat" || command === "settings" || command === "keys" || command === "models" || command === "login";
23705
+ return command === void 0 || command === "chat" || command === "web" || command === "settings" || command === "keys" || command === "models" || command === "login";
22833
23706
  }
22834
23707
  function writeJson(io, value) {
22835
23708
  if (io.stdin?.isTTY === true && !io.argv.includes("--json") && interactiveCommand(io.argv)) return;
@@ -22894,6 +23767,45 @@ ${helpText}`);
22894
23767
  });
22895
23768
  return CRITIQUE_CODE_EXIT.ok;
22896
23769
  }
23770
+ if (invocation.command === "web") {
23771
+ const started = await (io.web ?? startCritiqueCodeWebServer)({
23772
+ cwd: invocation.cwd ?? io.cwd,
23773
+ env: io.env ?? process.env,
23774
+ stdout: io.stdout,
23775
+ stderr: io.stderr,
23776
+ open: !io.web,
23777
+ ...io.driver ? { driver: io.driver } : {},
23778
+ ...io.review ? { review: io.review } : {},
23779
+ ...invocation.store_root ? { store_root: invocation.store_root } : {},
23780
+ ...invocation.specialist_models ? { specialist_models: invocation.specialist_models } : {},
23781
+ ...invocation.author_models ? { author_models: invocation.author_models } : {},
23782
+ ...invocation.port !== void 0 ? { port: invocation.port } : {}
23783
+ });
23784
+ if ("kind" in started && started.kind === "kernel_required") {
23785
+ writeJson(io, { schema_version: "critique.code-cli.v1", command: "web", ...started });
23786
+ io.stderr.write("Interactive author is kernel_required without a live Pi driver.\nnone_promoted is not a correctness proof.\n");
23787
+ return CRITIQUE_CODE_EXIT.kernel_required;
23788
+ }
23789
+ const server = started;
23790
+ writeJson(io, {
23791
+ schema_version: "critique.code-cli.v1",
23792
+ command: "web",
23793
+ url: server.url,
23794
+ bind: "127.0.0.1"
23795
+ });
23796
+ if (io.web) {
23797
+ await server.close();
23798
+ return CRITIQUE_CODE_EXIT.ok;
23799
+ }
23800
+ await new Promise((resolve13) => {
23801
+ const stop = () => {
23802
+ void server.close().finally(resolve13);
23803
+ };
23804
+ process.once("SIGINT", stop);
23805
+ process.once("SIGTERM", stop);
23806
+ });
23807
+ return CRITIQUE_CODE_EXIT.ok;
23808
+ }
22897
23809
  if (invocation.command === "chat") {
22898
23810
  const result2 = await (io.chat ?? runLocalCritiqueCodeAuthor)({
22899
23811
  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.4",
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": {
@@ -34,7 +34,7 @@
34
34
  "start": "node --import ../../scripts/register-test-path-alias.mjs --experimental-strip-types src/cli.ts",
35
35
  "install:pi": "npm install --prefix .vendor",
36
36
  "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"
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 ../../lib/finish/critique-code-web-server.test.ts"
38
38
  },
39
39
  "dependencies": {
40
40
  "typescript": "^5.9.3"