@loicngr/kobo 1.7.10 → 1.7.12

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 (108) hide show
  1. package/AGENTS.md +17 -0
  2. package/dist/server/routes/notion.js +12 -1
  3. package/dist/server/routes/templates.js +11 -0
  4. package/dist/server/routes/workspaces.js +44 -2
  5. package/dist/server/services/auto-loop-service.js +2 -0
  6. package/dist/server/services/notion-service.js +113 -0
  7. package/dist/server/services/sentry-service.js +66 -0
  8. package/dist/server/services/settings-service.js +36 -0
  9. package/dist/server/services/skill-suite-prompts.js +83 -2
  10. package/dist/server/services/templates-service.js +72 -72
  11. package/dist/shared/skill-suite-prompts.js +5 -1
  12. package/package.json +1 -1
  13. package/src/client/dist/spa/assets/ActivityFeed-FwS4TWtE.js +8 -0
  14. package/src/client/dist/spa/assets/{ClosePopup-DMnQG6nw.js → ClosePopup-CXORQZjI.js} +1 -1
  15. package/src/client/dist/spa/assets/{CreatePage-BhFrUkEN.js → CreatePage-BEwWpFSM.js} +2 -2
  16. package/src/client/dist/spa/assets/DiffViewer-C5KE6OEn.js +7 -0
  17. package/src/client/dist/spa/assets/HealthPage-C5taBe5N.js +1 -0
  18. package/src/client/dist/spa/assets/MainLayout-Bp0oVWa-.css +1 -0
  19. package/src/client/dist/spa/assets/MainLayout-Cx1buAto.js +37 -0
  20. package/src/client/dist/spa/assets/{QBadge-C7r6oPSi.js → QBadge-u0mEz_W1.js} +1 -1
  21. package/src/client/dist/spa/assets/{QBtn-CaJSOyt8.js → QBtn-Bt7WPzYv.js} +1 -1
  22. package/src/client/dist/spa/assets/{QCheckbox-BvHfXBFY.js → QCheckbox-skYuqkHX.js} +1 -1
  23. package/src/client/dist/spa/assets/QChip-DuOEr0aU.js +36 -0
  24. package/src/client/dist/spa/assets/QExpansionItem-Cwk7gp3q.js +1 -0
  25. package/src/client/dist/spa/assets/QIcon-0rjEivgj.js +1 -0
  26. package/src/client/dist/spa/assets/QInput-Ciqjq5-e.js +1 -0
  27. package/src/client/dist/spa/assets/{QItemLabel-CHkgkZVj.js → QItemLabel-B0tYxHQg.js} +1 -1
  28. package/src/client/dist/spa/assets/{QItemSection-Bz1ZDJO5.js → QItemSection-DOOD8VCh.js} +1 -1
  29. package/src/client/dist/spa/assets/{QList-BbnN_oNX.js → QList-MfhZa-uv.js} +1 -1
  30. package/src/client/dist/spa/assets/QMenu-Okyi4ppC.js +1 -0
  31. package/src/client/dist/spa/assets/{QPage-Co2h9wd_.js → QPage-CGYPttdA.js} +1 -1
  32. package/src/client/dist/spa/assets/{QRadio-4HnR_A-K.js → QRadio-DoFiKjZ-.js} +1 -1
  33. package/src/client/dist/spa/assets/QSpace-BrtkvWzZ.js +1 -0
  34. package/src/client/dist/spa/assets/{QSpinnerDots-Bfl2RMy4.js → QSpinnerDots-CluOpUgq.js} +1 -1
  35. package/src/client/dist/spa/assets/{QToggle-DNOTC_3a.js → QToggle-aBvIHg6j.js} +1 -1
  36. package/src/client/dist/spa/assets/QTooltip-CgOu9HtZ.js +1 -0
  37. package/src/client/dist/spa/assets/SearchPage-CdOUi_8L.js +1 -0
  38. package/src/client/dist/spa/assets/SettingsPage-CjBdkOrK.css +1 -0
  39. package/src/client/dist/spa/assets/SettingsPage-Jdin4GXA.js +9 -0
  40. package/src/client/dist/spa/assets/TouchPan-BYRVZE1k.js +1 -0
  41. package/src/client/dist/spa/assets/WorkspacePage-z3JJnma_.js +4 -0
  42. package/src/client/dist/spa/assets/{build-path-tree-BmBqRiCQ.js → build-path-tree-BamKKF8S.js} +1 -1
  43. package/src/client/dist/spa/assets/{cssMode-ypFF7quM.js → cssMode-Co6CFIGH.js} +1 -1
  44. package/src/client/dist/spa/assets/{documents-soWtna0O.js → documents-qOarUoMj.js} +1 -1
  45. package/src/client/dist/spa/assets/{editor.api-DtpZuH_B.js → editor.api-YeMDjOUn.js} +1 -1
  46. package/src/client/dist/spa/assets/{editor.main-C7a7L2WP.js → editor.main-CnghWnPN.js} +3 -3
  47. package/src/client/dist/spa/assets/engineFeatures-DG2lRjWW.js +1 -0
  48. package/src/client/dist/spa/assets/expand-template-DUQqF-Zq.js +1 -0
  49. package/src/client/dist/spa/assets/{formatters-h0XBETG5.js → formatters-ofO3MWTW.js} +1 -1
  50. package/src/client/dist/spa/assets/{freemarker2-DUBmhe3W.js → freemarker2-cMpcLQzW.js} +1 -1
  51. package/src/client/dist/spa/assets/{handlebars-_XEXkADl.js → handlebars-tAszxpzk.js} +1 -1
  52. package/src/client/dist/spa/assets/{html-D8gmyhgI.js → html-B8fakN_A.js} +1 -1
  53. package/src/client/dist/spa/assets/{htmlMode-B84S5YOM.js → htmlMode-C6OieX5l.js} +1 -1
  54. package/src/client/dist/spa/assets/i18n-BoLFsYKz.js +1 -0
  55. package/src/client/dist/spa/assets/{index-eX_lKHSg.css → index-5ydpLSpt.css} +1 -1
  56. package/src/client/dist/spa/assets/index-BskEbmdO.js +2 -0
  57. package/src/client/dist/spa/assets/{javascript-500DcdS9.js → javascript-D0LpfZ_A.js} +1 -1
  58. package/src/client/dist/spa/assets/{jsonMode-DmrWg6b7.js → jsonMode-Doglp1SN.js} +1 -1
  59. package/src/client/dist/spa/assets/{kobo-commands-DCoQW_NQ.js → kobo-commands-CHoV7ovn.js} +1 -1
  60. package/src/client/dist/spa/assets/{liquid-CfPJszlt.js → liquid-CiJ8hylW.js} +1 -1
  61. package/src/client/dist/spa/assets/{mdx-DtjLwENT.js → mdx-CgUva_L1.js} +1 -1
  62. package/src/client/dist/spa/assets/{monaco.contribution-CxiO5UJd.js → monaco.contribution-DG0R-WYt.js} +2 -2
  63. package/src/client/dist/spa/assets/notifications-CR-qjSNX.js +1 -0
  64. package/src/client/dist/spa/assets/{permissionModes-CPZlEHoF.js → permissionModes-DbC_sc_F.js} +1 -1
  65. package/src/client/dist/spa/assets/project-color-ANjHSR5A.js +1 -0
  66. package/src/client/dist/spa/assets/purify.es-4cZTowxv.js +60 -0
  67. package/src/client/dist/spa/assets/{python-BS46_AMt.js → python-Cvb8mJfR.js} +1 -1
  68. package/src/client/dist/spa/assets/{razor-Ce9zcIFo.js → razor-DbKqjziV.js} +1 -1
  69. package/src/client/dist/spa/assets/{render-chat-markdown-BvJwlMiW.js → render-chat-markdown-Ckao9ANM.js} +1 -1
  70. package/src/client/dist/spa/assets/runtime-core.esm-bundler-DPcTPMmX.js +1 -0
  71. package/src/client/dist/spa/assets/{tsMode-Cr9FJjYY.js → tsMode-0DwTd7Q5.js} +1 -1
  72. package/src/client/dist/spa/assets/{typescript-Ov3wChBg.js → typescript-DDoJP3Mo.js} +1 -1
  73. package/src/client/dist/spa/assets/{use-checkbox-y_fOkYZN.js → use-checkbox-DzLCp4E3.js} +1 -1
  74. package/src/client/dist/spa/assets/use-id-BQW6DfJU.js +1 -0
  75. package/src/client/dist/spa/assets/use-quasar-C5gKpYwL.js +1 -0
  76. package/src/client/dist/spa/assets/{vue-i18n-CKCtKE87.js → vue-i18n-BXnT4vor.js} +2 -2
  77. package/src/client/dist/spa/assets/{xml-euA4jBI1.js → xml-BrD_WJYH.js} +1 -1
  78. package/src/client/dist/spa/assets/{yaml-BuPSq_BT.js → yaml-BeBHYmTS.js} +1 -1
  79. package/src/client/dist/spa/index.html +13 -13
  80. package/src/client/dist/spa/assets/ActivityFeed-CKjFT9t6.js +0 -8
  81. package/src/client/dist/spa/assets/DiffViewer-BSnvba7W.js +0 -7
  82. package/src/client/dist/spa/assets/HealthPage-DZYZWGHp.js +0 -1
  83. package/src/client/dist/spa/assets/MainLayout-C45J7rSF.css +0 -1
  84. package/src/client/dist/spa/assets/MainLayout-CMuiNpet.js +0 -37
  85. package/src/client/dist/spa/assets/QChip-BgzxI33B.js +0 -36
  86. package/src/client/dist/spa/assets/QExpansionItem-Fij7yBbG.js +0 -1
  87. package/src/client/dist/spa/assets/QIcon-qfJNZLIW.js +0 -1
  88. package/src/client/dist/spa/assets/QInput-DCJEwE8V.js +0 -1
  89. package/src/client/dist/spa/assets/QMenu-CMoolewZ.js +0 -1
  90. package/src/client/dist/spa/assets/QSpace-DKIph84L.js +0 -1
  91. package/src/client/dist/spa/assets/QTooltip-CbLXk2Bs.js +0 -1
  92. package/src/client/dist/spa/assets/SearchPage-CBSgEvVF.js +0 -1
  93. package/src/client/dist/spa/assets/SettingsPage-C7TkcKXU.css +0 -1
  94. package/src/client/dist/spa/assets/SettingsPage-pY-zbPxn.js +0 -9
  95. package/src/client/dist/spa/assets/TouchPan-DiBNjOPH.js +0 -1
  96. package/src/client/dist/spa/assets/WorkspacePage-BTHvQga-.js +0 -4
  97. package/src/client/dist/spa/assets/engineFeatures-BxAOQcPU.js +0 -1
  98. package/src/client/dist/spa/assets/expand-template-GaEux9_o.js +0 -1
  99. package/src/client/dist/spa/assets/i18n-DLoe3l25.js +0 -1
  100. package/src/client/dist/spa/assets/index-Dx_W9yYo.js +0 -2
  101. package/src/client/dist/spa/assets/notifications-CEyiPnmw.js +0 -1
  102. package/src/client/dist/spa/assets/project-color-C4vMEn4C.js +0 -1
  103. package/src/client/dist/spa/assets/purify.es-C92_EGvT.js +0 -60
  104. package/src/client/dist/spa/assets/runtime-core.esm-bundler-9Z0QAO_7.js +0 -1
  105. package/src/client/dist/spa/assets/use-id-_7wiRcgb.js +0 -1
  106. package/src/client/dist/spa/assets/use-panel-lBh91vcW.js +0 -1
  107. package/src/client/dist/spa/assets/use-quasar-DQYS47mh.js +0 -1
  108. /package/src/client/dist/spa/assets/{touch-HRdTUO2o.js → touch-2Qa-HSDZ.js} +0 -0
