repoburg 1.3.253 → 1.3.255

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.
@@ -43557,12 +43557,25 @@ function examplesRoot() {
43557
43557
  }
43558
43558
  return null;
43559
43559
  }
43560
+ function isInAppFolder(cwd) {
43561
+ if (!cwd) return false;
43562
+ const rel = path3.relative(path3.resolve(appsRoot()), path3.resolve(cwd));
43563
+ return rel !== "" && !path3.isAbsolute(rel) && rel.split(path3.sep)[0] !== "..";
43564
+ }
43565
+ function appsConventions() {
43566
+ return APPS_MD_SKELETON;
43567
+ }
43560
43568
  function ensureAppsRoot() {
43561
43569
  const root = appsRoot();
43562
43570
  fs.mkdirSync(root, { recursive: true });
43563
43571
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
43564
43572
  const file = path3.join(root, name);
43565
- if (!fs.existsSync(file)) fs.writeFileSync(file, APPS_MD_SKELETON);
43573
+ try {
43574
+ if (fs.existsSync(file) && fs.readFileSync(file, "utf-8") === APPS_MD_FILE) continue;
43575
+ fs.writeFileSync(file, APPS_MD_FILE);
43576
+ } catch (err) {
43577
+ console.warn(`[apps] could not update ${file}: ${err.message}`);
43578
+ }
43566
43579
  }
43567
43580
  }
43568
43581
  var APPS_MD_SKELETON = `# Mini Apps
@@ -43624,6 +43637,11 @@ open in AgentPane for a tool call to succeed in that case.
43624
43637
  To give the app a structured result, put this requirement in the job prompt:
43625
43638
  "End your answer with a single JSON code block matching this schema: ..."
43626
43639
  `;
