@loicngr/kobo 1.11.45 → 2.1.0

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 (176) hide show
  1. package/AGENTS.md +17 -2
  2. package/CHANGELOG.md +8 -0
  3. package/CODE_OF_CONDUCT.md +16 -0
  4. package/CONFIGURATION.md +1968 -0
  5. package/CONTRIBUTING.md +83 -0
  6. package/DESIGN.md +296 -0
  7. package/README.md +35 -7
  8. package/SECURITY.md +54 -0
  9. package/THIRD_PARTY_NOTICES.md +20 -0
  10. package/dist/server/db/migrations.js +23 -0
  11. package/dist/server/db/schema.js +2 -0
  12. package/dist/server/index.js +6 -0
  13. package/dist/server/routes/environment.js +18 -0
  14. package/dist/server/routes/health.js +17 -2
  15. package/dist/server/routes/integrations.js +29 -0
  16. package/dist/server/routes/settings.js +5 -0
  17. package/dist/server/routes/sounds.js +60 -0
  18. package/dist/server/routes/workspaces.js +46 -12
  19. package/dist/server/services/agent/engines/codex/options-builder.js +1 -0
  20. package/dist/server/services/agent/engines/codex/spawn.js +2 -2
  21. package/dist/server/services/agent/integration-mcp.js +49 -0
  22. package/dist/server/services/agent/orchestrator.js +6 -2
  23. package/dist/server/services/auto-loop-service.js +45 -25
  24. package/dist/server/services/custom-sound-service.js +0 -0
  25. package/dist/server/services/environment-check-service.js +125 -0
  26. package/dist/server/services/initial-prompt-template-service.js +8 -21
  27. package/dist/server/services/integration-config-service.js +65 -0
  28. package/dist/server/services/legacy-public-prompts.js +112 -0
  29. package/dist/server/services/notion-service.js +25 -61
  30. package/dist/server/services/sentry-service.js +17 -12
  31. package/dist/server/services/settings-defaults.js +5 -0
  32. package/dist/server/services/settings-service.js +112 -23
  33. package/dist/server/services/skill-suite-prompts.js +29 -22
  34. package/dist/server/services/templates-service.js +31 -8
  35. package/dist/server/services/terminal-service.js +16 -0
  36. package/dist/server/services/wakeup-service.js +7 -5
  37. package/dist/server/services/workspace-service.js +25 -4
  38. package/dist/server/services/worktree-service.js +3 -1
  39. package/dist/server/utils/git-repo-lock.js +7 -3
  40. package/dist/server/utils/mcp-client.js +26 -12
  41. package/dist/server/utils/mcp-process.js +25 -0
  42. package/dist/server/utils/node-pty-spawn-helper.js +31 -0
  43. package/dist/server/utils/paths.js +7 -0
  44. package/dist/shared/auto-loop-prompts.js +6 -2
  45. package/dist/shared/automatic-admission.js +1 -0
  46. package/dist/shared/environment.js +1 -0
  47. package/dist/shared/notification-assets.js +73 -0
  48. package/dist/shared/skill-suite-prompts.js +6 -3
  49. package/dist/shared/workflow-policy.js +25 -0
  50. package/docs/getting-started.md +47 -0
  51. package/docs/troubleshooting.md +49 -0
  52. package/package.json +19 -7
  53. package/src/client/dist/pwa/assets/ActivityFeed-DRgetl1M.js +8 -0
  54. package/src/client/dist/pwa/assets/{ChangelogPage-CnXTnFhe.js → ChangelogPage-BJdICFvz.js} +1 -1
  55. package/src/client/dist/pwa/assets/{ComparisonPanel-CKylN45q.js → ComparisonPanel-2GLd8KIy.js} +2 -2
  56. package/src/client/dist/pwa/assets/CreatePage-B9AdNJZj.js +2 -0
  57. package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +1 -0
  58. package/src/client/dist/pwa/assets/DashboardPage-CzVOvjSP.js +1 -0
  59. package/src/client/dist/pwa/assets/DiffViewer-X25vuf83.js +8 -0
  60. package/src/client/dist/pwa/assets/{DocumentsPanel-BeCttB-x.js → DocumentsPanel-BastjrEm.js} +1 -1
  61. package/src/client/dist/pwa/assets/{DrawerToggleButton-BJBBYeOc.js → DrawerToggleButton-DbipGmBL.js} +1 -1
  62. package/src/client/dist/pwa/assets/{GitPanel-BFCnyhE2.css → GitPanel-C4wOcZV2.css} +1 -1
  63. package/src/client/dist/pwa/assets/GitPanel-CnW4iVao.js +3 -0
  64. package/src/client/dist/pwa/assets/{HealthPage-l88TEk-k.js → HealthPage-aieNQly-.js} +1 -1
  65. package/src/client/dist/pwa/assets/MainLayout-Cc4XzCAy.css +1 -0
  66. package/src/client/dist/pwa/assets/MainLayout-DqBbE8o4.js +2 -0
  67. package/src/client/dist/pwa/assets/{QBtnDropdown-44Mj-y_1.js → QBtnDropdown-BWy-rh45.js} +1 -1
  68. package/src/client/dist/pwa/assets/{QExpansionItem-Bok1m8Wi.js → QExpansionItem-DfmdBGuW.js} +1 -1
  69. package/src/client/dist/pwa/assets/QSelect-BfqaWakg.js +1 -0
  70. package/src/client/dist/pwa/assets/{QTooltip-BDihn2aY.js → QTooltip-Cy0T0vk2.js} +1 -1
  71. package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +1 -0
  72. package/src/client/dist/pwa/assets/SchedulePanel-YQPObRR3.js +1 -0
  73. package/src/client/dist/pwa/assets/{SearchPage-BwZ2TX1G.js → SearchPage-BoBjmItc.js} +1 -1
  74. package/src/client/dist/pwa/assets/SettingsPage-0GtDcXRC.js +9 -0
  75. package/src/client/dist/pwa/assets/{SettingsPage-W6N01tGz.css → SettingsPage-BYdLXJEJ.css} +1 -1
  76. package/src/client/dist/pwa/assets/SplitWorkspacePage-4wFh65Lh.js +1 -0
  77. package/src/client/dist/pwa/assets/{TerminalPanel-Cq-FR7A7.js → TerminalPanel-b70x_jqH.js} +4 -4
  78. package/src/client/dist/pwa/assets/{TouchPan-BQ1cvzRC.js → TouchPan-CcTVcxwd.js} +1 -1
  79. package/src/client/dist/pwa/assets/{TourReplayButton-Dz1yEz_z.js → TourReplayButton-CpEMnQS8.js} +1 -1
  80. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-C5lTHTp2.js +1 -0
  81. package/src/client/dist/pwa/assets/{WorkspacePage-BbUvR1Vf.css → WorkspacePage--5HsnMQ1.css} +1 -1
  82. package/src/client/dist/pwa/assets/WorkspacePage-D_V9gNON.js +4 -0
  83. package/src/client/dist/pwa/assets/{browser-BmWgBx9M.js → browser-LBbxVwWf.js} +1 -1
  84. package/src/client/dist/pwa/assets/{build-path-tree-6EjBmYYY.js → build-path-tree-DyvtWxb0.js} +1 -1
  85. package/src/client/dist/pwa/assets/custom-sounds-BpeLZUDW.js +1 -0
  86. package/src/client/dist/pwa/assets/de-v6i2HlE2.js +17 -0
  87. package/src/client/dist/pwa/assets/{documents-BZ8bwcjr.js → documents-Bjx8lz-h.js} +1 -1
  88. package/src/client/dist/pwa/assets/{editor.api-CK7--ckY.js → editor.api-BC3PbJkx.js} +1 -1
  89. package/src/client/dist/pwa/assets/efforts-BDHAnNUs.js +9 -0
  90. package/src/client/dist/pwa/assets/engineFeatures-hlEiwqt-.js +1 -0
  91. package/src/client/dist/pwa/assets/es-Bc1KgkAT.js +17 -0
  92. package/src/client/dist/pwa/assets/{expand-template-C8VyjASo.js → expand-template-DLDl1qPx.js} +1 -1
  93. package/src/client/dist/pwa/assets/{format-LtTLLitJ.js → format-Q94deO3I.js} +7 -7
  94. package/src/client/dist/pwa/assets/{formatters-Bl9zA7Ai.js → formatters-CWxRHuUu.js} +1 -1
  95. package/src/client/dist/pwa/assets/fr-ByYPK5vr.js +17 -0
  96. package/src/client/dist/pwa/assets/{freemarker2-DxezMaje.js → freemarker2-MPhk0eTh.js} +1 -1
  97. package/src/client/dist/pwa/assets/{handlebars-DK_Bj6Rx.js → handlebars-C60ru5gR.js} +1 -1
  98. package/src/client/dist/pwa/assets/{html-DFfsfStD.js → html-ClzchR9j.js} +1 -1
  99. package/src/client/dist/pwa/assets/i18n-Sy42tm-V.js +1 -0
  100. package/src/client/dist/pwa/assets/index-CqZ7sDcM.js +20 -0
  101. package/src/client/dist/pwa/assets/it-BYF_88z6.js +17 -0
  102. package/src/client/dist/pwa/assets/{javascript-DuNbcaL7.js → javascript-C43UJmMx.js} +1 -1
  103. package/src/client/dist/pwa/assets/{liquid-CM3JKyhv.js → liquid-CYTrvTKF.js} +1 -1
  104. package/src/client/dist/pwa/assets/{mdx-BG3zYwNF.js → mdx-CxsXOvkQ.js} +1 -1
  105. package/src/client/dist/pwa/assets/{monaco-contributions-DLscZGH-.js → monaco-contributions-Cl4-iVpK.js} +1 -1
  106. package/src/client/dist/pwa/assets/{monaco.contribution-DnhZHwXT.js → monaco.contribution-DkjLUmyA.js} +2 -2
  107. package/src/client/dist/pwa/assets/network-auth-CXQn2wO7.js +1 -0
  108. package/src/client/dist/pwa/assets/notify-theme-BrueBr2j.js +1 -0
  109. package/src/client/dist/pwa/assets/{pending-diff-open-dYIFdS4r.js → pending-diff-open-B4POItbR.js} +1 -1
  110. package/src/client/dist/pwa/assets/permissionModes-B-ciuUbz.js +36 -0
  111. package/src/client/dist/pwa/assets/{project-color-R_1je4vW.js → project-color-CM3hflLH.js} +1 -1
  112. package/src/client/dist/pwa/assets/{python-COs6pHk2.js → python-s2IMpsoV.js} +1 -1
  113. package/src/client/dist/pwa/assets/{razor-Jl1VqMsk.js → razor-DVNXiVBh.js} +1 -1
  114. package/src/client/dist/pwa/assets/render-chat-markdown-TU6CDfD8.js +69 -0
  115. package/src/client/dist/pwa/assets/{typescript-BU_TUuAM.js → typescript-BYT_8ABE.js} +1 -1
  116. package/src/client/dist/pwa/assets/use-tours-CNe8qb1q.js +2 -0
  117. package/src/client/dist/pwa/assets/workspace-templates-tBxXE52A.js +1 -0
  118. package/src/client/dist/pwa/assets/{xml-gFFsobxE.js → xml-BIG-fmxk.js} +1 -1
  119. package/src/client/dist/pwa/assets/{yaml-DaXaqaGD.js → yaml-D7gu2CQh.js} +1 -1
  120. package/src/client/dist/pwa/index.html +1 -1
  121. package/src/client/dist/pwa/sounds/neutral.wav +0 -0
  122. package/src/client/dist/pwa/sounds/ready.wav +0 -0
  123. package/src/client/dist/pwa/sw.js +1 -1
  124. package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +0 -8
  125. package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +0 -2
  126. package/src/client/dist/pwa/assets/CreatePage-TY9g3hZb.css +0 -1
  127. package/src/client/dist/pwa/assets/DashboardPage-CZQLfoV1.js +0 -1
  128. package/src/client/dist/pwa/assets/DiffViewer-Csf3WmLc.js +0 -8
  129. package/src/client/dist/pwa/assets/GitPanel-B-ZHgghQ.js +0 -3
  130. package/src/client/dist/pwa/assets/MainLayout-BAAQLq95.css +0 -1
  131. package/src/client/dist/pwa/assets/MainLayout-Cf4Q9x1u.js +0 -2
  132. package/src/client/dist/pwa/assets/QDrawer-B4qHw8wt.js +0 -1
  133. package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +0 -1
  134. package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +0 -1
  135. package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +0 -1
  136. package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +0 -9
  137. package/src/client/dist/pwa/assets/SplitWorkspacePage-Bg6nKBcA.js +0 -1
  138. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BCVOP8vp.js +0 -1
  139. package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +0 -4
  140. package/src/client/dist/pwa/assets/de-COObokpu.js +0 -17
  141. package/src/client/dist/pwa/assets/efforts-Bw0o2Bud.js +0 -9
  142. package/src/client/dist/pwa/assets/engineFeatures-DQO9HHXI.js +0 -1
  143. package/src/client/dist/pwa/assets/es-By2Q9Tca.js +0 -17
  144. package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
  145. package/src/client/dist/pwa/assets/fr-D-sd0ddn.js +0 -17
  146. package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +0 -1
  147. package/src/client/dist/pwa/assets/index-BLd8DAzg.js +0 -20
  148. package/src/client/dist/pwa/assets/it-CeK5lyIA.js +0 -17
  149. package/src/client/dist/pwa/assets/network-auth-BJiPbO5l.js +0 -1
  150. package/src/client/dist/pwa/assets/notify-theme-D4p3qPkp.js +0 -1
  151. package/src/client/dist/pwa/assets/permissionModes-B7fuUegU.js +0 -36
  152. package/src/client/dist/pwa/assets/render-chat-markdown-AaOo0Tgq.js +0 -67
  153. package/src/client/dist/pwa/assets/use-tours-CVn874kD.js +0 -2
  154. package/src/client/dist/pwa/assets/workspace-templates-BCKR8TXr.js +0 -1
  155. package/src/client/dist/pwa/sounds/7eme-compagnie-03.mp3 +0 -0
  156. package/src/client/dist/pwa/sounds/aller-ftg.mp3 +0 -0
  157. package/src/client/dist/pwa/sounds/arrete-de-mentir.mp3 +0 -0
  158. package/src/client/dist/pwa/sounds/arretez-les-messages.mp3 +0 -0
  159. package/src/client/dist/pwa/sounds/bah-alors-on-est-nul.mp3 +0 -0
  160. package/src/client/dist/pwa/sounds/basic-notification.mp3 +0 -0
  161. package/src/client/dist/pwa/sounds/ca_va_peter.mp3 +0 -0
  162. package/src/client/dist/pwa/sounds/dry-fart.mp3 +0 -0
  163. package/src/client/dist/pwa/sounds/faaah.mp3 +0 -0
  164. package/src/client/dist/pwa/sounds/for-shure.mp3 +0 -0
  165. package/src/client/dist/pwa/sounds/gta-v-death.mp3 +0 -0
  166. package/src/client/dist/pwa/sounds/hey.mp3 +0 -0
  167. package/src/client/dist/pwa/sounds/mais-laisse-moi-dormir-zebi.mp3 +0 -0
  168. package/src/client/dist/pwa/sounds/nan-tu-degages.mp3 +0 -0
  169. package/src/client/dist/pwa/sounds/nan-wallah-pardon.mp3 +0 -0
  170. package/src/client/dist/pwa/sounds/ouais-cest-greg.mp3 +0 -0
  171. package/src/client/dist/pwa/sounds/pas-ca-zinedine.mp3 +0 -0
  172. package/src/client/dist/pwa/sounds/ta-gueule.mp3 +0 -0
  173. package/src/client/dist/pwa/sounds/travail_termine.mp3 +0 -0
  174. package/src/client/dist/pwa/sounds/tu-vas-la-fermer.mp3 +0 -0
  175. package/src/client/dist/pwa/sounds/warcraft-3-humain-travail.mp3 +0 -0
  176. package/src/client/dist/pwa/sounds/yaaa.mp3 +0 -0
