@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.
- package/AGENTS.md +29 -0
- package/README.md +62 -4
- package/dist/mcp-server/kobo-tasks-server.js +27 -0
- package/dist/server/routes/health.js +14 -0
- package/dist/server/routes/workspaces.js +60 -16
- package/dist/server/services/agent/engines/claude-code/capabilities.js +7 -0
- package/dist/server/services/agent/engines/codex/capabilities.js +18 -0
- package/dist/server/services/agent/engines/codex/client.js +36 -0
- package/dist/server/services/agent/engines/codex/engine.js +276 -0
- package/dist/server/services/agent/engines/codex/event-mapper.js +473 -0
- package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +60 -0
- package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +31 -0
- package/dist/server/services/agent/engines/codex/options-builder.js +81 -0
- package/dist/server/services/agent/engines/codex/protocol/types.js +11 -0
- package/dist/server/services/agent/engines/codex/server-requests.js +99 -0
- package/dist/server/services/agent/engines/codex/spawn.js +27 -0
- package/dist/server/services/agent/engines/registry.js +2 -0
- package/dist/server/services/agent/orchestrator.js +1 -1
- 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 +140 -6
- package/dist/server/services/skill-suite-prompts.js +80 -0
- package/dist/server/utils/git-ops.js +69 -15
- package/dist/server/utils/paths.js +7 -0
- package/dist/shared/auto-loop-prompts.js +18 -1
- package/dist/shared/codex-models.js +43 -0
- package/dist/shared/project-colors.js +23 -0
- package/dist/shared/skill-suite-prompts.js +66 -0
- package/package.json +2 -1
- package/src/client/dist/spa/assets/ActivityFeed-CKjFT9t6.js +8 -0
- package/src/client/dist/spa/assets/{ActivityFeed-tE4LVYck.css → ActivityFeed-WjiQ9716.css} +1 -1
- package/src/client/dist/spa/assets/{ClosePopup-DTcbxsC0.js → ClosePopup-DMnQG6nw.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-BhFrUkEN.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +1 -0
- package/src/client/dist/spa/assets/{DiffViewer-D-uNbBq0.js → DiffViewer-BSnvba7W.js} +3 -3
- package/src/client/dist/spa/assets/HealthPage-DZYZWGHp.js +1 -0
- 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-BGg74no1.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-D6uqosRg.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-DUGPNNeQ.js → QTooltip-CbLXk2Bs.js} +1 -1
- package/src/client/dist/spa/assets/{SearchPage-C07dgzT9.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-DvVlszwO.js → TouchPan-DiBNjOPH.js} +1 -1
- package/src/client/dist/spa/assets/{WorkspacePage-CRIcsASQ.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-CCMckvpr.js → build-path-tree-BmBqRiCQ.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-D6XTTdwy.js → cssMode-ypFF7quM.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-6hDVHddO.js → editor.api-DtpZuH_B.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-DsLU1RWu.js → editor.main-C7a7L2WP.js} +3 -3
- package/src/client/dist/spa/assets/{AutoLoopChip-CkSzkC0C.js → engineFeatures-BxAOQcPU.js} +1 -1
- package/src/client/dist/spa/assets/{expand-template-Crz1uiBt.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-Bn1f0t2U.js → freemarker2-DUBmhe3W.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-O92Cbq66.js → handlebars-_XEXkADl.js} +1 -1
- package/src/client/dist/spa/assets/{html-Ck95BMBU.js → html-D8gmyhgI.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-DDYhH2FJ.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-Cy2ddqHg.js → javascript-500DcdS9.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-BIfVcp5z.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-B287eegh.js → liquid-CfPJszlt.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-B8HSzGai.js → mdx-DtjLwENT.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-CofcHzEf.js → monaco.contribution-CxiO5UJd.js} +2 -2
- package/src/client/dist/spa/assets/{notifications-BPnKFW60.js → notifications-CEyiPnmw.js} +1 -1
- package/src/client/dist/spa/assets/permissionModes-CPZlEHoF.js +1 -0
- package/src/client/dist/spa/assets/project-color-C4vMEn4C.js +1 -0
- package/src/client/dist/spa/assets/{purify.es-BCEwTYRx.js → purify.es-C92_EGvT.js} +1 -1
- package/src/client/dist/spa/assets/{python-csaKR6_U.js → python-BS46_AMt.js} +1 -1
- package/src/client/dist/spa/assets/{razor-C2wEv-nX.js → razor-Ce9zcIFo.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-Bjcei0vn.js → render-chat-markdown-BvJwlMiW.js} +1 -1
- package/src/client/dist/spa/assets/{tsMode-DGLVs57K.js → tsMode-Cr9FJjYY.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-w0GWHzZ3.js → typescript-Ov3wChBg.js} +1 -1
- package/src/client/dist/spa/assets/{use-panel-CbJ44rqY.js → use-panel-lBh91vcW.js} +1 -1
- package/src/client/dist/spa/assets/{xml-CTn-vnEd.js → xml-euA4jBI1.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-CTyUSvLZ.js → yaml-BuPSq_BT.js} +1 -1
- package/src/client/dist/spa/index.html +5 -5
- package/src/mcp-server/kobo-tasks-server.ts +27 -0
- package/src/client/dist/spa/assets/ActivityFeed-DlPVoOGb.js +0 -7
- package/src/client/dist/spa/assets/CreatePage-BoRappO3.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-DpCVNwYk.js +0 -2
- package/src/client/dist/spa/assets/HealthPage-xZ0PP4F-.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-DdkKM2ba.js +0 -37
- package/src/client/dist/spa/assets/MainLayout-drolsINz.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-ClPY9y4T.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CLNmI0Rr.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-D0CZNqkA.js +0 -9
- package/src/client/dist/spa/assets/WorkspacePage-CKeCLPi0.js +0 -4
- package/src/client/dist/spa/assets/i18n-BLgknHpf.js +0 -1
- package/src/client/dist/spa/assets/index-CdHDdk1y.js +0 -2
- package/src/client/dist/spa/assets/kobo-commands-w8VepGvD.js +0 -11
- package/src/client/dist/spa/assets/models-Bd_v3W7Q.js +0 -1
- /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
|
@@ -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: '
|
|
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.
|
|
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),
|