@loicngr/kobo 1.7.8 → 1.7.10

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 (76) hide show
  1. package/dist/server/routes/workspaces.js +27 -7
  2. package/dist/server/services/auto-loop-service.js +16 -2
  3. package/dist/server/services/pr-watcher-service.js +84 -33
  4. package/dist/server/services/review-template-service.js +24 -31
  5. package/dist/server/services/settings-service.js +72 -0
  6. package/dist/server/services/skill-suite-prompts.js +80 -0
  7. package/dist/server/utils/git-ops.js +77 -15
  8. package/dist/shared/auto-loop-prompts.js +18 -1
  9. package/dist/shared/project-colors.js +23 -0
  10. package/dist/shared/skill-suite-prompts.js +66 -0
  11. package/package.json +1 -1
  12. package/src/client/dist/spa/assets/ActivityFeed-CKjFT9t6.js +8 -0
  13. package/src/client/dist/spa/assets/{ClosePopup-C5JlH6Hy.js → ClosePopup-DMnQG6nw.js} +1 -1
  14. package/src/client/dist/spa/assets/{CreatePage-CdfbFlXf.js → CreatePage-BhFrUkEN.js} +1 -1
  15. package/src/client/dist/spa/assets/{DiffViewer-DkiP6nWz.js → DiffViewer-BSnvba7W.js} +3 -3
  16. package/src/client/dist/spa/assets/{HealthPage-BHGZJTgS.js → HealthPage-DZYZWGHp.js} +1 -1
  17. package/src/client/dist/spa/assets/MainLayout-C45J7rSF.css +1 -0
  18. package/src/client/dist/spa/assets/MainLayout-CMuiNpet.js +37 -0
  19. package/src/client/dist/spa/assets/QChip-BgzxI33B.js +36 -0
  20. package/src/client/dist/spa/assets/{QExpansionItem-CW6sPoP9.js → QExpansionItem-Fij7yBbG.js} +1 -1
  21. package/src/client/dist/spa/assets/{QItemSection-CQUDd0Vg.js → QItemSection-Bz1ZDJO5.js} +1 -1
  22. package/src/client/dist/spa/assets/{QMenu-CaVfoMu6.js → QMenu-CMoolewZ.js} +1 -1
  23. package/src/client/dist/spa/assets/QRadio-4HnR_A-K.js +1 -0
  24. package/src/client/dist/spa/assets/{QTooltip-DYey0zHV.js → QTooltip-CbLXk2Bs.js} +1 -1
  25. package/src/client/dist/spa/assets/{SearchPage-BaI3iU58.js → SearchPage-CBSgEvVF.js} +1 -1
  26. package/src/client/dist/spa/assets/SettingsPage-C7TkcKXU.css +1 -0
  27. package/src/client/dist/spa/assets/SettingsPage-pY-zbPxn.js +9 -0
  28. package/src/client/dist/spa/assets/{TouchPan-DQILDzd3.js → TouchPan-DiBNjOPH.js} +1 -1
  29. package/src/client/dist/spa/assets/{WorkspacePage-C9eT5LAo.css → WorkspacePage-B4YnZ6re.css} +1 -1
  30. package/src/client/dist/spa/assets/WorkspacePage-BTHvQga-.js +4 -0
  31. package/src/client/dist/spa/assets/{build-path-tree-BpcCBm9A.js → build-path-tree-BmBqRiCQ.js} +1 -1
  32. package/src/client/dist/spa/assets/{cssMode-BaeNVqUm.js → cssMode-ypFF7quM.js} +1 -1
  33. package/src/client/dist/spa/assets/{editor.api-DMLl_PBy.js → editor.api-DtpZuH_B.js} +1 -1
  34. package/src/client/dist/spa/assets/{editor.main-D2pRsQAX.js → editor.main-C7a7L2WP.js} +3 -3
  35. package/src/client/dist/spa/assets/{engineFeatures-RffgP255.js → engineFeatures-BxAOQcPU.js} +1 -1
  36. package/src/client/dist/spa/assets/{expand-template-z2wIJOD2.js → expand-template-GaEux9_o.js} +1 -1
  37. package/src/client/dist/spa/assets/{formatters-guwb-rzl.js → formatters-h0XBETG5.js} +1 -1
  38. package/src/client/dist/spa/assets/{freemarker2-Bh6ItnVy.js → freemarker2-DUBmhe3W.js} +1 -1
  39. package/src/client/dist/spa/assets/{handlebars-D8OXeysi.js → handlebars-_XEXkADl.js} +1 -1
  40. package/src/client/dist/spa/assets/{html-9Y1AHhvw.js → html-D8gmyhgI.js} +1 -1
  41. package/src/client/dist/spa/assets/{htmlMode-z00se0fQ.js → htmlMode-B84S5YOM.js} +1 -1
  42. package/src/client/dist/spa/assets/i18n-DLoe3l25.js +1 -0
  43. package/src/client/dist/spa/assets/index-Dx_W9yYo.js +2 -0
  44. package/src/client/dist/spa/assets/{javascript-D0LSb7WU.js → javascript-500DcdS9.js} +1 -1
  45. package/src/client/dist/spa/assets/{jsonMode-BSmyaoX3.js → jsonMode-DmrWg6b7.js} +1 -1
  46. package/src/client/dist/spa/assets/kobo-commands-DCoQW_NQ.js +9 -0
  47. package/src/client/dist/spa/assets/{liquid-BsY5UXNl.js → liquid-CfPJszlt.js} +1 -1
  48. package/src/client/dist/spa/assets/{mdx-BUcXih4e.js → mdx-DtjLwENT.js} +1 -1
  49. package/src/client/dist/spa/assets/{monaco.contribution-DrpufOT3.js → monaco.contribution-CxiO5UJd.js} +2 -2
  50. package/src/client/dist/spa/assets/{notifications-C255ApfS.js → notifications-CEyiPnmw.js} +1 -1
  51. package/src/client/dist/spa/assets/{permissionModes-BocOmzU8.js → permissionModes-CPZlEHoF.js} +1 -1
  52. package/src/client/dist/spa/assets/project-color-C4vMEn4C.js +1 -0
  53. package/src/client/dist/spa/assets/{purify.es-aV6SU8N4.js → purify.es-C92_EGvT.js} +1 -1
  54. package/src/client/dist/spa/assets/{python-C0PoB7M8.js → python-BS46_AMt.js} +1 -1
  55. package/src/client/dist/spa/assets/{razor-Bu0-fwxD.js → razor-Ce9zcIFo.js} +1 -1
  56. package/src/client/dist/spa/assets/{render-chat-markdown-DALCdDVE.js → render-chat-markdown-BvJwlMiW.js} +1 -1
  57. package/src/client/dist/spa/assets/{tsMode-Blc1d2dp.js → tsMode-Cr9FJjYY.js} +1 -1
  58. package/src/client/dist/spa/assets/{typescript-CV4ME9fo.js → typescript-Ov3wChBg.js} +1 -1
  59. package/src/client/dist/spa/assets/{use-panel-DCPiSURS.js → use-panel-lBh91vcW.js} +1 -1
  60. package/src/client/dist/spa/assets/{xml-DLYRBBbI.js → xml-euA4jBI1.js} +1 -1
  61. package/src/client/dist/spa/assets/{yaml-QIBjI5Dl.js → yaml-BuPSq_BT.js} +1 -1
  62. package/src/client/dist/spa/index.html +5 -5
  63. package/src/client/dist/spa/assets/ActivityFeed-CPZdjJpH.js +0 -8
  64. package/src/client/dist/spa/assets/MainLayout-C0tClQZl.js +0 -37
  65. package/src/client/dist/spa/assets/MainLayout-DKnTGN_Q.css +0 -1
  66. package/src/client/dist/spa/assets/QChip-erWIZgxW.js +0 -1
  67. package/src/client/dist/spa/assets/QRadio-DJxOyOA3.js +0 -1
  68. package/src/client/dist/spa/assets/QTabPanels-E66qDYmr.js +0 -1
  69. package/src/client/dist/spa/assets/SettingsPage-BqBOQKeM.js +0 -9
  70. package/src/client/dist/spa/assets/SettingsPage-Zeu2cZqi.css +0 -1
  71. package/src/client/dist/spa/assets/WorkspacePage-DqMyUSFG.js +0 -4
  72. package/src/client/dist/spa/assets/i18n-C-VMW7h5.js +0 -1
  73. package/src/client/dist/spa/assets/index-BLlWqEZC.js +0 -2
  74. package/src/client/dist/spa/assets/kobo-commands-w8VepGvD.js +0 -11
  75. /package/src/client/dist/spa/assets/{QBtn-DEuWKHbR.js → QBtn-CaJSOyt8.js} +0 -0
  76. /package/src/client/dist/spa/assets/{vue-i18n-DI-gS-CC.js → vue-i18n-CKCtKE87.js} +0 -0
