@loicngr/kobo 1.11.44 → 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.
- package/AGENTS.md +57 -4
- package/CHANGELOG.md +13 -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 +48 -8
- package/SECURITY.md +54 -0
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +2 -1
- package/dist/mcp-server/kobo-tasks-server.js +31 -5
- package/dist/server/db/migrations.js +37 -0
- package/dist/server/db/schema.js +6 -1
- package/dist/server/db/session-handoff-schema.js +24 -0
- package/dist/server/index.js +8 -0
- package/dist/server/routes/documents.js +1 -1
- 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/workspaces.js +118 -20
- package/dist/server/services/agent/engines/claude-code/engine.js +15 -0
- package/dist/server/services/agent/engines/codex/engine.js +1 -0
- 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 +68 -11
- package/dist/server/services/auto-loop-service.js +45 -25
- package/dist/server/services/custom-sound-service.js +0 -0
- package/dist/server/services/engine-handoff-service.js +5 -5
- 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/session-activity-service.js +17 -0
- package/dist/server/services/session-handoff-runtime.js +26 -0
- package/dist/server/services/session-handoff-service.js +479 -0
- 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/wakeup-service.js +15 -5
- package/dist/server/services/workspace-service.js +30 -6
- 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/server/utils/workspace-lifecycle-guard.js +35 -20
- package/dist/shared/auto-loop-prompts.js +6 -2
- package/dist/shared/automatic-admission.js +1 -0
- package/dist/shared/document-roots.js +2 -0
- package/dist/shared/environment.js +1 -0
- package/dist/shared/notification-assets.js +73 -0
- package/dist/shared/session-handoff.js +4 -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-DRgetl1M.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DSKCvpr2.js → ChangelogPage-BJdICFvz.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-DDxcsxyC.js → ComparisonPanel-2GLd8KIy.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-B9AdNJZj.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-CzVOvjSP.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-X25vuf83.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-DfEoI_f7.js → DocumentsPanel-BastjrEm.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-D85PwhKb.js → DrawerToggleButton-DbipGmBL.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-BFCnyhE2.css → GitPanel-C4wOcZV2.css} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-CnW4iVao.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-CaLCpejR.js → HealthPage-aieNQly-.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Cc4XzCAy.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-DqBbE8o4.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-qJWGcZpC.js → QBtnDropdown-BWy-rh45.js} +1 -1
- package/src/client/dist/pwa/assets/QExpansionItem-DfmdBGuW.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-BfqaWakg.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-cDK5AOQ5.js → QTooltip-Cy0T0vk2.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-YQPObRR3.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-BO-J4aNc.js → SearchPage-BoBjmItc.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-0GtDcXRC.js +9 -0
- package/src/client/dist/pwa/assets/{SettingsPage-W6N01tGz.css → SettingsPage-BYdLXJEJ.css} +1 -1
- package/src/client/dist/pwa/assets/SplitWorkspacePage-4wFh65Lh.js +1 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-DitOCad1.js → TerminalPanel-b70x_jqH.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-DvPAhZsa.js → TouchPan-CcTVcxwd.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-CJYdH3Jf.js → TourReplayButton-CpEMnQS8.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-C5lTHTp2.js +1 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-DAVo-E4p.css → WorkspacePage--5HsnMQ1.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-D_V9gNON.js +4 -0
- package/src/client/dist/pwa/assets/{browser-BsB6cFv4.js → browser-LBbxVwWf.js} +1 -1
- package/src/client/dist/pwa/assets/build-path-tree-DyvtWxb0.js +1 -0
- package/src/client/dist/pwa/assets/custom-sounds-BpeLZUDW.js +1 -0
- package/src/client/dist/pwa/assets/de-v6i2HlE2.js +17 -0
- package/src/client/dist/pwa/assets/{documents-SCd3w8xV.js → documents-Bjx8lz-h.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-BUdM7I6y.js → editor.api-BC3PbJkx.js} +1 -1
- package/src/client/dist/pwa/assets/efforts-BDHAnNUs.js +9 -0
- package/src/client/dist/pwa/assets/engineFeatures-hlEiwqt-.js +1 -0
- package/src/client/dist/pwa/assets/es-Bc1KgkAT.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-DLDl1qPx.js +1 -0
- package/src/client/dist/pwa/assets/{format-BjQxf4pe.js → format-Q94deO3I.js} +7 -7
- package/src/client/dist/pwa/assets/{formatters-BejhmNTV.js → formatters-CWxRHuUu.js} +1 -1
- package/src/client/dist/pwa/assets/fr-ByYPK5vr.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-5d5GhyRp.js → freemarker2-MPhk0eTh.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-DaafEsDK.js → handlebars-C60ru5gR.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CwHBDd2A.js → html-ClzchR9j.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-Sy42tm-V.js +1 -0
- package/src/client/dist/pwa/assets/index-CqZ7sDcM.js +20 -0
- package/src/client/dist/pwa/assets/it-BYF_88z6.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-DSNIEEH5.js → javascript-C43UJmMx.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Dma1UpA1.js → liquid-CYTrvTKF.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-D8rqEAws.js → mdx-CxsXOvkQ.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-2E7O2zoi.js → monaco-contributions-Cl4-iVpK.js} +1 -1
- package/src/client/dist/pwa/assets/monaco.contribution-DkjLUmyA.js +2 -0
- package/src/client/dist/pwa/assets/network-auth-CXQn2wO7.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-BrueBr2j.js +1 -0
- package/src/client/dist/pwa/assets/{pending-diff-open-CeiVIFZn.js → pending-diff-open-B4POItbR.js} +1 -1
- package/src/client/dist/pwa/assets/permissionModes-B-ciuUbz.js +36 -0
- package/src/client/dist/pwa/assets/{project-color-BCp9SWoI.js → project-color-CM3hflLH.js} +1 -1
- package/src/client/dist/pwa/assets/{python-f-9NqRlI.js → python-s2IMpsoV.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-DsCmoX0h.js → razor-DVNXiVBh.js} +1 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-TU6CDfD8.js +69 -0
- package/src/client/dist/pwa/assets/{typescript-cEHg4L10.js → typescript-BYT_8ABE.js} +1 -1
- package/src/client/dist/pwa/assets/use-slide-transition-CiaDm-tR.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-CNe8qb1q.js +2 -0
- package/src/client/dist/pwa/assets/workspace-templates-tBxXE52A.js +1 -0
- package/src/client/dist/pwa/assets/{xml-Plt-Y7we.js → xml-BIG-fmxk.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-ScEmaAeN.js → yaml-D7gu2CQh.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/mcp-server/README.md +8 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +4 -1
- package/src/mcp-server/kobo-tasks-server.ts +38 -5
- package/src/client/dist/pwa/assets/ActivityFeed-D_y4aikp.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-DKVEknbL.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-TY9g3hZb.css +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-Dg2s9nnv.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-BSzNScIr.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-CuG8MlgF.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-CDxYIJ_s.js +0 -2
- package/src/client/dist/pwa/assets/MainLayout-DTLNunOi.css +0 -1
- package/src/client/dist/pwa/assets/QDrawer-DX3PhMqC.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-CY4TWW_j.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-LvHiZ6ZH.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Aw89fRSq.js +0 -9
- package/src/client/dist/pwa/assets/SplitWorkspacePage-BIEceNwH.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CY-9z9nw.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-xH_OyA3r.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-CW-ER1Mt.js +0 -1
- package/src/client/dist/pwa/assets/de-Mu7biceO.js +0 -17
- package/src/client/dist/pwa/assets/efforts-DTJQP9or.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-Bj6Ob41d.js +0 -1
- package/src/client/dist/pwa/assets/es-BY5IfNI2.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-SM8vvYyO.js +0 -1
- package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
- package/src/client/dist/pwa/assets/fr-BMXCzFCM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CgWW5vfb.js +0 -1
- package/src/client/dist/pwa/assets/index-Di9Sz5E8.js +0 -20
- package/src/client/dist/pwa/assets/it-ByPZQK_K.js +0 -17
- package/src/client/dist/pwa/assets/monaco.contribution-DqOgo72G.js +0 -2
- package/src/client/dist/pwa/assets/network-auth-DNmhJTZn.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-B6Hiwvwb.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-DWdtf81n.js +0 -36
- package/src/client/dist/pwa/assets/render-chat-markdown-DK5BD9mU.js +0 -67
- package/src/client/dist/pwa/assets/use-tours-BmTbosmB.js +0 -2
- package/src/client/dist/pwa/assets/workspace-templates-DUWDLMIX.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) {
|
|
@@ -76,6 +76,14 @@ export function schedule(workspaceId, delaySeconds, prompt, reason, agentSession
|
|
|
76
76
|
console.error('[wakeup-service] schedule failed:', err);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
/** Move a source conversation's pending check only once its handoff succeeds. */
|
|
80
|
+
export function transferSession(workspaceId, sourceSessionId, targetSessionId) {
|
|
81
|
+
if (!sourceSessionId)
|
|
82
|
+
return;
|
|
83
|
+
getDb()
|
|
84
|
+
.prepare('UPDATE pending_wakeups SET agent_session_id = ? WHERE workspace_id = ? AND agent_session_id = ?')
|
|
85
|
+
.run(targetSessionId, workspaceId, sourceSessionId);
|
|
86
|
+
}
|
|
79
87
|
/** Cancel any pending wakeup for the workspace. Idempotent. */
|
|
80
88
|
export function cancel(workspaceId, reason) {
|
|
81
89
|
try {
|
|
@@ -116,7 +124,7 @@ export function getPending(workspaceId) {
|
|
|
116
124
|
export function isWakeupScheduled(workspaceId) {
|
|
117
125
|
return getPending(workspaceId) !== null;
|
|
118
126
|
}
|
|
119
|
-
/** Re-register timers
|
|
127
|
+
/** Re-register persisted timers. Previously deferred work survives downtime; only never-attempted stale wakeups expire. */
|
|
120
128
|
export function rehydrate() {
|
|
121
129
|
suspendForShutdown();
|
|
122
130
|
suspended = false;
|
|
@@ -126,14 +134,16 @@ export function rehydrate() {
|
|
|
126
134
|
const now = Date.now();
|
|
127
135
|
for (const row of rows) {
|
|
128
136
|
try {
|
|
129
|
-
const target = new Date(row.target_at).getTime();
|
|
137
|
+
const target = new Date(row.retry_at ?? row.target_at).getTime();
|
|
130
138
|
const delay = target - now;
|
|
131
139
|
if (delay > 0) {
|
|
132
140
|
const timeout = setTimeout(() => fire(row.workspace_id), delay);
|
|
133
141
|
timeout.unref?.();
|
|
134
142
|
timers.set(row.workspace_id, timeout);
|
|
135
143
|
}
|
|
136
|
-
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.
|
|
137
147
|
const timeout = setTimeout(() => fire(row.workspace_id), 0);
|
|
138
148
|
timeout.unref?.();
|
|
139
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';
|
|
@@ -7,6 +8,7 @@ import * as orchestrator from './agent/orchestrator.js';
|
|
|
7
8
|
import * as autoLoopService from './auto-loop-service.js';
|
|
8
9
|
import * as cronService from './cron-service.js';
|
|
9
10
|
import * as quotaBackoffService from './quota-backoff-service.js';
|
|
11
|
+
import { SESSION_RECENCY_ORDER } from './session-activity-service.js';
|
|
10
12
|
import { createTaskRecord, deleteTaskRecord, updateTaskRecord } from './task-mutations.js';
|
|
11
13
|
import * as wakeupService from './wakeup-service.js';
|
|
12
14
|
import { emitEphemeral } from './websocket-service.js';
|
|
@@ -53,7 +55,17 @@ function coerceAgentPermissionMode(raw) {
|
|
|
53
55
|
return 'bypass';
|
|
54
56
|
}
|
|
55
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
|
+
}
|
|
56
67
|
return {
|
|
68
|
+
workflowPolicy,
|
|
57
69
|
id: row.id,
|
|
58
70
|
name: row.name,
|
|
59
71
|
projectPath: row.project_path,
|
|
@@ -127,6 +139,9 @@ export function createWorkspace(data) {
|
|
|
127
139
|
const id = nanoid();
|
|
128
140
|
const computedWorktreePath = data.worktreePath ?? resolveWorkspaceWorktreePath(data.projectPath, data.workingBranch, data.worktreesPath);
|
|
129
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);
|
|
130
145
|
// Mirror the unified mode into the legacy columns so older readers (in-flight
|
|
131
146
|
// requests during deploy, external scripts) still see a sane value.
|
|
132
147
|
const unifiedMode = data.agentPermissionMode ?? 'bypass';
|
|
@@ -136,12 +151,12 @@ export function createWorkspace(data) {
|
|
|
136
151
|
INSERT INTO workspaces (
|
|
137
152
|
id, name, project_path, source_branch, working_branch, status,
|
|
138
153
|
notion_url, notion_page_id, sentry_url, pr_url, worktree_path, worktree_owned,
|
|
139
|
-
model, brainstorm_model, reasoning_effort, permission_mode, permission_profile, agent_permission_mode, engine, comparison_id, created_at, updated_at
|
|
140
|
-
) 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', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
141
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',
|
|
142
157
|
// Empty string is not a group: it would silently gather every workspace
|
|
143
158
|
// created with a blank id into one comparison.
|
|
144
|
-
data.comparisonId?.trim() ? data.comparisonId : null, now, now);
|
|
159
|
+
data.comparisonId?.trim() ? data.comparisonId : null, now, now, JSON.stringify(workflowPolicy));
|
|
145
160
|
return getWorkspace(id);
|
|
146
161
|
}
|
|
147
162
|
/** Fetch a single workspace by ID, or null if not found. */
|
|
@@ -161,7 +176,8 @@ export function listComparisonMembers(comparisonId) {
|
|
|
161
176
|
if (!comparisonId.trim())
|
|
162
177
|
return [];
|
|
163
178
|
const rows = getDb()
|
|
164
|
-
|
|
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')
|
|
165
181
|
.all(comparisonId);
|
|
166
182
|
return rows.map(mapWorkspace);
|
|
167
183
|
}
|
|
@@ -388,6 +404,12 @@ export function updateWorkspaceFields(id, fields) {
|
|
|
388
404
|
throw new Error(`Workspace '${id}' not found`);
|
|
389
405
|
const assignments = [];
|
|
390
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
|
+
}
|
|
391
413
|
if (fields.model !== undefined) {
|
|
392
414
|
if (typeof fields.model !== 'string' || !fields.model.trim())
|
|
393
415
|
throw new Error('model must be a non-empty string');
|
|
@@ -776,6 +798,7 @@ function mapSession(row) {
|
|
|
776
798
|
model: row.model,
|
|
777
799
|
startedAt: row.started_at,
|
|
778
800
|
endedAt: row.ended_at,
|
|
801
|
+
activationOrder: row.activation_order,
|
|
779
802
|
name: row.name,
|
|
780
803
|
};
|
|
781
804
|
}
|
|
@@ -799,7 +822,8 @@ export function getLatestSession(workspaceId) {
|
|
|
799
822
|
* Return the "active" session for tagging events like push/pull/open-pr traces
|
|
800
823
|
* or resumed chat messages. Skips idle sessions (never started) and prefers a
|
|
801
824
|
* running session, falling back to the last used session (any non-idle status).
|
|
802
|
-
*
|
|
825
|
+
* Starts and handoff rollbacks persist activation order independently of execution dates.
|
|
826
|
+
* Legacy sessions fall back to their last start/end time.
|
|
803
827
|
*/
|
|
804
828
|
export function getActiveSession(workspaceId) {
|
|
805
829
|
const db = getDb();
|
|
@@ -811,7 +835,7 @@ export function getActiveSession(workspaceId) {
|
|
|
811
835
|
return mapSession(running);
|
|
812
836
|
// Otherwise the last used non-idle session (completed, error, quota, etc.)
|
|
813
837
|
const latestNonIdle = db
|
|
814
|
-
.prepare(
|
|
838
|
+
.prepare(`SELECT * FROM agent_sessions WHERE workspace_id = ? AND status != 'idle' AND activation_order >= 0 ORDER BY ${SESSION_RECENCY_ORDER} LIMIT 1`)
|
|
815
839
|
.get(workspaceId);
|
|
816
840
|
return latestNonIdle ? mapSession(latestNonIdle) : null;
|
|
817
841
|
}
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const busyWorkspaces = new
|
|
1
|
+
const busyWorkspaces = new Map();
|
|
2
2
|
const deferred = new Map();
|
|
3
3
|
export class WorkspaceLifecycleBusyError extends Error {
|
|
4
4
|
code = 'workspace-busy';
|
|
@@ -9,10 +9,40 @@ export class WorkspaceLifecycleBusyError extends Error {
|
|
|
9
9
|
export function isWorkspaceLifecycleBusy(id) {
|
|
10
10
|
return busyWorkspaces.has(id);
|
|
11
11
|
}
|
|
12
|
-
export function assertWorkspaceLifecycleAvailable(id) {
|
|
13
|
-
|
|
12
|
+
export function assertWorkspaceLifecycleAvailable(id, owner) {
|
|
13
|
+
const reservation = busyWorkspaces.get(id);
|
|
14
|
+
if (owner ? reservation?.owner !== owner : !!reservation)
|
|
14
15
|
throw new WorkspaceLifecycleBusyError();
|
|
15
16
|
}
|
|
17
|
+
export function workspaceLifecycleReason(id) {
|
|
18
|
+
return busyWorkspaces.get(id)?.reason;
|
|
19
|
+
}
|
|
20
|
+
export function reserveWorkspaceLifecycle(id, reason) {
|
|
21
|
+
assertWorkspaceLifecycleAvailable(id);
|
|
22
|
+
const owner = Symbol(id);
|
|
23
|
+
busyWorkspaces.set(id, { owner, reason });
|
|
24
|
+
return {
|
|
25
|
+
owner,
|
|
26
|
+
release() {
|
|
27
|
+
if (busyWorkspaces.get(id)?.owner !== owner)
|
|
28
|
+
return;
|
|
29
|
+
busyWorkspaces.delete(id);
|
|
30
|
+
const callbacks = deferred.get(id);
|
|
31
|
+
deferred.delete(id);
|
|
32
|
+
if (callbacks)
|
|
33
|
+
queueMicrotask(() => {
|
|
34
|
+
for (const retry of callbacks) {
|
|
35
|
+
try {
|
|
36
|
+
retry();
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error('[workspace-lifecycle] deferred operation failed:', err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
16
46
|
/** Keep automatic work pending without polling or acquiring the guard recursively. */
|
|
17
47
|
export function deferUntilWorkspaceAvailable(id, retry) {
|
|
18
48
|
if (!busyWorkspaces.has(id))
|
|
@@ -24,26 +54,11 @@ export function deferUntilWorkspaceAvailable(id, retry) {
|
|
|
24
54
|
}
|
|
25
55
|
/** Exclude competing restore/purge/delete operations, including their awaits. */
|
|
26
56
|
export async function withWorkspaceLifecycleGuard(id, action) {
|
|
27
|
-
|
|
28
|
-
throw new WorkspaceLifecycleBusyError();
|
|
29
|
-
busyWorkspaces.add(id);
|
|
57
|
+
const reservation = reserveWorkspaceLifecycle(id);
|
|
30
58
|
try {
|
|
31
59
|
return await action();
|
|
32
60
|
}
|
|
33
61
|
finally {
|
|
34
|
-
|
|
35
|
-
const callbacks = deferred.get(id);
|
|
36
|
-
deferred.delete(id);
|
|
37
|
-
if (callbacks)
|
|
38
|
-
queueMicrotask(() => {
|
|
39
|
-
for (const retry of callbacks) {
|
|
40
|
-
try {
|
|
41
|
-
retry();
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
console.error('[workspace-lifecycle] deferred operation failed:', err);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
});
|
|
62
|
+
reservation.release();
|
|
48
63
|
}
|
|
49
64
|
}
|
|
@@ -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;
|