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,296 @@
1
+ /**
2
+ * Issue-splitting heuristics — deterministic TS port.
3
+ *
4
+ * SOURCE: the `multi-agent-orchestrator` custom mode's Step 2 "Decide the
5
+ * split". The four
6
+ * heuristics are reused unchanged in spirit:
7
+ *
8
+ * 1. Isolate anything touching a hot path alone (request handling,
9
+ * database sessions/migrations, auth, encryption, anything the
10
+ * codebase's own conventions flag as high blast radius) — never
11
+ * bundle a hot-path change with unrelated work, and never put two
12
+ * independent hot-path changes in the same worktree if avoidable.
13
+ * 2. Group same-shape mechanical work together (e.g. multiple "split
14
+ * this file" issues, multiple "add test coverage" issues) — they
15
+ * share a risk profile and review checklist, and batching them
16
+ * means one worker/PR/review cycle instead of several.
17
+ * 3. Never let a worktree exceed roughly a day of sequential work for
18
+ * one agent — if a single issue is enormous, it can be its own
19
+ * worktree; don't pad it with more work.
20
+ * 4. Prefer 2-4 worktrees over 5+ in most cases.
21
+ *
22
+ * This module turns those judgment calls into a small deterministic
23
+ * algorithm so the same decisions can be made headlessly, re-verified in
24
+ * unit tests, and printed by `orchestrate --dry-run` before anything is
25
+ * spawned. It is pure (no fs, no network) and fully unit-testable.
26
+ *
27
+ * Output naming follows the task-file convention:
28
+ * plans/parallel-tasks/<name>-issue<numbers>.md
29
+ * e.g. `w1-issue27.md`, `w2-issue29-36.md`.
30
+ */
31
+
32
+ /** One open GitHub issue (`gh issue view --json ...` shape). */
33
+ export interface SplitIssue {
34
+ number: number
35
+ title: string
36
+ body?: string
37
+ }
38
+
39
+ /** A proposed worktree group, one harness worker will own it. */
40
+ export interface WorktreeSpec {
41
+ /** Worktree name, e.g. `w1`. */
42
+ name: string
43
+ /** Issue numbers assigned to this worktree, ascending. */
44
+ issues: number[]
45
+ /** Task file name under plans/parallel-tasks/ (conventional naming). */
46
+ taskFile: string
47
+ }
48
+
49
+ /** Hot-path keywords from heuristic 1 (matched against title + body). */
50
+ const HOT_PATH_RE = /(request|handler|migration|database|session|auth|encryption|crypto|password|token)/i
51
+
52
+ /**
53
+ * Same-shape mechanical-work patterns from heuristic 2 (matched against the
54
+ * title). The key is the "shape" used to group issues; all issues whose title
55
+ * matches the same pattern share a risk profile + review checklist.
56
+ */
57
+ const SAME_SHAPE_PATTERNS: Array<{ key: string; re: RegExp }> = [
58
+ // "split" + "decompose" are the same mechanical shape — grouped
59
+ // #29 "Split the 10 multi-class files" with #36 "Decompose remaining 25
60
+ // files" in one worktree (w2-issue29-36.md) for exactly this reason.
61
+ { key: "split", re: /(split|decompose)/i },
62
+ { key: "coverage", re: /coverage/i },
63
+ { key: "test", re: /test/i },
64
+ { key: "docs", re: /docs/i },
65
+ { key: "refactor", re: /refactor/i },
66
+ ]
67
+
68
+ /** Issues with no recognized mechanical shape land here. */
69
+ const GENERIC_KEY = "generic"
70
+
71
+ /** Max issues in a generic (non-same-shape) group — heuristic 3 (~1 day). */
72
+ export const GENERIC_GROUP_CAP = 2
73
+
74
+ /** Max issues in a same-shape group — mechanical batches can be larger. */
75
+ export const SAME_SHAPE_GROUP_CAP = 3
76
+
77
+ /** Prefer at most this many groups (heuristic 4: 2-4, never 5+). */
78
+ export const MAX_GROUPS = 4
79
+
80
+ function isHotPath(issue: SplitIssue): boolean {
81
+ const haystack = `${issue.title}\n${issue.body ?? ""}`
82
+ return HOT_PATH_RE.test(haystack)
83
+ }
84
+
85
+ /**
86
+ * The mechanical "shape" of an issue (heuristic 2's grouping key): the first
87
+ * same-shape pattern matched against the TITLE (split/coverage/test/docs/
88
+ * refactor), or "generic" when none matches. Pure title-based — the same key
89
+ * used to batch issues into worktrees. Exported because it doubles as the
90
+ * similarity signal for cost estimation (cost-estimate.ts, issue #16): a NEW
91
+ * issue whose shape matches past ones inherits their recorded cost profile.
92
+ */
93
+ export function shapeOf(issue: SplitIssue): string {
94
+ for (const { key, re } of SAME_SHAPE_PATTERNS) {
95
+ if (re.test(issue.title)) {
96
+ return key
97
+ }
98
+ }
99
+ return GENERIC_KEY
100
+ }
101
+
102
+ /**
103
+ * The full shape taxonomy used for cost estimation: "hot" when the issue
104
+ * touches a hot path (title OR body — the same scan that isolates hot-path
105
+ * issues into their own worktree), else shapeOf's mechanical shape. A hot
106
+ * issue keeps its hot label even when its title also matches a mechanical
107
+ * pattern: hot-path work has its own risk/cost profile, and split.ts treats
108
+ * it as its own category for isolation purposes.
109
+ */
110
+ export function issueShape(issue: SplitIssue): string {
111
+ return isHotPath(issue) ? "hot" : shapeOf(issue)
112
+ }
113
+
114
+ /**
115
+ * Pre-flight issue-size check (issue #53). The two worst cost/iteration
116
+ * outliers this repo has seen all
117
+ * share one trait: the issue body asks for several substantial, largely
118
+ * independent pieces of work in a single shot. This is the crude, free,
119
+ * deterministic signal for that — the issue text itself explicitly asks for
120
+ * "does the issue body have 3+ top-level numbered/bulleted sections" as an
121
+ * acceptable first cut. Warning-only by design: a crude heuristic can false-
122
+ * positive, so it never aborts a round, only warns loudly before dispatch.
123
+ */
124
+
125
+ /** Flag an issue whose body reads like at least this many independent pieces. */
126
+ export const ISSUE_SIZE_WARN_THRESHOLD = 3
127
+
128
+ /** An issue flagged by the pre-flight size check, with the detected count. */
129
+ export interface IssueSizeWarning {
130
+ number: number
131
+ title: string
132
+ /** Number of top-level numbered/bulleted sections detected in the body. */
133
+ sections: number
134
+ }
135
+
136
+ /**
137
+ * Count top-level numbered/bulleted list items in a markdown body — the crude
138
+ * "N independent pieces" signal. Top-level = at most 3 leading spaces (the
139
+ * CommonMark container rule); nested list items are indented deeper and are
140
+ * deliberately excluded. Also counts headings carrying an explicit ordinal
141
+ * ("## 1. Client session cards"). Pure line scan: prose without list markers
142
+ * scores 0, so inline numbers ("Python 3.10", "$1.50", "#17") never
143
+ * false-positive.
144
+ */
145
+ export function topLevelSectionCount(body: string | undefined): number {
146
+ if (!body) {
147
+ return 0
148
+ }
149
+ let count = 0
150
+ for (const line of body.split("\n")) {
151
+ if (
152
+ /^ {0,3}\d{1,3}[.)]\s+\S/.test(line) ||
153
+ /^ {0,3}[-*+]\s+\S/.test(line) ||
154
+ /^#{1,6} {0,3}\d{1,3}[.)]?\s+\S/.test(line)
155
+ ) {
156
+ count++
157
+ }
158
+ }
159
+ return count
160
+ }
161
+
162
+ /**
163
+ * Issues whose body reads like `ISSUE_SIZE_WARN_THRESHOLD`+ independent pieces
164
+ * of work — the exact shape that burned iteration caps and budget on real
165
+ * rounds before dispatch-time detection existed.
166
+ */
167
+ export function issueSizeWarnings(issues: SplitIssue[]): IssueSizeWarning[] {
168
+ const warnings: IssueSizeWarning[] = []
169
+ for (const issue of issues) {
170
+ const sections = topLevelSectionCount(issue.body)
171
+ if (sections >= ISSUE_SIZE_WARN_THRESHOLD) {
172
+ warnings.push({ number: issue.number, title: issue.title, sections })
173
+ }
174
+ }
175
+ return warnings
176
+ }
177
+
178
+ function capFor(shape: string): number {
179
+ return shape === GENERIC_KEY ? GENERIC_GROUP_CAP : SAME_SHAPE_GROUP_CAP
180
+ }
181
+
182
+ /** Internal group shape used while deciding; not part of the public spec. */
183
+ interface InternalGroup {
184
+ shape: string
185
+ issues: number[]
186
+ }
187
+
188
+ /** Sort issue-number lists ascending (stable, deterministic). */
189
+ function sortedIssues(numbers: number[]): number[] {
190
+ return [...numbers].sort((a, b) => a - b)
191
+ }
192
+
193
+ function minIssue(group: InternalGroup): number {
194
+ return group.issues.length > 0 ? Math.min(...group.issues) : Number.MAX_SAFE_INTEGER
195
+ }
196
+
197
+ /**
198
+ * Split issues into worktree groups using the orchestration heuristics.
199
+ *
200
+ * - Every hot-path issue is isolated alone (heuristic 1).
201
+ * - Non-hot-path issues are batched by mechanical shape, capped at
202
+ * `capFor(shape)` issues per group (heuristics 2 + 3).
203
+ * - If that yields more than `MAX_GROUPS` groups, the smallest groups are
204
+ * merged (same-shape pairs first, then the smallest remaining) until the
205
+ * round fits in 2-4 worktrees (heuristic 4).
206
+ *
207
+ * @param issues issues to split (order-independent; output is sorted)
208
+ * @param opts.occupiedNames worktree names already in use in the TARGET repo
209
+ * (e.g. a still-running round's `.worktrees/w1`) — naming skips over
210
+ * these instead of colliding, so a second `orchestrate` invocation
211
+ * against the same repo can run alongside an in-flight round instead
212
+ * of failing the pre-spawn stale-worktree check. See cli.ts, which
213
+ * populates this from the repo's actual `.worktrees/` listing.
214
+ * @returns worktree specs in ascending min-issue order, named around the
215
+ * first free wN slots with convention-named task files
216
+ */
217
+ export function splitIssues(
218
+ issues: SplitIssue[],
219
+ opts: { occupiedNames?: ReadonlySet<string> } = {},
220
+ ): WorktreeSpec[] {
221
+ if (issues.length === 0) {
222
+ return []
223
+ }
224
+
225
+ const hot: SplitIssue[] = []
226
+ const rest: SplitIssue[] = []
227
+ for (const issue of issues) {
228
+ ;(isHotPath(issue) ? hot : rest).push(issue)
229
+ }
230
+ hot.sort((a, b) => a.number - b.number)
231
+ rest.sort((a, b) => a.number - b.number)
232
+
233
+ // Heuristic 1: each hot-path issue alone.
234
+ const groups: InternalGroup[] = hot.map((issue) => ({ shape: "hot", issues: [issue.number] }))
235
+
236
+ // Heuristics 2 + 3: batch same-shape work, cap non-same-shape at ~2.
237
+ const buckets = new Map<string, InternalGroup[]>()
238
+ for (const issue of rest) {
239
+ const shape = shapeOf(issue)
240
+ const list = buckets.get(shape) ?? []
241
+ let bucket = list[list.length - 1]
242
+ if (!bucket || bucket.issues.length >= capFor(shape)) {
243
+ bucket = { shape, issues: [] }
244
+ list.push(bucket)
245
+ buckets.set(shape, list)
246
+ }
247
+ bucket.issues.push(issue.number)
248
+ }
249
+ for (const list of buckets.values()) {
250
+ groups.push(...list)
251
+ }
252
+
253
+ // Deterministic group ordering by smallest issue number.
254
+ groups.sort((a, b) => minIssue(a) - minIssue(b))
255
+
256
+ // Heuristic 4: never exceed MAX_GROUPS. Merge smallest groups, preferring
257
+ // same-shape pairs so merged work still shares one review checklist.
258
+ while (groups.length > MAX_GROUPS) {
259
+ groups.sort((a, b) => {
260
+ const bySize = a.issues.length - b.issues.length
261
+ if (bySize !== 0) {
262
+ return bySize
263
+ }
264
+ return minIssue(a) - minIssue(b)
265
+ })
266
+ const smallest = groups[0]
267
+ const sameShape = groups
268
+ .slice(1)
269
+ .find((g) => g.shape === smallest.shape && g.shape !== "hot")
270
+ const target = sameShape ?? groups[1]
271
+ target.issues = sortedIssues([...target.issues, ...smallest.issues])
272
+ groups.splice(0, 1)
273
+ }
274
+
275
+ // Final sort by min issue number, then assign names + task files. Skips
276
+ // over any name already reserved via opts.occupiedNames (a still-running
277
+ // round in the same repo) — reserves each assigned name as it goes so two
278
+ // groups from THIS call never double-book the same slot either.
279
+ groups.sort((a, b) => minIssue(a) - minIssue(b))
280
+ const reserved = new Set(opts.occupiedNames ?? [])
281
+ let nextSlot = 1
282
+ return groups.map((group) => {
283
+ while (reserved.has(`w${nextSlot}`)) {
284
+ nextSlot++
285
+ }
286
+ const name = `w${nextSlot}`
287
+ reserved.add(name)
288
+ nextSlot++
289
+ const issues = sortedIssues(group.issues)
290
+ return {
291
+ name,
292
+ issues,
293
+ taskFile: `${name}-issue${issues.join("-")}.md`,
294
+ }
295
+ })
296
+ }