@loicngr/kobo 1.7.7 → 1.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/AGENTS.md +29 -0
  2. package/README.md +62 -4
  3. package/dist/mcp-server/kobo-tasks-server.js +27 -0
  4. package/dist/server/routes/health.js +14 -0
  5. package/dist/server/routes/workspaces.js +60 -16
  6. package/dist/server/services/agent/engines/claude-code/capabilities.js +7 -0
  7. package/dist/server/services/agent/engines/codex/capabilities.js +18 -0
  8. package/dist/server/services/agent/engines/codex/client.js +36 -0
  9. package/dist/server/services/agent/engines/codex/engine.js +276 -0
  10. package/dist/server/services/agent/engines/codex/event-mapper.js +473 -0
  11. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +60 -0
  12. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +31 -0
  13. package/dist/server/services/agent/engines/codex/options-builder.js +81 -0
  14. package/dist/server/services/agent/engines/codex/protocol/types.js +11 -0
  15. package/dist/server/services/agent/engines/codex/server-requests.js +99 -0
  16. package/dist/server/services/agent/engines/codex/spawn.js +27 -0
  17. package/dist/server/services/agent/engines/registry.js +2 -0
  18. package/dist/server/services/agent/orchestrator.js +1 -1
  19. package/dist/server/services/auto-loop-service.js +16 -2
  20. package/dist/server/services/pr-watcher-service.js +84 -33
  21. package/dist/server/services/review-template-service.js +24 -31
  22. package/dist/server/services/settings-service.js +140 -6
  23. package/dist/server/services/skill-suite-prompts.js +80 -0
  24. package/dist/server/utils/git-ops.js +69 -15
  25. package/dist/server/utils/paths.js +7 -0
  26. package/dist/shared/auto-loop-prompts.js +18 -1
  27. package/dist/shared/codex-models.js +43 -0
  28. package/dist/shared/project-colors.js +23 -0
  29. package/dist/shared/skill-suite-prompts.js +66 -0
  30. package/package.json +2 -1
  31. package/src/client/dist/spa/assets/ActivityFeed-CKjFT9t6.js +8 -0
  32. package/src/client/dist/spa/assets/{ActivityFeed-tE4LVYck.css → ActivityFeed-WjiQ9716.css} +1 -1
  33. package/src/client/dist/spa/assets/{ClosePopup-DTcbxsC0.js → ClosePopup-DMnQG6nw.js} +1 -1
  34. package/src/client/dist/spa/assets/CreatePage-BhFrUkEN.js +2 -0
  35. package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +1 -0
  36. package/src/client/dist/spa/assets/{DiffViewer-D-uNbBq0.js → DiffViewer-BSnvba7W.js} +3 -3
  37. package/src/client/dist/spa/assets/HealthPage-DZYZWGHp.js +1 -0
  38. package/src/client/dist/spa/assets/MainLayout-C45J7rSF.css +1 -0
  39. package/src/client/dist/spa/assets/MainLayout-CMuiNpet.js +37 -0
  40. package/src/client/dist/spa/assets/QChip-BgzxI33B.js +36 -0
  41. package/src/client/dist/spa/assets/{QExpansionItem-BGg74no1.js → QExpansionItem-Fij7yBbG.js} +1 -1
  42. package/src/client/dist/spa/assets/{QItemSection-CQUDd0Vg.js → QItemSection-Bz1ZDJO5.js} +1 -1
  43. package/src/client/dist/spa/assets/{QMenu-D6uqosRg.js → QMenu-CMoolewZ.js} +1 -1
  44. package/src/client/dist/spa/assets/QRadio-4HnR_A-K.js +1 -0
  45. package/src/client/dist/spa/assets/{QTooltip-DUGPNNeQ.js → QTooltip-CbLXk2Bs.js} +1 -1
  46. package/src/client/dist/spa/assets/{SearchPage-C07dgzT9.js → SearchPage-CBSgEvVF.js} +1 -1
  47. package/src/client/dist/spa/assets/SettingsPage-C7TkcKXU.css +1 -0
  48. package/src/client/dist/spa/assets/SettingsPage-pY-zbPxn.js +9 -0
  49. package/src/client/dist/spa/assets/{TouchPan-DvVlszwO.js → TouchPan-DiBNjOPH.js} +1 -1
  50. package/src/client/dist/spa/assets/{WorkspacePage-CRIcsASQ.css → WorkspacePage-B4YnZ6re.css} +1 -1
  51. package/src/client/dist/spa/assets/WorkspacePage-BTHvQga-.js +4 -0
  52. package/src/client/dist/spa/assets/{build-path-tree-CCMckvpr.js → build-path-tree-BmBqRiCQ.js} +1 -1
  53. package/src/client/dist/spa/assets/{cssMode-D6XTTdwy.js → cssMode-ypFF7quM.js} +1 -1
  54. package/src/client/dist/spa/assets/{editor.api-6hDVHddO.js → editor.api-DtpZuH_B.js} +1 -1
  55. package/src/client/dist/spa/assets/{editor.main-DsLU1RWu.js → editor.main-C7a7L2WP.js} +3 -3
  56. package/src/client/dist/spa/assets/{AutoLoopChip-CkSzkC0C.js → engineFeatures-BxAOQcPU.js} +1 -1
  57. package/src/client/dist/spa/assets/{expand-template-Crz1uiBt.js → expand-template-GaEux9_o.js} +1 -1
  58. package/src/client/dist/spa/assets/{formatters-guwb-rzl.js → formatters-h0XBETG5.js} +1 -1
  59. package/src/client/dist/spa/assets/{freemarker2-Bn1f0t2U.js → freemarker2-DUBmhe3W.js} +1 -1
  60. package/src/client/dist/spa/assets/{handlebars-O92Cbq66.js → handlebars-_XEXkADl.js} +1 -1
  61. package/src/client/dist/spa/assets/{html-Ck95BMBU.js → html-D8gmyhgI.js} +1 -1
  62. package/src/client/dist/spa/assets/{htmlMode-DDYhH2FJ.js → htmlMode-B84S5YOM.js} +1 -1
  63. package/src/client/dist/spa/assets/i18n-DLoe3l25.js +1 -0
  64. package/src/client/dist/spa/assets/index-Dx_W9yYo.js +2 -0
  65. package/src/client/dist/spa/assets/{javascript-Cy2ddqHg.js → javascript-500DcdS9.js} +1 -1
  66. package/src/client/dist/spa/assets/{jsonMode-BIfVcp5z.js → jsonMode-DmrWg6b7.js} +1 -1
  67. package/src/client/dist/spa/assets/kobo-commands-DCoQW_NQ.js +9 -0
  68. package/src/client/dist/spa/assets/{liquid-B287eegh.js → liquid-CfPJszlt.js} +1 -1
  69. package/src/client/dist/spa/assets/{mdx-B8HSzGai.js → mdx-DtjLwENT.js} +1 -1
  70. package/src/client/dist/spa/assets/{monaco.contribution-CofcHzEf.js → monaco.contribution-CxiO5UJd.js} +2 -2
  71. package/src/client/dist/spa/assets/{notifications-BPnKFW60.js → notifications-CEyiPnmw.js} +1 -1
  72. package/src/client/dist/spa/assets/permissionModes-CPZlEHoF.js +1 -0
  73. package/src/client/dist/spa/assets/project-color-C4vMEn4C.js +1 -0
  74. package/src/client/dist/spa/assets/{purify.es-BCEwTYRx.js → purify.es-C92_EGvT.js} +1 -1
  75. package/src/client/dist/spa/assets/{python-csaKR6_U.js → python-BS46_AMt.js} +1 -1
  76. package/src/client/dist/spa/assets/{razor-C2wEv-nX.js → razor-Ce9zcIFo.js} +1 -1
  77. package/src/client/dist/spa/assets/{render-chat-markdown-Bjcei0vn.js → render-chat-markdown-BvJwlMiW.js} +1 -1
  78. package/src/client/dist/spa/assets/{tsMode-DGLVs57K.js → tsMode-Cr9FJjYY.js} +1 -1
  79. package/src/client/dist/spa/assets/{typescript-w0GWHzZ3.js → typescript-Ov3wChBg.js} +1 -1
  80. package/src/client/dist/spa/assets/{use-panel-CbJ44rqY.js → use-panel-lBh91vcW.js} +1 -1
  81. package/src/client/dist/spa/assets/{xml-CTn-vnEd.js → xml-euA4jBI1.js} +1 -1
  82. package/src/client/dist/spa/assets/{yaml-CTyUSvLZ.js → yaml-BuPSq_BT.js} +1 -1
  83. package/src/client/dist/spa/index.html +5 -5
  84. package/src/mcp-server/kobo-tasks-server.ts +27 -0
  85. package/src/client/dist/spa/assets/ActivityFeed-DlPVoOGb.js +0 -7
  86. package/src/client/dist/spa/assets/CreatePage-BoRappO3.css +0 -1
  87. package/src/client/dist/spa/assets/CreatePage-DpCVNwYk.js +0 -2
  88. package/src/client/dist/spa/assets/HealthPage-xZ0PP4F-.js +0 -1
  89. package/src/client/dist/spa/assets/MainLayout-DdkKM2ba.js +0 -37
  90. package/src/client/dist/spa/assets/MainLayout-drolsINz.css +0 -1
  91. package/src/client/dist/spa/assets/QChip-erWIZgxW.js +0 -1
  92. package/src/client/dist/spa/assets/QRadio-DJxOyOA3.js +0 -1
  93. package/src/client/dist/spa/assets/QTabPanels-ClPY9y4T.js +0 -1
  94. package/src/client/dist/spa/assets/SettingsPage-CLNmI0Rr.css +0 -1
  95. package/src/client/dist/spa/assets/SettingsPage-D0CZNqkA.js +0 -9
  96. package/src/client/dist/spa/assets/WorkspacePage-CKeCLPi0.js +0 -4
  97. package/src/client/dist/spa/assets/i18n-BLgknHpf.js +0 -1
  98. package/src/client/dist/spa/assets/index-CdHDdk1y.js +0 -2
  99. package/src/client/dist/spa/assets/kobo-commands-w8VepGvD.js +0 -11
  100. package/src/client/dist/spa/assets/models-Bd_v3W7Q.js +0 -1
  101. /package/src/client/dist/spa/assets/{QBtn-DEuWKHbR.js → QBtn-CaJSOyt8.js} +0 -0
  102. /package/src/client/dist/spa/assets/{vue-i18n-DI-gS-CC.js → vue-i18n-CKCtKE87.js} +0 -0