@@ -4,7 +4,7 @@ const execFileAsync = promisify(execFileCb);
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { Hono } from 'hono';
7
- import { AUTO_LOOP_HARD_RULES, buildAutoLoopGroomingSteps, PREP_AUTOLOOP_INTRO, } from '../../shared/auto-loop-prompts.js';
7
+ import { AUTO_LOOP_HARD_RULES, buildAutoLoopGroomingSteps, buildGroomingIntro } from '../../shared/auto-loop-prompts.js';
8
8
  import { getDb } from '../db/index.js';
9
9
  import { migrationGuard } from '../middleware/migration-guard.js';
10
10
  import { listEngines } from '../services/agent/engines/registry.js';
@@ -15,9 +15,9 @@ import * as devServerService from '../services/dev-server-service.js';
15
15
  import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, renderNotionInitialPrompt, renderSentryInitialPrompt, } from '../services/initial-prompt-template-service.js';
16
16
  import * as notionService from '../services/notion-service.js';
17
17
  import { renderPrTemplate } from '../services/pr-template-service.js';
18
- import { getAllPrStates } from '../services/pr-watcher-service.js';
18
+ import { getAllPrSnapshots, refreshPrSnapshot } from '../services/pr-watcher-service.js';
19
19
  import * as quotaBackoffService from '../services/quota-backoff-service.js';