package/AGENTS.md CHANGED
@@ -273,6 +273,22 @@ These rules are the source of truth and are also written to `.ai/.git-convention
273
273
 
274
274
  The human user of this repository prefers French for conversational exchanges. Code, tests, commit messages, and documentation (including this file) remain in English for toolchain compatibility, but chat responses should be in French unless the user switches.
275
275
 
276
+ ## Design System
277
+
278
+ Always read `DESIGN.md` (repo root) before making any visual or UI decisions. Every
279
+ font choice, color, spacing value, and aesthetic decision is defined there. The CSS
280
+ variables in `src/client/src/css/design-tokens.scss` are the runtime source of truth
281
+ for the values documented in `DESIGN.md` — never hardcode hex colors or spacing
282
+ literals in components.
283
+
284
+ Aesthetic direction: **Brutally Minimal × Industrial** (Linear / Anthropic Console
285
+ reference). Dark-native, monochrome, single indigo accent (`--kobo-accent` /
286
+ `#6c63ff`) used rarely. No purple gradients, no decorative illustrations, no
287
+ bubble-pill shapes, no spring physics. Geist + Geist Mono for technical values.
288
+
289
+ When reviewing or writing UI code, flag any deviation from `DESIGN.md`. Do not
290
+ deviate from the documented system without explicit user approval.
291
+
276
292
  ## What NOT to do
