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,256 @@
1
+ /**
2
+ * `headlesscode checkpoints` subcommand.
3
+ *
4
+ * npx tsx src/cli.ts checkpoints --workspace <path> list
5
+ * npx tsx src/cli.ts checkpoints --workspace <path> restore <hash>
6
+ * npx tsx src/cli.ts checkpoints --workspace <path> diff <hash> [<to-hash>]
7
+ *
8
+ * A human-facing wrapper around src/checkpoints/service.ts. Since each
9
+ * `HeadlessSession` gets its own random taskId (see src/engine/loop.ts), a
10
+ * workspace can have checkpoints from multiple past sessions. This CLI
11
+ * operates on the MOST RECENT session's checkpoints for the workspace (the
12
+ * one most likely relevant to "undo my last run") — found by scanning
13
+ * `<checkpointDir>/tasks/*\/checkpoints` for the shadow repo whose
14
+ * `core.worktree` matches `--workspace` and picking the one with the newest
15
+ * commit. Pass `--task-id <id>` to target a specific session explicitly.
16
+ */
17
+
18
+ import { execFileSync } from "node:child_process"
19
+ import * as fs from "node:fs"
20
+ import * as path from "node:path"
21
+
22
+ import { createCheckpointService, defaultCheckpointDir, type CheckpointService } from "./service.js"
23
+
24
+ const CHECKPOINTS_USAGE = `headlesscode checkpoints — inspect/revert shadow-git checkpoints
25
+
26
+ Usage:
27
+ headlesscode checkpoints --workspace <path> list
28
+ headlesscode checkpoints --workspace <path> restore <hash>
29
+ headlesscode checkpoints --workspace <path> diff [<from-hash>] [<to-hash>]
30
+
31
+ Options:
32
+ --workspace <path> Workspace root whose checkpoints to inspect (required)
33
+ --checkpoint-dir <path> Shadow-git storage root (default: ${defaultCheckpointDir()})
34
+ --task-id <id> Operate on this session's checkpoints instead of
35
+ auto-detecting the most recent one for --workspace
36
+ --help Show this help and exit
37
+
38
+ Commands:
39
+ list List checkpoints for the workspace, oldest-first
40
+ (hash, date, message)
41
+ restore <hash> Check the shadow ref out onto the real workspace,
42
+ reverting files to that checkpoint. DESTRUCTIVE:
43
+ uncommitted changes since that checkpoint are lost.
44
+ diff [<from>] [<to>] Show changed files between two checkpoints (default
45
+ from: the first checkpoint; default to: the current
46
+ working tree)
47
+ `
48
+
49
+ interface CheckpointsCliOptions {
50
+ workspace?: string
51
+ checkpointDir?: string
52
+ taskId?: string
53
+ help: boolean
54
+ command?: "list" | "restore" | "diff"
55
+ commandArgs: string[]
56
+ }
57
+
58
+ export function parseCheckpointsArgs(argv: string[]): { options: CheckpointsCliOptions; error?: string } {
59
+ const options: CheckpointsCliOptions = { help: false, commandArgs: [] }
60
+
61
+ for (let i = 0; i < argv.length; i++) {
62
+ const arg = argv[i]
63
+ const eq = arg.indexOf("=")
64
+ const flag = eq === -1 ? arg : arg.slice(0, eq)
65
+ const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
66
+ const next = (): string | undefined => {
67
+ if (inlineValue !== undefined) {
68
+ return inlineValue
69
+ }
70
+ const v = argv[i + 1]
71
+ if (v === undefined || v.startsWith("--")) {
72
+ return undefined
73
+ }
74
+ i++
75
+ return v
76
+ }
77
+
78
+ switch (flag) {
79
+ case "--workspace": {
80
+ const v = next()
81
+ if (v === undefined) {
82
+ return { options, error: "Missing value for --workspace" }
83
+ }
84
+ options.workspace = v
85
+ break
86
+ }
87
+ case "--checkpoint-dir": {
88
+ const v = next()
89
+ if (v === undefined) {
90
+ return { options, error: "Missing value for --checkpoint-dir" }
91
+ }
92
+ options.checkpointDir = v
93
+ break
94
+ }
95
+ case "--task-id": {
96
+ const v = next()
97
+ if (v === undefined) {
98
+ return { options, error: "Missing value for --task-id" }
99
+ }
100
+ options.taskId = v
101
+ break
102
+ }
103
+ case "--help":
104
+ case "-h":
105
+ options.help = true
106
+ break
107
+ case "list":
108
+ case "restore":
109
+ case "diff":
110
+ if (options.command) {
111
+ return { options, error: `Unexpected extra command: ${arg}` }
112
+ }
113
+ options.command = flag
114
+ break
115
+ default:
116
+ if (arg.startsWith("--")) {
117
+ return { options, error: `Unknown argument: ${arg}` }
118
+ }
119
+ options.commandArgs.push(arg)
120
+ break
121
+ }
122
+ }
123
+
124
+ return { options }
125
+ }
126
+
127
+ /**
128
+ * Find the most recently active session (taskId) for `workspace` under
129
+ * `checkpointDir`, by scanning `<checkpointDir>/tasks/*\/checkpoints` shadow
130
+ * repos and picking the one whose `core.worktree` matches and whose `.git`
131
+ * dir has the newest mtime.
132
+ */
133
+ function findMostRecentTaskId(checkpointDir: string, workspace: string): string | undefined {
134
+ const tasksDir = path.join(checkpointDir, "tasks")
135
+ let entries: string[]
136
+ try {
137
+ entries = fs.readdirSync(tasksDir)
138
+ } catch {
139
+ return undefined
140
+ }
141
+
142
+ const workspaceAbs = path.resolve(workspace)
143
+ let best: { taskId: string; mtimeMs: number } | undefined
144
+
145
+ for (const taskId of entries) {
146
+ const gitDir = path.join(tasksDir, taskId, "checkpoints", ".git")
147
+ let worktree: string
148
+ try {
149
+ worktree = execFileSync("git", ["--git-dir", gitDir, "config", "core.worktree"], {
150
+ encoding: "utf-8",
151
+ }).trim()
152
+ } catch {
153
+ continue
154
+ }
155
+ if (path.resolve(worktree) !== workspaceAbs) {
156
+ continue
157
+ }
158
+ let mtimeMs: number
159
+ try {
160
+ mtimeMs = fs.statSync(gitDir).mtimeMs
161
+ } catch {
162
+ continue
163
+ }
164
+ if (!best || mtimeMs > best.mtimeMs) {
165
+ best = { taskId, mtimeMs }
166
+ }
167
+ }
168
+
169
+ return best?.taskId
170
+ }
171
+
172
+ async function resolveService(options: CheckpointsCliOptions): Promise<{ service: CheckpointService } | { error: string }> {
173
+ if (!options.workspace) {
174
+ return { error: "--workspace <path> is required" }
175
+ }
176
+ const workspace = path.resolve(options.workspace)
177
+ const checkpointDir = options.checkpointDir ? path.resolve(options.checkpointDir) : defaultCheckpointDir()
178
+
179
+ const taskId = options.taskId ?? findMostRecentTaskId(checkpointDir, workspace)
180
+ if (!taskId) {
181
+ return {
182
+ error: `No checkpoints found for workspace '${workspace}' under '${checkpointDir}'. Has a session run there yet?`,
183
+ }
184
+ }
185
+
186
+ const service = createCheckpointService({ taskId, workspaceRoot: workspace, checkpointDir })
187
+ await service.init()
188
+ return { service }
189
+ }
190
+
191
+ export async function checkpointsMain(argv: string[]): Promise<number> {
192
+ const { options, error } = parseCheckpointsArgs(argv)
193
+ if (error) {
194
+ process.stderr.write(`headlesscode checkpoints: ${error}\n\n${CHECKPOINTS_USAGE}`)
195
+ return 2
196
+ }
197
+ if (options.help || !options.command) {
198
+ process.stdout.write(CHECKPOINTS_USAGE)
199
+ return options.help ? 0 : 2
200
+ }
201
+
202
+ const resolved = await resolveService(options)
203
+ if ("error" in resolved) {
204
+ process.stderr.write(`headlesscode checkpoints: ${resolved.error}\n`)
205
+ return 2
206
+ }
207
+ const { service } = resolved
208
+
209
+ try {
210
+ switch (options.command) {
211
+ case "list": {
212
+ const entries = await service.list()
213
+ if (entries.length === 0) {
214
+ process.stdout.write("(no checkpoints)\n")
215
+ return 0
216
+ }
217
+ for (const entry of entries) {
218
+ process.stdout.write(`${entry.hash.slice(0, 12)} ${entry.date} ${entry.message}\n`)
219
+ }
220
+ return 0
221
+ }
222
+ case "restore": {
223
+ const hash = options.commandArgs[0]
224
+ if (!hash) {
225
+ process.stderr.write("headlesscode checkpoints restore: a commit hash is required\n")
226
+ return 2
227
+ }
228
+ await service.restore(hash)
229
+ process.stdout.write(`Restored workspace to checkpoint ${hash}\n`)
230
+ return 0
231
+ }
232
+ case "diff": {
233
+ const [from, to] = options.commandArgs
234
+ const changes = await service.diff({ from, to })
235
+ if (changes.length === 0) {
236
+ process.stdout.write("(no changes)\n")
237
+ return 0
238
+ }
239
+ for (const change of changes) {
240
+ process.stdout.write(`--- ${change.paths.relative}\n`)
241
+ process.stdout.write(`+++ ${change.paths.relative}\n`)
242
+ process.stdout.write(` before: ${change.content.before.length} chars\n`)
243
+ process.stdout.write(` after: ${change.content.after.length} chars\n`)
244
+ }
245
+ return 0
246
+ }
247
+ }
248
+ } catch (err) {
249
+ process.stderr.write(
250
+ `headlesscode checkpoints ${options.command}: ${err instanceof Error ? err.message : String(err)}\n`,
251
+ )
252
+ return 1
253
+ }
254
+
255
+ return 2
256
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Checkpoint service — thin wrapper around the vendored
3
+ * `RepoPerTaskCheckpointService` (a shadow git repo, per-task, tracking the
4
+ * real workspace via `core.worktree`; see
5
+ * `src/vendor/zoo-code/src/services/checkpoints/`).
6
+ *
7
+ * A "checkpoint" is a commit in a hidden shadow git repository — separate
8
+ * from any real git repo the workspace happens to have — so it never
9
+ * interferes with the user's own commits/branches/index. Restoring a
10
+ * checkpoint checks the shadow ref out onto the REAL working tree
11
+ * (`core.worktree` points at the workspace), so it genuinely reverts files.
12
+ *
13
+ * IMPORTANT — the shadow dir must live OUTSIDE the workspace it tracks. Zoo
14
+ * Code's real shadow dir is always outside any workspace (a VS Code
15
+ * extension's `globalStorageUri`, e.g. under the user's app-data profile).
16
+ * Two vendored operations assume that:
17
+ * - `stageAll()` (called by `initShadowGit`/`saveCheckpoint`/`getDiff`) runs
18
+ * `git add` with `cwd` set to the shadow dir itself. If the shadow dir is
19
+ * nested inside the workspace, `git add .`'s `.` pathspec resolves
20
+ * relative to that nested `cwd`, not the worktree root — so it only ever
21
+ * matches the (nearly empty) shadow dir, and both the "initial commit"
22
+ * and every checkpoint silently become empty-tree commits (verified
23
+ * empirically). This wrapper patches `stageAll` to `git add -A` (which
24
+ * resolves against the repo root regardless of `cwd`) as a belt-and-suspenders
25
+ * fix — see the patch below — but that alone does not make nesting safe:
26
+ * - `restoreCheckpoint()` runs `git clean -f -d -f` against the worktree.
27
+ * If the shadow dir is nested inside the workspace and isn't part of the
28
+ * shadow repo's own tracked tree (it never is — a git repo can't usefully
29
+ * track its own `.git`), `git clean` treats the entire shadow dir —
30
+ * `.git` included — as untracked cruft in the worktree and DELETES IT,
31
+ * destroying the checkpoint history it's mid-restore from. Verified
32
+ * empirically: a nested shadow dir does not survive a single restore.
33
+ *
34
+ * For both reasons, `createCheckpointService` refuses to create a service
35
+ * whose `checkpointDir` resolves inside `workspaceRoot`, and defaults to a
36
+ * per-user location outside any workspace: the central store's
37
+ * `~/.local/share/headlesscode/checkpoints` (mirroring Zoo Code's
38
+ * `globalStorageUri` design; consolidated here from the old
39
+ * `~/.headlesscode/checkpoints` — see src/project-store.ts's
40
+ * migrateCheckpointStore). Each session's shadow repo is further namespaced by
41
+ * a random `taskId`, so unrelated workspaces/sessions never collide there.
42
+ *
43
+ * Failure handling: every method here can throw (init can fail if git isn't
44
+ * installed, a nested git repo is detected, etc.). This wrapper does NOT
45
+ * swallow errors — callers (e.g. `HeadlessSession` in `src/engine/loop.ts`)
46
+ * are expected to wrap calls in try/catch and treat failures as non-fatal,
47
+ * exactly like the existing Phase 3 memory-recall pattern. The CLI
48
+ * (`src/checkpoints/cli.ts`) surfaces errors directly since a human is
49
+ * driving it there.
50
+ */
51
+
52
+ import * as path from "node:path"
53
+
54
+ import { ensureCheckpointMigration, projectStoreRoot } from "../project-store.js"
55
+ import { ShadowCheckpointService } from "../vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.js"
56
+ import type { CheckpointDiff, CheckpointResult } from "../vendor/zoo-code/src/services/checkpoints/types.js"
57
+
58
+ /**
59
+ * Default shadow-git storage root, outside any workspace (see file header).
60
+ * Lives under the central data store (`~/.local/share/headlesscode/checkpoints`)
61
+ * — consolidated from the old `~/.headlesscode/checkpoints`, which is migrated
62
+ * once automatically on first use (see ensureCheckpointMigration).
63
+ */
64
+ export function defaultCheckpointDir(): string {
65
+ ensureCheckpointMigration()
66
+ return path.join(projectStoreRoot(), "checkpoints")
67
+ }
68
+
69
+ export interface CreateCheckpointServiceOptions {
70
+ /** Session/task id — the shadow repo lives at `<checkpointDir>/tasks/<taskId>/checkpoints`. */
71
+ taskId: string
72
+ /** The real workspace whose files are being tracked (not the shadow dir itself). */
73
+ workspaceRoot: string
74
+ /** Base shadow-git storage dir. Default: `defaultCheckpointDir()`. Must NOT be inside workspaceRoot. */
75
+ checkpointDir?: string
76
+ /** Optional log sink (default: no-op — callers typically wire this to their own logger). */
77
+ log?: (message: string) => void
78
+ }
79
+
80
+ export interface CheckpointLogEntry {
81
+ hash: string
82
+ date: string
83
+ message: string
84
+ }
85
+
86
+ /** True when `child` is `parent` itself or nested anywhere inside it. */
87
+ function isSameOrInside(parent: string, child: string): boolean {
88
+ const parentAbs = path.resolve(parent)
89
+ const childAbs = path.resolve(child)
90
+ if (parentAbs === childAbs) {
91
+ return true
92
+ }
93
+ const rel = path.relative(parentAbs, childAbs)
94
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
95
+ }
96
+
97
+ /**
98
+ * Internal subclass that exposes just enough of `ShadowCheckpointService`'s
99
+ * protected `git` handle to list commits with metadata (message/date) —
100
+ * `getCheckpoints()` on the base class only returns bare hashes, which isn't
101
+ * enough for a human-facing `checkpoints list` CLI. Everything else is
102
+ * inherited from the vendored class unmodified. Named "RepoPerTask..." to
103
+ * match the vendored subclass's directory-layout convention
104
+ * (`shadowDir/tasks/<taskId>/checkpoints`).
105
+ */
106
+ class RepoPerTaskCheckpointServiceWithLog extends ShadowCheckpointService {
107
+ static create({ taskId, workspaceRoot, checkpointDir, log }: Required<CreateCheckpointServiceOptions>) {
108
+ return new RepoPerTaskCheckpointServiceWithLog(
109
+ taskId,
110
+ path.join(checkpointDir, "tasks", taskId, "checkpoints"),
111
+ workspaceRoot,
112
+ log,
113
+ )
114
+ }
115
+
116
+ async logEntries(): Promise<CheckpointLogEntry[]> {
117
+ if (!this.git) {
118
+ throw new Error("Shadow git repo not initialized")
119
+ }
120
+ const log = await this.git.log()
121
+ // simple-git returns newest-first; checkpoints read more naturally oldest-first.
122
+ return log.all.map((c) => ({ hash: c.hash, date: c.date, message: c.message })).reverse()
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Patch `ShadowCheckpointService`'s private `stageAll()` to use `git add -A`
128
+ * instead of the vendored `git add . --ignore-errors` — see the file header
129
+ * for why. `stageAll` is declared `private` on `ShadowCheckpointService`,
130
+ * which is a compile-time-only restriction in TypeScript; the method still
131
+ * exists on the prototype at runtime and can be replaced like any other.
132
+ * This patches ONE method's implementation on our own subclass's prototype;
133
+ * every other vendored code path (`initShadowGit`, `saveCheckpoint`,
134
+ * `restoreCheckpoint`, `getDiff`) is untouched and calls through to this
135
+ * patched `stageAll` exactly as before. `git add -A` behaves identically to
136
+ * `git add .` when `cwd` is already the repo root (the expected/enforced
137
+ * layout here), so this is a no-op behavior change in the supported
138
+ * configuration and only matters as defense in depth.
139
+ */
140
+ /* eslint-disable @typescript-eslint/no-explicit-any */
141
+ ;(RepoPerTaskCheckpointServiceWithLog.prototype as any).stageAll = async function (
142
+ this: { log: (message: string) => void },
143
+ git: { add: (args: string[]) => Promise<unknown> },
144
+ ) {
145
+ try {
146
+ await git.add(["-A", "--ignore-errors"])
147
+ } catch (error) {
148
+ this.log(`[stageAll] failed to add files to git: ${error instanceof Error ? error.message : String(error)}`)
149
+ }
150
+ }
151
+ /* eslint-enable @typescript-eslint/no-explicit-any */
152
+
153
+ export class CheckpointService {
154
+ private readonly svc: RepoPerTaskCheckpointServiceWithLog
155
+ private initialized = false
156
+
157
+ constructor(private readonly options: Required<CreateCheckpointServiceOptions>) {
158
+ this.svc = RepoPerTaskCheckpointServiceWithLog.create(options)
159
+ }
160
+
161
+ get taskId(): string {
162
+ return this.options.taskId
163
+ }
164
+
165
+ get checkpointsDir(): string {
166
+ return this.svc.checkpointsDir
167
+ }
168
+
169
+ /** Initialize the shadow git repo (idempotent per process — safe to call once per session). */
170
+ async init(): Promise<{ created: boolean; duration: number }> {
171
+ if (this.initialized) {
172
+ return { created: false, duration: 0 }
173
+ }
174
+ const result = await this.svc.initShadowGit()
175
+ this.initialized = true
176
+ return result
177
+ }
178
+
179
+ /** Save a checkpoint (commit). Returns undefined when there was nothing to commit (no changes, allowEmpty false). */
180
+ async save(message: string, options?: { allowEmpty?: boolean }): Promise<CheckpointResult | undefined> {
181
+ return this.svc.saveCheckpoint(message, options)
182
+ }
183
+
184
+ /** Check the shadow ref out onto the real workspace, reverting files to that checkpoint. */
185
+ async restore(commitHash: string): Promise<void> {
186
+ return this.svc.restoreCheckpoint(commitHash)
187
+ }
188
+
189
+ /** Diff between two checkpoints (or a checkpoint and the current working tree when `to` is omitted). */
190
+ async diff(options: { from?: string; to?: string }): Promise<CheckpointDiff[]> {
191
+ return this.svc.getDiff(options)
192
+ }
193
+
194
+ /** List checkpoints oldest-first, with commit message + date. */
195
+ async list(): Promise<CheckpointLogEntry[]> {
196
+ return this.svc.logEntries()
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Create (but do not initialize) a checkpoint service for a task/session.
202
+ * Call `.init()` before `.save()`/`.restore()`/`.diff()`/`.list()`.
203
+ *
204
+ * Throws synchronously if `checkpointDir` resolves inside `workspaceRoot` —
205
+ * see the file header for why that combination is unsafe (a restore would
206
+ * delete the shadow repo, and possibly nothing would ever get staged).
207
+ */
208
+ export function createCheckpointService(options: CreateCheckpointServiceOptions): CheckpointService {
209
+ const checkpointDir = options.checkpointDir ?? defaultCheckpointDir()
210
+
211
+ if (isSameOrInside(options.workspaceRoot, checkpointDir)) {
212
+ throw new Error(
213
+ `Checkpoint dir '${checkpointDir}' is inside the workspace it tracks ('${options.workspaceRoot}'). ` +
214
+ "The shadow git repo must live outside the workspace (restoring a checkpoint runs " +
215
+ "`git clean` against the workspace, which would delete a nested shadow repo). " +
216
+ "Pass a checkpointDir outside the workspace, or omit it to use the default " +
217
+ `(${defaultCheckpointDir()}).`,
218
+ )
219
+ }
220
+
221
+ return new CheckpointService({
222
+ taskId: options.taskId,
223
+ workspaceRoot: options.workspaceRoot,
224
+ checkpointDir,
225
+ log: options.log ?? (() => {}),
226
+ })
227
+ }