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,157 @@
1
+ /**
2
+ * UwUChatMemoryStore — the harness-side client for the future AIRunner/UwUChat
3
+ * memory API (Phase 3). See docs/memory-uwuchat-contract.md for the full
4
+ * endpoint contract + the data-isolation guarantee.
5
+ *
6
+ * STATUS: awaiting the AIRunner endpoints. Until a base URL + token are
7
+ * provided, every method throws a typed `UwUChatMemoryError` with the message
8
+ * "not implemented — awaiting AIRunner endpoint". Once `UWUCHAT_BASE_URL` (and
9
+ * optionally `UWUCHAT_TOKEN`) are set, the store fetches the documented
10
+ * endpoints with native `fetch` and Bearer-token auth.
11
+ *
12
+ * It implements the SAME `MemoryStore` interface as `LocalMemoryStore`, so the
13
+ * harness can swap backends without changing any caller.
14
+ */
15
+
16
+ import type { Embedder, FactInput, MemoryFact, MemoryStore, RecallResult, SessionSummary } from "./types.js"
17
+
18
+ export interface UwUChatMemoryOptions {
19
+ /** AIRunner/UwUChat base URL. Default: $UWUCHAT_BASE_URL. */
20
+ baseUrl?: string
21
+ /** Scoped service token. Default: $UWUCHAT_TOKEN. */
22
+ token?: string
23
+ /** Injectable fetch (tests use a fake; default: global fetch). */
24
+ fetchImpl?: typeof fetch
25
+ /** Unused here (embeddings are computed harness-side per the spec). */
26
+ embedder?: Embedder
27
+ }
28
+
29
+ /** Typed error for UwUChat API failures. */
30
+ export class UwUChatMemoryError extends Error {
31
+ constructor(
32
+ message: string,
33
+ readonly status?: number,
34
+ readonly body?: unknown,
35
+ ) {
36
+ super(message)
37
+ this.name = "UwUChatMemoryError"
38
+ }
39
+ }
40
+
41
+ /** Base path prefix for all harness memory endpoints on the AIRunner side. */
42
+ export const API_PREFIX = "/api/v1/harness/memory"
43
+
44
+ export class UwUChatMemoryStore implements MemoryStore {
45
+ readonly baseUrl: string
46
+ private readonly token: string | undefined
47
+ private readonly fetchImpl: typeof fetch
48
+
49
+ constructor(options: UwUChatMemoryOptions = {}) {
50
+ this.baseUrl = (options.baseUrl ?? process.env.UWUCHAT_BASE_URL ?? "").replace(/\/+$/, "")
51
+ this.token = options.token ?? process.env.UWUCHAT_TOKEN
52
+ this.fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init))
53
+ }
54
+
55
+ /** Build the absolute URL for an API path under the memory prefix. */
56
+ endpoint(apiPath: string): string {
57
+ return `${this.baseUrl}${apiPath}`
58
+ }
59
+
60
+ /** Auth + content headers (Bearer token when configured). */
61
+ authHeaders(): Record<string, string> {
62
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
63
+ if (this.token) {
64
+ headers["Authorization"] = `Bearer ${this.token}`
65
+ }
66
+ return headers
67
+ }
68
+
69
+ private ensureConfigured(): void {
70
+ if (!this.baseUrl) {
71
+ throw new UwUChatMemoryError(
72
+ "UwUChatMemoryStore: not implemented — awaiting AIRunner endpoint (set UWUCHAT_BASE_URL and, if required, UWUCHAT_TOKEN)",
73
+ )
74
+ }
75
+ }
76
+
77
+ private async request<T>(method: string, apiPath: string, body?: unknown): Promise<T> {
78
+ this.ensureConfigured()
79
+ const init: RequestInit = { method, headers: this.authHeaders() }
80
+ if (body !== undefined) {
81
+ init.body = JSON.stringify(body)
82
+ }
83
+
84
+ let response: Response
85
+ try {
86
+ response = await this.fetchImpl(this.endpoint(apiPath), init)
87
+ } catch (error) {
88
+ throw new UwUChatMemoryError(
89
+ `UwUChat ${method} ${apiPath} request failed: ${error instanceof Error ? error.message : String(error)}`,
90
+ )
91
+ }
92
+
93
+ if (!response.ok) {
94
+ let bodyText = ""
95
+ try {
96
+ bodyText = await response.text()
97
+ } catch {
98
+ // ignore body read errors
99
+ }
100
+ throw new UwUChatMemoryError(
101
+ `UwUChat ${method} ${apiPath} failed with status ${response.status}`,
102
+ response.status,
103
+ bodyText,
104
+ )
105
+ }
106
+
107
+ try {
108
+ return (await response.json()) as T
109
+ } catch (error) {
110
+ throw new UwUChatMemoryError(`UwUChat ${method} ${apiPath} returned invalid JSON`, response.status)
111
+ }
112
+ }
113
+
114
+ // ─── MemoryStore implementation (project-scoped) ─────────────────────────
115
+
116
+ async listFacts(project: string): Promise<MemoryFact[]> {
117
+ const data = await this.request<{ facts?: MemoryFact[] }>(
118
+ "GET",
119
+ `${API_PREFIX}/facts?project=${encodeURIComponent(project)}`,
120
+ )
121
+ return data.facts ?? []
122
+ }
123
+
124
+ async addFact(project: string, factInput: FactInput): Promise<MemoryFact> {
125
+ const data = await this.request<{ fact: MemoryFact }>("POST", `${API_PREFIX}/record-fact`, {
126
+ project,
127
+ fact: factInput,
128
+ })
129
+ return data.fact
130
+ }
131
+
132
+ async queryRecall(project: string, query: string, limit = 5): Promise<RecallResult> {
133
+ const data = await this.request<RecallResult>("POST", `${API_PREFIX}/query-recall`, { project, query, limit })
134
+ return { facts: data.facts ?? [], summaries: data.summaries ?? [] }
135
+ }
136
+
137
+ async recordSession(project: string, summary: SessionSummary): Promise<void> {
138
+ await this.request<{ ok?: boolean }>("POST", `${API_PREFIX}/record-session`, { project, session: summary })
139
+ }
140
+
141
+ async listSessions(project: string): Promise<SessionSummary[]> {
142
+ const data = await this.request<{ sessions?: SessionSummary[] }>(
143
+ "GET",
144
+ `${API_PREFIX}/sessions?project=${encodeURIComponent(project)}`,
145
+ )
146
+ return data.sessions ?? []
147
+ }
148
+
149
+ async summarize(project: string, options: { maxEntries?: number } = {}): Promise<string> {
150
+ const query = new URLSearchParams({ project })
151
+ if (options.maxEntries !== undefined) {
152
+ query.set("maxEntries", String(options.maxEntries))
153
+ }
154
+ const data = await this.request<{ summary: string }>("GET", `${API_PREFIX}/summaries?${query.toString()}`)
155
+ return data.summary
156
+ }
157
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * `headlesscode migrate` subcommand — the explicit, human-triggerable version
3
+ * of the one-time migrations the central store performs automatically on first
4
+ * use (see src/project-store.ts). Running it is always safe (idempotent): each
5
+ * migration is skipped when the source is absent or the target already exists,
6
+ * and every move verifies the content landed before the source is removed.
7
+ *
8
+ * headlesscode migrate [--workspace <path>]
9
+ *
10
+ * Migrates:
11
+ * 1. Global shared instructions out of `~/.roo/` (Zoo-Code-branded) into
12
+ * `~/.local/share/headlesscode/shared/` (modes.yaml + rules(-<mode>)/).
13
+ * 2. The checkpoint store from `~/.headlesscode/checkpoints` into
14
+ * `~/.local/share/headlesscode/checkpoints` (a MOVE — real, potentially
15
+ * large data is never copied-and-orphaned).
16
+ * 3. When `--workspace <path>` is given (default: the current directory),
17
+ * that workspace's legacy `<root>/.headlesscode/{codesearch,
18
+ * mode-models.json, permissions.json}` into the central project store —
19
+ * keyed by the repo's git-common-dir, so worktrees share it.
20
+ */
21
+
22
+ import * as path from "node:path"
23
+
24
+ import {
25
+ isStoreOverridden,
26
+ migrateCheckpointStore,
27
+ migrateLegacyProjectData,
28
+ migrateSharedInstructions,
29
+ resolveProjectDataDir,
30
+ resolveProjectIdentity,
31
+ } from "../project-store.js"
32
+
33
+ const MIGRATE_USAGE = `headlesscode migrate — move legacy headlesscode data into the central store
34
+
35
+ Usage:
36
+ headlesscode migrate [--workspace <path>]
37
+
38
+ The central store is ~/.local/share/headlesscode/ (see src/project-store.ts).
39
+ Migrations are one-time and idempotent; each move is verified before the
40
+ source is removed, so re-running is always safe.
41
+
42
+ Steps:
43
+ 1. ~/.roo/ -> ~/.local/share/headlesscode/shared/ (global modes + rules)
44
+ 2. ~/.headlesscode/checkpoints -> ~/.local/share/headlesscode/checkpoints
45
+ 3. <workspace>/.headlesscode/{codesearch,mode-models.json,permissions.json}
46
+ -> ~/.local/share/headlesscode/projects/<key>/ (when --workspace given)
47
+
48
+ Options:
49
+ --workspace <path> Workspace whose legacy .headlesscode/ content to migrate
50
+ (default: the current directory)
51
+ --help Show this help and exit
52
+ `
53
+
54
+ export function parseMigrateArgs(argv: string[]): { workspace?: string; help: boolean; error?: string } {
55
+ const options: { workspace?: string; help: boolean } = { help: false }
56
+ for (let i = 0; i < argv.length; i++) {
57
+ const arg = argv[i]
58
+ switch (arg) {
59
+ case "--help":
60
+ case "-h":
61
+ options.help = true
62
+ break
63
+ case "--workspace": {
64
+ const v = argv[i + 1]
65
+ if (v === undefined || v.startsWith("--")) {
66
+ return { ...options, error: "Missing value for --workspace" }
67
+ }
68
+ options.workspace = v
69
+ i++
70
+ break
71
+ }
72
+ default:
73
+ return { ...options, error: `Unknown argument: ${arg}` }
74
+ }
75
+ }
76
+ return options
77
+ }
78
+
79
+ export async function migrateMain(argv: string[]): Promise<number> {
80
+ const { workspace, help, error } = parseMigrateArgs(argv)
81
+ if (error) {
82
+ process.stderr.write(`headlesscode migrate: ${error}\n\n${MIGRATE_USAGE}`)
83
+ return 2
84
+ }
85
+ if (help) {
86
+ process.stdout.write(MIGRATE_USAGE)
87
+ return 0
88
+ }
89
+
90
+ // Step 1 + 2: global shared instructions + checkpoint store. Log to stdout
91
+ // (this is the explicit command's report channel). Under a
92
+ // $HEADLESSCODE_DATA_DIR override (tests/scratch) these two REAL-home-data
93
+ // migrations are skipped — the override means "don't touch real home data";
94
+ // the workspace migration below still runs against the override store.
95
+ const log = (msg: string): void => {
96
+ process.stdout.write(`[migrate] ${msg}\n`)
97
+ }
98
+ if (isStoreOverridden()) {
99
+ log("skipping global migrations (shared instructions + checkpoints) under a HEADLESSCODE_DATA_DIR override")
100
+ } else {
101
+ migrateSharedInstructions({ log })
102
+ migrateCheckpointStore({ log })
103
+ }
104
+
105
+ // Step 3: the workspace's legacy .headlesscode/ content.
106
+ const ws = path.resolve(workspace ?? process.cwd())
107
+ const { keySource } = resolveProjectIdentity(ws)
108
+ const centralDir = resolveProjectDataDir(ws)
109
+ migrateLegacyProjectData(ws, keySource, centralDir, { log })
110
+
111
+ process.stdout.write(
112
+ "[migrate] done. New central store root: ~/.local/share/headlesscode/ (per-project data under projects/, checkpoints under checkpoints/, shared instructions under shared/).\n",
113
+ )
114
+ return 0
115
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `headlesscode analyze-worktree` subcommand — ad-hoc, standalone entry
3
+ * point for src/orchestrator/log-analysis.ts. The orchestrator already
4
+ * runs this automatically once each group reaches "done" (see cli.ts's
5
+ * onGroupUpdate); this subcommand exists for pointing it at ANY worktree
6
+ * by hand — a stalled group mid-round, a worktree from a round that
7
+ * predates this feature, or just re-reading a report without waiting for
8
+ * the next orchestrate poll.
9
+ *
10
+ * headlesscode analyze-worktree --worktree <path> [--json]
11
+ * [--stall-minutes <n>] [--repeat-threshold <n>]
12
+ */
13
+
14
+ import { analyzeWorktreeSessions, formatAnalysisReport } from "./log-analysis.js"
15
+
16
+ const ANALYZE_USAGE = `headlesscode analyze-worktree — deterministic session log analysis
17
+
18
+ Usage:
19
+ headlesscode analyze-worktree --worktree <path> [options]
20
+
21
+ Options:
22
+ --worktree <path> Worktree root to analyze (required) — the dir
23
+ containing .headlesscode/events/*.jsonl
24
+ --json Print the raw SessionLogAnalysis as JSON instead
25
+ of the human-readable report
26
+ --stall-minutes <n> Gap between events considered a stall (default 15)
27
+ --repeat-threshold <n> Min verbatim repeats to flag a command (default 3)
28
+ --help Show this help and exit
29
+ `
30
+
31
+ interface AnalyzeCliOptions {
32
+ worktree: string
33
+ json: boolean
34
+ stallMinutes?: number
35
+ repeatThreshold?: number
36
+ }
37
+
38
+ export function parseAnalyzeArgs(argv: string[]): AnalyzeCliOptions | { help: true } | { error: string } {
39
+ let worktree: string | undefined
40
+ let json = false
41
+ let stallMinutes: number | undefined
42
+ let repeatThreshold: number | undefined
43
+
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const arg = argv[i]
46
+ if (arg === "--help" || arg === "-h") {
47
+ return { help: true }
48
+ } else if (arg === "--worktree") {
49
+ worktree = argv[++i]
50
+ } else if (arg === "--json") {
51
+ json = true
52
+ } else if (arg === "--stall-minutes") {
53
+ const raw = argv[++i]
54
+ const n = Number(raw)
55
+ if (!raw || Number.isNaN(n) || n <= 0) {
56
+ return { error: `--stall-minutes must be a positive number, got: ${raw}` }
57
+ }
58
+ stallMinutes = n
59
+ } else if (arg === "--repeat-threshold") {
60
+ const raw = argv[++i]
61
+ const n = Number(raw)
62
+ if (!raw || Number.isNaN(n) || n <= 0) {
63
+ return { error: `--repeat-threshold must be a positive number, got: ${raw}` }
64
+ }
65
+ repeatThreshold = n
66
+ } else {
67
+ return { error: `unknown argument: ${arg}` }
68
+ }
69
+ }
70
+
71
+ if (!worktree) {
72
+ return { error: "--worktree is required" }
73
+ }
74
+ return { worktree, json, stallMinutes, repeatThreshold }
75
+ }
76
+
77
+ export async function analyzeCliMain(argv: string[]): Promise<number> {
78
+ const parsed = parseAnalyzeArgs(argv)
79
+ if ("help" in parsed) {
80
+ process.stdout.write(ANALYZE_USAGE)
81
+ return 0
82
+ }
83
+ if ("error" in parsed) {
84
+ process.stderr.write(`analyze-worktree: ${parsed.error}\n\n${ANALYZE_USAGE}`)
85
+ return 2
86
+ }
87
+
88
+ const analysis = await analyzeWorktreeSessions(parsed.worktree, {
89
+ stallThresholdMs: parsed.stallMinutes !== undefined ? parsed.stallMinutes * 60 * 1000 : undefined,
90
+ repeatThreshold: parsed.repeatThreshold,
91
+ })
92
+
93
+ if (!analysis) {
94
+ process.stderr.write(`analyze-worktree: no .headlesscode/events feed found under ${parsed.worktree}\n`)
95
+ return 1
96
+ }
97
+
98
+ if (parsed.json) {
99
+ process.stdout.write(JSON.stringify(analysis, null, 2) + "\n")
100
+ } else {
101
+ process.stdout.write(formatAnalysisReport(analysis) + "\n")
102
+ }
103
+ return 0
104
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Auto-split for oversized issues (issue #53's follow-up): the pre-flight
3
+ * heuristic in split.ts (topLevelSectionCount) was warn-only by design
4
+ * because a crude "count top-level bullets" signal false-positives on issues
5
+ * that are genuinely ONE coherent piece of work with many sequential steps —
6
+ * exploding every bullet into its own issue would be actively worse than
7
+ * dispatching the oversized issue as-is (it fragments one fix's sequential
8
+ * steps across unrelated issues that make no sense in isolation).
9
+ *
10
+ * This module replaces "warn and let a human decide" with a SEMANTIC split:
11
+ * an LLM reads the flagged issue and proposes the smallest number of
12
+ * independently-shippable, coherent sub-issues that partition the work (for
13
+ * a two-part bug report like "server query is unbounded AND client loading
14
+ * state is misleading", that's 2 sub-issues, not one per bullet). Fails
15
+ * OPEN: any failure (LLM error, unparseable response, model proposes fewer
16
+ * than 2 pieces meaning "this doesn't actually decompose") leaves the
17
+ * original issue untouched and dispatches it as-is — the same behavior as
18
+ * before this module existed. A bad split must never be worse than no split.
19
+ */
20
+
21
+ import type { LlmClient } from "../engine/types.js"
22
+ import type { IssueSizeWarning, SplitIssue } from "./split.js"
23
+
24
+ /** One sub-issue the model proposed as an independently-shippable slice of the original. */
25
+ export interface ProposedSubIssue {
26
+ title: string
27
+ body: string
28
+ }
29
+
30
+ /** Never propose fewer than this many pieces — 1 means "don't split", handled as a no-op by the caller. */
31
+ const MIN_SUBISSUES = 2
32
+
33
+ /** Never propose more than this many — a runaway response must not file dozens of issues from one call. */
34
+ const MAX_SUBISSUES = 8
35
+
36
+ const SPLIT_SYSTEM_PROMPT =
37
+ "You split an oversized GitHub issue into a SMALL number of independently-shippable sub-issues. " +
38
+ "HARD RULES:\n" +
39
+ "1. Group by INDEPENDENT DELIVERABLE, not by bullet point. If the issue describes N sequential steps of " +
40
+ "ONE fix (e.g. 'step 1: add an index, step 2: change the query, step 3: verify'), those steps belong in " +
41
+ "ONE sub-issue together — splitting sequential steps of a single fix across issues is WRONG.\n" +
42
+ "2. Only split along boundaries where the pieces could genuinely ship, be reviewed, and be tested " +
43
+ "independently (e.g. a server-side fix vs. a client-side fix for the same bug are usually independent; " +
44
+ "the individual code changes inside ONE of those fixes usually are not).\n" +
45
+ "3. If the issue is really just one coherent piece of work with a long list of steps/details — NOT " +
46
+ `multiple independent deliverables — return a JSON array with FEWER THAN ${MIN_SUBISSUES} elements ` +
47
+ "(e.g. an empty array) to say so. Do not force a split that doesn't exist.\n" +
48
+ "4. Each sub-issue's body must be self-contained: include everything from the original body relevant to " +
49
+ "that piece (file paths, line numbers, root-cause analysis, verification steps) so implementing it doesn't " +
50
+ "require re-reading the original issue. Do not invent details the original didn't contain.\n" +
51
+ "5. Output ONLY a JSON array of objects with exactly two string fields, \"title\" and \"body\" — no " +
52
+ "markdown fences, no preamble, no commentary."
53
+
54
+ function buildSplitUserPrompt(issue: SplitIssue): string {
55
+ return (
56
+ `Issue #${issue.number}: ${issue.title}\n\n` +
57
+ `=== BODY BEGIN ===\n${issue.body ?? "(no body)"}\n=== BODY END ===`
58
+ )
59
+ }
60
+
61
+ /** Extract a JSON array from a model response that may still wrap it in prose or a code fence, despite rule 5. */
62
+ function extractJsonArray(text: string): unknown {
63
+ const trimmed = text.trim()
64
+ try {
65
+ return JSON.parse(trimmed)
66
+ } catch {
67
+ // Fall back to the first [...] span — tolerates an accidental ```json fence or a stray leading sentence.
68
+ const match = trimmed.match(/\[[\s\S]*\]/)
69
+ if (!match) {
70
+ throw new Error("no JSON array found in split proposal response")
71
+ }
72
+ return JSON.parse(match[0])
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Ask the model to propose a semantic split for one oversized issue. Returns
78
+ * an empty array (not an error) when the model determines the issue is
79
+ * genuinely one coherent piece of work — that is a valid, expected outcome,
80
+ * not a failure. Throws only on a real failure (LLM error, garbage response)
81
+ * so the caller can distinguish "model said don't split" from "couldn't ask."
82
+ */
83
+ export async function proposeSemanticSplit(
84
+ issue: SplitIssue,
85
+ llmClient: LlmClient,
86
+ model: string,
87
+ ): Promise<ProposedSubIssue[]> {
88
+ const response = await llmClient.createChatCompletion({
89
+ model,
90
+ messages: [
91
+ { role: "system", content: SPLIT_SYSTEM_PROMPT },
92
+ { role: "user", content: buildSplitUserPrompt(issue) },
93
+ ],
94
+ })
95
+ const text = response.message.content
96
+ if (typeof text !== "string" || text.trim() === "") {
97
+ throw new Error("split proposal returned an empty message")
98
+ }
99
+ const parsed = extractJsonArray(text)
100
+ if (!Array.isArray(parsed)) {
101
+ throw new Error("split proposal response was not a JSON array")
102
+ }
103
+ const proposals: ProposedSubIssue[] = []
104
+ for (const entry of parsed) {
105
+ if (
106
+ entry &&
107
+ typeof entry === "object" &&
108
+ typeof (entry as { title?: unknown }).title === "string" &&
109
+ typeof (entry as { body?: unknown }).body === "string"
110
+ ) {
111
+ proposals.push({ title: (entry as { title: string }).title, body: (entry as { body: string }).body })
112
+ }
113
+ }
114
+ if (proposals.length < MIN_SUBISSUES) {
115
+ return [] // model said (or effectively said, via malformed entries): don't split.
116
+ }
117
+ return proposals.slice(0, MAX_SUBISSUES)
118
+ }
119
+
120
+ /** One flagged issue's outcome, for a human-readable dispatch-time report. */
121
+ export interface AutoSplitOutcome {
122
+ number: number
123
+ title: string
124
+ outcome: "split" | "kept-as-is" | "failed"
125
+ /** Present when outcome is "split": the newly filed sub-issues. */
126
+ created?: Array<{ number: number; title: string; url: string }>
127
+ /** Present when outcome is "failed": why the split attempt didn't happen. */
128
+ reason?: string
129
+ }
130
+
131
+ /**
132
+ * For every issue flagged by the pre-flight size check, attempt a semantic
133
+ * split and file the result as real GitHub issues, replacing the oversized
134
+ * issue in the returned list. Fails open per-issue: any single issue's
135
+ * failure (LLM error, filing error) falls back to keeping that issue
136
+ * unchanged and dispatching it as-is — one bad split must never abort the
137
+ * whole round or leave OTHER issues un-split.
138
+ *
139
+ * `proposeSplit`/`createIssue`/`closeParent` are injected (not hardcoded to
140
+ * OpenRouter/gh) so this is unit-testable without network access — the real
141
+ * orchestrate command wires them to proposeSemanticSplit/createGhIssue/a
142
+ * `gh issue close --comment` call.
143
+ */
144
+ export async function autoSplitOversizedIssues(
145
+ issues: SplitIssue[],
146
+ warnings: IssueSizeWarning[],
147
+ deps: {
148
+ proposeSplit: (issue: SplitIssue) => Promise<ProposedSubIssue[]>
149
+ createIssue: (issue: SplitIssue) => { number: number; url: string }
150
+ closeParent: (issueNumber: number, comment: string) => void
151
+ },
152
+ ): Promise<{ issues: SplitIssue[]; outcomes: AutoSplitOutcome[] }> {
153
+ const flagged = new Set(warnings.map((w) => w.number))
154
+ const outcomes: AutoSplitOutcome[] = []
155
+ const next: SplitIssue[] = []
156
+
157
+ for (const issue of issues) {
158
+ if (!flagged.has(issue.number)) {
159
+ next.push(issue)
160
+ continue
161
+ }
162
+ let proposals: ProposedSubIssue[]
163
+ try {
164
+ proposals = await deps.proposeSplit(issue)
165
+ } catch (err) {
166
+ outcomes.push({
167
+ number: issue.number,
168
+ title: issue.title,
169
+ outcome: "failed",
170
+ reason: err instanceof Error ? err.message : String(err),
171
+ })
172
+ next.push(issue)
173
+ continue
174
+ }
175
+ if (proposals.length < MIN_SUBISSUES) {
176
+ outcomes.push({ number: issue.number, title: issue.title, outcome: "kept-as-is" })
177
+ next.push(issue)
178
+ continue
179
+ }
180
+ try {
181
+ const created: Array<{ number: number; title: string; url: string }> = []
182
+ for (const p of proposals) {
183
+ const real = deps.createIssue({ number: -1, title: p.title, body: `${p.body}\n\n---\nSplit from #${issue.number}.` })
184
+ created.push({ number: real.number, title: p.title, url: real.url })
185
+ next.push({ number: real.number, title: p.title, body: p.body })
186
+ }
187
+ deps.closeParent(
188
+ issue.number,
189
+ `Auto-split into ${created.length} sub-issue(s) by headlesscode's pre-flight size check (issue #53) — ` +
190
+ `the body read like independent pieces of work, so each is dispatched separately instead of risking an ` +
191
+ `iteration-cap/budget burn on one worker:\n\n` +
192
+ created.map((c) => `- #${c.number}: ${c.title}`).join("\n"),
193
+ )
194
+ outcomes.push({ number: issue.number, title: issue.title, outcome: "split", created })
195
+ } catch (err) {
196
+ outcomes.push({
197
+ number: issue.number,
198
+ title: issue.title,
199
+ outcome: "failed",
200
+ reason: err instanceof Error ? err.message : String(err),
201
+ })
202
+ next.push(issue)
203
+ }
204
+ }
205
+ return { issues: next, outcomes }
206
+ }