@goodandready/dsh-goal 0.2.12 → 0.2.14

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,213 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Lightweight JSON persistence for dsh-goal addon metadata:
6
+ * milestones, checklists, snapshots, and token budget metrics.
7
+ */
8
+ export class AddonStore {
9
+ constructor(storagePath, logger = console) {
10
+ this.storagePath = storagePath || null;
11
+ this.logger = logger;
12
+ this.goals = new Map(); // goalId -> { milestones, snapshots, budget, issue }
13
+ this.saveTimer = null;
14
+ this.loadFromDisk();
15
+ }
16
+
17
+ setStoragePath(storagePath) {
18
+ this.storagePath = storagePath || null;
19
+ this.loadFromDisk();
20
+ }
21
+
22
+ getGoalData(goalId) {
23
+ if (!goalId) return null;
24
+ if (!this.goals.has(goalId)) {
25
+ this.goals.set(goalId, {
26
+ milestones: [],
27
+ snapshots: [],
28
+ budget: { maxTokenBudget: 0, warnedAt: null, blockedAt: null },
29
+ issue: null,
30
+ });
31
+ }
32
+ return this.goals.get(goalId);
33
+ }
34
+
35
+ getMilestones(goalId) {
36
+ const data = this.getGoalData(goalId);
37
+ return data ? data.milestones : [];
38
+ }
39
+
40
+ setMilestones(goalId, milestones) {
41
+ const data = this.getGoalData(goalId);
42
+ if (!data) return;
43
+ data.milestones = Array.isArray(milestones) ? milestones : [];
44
+ this.scheduleSave();
45
+ }
46
+
47
+ getSnapshots(goalId) {
48
+ const data = this.getGoalData(goalId);
49
+ return data ? data.snapshots : [];
50
+ }
51
+
52
+ addSnapshot(goalId, snapshot) {
53
+ const data = this.getGoalData(goalId);
54
+ if (!data) return;
55
+ if (!Array.isArray(data.snapshots)) data.snapshots = [];
56
+ data.snapshots.push(snapshot);
57
+ this.scheduleSave();
58
+ }
59
+
60
+ getBudget(goalId) {
61
+ const data = this.getGoalData(goalId);
62
+ return data ? data.budget : null;
63
+ }
64
+
65
+ setBudget(goalId, budget) {
66
+ const data = this.getGoalData(goalId);
67
+ if (!data) return;
68
+ data.budget = { ...(data.budget || {}), ...budget };
69
+ this.scheduleSave();
70
+ }
71
+
72
+ loadFromDisk() {
73
+ if (!this.storagePath) return;
74
+ try {
75
+ if (fs.existsSync(this.storagePath)) {
76
+ const raw = fs.readFileSync(this.storagePath, 'utf8');
77
+ const parsed = JSON.parse(raw);
78
+ if (parsed && typeof parsed === 'object' && parsed.goals) {
79
+ for (const [gid, val] of Object.entries(parsed.goals)) {
80
+ this.goals.set(gid, {
81
+ milestones: Array.isArray(val.milestones) ? val.milestones : [],
82
+ snapshots: Array.isArray(val.snapshots) ? val.snapshots : [],
83
+ budget: val.budget || { maxTokenBudget: 0, warnedAt: null, blockedAt: null },
84
+ issue: val.issue || null,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ } catch (err) {
90
+ this.logger.warn?.('[AddonStore] Failed to load addon state from disk:', err?.message || err);
91
+ }
92
+ }
93
+
94
+ scheduleSave(immediate = false) {
95
+ if (!this.storagePath) return;
96
+ if (immediate) {
97
+ if (this.saveTimer) {
98
+ clearTimeout(this.saveTimer);
99
+ this.saveTimer = null;
100
+ }
101
+ this.writeToDiskSync();
102
+ return;
103
+ }
104
+ if (!this.saveTimer) {
105
+ this.saveTimer = setTimeout(() => {
106
+ this.saveTimer = null;
107
+ this.writeToDiskSync();
108
+ }, 200);
109
+ if (typeof this.saveTimer.unref === 'function') {
110
+ this.saveTimer.unref();
111
+ }
112
+ }
113
+ }
114
+
115
+ writeToDiskSync() {
116
+ if (!this.storagePath) return;
117
+ try {
118
+ const goalsObj = {};
119
+ for (const [gid, val] of this.goals.entries()) {
120
+ goalsObj[gid] = val;
121
+ }
122
+ const payload = {
123
+ version: 3,
124
+ goals: goalsObj,
125
+ updatedAt: Date.now(),
126
+ };
127
+ const dir = path.dirname(this.storagePath);
128
+ if (!fs.existsSync(dir)) {
129
+ fs.mkdirSync(dir, { recursive: true });
130
+ }
131
+ const tmp = `${this.storagePath}.tmp.${Date.now()}`;
132
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
133
+ fs.renameSync(tmp, this.storagePath);
134
+ } catch (err) {
135
+ this.logger.warn?.('[AddonStore] Failed to write addon state to disk:', err?.message || err);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Migrate legacy state from dsh-goal-state.json to addon format and core GoalService
141
+ */
142
+ migrateLegacyState(legacyPath, coreGoals, agent) {
143
+ if (!legacyPath || !fs.existsSync(legacyPath)) return false;
144
+ try {
145
+ const raw = fs.readFileSync(legacyPath, 'utf8');
146
+ const data = JSON.parse(raw);
147
+ if (!data || typeof data !== 'object' || data._migratedToAddon) return false;
148
+
149
+ let migratedAny = false;
150
+ const candidates = [];
151
+ if (data.sessions && typeof data.sessions === 'object') {
152
+ for (const [sid, goal] of Object.entries(data.sessions)) {
153
+ if (goal?.title && goal?.hasActiveGoal) {
154
+ candidates.push({ sessionId: sid, goal });
155
+ }
156
+ }
157
+ } else if (data.title && data.hasActiveGoal) {
158
+ candidates.push({ sessionId: 'default', goal: data });
159
+ }
160
+
161
+ for (const item of candidates) {
162
+ const legacyGoal = item.goal;
163
+ const goalId = legacyGoal.id || `legacy-${Date.now()}`;
164
+
165
+ // Import milestones into addon store
166
+ if (Array.isArray(legacyGoal.milestones) && legacyGoal.milestones.length > 0) {
167
+ this.setMilestones(goalId, legacyGoal.milestones.map((m, idx) => ({
168
+ id: m.id || `m-${idx + 1}`,
169
+ title: m.title || `Milestone ${idx + 1}`,
170
+ completed: m.status === 'completed',
171
+ completedAt: m.completedAt || null,
172
+ notes: m.notes || '',
173
+ dependsOn: Array.isArray(m.dependsOn) ? m.dependsOn : [],
174
+ checklist: Array.isArray(m.checklist) ? m.checklist : [],
175
+ })));
176
+ }
177
+
178
+ // If coreGoals is available and agent session is active, bridge to core
179
+ if (coreGoals && agent && typeof coreGoals.create === 'function') {
180
+ try {
181
+ const current = coreGoals.get ? coreGoals.get(agent) : null;
182
+ if (!current) {
183
+ coreGoals.create(agent, {
184
+ objective: legacyGoal.title,
185
+ maxGoalRounds: legacyGoal.maxIterations || 25,
186
+ });
187
+ }
188
+ } catch (e) {
189
+ this.logger.warn?.('[AddonStore] Migration: could not create goal in core:', e.message);
190
+ }
191
+ }
192
+ migratedAny = true;
193
+ }
194
+
195
+ data._migratedToAddon = true;
196
+ data._migratedAt = Date.now();
197
+ fs.writeFileSync(legacyPath, JSON.stringify(data, null, 2), 'utf8');
198
+ this.scheduleSave(true);
199
+ return migratedAny;
200
+ } catch (err) {
201
+ this.logger.warn?.('[AddonStore] Legacy migration failed:', err.message);
202
+ return false;
203
+ }
204
+ }
205
+
206
+ flushSync() {
207
+ if (this.saveTimer) {
208
+ clearTimeout(this.saveTimer);
209
+ this.saveTimer = null;
210
+ }
211
+ this.writeToDiskSync();
212
+ }
213
+ }
@@ -1,4 +1,8 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
1
4
  import { execFileSync } from 'node:child_process';
5
+ import { generateRetrospectiveData } from './engine-retrospective.js';
2
6
 
3
7
  /**
4
8
  * Safely get current short git commit hash
@@ -19,48 +23,119 @@ export function getGitCurrentCommit(cwd) {
19
23
  }
20
24
 
21
25
  /**
22
- * Generate Markdown report for goal results
23
- * @param {Object} state
26
+ * Format elapsed time in human readable format
27
+ * @param {number} seconds
24
28
  * @returns {string}
25
29
  */
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}\`` : '';
30
+ export function formatElapsedSeconds(seconds) {
31
+ const s = Math.max(0, Math.floor(seconds || 0));
32
+ if (s < 60) return `${s}s`;
33
+ const m = Math.floor(s / 60);
34
+ const remSec = s % 60;
35
+ if (m < 60) return `${m}m ${remSec}s`;
36
+ const h = Math.floor(m / 60);
37
+ const remMin = m % 60;
38
+ return `${h}h ${remMin}m`;
39
+ }
40
+
41
+ /**
42
+ * Generate Markdown report for goal completion or archival
43
+ * @param {object} goal - core goal view or state
44
+ * @param {object} [addonData] - { milestones, snapshots, tokensUsage }
45
+ * @param {string} [cwd]
46
+ * @returns {string}
47
+ */
48
+ export function exportReportMarkdown(goal, addonData = {}, cwd = process.cwd()) {
49
+ if (!goal) return '';
50
+ const title = goal.objective || goal.title || 'Goal Report';
51
+ const phase = goal.phase || goal.state || 'COMPLETED';
52
+ const rounds = goal.roundsStarted || goal.iterationsCount || 0;
53
+ const maxRounds = goal.maxGoalRounds || goal.maxIterations || 25;
54
+
55
+ const elapsed = goal.formattedElapsed || formatElapsedSeconds(Math.max(0, Math.round(((goal.updatedAt || goal.completedAt || Date.now()) - (goal.createdAt || goal.startedAt || Date.now())) / 1000)));
56
+
57
+ const totalTokens = addonData?.tokensUsage?.totalTokens || goal?.tokensUsage?.totalTokens || 0;
58
+ const promptTokens = addonData?.tokensUsage?.promptTokens || goal?.tokensUsage?.promptTokens || 0;
59
+ const compTokens = addonData?.tokensUsage?.completionTokens || goal?.tokensUsage?.completionTokens || 0;
60
+ const gitCommit = goal.gitStartCommit ? ` | **Git Start:** \`${goal.gitStartCommit}\`` : '';
36
61
 
37
62
  let md = `# 🎯 Goal Report: ${title}\n\n`;
38
- md += `**Status:** \`${status}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${iter}\`${gitCommit}\n`;
63
+ md += `**Status:** \`${phase}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${rounds}/${maxRounds}\`${gitCommit}\n`;
39
64
  if (totalTokens > 0) {
40
65
  md += `**Tokens:** \`${totalTokens.toLocaleString('en-US')}\` (Prompt: \`${promptTokens.toLocaleString('en-US')}\`, Completion: \`${compTokens.toLocaleString('en-US')}\`)\n`;
41
66
  }
42
67
  md += '\n';
43
68
 
44
- if (state.description) {
45
- md += `### Description\n${state.description}\n\n`;
69
+ if (goal.description) {
70
+ md += `### Description\n${goal.description}\n\n`;
46
71
  }
47
72
 
48
- if (state.resultSummary) {
49
- md += `### Summary & Deliverables\n${state.resultSummary}\n\n`;
73
+ if (goal.resultSummary) {
74
+ md += `### Summary & Deliverables\n${goal.resultSummary}\n\n`;
50
75
  }
51
76
 
52
- const milestones = state.milestones || [];
77
+ // Milestones & Checklists
78
+ const milestones = addonData?.milestones || goal?.milestones || [];
53
79
  if (milestones.length > 0) {
54
- md += `### Milestones\n| # | Status | Title | Notes |\n|---|---|---|---|\n`;
80
+ md += `### Milestones & Checklists\n`;
81
+ md += `| # | Status | Title | Notes | Checklists |\n`;
82
+ md += `|---|---|---|---|---|\n`;
55
83
  milestones.forEach((m, idx) => {
56
- const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
84
+ const isDone = m.status === 'completed' || m.completed === true;
85
+ const isInProg = m.status === 'in_progress';
86
+ const mark = isDone ? '✅ Done' : isInProg ? '🔄 In Progress' : '⏳ Pending';
57
87
  const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
58
- const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
59
- md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
88
+ const cleanNotes = (m.notes || '—').replace(/\|/g, '\\|');
89
+ let clSummary = '—';
90
+ if (Array.isArray(m.checklist) && m.checklist.length > 0) {
91
+ const doneCount = m.checklist.filter(c => c.done).length;
92
+ clSummary = `${doneCount}/${m.checklist.length}`;
93
+ }
94
+ md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes} | ${clSummary} |\n`;
60
95
  });
61
96
  md += '\n';
62
97
  }
63
98
 
99
+ // Snapshots
100
+ const snapshots = addonData?.snapshots || [];
101
+ if (snapshots.length > 0) {
102
+ md += `### Git Checkpoints & Snapshots\n`;
103
+ md += `| Milestone | Commit | Tag | Timestamp |\n`;
104
+ md += `|---|---|---|---|\n`;
105
+ snapshots.forEach((s) => {
106
+ const dt = new Date(s.timestamp || Date.now()).toISOString();
107
+ md += `| \`${s.milestoneId}\` | \`${s.commit}\` | \`${s.tag || '—'}\` | ${dt} |\n`;
108
+ });
109
+ md += '\n';
110
+ }
111
+
112
+ // Retrospective
113
+ try {
114
+ const retro = generateRetrospectiveData({
115
+ ...goal,
116
+ milestones,
117
+ tokensUsage: { totalTokens, promptTokens, completionTokens },
118
+ startedAt: goal.createdAt || goal.startedAt || Date.now(),
119
+ completedAt: goal.updatedAt || goal.completedAt || Date.now(),
120
+ }, cwd);
121
+ if (retro) {
122
+ md += `### Retrospective & Execution Analytics\n`;
123
+ if (retro.estimatedCostUsd) {
124
+ md += `- **Estimated Cost:** ~\$${retro.estimatedCostUsd} USD\n`;
125
+ }
126
+ if (retro.filesChangedCount !== undefined) {
127
+ md += `- **Files Changed:** ${retro.filesChangedCount}\n`;
128
+ }
129
+ if (retro.recommendations?.length > 0) {
130
+ md += `\n**Recommendations:**\n`;
131
+ retro.recommendations.forEach(r => { md += `- ${r}\n`; });
132
+ }
133
+ md += '\n';
134
+ }
135
+ } catch (err) {
136
+ // Retrospective non-fatal
137
+ }
138
+
64
139
  md += `*Generated by DSH Goal Engine at ${new Date().toISOString()}*\n`;
65
140
  return md;
66
141
  }
@@ -72,10 +147,10 @@ export function exportReportMarkdown(state) {
72
147
  */
73
148
  export function exportReportGitHubPR(state) {
74
149
  if (!state) return '';
75
- const title = state.title || 'Goal Report';
76
- const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
150
+ const title = state.objective || state.title || 'Goal Report';
151
+ const status = state.phase || state.state || 'COMPLETED';
77
152
  const elapsed = state.formattedElapsed || '0s';
78
- const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
153
+ const iter = `${state.roundsStarted || state.iterationsCount || 0}/${state.maxGoalRounds || state.maxIterations || 25}`;
79
154
  const totalTokens = state.tokensUsage?.totalTokens || 0;
80
155
  const promptTokens = state.tokensUsage?.promptTokens || 0;
81
156
  const compTokens = state.tokensUsage?.completionTokens || 0;
@@ -97,11 +172,11 @@ export function exportReportGitHubPR(state) {
97
172
 
98
173
  const milestones = state.milestones || [];
99
174
  if (milestones.length > 0) {
100
- const completedCount = milestones.filter((m) => m.status === 'completed').length;
175
+ const completedCount = milestones.filter((m) => m.status === 'completed' || m.completed === true).length;
101
176
  md += `<details>\n<summary><b>📋 Milestones Breakdown (${completedCount}/${milestones.length} Completed)</b></summary>\n\n`;
102
177
  md += `| # | Status | Milestone | Notes |\n|---|---|---|---|\n`;
103
178
  milestones.forEach((m, idx) => {
104
- const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
179
+ const mark = (m.status === 'completed' || m.completed === true) ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
105
180
  const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
106
181
  const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
107
182
  md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
@@ -113,9 +188,6 @@ export function exportReportGitHubPR(state) {
113
188
  return md;
114
189
  }
115
190
 
116
- import fs from 'node:fs';
117
- import path from 'node:path';
118
-
119
191
  /**
120
192
  * Safely create a git milestone checkpoint
121
193
  * @param {object} milestone
@@ -133,7 +205,6 @@ export function createMilestoneCheckpoint(milestone, sessionId = 'default', cwd)
133
205
  cwd: cwd || undefined,
134
206
  }).trim();
135
207
 
136
- // If there are changes, auto-commit checkpoint
137
208
  if (status) {
138
209
  execFileSync('git', ['add', '-A'], {
139
210
  stdio: ['ignore', 'ignore', 'ignore'],
@@ -191,7 +262,7 @@ export function rollbackToCheckpoint(commitHash, cwd) {
191
262
  * @returns {{ relativePath: string, fullPath: string, content: string }|null}
192
263
  */
193
264
  export function saveGoalArtifact(state, cwd) {
194
- if (!state || !state.title) return null;
265
+ if (!state || (!state.title && !state.objective)) return null;
195
266
  try {
196
267
  const root = cwd || process.cwd();
197
268
  const targetDir = path.join(root, '.dsh', 'goals');
@@ -200,7 +271,7 @@ export function saveGoalArtifact(state, cwd) {
200
271
  }
201
272
 
202
273
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
203
- const safeTitle = (state.title || 'goal')
274
+ const safeTitle = (state.objective || state.title || 'goal')
204
275
  .toLowerCase()
205
276
  .replace(/[^a-z0-9]+/g, '-')
206
277
  .replace(/^-+|-+$/g, '')
@@ -218,3 +289,11 @@ export function saveGoalArtifact(state, cwd) {
218
289
  return null;
219
290
  }
220
291
  }
292
+
293
+ /**
294
+ * Save goal report to disk
295
+ */
296
+ export function saveGoalReportFile(goal, addonData = {}, baseDir = null) {
297
+ const root = baseDir || process.cwd();
298
+ return saveGoalArtifact(goal, root)?.fullPath || null;
299
+ }
@@ -0,0 +1,99 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+
5
+ /**
6
+ * Git snapshot and checkpoint manager for goal milestones
7
+ */
8
+ export class GitSnapshotManager {
9
+ constructor(addonStore, { logger = console } = {}) {
10
+ this.store = addonStore;
11
+ this.logger = logger;
12
+ }
13
+
14
+ isGitRepository(workspaceRoot) {
15
+ if (!workspaceRoot || typeof workspaceRoot !== 'string') return false;
16
+ try {
17
+ if (!fs.existsSync(workspaceRoot)) return false;
18
+ execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
19
+ cwd: workspaceRoot,
20
+ stdio: ['ignore', 'pipe', 'ignore'],
21
+ timeout: 2000,
22
+ });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ createSnapshot(goalId, milestoneId, workspaceRoot, message = '') {
30
+ if (!goalId || !milestoneId || !this.isGitRepository(workspaceRoot)) {
31
+ return null;
32
+ }
33
+
34
+ try {
35
+ const commit = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
36
+ cwd: workspaceRoot,
37
+ encoding: 'utf8',
38
+ stdio: ['ignore', 'pipe', 'ignore'],
39
+ timeout: 2000,
40
+ }).trim();
41
+
42
+ const safeGoalId = String(goalId).replace(/[^a-zA-Z0-9_-]/g, '_');
43
+ const safeMilestoneId = String(milestoneId).replace(/[^a-zA-Z0-9_-]/g, '_');
44
+ const tagName = `dsh-goal-snapshot/${safeGoalId}/${safeMilestoneId}-${commit}`;
45
+
46
+ try {
47
+ execFileSync('git', ['tag', '-f', tagName, 'HEAD'], {
48
+ cwd: workspaceRoot,
49
+ stdio: ['ignore', 'pipe', 'ignore'],
50
+ timeout: 2000,
51
+ });
52
+ } catch (tagErr) {
53
+ // Tag creation non-fatal
54
+ }
55
+
56
+ const snapshot = {
57
+ goalId,
58
+ milestoneId,
59
+ commit,
60
+ tag: tagName,
61
+ timestamp: Date.now(),
62
+ message: message || `Milestone ${milestoneId} checkpoint`,
63
+ };
64
+
65
+ if (this.store) {
66
+ this.store.addSnapshot(goalId, snapshot);
67
+ }
68
+ this.logger.info?.(`[GitSnapshotManager] Created snapshot for milestone "${milestoneId}" at commit ${commit}`);
69
+ return snapshot;
70
+ } catch (err) {
71
+ this.logger.warn?.(`[GitSnapshotManager] Failed to create git snapshot: ${err.message}`);
72
+ return null;
73
+ }
74
+ }
75
+
76
+ rollback(goalId, milestoneId, workspaceRoot) {
77
+ if (!this.isGitRepository(workspaceRoot)) {
78
+ throw new Error(`Workspace "${workspaceRoot}" is not a valid git repository`);
79
+ }
80
+
81
+ const snapshots = this.store ? this.store.getSnapshots(goalId) : [];
82
+ const target = snapshots.find(s => s.milestoneId === milestoneId);
83
+ if (!target || !target.commit) {
84
+ throw new Error(`Snapshot for milestone "${milestoneId}" not found in goal "${goalId}"`);
85
+ }
86
+
87
+ try {
88
+ execFileSync('git', ['checkout', target.commit], {
89
+ cwd: workspaceRoot,
90
+ stdio: ['ignore', 'pipe', 'pipe'],
91
+ timeout: 5000,
92
+ });
93
+ this.logger.info?.(`[GitSnapshotManager] Rolled back workspace to commit ${target.commit}`);
94
+ return { success: true, commit: target.commit, milestoneId };
95
+ } catch (err) {
96
+ throw new Error(`Git rollback to commit ${target.commit} failed: ${err.message}`);
97
+ }
98
+ }
99
+ }