@goodandready/dsh-goal 0.2.12 → 0.2.14
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/engine-reports.js +112 -33
- package/lib/git-snapshot-manager.js +99 -0
- package/lib/index.js +226 -395
- package/lib/milestone-manager.js +141 -0
- package/lib/routes.js +249 -138
- package/lib/token-budget-guard.js +128 -0
- package/lib/tools.js +115 -257
- package/package.json +9 -3
package/lib/routes.js
CHANGED
|
@@ -3,6 +3,7 @@ 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
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,17 +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
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
353
|
|
|
303
354
|
const startPrompt = detectedLang === 'zh'
|
|
304
|
-
? `🎯 目标已确立:“${cleanTitle}”。请立即通过
|
|
305
|
-
: `🎯 Goal established: "${cleanTitle}".
|
|
355
|
+
? `🎯 目标已确立:“${cleanTitle}”。请立即通过 goal_milestones 制定工作计划并开始执行。`
|
|
356
|
+
: `🎯 Goal established: "${cleanTitle}". Formulate milestones via goal_milestones and proceed.`;
|
|
306
357
|
|
|
307
358
|
if (typeof resumeActiveAgent === 'function') {
|
|
308
359
|
resumeActiveAgent(startPrompt, sid);
|
|
@@ -316,40 +367,52 @@ export function registerRoutes(ctx, {
|
|
|
316
367
|
res.statusCode = 400;
|
|
317
368
|
return res.end(JSON.stringify({ error: 'Nudge text cannot be empty' }));
|
|
318
369
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
378
|
}
|
|
379
|
+
} else {
|
|
380
|
+
result = { nudged: true, text };
|
|
326
381
|
}
|
|
327
382
|
break;
|
|
328
383
|
}
|
|
329
384
|
|
|
330
385
|
case 'pause':
|
|
331
|
-
|
|
386
|
+
if (engine) {
|
|
387
|
+
result = engine.pause(reason || 'Paused by user interface', sid);
|
|
388
|
+
}
|
|
332
389
|
if (typeof stopRunningAgents === 'function') {
|
|
333
390
|
stopRunningAgents(sid);
|
|
334
391
|
}
|
|
335
392
|
break;
|
|
336
393
|
|
|
337
394
|
case 'resume':
|
|
338
|
-
|
|
395
|
+
if (engine) {
|
|
396
|
+
result = engine.resume(sid);
|
|
397
|
+
}
|
|
339
398
|
if (typeof resumeActiveAgent === 'function') {
|
|
340
399
|
resumeActiveAgent(undefined, sid);
|
|
341
400
|
}
|
|
342
401
|
break;
|
|
343
402
|
|
|
344
403
|
case 'cancel':
|
|
345
|
-
|
|
404
|
+
if (engine) {
|
|
405
|
+
result = engine.cancel(reason || 'Goal cancelled by user', sid);
|
|
406
|
+
}
|
|
346
407
|
if (typeof stopRunningAgents === 'function') {
|
|
347
408
|
stopRunningAgents(sid);
|
|
348
409
|
}
|
|
349
410
|
break;
|
|
350
411
|
|
|
351
412
|
case 'clear':
|
|
352
|
-
|
|
413
|
+
if (engine) {
|
|
414
|
+
result = engine.clear(sid);
|
|
415
|
+
}
|
|
353
416
|
if (typeof stopRunningAgents === 'function') {
|
|
354
417
|
stopRunningAgents(sid);
|
|
355
418
|
}
|
|
@@ -368,12 +431,21 @@ export function registerRoutes(ctx, {
|
|
|
368
431
|
res.statusCode = 400;
|
|
369
432
|
return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
|
|
370
433
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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) };
|
|
375
448
|
}
|
|
376
|
-
result = engine.getSnapshot(sid);
|
|
377
449
|
break;
|
|
378
450
|
}
|
|
379
451
|
|
|
@@ -382,26 +454,40 @@ export function registerRoutes(ctx, {
|
|
|
382
454
|
res.statusCode = 400;
|
|
383
455
|
return res.end(JSON.stringify({ error: 'milestoneId and itemIndex are required' }));
|
|
384
456
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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) };
|
|
389
471
|
}
|
|
390
|
-
result = engine.getSnapshot(sid);
|
|
391
472
|
break;
|
|
392
473
|
}
|
|
393
474
|
|
|
394
475
|
case 'extend_budget': {
|
|
395
476
|
const addTokens = data.addTokens || 50000;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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 };
|
|
403
490
|
}
|
|
404
|
-
result = engine.getSnapshot(sid);
|
|
405
491
|
break;
|
|
406
492
|
}
|
|
407
493
|
|
|
@@ -411,7 +497,7 @@ export function registerRoutes(ctx, {
|
|
|
411
497
|
res.statusCode = 400;
|
|
412
498
|
return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
|
|
413
499
|
}
|
|
414
|
-
const sessionGoal = engine.getGoal(sid);
|
|
500
|
+
const sessionGoal = engine ? engine.getGoal(sid) : null;
|
|
415
501
|
const allowedRoots = [process.cwd()];
|
|
416
502
|
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
417
503
|
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
@@ -432,17 +518,17 @@ export function registerRoutes(ctx, {
|
|
|
432
518
|
res.statusCode = 500;
|
|
433
519
|
return res.end(JSON.stringify({ error: `Failed to rollback to checkpoint ${commit}` }));
|
|
434
520
|
}
|
|
435
|
-
result = engine.getSnapshot(sid);
|
|
521
|
+
result = engine ? engine.getSnapshot(sid) : { ok: true, rolledBack: commit };
|
|
436
522
|
break;
|
|
437
523
|
}
|
|
438
524
|
|
|
439
525
|
case 'save_artifact': {
|
|
440
|
-
const snap = engine.getSnapshot(sid);
|
|
441
|
-
if (!snap || !snap.hasActiveGoal) {
|
|
526
|
+
const snap = engine ? engine.getSnapshot(sid) : null;
|
|
527
|
+
if (engine && (!snap || !snap.hasActiveGoal)) {
|
|
442
528
|
res.statusCode = 404;
|
|
443
529
|
return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
|
|
444
530
|
}
|
|
445
|
-
const sessionGoal = engine.getGoal(sid);
|
|
531
|
+
const sessionGoal = engine ? engine.getGoal(sid) : null;
|
|
446
532
|
const allowedRoots = [process.cwd()];
|
|
447
533
|
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
448
534
|
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
@@ -458,32 +544,38 @@ export function registerRoutes(ctx, {
|
|
|
458
544
|
res.statusCode = 400;
|
|
459
545
|
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
460
546
|
}
|
|
461
|
-
const artifact = saveGoalArtifact(snap, safeCwd);
|
|
547
|
+
const artifact = saveGoalArtifact(snap || { objective: 'Goal Report' }, safeCwd);
|
|
462
548
|
if (!artifact) {
|
|
463
549
|
res.statusCode = 500;
|
|
464
550
|
return res.end(JSON.stringify({ error: 'Failed to write goal artifact to disk' }));
|
|
465
551
|
}
|
|
466
552
|
res.statusCode = 200;
|
|
467
|
-
return res.end(JSON.stringify({ ok: true, artifact, state: snap }));
|
|
553
|
+
return res.end(JSON.stringify({ ok: true, artifact, state: snap || {} }));
|
|
468
554
|
}
|
|
469
555
|
|
|
470
556
|
case 'intervene': {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
}
|
|
477
565
|
}
|
|
566
|
+
} else {
|
|
567
|
+
result = { intervened: true };
|
|
478
568
|
}
|
|
479
569
|
break;
|
|
480
570
|
}
|
|
481
571
|
|
|
482
572
|
case 'preplan_approve': {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
573
|
+
if (engine) {
|
|
574
|
+
result = engine.approvePreplan(sid);
|
|
575
|
+
const prompt = engine.getStatePromptInjection(sid);
|
|
576
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
577
|
+
resumeActiveAgent(prompt, sid);
|
|
578
|
+
}
|
|
487
579
|
}
|
|
488
580
|
break;
|
|
489
581
|
}
|
|
@@ -491,20 +583,22 @@ export function registerRoutes(ctx, {
|
|
|
491
583
|
case 'branch_action': {
|
|
492
584
|
const op = String(data.operation || data.branchOperation || 'merge').toLowerCase();
|
|
493
585
|
const targetBranch = String(data.targetBranch || 'main');
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
+
}
|
|
505
599
|
}
|
|
600
|
+
result = engine.getSnapshot(sid);
|
|
506
601
|
}
|
|
507
|
-
result = engine.getSnapshot(sid);
|
|
508
602
|
break;
|
|
509
603
|
}
|
|
510
604
|
|
|
@@ -533,12 +627,19 @@ export function registerRoutes(ctx, {
|
|
|
533
627
|
}
|
|
534
628
|
|
|
535
629
|
res.statusCode = 200;
|
|
536
|
-
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) : {}) }));
|
|
537
631
|
} catch (parseErr) {
|
|
538
632
|
res.statusCode = 400;
|
|
539
633
|
return res.end(JSON.stringify({ error: parseErr.message }));
|
|
540
634
|
}
|
|
541
|
-
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
if (typeof req.on === 'function') {
|
|
638
|
+
req.on('data', onData);
|
|
639
|
+
req.on('end', onEnd);
|
|
640
|
+
} else {
|
|
641
|
+
onEnd();
|
|
642
|
+
}
|
|
542
643
|
return;
|
|
543
644
|
}
|
|
544
645
|
|
|
@@ -552,7 +653,7 @@ export function registerRoutes(ctx, {
|
|
|
552
653
|
const allowedMethods = KNOWN_ROUTES[cleanPath];
|
|
553
654
|
if (allowedMethods && !allowedMethods.includes(req.method)) {
|
|
554
655
|
res.statusCode = 405;
|
|
555
|
-
res.setHeader('Allow', allowedMethods.join(', '));
|
|
656
|
+
if (typeof res.setHeader === 'function') res.setHeader('Allow', allowedMethods.join(', '));
|
|
556
657
|
return res.end(JSON.stringify({ error: `Method Not Allowed: use ${allowedMethods.join(', ')}` }));
|
|
557
658
|
}
|
|
558
659
|
|
|
@@ -565,15 +666,25 @@ export function registerRoutes(ctx, {
|
|
|
565
666
|
clearInterval(keepaliveTimer);
|
|
566
667
|
if (typeof unreg === 'function') unreg();
|
|
567
668
|
for (const clients of sseClients.values()) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
res.end();
|
|
571
|
-
} catch (err) {
|
|
572
|
-
// Already closed
|
|
669
|
+
if (clients instanceof Set) {
|
|
670
|
+
for (const res of clients) {
|
|
671
|
+
try { res.end(); } catch (err) { /* closed */ }
|
|
573
672
|
}
|
|
574
673
|
}
|
|
575
674
|
}
|
|
576
675
|
sseClients.clear();
|
|
577
676
|
};
|
|
578
|
-
}
|
|
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
|
+
};
|
|
579
690
|
}
|