@goodandready/dsh-goal 0.2.8 → 0.2.10
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/client.js +350 -13
- package/lib/engine-git-branch.js +105 -0
- package/lib/engine-intervention.js +150 -0
- package/lib/engine-issue-sync.js +66 -0
- package/lib/engine-milestones.js +42 -3
- package/lib/engine-prompt.js +28 -4
- package/lib/engine-retrospective.js +79 -0
- package/lib/engine-sessions.js +82 -0
- package/lib/engine-templates.js +167 -0
- package/lib/goal-engine-constants.js +30 -0
- package/lib/goal-engine.js +199 -233
- package/lib/index.js +36 -0
- package/lib/routes.js +139 -9
- package/lib/tools.js +11 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -22,6 +22,15 @@ export const Config = z.object({
|
|
|
22
22
|
maxTokenBudget: z.number().default(0).description('Maximum token budget per goal session (0 to disable)'),
|
|
23
23
|
budgetWarningThreshold: z.number().default(80).description('Percentage of token budget consumed before model warning injection (e.g. 80)'),
|
|
24
24
|
autoCheckpointOnMilestone: z.boolean().default(false).description('Automatically create git commit checkpoint upon milestone completion'),
|
|
25
|
+
enableLiveActivityFeed: z.boolean().default(true).description('Display compact recent tool and control activity feed in details modal'),
|
|
26
|
+
enablePreplanning: z.boolean().default(true).description('Generate initial draft plan for approval before launching autonomous turns'),
|
|
27
|
+
autoBranchOnGoalStart: z.boolean().default(true).description('Automatically create dedicated isolated git branch for goal execution'),
|
|
28
|
+
enablePostGoalRetrospective: z.boolean().default(true).description('Generate structured retrospective and analytics card upon goal completion'),
|
|
29
|
+
autoScaleBudgetNearCompletion: z.boolean().default(false).description('Automatically grant token buffer extension if budget reaches 100% near completion'),
|
|
30
|
+
enableTemplatesDrawer: z.boolean().default(true).description('Provide pre-configured engineering goal templates in UI drawer'),
|
|
31
|
+
enableMilestoneDependencies: z.boolean().default(true).description('Enforce milestone dependency ordering and block premature execution'),
|
|
32
|
+
soundScheme: z.string().default('default').description('Audio theme for status and milestone notifications'),
|
|
33
|
+
enableVoiceAnnouncements: z.boolean().default(false).description('Use Web Speech API for voice announcements on milestone events'),
|
|
25
34
|
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
26
35
|
});
|
|
27
36
|
|
|
@@ -43,6 +52,15 @@ export function apply(ctx, config = {}) {
|
|
|
43
52
|
maxTokenBudget: config?.maxTokenBudget ?? 0,
|
|
44
53
|
budgetWarningThreshold: config?.budgetWarningThreshold ?? 80,
|
|
45
54
|
autoCheckpointOnMilestone: config?.autoCheckpointOnMilestone ?? false,
|
|
55
|
+
enableLiveActivityFeed: config?.enableLiveActivityFeed ?? true,
|
|
56
|
+
enablePreplanning: config?.enablePreplanning ?? true,
|
|
57
|
+
autoBranchOnGoalStart: config?.autoBranchOnGoalStart ?? true,
|
|
58
|
+
enablePostGoalRetrospective: config?.enablePostGoalRetrospective ?? true,
|
|
59
|
+
autoScaleBudgetNearCompletion: config?.autoScaleBudgetNearCompletion ?? false,
|
|
60
|
+
enableTemplatesDrawer: config?.enableTemplatesDrawer ?? true,
|
|
61
|
+
enableMilestoneDependencies: config?.enableMilestoneDependencies ?? true,
|
|
62
|
+
soundScheme: config?.soundScheme ?? 'default',
|
|
63
|
+
enableVoiceAnnouncements: config?.enableVoiceAnnouncements ?? false,
|
|
46
64
|
storagePath: config?.storagePath,
|
|
47
65
|
};
|
|
48
66
|
|
|
@@ -60,6 +78,15 @@ export function apply(ctx, config = {}) {
|
|
|
60
78
|
maxTokenBudget: currentSettings.maxTokenBudget,
|
|
61
79
|
budgetWarningThreshold: currentSettings.budgetWarningThreshold,
|
|
62
80
|
autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
|
|
81
|
+
enableLiveActivityFeed: currentSettings.enableLiveActivityFeed,
|
|
82
|
+
enablePreplanning: currentSettings.enablePreplanning,
|
|
83
|
+
autoBranchOnGoalStart: currentSettings.autoBranchOnGoalStart,
|
|
84
|
+
enablePostGoalRetrospective: currentSettings.enablePostGoalRetrospective,
|
|
85
|
+
autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
|
|
86
|
+
enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
|
|
87
|
+
enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
|
|
88
|
+
soundScheme: currentSettings.soundScheme,
|
|
89
|
+
enableVoiceAnnouncements: currentSettings.enableVoiceAnnouncements,
|
|
63
90
|
storagePath,
|
|
64
91
|
});
|
|
65
92
|
|
|
@@ -220,6 +247,15 @@ export function apply(ctx, config = {}) {
|
|
|
220
247
|
maxTokenBudget: live.maxTokenBudget,
|
|
221
248
|
budgetWarningThreshold: live.budgetWarningThreshold,
|
|
222
249
|
autoCheckpointOnMilestone: live.autoCheckpointOnMilestone,
|
|
250
|
+
enableLiveActivityFeed: live.enableLiveActivityFeed,
|
|
251
|
+
enablePreplanning: live.enablePreplanning,
|
|
252
|
+
autoBranchOnGoalStart: live.autoBranchOnGoalStart,
|
|
253
|
+
enablePostGoalRetrospective: live.enablePostGoalRetrospective,
|
|
254
|
+
autoScaleBudgetNearCompletion: live.autoScaleBudgetNearCompletion,
|
|
255
|
+
enableTemplatesDrawer: live.enableTemplatesDrawer,
|
|
256
|
+
enableMilestoneDependencies: live.enableMilestoneDependencies,
|
|
257
|
+
soundScheme: live.soundScheme,
|
|
258
|
+
enableVoiceAnnouncements: live.enableVoiceAnnouncements,
|
|
223
259
|
});
|
|
224
260
|
};
|
|
225
261
|
|
package/lib/routes.js
CHANGED
|
@@ -2,17 +2,84 @@ 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';
|
|
6
|
+
|
|
7
|
+
export function isLoopback(value) {
|
|
8
|
+
if (!value) return false;
|
|
9
|
+
const address = String(value).toLowerCase().replace(/^\[|\]$/g, '');
|
|
10
|
+
return Boolean(
|
|
11
|
+
address === 'localhost' ||
|
|
12
|
+
address === 'localhost.' ||
|
|
13
|
+
address === '::1' ||
|
|
14
|
+
address.startsWith('127.') ||
|
|
15
|
+
address.startsWith('::ffff:127.')
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isTrustedCaller(req) {
|
|
20
|
+
if (!req) return false;
|
|
21
|
+
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || '';
|
|
22
|
+
if (isLoopback(remote)) return true;
|
|
23
|
+
|
|
24
|
+
const secSite = String(req.headers?.['sec-fetch-site'] || '').toLowerCase();
|
|
25
|
+
if (secSite === 'same-origin' || secSite === 'same-site' || secSite === 'none') return true;
|
|
26
|
+
if (secSite === 'cross-site') return false;
|
|
27
|
+
|
|
28
|
+
const host = String(req.headers?.host || '').toLowerCase();
|
|
29
|
+
const origin = String(req.headers?.origin || '').trim().toLowerCase();
|
|
30
|
+
if (origin) {
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(origin);
|
|
33
|
+
if (u.host === host) return true;
|
|
34
|
+
} catch (err) { /* invalid url */ }
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const referer = String(req.headers?.referer || '').trim().toLowerCase();
|
|
39
|
+
if (referer) {
|
|
40
|
+
try {
|
|
41
|
+
const u = new URL(referer);
|
|
42
|
+
if (u.host === host) return true;
|
|
43
|
+
} catch (err) { /* invalid url */ }
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fallback for tests or local CLI without socket where host header targets loopback
|
|
48
|
+
if (!remote && host) {
|
|
49
|
+
const hostName = host.split(':')[0];
|
|
50
|
+
if (isLoopback(hostName)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isPathInsideOrEqual(parent, child) {
|
|
59
|
+
if (!parent || !child) return false;
|
|
60
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
61
|
+
return !rel || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
62
|
+
}
|
|
5
63
|
|
|
6
|
-
function getSafeCwd(requestedCwd) {
|
|
7
|
-
|
|
8
|
-
|
|
64
|
+
export function getSafeCwd(requestedCwd, allowedRoots = []) {
|
|
65
|
+
const defaultRoot = allowedRoots[0] || process.cwd();
|
|
66
|
+
if (!requestedCwd || typeof requestedCwd !== 'string' || !requestedCwd.trim()) {
|
|
67
|
+
return defaultRoot;
|
|
9
68
|
}
|
|
69
|
+
|
|
10
70
|
try {
|
|
11
71
|
const resolved = path.resolve(requestedCwd.trim());
|
|
12
72
|
const st = fs.statSync(resolved);
|
|
13
73
|
if (!st.isDirectory()) {
|
|
14
74
|
return null;
|
|
15
75
|
}
|
|
76
|
+
|
|
77
|
+
const roots = allowedRoots.length > 0 ? allowedRoots : [process.cwd()];
|
|
78
|
+
const isAllowed = roots.some((root) => isPathInsideOrEqual(root, resolved));
|
|
79
|
+
if (!isAllowed) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
16
83
|
return resolved;
|
|
17
84
|
} catch (err) {
|
|
18
85
|
return null;
|
|
@@ -128,6 +195,12 @@ export function registerRoutes(ctx, {
|
|
|
128
195
|
|
|
129
196
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
130
197
|
|
|
198
|
+
// GET /dsh-goal/templates
|
|
199
|
+
if (req.method === 'GET' && (pathname === '/dsh-goal/templates' || pathname === '/dsh-goal/templates/')) {
|
|
200
|
+
res.statusCode = 200;
|
|
201
|
+
return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
|
|
202
|
+
}
|
|
203
|
+
|
|
131
204
|
// GET /dsh-goal/state
|
|
132
205
|
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
133
206
|
const sid = sessionIdOf(req, 'default');
|
|
@@ -144,8 +217,8 @@ export function registerRoutes(ctx, {
|
|
|
144
217
|
return res.end(JSON.stringify({ error: 'Forbidden: cross-site requests are rejected' }));
|
|
145
218
|
}
|
|
146
219
|
|
|
147
|
-
const origin = req.headers
|
|
148
|
-
const host = req.headers
|
|
220
|
+
const origin = req.headers?.origin;
|
|
221
|
+
const host = req.headers?.host;
|
|
149
222
|
if (origin && host) {
|
|
150
223
|
try {
|
|
151
224
|
const originHost = new URL(origin).host;
|
|
@@ -159,6 +232,11 @@ export function registerRoutes(ctx, {
|
|
|
159
232
|
}
|
|
160
233
|
}
|
|
161
234
|
|
|
235
|
+
if (!isTrustedCaller(req)) {
|
|
236
|
+
res.statusCode = 403;
|
|
237
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
238
|
+
}
|
|
239
|
+
|
|
162
240
|
let body = '';
|
|
163
241
|
let bodySize = 0;
|
|
164
242
|
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
@@ -196,6 +274,7 @@ export function registerRoutes(ctx, {
|
|
|
196
274
|
description: typeof description === 'string' ? description.trim() : '',
|
|
197
275
|
maxIterations: conf.maxIterations || 25,
|
|
198
276
|
lang: detectedLang,
|
|
277
|
+
cwd: data.cwd,
|
|
199
278
|
}, sid);
|
|
200
279
|
|
|
201
280
|
const startPrompt = detectedLang === 'zh'
|
|
@@ -309,8 +388,11 @@ export function registerRoutes(ctx, {
|
|
|
309
388
|
res.statusCode = 400;
|
|
310
389
|
return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
|
|
311
390
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
391
|
+
const sessionGoal = engine.getGoal(sid);
|
|
392
|
+
const allowedRoots = [process.cwd()];
|
|
393
|
+
if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
|
|
394
|
+
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
395
|
+
if (!safeCwd) {
|
|
314
396
|
res.statusCode = 400;
|
|
315
397
|
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
316
398
|
}
|
|
@@ -329,8 +411,11 @@ export function registerRoutes(ctx, {
|
|
|
329
411
|
res.statusCode = 404;
|
|
330
412
|
return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
|
|
331
413
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
414
|
+
const sessionGoal = engine.getGoal(sid);
|
|
415
|
+
const allowedRoots = [process.cwd()];
|
|
416
|
+
if (sessionGoal?.cwd) allowedRoots.push(sessionGoal.cwd);
|
|
417
|
+
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
418
|
+
if (!safeCwd) {
|
|
334
419
|
res.statusCode = 400;
|
|
335
420
|
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
336
421
|
}
|
|
@@ -343,6 +428,51 @@ export function registerRoutes(ctx, {
|
|
|
343
428
|
return res.end(JSON.stringify({ ok: true, artifact, state: snap }));
|
|
344
429
|
}
|
|
345
430
|
|
|
431
|
+
case 'intervene': {
|
|
432
|
+
result = engine.intervene(data, sid);
|
|
433
|
+
if (data.resume) {
|
|
434
|
+
engine.resume(sid);
|
|
435
|
+
const prompt = engine.getStatePromptInjection(sid);
|
|
436
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
437
|
+
resumeActiveAgent(prompt, sid);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
case 'preplan_approve': {
|
|
444
|
+
result = engine.approvePreplan(sid);
|
|
445
|
+
const prompt = engine.getStatePromptInjection(sid);
|
|
446
|
+
if (typeof resumeActiveAgent === 'function') {
|
|
447
|
+
resumeActiveAgent(prompt, sid);
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
case 'branch_action': {
|
|
453
|
+
const op = String(data.operation || data.branchOperation || 'merge').toLowerCase();
|
|
454
|
+
const targetBranch = String(data.targetBranch || 'main');
|
|
455
|
+
if (op === 'merge') {
|
|
456
|
+
const ok = engine.mergeGoalBranch(targetBranch, sid);
|
|
457
|
+
if (!ok) {
|
|
458
|
+
res.statusCode = 500;
|
|
459
|
+
return res.end(JSON.stringify({ error: `Failed to merge goal branch into ${targetBranch}` }));
|
|
460
|
+
}
|
|
461
|
+
} else if (op === 'discard') {
|
|
462
|
+
const ok = engine.discardGoalBranch(targetBranch, sid);
|
|
463
|
+
if (!ok) {
|
|
464
|
+
res.statusCode = 500;
|
|
465
|
+
return res.end(JSON.stringify({ error: 'Failed to discard goal branch' }));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
result = engine.getSnapshot(sid);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
case 'templates':
|
|
473
|
+
res.statusCode = 200;
|
|
474
|
+
return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
|
|
475
|
+
|
|
346
476
|
default:
|
|
347
477
|
res.statusCode = 400;
|
|
348
478
|
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
package/lib/tools.js
CHANGED
|
@@ -195,6 +195,9 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
195
195
|
const handleSetMilestones = async ({ milestones }, toolCtx) => {
|
|
196
196
|
const sid = sessionIdOf(toolCtx, 'default');
|
|
197
197
|
const snap = engine.setMilestones(milestones, sid);
|
|
198
|
+
if (typeof engine.recordToolActivity === 'function') {
|
|
199
|
+
engine.recordToolActivity('goal_set_milestones', 'success', 'Defined milestones', sid);
|
|
200
|
+
}
|
|
198
201
|
return {
|
|
199
202
|
success: true,
|
|
200
203
|
count: snap.milestones?.length || 0,
|
|
@@ -215,6 +218,7 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
215
218
|
properties: {
|
|
216
219
|
id: { type: 'string', description: 'Short identifier, e.g. M1, step-1' },
|
|
217
220
|
title: { type: 'string', description: 'Concise actionable title' },
|
|
221
|
+
dependsOn: { type: 'array', items: { type: 'string' }, description: 'Prerequisite milestone IDs' },
|
|
218
222
|
},
|
|
219
223
|
required: ['id', 'title'],
|
|
220
224
|
},
|
|
@@ -231,7 +235,10 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
231
235
|
// Tool 5: goal_update_progress
|
|
232
236
|
const handleUpdateProgress = async ({ milestone_id, status, notes, checklist }, toolCtx) => {
|
|
233
237
|
const sid = sessionIdOf(toolCtx, 'default');
|
|
234
|
-
engine.updateMilestone(milestone_id, status, notes, sid, checklist);
|
|
238
|
+
const ok = engine.updateMilestone(milestone_id, status, notes, sid, checklist);
|
|
239
|
+
if (typeof engine.recordToolActivity === 'function') {
|
|
240
|
+
engine.recordToolActivity('goal_update_progress', ok ? 'success' : 'error', String(milestone_id) + ' -> ' + String(status), sid);
|
|
241
|
+
}
|
|
235
242
|
const snap = engine.getSnapshot(sid);
|
|
236
243
|
const targetMilestone = snap.milestones?.find((m) => m.id === String(milestone_id) || m.id.toLowerCase().replace(/[^a-z0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^a-z0-9]/g, '') || m.id.toLowerCase().replace(/[^0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^0-9]/g, ''));
|
|
237
244
|
return {
|
|
@@ -282,6 +289,9 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
282
289
|
const handleGoalFinish = async ({ summary }, toolCtx) => {
|
|
283
290
|
const sid = sessionIdOf(toolCtx, 'default');
|
|
284
291
|
const snap = engine.completeGoal(summary, sid);
|
|
292
|
+
if (typeof engine.recordToolActivity === 'function') {
|
|
293
|
+
engine.recordToolActivity('goal_finish', 'success', summary ? summary.slice(0, 80) : 'Goal finished', sid);
|
|
294
|
+
}
|
|
285
295
|
return {
|
|
286
296
|
success: true,
|
|
287
297
|
completed: true,
|
package/package.json
CHANGED