@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.
- package/dist/server/routes/workspaces.js +27 -7
- package/dist/server/services/auto-loop-service.js +16 -2
- package/dist/server/services/pr-watcher-service.js +84 -33
- package/dist/server/services/review-template-service.js +24 -31
- package/dist/server/services/settings-service.js +72 -0
- package/dist/server/services/skill-suite-prompts.js +80 -0
- package/dist/server/utils/git-ops.js +77 -15
- package/dist/shared/auto-loop-prompts.js +18 -1
- package/dist/shared/project-colors.js +23 -0
- package/dist/shared/skill-suite-prompts.js +66 -0
- package/package.json +1 -1
- package/src/client/dist/spa/assets/ActivityFeed-CKjFT9t6.js +8 -0
- package/src/client/dist/spa/assets/{ClosePopup-C5JlH6Hy.js → ClosePopup-DMnQG6nw.js} +1 -1
- package/src/client/dist/spa/assets/{CreatePage-CdfbFlXf.js → CreatePage-BhFrUkEN.js} +1 -1
- package/src/client/dist/spa/assets/{DiffViewer-DkiP6nWz.js → DiffViewer-BSnvba7W.js} +3 -3
- package/src/client/dist/spa/assets/{HealthPage-BHGZJTgS.js → HealthPage-DZYZWGHp.js} +1 -1
- package/src/client/dist/spa/assets/MainLayout-C45J7rSF.css +1 -0
- package/src/client/dist/spa/assets/MainLayout-CMuiNpet.js +37 -0
- package/src/client/dist/spa/assets/QChip-BgzxI33B.js +36 -0
- package/src/client/dist/spa/assets/{QExpansionItem-CW6sPoP9.js → QExpansionItem-Fij7yBbG.js} +1 -1
- package/src/client/dist/spa/assets/{QItemSection-CQUDd0Vg.js → QItemSection-Bz1ZDJO5.js} +1 -1
- package/src/client/dist/spa/assets/{QMenu-CaVfoMu6.js → QMenu-CMoolewZ.js} +1 -1
- package/src/client/dist/spa/assets/QRadio-4HnR_A-K.js +1 -0
- package/src/client/dist/spa/assets/{QTooltip-DYey0zHV.js → QTooltip-CbLXk2Bs.js} +1 -1
- package/src/client/dist/spa/assets/{SearchPage-BaI3iU58.js → SearchPage-CBSgEvVF.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-C7TkcKXU.css +1 -0
- package/src/client/dist/spa/assets/SettingsPage-pY-zbPxn.js +9 -0
- package/src/client/dist/spa/assets/{TouchPan-DQILDzd3.js → TouchPan-DiBNjOPH.js} +1 -1
- package/src/client/dist/spa/assets/{WorkspacePage-C9eT5LAo.css → WorkspacePage-B4YnZ6re.css} +1 -1
- package/src/client/dist/spa/assets/WorkspacePage-BTHvQga-.js +4 -0
- package/src/client/dist/spa/assets/{build-path-tree-BpcCBm9A.js → build-path-tree-BmBqRiCQ.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-BaeNVqUm.js → cssMode-ypFF7quM.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-DMLl_PBy.js → editor.api-DtpZuH_B.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-D2pRsQAX.js → editor.main-C7a7L2WP.js} +3 -3
- package/src/client/dist/spa/assets/{engineFeatures-RffgP255.js → engineFeatures-BxAOQcPU.js} +1 -1
- package/src/client/dist/spa/assets/{expand-template-z2wIJOD2.js → expand-template-GaEux9_o.js} +1 -1
- package/src/client/dist/spa/assets/{formatters-guwb-rzl.js → formatters-h0XBETG5.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-Bh6ItnVy.js → freemarker2-DUBmhe3W.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-D8OXeysi.js → handlebars-_XEXkADl.js} +1 -1
- package/src/client/dist/spa/assets/{html-9Y1AHhvw.js → html-D8gmyhgI.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-z00se0fQ.js → htmlMode-B84S5YOM.js} +1 -1
- package/src/client/dist/spa/assets/i18n-DLoe3l25.js +1 -0
- package/src/client/dist/spa/assets/index-Dx_W9yYo.js +2 -0
- package/src/client/dist/spa/assets/{javascript-D0LSb7WU.js → javascript-500DcdS9.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-BSmyaoX3.js → jsonMode-DmrWg6b7.js} +1 -1
- package/src/client/dist/spa/assets/kobo-commands-DCoQW_NQ.js +9 -0
- package/src/client/dist/spa/assets/{liquid-BsY5UXNl.js → liquid-CfPJszlt.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BUcXih4e.js → mdx-DtjLwENT.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-DrpufOT3.js → monaco.contribution-CxiO5UJd.js} +2 -2
- package/src/client/dist/spa/assets/{notifications-C255ApfS.js → notifications-CEyiPnmw.js} +1 -1
- package/src/client/dist/spa/assets/{permissionModes-BocOmzU8.js → permissionModes-CPZlEHoF.js} +1 -1
- package/src/client/dist/spa/assets/project-color-C4vMEn4C.js +1 -0
- package/src/client/dist/spa/assets/{purify.es-aV6SU8N4.js → purify.es-C92_EGvT.js} +1 -1
- package/src/client/dist/spa/assets/{python-C0PoB7M8.js → python-BS46_AMt.js} +1 -1
- package/src/client/dist/spa/assets/{razor-Bu0-fwxD.js → razor-Ce9zcIFo.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-DALCdDVE.js → render-chat-markdown-BvJwlMiW.js} +1 -1
- package/src/client/dist/spa/assets/{tsMode-Blc1d2dp.js → tsMode-Cr9FJjYY.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-CV4ME9fo.js → typescript-Ov3wChBg.js} +1 -1
- package/src/client/dist/spa/assets/{use-panel-DCPiSURS.js → use-panel-lBh91vcW.js} +1 -1
- package/src/client/dist/spa/assets/{xml-DLYRBBbI.js → xml-euA4jBI1.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-QIBjI5Dl.js → yaml-BuPSq_BT.js} +1 -1
- package/src/client/dist/spa/index.html +5 -5
- package/src/client/dist/spa/assets/ActivityFeed-CPZdjJpH.js +0 -8
- package/src/client/dist/spa/assets/MainLayout-C0tClQZl.js +0 -37
- package/src/client/dist/spa/assets/MainLayout-DKnTGN_Q.css +0 -1
- package/src/client/dist/spa/assets/QChip-erWIZgxW.js +0 -1
- package/src/client/dist/spa/assets/QRadio-DJxOyOA3.js +0 -1
- package/src/client/dist/spa/assets/QTabPanels-E66qDYmr.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-BqBOQKeM.js +0 -9
- package/src/client/dist/spa/assets/SettingsPage-Zeu2cZqi.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-DqMyUSFG.js +0 -4
- package/src/client/dist/spa/assets/i18n-C-VMW7h5.js +0 -1
- package/src/client/dist/spa/assets/index-BLlWqEZC.js +0 -2
- package/src/client/dist/spa/assets/kobo-commands-w8VepGvD.js +0 -11
- /package/src/client/dist/spa/assets/{QBtn-DEuWKHbR.js → QBtn-CaJSOyt8.js} +0 -0
- /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,
|
|
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 {
|
|
18
|
+
import { getAllPrSnapshots, refreshPrSnapshot } from '../services/pr-watcher-service.js';
|
|
19
19
|
import * as quotaBackoffService from '../services/quota-backoff-service.js';
|
|
20
|
-
import {
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
20
|
-
*
|
|
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
|
|
24
|
+
export function getAllPrSnapshots() {
|
|
26
25
|
const out = {};
|
|
27
|
-
for (const [id,
|
|
28
|
-
out[id] =
|
|
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
|
-
|
|
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
|
|
104
|
-
lastKnownPr.set(ws.id,
|
|
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,
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|