@goodandready/dsh-goal 0.2.2 → 0.2.3
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/README.md +12 -0
- package/README.ru.md +12 -0
- package/README.zh.md +12 -0
- package/lib/client.js +4384 -4498
- package/lib/command-handler.js +242 -276
- package/lib/engine-prompt.js +57 -0
- package/lib/engine-reports.js +114 -0
- package/lib/engine-store.js +142 -0
- package/lib/goal-engine-constants.js +100 -0
- package/lib/goal-engine.js +579 -2018
- package/lib/index.js +452 -1960
- package/lib/routes.js +294 -0
- package/lib/tools.js +294 -0
- package/lib/updater.js +317 -0
- package/package.json +62 -63
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely get current short git commit hash
|
|
5
|
+
* @param {string} [cwd]
|
|
6
|
+
* @returns {string|null}
|
|
7
|
+
*/
|
|
8
|
+
export function getGitCurrentCommit(cwd) {
|
|
9
|
+
try {
|
|
10
|
+
return execSync('git rev-parse --short HEAD', {
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
|
+
timeout: 1000,
|
|
14
|
+
cwd: cwd || undefined,
|
|
15
|
+
}).trim();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate Markdown report for goal results
|
|
23
|
+
* @param {Object} state
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function exportReportMarkdown(state) {
|
|
27
|
+
if (!state) return '';
|
|
28
|
+
const title = state.title || 'Goal Report';
|
|
29
|
+
const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
|
|
30
|
+
const elapsed = state.formattedElapsed || '0s';
|
|
31
|
+
const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
|
|
32
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
33
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
34
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
35
|
+
const gitCommit = state.gitStartCommit ? ` | **Git Start:** \`${state.gitStartCommit}\`` : '';
|
|
36
|
+
|
|
37
|
+
let md = `# 🎯 Goal Report: ${title}\n\n`;
|
|
38
|
+
md += `**Status:** \`${status}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${iter}\`${gitCommit}\n`;
|
|
39
|
+
if (totalTokens > 0) {
|
|
40
|
+
md += `**Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n`;
|
|
41
|
+
}
|
|
42
|
+
md += '\n';
|
|
43
|
+
|
|
44
|
+
if (state.description) {
|
|
45
|
+
md += `### Description\n${state.description}\n\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (state.resultSummary) {
|
|
49
|
+
md += `### Summary & Deliverables\n${state.resultSummary}\n\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const milestones = state.milestones || [];
|
|
53
|
+
if (milestones.length > 0) {
|
|
54
|
+
md += `### Milestones\n| # | Status | Title | Notes |\n|---|---|---|---|\n`;
|
|
55
|
+
milestones.forEach((m, idx) => {
|
|
56
|
+
const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
|
|
57
|
+
const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
|
|
58
|
+
const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
|
|
59
|
+
md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
|
|
60
|
+
});
|
|
61
|
+
md += '\n';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
md += `*Generated by DSH Goal Engine at ${new Date().toISOString()}*\n`;
|
|
65
|
+
return md;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate GitHub / Gitea PR comment report with collapsible details
|
|
70
|
+
* @param {Object} state
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function exportReportGitHubPR(state) {
|
|
74
|
+
if (!state) return '';
|
|
75
|
+
const title = state.title || 'Goal Report';
|
|
76
|
+
const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
|
|
77
|
+
const elapsed = state.formattedElapsed || '0s';
|
|
78
|
+
const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
|
|
79
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
80
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
81
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
82
|
+
const gitCommit = state.gitStartCommit ? ` | **Git Start:** \`${state.gitStartCommit}\` 📌` : '';
|
|
83
|
+
|
|
84
|
+
let md = `## 🎯 Autonomous Goal Resolution: ${title}\n\n`;
|
|
85
|
+
md += `> **Status:** \`${status}\` 🚀 | **Duration:** \`${elapsed}\` ⏱️ | **Iterations:** \`${iter}\` 🔄${gitCommit}\n\n`;
|
|
86
|
+
|
|
87
|
+
if (totalTokens > 0) {
|
|
88
|
+
md += `### 📊 Telemetry & Token Usage\n`;
|
|
89
|
+
md += `- **Total Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (state.resultSummary) {
|
|
93
|
+
md += `### 📦 Deliverables & Achievements\n${state.resultSummary}\n\n`;
|
|
94
|
+
} else if (state.description) {
|
|
95
|
+
md += `### 📝 Objective\n${state.description}\n\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const milestones = state.milestones || [];
|
|
99
|
+
if (milestones.length > 0) {
|
|
100
|
+
const completedCount = milestones.filter((m) => m.status === 'completed').length;
|
|
101
|
+
md += `<details>\n<summary><b>📋 Milestones Breakdown (${completedCount}/${milestones.length} Completed)</b></summary>\n\n`;
|
|
102
|
+
md += `| # | Status | Milestone | Notes |\n|---|---|---|---|\n`;
|
|
103
|
+
milestones.forEach((m, idx) => {
|
|
104
|
+
const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
|
|
105
|
+
const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
|
|
106
|
+
const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
|
|
107
|
+
md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
|
|
108
|
+
});
|
|
109
|
+
md += `\n</details>\n\n`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
md += `*Automated by [@goodandready/dsh-goal](https://github.com/GooDAnDReaDY/dsh-goal)*\n`;
|
|
113
|
+
return md;
|
|
114
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { GoalState } from './goal-engine-constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage manager for goal engine disk state persistence
|
|
7
|
+
*/
|
|
8
|
+
export class EngineStore {
|
|
9
|
+
constructor(engine, storagePath) {
|
|
10
|
+
this.engine = engine;
|
|
11
|
+
this.storagePath = storagePath || null;
|
|
12
|
+
this.saveTimer = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setStoragePath(storagePath) {
|
|
16
|
+
this.storagePath = storagePath || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
loadStateFromDisk() {
|
|
20
|
+
if (!this.storagePath) return;
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(this.storagePath)) {
|
|
23
|
+
const raw = fs.readFileSync(this.storagePath, 'utf8');
|
|
24
|
+
const data = JSON.parse(raw);
|
|
25
|
+
if (data && typeof data === 'object') {
|
|
26
|
+
let dirty = false;
|
|
27
|
+
if (data.sessions && typeof data.sessions === 'object') {
|
|
28
|
+
for (const [sid, goal] of Object.entries(data.sessions)) {
|
|
29
|
+
if (goal && goal.id && goal.title) {
|
|
30
|
+
// Crash Hydration: transition leftover RUNNING goals to PAUSED
|
|
31
|
+
if (goal.state === GoalState.RUNNING) {
|
|
32
|
+
goal.state = GoalState.PAUSED;
|
|
33
|
+
goal.pausedAt = Date.now();
|
|
34
|
+
if (!Array.isArray(goal.logs)) goal.logs = [];
|
|
35
|
+
goal.logs.push({
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
type: 'warning',
|
|
38
|
+
message: 'Harness was restarted — click ▶️ to resume',
|
|
39
|
+
});
|
|
40
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
41
|
+
dirty = true;
|
|
42
|
+
}
|
|
43
|
+
if (!goal.tokensUsage) {
|
|
44
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
45
|
+
}
|
|
46
|
+
if (!goal.lang) {
|
|
47
|
+
goal.lang = 'en';
|
|
48
|
+
}
|
|
49
|
+
this.engine.goals.set(sid, goal);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else if (data.id && data.title) {
|
|
53
|
+
if (data.state === GoalState.RUNNING) {
|
|
54
|
+
data.state = GoalState.PAUSED;
|
|
55
|
+
data.pausedAt = Date.now();
|
|
56
|
+
if (!Array.isArray(data.logs)) data.logs = [];
|
|
57
|
+
data.logs.push({
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
type: 'warning',
|
|
60
|
+
message: 'Harness was restarted — click ▶️ to resume',
|
|
61
|
+
});
|
|
62
|
+
if (data.logs.length > 100) data.logs = data.logs.slice(-100);
|
|
63
|
+
dirty = true;
|
|
64
|
+
}
|
|
65
|
+
if (!data.tokensUsage) {
|
|
66
|
+
data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
67
|
+
}
|
|
68
|
+
if (!data.lang) {
|
|
69
|
+
data.lang = 'en';
|
|
70
|
+
}
|
|
71
|
+
this.engine.goals.set('default', data);
|
|
72
|
+
}
|
|
73
|
+
if (dirty) {
|
|
74
|
+
this.scheduleSave(true);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// best-effort load failure logged defensively
|
|
80
|
+
console.warn?.('[GoalEngine] Failed to load state from disk:', err?.message || err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
scheduleSave(immediate = false) {
|
|
85
|
+
if (!this.storagePath) return;
|
|
86
|
+
if (immediate) {
|
|
87
|
+
if (this.saveTimer) {
|
|
88
|
+
clearTimeout(this.saveTimer);
|
|
89
|
+
this.saveTimer = null;
|
|
90
|
+
}
|
|
91
|
+
this.writeStateToDiskSync();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!this.saveTimer) {
|
|
95
|
+
this.saveTimer = setTimeout(() => {
|
|
96
|
+
this.saveTimer = null;
|
|
97
|
+
this.writeStateToDiskSync();
|
|
98
|
+
}, 250);
|
|
99
|
+
if (typeof this.saveTimer.unref === 'function') {
|
|
100
|
+
this.saveTimer.unref();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
writeStateToDiskSync() {
|
|
106
|
+
if (!this.storagePath) return;
|
|
107
|
+
try {
|
|
108
|
+
if (this.engine.goals.size === 0) {
|
|
109
|
+
if (fs.existsSync(this.storagePath)) {
|
|
110
|
+
fs.unlinkSync(this.storagePath);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const sessionsObj = {};
|
|
115
|
+
for (const [sid, goal] of this.engine.goals.entries()) {
|
|
116
|
+
sessionsObj[sid] = goal;
|
|
117
|
+
}
|
|
118
|
+
const payload = {
|
|
119
|
+
version: 2,
|
|
120
|
+
sessions: sessionsObj,
|
|
121
|
+
...(this.engine.goals.has('default') ? this.engine.goals.get('default') : {}),
|
|
122
|
+
};
|
|
123
|
+
const dir = path.dirname(this.storagePath);
|
|
124
|
+
if (!fs.existsSync(dir)) {
|
|
125
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
const tmp = `${this.storagePath}.tmp.${Date.now()}`;
|
|
128
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
129
|
+
fs.renameSync(tmp, this.storagePath);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
flushSync() {
|
|
136
|
+
if (this.saveTimer) {
|
|
137
|
+
clearTimeout(this.saveTimer);
|
|
138
|
+
this.saveTimer = null;
|
|
139
|
+
}
|
|
140
|
+
this.writeStateToDiskSync();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enums and helpers for DSH Goal Engine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const GoalState = {
|
|
6
|
+
IDLE: 'IDLE',
|
|
7
|
+
PLANNING: 'PLANNING',
|
|
8
|
+
RUNNING: 'RUNNING',
|
|
9
|
+
PAUSED: 'PAUSED',
|
|
10
|
+
COMPLETED: 'COMPLETED',
|
|
11
|
+
FAILED: 'FAILED',
|
|
12
|
+
CANCELLED: 'CANCELLED',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const MilestoneStatus = {
|
|
16
|
+
PENDING: 'pending',
|
|
17
|
+
IN_PROGRESS: 'in_progress',
|
|
18
|
+
COMPLETED: 'completed',
|
|
19
|
+
FAILED: 'failed',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format total elapsed seconds into concise string (e.g. "2s", "45s", "1m 15s", "2h 5m")
|
|
24
|
+
* @param {number} totalSeconds
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function formatElapsed(totalSeconds) {
|
|
28
|
+
const sec = Math.max(0, Math.floor(totalSeconds));
|
|
29
|
+
if (sec < 60) return `${sec}s`;
|
|
30
|
+
const mins = Math.floor(sec / 60);
|
|
31
|
+
const remainingSec = sec % 60;
|
|
32
|
+
if (mins < 60) {
|
|
33
|
+
return remainingSec > 0 ? `${mins}m ${remainingSec}s` : `${mins}m`;
|
|
34
|
+
}
|
|
35
|
+
const hours = Math.floor(mins / 60);
|
|
36
|
+
const remainingMins = mins % 60;
|
|
37
|
+
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format estimated remaining time (ETA)
|
|
42
|
+
* @param {number|null} seconds
|
|
43
|
+
* @returns {string|null}
|
|
44
|
+
*/
|
|
45
|
+
export function formatETA(seconds) {
|
|
46
|
+
if (seconds == null || isNaN(seconds)) return null;
|
|
47
|
+
const sec = Math.max(0, Math.floor(seconds));
|
|
48
|
+
if (sec < 60) return `~${sec}s`;
|
|
49
|
+
const mins = Math.round(sec / 60);
|
|
50
|
+
if (mins < 60) return `~${mins}m`;
|
|
51
|
+
const hours = Math.floor(mins / 60);
|
|
52
|
+
const remainingMins = mins % 60;
|
|
53
|
+
return remainingMins > 0 ? `~${hours}h ${remainingMins}m` : `~${hours}h`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect language of text (Chinese characters -> zh, otherwise en)
|
|
58
|
+
* @param {string} text
|
|
59
|
+
* @param {string} [fallback='en']
|
|
60
|
+
* @returns {'en' | 'zh'}
|
|
61
|
+
*/
|
|
62
|
+
export function detectLanguage(text, fallback = 'en') {
|
|
63
|
+
if (!text || typeof text !== 'string') return fallback;
|
|
64
|
+
if (/[\u4e00-\u9fa5]/.test(text)) {
|
|
65
|
+
return 'zh';
|
|
66
|
+
}
|
|
67
|
+
return 'en';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract session ID from invocation or HTTP request
|
|
72
|
+
* @param {any} invocationOrReq
|
|
73
|
+
* @param {string} [fallback='default']
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function sessionIdOf(invocationOrReq, fallback = 'default') {
|
|
77
|
+
if (!invocationOrReq) return fallback;
|
|
78
|
+
try {
|
|
79
|
+
if (invocationOrReq.sessionId) return String(invocationOrReq.sessionId);
|
|
80
|
+
if (invocationOrReq.session) {
|
|
81
|
+
return String(invocationOrReq.session.id || invocationOrReq.session.header?.id || fallback);
|
|
82
|
+
}
|
|
83
|
+
if (invocationOrReq.data?.sessionId) return String(invocationOrReq.data.sessionId);
|
|
84
|
+
if (invocationOrReq.agent?.session) {
|
|
85
|
+
return String(invocationOrReq.agent.session.id || invocationOrReq.agent.session.header?.id || fallback);
|
|
86
|
+
}
|
|
87
|
+
if (invocationOrReq.headers) {
|
|
88
|
+
const headerSid = invocationOrReq.headers['x-dsh-session-id'];
|
|
89
|
+
if (headerSid) return String(headerSid);
|
|
90
|
+
if (invocationOrReq.url) {
|
|
91
|
+
const url = new URL(invocationOrReq.url, 'http://localhost');
|
|
92
|
+
const querySid = url.searchParams.get('sessionId') || url.searchParams.get('session');
|
|
93
|
+
if (querySid) return String(querySid);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Non-fatal inspection error on malformed request or object
|
|
98
|
+
}
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|