43640
+ var APPS_MD_FILE = `<!-- Generated by AgentPane: overwritten whenever it differs from the built-in
43641
+ conventions, so edits here are lost. Put app-specific notes in the app
43642
+ folder's own CLAUDE.md / AGENTS.md. -->
43643
+
43644
+ ${APPS_MD_SKELETON}`;
43627
43645
  function readManifestIn(dir) {
43628
43646
  try {
43629
43647
  const raw = fs.readFileSync(path3.join(dir, "manifest.json"), "utf-8");
@@ -44234,6 +44252,7 @@ function render(): void {
44234
44252
  async function registerAppRoutes(fastify, opts) {
44235
44253
  setAppsRootOverride(opts?.appsRoot ?? null);
44236
44254
  setExamplesRootOverride(opts?.examplesRoot ?? null);
44255
+ ensureAppsRoot();
44237
44256
  startAppsWatch((appId) => {
44238
44257
  invalidateHeadlessRuntime(appId);
44239
44258
  broadcast("app:source:updated", { appId });
@@ -44763,6 +44782,9 @@ function appServerClientOptions(_session, cwd) {
44763
44782
  const launchArgs = codexMiniAppLaunchArgs(cwd);
44764
44783
  return launchArgs ? { cwd, launchArgs } : { cwd, launchArgs: [] };
44765
44784
  }
44785
+ function threadInstructionParams(cwd) {
44786
+ return isInAppFolder(cwd) ? { developerInstructions: appsConventions() } : {};
44787
+ }
44766
44788
  function autoApproveToolUserInput(client) {
44767
44789
  client.onRequest("item/tool/requestUserInput", async (params) => {
44768
44790
  const questions = params.questions ?? [];
@@ -44787,6 +44809,7 @@ async function getOrCreateThread(session) {
44787
44809
  }
44788
44810
  const config = parseRuntimeConfig(session);
44789
44811
  const sandbox = sandboxType(config);
44812
+ const instructions = threadInstructionParams(cwd);
44790
44813
  let threadId;
44791
44814
  if (session.external_thread_id) {
44792
44815
  const result = await client.request("thread/resume", {
@@ -44794,7 +44817,8 @@ async function getOrCreateThread(session) {
44794
44817
  cwd,
44795
44818
  approvalPolicy: "never",
44796
44819
  sandbox,
44797
- model: session.model ?? void 0
44820
+ model: session.model ?? void 0,
44821
+ ...instructions
44798
44822
  });
44799
44823
  threadId = result.thread.id;
44800
44824
  } else {
@@ -44802,7 +44826,8 @@ async function getOrCreateThread(session) {
44802
44826
  cwd,
44803
44827
  approvalPolicy: "never",
44804
44828
  sandbox,
44805
- model: session.model ?? void 0
44829
+ model: session.model ?? void 0,
44830
+ ...instructions
44806
44831
  });
44807
44832
  threadId = result.thread.id;
44808
44833
  const db2 = getDb();
@@ -47716,6 +47741,16 @@ function turnImageDataUrls(turn) {
47716
47741
  if (!attachments) return [];
47717
47742
  return attachments.filter((a) => a?.media_type?.startsWith("image/") && a.data).map((a) => `data:${a.media_type};base64,${a.data}`);
47718
47743
  }
47744
+ function runtimePrompt(session, prompt, firstInput) {
47745
+ if (!firstInput || !isInAppFolder(session.cwd)) return prompt;
47746
+ const conventions = `<mini_app_conventions>
47747
+ ${appsConventions()}</mini_app_conventions>`;
47748
+ return prompt.startsWith("/") ? `${prompt}
47749
+
47750
+ ${conventions}` : `${conventions}
47751
+
47752
+ ${prompt}`;
47753
+ }
47719
47754
  async function startTurn3(session, turn) {
47720
47755
  const db2 = getDb();
47721
47756
  let client;
@@ -47736,6 +47771,7 @@ async function startTurn3(session, turn) {
47736
47771
  );
47737
47772
  }
47738
47773
  void registerMiniAppMcp(baseUrl, session.cwd ?? "");
47774
+ const firstInput = !turn.thread_id && !sessionMap3.has(session.id) && !session.external_thread_id;
47739
47775
  let entry;
47740
47776
  try {
47741
47777
  entry = turn.thread_id ? await ensureThreadSession(session, turn, client, baseUrl) : await ensureSession(session, client, baseUrl);
@@ -47781,7 +47817,7 @@ async function startTurn3(session, turn) {
47781
47817
  try {
47782
47818
  const images = turnImageDataUrls(turn);
47783
47819
  await entry.client.createInput(entry.repoburgSessionId, {
47784
- user_prompt: turn.user_prompt,
47820
+ user_prompt: runtimePrompt(session, turn.user_prompt, firstInput),
47785
47821
  execution_strategy: "auto_apply",
47786
47822
  ...images.length > 0 ? { images } : {}
47787
47823
  });
@@ -43544,12 +43544,25 @@ function examplesRoot() {
43544
43544
  }
43545
43545
  return null;
43546
43546
  }
43547
+ function isInAppFolder(cwd) {
43548
+ if (!cwd) return false;
43549
+ const rel = path3.relative(path3.resolve(appsRoot()), path3.resolve(cwd));
43550
+ return rel !== "" && !path3.isAbsolute(rel) && rel.split(path3.sep)[0] !== "..";
43551
+ }
43552
+ function appsConventions() {
43553
+ return APPS_MD_SKELETON;
43554
+ }
43547
43555
  function ensureAppsRoot() {
43548
43556
  const root = appsRoot();
43549
43557
  fs.mkdirSync(root, { recursive: true });
43550
43558
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
43551
43559
  const file = path3.join(root, name);
43552
- if (!fs.existsSync(file)) fs.writeFileSync(file, APPS_MD_SKELETON);
43560
+ try {
43561
+ if (fs.existsSync(file) && fs.readFileSync(file, "utf-8") === APPS_MD_FILE) continue;
43562
+ fs.writeFileSync(file, APPS_MD_FILE);
43563
+ } catch (err) {
43564
+ console.warn(`[apps] could not update ${file}: ${err.message}`);
43565
+ }
43553
43566
  }
43554
43567
  }
43555
43568
  var APPS_MD_SKELETON = `# Mini Apps
@@ -43611,6 +43624,11 @@ open in AgentPane for a tool call to succeed in that case.
43611
43624
  To give the app a structured result, put this requirement in the job prompt:
43612
43625
  "End your answer with a single JSON code block matching this schema: ..."
43613
43626
  `;
43627
+ var APPS_MD_FILE = `<!-- Generated by AgentPane: overwritten whenever it differs from the built-in
43628
+ conventions, so edits here are lost. Put app-specific notes in the app
43629
+ folder's own CLAUDE.md / AGENTS.md. -->
43630
+
43631
+ ${APPS_MD_SKELETON}`;
43614
43632
  function readManifestIn(dir) {
43615
43633
  try {
43616
43634
  const raw = fs.readFileSync(path3.join(dir, "manifest.json"), "utf-8");
@@ -44221,6 +44239,7 @@ function render(): void {
44221
44239
  async function registerAppRoutes(fastify, opts) {
44222
44240
  setAppsRootOverride(opts?.appsRoot ?? null);
44223
44241
  setExamplesRootOverride(opts?.examplesRoot ?? null);
44242
+ ensureAppsRoot();
44224
44243
  startAppsWatch((appId) => {
44225
44244
  invalidateHeadlessRuntime(appId);
44226
44245
  broadcast("app:source:updated", { appId });
@@ -44750,6 +44769,9 @@ function appServerClientOptions(_session, cwd) {
44750
44769
  const launchArgs = codexMiniAppLaunchArgs(cwd);
44751
44770
  return launchArgs ? { cwd, launchArgs } : { cwd, launchArgs: [] };
44752
44771
  }
44772
+ function threadInstructionParams(cwd) {
44773
+ return isInAppFolder(cwd) ? { developerInstructions: appsConventions() } : {};
44774
+ }
44753
44775
  function autoApproveToolUserInput(client) {
44754
44776
  client.onRequest("item/tool/requestUserInput", async (params) => {
44755
44777
  const questions = params.questions ?? [];
@@ -44774,6 +44796,7 @@ async function getOrCreateThread(session) {
44774
44796
  }
44775
44797
  const config = parseRuntimeConfig(session);
44776
44798
  const sandbox = sandboxType(config);
44799
+ const instructions = threadInstructionParams(cwd);
44777
44800
  let threadId;
44778
44801
  if (session.external_thread_id) {
44779
44802
  const result = await client.request("thread/resume", {
@@ -44781,7 +44804,8 @@ async function getOrCreateThread(session) {
44781
44804
  cwd,
44782
44805
  approvalPolicy: "never",
44783
44806
  sandbox,
44784
- model: session.model ?? void 0
44807
+ model: session.model ?? void 0,
44808
+ ...instructions
44785
44809
  });
44786
44810
  threadId = result.thread.id;
44787
44811
  } else {
@@ -44789,7 +44813,8 @@ async function getOrCreateThread(session) {
44789
44813
  cwd,
44790
44814
  approvalPolicy: "never",
44791
44815
  sandbox,
44792
- model: session.model ?? void 0
44816
+ model: session.model ?? void 0,
44817
+ ...instructions
44793
44818
  });
44794
44819
  threadId = result.thread.id;
44795
44820
  const db2 = getDb();
@@ -47703,6 +47728,16 @@ function turnImageDataUrls(turn) {
47703
47728
  if (!attachments) return [];
47704
47729
  return attachments.filter((a) => a?.media_type?.startsWith("image/") && a.data).map((a) => `data:${a.media_type};base64,${a.data}`);
47705
47730
  }
47731
+ function runtimePrompt(session, prompt, firstInput) {
47732
+ if (!firstInput || !isInAppFolder(session.cwd)) return prompt;
47733
+ const conventions = `<mini_app_conventions>
47734
+ ${appsConventions()}</mini_app_conventions>`;
47735
+ return prompt.startsWith("/") ? `${prompt}
47736
+
47737
+ ${conventions}` : `${conventions}
47738
+
47739
+ ${prompt}`;
47740
+ }
47706
47741
  async function startTurn3(session, turn) {
47707
47742
  const db2 = getDb();
47708
47743
  let client;
@@ -47723,6 +47758,7 @@ async function startTurn3(session, turn) {
47723
47758
  );
47724
47759
  }
47725
47760
  void registerMiniAppMcp(baseUrl, session.cwd ?? "");
47761
+ const firstInput = !turn.thread_id && !sessionMap3.has(session.id) && !session.external_thread_id;
47726
47762
  let entry;
47727
47763
  try {
47728
47764
  entry = turn.thread_id ? await ensureThreadSession(session, turn, client, baseUrl) : await ensureSession(session, client, baseUrl);
@@ -47768,7 +47804,7 @@ async function startTurn3(session, turn) {
47768
47804
  try {
47769
47805
  const images = turnImageDataUrls(turn);
47770
47806
  await entry.client.createInput(entry.repoburgSessionId, {
47771
- user_prompt: turn.user_prompt,
47807
+ user_prompt: runtimePrompt(session, turn.user_prompt, firstInput),
47772
47808
  execution_strategy: "auto_apply",
47773
47809
  ...images.length > 0 ? { images } : {}
47774
47810
  });
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Sample mini app — PR Review (multi-file TypeScript).
3
+ *
4
+ * Entry file: owns the UI state and wires window.AgentPane (turn events,
5
+ * the submit_review tool call, persistent state). Domain helpers (review
6
+ * prompt, verdict parsing) live in review.ts; shared types in types.ts.
7
+ *
8
+ * The pane bundles this file together with every module it imports into
9
+ * ONE compiled script — edit freely, the imports just work.
10
+ */
11
+ import type { AppUi, ReviewVerdict } from './types';
12
+ import { isReviewStatus } from './types';
13
+ import { REVIEW_PROMPT, parseToolVerdict, parseTurnVerdict } from './review';
14
+
15
+ const ui: AppUi = {
16
+ status: 'idle',
17
+ blockers: 0,
18
+ summary: '',
19
+ repo: '',
20
+ runtime: 'repoburg',
21
+ sessionId: null,
22
+ sessionRepo: null,
23
+ sessionRuntime: null,
24
+ running: false,
25
+ };
26
+
27
+ /** Typed DOM lookup (markup lives in index.html). */
28
+ function byId<T extends HTMLElement>(id: string): T | null {
29
+ return document.getElementById(id) as T | null;
30
+ }
31
+
32
+ /** Persist the UI state — survives pane reloads (app_instances row). */
33
+ function save(): Promise<unknown> {
34
+ return AgentPane.setState({ ...ui });
35
+ }
36
+
37
+ function render(): void {
38
+ const verdict = byId('verdict');
39
+ if (verdict) verdict.className = 'verdict ' + ui.status;
40
+ const count = byId('count');
41
+ if (count) count.textContent = ui.status === 'green' ? '\u2714' : String(ui.blockers);
42
+ const text = byId('verdict-text');
43
+ if (text) {
44
+ text.textContent =
45
+ ui.status === 'green' ? 'Review passed' :
46
+ ui.status === 'red' ? 'Review failed \u2014 ' + ui.blockers + ' blocker(s)' :
47
+ ui.status === 'running' ? 'Review running\u2026' :
48
+ ui.status === 'error' ? 'Review errored' : 'No review yet';
49
+ }
50
+ const summary = byId('summary');
51
+ if (summary) summary.textContent = ui.summary;
52
+ const repo = byId<HTMLInputElement>('repo');
53
+ if (repo) repo.value = ui.repo;
54
+ const runtime = byId<HTMLSelectElement>('runtime');
55
+ if (runtime) runtime.value = ui.runtime;
56
+ const run = byId<HTMLButtonElement>('run');
57
+ if (run) {
58
+ run.disabled = ui.running;
59
+ run.textContent = ui.running ? 'Running\u2026' : 'Run review';
60
+ }
61
+ if (!ui.running) {
62
+ const progress = byId('progress');
63
+ if (progress) progress.textContent = '';
64
+ }
65
+ }
66
+
67
+ function applyVerdict(verdict: ReviewVerdict): void {
68
+ ui.status = verdict.status;
69
+ ui.blockers = verdict.blockers;
70
+ ui.summary = verdict.summary;
71
+ ui.running = false;
72
+ render();
73
+ }
74
+
75
+ // Agent → app tool call (MCP gateway). Registered SYNCHRONOUSLY at startup
76
+ // so the headless runtime can serve it without an open tab. The verdict
77
+ // arrives structured — no JSON parsing of the agent answer is needed.
78
+ AgentPane.onToolCall(async (payload) => {
79
+ if (payload.tool !== 'submit_review') {
80
+ await AgentPane.resolveToolCall(payload.callId, { error: 'unknown tool: ' + payload.tool });
81
+ return;
82
+ }
83
+ const verdict = parseToolVerdict(payload.args ?? {});
84
+ if (!verdict) {
85
+ await AgentPane.resolveToolCall(payload.callId, { error: 'invalid submit_review arguments' });
86
+ return;
87
+ }
88
+ applyVerdict(verdict);
89
+ await save();
90
+ await AgentPane.resolveToolCall(payload.callId, { recorded: true, status: ui.status });
91
+ });
92
+
93
+ async function init(): Promise<void> {
94
+ const state = await AgentPane.getState();
95
+ const d = state?.data ?? {};
96
+ ui.repo = typeof d.repo === 'string' ? d.repo : '';
97
+ ui.runtime = typeof d.runtime === 'string' && d.runtime ? d.runtime : 'repoburg';
98
+ ui.status = isReviewStatus(d.status) ? d.status : 'idle';
99
+ ui.blockers = typeof d.blockers === 'number' ? d.blockers : 0;
100
+ ui.summary = typeof d.summary === 'string' ? d.summary : '';
101
+ ui.sessionId = typeof d.sessionId === 'string' ? d.sessionId : null;
102
+ ui.sessionRepo = typeof d.sessionRepo === 'string' ? d.sessionRepo : null;
103
+ ui.sessionRuntime = typeof d.sessionRuntime === 'string' ? d.sessionRuntime : null;
104
+ ui.running = d.running === true;
105
+ render();
106
+
107
+ // Turn lifecycle pushed by the pane (host → app relay). Only turns of a
108
+ // review job started here (Run review) are parsed — a verdict submitted
109
+ // via the submit_review tool already set the state and must not be
110
+ // clobbered by the same turn's plain-text completion.
111
+ AgentPane.onTurnCompleted((event, payload) => {
112
+ if (!ui.running) return;
113
+ ui.running = false;
114
+ if (event === 'turn:failed') {
115
+ ui.status = 'error';
116
+ ui.summary = payload?.error ? String(payload.error) : 'turn failed';
117
+ } else {
118
+ const verdict = parseTurnVerdict(payload?.response ?? '');
119
+ if (verdict) {
120
+ applyVerdict(verdict);
121
+ } else {
122
+ ui.status = 'error';
123
+ ui.summary = 'The agent answer did not end with a JSON block.';
124
+ }
125
+ }
126
+ save();
127
+ render();
128
+ });
129
+
130
+ // Live streaming: show the agent's latest delta text while the review runs.
131
+ AgentPane.onTurnDelta((payload) => {
132
+ if (!ui.running) return;
133
+ const el = byId('progress');
134
+ if (el) el.textContent = String(payload.delta ?? '');
135
+ });
136
+ }
137
+
138
+ const runBtn = byId<HTMLButtonElement>('run');
139
+ if (runBtn) {
140
+ runBtn.addEventListener('click', async () => {
141
+ const repoInput = byId<HTMLInputElement>('repo');
142
+ const runtimeInput = byId<HTMLSelectElement>('runtime');
143
+ if (!repoInput || !runtimeInput) return;
144
+ ui.repo = repoInput.value.trim();
145
+ ui.runtime = runtimeInput.value;
146
+ if (!ui.repo) return;
147
+ ui.running = true;
148
+ ui.status = 'running';
149
+ render();
150
+ try {
151
+ // Reuse the session only when it was created for THIS repo + runtime;
152
+ // a changed repo/runtime needs a fresh session (cwd is fixed per session).
153
+ let sessionId: string;
154
+ if (ui.sessionId === null || ui.sessionRepo !== ui.repo || ui.sessionRuntime !== ui.runtime) {
155
+ const session = await AgentPane.createSession({
156
+ title: 'PR Review job',
157
+ runtime_type: ui.runtime,
158
+ cwd: ui.repo,
159
+ });
160
+ sessionId = session.id;
161
+ ui.sessionRepo = ui.repo;
162
+ ui.sessionRuntime = ui.runtime;
163
+ } else {
164
+ sessionId = ui.sessionId;
165
+ }
166
+ ui.sessionId = sessionId;
167
+ await save();
168
+ await AgentPane.submitTurn(sessionId, REVIEW_PROMPT);
169
+ } catch (err) {
170
+ ui.running = false;
171
+ ui.status = 'error';
172
+ ui.summary = err instanceof Error ? err.message : String(err);
173
+ // A cached session id can go stale (session deleted, other pane DB) —
174
+ // drop it so the next run creates a fresh session instead of 404 loops.
175
+ ui.sessionId = null;
176
+ ui.sessionRepo = null;
177
+ ui.sessionRuntime = null;
178
+ save();
179
+ render();
180
+ }
181
+ });
182
+ }
183
+
184
+ init();
@@ -0,0 +1,43 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ body { font-family: Inter, system-ui, sans-serif; margin: 24px; color: #3c3c3c; background: #fff; }
7
+ h1 { font-size: 16px; margin: 0 0 16px; }
8
+ .row { display: flex; gap: 8px; margin-bottom: 16px; }
9
+ input { flex: 1; padding: 8px 10px; font-size: 13px; border: 1px solid #e2e0db; border-radius: 6px; }
10
+ select { padding: 8px; font-size: 13px; border: 1px solid #e2e0db; border-radius: 6px; background: #fff; }
11
+ button { padding: 8px 16px; font-size: 13px; background: #d97706; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
12
+ button:disabled { opacity: 0.5; cursor: default; }
13
+ .verdict { display: none; align-items: center; gap: 8px; padding: 16px; border-radius: 8px; font-size: 14px; font-weight: 600; margin-bottom: 16px; }
14
+ .verdict.idle, .verdict.running, .verdict.error { display: flex; background: #f5f3ef; color: #8a857c; }
15
+ .verdict.green { display: flex; background: rgba(34,197,94,0.1); color: #15803d; }
16
+ .verdict.red { display: flex; background: rgba(239,68,68,0.1); color: #b91c1c; }
17
+ .count { font-size: 22px; }
18
+ .summary { font-size: 13px; color: #8a857c; margin-bottom: 8px; }
19
+ .progress { font-size: 11px; color: #b0aaa0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; margin-bottom: 8px; min-height: 14px; }
20
+ .note { font-size: 12px; color: #8a857c; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <h1>PR Review</h1>
25
+ <!-- Markup only — the pane injects the shim (window.AgentPane) into the
26
+ head and the compiled app logic (app.ts + its imported modules,
27
+ bundled) before the closing body tag. Do NOT reference the logic
28
+ files from here (a relative src cannot resolve in a srcdoc iframe). -->
29
+ <div id="verdict" class="verdict"><span id="count" class="count"></span><span id="verdict-text"></span></div>
30
+ <div class="row">
31
+ <input id="repo" placeholder="Repository path (cwd for the review session)" />
32
+ <select id="runtime" title="Agent runtime for the review session">
33
+ <option value="repoburg">Repoburg</option>
34
+ <option value="claude">Claude</option>
35
+ <option value="codex">Codex</option>
36
+ </select>
37
+ <button id="run">Run review</button>
38
+ </div>
39
+ <div id="summary" class="summary"></div>
40
+ <div id="progress" class="progress"></div>
41
+ <p class="note">This is a sample mini app. It opens an agent session on the repository, runs a review job, and stores the verdict in its persistent state.</p>
42
+ </body>
43
+ </html>
@@ -0,0 +1,30 @@
1
+ {
2
+ "title": "PR Review",
3
+ "description": "Sample mini app: run a repository review and show a green/red verdict with the blocker count.",
4
+ "icon": "🔍",
5
+ "tools": [
6
+ {
7
+ "name": "submit_review",
8
+ "description": "Submit the final verdict of the repository review. Call this exactly once, at the end of the review.",
9
+ "inputSchema": {
10
+ "type": "object",
11
+ "properties": {
12
+ "status": {
13
+ "type": "string",
14
+ "enum": ["green", "red"],
15
+ "description": "green = review passed, red = blocking issues found"
16
+ },
17
+ "blockers": {
18
+ "type": "number",
19
+ "description": "Number of blocking issues found (0 for green)."
20
+ },
21
+ "summary": {
22
+ "type": "string",
23
+ "description": "One-line verdict shown in the app."
24
+ }
25
+ },
26
+ "required": ["status", "blockers", "summary"]
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PR Review domain logic — the review job prompt and verdict parsing
3
+ * (both the fallback JSON-block contract and the submit_review tool args).
4
+ */
5
+ import type { ReviewVerdict } from './types';
6
+
7
+ /** Review job sent to the agent: JSON result contract + tool call. */
8
+ export const REVIEW_PROMPT =
9
+ 'Review the current repository as a senior engineer. Look at the recent ' +
10
+ 'changes (git status / recent commits) and report blocking issues only. ' +
11
+ 'When the review is done, call the MCP tool `pr-review__submit_review` ' +
12
+ 'exactly once with the verdict: status ("green" or "red"), blockers ' +
13
+ '(number), and a one-line summary.';
14
+
15
+ /** Parse the LAST ```json fenced block from the agent's answer. */
16
+ export function parseJsonBlock(text: string): unknown {
17
+ const blocks = text.match(/```json\n([\s\S]*?)\n```/g) ?? [];
18
+ if (blocks.length === 0) return null;
19
+ try {
20
+ return JSON.parse(blocks[blocks.length - 1].replace(/```json\n|\n```/g, ''));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /** Fallback verdict from the turn's plain-text answer (JSON block). */
27
+ export function parseTurnVerdict(text: string): ReviewVerdict | null {
28
+ const raw = parseJsonBlock(text);
29
+ if (!raw || typeof raw !== 'object') return null;
30
+ const obj = raw as Record<string, unknown>;
31
+ return normalizeVerdict(obj);
32
+ }
33
+
34
+ /** Structured verdict from the submit_review tool call arguments. */
35
+ export function parseToolVerdict(args: Record<string, unknown>): ReviewVerdict | null {
36
+ return normalizeVerdict(args);
37
+ }
38
+
39
+ /** Map one verdict-shaped object to ReviewVerdict; null when invalid. */
40
+ function normalizeVerdict(obj: Record<string, unknown>): ReviewVerdict | null {
41
+ if (obj.status !== 'green' && obj.status !== 'red') return null;
42
+ return {
43
+ status: obj.status,
44
+ blockers: typeof obj.blockers === 'number' ? obj.blockers : 0,
45
+ summary: typeof obj.summary === 'string' ? obj.summary : '',
46
+ };
47
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared types for the PR Review mini app.
3
+ */
4
+
5
+ /** Verdict/UI status machine of the app. */
6
+ export type ReviewStatus = 'idle' | 'running' | 'green' | 'red' | 'error';
7
+
8
+ /** A review outcome — from the submit_review tool or a JSON result block. */
9
+ export interface ReviewVerdict {
10
+ status: 'green' | 'red';
11
+ blockers: number;
12
+ summary: string;
13
+ }
14
+
15
+ /** Persistent UI state — survives pane reloads (app_instances row). */
16
+ export interface AppUi {
17
+ status: ReviewStatus;
18
+ blockers: number;
19
+ summary: string;
20
+ repo: string;
21
+ runtime: string;
22
+ sessionId: string | null;
23
+ sessionRepo: string | null;
24
+ sessionRuntime: string | null;
25
+ running: boolean;
26
+ }
27
+
28
+ /** Narrow an unknown persisted field back to ReviewStatus. */
29
+ export function isReviewStatus(value: unknown): value is ReviewStatus {
30
+ return (
31
+ value === 'idle' ||
32
+ value === 'running' ||
33
+ value === 'green' ||
34
+ value === 'red' ||
35
+ value === 'error'
36
+ );
37
+ }