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.
- package/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- 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
|
+
}
|