@goodandready/dsh-goal 0.2.11 → 0.2.13
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/lib/addon-store.js +213 -0
- package/lib/client.js +142 -3
- package/lib/engine-reports.js +112 -33
- package/lib/engine-store.js +4 -3
- package/lib/git-snapshot-manager.js +99 -0
- package/lib/goal-engine.js +10 -3
- package/lib/index.js +215 -394
- package/lib/milestone-manager.js +141 -0
- package/lib/routes.js +278 -137
- package/lib/token-budget-guard.js +128 -0
- package/lib/tools.js +115 -257
- package/package.json +4 -3
package/lib/routes.js
CHANGED
|
@@ -2,7 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
4
4
|
import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
|
|
5
|
-
import { getTemplatesCatalogue } from './engine-templates.js';
|
|
5
|
+
import { getTemplatesCatalogue, instantiateTemplate } from './engine-templates.js';
|
|
6
|
+
import { parseIssueChecklist } from './engine-issue-sync.js';
|
|
6
7
|
|
|
7
8
|
export function isLoopback(value) {
|
|
8
9
|
if (!value) return false;
|
|
@@ -84,71 +85,86 @@ export function getSafeCwd(requestedCwd, allowedRoots = []) {
|
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
87
|
* Register webServer HTTP routes and SSE stream for DSH Goal
|
|
87
|
-
* @param {any} ctx Cordis context
|
|
88
|
-
* @param {object} deps Dependencies
|
|
89
|
-
* @param {any} deps.engine GoalEngine instance
|
|
90
|
-
* @param {Function} deps.getConfig Function returning current config
|
|
91
|
-
* @param {Function} deps.stopRunningAgents Stop agents function
|
|
92
|
-
* @param {Function} deps.resumeActiveAgent Resume agent function
|
|
93
|
-
* @param {Map} deps.sessionAgents Session agent map
|
|
94
|
-
* @param {Map} [deps.sseClients] SSE clients map (sid -> Set)
|
|
95
88
|
*/
|
|
96
89
|
export function registerRoutes(ctx, {
|
|
97
90
|
engine,
|
|
91
|
+
goalsService,
|
|
98
92
|
getConfig,
|
|
99
93
|
stopRunningAgents,
|
|
100
94
|
resumeActiveAgent,
|
|
101
95
|
sessionAgents,
|
|
102
96
|
sseClients = new Map(),
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
try { clientRes.end(); } catch (e) { /* closed */ }
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (clients.size === 0) sseClients.delete(sid);
|
|
118
|
-
}
|
|
119
|
-
if (sid !== 'default' && sseClients.has('default')) {
|
|
120
|
-
const defClients = sseClients.get('default');
|
|
121
|
-
if (defClients && defClients.size > 0) {
|
|
97
|
+
addonStore,
|
|
98
|
+
budgetGuard,
|
|
99
|
+
snapshotManager,
|
|
100
|
+
milestoneManager,
|
|
101
|
+
logger = console,
|
|
102
|
+
} = {}) {
|
|
103
|
+
// Subscribe to engine if provided (backward compatibility)
|
|
104
|
+
if (engine && typeof engine.subscribe === 'function') {
|
|
105
|
+
engine.subscribe((snapshot, sid) => {
|
|
106
|
+
const clients = sseClients.get(sid);
|
|
107
|
+
if (clients && clients.size > 0) {
|
|
122
108
|
const payload = `data: ${JSON.stringify(snapshot)}\n\n`;
|
|
123
|
-
for (const clientRes of Array.from(
|
|
109
|
+
for (const clientRes of Array.from(clients)) {
|
|
124
110
|
try {
|
|
125
111
|
clientRes.write(payload);
|
|
126
112
|
} catch (err) {
|
|
127
|
-
|
|
113
|
+
clients.delete(clientRes);
|
|
128
114
|
try { clientRes.end(); } catch (e) { /* closed */ }
|
|
129
115
|
}
|
|
130
116
|
}
|
|
131
|
-
if (
|
|
117
|
+
if (clients.size === 0) sseClients.delete(sid);
|
|
118
|
+
}
|
|
119
|
+
if (sid !== 'default' && sseClients.has('default')) {
|
|
120
|
+
const defClients = sseClients.get('default');
|
|
121
|
+
if (defClients && defClients.size > 0) {
|
|
122
|
+
const payload = `data: ${JSON.stringify(snapshot)}\n\n`;
|
|
123
|
+
for (const clientRes of Array.from(defClients)) {
|
|
124
|
+
try {
|
|
125
|
+
clientRes.write(payload);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
defClients.delete(clientRes);
|
|
128
|
+
try { clientRes.end(); } catch (e) { /* closed */ }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (defClients.size === 0) sseClients.delete('default');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const broadcastEvent = (event, data) => {
|
|
138
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
139
|
+
for (const clientSet of sseClients.values()) {
|
|
140
|
+
if (clientSet instanceof Set) {
|
|
141
|
+
for (const res of clientSet) {
|
|
142
|
+
try { res.write(payload); } catch (e) { clientSet.delete(res); }
|
|
143
|
+
}
|
|
144
|
+
} else if (clientSet && typeof clientSet.write === 'function') {
|
|
145
|
+
try { clientSet.write(payload); } catch (e) { /* closed */ }
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
|
-
}
|
|
148
|
+
};
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
if (!ctx
|
|
150
|
+
const setupWebServer = () => {
|
|
151
|
+
if (!ctx?.webServer?.register) return () => {};
|
|
138
152
|
|
|
139
153
|
// Keepalive ping timer for SSE connections (every 20s)
|
|
140
154
|
const keepaliveTimer = setInterval(() => {
|
|
141
155
|
for (const [sid, clients] of sseClients.entries()) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
if (clients instanceof Set) {
|
|
157
|
+
for (const res of Array.from(clients)) {
|
|
158
|
+
try {
|
|
159
|
+
res.write(': keepalive\n\n');
|
|
160
|
+
} catch (err) {
|
|
161
|
+
clients.delete(res);
|
|
162
|
+
try { res.end(); } catch (e) { /* closed */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (clients.size === 0) {
|
|
166
|
+
sseClients.delete(sid);
|
|
148
167
|
}
|
|
149
|
-
}
|
|
150
|
-
if (clients.size === 0) {
|
|
151
|
-
sseClients.delete(sid);
|
|
152
168
|
}
|
|
153
169
|
}
|
|
154
170
|
}, 20000);
|
|
@@ -165,36 +181,48 @@ export function registerRoutes(ctx, {
|
|
|
165
181
|
if (req.method === 'GET' && (pathname === '/dsh-goal/events' || pathname === '/dsh-goal/events/')) {
|
|
166
182
|
if (!isTrustedCaller(req)) {
|
|
167
183
|
res.statusCode = 403;
|
|
168
|
-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
184
|
+
if (typeof res.setHeader === 'function') res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
169
185
|
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
170
186
|
}
|
|
171
187
|
const sid = sessionIdOf(req, 'default');
|
|
172
|
-
res.writeHead
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
188
|
+
if (typeof res.writeHead === 'function') {
|
|
189
|
+
res.writeHead(200, {
|
|
190
|
+
'Content-Type': 'text/event-stream',
|
|
191
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
192
|
+
'Connection': 'keep-alive',
|
|
193
|
+
'X-Accel-Buffering': 'no',
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
res.statusCode = 200;
|
|
197
|
+
if (typeof res.setHeader === 'function') {
|
|
198
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
199
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
178
202
|
|
|
179
203
|
if (!sseClients.has(sid)) {
|
|
180
204
|
sseClients.set(sid, new Set());
|
|
181
205
|
}
|
|
182
206
|
sseClients.get(sid).add(res);
|
|
183
207
|
|
|
184
|
-
const initialSnap = engine.getSnapshot(sid);
|
|
208
|
+
const initialSnap = engine ? engine.getSnapshot(sid) : { sessionId: sid, hasActiveGoal: false, status: 'IDLE' };
|
|
185
209
|
res.write(`data: ${JSON.stringify(initialSnap)}\n\n`);
|
|
186
210
|
|
|
187
|
-
req.on
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
set
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
if (typeof req.on === 'function') {
|
|
212
|
+
req.on('close', () => {
|
|
213
|
+
const set = sseClients.get(sid);
|
|
214
|
+
if (set) {
|
|
215
|
+
set.delete(res);
|
|
216
|
+
if (set.size === 0) sseClients.delete(sid);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
194
220
|
return;
|
|
195
221
|
}
|
|
196
222
|
|
|
197
|
-
res.setHeader
|
|
223
|
+
if (typeof res.setHeader === 'function') {
|
|
224
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
225
|
+
}
|
|
198
226
|
|
|
199
227
|
// GET /dsh-goal/templates
|
|
200
228
|
if (req.method === 'GET' && (pathname === '/dsh-goal/templates' || pathname === '/dsh-goal/templates/')) {
|
|
@@ -214,7 +242,15 @@ export function registerRoutes(ctx, {
|
|
|
214
242
|
}
|
|
215
243
|
const sid = sessionIdOf(req, 'default');
|
|
216
244
|
res.statusCode = 200;
|
|
217
|
-
|
|
245
|
+
if (engine) {
|
|
246
|
+
return res.end(JSON.stringify(engine.getSnapshot(sid)));
|
|
247
|
+
}
|
|
248
|
+
return res.end(JSON.stringify({
|
|
249
|
+
sessionId: sid,
|
|
250
|
+
hasActiveGoal: false,
|
|
251
|
+
state: 'IDLE',
|
|
252
|
+
milestones: [],
|
|
253
|
+
}));
|
|
218
254
|
}
|
|
219
255
|
|
|
220
256
|
// POST /dsh-goal/action
|
|
@@ -251,18 +287,18 @@ export function registerRoutes(ctx, {
|
|
|
251
287
|
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
252
288
|
let limitExceeded = false;
|
|
253
289
|
|
|
254
|
-
|
|
290
|
+
const onData = (chunk) => {
|
|
255
291
|
bodySize += chunk.length;
|
|
256
292
|
if (bodySize > MAX_PAYLOAD_BYTES) {
|
|
257
293
|
limitExceeded = true;
|
|
258
|
-
req.pause();
|
|
294
|
+
if (typeof req.pause === 'function') req.pause();
|
|
259
295
|
res.statusCode = 413;
|
|
260
296
|
return res.end(JSON.stringify({ error: 'Payload too large: max 256 KB allowed' }));
|
|
261
297
|
}
|
|
262
298
|
body += chunk;
|
|
263
|
-
}
|
|
299
|
+
};
|
|
264
300
|
|
|
265
|
-
|
|
301
|
+
const onEnd = () => {
|
|
266
302
|
if (limitExceeded) return;
|
|
267
303
|
try {
|
|
268
304
|
const data = JSON.parse(body || '{}');
|
|
@@ -292,16 +328,32 @@ export function registerRoutes(ctx, {
|
|
|
292
328
|
}
|
|
293
329
|
const detectedLang = data.lang || detectLanguage(cleanTitle);
|
|
294
330
|
const conf = typeof getConfig === 'function' ? getConfig() : { maxIterations: 25 };
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
331
|
+
|
|
332
|
+
if (engine) {
|
|
333
|
+
result = engine.startGoal(cleanTitle, {
|
|
334
|
+
description: typeof description === 'string' ? description.trim() : '',
|
|
335
|
+
maxIterations: conf.maxIterations || 25,
|
|
336
|
+
lang: detectedLang,
|
|
337
|
+
cwd: safeStartCwd,
|
|
338
|
+
issueBody: data.issueBody,
|
|
339
|
+
}, sid);
|
|
340
|
+
} else {
|
|
341
|
+
let parsedMilestones = [];
|
|
342
|
+
if (data.issueBody) {
|
|
343
|
+
parsedMilestones = parseIssueChecklist(data.issueBody);
|
|
344
|
+
}
|
|
345
|
+
result = {
|
|
346
|
+
title: cleanTitle,
|
|
347
|
+
sessionId: sid,
|
|
348
|
+
hasActiveGoal: true,
|
|
349
|
+
state: 'RUNNING',
|
|
350
|
+
milestones: parsedMilestones,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
301
353
|
|
|
302
354
|
const startPrompt = detectedLang === 'zh'
|
|
303
|
-
? `🎯 目标已确立:“${cleanTitle}”。请立即通过
|
|
304
|
-
: `🎯 Goal established: "${cleanTitle}".
|
|
355
|
+
? `🎯 目标已确立:“${cleanTitle}”。请立即通过 goal_milestones 制定工作计划并开始执行。`
|
|
356
|
+
: `🎯 Goal established: "${cleanTitle}". Formulate milestones via goal_milestones and proceed.`;
|
|
305
357
|
|
|
306
358
|
if (typeof resumeActiveAgent === 'function') {
|
|
307
359
|
resumeActiveAgent(startPrompt, sid);
|
|
@@ -315,40 +367,52 @@ export function registerRoutes(ctx, {
|
|
|
315
367
|
res.statusCode = 400;
|
|
316
368
|
return res.end(JSON.stringify({ error: 'Nudge text cannot be empty' }));
|
|
317
369
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
370
|
+
if (engine) {
|
|
371
|
+
result = engine.nudge(text, sid);
|
|
372
|
+
if (data.resume) {
|
|
373
|
+
engine.resume(sid);
|
|
374
|
+
const prompt = engine.getStatePromptInjection(sid);
|
|
375
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
376
|
+
resumeActiveAgent(prompt, sid);
|
|
377
|
+
}
|
|
324
378
|
}
|
|
379
|
+
} else {
|
|
380
|
+
result = { nudged: true, text };
|
|
325
381
|
}
|
|
326
382
|
break;
|
|
327
383
|
}
|
|
328
384
|
|
|
329
385
|
case 'pause':
|
|
330
|
-
|
|
386
|
+
if (engine) {
|
|
387
|
+
result = engine.pause(reason || 'Paused by user interface', sid);
|
|
388
|
+
}
|
|
331
389
|
if (typeof stopRunningAgents === 'function') {
|
|
332
390
|
stopRunningAgents(sid);
|
|
333
391
|
}
|
|
334
392
|
break;
|
|
335
393
|
|
|
336
394
|
case 'resume':
|
|
337
|
-
|
|
395
|
+
if (engine) {
|
|
396
|
+
result = engine.resume(sid);
|
|
397
|
+
}
|
|
338
398
|
if (typeof resumeActiveAgent === 'function') {
|
|
339
399
|
resumeActiveAgent(undefined, sid);
|
|
340
400
|
}
|
|
341
401
|
break;
|
|
342
402
|
|
|
343
403
|
case 'cancel':
|
|
344
|
-
|
|
404
|
+
if (engine) {
|
|
405
|
+
result = engine.cancel(reason || 'Goal cancelled by user', sid);
|
|
406
|
+
}
|
|
345
407
|
if (typeof stopRunningAgents === 'function') {
|
|
346
408
|
stopRunningAgents(sid);
|
|
347
409
|
}
|
|
348
410
|
break;
|
|
349
411
|
|
|
350
412
|
case 'clear':
|
|
351
|
-
|
|
413
|
+
if (engine) {
|
|
414
|
+
result = engine.clear(sid);
|
|
415
|
+
}
|
|
352
416
|
if (typeof stopRunningAgents === 'function') {
|
|
353
417
|
stopRunningAgents(sid);
|
|
354
418
|
}
|
|
@@ -367,12 +431,21 @@ export function registerRoutes(ctx, {
|
|
|
367
431
|
res.statusCode = 400;
|
|
368
432
|
return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
|
|
369
433
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
434
|
+
if (engine) {
|
|
435
|
+
const ok = engine.updateMilestone(milestoneId, status, notes, sid, data.checklist);
|
|
436
|
+
if (!ok) {
|
|
437
|
+
res.statusCode = 404;
|
|
438
|
+
return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
|
|
439
|
+
}
|
|
440
|
+
result = engine.getSnapshot(sid);
|
|
441
|
+
} else if (milestoneManager) {
|
|
442
|
+
const ok = milestoneManager.updateMilestone(sid, milestoneId, { status, notes });
|
|
443
|
+
if (!ok) {
|
|
444
|
+
res.statusCode = 404;
|
|
445
|
+
return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
|
|
446
|
+
}
|
|
447
|
+
result = { ok: true, milestones: milestoneManager.getMilestones(sid) };
|
|
374
448
|
}
|
|
375
|
-
result = engine.getSnapshot(sid);
|
|
376
449
|
break;
|
|
377
450
|
}
|
|
378
451
|
|
|
@@ -381,26 +454,40 @@ export function registerRoutes(ctx, {
|
|
|
381
454
|
res.statusCode = 400;
|
|
382
455
|
return res.end(JSON.stringify({ error: 'milestoneId and itemIndex are required' }));
|
|
383
456
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
457
|
+
if (engine) {
|
|
458
|
+
const ok = engine.toggleChecklistItem(milestoneId, data.itemIndex, data.done, sid);
|
|
459
|
+
if (!ok) {
|
|
460
|
+
res.statusCode = 404;
|
|
461
|
+
return res.end(JSON.stringify({ error: `Milestone checklist item not found` }));
|
|
462
|
+
}
|
|
463
|
+
result = engine.getSnapshot(sid);
|
|
464
|
+
} else if (milestoneManager) {
|
|
465
|
+
const ok = milestoneManager.toggleChecklistItem(sid, milestoneId, data.itemIndex, data.done);
|
|
466
|
+
if (!ok) {
|
|
467
|
+
res.statusCode = 404;
|
|
468
|
+
return res.end(JSON.stringify({ error: `Milestone checklist item not found` }));
|
|
469
|
+
}
|
|
470
|
+
result = { ok: true, milestones: milestoneManager.getMilestones(sid) };
|
|
388
471
|
}
|
|
389
|
-
result = engine.getSnapshot(sid);
|
|
390
472
|
break;
|
|
391
473
|
}
|
|
392
474
|
|
|
393
475
|
case 'extend_budget': {
|
|
394
476
|
const addTokens = data.addTokens || 50000;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
477
|
+
if (engine) {
|
|
478
|
+
const ok = engine.extendBudget(addTokens, sid);
|
|
479
|
+
if (!ok) {
|
|
480
|
+
res.statusCode = 404;
|
|
481
|
+
return res.end(JSON.stringify({ error: 'No active goal found to extend budget' }));
|
|
482
|
+
}
|
|
483
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
484
|
+
resumeActiveAgent(undefined, sid);
|
|
485
|
+
}
|
|
486
|
+
result = engine.getSnapshot(sid);
|
|
487
|
+
} else if (budgetGuard) {
|
|
488
|
+
budgetGuard.extendBudget(sid, addTokens);
|
|
489
|
+
result = { ok: true, extendedBy: addTokens };
|
|
402
490
|
}
|
|
403
|
-
result = engine.getSnapshot(sid);
|
|
404
491
|
break;
|
|
405
492
|
}
|
|
406
493
|
|
|
@@ -410,7 +497,7 @@ export function registerRoutes(ctx, {
|
|
|
410
497
|
res.statusCode = 400;
|
|
411
498
|
return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
|
|
412
499
|
}
|
|
413
|
-
const sessionGoal = engine.getGoal(sid);
|
|
500
|
+
const sessionGoal = engine ? engine.getGoal(sid) : null;
|
|
414
501
|
const allowedRoots = [process.cwd()];
|
|
415
502
|
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
416
503
|
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
@@ -431,17 +518,17 @@ export function registerRoutes(ctx, {
|
|
|
431
518
|
res.statusCode = 500;
|
|
432
519
|
return res.end(JSON.stringify({ error: `Failed to rollback to checkpoint ${commit}` }));
|
|
433
520
|
}
|
|
434
|
-
result = engine.getSnapshot(sid);
|
|
521
|
+
result = engine ? engine.getSnapshot(sid) : { ok: true, rolledBack: commit };
|
|
435
522
|
break;
|
|
436
523
|
}
|
|
437
524
|
|
|
438
525
|
case 'save_artifact': {
|
|
439
|
-
const snap = engine.getSnapshot(sid);
|
|
440
|
-
if (!snap || !snap.hasActiveGoal) {
|
|
526
|
+
const snap = engine ? engine.getSnapshot(sid) : null;
|
|
527
|
+
if (engine && (!snap || !snap.hasActiveGoal)) {
|
|
441
528
|
res.statusCode = 404;
|
|
442
529
|
return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
|
|
443
530
|
}
|
|
444
|
-
const sessionGoal = engine.getGoal(sid);
|
|
531
|
+
const sessionGoal = engine ? engine.getGoal(sid) : null;
|
|
445
532
|
const allowedRoots = [process.cwd()];
|
|
446
533
|
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
447
534
|
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
@@ -457,32 +544,38 @@ export function registerRoutes(ctx, {
|
|
|
457
544
|
res.statusCode = 400;
|
|
458
545
|
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
459
546
|
}
|
|
460
|
-
const artifact = saveGoalArtifact(snap, safeCwd);
|
|
547
|
+
const artifact = saveGoalArtifact(snap || { objective: 'Goal Report' }, safeCwd);
|
|
461
548
|
if (!artifact) {
|
|
462
549
|
res.statusCode = 500;
|
|
463
550
|
return res.end(JSON.stringify({ error: 'Failed to write goal artifact to disk' }));
|
|
464
551
|
}
|
|
465
552
|
res.statusCode = 200;
|
|
466
|
-
return res.end(JSON.stringify({ ok: true, artifact, state: snap }));
|
|
553
|
+
return res.end(JSON.stringify({ ok: true, artifact, state: snap || {} }));
|
|
467
554
|
}
|
|
468
555
|
|
|
469
556
|
case 'intervene': {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
557
|
+
if (engine) {
|
|
558
|
+
result = engine.intervene(data, sid);
|
|
559
|
+
if (data.resume) {
|
|
560
|
+
engine.resume(sid);
|
|
561
|
+
const prompt = engine.getStatePromptInjection(sid);
|
|
562
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
563
|
+
resumeActiveAgent(prompt, sid);
|
|
564
|
+
}
|
|
476
565
|
}
|
|
566
|
+
} else {
|
|
567
|
+
result = { intervened: true };
|
|
477
568
|
}
|
|
478
569
|
break;
|
|
479
570
|
}
|
|
480
571
|
|
|
481
572
|
case 'preplan_approve': {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
573
|
+
if (engine) {
|
|
574
|
+
result = engine.approvePreplan(sid);
|
|
575
|
+
const prompt = engine.getStatePromptInjection(sid);
|
|
576
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
577
|
+
resumeActiveAgent(prompt, sid);
|
|
578
|
+
}
|
|
486
579
|
}
|
|
487
580
|
break;
|
|
488
581
|
}
|
|
@@ -490,20 +583,22 @@ export function registerRoutes(ctx, {
|
|
|
490
583
|
case 'branch_action': {
|
|
491
584
|
const op = String(data.operation || data.branchOperation || 'merge').toLowerCase();
|
|
492
585
|
const targetBranch = String(data.targetBranch || 'main');
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
586
|
+
if (engine) {
|
|
587
|
+
if (op === 'merge') {
|
|
588
|
+
const ok = engine.mergeGoalBranch(targetBranch, sid);
|
|
589
|
+
if (!ok) {
|
|
590
|
+
res.statusCode = 500;
|
|
591
|
+
return res.end(JSON.stringify({ error: `Failed to merge goal branch into ${targetBranch}` }));
|
|
592
|
+
}
|
|
593
|
+
} else if (op === 'discard') {
|
|
594
|
+
const ok = engine.discardGoalBranch(targetBranch, sid);
|
|
595
|
+
if (!ok) {
|
|
596
|
+
res.statusCode = 500;
|
|
597
|
+
return res.end(JSON.stringify({ error: 'Failed to discard goal branch' }));
|
|
598
|
+
}
|
|
504
599
|
}
|
|
600
|
+
result = engine.getSnapshot(sid);
|
|
505
601
|
}
|
|
506
|
-
result = engine.getSnapshot(sid);
|
|
507
602
|
break;
|
|
508
603
|
}
|
|
509
604
|
|
|
@@ -511,21 +606,57 @@ export function registerRoutes(ctx, {
|
|
|
511
606
|
res.statusCode = 200;
|
|
512
607
|
return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
|
|
513
608
|
|
|
609
|
+
case 'instantiate_template': {
|
|
610
|
+
const tplId = data.templateId || data.id;
|
|
611
|
+
if (!tplId) {
|
|
612
|
+
res.statusCode = 400;
|
|
613
|
+
return res.end(JSON.stringify({ error: 'templateId is required' }));
|
|
614
|
+
}
|
|
615
|
+
const instance = instantiateTemplate(tplId, data.inputs || {});
|
|
616
|
+
if (!instance) {
|
|
617
|
+
res.statusCode = 404;
|
|
618
|
+
return res.end(JSON.stringify({ error: `Template "${tplId}" not found` }));
|
|
619
|
+
}
|
|
620
|
+
res.statusCode = 200;
|
|
621
|
+
return res.end(JSON.stringify({ ok: true, template: instance }));
|
|
622
|
+
}
|
|
623
|
+
|
|
514
624
|
default:
|
|
515
625
|
res.statusCode = 400;
|
|
516
626
|
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
517
627
|
}
|
|
518
628
|
|
|
519
629
|
res.statusCode = 200;
|
|
520
|
-
return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot(sid) }));
|
|
630
|
+
return res.end(JSON.stringify({ ok: true, state: result || (engine ? engine.getSnapshot(sid) : {}) }));
|
|
521
631
|
} catch (parseErr) {
|
|
522
632
|
res.statusCode = 400;
|
|
523
633
|
return res.end(JSON.stringify({ error: parseErr.message }));
|
|
524
634
|
}
|
|
525
|
-
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
if (typeof req.on === 'function') {
|
|
638
|
+
req.on('data', onData);
|
|
639
|
+
req.on('end', onEnd);
|
|
640
|
+
} else {
|
|
641
|
+
onEnd();
|
|
642
|
+
}
|
|
526
643
|
return;
|
|
527
644
|
}
|
|
528
645
|
|
|
646
|
+
const KNOWN_ROUTES = {
|
|
647
|
+
'/dsh-goal/events': ['GET'],
|
|
648
|
+
'/dsh-goal/templates': ['GET'],
|
|
649
|
+
'/dsh-goal/state': ['GET'],
|
|
650
|
+
'/dsh-goal/action': ['POST'],
|
|
651
|
+
};
|
|
652
|
+
const cleanPath = pathname.replace(/\/+$/, '') || '/';
|
|
653
|
+
const allowedMethods = KNOWN_ROUTES[cleanPath];
|
|
654
|
+
if (allowedMethods && !allowedMethods.includes(req.method)) {
|
|
655
|
+
res.statusCode = 405;
|
|
656
|
+
if (typeof res.setHeader === 'function') res.setHeader('Allow', allowedMethods.join(', '));
|
|
657
|
+
return res.end(JSON.stringify({ error: `Method Not Allowed: use ${allowedMethods.join(', ')}` }));
|
|
658
|
+
}
|
|
659
|
+
|
|
529
660
|
res.statusCode = 404;
|
|
530
661
|
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
531
662
|
},
|
|
@@ -535,15 +666,25 @@ export function registerRoutes(ctx, {
|
|
|
535
666
|
clearInterval(keepaliveTimer);
|
|
536
667
|
if (typeof unreg === 'function') unreg();
|
|
537
668
|
for (const clients of sseClients.values()) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
res.end();
|
|
541
|
-
} catch (err) {
|
|
542
|
-
// Already closed
|
|
669
|
+
if (clients instanceof Set) {
|
|
670
|
+
for (const res of clients) {
|
|
671
|
+
try { res.end(); } catch (err) { /* closed */ }
|
|
543
672
|
}
|
|
544
673
|
}
|
|
545
674
|
}
|
|
546
675
|
sseClients.clear();
|
|
547
676
|
};
|
|
548
|
-
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
let cleanup = null;
|
|
680
|
+
if (typeof ctx?.effect === 'function') {
|
|
681
|
+
cleanup = ctx.effect(() => setupWebServer(), 'dsh-goal: HTTP WebServer Routes & SSE');
|
|
682
|
+
} else {
|
|
683
|
+
cleanup = setupWebServer();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
broadcastEvent,
|
|
688
|
+
cleanup,
|
|
689
|
+
};
|
|
549
690
|
}
|