@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.
@@ -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
+ }