20
- import { DEFAULT_REVIEW_PROMPT_TEMPLATE, renderReviewTemplate } from '../services/review-template-service.js';
20
+ import { getActiveReviewTemplate, renderReviewTemplate } from '../services/review-template-service.js';
21
21
  import * as sentryService from '../services/sentry-service.js';
22
22
  import * as settingsService from '../services/settings-service.js';
23
23
  import { runSetupScript } from '../services/setup-script-service.js';
@@ -720,13 +720,32 @@ app.get('/:id/sessions', (c) => {
720
720
  // exhaustive over the workspace list).
721
721
  app.get('/pr-states', (c) => {
722
722
  try {
723
- return c.json(getAllPrStates());
723
+ return c.json(getAllPrSnapshots());
724
724
  }
725
725
  catch (err) {
726
726
  const message = err instanceof Error ? err.message : String(err);
727
727
  return c.json({ error: message }, 500);
728
728
  }
729
729
  });
730
+ // POST /api/workspaces/pr-snapshot/refresh/:id — on-demand refresh of a single
731
+ // workspace's PR snapshot, driven by the Git tab refresh button. Static prefix
732
+ // keeps it ahead of `/:id` in the Hono matcher.
733
+ app.post('/pr-snapshot/refresh/:id', async (c) => {
734
+ const id = c.req.param('id');
735
+ try {
736
+ const snapshot = await refreshPrSnapshot(id);
737
+ if (snapshot === null) {
738
+ return c.json({ error: 'No PR for this workspace' }, 404);
739
+ }
740
+ return c.json({ snapshot });
741
+ }
742
+ catch (err) {
743
+ const message = err instanceof Error ? err.message : String(err);
744
+ if (/not found/i.test(message))
745
+ return c.json({ error: message }, 404);
746
+ return c.json({ error: message }, 500);
747
+ }
748
+ });
730
749
  // GET /api/workspaces/auto-loop-states — batch snapshot keyed by workspace id.
731
750
  // Used by the drawer + Pinia store. Static path — must be BEFORE /:id.
732
751
  app.get('/auto-loop-states', (c) => {
@@ -1367,7 +1386,9 @@ app.get('/:id/prep-autoloop-prompt', (c) => {
1367
1386
  const projectSettings = settingsService.getProjectSettings(workspace.projectPath);
1368
1387
  const e2eSettings = projectSettings?.e2e ?? { framework: '', skill: '', prompt: '' };
1369
1388
  const finalizationSettings = projectSettings?.finalization ?? { prompt: '' };
1370
- const prompt = `${PREP_AUTOLOOP_INTRO}
1389
+ const globalSettings = settingsService.getGlobalSettings();
1390
+ const intro = buildGroomingIntro(globalSettings.skillSuite, globalSettings.customAutoLoopGroomingIntro);
1391
+ const prompt = `${intro}
1371
1392
 
1372
1393
  ${buildAutoLoopGroomingSteps(e2eSettings, finalizationSettings)}
1373
1394
 
@@ -2477,8 +2498,7 @@ app.post('/:id/start-review', async (c) => {
2477
2498
  const diffStats = workingTreeStats.trim().length > 0
2478
2499
  ? `${committedStats}\n\n— Working tree (uncommitted) —\n${workingTreeStats}`
2479
2500
  : committedStats;
2480
- const effective = settingsService.getEffectiveSettings(workspace.projectPath);
2481
- const template = effective.reviewPromptTemplate || DEFAULT_REVIEW_PROMPT_TEMPLATE;
2501
+ const template = getActiveReviewTemplate();
2482
2502
  const rendered = renderReviewTemplate(template, {
2483
2503
  workspace,
2484
2504
  commits,
@@ -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),
@@ -1,11 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { WORKTREES_PATH } from '../../shared/consts.js';
4
+ import { isValidProjectColor } from '../../shared/project-colors.js';
5
+ import { isValidSkillSuite } from '../../shared/skill-suite-prompts.js';
4
6
  import { listClaudeMcpEntries } from '../utils/mcp-client.js';
5
7
  import { getSettingsPath } from '../utils/paths.js';
6
8
  import { InvalidWorktreesPathError, resolveGlobalWorktreesRoot, sanitizeWorktreesPath, validateWorktreesPath, } from '../utils/worktree-paths.js';
7
9
  import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT } from './initial-prompt-template-service.js';
8
10
  import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from './review-template-service.js';
11
+ import { AGNOSTIC_PROMPTS } from './skill-suite-prompts.js';
9
12
  export const DEFAULT_GIT_CONVENTIONS = `# Git conventions
10
13
 
11
14
  ## Commits
@@ -354,6 +357,46 @@ const settingsMigrations = [
354
357
  delete global.defaultPermissionMode;
355
358
  },
356
359
  },
360
+ {
361
+ version: 21,
362
+ name: 'add-project-color-and-flatten',
363
+ migrate: ({ global, projects }) => {
364
+ if (typeof global.flattenWorkspaceList !== 'boolean') {
365
+ global.flattenWorkspaceList = false;
366
+ }
367
+ for (const p of projects) {
368
+ if (!('color' in p) || (p.color !== null && !isValidProjectColor(p.color))) {
369
+ ;
370
+ p.color = null;
371
+ }
372
+ }
373
+ },
374
+ },
375
+ {
376
+ version: 22,
377
+ name: 'add-skill-suite-selector',
378
+ // Auto-migrate every existing user to `superpowers` (the closest match to
379
+ // today's behaviour). New installs get the same default via
380
+ // `defaultSettings()`. The 4 `custom*` fields seed with the agnostic
381
+ // baseline so users switching to `custom` mode have a sane editable start.
382
+ migrate: ({ global }) => {
383
+ if (!isValidSkillSuite(global.skillSuite)) {
384
+ global.skillSuite = 'superpowers';
385
+ }
386
+ if (typeof global.customReviewTemplate !== 'string') {
387
+ global.customReviewTemplate = AGNOSTIC_PROMPTS.reviewTemplate;
388
+ }
389
+ if (typeof global.customAutoLoopReviewGate !== 'string') {
390
+ global.customAutoLoopReviewGate = AGNOSTIC_PROMPTS.autoLoopReviewGate;
391
+ }
392
+ if (typeof global.customAutoLoopGroomingIntro !== 'string') {
393
+ global.customAutoLoopGroomingIntro = AGNOSTIC_PROMPTS.autoLoopGroomingIntro;
394
+ }
395
+ if (typeof global.customQaPromptTemplate !== 'string') {
396
+ global.customQaPromptTemplate = AGNOSTIC_PROMPTS.qaPromptTemplate;
397
+ }
398
+ },
399
+ },
357
400
  ];
358
401
  /** Current settings schema version — always equals the highest migration version. */
359
402
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -411,6 +454,12 @@ function defaultSettings() {
411
454
  voicePrompt: '',
412
455
  voiceTranslateToEnglish: false,
413
456
  voiceSuppressNonSpeechTokens: true,
457
+ flattenWorkspaceList: false,
458
+ skillSuite: 'superpowers',
459
+ customReviewTemplate: AGNOSTIC_PROMPTS.reviewTemplate,
460
+ customAutoLoopReviewGate: AGNOSTIC_PROMPTS.autoLoopReviewGate,
461
+ customAutoLoopGroomingIntro: AGNOSTIC_PROMPTS.autoLoopGroomingIntro,
462
+ customQaPromptTemplate: AGNOSTIC_PROMPTS.qaPromptTemplate,
414
463
  },
415
464
  projects: [],
416
465
  };
@@ -440,6 +489,7 @@ function defaultProjectSettings(projectPath) {
440
489
  finalization: {
441
490
  prompt: DEFAULT_FINALIZATION_PROMPT,
442
491
  },
492
+ color: null,
443
493
  };
444
494
  }
445
495
  function pickKnownKeys(data, allowedKeys) {
@@ -635,6 +685,14 @@ export function getEffectiveSettings(projectPath) {
635
685
  /** Merge partial updates into global settings and persist. */
636
686
  export function updateGlobalSettings(data) {
637
687
  const settings = readSettings();
688
+ // Validate skillSuite before merging: drop invalid values so the previous
689
+ // value is preserved (same pattern as `upsertProject`'s color validation).
690
+ if ('skillSuite' in data) {
691
+ if (!isValidSkillSuite(data.skillSuite)) {
692
+ console.warn(`[settings] Invalid skillSuite value rejected: ${data.skillSuite}`);
693
+ delete data.skillSuite;
694
+ }
695
+ }
638
696
  const allowedGlobalKeys = [
639
697
  'defaultModelByEngine',
640
698
  'dangerouslySkipPermissions',
@@ -666,6 +724,12 @@ export function updateGlobalSettings(data) {
666
724
  'voicePrompt',
667
725
  'voiceTranslateToEnglish',
668
726
  'voiceSuppressNonSpeechTokens',
727
+ 'flattenWorkspaceList',
728
+ 'skillSuite',
729
+ 'customReviewTemplate',
730
+ 'customAutoLoopReviewGate',
731
+ 'customAutoLoopGroomingIntro',
732
+ 'customQaPromptTemplate',
669
733
  ];
670
734
  const filtered = pickKnownKeys(data, allowedGlobalKeys);
671
735
  if (filtered.tags !== undefined) {
@@ -709,6 +773,13 @@ function isNonNativeWindowsPath(value) {
709
773
  }
710
774
  /** Create or update project-specific settings. Merges devServer, e2e, and finalization fields on update. */
711
775
  export function upsertProject(projectPath, data) {
776
+ // Validate color: accept null or a valid palette entry; drop anything else.
777
+ if ('color' in data) {
778
+ if (data.color !== null && !isValidProjectColor(data.color)) {
779
+ console.warn(`[settings] Invalid color value rejected for project '${projectPath}': ${data.color}`);
780
+ delete data.color;
781
+ }
782
+ }
712
783
  const allowedProjectKeys = [
713
784
  'displayName',
714
785
  'defaultSourceBranch',
@@ -723,6 +794,7 @@ export function upsertProject(projectPath, data) {
723
794
  'devServer',
724
795
  'e2e',
725
796
  'finalization',
797
+ 'color',
726
798
  ];
727
799
  const allowedDevServerKeys = ['startCommand', 'stopCommand'];
728
800
  const allowedE2eKeys = ['framework', 'skill', 'prompt'];
@@ -0,0 +1,80 @@
1
+ import { AGNOSTIC_AUTO_LOOP_GROOMING_INTRO, AGNOSTIC_AUTO_LOOP_REVIEW_GATE, AGNOSTIC_QA_PROMPT_TEMPLATE, AGNOSTIC_REVIEW_TEMPLATE, GROOMING_INTRO_GSTACK, GROOMING_INTRO_SUPERPOWERS, } from '../../shared/skill-suite-prompts.js';
2
+ // Headers and body shared with the agnostic baseline. We reconstruct them
3
+ // locally here so the suite-specific text can sit between header and body
4
+ // in the same shape across all 3 suites.
5
+ const REVIEW_HEADER = `You are reviewing code changes on workspace "{{workspace_name}}" in project {{project_name}}.
6
+
7
+ Branch: {{branch_name}} (base: {{source_branch}})
8
+ Base commit: {{base_commit}}
9
+
10
+ `;
11
+ const REVIEW_BODY = `## Scope
12
+
13
+ Review ALL changes — both committed and uncommitted in the working tree:
14
+ - \`git diff {{base_commit}}..HEAD\` — committed changes on this branch
15
+ - \`git status\` and \`git diff\` — uncommitted changes (staged + unstaged)
16
+
17
+ ## Diff summary
18
+ {{diff_stats}}
19
+
20
+ ## Commits
21
+ {{commits}}
22
+
23
+ ## Additional instructions
24
+ {{additional_instructions}}
25
+
26
+ ## Output
27
+
28
+ If no review skill is available, structure your reply as:
29
+ 1. Summary — what changed and why
30
+ 2. Issues — bugs, regressions, security or perf concerns (with file:line)
31
+ 3. Suggestions — refactor / improvement opportunities
32
+ 4. Tests — coverage gaps
33
+ 5. Verdict — ship / fix-then-ship / blocked
34
+ `;
35
+ export const SUPERPOWERS_PROMPTS = {
36
+ reviewTemplate: REVIEW_HEADER +
37
+ '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.\n\n' +
38
+ REVIEW_BODY,
39
+ autoLoopReviewGate: '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.',
40
+ autoLoopGroomingIntro: GROOMING_INTRO_SUPERPOWERS,
41
+ qaPromptTemplate: 'QA pass for workspace "{{workspace_name}}" in project {{project_name}}.\n\nBranch: {{branch_name}}\nStaging URL: {{staging_url}}\n\nIf a QA-style skill that drives a real browser is available in this environment (e.g. via the superpowers-chrome browsing skill), use it to navigate the staging URL and exercise the changes. Otherwise, fall back to manually scripting the smoke checks and recording your findings as a bug report.',
42
+ };
43
+ export const GSTACK_PROMPTS = {
44
+ reviewTemplate: REVIEW_HEADER +
45
+ 'Run /review to drive this audit (the gstack Staff Engineer skill — finds bugs that pass CI but blow up in production, auto-fixes the obvious ones, flags completeness gaps). If /review is unavailable in this environment, fall back to the manual checklist below.\n\n' +
46
+ REVIEW_BODY,
47
+ autoLoopReviewGate: 'Code review gate — BEFORE marking the task done, run /review (the gstack Staff Engineer skill). Brief it 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. If /review auto-fixes minor issues, accept the fixes via an amend or fix-up commit, then re-run step 3 checks.',
48
+ autoLoopGroomingIntro: GROOMING_INTRO_GSTACK,
49
+ qaPromptTemplate: 'QA pass for workspace "{{workspace_name}}" in project {{project_name}}.\n\nBranch: {{branch_name}}\nStaging URL: {{staging_url}}\n\nRun /qa {{staging_url}} (gstack QA Lead skill — opens a real browser, clicks through flows, finds bugs, fixes them with atomic commits, generates regression tests). If you only want a bug report without code changes, use /qa-only {{staging_url}} instead.',
50
+ };
51
+ export const AGNOSTIC_PROMPTS = {
52
+ reviewTemplate: AGNOSTIC_REVIEW_TEMPLATE,
53
+ autoLoopReviewGate: AGNOSTIC_AUTO_LOOP_REVIEW_GATE,
54
+ autoLoopGroomingIntro: AGNOSTIC_AUTO_LOOP_GROOMING_INTRO,
55
+ qaPromptTemplate: AGNOSTIC_QA_PROMPT_TEMPLATE,
56
+ };
57
+ /**
58
+ * Resolve the suite prompts to use right now, given the global `skillSuite`
59
+ * and the four user-editable `custom*` fields (only consulted in `custom` mode).
60
+ * Empty-string or whitespace-only overrides fall back to AGNOSTIC defaults.
61
+ */
62
+ export function getSuitePrompts(suite, overrides) {
63
+ if (suite === 'superpowers')
64
+ return SUPERPOWERS_PROMPTS;
65
+ if (suite === 'gstack')
66
+ return GSTACK_PROMPTS;
67
+ // custom mode: per-field fallback to AGNOSTIC when the override is missing/blank
68
+ const pick = (k) => {
69
+ const value = overrides[k];
70
+ if (typeof value !== 'string')
71
+ return AGNOSTIC_PROMPTS[k];
72
+ return value.trim() ? value : AGNOSTIC_PROMPTS[k];
73
+ };
74
+ return {
75
+ reviewTemplate: pick('reviewTemplate'),
76
+ autoLoopReviewGate: pick('autoLoopReviewGate'),
77
+ autoLoopGroomingIntro: pick('autoLoopGroomingIntro'),
78
+ qaPromptTemplate: pick('qaPromptTemplate'),
79
+ };
80
+ }