@loicngr/kobo 1.7.7 → 1.7.9

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 (102) hide show
  1. package/AGENTS.md +29 -0
  2. package/README.md +62 -4
  3. package/dist/mcp-server/kobo-tasks-server.js +27 -0
  4. package/dist/server/routes/health.js +14 -0
  5. package/dist/server/routes/workspaces.js +60 -16
  6. package/dist/server/services/agent/engines/claude-code/capabilities.js +7 -0
  7. package/dist/server/services/agent/engines/codex/capabilities.js +18 -0
  8. package/dist/server/services/agent/engines/codex/client.js +36 -0
  9. package/dist/server/services/agent/engines/codex/engine.js +276 -0
  10. package/dist/server/services/agent/engines/codex/event-mapper.js +473 -0
  11. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +60 -0
  12. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +31 -0
  13. package/dist/server/services/agent/engines/codex/options-builder.js +81 -0
  14. package/dist/server/services/agent/engines/codex/protocol/types.js +11 -0
  15. package/dist/server/services/agent/engines/codex/server-requests.js +99 -0
  16. package/dist/server/services/agent/engines/codex/spawn.js +27 -0
  17. package/dist/server/services/agent/engines/registry.js +2 -0
  18. package/dist/server/services/agent/orchestrator.js +1 -1
  19. package/dist/server/services/auto-loop-service.js +16 -2
  20. package/dist/server/services/pr-watcher-service.js +84 -33
  21. package/dist/server/services/review-template-service.js +24 -31
  22. package/dist/server/services/settings-service.js +140 -6
  23. package/dist/server/services/skill-suite-prompts.js +80 -0
  24. package/dist/server/utils/git-ops.js +69 -15
  25. package/dist/server/utils/paths.js +7 -0
  26. package/dist/shared/auto-loop-prompts.js +18 -1
  27. package/dist/shared/codex-models.js +43 -0
  28. package/dist/shared/project-colors.js +23 -0
  29. package/dist/shared/skill-suite-prompts.js +66 -0
  30. package/package.json +2 -1
  31. package/src/client/dist/spa/assets/ActivityFeed-CKjFT9t6.js +8 -0
  32. package/src/client/dist/spa/assets/{ActivityFeed-tE4LVYck.css → ActivityFeed-WjiQ9716.css} +1 -1
  33. package/src/client/dist/spa/assets/{ClosePopup-DTcbxsC0.js → ClosePopup-DMnQG6nw.js} +1 -1
  34. package/src/client/dist/spa/assets/CreatePage-BhFrUkEN.js +2 -0
  35. package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +1 -0
  36. package/src/client/dist/spa/assets/{DiffViewer-D-uNbBq0.js → DiffViewer-BSnvba7W.js} +3 -3
  37. package/src/client/dist/spa/assets/HealthPage-DZYZWGHp.js +1 -0
  38. package/src/client/dist/spa/assets/MainLayout-C45J7rSF.css +1 -0
  39. package/src/client/dist/spa/assets/MainLayout-CMuiNpet.js +37 -0
  40. package/src/client/dist/spa/assets/QChip-BgzxI33B.js +36 -0
  41. package/src/client/dist/spa/assets/{QExpansionItem-BGg74no1.js → QExpansionItem-Fij7yBbG.js} +1 -1
  42. package/src/client/dist/spa/assets/{QItemSection-CQUDd0Vg.js → QItemSection-Bz1ZDJO5.js} +1 -1
  43. package/src/client/dist/spa/assets/{QMenu-D6uqosRg.js → QMenu-CMoolewZ.js} +1 -1
  44. package/src/client/dist/spa/assets/QRadio-4HnR_A-K.js +1 -0
  45. package/src/client/dist/spa/assets/{QTooltip-DUGPNNeQ.js → QTooltip-CbLXk2Bs.js} +1 -1
  46. package/src/client/dist/spa/assets/{SearchPage-C07dgzT9.js → SearchPage-CBSgEvVF.js} +1 -1
  47. package/src/client/dist/spa/assets/SettingsPage-C7TkcKXU.css +1 -0
  48. package/src/client/dist/spa/assets/SettingsPage-pY-zbPxn.js +9 -0
  49. package/src/client/dist/spa/assets/{TouchPan-DvVlszwO.js → TouchPan-DiBNjOPH.js} +1 -1
  50. package/src/client/dist/spa/assets/{WorkspacePage-CRIcsASQ.css → WorkspacePage-B4YnZ6re.css} +1 -1
  51. package/src/client/dist/spa/assets/WorkspacePage-BTHvQga-.js +4 -0
  52. package/src/client/dist/spa/assets/{build-path-tree-CCMckvpr.js → build-path-tree-BmBqRiCQ.js} +1 -1
  53. package/src/client/dist/spa/assets/{cssMode-D6XTTdwy.js → cssMode-ypFF7quM.js} +1 -1
  54. package/src/client/dist/spa/assets/{editor.api-6hDVHddO.js → editor.api-DtpZuH_B.js} +1 -1
  55. package/src/client/dist/spa/assets/{editor.main-DsLU1RWu.js → editor.main-C7a7L2WP.js} +3 -3
  56. package/src/client/dist/spa/assets/{AutoLoopChip-CkSzkC0C.js → engineFeatures-BxAOQcPU.js} +1 -1
  57. package/src/client/dist/spa/assets/{expand-template-Crz1uiBt.js → expand-template-GaEux9_o.js} +1 -1
  58. package/src/client/dist/spa/assets/{formatters-guwb-rzl.js → formatters-h0XBETG5.js} +1 -1
  59. package/src/client/dist/spa/assets/{freemarker2-Bn1f0t2U.js → freemarker2-DUBmhe3W.js} +1 -1
  60. package/src/client/dist/spa/assets/{handlebars-O92Cbq66.js → handlebars-_XEXkADl.js} +1 -1
  61. package/src/client/dist/spa/assets/{html-Ck95BMBU.js → html-D8gmyhgI.js} +1 -1
  62. package/src/client/dist/spa/assets/{htmlMode-DDYhH2FJ.js → htmlMode-B84S5YOM.js} +1 -1
  63. package/src/client/dist/spa/assets/i18n-DLoe3l25.js +1 -0
  64. package/src/client/dist/spa/assets/index-Dx_W9yYo.js +2 -0
  65. package/src/client/dist/spa/assets/{javascript-Cy2ddqHg.js → javascript-500DcdS9.js} +1 -1
  66. package/src/client/dist/spa/assets/{jsonMode-BIfVcp5z.js → jsonMode-DmrWg6b7.js} +1 -1
  67. package/src/client/dist/spa/assets/kobo-commands-DCoQW_NQ.js +9 -0
  68. package/src/client/dist/spa/assets/{liquid-B287eegh.js → liquid-CfPJszlt.js} +1 -1
  69. package/src/client/dist/spa/assets/{mdx-B8HSzGai.js → mdx-DtjLwENT.js} +1 -1
  70. package/src/client/dist/spa/assets/{monaco.contribution-CofcHzEf.js → monaco.contribution-CxiO5UJd.js} +2 -2
  71. package/src/client/dist/spa/assets/{notifications-BPnKFW60.js → notifications-CEyiPnmw.js} +1 -1
  72. package/src/client/dist/spa/assets/permissionModes-CPZlEHoF.js +1 -0
  73. package/src/client/dist/spa/assets/project-color-C4vMEn4C.js +1 -0
  74. package/src/client/dist/spa/assets/{purify.es-BCEwTYRx.js → purify.es-C92_EGvT.js} +1 -1
  75. package/src/client/dist/spa/assets/{python-csaKR6_U.js → python-BS46_AMt.js} +1 -1
  76. package/src/client/dist/spa/assets/{razor-C2wEv-nX.js → razor-Ce9zcIFo.js} +1 -1
  77. package/src/client/dist/spa/assets/{render-chat-markdown-Bjcei0vn.js → render-chat-markdown-BvJwlMiW.js} +1 -1
  78. package/src/client/dist/spa/assets/{tsMode-DGLVs57K.js → tsMode-Cr9FJjYY.js} +1 -1
  79. package/src/client/dist/spa/assets/{typescript-w0GWHzZ3.js → typescript-Ov3wChBg.js} +1 -1
  80. package/src/client/dist/spa/assets/{use-panel-CbJ44rqY.js → use-panel-lBh91vcW.js} +1 -1
  81. package/src/client/dist/spa/assets/{xml-CTn-vnEd.js → xml-euA4jBI1.js} +1 -1
  82. package/src/client/dist/spa/assets/{yaml-CTyUSvLZ.js → yaml-BuPSq_BT.js} +1 -1
  83. package/src/client/dist/spa/index.html +5 -5
  84. package/src/mcp-server/kobo-tasks-server.ts +27 -0
  85. package/src/client/dist/spa/assets/ActivityFeed-DlPVoOGb.js +0 -7
  86. package/src/client/dist/spa/assets/CreatePage-BoRappO3.css +0 -1
  87. package/src/client/dist/spa/assets/CreatePage-DpCVNwYk.js +0 -2
  88. package/src/client/dist/spa/assets/HealthPage-xZ0PP4F-.js +0 -1
  89. package/src/client/dist/spa/assets/MainLayout-DdkKM2ba.js +0 -37
  90. package/src/client/dist/spa/assets/MainLayout-drolsINz.css +0 -1
  91. package/src/client/dist/spa/assets/QChip-erWIZgxW.js +0 -1
  92. package/src/client/dist/spa/assets/QRadio-DJxOyOA3.js +0 -1
  93. package/src/client/dist/spa/assets/QTabPanels-ClPY9y4T.js +0 -1
  94. package/src/client/dist/spa/assets/SettingsPage-CLNmI0Rr.css +0 -1
  95. package/src/client/dist/spa/assets/SettingsPage-D0CZNqkA.js +0 -9
  96. package/src/client/dist/spa/assets/WorkspacePage-CKeCLPi0.js +0 -4
  97. package/src/client/dist/spa/assets/i18n-BLgknHpf.js +0 -1
  98. package/src/client/dist/spa/assets/index-CdHDdk1y.js +0 -2
  99. package/src/client/dist/spa/assets/kobo-commands-w8VepGvD.js +0 -11
  100. package/src/client/dist/spa/assets/models-Bd_v3W7Q.js +0 -1
  101. /package/src/client/dist/spa/assets/{QBtn-DEuWKHbR.js → QBtn-CaJSOyt8.js} +0 -0
  102. /package/src/client/dist/spa/assets/{vue-i18n-DI-gS-CC.js → vue-i18n-CKCtKE87.js} +0 -0
