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,411 @@
1
+ /**
2
+ * Stage-isolated pipeline sequencing (issue #148).
3
+ *
4
+ * Generalizes the reviewer pattern (src/orchestrator/reviewer.ts) from ONE
5
+ * stage — review — to the pipeline stages that need it: research (produce a
6
+ * finding doc) and filing (turn that doc into a real GitHub issue). Each
7
+ * stage runs as a genuinely FRESH `HeadlessSession` (in-process, own
8
+ * context, own mode, own executor) that takes the PRIOR STAGE'S ARTIFACT as
9
+ * input — never the prior stage's raw conversation — exactly as
10
+ * `runReview`/`parseReviewResult` already do for review and `--plan-first`
11
+ * does for planning.
12
+ *
13
+ * This is deliberately NOT the per-worker subprocess spawn path
14
+ * (`run-worker.sh`): those already give process isolation between worktrees.
15
+ * This layer is for the stages that run IN-PROCESS today as one continuous
16
+ * session (the top-level explore→file→implement→review pipeline), so each
17
+ * one gets the same fresh-context treatment review already has.
18
+ *
19
+ * Design notes:
20
+ * - The research stage binds `requireArtifactPathPattern` /
21
+ * `requireArtifactMinCitations` / `requireArtifactSections` BY DEFAULT —
22
+ * the CLI flags exist (src/cli.ts) but nothing binds them per-mode; a
23
+ * researcher session must not complete without a real, well-cited doc on
24
+ * disk at the known path.
25
+ * - The filing stage's task input is built from the research doc's CONTENT
26
+ * read off disk (not the research session's conversation), and its result
27
+ * is parsed deterministically (mirroring `parseReviewResult`) into the
28
+ * real issue number(s) `gh issue create` returned.
29
+ */
30
+
31
+ import * as fsSync from "node:fs"
32
+ import * as fsp from "node:fs/promises"
33
+ import * as path from "node:path"
34
+
35
+ import { HeadlessSession } from "../engine/loop.js"
36
+ import { Logger } from "../engine/logger.js"
37
+ import { OpenRouterClient } from "../llm/openrouter.js"
38
+ import { OllamaClient } from "../llm/ollama.js"
39
+ import { loadCustomModes, selectToolsForMode } from "../engine/prompt.js"
40
+ import { createHeadlessExecutor } from "../tools/executor.js"
41
+ import { resolvePerModeEnv } from "../cli.js"
42
+ import type { LlmClient, SessionResult } from "../engine/types.js"
43
+
44
+ /** Default path pattern for a research stage's artifact (relative to workspace). */
45
+ export const DEFAULT_RESEARCH_ARTIFACT_PATTERN = "research/*.md"
46
+
47
+ /** Default minimum real file:line citations the research doc must contain. */
48
+ export const DEFAULT_RESEARCH_MIN_CITATIONS = 3
49
+
50
+ /** Default required section headings in the research doc. */
51
+ export const DEFAULT_RESEARCH_SECTIONS = ["Finding", "What to build", "What NOT to do", "How to verify"]
52
+
53
+ /** Mode slug the research stage runs (defined in .roomodes). */
54
+ export const RESEARCH_MODE = "researcher"
55
+
56
+ /** Mode slug the filing stage runs (defined in .roomodes). */
57
+ export const FILER_MODE = "issue-filer"
58
+
59
+ /** Max iterations for a stage session (research/filing are bounded, short). */
60
+ export const STAGE_MAX_ITERATIONS = 60
61
+
62
+ // ─── Types ──────────────────────────────────────────────────────────────────
63
+
64
+ export interface PipelineStageOptions {
65
+ /** The worktree/repo the stage runs against. */
66
+ workspaceRoot: string
67
+ /** Mode slug for the stage session (default: stage's own). */
68
+ mode?: string
69
+ /** Model id (default: env OPENROUTER_MODEL / client default). */
70
+ model?: string
71
+ /** LLM client; inject a fake in tests (default: OpenRouterClient). */
72
+ llmClient?: LlmClient
73
+ /** Per-stage iteration cap (default: STAGE_MAX_ITERATIONS). */
74
+ maxIterations?: number
75
+ }
76
+
77
+ export interface ResearchStageOptions extends PipelineStageOptions {
78
+ /** Task text for the research session (default: built from the workspace). */
79
+ taskText?: string
80
+ /**
81
+ * Glob (relative to workspaceRoot) that the research doc must match on
82
+ * disk at completion. Bound by default — the research stage is the one
83
+ * that needs the artifact gate as a structural guarantee, not a
84
+ * per-invocation flag.
85
+ */
86
+ artifactPathPattern?: string
87
+ /** Minimum real file:line citations the doc must contain. */
88
+ minCitations?: number
89
+ /** Required section headings (case-insensitive substring match). */
90
+ sections?: string[]
91
+ }
92
+
93
+ export interface ResearchStageResult {
94
+ /** Path (absolute) of the research doc on disk, when produced. */
95
+ artifactPath?: string
96
+ /** Status: ok = doc produced + artifact gate passed; error = session/artifact failure. */
97
+ status: "ok" | "error"
98
+ /** The session's final summary. */
99
+ summary: string
100
+ }
101
+
102
+ export interface FilingStageOptions extends PipelineStageOptions {
103
+ /** Task text for the filing session (default: built from the research doc). */
104
+ taskText?: string
105
+ /**
106
+ * Path of the research doc whose CONTENT becomes the filing task input
107
+ * (read off disk — never the research session's conversation).
108
+ */
109
+ researchArtifactPath?: string
110
+ }
111
+
112
+ export interface FilingStageResult {
113
+ /** Real GitHub issue number(s) parsed from the session's completion. */
114
+ issueNumbers: number[]
115
+ /** Status: ok = at least one issue number parsed; error = none found. */
116
+ status: "ok" | "error"
117
+ /** The session's final summary. */
118
+ summary: string
119
+ }
120
+
121
+ // ─── LLM client resolution (mirrors reviewer.ts) ─────────────────────────────
122
+
123
+ function resolveStageClient(mode: string, model?: string, llmClient?: LlmClient): LlmClient {
124
+ if (llmClient) {
125
+ return llmClient
126
+ }
127
+ const useLocalBackend =
128
+ process.env.HEADLESSCODE_CODE_MODE_BACKEND === "ollama" &&
129
+ (process.env.HEADLESSCODE_LOCAL_BACKEND_MODES ?? "code")
130
+ .split(",")
131
+ .map((s) => s.trim())
132
+ .filter(Boolean)
133
+ .includes(mode)
134
+ if (useLocalBackend) {
135
+ return new OllamaClient({
136
+ baseUrl: resolvePerModeEnv("HEADLESSCODE_OLLAMA_URL", mode),
137
+ defaultModel: resolvePerModeEnv("HEADLESSCODE_CODE_MODE_MODEL", mode) ?? model,
138
+ // OllamaClient has its own independent abort timer (ollama.ts's
139
+ // DEFAULT_OLLAMA_TIMEOUT_MS, 300s) — see cli.ts's
140
+ // LOCAL_LLM_TIMEOUT_MS doc comment (verified live 2026-08-28)
141
+ // for why local sessions need this raised past the shim's own
142
+ // 600s upstream patience.
143
+ timeoutMs: 630_000,
144
+ })
145
+ }
146
+ return new OpenRouterClient({ apiKey: process.env.HEADLESSCODE_OPENROUTER_API_KEY, defaultModel: model })
147
+ }
148
+
149
+ function buildLogger(workspaceRoot: string, label: string): Logger {
150
+ const logFilePath = path.join(workspaceRoot, `${label}.log`)
151
+ fsSync.appendFileSync(
152
+ logFilePath,
153
+ `\n===== headlesscode ${label} stage start: ${new Date().toISOString()} =====\n`,
154
+ "utf-8",
155
+ )
156
+ return new Logger({ level: "info", filePath: logFilePath })
157
+ }
158
+
159
+ // ─── Research stage ──────────────────────────────────────────────────────────
160
+
161
+ export function defaultResearchTaskText(workspaceRoot: string, artifactPathPattern: string): string {
162
+ return (
163
+ `Research the codebase in this workspace exactly as your operating procedure instructs, and ` +
164
+ `produce ONE written research document. Your task text names an exact output path — that path ` +
165
+ `is your deliverable, not a suggestion.\n\n` +
166
+ `Write the document to a path matching \`${artifactPathPattern}\` (relative to this workspace ` +
167
+ `root). The document MUST contain:\n` +
168
+ `- at least one clearly-labeled "Finding" section;\n` +
169
+ `- a "What to build" section proposing the concrete change;\n` +
170
+ `- a "What NOT to do" section bounding scope;\n` +
171
+ `- a "How to verify" section with real checkable steps.\n\n` +
172
+ `Ground every claim in real file:line citations from files you actually opened ` +
173
+ `(e.g. \`src/engine/loop.ts:123\`) — never invent a citation. Any scratch you need goes in ` +
174
+ `\`.headlesscode/scratch/\` inside this workspace — NEVER write to \`/tmp\` or any other path ` +
175
+ `outside the workspace.\n\n` +
176
+ `You do NOT implement anything, and you do NOT decide there is a different, better task to ` +
177
+ `work on. When the document exists on disk with real content, call attempt_completion naming ` +
178
+ `the path you wrote.`
179
+ )
180
+ }
181
+
182
+ /**
183
+ * Run the research stage: a fresh, bounded `researcher`-mode session that
184
+ * must produce a real, well-cited `.md` doc on disk (artifact gate bound by
185
+ * default). Returns the artifact path when the gate passed.
186
+ */
187
+ export async function runResearchStage(options: ResearchStageOptions): Promise<ResearchStageResult> {
188
+ const {
189
+ workspaceRoot,
190
+ mode = RESEARCH_MODE,
191
+ model,
192
+ llmClient,
193
+ maxIterations = STAGE_MAX_ITERATIONS,
194
+ artifactPathPattern = DEFAULT_RESEARCH_ARTIFACT_PATTERN,
195
+ minCitations = DEFAULT_RESEARCH_MIN_CITATIONS,
196
+ sections = DEFAULT_RESEARCH_SECTIONS,
197
+ } = options
198
+
199
+ const customModes = await loadCustomModes(workspaceRoot)
200
+ const tools = selectToolsForMode(mode, customModes)
201
+ // The researcher writes a markdown doc — it needs the write tools, but
202
+ // the mode's own `edit` group already restricts to `*.md`; the executor
203
+ // is the full headless one (write_to_file registered) so the artifact
204
+ // can actually be created.
205
+ const executor = createHeadlessExecutor(workspaceRoot)
206
+
207
+ const session = new HeadlessSession({
208
+ workspaceRoot,
209
+ mode,
210
+ model,
211
+ taskText: options.taskText ?? defaultResearchTaskText(workspaceRoot, artifactPathPattern),
212
+ maxIterations,
213
+ tools,
214
+ executor,
215
+ customModes,
216
+ llmClient: resolveStageClient(mode, model, llmClient),
217
+ logger: buildLogger(workspaceRoot, "research"),
218
+ // Structural guarantee: the research stage REQUIRES a real artifact
219
+ // on disk before attempt_completion is accepted.
220
+ requireArtifactPathPattern: artifactPathPattern,
221
+ requireArtifactMinCitations: minCitations,
222
+ requireArtifactSections: sections,
223
+ })
224
+
225
+ const result: SessionResult = await session.run()
226
+ if (result.status !== "success" || result.result === undefined) {
227
+ return {
228
+ status: "error",
229
+ summary: `Research session failed: ${result.error ?? "unknown error"}`,
230
+ }
231
+ }
232
+
233
+ // The artifact gate already confirmed a matching file exists on disk at
234
+ // completion; resolve its real path for the caller (first match).
235
+ const artifactPath = await firstMatchingArtifact(workspaceRoot, artifactPathPattern)
236
+ if (!artifactPath) {
237
+ return {
238
+ status: "error",
239
+ summary: "Research session completed but no artifact matching the required pattern was found on disk",
240
+ }
241
+ }
242
+ return { status: "ok", artifactPath, summary: result.result }
243
+ }
244
+
245
+ // ─── Filing stage ────────────────────────────────────────────────────────────
246
+
247
+ export function defaultFilingTaskText(researchArtifactPath: string, researchContent: string): string {
248
+ return (
249
+ `File REAL GitHub issue(s) from the research finding below, exactly as your operating ` +
250
+ `procedure instructs. The finding was produced by an earlier research stage and is ` +
251
+ `already well-scoped — you do NOT investigate from scratch.\n\n` +
252
+ `Use \`gh issue create\` to file the issue(s) in the repo that owns this workspace. ` +
253
+ `Confirm the repo with \`gh repo view --json nameWithOwner -q .nameWithOwner\` first, ` +
254
+ `check for duplicates, and report EVERY issue URL \`gh issue create\` returned — never ` +
255
+ `claim an issue was filed without the real URL.\n\n` +
256
+ `Your attempt_completion result MUST end with a line of the exact form:\n` +
257
+ `ISSUES: <number1>, <number2>, ...\n` +
258
+ `listing every issue number you actually filed. Nothing else on that line — this is the ` +
259
+ `only line the orchestrator parses.\n\n` +
260
+ `===== Research finding =====\n${researchContent}`
261
+ )
262
+ }
263
+
264
+ /**
265
+ * Run the filing stage: a fresh `issue-filer`-mode session whose task input
266
+ * is built from the RESEARCH DOC'S CONTENT read off disk (never the research
267
+ * session's conversation). Parses the real issue number(s) from the
268
+ * completion deterministically.
269
+ */
270
+ export async function runFilingStage(options: FilingStageOptions): Promise<FilingStageResult> {
271
+ const {
272
+ workspaceRoot,
273
+ mode = FILER_MODE,
274
+ model,
275
+ llmClient,
276
+ maxIterations = STAGE_MAX_ITERATIONS,
277
+ } = options
278
+
279
+ let taskText = options.taskText
280
+ if (taskText === undefined) {
281
+ if (!options.researchArtifactPath) {
282
+ return {
283
+ status: "error",
284
+ summary: "Filing stage requires either taskText or researchArtifactPath",
285
+ issueNumbers: [],
286
+ }
287
+ }
288
+ let content = ""
289
+ try {
290
+ content = await fsp.readFile(options.researchArtifactPath, "utf-8")
291
+ } catch (err) {
292
+ return {
293
+ status: "error",
294
+ summary: `Filing stage could not read research artifact: ${err instanceof Error ? err.message : String(err)}`,
295
+ issueNumbers: [],
296
+ }
297
+ }
298
+ taskText = defaultFilingTaskText(options.researchArtifactPath, content)
299
+ }
300
+
301
+ const customModes = await loadCustomModes(workspaceRoot)
302
+ const tools = selectToolsForMode(mode, customModes)
303
+ const executor = createHeadlessExecutor(workspaceRoot)
304
+
305
+ const session = new HeadlessSession({
306
+ workspaceRoot,
307
+ mode,
308
+ model,
309
+ taskText,
310
+ maxIterations,
311
+ tools,
312
+ executor,
313
+ customModes,
314
+ llmClient: resolveStageClient(mode, model, llmClient),
315
+ logger: buildLogger(workspaceRoot, "filer"),
316
+ })
317
+
318
+ const result: SessionResult = await session.run()
319
+ if (result.status !== "success" || result.result === undefined) {
320
+ return {
321
+ status: "error",
322
+ summary: `Filing session failed: ${result.error ?? "unknown error"}`,
323
+ issueNumbers: [],
324
+ }
325
+ }
326
+
327
+ return { ...parseFilingResult(result.result), summary: result.result }
328
+ }
329
+
330
+ /**
331
+ * Parse a filing session's completion into real issue number(s).
332
+ *
333
+ * Deterministic: looks for the required `ISSUES: <n1>, <n2>` line (exact
334
+ * match), then falls back to any `#\d+` references that look like the URLs
335
+ * `gh issue create` returns. Returns `status: "error"` (not a partial-ok)
336
+ * when NO issue number could be parsed — an unverifiable filing claim must
337
+ * not be trusted, mirroring the reviewer's fail-closed verdict.
338
+ */
339
+ export function parseFilingResult(text: string): { issueNumbers: number[]; status: "ok" | "error" } {
340
+ const summary = text.trim()
341
+
342
+ // Primary path: the required exact `ISSUES: n1, n2` line.
343
+ const structured = summary.match(/^ISSUES\s*:\s*([\d,\s]+)\s*$/im)
344
+ if (structured?.[1]) {
345
+ const numbers = structured[1]
346
+ .split(/[\s,]+/)
347
+ .map((s) => Number(s))
348
+ .filter((n) => Number.isInteger(n) && n > 0)
349
+ if (numbers.length > 0) {
350
+ return { issueNumbers: [...new Set(numbers)], status: "ok" }
351
+ }
352
+ }
353
+
354
+ // Fallback: any issue URL `gh issue create` returns looks like
355
+ // `https://github.com/<owner>/<repo>/issues/<n>` — pull the numbers from
356
+ // those, but ONLY from real URL-shaped references (a bare "#42" in prose
357
+ // is not proof of a filed issue).
358
+ const urlNumbers = [...summary.matchAll(/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)/gi)].map((m) =>
359
+ Number(m[1]),
360
+ )
361
+ const unique = [...new Set(urlNumbers)].filter((n) => Number.isInteger(n) && n > 0)
362
+ if (unique.length > 0) {
363
+ return { issueNumbers: unique, status: "ok" }
364
+ }
365
+
366
+ return { issueNumbers: [], status: "error" }
367
+ }
368
+
369
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
370
+
371
+ /**
372
+ * Return the absolute path of the first file matching a single-directory
373
+ * glob pattern (relative to workspaceRoot) that is real and non-empty.
374
+ * Mirrors the artifact gate's own `matchingArtifactFileStatus` semantics for
375
+ * the caller-facing result.
376
+ */
377
+ async function firstMatchingArtifact(workspaceRoot: string, pattern: string): Promise<string | undefined> {
378
+ const base = path.resolve(workspaceRoot)
379
+ const dir = path.posix.dirname(pattern)
380
+ const fileGlob = path.posix.basename(pattern)
381
+ const dirAbs = path.resolve(base, dir)
382
+ if (!dirAbs.startsWith(base + path.sep) && dirAbs !== base) {
383
+ return undefined
384
+ }
385
+ let entries: fsSync.Dirent[]
386
+ try {
387
+ entries = fsSync.readdirSync(dirAbs, { withFileTypes: true })
388
+ } catch {
389
+ return undefined
390
+ }
391
+ // Convert the glob's `*` into a regex (only `*` is used in practice).
392
+ const re = new RegExp(`^${fileGlob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`)
393
+ for (const entry of entries) {
394
+ if (!entry.isFile()) {
395
+ continue
396
+ }
397
+ if (!re.test(entry.name)) {
398
+ continue
399
+ }
400
+ const full = path.join(dirAbs, entry.name)
401
+ try {
402
+ const stat = fsSync.statSync(full)
403
+ if (stat.size > 0) {
404
+ return full
405
+ }
406
+ } catch {
407
+ // ignore unreadable
408
+ }
409
+ }
410
+ return undefined
411
+ }