@goodandready/dsh-goal 0.2.10 → 0.2.11
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/engine-issue-sync.js +128 -0
- package/lib/goal-engine.js +17 -6
- package/lib/index.js +18 -27
- package/lib/routes.js +45 -7
- package/lib/tools.js +8 -1
- package/package.json +1 -1
package/lib/engine-issue-sync.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Parse issue identifier from goal title or string
|
|
3
6
|
* @param {string} input
|
|
@@ -64,3 +67,128 @@ export function updateChecklistInMarkdown(markdown, itemText, done) {
|
|
|
64
67
|
return line;
|
|
65
68
|
}).join('\n');
|
|
66
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve Gitea API credentials from options, environment, or agent config
|
|
73
|
+
* @param {object} [options]
|
|
74
|
+
* @returns {{ token?: string, baseUrl: string }}
|
|
75
|
+
*/
|
|
76
|
+
export function resolveGiteaAuth(options = {}) {
|
|
77
|
+
const defaultUrl = options.baseUrl || process.env.GITEA_BASE_URL || 'http://127.0.0.1:3005';
|
|
78
|
+
if (options.token) {
|
|
79
|
+
return { token: options.token, baseUrl: defaultUrl };
|
|
80
|
+
}
|
|
81
|
+
if (process.env.GITEA_TOKEN) {
|
|
82
|
+
return { token: process.env.GITEA_TOKEN, baseUrl: defaultUrl };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const credPath = process.env.DSH_GITEA_CREDENTIALS || '/mnt/external/Project/DEV/.gitea-agent-credentials.json';
|
|
86
|
+
try {
|
|
87
|
+
if (fs.existsSync(credPath)) {
|
|
88
|
+
const raw = fs.readFileSync(credPath, 'utf8');
|
|
89
|
+
const creds = JSON.parse(raw);
|
|
90
|
+
const agent = creds?.agents?.['deepseek-harness'] || creds?.agents?.antigravity;
|
|
91
|
+
if (agent?.token) {
|
|
92
|
+
return {
|
|
93
|
+
token: agent.token,
|
|
94
|
+
baseUrl: options.baseUrl || agent.http_remote_base || defaultUrl,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Non-fatal credential read error
|
|
100
|
+
}
|
|
101
|
+
return { baseUrl: defaultUrl };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve repository owner and repo name from git remote or options
|
|
106
|
+
* @param {string} [cwd]
|
|
107
|
+
* @param {object} [options]
|
|
108
|
+
* @returns {{ owner: string, repo: string }}
|
|
109
|
+
*/
|
|
110
|
+
export function resolveRepoInfo(cwd = process.cwd(), options = {}) {
|
|
111
|
+
if (options.owner && options.repo) {
|
|
112
|
+
return { owner: options.owner, repo: options.repo };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const remote = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
116
|
+
cwd,
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
119
|
+
}).trim();
|
|
120
|
+
const match = remote.match(/(?:[:/])([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
121
|
+
if (match) {
|
|
122
|
+
return { owner: match[1], repo: match[2] };
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// Fallback to default repository
|
|
126
|
+
}
|
|
127
|
+
return { owner: 'goodandready', repo: 'dsh-goal' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Actively sync completed milestone checklist item with Gitea issue
|
|
132
|
+
* @param {object} goal
|
|
133
|
+
* @param {object} milestone
|
|
134
|
+
* @param {object} [options]
|
|
135
|
+
* @returns {Promise<{ synced: boolean, updated?: boolean, issueNumber?: number, error?: string }>}
|
|
136
|
+
*/
|
|
137
|
+
export async function syncMilestoneWithIssue(goal, milestone, options = {}) {
|
|
138
|
+
if (!goal?.issueRef?.issueNumber || !milestone?.title) {
|
|
139
|
+
return { synced: false, error: 'No issue reference or milestone title' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const auth = resolveGiteaAuth(options);
|
|
143
|
+
if (!auth.token) {
|
|
144
|
+
return { synced: false, error: 'No Gitea API token available' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const repoInfo = resolveRepoInfo(goal.cwd || process.cwd(), options);
|
|
148
|
+
const issueNum = goal.issueRef.issueNumber;
|
|
149
|
+
const issueApiUrl = `${auth.baseUrl}/api/v1/repos/${repoInfo.owner}/${repoInfo.repo}/issues/${issueNum}`;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const fetchFn = options.fetch || globalThis.fetch;
|
|
153
|
+
if (typeof fetchFn !== 'function') {
|
|
154
|
+
return { synced: false, error: 'Fetch API unavailable' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const getRes = await fetchFn(issueApiUrl, {
|
|
158
|
+
headers: {
|
|
159
|
+
'Authorization': `token ${auth.token}`,
|
|
160
|
+
'Accept': 'application/json',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!getRes.ok) {
|
|
165
|
+
return { synced: false, error: `HTTP ${getRes.status} fetching issue #${issueNum}` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const issueData = await getRes.json();
|
|
169
|
+
const currentBody = issueData.body || '';
|
|
170
|
+
const updatedBody = updateChecklistInMarkdown(currentBody, milestone.title, true);
|
|
171
|
+
|
|
172
|
+
if (updatedBody === currentBody) {
|
|
173
|
+
return { synced: true, updated: false, issueNumber: issueNum };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const patchRes = await fetchFn(issueApiUrl, {
|
|
177
|
+
method: 'PATCH',
|
|
178
|
+
headers: {
|
|
179
|
+
'Authorization': `token ${auth.token}`,
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
'Accept': 'application/json',
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify({ body: updatedBody }),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!patchRes.ok) {
|
|
187
|
+
return { synced: false, error: `HTTP ${patchRes.status} updating issue #${issueNum}` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { synced: true, updated: true, issueNumber: issueNum };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { synced: false, error: err.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -5,7 +5,7 @@ import { buildStatePromptInjection } from './engine-prompt.js';
|
|
|
5
5
|
import { matchMilestone, parseMilestoneItems, applyMilestoneUpdate, toggleMilestoneChecklistItem } from './engine-milestones.js';
|
|
6
6
|
import { applyIntervention, recordActivity, processTokenBudget } from './engine-intervention.js';
|
|
7
7
|
import { createGoalBranch, sanitizeBranchSlug, getCurrentGitBranch, mergeGoalBranch, discardGoalBranch } from './engine-git-branch.js';
|
|
8
|
-
import { parseIssueRef } from './engine-issue-sync.js';
|
|
8
|
+
import { parseIssueRef, syncMilestoneWithIssue } from './engine-issue-sync.js';
|
|
9
9
|
import { generateRetrospectiveData } from './engine-retrospective.js';
|
|
10
10
|
import { generatePreplanDraft } from './engine-templates.js';
|
|
11
11
|
import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget } from './engine-sessions.js';
|
|
@@ -378,11 +378,22 @@ export class GoalEngine {
|
|
|
378
378
|
const prevStatus = res.prevStatus;
|
|
379
379
|
recordActivity(goal, { type: 'milestone', tool: 'updateMilestone', status: 'success', details: `${id} -> ${status}` });
|
|
380
380
|
|
|
381
|
-
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
381
|
+
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED) {
|
|
382
|
+
if (this.autoCheckpointOnMilestone) {
|
|
383
|
+
const commitHash = createMilestoneCheckpoint(target, sid);
|
|
384
|
+
if (commitHash) {
|
|
385
|
+
target.checkpointCommit = commitHash;
|
|
386
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Git checkpoint saved for [${target.title}]: ${commitHash}` });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (goal.issueRef) {
|
|
390
|
+
syncMilestoneWithIssue(goal, target).then((syncRes) => {
|
|
391
|
+
if (syncRes?.updated) {
|
|
392
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Synced checklist for issue #${syncRes.issueNumber}` });
|
|
393
|
+
recordActivity(goal, { type: 'issue', tool: 'syncIssueChecklist', status: 'success', details: `Issue #${syncRes.issueNumber} updated` });
|
|
394
|
+
this.emit(sid);
|
|
395
|
+
}
|
|
396
|
+
}).catch(() => {});
|
|
386
397
|
}
|
|
387
398
|
}
|
|
388
399
|
|
package/lib/index.js
CHANGED
|
@@ -202,35 +202,19 @@ export function apply(ctx, config = {}) {
|
|
|
202
202
|
const getConfig = () => {
|
|
203
203
|
if (settingsScope) {
|
|
204
204
|
const snap = typeof settingsScope.getSnapshot === 'function' ? settingsScope.getSnapshot() : null;
|
|
205
|
+
let live = null;
|
|
205
206
|
if (snap) {
|
|
206
|
-
if (snap.status === 'ready' || snap.status === undefined) {
|
|
207
|
-
|
|
208
|
-
if (live && typeof live === 'object') {
|
|
209
|
-
return {
|
|
210
|
-
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
211
|
-
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
212
|
-
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
213
|
-
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
214
|
-
consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
|
|
215
|
-
enableBrowserNotifications: typeof live.enableBrowserNotifications === 'boolean' ? live.enableBrowserNotifications : currentSettings.enableBrowserNotifications,
|
|
216
|
-
maxTokenBudget: typeof live.maxTokenBudget === 'number' ? live.maxTokenBudget : currentSettings.maxTokenBudget,
|
|
217
|
-
budgetWarningThreshold: typeof live.budgetWarningThreshold === 'number' ? live.budgetWarningThreshold : currentSettings.budgetWarningThreshold,
|
|
218
|
-
autoCheckpointOnMilestone: typeof live.autoCheckpointOnMilestone === 'boolean' ? live.autoCheckpointOnMilestone : currentSettings.autoCheckpointOnMilestone,
|
|
219
|
-
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
207
|
+
if (snap.status === 'ready' || snap.status === 'writable' || snap.status === undefined) {
|
|
208
|
+
live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
|
|
222
209
|
}
|
|
223
210
|
} else if (typeof settingsScope.get === 'function') {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
211
|
+
live = settingsScope.get();
|
|
212
|
+
}
|
|
213
|
+
if (live && typeof live === 'object') {
|
|
214
|
+
return {
|
|
215
|
+
...currentSettings,
|
|
216
|
+
...live,
|
|
217
|
+
};
|
|
234
218
|
}
|
|
235
219
|
}
|
|
236
220
|
return currentSettings;
|
|
@@ -316,7 +300,9 @@ export function apply(ctx, config = {}) {
|
|
|
316
300
|
try {
|
|
317
301
|
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
318
302
|
const globalSections = pctx.systemPrompt.layers?.global?.sections;
|
|
303
|
+
let displacedPromptSection = null;
|
|
319
304
|
if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
|
|
305
|
+
displacedPromptSection = globalSections.data.get('tool:goal');
|
|
320
306
|
globalSections.data.delete('tool:goal');
|
|
321
307
|
}
|
|
322
308
|
|
|
@@ -334,7 +320,12 @@ export function apply(ctx, config = {}) {
|
|
|
334
320
|
});
|
|
335
321
|
|
|
336
322
|
if (typeof unregisterSection === 'function') {
|
|
337
|
-
pctx.effect(() => () =>
|
|
323
|
+
pctx.effect(() => () => {
|
|
324
|
+
unregisterSection();
|
|
325
|
+
if (displacedPromptSection && globalSections?.data instanceof Map) {
|
|
326
|
+
globalSections.data.set('tool:goal', displacedPromptSection);
|
|
327
|
+
}
|
|
328
|
+
}, 'dsh-goal: system prompt section');
|
|
338
329
|
}
|
|
339
330
|
}
|
|
340
331
|
} catch (err) {
|
package/lib/routes.js
CHANGED
|
@@ -21,10 +21,6 @@ export function isTrustedCaller(req) {
|
|
|
21
21
|
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || '';
|
|
22
22
|
if (isLoopback(remote)) return true;
|
|
23
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
24
|
const host = String(req.headers?.host || '').toLowerCase();
|
|
29
25
|
const origin = String(req.headers?.origin || '').trim().toLowerCase();
|
|
30
26
|
if (origin) {
|
|
@@ -167,6 +163,11 @@ export function registerRoutes(ctx, {
|
|
|
167
163
|
|
|
168
164
|
// GET /dsh-goal/events: Server-Sent Events realtime snapshot stream
|
|
169
165
|
if (req.method === 'GET' && (pathname === '/dsh-goal/events' || pathname === '/dsh-goal/events/')) {
|
|
166
|
+
if (!isTrustedCaller(req)) {
|
|
167
|
+
res.statusCode = 403;
|
|
168
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
169
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
170
|
+
}
|
|
170
171
|
const sid = sessionIdOf(req, 'default');
|
|
171
172
|
res.writeHead(200, {
|
|
172
173
|
'Content-Type': 'text/event-stream',
|
|
@@ -197,12 +198,20 @@ export function registerRoutes(ctx, {
|
|
|
197
198
|
|
|
198
199
|
// GET /dsh-goal/templates
|
|
199
200
|
if (req.method === 'GET' && (pathname === '/dsh-goal/templates' || pathname === '/dsh-goal/templates/')) {
|
|
201
|
+
if (!isTrustedCaller(req)) {
|
|
202
|
+
res.statusCode = 403;
|
|
203
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
204
|
+
}
|
|
200
205
|
res.statusCode = 200;
|
|
201
206
|
return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
// GET /dsh-goal/state
|
|
205
210
|
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
211
|
+
if (!isTrustedCaller(req)) {
|
|
212
|
+
res.statusCode = 403;
|
|
213
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
214
|
+
}
|
|
206
215
|
const sid = sessionIdOf(req, 'default');
|
|
207
216
|
res.statusCode = 200;
|
|
208
217
|
return res.end(JSON.stringify(engine.getSnapshot(sid)));
|
|
@@ -268,13 +277,26 @@ export function registerRoutes(ctx, {
|
|
|
268
277
|
res.statusCode = 400;
|
|
269
278
|
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
270
279
|
}
|
|
280
|
+
const trustedRoots = [process.cwd()];
|
|
281
|
+
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
282
|
+
trustedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
283
|
+
}
|
|
284
|
+
let safeStartCwd = undefined;
|
|
285
|
+
if (data.cwd) {
|
|
286
|
+
const validated = getSafeCwd(data.cwd, trustedRoots);
|
|
287
|
+
if (!validated) {
|
|
288
|
+
res.statusCode = 400;
|
|
289
|
+
return res.end(JSON.stringify({ error: 'Invalid or unauthorized working directory' }));
|
|
290
|
+
}
|
|
291
|
+
safeStartCwd = validated;
|
|
292
|
+
}
|
|
271
293
|
const detectedLang = data.lang || detectLanguage(cleanTitle);
|
|
272
294
|
const conf = typeof getConfig === 'function' ? getConfig() : { maxIterations: 25 };
|
|
273
295
|
result = engine.startGoal(cleanTitle, {
|
|
274
296
|
description: typeof description === 'string' ? description.trim() : '',
|
|
275
297
|
maxIterations: conf.maxIterations || 25,
|
|
276
298
|
lang: detectedLang,
|
|
277
|
-
cwd:
|
|
299
|
+
cwd: safeStartCwd,
|
|
278
300
|
}, sid);
|
|
279
301
|
|
|
280
302
|
const startPrompt = detectedLang === 'zh'
|
|
@@ -390,7 +412,15 @@ export function registerRoutes(ctx, {
|
|
|
390
412
|
}
|
|
391
413
|
const sessionGoal = engine.getGoal(sid);
|
|
392
414
|
const allowedRoots = [process.cwd()];
|
|
393
|
-
if (
|
|
415
|
+
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
416
|
+
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
417
|
+
}
|
|
418
|
+
if (sessionGoal?.cwd) {
|
|
419
|
+
const isCwdAllowed = allowedRoots.some((root) => isPathInsideOrEqual(root, sessionGoal.cwd));
|
|
420
|
+
if (isCwdAllowed) {
|
|
421
|
+
allowedRoots.push(sessionGoal.cwd);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
394
424
|
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
395
425
|
if (!safeCwd) {
|
|
396
426
|
res.statusCode = 400;
|
|
@@ -413,7 +443,15 @@ export function registerRoutes(ctx, {
|
|
|
413
443
|
}
|
|
414
444
|
const sessionGoal = engine.getGoal(sid);
|
|
415
445
|
const allowedRoots = [process.cwd()];
|
|
416
|
-
if (
|
|
446
|
+
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
447
|
+
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
448
|
+
}
|
|
449
|
+
if (sessionGoal?.cwd) {
|
|
450
|
+
const isCwdAllowed = allowedRoots.some((root) => isPathInsideOrEqual(root, sessionGoal.cwd));
|
|
451
|
+
if (isCwdAllowed) {
|
|
452
|
+
allowedRoots.push(sessionGoal.cwd);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
417
455
|
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
418
456
|
if (!safeCwd) {
|
|
419
457
|
res.statusCode = 400;
|
package/lib/tools.js
CHANGED
|
@@ -46,16 +46,23 @@ export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
|
46
46
|
ctx.inject(['tools'], (tctx) => {
|
|
47
47
|
if (!tctx.tools?.register) return;
|
|
48
48
|
|
|
49
|
+
const displacedTools = new Map();
|
|
49
50
|
const safeRegister = (definition) => {
|
|
50
51
|
try {
|
|
51
52
|
const name = definition.name;
|
|
52
53
|
const globalTools = tctx.tools.layers?.global?.tools;
|
|
53
54
|
if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
|
|
55
|
+
displacedTools.set(name, globalTools.data.get(name));
|
|
54
56
|
globalTools.data.delete(name);
|
|
55
57
|
}
|
|
56
58
|
const unregister = tctx.tools.register(definition);
|
|
57
59
|
if (typeof unregister === 'function') {
|
|
58
|
-
tctx.effect(() => () =>
|
|
60
|
+
tctx.effect(() => () => {
|
|
61
|
+
unregister();
|
|
62
|
+
if (displacedTools.has(name) && globalTools?.data instanceof Map) {
|
|
63
|
+
globalTools.data.set(name, displacedTools.get(name));
|
|
64
|
+
}
|
|
65
|
+
}, `dsh-goal: tool ${name}`);
|
|
59
66
|
}
|
|
60
67
|
} catch (err) {
|
|
61
68
|
console.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
|
package/package.json
CHANGED