@goodandready/dsh-goal 0.2.30 → 0.2.31
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 +315 -4
- package/lib/engine-diff.js +147 -0
- package/lib/engine-kanban-sync.js +242 -0
- package/lib/goal-engine.js +13 -5
- package/lib/routes.js +46 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1808,9 +1808,129 @@ window.__ModuleLoader__.load({
|
|
|
1808
1808
|
background: var(--dsw-alias-bg-layer-2);
|
|
1809
1809
|
font-family: monospace;
|
|
1810
1810
|
}
|
|
1811
|
-
|
|
1811
|
+
.dsh-goal-tabs {
|
|
1812
|
+
display: flex;
|
|
1813
|
+
gap: 8px;
|
|
1814
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
1815
|
+
margin: 10px 0 14px 0;
|
|
1816
|
+
padding-bottom: 6px;
|
|
1817
|
+
}
|
|
1818
|
+
.dsh-goal-tab-btn {
|
|
1819
|
+
background: transparent;
|
|
1820
|
+
border: none;
|
|
1821
|
+
font-size: 13px;
|
|
1822
|
+
font-weight: 500;
|
|
1823
|
+
color: var(--dsw-alias-label-secondary);
|
|
1824
|
+
cursor: pointer;
|
|
1825
|
+
padding: 6px 12px;
|
|
1826
|
+
border-radius: 6px;
|
|
1827
|
+
display: inline-flex;
|
|
1828
|
+
align-items: center;
|
|
1829
|
+
gap: 6px;
|
|
1830
|
+
transition: all 0.15s ease;
|
|
1831
|
+
}
|
|
1832
|
+
.dsh-goal-tab-btn:hover {
|
|
1833
|
+
color: var(--dsw-alias-label-primary);
|
|
1834
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
1835
|
+
}
|
|
1836
|
+
.dsh-goal-tab-btn.active {
|
|
1837
|
+
color: var(--dsw-alias-brand-primary);
|
|
1838
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
1839
|
+
font-weight: 600;
|
|
1840
|
+
}
|
|
1841
|
+
.dsh-goal-diff-box {
|
|
1842
|
+
display: flex;
|
|
1843
|
+
flex-direction: column;
|
|
1844
|
+
gap: 10px;
|
|
1845
|
+
}
|
|
1846
|
+
.dsh-goal-diff-summary {
|
|
1847
|
+
display: flex;
|
|
1848
|
+
align-items: center;
|
|
1849
|
+
justify-content: space-between;
|
|
1850
|
+
background: var(--dsw-alias-bg-sunken);
|
|
1851
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
1852
|
+
border-radius: 6px;
|
|
1853
|
+
padding: 8px 12px;
|
|
1854
|
+
font-size: 12px;
|
|
1855
|
+
}
|
|
1856
|
+
.dsh-goal-diff-stat-pill {
|
|
1857
|
+
display: inline-flex;
|
|
1858
|
+
align-items: center;
|
|
1859
|
+
gap: 8px;
|
|
1860
|
+
font-weight: 600;
|
|
1861
|
+
}
|
|
1862
|
+
.dsh-goal-diff-ins {
|
|
1863
|
+
color: var(--dsw-alias-state-success-primary);
|
|
1864
|
+
}
|
|
1865
|
+
.dsh-goal-diff-del {
|
|
1866
|
+
color: var(--dsw-alias-state-danger-primary);
|
|
1867
|
+
}
|
|
1868
|
+
.dsh-goal-diff-files-list {
|
|
1869
|
+
display: flex;
|
|
1870
|
+
flex-wrap: wrap;
|
|
1871
|
+
gap: 6px;
|
|
1872
|
+
max-height: 100px;
|
|
1873
|
+
overflow-y: auto;
|
|
1874
|
+
}
|
|
1875
|
+
.dsh-goal-diff-file-chip {
|
|
1876
|
+
display: inline-flex;
|
|
1877
|
+
align-items: center;
|
|
1878
|
+
gap: 6px;
|
|
1879
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
1880
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
1881
|
+
border-radius: 4px;
|
|
1882
|
+
padding: 3px 8px;
|
|
1883
|
+
font-size: 11px;
|
|
1884
|
+
font-family: monospace;
|
|
1885
|
+
cursor: pointer;
|
|
1886
|
+
color: var(--dsw-alias-label-secondary);
|
|
1887
|
+
}
|
|
1888
|
+
.dsh-goal-diff-file-chip:hover,
|
|
1889
|
+
.dsh-goal-diff-file-chip.active {
|
|
1890
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
1891
|
+
color: var(--dsw-alias-label-primary);
|
|
1892
|
+
}
|
|
1893
|
+
.dsh-goal-diff-viewer {
|
|
1894
|
+
background: var(--dsw-alias-bg-sunken);
|
|
1895
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
1896
|
+
border-radius: 6px;
|
|
1897
|
+
padding: 10px;
|
|
1898
|
+
font-family: monospace;
|
|
1899
|
+
font-size: 12px;
|
|
1900
|
+
line-height: 1.45;
|
|
1901
|
+
max-height: 380px;
|
|
1902
|
+
overflow: auto;
|
|
1903
|
+
white-space: pre-wrap;
|
|
1904
|
+
word-break: break-all;
|
|
1905
|
+
}
|
|
1906
|
+
.dsh-goal-diff-line {
|
|
1907
|
+
display: block;
|
|
1908
|
+
padding: 1px 4px;
|
|
1909
|
+
border-radius: 2px;
|
|
1910
|
+
}
|
|
1911
|
+
.dsh-goal-diff-line-add {
|
|
1912
|
+
background: var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-hover));
|
|
1913
|
+
color: var(--dsw-alias-state-success-primary);
|
|
1914
|
+
}
|
|
1915
|
+
.dsh-goal-diff-line-del {
|
|
1916
|
+
background: var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-hover));
|
|
1917
|
+
color: var(--dsw-alias-state-danger-primary);
|
|
1918
|
+
}
|
|
1919
|
+
.dsh-goal-diff-line-hunk {
|
|
1920
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
1921
|
+
color: var(--dsw-alias-state-info-primary);
|
|
1922
|
+
}
|
|
1923
|
+
.dsh-goal-diff-line-header {
|
|
1924
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1925
|
+
font-weight: 600;
|
|
1926
|
+
}
|
|
1927
|
+
.dsh-goal-diff-empty {
|
|
1928
|
+
padding: 24px;
|
|
1929
|
+
text-align: center;
|
|
1930
|
+
color: var(--dsw-alias-label-secondary);
|
|
1931
|
+
font-size: 13px;
|
|
1932
|
+
}
|
|
1812
1933
|
`;
|
|
1813
|
-
|
|
1814
1934
|
function injectStylesOnce() {
|
|
1815
1935
|
|
|
1816
1936
|
if (typeof document === 'undefined') return;
|
|
@@ -2103,6 +2223,146 @@ window.__ModuleLoader__.load({
|
|
|
2103
2223
|
const [interveneText, setInterveneText] = useState('');
|
|
2104
2224
|
|
|
2105
2225
|
const [interveneSent, setInterveneSent] = useState(false);
|
|
2226
|
+
const [activeTab, setActiveTab] = useState("overview");
|
|
2227
|
+
const [diffData, setDiffData] = useState(null);
|
|
2228
|
+
const [diffLoading, setDiffLoading] = useState(false);
|
|
2229
|
+
const [diffError, setDiffError] = useState(null);
|
|
2230
|
+
const [selectedFile, setSelectedFile] = useState(null);
|
|
2231
|
+
|
|
2232
|
+
const fetchDiff = useCallback(() => {
|
|
2233
|
+
setDiffLoading(true);
|
|
2234
|
+
setDiffError(null);
|
|
2235
|
+
const sid = state.sessionId || "default";
|
|
2236
|
+
const url = "/dsh-goal/diff?sessionId=" + encodeURIComponent(sid);
|
|
2237
|
+
if (typeof fetch === "function") {
|
|
2238
|
+
fetch(url)
|
|
2239
|
+
.then((res) => {
|
|
2240
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
2241
|
+
return res.json();
|
|
2242
|
+
})
|
|
2243
|
+
.then((data) => {
|
|
2244
|
+
if (data && data.ok) {
|
|
2245
|
+
setDiffData(data);
|
|
2246
|
+
} else {
|
|
2247
|
+
setDiffError((data && data.error) || "Failed to load diff");
|
|
2248
|
+
}
|
|
2249
|
+
})
|
|
2250
|
+
.catch((err) => {
|
|
2251
|
+
setDiffError(err.message || "Network error");
|
|
2252
|
+
})
|
|
2253
|
+
.finally(() => {
|
|
2254
|
+
setDiffLoading(false);
|
|
2255
|
+
});
|
|
2256
|
+
} else {
|
|
2257
|
+
setDiffLoading(false);
|
|
2258
|
+
}
|
|
2259
|
+
}, [state.sessionId]);
|
|
2260
|
+
|
|
2261
|
+
useEffect(() => {
|
|
2262
|
+
if (activeTab === "diff") {
|
|
2263
|
+
fetchDiff();
|
|
2264
|
+
}
|
|
2265
|
+
}, [activeTab, fetchDiff]);
|
|
2266
|
+
|
|
2267
|
+
const renderDiffBox = () => {
|
|
2268
|
+
return React.createElement(
|
|
2269
|
+
"div",
|
|
2270
|
+
{ className: "dsh-goal-diff-box" },
|
|
2271
|
+
React.createElement(
|
|
2272
|
+
"div",
|
|
2273
|
+
{ className: "dsh-goal-diff-summary" },
|
|
2274
|
+
React.createElement(
|
|
2275
|
+
"div",
|
|
2276
|
+
{ style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
2277
|
+
React.createElement("span", null, (t("diffBaseCommit") || "Base:") + " ", React.createElement("code", null, (diffData && diffData.base ? diffData.base : (state.gitStartCommit || "main")).slice(0, 7))),
|
|
2278
|
+
React.createElement("span", null, "→"),
|
|
2279
|
+
React.createElement("span", null, (t("diffTargetBranch") || "Target:") + " ", React.createElement("code", null, (diffData && diffData.target) || state.gitBranch || state.branchName || "HEAD"))
|
|
2280
|
+
),
|
|
2281
|
+
React.createElement(
|
|
2282
|
+
"div",
|
|
2283
|
+
{ style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
2284
|
+
diffData && diffData.stats ? React.createElement(
|
|
2285
|
+
"span",
|
|
2286
|
+
{ className: "dsh-goal-diff-stat-pill" },
|
|
2287
|
+
React.createElement("span", { className: "dsh-goal-diff-ins" }, "+" + diffData.stats.insertions),
|
|
2288
|
+
React.createElement("span", { className: "dsh-goal-diff-del" }, "-" + diffData.stats.deletions)
|
|
2289
|
+
) : null,
|
|
2290
|
+
React.createElement(
|
|
2291
|
+
"button",
|
|
2292
|
+
{
|
|
2293
|
+
type: "button",
|
|
2294
|
+
className: "dsh-goal-btn icon-only",
|
|
2295
|
+
style: { padding: "2px 8px", fontSize: 11 },
|
|
2296
|
+
onClick: fetchDiff,
|
|
2297
|
+
title: t("diffRefresh") || "Refresh Diff",
|
|
2298
|
+
},
|
|
2299
|
+
"🔄"
|
|
2300
|
+
)
|
|
2301
|
+
)
|
|
2302
|
+
),
|
|
2303
|
+
diffData && diffData.files && diffData.files.length > 0 ? React.createElement(
|
|
2304
|
+
"div",
|
|
2305
|
+
{ className: "dsh-goal-diff-files-list" },
|
|
2306
|
+
React.createElement(
|
|
2307
|
+
"span",
|
|
2308
|
+
{
|
|
2309
|
+
className: `dsh-goal-diff-file-chip${selectedFile === null ? " active" : ""}`,
|
|
2310
|
+
onClick: () => setSelectedFile(null),
|
|
2311
|
+
},
|
|
2312
|
+
(t("allFiles") || "All Files") + ` (${diffData.files.length})`
|
|
2313
|
+
),
|
|
2314
|
+
diffData.files.map((f) => React.createElement(
|
|
2315
|
+
"span",
|
|
2316
|
+
{
|
|
2317
|
+
key: f.path,
|
|
2318
|
+
className: `dsh-goal-diff-file-chip${selectedFile === f.path ? " active" : ""}`,
|
|
2319
|
+
onClick: () => setSelectedFile(selectedFile === f.path ? null : f.path),
|
|
2320
|
+
title: `${f.path} (+${f.insertions} / -${f.deletions})`,
|
|
2321
|
+
},
|
|
2322
|
+
`${f.path.split("/").pop()} `,
|
|
2323
|
+
React.createElement("span", { className: "dsh-goal-diff-ins" }, "+" + f.insertions),
|
|
2324
|
+
"/",
|
|
2325
|
+
React.createElement("span", { className: "dsh-goal-diff-del" }, "-" + f.deletions)
|
|
2326
|
+
))
|
|
2327
|
+
) : null,
|
|
2328
|
+
diffLoading ? React.createElement("div", { className: "dsh-goal-diff-empty" }, t("diffLoading") || "Loading diff...")
|
|
2329
|
+
: diffError ? React.createElement("div", { className: "dsh-goal-diff-empty dsh-goal-gate-error" }, "Error: " + diffError)
|
|
2330
|
+
: !diffData || !diffData.diff || diffData.diff.trim().length === 0 ? React.createElement("div", { className: "dsh-goal-diff-empty" }, t("diffNoChanges") || "No git changes detected relative to base commit.")
|
|
2331
|
+
: React.createElement(
|
|
2332
|
+
"div",
|
|
2333
|
+
{ className: "dsh-goal-diff-viewer" },
|
|
2334
|
+
(() => {
|
|
2335
|
+
const allLines = diffData.diff.split("\n");
|
|
2336
|
+
let linesToShow = allLines;
|
|
2337
|
+
if (selectedFile) {
|
|
2338
|
+
const fileChunks = [];
|
|
2339
|
+
let collecting = false;
|
|
2340
|
+
for (const l of allLines) {
|
|
2341
|
+
if (l.startsWith("diff --git ")) {
|
|
2342
|
+
const bPart = l.split(" b/")[1] || "";
|
|
2343
|
+
collecting = bPart === selectedFile;
|
|
2344
|
+
}
|
|
2345
|
+
if (collecting) fileChunks.push(l);
|
|
2346
|
+
}
|
|
2347
|
+
if (fileChunks.length > 0) linesToShow = fileChunks;
|
|
2348
|
+
}
|
|
2349
|
+
return linesToShow.map((line, idx) => {
|
|
2350
|
+
let lineClass = "dsh-goal-diff-line";
|
|
2351
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2352
|
+
lineClass += " dsh-goal-diff-line-add";
|
|
2353
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2354
|
+
lineClass += " dsh-goal-diff-line-del";
|
|
2355
|
+
} else if (line.startsWith("@@")) {
|
|
2356
|
+
lineClass += " dsh-goal-diff-line-hunk";
|
|
2357
|
+
} else if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++")) {
|
|
2358
|
+
lineClass += " dsh-goal-diff-line-header";
|
|
2359
|
+
}
|
|
2360
|
+
return React.createElement("span", { key: idx, className: lineClass }, line || " ");
|
|
2361
|
+
});
|
|
2362
|
+
})()
|
|
2363
|
+
)
|
|
2364
|
+
);
|
|
2365
|
+
};
|
|
2106
2366
|
|
|
2107
2367
|
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
2108
2368
|
|
|
@@ -2222,7 +2482,35 @@ window.__ModuleLoader__.load({
|
|
|
2222
2482
|
|
|
2223
2483
|
),
|
|
2224
2484
|
|
|
2225
|
-
|
|
2485
|
+
React.createElement(
|
|
2486
|
+
'div',
|
|
2487
|
+
{ className: 'dsh-goal-tabs' },
|
|
2488
|
+
React.createElement(
|
|
2489
|
+
'button',
|
|
2490
|
+
{
|
|
2491
|
+
type: 'button',
|
|
2492
|
+
className: `dsh-goal-tab-btn${activeTab === "overview" ? " active" : ""}`,
|
|
2493
|
+
onClick: () => setActiveTab("overview"),
|
|
2494
|
+
},
|
|
2495
|
+
"📋 " + (t("tabOverview") || "Plan & Overview")
|
|
2496
|
+
),
|
|
2497
|
+
React.createElement(
|
|
2498
|
+
'button',
|
|
2499
|
+
{
|
|
2500
|
+
type: 'button',
|
|
2501
|
+
className: `dsh-goal-tab-btn${activeTab === "diff" ? " active" : ""}`,
|
|
2502
|
+
onClick: () => setActiveTab("diff"),
|
|
2503
|
+
},
|
|
2504
|
+
"🔍 " + (t("tabDiff") || "Changes / Diff") + (diffData && diffData.stats && diffData.stats.filesChanged ? ` (${diffData.stats.filesChanged})` : "")
|
|
2505
|
+
)
|
|
2506
|
+
),
|
|
2507
|
+
|
|
2508
|
+
activeTab === 'diff'
|
|
2509
|
+
? renderDiffBox()
|
|
2510
|
+
: React.createElement(
|
|
2511
|
+
React.Fragment,
|
|
2512
|
+
null,
|
|
2513
|
+
isPlanMode && !isCompleted
|
|
2226
2514
|
? React.createElement(
|
|
2227
2515
|
'div',
|
|
2228
2516
|
{ className: 'dsh-goal-plan-banner' },
|
|
@@ -2716,7 +3004,8 @@ window.__ModuleLoader__.load({
|
|
|
2716
3004
|
)
|
|
2717
3005
|
)
|
|
2718
3006
|
)
|
|
2719
|
-
: null
|
|
3007
|
+
: null
|
|
3008
|
+
),
|
|
2720
3009
|
|
|
2721
3010
|
),
|
|
2722
3011
|
|
|
@@ -5533,6 +5822,17 @@ window.__ModuleLoader__.load({
|
|
|
5533
5822
|
planningPhase: 'Planning Phase (Read-Only)',
|
|
5534
5823
|
planPhaseNotice: 'Agent is formulating the plan. File modifications are guarded until operator approval.',
|
|
5535
5824
|
launchModeLabel: 'Launch Mode:',
|
|
5825
|
+
tabOverview: 'Plan & Overview',
|
|
5826
|
+
tabDiff: 'Changes / Diff',
|
|
5827
|
+
diffLoading: 'Loading diff...',
|
|
5828
|
+
diffNoChanges: 'No git changes detected relative to base commit.',
|
|
5829
|
+
diffFilesChanged: 'Files changed:',
|
|
5830
|
+
diffInsertions: 'insertions',
|
|
5831
|
+
diffDeletions: 'deletions',
|
|
5832
|
+
diffBaseCommit: 'Base:',
|
|
5833
|
+
diffTargetBranch: 'Target:',
|
|
5834
|
+
diffRefresh: 'Refresh Diff',
|
|
5835
|
+
allFiles: 'All Files',
|
|
5536
5836
|
planModeDesc: 'Plan first, review milestones, then execute',
|
|
5537
5837
|
actModeDesc: 'Direct execution right away',
|
|
5538
5838
|
|
|
@@ -5684,6 +5984,17 @@ window.__ModuleLoader__.load({
|
|
|
5684
5984
|
planningPhase: '规划阶段(只读)',
|
|
5685
5985
|
planPhaseNotice: '智能体正在制定方案与里程碑。在操作者批准前,禁止修改文件。',
|
|
5686
5986
|
launchModeLabel: '启动模式:',
|
|
5987
|
+
tabOverview: '方案与概览',
|
|
5988
|
+
tabDiff: '代码变更 / Diff',
|
|
5989
|
+
diffLoading: '正在加载差异...',
|
|
5990
|
+
diffNoChanges: '未检测到相对于基线提交的代码变更。',
|
|
5991
|
+
diffFilesChanged: '变更文件数:',
|
|
5992
|
+
diffInsertions: '行新增',
|
|
5993
|
+
diffDeletions: '行删除',
|
|
5994
|
+
diffBaseCommit: '基线:',
|
|
5995
|
+
diffTargetBranch: '目标:',
|
|
5996
|
+
diffRefresh: '刷新 Diff',
|
|
5997
|
+
allFiles: '所有文件',
|
|
5687
5998
|
planModeDesc: '先制定里程碑方案,人工确认后再执行',
|
|
5688
5999
|
actModeDesc: '立即开始编码与任务执行',
|
|
5689
6000
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { getSafeCwd } from './goal-engine-constants.js';
|
|
3
|
+
import { isGitRepository, isValidGitRef } from './engine-git-branch.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse git diff --numstat output into structured file statistics
|
|
7
|
+
* @param {string} output
|
|
8
|
+
* @returns {Array<{ path: string, insertions: number, deletions: number, binary: boolean }>}
|
|
9
|
+
*/
|
|
10
|
+
export function parseNumstat(output) {
|
|
11
|
+
if (!output || typeof output !== 'string') return [];
|
|
12
|
+
const lines = output.trim().split('\n');
|
|
13
|
+
const files = [];
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (!line.trim()) continue;
|
|
16
|
+
const parts = line.split('\t');
|
|
17
|
+
if (parts.length < 3) continue;
|
|
18
|
+
const [rawIns, rawDel, filePath] = parts;
|
|
19
|
+
const isBinary = rawIns === '-' || rawDel === '-';
|
|
20
|
+
const ins = isBinary ? 0 : parseInt(rawIns, 10) || 0;
|
|
21
|
+
const del = isBinary ? 0 : parseInt(rawDel, 10) || 0;
|
|
22
|
+
files.push({
|
|
23
|
+
path: filePath,
|
|
24
|
+
insertions: ins,
|
|
25
|
+
deletions: del,
|
|
26
|
+
binary: isBinary,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return files;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get git diff between refs or current working tree
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* @returns {object} diff result payload
|
|
36
|
+
*/
|
|
37
|
+
export function getGitDiff({
|
|
38
|
+
cwd = process.cwd(),
|
|
39
|
+
base,
|
|
40
|
+
target,
|
|
41
|
+
file,
|
|
42
|
+
allowedRoots = [],
|
|
43
|
+
maxBytes = 256 * 1024,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const safeCwd = getSafeCwd(cwd, allowedRoots);
|
|
46
|
+
if (!safeCwd || !isGitRepository(safeCwd, allowedRoots)) {
|
|
47
|
+
return { ok: false, error: 'Not a git repository or invalid path' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ref validation: disallow flag injection and special chars
|
|
51
|
+
if (base && !isValidGitRef(base)) {
|
|
52
|
+
return { ok: false, error: 'Invalid base git ref' };
|
|
53
|
+
}
|
|
54
|
+
if (target && !isValidGitRef(target)) {
|
|
55
|
+
return { ok: false, error: 'Invalid target git ref' };
|
|
56
|
+
}
|
|
57
|
+
if (file && (typeof file !== 'string' || file.startsWith('-') || /[\0\r\n]/.test(file))) {
|
|
58
|
+
return { ok: false, error: 'Invalid file parameter' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const rangeArgs = [];
|
|
63
|
+
if (base && target) {
|
|
64
|
+
rangeArgs.push(`${base}..${target}`);
|
|
65
|
+
} else if (base) {
|
|
66
|
+
rangeArgs.push(base);
|
|
67
|
+
} else if (target) {
|
|
68
|
+
rangeArgs.push(target);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 1. Fetch numstat for file summary
|
|
72
|
+
const numstatArgs = ['diff', '--numstat', ...rangeArgs];
|
|
73
|
+
if (file) {
|
|
74
|
+
numstatArgs.push('--', file);
|
|
75
|
+
}
|
|
76
|
+
const numstatOut = execFileSync('git', numstatArgs, {
|
|
77
|
+
cwd: safeCwd,
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
timeout: 5000,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const files = parseNumstat(numstatOut);
|
|
84
|
+
const stats = {
|
|
85
|
+
filesChanged: files.length,
|
|
86
|
+
insertions: files.reduce((acc, f) => acc + f.insertions, 0),
|
|
87
|
+
deletions: files.reduce((acc, f) => acc + f.deletions, 0),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// 2. Fetch patch
|
|
91
|
+
const patchArgs = ['diff', '-p', '--no-color', ...rangeArgs];
|
|
92
|
+
if (file) {
|
|
93
|
+
patchArgs.push('--', file);
|
|
94
|
+
}
|
|
95
|
+
let patchOut = execFileSync('git', patchArgs, {
|
|
96
|
+
cwd: safeCwd,
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
timeout: 5000,
|
|
100
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
let truncated = false;
|
|
104
|
+
if (patchOut.length > maxBytes) {
|
|
105
|
+
patchOut = patchOut.slice(0, maxBytes);
|
|
106
|
+
truncated = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
cwd: safeCwd,
|
|
112
|
+
base: base || null,
|
|
113
|
+
target: target || 'HEAD',
|
|
114
|
+
stats,
|
|
115
|
+
files,
|
|
116
|
+
diff: patchOut,
|
|
117
|
+
truncated,
|
|
118
|
+
};
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return { ok: false, error: err.message || 'git diff execution failed' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* GoalEngine helper to fetch diff for active or specified session
|
|
126
|
+
* @param {object} engine
|
|
127
|
+
* @param {string} sessionId
|
|
128
|
+
* @param {object} options
|
|
129
|
+
* @returns {object} diff result
|
|
130
|
+
*/
|
|
131
|
+
export function getEngineGoalDiff(engine, sessionId = 'default', options = {}) {
|
|
132
|
+
const sid = sessionId || 'default';
|
|
133
|
+
const goal = engine.goals?.get(sid);
|
|
134
|
+
const allowedRoots = options.allowedRoots || engine.config?.allowedRoots || (goal?.cwd ? [goal.cwd, process.cwd()] : [process.cwd()]);
|
|
135
|
+
const cwd = options.cwd || goal?.cwd || engine.config?.cwd || process.cwd();
|
|
136
|
+
const base = options.base || goal?.gitStartCommit || null;
|
|
137
|
+
const target = options.target || goal?.gitBranch || goal?.branchName || 'HEAD';
|
|
138
|
+
|
|
139
|
+
return getGitDiff({
|
|
140
|
+
cwd,
|
|
141
|
+
base,
|
|
142
|
+
target,
|
|
143
|
+
file: options.file,
|
|
144
|
+
allowedRoots,
|
|
145
|
+
maxBytes: options.maxBytes,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { MilestoneStatus } from './goal-engine-constants.js';
|
|
2
|
+
|
|
3
|
+
export const MILESTONE_TO_KANBAN_COLUMN = {
|
|
4
|
+
[MilestoneStatus.PENDING]: 'todo',
|
|
5
|
+
[MilestoneStatus.IN_PROGRESS]: 'in_progress',
|
|
6
|
+
[MilestoneStatus.COMPLETED]: 'done',
|
|
7
|
+
pending: 'todo',
|
|
8
|
+
in_progress: 'in_progress',
|
|
9
|
+
active: 'in_progress',
|
|
10
|
+
completed: 'done',
|
|
11
|
+
PENDING: 'todo',
|
|
12
|
+
ACTIVE: 'in_progress',
|
|
13
|
+
COMPLETED: 'done',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const KANBAN_COLUMN_TO_MILESTONE_STATUS = {
|
|
17
|
+
backlog: MilestoneStatus.PENDING,
|
|
18
|
+
todo: MilestoneStatus.PENDING,
|
|
19
|
+
in_progress: MilestoneStatus.IN_PROGRESS,
|
|
20
|
+
doing: MilestoneStatus.IN_PROGRESS,
|
|
21
|
+
active: MilestoneStatus.IN_PROGRESS,
|
|
22
|
+
review: MilestoneStatus.COMPLETED,
|
|
23
|
+
done: MilestoneStatus.COMPLETED,
|
|
24
|
+
completed: MilestoneStatus.COMPLETED,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format externalRef string for a goal milestone
|
|
29
|
+
* @param {string} goalId
|
|
30
|
+
* @param {string} milestoneId
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function formatMilestoneExternalRef(goalId, milestoneId) {
|
|
34
|
+
return `dsh-goal:${goalId || 'default'}:${milestoneId}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse milestone externalRef
|
|
39
|
+
* @param {string} externalRef
|
|
40
|
+
* @returns {{ goalId: string, milestoneId: string } | null}
|
|
41
|
+
*/
|
|
42
|
+
export function parseMilestoneExternalRef(externalRef) {
|
|
43
|
+
if (typeof externalRef !== 'string' || !externalRef.startsWith('dsh-goal:')) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const parts = externalRef.split(':');
|
|
47
|
+
if (parts.length < 3) return null;
|
|
48
|
+
return {
|
|
49
|
+
goalId: parts[1],
|
|
50
|
+
milestoneId: parts.slice(2).join(':'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve active kanban service or store from context/options
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @returns {{ service: object | null, store: object | null }}
|
|
58
|
+
*/
|
|
59
|
+
export function resolveKanbanServices(options = {}) {
|
|
60
|
+
const ctx = options.ctx;
|
|
61
|
+
let service = options.kanbanService || null;
|
|
62
|
+
let store = options.store || null;
|
|
63
|
+
|
|
64
|
+
if (!service && ctx) {
|
|
65
|
+
service = ctx.dshKanban || (typeof ctx.get === 'function' ? ctx.get('dshKanban') : null);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!store) {
|
|
69
|
+
if (service && typeof service.getStore === 'function') {
|
|
70
|
+
store = service.getStore();
|
|
71
|
+
} else if (ctx?.kanbanStore) {
|
|
72
|
+
store = ctx.kanbanStore;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { service, store };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Synchronize all milestones of a goal with dsh-kanban
|
|
81
|
+
* Safe degradation if kanban is not available
|
|
82
|
+
* @param {object} engine
|
|
83
|
+
* @param {string} sessionId
|
|
84
|
+
* @param {object} options
|
|
85
|
+
* @returns {Promise<object>} sync summary
|
|
86
|
+
*/
|
|
87
|
+
export async function syncGoalToKanban(engine, sessionId = 'default', options = {}) {
|
|
88
|
+
const sid = sessionId || 'default';
|
|
89
|
+
const goal = engine.goals?.get(sid);
|
|
90
|
+
if (!goal) {
|
|
91
|
+
return { ok: false, error: 'Goal not found' };
|
|
92
|
+
}
|
|
93
|
+
if (!goal.milestones || goal.milestones.length === 0) {
|
|
94
|
+
return { ok: true, synced: 0, reason: 'No milestones in goal' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { service, store } = resolveKanbanServices({
|
|
98
|
+
kanbanService: options.kanbanService || engine.kanbanService,
|
|
99
|
+
store: options.store || engine.kanbanStore,
|
|
100
|
+
ctx: options.ctx || engine.ctx,
|
|
101
|
+
...options,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!service && !store) {
|
|
105
|
+
return { ok: false, skipped: true, reason: 'kanban-unavailable' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const ms of goal.milestones) {
|
|
110
|
+
const externalRef = formatMilestoneExternalRef(goal.id, ms.id);
|
|
111
|
+
const col = MILESTONE_TO_KANBAN_COLUMN[ms.status] || 'todo';
|
|
112
|
+
const title = `[Goal] ${ms.title}`;
|
|
113
|
+
const body = ms.notes || `Milestone ${ms.id} for goal: ${goal.title}`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
if (service && typeof service.createTask === 'function') {
|
|
117
|
+
const res = await service.createTask({
|
|
118
|
+
title,
|
|
119
|
+
body,
|
|
120
|
+
column: col,
|
|
121
|
+
externalRef,
|
|
122
|
+
labels: ['dsh-goal', String(goal.id)],
|
|
123
|
+
});
|
|
124
|
+
results.push({ milestoneId: ms.id, taskId: res.taskId || res.task?.id, ok: res.ok, alreadyExists: res.alreadyExists });
|
|
125
|
+
} else if (store && typeof store.createTask === 'function') {
|
|
126
|
+
const existing = typeof store.findTaskByExternalRef === 'function' ? store.findTaskByExternalRef(externalRef) : null;
|
|
127
|
+
if (!existing) {
|
|
128
|
+
const task = store.createTask({
|
|
129
|
+
title,
|
|
130
|
+
body,
|
|
131
|
+
column: col,
|
|
132
|
+
externalRef,
|
|
133
|
+
labels: ['dsh-goal', String(goal.id)],
|
|
134
|
+
});
|
|
135
|
+
results.push({ milestoneId: ms.id, taskId: task.id, ok: true, alreadyExists: false });
|
|
136
|
+
} else {
|
|
137
|
+
results.push({ milestoneId: ms.id, taskId: existing.id, ok: true, alreadyExists: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
engine.logger?.warn?.(`[dsh-goal] Kanban milestone sync failed for ${ms.id}:`, err?.message || err);
|
|
142
|
+
results.push({ milestoneId: ms.id, ok: false, error: err?.message || String(err) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { ok: true, synced: results.filter((r) => r.ok).length, total: goal.milestones.length, results };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Update corresponding kanban card column when milestone status changes
|
|
151
|
+
* @param {object} milestone
|
|
152
|
+
* @param {object} options
|
|
153
|
+
* @returns {Promise<object>}
|
|
154
|
+
*/
|
|
155
|
+
export async function syncMilestoneStatusToKanban(milestone, { goal, ctx, store, kanbanService } = {}) {
|
|
156
|
+
if (!milestone || !goal) return { ok: false, error: 'Invalid milestone or goal' };
|
|
157
|
+
|
|
158
|
+
const { store: activeStore } = resolveKanbanServices({
|
|
159
|
+
store,
|
|
160
|
+
kanbanService,
|
|
161
|
+
ctx,
|
|
162
|
+
});
|
|
163
|
+
if (!activeStore || typeof activeStore.findTaskByExternalRef !== 'function') {
|
|
164
|
+
return { ok: false, skipped: true, reason: 'kanban-store-unavailable' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const externalRef = formatMilestoneExternalRef(goal.id, milestone.id);
|
|
168
|
+
const targetCol = MILESTONE_TO_KANBAN_COLUMN[milestone.status] || 'todo';
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const task = activeStore.findTaskByExternalRef(externalRef);
|
|
172
|
+
if (!task) {
|
|
173
|
+
return { ok: false, skipped: true, reason: 'kanban-task-not-found' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (task.column !== targetCol && typeof activeStore.moveTask === 'function') {
|
|
177
|
+
activeStore.moveTask(task.id, { column: targetCol });
|
|
178
|
+
return { ok: true, synced: true, taskId: task.id, column: targetCol };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { ok: true, synced: true, taskId: task.id, column: task.column, unchanged: true };
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return { ok: false, error: err?.message || String(err) };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Handle external card movement from dsh-kanban board
|
|
189
|
+
* @param {object} engine
|
|
190
|
+
* @param {object} payload
|
|
191
|
+
* @param {string} sessionId
|
|
192
|
+
* @returns {object}
|
|
193
|
+
*/
|
|
194
|
+
export function handleKanbanCardMoved(engine, payload = {}, sessionId = 'default') {
|
|
195
|
+
const externalRef = payload.externalRef;
|
|
196
|
+
const column = payload.column;
|
|
197
|
+
const targetStatus = payload.status || (column ? KANBAN_COLUMN_TO_MILESTONE_STATUS[column] : null);
|
|
198
|
+
|
|
199
|
+
if (!targetStatus) {
|
|
200
|
+
return { ok: false, error: 'Invalid or missing target milestone status/column' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let goalId = null;
|
|
204
|
+
let milestoneId = payload.milestoneId || null;
|
|
205
|
+
|
|
206
|
+
if (externalRef) {
|
|
207
|
+
const parsed = parseMilestoneExternalRef(externalRef);
|
|
208
|
+
if (parsed) {
|
|
209
|
+
goalId = parsed.goalId;
|
|
210
|
+
milestoneId = parsed.milestoneId;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const candidateSessions = sessionId && sessionId !== 'default'
|
|
215
|
+
? [sessionId, 'default']
|
|
216
|
+
: ['default', ...engine.goals.keys()];
|
|
217
|
+
|
|
218
|
+
for (const sid of candidateSessions) {
|
|
219
|
+
const goal = engine.goals?.get(sid);
|
|
220
|
+
if (!goal || (goalId && String(goal.id) !== String(goalId))) continue;
|
|
221
|
+
|
|
222
|
+
const ms = goal.milestones?.find((m) => String(m.id) === String(milestoneId) || m.title === payload.title);
|
|
223
|
+
if (ms) {
|
|
224
|
+
const updateRes = engine.updateMilestone(
|
|
225
|
+
ms.id,
|
|
226
|
+
targetStatus,
|
|
227
|
+
`Synced from Kanban card move to column '${column || targetStatus}'`,
|
|
228
|
+
sid
|
|
229
|
+
);
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
sessionId: sid,
|
|
233
|
+
milestoneId: ms.id,
|
|
234
|
+
prevStatus: ms.status,
|
|
235
|
+
newStatus: targetStatus,
|
|
236
|
+
updateResult: updateRes,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { ok: false, error: 'Matching milestone not found for external reference' };
|
|
242
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { verifyMilestoneAcceptance } from './engine-verification.js';
|
|
2
|
+
import { getEngineGoalDiff } from './engine-diff.js';
|
|
3
|
+
import { syncGoalToKanban, syncMilestoneStatusToKanban, handleKanbanCardMoved } from './engine-kanban-sync.js';
|
|
2
4
|
import { askOperatorQuery, answerOperatorQuery } from './engine-operator-query.js';
|
|
3
5
|
import { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot, getSafeCwd } from './goal-engine-constants.js';
|
|
4
6
|
import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
|
|
@@ -31,7 +33,7 @@ export class GoalEngine {
|
|
|
31
33
|
enableLiveActivityFeed: options.enableLiveActivityFeed ?? true, enablePreplanning: options.enablePreplanning ?? true,
|
|
32
34
|
autoBranchOnGoalStart: options.autoBranchOnGoalStart ?? true, enablePostGoalRetrospective: options.enablePostGoalRetrospective ?? true,
|
|
33
35
|
autoScaleBudgetNearCompletion: options.autoScaleBudgetNearCompletion ?? false, enableTemplatesDrawer: options.enableTemplatesDrawer ?? true,
|
|
34
|
-
enableMilestoneDependencies: options.enableMilestoneDependencies ?? true, soundScheme: options.soundScheme ?? 'default',
|
|
36
|
+
enableMilestoneDependencies: options.enableMilestoneDependencies ?? true, soundScheme: options.soundScheme ?? 'default', enableKanbanSync: options.enableKanbanSync ?? true, ctx: options.ctx || null, kanbanService: options.kanbanService || null, kanbanStore: options.kanbanStore || null,
|
|
35
37
|
enableVoiceAnnouncements: options.enableVoiceAnnouncements ?? false, approvalGate: options.approvalGate || null,
|
|
36
38
|
checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100,
|
|
37
39
|
});
|
|
@@ -180,7 +182,7 @@ export class GoalEngine {
|
|
|
180
182
|
title: cleanTitle,
|
|
181
183
|
description: options.description?.trim() || '',
|
|
182
184
|
lang: options.lang || detectLanguage(cleanTitle),
|
|
183
|
-
cwd: options.cwd ? getSafeCwd(options.cwd) : null,
|
|
185
|
+
cwd: options.cwd ? getSafeCwd(options.cwd, options.allowedRoots || this.allowedRoots || []) : null,
|
|
184
186
|
mode: initialMode,
|
|
185
187
|
state: initialState,
|
|
186
188
|
startedAt: now,
|
|
@@ -193,7 +195,7 @@ export class GoalEngine {
|
|
|
193
195
|
budgetWarningTriggered: false,
|
|
194
196
|
budgetAutoScaled: false,
|
|
195
197
|
gitStartCommit: gitCommit || null,
|
|
196
|
-
gitBranch: null,
|
|
198
|
+
gitBranch: options.gitBranch || options.branchName || null,
|
|
197
199
|
gitParentBranch: null,
|
|
198
200
|
pendingNudge: null,
|
|
199
201
|
nudges: [],
|
|
@@ -315,7 +317,8 @@ export class GoalEngine {
|
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
addMilestones(milestonesList, shouldEmit = true, sessionId = 'default') {
|
|
318
|
-
const sid = sessionId || 'default';
|
|
320
|
+
const sid = typeof shouldEmit === 'string' ? shouldEmit : (sessionId || 'default');
|
|
321
|
+
const shouldEmitFlag = typeof shouldEmit === 'boolean' ? shouldEmit : true;
|
|
319
322
|
const goal = this.goals.get(sid);
|
|
320
323
|
if (!goal || !Array.isArray(milestonesList)) return [];
|
|
321
324
|
|
|
@@ -325,7 +328,7 @@ export class GoalEngine {
|
|
|
325
328
|
}
|
|
326
329
|
this.recordProgress(sid);
|
|
327
330
|
recordActivity(goal, { type: 'milestone', tool: 'addMilestones', status: 'success', details: `Added ${newMilestones.length} milestones` });
|
|
328
|
-
if (
|
|
331
|
+
if (shouldEmitFlag) this.emit(sid);
|
|
329
332
|
return newMilestones;
|
|
330
333
|
}
|
|
331
334
|
|
|
@@ -359,6 +362,7 @@ export class GoalEngine {
|
|
|
359
362
|
|
|
360
363
|
const prevStatus = res.prevStatus;
|
|
361
364
|
recordActivity(goal, { type: 'milestone', tool: 'updateMilestone', status: 'success', details: `${id} -> ${status}` });
|
|
365
|
+
if (this.enableKanbanSync) syncMilestoneStatusToKanban(target, { goal, ctx: this.ctx, store: this.kanbanStore, kanbanService: this.kanbanService }).catch(() => {});
|
|
362
366
|
|
|
363
367
|
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED) {
|
|
364
368
|
if (this.autoCheckpointOnMilestone) {
|
|
@@ -579,6 +583,10 @@ export class GoalEngine {
|
|
|
579
583
|
return res;
|
|
580
584
|
}
|
|
581
585
|
|
|
586
|
+
getDiff(sessionId = 'default', options = {}) { return getEngineGoalDiff(this, sessionId, options); }
|
|
587
|
+
syncKanban(sessionId = 'default', options = {}) { return syncGoalToKanban(this, sessionId, options); }
|
|
588
|
+
handleKanbanCardMove(payload = {}, sessionId = 'default') { return handleKanbanCardMoved(this, payload, sessionId); }
|
|
589
|
+
|
|
582
590
|
isPlanMode(sessionId = 'default') {
|
|
583
591
|
const goal = this.goals.get(sessionId || 'default');
|
|
584
592
|
return Boolean(goal && (goal.mode === GoalMode.PLAN || goal.state === GoalState.PLANNING));
|
package/lib/routes.js
CHANGED
|
@@ -383,6 +383,28 @@ export function registerRoutes(ctx, {
|
|
|
383
383
|
}));
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
// GET /dsh-goal/diff
|
|
387
|
+
if (req.method === 'GET' && (pathname === '/dsh-goal/diff' || pathname === '/dsh-goal/diff/')) {
|
|
388
|
+
const rejection = getRequestRejection(req, { ctx });
|
|
389
|
+
if (rejection !== undefined) {
|
|
390
|
+
res.statusCode = rejection;
|
|
391
|
+
return res.end(JSON.stringify({ error: rejection === 401 ? 'Unauthorized: authentication required' : 'Forbidden: untrusted caller origin' }));
|
|
392
|
+
}
|
|
393
|
+
const sid = sessionIdOf(req, 'default');
|
|
394
|
+
const target = url.searchParams.get('target') || undefined;
|
|
395
|
+
const base = url.searchParams.get('base') || undefined;
|
|
396
|
+
const file = url.searchParams.get('file') || undefined;
|
|
397
|
+
const cwd = url.searchParams.get('cwd') || undefined;
|
|
398
|
+
|
|
399
|
+
if (engine) {
|
|
400
|
+
const diffResult = engine.getDiff(sid, { target, base, file, cwd });
|
|
401
|
+
res.statusCode = diffResult.ok ? 200 : 400;
|
|
402
|
+
return res.end(JSON.stringify(diffResult));
|
|
403
|
+
}
|
|
404
|
+
res.statusCode = 404;
|
|
405
|
+
return res.end(JSON.stringify({ ok: false, error: 'Goal engine unavailable' }));
|
|
406
|
+
}
|
|
407
|
+
|
|
386
408
|
// POST /dsh-goal/action
|
|
387
409
|
// CSRF & Same-Origin guard
|
|
388
410
|
if (req.method === 'POST' && (pathname === '/dsh-goal/action' || pathname === '/dsh-goal/action/')) {
|
|
@@ -429,7 +451,7 @@ export function registerRoutes(ctx, {
|
|
|
429
451
|
body += chunk;
|
|
430
452
|
};
|
|
431
453
|
|
|
432
|
-
const onEnd = () => {
|
|
454
|
+
const onEnd = async () => {
|
|
433
455
|
if (limitExceeded) return;
|
|
434
456
|
try {
|
|
435
457
|
const data = JSON.parse(body || '{}');
|
|
@@ -829,6 +851,28 @@ export function registerRoutes(ctx, {
|
|
|
829
851
|
res.statusCode = 200;
|
|
830
852
|
return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
|
|
831
853
|
|
|
854
|
+
case 'kanban_move': {
|
|
855
|
+
if (engine) {
|
|
856
|
+
result = engine.handleKanbanCardMove(data, sid);
|
|
857
|
+
if (!result.ok) res.statusCode = 400;
|
|
858
|
+
} else {
|
|
859
|
+
res.statusCode = 404;
|
|
860
|
+
result = { ok: false, error: 'Goal engine unavailable' };
|
|
861
|
+
}
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
case 'kanban_sync': {
|
|
866
|
+
if (engine) {
|
|
867
|
+
result = await engine.syncKanban(sid, { ctx });
|
|
868
|
+
if (!result.ok && !result.skipped) res.statusCode = 400;
|
|
869
|
+
} else {
|
|
870
|
+
res.statusCode = 404;
|
|
871
|
+
result = { ok: false, error: 'Goal engine unavailable' };
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
|
|
832
876
|
case 'instantiate_template': {
|
|
833
877
|
const tplId = data.templateId || data.id;
|
|
834
878
|
if (!tplId) {
|
|
@@ -870,6 +914,7 @@ export function registerRoutes(ctx, {
|
|
|
870
914
|
'/dsh-goal/events': ['GET'],
|
|
871
915
|
'/dsh-goal/templates': ['GET'],
|
|
872
916
|
'/dsh-goal/state': ['GET'],
|
|
917
|
+
'/dsh-goal/diff': ['GET'],
|
|
873
918
|
'/dsh-goal/action': ['POST'],
|
|
874
919
|
};
|
|
875
920
|
const cleanPath = pathname.replace(/\/+$/, '') || '/';
|
package/package.json
CHANGED