@@ -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
@@ -299,6 +302,101 @@ const settingsMigrations = [
299
302
  global.voiceSuppressNonSpeechTokens = true;
300
303
  },
301
304
  },
305
+ {
306
+ version: 19,
307
+ name: 'split-default-model-by-engine',
308
+ // Codex was added alongside Claude Code; the single `defaultModel` is now
309
+ // ambiguous (different engines have different model catalogues). Split it
310
+ // into a per-engine map. Preserve the legacy value as the claude-code
311
+ // default for back-compat, and seed codex with `'auto'`.
312
+ migrate({ global }) {
313
+ const existing = global.defaultModelByEngine;
314
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
315
+ const legacyModel = typeof global.defaultModel === 'string' && global.defaultModel.length > 0
316
+ ? global.defaultModel
317
+ : 'auto';
318
+ global.defaultModelByEngine = {
319
+ 'claude-code': legacyModel,
320
+ codex: 'auto',
321
+ };
322
+ }
323
+ else {
324
+ // Backfill any missing engine entries idempotently.
325
+ const map = existing;
326
+ if (typeof map['claude-code'] !== 'string')
327
+ map['claude-code'] = 'auto';
328
+ if (typeof map.codex !== 'string')
329
+ map.codex = 'auto';
330
+ }
331
+ // Drop the legacy field once migrated.
332
+ delete global.defaultModel;
333
+ },
334
+ },
335
+ {
336
+ version: 20,
337
+ name: 'split-default-permission-mode-by-engine',
338
+ // Mirrors v19 for permission modes. Both engines accept the full mode set.
339
+ migrate({ global }) {
340
+ const existing = global.defaultPermissionModeByEngine;
341
+ const legacyMode = typeof global.defaultPermissionMode === 'string' && global.defaultPermissionMode.length > 0
342
+ ? global.defaultPermissionMode
343
+ : 'plan';
344
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
345
+ global.defaultPermissionModeByEngine = {
346
+ 'claude-code': legacyMode,
347
+ codex: legacyMode,
348
+ };
349
+ }
350
+ else {
351
+ const map = existing;
352
+ if (typeof map['claude-code'] !== 'string')
353
+ map['claude-code'] = legacyMode;
354
+ if (typeof map.codex !== 'string')
355
+ map.codex = legacyMode;
356
+ }
357
+ delete global.defaultPermissionMode;
358
+ },
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
+ },
302
400
  ];
