autohand-cli 0.6.11 → 0.7.2
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 +60 -48
- package/dist/AutomodeManager-FQQPBHAD.js +1422 -0
- package/dist/AutomodeManager-PUNJS7IZ.cjs +1422 -0
- package/dist/{CommunitySkillsCache-FXWTOBSZ.js → CommunitySkillsCache-AY3ZYDTN.js} +1 -1
- package/dist/{CommunitySkillsCache-MQOTKCVS.cjs → CommunitySkillsCache-KO4DSKMA.cjs} +1 -1
- package/dist/{GitHubRegistryFetcher-S7QFUEKV.cjs → GitHubRegistryFetcher-S4JREWUQ.cjs} +1 -1
- package/dist/{GitHubRegistryFetcher-6JQ5JEDZ.js → GitHubRegistryFetcher-V23KTTLM.js} +1 -1
- package/dist/HookManager-OGINWAJN.cjs +7 -0
- package/dist/HookManager-VG46FXSZ.js +7 -0
- package/dist/MemoryManager-AFS5EZJ2.js +8 -0
- package/dist/MemoryManager-KE6EKW7Y.cjs +8 -0
- package/dist/{PermissionManager-RY3K6CVU.cjs → PermissionManager-MAPKIJMD.cjs} +1 -1
- package/dist/{PermissionManager-XIMDETMX.js → PermissionManager-V2Q2OAS2.js} +1 -1
- package/dist/SessionManager-MKLGLZWC.cjs +10 -0
- package/dist/SessionManager-V25OJDDY.js +10 -0
- package/dist/{SkillsRegistry-LXDK73BL.cjs → SkillsRegistry-4RU2OAEQ.cjs} +1 -1
- package/dist/{SkillsRegistry-SP5MX7OA.js → SkillsRegistry-FTLVJZMA.js} +1 -1
- package/dist/{agents-FH47ZMOI.cjs → agents-CHNQ7LQ4.cjs} +1 -1
- package/dist/{agents-NB5VQN6H.js → agents-E4NEH2PN.js} +1 -1
- package/dist/{agents-new-XLEU26YI.cjs → agents-new-GHVWXW7T.cjs} +1 -1
- package/dist/{agents-new-M325HGWT.js → agents-new-W6HMQ7V5.js} +1 -1
- package/dist/automode-BJYGRMEQ.cjs +9 -0
- package/dist/automode-XCNP6HP4.js +9 -0
- package/dist/chunk-2FLBGPE3.js +199 -0
- package/dist/{chunk-5RX6NVQO.cjs → chunk-3L76MLO5.cjs} +20 -2
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-4L5WYXHN.js +246 -0
- package/dist/{chunk-SM4I2535.js → chunk-52HPEJ4P.js} +1 -1
- package/dist/{chunk-O2GOSS3V.js → chunk-6XITU2RU.js} +1 -1
- package/dist/{chunk-GNKSRX4R.js → chunk-B2BPL6IL.js} +216 -27
- package/dist/{chunk-SFT6BEYU.js → chunk-BAFJQUWR.js} +20 -2
- package/dist/chunk-CXZEPTRI.js +172 -0
- package/dist/{chunk-O3UH2TS6.cjs → chunk-E6U4UE3B.cjs} +1 -1
- package/dist/{chunk-CJ7JHMK7.cjs → chunk-EBM5QDJ4.cjs} +2 -2
- package/dist/{chunk-TF4ROETV.cjs → chunk-FEI2GPAW.cjs} +2 -2
- package/dist/{chunk-V6EL2YTY.js → chunk-HUE3GT5M.js} +1 -1
- package/dist/{chunk-DGVYSTC6.cjs → chunk-HXAAED4W.cjs} +10 -12
- package/dist/{chunk-QF2P64F7.js → chunk-LCCJ26QR.js} +2 -2
- package/dist/chunk-OBGZSXTJ.cjs +10 -0
- package/dist/chunk-OTS4YFSZ.cjs +172 -0
- package/dist/{chunk-6SAX2G75.cjs → chunk-SXUZ3CX3.cjs} +215 -26
- package/dist/chunk-SXYYH3VD.cjs +497 -0
- package/dist/{chunk-NLJMRNGL.cjs → chunk-U43RFUBR.cjs} +2 -2
- package/dist/chunk-UOLJZFPJ.js +497 -0
- package/dist/chunk-UPR5PKX4.cjs +199 -0
- package/dist/chunk-URY4AS4L.cjs +246 -0
- package/dist/{chunk-ERBSCRG6.js → chunk-ZYHQ652D.js} +3 -5
- package/dist/{completion-HR3ZT55J.js → completion-2XTEWNMC.js} +1 -1
- package/dist/{completion-AMEZDTFT.cjs → completion-BTDPVWVB.cjs} +1 -1
- package/dist/{constants-N3I2FHCM.js → constants-RVCOJL3L.js} +1 -1
- package/dist/{constants-ICQLSGZN.cjs → constants-V4V43DZQ.cjs} +1 -1
- package/dist/{defaultHooks-A7TTVVRI.js → defaultHooks-3GRSZNKA.js} +1 -1
- package/dist/{defaultHooks-XGIBEP3Z.cjs → defaultHooks-7EAUU6MN.cjs} +1 -1
- package/dist/{export-ULYYSO5V.cjs → export-CB34FSEH.cjs} +1 -1
- package/dist/{export-MYBJZD3H.js → export-LNPP3XXH.js} +1 -1
- package/dist/{feedback-YGSYBQEW.js → feedback-IZKDNGHP.js} +1 -1
- package/dist/{feedback-TOGESBX7.cjs → feedback-OKGBXZZ4.cjs} +1 -1
- package/dist/{formatters-3POW3KMP.js → formatters-JJK6RS55.js} +1 -1
- package/dist/{formatters-UMUJYWV5.cjs → formatters-OIGPANHJ.cjs} +1 -1
- package/dist/{help-AW4QPGIW.js → help-MU553D6W.js} +1 -1
- package/dist/{help-2PR7P4UJ.cjs → help-QAAUDLFS.cjs} +1 -1
- package/dist/{hooks-2CLU2GWV.js → hooks-4N5VRVOF.js} +2 -2
- package/dist/hooks-7DZGBMXP.cjs +10 -0
- package/dist/index.cjs +990 -1447
- package/dist/index.js +871 -1328
- package/dist/{init-OLSCW7ZC.cjs → init-2QEB7GL5.cjs} +1 -1
- package/dist/{init-HAQKREMF.js → init-WW4RITLV.js} +1 -1
- package/dist/{lint-NJPZWVN2.js → lint-L2TD6NY6.js} +1 -1
- package/dist/{lint-D5UOJWIK.cjs → lint-ZLRBEAFF.cjs} +1 -1
- package/dist/{localProjectPermissions-5RX7NHAH.cjs → localProjectPermissions-2DP6X55S.cjs} +1 -1
- package/dist/{localProjectPermissions-W2CMGIFT.js → localProjectPermissions-5CXHD4DO.js} +1 -1
- package/dist/{login-YNFMV67B.js → login-LH62FYMH.js} +3 -4
- package/dist/login-TWWYJKX6.cjs +13 -0
- package/dist/logout-35XNU6Q2.cjs +13 -0
- package/dist/{logout-TSQ4O4GS.js → logout-KPHUXO23.js} +3 -4
- package/dist/{memory-F3V5FW6W.js → memory-2SGSO4MW.js} +1 -1
- package/dist/{memory-77SWEZYB.cjs → memory-ALXCFFQY.cjs} +1 -1
- package/dist/{model-B2PE6XFS.cjs → model-JC43B3KM.cjs} +1 -1
- package/dist/{model-JC53RT7A.js → model-U3BWIWVH.js} +1 -1
- package/dist/{new-J3ABPMW4.cjs → new-24YT5KVI.cjs} +1 -1
- package/dist/{new-356ITOM7.js → new-PCOF6OLV.js} +1 -1
- package/dist/{patch-MOD7QC3D.cjs → patch-ETANEGRW.cjs} +1 -1
- package/dist/{patch-5F6VBIT3.js → patch-RPK3BZQR.js} +1 -1
- package/dist/{permissions-3GS4ZWVA.js → permissions-5W5JOVM7.js} +1 -1
- package/dist/{permissions-E3MTIE7D.cjs → permissions-NHPJPHDV.cjs} +1 -1
- package/dist/{quit-2QWJ75GZ.js → quit-NIDVPHNL.js} +1 -1
- package/dist/{quit-DQ57J67A.cjs → quit-TMKMX2YF.cjs} +1 -1
- package/dist/{resume-43XMN4CL.cjs → resume-5C44HAAH.cjs} +1 -1
- package/dist/{resume-GA7YZJSO.js → resume-ZZ2D2NMB.js} +1 -1
- package/dist/{session-BSU2L5UI.cjs → session-6TMBGN7N.cjs} +1 -1
- package/dist/{session-SZMRN5KG.js → session-76F55XKA.js} +1 -1
- package/dist/{sessions-OJ4DRK3P.js → sessions-ERKBJ7MC.js} +1 -1
- package/dist/{sessions-CVOZGTKR.cjs → sessions-NUPCJVCF.cjs} +1 -1
- package/dist/{skills-APRIHHHU.js → skills-R25OBHE5.js} +2 -2
- package/dist/skills-UCWKIHHG.cjs +13 -0
- package/dist/{skills-install-52LS6X6D.js → skills-install-GNTBBL46.js} +1 -1
- package/dist/{skills-install-PDWIMVD5.cjs → skills-install-VUSVGSON.cjs} +1 -1
- package/dist/{skills-new-XDYS24XW.cjs → skills-new-AGXQNBRA.cjs} +1 -1
- package/dist/{skills-new-KIBUN63X.js → skills-new-L36LQXIE.js} +1 -1
- package/dist/status-2QV7C3JJ.cjs +9 -0
- package/dist/{status-DHA2IL2O.js → status-VJ6FOSRI.js} +2 -2
- package/dist/theme-MI3BM56M.cjs +13 -0
- package/dist/theme-ZXGSJBZI.js +13 -0
- package/dist/{undo-3Q44LSVS.js → undo-3UU5LWQS.js} +1 -1
- package/dist/{undo-WF5HB5VU.cjs → undo-W4VN2Y37.cjs} +1 -1
- package/package.json +2 -2
- package/dist/InkRenderer-4HTMUNIP.js +0 -2160
- package/dist/InkRenderer-DSNXNSRQ.cjs +0 -2160
- package/dist/chunk-JSBRDJBE.js +0 -30
- package/dist/chunk-N254NRHT.cjs +0 -30
- package/dist/chunk-QSPEMIKE.js +0 -230
- package/dist/chunk-UTNMIGOL.cjs +0 -230
- package/dist/filePalette-3JAGHGK5.js +0 -107
- package/dist/filePalette-PBE5D2OV.cjs +0 -107
- package/dist/hooks-JTRUWAEF.cjs +0 -10
- package/dist/login-2JO6W6PP.cjs +0 -14
- package/dist/logout-OGJQLG7K.cjs +0 -14
- package/dist/skills-UIIM5CKA.cjs +0 -13
- package/dist/status-VAGYUU3H.cjs +0 -9
- package/dist/theme-GGDFOBKE.js +0 -14
- package/dist/theme-PETVTBN7.cjs +0 -14
|
@@ -0,0 +1,1422 @@
|
|
|
1
|
+
import "./chunk-3RG5ZIWI.js";
|
|
2
|
+
|
|
3
|
+
// src/core/AutomodeManager.ts
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import path5 from "path";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
|
|
10
|
+
// src/core/AutomodeState.ts
|
|
11
|
+
import fs from "fs-extra";
|
|
12
|
+
import path from "path";
|
|
13
|
+
var STATE_FILE_PATH = ".autohand/automode.local.md";
|
|
14
|
+
var DEFAULT_THRESHOLDS = {
|
|
15
|
+
noProgress: 3,
|
|
16
|
+
sameError: 5,
|
|
17
|
+
testOnly: 3
|
|
18
|
+
};
|
|
19
|
+
var AutomodeState = class {
|
|
20
|
+
constructor(workspaceRoot) {
|
|
21
|
+
this.state = null;
|
|
22
|
+
this.iterations = [];
|
|
23
|
+
this.circuitBreaker = {
|
|
24
|
+
noProgressCount: 0,
|
|
25
|
+
sameErrorCount: 0,
|
|
26
|
+
testOnlyCount: 0
|
|
27
|
+
};
|
|
28
|
+
this.workspaceRoot = workspaceRoot;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get the state file path
|
|
32
|
+
*/
|
|
33
|
+
getStatePath() {
|
|
34
|
+
return path.join(this.workspaceRoot, STATE_FILE_PATH);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if an auto-mode session is active
|
|
38
|
+
*/
|
|
39
|
+
async hasActiveSession() {
|
|
40
|
+
try {
|
|
41
|
+
const exists = await fs.pathExists(this.getStatePath());
|
|
42
|
+
if (!exists) return false;
|
|
43
|
+
const state = await this.load();
|
|
44
|
+
return state !== null && state.status === "running";
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Initialize a new auto-mode session
|
|
51
|
+
*/
|
|
52
|
+
async initialize(options) {
|
|
53
|
+
this.state = {
|
|
54
|
+
sessionId: options.sessionId,
|
|
55
|
+
prompt: options.prompt,
|
|
56
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57
|
+
currentIteration: 0,
|
|
58
|
+
maxIterations: options.maxIterations,
|
|
59
|
+
status: "running",
|
|
60
|
+
branch: options.branch,
|
|
61
|
+
worktreePath: options.worktreePath,
|
|
62
|
+
filesCreated: 0,
|
|
63
|
+
filesModified: 0,
|
|
64
|
+
completionPromise: options.completionPromise
|
|
65
|
+
};
|
|
66
|
+
this.iterations = [];
|
|
67
|
+
this.circuitBreaker = {
|
|
68
|
+
noProgressCount: 0,
|
|
69
|
+
sameErrorCount: 0,
|
|
70
|
+
testOnlyCount: 0
|
|
71
|
+
};
|
|
72
|
+
await this.save();
|
|
73
|
+
return this.state;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load state from file
|
|
77
|
+
*/
|
|
78
|
+
async load() {
|
|
79
|
+
try {
|
|
80
|
+
const filePath = this.getStatePath();
|
|
81
|
+
if (!await fs.pathExists(filePath)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
85
|
+
const state = this.parseStateMarkdown(content);
|
|
86
|
+
this.state = state;
|
|
87
|
+
return state;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("[automode] Failed to load state:", error);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Save state to file
|
|
95
|
+
*/
|
|
96
|
+
async save() {
|
|
97
|
+
if (!this.state) return;
|
|
98
|
+
try {
|
|
99
|
+
const filePath = this.getStatePath();
|
|
100
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
101
|
+
const content = this.formatStateMarkdown(this.state);
|
|
102
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("[automode] Failed to save state:", error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get current state
|
|
109
|
+
*/
|
|
110
|
+
getState() {
|
|
111
|
+
return this.state;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get all iteration logs
|
|
115
|
+
*/
|
|
116
|
+
getIterations() {
|
|
117
|
+
return [...this.iterations];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Update iteration count and track actions
|
|
121
|
+
*/
|
|
122
|
+
async recordIteration(log) {
|
|
123
|
+
if (!this.state) return;
|
|
124
|
+
this.state.currentIteration += 1;
|
|
125
|
+
const iterationLog = {
|
|
126
|
+
...log,
|
|
127
|
+
iteration: this.state.currentIteration
|
|
128
|
+
};
|
|
129
|
+
this.iterations.push(iterationLog);
|
|
130
|
+
await this.save();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Record a checkpoint (git commit)
|
|
134
|
+
*/
|
|
135
|
+
async recordCheckpoint(commit, message) {
|
|
136
|
+
if (!this.state) return;
|
|
137
|
+
this.state.lastCheckpoint = {
|
|
138
|
+
commit,
|
|
139
|
+
message,
|
|
140
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
141
|
+
};
|
|
142
|
+
if (this.iterations.length > 0) {
|
|
143
|
+
const lastIteration = this.iterations[this.iterations.length - 1];
|
|
144
|
+
lastIteration.checkpoint = { commit, message };
|
|
145
|
+
}
|
|
146
|
+
await this.save();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Update file counts
|
|
150
|
+
*/
|
|
151
|
+
async updateFileCounts(created, modified) {
|
|
152
|
+
if (!this.state) return;
|
|
153
|
+
this.state.filesCreated += created;
|
|
154
|
+
this.state.filesModified += modified;
|
|
155
|
+
await this.save();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Update status
|
|
159
|
+
*/
|
|
160
|
+
async setStatus(status, reason, errorMessage) {
|
|
161
|
+
if (!this.state) return;
|
|
162
|
+
this.state.status = status;
|
|
163
|
+
if (reason) {
|
|
164
|
+
this.state.cancelReason = reason;
|
|
165
|
+
}
|
|
166
|
+
if (errorMessage) {
|
|
167
|
+
this.state.errorMessage = errorMessage;
|
|
168
|
+
}
|
|
169
|
+
await this.save();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check and update circuit breaker state
|
|
173
|
+
* Returns true if circuit breaker should trigger
|
|
174
|
+
*/
|
|
175
|
+
checkCircuitBreaker(hasFileChanges, errorHash, isTestOnly, thresholds = DEFAULT_THRESHOLDS) {
|
|
176
|
+
if (!hasFileChanges) {
|
|
177
|
+
this.circuitBreaker.noProgressCount += 1;
|
|
178
|
+
if (this.circuitBreaker.noProgressCount >= thresholds.noProgress) {
|
|
179
|
+
return {
|
|
180
|
+
triggered: true,
|
|
181
|
+
reason: `No file changes for ${this.circuitBreaker.noProgressCount} consecutive iterations`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
this.circuitBreaker.noProgressCount = 0;
|
|
186
|
+
}
|
|
187
|
+
if (errorHash) {
|
|
188
|
+
if (this.circuitBreaker.lastErrorHash === errorHash) {
|
|
189
|
+
this.circuitBreaker.sameErrorCount += 1;
|
|
190
|
+
if (this.circuitBreaker.sameErrorCount >= thresholds.sameError) {
|
|
191
|
+
return {
|
|
192
|
+
triggered: true,
|
|
193
|
+
reason: `Same error for ${this.circuitBreaker.sameErrorCount} consecutive iterations`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
this.circuitBreaker.sameErrorCount = 1;
|
|
198
|
+
this.circuitBreaker.lastErrorHash = errorHash;
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
this.circuitBreaker.sameErrorCount = 0;
|
|
202
|
+
this.circuitBreaker.lastErrorHash = void 0;
|
|
203
|
+
}
|
|
204
|
+
if (isTestOnly) {
|
|
205
|
+
this.circuitBreaker.testOnlyCount += 1;
|
|
206
|
+
if (this.circuitBreaker.testOnlyCount >= thresholds.testOnly) {
|
|
207
|
+
return {
|
|
208
|
+
triggered: true,
|
|
209
|
+
reason: `Only running tests for ${this.circuitBreaker.testOnlyCount} consecutive iterations`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
this.circuitBreaker.testOnlyCount = 0;
|
|
214
|
+
}
|
|
215
|
+
return { triggered: false };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Check if completion promise is in the output
|
|
219
|
+
*/
|
|
220
|
+
checkCompletionPromise(output) {
|
|
221
|
+
if (!this.state) return false;
|
|
222
|
+
const promise = this.state.completionPromise;
|
|
223
|
+
return output.includes(promise) || output.includes(`<promise>${promise}</promise>`);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Clean up state file after session ends
|
|
227
|
+
*/
|
|
228
|
+
async cleanup() {
|
|
229
|
+
try {
|
|
230
|
+
const filePath = this.getStatePath();
|
|
231
|
+
if (await fs.pathExists(filePath)) {
|
|
232
|
+
if (this.state && this.state.status === "running") {
|
|
233
|
+
this.state.status = "cancelled";
|
|
234
|
+
this.state.cancelReason = "error";
|
|
235
|
+
await this.save();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error("[automode] Failed to cleanup state:", error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Parse state from markdown format
|
|
244
|
+
*/
|
|
245
|
+
parseStateMarkdown(content) {
|
|
246
|
+
try {
|
|
247
|
+
const getValue = (key) => {
|
|
248
|
+
const regex = new RegExp(`\\*\\*${key}:\\*\\*\\s*(.+)`, "i");
|
|
249
|
+
const match = content.match(regex);
|
|
250
|
+
return match?.[1]?.trim();
|
|
251
|
+
};
|
|
252
|
+
const sessionId = getValue("Session ID") ?? getValue("sessionId");
|
|
253
|
+
const prompt = getValue("Prompt");
|
|
254
|
+
const startedAt = getValue("Started");
|
|
255
|
+
const currentIteration = parseInt(getValue("Current Iteration") ?? "0", 10);
|
|
256
|
+
const maxIterations = parseInt(getValue("Max Iterations") ?? "50", 10);
|
|
257
|
+
const status = getValue("Status") ?? "running";
|
|
258
|
+
const branch = getValue("Branch");
|
|
259
|
+
const worktreePath = getValue("Worktree");
|
|
260
|
+
const filesCreated = parseInt(getValue("Files Created") ?? "0", 10);
|
|
261
|
+
const filesModified = parseInt(getValue("Files Modified") ?? "0", 10);
|
|
262
|
+
const completionPromise = getValue("Completion Promise") ?? "DONE";
|
|
263
|
+
if (!sessionId || !prompt || !startedAt) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
sessionId,
|
|
268
|
+
prompt,
|
|
269
|
+
startedAt,
|
|
270
|
+
currentIteration,
|
|
271
|
+
maxIterations,
|
|
272
|
+
status,
|
|
273
|
+
branch,
|
|
274
|
+
worktreePath,
|
|
275
|
+
filesCreated,
|
|
276
|
+
filesModified,
|
|
277
|
+
completionPromise
|
|
278
|
+
};
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Format state as markdown
|
|
285
|
+
*/
|
|
286
|
+
formatStateMarkdown(state) {
|
|
287
|
+
const lines = [
|
|
288
|
+
"# Auto-Mode State",
|
|
289
|
+
"",
|
|
290
|
+
"## Session",
|
|
291
|
+
`- **Session ID:** ${state.sessionId}`,
|
|
292
|
+
`- **Started:** ${state.startedAt}`,
|
|
293
|
+
`- **Prompt:** ${state.prompt}`
|
|
294
|
+
];
|
|
295
|
+
if (state.branch) {
|
|
296
|
+
lines.push(`- **Branch:** ${state.branch}`);
|
|
297
|
+
}
|
|
298
|
+
if (state.worktreePath) {
|
|
299
|
+
lines.push(`- **Worktree:** ${state.worktreePath}`);
|
|
300
|
+
}
|
|
301
|
+
lines.push(
|
|
302
|
+
"",
|
|
303
|
+
"## Progress",
|
|
304
|
+
`- **Current Iteration:** ${state.currentIteration}`,
|
|
305
|
+
`- **Max Iterations:** ${state.maxIterations}`,
|
|
306
|
+
`- **Status:** ${state.status}`,
|
|
307
|
+
"",
|
|
308
|
+
"## Metrics",
|
|
309
|
+
`- **Files Created:** ${state.filesCreated}`,
|
|
310
|
+
`- **Files Modified:** ${state.filesModified}`
|
|
311
|
+
);
|
|
312
|
+
if (state.lastCheckpoint) {
|
|
313
|
+
lines.push(
|
|
314
|
+
"",
|
|
315
|
+
"## Last Checkpoint",
|
|
316
|
+
`- **Commit:** ${state.lastCheckpoint.commit}`,
|
|
317
|
+
`- **Message:** ${state.lastCheckpoint.message}`,
|
|
318
|
+
`- **Timestamp:** ${state.lastCheckpoint.timestamp}`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
lines.push(
|
|
322
|
+
"",
|
|
323
|
+
"## Settings",
|
|
324
|
+
`- **Completion Promise:** ${state.completionPromise}`
|
|
325
|
+
);
|
|
326
|
+
if (state.cancelReason) {
|
|
327
|
+
lines.push(`- **Cancel Reason:** ${state.cancelReason}`);
|
|
328
|
+
}
|
|
329
|
+
if (state.errorMessage) {
|
|
330
|
+
lines.push(`- **Error:** ${state.errorMessage}`);
|
|
331
|
+
}
|
|
332
|
+
lines.push("");
|
|
333
|
+
return lines.join("\n");
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
function hashError(error) {
|
|
337
|
+
return error.slice(0, 100).toLowerCase().replace(/\s+/g, " ").trim();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/core/AutomodeChangelog.ts
|
|
341
|
+
import fs2 from "fs-extra";
|
|
342
|
+
import path2 from "path";
|
|
343
|
+
var CHANGELOG_FILE = "AUTOMODE_CHANGELOG.md";
|
|
344
|
+
var STATUS_EMOJI = {
|
|
345
|
+
running: "\u{1F504}",
|
|
346
|
+
paused: "\u23F8\uFE0F",
|
|
347
|
+
completed: "\u2705",
|
|
348
|
+
cancelled: "\u26A0\uFE0F",
|
|
349
|
+
failed: "\u274C"
|
|
350
|
+
};
|
|
351
|
+
var STATUS_TEXT = {
|
|
352
|
+
running: "Running",
|
|
353
|
+
paused: "Paused",
|
|
354
|
+
completed: "Completed successfully",
|
|
355
|
+
cancelled: "Cancelled",
|
|
356
|
+
failed: "Failed"
|
|
357
|
+
};
|
|
358
|
+
async function generateChangelog(workspaceRoot, state, iterations, gitCommits = []) {
|
|
359
|
+
const changelogPath = path2.join(workspaceRoot, CHANGELOG_FILE);
|
|
360
|
+
const startTime = new Date(state.startedAt);
|
|
361
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
362
|
+
const durationMs = endTime.getTime() - startTime.getTime();
|
|
363
|
+
const durationMinutes = Math.round(durationMs / 6e4);
|
|
364
|
+
const totalTokens = iterations.reduce((sum, it) => sum + (it.tokensUsed ?? 0), 0);
|
|
365
|
+
const totalCost = iterations.reduce((sum, it) => sum + (it.cost ?? 0), 0);
|
|
366
|
+
const lines = [
|
|
367
|
+
"# Auto-Mode Session Report",
|
|
368
|
+
"",
|
|
369
|
+
"## Summary",
|
|
370
|
+
"",
|
|
371
|
+
`- **Task:** ${state.prompt.slice(0, 100)}${state.prompt.length > 100 ? "..." : ""}`,
|
|
372
|
+
`- **Duration:** ${durationMinutes} minutes (${state.currentIteration} iterations)`,
|
|
373
|
+
`- **Result:** ${STATUS_EMOJI[state.status]} ${STATUS_TEXT[state.status]}`
|
|
374
|
+
];
|
|
375
|
+
if (state.branch) {
|
|
376
|
+
const merged = state.status === "completed" ? " \u2192 merged to main" : "";
|
|
377
|
+
lines.push(`- **Branch:** ${state.branch}${merged}`);
|
|
378
|
+
}
|
|
379
|
+
if (state.cancelReason && state.status !== "completed") {
|
|
380
|
+
lines.push(`- **Reason:** ${formatCancelReason(state.cancelReason)}`);
|
|
381
|
+
}
|
|
382
|
+
if (state.errorMessage) {
|
|
383
|
+
lines.push(`- **Error:** ${state.errorMessage}`);
|
|
384
|
+
}
|
|
385
|
+
lines.push("", "## Iterations", "");
|
|
386
|
+
for (const iteration of iterations) {
|
|
387
|
+
const time = new Date(iteration.timestamp).toLocaleTimeString();
|
|
388
|
+
lines.push(`### Iteration ${iteration.iteration} (${time})`);
|
|
389
|
+
lines.push("");
|
|
390
|
+
if (iteration.actions.length > 0) {
|
|
391
|
+
for (const action of iteration.actions) {
|
|
392
|
+
lines.push(`- ${action}`);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
lines.push("- No actions recorded");
|
|
396
|
+
}
|
|
397
|
+
if (iteration.checkpoint) {
|
|
398
|
+
lines.push("");
|
|
399
|
+
lines.push(`**Checkpoint:** \`${iteration.checkpoint.commit}\` - "${iteration.checkpoint.message}"`);
|
|
400
|
+
}
|
|
401
|
+
if (iteration.tokensUsed) {
|
|
402
|
+
lines.push("");
|
|
403
|
+
lines.push(`*Tokens: ${iteration.tokensUsed.toLocaleString()}*`);
|
|
404
|
+
}
|
|
405
|
+
lines.push("");
|
|
406
|
+
}
|
|
407
|
+
lines.push("## Final State", "");
|
|
408
|
+
lines.push(`- **Files Created:** ${state.filesCreated}`);
|
|
409
|
+
lines.push(`- **Files Modified:** ${state.filesModified}`);
|
|
410
|
+
lines.push(`- **Total Iterations:** ${state.currentIteration}`);
|
|
411
|
+
if (totalTokens > 0) {
|
|
412
|
+
lines.push(`- **Total Tokens:** ${totalTokens.toLocaleString()}`);
|
|
413
|
+
}
|
|
414
|
+
if (totalCost > 0) {
|
|
415
|
+
lines.push(`- **Estimated Cost:** $${totalCost.toFixed(2)}`);
|
|
416
|
+
}
|
|
417
|
+
if (gitCommits.length > 0) {
|
|
418
|
+
lines.push("", "## Commits Made", "");
|
|
419
|
+
gitCommits.forEach((commit, index) => {
|
|
420
|
+
lines.push(`${index + 1}. \`${commit.hash}\` - ${commit.message}`);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
lines.push("", "---", "");
|
|
424
|
+
lines.push(`*Generated by autohand auto-mode on ${endTime.toISOString()}*`);
|
|
425
|
+
lines.push("");
|
|
426
|
+
const content = lines.join("\n");
|
|
427
|
+
await fs2.writeFile(changelogPath, content, "utf-8");
|
|
428
|
+
return changelogPath;
|
|
429
|
+
}
|
|
430
|
+
function formatCancelReason(reason) {
|
|
431
|
+
const reasonMap = {
|
|
432
|
+
user_escape: "User pressed ESC",
|
|
433
|
+
user_cancel: "User cancelled via command",
|
|
434
|
+
hook_cancel: "Cancelled by hook",
|
|
435
|
+
rpc_cancel: "Cancelled via RPC",
|
|
436
|
+
acp_cancel: "Cancelled via ACP",
|
|
437
|
+
max_iterations: "Maximum iterations reached",
|
|
438
|
+
max_runtime: "Maximum runtime exceeded",
|
|
439
|
+
max_cost: "Maximum cost exceeded",
|
|
440
|
+
circuit_breaker: "Circuit breaker triggered",
|
|
441
|
+
completion: "Completion promise detected",
|
|
442
|
+
error: "Error occurred"
|
|
443
|
+
};
|
|
444
|
+
return reasonMap[reason] ?? reason;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/core/PatternDetector.ts
|
|
448
|
+
import path3 from "path";
|
|
449
|
+
import fs3 from "fs-extra";
|
|
450
|
+
var PatternDetector = class {
|
|
451
|
+
constructor(workspaceRoot) {
|
|
452
|
+
this.workspaceRoot = workspaceRoot;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Detect all project patterns
|
|
456
|
+
*/
|
|
457
|
+
async detect() {
|
|
458
|
+
const [techStack, commands, framework, packageManager] = await Promise.all([
|
|
459
|
+
this.detectTechStack(),
|
|
460
|
+
this.detectCommands(),
|
|
461
|
+
this.detectFramework(),
|
|
462
|
+
this.detectPackageManager()
|
|
463
|
+
]);
|
|
464
|
+
return {
|
|
465
|
+
techStack,
|
|
466
|
+
testCommand: commands.test,
|
|
467
|
+
buildCommand: commands.build,
|
|
468
|
+
lintCommand: commands.lint,
|
|
469
|
+
framework,
|
|
470
|
+
packageManager
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Detect tech stack from project files
|
|
475
|
+
*/
|
|
476
|
+
async detectTechStack() {
|
|
477
|
+
const stack = [];
|
|
478
|
+
const packageJsonPath = path3.join(this.workspaceRoot, "package.json");
|
|
479
|
+
if (await fs3.pathExists(packageJsonPath)) {
|
|
480
|
+
try {
|
|
481
|
+
const pkg = await fs3.readJSON(packageJsonPath);
|
|
482
|
+
const allDeps = {
|
|
483
|
+
...pkg.dependencies,
|
|
484
|
+
...pkg.devDependencies
|
|
485
|
+
};
|
|
486
|
+
if (allDeps.typescript || await fs3.pathExists(path3.join(this.workspaceRoot, "tsconfig.json"))) {
|
|
487
|
+
stack.push("TypeScript");
|
|
488
|
+
} else {
|
|
489
|
+
stack.push("JavaScript");
|
|
490
|
+
}
|
|
491
|
+
if (allDeps.react) {
|
|
492
|
+
stack.push("React");
|
|
493
|
+
}
|
|
494
|
+
if (allDeps.vue) {
|
|
495
|
+
stack.push("Vue");
|
|
496
|
+
}
|
|
497
|
+
if (allDeps.svelte) {
|
|
498
|
+
stack.push("Svelte");
|
|
499
|
+
}
|
|
500
|
+
stack.push("Node.js");
|
|
501
|
+
if (allDeps.vitest) {
|
|
502
|
+
stack.push("Vitest");
|
|
503
|
+
} else if (allDeps.jest) {
|
|
504
|
+
stack.push("Jest");
|
|
505
|
+
} else if (allDeps.mocha) {
|
|
506
|
+
stack.push("Mocha");
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "Cargo.toml"))) {
|
|
512
|
+
stack.push("Rust");
|
|
513
|
+
}
|
|
514
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "go.mod"))) {
|
|
515
|
+
stack.push("Go");
|
|
516
|
+
}
|
|
517
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "pyproject.toml")) || await fs3.pathExists(path3.join(this.workspaceRoot, "setup.py")) || await fs3.pathExists(path3.join(this.workspaceRoot, "requirements.txt"))) {
|
|
518
|
+
stack.push("Python");
|
|
519
|
+
}
|
|
520
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "pom.xml")) || await fs3.pathExists(path3.join(this.workspaceRoot, "build.gradle"))) {
|
|
521
|
+
stack.push("Java");
|
|
522
|
+
}
|
|
523
|
+
return stack;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Detect test, build, and lint commands from package.json scripts
|
|
527
|
+
*/
|
|
528
|
+
async detectCommands() {
|
|
529
|
+
const commands = {};
|
|
530
|
+
const packageJsonPath = path3.join(this.workspaceRoot, "package.json");
|
|
531
|
+
if (await fs3.pathExists(packageJsonPath)) {
|
|
532
|
+
try {
|
|
533
|
+
const pkg = await fs3.readJSON(packageJsonPath);
|
|
534
|
+
const scripts = pkg.scripts ?? {};
|
|
535
|
+
const pm = await this.detectPackageManager();
|
|
536
|
+
const runCmd = pm === "bun" ? "bun" : pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm" : "npm run";
|
|
537
|
+
if (scripts.test) {
|
|
538
|
+
commands.test = `${runCmd} test`;
|
|
539
|
+
}
|
|
540
|
+
if (scripts.build) {
|
|
541
|
+
commands.build = `${runCmd} build`;
|
|
542
|
+
}
|
|
543
|
+
if (scripts.lint) {
|
|
544
|
+
commands.lint = `${runCmd} lint`;
|
|
545
|
+
} else if (scripts.eslint) {
|
|
546
|
+
commands.lint = `${runCmd} eslint`;
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "Makefile"))) {
|
|
552
|
+
try {
|
|
553
|
+
const makefile = await fs3.readFile(path3.join(this.workspaceRoot, "Makefile"), "utf-8");
|
|
554
|
+
if (!commands.test && makefile.includes("test:")) {
|
|
555
|
+
commands.test = "make test";
|
|
556
|
+
}
|
|
557
|
+
if (!commands.build && makefile.includes("build:")) {
|
|
558
|
+
commands.build = "make build";
|
|
559
|
+
}
|
|
560
|
+
if (!commands.lint && makefile.includes("lint:")) {
|
|
561
|
+
commands.lint = "make lint";
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "Cargo.toml"))) {
|
|
567
|
+
if (!commands.test) commands.test = "cargo test";
|
|
568
|
+
if (!commands.build) commands.build = "cargo build";
|
|
569
|
+
if (!commands.lint) commands.lint = "cargo clippy";
|
|
570
|
+
}
|
|
571
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "go.mod"))) {
|
|
572
|
+
if (!commands.test) commands.test = "go test ./...";
|
|
573
|
+
if (!commands.build) commands.build = "go build";
|
|
574
|
+
if (!commands.lint) commands.lint = "golangci-lint run";
|
|
575
|
+
}
|
|
576
|
+
return commands;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Detect the main framework used
|
|
580
|
+
*/
|
|
581
|
+
async detectFramework() {
|
|
582
|
+
const packageJsonPath = path3.join(this.workspaceRoot, "package.json");
|
|
583
|
+
if (await fs3.pathExists(packageJsonPath)) {
|
|
584
|
+
try {
|
|
585
|
+
const pkg = await fs3.readJSON(packageJsonPath);
|
|
586
|
+
const allDeps = {
|
|
587
|
+
...pkg.dependencies,
|
|
588
|
+
...pkg.devDependencies
|
|
589
|
+
};
|
|
590
|
+
if (allDeps.next) {
|
|
591
|
+
const version = allDeps.next.replace(/[^0-9.]/g, "").split(".")[0];
|
|
592
|
+
return `Next.js ${version}`;
|
|
593
|
+
}
|
|
594
|
+
if (allDeps["@remix-run/node"] || allDeps["@remix-run/react"]) {
|
|
595
|
+
return "Remix";
|
|
596
|
+
}
|
|
597
|
+
if (allDeps.astro) {
|
|
598
|
+
return "Astro";
|
|
599
|
+
}
|
|
600
|
+
if (allDeps.express) {
|
|
601
|
+
return "Express";
|
|
602
|
+
}
|
|
603
|
+
if (allDeps.fastify) {
|
|
604
|
+
return "Fastify";
|
|
605
|
+
}
|
|
606
|
+
if (allDeps["@nestjs/core"]) {
|
|
607
|
+
return "NestJS";
|
|
608
|
+
}
|
|
609
|
+
if (allDeps.hono) {
|
|
610
|
+
return "Hono";
|
|
611
|
+
}
|
|
612
|
+
if (allDeps.elysia) {
|
|
613
|
+
return "Elysia";
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return void 0;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Detect the package manager used
|
|
622
|
+
*/
|
|
623
|
+
async detectPackageManager() {
|
|
624
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "bun.lockb"))) {
|
|
625
|
+
return "bun";
|
|
626
|
+
}
|
|
627
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "pnpm-lock.yaml"))) {
|
|
628
|
+
return "pnpm";
|
|
629
|
+
}
|
|
630
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "yarn.lock"))) {
|
|
631
|
+
return "yarn";
|
|
632
|
+
}
|
|
633
|
+
if (await fs3.pathExists(path3.join(this.workspaceRoot, "package-lock.json"))) {
|
|
634
|
+
return "npm";
|
|
635
|
+
}
|
|
636
|
+
return "npm";
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// src/core/AgentsMdUpdater.ts
|
|
641
|
+
import path4 from "path";
|
|
642
|
+
import fs4 from "fs-extra";
|
|
643
|
+
var AgentsMdUpdater = class {
|
|
644
|
+
constructor(workspaceRoot) {
|
|
645
|
+
this.workspaceRoot = workspaceRoot;
|
|
646
|
+
this.agentsPath = path4.join(workspaceRoot, "AGENTS.md");
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Update AGENTS.md with discovered patterns and skills
|
|
650
|
+
*/
|
|
651
|
+
async update(options) {
|
|
652
|
+
const { patterns, usedSkills, conventions } = options;
|
|
653
|
+
let content = await this.loadOrCreateAgentsMd();
|
|
654
|
+
if (patterns) {
|
|
655
|
+
content = this.updateTechStackSection(content, patterns);
|
|
656
|
+
content = this.updateCommandsSection(content, patterns);
|
|
657
|
+
}
|
|
658
|
+
if (usedSkills && usedSkills.length > 0) {
|
|
659
|
+
content = this.updateSkillsSection(content, usedSkills);
|
|
660
|
+
}
|
|
661
|
+
if (conventions && conventions.length > 0) {
|
|
662
|
+
content = this.updateConventionsSection(content, conventions);
|
|
663
|
+
}
|
|
664
|
+
await fs4.writeFile(this.agentsPath, content, "utf-8");
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Load existing AGENTS.md or create a new one
|
|
669
|
+
*/
|
|
670
|
+
async loadOrCreateAgentsMd() {
|
|
671
|
+
if (await fs4.pathExists(this.agentsPath)) {
|
|
672
|
+
return await fs4.readFile(this.agentsPath, "utf-8");
|
|
673
|
+
}
|
|
674
|
+
return `# Project Autopilot
|
|
675
|
+
|
|
676
|
+
This file is automatically updated by Autohand to help AI assistants understand your project.
|
|
677
|
+
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Update the Tech Stack section
|
|
682
|
+
*/
|
|
683
|
+
updateTechStackSection(content, patterns) {
|
|
684
|
+
if (patterns.techStack.length === 0) return content;
|
|
685
|
+
const techStackContent = patterns.techStack.map((tech) => `- ${tech}`).join("\n");
|
|
686
|
+
const sectionContent = `## Tech Stack
|
|
687
|
+
|
|
688
|
+
${techStackContent}
|
|
689
|
+
`;
|
|
690
|
+
return this.updateSection(content, "Tech Stack", sectionContent);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Update the Commands section
|
|
694
|
+
*/
|
|
695
|
+
updateCommandsSection(content, patterns) {
|
|
696
|
+
const commands = [];
|
|
697
|
+
if (patterns.packageManager) {
|
|
698
|
+
commands.push(`- **Package Manager:** \`${patterns.packageManager}\``);
|
|
699
|
+
}
|
|
700
|
+
if (patterns.testCommand) {
|
|
701
|
+
commands.push(`- **Test:** \`${patterns.testCommand}\``);
|
|
702
|
+
}
|
|
703
|
+
if (patterns.buildCommand) {
|
|
704
|
+
commands.push(`- **Build:** \`${patterns.buildCommand}\``);
|
|
705
|
+
}
|
|
706
|
+
if (patterns.lintCommand) {
|
|
707
|
+
commands.push(`- **Lint:** \`${patterns.lintCommand}\``);
|
|
708
|
+
}
|
|
709
|
+
if (patterns.framework) {
|
|
710
|
+
commands.push(`- **Framework:** ${patterns.framework}`);
|
|
711
|
+
}
|
|
712
|
+
if (commands.length === 0) return content;
|
|
713
|
+
const sectionContent = `## Commands
|
|
714
|
+
|
|
715
|
+
${commands.join("\n")}
|
|
716
|
+
`;
|
|
717
|
+
return this.updateSection(content, "Commands", sectionContent);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Update the Skills section
|
|
721
|
+
*/
|
|
722
|
+
updateSkillsSection(content, usedSkills) {
|
|
723
|
+
if (usedSkills.length === 0) return content;
|
|
724
|
+
const skillCounts = usedSkills.reduce((acc, skill) => {
|
|
725
|
+
acc[skill] = (acc[skill] || 0) + 1;
|
|
726
|
+
return acc;
|
|
727
|
+
}, {});
|
|
728
|
+
const skillsList = Object.entries(skillCounts).sort((a, b) => b[1] - a[1]).map(([skill, count]) => `- ${skill} (${count}x)`).join("\n");
|
|
729
|
+
const sectionContent = `## Skills Used
|
|
730
|
+
|
|
731
|
+
${skillsList}
|
|
732
|
+
`;
|
|
733
|
+
return this.updateSection(content, "Skills Used", sectionContent);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Update the Conventions section
|
|
737
|
+
*/
|
|
738
|
+
updateConventionsSection(content, conventions) {
|
|
739
|
+
if (conventions.length === 0) return content;
|
|
740
|
+
const conventionsList = conventions.map((c) => `- ${c}`).join("\n");
|
|
741
|
+
const sectionContent = `## Conventions
|
|
742
|
+
|
|
743
|
+
${conventionsList}
|
|
744
|
+
`;
|
|
745
|
+
return this.updateSection(content, "Conventions", sectionContent);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Update or insert a section in the content
|
|
749
|
+
*/
|
|
750
|
+
updateSection(content, sectionName, newSectionContent) {
|
|
751
|
+
const sectionRegex = new RegExp(
|
|
752
|
+
`## ${this.escapeRegex(sectionName)}[\\s\\S]*?(?=\\n## |$)`,
|
|
753
|
+
"g"
|
|
754
|
+
);
|
|
755
|
+
if (sectionRegex.test(content)) {
|
|
756
|
+
return content.replace(sectionRegex, newSectionContent.trim() + "\n");
|
|
757
|
+
}
|
|
758
|
+
const conventionsMatch = content.match(/\n## Conventions/);
|
|
759
|
+
if (conventionsMatch && conventionsMatch.index !== void 0) {
|
|
760
|
+
return content.slice(0, conventionsMatch.index) + "\n" + newSectionContent + "\n" + content.slice(conventionsMatch.index);
|
|
761
|
+
}
|
|
762
|
+
return content.trimEnd() + "\n\n" + newSectionContent;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Escape regex special characters
|
|
766
|
+
*/
|
|
767
|
+
escapeRegex(str) {
|
|
768
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Check if AGENTS.md exists
|
|
772
|
+
*/
|
|
773
|
+
async exists() {
|
|
774
|
+
return fs4.pathExists(this.agentsPath);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get the detected patterns summary for display
|
|
778
|
+
*/
|
|
779
|
+
static formatPatternsSummary(patterns) {
|
|
780
|
+
const parts = [];
|
|
781
|
+
if (patterns.techStack.length > 0) {
|
|
782
|
+
parts.push(`Tech: ${patterns.techStack.join(", ")}`);
|
|
783
|
+
}
|
|
784
|
+
if (patterns.framework) {
|
|
785
|
+
parts.push(`Framework: ${patterns.framework}`);
|
|
786
|
+
}
|
|
787
|
+
if (patterns.packageManager) {
|
|
788
|
+
parts.push(`PM: ${patterns.packageManager}`);
|
|
789
|
+
}
|
|
790
|
+
return parts.join(" | ") || "No patterns detected";
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// src/core/AutomodeManager.ts
|
|
795
|
+
var DEFAULTS = {
|
|
796
|
+
maxIterations: 50,
|
|
797
|
+
maxRuntime: 120,
|
|
798
|
+
// minutes
|
|
799
|
+
maxCost: 10,
|
|
800
|
+
// dollars
|
|
801
|
+
checkpointInterval: 5,
|
|
802
|
+
completionPromise: "DONE",
|
|
803
|
+
useWorktree: true,
|
|
804
|
+
noProgressThreshold: 3,
|
|
805
|
+
sameErrorThreshold: 5,
|
|
806
|
+
testOnlyThreshold: 3
|
|
807
|
+
};
|
|
808
|
+
var AutomodeManager = class extends EventEmitter {
|
|
809
|
+
constructor(config, workspaceRoot, hookManager, session, memoryManager) {
|
|
810
|
+
super();
|
|
811
|
+
this.abortController = null;
|
|
812
|
+
this.startTime = 0;
|
|
813
|
+
this.totalCost = 0;
|
|
814
|
+
this.worktreePath = null;
|
|
815
|
+
this.originalBranch = null;
|
|
816
|
+
this.branchName = null;
|
|
817
|
+
this.gitCommits = [];
|
|
818
|
+
this.isRunning = false;
|
|
819
|
+
this.isPaused = false;
|
|
820
|
+
this.config = config;
|
|
821
|
+
this.workspaceRoot = workspaceRoot;
|
|
822
|
+
this.hookManager = hookManager;
|
|
823
|
+
this.session = session;
|
|
824
|
+
this.memoryManager = memoryManager;
|
|
825
|
+
this.patternDetector = new PatternDetector(workspaceRoot);
|
|
826
|
+
this.agentsMdUpdater = new AgentsMdUpdater(workspaceRoot);
|
|
827
|
+
this.state = new AutomodeState(workspaceRoot);
|
|
828
|
+
this.settings = {
|
|
829
|
+
...DEFAULTS,
|
|
830
|
+
...config.automode
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Check if auto-mode is currently running
|
|
835
|
+
*/
|
|
836
|
+
isActive() {
|
|
837
|
+
return this.isRunning;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Check if auto-mode is paused
|
|
841
|
+
*/
|
|
842
|
+
isPausedState() {
|
|
843
|
+
return this.isPaused;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get current state
|
|
847
|
+
*/
|
|
848
|
+
getState() {
|
|
849
|
+
return this.state.getState();
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Start auto-mode loop
|
|
853
|
+
*/
|
|
854
|
+
async start(options, runIteration) {
|
|
855
|
+
if (this.isRunning) {
|
|
856
|
+
throw new Error("Auto-mode is already running");
|
|
857
|
+
}
|
|
858
|
+
if (await this.state.hasActiveSession()) {
|
|
859
|
+
const existingState = await this.state.load();
|
|
860
|
+
if (existingState) {
|
|
861
|
+
console.log(chalk.yellow(`
|
|
862
|
+
\u26A0 Existing auto-mode session found: ${existingState.sessionId}`));
|
|
863
|
+
console.log(chalk.gray(` Status: ${existingState.status}, Iteration: ${existingState.currentIteration}`));
|
|
864
|
+
console.log(chalk.gray(` Use /automode resume to continue or /automode cancel to start fresh
|
|
865
|
+
`));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
this.isRunning = true;
|
|
870
|
+
this.isPaused = false;
|
|
871
|
+
this.abortController = new AbortController();
|
|
872
|
+
this.startTime = Date.now();
|
|
873
|
+
this.totalCost = 0;
|
|
874
|
+
this.gitCommits = [];
|
|
875
|
+
const maxIterations = options.maxIterations ?? this.settings.maxIterations;
|
|
876
|
+
const completionPromise = options.completionPromise ?? this.settings.completionPromise;
|
|
877
|
+
const useWorktree = options.useWorktree ?? this.settings.useWorktree;
|
|
878
|
+
const checkpointInterval = options.checkpointInterval ?? this.settings.checkpointInterval;
|
|
879
|
+
const maxRuntime = options.maxRuntime ?? this.settings.maxRuntime;
|
|
880
|
+
const maxCost = options.maxCost ?? this.settings.maxCost;
|
|
881
|
+
const sessionId = `automode-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
882
|
+
console.log(chalk.cyan("\n\u{1F504} Starting Auto-Mode"));
|
|
883
|
+
console.log(chalk.gray(` Session: ${sessionId}`));
|
|
884
|
+
console.log(chalk.gray(` Max iterations: ${maxIterations}`));
|
|
885
|
+
console.log(chalk.gray(` Completion promise: "${completionPromise}"`));
|
|
886
|
+
console.log(chalk.gray(` Worktree isolation: ${useWorktree ? "enabled" : "disabled"}`));
|
|
887
|
+
if (useWorktree) {
|
|
888
|
+
try {
|
|
889
|
+
await this.setupWorktree(sessionId);
|
|
890
|
+
console.log(chalk.gray(` Branch: ${this.branchName}`));
|
|
891
|
+
} catch (error) {
|
|
892
|
+
console.log(chalk.yellow(` \u26A0 Worktree setup failed, continuing in current branch`));
|
|
893
|
+
console.log(chalk.gray(` ${error}`));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
await this.state.initialize({
|
|
897
|
+
sessionId,
|
|
898
|
+
prompt: options.prompt,
|
|
899
|
+
maxIterations,
|
|
900
|
+
completionPromise,
|
|
901
|
+
branch: this.branchName ?? void 0,
|
|
902
|
+
worktreePath: this.worktreePath ?? void 0
|
|
903
|
+
});
|
|
904
|
+
console.log(chalk.cyan("\n Press ESC to cancel auto-mode\n"));
|
|
905
|
+
await this.session?.append({
|
|
906
|
+
role: "user",
|
|
907
|
+
content: `[Auto-Mode Start] ${options.prompt}`,
|
|
908
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
909
|
+
_meta: {
|
|
910
|
+
automode: true,
|
|
911
|
+
sessionId,
|
|
912
|
+
maxIterations,
|
|
913
|
+
completionPromise
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
await this.emitHookEvent("automode:start", {
|
|
917
|
+
sessionId,
|
|
918
|
+
prompt: options.prompt,
|
|
919
|
+
maxIterations
|
|
920
|
+
});
|
|
921
|
+
try {
|
|
922
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
923
|
+
if (this.abortController.signal.aborted) {
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
while (this.isPaused && !this.abortController.signal.aborted) {
|
|
927
|
+
await this.delay(500);
|
|
928
|
+
}
|
|
929
|
+
const elapsedMinutes = (Date.now() - this.startTime) / 6e4;
|
|
930
|
+
if (elapsedMinutes >= maxRuntime) {
|
|
931
|
+
await this.cancel("max_runtime");
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
if (this.totalCost >= maxCost) {
|
|
935
|
+
await this.cancel("max_cost");
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
console.log(chalk.cyan(`
|
|
939
|
+
\u{1F4CD} Iteration ${iteration}/${maxIterations}`));
|
|
940
|
+
const result = await runIteration(
|
|
941
|
+
iteration,
|
|
942
|
+
options.prompt,
|
|
943
|
+
this.abortController.signal
|
|
944
|
+
);
|
|
945
|
+
if (result.cost) {
|
|
946
|
+
this.totalCost += result.cost;
|
|
947
|
+
}
|
|
948
|
+
await this.session?.append({
|
|
949
|
+
role: "assistant",
|
|
950
|
+
content: result.output || `[Iteration ${iteration}] Actions: ${result.actions.join(", ")}`,
|
|
951
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
952
|
+
_meta: {
|
|
953
|
+
automode: true,
|
|
954
|
+
iteration,
|
|
955
|
+
actions: result.actions,
|
|
956
|
+
filesCreated: result.filesCreated,
|
|
957
|
+
filesModified: result.filesModified,
|
|
958
|
+
tokensUsed: result.tokensUsed,
|
|
959
|
+
cost: result.cost,
|
|
960
|
+
error: result.error
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
const iterationLog = {
|
|
964
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
965
|
+
actions: result.actions,
|
|
966
|
+
tokensUsed: result.tokensUsed,
|
|
967
|
+
cost: result.cost
|
|
968
|
+
};
|
|
969
|
+
if (result.filesCreated || result.filesModified) {
|
|
970
|
+
await this.state.updateFileCounts(
|
|
971
|
+
result.filesCreated ?? 0,
|
|
972
|
+
result.filesModified ?? 0
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
await this.emitHookEvent("automode:iteration", {
|
|
976
|
+
sessionId,
|
|
977
|
+
iteration,
|
|
978
|
+
actions: result.actions,
|
|
979
|
+
tokensUsed: result.tokensUsed
|
|
980
|
+
});
|
|
981
|
+
const hasChanges = (result.filesCreated ?? 0) + (result.filesModified ?? 0) > 0;
|
|
982
|
+
const errorHash = result.error ? hashError(result.error) : null;
|
|
983
|
+
const isTestOnly = result.actions.every(
|
|
984
|
+
(a) => a.toLowerCase().includes("test") || a.toLowerCase().includes("spec")
|
|
985
|
+
);
|
|
986
|
+
const circuitResult = this.state.checkCircuitBreaker(
|
|
987
|
+
hasChanges,
|
|
988
|
+
errorHash,
|
|
989
|
+
isTestOnly,
|
|
990
|
+
{
|
|
991
|
+
noProgress: this.settings.noProgressThreshold,
|
|
992
|
+
sameError: this.settings.sameErrorThreshold,
|
|
993
|
+
testOnly: this.settings.testOnlyThreshold
|
|
994
|
+
}
|
|
995
|
+
);
|
|
996
|
+
if (circuitResult.triggered) {
|
|
997
|
+
console.log(chalk.yellow(`
|
|
998
|
+
\u26A1 Circuit breaker triggered: ${circuitResult.reason}`));
|
|
999
|
+
await this.cancel("circuit_breaker");
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
if (result.output && this.state.checkCompletionPromise(result.output)) {
|
|
1003
|
+
console.log(chalk.green(`
|
|
1004
|
+
\u2705 Completion promise detected!`));
|
|
1005
|
+
await this.complete();
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
if (iteration % checkpointInterval === 0) {
|
|
1009
|
+
await this.createCheckpoint(iteration);
|
|
1010
|
+
}
|
|
1011
|
+
await this.state.recordIteration(iterationLog);
|
|
1012
|
+
}
|
|
1013
|
+
const currentState = this.state.getState();
|
|
1014
|
+
if (currentState && currentState.status === "running") {
|
|
1015
|
+
if (currentState.currentIteration >= maxIterations) {
|
|
1016
|
+
console.log(chalk.yellow(`
|
|
1017
|
+
\u26A0 Maximum iterations (${maxIterations}) reached`));
|
|
1018
|
+
await this.cancel("max_iterations");
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
console.error(chalk.red(`
|
|
1023
|
+
\u274C Auto-mode error: ${error}`));
|
|
1024
|
+
await this.state.setStatus("failed", "error", String(error));
|
|
1025
|
+
await this.emitHookEvent("automode:error", {
|
|
1026
|
+
sessionId,
|
|
1027
|
+
error: String(error)
|
|
1028
|
+
});
|
|
1029
|
+
} finally {
|
|
1030
|
+
await this.cleanup();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Cancel the auto-mode loop
|
|
1035
|
+
*/
|
|
1036
|
+
async cancel(reason = "user_cancel") {
|
|
1037
|
+
if (!this.isRunning) return;
|
|
1038
|
+
console.log(chalk.yellow(`
|
|
1039
|
+
\u26A0 Auto-mode cancelled: ${reason}`));
|
|
1040
|
+
this.abortController?.abort();
|
|
1041
|
+
await this.state.setStatus("cancelled", reason);
|
|
1042
|
+
const currentState = this.state.getState();
|
|
1043
|
+
if (currentState) {
|
|
1044
|
+
await this.session?.append({
|
|
1045
|
+
role: "assistant",
|
|
1046
|
+
content: `[Auto-Mode Cancelled] Reason: ${reason}. Stopped at iteration ${currentState.currentIteration}. Files created: ${currentState.filesCreated}, Files modified: ${currentState.filesModified}`,
|
|
1047
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1048
|
+
_meta: {
|
|
1049
|
+
automode: true,
|
|
1050
|
+
status: "cancelled",
|
|
1051
|
+
reason,
|
|
1052
|
+
iterations: currentState.currentIteration,
|
|
1053
|
+
filesCreated: currentState.filesCreated,
|
|
1054
|
+
filesModified: currentState.filesModified,
|
|
1055
|
+
totalCost: this.totalCost
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
await this.emitHookEvent("automode:cancel", {
|
|
1059
|
+
sessionId: currentState.sessionId,
|
|
1060
|
+
reason,
|
|
1061
|
+
iteration: currentState.currentIteration
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Pause the auto-mode loop
|
|
1067
|
+
*/
|
|
1068
|
+
async pause() {
|
|
1069
|
+
if (!this.isRunning || this.isPaused) return;
|
|
1070
|
+
this.isPaused = true;
|
|
1071
|
+
await this.state.setStatus("paused");
|
|
1072
|
+
const currentState = this.state.getState();
|
|
1073
|
+
if (currentState) {
|
|
1074
|
+
console.log(chalk.yellow(`
|
|
1075
|
+
\u23F8\uFE0F Auto-mode paused at iteration ${currentState.currentIteration}`));
|
|
1076
|
+
await this.emitHookEvent("automode:pause", {
|
|
1077
|
+
sessionId: currentState.sessionId,
|
|
1078
|
+
iteration: currentState.currentIteration
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Resume the auto-mode loop
|
|
1084
|
+
*/
|
|
1085
|
+
async resume() {
|
|
1086
|
+
if (!this.isRunning || !this.isPaused) return;
|
|
1087
|
+
this.isPaused = false;
|
|
1088
|
+
await this.state.setStatus("running");
|
|
1089
|
+
const currentState = this.state.getState();
|
|
1090
|
+
if (currentState) {
|
|
1091
|
+
console.log(chalk.cyan(`
|
|
1092
|
+
\u25B6\uFE0F Auto-mode resumed at iteration ${currentState.currentIteration}`));
|
|
1093
|
+
await this.emitHookEvent("automode:resume", {
|
|
1094
|
+
sessionId: currentState.sessionId,
|
|
1095
|
+
iteration: currentState.currentIteration
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Mark auto-mode as complete
|
|
1101
|
+
*/
|
|
1102
|
+
async complete() {
|
|
1103
|
+
await this.state.setStatus("completed", "completion");
|
|
1104
|
+
const currentState = this.state.getState();
|
|
1105
|
+
if (currentState) {
|
|
1106
|
+
try {
|
|
1107
|
+
this.detectedPatterns = await this.patternDetector.detect();
|
|
1108
|
+
await this.storeDetectedPatterns();
|
|
1109
|
+
console.log(chalk.gray(` \u{1F50D} Detected patterns saved to memory`));
|
|
1110
|
+
await this.agentsMdUpdater.update({ patterns: this.detectedPatterns });
|
|
1111
|
+
console.log(chalk.gray(` \u{1F4DD} AGENTS.md updated with project info`));
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
console.log(chalk.gray(` \u26A0 Pattern detection failed: ${error}`));
|
|
1114
|
+
}
|
|
1115
|
+
await this.session?.append({
|
|
1116
|
+
role: "assistant",
|
|
1117
|
+
content: `[Auto-Mode Complete] Task completed successfully after ${currentState.currentIteration} iterations. Files created: ${currentState.filesCreated}, Files modified: ${currentState.filesModified}`,
|
|
1118
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1119
|
+
_meta: {
|
|
1120
|
+
automode: true,
|
|
1121
|
+
status: "completed",
|
|
1122
|
+
iterations: currentState.currentIteration,
|
|
1123
|
+
filesCreated: currentState.filesCreated,
|
|
1124
|
+
filesModified: currentState.filesModified,
|
|
1125
|
+
totalCost: this.totalCost,
|
|
1126
|
+
detectedPatterns: this.detectedPatterns
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
await this.emitHookEvent("automode:complete", {
|
|
1130
|
+
sessionId: currentState.sessionId,
|
|
1131
|
+
iterations: currentState.currentIteration,
|
|
1132
|
+
filesCreated: currentState.filesCreated,
|
|
1133
|
+
filesModified: currentState.filesModified
|
|
1134
|
+
});
|
|
1135
|
+
if (this.worktreePath && this.branchName && this.originalBranch) {
|
|
1136
|
+
await this.mergeWorktree();
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Store detected patterns in project memory
|
|
1142
|
+
*/
|
|
1143
|
+
async storeDetectedPatterns() {
|
|
1144
|
+
if (!this.memoryManager || !this.detectedPatterns) return;
|
|
1145
|
+
const patterns = this.detectedPatterns;
|
|
1146
|
+
if (patterns.techStack.length > 0) {
|
|
1147
|
+
await this.memoryManager.store(
|
|
1148
|
+
`Tech Stack: ${patterns.techStack.join(", ")}`,
|
|
1149
|
+
"project",
|
|
1150
|
+
["auto-detected", "tech-stack"],
|
|
1151
|
+
"automode"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (patterns.framework) {
|
|
1155
|
+
await this.memoryManager.store(
|
|
1156
|
+
`Framework: ${patterns.framework}`,
|
|
1157
|
+
"project",
|
|
1158
|
+
["auto-detected", "framework"],
|
|
1159
|
+
"automode"
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
if (patterns.packageManager) {
|
|
1163
|
+
await this.memoryManager.store(
|
|
1164
|
+
`Package Manager: ${patterns.packageManager}`,
|
|
1165
|
+
"project",
|
|
1166
|
+
["auto-detected", "package-manager"],
|
|
1167
|
+
"automode"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
if (patterns.testCommand) {
|
|
1171
|
+
await this.memoryManager.store(
|
|
1172
|
+
`Test Command: ${patterns.testCommand}`,
|
|
1173
|
+
"project",
|
|
1174
|
+
["auto-detected", "command", "test"],
|
|
1175
|
+
"automode"
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
if (patterns.buildCommand) {
|
|
1179
|
+
await this.memoryManager.store(
|
|
1180
|
+
`Build Command: ${patterns.buildCommand}`,
|
|
1181
|
+
"project",
|
|
1182
|
+
["auto-detected", "command", "build"],
|
|
1183
|
+
"automode"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
if (patterns.lintCommand) {
|
|
1187
|
+
await this.memoryManager.store(
|
|
1188
|
+
`Lint Command: ${patterns.lintCommand}`,
|
|
1189
|
+
"project",
|
|
1190
|
+
["auto-detected", "command", "lint"],
|
|
1191
|
+
"automode"
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Set up git worktree for isolation
|
|
1197
|
+
*/
|
|
1198
|
+
async setupWorktree(sessionId) {
|
|
1199
|
+
try {
|
|
1200
|
+
this.originalBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1201
|
+
cwd: this.workspaceRoot,
|
|
1202
|
+
encoding: "utf-8"
|
|
1203
|
+
}).trim();
|
|
1204
|
+
} catch {
|
|
1205
|
+
throw new Error("Not a git repository");
|
|
1206
|
+
}
|
|
1207
|
+
this.branchName = `autohand-automode-${Date.now()}`;
|
|
1208
|
+
const tempDir = path5.join("/tmp", `autohand-worktree-${crypto.randomBytes(4).toString("hex")}`);
|
|
1209
|
+
this.worktreePath = tempDir;
|
|
1210
|
+
try {
|
|
1211
|
+
execSync(`git worktree add -b ${this.branchName} ${tempDir}`, {
|
|
1212
|
+
cwd: this.workspaceRoot,
|
|
1213
|
+
encoding: "utf-8"
|
|
1214
|
+
});
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
this.worktreePath = null;
|
|
1217
|
+
this.branchName = null;
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Create a checkpoint (git commit)
|
|
1223
|
+
*/
|
|
1224
|
+
async createCheckpoint(iteration) {
|
|
1225
|
+
const workDir = this.worktreePath ?? this.workspaceRoot;
|
|
1226
|
+
try {
|
|
1227
|
+
const status = execSync("git status --porcelain", {
|
|
1228
|
+
cwd: workDir,
|
|
1229
|
+
encoding: "utf-8"
|
|
1230
|
+
}).trim();
|
|
1231
|
+
if (!status) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
execSync("git add -A", { cwd: workDir, encoding: "utf-8" });
|
|
1235
|
+
const message = `automode: checkpoint at iteration ${iteration}`;
|
|
1236
|
+
execSync(`git commit -m "${message}"`, { cwd: workDir, encoding: "utf-8" });
|
|
1237
|
+
const hash = execSync("git rev-parse --short HEAD", {
|
|
1238
|
+
cwd: workDir,
|
|
1239
|
+
encoding: "utf-8"
|
|
1240
|
+
}).trim();
|
|
1241
|
+
await this.state.recordCheckpoint(hash, message);
|
|
1242
|
+
this.gitCommits.push({ hash, message });
|
|
1243
|
+
console.log(chalk.gray(` \u{1F4CC} Checkpoint: ${hash}`));
|
|
1244
|
+
const currentState = this.state.getState();
|
|
1245
|
+
if (currentState) {
|
|
1246
|
+
await this.emitHookEvent("automode:checkpoint", {
|
|
1247
|
+
sessionId: currentState.sessionId,
|
|
1248
|
+
iteration,
|
|
1249
|
+
commit: hash
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
console.log(chalk.gray(` \u26A0 Checkpoint failed: ${error}`));
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Merge worktree back to original branch
|
|
1258
|
+
*/
|
|
1259
|
+
async mergeWorktree() {
|
|
1260
|
+
if (!this.worktreePath || !this.branchName || !this.originalBranch) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
console.log(chalk.cyan(`
|
|
1265
|
+
\u{1F500} Merging ${this.branchName} to ${this.originalBranch}...`));
|
|
1266
|
+
execSync(`git checkout ${this.originalBranch}`, {
|
|
1267
|
+
cwd: this.workspaceRoot,
|
|
1268
|
+
encoding: "utf-8"
|
|
1269
|
+
});
|
|
1270
|
+
execSync(`git merge ${this.branchName} --no-edit`, {
|
|
1271
|
+
cwd: this.workspaceRoot,
|
|
1272
|
+
encoding: "utf-8"
|
|
1273
|
+
});
|
|
1274
|
+
console.log(chalk.green(` \u2705 Successfully merged to ${this.originalBranch}`));
|
|
1275
|
+
await this.cleanupWorktree();
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
console.log(chalk.yellow(` \u26A0 Merge failed: ${error}`));
|
|
1278
|
+
console.log(chalk.gray(` Worktree preserved at: ${this.worktreePath}`));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Clean up worktree
|
|
1283
|
+
*/
|
|
1284
|
+
async cleanupWorktree() {
|
|
1285
|
+
if (!this.worktreePath || !this.branchName) return;
|
|
1286
|
+
try {
|
|
1287
|
+
execSync(`git worktree remove ${this.worktreePath} --force`, {
|
|
1288
|
+
cwd: this.workspaceRoot,
|
|
1289
|
+
encoding: "utf-8"
|
|
1290
|
+
});
|
|
1291
|
+
execSync(`git branch -d ${this.branchName}`, {
|
|
1292
|
+
cwd: this.workspaceRoot,
|
|
1293
|
+
encoding: "utf-8"
|
|
1294
|
+
});
|
|
1295
|
+
} catch {
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Clean up after auto-mode ends
|
|
1300
|
+
*/
|
|
1301
|
+
async cleanup() {
|
|
1302
|
+
this.isRunning = false;
|
|
1303
|
+
this.isPaused = false;
|
|
1304
|
+
this.abortController = null;
|
|
1305
|
+
const currentState = this.state.getState();
|
|
1306
|
+
if (currentState) {
|
|
1307
|
+
try {
|
|
1308
|
+
const changelogPath = await generateChangelog(
|
|
1309
|
+
this.workspaceRoot,
|
|
1310
|
+
currentState,
|
|
1311
|
+
this.state.getIterations(),
|
|
1312
|
+
this.gitCommits
|
|
1313
|
+
);
|
|
1314
|
+
console.log(chalk.gray(`
|
|
1315
|
+
\u{1F4DD} Changelog saved: ${changelogPath}`));
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
console.log(chalk.gray(` \u26A0 Changelog generation failed: ${error}`));
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
this.printSummary();
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Print session summary
|
|
1324
|
+
*/
|
|
1325
|
+
printSummary() {
|
|
1326
|
+
const state = this.state.getState();
|
|
1327
|
+
if (!state) return;
|
|
1328
|
+
const durationMs = Date.now() - this.startTime;
|
|
1329
|
+
const durationMinutes = Math.round(durationMs / 6e4);
|
|
1330
|
+
console.log(chalk.cyan("\n\u{1F4CA} Auto-Mode Summary"));
|
|
1331
|
+
console.log(chalk.gray(` Status: ${state.status}`));
|
|
1332
|
+
console.log(chalk.gray(` Iterations: ${state.currentIteration}`));
|
|
1333
|
+
console.log(chalk.gray(` Duration: ${durationMinutes} minutes`));
|
|
1334
|
+
console.log(chalk.gray(` Files created: ${state.filesCreated}`));
|
|
1335
|
+
console.log(chalk.gray(` Files modified: ${state.filesModified}`));
|
|
1336
|
+
if (this.totalCost > 0) {
|
|
1337
|
+
console.log(chalk.gray(` Estimated cost: $${this.totalCost.toFixed(2)}`));
|
|
1338
|
+
}
|
|
1339
|
+
if (state.branch) {
|
|
1340
|
+
console.log(chalk.gray(` Branch: ${state.branch}`));
|
|
1341
|
+
}
|
|
1342
|
+
console.log("");
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Emit a hook event
|
|
1346
|
+
*/
|
|
1347
|
+
async emitHookEvent(event, context) {
|
|
1348
|
+
this.emit(event, context);
|
|
1349
|
+
if (this.hookManager) {
|
|
1350
|
+
try {
|
|
1351
|
+
await this.hookManager.executeHooks(event, context);
|
|
1352
|
+
} catch {
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Delay helper
|
|
1358
|
+
*/
|
|
1359
|
+
delay(ms) {
|
|
1360
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
function getAutomodeOptions(cliOptions, config) {
|
|
1364
|
+
if (!cliOptions.autoMode) {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
const configSettings = config.automode ?? {};
|
|
1368
|
+
return {
|
|
1369
|
+
prompt: cliOptions.autoMode,
|
|
1370
|
+
maxIterations: cliOptions.maxIterations ?? configSettings.maxIterations ?? DEFAULTS.maxIterations,
|
|
1371
|
+
completionPromise: cliOptions.completionPromise ?? configSettings.completionPromise ?? DEFAULTS.completionPromise,
|
|
1372
|
+
useWorktree: cliOptions.noWorktree === true ? false : configSettings.useWorktree ?? DEFAULTS.useWorktree,
|
|
1373
|
+
checkpointInterval: cliOptions.checkpointInterval ?? configSettings.checkpointInterval ?? DEFAULTS.checkpointInterval,
|
|
1374
|
+
maxRuntime: cliOptions.maxRuntime ?? configSettings.maxRuntime ?? DEFAULTS.maxRuntime,
|
|
1375
|
+
maxCost: cliOptions.maxCost ?? configSettings.maxCost ?? DEFAULTS.maxCost,
|
|
1376
|
+
dryRun: cliOptions.dryRun
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
export {
|
|
1380
|
+
AutomodeManager,
|
|
1381
|
+
getAutomodeOptions
|
|
1382
|
+
};
|
|
1383
|
+
/**
|
|
1384
|
+
* Auto-Mode State Management
|
|
1385
|
+
*
|
|
1386
|
+
* Handles reading/writing the .autohand/automode.local.md state file
|
|
1387
|
+
* that tracks auto-mode session progress.
|
|
1388
|
+
*
|
|
1389
|
+
* @license Apache-2.0
|
|
1390
|
+
*/
|
|
1391
|
+
/**
|
|
1392
|
+
* Auto-Mode Changelog Generation
|
|
1393
|
+
*
|
|
1394
|
+
* Generates AUTOMODE_CHANGELOG.md after auto-mode session completes.
|
|
1395
|
+
*
|
|
1396
|
+
* @license Apache-2.0
|
|
1397
|
+
*/
|
|
1398
|
+
/**
|
|
1399
|
+
* Pattern Detector
|
|
1400
|
+
*
|
|
1401
|
+
* Automatically detects project patterns like tech stack, test/build/lint commands,
|
|
1402
|
+
* and frameworks from project configuration files.
|
|
1403
|
+
*
|
|
1404
|
+
* @license Apache-2.0
|
|
1405
|
+
*/
|
|
1406
|
+
/**
|
|
1407
|
+
* AGENTS.md Updater
|
|
1408
|
+
*
|
|
1409
|
+
* Automatically updates AGENTS.md with discovered project patterns,
|
|
1410
|
+
* tech stack, commands, and skills used during auto-mode sessions.
|
|
1411
|
+
*
|
|
1412
|
+
* @license Apache-2.0
|
|
1413
|
+
*/
|
|
1414
|
+
/**
|
|
1415
|
+
* Auto-Mode Manager
|
|
1416
|
+
*
|
|
1417
|
+
* Orchestrates the autonomous loop feature inspired by the Ralph technique.
|
|
1418
|
+
* Manages iteration cycles, git worktree isolation, checkpointing, and
|
|
1419
|
+
* cancellation via ESC, hooks, RPC, and ACP.
|
|
1420
|
+
*
|
|
1421
|
+
* @license Apache-2.0
|
|
1422
|
+
*/
|