headlesscode 1.0.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.
Files changed (232) hide show
  1. package/ATTRIBUTION.md +53 -0
  2. package/CODE_OF_CONDUCT.md +130 -0
  3. package/CONTRIBUTING.md +107 -0
  4. package/LICENSE +202 -0
  5. package/README.md +486 -0
  6. package/SECURITY.md +211 -0
  7. package/bin/headlesscode.mjs +83 -0
  8. package/package.json +63 -0
  9. package/shared/prompts/review-mode-prompt-short.md +93 -0
  10. package/shared/prompts/review-mode-prompt.md +281 -0
  11. package/shared/rules-code/rules.md +22 -0
  12. package/shared/stacks/cpp/rules.md +30 -0
  13. package/shared/stacks/fastapi/rules.md +30 -0
  14. package/shared/stacks/javascript/rules.md +37 -0
  15. package/shared/stacks/postgresql/rules.md +31 -0
  16. package/shared/stacks/python/rules.md +35 -0
  17. package/shared/stacks/react/rules.md +11 -0
  18. package/shared/stacks/typescript/rules.md +10 -0
  19. package/src/budget/budget.ts +221 -0
  20. package/src/budget/concurrency.ts +126 -0
  21. package/src/budget/cost.ts +309 -0
  22. package/src/budget/index.ts +8 -0
  23. package/src/checkpoints/cli.ts +256 -0
  24. package/src/checkpoints/service.ts +227 -0
  25. package/src/cli.ts +1535 -0
  26. package/src/cloud/docker-provider.ts +334 -0
  27. package/src/cloud/provider.ts +300 -0
  28. package/src/codeintel/call-graph.ts +78 -0
  29. package/src/codeintel/find-references.ts +123 -0
  30. package/src/codeintel/go-to-definition.ts +193 -0
  31. package/src/codeintel/handlers.ts +190 -0
  32. package/src/codeintel/import-graph.ts +173 -0
  33. package/src/codeintel/outline.ts +180 -0
  34. package/src/codeintel/position.ts +77 -0
  35. package/src/codeintel/program.ts +350 -0
  36. package/src/codeintel/rename-symbol.ts +213 -0
  37. package/src/codeintel/tools.ts +280 -0
  38. package/src/codemap/build.ts +135 -0
  39. package/src/codemap/cli.ts +190 -0
  40. package/src/codemap/extract.ts +339 -0
  41. package/src/codemap/files.ts +236 -0
  42. package/src/codemap/fingerprint.ts +65 -0
  43. package/src/codemap/flows.ts +62 -0
  44. package/src/codemap/html.ts +451 -0
  45. package/src/codemap/lock.ts +80 -0
  46. package/src/codemap/types.ts +101 -0
  47. package/src/codesearch/airunner-embedder.ts +185 -0
  48. package/src/codesearch/chunk.ts +339 -0
  49. package/src/codesearch/cli.ts +223 -0
  50. package/src/codesearch/embedder.ts +332 -0
  51. package/src/codesearch/files.ts +280 -0
  52. package/src/codesearch/index.ts +469 -0
  53. package/src/codesearch/ollama-embedder.ts +205 -0
  54. package/src/codesearch/search.ts +141 -0
  55. package/src/codesearch/types.ts +100 -0
  56. package/src/config/mode-models.ts +218 -0
  57. package/src/dashboard/aggregate.ts +364 -0
  58. package/src/dashboard/chat-thread.ts +141 -0
  59. package/src/dashboard/checkpoints.ts +124 -0
  60. package/src/dashboard/cli.ts +193 -0
  61. package/src/dashboard/codemap.ts +44 -0
  62. package/src/dashboard/files.ts +121 -0
  63. package/src/dashboard/page.ts +2803 -0
  64. package/src/dashboard/self-improvement-metrics.ts +282 -0
  65. package/src/dashboard/server.ts +1103 -0
  66. package/src/dashboard/session-launch.ts +310 -0
  67. package/src/dashboard/timeline.ts +273 -0
  68. package/src/dashboard/tool-exec.ts +107 -0
  69. package/src/dashboard/trend-cli.ts +141 -0
  70. package/src/dashboard/trend.ts +413 -0
  71. package/src/decision-proxy/cli.ts +261 -0
  72. package/src/decision-proxy/proxy.ts +569 -0
  73. package/src/deploy/gate-cli.ts +147 -0
  74. package/src/deploy/gate.ts +254 -0
  75. package/src/engine/condense.ts +512 -0
  76. package/src/engine/events.ts +428 -0
  77. package/src/engine/handoff.ts +71 -0
  78. package/src/engine/lazy-tools.ts +160 -0
  79. package/src/engine/local-explore.ts +653 -0
  80. package/src/engine/logger.ts +96 -0
  81. package/src/engine/loop.ts +5517 -0
  82. package/src/engine/parser.ts +347 -0
  83. package/src/engine/prompt.ts +860 -0
  84. package/src/engine/reports.ts +47 -0
  85. package/src/engine/stacks.ts +448 -0
  86. package/src/engine/types.ts +291 -0
  87. package/src/engine/usage.ts +186 -0
  88. package/src/github/app-auth.ts +161 -0
  89. package/src/github/cli.ts +448 -0
  90. package/src/github/installations.ts +133 -0
  91. package/src/github/pr.ts +321 -0
  92. package/src/github/provision.ts +118 -0
  93. package/src/github/push.ts +122 -0
  94. package/src/index-util.ts +50 -0
  95. package/src/index.ts +81 -0
  96. package/src/init/cli.ts +248 -0
  97. package/src/init/gitignore.ts +74 -0
  98. package/src/llm/ollama.ts +308 -0
  99. package/src/llm/openrouter.ts +868 -0
  100. package/src/llm/preflight.ts +367 -0
  101. package/src/llm/transcript-capture.ts +84 -0
  102. package/src/memory/embed.ts +110 -0
  103. package/src/memory/index.ts +22 -0
  104. package/src/memory/local.ts +259 -0
  105. package/src/memory/summarizer.ts +283 -0
  106. package/src/memory/types.ts +153 -0
  107. package/src/memory/uwuchat.ts +157 -0
  108. package/src/migrate/cli.ts +115 -0
  109. package/src/orchestrator/analyze-cli.ts +104 -0
  110. package/src/orchestrator/auto-split.ts +206 -0
  111. package/src/orchestrator/cleanup.ts +1003 -0
  112. package/src/orchestrator/cli.ts +3571 -0
  113. package/src/orchestrator/cost-estimate.ts +564 -0
  114. package/src/orchestrator/cost-history-cli.ts +242 -0
  115. package/src/orchestrator/cost-history.ts +397 -0
  116. package/src/orchestrator/git-sync.ts +250 -0
  117. package/src/orchestrator/index.ts +153 -0
  118. package/src/orchestrator/log-analysis.ts +0 -0
  119. package/src/orchestrator/merge-check.ts +108 -0
  120. package/src/orchestrator/pipeline.ts +411 -0
  121. package/src/orchestrator/resume.ts +1940 -0
  122. package/src/orchestrator/reviewer.ts +503 -0
  123. package/src/orchestrator/split.ts +296 -0
  124. package/src/orchestrator/state.ts +542 -0
  125. package/src/orchestrator/status.ts +697 -0
  126. package/src/orchestrator/verification-gate.ts +134 -0
  127. package/src/orchestrator/watch.ts +898 -0
  128. package/src/permissions/commands.ts +1083 -0
  129. package/src/permissions/config.ts +241 -0
  130. package/src/permissions/index.ts +12 -0
  131. package/src/permissions/protected-files.ts +96 -0
  132. package/src/permissions/store-protection.ts +272 -0
  133. package/src/project-store.ts +648 -0
  134. package/src/projects/cli.ts +382 -0
  135. package/src/qa/qa.ts +487 -0
  136. package/src/tools/browser/handler.ts +346 -0
  137. package/src/tools/browser/service.ts +406 -0
  138. package/src/tools/browser/smoke.ts +78 -0
  139. package/src/tools/browser/tool.ts +99 -0
  140. package/src/tools/executor.ts +2575 -0
  141. package/src/tools/language-detect.ts +183 -0
  142. package/src/tools/output-summarizer.ts +369 -0
  143. package/src/tools/run-tests.ts +302 -0
  144. package/src/tools/set-indentation-tool.ts +49 -0
  145. package/src/tools/test-selection.ts +160 -0
  146. package/src/vendor/tests/smoke.ts +103 -0
  147. package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
  148. package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
  149. package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
  150. package/src/vendor/zoo-code/shim/os-name.ts +18 -0
  151. package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
  152. package/src/vendor/zoo-code/shim/vscode.ts +76 -0
  153. package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
  154. package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
  155. package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
  156. package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
  157. package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
  158. package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
  159. package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
  160. package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
  161. package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
  162. package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
  163. package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
  164. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
  165. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
  166. package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
  167. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
  168. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
  169. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
  170. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
  171. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
  172. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
  173. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
  174. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
  175. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
  176. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
  177. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
  178. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
  179. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
  180. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
  181. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
  182. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
  183. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
  184. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
  185. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
  186. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
  187. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
  188. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
  189. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
  190. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
  191. package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
  192. package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
  193. package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
  194. package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
  195. package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
  196. package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
  197. package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
  198. package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
  199. package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
  200. package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
  201. package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
  202. package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
  203. package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
  204. package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
  205. package/src/vendor/zoo-code/src/shared/language.ts +43 -0
  206. package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
  207. package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
  208. package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
  209. package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
  210. package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
  211. package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
  212. package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
  213. package/src/vendor/zoo-code/src/utils/object.ts +18 -0
  214. package/src/vendor/zoo-code/src/utils/path.ts +94 -0
  215. package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
  216. package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
  217. package/src/vendor/zoo-code/types/global-settings.ts +19 -0
  218. package/src/vendor/zoo-code/types/index.ts +22 -0
  219. package/src/vendor/zoo-code/types/message.ts +375 -0
  220. package/src/vendor/zoo-code/types/mode.ts +241 -0
  221. package/src/vendor/zoo-code/types/todo.ts +19 -0
  222. package/src/vendor/zoo-code/types/tool-params.ts +116 -0
  223. package/src/vendor/zoo-code/types/tool.ts +67 -0
  224. package/src/vendor/zoo-code/types/vscode.ts +84 -0
  225. package/src/vision/describe.ts +242 -0
  226. package/src/vision/tool.ts +91 -0
  227. package/src/watcher/cli.ts +369 -0
  228. package/src/watcher/github.ts +304 -0
  229. package/src/watcher/index.ts +59 -0
  230. package/src/watcher/state.ts +254 -0
  231. package/src/watcher/watch.ts +562 -0
  232. package/tsconfig.json +18 -0