277
293
 
278
294
  - Don't drop-and-recreate the database to apply schema changes. The project is in production — every schema change ships as a migration that preserves data (see [Database migrations](#database-migrations)).
@@ -282,3 +298,4 @@ The human user of this repository prefers French for conversational exchanges. C
282
298
  - Don't break the single-source-of-truth of `CLAUDE.md` → `AGENTS.md` symlink. Edit `AGENTS.md`; `CLAUDE.md` follows automatically.
283
299
  - Don't skip `try/catch` swallowing on best-effort cleanup (agent stop, dev-server stop, worktree removal). These must never break the primary operation.
284
300
  - Don't hardcode user-visible text in the frontend. Every string must go through `$t()` / `t()` with keys in all 5 locale files. See [Internationalization (i18n)](#internationalization-i18n).
301
+ - Don't hardcode hex colors, spacing literals, or font names in components. Use the CSS variables from `src/client/src/css/design-tokens.scss` and the patterns in `DESIGN.md`. See [Design System](#design-system).
@@ -1,7 +1,18 @@
1
1
  import { Hono } from 'hono';
2
- import { extractNotionPage } from '../services/notion-service.js';
2
+ import { extractNotionPage, listNotionUsers } from '../services/notion-service.js';
3
3
  /** Hono sub-router for Notion page extraction. */
4
4
  const app = new Hono();
5
+ // GET /api/notion/users — list workspace users (humans only, for the settings dropdown)
6
+ app.get('/users', async (c) => {
7
+ try {
8
+ const users = await listNotionUsers();
9
+ return c.json({ users });
10
+ }
11
+ catch (err) {
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ return c.json({ error: message }, 500);
14
+ }
15
+ });
5
16
  // POST /api/notion/extract — extract a Notion page
