@mndrk/agx 1.4.64 → 1.4.65

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/cli/runCli.js CHANGED
@@ -704,7 +704,7 @@ async function checkOnboarding() {
704
704
  }
705
705
 
706
706
  async function runTaskInline(rawTaskId, options = {}) {
707
- const { resetFirst = false, forceSwarm = false, fromStage = null } = options;
707
+ const { resetFirst = false, forceSwarm = false, fromStage = null, resumePrompt = '' } = options;
708
708
  const taskId = await resolveTaskId(rawTaskId);
709
709
 
710
710
  if (fromStage) {
@@ -736,7 +736,11 @@ async function checkOnboarding() {
736
736
  throw new Error(`Task not found: ${taskId}`);
737
737
  }
738
738
 
739
- const effectiveTask = forceSwarm ? { ...task, swarm: true } : task;
739
+ const effectiveTask = forceSwarm ? { ...task, swarm: true } : { ...task };
740
+ const normalizedResumePrompt = typeof resumePrompt === 'string' ? resumePrompt.trim() : '';
741
+ if (normalizedResumePrompt) {
742
+ effectiveTask.prompt = normalizedResumePrompt;
743
+ }
740
744
  console.log(`${c.green}✓${c.reset} Running task inline`);
741
745
  console.log(`${c.dim}Task: ${taskId}${c.reset}`);
742
746
 
@@ -1801,6 +1805,213 @@ async function checkOnboarding() {
1801
1805
  process.exit(0);
1802
1806
  }
1803
1807
 
1808
+ // agx resume [<project>:]<task> [--from <checkpoint-id>] [--git-restore] [--dry-run]
1809
+ // Resume execution from last checkpoint (or specified checkpoint)
1810
+ if (cmd === 'resume') {
1811
+ const { getHead, readCheckpointsFile } = require('../storage/checkpoints');
1812
+ const { restoreGitState } = require('../storage/git');
1813
+ const { buildResumePrompt } = require('../prompts/resume');
1814
+
1815
+ let taskIdentifier = null;
1816
+ let fromCheckpointId = null;
1817
+ let gitRestore = false;
1818
+ let dryRun = false;
1819
+
1820
+ for (let i = 1; i < args.length; i++) {
1821
+ if (args[i] === '--from' || args[i] === '-f') {
1822
+ fromCheckpointId = args[++i];
1823
+ } else if (args[i] === '--git-restore' || args[i] === '--restore-git') {
1824
+ gitRestore = true;
1825
+ } else if (args[i] === '--dry-run' || args[i] === '-n') {
1826
+ dryRun = true;
1827
+ } else if (!args[i].startsWith('-')) {
1828
+ taskIdentifier = args[i];
1829
+ }
1830
+ }
1831
+
1832
+ if (!taskIdentifier) {
1833
+ console.log(`${c.yellow}Usage:${c.reset} agx resume [<project>:]<task>`);
1834
+ console.log(`${c.dim}Options:${c.reset}`);
1835
+ console.log(` --from <id> Resume from specific checkpoint (default: latest)`);
1836
+ console.log(` --git-restore Restore git state from checkpoint`);
1837
+ console.log(` --dry-run Show what would be resumed without executing`);
1838
+ process.exit(1);
1839
+ }
1840
+
1841
+ // Parse project:task or just task
1842
+ let projectSlug = null;
1843
+ let taskSlug = null;
1844
+ if (taskIdentifier.includes(':')) {
1845
+ [projectSlug, taskSlug] = taskIdentifier.split(':');
1846
+ } else {
1847
+ // Try to infer project from current directory or use default
1848
+ taskSlug = taskIdentifier;
1849
+ // Look for most recent project with this task
1850
+ const projectsDir = path.join(os.homedir(), '.agx', 'projects');
1851
+ if (fs.existsSync(projectsDir)) {
1852
+ const projects = fs.readdirSync(projectsDir).filter(p => {
1853
+ const taskDir = path.join(projectsDir, p, 'tasks', taskSlug);
1854
+ return fs.existsSync(taskDir);
1855
+ });
1856
+ if (projects.length === 1) {
1857
+ projectSlug = projects[0];
1858
+ } else if (projects.length > 1) {
1859
+ console.log(`${c.yellow}Multiple projects contain task '${taskSlug}':${c.reset}`);
1860
+ projects.forEach(p => console.log(` - ${p}:${taskSlug}`));
1861
+ console.log(`${c.dim}Specify project: agx resume <project>:${taskSlug}${c.reset}`);
1862
+ process.exit(1);
1863
+ }
1864
+ }
1865
+ }
1866
+
1867
+ if (!projectSlug || !taskSlug) {
1868
+ console.log(`${c.red}✗${c.reset} Could not resolve task '${taskIdentifier}'`);
1869
+ console.log(`${c.dim}Use format: agx resume <project>:<task>${c.reset}`);
1870
+ process.exit(1);
1871
+ }
1872
+
1873
+ try {
1874
+ // Load checkpoint
1875
+ let checkpoint = null;
1876
+ if (fromCheckpointId) {
1877
+ const checkpointsData = await readCheckpointsFile(projectSlug, taskSlug);
1878
+ checkpoint = checkpointsData.history.find(cp => cp.id === fromCheckpointId);
1879
+ if (!checkpoint) {
1880
+ console.log(`${c.red}✗${c.reset} Checkpoint '${fromCheckpointId}' not found`);
1881
+ const available = checkpointsData.history.slice(0, 5).map(cp => cp.id).join(', ');
1882
+ if (available) {
1883
+ console.log(`${c.dim}Available: ${available}${c.reset}`);
1884
+ }
1885
+ process.exit(1);
1886
+ }
1887
+ } else {
1888
+ checkpoint = await getHead(projectSlug, taskSlug);
1889
+ }
1890
+
1891
+ if (!checkpoint) {
1892
+ console.log(`${c.red}✗${c.reset} No checkpoints found for ${projectSlug}:${taskSlug}`);
1893
+ console.log(`${c.dim}Task must have emitted [checkpoint: ...] markers to resume${c.reset}`);
1894
+ process.exit(1);
1895
+ }
1896
+
1897
+ // Display checkpoint info
1898
+ console.log(`${c.bold}Resume from Checkpoint${c.reset}\n`);
1899
+ console.log(` ${c.dim}ID:${c.reset} ${checkpoint.id}`);
1900
+ console.log(` ${c.dim}Created:${c.reset} ${checkpoint.createdAt || 'unknown'}`);
1901
+ if (checkpoint.label) {
1902
+ console.log(` ${c.dim}Label:${c.reset} ${checkpoint.label}`);
1903
+ }
1904
+ if (checkpoint.iteration !== undefined) {
1905
+ console.log(` ${c.dim}Iter:${c.reset} ${checkpoint.iteration}`);
1906
+ }
1907
+
1908
+ // Show git state if present
1909
+ if (checkpoint.git) {
1910
+ const { sha, branch, dirty, patchFile } = checkpoint.git;
1911
+ console.log(`\n ${c.dim}Git:${c.reset} ${branch || 'detached'} @ ${(sha || '').slice(0, 7)}${dirty ? ' (dirty)' : ''}`);
1912
+ if (patchFile) {
1913
+ console.log(` ${c.dim}Patch:${c.reset} ${patchFile}`);
1914
+ }
1915
+ }
1916
+
1917
+ // Build resume prompt
1918
+ const resumePrompt = buildResumePrompt(checkpoint);
1919
+ console.log(`\n${c.bold}Resume Prompt${c.reset} (${resumePrompt.length} chars)\n`);
1920
+ console.log(c.dim + '─'.repeat(60) + c.reset);
1921
+ console.log(resumePrompt.slice(0, 1500) + (resumePrompt.length > 1500 ? '\n...(truncated)' : ''));
1922
+ console.log(c.dim + '─'.repeat(60) + c.reset);
1923
+
1924
+ if (dryRun) {
1925
+ console.log(`\n${c.yellow}Dry run - no execution${c.reset}`);
1926
+ process.exit(0);
1927
+ }
1928
+
1929
+ // Optionally restore git state
1930
+ if (gitRestore && checkpoint.git) {
1931
+ console.log(`\n${c.dim}Restoring git state...${c.reset}`);
1932
+ try {
1933
+ const cwd = process.cwd();
1934
+ restoreGitState(checkpoint.git, cwd, { force: false });
1935
+ console.log(`${c.green}✓${c.reset} Git state restored`);
1936
+ } catch (err) {
1937
+ console.log(`${c.yellow}⚠${c.reset} Git restore failed: ${err.message}`);
1938
+ console.log(`${c.dim}Continuing without git restore. Use --force if working tree has changes.${c.reset}`);
1939
+ }
1940
+ }
1941
+
1942
+ // Continue execution with resume prompt
1943
+ if (!dryRun) {
1944
+ // Load task.json to get cloud task ID
1945
+ const storage = require('../storage');
1946
+ const taskJsonPath = path.join(storage.projectRoot(projectSlug), taskSlug, 'task.json');
1947
+ let cloudTaskId = null;
1948
+
1949
+ if (fs.existsSync(taskJsonPath)) {
1950
+ try {
1951
+ const taskData = JSON.parse(fs.readFileSync(taskJsonPath, 'utf8'));
1952
+ cloudTaskId = taskData?.cloud?.task_id;
1953
+ } catch {
1954
+ // Ignore parse errors
1955
+ }
1956
+ }
1957
+
1958
+ if (!cloudTaskId) {
1959
+ console.log(`${c.yellow}⚠${c.reset} No cloud task ID found. Running locally.`);
1960
+ // For now, just use the resume prompt with the default provider
1961
+ const provider = 'claude';
1962
+ console.log(`\n${c.dim}Executing with ${provider}...${c.reset}`);
1963
+
1964
+ try {
1965
+ const { executeTask } = require('../executor');
1966
+ await executeTask({
1967
+ taskId: `${projectSlug}:${taskSlug}`,
1968
+ title: checkpoint.objective || 'Resume task',
1969
+ content: resumePrompt,
1970
+ stage: 'execute',
1971
+ engine: provider,
1972
+ projectSlug,
1973
+ taskSlug,
1974
+ iteration: (checkpoint.iteration || 0) + 1,
1975
+ objective: checkpoint.objective,
1976
+ constraints: checkpoint.constraints,
1977
+ dontRepeat: checkpoint.dontRepeat,
1978
+ verifyFailures: checkpoint.verifyFailures || 0,
1979
+ plan: checkpoint.plan,
1980
+ criteria: checkpoint.criteria,
1981
+ onLog: (msg) => console.log(msg),
1982
+ onProgress: () => {},
1983
+ });
1984
+ console.log(`${c.green}✓${c.reset} Resume execution completed`);
1985
+ } catch (err) {
1986
+ console.log(`${c.red}✗${c.reset} Execution failed: ${err.message}`);
1987
+ process.exit(1);
1988
+ }
1989
+ } else {
1990
+ // Use cloud task flow
1991
+ console.log(`\n${c.dim}Resuming cloud task ${cloudTaskId.slice(0, 8)}...${c.reset}`);
1992
+ try {
1993
+ const exitCode = await runTaskInline(cloudTaskId, {
1994
+ resumePrompt,
1995
+ fromStage: 'execution',
1996
+ });
1997
+ process.exit(exitCode);
1998
+ } catch (err) {
1999
+ console.log(`${c.red}✗${c.reset} Resume failed: ${err.message}`);
2000
+ process.exit(1);
2001
+ }
2002
+ }
2003
+ }
2004
+
2005
+ process.exit(0);
2006
+ } catch (err) {
2007
+ console.log(`${c.red}✗${c.reset} Resume failed: ${err.message}`);
2008
+ if (process.env.DEBUG) {
2009
+ console.error(err);
2010
+ }
2011
+ process.exit(1);
2012
+ }
2013
+ }
2014
+
1804
2015
  // agx retry <taskId> [--task <id>] [--swarm] [--async]
1805
2016
  if (cmd === 'retry' || (cmd === 'task' && args[1] === 'retry')) {
1806
2017
  const runArgs = cmd === 'task' ? args.slice(1) : args;
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Cloud Sync - Sync local checkpoints to agx cloud API.
3
+ *
4
+ * Local-first with cloud sync:
5
+ * - Checkpoints are always saved locally first
6
+ * - Cloud sync happens asynchronously (best-effort)
7
+ * - Failures don't block local operations
8
+ *
9
+ * Sync strategy:
10
+ * - On checkpoint create: queue for cloud sync
11
+ * - Background worker processes queue
12
+ * - Retry with exponential backoff
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const SYNC_QUEUE_FILE = 'sync-queue.json';
19
+ const MAX_RETRY_ATTEMPTS = 3;
20
+ const RETRY_DELAY_MS = 1000;
21
+
22
+ /**
23
+ * Load cloud config from environment or config file.
24
+ * @returns {{ apiUrl: string, token: string, userId: string } | null}
25
+ */
26
+ function loadCloudConfig() {
27
+ // Try environment first
28
+ if (process.env.AGX_CLOUD_URL) {
29
+ return {
30
+ apiUrl: process.env.AGX_CLOUD_URL,
31
+ token: process.env.AGX_CLOUD_TOKEN || '',
32
+ userId: process.env.AGX_CLOUD_USER_ID || '',
33
+ };
34
+ }
35
+
36
+ // Try config file
37
+ try {
38
+ const configPath = path.join(process.env.HOME || '', '.agx', 'cloud-config.json');
39
+ if (fs.existsSync(configPath)) {
40
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
41
+ }
42
+ } catch {
43
+ // Ignore
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Sync a checkpoint to the cloud.
51
+ * @param {object} options
52
+ * @param {string} options.taskId - Cloud task ID
53
+ * @param {object} options.checkpoint - Checkpoint data
54
+ * @param {function} options.onLog - Logging callback
55
+ * @returns {Promise<{ success: boolean, error?: string }>}
56
+ */
57
+ async function syncCheckpointToCloud({ taskId, checkpoint, onLog = () => {} }) {
58
+ const config = loadCloudConfig();
59
+ if (!config?.apiUrl) {
60
+ return { success: false, error: 'No cloud config' };
61
+ }
62
+
63
+ if (!taskId) {
64
+ return { success: false, error: 'No task ID' };
65
+ }
66
+
67
+ try {
68
+ const url = `${config.apiUrl}/api/tasks/${taskId}/checkpoints`;
69
+
70
+ // Prepare checkpoint for cloud (remove large fields)
71
+ const cloudCheckpoint = {
72
+ id: checkpoint.id,
73
+ label: checkpoint.label,
74
+ createdAt: checkpoint.createdAt,
75
+ iteration: checkpoint.iteration,
76
+ objective: checkpoint.objective,
77
+ // Include plan summary, not full plan
78
+ planStepCount: checkpoint.plan?.steps?.length || 0,
79
+ currentStep: checkpoint.plan?.currentStep,
80
+ // Include criteria summary
81
+ criteriaCount: checkpoint.criteria?.length || 0,
82
+ criteriaPassed: checkpoint.criteria?.filter(c => c.passed)?.length || 0,
83
+ // Git info (no patch content)
84
+ gitSha: checkpoint.git?.sha,
85
+ gitBranch: checkpoint.git?.branch,
86
+ gitDirty: checkpoint.git?.dirty,
87
+ // Blocked info
88
+ blockedAt: checkpoint.blockedAt,
89
+ blockedReason: checkpoint.reason,
90
+ };
91
+
92
+ const response = await fetch(url, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ ...(config.token ? { 'Authorization': `Bearer ${config.token}` } : {}),
97
+ ...(config.userId ? { 'x-user-id': config.userId } : {}),
98
+ },
99
+ body: JSON.stringify(cloudCheckpoint),
100
+ });
101
+
102
+ if (!response.ok) {
103
+ const error = `HTTP ${response.status}`;
104
+ onLog(`[cloud-sync] Failed to sync checkpoint: ${error}`);
105
+ return { success: false, error };
106
+ }
107
+
108
+ onLog(`[cloud-sync] Checkpoint ${checkpoint.id} synced`);
109
+ return { success: true };
110
+ } catch (err) {
111
+ const error = err?.message || String(err);
112
+ onLog(`[cloud-sync] Sync error: ${error}`);
113
+ return { success: false, error };
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Queue a checkpoint for cloud sync.
119
+ * @param {string} taskRoot - Local task directory
120
+ * @param {object} checkpoint - Checkpoint data
121
+ * @param {string} cloudTaskId - Cloud task ID
122
+ */
123
+ async function queueForSync(taskRoot, checkpoint, cloudTaskId) {
124
+ if (!taskRoot || !checkpoint || !cloudTaskId) return;
125
+
126
+ try {
127
+ const queuePath = path.join(taskRoot, SYNC_QUEUE_FILE);
128
+ let queue = [];
129
+
130
+ if (fs.existsSync(queuePath)) {
131
+ try {
132
+ queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
133
+ } catch {
134
+ queue = [];
135
+ }
136
+ }
137
+
138
+ queue.push({
139
+ checkpointId: checkpoint.id,
140
+ cloudTaskId,
141
+ checkpoint,
142
+ queuedAt: new Date().toISOString(),
143
+ attempts: 0,
144
+ });
145
+
146
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2));
147
+ } catch {
148
+ // Ignore queue errors
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Process the sync queue for a task.
154
+ * @param {string} taskRoot - Local task directory
155
+ * @param {function} onLog - Logging callback
156
+ * @returns {Promise<{ synced: number, failed: number }>}
157
+ */
158
+ async function processSyncQueue(taskRoot, onLog = () => {}) {
159
+ const queuePath = path.join(taskRoot, SYNC_QUEUE_FILE);
160
+ if (!fs.existsSync(queuePath)) {
161
+ return { synced: 0, failed: 0 };
162
+ }
163
+
164
+ let queue;
165
+ try {
166
+ queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
167
+ } catch {
168
+ return { synced: 0, failed: 0 };
169
+ }
170
+
171
+ if (!Array.isArray(queue) || queue.length === 0) {
172
+ return { synced: 0, failed: 0 };
173
+ }
174
+
175
+ let synced = 0;
176
+ let failed = 0;
177
+ const remaining = [];
178
+
179
+ for (const item of queue) {
180
+ const result = await syncCheckpointToCloud({
181
+ taskId: item.cloudTaskId,
182
+ checkpoint: item.checkpoint,
183
+ onLog,
184
+ });
185
+
186
+ if (result.success) {
187
+ synced++;
188
+ } else {
189
+ item.attempts++;
190
+ item.lastError = result.error;
191
+ item.lastAttempt = new Date().toISOString();
192
+
193
+ if (item.attempts < MAX_RETRY_ATTEMPTS) {
194
+ remaining.push(item);
195
+ } else {
196
+ failed++;
197
+ onLog(`[cloud-sync] Checkpoint ${item.checkpointId} failed after ${MAX_RETRY_ATTEMPTS} attempts`);
198
+ }
199
+ }
200
+ }
201
+
202
+ // Update queue with remaining items
203
+ if (remaining.length > 0) {
204
+ fs.writeFileSync(queuePath, JSON.stringify(remaining, null, 2));
205
+ } else {
206
+ try {
207
+ fs.unlinkSync(queuePath);
208
+ } catch {
209
+ // Ignore
210
+ }
211
+ }
212
+
213
+ return { synced, failed };
214
+ }
215
+
216
+ /**
217
+ * Create a cloud syncer that automatically syncs checkpoints.
218
+ * @param {object} options
219
+ * @param {string} options.taskRoot - Local task directory
220
+ * @param {string} options.cloudTaskId - Cloud task ID
221
+ * @param {function} options.onLog - Logging callback
222
+ * @returns {{ sync: function, processQueue: function }}
223
+ */
224
+ function createCloudSyncer(options = {}) {
225
+ const { taskRoot, cloudTaskId, onLog = () => {} } = options;
226
+
227
+ return {
228
+ /**
229
+ * Sync a checkpoint immediately (best-effort).
230
+ */
231
+ async sync(checkpoint) {
232
+ if (!cloudTaskId) {
233
+ // Queue for later if no cloud task ID yet
234
+ await queueForSync(taskRoot, checkpoint, cloudTaskId);
235
+ return { success: false, queued: true };
236
+ }
237
+
238
+ const result = await syncCheckpointToCloud({
239
+ taskId: cloudTaskId,
240
+ checkpoint,
241
+ onLog,
242
+ });
243
+
244
+ if (!result.success) {
245
+ // Queue for retry
246
+ await queueForSync(taskRoot, checkpoint, cloudTaskId);
247
+ }
248
+
249
+ return result;
250
+ },
251
+
252
+ /**
253
+ * Process queued checkpoints.
254
+ */
255
+ async processQueue() {
256
+ return processSyncQueue(taskRoot, onLog);
257
+ },
258
+ };
259
+ }
260
+
261
+ module.exports = {
262
+ loadCloudConfig,
263
+ syncCheckpointToCloud,
264
+ queueForSync,
265
+ processSyncQueue,
266
+ createCloudSyncer,
267
+ };