@@ -0,0 +1,15 @@
1
+ import * as path from "path"
2
+
3
+ import { CheckpointServiceOptions } from "./types"
4
+ import { ShadowCheckpointService } from "./ShadowCheckpointService"
5
+
6
+ export class RepoPerTaskCheckpointService extends ShadowCheckpointService {
7
+ public static create({ taskId, workspaceDir, shadowDir, log = console.log }: CheckpointServiceOptions) {
8
+ return new RepoPerTaskCheckpointService(
9
+ taskId,
10
+ path.join(shadowDir, "tasks", taskId, "checkpoints"),
11
+ workspaceDir,
12
+ log,
13
+ )
14
+ }
15
+ }
@@ -0,0 +1,553 @@
1
+ import fs from "fs/promises"
2
+ import os from "os"
3
+ import * as path from "path"
4
+ import crypto from "crypto"
5
+ import EventEmitter from "events"
6
+
7
+ import simpleGit, { SimpleGit, SimpleGitOptions } from "simple-git"
8
+ import pWaitFor from "p-wait-for"
9
+ import * as vscode from "../../../shim/vscode"
10
+
11
+ import { fileExistsAtPath } from "../../utils/fs"
12
+ import { arePathsEqual } from "../../utils/path"
13
+ import { executeRipgrep } from "../../services/search/file-search"
14
+ import { t } from "../../i18n"
15
+
16
+ import { CheckpointDiff, CheckpointResult, CheckpointEventMap } from "./types"
17
+ import { getExcludePatterns } from "./excludes"
18
+
19
+ /**
20
+ * Environment variables stripped before passing the env to simple-git.
21
+ *
22
+ * Two categories:
23
+ * - Location-override vars (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY,
24
+ * GIT_ALTERNATE_OBJECT_DIRECTORIES, GIT_CEILING_DIRECTORIES): redirect git operations to
25
+ * unintended repositories or limit where git searches.
26
+ * - Code-execution vectors blocked by simple-git ≥3.36's blockUnsafeOperationsPlugin when
27
+ * passed via .env(): GIT_EDITOR, GIT_SSH_COMMAND, GIT_PAGER, PREFIX, etc.
28
+ *
29
+ * Stripping GIT_CONFIG_COUNT also neutralises the entire GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n
30
+ * family — git ignores those per-key entries when the count key is absent.
31
+ */
32
+ export const BLOCKED_ENV_KEYS = new Set([
33
+ "GIT_DIR",
34
+ "GIT_WORK_TREE",
35
+ "GIT_INDEX_FILE",
36
+ "GIT_OBJECT_DIRECTORY",
37
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
38
+ "GIT_CEILING_DIRECTORIES",
39
+ "GIT_TEMPLATE_DIR",
40
+ "GIT_EDITOR",
41
+ "GIT_SEQUENCE_EDITOR",
42
+ "GIT_ASKPASS",
43
+ "GIT_SSH",
44
+ "GIT_SSH_COMMAND",
45
+ "GIT_PAGER",
46
+ "GIT_PROXY_COMMAND",
47
+ "GIT_EXEC_PATH",
48
+ "GIT_EXTERNAL_DIFF",
49
+ "GIT_CONFIG",
50
+ "GIT_CONFIG_GLOBAL",
51
+ "GIT_CONFIG_SYSTEM",
52
+ "GIT_CONFIG_COUNT",
53
+ "PREFIX",
54
+ "EDITOR",
55
+ "PAGER",
56
+ "SSH_ASKPASS",
57
+ ])
58
+
59
+ // Lowercase set for case-insensitive lookup — the plugin uses toLowerCase() internally,
60
+ // so a var like Git_Editor would bypass an exact-match check on Linux.
61
+ const BLOCKED_ENV_KEYS_LOWER = new Set([...BLOCKED_ENV_KEYS].map((k) => k.toLowerCase()))
62
+
63
+ /**
64
+ * Creates a SimpleGit instance with sanitized environment variables to prevent
65
+ * interference from inherited git environment variables like GIT_DIR and GIT_WORK_TREE.
66
+ * This ensures checkpoint operations always target the intended shadow repository.
67
+ *
68
+ * @param baseDir - The directory where git operations should be executed
69
+ * @returns A SimpleGit instance with sanitized environment
70
+ */
71
+ function createSanitizedGit(baseDir: string): SimpleGit {
72
+ const sanitizedEnv: Record<string, string> = {}
73
+ const removedKeys: string[] = []
74
+
75
+ for (const [key, value] of Object.entries(process.env)) {
76
+ if (BLOCKED_ENV_KEYS_LOWER.has(key.toLowerCase())) {
77
+ removedKeys.push(key)
78
+ continue
79
+ }
80
+
81
+ if (value !== undefined) {
82
+ sanitizedEnv[key] = value
83
+ }
84
+ }
85
+
86
+ // Log which git env vars were removed (helps with debugging Dev Container issues)
87
+ if (removedKeys.length > 0) {
88
+ console.log(
89
+ `[createSanitizedGit] Removed git environment variables for checkpoint isolation: ${removedKeys.join(", ")}`,
90
+ )
91
+ }
92
+
93
+ const options: Partial<SimpleGitOptions> = {
94
+ baseDir,
95
+ config: [],
96
+ // --template="" stops git copying hooks/templates into the shadow repo (axis 1).
97
+ // GIT_TEMPLATE_DIR is stripped from the env above to block the env-var path (axis 2).
98
+ // allowUnsafeTemplateDir opts out of simple-git ≥3.36's blockUnsafeOperationsPlugin
99
+ // so the --template arg is not rejected before reaching git.
100
+ unsafe: { allowUnsafeTemplateDir: true },
101
+ }
102
+
103
+ // Create git instance and set the sanitized environment
104
+ const git = simpleGit(options)
105
+
106
+ // Use the .env() method to set the complete sanitized environment
107
+ // This replaces the inherited environment with our sanitized version
108
+ git.env(sanitizedEnv)
109
+
110
+ console.log(`[createSanitizedGit] Created git instance for baseDir: ${baseDir}`)
111
+
112
+ return git
113
+ }
114
+
115
+ export abstract class ShadowCheckpointService extends EventEmitter {
116
+ public readonly taskId: string
117
+ public readonly checkpointsDir: string
118
+ public readonly workspaceDir: string
119
+
120
+ protected _checkpoints: string[] = []
121
+ protected _baseHash?: string
122
+
123
+ protected readonly dotGitDir: string
124
+ protected git?: SimpleGit
125
+ protected readonly log: (message: string) => void
126
+ protected shadowGitConfigWorktree?: string
127
+
128
+ public get baseHash() {
129
+ return this._baseHash
130
+ }
131
+
132
+ protected set baseHash(value: string | undefined) {
133
+ this._baseHash = value
134
+ }
135
+
136
+ public get isInitialized() {
137
+ return !!this.git
138
+ }
139
+
140
+ public getCheckpoints(): string[] {
141
+ return this._checkpoints.slice()
142
+ }
143
+
144
+ constructor(taskId: string, checkpointsDir: string, workspaceDir: string, log: (message: string) => void) {
145
+ super()
146
+
147
+ const homedir = os.homedir()
148
+ const desktopPath = path.join(homedir, "Desktop")
149
+ const documentsPath = path.join(homedir, "Documents")
150
+ const downloadsPath = path.join(homedir, "Downloads")
151
+ const protectedPaths = [homedir, desktopPath, documentsPath, downloadsPath]
152
+
153
+ if (protectedPaths.includes(workspaceDir)) {
154
+ throw new Error(`Cannot use checkpoints in ${workspaceDir}`)
155
+ }
156
+
157
+ this.taskId = taskId
158
+ this.checkpointsDir = checkpointsDir
159
+ this.workspaceDir = workspaceDir
160
+
161
+ this.dotGitDir = path.join(this.checkpointsDir, ".git")
162
+ this.log = log
163
+ }
164
+
165
+ public async initShadowGit(onInit?: () => Promise<void>) {
166
+ if (this.git) {
167
+ throw new Error("Shadow git repo already initialized")
168
+ }
169
+
170
+ const nestedGitPath = await this.getNestedGitRepository()
171
+
172
+ if (nestedGitPath) {
173
+ // Show persistent error message with the offending path
174
+ const relativePath = path.relative(this.workspaceDir, nestedGitPath)
175
+ const message = t("common:errors.nested_git_repos_warning", { path: relativePath })
176
+ vscode.window.showErrorMessage(message)
177
+
178
+ throw new Error(
179
+ `Checkpoints are disabled because a nested git repository was detected at: ${relativePath}. ` +
180
+ "Please remove or relocate nested git repositories to use the checkpoints feature.",
181
+ )
182
+ }
183
+
184
+ await fs.mkdir(this.checkpointsDir, { recursive: true })
185
+ const git = createSanitizedGit(this.checkpointsDir)
186
+ const gitVersion = await git.version()
187
+ this.log(`[${this.constructor.name}#create] git = ${gitVersion}`)
188
+
189
+ let created = false
190
+ const startTime = Date.now()
191
+
192
+ if (await fileExistsAtPath(this.dotGitDir)) {
193
+ this.log(`[${this.constructor.name}#initShadowGit] shadow git repo already exists at ${this.dotGitDir}`)
194
+ const worktree = await this.getShadowGitConfigWorktree(git)
195
+
196
+ if (!worktree) {
197
+ throw new Error("Checkpoints require core.worktree to be set in the shadow git config")
198
+ }
199
+
200
+ const worktreeTrimmed = worktree.trim()
201
+
202
+ if (!arePathsEqual(worktreeTrimmed, this.workspaceDir)) {
203
+ throw new Error(
204
+ `Checkpoints can only be used in the original workspace: ${worktreeTrimmed} !== ${this.workspaceDir}`,
205
+ )
206
+ }
207
+
208
+ await this.writeExcludeFile()
209
+ this.baseHash = await git.revparse(["HEAD"])
210
+ } else {
211
+ this.log(`[${this.constructor.name}#initShadowGit] creating shadow git repo at ${this.checkpointsDir}`)
212
+ await git.init({ "--template": "" })
213
+ await git.addConfig("core.worktree", this.workspaceDir) // Sets the working tree to the current workspace.
214
+ await git.addConfig("commit.gpgSign", "false") // Disable commit signing for shadow repo.
215
+ await git.addConfig("user.name", "Roo Code")
216
+ await git.addConfig("user.email", "noreply@example.com")
217
+ await this.writeExcludeFile()
218
+ await this.stageAll(git)
219
+ const { commit } = await git.commit("initial commit", { "--allow-empty": null })
220
+ this.baseHash = commit
221
+ created = true
222
+ }
223
+
224
+ const duration = Date.now() - startTime
225
+
226
+ this.log(
227
+ `[${this.constructor.name}#initShadowGit] initialized shadow repo with base commit ${this.baseHash} in ${duration}ms`,
228
+ )
229
+
230
+ this.git = git
231
+
232
+ await onInit?.()
233
+
234
+ this.emit("initialize", {
235
+ type: "initialize",
236
+ workspaceDir: this.workspaceDir,
237
+ baseHash: this.baseHash,
238
+ created,
239
+ duration,
240
+ })
241
+
242
+ return { created, duration }
243
+ }
244
+
245
+ // Add basic excludes directly in git config, while respecting any
246
+ // .gitignore in the workspace.
247
+ // .git/info/exclude is local to the shadow git repo, so it's not
248
+ // shared with the main repo - and won't conflict with user's
249
+ // .gitignore.
250
+ protected async writeExcludeFile() {
251
+ await fs.mkdir(path.join(this.dotGitDir, "info"), { recursive: true })
252
+ const patterns = await getExcludePatterns(this.workspaceDir)
253
+ await fs.writeFile(path.join(this.dotGitDir, "info", "exclude"), patterns.join("\n"))
254
+ }
255
+
256
+ private async stageAll(git: SimpleGit) {
257
+ try {
258
+ await git.add([".", "--ignore-errors"])
259
+ } catch (error) {
260
+ this.log(
261
+ `[${this.constructor.name}#stageAll] failed to add files to git: ${error instanceof Error ? error.message : String(error)}`,
262
+ )
263
+ }
264
+ }
265
+
266
+ private async getNestedGitRepository(): Promise<string | null> {
267
+ try {
268
+ // Find all .git/HEAD files that are not at the root level.
269
+ const args = ["--files", "--hidden", "--follow", "-g", "**/.git/HEAD", this.workspaceDir]
270
+
271
+ const gitPaths = await executeRipgrep({ args, workspacePath: this.workspaceDir })
272
+
273
+ // Filter to only include nested git directories (not the root .git).
274
+ // Since we're searching for HEAD files, we expect type to be "file"
275
+ const nestedGitPaths = gitPaths.filter(({ type, path: filePath }) => {
276
+ // Check if it's a file and is a nested .git/HEAD (not at root)
277
+ if (type !== "file") return false
278
+
279
+ // Ensure it's a .git/HEAD file and not the root one
280
+ const normalizedPath = filePath.replace(/\\/g, "/")
281
+ return (
282
+ normalizedPath.includes(".git/HEAD") &&
283
+ !normalizedPath.startsWith(".git/") &&
284
+ normalizedPath !== ".git/HEAD"
285
+ )
286
+ })
287
+
288
+ if (nestedGitPaths.length > 0) {
289
+ // Get the first nested git repository path
290
+ // Remove .git/HEAD from the path to get the repository directory
291
+ const headPath = nestedGitPaths[0].path
292
+
293
+ // Use path module to properly extract the repository directory
294
+ // The HEAD file is at .git/HEAD, so we need to go up two directories
295
+ const gitDir = path.dirname(headPath) // removes HEAD, gives us .git
296
+ const repoDir = path.dirname(gitDir) // removes .git, gives us the repo directory
297
+
298
+ const absolutePath = path.join(this.workspaceDir, repoDir)
299
+
300
+ this.log(
301
+ `[${this.constructor.name}#getNestedGitRepository] found ${nestedGitPaths.length} nested git repositories, first at: ${repoDir}`,
302
+ )
303
+ return absolutePath
304
+ }
305
+
306
+ return null
307
+ } catch (error) {
308
+ this.log(
309
+ `[${this.constructor.name}#getNestedGitRepository] failed to check for nested git repos: ${error instanceof Error ? error.message : String(error)}`,
310
+ )
311
+
312
+ // If we can't check, assume there are no nested repos to avoid blocking the feature.
313
+ return null
314
+ }
315
+ }
316
+
317
+ private async getShadowGitConfigWorktree(git: SimpleGit) {
318
+ if (!this.shadowGitConfigWorktree) {
319
+ try {
320
+ this.shadowGitConfigWorktree = (await git.getConfig("core.worktree")).value || undefined
321
+ } catch (error) {
322
+ this.log(
323
+ `[${this.constructor.name}#getShadowGitConfigWorktree] failed to get core.worktree: ${error instanceof Error ? error.message : String(error)}`,
324
+ )
325
+ }
326
+ }
327
+
328
+ return this.shadowGitConfigWorktree
329
+ }
330
+
331
+ public async saveCheckpoint(
332
+ message: string,
333
+ options?: { allowEmpty?: boolean; suppressMessage?: boolean },
334
+ ): Promise<CheckpointResult | undefined> {
335
+ try {
336
+ this.log(
337
+ `[${this.constructor.name}#saveCheckpoint] starting checkpoint save (allowEmpty: ${options?.allowEmpty ?? false})`,
338
+ )
339
+
340
+ if (!this.git) {
341
+ throw new Error("Shadow git repo not initialized")
342
+ }
343
+
344
+ const startTime = Date.now()
345
+ await this.stageAll(this.git)
346
+ const commitArgs = options?.allowEmpty ? { "--allow-empty": null } : undefined
347
+ const result = await this.git.commit(message, commitArgs)
348
+ const fromHash = this._checkpoints[this._checkpoints.length - 1] ?? this.baseHash!
349
+ const toHash = result.commit || fromHash
350
+ this._checkpoints.push(toHash)
351
+ const duration = Date.now() - startTime
352
+
353
+ if (result.commit) {
354
+ this.emit("checkpoint", {
355
+ type: "checkpoint",
356
+ fromHash,
357
+ toHash,
358
+ duration,
359
+ suppressMessage: options?.suppressMessage ?? false,
360
+ })
361
+ }
362
+
363
+ if (result.commit) {
364
+ this.log(
365
+ `[${this.constructor.name}#saveCheckpoint] checkpoint saved in ${duration}ms -> ${result.commit}`,
366
+ )
367
+ return result
368
+ } else {
369
+ this.log(`[${this.constructor.name}#saveCheckpoint] found no changes to commit in ${duration}ms`)
370
+ return undefined
371
+ }
372
+ } catch (e) {
373
+ const error = e instanceof Error ? e : new Error(String(e))
374
+ this.log(`[${this.constructor.name}#saveCheckpoint] failed to create checkpoint: ${error.message}`)
375
+ this.emit("error", { type: "error", error })
376
+ throw error
377
+ }
378
+ }
379
+
380
+ public async restoreCheckpoint(commitHash: string) {
381
+ try {
382
+ this.log(`[${this.constructor.name}#restoreCheckpoint] starting checkpoint restore`)
383
+
384
+ if (!this.git) {
385
+ throw new Error("Shadow git repo not initialized")
386
+ }
387
+
388
+ const start = Date.now()
389
+ await this.git.clean("f", ["-d", "-f"])
390
+ await this.git.reset(["--hard", commitHash])
391
+
392
+ // Remove all checkpoints after the specified commitHash.
393
+ const checkpointIndex = this._checkpoints.indexOf(commitHash)
394
+
395
+ if (checkpointIndex !== -1) {
396
+ this._checkpoints = this._checkpoints.slice(0, checkpointIndex + 1)
397
+ }
398
+
399
+ const duration = Date.now() - start
400
+ this.emit("restore", { type: "restore", commitHash, duration })
401
+ this.log(`[${this.constructor.name}#restoreCheckpoint] restored checkpoint ${commitHash} in ${duration}ms`)
402
+ } catch (e) {
403
+ const error = e instanceof Error ? e : new Error(String(e))
404
+ this.log(`[${this.constructor.name}#restoreCheckpoint] failed to restore checkpoint: ${error.message}`)
405
+ this.emit("error", { type: "error", error })
406
+ throw error
407
+ }
408
+ }
409
+
410
+ public async getDiff({ from, to }: { from?: string; to?: string }): Promise<CheckpointDiff[]> {
411
+ if (!this.git) {
412
+ throw new Error("Shadow git repo not initialized")
413
+ }
414
+
415
+ const result = []
416
+
417
+ if (!from) {
418
+ from = (await this.git.raw(["rev-list", "--max-parents=0", "HEAD"])).trim()
419
+ }
420
+
421
+ // Stage all changes so that untracked files appear in diff summary.
422
+ await this.stageAll(this.git)
423
+
424
+ this.log(`[${this.constructor.name}#getDiff] diffing ${to ? `${from}..${to}` : `${from}..HEAD`}`)
425
+ const { files } = to ? await this.git.diffSummary([`${from}..${to}`]) : await this.git.diffSummary([from])
426
+
427
+ const cwdPath = (await this.getShadowGitConfigWorktree(this.git)) || this.workspaceDir || ""
428
+
429
+ for (const file of files) {
430
+ const relPath = file.file
431
+ const absPath = path.join(cwdPath, relPath)
432
+ const before = await this.git.show([`${from}:${relPath}`]).catch(() => "")
433
+
434
+ const after = to
435
+ ? await this.git.show([`${to}:${relPath}`]).catch(() => "")
436
+ : await fs.readFile(absPath, "utf8").catch(() => "")
437
+
438
+ result.push({ paths: { relative: relPath, absolute: absPath }, content: { before, after } })
439
+ }
440
+
441
+ return result
442
+ }
443
+
444
+ /**
445
+ * EventEmitter
446
+ */
447
+
448
+ override emit<K extends keyof CheckpointEventMap>(event: K, data: CheckpointEventMap[K]) {
449
+ return super.emit(event, data)
450
+ }
451
+
452
+ override on<K extends keyof CheckpointEventMap>(event: K, listener: (data: CheckpointEventMap[K]) => void) {
453
+ return super.on(event, listener)
454
+ }
455
+
456
+ override off<K extends keyof CheckpointEventMap>(event: K, listener: (data: CheckpointEventMap[K]) => void) {
457
+ return super.off(event, listener)
458
+ }
459
+
460
+ override once<K extends keyof CheckpointEventMap>(event: K, listener: (data: CheckpointEventMap[K]) => void) {
461
+ return super.once(event, listener)
462
+ }
463
+
464
+ /**
465
+ * Storage
466
+ */
467
+
468
+ public static hashWorkspaceDir(workspaceDir: string) {
469
+ return crypto.createHash("sha256").update(workspaceDir).digest("hex").toString().slice(0, 8)
470
+ }
471
+
472
+ protected static taskRepoDir({ taskId, globalStorageDir }: { taskId: string; globalStorageDir: string }) {
473
+ return path.join(globalStorageDir, "tasks", taskId, "checkpoints")
474
+ }
475
+
476
+ protected static workspaceRepoDir({
477
+ globalStorageDir,
478
+ workspaceDir,
479
+ }: {
480
+ globalStorageDir: string
481
+ workspaceDir: string
482
+ }) {
483
+ return path.join(globalStorageDir, "checkpoints", this.hashWorkspaceDir(workspaceDir))
484
+ }
485
+
486
+ public static async deleteTask({
487
+ taskId,
488
+ globalStorageDir,
489
+ workspaceDir,
490
+ }: {
491
+ taskId: string
492
+ globalStorageDir: string
493
+ workspaceDir: string
494
+ }) {
495
+ const workspaceRepoDir = this.workspaceRepoDir({ globalStorageDir, workspaceDir })
496
+ const branchName = `roo-${taskId}`
497
+ const git = createSanitizedGit(workspaceRepoDir)
498
+ const success = await this.deleteBranch(git, branchName)
499
+
500
+ if (success) {
501
+ console.log(`[${this.name}#deleteTask.${taskId}] deleted branch ${branchName}`)
502
+ } else {
503
+ console.error(`[${this.name}#deleteTask.${taskId}] failed to delete branch ${branchName}`)
504
+ }
505
+ }
506
+
507
+ public static async deleteBranch(git: SimpleGit, branchName: string) {
508
+ const branches = await git.branchLocal()
509
+
510
+ if (!branches.all.includes(branchName)) {
511
+ console.error(`[${this.constructor.name}#deleteBranch] branch ${branchName} does not exist`)
512
+ return false
513
+ }
514
+
515
+ const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"])
516
+
517
+ if (currentBranch === branchName) {
518
+ const worktree = await git.getConfig("core.worktree")
519
+
520
+ try {
521
+ await git.raw(["config", "--unset", "core.worktree"])
522
+ await git.reset(["--hard"])
523
+ await git.clean("f", ["-d"])
524
+ const defaultBranch = branches.all.includes("main") ? "main" : "master"
525
+ await git.checkout([defaultBranch, "--force"])
526
+
527
+ await pWaitFor(
528
+ async () => {
529
+ const newBranch = await git.revparse(["--abbrev-ref", "HEAD"])
530
+ return newBranch === defaultBranch
531
+ },
532
+ { interval: 500, timeout: 2_000 },
533
+ )
534
+
535
+ await git.branch(["-D", branchName])
536
+ return true
537
+ } catch (error) {
538
+ console.error(
539
+ `[${this.constructor.name}#deleteBranch] failed to delete branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`,
540
+ )
541
+
542
+ return false
543
+ } finally {
544
+ if (worktree.value) {
545
+ await git.addConfig("core.worktree", worktree.value)
546
+ }
547
+ }
548
+ } else {
549
+ await git.branch(["-D", branchName])
550
+ return true
551
+ }
552
+ }
553
+ }