6
17
  app.post('/extract', async (c) => {
7
18
  try {
@@ -64,6 +64,17 @@ app.patch('/:slug', async (c) => {
64
64
  return c.json({ error: message }, statusForServiceError(message));
65
65
  }
66
66
  });
67
+ // POST /api/templates/reload-defaults — re-apply default seed without overwriting user templates
68
+ app.post('/reload-defaults', (c) => {
69
+ try {
70
+ const result = templatesService.reloadDefaultTemplates();
71
+ return c.json(result);
72
+ }
73
+ catch (err) {
74
+ const message = err instanceof Error ? err.message : String(err);
75
+ return c.json({ error: message }, 500);
76
+ }
77
+ });
67
78
  // DELETE /api/templates/:slug — delete a template
68
79
  app.delete('/:slug', (c) => {
69
80
  try {
@@ -21,6 +21,7 @@ import { getActiveReviewTemplate, renderReviewTemplate } from '../services/revie
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';
24
+ import { getSuitePrompts } from '../services/skill-suite-prompts.js';
24
25
  import * as terminalService from '../services/terminal-service.js';
25
26
  import * as wakeupService from '../services/wakeup-service.js';
26
27
  import * as wsService from '../services/websocket-service.js';
@@ -160,6 +161,18 @@ app.post('/', migrationGuard, async (c) => {
160
161
  const message = err instanceof Error ? err.message : String(err);
161
162
  return c.json({ error: `Failed to extract Notion page: ${message}` }, 422);
162
163
  }
164
+ const assigneeProperty = settingsService.getGlobalSettings().notionAssigneeProperty;
165
+ if (assigneeProperty && body.notionUrl) {
166
+ const notionUrl = body.notionUrl;
167
+ notionService
168
+ .assignNotionPageToSelf(notionUrl, assigneeProperty)
169
+ .then((result) => {
170
+ if (!result.assigned) {
171
+ console.warn(`[notion] Auto-assign skipped for ${notionUrl}: ${result.reason}`);
172
+ }
173
+ })
174
+ .catch((err) => console.error('[notion] Auto-assign threw unexpectedly:', err));
175
+ }
163
176
  }
164
177
  let sentryContent = null;
165
178
  if (body.sentryUrl) {
@@ -170,6 +183,17 @@ app.post('/', migrationGuard, async (c) => {
170
183
  const message = err instanceof Error ? err.message : String(err);
171
184
  return c.json({ error: `Failed to extract Sentry issue: ${message}` }, 422);
172
185
  }
186
+ if (sentryContent && sentryContent.assignee.length === 0) {
187
+ const sentryUrl = body.sentryUrl;
188
+ sentryService
189
+ .assignSentryIssueToSelf(sentryUrl)
190
+ .then((result) => {
191
+ if (!result.assigned) {
192
+ console.warn(`[sentry] Auto-assign skipped for ${sentryUrl}: ${result.reason}`);
193
+ }
194
+ })
195
+ .catch((err) => console.error('[sentry] Auto-assign threw unexpectedly:', err));
196
+ }
173
197
  }
174
198
  // Create workspace record
175
199
  const globalSettings = settingsService.getGlobalSettings();
@@ -608,6 +632,22 @@ app.post('/', migrationGuard, async (c) => {
608
632
  brainstormPrompt += `\n# Git conventions\nIMPORTANT: Before any git operation (commit, branch, rebase, merge, push), read and apply the conventions defined in \`.ai/.git-conventions.md\`. They are project-specific and override any default behavior. Re-read this file if you're unsure or if context was compacted.\n`;
609
633
  }
610
634
  brainstormPrompt += `\nIMPORTANT: Start by reading CLAUDE.md and/or AGENTS.md at the project root if they exist — they contain project conventions and instructions you must follow.`;
635
+ // Resolve the suite-specific brainstorming instruction (citing
636
+ // superpowers:brainstorming / writing-plans, or gstack /office-hours +
637
+ // /autoplan, etc.). The marker `[BRAINSTORM_COMPLETE]` stays universal —
638
+ // only the path to it changes per suite.
639
+ const globalSettings = settingsService.getGlobalSettings();
640
+ const suitePrompts = getSuitePrompts(globalSettings.skillSuite, {
641
+ brainstormingInstruction: globalSettings.customBrainstormingInstruction,
642
+ });
643
+ // gstack brain sync — load project-specific learnings (patterns, pitfalls,
644
+ // preferences) accumulated by /learn across prior sessions. Only when the
645
+ // active skill suite includes gstack, and only if the user has actually
646
+ // installed gstack locally (the binary may be absent — skip silently).
647
+ // Fast + idempotent; safe to run on every workspace boot.
648
+ if (globalSettings.skillSuite === 'gstack' || globalSettings.skillSuite === 'superpowers+gstack') {
649
+ brainstormPrompt += `\n\nIMPORTANT (gstack): Before brainstorming, check whether \`~/.claude/skills/gstack/bin/gstack-brain-sync\` exists. If it does, run it with \`--once\` to load gstack learnings (project-specific patterns, pitfalls, and preferences from prior sessions) into memory. If the binary is missing (gstack not installed in this environment), skip silently and continue — do NOT install anything and do NOT mention the absence. Fast and idempotent when present.`;
650
+ }
611
651
  if (body.autoLoop === true) {
612
652
  // Auto-loop is armed — brainstorm must end with task seeding + mark-ready,
613
653
  // NOT with implementation. The auto-loop will drive implementation after.
@@ -621,7 +661,7 @@ app.post('/', migrationGuard, async (c) => {
621
661
  const projectSettingsForE2e = settingsService.getProjectSettings(body.projectPath);
622
662
  const e2eSettings = projectSettingsForE2e?.e2e ?? { framework: '', skill: '', prompt: '' };
623
663
  const finalizationSettings = projectSettingsForE2e?.finalization ?? { prompt: '' };
624
- brainstormPrompt += `\n\nThen brainstorm the implementation approach. Explore the codebase to understand the existing structure. Ask clarifying questions if needed. When you have a clear plan, create a plan file.
664
+ brainstormPrompt += `\n\n${suitePrompts.brainstormingInstruction}
625
665
 
626
666
  Auto-loop mode is active for this workspace. After the plan is ready, DO NOT implement anything. Instead:
627
667
 
@@ -632,7 +672,9 @@ When the steps above are complete, output [BRAINSTORM_COMPLETE] on its own line
632
672
  ${AUTO_LOOP_HARD_RULES}`;
633
673
  }
634
674
  else {
635
- brainstormPrompt += `\n\nThen brainstorm the implementation approach. Explore the codebase to understand the existing structure. Ask clarifying questions if needed. When you're done brainstorming and have a clear plan, create a plan file and proceed with implementation. Once you have completed the brainstorming phase, output [BRAINSTORM_COMPLETE] on its own line.`;
675
+ brainstormPrompt += `\n\n${suitePrompts.brainstormingInstruction}
676
+
677
+ Once the brainstorming + planning steps above are complete and you have a saved plan file, output [BRAINSTORM_COMPLETE] on its own line BEFORE starting implementation. Kōbō uses that marker to transition the workspace from \`brainstorming\` to \`executing\`. Then proceed with implementation.`;
636
678
  }
637
679
  try {
638
680
  const agent = agentManager.startAgent(workspace.id, worktreePath, brainstormPrompt, workspace.model, false, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
@@ -184,6 +184,8 @@ Current pending task (highest priority, non-acceptance-criterion first):
184
184
  - Title: {taskTitle}
185
185
  - Is acceptance criterion: {isAcceptanceCriterion}
186
186
  {overrideBlock}
187
+ Throughout the steps below, keep the workspace description current via \`kobo__set_workspace_agent_description(description)\` so the user sees your state in the sidebar without opening the workspace. Update it at the start of the iteration (e.g. "Iter #{n}: implementing <short task title>") AND whenever your focus shifts (e.g. "Running tests", "Awaiting code review", "Fixing review feedback", "Marking task done"). Plain text, ≤200 chars.
188
+
187
189
  Your job this iteration:
188
190
  1. Read \`kobo__list_tasks\` to see all tasks and the big picture.
189
191
  2. Implement the SINGLE task above and nothing else. Do not pick a different task.
@@ -269,6 +269,119 @@ export async function extractNotionPage(notionUrl) {
269
269
  mcpProcess.kill();
270
270
  }
271
271
  }
272
+ export async function listNotionUsers() {
273
+ const global = getGlobalSettings();
274
+ const mcpProcess = spawnNotionMcp(global.notionMcpKey);
275
+ try {
276
+ await new Promise((resolve, reject) => {
277
+ const timeout = setTimeout(() => resolve(), 1000);
278
+ mcpProcess.on('error', (err) => {
279
+ clearTimeout(timeout);
280
+ reject(new Error(`Failed to start MCP Notion server: ${err.message}`));
281
+ });
282
+ mcpProcess.stdout?.once('data', () => {
283
+ clearTimeout(timeout);
284
+ resolve();
285
+ });
286
+ });
287
+ await initializeMcp(mcpProcess);
288
+ const collected = [];
289
+ let startCursor;
290
+ for (let i = 0; i < 50; i++) {
291
+ const args = { page_size: 100 };
292
+ if (startCursor)
293
+ args.start_cursor = startCursor;
294
+ const raw = await callMcpTool(mcpProcess, 'API-get-users', args);
295
+ const result = unwrapMcpResult(raw);
296
+ if (!result || typeof result !== 'object')
297
+ break;
298
+ const page = result;
299
+ const results = Array.isArray(page.results) ? page.results : [];
300
+ for (const entry of results) {
301
+ if (entry.type !== 'person')
302
+ continue;
303
+ const id = typeof entry.id === 'string' ? entry.id : '';
304
+ const name = typeof entry.name === 'string' ? entry.name.trim() : '';
305
+ const person = entry.person;
306
+ const email = person && typeof person.email === 'string' ? person.email : '';
307
+ if (!id || !email)
308
+ continue;
309
+ const avatarUrl = typeof entry.avatar_url === 'string' && entry.avatar_url.length > 0 ? entry.avatar_url : null;
310
+ collected.push({ id, name: name || email, email, avatarUrl });
311
+ }
312
+ if (!page.has_more)
313
+ break;
314
+ startCursor = typeof page.next_cursor === 'string' ? page.next_cursor : undefined;
315
+ if (!startCursor)
316
+ break;
317
+ }
318
+ collected.sort((a, b) => a.name.localeCompare(b.name, 'fr'));
319
+ return collected;
320
+ }
321
+ finally {
322
+ mcpProcess.stdin?.end();
323
+ mcpProcess.kill();
324
+ }
325
+ }
326
+ export async function assignNotionPageToSelf(notionUrl, propertyName) {
327
+ if (!propertyName)
328
+ return { assigned: false, reason: 'no property name configured' };
329
+ const global = getGlobalSettings();
330
+ const userId = global.notionUserId.trim();
331
+ if (!userId)
332
+ return { assigned: false, reason: 'no Notion user UUID configured in settings' };
333
+ let pageId;
334
+ try {
335
+ pageId = parseNotionUrl(notionUrl);
336
+ }
337
+ catch (err) {
338
+ return { assigned: false, reason: err instanceof Error ? err.message : String(err) };
339
+ }
340
+ let mcpProcess;
341
+ try {
342
+ mcpProcess = spawnNotionMcp(global.notionMcpKey);
343
+ }
344
+ catch (err) {
345
+ return { assigned: false, reason: err instanceof Error ? err.message : String(err) };
346
+ }
347
+ try {
348
+ await new Promise((resolve, reject) => {
349
+ const timeout = setTimeout(() => resolve(), 1000);
350
+ mcpProcess.on('error', (err) => {
351
+ clearTimeout(timeout);
352
+ reject(new Error(`Failed to start MCP Notion server: ${err.message}`));
353
+ });
354
+ mcpProcess.stdout?.once('data', () => {
355
+ clearTimeout(timeout);
356
+ resolve();
357
+ });
358
+ });
359
+ await initializeMcp(mcpProcess);
360
+ const pageRaw = await callMcpTool(mcpProcess, 'API-retrieve-a-page', { page_id: pageId });
361
+ const pageResult = unwrapMcpResult(pageRaw);
362
+ if (pageResult && typeof pageResult === 'object') {
363
+ const props = pageResult.properties;
364
+ const prop = props?.[propertyName];
365
+ if (prop && Array.isArray(prop.people) && prop.people.length > 0) {
366
+ return { assigned: false, reason: 'property already has an assignee' };
367
+ }
368
+ }
369
+ await callMcpTool(mcpProcess, 'API-patch-page', {
370
+ page_id: pageId,
371
+ properties: {
372
+ [propertyName]: { people: [{ id: userId }] },
373
+ },
374
+ });
375
+ return { assigned: true, reason: `assigned ${userId} to "${propertyName}"` };
376
+ }
377
+ catch (err) {
378
+ return { assigned: false, reason: err instanceof Error ? err.message : String(err) };
379
+ }
380
+ finally {
381
+ mcpProcess.stdin?.end();
382
+ mcpProcess.kill();
383
+ }
384
+ }
272
385
  /** Update a status property on a Notion page. Best-effort, does not throw. */
273
386
  export async function updateNotionStatus(notionUrl, propertyName, statusValue) {
274
387
  const pageId = parseNotionUrl(notionUrl);
@@ -87,6 +87,8 @@ export function parseSentryResponse(markdown, numericId) {
87
87
  const extraData = matchSection(markdown, 'Extra Data');
88
88
  const additionalContext = matchSection(markdown, 'Additional Context');
89
89
  const extraContext = [extraData, additionalContext].filter((s) => s.length > 0).join('\n\n');
90
+ const rawAssignee = matchField(markdown, 'Assigned To') || matchField(markdown, 'Assignee') || matchField(markdown, 'Assigned');
91
+ const assignee = /^(unassigned|none|-|n\/a)$/i.test(rawAssignee.trim()) ? '' : rawAssignee.trim();
90
92
  return {
91
93
  issueId,
92
94
  issueNumericId: numericId,
@@ -100,6 +102,7 @@ export function parseSentryResponse(markdown, numericId) {
100
102
  tags,
101
103
  offendingSpans: parseOffendingSpans(markdown),
102
104
  extraContext,
105
+ assignee,
103
106
  };
104
107
  }
105
108
  // ─── extractSentryIssue ───────────────────────────────────────────────────────
@@ -133,3 +136,66 @@ export async function extractSentryIssue(url) {
133
136
  mcpProcess.kill();
134
137
  }
135
138
  }
139
+ function extractSentryUserId(whoamiResult) {
140
+ if (typeof whoamiResult === 'string') {
141
+ const match = whoamiResult.match(/User\s*ID(?:\s*is)?\s*[:=]?\s*(\d+)/i);
142
+ return match ? match[1] : null;
143
+ }
144
+ if (!whoamiResult || typeof whoamiResult !== 'object')
145
+ return null;
146
+ const r = whoamiResult;
147
+ if (typeof r.id === 'string' && r.id.length > 0)
148
+ return r.id;
149
+ if (typeof r.id === 'number')
150
+ return String(r.id);
151
+ for (const key of ['user', 'data', 'me']) {
152
+ const nested = r[key];
153
+ if (nested && typeof nested === 'object') {
154
+ const inner = nested;
155
+ if (typeof inner.id === 'string' && inner.id.length > 0)
156
+ return inner.id;
157
+ if (typeof inner.id === 'number')
158
+ return String(inner.id);
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+ export async function assignSentryIssueToSelf(issueUrl) {
164
+ let config;
165
+ try {
166
+ const global = getGlobalSettings();
167
+ config = readSentryMcpConfig(global.sentryMcpKey);
168
+ }
169
+ catch (err) {
170
+ return { assigned: false, reason: err instanceof Error ? err.message : String(err) };
171
+ }
172
+ const mcpProcess = spawnMcpProcess(config.command, config.args, config.env);
173
+ try {
174
+ await new Promise((resolve, reject) => {
175
+ const timeout = setTimeout(() => resolve(), 1000);
176
+ mcpProcess.on('error', (err) => {
177
+ clearTimeout(timeout);
178
+ reject(new Error(`Failed to start Sentry MCP server: ${err.message}`));
179
+ });
180
+ });
181
+ await initializeMcp(mcpProcess);
182
+ const whoamiRaw = await callMcpTool(mcpProcess, 'whoami', {});
183
+ const whoami = unwrapMcpResult(whoamiRaw);
184
+ const userId = extractSentryUserId(whoami);
185
+ if (!userId) {
186
+ return { assigned: false, reason: 'whoami did not return a user id' };
187
+ }
188
+ await callMcpTool(mcpProcess, 'update_issue', {
189
+ issueUrl,
190
+ assignedTo: `user:${userId}`,
191
+ });
192
+ return { assigned: true, reason: `assigned to user:${userId}` };
193
+ }
194
+ catch (err) {
195
+ return { assigned: false, reason: err instanceof Error ? err.message : String(err) };
196
+ }
197
+ finally {
198
+ mcpProcess.stdin?.end();
199
+ mcpProcess.kill();
200
+ }
201
+ }
@@ -397,6 +397,36 @@ const settingsMigrations = [
397
397
  }
398
398
  },
399
399
  },
400
+ {
401
+ version: 23,
402
+ name: 'add-custom-brainstorming-instruction',
403
+ // Adds the 5th `custom*` field — the brainstorming-phase instruction
404
+ // injected into the workspace bootstrap prompt. Seeds with the agnostic
405
+ // baseline so users in `custom` mode can edit from a neutral start.
406
+ migrate: ({ global }) => {
407
+ if (typeof global.customBrainstormingInstruction !== 'string') {
408
+ global.customBrainstormingInstruction = AGNOSTIC_PROMPTS.brainstormingInstruction;
409
+ }
410
+ },
411
+ },
412
+ {
413
+ version: 24,
414
+ name: 'add-notion-assignee-property',
415
+ migrate: ({ global }) => {
416
+ if (typeof global.notionAssigneeProperty !== 'string') {
417
+ global.notionAssigneeProperty = '';
418
+ }
419
+ },
420
+ },
421
+ {
422
+ version: 25,
423
+ name: 'add-notion-user-id',
424
+ migrate: ({ global }) => {
425
+ if (typeof global.notionUserId !== 'string') {
426
+ global.notionUserId = '';
427
+ }
428
+ },
429
+ },
400
430
  ];
401
431
  /** Current settings schema version — always equals the highest migration version. */
402
432
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -438,6 +468,8 @@ function defaultSettings() {
438
468
  audioNotificationVolume: 1,
439
469
  notionStatusProperty: '',
440
470
  notionInProgressStatus: '',
471
+ notionAssigneeProperty: '',
472
+ notionUserId: '',
441
473
  defaultPermissionModeByEngine: { 'claude-code': 'plan', codex: 'plan' },
442
474
  notionMcpKey: '',
443
475
  sentryMcpKey: '',
@@ -460,6 +492,7 @@ function defaultSettings() {
460
492
  customAutoLoopReviewGate: AGNOSTIC_PROMPTS.autoLoopReviewGate,
461
493
  customAutoLoopGroomingIntro: AGNOSTIC_PROMPTS.autoLoopGroomingIntro,
462
494
  customQaPromptTemplate: AGNOSTIC_PROMPTS.qaPromptTemplate,
495
+ customBrainstormingInstruction: AGNOSTIC_PROMPTS.brainstormingInstruction,
463
496
  },
464
497
  projects: [],
465
498
  };
@@ -708,6 +741,8 @@ export function updateGlobalSettings(data) {
708
741
  'audioNotificationVolume',
709
742
  'notionStatusProperty',
710
743
  'notionInProgressStatus',
744
+ 'notionAssigneeProperty',
745
+ 'notionUserId',
711
746
  'defaultPermissionModeByEngine',
712
747
  'notionMcpKey',
713
748
  'sentryMcpKey',
@@ -730,6 +765,7 @@ export function updateGlobalSettings(data) {
730
765
  'customAutoLoopReviewGate',
731
766
  'customAutoLoopGroomingIntro',
732
767
  'customQaPromptTemplate',
768
+ 'customBrainstormingInstruction',
733
769
  ];
734
770
  const filtered = pickKnownKeys(data, allowedGlobalKeys);
735
771
  if (filtered.tags !== undefined) {
@@ -1,4 +1,4 @@
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';
1
+ import { AGNOSTIC_AUTO_LOOP_GROOMING_INTRO, AGNOSTIC_AUTO_LOOP_REVIEW_GATE, AGNOSTIC_BRAINSTORMING_INSTRUCTION, AGNOSTIC_QA_PROMPT_TEMPLATE, AGNOSTIC_REVIEW_TEMPLATE, GROOMING_INTRO_COMBINED, GROOMING_INTRO_GSTACK, GROOMING_INTRO_SUPERPOWERS, } from '../../shared/skill-suite-prompts.js';
2
2
  // Headers and body shared with the agnostic baseline. We reconstruct them
3
3
  // locally here so the suite-specific text can sit between header and body
4
4
  // in the same shape across all 3 suites.
@@ -39,6 +39,11 @@ export const SUPERPOWERS_PROMPTS = {
39
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
40
  autoLoopGroomingIntro: GROOMING_INTRO_SUPERPOWERS,
41
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
+ brainstormingInstruction: 'Brainstorm the implementation approach with discipline:\n' +
43
+ '1. Use the `superpowers:brainstorming` skill — it walks you through purpose, requirements, and design BEFORE any code. Ask clarifying questions and wait for explicit user approval on the design before moving on.\n' +
44
+ '2. Use `superpowers:writing-plans` to turn the approved design into a multi-step implementation plan, saved under `docs/superpowers/plans/`.\n' +
45
+ '3. If you encounter a bug or unexpected behaviour during exploration, use `superpowers:systematic-debugging` rather than guessing.\n' +
46
+ "Do NOT skip the skills or rationalise around them — that's how the rigour gets lost.",
42
47
  };
43
48
  export const GSTACK_PROMPTS = {
44
49
  reviewTemplate: REVIEW_HEADER +
@@ -46,13 +51,86 @@ export const GSTACK_PROMPTS = {
46
51
  REVIEW_BODY,
47
52
  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
53
  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.',
54
+ qaPromptTemplate: `QA pass for workspace "{{workspace_name}}" in project {{project_name}}.
55
+
56
+ Branch: {{branch_name}}
57
+ Staging URL: {{staging_url}}
58
+
59
+ Pick the right gstack tool for the situation:
60
+
61
+ - \`/browse\` — Headless navigation: URL → interactions → screenshots. Use for quick dogfooding a flow or sanity-checking a PR.
62
+ - \`/qa {{staging_url}}\` — Systematic QA with automatic bug fixing. Three tiers (invoke as \`/qa Quick\`, \`/qa Standard\`, or \`/qa Exhaustive\`):
63
+ - **Quick** — critical and high-severity bugs only.
64
+ - **Standard** — adds medium-severity bugs.
65
+ - **Exhaustive** — adds cosmetic issues.
66
+ - \`/qa-only {{staging_url}}\` — Same methodology as \`/qa\` but report only, no code changes. Use when you only want a bug report.
67
+ - \`/design-review\` — Visual audit (consistency, spacing, hierarchy, AI slop). Commits atomic fixes with before/after screenshots.
68
+
69
+ For reproducible regression coverage that runs on every PR, prefer **Cypress** specs in \`test/cypress/\` instead of \`/qa\`. Reserve the gstack tools above for exploration, dogfooding, and one-shot visual debugging.`,
70
+ brainstormingInstruction: 'Brainstorm the implementation approach using the gstack sprint pipeline:\n' +
71
+ '1. Run `/office-hours` — six forcing questions that reframe the problem and write a design doc. This is where you challenge premises and surface alternatives before any code.\n' +
72
+ '2. Run `/autoplan` — it chains CEO → design → eng → DX reviews automatically (auto-detects which apply) and surfaces only the taste decisions you need to approve. Prefer this over manual orchestration.\n' +
73
+ '3. If you need fine control (e.g. skip a step, run one in isolation), invoke the individual skills instead: `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review`, `/plan-devex-review`.\n' +
74
+ '4. Wait for explicit user approval on the final plan before announcing brainstorming is done.',
50
75
  };
51
76
  export const AGNOSTIC_PROMPTS = {
52
77
  reviewTemplate: AGNOSTIC_REVIEW_TEMPLATE,
53
78
  autoLoopReviewGate: AGNOSTIC_AUTO_LOOP_REVIEW_GATE,
54
79
  autoLoopGroomingIntro: AGNOSTIC_AUTO_LOOP_GROOMING_INTRO,
55
80
  qaPromptTemplate: AGNOSTIC_QA_PROMPT_TEMPLATE,
81
+ brainstormingInstruction: AGNOSTIC_BRAINSTORMING_INSTRUCTION,
82
+ };
83
+ // ── Combined: superpowers + gstack ────────────────────────────────────────────
84
+ // For users who install BOTH suites and want prompts that surface each suite's
85
+ // strengths. Specialised by intent, not "use whichever is available":
86
+ // - /review (gstack) → tactical code-level bug-hunting + auto-fix
87
+ // - superpowers:requesting-code-review → principles-level critique (silent
88
+ // failures, surface-area discipline, test-design soundness)
89
+ // - /qa, /design-review, /browse → interactive QA (gstack only)
90
+ // - superpowers-chrome:browsing → low-level browser control fallback
91
+ export const COMBINED_PROMPTS = {
92
+ reviewTemplate: REVIEW_HEADER +
93
+ 'Two complementary review skills are available — pick by intent:\n' +
94
+ '- `/review` (gstack Staff Engineer) for tactical bug-hunting that finds issues passing CI but blowing up in production. Auto-fixes the obvious ones.\n' +
95
+ '- `superpowers:requesting-code-review` for principles-level critique — silent failures, test-design soundness, surface-area discipline.\n' +
96
+ 'You can run both on the same diff if the change is large. If neither is available, fall back to the manual checklist below.\n\n' +
97
+ REVIEW_BODY,
98
+ autoLoopReviewGate: 'Code review gate — BEFORE marking the task done, pick the appropriate review skill (two are installed):\n' +
99
+ '- Default to `/review` (gstack Staff Engineer) for tactical code-level bugs and auto-fixes.\n' +
100
+ '- Use `superpowers:requesting-code-review` instead when the task introduces tests, refactors, or design decisions worth a principles-level critique.\n\n' +
101
+ 'Brief the chosen 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. If the reviewer auto-fixes minor issues, accept the fixes via an amend or fix-up commit, then re-run step 3 checks.',
102
+ autoLoopGroomingIntro: GROOMING_INTRO_COMBINED,
103
+ qaPromptTemplate: `QA pass for workspace "{{workspace_name}}" in project {{project_name}}.
104
+
105
+ Branch: {{branch_name}}
106
+ Staging URL: {{staging_url}}
107
+
108
+ Pick the right tool for the situation. gstack covers interactive QA; superpowers covers low-level browser control as a fallback.
109
+
110
+ gstack QA toolkit (preferred for interactive QA):
111
+ - \`/browse\` — Headless navigation: URL → interactions → screenshots. Use for quick dogfooding a flow or sanity-checking a PR.
112
+ - \`/qa {{staging_url}}\` — Systematic QA with automatic bug fixing. Three tiers (invoke as \`/qa Quick\`, \`/qa Standard\`, or \`/qa Exhaustive\`):
113
+ - **Quick** — critical and high-severity bugs only.
114
+ - **Standard** — adds medium-severity bugs.
115
+ - **Exhaustive** — adds cosmetic issues.
116
+ - \`/qa-only {{staging_url}}\` — Same methodology as \`/qa\` but report only, no code changes.
117
+ - \`/design-review\` — Visual audit (consistency, spacing, hierarchy, AI slop). Commits atomic fixes with before/after screenshots.
118
+
119
+ Superpowers alternative (low-level browser control):
120
+ - \`superpowers-chrome:browsing\` — Direct Chrome DevTools Protocol control over an existing Chrome session: multi-tab management, form automation, content extraction. Use when you need fine-grained control beyond what \`/browse\` exposes.
121
+
122
+ For reproducible regression coverage that runs on every PR, prefer **Cypress** specs in \`test/cypress/\` instead of any interactive QA skill. Reserve the tools above for exploration, dogfooding, and one-shot visual debugging.`,
123
+ brainstormingInstruction: 'Brainstorm using both suites — each plays to its strength:\n' +
124
+ '1. Early product framing: prefer gstack `/office-hours` for product-shaped work (six forcing questions + design doc). Fall back to `superpowers:brainstorming` for purely infra/refactor work where the product lens does not apply.\n' +
125
+ '2. Plan construction — pick whichever fits the work better:\n' +
126
+ ' - gstack `/autoplan` for the chained CEO/design/eng/DX pipeline (auto-detects which apply).\n' +
127
+ ' - `superpowers:writing-plans` for a TDD-shaped multi-step plan that maps cleanly onto subagent dispatch.\n' +
128
+ '3. For debugging during exploration, prefer `/investigate` (gstack — root-cause methodology) or `superpowers:systematic-debugging`, whichever you reach for first.\n' +
129
+ '4. Plan review gate (MANDATORY before announcing brainstorming is done): once the plan is ready, it MUST pass the gstack plan-review skills before you proceed.\n' +
130
+ ' - If you built the plan via `/autoplan`, you have ALREADY passed the chained reviews — skip this step.\n' +
131
+ ' - Otherwise (plan came from `superpowers:writing-plans` or any other path), run `/autoplan` now on the existing plan to chain the reviews, OR invoke the relevant ones individually: `/plan-ceo-review` (scope challenge), `/plan-eng-review` (architecture / edge cases / tests), `/plan-design-review` (UI / AI slop, when there is UI scope), `/plan-devex-review` (when the work has developer-facing surface).\n' +
132
+ ' - Apply any changes the reviews recommend before moving on.\n' +
133
+ '5. Wait for explicit user approval on the final reviewed plan before announcing brainstorming is done.',
56
134
  };
57
135
  /**
58
136
  * Resolve the suite prompts to use right now, given the global `skillSuite`
@@ -64,6 +142,8 @@ export function getSuitePrompts(suite, overrides) {
64
142
  return SUPERPOWERS_PROMPTS;
65
143
  if (suite === 'gstack')
66
144
  return GSTACK_PROMPTS;
145
+ if (suite === 'superpowers+gstack')
146
+ return COMBINED_PROMPTS;
67
147
  // custom mode: per-field fallback to AGNOSTIC when the override is missing/blank
68
148
  const pick = (k) => {
69
149
  const value = overrides[k];
@@ -76,5 +156,6 @@ export function getSuitePrompts(suite, overrides) {
76
156
  autoLoopReviewGate: pick('autoLoopReviewGate'),
77
157
  autoLoopGroomingIntro: pick('autoLoopGroomingIntro'),
78
158
  qaPromptTemplate: pick('qaPromptTemplate'),
159
+ brainstormingInstruction: pick('brainstormingInstruction'),
79
160
  };
80
161
  }