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,47 @@
1
+ /**
2
+ * Per-session final-report persistence — `<workspaceRoot>/.headlesscode/reports/`.
3
+ *
4
+ * Mirrors the on-disk-layout modules idiom (src/engine/events.ts for the
5
+ * events feed, src/engine/usage.ts for usage): one module owns one layout.
6
+ *
7
+ * Issue #34: a review/QA/worker session's final `attempt_completion` report
8
+ * (the single most information-dense moment of any session) was never
9
+ * durably persisted in full — the events feed recorded no tool_call/tool_result
10
+ * pair for it (the loop intercepts attempt_completion before the tool-execution
11
+ * loop), and the orchestrator state file only kept a narrow parsed slice
12
+ * (`qa.evidence`, capped at 4000 chars, or the reviewer's findings list). A
13
+ * surprising verdict therefore required re-running the whole session just to
14
+ * see the reasoning that justified it. The loop writes the complete report
15
+ * here at completion time, so the full text is one file-read away, never a
16
+ * re-run away.
17
+ *
18
+ * The reports dir lives under `/.headlesscode/`, which is already gitignored,
19
+ * and like every other auxiliary write path this is deliberately non-fatal:
20
+ * callers wrap the write in try/catch and log a warning — a failed report
21
+ * write must never abort or corrupt the session that produced the report.
22
+ */
23
+
24
+ import * as fsp from "node:fs/promises"
25
+ import * as path from "node:path"
26
+
27
+ /** The reports dir for a workspace: `<workspaceRoot>/.headlesscode/reports`. */
28
+ export function reportsDir(workspaceRoot: string): string {
29
+ return path.join(workspaceRoot, ".headlesscode", "reports")
30
+ }
31
+
32
+ /** The report file path for one session: `<reportsDir>/<sessionId>.md`. */
33
+ export function reportFilePath(workspaceRoot: string, sessionId: string): string {
34
+ return path.join(reportsDir(workspaceRoot), `${sessionId}.md`)
35
+ }
36
+
37
+ /**
38
+ * Persist a session's complete final report as `<reportsDir>/<sessionId>.md`.
39
+ * Creates the dir if needed and writes the raw report text verbatim. Throws
40
+ * on failure (callers wrap this non-fatally — see the module doc comment).
41
+ */
42
+ export async function writeSessionReport(workspaceRoot: string, sessionId: string, reportText: string): Promise<string> {
43
+ const file = reportFilePath(workspaceRoot, sessionId)
44
+ await fsp.mkdir(path.dirname(file), { recursive: true })
45
+ await fsp.writeFile(file, reportText, "utf-8")
46
+ return file
47
+ }
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Stack detection + stack-specific rules splicing for the system prompt.
3
+ *
4
+ * headlesscode runs against ANY target codebase, so stack-specific guidance
5
+ * (a Python project's test step vs. a C++ project's) is keyed by detected
6
+ * stack rather than stuffed into the generic prompt template. This module is
7
+ * the detection + loading half of that:
8
+ *
9
+ * - `detectStacks(workspaceRoot)` — cheap, filesystem-only checks. A project
10
+ * can match MULTIPLE stacks at once (e.g. a FastAPI + React + PostgreSQL
11
+ * repo), and the checks follow the per-stack heuristics below.
12
+ * - `loadStackRules(workspaceRoot)` — for every detected stack (in canonical
13
+ * alphabetical order, so prompt content is reproducible across runs) reads
14
+ * that stack's rules.md from the central global set
15
+ * (`<store>/shared/stacks/<stack>/rules.md`, where `<store>` is
16
+ * `projectStoreRoot()`) and the optional project-local override
17
+ * (`<workspaceRoot>/.roo/rules-stack-<stack>/rules.md`). Both are additive,
18
+ * global first — the same precedence shape as the existing global+project
19
+ * rules splice in the vendored `addCustomInstructions` — and a missing file
20
+ * is never an error.
21
+ *
22
+ * Zero config = zero behavior change: a project matching no stacks (or stacks
23
+ * with no rules.md yet) produces empty content, exactly like the existing
24
+ * missing-file handling for `mode-models.json` / `.roomodes`. Stack CONTENT
25
+ * (the actual per-stack rules.md text) is deliberately out of scope here.
26
+ */
27
+
28
+ import { execFile } from "node:child_process"
29
+ import * as fsp from "node:fs/promises"
30
+ import * as path from "node:path"
31
+ import { promisify } from "node:util"
32
+
33
+ import { projectStoreRoot } from "../project-store.js"
34
+ import * as yaml from "yaml"
35
+
36
+ const execFileP = promisify(execFile)
37
+
38
+ /**
39
+ * Canonical stack names, alphabetical — this ordering is what makes the
40
+ * spliced prompt content deterministic.
41
+ */
42
+ export const STACK_NAMES = ["cpp", "fastapi", "javascript", "postgresql", "python", "react", "typescript"] as const
43
+
44
+ export type StackName = (typeof STACK_NAMES)[number]
45
+
46
+ /** Header of the section `buildSystemPrompt` appends when stack rules exist. */
47
+ export const STACK_RULES_HEADER = "STACK-SPECIFIC INSTRUCTIONS"
48
+
49
+ // ─── Storage conventions ────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Central (machine-global) stack rules directory:
53
+ * `<store>/shared/stacks/<stack>/rules.md`, where `<store>` is
54
+ * `projectStoreRoot()` — so the `$HEADLESSCODE_DATA_DIR` override is honored
55
+ * exactly like every other central-store consumer (checkpoints, settings,
56
+ * shared instructions, project data), and `scripts/sync-shared-rules.sh`
57
+ * installs to the same root the engine reads. Resolved at call time (not
58
+ * module load) so the override / sandboxed $HOME are picked up per call.
59
+ */
60
+ export function getCentralStackRulesDir(): string {
61
+ return path.join(projectStoreRoot(), "shared", "stacks")
62
+ }
63
+
64
+ /** Absolute path of a stack's central rules.md. */
65
+ export function getCentralStackRulesFile(stack: StackName): string {
66
+ return path.join(getCentralStackRulesDir(), stack, "rules.md")
67
+ }
68
+
69
+ /** Absolute path of a stack's project-local rules.md. */
70
+ export function getProjectStackRulesFile(workspaceRoot: string, stack: StackName): string {
71
+ return path.join(workspaceRoot, ".roo", `rules-stack-${stack}`, "rules.md")
72
+ }
73
+
74
+ // ─── Detection ──────────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Per-stack detection heuristics (starting point — stack content, not these
78
+ * signals, is what the per-stack child issues fill in):
79
+ *
80
+ * - typescript: `tsconfig.json`, or any project `.ts`/`.tsx` file
81
+ * - javascript: `package.json`
82
+ * - python: `pyproject.toml` / `requirements*.txt` / `setup.py`, or any
83
+ * project `.py` file
84
+ * - react: `react` in `package.json` `dependencies`
85
+ * - fastapi: `fastapi` in `pyproject.toml` / `requirements*.txt`
86
+ * - postgresql: a postgres driver (`psycopg`/`psycopg2`/`asyncpg`) in Python
87
+ * deps, OR a `postgres`-image service in `docker-compose*.yml`
88
+ * / `compose*.yml`, OR a `migrations/`/`alembic`-style dir
89
+ * - cpp: `CMakeLists.txt`, or any project `.cpp`/`.hpp`/`.cc`/`.hh`
90
+ * file
91
+ */
92
+
93
+ const PYTHON_DEP_STACK_PATTERNS: Array<[StackName, RegExp]> = [
94
+ ["fastapi", /\bfastapi\b/i],
95
+ ["postgresql", /\b(?:psycopg|psycopg2|asyncpg)\b/i],
96
+ ]
97
+
98
+ const TS_EXTENSIONS = new Set([".ts", ".tsx"])
99
+ const PY_EXTENSIONS = new Set([".py"])
100
+ const CPP_EXTENSIONS = new Set([".cpp", ".hpp", ".cc", ".hh"])
101
+
102
+ /** Directories skipped by the non-git fallback walk (mirrors codesearch/files.ts). */
103
+ const FALLBACK_EXCLUDES = new Set([
104
+ ".git",
105
+ ".gitignore",
106
+ ".headlesscode",
107
+ ".worktrees",
108
+ ".roo",
109
+ "node_modules",
110
+ "dist",
111
+ "build",
112
+ "out",
113
+ "coverage",
114
+ ".next",
115
+ ".nuxt",
116
+ "vendor",
117
+ "target",
118
+ "__pycache__",
119
+ "venv",
120
+ ".venv",
121
+ "env",
122
+ "Pods",
123
+ ".idea",
124
+ ".vscode",
125
+ ".parcel-cache",
126
+ ".pytest_cache",
127
+ ".terraform",
128
+ ".terragrunt-cache",
129
+ "bin",
130
+ "obj",
131
+ ])
132
+
133
+ async function readFileSafe(filePath: string): Promise<string | null> {
134
+ try {
135
+ return await fsp.readFile(filePath, "utf-8")
136
+ } catch {
137
+ return null
138
+ }
139
+ }
140
+
141
+ async function fileIsPresent(filePath: string): Promise<boolean> {
142
+ try {
143
+ return (await fsp.stat(filePath)).isFile()
144
+ } catch {
145
+ return false
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Git-tracked file list via `git ls-files --cached --others --exclude-standard`
151
+ * — tracked files PLUS untracked-but-not-ignored ones, with proper .gitignore
152
+ * semantics (the same convention as src/codesearch/files.ts, which documents
153
+ * why this beats a hand-rolled ignore matcher). Returns null when the
154
+ * workspace is not a git repo or git reports nothing, so callers fall back to
155
+ * a bounded walk.
156
+ */
157
+ async function gitTrackedFiles(root: string): Promise<string[] | null> {
158
+ try {
159
+ const { stdout } = await execFileP("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
160
+ cwd: root,
161
+ maxBuffer: 16 * 1024 * 1024,
162
+ })
163
+ const files = stdout
164
+ .split("\n")
165
+ .map((line) => line.trim())
166
+ .filter((line) => line.length > 0)
167
+ return files.length > 0 ? files : null
168
+ } catch {
169
+ return null
170
+ }
171
+ }
172
+
173
+ /** Bounded recursive walk used when the workspace is not a git repo. */
174
+ async function walkProjectFiles(root: string): Promise<string[]> {
175
+ const out: string[] = []
176
+ const walk = async (dir: string): Promise<void> => {
177
+ let entries
178
+ try {
179
+ entries = await fsp.readdir(dir, { withFileTypes: true })
180
+ } catch {
181
+ return
182
+ }
183
+ for (const entry of entries) {
184
+ const abs = path.join(dir, entry.name)
185
+ if (entry.isDirectory()) {
186
+ if (FALLBACK_EXCLUDES.has(entry.name)) {
187
+ continue
188
+ }
189
+ await walk(abs)
190
+ } else if (entry.isFile()) {
191
+ out.push(path.relative(root, abs).split(path.sep).join("/"))
192
+ }
193
+ }
194
+ }
195
+ await walk(root)
196
+ return out
197
+ }
198
+
199
+ async function listProjectFiles(root: string): Promise<string[]> {
200
+ const tracked = await gitTrackedFiles(root)
201
+ return tracked ?? (await walkProjectFiles(root))
202
+ }
203
+
204
+ interface ComposeService {
205
+ image?: unknown
206
+ }
207
+
208
+ /** True when any top-level `docker-compose*.yml`/`compose*.yml` has a `postgres`-image service. */
209
+ async function hasPostgresComposeService(root: string): Promise<boolean> {
210
+ let entries: string[]
211
+ try {
212
+ entries = await fsp.readdir(root)
213
+ } catch {
214
+ return false
215
+ }
216
+ for (const name of entries) {
217
+ if (!/^(?:docker-compose|compose).*\.ya?ml$/i.test(name)) {
218
+ continue
219
+ }
220
+ const raw = await readFileSafe(path.join(root, name))
221
+ if (!raw) {
222
+ continue
223
+ }
224
+ try {
225
+ const parsed = yaml.parse(raw) as { services?: Record<string, ComposeService> } | null
226
+ for (const service of Object.values(parsed?.services ?? {})) {
227
+ if (typeof service.image === "string" && /postgres/i.test(service.image)) {
228
+ return true
229
+ }
230
+ }
231
+ } catch {
232
+ // Unparseable compose file is no signal (non-fatal).
233
+ }
234
+ }
235
+ return false
236
+ }
237
+
238
+ // ─── Detection cache ─────────────────────────────────────────────────────────
239
+
240
+ /**
241
+ * Per-workspace in-process cache for detectStacks, keyed by the resolved root
242
+ * and invalidated by the root directory's own mtime: a stack signal can only
243
+ * appear when a top-level entry is added/removed/renamed, which bumps that
244
+ * mtime. Callers receive a copy so the cached Set is never mutated. mtimeMs
245
+ * (float, sub-ms precision) is the highest-resolution mtime a non-bigint stat
246
+ * exposes on this Node (mtimeNs needs { bigint: true }).
247
+ */
248
+ interface DetectStacksCacheEntry {
249
+ rootMtimeMs: number
250
+ stacks: Set<StackName>
251
+ }
252
+ const detectStacksCache = new Map<string, DetectStacksCacheEntry>()
253
+
254
+ /**
255
+ * Detect the stack(s) a target project uses. Cheap, filesystem-only, and a
256
+ * project can match MULTIPLE stacks at once. Never throws: a missing or
257
+ * unreadable workspace root simply yields an empty set.
258
+ */
259
+ export async function detectStacks(workspaceRoot: string): Promise<Set<StackName>> {
260
+ const root = path.resolve(workspaceRoot)
261
+
262
+ // Stat the root once and reuse it for both the cache check and the cache
263
+ // store. A failed stat (missing/unreadable root) means no caching — the
264
+ // full detection below still runs and still never throws.
265
+ let rootMtimeMs: number | undefined
266
+ try {
267
+ rootMtimeMs = (await fsp.stat(root)).mtimeMs
268
+ } catch {
269
+ rootMtimeMs = undefined
270
+ }
271
+ const cached = detectStacksCache.get(root)
272
+ if (cached !== undefined && rootMtimeMs !== undefined && rootMtimeMs === cached.rootMtimeMs) {
273
+ return new Set(cached.stacks)
274
+ }
275
+
276
+ const stacks = new Set<StackName>()
277
+
278
+ // ── Manifest checks (top-level, cheap stats/reads) ──
279
+ if (await fileIsPresent(path.join(root, "tsconfig.json"))) {
280
+ stacks.add("typescript")
281
+ }
282
+ if (await fileIsPresent(path.join(root, "CMakeLists.txt"))) {
283
+ stacks.add("cpp")
284
+ }
285
+
286
+ const pythonDepText: string[] = []
287
+ if (await fileIsPresent(path.join(root, "pyproject.toml"))) {
288
+ stacks.add("python")
289
+ const content = await readFileSafe(path.join(root, "pyproject.toml"))
290
+ if (content) {
291
+ pythonDepText.push(content)
292
+ }
293
+ }
294
+ if (await fileIsPresent(path.join(root, "setup.py"))) {
295
+ stacks.add("python")
296
+ }
297
+
298
+ let topLevelEntries: string[] = []
299
+ try {
300
+ topLevelEntries = await fsp.readdir(root)
301
+ } catch {
302
+ topLevelEntries = []
303
+ }
304
+ for (const name of topLevelEntries) {
305
+ if (/^requirements.*\.txt$/i.test(name)) {
306
+ stacks.add("python")
307
+ const content = await readFileSafe(path.join(root, name))
308
+ if (content) {
309
+ pythonDepText.push(content)
310
+ }
311
+ }
312
+ }
313
+
314
+ const packageJsonRaw = await readFileSafe(path.join(root, "package.json"))
315
+ if (packageJsonRaw !== null) {
316
+ stacks.add("javascript")
317
+ try {
318
+ const pkg = JSON.parse(packageJsonRaw) as { dependencies?: Record<string, unknown> }
319
+ if (typeof pkg.dependencies?.react === "string") {
320
+ stacks.add("react")
321
+ }
322
+ } catch {
323
+ // Malformed package.json still signals JavaScript; react is just not detected.
324
+ }
325
+ }
326
+
327
+ for (const content of pythonDepText) {
328
+ for (const [stack, pattern] of PYTHON_DEP_STACK_PATTERNS) {
329
+ if (pattern.test(content)) {
330
+ stacks.add(stack)
331
+ }
332
+ }
333
+ }
334
+
335
+ if (await hasPostgresComposeService(root)) {
336
+ stacks.add("postgresql")
337
+ }
338
+
339
+ // ── Extension scan over project files (git-tracked, or bounded walk) ──
340
+ for (const rel of await listProjectFiles(root)) {
341
+ const ext = path.extname(rel).toLowerCase()
342
+ if (TS_EXTENSIONS.has(ext)) {
343
+ stacks.add("typescript")
344
+ }
345
+ if (PY_EXTENSIONS.has(ext)) {
346
+ stacks.add("python")
347
+ }
348
+ if (CPP_EXTENSIONS.has(ext)) {
349
+ stacks.add("cpp")
350
+ }
351
+ const segments = rel.split("/")
352
+ if ((ext === ".sql" && segments.includes("migrations")) || segments.includes("alembic")) {
353
+ stacks.add("postgresql")
354
+ }
355
+ }
356
+
357
+ if (rootMtimeMs !== undefined) {
358
+ detectStacksCache.set(root, { rootMtimeMs, stacks: new Set(stacks) })
359
+ }
360
+ return stacks
361
+ }
362
+
363
+ // ─── Loading + splicing ─────────────────────────────────────────────────────
364
+
365
+ /** Result of `loadStackRules`: the content to splice plus what was detected. */
366
+ export interface LoadedStackRules {
367
+ /** Non-empty rules content, or "" when nothing matched (zero behavior change). */
368
+ content: string
369
+ /** Stacks detected for the workspace (used for the spliced section header). */
370
+ stacks: Set<StackName>
371
+ }
372
+
373
+ /**
374
+ * True when there is anywhere a stack rules.md could live: the central global
375
+ * stacks dir, or a project-local `.roo/rules-stack-*` dir. Used to skip
376
+ * detection entirely for zero-config sessions (detection is cheap, but a
377
+ * stat+readdir on the common no-rules path is cheaper still).
378
+ */
379
+ async function hasAnyStackRulesSource(root: string): Promise<boolean> {
380
+ try {
381
+ if ((await fsp.stat(getCentralStackRulesDir())).isDirectory()) {
382
+ return true
383
+ }
384
+ } catch {
385
+ // Central dir absent — fall through to the project-local check.
386
+ }
387
+ try {
388
+ const entries = await fsp.readdir(path.join(root, ".roo"))
389
+ return entries.some((name) => name.startsWith("rules-stack-"))
390
+ } catch {
391
+ return false
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Load stack-specific rules for a workspace: for every detected stack in
397
+ * canonical alphabetical order, concatenate the central rules.md and the
398
+ * project-local `.roo/rules-stack-<stack>/rules.md` (global first, project
399
+ * second — the same additive precedence as the existing rules splice).
400
+ * Returns empty content when nothing matches; never throws.
401
+ */
402
+ export async function loadStackRules(workspaceRoot: string): Promise<LoadedStackRules> {
403
+ const root = path.resolve(workspaceRoot)
404
+
405
+ if (!(await hasAnyStackRulesSource(root))) {
406
+ return { content: "", stacks: new Set<StackName>() }
407
+ }
408
+
409
+ const stacks = await detectStacks(root)
410
+ if (stacks.size === 0) {
411
+ return { content: "", stacks }
412
+ }
413
+
414
+ const sections: string[] = []
415
+ for (const stack of STACK_NAMES) {
416
+ if (!stacks.has(stack)) {
417
+ continue
418
+ }
419
+ const files: Array<{ label: string; content: string }> = []
420
+ const central = await readFileSafe(getCentralStackRulesFile(stack))
421
+ if (central?.trim()) {
422
+ files.push({ label: getCentralStackRulesFile(stack), content: central.trim() })
423
+ }
424
+ const project = await readFileSafe(getProjectStackRulesFile(root, stack))
425
+ if (project?.trim()) {
426
+ files.push({ label: getProjectStackRulesFile(root, stack), content: project.trim() })
427
+ }
428
+ if (files.length > 0) {
429
+ sections.push(files.map((f) => `# Rules from ${f.label}:\n${f.content}`).join("\n\n"))
430
+ }
431
+ }
432
+
433
+ return { content: sections.join("\n\n"), stacks }
434
+ }
435
+
436
+ /**
437
+ * Append a well-delimited stack-rules section to a built system prompt. Only
438
+ * called when `stackRules.content` is non-empty, so zero-config builds stay
439
+ * byte-identical to before.
440
+ */
441
+ export function appendStackRulesSection(
442
+ prompt: string,
443
+ stackRules: string,
444
+ detectedStacks: ReadonlySet<StackName>,
445
+ ): string {
446
+ const stackList = STACK_NAMES.filter((s) => detectedStacks.has(s))
447
+ return `${prompt}\n\n====\n\n${STACK_RULES_HEADER}\n\nThe target project was detected as using the following stack(s): ${stackList.join(", ")}.\n\n${stackRules}`
448
+ }