303
401
  /** Current settings schema version — always equals the highest migration version. */
304
402
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -326,7 +424,7 @@ function defaultSettings() {
326
424
  return {
327
425
  schemaVersion: SETTINGS_SCHEMA_VERSION,
328
426
  global: {
329
- defaultModel: 'claude-opus-4-7',
427
+ defaultModelByEngine: { 'claude-code': 'auto', codex: 'auto' },
330
428
  dangerouslySkipPermissions: true,
331
429
  prPromptTemplate: DEFAULT_PR_PROMPT_TEMPLATE,
332
430
  reviewPromptTemplate: DEFAULT_REVIEW_PROMPT_TEMPLATE,
@@ -340,7 +438,7 @@ function defaultSettings() {
340
438
  audioNotificationVolume: 1,
341
439
  notionStatusProperty: '',
342
440
  notionInProgressStatus: '',
343
- defaultPermissionMode: 'plan',
441
+ defaultPermissionModeByEngine: { 'claude-code': 'plan', codex: 'plan' },
344
442
  notionMcpKey: '',
345
443
  sentryMcpKey: '',
346
444
  tags: [...DEFAULT_WORKSPACE_TAGS],
@@ -356,6 +454,12 @@ function defaultSettings() {
356
454
  voicePrompt: '',
357
455
  voiceTranslateToEnglish: false,
358
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,
359
463
  },
360
464
  projects: [],
361
465
  };
@@ -385,6 +489,7 @@ function defaultProjectSettings(projectPath) {
385
489
  finalization: {
386
490
  prompt: DEFAULT_FINALIZATION_PROMPT,
387
491
  },
492
+ color: null,
388
493
  };
389
494
  }
390
495
  function pickKnownKeys(data, allowedKeys) {
@@ -540,8 +645,9 @@ export function getEffectiveSettings(projectPath) {
540
645
  const settings = readSettings();
541
646
  const project = settings.projects.find((p) => p.path === projectPath) ?? null;
542
647
  if (!project) {
648
+ const claudeCodeDefault = settings.global.defaultModelByEngine?.['claude-code'] ?? 'auto';
543
649
  return {
544
- model: settings.global.defaultModel,
650
+ model: claudeCodeDefault,
545
651
  dangerouslySkipPermissions: settings.global.dangerouslySkipPermissions,
546
652
  prPromptTemplate: settings.global.prPromptTemplate,
547
653
  reviewPromptTemplate: settings.global.reviewPromptTemplate,
@@ -555,8 +661,14 @@ export function getEffectiveSettings(projectPath) {
555
661
  notionInProgressStatus: settings.global.notionInProgressStatus,
556
662
  };
557
663
  }
664
+ // `model` here is the legacy single-string field exposed via EffectiveSettings
665
+ // for back-compat with existing callers. The engine-aware default lives in
666
+ // `global.defaultModelByEngine[engineId]` and is read directly by the create
667
+ // flow / settings UI. Fall back through claude-code's entry (the historical
668
+ // semantics) so this field never goes empty.
669
+ const claudeCodeDefault = settings.global.defaultModelByEngine?.['claude-code'] ?? 'auto';
558
670
  return {
559
- model: project.defaultModel || settings.global.defaultModel,
671
+ model: project.defaultModel || claudeCodeDefault,
560
672
  dangerouslySkipPermissions: project.dangerouslySkipPermissions ?? settings.global.dangerouslySkipPermissions,
561
673
  prPromptTemplate: project.prPromptTemplate || settings.global.prPromptTemplate,
562
674
  reviewPromptTemplate: project.reviewPromptTemplate || settings.global.reviewPromptTemplate,
@@ -573,8 +685,16 @@ export function getEffectiveSettings(projectPath) {
573
685
  /** Merge partial updates into global settings and persist. */
574
686
  export function updateGlobalSettings(data) {
575
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
+ }
576
696
  const allowedGlobalKeys = [
577
- 'defaultModel',
697
+ 'defaultModelByEngine',
578
698
  'dangerouslySkipPermissions',
579
699
  'prPromptTemplate',
580
700
  'reviewPromptTemplate',
@@ -588,7 +708,7 @@ export function updateGlobalSettings(data) {
588
708
  'audioNotificationVolume',
589
709
  'notionStatusProperty',
590
710
  'notionInProgressStatus',
591
- 'defaultPermissionMode',
711
+ 'defaultPermissionModeByEngine',
592
712
  'notionMcpKey',
593
713
  'sentryMcpKey',
594
714
  'tags',
@@ -604,6 +724,12 @@ export function updateGlobalSettings(data) {
604
724
  'voicePrompt',
605
725
  'voiceTranslateToEnglish',
606
726
  'voiceSuppressNonSpeechTokens',
727
+ 'flattenWorkspaceList',
728
+ 'skillSuite',
729
+ 'customReviewTemplate',
730
+ 'customAutoLoopReviewGate',
731
+ 'customAutoLoopGroomingIntro',
732
+ 'customQaPromptTemplate',
607
733
  ];
608
734
  const filtered = pickKnownKeys(data, allowedGlobalKeys);
609
735
  if (filtered.tags !== undefined) {
@@ -647,6 +773,13 @@ function isNonNativeWindowsPath(value) {
647
773
  }
648
774
  /** Create or update project-specific settings. Merges devServer, e2e, and finalization fields on update. */
649
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
+ }
650
783
  const allowedProjectKeys = [
651
784
  'displayName',
652
785
  'defaultSourceBranch',
@@ -661,6 +794,7 @@ export function upsertProject(projectPath, data) {
661
794
  'devServer',
662
795
  'e2e',
663
796
  'finalization',
797
+ 'color',
664
798
  ];
665
799
  const allowedDevServerKeys = ['startCommand', 'stopCommand'];
666
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
+ }
@@ -409,21 +409,80 @@ export function getPrUrl(repoPath, branchName) {
409
409
  return null;
410
410
  }
411
411
  }
412
- /** Get the state and URL of the PR for a branch. Returns null if no PR exists. */
412
+ const GH_PR_FIELDS = [
413
+ 'number',
414
+ 'title',
415
+ 'url',
416
+ 'state',
417
+ 'baseRefName',
418
+ 'reviewDecision',
419
+ 'author',
420
+ 'assignees',
421
+ 'labels',
422
+ 'latestReviews',
423
+ 'reviewRequests',
424
+ 'reviewThreads',
425
+ 'statusCheckRollup',
426
+ 'updatedAt',
427
+ ].join(',');
428
+ function mapGhPrToSnapshot(raw) {
429
+ const reviewers = [];
430
+ const seen = new Set();
431
+ for (const r of raw.latestReviews ?? []) {
432
+ const login = r.author?.login;
433
+ if (!login || seen.has(login))
434
+ continue;
435
+ seen.add(login);
436
+ reviewers.push({ login, state: r.state ?? 'COMMENTED' });
437
+ }
438
+ for (const r of raw.reviewRequests ?? []) {
439
+ if (!r.login || seen.has(r.login))
440
+ continue;
441
+ seen.add(r.login);
442
+ reviewers.push({ login: r.login, state: 'PENDING' });
443
+ }
444
+ const checks = (raw.statusCheckRollup ?? []).map((c) => ({
445
+ name: c.name,
446
+ conclusion: c.conclusion ?? null,
447
+ status: c.status,
448
+ detailsUrl: c.detailsUrl ?? null,
449
+ }));
450
+ let rollup = null;
451
+ if (checks.length > 0) {
452
+ if (checks.some((c) => c.conclusion === 'FAILURE'))
453
+ rollup = 'FAILURE';
454
+ else if (checks.some((c) => c.status !== 'COMPLETED'))
455
+ rollup = 'PENDING';
456
+ else
457
+ rollup = 'SUCCESS';
458
+ }
459
+ const unresolvedReviewThreadsCount = (raw.reviewThreads ?? []).reduce((acc, t) => acc + (t.isResolved ? 0 : 1), 0);
460
+ return {
461
+ number: raw.number,
462
+ title: raw.title,
463
+ url: raw.url,
464
+ state: raw.state,
465
+ base: raw.baseRefName ?? '',
466
+ reviewDecision: raw.reviewDecision ?? null,
467
+ author: { login: raw.author?.login ?? '' },
468
+ assignees: (raw.assignees ?? []).map((a) => ({ login: a.login })),
469
+ reviewers,
470
+ labels: (raw.labels ?? []).map((l) => ({ name: l.name, color: l.color })),
471
+ ci: { rollup, checks },
472
+ updatedAt: raw.updatedAt ?? '',
473
+ unresolvedReviewThreadsCount,
474
+ };
475
+ }
476
+ /** Get a rich snapshot of the PR for a branch. Returns null if no PR exists. */
413
477
  export function getPrStatus(repoPath, branchName) {
414
478
  try {
415
- const raw = execFileSync('gh', ['pr', 'view', branchName, '--json', 'state,url,baseRefName'], {
479
+ const raw = execFileSync('gh', ['pr', 'view', branchName, '--json', GH_PR_FIELDS], {
416
480
  cwd: repoPath,
417
481
  encoding: 'utf-8',
418
482
  }).trim();
419
483
  if (!raw)
420
484
  return null;
421
- const parsed = JSON.parse(raw);
422
- return {
423
- state: parsed.state,
424
- url: parsed.url,
425
- base: parsed.baseRefName || undefined,
426
- };
485
+ return mapGhPrToSnapshot(JSON.parse(raw));
427
486
  }
428
487
  catch {
429
488
  return null;
@@ -766,19 +825,14 @@ export async function getPrUrlAsync(repoPath, branchName) {
766
825
  /** Async version of getPrStatus. Returns null if no PR exists. */
767
826
  export async function getPrStatusAsync(repoPath, branchName) {
768
827
  try {
769
- const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', 'state,url,baseRefName'], {
828
+ const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', GH_PR_FIELDS], {
770
829
  cwd: repoPath,
771
830
  encoding: 'utf-8',
772
831
  });
773
832
  const raw = stdout.trim();
774
833
  if (!raw)
775
834
  return null;
776
- const parsed = JSON.parse(raw);
777
- return {
778
- state: parsed.state,
779
- url: parsed.url,
780
- base: parsed.baseRefName || undefined,
781
- };
835
+ return mapGhPrToSnapshot(JSON.parse(raw));
782
836
  }
783
837
  catch {
784
838
  return null;
@@ -100,8 +100,15 @@ export function getTemplatesPath() {
100
100
  * Absolute path to the compiled MCP server entry (shipped in the published
101
101
  * package as dist/mcp-server/kobo-tasks-server.js). Returns null if not
102
102
  * present — callers (orchestrator) then fall back to the TS source for dev.
103
+ *
104
+ * `KOBO_ENFORCE_LOCAL_HOME=1` (set by `npm run dev`) forces the source path
105
+ * even if a stale `dist/mcp-server/` from a prior `npm run build` is hanging
106
+ * around. Without this guard, edits to `kobo-tasks-server.ts` are silently
107
+ * ignored during dev (the orchestrator spawns the months-old compiled binary).
103
108
  */
104
109
  export function getCompiledMcpServerPath() {
110
+ if (process.env.KOBO_ENFORCE_LOCAL_HOME === '1')
111
+ return null;
105
112
  const compiled = getPackageAssetPath('dist', 'mcp-server', 'kobo-tasks-server.js');
106
113
  return fs.existsSync(compiled) ? compiled : null;
107
114
  }
@@ -16,7 +16,24 @@
16
16
  *
17
17
  * `AUTO_LOOP_HARD_RULES` is the trailing hard-rules block, same for both.
18
18
  */
19
- export const PREP_AUTOLOOP_INTRO = `You are preparing this workspace for Kōbō auto-loop mode. This is a GROOMING session only — DO NOT implement anything, DO NOT write or edit code, DO NOT run tests or builds, DO NOT invoke \`superpowers:executing-plans\` or any implementation skill. Your ONLY job is to curate the Kōbō task list via MCP tools.`;
19
+ import { getGroomingIntro } from './skill-suite-prompts.js';
20
+ /**
21
+ * Build the grooming intro for the chosen skill suite. In `custom` mode the
22
+ * user's `customAutoLoopGroomingIntro` overrides the agnostic default (when
23
+ * non-empty). This is the runtime-correct way to obtain the intro — every
24
+ * new consumer should call this rather than reading the legacy
25
+ * `PREP_AUTOLOOP_INTRO` constant below.
26
+ */
27
+ export function buildGroomingIntro(suite, customOverride) {
28
+ return getGroomingIntro(suite, customOverride);
29
+ }
30
+ /**
31
+ * Back-compat alias resolving to the `superpowers` variant (the historical
32
+ * inlined text). Kept so any existing import still works without a
33
+ * behaviour change. New code should call `buildGroomingIntro(suite, override)`
34
+ * with live settings so the user's `skillSuite` choice is honoured.
35
+ */
36
+ export const PREP_AUTOLOOP_INTRO = getGroomingIntro('superpowers');
20
37
  export function buildAutoLoopGroomingSteps(e2e, finalization) {
21
38
  const steps = [
22
39
  `1. Call \`kobo__list_tasks\` FIRST to inspect any pre-existing tasks (they may have been seeded from Notion, a template, or the CreatePage form).`,
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Codex model catalogue — kept in sync with the official roster published at
3
+ * developers.openai.com/codex/models. The Codex CLI accepts arbitrary strings
4
+ * in `--model`, so power users can still pin a model not listed here by
5
+ * editing the workspace `model` field directly. This list reflects the
6
+ * recommended set surfaced in the create-workspace selector.
7
+ *
8
+ * Auth caveat: `gpt-5.5` is currently only reachable when authenticated via
9
+ * ChatGPT (Plus/Pro/Team/Enterprise). API-key auth is limited to `gpt-5.4`
10
+ * and below.
11
+ */
12
+ export const CODEX_MODELS = [
13
+ {
14
+ id: 'auto',
15
+ label: 'Auto',
16
+ i18nLabelKey: 'model.auto',
17
+ i18nDescriptionKey: 'model.autoDescription',
18
+ },
19
+ {
20
+ id: 'gpt-5.5',
21
+ label: 'GPT-5.5',
22
+ i18nLabelKey: 'model.gpt55',
23
+ i18nDescriptionKey: 'model.gpt55Description',
24
+ },
25
+ {
26
+ id: 'gpt-5.4',
27
+ label: 'GPT-5.4',
28
+ i18nLabelKey: 'model.gpt54',
29
+ i18nDescriptionKey: 'model.gpt54Description',
30
+ },
31
+ {
32
+ id: 'gpt-5.4-mini',
33
+ label: 'GPT-5.4 mini',
34
+ i18nLabelKey: 'model.gpt54mini',
35
+ i18nDescriptionKey: 'model.gpt54miniDescription',
36
+ },
37
+ {
38
+ id: 'gpt-5.3-codex',
39
+ label: 'GPT-5.3 Codex',
40
+ i18nLabelKey: 'model.gpt53codex',
41
+ i18nDescriptionKey: 'model.gpt53codexDescription',
42
+ },
43
+ ];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Curated palette for per-project colours. Quasar colour keys at the `-5`
3
+ * intensity — picked to read well against Kōbō's dark theme. Append-only —
4
+ * existing values must never move or be removed (settings.json may reference
5
+ * them). New colours can be added at the end.
6
+ */
7
+ export const PROJECT_COLOR_PALETTE = [
8
+ 'red-5',
9
+ 'pink-5',
10
+ 'purple-5',
11
+ 'deep-purple-5',
12
+ 'indigo-5',
13
+ 'blue-5',
14
+ 'cyan-5',
15
+ 'teal-5',
16
+ 'green-5',
17
+ 'amber-5',
18
+ 'orange-5',
19
+ 'brown-5',
20
+ ];
21
+ export function isValidProjectColor(value) {
22
+ return typeof value === 'string' && PROJECT_COLOR_PALETTE.includes(value);
23
+ }
@@ -0,0 +1,66 @@
1
+ // src/shared/skill-suite-prompts.ts
2
+ export function isValidSkillSuite(value) {
3
+ return value === 'superpowers' || value === 'gstack' || value === 'custom';
4
+ }
5
+ // ── Agnostic prompt strings ───────────────────────────────────────────────────
6
+ // These are the neutral, suite-free versions used both:
7
+ // 1. As the seed for `custom` mode (so the user has a sane base to edit).
8
+ // 2. As building blocks for the backend full prompt sets in
9
+ // `src/server/services/skill-suite-prompts.ts`.
10
+ //
11
+ // They MUST NOT mention any specific skill suite by name.
12
+ const REVIEW_HEADER = `You are reviewing code changes on workspace "{{workspace_name}}" in project {{project_name}}.
13
+
14
+ Branch: {{branch_name}} (base: {{source_branch}})
15
+ Base commit: {{base_commit}}
16
+
17
+ `;
18
+ const REVIEW_BODY = `## Scope
19
+
20
+ Review ALL changes — both committed and uncommitted in the working tree:
21
+ - \`git diff {{base_commit}}..HEAD\` — committed changes on this branch
22
+ - \`git status\` and \`git diff\` — uncommitted changes (staged + unstaged)
23
+
24
+ ## Diff summary
25
+ {{diff_stats}}
26
+
27
+ ## Commits
28
+ {{commits}}
29
+
30
+ ## Additional instructions
31
+ {{additional_instructions}}
32
+
33
+ ## Output
34
+
35
+ If no review skill is available, structure your reply as:
36
+ 1. Summary — what changed and why
37
+ 2. Issues — bugs, regressions, security or perf concerns (with file:line)
38
+ 3. Suggestions — refactor / improvement opportunities
39
+ 4. Tests — coverage gaps
40
+ 5. Verdict — ship / fix-then-ship / blocked
41
+ `;
42
+ export const AGNOSTIC_REVIEW_TEMPLATE = REVIEW_HEADER +
43
+ 'If a code-review skill is available in this environment, invoke it to drive this audit. Otherwise follow the manual checklist below.\n\n' +
44
+ REVIEW_BODY;
45
+ export const AGNOSTIC_AUTO_LOOP_REVIEW_GATE = 'Code review gate — BEFORE marking the task done, run whichever code-review skill is configured in this environment. 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 no review skill is available, do a manual self-review against the same criteria.';
46
+ export const AGNOSTIC_AUTO_LOOP_GROOMING_INTRO = 'You are preparing this workspace for Kōbō auto-loop mode. This is a GROOMING session only — DO NOT implement anything, DO NOT write or edit code, DO NOT run tests or builds, DO NOT invoke any implementation, planning, or release skill (your environment may have several). Your ONLY job is to curate the Kōbō task list via MCP tools.';
47
+ export const AGNOSTIC_QA_PROMPT_TEMPLATE = 'QA pass for workspace "{{workspace_name}}" in project {{project_name}}.\n\nBranch: {{branch_name}}\nStaging URL: {{staging_url}}\n\nIf a QA skill is configured in this environment, invoke it on {{staging_url}}. Otherwise, manually exercise the staging URL against the workspace\'s acceptance criteria and report findings.';
48
+ // ── Grooming intro: superpowers + gstack variants ────────────────────────────
49
+ // Only the grooming intro is needed shared (both backend auto-loop-prompts and
50
+ // frontend kobo-commands use it). The review/review-gate/QA variants are
51
+ // backend-only — they live in src/server/services/skill-suite-prompts.ts.
52
+ export const GROOMING_INTRO_SUPERPOWERS = 'You are preparing this workspace for Kōbō auto-loop mode. This is a GROOMING session only — DO NOT implement anything, DO NOT write or edit code, DO NOT run tests or builds, DO NOT invoke `superpowers:executing-plans` or any implementation skill. Your ONLY job is to curate the Kōbō task list via MCP tools.';
53
+ export const GROOMING_INTRO_GSTACK = 'You are preparing this workspace for Kōbō auto-loop mode. This is a GROOMING session only — DO NOT implement anything, DO NOT write or edit code, DO NOT run tests or builds, DO NOT invoke `/ship`, `/autoplan`, `/land-and-deploy`, or any implementation skill. Your ONLY job is to curate the Kōbō task list via MCP tools.';
54
+ /**
55
+ * Resolve the grooming intro for the given suite. In `custom` mode, the
56
+ * user-provided override is used (or AGNOSTIC if the override is empty/blank).
57
+ */
58
+ export function getGroomingIntro(suite, customOverride) {
59
+ if (suite === 'custom') {
60
+ const trimmed = (customOverride ?? '').trim();
61
+ return trimmed || AGNOSTIC_AUTO_LOOP_GROOMING_INTRO;
62
+ }
63
+ if (suite === 'gstack')
64
+ return GROOMING_INTRO_GSTACK;
65
+ return GROOMING_INTRO_SUPERPOWERS;
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.7.7",
3
+ "version": "1.7.9",
4
4
  "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -71,6 +71,7 @@
71
71
  "@emnapi/runtime": "^1.10.0",
72
72
  "@hono/node-server": "^2.0.2",
73
73
  "@modelcontextprotocol/sdk": "^1.29.0",
74
+ "@openai/codex": "^0.130.0",
74
75
  "better-sqlite3": "^12.9.0",
75
76
  "cron-parser": "^5.5.0",
76
77
  "hono": "^4.12.18",