@loicngr/kobo 1.11.45 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -2
- package/CHANGELOG.md +12 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONFIGURATION.md +1968 -0
- package/CONTRIBUTING.md +83 -0
- package/DESIGN.md +296 -0
- package/README.md +35 -7
- package/SECURITY.md +54 -0
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/dist/server/db/migrations.js +31 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/index.js +6 -0
- package/dist/server/routes/environment.js +18 -0
- package/dist/server/routes/health.js +17 -2
- package/dist/server/routes/integrations.js +29 -0
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/sounds.js +60 -0
- package/dist/server/routes/usage.js +28 -2
- package/dist/server/routes/workspaces.js +46 -12
- package/dist/server/services/agent/engines/claude-code/engine.js +37 -4
- package/dist/server/services/agent/engines/codex/options-builder.js +1 -0
- package/dist/server/services/agent/engines/codex/spawn.js +2 -2
- package/dist/server/services/agent/integration-mcp.js +49 -0
- package/dist/server/services/agent/orchestrator.js +6 -2
- package/dist/server/services/auto-loop-service.js +45 -25
- package/dist/server/services/custom-sound-service.js +0 -0
- package/dist/server/services/environment-check-service.js +125 -0
- package/dist/server/services/initial-prompt-template-service.js +8 -21
- package/dist/server/services/integration-config-service.js +65 -0
- package/dist/server/services/legacy-public-prompts.js +112 -0
- package/dist/server/services/notion-service.js +25 -61
- package/dist/server/services/sentry-service.js +17 -12
- package/dist/server/services/settings-defaults.js +5 -0
- package/dist/server/services/settings-service.js +112 -23
- package/dist/server/services/skill-suite-prompts.js +29 -22
- package/dist/server/services/templates-service.js +31 -8
- package/dist/server/services/terminal-service.js +16 -0
- package/dist/server/services/usage/reliability.js +24 -0
- package/dist/server/services/wakeup-service.js +7 -5
- package/dist/server/services/workspace-service.js +25 -4
- package/dist/server/services/worktree-service.js +3 -1
- package/dist/server/utils/git-repo-lock.js +7 -3
- package/dist/server/utils/mcp-client.js +26 -12
- package/dist/server/utils/mcp-process.js +25 -0
- package/dist/server/utils/node-pty-spawn-helper.js +31 -0
- package/dist/server/utils/paths.js +7 -0
- package/dist/shared/auto-loop-prompts.js +6 -2
- package/dist/shared/automatic-admission.js +1 -0
- package/dist/shared/environment.js +1 -0
- package/dist/shared/notification-assets.js +73 -0
- package/dist/shared/skill-suite-prompts.js +6 -3
- package/dist/shared/workflow-policy.js +25 -0
- package/docs/getting-started.md +47 -0
- package/docs/troubleshooting.md +49 -0
- package/package.json +19 -7
- package/src/client/dist/pwa/assets/ActivityFeed-DeJ0V6CW.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-CnXTnFhe.js → ChangelogPage-OoOnn4q9.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-CKylN45q.js → ComparisonPanel-BYpe6Nch.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-COQwHyqL.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-CZ2036MC.js +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-XiCj24i2.css +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-CXBmMV9R.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-BeCttB-x.js → DocumentsPanel-mLGU5aTb.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BJBBYeOc.js → DrawerToggleButton-C39pfWBD.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-BQk7kJO-.js +3 -0
- package/src/client/dist/pwa/assets/{GitPanel-BFCnyhE2.css → GitPanel-C4wOcZV2.css} +1 -1
- package/src/client/dist/pwa/assets/{HealthPage-l88TEk-k.js → HealthPage-CnJWXfa8.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Cc4XzCAy.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-D3zfT61Y.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-44Mj-y_1.js → QBtnDropdown-DGLLlBf3.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-Bok1m8Wi.js → QExpansionItem-CQ2LzM8P.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DoTWgQAO.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-BDihn2aY.js → QTooltip-DKvUeiR_.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-DIMzNIO3.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-BwZ2TX1G.js → SearchPage-COWnMxRZ.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-B9cLhFnO.js +9 -0
- package/src/client/dist/pwa/assets/{SettingsPage-W6N01tGz.css → SettingsPage-BYdLXJEJ.css} +1 -1
- package/src/client/dist/pwa/assets/SplitWorkspacePage-BJKssJyI.js +1 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-Cq-FR7A7.js → TerminalPanel-C_HNpN1X.js} +4 -4
- package/src/client/dist/pwa/assets/{TouchPan-BQ1cvzRC.js → TouchPan-CCllyaAF.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Dz1yEz_z.js → TourReplayButton-Cp2UnxWa.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B-SuQuS_.js +1 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-BbUvR1Vf.css → WorkspacePage--5HsnMQ1.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-6lN4iF5W.js +4 -0
- package/src/client/dist/pwa/assets/{browser-BmWgBx9M.js → browser-uy3hmwKo.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-6EjBmYYY.js → build-path-tree-DZrp6Aji.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-Du8ReMlU.js +1 -0
- package/src/client/dist/pwa/assets/de-CTE3Xb7Z.js +17 -0
- package/src/client/dist/pwa/assets/{documents-BZ8bwcjr.js → documents-BtNrB6d0.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-CK7--ckY.js → editor.api-P-eJRcVK.js} +1 -1
- package/src/client/dist/pwa/assets/efforts-ChUC4xtk.js +9 -0
- package/src/client/dist/pwa/assets/engineFeatures-D-0NPZBE.js +1 -0
- package/src/client/dist/pwa/assets/es-CZxg1xjh.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-C8VyjASo.js → expand-template-ByoC_kxX.js} +1 -1
- package/src/client/dist/pwa/assets/{format-LtTLLitJ.js → format-CeidAPFn.js} +7 -7
- package/src/client/dist/pwa/assets/{formatters-Bl9zA7Ai.js → formatters-BtG6lb4d.js} +1 -1
- package/src/client/dist/pwa/assets/fr-C7HCclxj.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DxezMaje.js → freemarker2-CDYXi2tK.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-DK_Bj6Rx.js → handlebars-AVkJuN-_.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DFfsfStD.js → html-CpvEZlmO.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-KVkTJG-4.js +1 -0
- package/src/client/dist/pwa/assets/index-DsaWUT_Z.js +20 -0
- package/src/client/dist/pwa/assets/it-DoOWwqZz.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-DuNbcaL7.js → javascript-BYnnBLH9.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CM3JKyhv.js → liquid-CzqlB3Um.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-BG3zYwNF.js → mdx-hrYFnJVZ.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-DLscZGH-.js → monaco-contributions-D2YXqNaH.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-DnhZHwXT.js → monaco.contribution-Dl22KqTL.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-Dq0I5kjv.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-vV3Rm4_v.js +1 -0
- package/src/client/dist/pwa/assets/{pending-diff-open-dYIFdS4r.js → pending-diff-open-BJYqBOUG.js} +1 -1
- package/src/client/dist/pwa/assets/permissionModes-B-ciuUbz.js +36 -0
- package/src/client/dist/pwa/assets/{project-color-R_1je4vW.js → project-color-W5mMgIKK.js} +1 -1
- package/src/client/dist/pwa/assets/{python-COs6pHk2.js → python-DafgCozG.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-Jl1VqMsk.js → razor-CWYrTLCm.js} +1 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-CE5RIL0s.js +69 -0
- package/src/client/dist/pwa/assets/{typescript-BU_TUuAM.js → typescript-DLSIuW9u.js} +1 -1
- package/src/client/dist/pwa/assets/use-tours-CDdnvIaG.js +2 -0
- package/src/client/dist/pwa/assets/workspace-templates-C88VmFdQ.js +1 -0
- package/src/client/dist/pwa/assets/{xml-gFFsobxE.js → xml-BAmdYarn.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-DaXaqaGD.js → yaml-RQqNMXWh.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sounds/neutral.wav +0 -0
- package/src/client/dist/pwa/sounds/ready.wav +0 -0
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-TY9g3hZb.css +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-CZQLfoV1.js +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-D8g7_r4S.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-Csf3WmLc.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-B-ZHgghQ.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BAAQLq95.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-Cf4Q9x1u.js +0 -2
- package/src/client/dist/pwa/assets/QDrawer-B4qHw8wt.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +0 -9
- package/src/client/dist/pwa/assets/SplitWorkspacePage-Bg6nKBcA.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BCVOP8vp.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +0 -4
- package/src/client/dist/pwa/assets/de-COObokpu.js +0 -17
- package/src/client/dist/pwa/assets/efforts-Bw0o2Bud.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-DQO9HHXI.js +0 -1
- package/src/client/dist/pwa/assets/es-By2Q9Tca.js +0 -17
- package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
- package/src/client/dist/pwa/assets/fr-D-sd0ddn.js +0 -17
- package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +0 -1
- package/src/client/dist/pwa/assets/index-BLd8DAzg.js +0 -20
- package/src/client/dist/pwa/assets/it-CeK5lyIA.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-BJiPbO5l.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-D4p3qPkp.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-B7fuUegU.js +0 -36
- package/src/client/dist/pwa/assets/render-chat-markdown-AaOo0Tgq.js +0 -67
- package/src/client/dist/pwa/assets/use-tours-CVn874kD.js +0 -2
- package/src/client/dist/pwa/assets/workspace-templates-BCKR8TXr.js +0 -1
- package/src/client/dist/pwa/sounds/7eme-compagnie-03.mp3 +0 -0
- package/src/client/dist/pwa/sounds/aller-ftg.mp3 +0 -0
- package/src/client/dist/pwa/sounds/arrete-de-mentir.mp3 +0 -0
- package/src/client/dist/pwa/sounds/arretez-les-messages.mp3 +0 -0
- package/src/client/dist/pwa/sounds/bah-alors-on-est-nul.mp3 +0 -0
- package/src/client/dist/pwa/sounds/basic-notification.mp3 +0 -0
- package/src/client/dist/pwa/sounds/ca_va_peter.mp3 +0 -0
- package/src/client/dist/pwa/sounds/dry-fart.mp3 +0 -0
- package/src/client/dist/pwa/sounds/faaah.mp3 +0 -0
- package/src/client/dist/pwa/sounds/for-shure.mp3 +0 -0
- package/src/client/dist/pwa/sounds/gta-v-death.mp3 +0 -0
- package/src/client/dist/pwa/sounds/hey.mp3 +0 -0
- package/src/client/dist/pwa/sounds/mais-laisse-moi-dormir-zebi.mp3 +0 -0
- package/src/client/dist/pwa/sounds/nan-tu-degages.mp3 +0 -0
- package/src/client/dist/pwa/sounds/nan-wallah-pardon.mp3 +0 -0
- package/src/client/dist/pwa/sounds/ouais-cest-greg.mp3 +0 -0
- package/src/client/dist/pwa/sounds/pas-ca-zinedine.mp3 +0 -0
- package/src/client/dist/pwa/sounds/ta-gueule.mp3 +0 -0
- package/src/client/dist/pwa/sounds/travail_termine.mp3 +0 -0
- package/src/client/dist/pwa/sounds/tu-vas-la-fermer.mp3 +0 -0
- package/src/client/dist/pwa/sounds/warcraft-3-humain-travail.mp3 +0 -0
- package/src/client/dist/pwa/sounds/yaaa.mp3 +0 -0
|
@@ -45,11 +45,11 @@ const failedRetries = new Map();
|
|
|
45
45
|
/** Keep a claimed wakeup durable while the session is still unavailable. */
|
|
46
46
|
function defer(workspaceId, row) {
|
|
47
47
|
const targetAt = new Date(Date.now() + ACTIVE_SESSION_RETRY_MS).toISOString();
|
|
48
|
-
getDb().prepare('UPDATE pending_wakeups SET
|
|
48
|
+
getDb().prepare('UPDATE pending_wakeups SET retry_at = ? WHERE workspace_id = ?').run(targetAt, workspaceId);
|
|
49
49
|
const timeout = setTimeout(() => fire(workspaceId), ACTIVE_SESSION_RETRY_MS);
|
|
50
50
|
timeout.unref?.();
|
|
51
51
|
timers.set(workspaceId, timeout);
|
|
52
|
-
emitEphemeral(workspaceId, 'wakeup:scheduled', { targetAt, reason: row.reason ?? undefined });
|
|
52
|
+
emitEphemeral(workspaceId, 'wakeup:scheduled', { targetAt: row.target_at, reason: row.reason ?? undefined });
|
|
53
53
|
}
|
|
54
54
|
/** Schedule a wakeup for the given workspace. Replaces any existing pending wakeup. */
|
|
55
55
|
export function schedule(workspaceId, delaySeconds, prompt, reason, agentSessionId) {
|
|
@@ -124,7 +124,7 @@ export function getPending(workspaceId) {
|
|
|
124
124
|
export function isWakeupScheduled(workspaceId) {
|
|
125
125
|
return getPending(workspaceId) !== null;
|
|
126
126
|
}
|
|
127
|
-
/** Re-register timers
|
|
127
|
+
/** Re-register persisted timers. Previously deferred work survives downtime; only never-attempted stale wakeups expire. */
|
|
128
128
|
export function rehydrate() {
|
|
129
129
|
suspendForShutdown();
|
|
130
130
|
suspended = false;
|
|
@@ -134,14 +134,16 @@ export function rehydrate() {
|
|
|
134
134
|
const now = Date.now();
|
|
135
135
|
for (const row of rows) {
|
|
136
136
|
try {
|
|
137
|
-
const target = new Date(row.target_at).getTime();
|
|
137
|
+
const target = new Date(row.retry_at ?? row.target_at).getTime();
|
|
138
138
|
const delay = target - now;
|
|
139
139
|
if (delay > 0) {
|
|
140
140
|
const timeout = setTimeout(() => fire(row.workspace_id), delay);
|
|
141
141
|
timeout.unref?.();
|
|
142
142
|
timers.set(row.workspace_id, timeout);
|
|
143
143
|
}
|
|
144
|
-
else if (-delay <= STALE_WAKEUP_GRACE_MS) {
|
|
144
|
+
else if (row.retry_at != null || -delay <= STALE_WAKEUP_GRACE_MS) {
|
|
145
|
+
// Admission deferred this wakeup after its deadline. Downtime does not
|
|
146
|
+
// revoke the pending instruction; retry through the same admission gate.
|
|
145
147
|
const timeout = setTimeout(() => fire(row.workspace_id), 0);
|
|
146
148
|
timeout.unref?.();
|
|
147
149
|
timers.set(row.workspace_id, timeout);
|
|
@@ -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
|
-
|
|
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) ===
|
|
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
|
|
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
|
|
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
|
|
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', (
|
|
71
|
-
console.error(
|
|
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
|
|
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
|
|
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 = (
|
|
138
|
+
const onError = () => {
|
|
134
139
|
clearTimeout(timeout);
|
|
135
140
|
mcpProcess.stdout?.removeListener('data', onData);
|
|
136
|
-
reject(
|
|
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
|
|
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
|
|
184
|
+
reject(new Error(`MCP tool failed (code: ${Number.isInteger(parsed.error.code) ? parsed.error.code : 'unknown'})`));
|
|
180
185
|
}
|
|
181
186
|
else {
|
|
182
|
-
|
|
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 = (
|
|
203
|
+
const onError = () => {
|
|
192
204
|
clearTimeout(timeout);
|
|
193
205
|
mcpProcess.stdout?.removeListener('data', onData);
|
|
194
|
-
reject(
|
|
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
|
|
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
|
|
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 === '
|
|
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\` —
|
|
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
|
|
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).
|