@goodandready/dsh-goal 0.2.18 → 0.2.20
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-git-branch.js +62 -13
- package/lib/engine-issue-sync.js +30 -21
- package/lib/engine-reports.js +2 -2
- package/lib/git-snapshot-manager.js +1 -1
- package/lib/goal-engine-constants.js +46 -0
- package/lib/goal-engine.js +5 -4
- package/lib/index.js +1 -1
- package/lib/routes.js +43 -52
- package/lib/tools.js +5 -3
- package/package.json +2 -2
package/lib/engine-git-branch.js
CHANGED
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { getSafeCwd } from './goal-engine-constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if directory is inside a valid git repository
|
|
7
|
+
* @param {string} workspaceRoot
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isGitRepository(workspaceRoot) {
|
|
11
|
+
if (!workspaceRoot || typeof workspaceRoot !== 'string') return false;
|
|
12
|
+
try {
|
|
13
|
+
const safePath = getSafeCwd(workspaceRoot);
|
|
14
|
+
if (!fs.existsSync(safePath)) return false;
|
|
15
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
16
|
+
cwd: safePath,
|
|
17
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
18
|
+
timeout: 2000,
|
|
19
|
+
});
|
|
20
|
+
return true;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate that a git ref name is safe to pass to git commands
|
|
28
|
+
* Prevents flag injection (-*), path traversal, and illegal ref characters
|
|
29
|
+
* @param {string} ref
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
export function isValidGitRef(ref) {
|
|
33
|
+
if (!ref || typeof ref !== 'string') return false;
|
|
34
|
+
const trimmed = ref.trim();
|
|
35
|
+
if (!trimmed || trimmed.startsWith('-') || trimmed.endsWith('/') || trimmed.endsWith('.lock')) return false;
|
|
36
|
+
// Disallow control characters, whitespace, and git-forbidden special characters
|
|
37
|
+
if (/[\x00-\x20\x7f~^:?*\[\\ ]/.test(trimmed)) return false;
|
|
38
|
+
// Disallow consecutive dots or slashes, and reflog syntax @{
|
|
39
|
+
if (/\.\.|\/\/|@\{/.test(trimmed)) return false;
|
|
40
|
+
// Must match valid git ref character set
|
|
41
|
+
return /^[a-zA-Z0-9._/-]+$/.test(trimmed);
|
|
42
|
+
}
|
|
2
43
|
|
|
3
44
|
/**
|
|
4
45
|
* Format a sanitized git branch name from goal title
|
|
@@ -21,9 +62,11 @@ export function sanitizeBranchSlug(title) {
|
|
|
21
62
|
* @returns {string} branch name or 'HEAD'
|
|
22
63
|
*/
|
|
23
64
|
export function getCurrentGitBranch(cwd = process.cwd()) {
|
|
65
|
+
const safeCwd = getSafeCwd(cwd);
|
|
66
|
+
if (!isGitRepository(safeCwd)) return 'main';
|
|
24
67
|
try {
|
|
25
68
|
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
26
|
-
cwd,
|
|
69
|
+
cwd: safeCwd,
|
|
27
70
|
encoding: 'utf8',
|
|
28
71
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
29
72
|
}).trim();
|
|
@@ -39,10 +82,12 @@ export function getCurrentGitBranch(cwd = process.cwd()) {
|
|
|
39
82
|
* @returns {boolean} success
|
|
40
83
|
*/
|
|
41
84
|
export function createGoalBranch(cwd = process.cwd(), branchName) {
|
|
42
|
-
if (!branchName) return false;
|
|
85
|
+
if (!isValidGitRef(branchName)) return false;
|
|
86
|
+
const safeCwd = getSafeCwd(cwd);
|
|
87
|
+
if (!isGitRepository(safeCwd)) return false;
|
|
43
88
|
try {
|
|
44
89
|
execFileSync('git', ['checkout', '-b', branchName], {
|
|
45
|
-
cwd,
|
|
90
|
+
cwd: safeCwd,
|
|
46
91
|
encoding: 'utf8',
|
|
47
92
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
93
|
});
|
|
@@ -60,15 +105,17 @@ export function createGoalBranch(cwd = process.cwd(), branchName) {
|
|
|
60
105
|
* @returns {boolean} success
|
|
61
106
|
*/
|
|
62
107
|
export function mergeGoalBranch(cwd = process.cwd(), branchName, targetBranch = 'main') {
|
|
63
|
-
if (!branchName) return false;
|
|
108
|
+
if (!isValidGitRef(branchName) || !isValidGitRef(targetBranch)) return false;
|
|
109
|
+
const safeCwd = getSafeCwd(cwd);
|
|
110
|
+
if (!isGitRepository(safeCwd)) return false;
|
|
64
111
|
try {
|
|
65
|
-
execFileSync('git', ['checkout', targetBranch], {
|
|
66
|
-
cwd,
|
|
112
|
+
execFileSync('git', ['checkout', '--', targetBranch], {
|
|
113
|
+
cwd: safeCwd,
|
|
67
114
|
encoding: 'utf8',
|
|
68
115
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
116
|
});
|
|
70
|
-
execFileSync('git', ['merge', '--no-ff', '-m', `chore(goal): merge ${branchName}`, branchName], {
|
|
71
|
-
cwd,
|
|
117
|
+
execFileSync('git', ['merge', '--no-ff', '-m', `chore(goal): merge ${branchName}`, '--', branchName], {
|
|
118
|
+
cwd: safeCwd,
|
|
72
119
|
encoding: 'utf8',
|
|
73
120
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
121
|
});
|
|
@@ -86,15 +133,17 @@ export function mergeGoalBranch(cwd = process.cwd(), branchName, targetBranch =
|
|
|
86
133
|
* @returns {boolean} success
|
|
87
134
|
*/
|
|
88
135
|
export function discardGoalBranch(cwd = process.cwd(), branchName, targetBranch = 'main') {
|
|
89
|
-
if (!branchName) return false;
|
|
136
|
+
if (!isValidGitRef(branchName) || !isValidGitRef(targetBranch)) return false;
|
|
137
|
+
const safeCwd = getSafeCwd(cwd);
|
|
138
|
+
if (!isGitRepository(safeCwd)) return false;
|
|
90
139
|
try {
|
|
91
|
-
execFileSync('git', ['checkout', targetBranch], {
|
|
92
|
-
cwd,
|
|
140
|
+
execFileSync('git', ['checkout', '--', targetBranch], {
|
|
141
|
+
cwd: safeCwd,
|
|
93
142
|
encoding: 'utf8',
|
|
94
143
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
95
144
|
});
|
|
96
|
-
execFileSync('git', ['branch', '-D', branchName], {
|
|
97
|
-
cwd,
|
|
145
|
+
execFileSync('git', ['branch', '-D', '--', branchName], {
|
|
146
|
+
cwd: safeCwd,
|
|
98
147
|
encoding: 'utf8',
|
|
99
148
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
149
|
});
|
package/lib/engine-issue-sync.js
CHANGED
|
@@ -74,31 +74,37 @@ export function updateChecklistInMarkdown(markdown, itemText, done) {
|
|
|
74
74
|
* @returns {{ token?: string, baseUrl: string }}
|
|
75
75
|
*/
|
|
76
76
|
export function resolveGiteaAuth(options = {}) {
|
|
77
|
-
const
|
|
77
|
+
const baseUrl = options.baseUrl || process.env.GITEA_BASE_URL || "";
|
|
78
78
|
if (options.token) {
|
|
79
|
-
return { token: options.token, baseUrl
|
|
79
|
+
return { token: options.token, baseUrl };
|
|
80
80
|
}
|
|
81
81
|
if (process.env.GITEA_TOKEN) {
|
|
82
|
-
return { token: process.env.GITEA_TOKEN, baseUrl
|
|
82
|
+
return { token: process.env.GITEA_TOKEN, baseUrl };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const credPath = process.env.DSH_GITEA_CREDENTIALS
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
const credPath = process.env.DSH_GITEA_CREDENTIALS;
|
|
86
|
+
if (credPath && typeof credPath === "string") {
|
|
87
|
+
try {
|
|
88
|
+
if (fs.existsSync(credPath)) {
|
|
89
|
+
const raw = fs.readFileSync(credPath, "utf8");
|
|
90
|
+
const creds = JSON.parse(raw);
|
|
91
|
+
const agentName = process.env.DSH_GITEA_AGENT;
|
|
92
|
+
const agent = (agentName && creds?.agents?.[agentName])
|
|
93
|
+
|| creds?.agents?.["deepseek-harness"]
|
|
94
|
+
|| creds?.agents?.antigravity
|
|
95
|
+
|| creds;
|
|
96
|
+
if (agent?.token) {
|
|
97
|
+
return {
|
|
98
|
+
token: agent.token,
|
|
99
|
+
baseUrl: baseUrl || agent.http_remote_base || "",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
96
102
|
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Non-fatal credential read error
|
|
97
105
|
}
|
|
98
|
-
} catch (err) {
|
|
99
|
-
// Non-fatal credential read error
|
|
100
106
|
}
|
|
101
|
-
return { baseUrl
|
|
107
|
+
return { baseUrl };
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
/**
|
|
@@ -122,9 +128,9 @@ export function resolveRepoInfo(cwd = process.cwd(), options = {}) {
|
|
|
122
128
|
return { owner: match[1], repo: match[2] };
|
|
123
129
|
}
|
|
124
130
|
} catch (err) {
|
|
125
|
-
// Fallback to
|
|
131
|
+
// Fallback to options or empty
|
|
126
132
|
}
|
|
127
|
-
return { owner:
|
|
133
|
+
return { owner: options.owner || "", repo: options.repo || "" };
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
/**
|
|
@@ -140,11 +146,14 @@ export async function syncMilestoneWithIssue(goal, milestone, options = {}) {
|
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
const auth = resolveGiteaAuth(options);
|
|
143
|
-
if (!auth.token) {
|
|
144
|
-
return { synced: false, error: 'No Gitea API token available' };
|
|
149
|
+
if (!auth.token || !auth.baseUrl) {
|
|
150
|
+
return { synced: false, error: 'No Gitea API token or base URL available' };
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
const repoInfo = resolveRepoInfo(goal.cwd || process.cwd(), options);
|
|
154
|
+
if (!repoInfo.owner || !repoInfo.repo) {
|
|
155
|
+
return { synced: false, error: "No Gitea repository owner or name available" };
|
|
156
|
+
}
|
|
148
157
|
const issueNum = goal.issueRef.issueNumber;
|
|
149
158
|
const issueApiUrl = `${auth.baseUrl}/api/v1/repos/${repoInfo.owner}/${repoInfo.repo}/issues/${issueNum}`;
|
|
150
159
|
|
package/lib/engine-reports.js
CHANGED
|
@@ -206,14 +206,14 @@ export function createMilestoneCheckpoint(milestone, sessionId = 'default', cwd)
|
|
|
206
206
|
}).trim();
|
|
207
207
|
|
|
208
208
|
if (status) {
|
|
209
|
-
execFileSync('git', ['add', '-
|
|
209
|
+
execFileSync('git', ['add', '-u'], {
|
|
210
210
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
211
211
|
timeout: 3000,
|
|
212
212
|
cwd: cwd || undefined,
|
|
213
213
|
});
|
|
214
214
|
const cleanTitle = (milestone.title || '').replace(/[\"\`\$]/g, '');
|
|
215
215
|
const msg = `checkpoint(goal): [${milestone.id}] ${cleanTitle}`;
|
|
216
|
-
execFileSync('git', ['commit', '-m', msg
|
|
216
|
+
execFileSync('git', ['commit', '-m', msg], {
|
|
217
217
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
218
218
|
timeout: 5000,
|
|
219
219
|
cwd: cwd || undefined,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
/**
|
|
2
4
|
* Enums and helpers for DSH Goal Engine
|
|
3
5
|
*/
|
|
@@ -244,3 +246,47 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
244
246
|
enableVoiceAnnouncements: engine.enableVoiceAnnouncements ?? false,
|
|
245
247
|
};
|
|
246
248
|
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if child path is inside parent directory or equal to it
|
|
253
|
+
* @param {string} parent
|
|
254
|
+
* @param {string} child
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
export function isPathInsideOrEqual(parent, child) {
|
|
258
|
+
if (!parent || !child) return false;
|
|
259
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
260
|
+
return !rel || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Validate and confine working directory to ensure it exists and is allowed
|
|
265
|
+
* @param {string} requestedCwd
|
|
266
|
+
* @param {string[]} [allowedRoots=[]]
|
|
267
|
+
* @returns {string} resolved safe cwd
|
|
268
|
+
*/
|
|
269
|
+
export function getSafeCwd(requestedCwd, allowedRoots = []) {
|
|
270
|
+
const defaultRoot = allowedRoots[0] || process.cwd();
|
|
271
|
+
if (!requestedCwd || typeof requestedCwd !== "string" || !requestedCwd.trim()) {
|
|
272
|
+
return defaultRoot;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const resolved = path.resolve(requestedCwd.trim());
|
|
277
|
+
const st = fs.statSync(resolved);
|
|
278
|
+
if (!st.isDirectory()) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const roots = allowedRoots.length > 0 ? allowedRoots : [process.cwd()];
|
|
283
|
+
const isAllowed = roots.some((root) => isPathInsideOrEqual(root, resolved));
|
|
284
|
+
if (!isAllowed) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return resolved;
|
|
289
|
+
} catch (err) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot } from './goal-engine-constants.js';
|
|
1
|
+
import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot, getSafeCwd } from './goal-engine-constants.js';
|
|
2
2
|
import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
|
|
3
3
|
import { EngineStore } from './engine-store.js';
|
|
4
4
|
import { buildStatePromptInjection } from './engine-prompt.js';
|
|
@@ -193,7 +193,7 @@ export class GoalEngine {
|
|
|
193
193
|
title: cleanTitle,
|
|
194
194
|
description: options.description?.trim() || '',
|
|
195
195
|
lang: options.lang || detectLanguage(cleanTitle),
|
|
196
|
-
cwd: options.cwd ?
|
|
196
|
+
cwd: options.cwd ? getSafeCwd(options.cwd) : null,
|
|
197
197
|
state: GoalState.RUNNING,
|
|
198
198
|
startedAt: now,
|
|
199
199
|
pausedAt: null,
|
|
@@ -227,8 +227,9 @@ export class GoalEngine {
|
|
|
227
227
|
|
|
228
228
|
if (this.autoBranchOnGoalStart && options.autoBranch !== false && this.storagePath !== null && process.env.NODE_ENV !== 'test') {
|
|
229
229
|
const slug = sanitizeBranchSlug(cleanTitle);
|
|
230
|
-
const
|
|
231
|
-
|
|
230
|
+
const targetCwd = goal.cwd || process.cwd();
|
|
231
|
+
const parentBranch = getCurrentGitBranch(targetCwd);
|
|
232
|
+
if (createGoalBranch(targetCwd, slug)) {
|
|
232
233
|
goal.gitBranch = slug;
|
|
233
234
|
goal.gitParentBranch = parentBranch;
|
|
234
235
|
}
|
package/lib/index.js
CHANGED
package/lib/routes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
3
|
+
import { MilestoneStatus, detectLanguage, sessionIdOf, isPathInsideOrEqual, getSafeCwd } from './goal-engine-constants.js';
|
|
4
|
+
import { isValidGitRef } from './engine-git-branch.js';
|
|
4
5
|
import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
|
|
5
6
|
import { getTemplatesCatalogue, instantiateTemplate } from './engine-templates.js';
|
|
6
7
|
import { parseIssueChecklist } from './engine-issue-sync.js';
|
|
@@ -19,31 +20,42 @@ export function isLoopback(value) {
|
|
|
19
20
|
|
|
20
21
|
export function isTrustedCaller(req) {
|
|
21
22
|
if (!req) return false;
|
|
22
|
-
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress ||
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || "";
|
|
24
|
+
const site = req.headers?.["sec-fetch-site"];
|
|
25
|
+
|
|
26
|
+
// 1. Cross-site and same-site browser requests are rejected
|
|
27
|
+
if (site === "cross-site" || site === "same-site") return false;
|
|
28
|
+
|
|
29
|
+
// 2. Same-origin browser UI requests (operator UI on LAN or localhost)
|
|
30
|
+
if (site === "same-origin") {
|
|
31
|
+
const host = String(req.headers?.host || "").toLowerCase();
|
|
32
|
+
const origin = String(req.headers?.origin || "").trim().toLowerCase();
|
|
33
|
+
if (origin && host) {
|
|
34
|
+
try {
|
|
35
|
+
const u = new URL(origin);
|
|
36
|
+
if (u.host === host) return true;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const referer = String(req.headers?.referer || "").trim().toLowerCase();
|
|
42
|
+
if (referer && host) {
|
|
43
|
+
try {
|
|
44
|
+
const u = new URL(referer);
|
|
45
|
+
if (u.host === host) return true;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
32
50
|
return false;
|
|
33
51
|
}
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
try {
|
|
38
|
-
const u = new URL(referer);
|
|
39
|
-
if (u.host === host) return true;
|
|
40
|
-
} catch (err) { /* invalid url */ }
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
53
|
+
// 3. No declared site: only loopback remote clients (CLI, reverse proxy) are trusted
|
|
54
|
+
if (isLoopback(remote)) return true;
|
|
43
55
|
|
|
44
|
-
// Fallback for
|
|
45
|
-
if (!remote && host) {
|
|
46
|
-
const hostName = host.split(
|
|
56
|
+
// 4. Fallback for test harnesses without remote socket where host header explicitly targets loopback
|
|
57
|
+
if (!remote && req.headers?.host) {
|
|
58
|
+
const hostName = String(req.headers.host).split(":")[0];
|
|
47
59
|
if (isLoopback(hostName)) {
|
|
48
60
|
return true;
|
|
49
61
|
}
|
|
@@ -52,36 +64,7 @@ export function isTrustedCaller(req) {
|
|
|
52
64
|
return false;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
export
|
|
56
|
-
if (!parent || !child) return false;
|
|
57
|
-
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
58
|
-
return !rel || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function getSafeCwd(requestedCwd, allowedRoots = []) {
|
|
62
|
-
const defaultRoot = allowedRoots[0] || process.cwd();
|
|
63
|
-
if (!requestedCwd || typeof requestedCwd !== 'string' || !requestedCwd.trim()) {
|
|
64
|
-
return defaultRoot;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const resolved = path.resolve(requestedCwd.trim());
|
|
69
|
-
const st = fs.statSync(resolved);
|
|
70
|
-
if (!st.isDirectory()) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const roots = allowedRoots.length > 0 ? allowedRoots : [process.cwd()];
|
|
75
|
-
const isAllowed = roots.some((root) => isPathInsideOrEqual(root, resolved));
|
|
76
|
-
if (!isAllowed) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return resolved;
|
|
81
|
-
} catch (err) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
67
|
+
export { isPathInsideOrEqual, getSafeCwd };
|
|
85
68
|
|
|
86
69
|
/**
|
|
87
70
|
* Register webServer HTTP routes and SSE stream for DSH Goal
|
|
@@ -583,6 +566,14 @@ export function registerRoutes(ctx, {
|
|
|
583
566
|
case 'branch_action': {
|
|
584
567
|
const op = String(data.operation || data.branchOperation || 'merge').toLowerCase();
|
|
585
568
|
const targetBranch = String(data.targetBranch || 'main');
|
|
569
|
+
if (!isValidGitRef(targetBranch)) {
|
|
570
|
+
res.statusCode = 400;
|
|
571
|
+
return res.end(JSON.stringify({ error: `Invalid targetBranch ref: "${targetBranch}"` }));
|
|
572
|
+
}
|
|
573
|
+
if (op !== 'merge' && op !== 'discard') {
|
|
574
|
+
res.statusCode = 400;
|
|
575
|
+
return res.end(JSON.stringify({ error: `Invalid branch operation: "${op}". Allowed: merge, discard` }));
|
|
576
|
+
}
|
|
586
577
|
if (engine) {
|
|
587
578
|
if (op === 'merge') {
|
|
588
579
|
const ok = engine.mergeGoalBranch(targetBranch, sid);
|
package/lib/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MilestoneManager } from './milestone-manager.js';
|
|
2
2
|
import { AddonStore } from './addon-store.js';
|
|
3
|
+
import { getSafeCwd } from './goal-engine-constants.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Tools module for @goodandready/dsh-goal (Core Addon).
|
|
@@ -83,9 +84,10 @@ export function registerTools(ctx, { milestoneManager, goalsService, logger = co
|
|
|
83
84
|
if (!milestoneId) {
|
|
84
85
|
throw new Error('milestoneId is required for complete action');
|
|
85
86
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const rawCwd = toolCtx?.session?.cwd || toolCtx?.cwd || process.cwd();
|
|
88
|
+
const safeCwd = getSafeCwd(rawCwd);
|
|
89
|
+
const notes = typeof args.notes === 'string' ? args.notes : '';
|
|
90
|
+
const completed = await mm.completeMilestone(goalId, milestoneId, notes, safeCwd);
|
|
89
91
|
return { ok: true, goalId, milestoneId, milestone: completed };
|
|
90
92
|
}
|
|
91
93
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-goal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
4
4
|
"description": "Core Goal Addon (Token Budget, Milestones, Git Snapshots & Retrospective Reports) for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "node --test test/*.test.mjs"
|
|
22
|
+
"test": "NODE_ENV=test node --test test/*.test.mjs"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"dsh",
|