@@ -1,5 +1,6 @@
1
1
  import { nanoid } from 'nanoid';
2
2
  import { parseTaskVerification } from '../../shared/task-verification.js';
3
+ import { isWorkflowPolicy, LEGACY_WORKFLOW_POLICY, resolveWorkflowPolicy, } from '../../shared/workflow-policy.js';
3
4
  import { getDb } from '../db/index.js';
4
5
  import { isValidBranchName } from '../utils/git-ops.js';
5
6
  import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
@@ -54,7 +55,17 @@ function coerceAgentPermissionMode(raw) {
54
55
  return 'bypass';
55
56
  }
56
57
  function mapWorkspace(row) {
58
+ let workflowPolicy = { ...LEGACY_WORKFLOW_POLICY };
59
+ if (row.workflow_policy) {
60
+ try {
61
+ workflowPolicy = resolveWorkflowPolicy(JSON.parse(row.workflow_policy));
62
+ }
63
+ catch {
64
+ workflowPolicy = resolveWorkflowPolicy();
65
+ }
66
+ }
57
67
  return {
68
+ workflowPolicy,
58
69
  id: row.id,
59
70
  name: row.name,
60
71
  projectPath: row.project_path,
@@ -128,6 +139,9 @@ export function createWorkspace(data) {
128
139
  const id = nanoid();
129
140
  const computedWorktreePath = data.worktreePath ?? resolveWorkspaceWorktreePath(data.projectPath, data.workingBranch, data.worktreesPath);
130
141
  const owned = data.worktreeOwned ?? true;
142
+ if (data.workflowPolicy !== undefined && !isWorkflowPolicy(data.workflowPolicy))
143
+ throw new Error('Invalid workflowPolicy');
144
+ const workflowPolicy = resolveWorkflowPolicy(data.workflowPolicy);
131
145
  // Mirror the unified mode into the legacy columns so older readers (in-flight
132
146
  // requests during deploy, external scripts) still see a sane value.
133
147
  const unifiedMode = data.agentPermissionMode ?? 'bypass';
@@ -137,12 +151,12 @@ export function createWorkspace(data) {
137
151
  INSERT INTO workspaces (
138
152
  id, name, project_path, source_branch, working_branch, status,
139
153
  notion_url, notion_page_id, sentry_url, pr_url, worktree_path, worktree_owned,
140
- model, brainstorm_model, reasoning_effort, permission_mode, permission_profile, agent_permission_mode, engine, comparison_id, created_at, updated_at
141
- ) VALUES (?, ?, ?, ?, ?, 'created', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
154
+ model, brainstorm_model, reasoning_effort, permission_mode, permission_profile, agent_permission_mode, engine, comparison_id, created_at, updated_at, workflow_policy
155
+ ) VALUES (?, ?, ?, ?, ?, 'created', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
142
156
  `).run(id, data.name, data.projectPath, data.sourceBranch, data.workingBranch, data.notionUrl ?? null, data.notionPageId ?? null, data.sentryUrl ?? null, data.prUrl ?? null, computedWorktreePath, owned ? 1 : 0, data.model ?? 'claude-opus-4-8', data.brainstormModel ?? null, data.reasoningEffort ?? 'auto', legacyMode, legacyProfile, unifiedMode, data.engine ?? 'claude-code',
143
157
  // Empty string is not a group: it would silently gather every workspace
144
158
  // created with a blank id into one comparison.
145
- data.comparisonId?.trim() ? data.comparisonId : null, now, now);
159
+ data.comparisonId?.trim() ? data.comparisonId : null, now, now, JSON.stringify(workflowPolicy));
146
160
  return getWorkspace(id);
147
161
  }
148
162
  /** Fetch a single workspace by ID, or null if not found. */
@@ -162,7 +176,8 @@ export function listComparisonMembers(comparisonId) {
162
176
  if (!comparisonId.trim())
163
177
  return [];
164
178
  const rows = getDb()
165
- .prepare('SELECT * FROM workspaces WHERE comparison_id = ? ORDER BY created_at, id')
179
+ // rowid breaks created_at ties in insertion order; the nanoid id is random.
180
+ .prepare('SELECT * FROM workspaces WHERE comparison_id = ? ORDER BY created_at, rowid')
166
181
  .all(comparisonId);
167
182
  return rows.map(mapWorkspace);
168
183
  }
@@ -389,6 +404,12 @@ export function updateWorkspaceFields(id, fields) {
389
404
  throw new Error(`Workspace '${id}' not found`);
390
405
  const assignments = [];
391
406
  const values = [];
407
+ if (fields.workflowPolicy !== undefined) {
408
+ if (!isWorkflowPolicy(fields.workflowPolicy))
409
+ throw new Error('Invalid workflowPolicy');
410
+ assignments.push('workflow_policy = ?');
411
+ values.push(JSON.stringify(resolveWorkflowPolicy(workspace.workflowPolicy, fields.workflowPolicy)));
412
+ }
392
413
  if (fields.model !== undefined) {
393
414
  if (typeof fields.model !== 'string' || !fields.model.trim())
394
415
  throw new Error('model must be a non-empty string');
@@ -450,8 +450,10 @@ export async function restoreWorktreeCheckoutUnlocked(input) {
450
450
  };
451
451
  }
452
452
  const registrations = await restoreRegistrations(projectPath);
453
+ // The destination is missing, so resolve its parent: git registers resolved paths.
454
+ const canonicalTarget = path.join(await canonicalize(path.dirname(worktreePath)), path.basename(worktreePath));
453
455
  // Do not globally prune: another missing checkout may still need its registration.
454
- if (registrations.some((entry) => path.resolve(entry.path) === worktreePath)) {
456
+ if (registrations.some((entry) => path.resolve(entry.path) === canonicalTarget)) {
455
457
  throw new WorktreeCheckoutError('path-conflict', 'Git still registers the missing destination. Repair that worktree registration before retrying.');
456
458
  }
457
459
  if (registrations.some((entry) => entry.branch === `refs/heads/${workingBranch}`)) {
@@ -1,4 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { realpath } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { promisify } from 'node:util';
4
5
  /**
@@ -14,7 +15,10 @@ const execFileAsync = promisify(execFile);
14
15
  // out of order. This queue covers resolution only, never the locked operation.
15
16
  let registrations = Promise.resolve();
16
17
  /**
17
- * Absolute path of the git directory shared by every worktree of a repository.
18
+ * Absolute, symlink-resolved path of the git directory shared by every worktree
19
+ * of a repository. Git reports worktree paths resolved, so an unresolved key
20
+ * would split one repository into two locks when it is reached through a link
21
+ * (e.g. macOS temp dirs under /var -> /private/var).
18
22
  *
19
23
  * Falls back to the resolved input path when git cannot answer (not a
20
24
  * repository, git missing): a wrong key only costs parallelism, never
@@ -22,14 +26,14 @@ let registrations = Promise.resolve();
22
26
  */
23
27
  export async function resolveGitCommonDir(repoPath) {
24
28
  try {
25
- const { stdout } = await execFileAsync('git', ['rev-parse', '--git-common-dir'], {
29
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
26
30
  cwd: repoPath,
27
31
  encoding: 'utf-8',
28
32
  timeout: 10_000,
29
33
  killSignal: 'SIGKILL',
30
34
  env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
31
35
  });
32
- return path.resolve(repoPath, stdout.replace(/\n$/, ''));
36
+ return await realpath(stdout.replace(/\n$/, ''));
33
37
  }
34
38
  catch {
35
39
  return path.resolve(repoPath);
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
+ import { stopMcpProcess } from './mcp-process.js';
3
4
  import { getPackageVersion } from './paths.js';
4
5
  function getClaudeConfigPath() {
5
6
  const homedir = process.env.HOME ?? process.env.USERPROFILE ?? '';
@@ -52,7 +53,7 @@ const nextRpcId = (() => {
52
53
  /**
53
54
  * Spawn an MCP server process given explicit command, args, and env.
54
55
  * The caller is responsible for constructing the full env (including auth headers).
55
- * stderr is consumed silently; set DEBUG_MCP_STDERR=1 to print it.
56
+ * stderr content is never logged: integrations may print credentials. Debug mode reports byte counts only.
56
57
  */
57
58
  export function spawnMcpProcess(command, args, env) {
58
59
  const mcpProcess = spawn(command, args, {
@@ -67,12 +68,12 @@ export function spawnMcpProcess(command, args, env) {
67
68
  // without any listener crashes the whole Node process (Unhandled 'error').
68
69
  // Callers may attach their own listener for typed handling — listeners are
69
70
  // additive on EventEmitter so this default never blocks them.
70
- mcpProcess.on('error', (err) => {
71
- console.error(`[mcp] spawn '${command}' failed: ${err.message}`);
71
+ mcpProcess.on('error', () => {
72
+ console.error('[mcp] Failed to start configured server; check executable and connection settings');
72
73
  });
73
74
  mcpProcess.stderr?.on('data', (data) => {
74
75
  if (process.env.DEBUG_MCP_STDERR) {
75
- console.error('[mcp stderr]', data.toString());
76
+ console.error('[mcp] stderr received (content withheld)', data.length);
76
77
  }
77
78
  });
78
79
  return mcpProcess;
@@ -101,7 +102,7 @@ export async function initializeMcp(mcpProcess) {
101
102
  const initTimeoutMs = Number(process.env.KOBO_MCP_INIT_TIMEOUT_MS) || 30_000;
102
103
  const timeout = setTimeout(() => {
103
104
  mcpProcess.stdout?.removeListener('data', onData);
104
- mcpProcess.kill();
105
+ stopMcpProcess(mcpProcess);
105
106
  reject(new Error(`initializeMcp timed out after ${initTimeoutMs}ms`));
106
107
  }, initTimeoutMs);
107
108
  const onData = (chunk) => {
@@ -117,6 +118,10 @@ export async function initializeMcp(mcpProcess) {
117
118
  if (parsed.id === id) {
118
119
  clearTimeout(timeout);
119
120
  mcpProcess.stdout?.removeListener('data', onData);
121
+ if (parsed.error) {
122
+ reject(new Error('MCP initialization failed; check integration configuration'));
123
+ return;
124
+ }
120
125
  const initialized = JSON.stringify({
121
126
  jsonrpc: '2.0',
122
127
  method: 'notifications/initialized',
@@ -130,10 +135,10 @@ export async function initializeMcp(mcpProcess) {
130
135
  }
131
136
  }
132
137
  };
133
- const onError = (err) => {
138
+ const onError = () => {
134
139
  clearTimeout(timeout);
135
140
  mcpProcess.stdout?.removeListener('data', onData);
136
- reject(err);
141
+ reject(new Error('MCP transport failed'));
137
142
  };
138
143
  mcpProcess.stdout.on('data', onData);
139
144
  mcpProcess.stdout.once('error', onError);
@@ -158,7 +163,7 @@ export async function callMcpTool(mcpProcess, toolName, args) {
158
163
  const timeout = setTimeout(() => {
159
164
  mcpProcess.stdout?.removeListener('data', onData);
160
165
  mcpProcess.stdout?.removeListener('error', onError);
161
- mcpProcess.kill();
166
+ stopMcpProcess(mcpProcess);
162
167
  reject(new Error(`callMcpTool('${toolName}') timed out after 30s`));
163
168
  }, 30_000);
164
169
  const onData = (chunk) => {
@@ -176,10 +181,17 @@ export async function callMcpTool(mcpProcess, toolName, args) {
176
181
  mcpProcess.stdout?.removeListener('data', onData);
177
182
  mcpProcess.stdout?.removeListener('error', onError);
178
183
  if (parsed.error) {
179
- reject(new Error(`MCP tool '${toolName}' error: ${parsed.error.message} (code: ${parsed.error.code})`));
184
+ reject(new Error(`MCP tool failed (code: ${Number.isInteger(parsed.error.code) ? parsed.error.code : 'unknown'})`));
180
185
  }
181
186
  else {
182
- resolve(parsed.result);
187
+ if (parsed.result &&
188
+ typeof parsed.result === 'object' &&
189
+ 'isError' in parsed.result &&
190
+ parsed.result.isError === true) {
191
+ reject(new Error('MCP tool reported a failure; check integration credentials and access'));
192
+ }
193
+ else
194
+ resolve(parsed.result);
183
195
  }
184
196
  }
185
197
  }
@@ -188,10 +200,10 @@ export async function callMcpTool(mcpProcess, toolName, args) {
188
200
  }
189
201
  }
190
202
  };
191
- const onError = (err) => {
203
+ const onError = () => {
192
204
  clearTimeout(timeout);
193
205
  mcpProcess.stdout?.removeListener('data', onData);
194
- reject(err);
206
+ reject(new Error('MCP transport failed'));
195
207
  };
196
208
  mcpProcess.stdout.on('data', onData);
197
209
  mcpProcess.stdout.once('error', onError);
@@ -206,6 +218,8 @@ export async function callMcpTool(mcpProcess, toolName, args) {
206
218
  export function unwrapMcpResult(result) {
207
219
  if (result && typeof result === 'object') {
208
220
  const obj = result;
221
+ if (obj.isError === true)
222
+ throw new Error('MCP tool reported a failure; check integration credentials and access');
209
223
  if (Array.isArray(obj.content)) {
210
224
  const first = obj.content[0];
211
225
  if (first?.type === 'text' && first.text) {
@@ -0,0 +1,25 @@
1
+ /** MCP commands run in their own process group, including wrapper descendants. */
2
+ export function stopMcpProcess(child) {
3
+ child.stdin?.end();
4
+ const pid = child.pid;
5
+ const signal = (value) => {
6
+ try {
7
+ if (pid && process.platform !== 'win32')
8
+ process.kill(-pid, value);
9
+ else
10
+ child.kill(value);
11
+ }
12
+ catch {
13
+ try {
14
+ child.kill(value);
15
+ }
16
+ catch {
17
+ /* Already gone. */
18
+ }
19
+ }
20
+ };
21
+ signal('SIGTERM');
22
+ // Keep the group cleanup even when a wrapper exits before its descendants.
23
+ const force = setTimeout(() => signal('SIGKILL'), 1000);
24
+ force.unref();
25
+ }
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ function nodePtyRoot() {
5
+ return path.dirname(createRequire(import.meta.url).resolve('node-pty/package.json'));
6
+ }
7
+ /**
8
+ * node-pty 1.1.0 publishes its macOS `spawn-helper` prebuilds without the
9
+ * execute bit, so every spawn fails with `posix_spawnp failed` (a blank
10
+ * terminal in Kōbō). Restore the bit at runtime rather than in an install
11
+ * script, which `--ignore-scripts` installs would skip.
12
+ *
13
+ * Returns the helper it made executable, or `null` when there is nothing to do.
14
+ * Throws when the helper cannot be changed (e.g. a root-owned global install).
15
+ */
16
+ export function ensureSpawnHelperExecutable({ platform = process.platform, arch = process.arch, root, } = {}) {
17
+ if (platform !== 'darwin')
18
+ return null;
19
+ const helper = path.join(root ?? nodePtyRoot(), 'prebuilds', `darwin-${arch}`, 'spawn-helper');
20
+ let mode;
21
+ try {
22
+ mode = fs.statSync(helper).mode;
23
+ }
24
+ catch {
25
+ return null; // Built from source: node-gyp output is already executable.
26
+ }
27
+ if ((mode & 0o111) !== 0)
28
+ return null;
29
+ fs.chmodSync(helper, (mode & 0o777) | 0o111);
30
+ return helper;
31
+ }
@@ -101,6 +101,13 @@ export function getTemplatesPath() {
101
101
  export function getWorkspaceTemplatesPath() {
102
102
  return path.join(getKoboHome(), 'workspace-templates.json');
103
103
  }
104
+ /**
105
+ * Directory holding the user's imported notification sounds and their manifest.
106
+ * Separate from the bundled tones shipped inside the SPA under `/sounds`.
107
+ */
108
+ export function getCustomSoundsDir() {
109
+ return path.join(getKoboHome(), 'sounds');
110
+ }
104
111
  /**
105
112
  * Absolute path to the compiled MCP server entry (shipped in the published
106
113
  * package as dist/mcp-server/kobo-tasks-server.js). Returns null if not
@@ -48,7 +48,9 @@ export function buildAutoLoopGroomingSteps(e2e, finalization) {
48
48
  ];
49
49
  let nextNum = 4;
50
50
  if (e2e.framework) {
51
- const skillHint = e2e.skill ? `Use the \`${e2e.skill}\` skill for this task. ` : '';
51
+ const skillHint = e2e.skill
52
+ ? `Use the \`${e2e.skill}\` skill for this task if available; otherwise use the existing project test tools directly. `
53
+ : '';
52
54
  const promptHint = e2e.prompt ? `Additional guidance: ${e2e.prompt}` : '';
53
55
  steps.push(`${nextNum}. **E2E review**: walk the task list and identify which tasks produce user-visible behavior (UI flows, form submissions, page renders, etc.). For each one that warrants regression coverage, INSERT a follow-up sub-task with title prefixed \`[E2E] \` describing the test to write. Use \`after_task_id\` to insert it directly after the parent task; \`list_tasks\` exposes \`sort_order\` so you can verify ordering. Skip tasks that don't produce user-visible behavior (refactors, infra, internal services) and briefly justify your choices in chat. The project uses \`${e2e.framework}\`. ${skillHint}${promptHint}`.trim());
54
56
  nextNum++;
@@ -84,7 +86,9 @@ Read kobo__list_tasks again. If any work or criterion remains open, or any requi
84
86
  export function buildE2eIterationBlock(e2e) {
85
87
  if (!e2e.framework)
86
88
  return '';
87
- const skillLine = e2e.skill ? `Use the \`${e2e.skill}\` skill for this task.\n` : '';
89
+ const skillLine = e2e.skill
90
+ ? `Use the \`${e2e.skill}\` skill for this task if available; otherwise use the existing project test tools directly.\n`
91
+ : '';
88
92
  const promptLine = e2e.prompt ? `Additional guidance: ${e2e.prompt}\n` : '';
89
93
  return `
90
94
  This is an **E2E regression test** task.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ /** Only original, reproducible sounds are included in the public distribution. */
2
+ export const PUBLIC_NOTIFICATION_SOUNDS = ['neutral.wav', 'ready.wav'];
3
+ export const DEFAULT_PUBLIC_NOTIFICATION_SOUND = PUBLIC_NOTIFICATION_SOUNDS[0];
4
+ /**
5
+ * User-imported sounds live in `KOBO_HOME/sounds/` and are referenced as
6
+ * `custom:<id>`. The id is generated server-side, so a reference never carries
7
+ * a user-supplied filename and can never become a path segment on its own.
8
+ */
9
+ export const CUSTOM_SOUND_PREFIX = 'custom:';
10
+ export const CUSTOM_SOUND_ID_LENGTH = 12;
11
+ const CUSTOM_SOUND_ID_PATTERN = new RegExp(`^[A-Za-z0-9_-]{${CUSTOM_SOUND_ID_LENGTH}}$`);
12
+ export const CUSTOM_SOUND_EXTENSIONS = ['.wav', '.mp3', '.ogg', '.webm'];
13
+ /** Formats every browser Kōbō targets can decode. */
14
+ export const CUSTOM_SOUND_CONTENT_TYPES = {
15
+ '.wav': 'audio/wav',
16
+ '.mp3': 'audio/mpeg',
17
+ '.ogg': 'audio/ogg',
18
+ '.webm': 'audio/webm',
19
+ };
20
+ export const CUSTOM_SOUND_ACCEPT = [...Object.values(CUSTOM_SOUND_CONTENT_TYPES), ...CUSTOM_SOUND_EXTENSIONS].join(',');
21
+ /** A notification is short: the largest removed bundled sound was 207 KB. */
22
+ export const MAX_CUSTOM_SOUND_BYTES = 2 * 1024 * 1024;
23
+ export const MAX_CUSTOM_SOUNDS = 20;
24
+ export function customSoundReference(id) {
25
+ return `${CUSTOM_SOUND_PREFIX}${id}`;
26
+ }
27
+ export function isCustomNotificationSound(value) {
28
+ return (typeof value === 'string' &&
29
+ value.startsWith(CUSTOM_SOUND_PREFIX) &&
30
+ CUSTOM_SOUND_ID_PATTERN.test(value.slice(CUSTOM_SOUND_PREFIX.length)));
31
+ }
32
+ /** Returns the bare id of a well-formed reference, or an empty string. */
33
+ export function customSoundId(value) {
34
+ return isCustomNotificationSound(value) ? value.slice(CUSTOM_SOUND_PREFIX.length) : '';
35
+ }
36
+ /** Browsers label audio MIME types inconsistently, so the extension decides. */
37
+ export function customSoundExtension(name) {
38
+ const extension = /\.[^.\\/]+$/.exec(name)?.[0]?.toLowerCase() ?? '';
39
+ return CUSTOM_SOUND_EXTENSIONS.includes(extension) ? extension : null;
40
+ }
41
+ /**
42
+ * A custom reference is kept on its shape alone: settings migrations run before
43
+ * the catalogue is readable, and both the client and the file route already fall
44
+ * back to the bundled tone when the file is gone.
45
+ */
46
+ export function normalizePublicNotificationSound(value, allowSentinel = true) {
47
+ if (allowSentinel && (value === 'inherit' || value === 'none'))
48
+ return value;
49
+ if (isCustomNotificationSound(value))
50
+ return value;
51
+ return typeof value === 'string' && PUBLIC_NOTIFICATION_SOUNDS.includes(value)
52
+ ? value
53
+ : DEFAULT_PUBLIC_NOTIFICATION_SOUND;
54
+ }
55
+ export const NOTIFICATION_SOUND_KEYS = [
56
+ 'audioNotificationSound',
57
+ 'audioQuestionSound',
58
+ 'audioWorkspaceCreatedSound',
59
+ 'audioAgentErrorSound',
60
+ 'audioPrCiFailedSound',
61
+ 'audioPrCiRecoveredSound',
62
+ 'audioPrChangesRequestedSound',
63
+ 'audioPrApprovedSound',
64
+ 'audioPrMergeConflictSound',
65
+ 'audioPrReadyToMergeSound',
66
+ 'audioPrMergedSound',
67
+ ];
68
+ export function normalizePublicSounds(settings) {
69
+ for (const key of NOTIFICATION_SOUND_KEYS) {
70
+ if (key in settings)
71
+ settings[key] = normalizePublicNotificationSound(settings[key], key !== 'audioNotificationSound');
72
+ }
73
+ }
@@ -1,6 +1,7 @@
1
1
  // src/shared/skill-suite-prompts.ts
2
2
  export function isValidSkillSuite(value) {
3
- return (value === 'superpowers' ||
3
+ return (value === 'standard' ||
4
+ value === 'superpowers' ||
4
5
  value === 'gstack' ||
5
6
  value === 'ecc' ||
6
7
  value === 'superpowers+gstack' ||
@@ -24,7 +25,7 @@ const REVIEW_BODY = `## Scope
24
25
 
25
26
  Review ALL changes — both committed and uncommitted in the working tree:
26
27
  - \`git diff {{base_commit}}..HEAD\` — committed changes on this branch
27
- - \`git status\` and \`git diff\` — uncommitted changes (staged + unstaged)
28
+ - \`git status\` and \`git diff\` / \`git diff --cached\` — unstaged / staged changes; inspect untracked files too
28
29
 
29
30
  ## Diff summary
30
31
  {{diff_stats}}
@@ -47,7 +48,7 @@ If no review skill is available, structure your reply as:
47
48
  export const AGNOSTIC_REVIEW_TEMPLATE = REVIEW_HEADER +
48
49
  'If a code-review skill is available in this environment, invoke it to drive this audit. Otherwise follow the manual checklist below.\n\n' +
49
50
  REVIEW_BODY;
50
- 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.';
51
+ 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 all committed and uncommitted changes are correct. Include staged, unstaged and untracked files. If no review skill is available, do a manual self-review against the same criteria.';
51
52
  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.';
52
53
  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.';
53
54
  export const AGNOSTIC_BRAINSTORMING_INSTRUCTION = 'Brainstorm the implementation approach. Explore the codebase to understand the existing structure. Ask clarifying questions if needed. When you have a clear, user-approved plan, save it as a plan file in the project.';
@@ -65,6 +66,8 @@ export const GROOMING_INTRO_ALL = 'You are preparing this workspace for Kōbō a
65
66
  * user-provided override is used (or AGNOSTIC if the override is empty/blank).
66
67
  */
67
68
  export function getGroomingIntro(suite, customOverride) {
69
+ if (suite === 'standard')
70
+ return AGNOSTIC_AUTO_LOOP_GROOMING_INTRO;
68
71
  if (suite === 'custom') {
69
72
  const trimmed = (customOverride ?? '').trim();
70
73
  return trimmed || AGNOSTIC_AUTO_LOOP_GROOMING_INTRO;
@@ -0,0 +1,25 @@
1
+ export const WORKFLOW_ACTIONS = ['commit', 'push', 'publish'];
2
+ export const MANUAL_WORKFLOW_POLICY = { commit: 'manual', push: 'manual', publish: 'manual' };
3
+ /** Existing installations keep their previous autonomous workflow; explicit user constraints still win. */
4
+ export const LEGACY_WORKFLOW_POLICY = {
5
+ commit: 'automatic',
6
+ push: 'automatic',
7
+ publish: 'automatic',
8
+ };
9
+ export function isWorkflowPolicy(value) {
10
+ return (typeof value === 'object' &&
11
+ value !== null &&
12
+ !Array.isArray(value) &&
13
+ Object.entries(value).every(([key, mode]) => WORKFLOW_ACTIONS.includes(key) && (mode === 'manual' || mode === 'automatic')));
14
+ }
15
+ export function resolveWorkflowPolicy(...layers) {
16
+ const result = { ...MANUAL_WORKFLOW_POLICY };
17
+ for (const layer of layers) {
18
+ if (isWorkflowPolicy(layer))
19
+ Object.assign(result, layer);
20
+ }
21
+ return result;
22
+ }
23
+ export function renderWorkflowPolicy(policy = MANUAL_WORKFLOW_POLICY) {
24
+ return `[Kōbō workflow preferences]\n${WORKFLOW_ACTIONS.map((action) => `- ${action}: ${policy[action]}`).join('\n')}\nManual means require explicit user authorization for that action; an explicit request authorizes that operation only, not future operations. Automatic means the configured preference supplies the authorization requested by built-in workflow prompts for that action when necessary for this mission. Always follow more restrictive user instructions (including no commits, no push or no publication), repository constraints and actual tool permissions. Publish covers creating or updating PR/MR descriptions and comments. Never merge a PR/MR based on these preferences; merging needs separate explicit authorization. These preferences are instructions, not an OS sandbox or a guarantee against arbitrary commands. Do not change sandbox or approval settings to satisfy a preference.\n`;
25
+ }
@@ -0,0 +1,47 @@
1
+ # Your first Kōbō workspace
2
+
3
+ Kōbō is a local, single-user application that runs Claude Code or OpenAI Codex in Git worktrees. Each user installs their own instance and supplies their own provider account. A shared network token is not a multi-user account system.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 24.15 or later, Git and Bash, with a writable local Git repository containing at least one commit.
8
+ - A working login or API credential for the engine you choose. Kōbō ships neither credentials nor a provider subscription. Provider usage can incur charges.
9
+ - Linux is the locally exercised platform for this readiness change. CI is configured to exercise Linux and macOS; a configured job is not a completed compatibility test. Windows users need a Linux environment such as WSL; native Windows and WSL have not been validated in this release rehearsal.
10
+
11
+ No skill plugin, forge CLI, Docker, Notion, Sentry or voice software is needed for a basic mission. A repository without a remote works; choose forge `none` to disable PR/MR features.
12
+
13
+ ## Start
14
+
15
+ ```bash
16
+ npx @loicngr/kobo@latest
17
+ ```
18
+
19
+ This installs the latest published version, which may precede changes on `develop`. To try an unreleased checkout, follow [Contributing](../CONTRIBUTING.md), run `npm run build`, and start with a disposable home:
20
+
21
+ ```bash
22
+ KOBO_HOME="/tmp/kobo-first-run" npm start
23
+ ```
24
+
25
+ Open <http://localhost:3000>. The server binds to loopback by default. Do not expose it to a network just to complete setup.
26
+
27
+ ## Create a mission
28
+
29
+ 1. In the first-run panel, choose an engine and the path of your local Git repository.
30
+ 2. Run **Check environment**. Resolve missing Git, Bash, runtime, or write access before continuing. Authentication and model availability remain **unverified**: diagnostics do not contact a paid model or read your credentials.
31
+ 3. Continue to the creation form. Name the mission and describe a small change, including its acceptance criteria.
32
+ 4. Keep **Standard** and **Plan** for the first review. Standard requires no external skill suite. Plan does not silently become unrestricted when you prepare or enable auto-loop.
33
+ 5. Review the plan, explicitly select an execution permission mode when you are ready, and send the implementation instruction. Review the resulting diff before committing or publishing.
34
+
35
+ New installations use manual commit, push and PR/MR publication preferences, disable audio and optional ticket integrations, and admit at most two concurrent unattended starts. Manual sessions can exceed that threshold. Agent workflow preferences are instructions; the engine permission mode supplies the actual sandbox and approval behavior.
36
+
37
+ You can skip setup and reopen it from **Help → First-run setup**. Setup checks do not start a mission automatically.
38
+
39
+ ## Optional features
40
+
41
+ Configure integrations only when you need them, in their Settings tabs. Notion and Sentry support direct command, JSON arguments and JSON environment configuration without a Claude installation; credentials are stored locally outside settings exports. Connection tests run only when explicitly requested. Forge actions use your authenticated forge CLI or configured Bitbucket credentials. See [Configuration](../CONFIGURATION.md) and [Security](../SECURITY.md).
42
+
43
+ ## Existing installations
44
+
45
+ Upgrades preserve customized prompts, plugin selection, integration toggles, permission modes and resource limits. Existing installations retain legacy automatic Git workflow preferences; review them in Settings if you prefer manual authorization. Existing workspaces receive a snapshot of that historical policy, while new workspaces snapshot the effective global/project/create policy. Changing a global policy does not rewrite existing missions.
46
+
47
+ Known shipped prompt text and retired sound selections are migrated; custom prompt text is preserved. Audio enablement and volume are retained. Take a complete backup before an upgrade; see [Troubleshooting](./troubleshooting.md).
@@ -0,0 +1,49 @@
1
+ # Troubleshooting
2
+
3
+ ## Environment check
4
+
5
+ Open the first-run setup from Help and run the check again after changing the selected engine or project. Reports show stable status codes and do not include raw command output or credentials.
6
+
7
+ | Check | Action |
8
+ |---|---|
9
+ | Node | Use Node 24.15 or later; contributors should use `.nvmrc`. |
10
+ | Git / Bash | Install the missing executable and make it available on the server's `PATH`. |
11
+ | Project | Select a writable Git repository containing at least one commit. |
12
+ | Storage / worktrees | Check permissions on `KOBO_HOME`, the project and configured worktree root. |
13
+ | Engine executable | Reinstall the package dependencies and check the chosen runtime installation. |
14
+ | Authentication / model: unknown | Authenticate with the selected provider, select a model available to that account, then run a small mission. The diagnostic deliberately makes no provider request. |
15
+
16
+ ## Native dependency installation fails
17
+
18
+ Some platforms compile `node-pty` locally and need Python, make and a C++ compiler. During this rehearsal, node-gyp failed to find Node headers when its cache path contained spaces. Keep the build cache/home path free of spaces, or configure a separate node-gyp development cache before reinstalling. Application installation and Kōbō data directories with spaces and accented characters passed the Linux tarball test with a space-free build cache. This does not establish native Windows or WSL compatibility.
19
+
20
+ ## The agent stays in Plan
21
+
22
+ Kōbō preserves your permission choice. Auto-loop can prepare a mission in Plan, but execution/finalization requires you to choose an execution mode explicitly. A plugin, prompt or workflow preference does not authorize an automatic permission escalation.
23
+
24
+ Commit, push and publication are independent preferences. Manual means an explicit request is needed for that action. More restrictive instructions such as “no commits” still win over automatic preferences. These prompt instructions do not replace a sandbox.
25
+
26
+ ## Scheduled work waits
27
+
28
+ The concurrent-agent setting controls unattended admission, including auto-loop, scheduled work and quota retries. Running manual sessions count against capacity, but starting a manual session is exempt. A terminating agent owns its slot until its process is confirmed closed. Waiting does not mean the scheduled instruction has run. Inspect the schedule/loop status, quota reset and current sessions before retrying.
29
+
30
+ ## Notion or Sentry fails
31
+
32
+ Enable the feature, save a direct connection or configure the legacy Claude MCP entry, and explicitly run Test connection. Check command availability, JSON arguments/environment, provider access and page/project permissions. Saving a connection replaces the complete private configuration; stored secrets are not returned to the browser. Clearing it restores legacy lookup if a legacy entry exists.
33
+
34
+ Kōbō hides MCP error text and stderr because third-party errors can include credentials. Inspect the MCP's configuration locally without pasting credentials into an issue. For Notion, server token environment variables override stored token variables; explicit `OPENAPI_MCP_HEADERS` remain authoritative for compatibility.
35
+
36
+ ## Backup and restore
37
+
38
+ Daily and pre-migration SQLite snapshots are not a backup of settings, private integration configuration, repositories or uncommitted worktree changes.
39
+
40
+ 1. Stop Kōbō and its agents cleanly. Copy the complete `KOBO_HOME` directory and separately preserve the project/worktree directories. Keep this backup private.
41
+ 2. Before restoring, retain a copy of the current home. Use a separate directory for the restoration test and a matching application version. Never overwrite your only copy.
42
+ 3. Copy the selected SQLite snapshot to `kobo.db` in that separate home. Remove only stale `kobo.db-wal`/`kobo.db-shm` files from the separate restore copy while no server is using it. Restore matching settings and other JSON files from the full backup.
43
+ 4. Start with `KOBO_HOME` pointing at that separate directory. Confirm missions, conversations and settings before replacing production data. Old absolute worktree paths must still be available; starting an agent may write to them.
44
+
45
+ Migrations move forward. Running an older binary on a newly migrated database is not a supported rollback. Restore a compatible backup instead.
46
+
47
+ ## Report a problem
48
+
49
+ Use GitHub Issues for reproducible bugs; include OS/architecture, Node and Kōbō versions, chosen engine, steps and expected/actual behavior. Remove prompts, repository paths, tokens and personal data from attachments. Do not upload `kobo.db`, your complete data home, integration configuration or raw conversation exports. Report vulnerabilities privately via [Security](../SECURITY.md).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.45",
4
- "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
3
+ "version": "2.1.0",
4
+ "description": "Kōbō — local multi-workspace orchestrator for Claude Code and OpenAI Codex, with isolated Git worktrees, live review and optional automation.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
7
7
  "bin": {
@@ -14,7 +14,17 @@
14
14
  "AGENTS.md",
15
15
  "README.md",
16
16
  "CHANGELOG.md",
17
- "LICENSE"
17
+ "LICENSE",
18
+ "CONFIGURATION.md",
19
+ "SECURITY.md",
20
+ "THIRD_PARTY_NOTICES.md",
21
+ "docs/getting-started.md",
22
+ "docs/troubleshooting.md",
23
+ "CONTRIBUTING.md",
24
+ "CODE_OF_CONDUCT.md",
25
+ "DESIGN.md",
26
+ "docs/release/public-readiness-evidence.md",
27
+ "docs/release/public-opening-checklist.md"
18
28
  ],
19
29
  "publishConfig": {
20
30
  "access": "public",
@@ -66,10 +76,12 @@
66
76
  "lint:fix": "biome check --write .",
67
77
  "format": "biome format --write .",
68
78
  "format:check": "biome format .",
69
- "prepublishOnly": "npm run build"
79
+ "prepublishOnly": "npm run build",
80
+ "test:package": "node scripts/smoke-package.mjs",
81
+ "security:scan": "node scripts/scan-secrets.mjs"
70
82
  },
71
83
  "dependencies": {
72
- "@anthropic-ai/claude-agent-sdk": "^0.3.274",
84
+ "@anthropic-ai/claude-agent-sdk": "^0.3.280",
73
85
  "@emnapi/core": "^1.11.3",
74
86
  "@emnapi/runtime": "^1.11.3",
75
87
  "@hono/node-server": "^2.1.1",
@@ -85,11 +97,11 @@
85
97
  "devDependencies": {
86
98
  "@biomejs/biome": "2.5.7",
87
99
  "@types/better-sqlite3": "^9.6.0",
88
- "@types/node": "^26.6.1",
100
+ "@types/node": "^26.6.2",
89
101
  "@types/ws": "^8.18.1",
90
102
  "@vitest/runner": "^4.1.11",
91
103
  "concurrently": "^10.0.5",
92
- "tsx": "^4.23.13",
104
+ "tsx": "^4.23.15",
93
105
  "typescript": "^6.0.3",
94
106
  "vitest": "^4.1.11"
95
107
  },