@@ -0,0 +1,99 @@
1
+ export function handleServerRequest(args) {
2
+ const { method, params, requestId, emit, register, respondError } = args;
3
+ const p = (params ?? {});
4
+ const callId = typeof p.callId === 'string' ? p.callId : `srv_${requestId}`;
5
+ if (method === 'mcpServer/elicitation/request') {
6
+ // Codex asks an external MCP server's elicitation prompt to be surfaced to
7
+ // the user. Kōbō doesn't model MCP elicitations yet — respond with a
8
+ // JSON-RPC "method not supported" error so the server doesn't block.
9
+ respondError?.(requestId, -32601, 'MCP elicitations not supported by this client');
10
+ return true;
11
+ }
12
+ // v2 and v1 method aliases for the same approval semantics. v1 legacy names
13
+ // (`execCommandApproval`, `applyPatchApproval`) are kept for compat with
14
+ // older Codex CLI builds that haven't transitioned to the v2 namespace.
15
+ if (method === 'item/commandExecution/requestApproval' || method === 'execCommandApproval') {
16
+ register(callId, { requestId, kind: 'command', payload: p });
17
+ emit({
18
+ kind: 'session:user-input-requested',
19
+ requestKind: 'permission',
20
+ toolCallId: callId,
21
+ toolName: 'Bash',
22
+ payload: { command: p.command, cwd: p.cwd, reason: p.reason },
23
+ });
24
+ return true;
25
+ }
26
+ if (method === 'item/fileChange/requestApproval' || method === 'applyPatchApproval') {
27
+ register(callId, { requestId, kind: 'file_change', payload: p });
28
+ emit({
29
+ kind: 'session:user-input-requested',
30
+ requestKind: 'permission',
31
+ toolCallId: callId,
32
+ toolName: 'Edit',
33
+ payload: { changes: p.changes, reason: p.reason },
34
+ });
35
+ return true;
36
+ }
37
+ if (method === 'item/tool/requestUserInput') {
38
+ register(callId, { requestId, kind: 'user_input', payload: p });
39
+ emit({
40
+ kind: 'session:user-input-requested',
41
+ requestKind: 'question',
42
+ toolCallId: callId,
43
+ toolName: 'AskUserQuestion',
44
+ payload: { questions: p.questions },
45
+ });
46
+ return true;
47
+ }
48
+ if (method === 'item/permissions/requestApproval') {
49
+ register(callId, { requestId, kind: 'permissions', payload: p });
50
+ emit({
51
+ kind: 'session:user-input-requested',
52
+ requestKind: 'permission',
53
+ toolCallId: callId,
54
+ toolName: 'Permissions',
55
+ payload: p,
56
+ });
57
+ return true;
58
+ }
59
+ return false; // unknown method
60
+ }
61
+ /**
62
+ * Build the JSON-RPC response Codex expects for a given pending request.
63
+ *
64
+ * Decision enum values come from
65
+ * `codex-rs/protocol/src/approvals.rs:CommandExecutionApprovalDecision`
66
+ * (and the matching `FileChangeApprovalDecision`): `'accept' | 'acceptForSession' | 'decline' | 'cancel'`.
67
+ * NOT `'approve' / 'reject'` — those would be silently rejected as unknown
68
+ * variants, which breaks the strict and interactive permission modes.
69
+ *
70
+ * `PermissionsRequestApprovalResponse` has a completely different shape:
71
+ * `{ permissions, scope, strictAutoReview? }` — no `decision` field. Since
72
+ * Kōbō doesn't yet model permission grants, we deny the request by sending
73
+ * an empty permissions response. A future iteration could add a UI for
74
+ * granular permission grants.
75
+ */
76
+ export function buildResponseForResolve(pending, response) {
77
+ if (pending.kind === 'command' || pending.kind === 'file_change') {
78
+ if (response.kind === 'permission-allow')
79
+ return { decision: 'accept' };
80
+ return { decision: 'decline' };
81
+ }
82
+ if (pending.kind === 'permissions') {
83
+ // Codex's PermissionsRequestApprovalResponse shape — not { decision }.
84
+ // We don't yet model granular permission grants, so deny by returning an
85
+ // empty permissions object; Codex falls back to the existing turn policy.
86
+ return { permissions: {}, scope: 'turn' };
87
+ }
88
+ if (pending.kind === 'user_input') {
89
+ if (response.kind === 'question') {
90
+ const answers = {};
91
+ for (const [qid, val] of Object.entries(response.answers)) {
92
+ answers[qid] = { answers: [val] };
93
+ }
94
+ return { answers };
95
+ }
96
+ return { answers: {} };
97
+ }
98
+ return null;
99
+ }
@@ -0,0 +1,27 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ const requireFn = createRequire(import.meta.url);
4
+ export function resolveCodexBinary() {
5
+ try {
6
+ const pkgPath = requireFn.resolve('@openai/codex/package.json');
7
+ const pkg = requireFn(pkgPath);
8
+ const binRel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.codex;
9
+ if (binRel) {
10
+ const url = new URL(binRel, `file://${pkgPath}`);
11
+ return url.pathname;
12
+ }
13
+ }
14
+ catch {
15
+ // fall through to default
16
+ }
17
+ return 'codex';
18
+ }
19
+ export function spawnAppServer(opts) {
20
+ const bin = resolveCodexBinary();
21
+ return nodeSpawn(bin, ['app-server'], {
22
+ stdio: ['pipe', 'pipe', 'pipe'],
23
+ cwd: opts.cwd,
24
+ env: opts.env,
25
+ signal: opts.signal,
26
+ });
27
+ }
@@ -1,6 +1,8 @@
1
1
  import { createClaudeCodeEngine } from './claude-code/engine.js';
