@loicngr/kobo 2.1.16 → 2.1.19
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 +101 -0
- package/CHANGELOG.md +12 -0
- package/CONFIGURATION.md +108 -17
- package/README.md +6 -2
- package/dist/mcp-server/kobo-tasks-server.js +28 -6
- package/dist/mcp-server/progress-bar-client.js +20 -0
- package/dist/mcp-server/workspace-dialogue-client.js +27 -7
- package/dist/server/db/migrations.js +2 -0
- package/dist/server/db/progress-bar-schema.js +27 -0
- package/dist/server/db/schema.js +2 -0
- package/dist/server/index.js +6 -0
- package/dist/server/middleware/workspace-creation-maintenance.js +13 -0
- package/dist/server/routes/maintenance.js +32 -0
- package/dist/server/routes/mcp.js +19 -9
- package/dist/server/routes/progress-bars.js +56 -0
- package/dist/server/routes/pull-requests.js +2 -1
- package/dist/server/routes/workspaces.js +35 -64
- package/dist/server/services/agent/engines/claude-code/engine.js +59 -16
- package/dist/server/services/agent/engines/claude-code/options-builder.js +13 -34
- package/dist/server/services/agent/engines/claude-code/stop-hook.js +8 -27
- package/dist/server/services/agent/engines/codex/engine.js +8 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +11 -16
- package/dist/server/services/agent/orchestrator.js +73 -11
- package/dist/server/services/auto-loop-final-review-service.js +1 -1
- package/dist/server/services/auto-loop-service.js +10 -10
- package/dist/server/services/cron-service.js +7 -0
- package/dist/server/services/progress-bar-mcp-service.js +44 -0
- package/dist/server/services/progress-bar-runtime.js +29 -0
- package/dist/server/services/progress-bar-service.js +232 -0
- package/dist/server/services/quota-backoff-service.js +7 -0
- package/dist/server/services/update-maintenance-service.js +44 -0
- package/dist/server/services/wakeup-service.js +21 -3
- package/dist/server/services/workspace-creation-mcp-service.js +16 -0
- package/dist/shared/auto-loop-prompts.js +12 -12
- package/dist/shared/launch-instructions.js +57 -0
- package/dist/shared/progress-bar-prompts.js +7 -0
- package/dist/shared/progress-bar-tools.js +73 -0
- package/dist/shared/progress-bars.js +3 -0
- package/docker-compose.vps.yml +60 -0
- package/docs/deployment-vps.md +506 -0
- package/docs/getting-started.md +9 -0
- package/package.json +8 -1
- package/scripts/kobo-gh +9 -0
- package/scripts/kobo-github-app-token +190 -0
- package/scripts/kobo-install +392 -0
- package/scripts/kobo-update +499 -0
- package/src/client/dist/pwa/assets/ActivityFeed-DiUxnmGJ.js +2 -0
- package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-Bbrq02Xt.js → AutoLoopFinalReviewSettings-f4VDR5dd.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-BJfkAVYp.js → ChangelogPage-dEWpjTlp.js} +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-Bwbqgvr4.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-CCCCAhrx.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-DZYsAh5y.js → DashboardPage-3byhBITf.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-Bj_NTfCF.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-BfPlfiwU.js → DocumentsPanel-xyLDV3T1.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-DG1Qjqk1.js → DrawerToggleButton-pwBSstQP.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-DvIdrkVw.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-_VXG1Zeo.js → HealthPage-BSwhZQRo.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-DZJ9BvZw.js +2 -0
- package/src/client/dist/pwa/assets/{MainLayout-DnmUxNk7.css → MainLayout-DoAEgANU.css} +1 -1
- package/src/client/dist/pwa/assets/{MemoryPanel-CVlvGZXZ.js → MemoryPanel-ChR032zP.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-SJsnzUAM.js → MemoryProposalList-BRKSoS4K.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-DBgGsN-D.js → QBtnDropdown-o3QH0Bb8.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-DKgaMJ_l.js → QExpansionItem-BhFHiyq9.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-D3OCsq3g.js → QScrollArea-BsNY6slB.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-bEq3g_io.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-X5ps8Opw.js → QTooltip-xYZ6KNu2.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-Ww_FVlW5.js → SchedulePanel-DIFv_FTY.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-B87WEHf3.js → SearchPage-BqTBIt5E.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BLD0xWFB.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-B99AJUbG.js → SplitWorkspacePage-D6r2pl5g.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DnA1MXwu.js → TerminalPanel-CladK9Vv.js} +9 -9
- package/src/client/dist/pwa/assets/{ToolCallItem-2vFggaur.js → ToolCallItem-CdzEME4w.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-BSU3z0aO.js → TouchPan-B4gQPwGN.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DQwS2xdD.js → TourReplayButton-B0j6AZwU.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CM1tGDje.js → WorkspaceAttentionLabels-CeVhoidC.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-CK5XCuG_.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-B248nKIU.css → WorkspacePage-w2bF8wTd.css} +1 -1
- package/src/client/dist/pwa/assets/{browser-DPsupI9Q.js → browser-B8tNScg1.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-CbIdt-p9.js → build-path-tree-kmVAcvs_.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-CJwd-8TL.js +1 -0
- package/src/client/dist/pwa/assets/{de-D3Ycvf_F.js → de-BaO6kbdO.js} +2 -2
- package/src/client/dist/pwa/assets/{editor.api-DvgAtnOj.js → editor.api-CgbPVW9s.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-nmxfTVQD.js → efforts-y7Wf_Rgd.js} +2 -2
- package/src/client/dist/pwa/assets/{es-TgeS5898.js → es-DTvSsbD0.js} +2 -2
- package/src/client/dist/pwa/assets/{expand-template-Brn5O4a1.js → expand-template-BFbbjj7X.js} +1 -1
- package/src/client/dist/pwa/assets/{format-DfJ3Fv1i.js → format-BETfc4z9.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-B30bNFhq.js → formatters-C22K9vlC.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-CJafryNb.js → fr-BH7V66Yz.js} +2 -2
- package/src/client/dist/pwa/assets/{freemarker2-DnZe74tc.js → freemarker2-DiRVvp9O.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-CmuMfxzO.js → handlebars-BLYRroSQ.js} +1 -1
- package/src/client/dist/pwa/assets/{html-BD7OJFGX.js → html-DAW2vteY.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-DnuTmw3O.js +1 -0
- package/src/client/dist/pwa/assets/index-CbyHfF8I.js +20 -0
- package/src/client/dist/pwa/assets/{it-C5mN8Y61.js → it-DZGbkkoy.js} +2 -2
- package/src/client/dist/pwa/assets/{javascript-Y-OrgcmW.js → javascript-Bb5J8n0k.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-BqOkKePH.js → liquid-Dqx3adnJ.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-D-58x2h0.js → mdx-D_7igLV3.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-BW4ZMKO6.js → monaco-contributions-C-0CbrAy.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-DJWqzNG-.js → monaco.contribution-c6JPBEZI.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-BE4qbvWm.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-BVFBBGrJ.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-gB5yeZPN.js → project-color-CHOR2jB5.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BkBj1wDC.js → python-Dqubw1F5.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-DOOJ_0b8.js → razor-BQphc9Kt.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Bheo43G5.js → typescript-Dep00l0X.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CBpnyV-R.js → use-search-index-status-CQHHvHwM.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-BHNasCvg.js → use-tours-IVbXTmz2.js} +2 -2
- package/src/client/dist/pwa/assets/{use-worktree-restore-CgABsWno.js → use-worktree-restore-B3s5xDGR.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-BnFkigYg.js → workspace-templates-CyuTpztP.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-vHt2BD26.js → xml-BMS2dvaF.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BvhLPzP9.js → yaml-CYw92aic.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +88 -1
- package/src/mcp-server/kobo-tasks-server.ts +34 -6
- package/src/mcp-server/progress-bar-client.ts +27 -0
- package/src/mcp-server/workspace-dialogue-client.ts +26 -7
- package/src/client/dist/pwa/assets/ActivityFeed-tJmJhmtl.js +0 -2
- package/src/client/dist/pwa/assets/ComparisonPanel-BfP8APKY.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-BbtNfo0x.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-4Q-GJgyA.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-ECnbb_rx.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BH6grTuW.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-Cb_s_nox.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-BAZLGUyg.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-CQN6EKIn.js +0 -4
- package/src/client/dist/pwa/assets/custom-sounds-DR68eyF8.js +0 -1
- package/src/client/dist/pwa/assets/i18n-BY4JeBjk.js +0 -1
- package/src/client/dist/pwa/assets/index-C5fAw9ll.js +0 -20
- package/src/client/dist/pwa/assets/network-auth-BGPRhUa6.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-DZDzfxph.js +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { PROGRESS_MAX_RETAINED, PROGRESS_MAX_RUNNING, } from '../../shared/progress-bars.js';
|
|
4
|
+
import { getDb } from '../db/index.js';
|
|
5
|
+
import { emitEphemeral } from './websocket-service.js';
|
|
6
|
+
export class ProgressBarError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
code;
|
|
9
|
+
retryAfterMs;
|
|
10
|
+
constructor(status, code, message, retryAfterMs) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.retryAfterMs = retryAfterMs;
|
|
15
|
+
this.name = 'ProgressBarError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function publicBar(row) {
|
|
19
|
+
return {
|
|
20
|
+
id: row.id,
|
|
21
|
+
workspaceId: row.workspace_id,
|
|
22
|
+
sessionId: row.session_id,
|
|
23
|
+
label: row.label,
|
|
24
|
+
step: row.step,
|
|
25
|
+
current: row.current,
|
|
26
|
+
total: row.total,
|
|
27
|
+
status: row.status,
|
|
28
|
+
dismissed: row.dismissed_at !== null,
|
|
29
|
+
dismissedAt: row.dismissed_at,
|
|
30
|
+
completedAt: row.completed_at,
|
|
31
|
+
createdAt: row.created_at,
|
|
32
|
+
updatedAt: row.updated_at,
|
|
33
|
+
revision: row.revision,
|
|
34
|
+
sequence: row.sequence,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function invalid(message) {
|
|
38
|
+
throw new ProgressBarError(400, 'invalid_input', message);
|
|
39
|
+
}
|
|
40
|
+
function text(value, name, max) {
|
|
41
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max)
|
|
42
|
+
invalid(`${name} must be nonempty text up to ${max} characters`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function stepText(value) {
|
|
46
|
+
if (value === undefined || value === null)
|
|
47
|
+
return null;
|
|
48
|
+
if (typeof value !== 'string' || value.length > 500)
|
|
49
|
+
invalid('step must be text up to 500 characters or null');
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function pair(current, total) {
|
|
53
|
+
if (current === null && total === null)
|
|
54
|
+
return { current: null, total: null };
|
|
55
|
+
if (typeof current !== 'number' ||
|
|
56
|
+
typeof total !== 'number' ||
|
|
57
|
+
!Number.isFinite(current) ||
|
|
58
|
+
!Number.isFinite(total) ||
|
|
59
|
+
total <= 0 ||
|
|
60
|
+
current < 0 ||
|
|
61
|
+
current > total)
|
|
62
|
+
invalid('current and total must be paired finite numbers with 0 <= current <= total and total > 0, or both null');
|
|
63
|
+
return { current, total };
|
|
64
|
+
}
|
|
65
|
+
export function validateProgressScope(scope) {
|
|
66
|
+
text(scope.workspaceId, 'workspaceId', 200);
|
|
67
|
+
text(scope.sessionId, 'sessionId', 200);
|
|
68
|
+
if (!getDb()
|
|
69
|
+
.prepare('SELECT id FROM agent_sessions WHERE id=? AND workspace_id=?')
|
|
70
|
+
.get(scope.sessionId, scope.workspaceId))
|
|
71
|
+
throw new ProgressBarError(404, 'session_not_found', 'Session does not belong to this workspace or no longer exists');
|
|
72
|
+
}
|
|
73
|
+
function validateOwner(owner) {
|
|
74
|
+
validateProgressScope(owner);
|
|
75
|
+
if (owner.kind !== 'internal' && owner.kind !== 'external')
|
|
76
|
+
invalid('Invalid progress owner');
|
|
77
|
+
if (owner.kind === 'internal')
|
|
78
|
+
text(owner.launchId, 'launchId', 200);
|
|
79
|
+
}
|
|
80
|
+
function fingerprint(value) {
|
|
81
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
82
|
+
}
|
|
83
|
+
function load(scope, id, owner) {
|
|
84
|
+
validateProgressScope(scope);
|
|
85
|
+
text(id, 'progressId', 200);
|
|
86
|
+
const row = getDb()
|
|
87
|
+
.prepare('SELECT * FROM session_progress_bars WHERE id=? AND workspace_id=? AND session_id=?')
|
|
88
|
+
.get(id, scope.workspaceId, scope.sessionId);
|
|
89
|
+
if (!row)
|
|
90
|
+
throw new ProgressBarError(404, 'progress_not_found', 'Progress bar does not exist in this session');
|
|
91
|
+
if (owner?.kind === 'internal' && (row.owner_kind !== 'internal' || row.owner_launch_id !== owner.launchId))
|
|
92
|
+
throw new ProgressBarError(403, 'progress_owner_mismatch', 'Progress bar belongs to another launch');
|
|
93
|
+
return row;
|
|
94
|
+
}
|
|
95
|
+
function mutate(operation) {
|
|
96
|
+
const result = getDb().transaction(operation)();
|
|
97
|
+
const bar = publicBar(result.row);
|
|
98
|
+
if (result.changed)
|
|
99
|
+
emitEphemeral(bar.workspaceId, 'progress:changed', bar, bar.sessionId);
|
|
100
|
+
return bar;
|
|
101
|
+
}
|
|
102
|
+
export function createProgressBar(owner, input) {
|
|
103
|
+
return mutate(() => {
|
|
104
|
+
validateOwner(owner);
|
|
105
|
+
const normalized = {
|
|
106
|
+
activityKey: text(input.activityKey, 'activityKey', 100),
|
|
107
|
+
label: text(input.label, 'label', 200),
|
|
108
|
+
step: stepText(input.step),
|
|
109
|
+
...(input.current === undefined && input.total === undefined
|
|
110
|
+
? { current: null, total: null }
|
|
111
|
+
: pair(input.current, input.total)),
|
|
112
|
+
};
|
|
113
|
+
const hash = fingerprint(normalized), launch = owner.kind === 'internal' ? owner.launchId : '';
|
|
114
|
+
const existing = getDb()
|
|
115
|
+
.prepare('SELECT * FROM session_progress_bars WHERE workspace_id=? AND session_id=? AND owner_kind=? AND owner_launch_id=? AND activity_key=?')
|
|
116
|
+
.get(owner.workspaceId, owner.sessionId, owner.kind, launch, normalized.activityKey);
|
|
117
|
+
if (existing) {
|
|
118
|
+
if (existing.creation_fingerprint !== hash)
|
|
119
|
+
throw new ProgressBarError(409, 'activity_key_conflict', 'Activity key already used with different creation inputs');
|
|
120
|
+
return { row: existing, changed: false };
|
|
121
|
+
}
|
|
122
|
+
const counts = getDb()
|
|
123
|
+
.prepare("SELECT count(*) AS total,sum(status='running') AS running FROM session_progress_bars WHERE workspace_id=? AND session_id=?")
|
|
124
|
+
.get(owner.workspaceId, owner.sessionId);
|
|
125
|
+
if (counts.total >= PROGRESS_MAX_RETAINED || counts.running >= PROGRESS_MAX_RUNNING)
|
|
126
|
+
throw new ProgressBarError(409, 'progress_limit', 'Session progress bar limit reached; finish running activities or use another session');
|
|
127
|
+
const id = nanoid(), now = new Date().toISOString();
|
|
128
|
+
getDb()
|
|
129
|
+
.prepare(`INSERT INTO session_progress_bars(id,workspace_id,session_id,owner_kind,owner_launch_id,activity_key,creation_fingerprint,label,step,current,total,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,'running',?,?)`)
|
|
130
|
+
.run(id, owner.workspaceId, owner.sessionId, owner.kind, launch, normalized.activityKey, hash, normalized.label, normalized.step, normalized.current, normalized.total, now, now);
|
|
131
|
+
return { row: load(owner, id), changed: true };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function update(owner, id, input, terminal) {
|
|
135
|
+
return mutate(() => {
|
|
136
|
+
validateOwner(owner);
|
|
137
|
+
const row = load(owner, id, owner);
|
|
138
|
+
if (!Number.isSafeInteger(input.sequence) || input.sequence <= 0)
|
|
139
|
+
invalid('sequence must be a positive safe integer');
|
|
140
|
+
const normalized = { sequence: input.sequence, operation: terminal ? 'finish' : 'update' };
|
|
141
|
+
if (input.step !== undefined)
|
|
142
|
+
normalized.step = stepText(input.step);
|
|
143
|
+
let progress = { current: row.current, total: row.total };
|
|
144
|
+
if (terminal) {
|
|
145
|
+
const outcome = input.outcome;
|
|
146
|
+
if (outcome !== 'succeeded' && outcome !== 'failed')
|
|
147
|
+
invalid('outcome must be succeeded or failed');
|
|
148
|
+
normalized.outcome = outcome;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const running = input;
|
|
152
|
+
if (running.current !== undefined || running.total !== undefined) {
|
|
153
|
+
progress = pair(running.current, running.total);
|
|
154
|
+
normalized.current = progress.current;
|
|
155
|
+
normalized.total = progress.total;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const hash = fingerprint(normalized);
|
|
159
|
+
if (input.sequence === row.sequence && hash === row.producer_fingerprint)
|
|
160
|
+
return { row, changed: false };
|
|
161
|
+
if (row.status !== 'running')
|
|
162
|
+
throw new ProgressBarError(409, 'progress_terminal', 'Progress bar is terminal and cannot be changed');
|
|
163
|
+
if (input.sequence <= row.sequence)
|
|
164
|
+
throw new ProgressBarError(409, 'sequence_conflict', 'Progress sequence is stale or conflicts with an accepted report');
|
|
165
|
+
const now = new Date().toISOString();
|
|
166
|
+
const retryAfter = row.producer_updated_at ? 1000 - (Date.now() - Date.parse(row.producer_updated_at)) : 0;
|
|
167
|
+
if (!terminal && retryAfter > 0)
|
|
168
|
+
throw new ProgressBarError(429, 'progress_rate_limited', `Progress update rate limit: retry after ${retryAfter}ms`, retryAfter);
|
|
169
|
+
getDb()
|
|
170
|
+
.prepare(`UPDATE session_progress_bars SET step=?,current=?,total=?,status=?,completed_at=?,updated_at=?,revision=revision+1,sequence=?,producer_fingerprint=?,producer_updated_at=? WHERE id=?`)
|
|
171
|
+
.run(input.step === undefined ? row.step : stepText(input.step), progress.current, progress.total, terminal ? input.outcome : 'running', terminal ? now : null, now, input.sequence, hash, now, id);
|
|
172
|
+
return { row: load(owner, id), changed: true };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export function updateProgressBar(owner, id, input) {
|
|
176
|
+
return update(owner, id, input, false);
|
|
177
|
+
}
|
|
178
|
+
export function finishProgressBar(owner, id, input) {
|
|
179
|
+
return update(owner, id, input, true);
|
|
180
|
+
}
|
|
181
|
+
export function dismissProgressBar(scope, id) {
|
|
182
|
+
return mutate(() => {
|
|
183
|
+
const row = load(scope, id);
|
|
184
|
+
if (row.dismissed_at)
|
|
185
|
+
return { row, changed: false };
|
|
186
|
+
const now = new Date().toISOString();
|
|
187
|
+
getDb()
|
|
188
|
+
.prepare('UPDATE session_progress_bars SET dismissed_at=?,updated_at=?,revision=revision+1 WHERE id=?')
|
|
189
|
+
.run(now, now, id);
|
|
190
|
+
return { row: load(scope, id), changed: true };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
export function listProgressBars(input) {
|
|
194
|
+
validateProgressScope(input);
|
|
195
|
+
const limit = input.limit ?? 50, offset = input.offset ?? 0;
|
|
196
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(offset) || offset < 0)
|
|
197
|
+
invalid('Invalid progress pagination: limit 1..100, nonnegative offset required');
|
|
198
|
+
const rows = getDb()
|
|
199
|
+
.prepare('SELECT * FROM session_progress_bars WHERE workspace_id=? AND session_id=? ORDER BY created_at,id LIMIT ? OFFSET ?')
|
|
200
|
+
.all(input.workspaceId, input.sessionId, limit, offset);
|
|
201
|
+
const { totalCount } = getDb()
|
|
202
|
+
.prepare('SELECT count(*) AS totalCount FROM session_progress_bars WHERE workspace_id=? AND session_id=?')
|
|
203
|
+
.get(input.workspaceId, input.sessionId);
|
|
204
|
+
return {
|
|
205
|
+
items: rows.map(publicBar),
|
|
206
|
+
totalCount,
|
|
207
|
+
nextOffset: offset + rows.length < totalCount ? offset + rows.length : null,
|
|
208
|
+
serverTime: new Date().toISOString(),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function interruptProgressBarsForLaunch(launchId) {
|
|
212
|
+
text(launchId, 'launchId', 200);
|
|
213
|
+
const rows = getDb()
|
|
214
|
+
.prepare("SELECT * FROM session_progress_bars WHERE owner_kind='internal' AND owner_launch_id=? AND status='running'")
|
|
215
|
+
.all(launchId);
|
|
216
|
+
for (const row of rows)
|
|
217
|
+
mutate(() => {
|
|
218
|
+
const now = new Date().toISOString();
|
|
219
|
+
getDb()
|
|
220
|
+
.prepare("UPDATE session_progress_bars SET status='interrupted',completed_at=?,updated_at=?,revision=revision+1 WHERE id=?")
|
|
221
|
+
.run(now, now, row.id);
|
|
222
|
+
return { row: load({ workspaceId: row.workspace_id, sessionId: row.session_id }, row.id), changed: true };
|
|
223
|
+
});
|
|
224
|
+
return rows.length;
|
|
225
|
+
}
|
|
226
|
+
export function reconcileOrphanedProgressBars(activeLaunchIds = []) {
|
|
227
|
+
const active = new Set(activeLaunchIds);
|
|
228
|
+
const rows = getDb()
|
|
229
|
+
.prepare("SELECT DISTINCT owner_launch_id FROM session_progress_bars WHERE owner_kind='internal' AND status='running'")
|
|
230
|
+
.all();
|
|
231
|
+
return rows.reduce((total, row) => total + (active.has(row.owner_launch_id) ? 0 : interruptProgressBarsForLaunch(row.owner_launch_id)), 0);
|
|
232
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getDb } from '../db/index.js';
|
|
2
|
+
import { deferForUpdateMaintenance } from './update-maintenance-service.js';
|
|
2
3
|
import { emitEphemeral } from './websocket-service.js';
|
|
3
4
|
import { getWorkspace } from './workspace-service.js';
|
|
4
5
|
const timers = new Map();
|
|
@@ -142,6 +143,12 @@ export function restoreOnBoot(onFire) {
|
|
|
142
143
|
function fireOrSkip(workspaceId) {
|
|
143
144
|
if (suspended)
|
|
144
145
|
return;
|
|
146
|
+
const pendingTimer = timers.get(workspaceId);
|
|
147
|
+
if (deferForUpdateMaintenance(`quota-backoff:${workspaceId}`, () => {
|
|
148
|
+
if (timers.get(workspaceId) === pendingTimer)
|
|
149
|
+
fireOrSkip(workspaceId);
|
|
150
|
+
}))
|
|
151
|
+
return;
|
|
145
152
|
timers.delete(workspaceId);
|
|
146
153
|
const pending = getPending(workspaceId);
|
|
147
154
|
if (!pending)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Process-local reservation: restart clears it; only its owner may cancel it. */
|
|
2
|
+
let owner;
|
|
3
|
+
const deferred = new Map();
|
|
4
|
+
const creations = new Set();
|
|
5
|
+
/** Reserve the entire creation/checkout, including asynchronous setup and rollback. */
|
|
6
|
+
export function beginWorkspaceCreation() {
|
|
7
|
+
if (owner !== undefined)
|
|
8
|
+
return null;
|
|
9
|
+
const creation = Symbol('workspace-creation');
|
|
10
|
+
creations.add(creation);
|
|
11
|
+
return () => {
|
|
12
|
+
creations.delete(creation);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function isUpdateMaintenanceActive() {
|
|
16
|
+
return owner !== undefined;
|
|
17
|
+
}
|
|
18
|
+
/** No await between inspecting runtime ownership and closing admission. */
|
|
19
|
+
export function acquireUpdateMaintenance(candidate, isIdle) {
|
|
20
|
+
if (owner !== undefined)
|
|
21
|
+
return owner === candidate ? 'acquired' : 'conflict';
|
|
22
|
+
if (creations.size || !isIdle())
|
|
23
|
+
return 'busy';
|
|
24
|
+
owner = candidate;
|
|
25
|
+
return 'acquired';
|
|
26
|
+
}
|
|
27
|
+
/** Keep scheduler rows/deadlines untouched while delivery is suspended. */
|
|
28
|
+
export function deferForUpdateMaintenance(key, callback) {
|
|
29
|
+
if (owner === undefined)
|
|
30
|
+
return false;
|
|
31
|
+
deferred.set(key, callback);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
export function releaseUpdateMaintenance(candidate) {
|
|
35
|
+
if (owner !== candidate)
|
|
36
|
+
return false;
|
|
37
|
+
owner = undefined;
|
|
38
|
+
for (const callback of deferred.values()) {
|
|
39
|
+
const timer = setTimeout(callback, 0);
|
|
40
|
+
timer.unref?.();
|
|
41
|
+
}
|
|
42
|
+
deferred.clear();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
@@ -5,6 +5,7 @@ import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
|
5
5
|
import * as orchestrator from './agent/orchestrator.js';
|
|
6
6
|
import * as autoLoopService from './auto-loop-service.js';
|
|
7
7
|
import * as settingsService from './settings-service.js';
|
|
8
|
+
import { deferForUpdateMaintenance, isUpdateMaintenanceActive } from './update-maintenance-service.js';
|
|
8
9
|
import { emitEphemeral } from './websocket-service.js';
|
|
9
10
|
const MIN_DELAY_SECONDS = 60;
|
|
10
11
|
const MAX_DELAY_SECONDS = 21600;
|
|
@@ -22,6 +23,17 @@ export function suspendForShutdown() {
|
|
|
22
23
|
clearTimeout(timer);
|
|
23
24
|
timers.clear();
|
|
24
25
|
}
|
|
26
|
+
/** Claim valid pending wakeups before planned downtime, without changing their requested deadline. */
|
|
27
|
+
export function preserveForUpdateMaintenance() {
|
|
28
|
+
const cutoff = new Date(Date.now() - STALE_WAKEUP_GRACE_MS).toISOString();
|
|
29
|
+
getDb()
|
|
30
|
+
.prepare(`UPDATE pending_wakeups SET retry_at = target_at
|
|
31
|
+
WHERE retry_at IS NULL AND julianday(target_at) >= julianday(?)
|
|
32
|
+
AND workspace_id IN (
|
|
33
|
+
SELECT id FROM workspaces WHERE archived_at IS NULL AND worktree_purged_at IS NULL
|
|
34
|
+
)`)
|
|
35
|
+
.run(cutoff);
|
|
36
|
+
}
|
|
25
37
|
function clamp(n, lo, hi) {
|
|
26
38
|
return Math.max(lo, Math.min(hi, n));
|
|
27
39
|
}
|
|
@@ -60,10 +72,10 @@ export function schedule(workspaceId, delaySeconds, prompt, reason, agentSession
|
|
|
60
72
|
// the original durable wakeup and its timer intact, and fail the MCP receipt.
|
|
61
73
|
const row = getDb()
|
|
62
74
|
.prepare(`INSERT OR REPLACE INTO pending_wakeups
|
|
63
|
-
(workspace_id, target_at, prompt, reason, created_at, agent_session_id)
|
|
64
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
75
|
+
(workspace_id, target_at, prompt, reason, created_at, agent_session_id, retry_at)
|
|
76
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
65
77
|
RETURNING target_at, reason`)
|
|
66
|
-
.get(workspaceId, targetAtIso, effectivePrompt, reason ?? null, new Date().toISOString(), agentSessionId ?? null);
|
|
78
|
+
.get(workspaceId, targetAtIso, effectivePrompt, reason ?? null, new Date().toISOString(), agentSessionId ?? null, isUpdateMaintenanceActive() ? targetAtIso : null);
|
|
67
79
|
const pending = { targetAt: row.target_at, reason: row.reason ?? undefined };
|
|
68
80
|
const existing = timers.get(workspaceId);
|
|
69
81
|
if (existing)
|
|
@@ -168,6 +180,12 @@ export function rehydrate() {
|
|
|
168
180
|
function fire(workspaceId) {
|
|
169
181
|
if (suspended)
|
|
170
182
|
return;
|
|
183
|
+
const pendingTimer = timers.get(workspaceId);
|
|
184
|
+
if (deferForUpdateMaintenance(`wakeup:${workspaceId}`, () => {
|
|
185
|
+
if (timers.get(workspaceId) === pendingTimer)
|
|
186
|
+
fire(workspaceId);
|
|
187
|
+
}))
|
|
188
|
+
return;
|
|
171
189
|
try {
|
|
172
190
|
const db = getDb();
|
|
173
191
|
const row = db.prepare('SELECT * FROM pending_wakeups WHERE workspace_id = ?').get(workspaceId);
|
|
@@ -3,6 +3,7 @@ import { validateWorkspaceCreationArguments } from '../../shared/workspace-creat
|
|
|
3
3
|
import { CLAUDE_CODE_CAPABILITIES } from './agent/engines/claude-code/capabilities.js';
|
|
4
4
|
import { CODEX_CAPABILITIES } from './agent/engines/codex/capabilities.js';
|
|
5
5
|
import { rollbackCreatedPrCheckout } from './pr-checkout-rollback-service.js';
|
|
6
|
+
import { beginWorkspaceCreation } from './update-maintenance-service.js';
|
|
6
7
|
export class WorkspaceCreationMcpError extends Error {
|
|
7
8
|
status;
|
|
8
9
|
stage;
|
|
@@ -79,6 +80,21 @@ function mapInput(input) {
|
|
|
79
80
|
}
|
|
80
81
|
/** Reuses the application's route validation, lifecycle guards, Git locks and rollback. */
|
|
81
82
|
export async function executeWorkspaceCreationTool(name, raw, dispatch) {
|
|
83
|
+
if (name !== 'create_workspace')
|
|
84
|
+
return executeCreation(name, raw, dispatch);
|
|
85
|
+
// One lease spans both in-process routes and any compensating checkout rollback.
|
|
86
|
+
// Each route also guards direct HTTP callers; nested leases are safe.
|
|
87
|
+
const release = beginWorkspaceCreation();
|
|
88
|
+
if (!release)
|
|
89
|
+
throw new WorkspaceCreationMcpError('Workspace creation is unavailable during update maintenance', 503, 'maintenance');
|
|
90
|
+
try {
|
|
91
|
+
return await executeCreation(name, raw, dispatch);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
release();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function executeCreation(name, raw, dispatch) {
|
|
82
98
|
let input;
|
|
83
99
|
try {
|
|
84
100
|
input = validateWorkspaceCreationArguments(name, raw);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `buildAutoLoopGroomingSteps(e2e)` is the numbered workflow, ready to be
|
|
10
10
|
* spliced into a larger prompt with a one-line intro. When `e2e.framework`
|
|
11
11
|
* is set, an additional step 4 covers E2E regression coverage and the
|
|
12
|
-
* `
|
|
12
|
+
* `mark_auto_loop_ready` call moves to step 5.
|
|
13
13
|
*
|
|
14
14
|
* `buildE2eIterationBlock(e2e)` returns the override block injected into
|
|
15
15
|
* the per-iteration prompt for tasks whose title starts with `[E2E]`.
|
|
@@ -36,15 +36,15 @@ export function buildGroomingIntro(suite, customOverride) {
|
|
|
36
36
|
export const PREP_AUTOLOOP_INTRO = getGroomingIntro('superpowers');
|
|
37
37
|
export function buildAutoLoopGroomingSteps(e2e, finalization) {
|
|
38
38
|
const steps = [
|
|
39
|
-
`1. Call \`
|
|
39
|
+
`1. Call \`list_tasks\` FIRST to inspect any pre-existing tasks (they may have been seeded from Notion, a template, or the CreatePage form).`,
|
|
40
40
|
`2. If tasks already exist: DO NOT delete or recreate them from scratch. Read each one, judge whether it is atomic and implementable in one session with clear completion criteria. Improve them in place:
|
|
41
|
-
- Use \`
|
|
42
|
-
- Use \`
|
|
43
|
-
- Use \`
|
|
41
|
+
- Use \`update_task\` to rename unclear titles, add completion criteria, or flip \`is_acceptance_criterion\` when needed.
|
|
42
|
+
- Use \`create_task\` to SPLIT a task that is too large into smaller atomic pieces (keep the original only if it still makes sense, otherwise update it to one of the split pieces and create the rest).
|
|
43
|
+
- Use \`create_task\` to ADD missing acceptance criteria or missing implementation steps the plan requires.`,
|
|
44
44
|
`3. If no tasks exist:
|
|
45
45
|
- If a plan file exists in \`docs/superpowers/plans/\` or similar, read it and derive the task list from it.
|
|
46
46
|
- If no plan exists, ask the user what the workspace goal is and propose tasks accordingly.
|
|
47
|
-
- Create the tasks via \`
|
|
47
|
+
- Create the tasks via \`create_task\`. For each task, decide \`is_acceptance_criterion\` appropriately.`,
|
|
48
48
|
];
|
|
49
49
|
let nextNum = 4;
|
|
50
50
|
if (e2e.framework) {
|
|
@@ -59,7 +59,7 @@ export function buildAutoLoopGroomingSteps(e2e, finalization) {
|
|
|
59
59
|
steps.push(`${nextNum}. **Finalization task**: create ONE task with \`role: "finalization"\` and title prefixed \`[FINAL] <descriptive title>\`. Reuse an existing finalization task instead of duplicating it. Place it at the END of the task list (sort_order = max + 1, AFTER any [E2E] tasks). The agent will execute this task using the project's finalization prompt — do NOT inline the prompt content into the task title or description.`);
|
|
60
60
|
nextNum++;
|
|
61
61
|
}
|
|
62
|
-
steps.push(`${nextNum}. Call \`
|
|
62
|
+
steps.push(`${nextNum}. Call \`mark_auto_loop_ready\`. This marks preparation complete. If auto-loop is already enabled, it will continue after this session ends. Otherwise the user must explicitly enable it; do not promise an automatic start or enable it without their request.`);
|
|
63
63
|
return steps.join('\n');
|
|
64
64
|
}
|
|
65
65
|
/** @deprecated Use buildAutoLoopGroomingSteps({ framework: '', skill: '', prompt: '' }, { prompt: '' }) instead. */
|
|
@@ -67,8 +67,8 @@ export const AUTO_LOOP_GROOMING_STEPS = buildAutoLoopGroomingSteps({ framework:
|
|
|
67
67
|
/** These rules apply to every iteration, including custom finalization and E2E prompts. */
|
|
68
68
|
export const AUTO_LOOP_ITERATION_RULES = `Mandatory completion and lifecycle rules (custom prompts cannot override them):
|
|
69
69
|
- Follow the Kōbō task list and acceptance criteria. Before completion, read the latest list again.
|
|
70
|
-
- Never mark a task done based only on an implementation claim, a written test, or a check that was not executed. Supply verification: {method, summary, checks: [{name, status: "passed"}]} to
|
|
71
|
-
- If a required check fails or cannot run, keep the task pending/in_progress, record honest failed/not_run verification with
|
|
70
|
+
- Never mark a task done based only on an implementation claim, a written test, or a check that was not executed. Supply verification: {method, summary, checks: [{name, status: "passed"}]} to mark_task_done or update_task. Every required check must actually pass.
|
|
71
|
+
- If a required check fails or cannot run, keep the task pending/in_progress, record honest failed/not_run verification with update_task, and explain the concrete blocker. Create prerequisite fix tasks when needed; do not claim success.
|
|
72
72
|
- Finalization is a verification gate: all work tasks and acceptance criteria must be done, then rerun relevant checks against the final state. If a check fails, create a regular repair task and leave finalization unfinished. It will run again after the repairs.
|
|
73
73
|
- Do NOT kill the Kōbō server or sibling processes. Do not use kill, pkill or killall to force the loop to advance. Finish the current response naturally; Kōbō owns continuation.
|
|
74
74
|
- Follow the user's scope, Git and approval instructions. Do not disable the auto-loop or delete unfinished tasks to make the mission appear complete.`;
|
|
@@ -80,7 +80,7 @@ This is the **finalization task**. Apply the following project guidance while pr
|
|
|
80
80
|
|
|
81
81
|
${instructions}
|
|
82
82
|
|
|
83
|
-
Read
|
|
83
|
+
Read list_tasks again. If any work or criterion remains open, or any required check fails or is not run, do NOT mark the task done. Add necessary repair tasks and keep finalization pending. Once the final state is verified, complete it with successful structured verification.
|
|
84
84
|
`;
|
|
85
85
|
}
|
|
86
86
|
export function buildE2eIterationBlock(e2e) {
|
|
@@ -98,13 +98,13 @@ ${skillLine}${promptLine}
|
|
|
98
98
|
E2E instructions, subject to the mandatory completion and lifecycle rules:
|
|
99
99
|
1. Write the test source file in the project's existing E2E directory (look at \`cypress/\`, \`e2e/\`, \`tests/e2e/\`, or follow the skill / guidance above). Reuse existing fixtures and patterns.
|
|
100
100
|
2. Run the tests and provide the actual successful results as structured verification before completion.
|
|
101
|
-
3. If the environment is broken (Docker down, browser missing, port busy, dependencies absent), do NOT mark the task done. Record \`not_run\` checks and the precise blocker using \`
|
|
101
|
+
3. If the environment is broken (Docker down, browser missing, port busy, dependencies absent), do NOT mark the task done. Record \`not_run\` checks and the precise blocker using \`update_task\`, leave this task pending/in_progress, and create a prerequisite task when appropriate. A test source file by itself is not a verified result.
|
|
102
102
|
4. The code-review gate still applies. Review both the relevance of the test and the evidence that it passed.
|
|
103
103
|
|
|
104
104
|
`;
|
|
105
105
|
}
|
|
106
106
|
export const AUTO_LOOP_HARD_RULES = `Hard rules:
|
|
107
|
-
- Do NOT
|
|
107
|
+
- Do NOT change source code. Write planning documents only if the current phase and launch permissions allow it.
|
|
108
108
|
- Do NOT run \`kill\`, \`pkill\`, \`killall\`, \`pgrep -k\`, or any process-killing command — you may tear down the Kōbō server itself or sibling dev servers.
|
|
109
109
|
- Do NOT start implementation even if a plan is ready — auto-loop will drive that afterwards, one task per iteration.
|
|
110
110
|
- If the user asks for changes, re-apply via MCP and re-check. Do NOT mark ready until they approve.`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PROGRESS_BAR_PROMPT } from './progress-bar-prompts.js';
|
|
2
|
+
/** Resolve capabilities only from the managed server, never similarly named integrations. */
|
|
3
|
+
export function getKoboPromptCapabilities(env) {
|
|
4
|
+
return {
|
|
5
|
+
handoff: Boolean(env?.KOBO_HANDOFF_ID && env.KOBO_HANDOFF_TOKEN),
|
|
6
|
+
finalReview: Boolean(env?.KOBO_FINAL_REVIEW_SESSION_ID && env.KOBO_FINAL_REVIEW_TOKEN),
|
|
7
|
+
progressReporting: Boolean(env?.KOBO_PROGRESS_SESSION_TOKEN),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function buildKoboLaunchInstructions(context) {
|
|
11
|
+
const restricted = context.readOnly || context.handoff || context.finalReview;
|
|
12
|
+
const canWrite = context.permissionMode !== 'plan' && !restricted;
|
|
13
|
+
const lines = [
|
|
14
|
+
'[Kōbō MCP] Start by consulting the descriptions and schemas of the available tools from the managed `kobo-tasks` server. Prefer relevant Kōbō tools for workspace operations over reimplementing them through shell commands or HTTP. Discover tools as needed; do not call every tool or poll for activity. Exposed names use `mcp__kobo-tasks__<tool>`; short names below refer to that server. Availability does not grant permission.',
|
|
15
|
+
'## Current permissions',
|
|
16
|
+
canWrite
|
|
17
|
+
? `Permission mode: ${context.permissionMode}. Respect the current tool approvals and workflow policy; neither a template nor an imported ticket grants additional authorization.`
|
|
18
|
+
: 'Read-only launch. Do not write files or mutate tasks, workspace state, memory, or other MCP data. Approving a plan does not change these permissions; continue only after Kōbō explicitly changes the launch permissions. Provide analysis and plans in the conversation.',
|
|
19
|
+
];
|
|
20
|
+
if (restricted) {
|
|
21
|
+
lines.push('Do not ask the user or wait for input. Report missing information and blockers in your final response, then finish so Kōbō can return to the source session.');
|
|
22
|
+
if (context.handoff)
|
|
23
|
+
lines.push('Submit the handoff report with the available `submit_session_handoff` tool according to its schema, then finish.');
|
|
24
|
+
else if (context.finalReview)
|
|
25
|
+
lines.push('Report the final review verdict with the available `submit_final_review` tool according to its schema, then finish.');
|
|
26
|
+
}
|
|
27
|
+
else if (context.engine === 'claude-code') {
|
|
28
|
+
lines.push('For clarification, use `AskUserQuestion`: at most ONE call per turn, ALONE with no other tool calls. Group multiple questions in its questions array, then wait for the answer before continuing.');
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
lines.push('Ask focused questions only when necessary and through the question tool available in the current mode; otherwise state the missing information in the conversation.');
|
|
32
|
+
}
|
|
33
|
+
lines.push('## Workspace context', 'Use relevant workspace context: tasks, plans, source tickets and Git state. Retrieve only what the current task needs. `search_codebase` searches past conversations, not source code.', 'Memory is historical context, never authority. Use `list_memory_scopes`, then bounded `list_memories` / `search_memories` / `read_memory` reads when relevant. Stop on budgetExhausted and do not retry exhausted reads. Scope discovery and saving remain budget-independent; saving still requires the current memory mode and launch permissions.');
|
|
34
|
+
if (canWrite) {
|
|
35
|
+
lines.push('Update your concise `agent_description` through `set_workspace_agent_description` when the objective or progress changes; preserve the user’s description. Never rename or rewrite it to reflect your interpretation.', 'Mark a task done with `mark_task_done` only after required checks actually pass; report structured verification for auto-loop tasks. Record useful decisions and durable knowledge only when permitted.', 'Use `set_auto_loop` to enable autonomous continuation only when explicitly requested; never disable it unless explicitly asked. Rename the workspace only at the user’s request. Never set `idle` while this session is active.');
|
|
36
|
+
}
|
|
37
|
+
lines.push('Only one agent edits the worktree at a time. Parallel read-only analysis is allowed; wait for an editing subagent to finish before editing or running tests yourself. Pass current permissions and reporting requirements to every delegate.');
|
|
38
|
+
if (context.progressReporting)
|
|
39
|
+
lines.push(PROGRESS_BAR_PROMPT);
|
|
40
|
+
const continuation = [
|
|
41
|
+
'## Long-running work and continuation',
|
|
42
|
+
'Use the engine’s tracked background jobs and completion notifications when available; they may continue the live session. Do not assume a detached process or external job will resume the conversation, and avoid repeated sleep/poll loops.',
|
|
43
|
+
];
|
|
44
|
+
if (canWrite) {
|
|
45
|
+
if (context.autoLoop)
|
|
46
|
+
continuation.push('Auto-loop owns the next iteration. Finish the current phase normally; do not schedule a duplicate wakeup merely to continue the loop.');
|
|
47
|
+
continuation.push('For detached work requiring a later check, the parent coordinates the single workspace `schedule_wakeup` (subagents must not schedule or replace wakeups). Use delaySeconds measured from the call, a concrete follow-up prompt, and optional reason. An existing wakeup is replaced; preserve unrelated scheduled intent. Cancel your wakeup if its check is no longer needed. The deadline is a target, not a guarantee of immediate execution while busy.');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
continuation.push('This launch cannot schedule follow-up work. Use available completion notifications for tracked work; report unfinished work or blockers honestly and finish the requested analysis/report.');
|
|
51
|
+
}
|
|
52
|
+
const continuationText = continuation.join('\n');
|
|
53
|
+
// Claude supports a durable application append; Codex receives the same rules in its input.
|
|
54
|
+
if (context.engine === 'codex')
|
|
55
|
+
lines.push(continuationText);
|
|
56
|
+
return { promptPrefix: lines.join('\n\n'), systemAppend: context.engine === 'claude-code' ? continuationText : '' };
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const PROGRESS_BAR_PLAN_EXCEPTION = 'Operational exception in Plan/read-only/review: the launch-scoped create_progress_bar, update_progress_bar, finish_progress_bar and list_progress_bars tools only report activity metadata and are allowed when provided by the managed kobo-tasks server. This does not authorize file, task, workspace, memory, or other MCP mutations.';
|
|
2
|
+
/** Shared across engines, fresh/resumed launches and delegated agents. */
|
|
3
|
+
export const PROGRESS_BAR_PROMPT = `<kobo-progress-reporting>
|
|
4
|
+
${PROGRESS_BAR_PLAN_EXCEPTION}
|
|
5
|
+
Use Kōbō's create_progress_bar, update_progress_bar and finish_progress_bar for long-running tasks, including tests and delegated work. Give each activity a clear label and stable activity_key; follow tool schemas for ordered updates and retries. Report meaningful steps and measured current/total only when known; otherwise keep progress indeterminate. Never invent percentages or poll these tools. Finish with succeeded or failed only when known. A dismissed bar stays hidden: continue without recreating it; dismissal is not cancellation. Reporting never substitutes for verification, and an interrupted reporter does not prove background work stopped.
|
|
6
|
+
Pass this requirement to all subagents when delegating. Subagents must not schedule or replace workspace wakeups; the parent coordinates follow-up. Each subagent reports its own long task through the inherited Kōbō MCP tools; the parent must not duplicate that activity. If a custom subagent lacks these tools, the parent reports its observed progress accurately instead. After resume, start new activity keys for new work; do not reopen a finished activity.
|
|
7
|
+
</kobo-progress-reporting>`;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const id = { type: 'string', minLength: 1, maxLength: 200 };
|
|
2
|
+
const numberOrNull = { type: ['number', 'null'] };
|
|
3
|
+
const step = { type: ['string', 'null'], maxLength: 500 };
|
|
4
|
+
const sequence = {
|
|
5
|
+
type: 'integer',
|
|
6
|
+
minimum: 1,
|
|
7
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
8
|
+
description: 'Strictly increasing producer sequence. Retry unchanged payload with the same sequence.',
|
|
9
|
+
};
|
|
10
|
+
function tools(external) {
|
|
11
|
+
const scope = external ? { workspace_id: id, session_id: id } : {};
|
|
12
|
+
const requiredScope = external ? ['workspace_id', 'session_id'] : [];
|
|
13
|
+
const definition = (name, description, properties, required, readOnly = false) => ({
|
|
14
|
+
name,
|
|
15
|
+
description,
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: { ...scope, ...properties },
|
|
19
|
+
required: [...requiredScope, ...required],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
},
|
|
22
|
+
annotations: { readOnlyHint: readOnly, destructiveHint: false, openWorldHint: false },
|
|
23
|
+
});
|
|
24
|
+
return [
|
|
25
|
+
definition('create_progress_bar', 'Report a labelled long-running activity in this session. Use a new activity_key per activity; identical retries return its current state, including dismissal. Report measured current/total together, or omit both for indeterminate progress. Closing a bar never cancels work.', {
|
|
26
|
+
activity_key: { type: 'string', minLength: 1, maxLength: 100 },
|
|
27
|
+
label: { type: 'string', minLength: 1, maxLength: 200 },
|
|
28
|
+
step,
|
|
29
|
+
current: numberOrNull,
|
|
30
|
+
total: numberOrNull,
|
|
31
|
+
}, ['activity_key', 'label']),
|
|
32
|
+
definition('update_progress_bar', 'Update absolute measured progress and/or step. Both numbers must be provided together, or both null. At most one accepted running update per second; respect retry guidance. Dismissed bars remain hidden. Never invent a percentage.', { progress_id: id, sequence, step, current: numberOrNull, total: numberOrNull }, ['progress_id', 'sequence']),
|
|
33
|
+
definition('finish_progress_bar', 'Finish reporting with succeeded or failed. Terminal state is immutable except an exact retry. Success hides after five seconds; failure remains until the user closes it. Reporting does not mark workspace tasks done.', { progress_id: id, sequence, step, outcome: { type: 'string', enum: ['succeeded', 'failed'] } }, ['progress_id', 'sequence', 'outcome']),
|
|
34
|
+
definition('list_progress_bars', 'Read progress bars in the explicitly selected session, including dismissed/terminal bars. Responses contain bounded pages and serverTime; do not poll without a concrete need.', {
|
|
35
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
|
36
|
+
offset: { type: 'integer', minimum: 0, default: 0 },
|
|
37
|
+
}, [], true),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
export const PROGRESS_BAR_TOOLS = tools(true);
|
|
41
|
+
export const INTERNAL_PROGRESS_BAR_TOOLS = tools(false);
|
|
42
|
+
export function isProgressBarTool(name) {
|
|
43
|
+
return PROGRESS_BAR_TOOLS.some((tool) => tool.name === name);
|
|
44
|
+
}
|
|
45
|
+
/** Shared transport validation; numeric relationships and scope ownership remain server-owned. */
|
|
46
|
+
export function validateProgressBarArguments(name, input, external) {
|
|
47
|
+
const definition = (external ? PROGRESS_BAR_TOOLS : INTERNAL_PROGRESS_BAR_TOOLS).find((tool) => tool.name === name);
|
|
48
|
+
if (!definition)
|
|
49
|
+
throw new TypeError('Unknown progress tool');
|
|
50
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
51
|
+
throw new TypeError('Expected an object');
|
|
52
|
+
const args = input;
|
|
53
|
+
const properties = definition.inputSchema.properties ?? {};
|
|
54
|
+
for (const key of Object.keys(args)) {
|
|
55
|
+
if (!Object.hasOwn(properties, key))
|
|
56
|
+
throw new TypeError(`Unknown argument: ${key}`);
|
|
57
|
+
const schema = properties[key];
|
|
58
|
+
const value = args[key];
|
|
59
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
60
|
+
const validType = types.some((type) => type === 'null' ? value === null : type === 'integer' ? Number.isSafeInteger(value) : typeof value === type);
|
|
61
|
+
if (!validType ||
|
|
62
|
+
(typeof value === 'number' &&
|
|
63
|
+
(!Number.isFinite(value) || value < (schema.minimum ?? -Infinity) || value > (schema.maximum ?? Infinity))) ||
|
|
64
|
+
(typeof value === 'string' &&
|
|
65
|
+
(value.trim().length < (schema.minLength ?? 0) || value.length > (schema.maxLength ?? Infinity))) ||
|
|
66
|
+
(schema.enum && !schema.enum.includes(value)))
|
|
67
|
+
throw new TypeError(`Invalid argument: ${key}`);
|
|
68
|
+
}
|
|
69
|
+
for (const key of definition.inputSchema.required ?? [])
|
|
70
|
+
if (args[key] === undefined)
|
|
71
|
+
throw new TypeError(`Missing argument: ${key}`);
|
|
72
|
+
return args;
|
|
73
|
+
}
|