* Autonomous backtrack: hard-reset to the last checkpoint (or the sandbox base * if none). This is the "this approach is wrong — undo it" lever. Returns the * SHA we reset to, or null on failure. * * The last checkpoint is RETAINED (not popped): backtracking means "return to * the las
(signal?: AbortSignal)
| 304 | * dropCheckpoint() first. |
| 305 | */ |
| 306 | async backtrack(signal?: AbortSignal): Promise<string | null> { |
| 307 | if (!this.state?.active) return null; |
| 308 | try { |
| 309 | // Safety: a `reset --hard` blows away the whole working tree of whatever |
| 310 | // branch HEAD points at. NEVER do that unless HEAD is still our sandbox |
| 311 | // branch — otherwise we'd destroy the user's real branch state. |
| 312 | const cur = await this.currentBranch(signal); |
| 313 | if (cur !== this.state.branch) { |
| 314 | logger.warn('GitSandbox.backtrack: HEAD is not the sandbox branch — aborting reset --hard', { expected: this.state.branch, actual: cur }); |
| 315 | return null; |
| 316 | } |
| 317 | const target = this.state.checkpoints[this.state.checkpoints.length - 1] ?? this.state.baseCommit; |
| 318 | const r = await git(['reset', '--hard', target], { cwd: this.cwd, signal }); |
| 319 | if (!(r.exitCode === 0)) return null; |
| 320 | logger.info('GitSandbox backtracked', { to: target.slice(0, 8) }); |
| 321 | return target; |
| 322 | } catch (e: any) { |
| 323 | logger.debug('GitSandbox.backtrack failed', { err: e?.message }); |
| 324 | return null; |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | /** Discard the most recent checkpoint so the next backtrack walks further back. */ |
| 329 | dropCheckpoint(): void { |
no test coverage detected