2
+ import { createCodexEngine } from './codex/engine.js';
2
3
  const ENGINES = {
3
4
  'claude-code': createClaudeCodeEngine(),
5
+ codex: createCodexEngine(),
4
6
  };
5
7
  export function listEngines() {
6
8
  return Object.values(ENGINES);
@@ -298,7 +298,7 @@ function readEffectiveSettingsSafe(projectPath) {
298
298
  catch (err) {
299
299
  console.warn('[orchestrator] Failed to load settings, using defaults:', err);
300
300
  return {
301
- model: 'claude-opus-4-7',
301
+ model: 'auto',
302
302
  dangerouslySkipPermissions: true,
303
303
  prPromptTemplate: '',
304
304
  reviewPromptTemplate: '',
@@ -5,6 +5,7 @@ import { slugifyProjectName } from '../utils/project-slug.js';
5
5
  import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
6
6
  import * as orchestrator from './agent/orchestrator.js';
7
7
  import * as settingsService from './settings-service.js';
8
+ import { getSuitePrompts } from './skill-suite-prompts.js';
8
9
  import { emit, emitEphemeral } from './websocket-service.js';
9
10
  import { listTasks } from './workspace-service.js';
10
11
  const NO_PROGRESS_STALL_THRESHOLD = 3;
@@ -189,7 +190,7 @@ Your job this iteration:
189
190
  3. Run the project's quality checks (lint, typecheck, tests). Check \`.ai/.git-conventions.md\` for the exact commands if unclear.
190
191
  4. If checks fail, fix until they pass. If blocked, leave the task unchanged and explain in chat.
191
192
  5. Commit with a conventional message (\`feat: [short description]\` or similar per repo conventions).
192
- 6. Code review gate — BEFORE marking the task done, dispatch an independent code-reviewer subagent via the Task tool with \`subagent_type: "code-reviewer"\` (or \`"superpowers:code-reviewer"\` / \`"pr-review-toolkit:code-reviewer"\` — use whichever exists in this environment; fall back to \`superpowers:requesting-code-review\` skill if none is available). Brief the reviewer with: what you just implemented, the task title, and the commit SHA (via \`git rev-parse HEAD\`). Ask specifically whether the change matches the task scope, whether edge cases are handled, and whether the commit is clean.
193
+ 6. {reviewGate}
193
194
  7. Act on the review:
194
195
  - If Critical/Important issues: fix them, amend or add a fix-up commit, re-run checks from step 3. Do NOT mark_task_done.
195
196
  - If only Minor issues: fix them if trivial (< 2 min), otherwise note them in the chat and proceed.
@@ -199,6 +200,18 @@ Your job this iteration:
199
200
  Do NOT modify other tasks' state. Do NOT create a PR. Do NOT skip the checks.
200
201
  Do NOT run \`kill\`, \`pkill\`, \`killall\`, \`pgrep -k\`, or any process-killing command — you may tear down the Kōbō server itself or sibling dev servers. If a dev server needs restarting, let the user do it.
201
202
  When you're done (success or blocked), end your turn cleanly.`;
203
+ /**
204
+ * Resolve the active auto-loop review gate sentence (step 6 of the iteration
205
+ * prompt) for the current global skill suite, honouring any user override in
206
+ * `custom` mode. On the default `superpowers` suite this returns the same
207
+ * text that was historically inlined into PROMPT_TEMPLATE.
208
+ */
209
+ function getActiveAutoLoopReviewGate() {
210
+ const global = settingsService.getGlobalSettings();
211
+ return getSuitePrompts(global.skillSuite, {
212
+ autoLoopReviewGate: global.customAutoLoopReviewGate,
213
+ }).autoLoopReviewGate;
214
+ }
202
215
  function pickNextTask(workspaceId) {
203
216
  const pending = listTasks(workspaceId).filter((t) => t.status !== 'done');
204
217
  if (pending.length === 0)
@@ -257,7 +270,8 @@ function spawnNextIteration(workspaceId, opts = {}) {
257
270
  .replaceAll('{taskId}', task.id)
258
271
  .replaceAll('{taskTitle}', task.title)
259
272
  .replaceAll('{isAcceptanceCriterion}', String(task.isAcceptanceCriterion))
260
- .replaceAll('{overrideBlock}', overrideBlock);
273
+ .replaceAll('{overrideBlock}', overrideBlock)
274
+ .replaceAll('{reviewGate}', getActiveAutoLoopReviewGate());
261
275
  const globalSettings = settingsService.getGlobalSettings();
262
276
  const projectSlug = globalSettings.worktreesPrefixByProject
263
277
  ? slugifyProjectName(projectSettings?.displayName ?? '', row.project_path)
@@ -2,7 +2,7 @@ import { getPrStatusAsync } from '../utils/git-ops.js';
2
2
  import { stopDevServer } from './dev-server-service.js';
3
3
  import { destroyTerminal } from './terminal-service.js';
4
4
  import { emitEphemeral } from './websocket-service.js';
5
- import { archiveWorkspace, listWorkspaces, updateWorkspaceSourceBranch } from './workspace-service.js';
5
+ import { archiveWorkspace, getWorkspace, listWorkspaces, updateWorkspaceSourceBranch } from './workspace-service.js';
6
6
  // ── PR Watcher ────────────────────────────────────────────────────────────────
7
7
  // Polls GitHub every POLL_INTERVAL_MS to detect merged/closed PRs and
8
8
  // automatically archive the corresponding workspace.
@@ -14,18 +14,17 @@ import { archiveWorkspace, listWorkspaces, updateWorkspaceSourceBranch } from '.
14
14
  const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
15
15
  let timer = null;
16
16
  let checking = false;
17
+ /** Tracks the last known PR snapshot per workspace, used to detect transitions
18
+ * (state, base, reviewDecision). */
17
19
  const lastKnownPr = new Map();
18
20
  /**
19
- * Read-only snapshot of PR states known to the watcher, keyed by workspace id.
20
- * Used by the drawer to show a small PR-open indicator without N separate
21
- * `gh pr view` calls per workspace. Only contains entries where a PR has been
22
- * detected at least once by the watcher since boot; workspaces without a PR
23
- * are absent from the map.
21
+ * Read-only snapshot map, keyed by workspace id. Used by the drawer indicator
22
+ * AND the Git panel. Workspaces without a known PR are absent.
24
23
  */
25
- export function getAllPrStates() {
24
+ export function getAllPrSnapshots() {
26
25
  const out = {};
27
- for (const [id, known] of lastKnownPr) {
28
- out[id] = known.state;
26
+ for (const [id, snap] of lastKnownPr) {
27
+ out[id] = snap;
29
28
  }
30
29
  return out;
31
30
  }
@@ -45,9 +44,6 @@ export async function checkPrStatuses() {
45
44
  }
46
45
  }
47
46
  for (const ws of workspaces) {
48
- // Only check workspaces that are not actively running an agent
49
- if (['extracting', 'brainstorming', 'executing'].includes(ws.status))
50
- continue;
51
47
  try {
52
48
  const pr = await getPrStatusAsync(ws.projectPath, ws.workingBranch);
53
49
  if (!pr)
@@ -61,6 +57,13 @@ export async function checkPrStatuses() {
61
57
  // Archive on a transition FROM OPEN to CLOSED/MERGED. Skips the
62
58
  // base-change detection below — archiving wins.
63
59
  if (prev?.state === 'OPEN' && (pr.state === 'MERGED' || pr.state === 'CLOSED')) {
60
+ if (['extracting', 'brainstorming', 'executing'].includes(ws.status)) {
61
+ // Agent is working — update the cache but skip auto-archive.
62
+ // (The defensive base preservation from the no-base branch doesn't apply here
63
+ // because we ARE in the OPEN→MERGED/CLOSED branch which always has a base.)
64
+ lastKnownPr.set(ws.id, pr);
65
+ continue;
66
+ }
64
67
  console.log(`[pr-watcher] PR ${pr.state.toLowerCase()} for workspace '${ws.name}' — archiving`);
65
68
  // Best-effort cleanup (same as manual archive): stop dev server + terminal.
66
69
  // Agent is already not running here (guarded above).
@@ -84,13 +87,32 @@ export async function checkPrStatuses() {
84
87
  });
85
88
  continue; // do not run base-change detection on a workspace we just archived
86
89
  }
90
+ // Review-decision transitions (only on OPEN PRs; first-sight is silent).
91
+ // Reuses the baseline rule from base-change detection: emits only when we
92
+ // observe an actual transition between two known states.
93
+ if (pr.state === 'OPEN' && prev) {
94
+ if (prev.reviewDecision !== 'CHANGES_REQUESTED' && pr.reviewDecision === 'CHANGES_REQUESTED') {
95
+ emitEphemeral(ws.id, 'pr:changes-requested', {
96
+ prNumber: pr.number,
97
+ prUrl: pr.url,
98
+ });
99
+ }
100
+ else if (prev.reviewDecision === 'CHANGES_REQUESTED' && pr.reviewDecision === 'APPROVED') {
101
+ emitEphemeral(ws.id, 'pr:approved', {
102
+ prNumber: pr.number,
103
+ prUrl: pr.url,
104
+ });
105
+ }
106
+ }
87
107
  // Base-branch change detection. Only relevant for OPEN PRs — closed/
88
108
  // merged PRs don't accept base changes. Skip if the GitHub response
89
109
  // didn't include a baseRefName (defensive against malformed data).
90
110
  if (pr.state !== 'OPEN' || !pr.base) {
91
111
  // Still update the cache for the state — keeps the OPEN→CLOSED/MERGED
92
- // archiving logic working on the next tick.
93
- lastKnownPr.set(ws.id, { state: pr.state, base: prev?.base });
112
+ // archiving logic working on the next tick. Preserve the previous
113
+ // `base` if the fresh snapshot is missing one (defensive).
114
+ const next = pr.base ? pr : { ...pr, base: prev?.base ?? pr.base };
115
+ lastKnownPr.set(ws.id, next);
94
116
  continue;
95
117
  }
96
118
  // Comparison baseline:
@@ -100,8 +122,8 @@ export async function checkPrStatuses() {
100
122
  // that happened while Kobo was offline.
101
123
  const previousBase = prev?.base ?? ws.sourceBranch;
102
124
  if (previousBase === pr.base) {
103
- // No-op path: still record the base so subsequent ticks have a baseline.
104
- lastKnownPr.set(ws.id, { state: pr.state, base: pr.base });
125
+ // No-op path: still record the snapshot so subsequent ticks have a baseline.
126
+ lastKnownPr.set(ws.id, pr);
105
127
  continue;
106
128
  }
107
129
  console.log(`[pr-watcher] PR base changed for workspace '${ws.name}': ${previousBase} → ${pr.base}`);
@@ -116,7 +138,7 @@ export async function checkPrStatuses() {
116
138
  }
117
139
  // Both the persistence and the emit are part of "we successfully
118
140
  // observed a base change" — only NOW commit the new state to the cache.
119
- lastKnownPr.set(ws.id, { state: pr.state, base: pr.base });
141
+ lastKnownPr.set(ws.id, pr);
120
142
  emitEphemeral(ws.id, 'pr:base-changed', {
121
143
  oldBase: previousBase,
122
144
  newBase: pr.base,
@@ -128,24 +150,49 @@ export async function checkPrStatuses() {
128
150
  }
129
151
  }
130
152
  }
153
+ /**
154
+ * On-demand refresh of a single workspace's PR snapshot. Bypasses the 30s tick.
155
+ * No side effects beyond cache update — no archive, no transition emits. The
156
+ * user is watching the UI; we don't replay events for state they're already
157
+ * looking at.
158
+ *
159
+ * Returns the fresh snapshot, or null if the workspace has no PR (cache entry
160
+ * cleared in that case). Throws if the workspace doesn't exist.
161
+ */
162
+ export async function refreshPrSnapshot(workspaceId) {
163
+ const ws = getWorkspace(workspaceId);
164
+ if (!ws)
165
+ throw new Error(`Workspace '${workspaceId}' not found`);
166
+ const snap = await getPrStatusAsync(ws.projectPath, ws.workingBranch);
167
+ if (snap === null) {
168
+ lastKnownPr.delete(workspaceId);
169
+ return null;
170
+ }
171
+ lastKnownPr.set(workspaceId, snap);
172
+ return snap;
173
+ }
174
+ /**
175
+ * Runs a single check while honouring the `checking` re-entrancy guard. Used
176
+ * both by the immediate boot-time kick-off and by the periodic timer tick.
177
+ */
178
+ async function runOneCheck() {
179
+ if (checking)
180
+ return;
181
+ checking = true;
182
+ try {
183
+ await checkPrStatuses();
184
+ }
185
+ catch (err) {
186
+ console.error('[pr-watcher] Unexpected error in checkPrStatuses:', err);
187
+ }
188
+ finally {
189
+ checking = false;
190
+ }
191
+ }
131
192
  function scheduleNext() {
132
193
  timer = setTimeout(async () => {
133
- if (checking) {
134
- // Previous run still in progress — skip and reschedule
135
- scheduleNext();
136
- return;
137
- }
138
- checking = true;
139
- try {
140
- await checkPrStatuses();
141
- }
142
- catch (err) {
143
- console.error('[pr-watcher] Unexpected error in checkPrStatuses:', err);
144
- }
145
- finally {
146
- checking = false;
147
- scheduleNext();
148
- }
194
+ await runOneCheck();
195
+ scheduleNext();
149
196
  }, POLL_INTERVAL_MS);
150
197
  timer.unref?.();
151
198
  }
@@ -153,6 +200,10 @@ function scheduleNext() {
153
200
  export function startPrWatcher() {
154
201
  if (timer)
155
202
  return;
203
+ // Kick off an immediate check so the front-end has fresh PR data on boot
204
+ // without waiting for the first 30s tick. Fire-and-forget; the recurring
205
+ // loop is scheduled independently and the `checking` guard prevents overlap.
206
+ void runOneCheck();
156
207
  scheduleNext();
157
208
  }
158
209
  /** Stop the PR watcher polling loop. */
@@ -1,35 +1,28 @@
1
1
  import path from 'node:path';
2
- export const DEFAULT_REVIEW_PROMPT_TEMPLATE = `You are reviewing code changes on workspace "{{workspace_name}}" in project {{project_name}}.
3
-
4
- Branch: {{branch_name}} (base: {{source_branch}})
5
- Base commit: {{base_commit}}
6
-
7
- If a code-review skill is available (e.g. superpowers:requesting-code-review), invoke it to drive this review. Otherwise follow the steps below directly.
8
-
9
- ## Scope
10
-
11
- Review ALL changes — both committed and uncommitted in the working tree:
12
- - \`git diff {{base_commit}}..HEAD\` — committed changes on this branch
13
- - \`git status\` and \`git diff\` — uncommitted changes (staged + unstaged)
14
-
15
- ## Diff summary
16
- {{diff_stats}}
17
-
18
- ## Commits
19
- {{commits}}
20
-
21
- ## Additional instructions
22
- {{additional_instructions}}
23
-
24
- ## Output
25
-
26
- If no review skill is available, structure your reply as:
27
- 1. Summary — what changed and why
28
- 2. Issues — bugs, regressions, security or perf concerns (with file:line)
29
- 3. Suggestions — refactor / improvement opportunities
30
- 4. Tests — coverage gaps
31
- 5. Verdict — ship / fix-then-ship / blocked
32
- `;
2
+ import { getGlobalSettings } from './settings-service.js';
3
+ import { getSuitePrompts, SUPERPOWERS_PROMPTS } from './skill-suite-prompts.js';
4
+ /**
5
+ * Back-compat alias for the legacy review-template default. The canonical
6
+ * default now lives in `SUPERPOWERS_PROMPTS.reviewTemplate`; this export is
7
+ * kept so existing seed/migration paths in `settings-service.ts` and
8
+ * `routes/settings.ts` (the "reset to default" endpoint) keep working
9
+ * without being rewritten. Runtime readers should call
10
+ * `getActiveReviewTemplate()` instead, which respects the user's
11
+ * `skillSuite` choice.
12
+ */
13
+ export const DEFAULT_REVIEW_PROMPT_TEMPLATE = SUPERPOWERS_PROMPTS.reviewTemplate;
14
+ /**
15
+ * Resolve the review prompt template active for this user right now:
16
+ * read the global `skillSuite` and, in `custom` mode, the
17
+ * `customReviewTemplate` override. On the default `superpowers` suite this
18
+ * returns the same text as the legacy `DEFAULT_REVIEW_PROMPT_TEMPLATE`.
19
+ */
20
+ export function getActiveReviewTemplate() {
21
+ const global = getGlobalSettings();
22
+ return getSuitePrompts(global.skillSuite, {
23
+ reviewTemplate: global.customReviewTemplate,
24
+ }).reviewTemplate;
25
+ }
33
26
  function buildVariableMap(ctx) {
34
27
  return {
35
28
  project_name: path.basename(ctx.workspace.projectPath),