diff --git a/.orchestration/active_intents.yaml b/.orchestration/active_intents.yaml new file mode 100644 index 00000000000..3491c122197 --- /dev/null +++ b/.orchestration/active_intents.yaml @@ -0,0 +1,13 @@ +active_intents: + - id: "INT-001" + name: "Build Weather API" + status: "IN_PROGRESS" + owned_scope: + - "src/api/weather/**" + - "src/middleware/weather.ts" + constraints: + - "Use only open APIs, no paid services" + - "Maintain RESTful structure" + acceptance_criteria: + - "Unit tests in tests/api/weather/ pass" + - "Endpoint responds in < 500ms" \ No newline at end of file diff --git a/.orchestration/agent_trace.jsonl b/.orchestration/agent_trace.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.orchestration/intent_map.md b/.orchestration/intent_map.md new file mode 100644 index 00000000000..898c07da866 --- /dev/null +++ b/.orchestration/intent_map.md @@ -0,0 +1,5 @@ +# Intent Map + +| Intent ID | Name | Scope | Files/Nodes | +| --------- | ---------------------------- | ------------------------------------ | ----------------------- | +| INT-001 | JWT Authentication Migration | src/auth/\*\*, src/middleware/jwt.ts | (to be filled by hooks) | diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 00000000000..e5332c43d36 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,13 @@ +# AGENT.md + +## Lessons Learned + +- Always enforce intent selection before code actions. +- Maintain spatial independence via content hashing in agent_trace.jsonl. +- Use .orchestration/ as the single source of truth for orchestration state. + +## Stylistic Rules + +- All code changes must be linked to an intent. +- Never bypass the Reasoning Loop handshake. +- Document architectural decisions here as they arise. diff --git a/ARCHITECTURE_NOTES.md b/ARCHITECTURE_NOTES.md new file mode 100644 index 00000000000..185bcf11739 --- /dev/null +++ b/ARCHITECTURE_NOTES.md @@ -0,0 +1,29 @@ +# ARCHITECTURE_NOTES.md + +## Phase 0: The Archaeological Dig + +1. Fork & Run: Roo Code is present and runs in VS Code via CLI or extension host. Entry point: `src/extension.ts` (VSCode), `apps/cli/src/agent/extension-host.ts` (CLI). + +2. Trace the Tool Loop: + + - Tool execution (e.g., `write_to_file`, `edit_file`) is implemented in `src/core/tools/WriteToFileTool.ts`, `EditFileTool.ts`, etc. + - All tools inherit from `BaseTool`. + - Tool calls are managed by the agent loop in the extension host (`apps/cli/src/agent/extension-host.ts`). + +3. Locate the Prompt Builder: + + - System prompt is constructed in `src/core/prompts/system.ts` (see `SYSTEM_PROMPT`). + - Prompt sections are composed from `src/core/prompts/sections/`. + +4. Architectural Decisions: + - Middleware pattern for hooks: Pre-Hook (context injection, intent validation), Post-Hook (trace update). + - Reasoning Loop: User Prompt -> Reasoning Intercept (select_active_intent) -> Pre-Hook (Context Injection) -> Tool Call -> Post-Hook (Trace Update) + +## Phase 0 Complete + +All required locations for tool execution, prompt building, and agent loop are mapped. Ready for Phase 1 implementation. + +## Hook System Schema + +- Pre-Hook: Intercepts select_active_intent, injects from active_intents.yaml. +- Post-Hook: Updates agent_trace.jsonl with content hash. diff --git a/README.md b/README.md index 75f37762f93..b812ca22745 100644 --- a/README.md +++ b/README.md @@ -174,3 +174,5 @@ We love community contributions! Get started by reading our [CONTRIBUTING.md](CO --- **Enjoy Roo Code!** Whether you keep it on a short leash or let it roam autonomously, we can’t wait to see what you build. If you have questions or feature ideas, drop by our [Reddit community](https://www.reddit.com/r/RooCode/) or [Discord](https://discord.gg/roocode). Happy coding! + +# Test to add content diff --git a/apps/web-evals/next-env.d.ts b/apps/web-evals/next-env.d.ts index 7506fe6afbc..7a70f65a1ee 100644 --- a/apps/web-evals/next-env.d.ts +++ b/apps/web-evals/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts" +import "./.next/types/routes.d.ts" // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index de8dff751cb..65be7d5d332 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "roo-code", "packageManager": "pnpm@10.8.1", "engines": { - "node": "20.19.2" + "node": ">=20.19.2" }, "scripts": { "preinstall": "node scripts/bootstrap.mjs", @@ -32,6 +32,7 @@ "@dotenvx/dotenvx": "^1.34.0", "@roo-code/config-typescript": "workspace:^", "@types/glob": "^9.0.0", + "@types/micromatch": "^4.0.10", "@types/node": "^24.1.0", "@vscode/vsce": "3.3.2", "esbuild": "^0.25.0", @@ -56,7 +57,16 @@ }, "pnpm": { "onlyBuiltDependencies": [ - "@vscode/ripgrep" + "@tailwindcss/oxide", + "@vscode/ripgrep", + "@vscode/vsce-sign", + "better-sqlite3", + "core-js", + "esbuild", + "keytar", + "protobufjs", + "puppeteer-chromium-resolver", + "sharp" ], "overrides": { "tar-fs": ">=3.1.1", @@ -70,5 +80,8 @@ "@types/react-dom": "^18.3.5", "zod": "3.25.76" } + }, + "dependencies": { + "micromatch": "^4.0.8" } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 278e727243c..1642117a487 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -32,3 +32,10 @@ export * from "./vscode.js" export * from "./worktree.js" export * from "./providers/index.js" + +// Settings, schemas, and keys for web-evals +export { EVALS_SETTINGS, GLOBAL_SETTINGS_KEYS, rooCodeSettingsSchema, globalSettingsSchema } from "./global-settings.js" +export { PROVIDER_SETTINGS_KEYS, providerSettingsSchema, getModelId } from "./provider-settings.js" +export { RooCodeEventName, taskEventSchema } from "./events.js" + +export * from "./providers/index.js" diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index 2a73ee92bb0..2de4dea313f 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -2,7 +2,9 @@ "extends": "@roo-code/config-typescript/base.json", "compilerOptions": { "types": ["vitest/globals"], - "outDir": "dist" + "outDir": "dist", + "moduleResolution": "node", + "module": "ESNext" }, "include": ["src", "scripts", "*.config.ts"], "exclude": ["node_modules"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b461926f5e..5823b01ab72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,10 @@ overrides: importers: .: + dependencies: + micromatch: + specifier: ^4.0.8 + version: 4.0.8 devDependencies: '@changesets/cli': specifier: ^2.27.10 @@ -32,6 +36,9 @@ importers: '@types/glob': specifier: ^9.0.0 version: 9.0.0 + '@types/micromatch': + specifier: ^4.0.10 + version: 4.0.10 '@types/node': specifier: ^24.1.0 version: 24.2.1 @@ -884,6 +891,9 @@ importers: isbinaryfile: specifier: ^5.0.2 version: 5.0.4 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 json-stream-stringify: specifier: ^3.1.6 version: 3.1.6 @@ -896,6 +906,9 @@ importers: mammoth: specifier: ^1.9.1 version: 1.9.1 + minimatch: + specifier: ^10.2.2 + version: 10.2.2 monaco-vscode-textmate-theme-converter: specifier: ^0.1.7 version: 0.1.7(tslib@2.8.1) @@ -1056,9 +1069,15 @@ importers: '@types/glob': specifier: ^8.1.0 version: 8.1.0 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/lodash.debounce': specifier: ^4.0.9 version: 4.0.9 + '@types/minimatch': + specifier: ^6.0.0 + version: 6.0.0 '@types/mocha': specifier: ^10.0.10 version: 10.0.10 @@ -2476,14 +2495,6 @@ packages: '@ioredis/commands@1.3.0': resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==} - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -4368,6 +4379,9 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/braces@3.0.5': + resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -4516,6 +4530,9 @@ packages: '@types/js-cookie@2.2.7': resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -4534,9 +4551,16 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/micromatch@4.0.10': + resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==} + '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + '@types/minimatch@6.0.0': + resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} + deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} @@ -6927,6 +6951,7 @@ packages: glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true global-agent@3.0.0: @@ -7611,6 +7636,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -8339,13 +8368,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.0.1: - resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} - engines: {node: 20 || >=22} - - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} + minimatch@10.2.2: + resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} + engines: {node: 18 || 20 || >=22} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -9016,6 +9041,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -10101,7 +10127,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -12598,12 +12624,6 @@ snapshots: '@ioredis/commands@1.3.0': {} - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -14560,6 +14580,8 @@ snapshots: dependencies: '@babel/types': 7.27.1 + '@types/braces@3.0.5': {} + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -14733,6 +14755,8 @@ snapshots: '@types/js-cookie@2.2.7': {} + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/katex@0.16.7': {} @@ -14751,8 +14775,16 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/micromatch@4.0.10': + dependencies: + '@types/braces': 3.0.5 + '@types/minimatch@5.1.2': {} + '@types/minimatch@6.0.0': + dependencies: + minimatch: 10.2.2 + '@types/mocha@10.0.10': {} '@types/ms@2.1.0': {} @@ -15073,7 +15105,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -17133,14 +17165,18 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.4.4(picomatch@4.0.2): + fdir@6.4.4(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 fdir@6.4.6(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -17440,7 +17476,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 - minimatch: 10.1.1 + minimatch: 10.2.2 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.0 @@ -18197,6 +18233,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsbn@1.1.0: {} jsdom@26.1.0: @@ -19176,14 +19216,10 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.0.1: + minimatch@10.2.2: dependencies: brace-expansion: 2.0.2 - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimatch@3.1.2: dependencies: brace-expansion: 2.0.2 @@ -19395,7 +19431,7 @@ snapshots: ansi-styles: 6.2.1 cross-spawn: 7.0.6 memorystream: 0.3.1 - minimatch: 10.0.1 + minimatch: 10.2.2 pidtree: 0.6.0 read-package-json-fast: 4.0.0 shell-quote: 1.8.3 @@ -21322,8 +21358,8 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 tinyglobby@0.2.15: dependencies: @@ -21876,8 +21912,8 @@ snapshots: vite@6.3.5(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.9 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.4(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.40.2 tinyglobby: 0.2.14 @@ -21892,8 +21928,8 @@ snapshots: vite@6.3.5(@types/node@20.17.57)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.9 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.4(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.40.2 tinyglobby: 0.2.14 @@ -21908,8 +21944,8 @@ snapshots: vite@6.3.5(@types/node@24.2.1)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.9 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.4(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.40.2 tinyglobby: 0.2.14 diff --git a/src/agent/AgentLoop.ts b/src/agent/AgentLoop.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/core/tools/SelectActiveIntentTool.ts b/src/core/tools/SelectActiveIntentTool.ts new file mode 100644 index 00000000000..01afae6afab --- /dev/null +++ b/src/core/tools/SelectActiveIntentTool.ts @@ -0,0 +1,48 @@ +import { BaseTool, ToolCallbacks } from "./BaseTool" +import type { ToolUse } from "../../shared/tools" +import { Task } from "../task/Task" +import fs from "fs/promises" +import path from "path" + +interface SelectActiveIntentParams { + intent_id: string +} + +export class SelectActiveIntentTool extends BaseTool { + readonly name = "select_active_intent" as const + + async execute(params: SelectActiveIntentParams, task: Task, callbacks: ToolCallbacks): Promise { + const { pushToolResult, handleError } = callbacks + try { + const orchestrationPath = path.join(task.cwd, ".orchestration", "active_intents.yaml") + const yamlRaw = await fs.readFile(orchestrationPath, "utf-8") + // Simple YAML parse (replace with a YAML parser if available) + const match = new RegExp(`- id: "${params.intent_id}"([\s\S]*?)(?=\n\s*- id:|$)`, "m").exec(yamlRaw) + if (!match) { + pushToolResult(`You must cite a valid active Intent ID`) + return + } + const block = match[1] + // Extract constraints and scope + const constraints = + /constraints:\n([\s\S]*?)\n\s*acceptance_criteria:/m + .exec(block)?.[1] + ?.trim() + .split("\n") + .map((l) => l.replace(/^- /, "").trim()) || [] + const scope = + /owned_scope:\n([\s\S]*?)\n\s*constraints:/m + .exec(block)?.[1] + ?.trim() + .split("\n") + .map((l) => l.replace(/^- /, "").trim()) || [] + // Build XML + const xml = `\n ${scope.join(", ")}\n ${constraints.join(", ")}\n` + pushToolResult(xml) + } catch (e) { + await handleError("select_active_intent", e as Error) + } + } +} + +export const selectActiveIntentTool = new SelectActiveIntentTool() diff --git a/src/core/tools/WriteToFileTool.ts b/src/core/tools/WriteToFileTool.ts index c8455ef3d97..66c20202e5a 100644 --- a/src/core/tools/WriteToFileTool.ts +++ b/src/core/tools/WriteToFileTool.ts @@ -17,10 +17,16 @@ import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } fr import type { ToolUse } from "../../shared/tools" import { BaseTool, ToolCallbacks } from "./BaseTool" +import { classifyTool, isInScope, isIntentIgnored } from "./intent-middleware" +import { sha256 } from "../utils/hash" +import fsSync from "fs" interface WriteToFileParams { path: string content: string + intent_id: string + mutation_class: "AST_REFACTOR" | "INTENT_EVOLUTION" + original_content_hash: string // SHA-256 hash of file as read by agent } export class WriteToFileTool extends BaseTool<"write_to_file"> { @@ -30,6 +36,77 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { const { pushToolResult, handleError, askApproval } = callbacks const relPath = params.path let newContent = params.content + const intentId = params.intent_id + const mutationClass = params.mutation_class + const originalContentHash = params.original_content_hash + // === PHASE 4: OPTIMISTIC LOCKING === + const absolutePath = path.resolve(task.cwd, relPath) + let currentContent = "" + let fileExists = false + try { + fileExists = await fileExistsAtPath(absolutePath) + if (fileExists) { + currentContent = await fs.readFile(absolutePath, "utf-8") + } + } catch {} + const currentContentHash = sha256(currentContent) + if (fileExists && originalContentHash && originalContentHash !== currentContentHash) { + pushToolResult( + `Stale File: The file [${relPath}] has changed since you last read it. Your write was blocked to prevent overwriting concurrent changes. Please re-read the file and try again.`, + ) + return + } + + // === INTENT-AWARE PRE-HOOK (Phase 2) === + // 1. Load active intent + let activeIntentId = intentId + let ownedScope: string[] = [] + try { + const orchestrationPath = path.join(task.cwd, ".orchestration", "active_intents.yaml") + if (fsSync.existsSync(orchestrationPath)) { + const yamlRaw = fsSync.readFileSync(orchestrationPath, "utf-8") + const match = new RegExp(`- id: "${intentId}"([\s\S]*?)(?=\n\s*- id:|$)`, "m").exec(yamlRaw) + if (match) { + const block = match[1] + ownedScope = + /owned_scope:\n([\s\S]*?)\n\s*constraints:/m + .exec(block)?.[1] + ?.trim() + .split("\n") + .map((l) => l.replace(/^- /, "").trim()) || [] + } + } + } catch {} + + // 2. Scope enforcement + if (activeIntentId && ownedScope.length > 0) { + if (!isInScope(relPath, ownedScope)) { + pushToolResult( + `Scope Violation: ${activeIntentId} is not authorized to edit [${relPath}]. Request scope expansion.`, + ) + return + } + } + + // 3. .intentignore enforcement + const ignored = await isIntentIgnored(task.cwd, relPath) + if (ignored) { + // Allow without approval + } else { + // 4. UI-blocking authorization for destructive tools + if (classifyTool(this.name) === "destructive") { + const approved = await askApproval( + "tool", + `Approve write to ${relPath} for intent ${activeIntentId || "(none)"}?`, + undefined, + false, + ) + if (!approved) { + pushToolResult("Action rejected by user. Autonomous recovery: self-correct or request approval.") + return + } + } + } if (!relPath) { task.consecutiveMistakeCount++ @@ -57,13 +134,11 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { const isWriteProtected = task.rooProtectedController?.isWriteProtected(relPath) || false - let fileExists: boolean - const absolutePath = path.resolve(task.cwd, relPath) - + // fileExists and absolutePath already set above for optimistic locking if (task.diffViewProvider.editType !== undefined) { - fileExists = task.diffViewProvider.editType === "modify" + // Do not override fileExists, just set editType + task.diffViewProvider.editType = fileExists ? "modify" : "create" } else { - fileExists = await fileExistsAtPath(absolutePath) task.diffViewProvider.editType = fileExists ? "modify" : "create" } @@ -177,6 +252,51 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { const message = await task.diffViewProvider.pushToolWriteResult(task, task.cwd, !fileExists) + // === PHASE 3: POST-HOOK TRACE SERIALIZATION === + try { + const tracePath = path.join(task.cwd, ".orchestration", "agent_trace.jsonl") + const vcsSha = null // Optionally, get git SHA here + const now = new Date().toISOString() + const fileContent = newContent + const contentHash = sha256(fileContent) + const trace = { + id: require("crypto").randomUUID ? require("crypto").randomUUID() : now, + timestamp: now, + vcs: { revision_id: vcsSha }, + files: [ + { + relative_path: relPath, + conversations: [ + { + url: "session_log_id", + contributor: { + entity_type: "AI", + model_identifier: "unknown", + }, + ranges: [ + { + start_line: 1, + end_line: fileContent.split("\n").length, + content_hash: `sha256:${contentHash}`, + }, + ], + related: [ + { + type: "specification", + value: intentId, + }, + ], + }, + ], + }, + ], + mutation_class: mutationClass, + } + fsSync.appendFileSync(tracePath, JSON.stringify(trace) + "\n") + } catch (e) { + // fail silently + } + pushToolResult(message) await task.diffViewProvider.reset() diff --git a/src/core/tools/intent-middleware.ts b/src/core/tools/intent-middleware.ts new file mode 100644 index 00000000000..d475f009123 --- /dev/null +++ b/src/core/tools/intent-middleware.ts @@ -0,0 +1,39 @@ +import path from "path" +import fs from "fs/promises" +import micromatch from "micromatch" + +// Command classification +const SAFE_TOOLS = ["read_file", "list_files", "search_files"] +const DESTRUCTIVE_TOOLS = ["write_to_file", "edit_file", "apply_patch", "delete_file", "execute_command"] + +// Load .intentignore if present +async function loadIntentIgnore(cwd: string): Promise { + try { + const ignorePath = path.join(cwd, ".intentignore") + const content = await fs.readFile(ignorePath, "utf-8") + return content + .split("\n") + .map((l) => l.trim()) + .filter(Boolean) + } catch { + return [] + } +} + +// Check if file is ignored by .intentignore +export async function isIntentIgnored(cwd: string, file: string): Promise { + const patterns = await loadIntentIgnore(cwd) + return micromatch.isMatch(file, patterns) +} + +// Check if file is in scope +export function isInScope(file: string, ownedScope: string[]): boolean { + return micromatch.isMatch(file, ownedScope) +} + +// Classify tool +export function classifyTool(toolName: string): "safe" | "destructive" | "unknown" { + if (SAFE_TOOLS.includes(toolName)) return "safe" + if (DESTRUCTIVE_TOOLS.includes(toolName)) return "destructive" + return "unknown" +} diff --git a/src/core/utils/hash.ts b/src/core/utils/hash.ts new file mode 100644 index 00000000000..ded7fcdad13 --- /dev/null +++ b/src/core/utils/hash.ts @@ -0,0 +1,5 @@ +import { createHash } from "crypto" + +export function sha256(content: string): string { + return createHash("sha256").update(content, "utf8").digest("hex") +} diff --git a/src/extension/agent/systemPrompt.ts b/src/extension/agent/systemPrompt.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/extension/agent/toolExecutor.ts b/src/extension/agent/toolExecutor.ts new file mode 100644 index 00000000000..1acc3a6fc1f --- /dev/null +++ b/src/extension/agent/toolExecutor.ts @@ -0,0 +1,28 @@ +import { HookEngine } from "../../hooks/hookEngine" // adjust relative path if needed + +export class ToolExecutor { + private hookEngine: HookEngine + constructor(private workspaceRoot: string) { + this.hookEngine = new HookEngine(this.workspaceRoot) + } + + async execute(invocation: { tool: string; arguments: any }) { + await this.hookEngine.onPreToolUse(invocation) + const res = await this.dispatch(invocation) // existing call that actually runs the tool + await this.hookEngine.onPostToolUse(invocation, res) + return res + } + + async dispatch(invocation: { tool: string; arguments: any }): Promise { + // This should be replaced by actual logic in the base class or injected + console.log(`Executing tool: ${invocation.tool}`) + return {} + } + + async preWriteCheck(args: { path: string; intentId: string; mutationClass: string }) { + await this.hookEngine.onPreWrite(args) + } + async postWriteTrace(args: { path: string; content: string; intentId: string; mutationClass: string }) { + await this.hookEngine.onPostWrite(args) + } +} diff --git a/src/extension/agent/toolRegistry.ts b/src/extension/agent/toolRegistry.ts new file mode 100644 index 00000000000..a284319564a --- /dev/null +++ b/src/extension/agent/toolRegistry.ts @@ -0,0 +1,28 @@ +import * as vscode from "vscode" +import { loadActiveIntentContext } from "../../hooks/preHooks/intentHandshakePreHook" +// import your ToolExecutor type if needed + +export function registerTools(executor: any) { + executor.register("select_active_intent", async (args: any) => { + const intentId = String(args.intent_id || "") + if (!intentId) throw new Error("You must provide intent_id") + const ctx = loadActiveIntentContext(executor.workspaceRoot, intentId) + return { intent_id: intentId, intent_context: ctx } + }) + + executor.register("write_file", async (args: any) => { + const rel = String(args.path) + const content = String(args.content ?? "") + const intentId = String(args.intent_id || "") + const mutationClass = String(args.mutation_class || "") + + await executor.preWriteCheck({ path: rel, intentId, mutationClass }) + + const uri = vscode.Uri.joinPath(vscode.Uri.file(executor.workspaceRoot), rel) + await vscode.workspace.fs.writeFile(uri, Buffer.from(content, "utf8")) + + await executor.postWriteTrace({ path: rel, content, intentId, mutationClass }) + + return { ok: true } + }) +} diff --git a/src/hooks/engine.ts b/src/hooks/engine.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/hooks/hookEngine.ts b/src/hooks/hookEngine.ts new file mode 100644 index 00000000000..b51b6ca5d29 --- /dev/null +++ b/src/hooks/hookEngine.ts @@ -0,0 +1,89 @@ +// src/hooks/hookEngine.ts +import * as fs from "fs" +import * as crypto from "crypto" +import * as path from "path" +import * as yaml from "js-yaml" // Assume installed or add dependency + +export class HookEngine { + private activeIntent: any = null + private workspaceRoot: string + + constructor(workspaceRoot: string = "") { + this.workspaceRoot = workspaceRoot + } + + async onPreToolUse(invocation: any) { + return this.preHook(invocation.tool, invocation.arguments) + } + + async onPostToolUse(invocation: any, result: any) { + return this.postHook(invocation.tool, invocation.arguments, result) + } + + async onPreWrite(args: any) { + if (!this.activeIntent && args.intentId) { + this.activeIntent = { id: args.intentId } // Minimal fallback + } + return this.preHook("write_to_file", { ...args, relative_path: args.path }) + } + + async onPostWrite(args: any) { + return this.postHook("write_to_file", { ...args, relative_path: args.path }, null) + } + + preHook(toolName: string, args: any) { + if (toolName === "select_active_intent") { + // Load context from active_intents.yaml + const configPath = path.join(this.workspaceRoot, ".orchestration", "active_intents.yaml") + if (!fs.existsSync(configPath)) return + const intents = yaml.load(fs.readFileSync(configPath, "utf8")) as any + this.activeIntent = intents.active_intents.find((i: any) => i.id === args.intent_id) + if (!this.activeIntent) throw new Error("Invalid Intent ID") + // Inject context (return XML block) + return `${JSON.stringify(this.activeIntent)}` + } + if (toolName === "write_to_file" && !this.activeIntent) { + throw new Error("Must select intent first") + } + // Scope check + if (toolName === "write_to_file" && this.activeIntent && this.activeIntent.owned_scope) { + if (!this.activeIntent.owned_scope.some((scope: string) => args.relative_path.startsWith(scope))) { + throw new Error("Scope Violation") + } + } + return null + } + + postHook(toolName: string, args: any, result: any) { + if (toolName === "write_to_file") { + // Compute hash + const contentHash = crypto + .createHash("sha256") + .update(args.content || "") + .digest("hex") + // Append to agent_trace.jsonl + const traceEntry = { + id: crypto.randomUUID(), + timestamp: new Date().toISOString(), + vcs: { revision_id: "git_sha_placeholder" }, // Integrate git rev-parse HEAD + files: [ + { + relative_path: args.relative_path, + conversations: [ + { + url: "session_placeholder", + contributor: { entity_type: "AI", model_identifier: "claude-3-5-sonnet" }, + ranges: [{ start_line: 1, end_line: 10, content_hash: contentHash }], // Adjust lines + related: [{ type: "specification", value: this.activeIntent?.id }], + }, + ], + }, + ], + } + const tracePath = path.join(this.workspaceRoot, ".orchestration", "agent_trace.jsonl") + const dir = path.dirname(tracePath) + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) + fs.appendFileSync(tracePath, JSON.stringify(traceEntry) + "\n") + } + } +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 00000000000..305822ae69b --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,2 @@ +// src/hooks/index.ts +export { HookEngine } from './hookEngine'; \ No newline at end of file diff --git a/src/hooks/interfaces.ts b/src/hooks/interfaces.ts new file mode 100644 index 00000000000..b19551c0b0a --- /dev/null +++ b/src/hooks/interfaces.ts @@ -0,0 +1,8 @@ +export interface PreHook { + onPreToolUse?(invocation: any): Promise + onPreWrite?(data: { path: string; intentId: string; mutationClass: string }): Promise +} +export interface PostHook { + onPostToolUse?(invocation: any, result: any): Promise + onPostWrite?(data: { path: string; content: string; intentId: string; mutationClass: string }): Promise +} diff --git a/src/hooks/postHooks/traceLedgerPostHook.ts b/src/hooks/postHooks/traceLedgerPostHook.ts new file mode 100644 index 00000000000..19bed93fe1a --- /dev/null +++ b/src/hooks/postHooks/traceLedgerPostHook.ts @@ -0,0 +1,32 @@ +import * as fs from "fs" +import * as path from "path" +import { PostHook } from "../interfaces" +import { sha256 } from "../utils/contentHash" + +export function traceLedgerPostHook(root: string): PostHook { + return { + async onPostWrite({ path: relPath, content, intentId, mutationClass }) { + const ledger = path.join(root, ".orchestration/agent_trace.jsonl") + const entry = { + id: simpleUUID(), + timestamp: new Date().toISOString(), + files: [ + { + relative_path: relPath, + ranges: [{ start_line: 0, end_line: 0, content_hash: sha256(content) }], + related: [{ type: "specification", value: intentId }], + mutation_class: mutationClass, + }, + ], + } + fs.appendFileSync(ledger, JSON.stringify(entry) + "\n") + }, + } +} +function simpleUUID() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0, + v = c === "x" ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/src/hooks/postToolUse.ts b/src/hooks/postToolUse.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/hooks/preHooks/intentHandshakePreHook.ts b/src/hooks/preHooks/intentHandshakePreHook.ts new file mode 100644 index 00000000000..1606029fe54 --- /dev/null +++ b/src/hooks/preHooks/intentHandshakePreHook.ts @@ -0,0 +1,42 @@ +import * as fs from "fs" +import * as path from "path" +import yaml from "js-yaml" +import { PreHook } from "../interfaces" + +export function intentHandshakePreHook(root: string): PreHook { + return { + async onPreToolUse(invocation) { + // reserved for turn-enforcement; gate at write-time too + if (invocation?.tool === "select_active_intent") return + }, + async onPreWrite({ intentId }) { + if (!intentId) throw new Error("Gatekeeper: missing intent_id") + const data = yaml.load( + fs.readFileSync(path.join(root, ".orchestration/active_intents.yaml"), "utf8"), + ) as any + const found = (data?.active_intents || []).find((i: any) => i.id === intentId) + if (!found) throw new Error(`Gatekeeper: unknown intent_id ${intentId}`) + }, + } +} + +export function loadActiveIntentContext(root: string, intentId: string) { + const file = path.join(root, ".orchestration/active_intents.yaml") + const data = yaml.load(fs.readFileSync(file, "utf8")) as any + const intent = (data?.active_intents || []).find((i: any) => i.id === intentId) + if (!intent) throw new Error(`No such active intent: ${intentId}`) + const constraints = (intent.constraints || []).map((c: string) => ` ${c}`).join("\n") + const scope = (intent.owned_scope || []).map((s: string) => ` ${s}`).join("\n") + return [ + "", + ` ${intent.id}`, + ` ${intent.name}`, + " ", + constraints || " ", + " ", + " ", + scope || " ", + " ", + "", + ].join("\n") +} diff --git a/src/hooks/preHooks/scopeEnforcementPreHook.ts b/src/hooks/preHooks/scopeEnforcementPreHook.ts new file mode 100644 index 00000000000..7d36f552431 --- /dev/null +++ b/src/hooks/preHooks/scopeEnforcementPreHook.ts @@ -0,0 +1,23 @@ +import * as fs from "fs" +import * as path from "path" +import yaml from "js-yaml" +import { minimatch } from "minimatch" +import { PreHook } from "../interfaces" + +export function scopeEnforcementPreHook(root: string): PreHook { + return { + async onPreWrite({ path: target, intentId }) { + const data = yaml.load( + fs.readFileSync(path.join(root, ".orchestration/active_intents.yaml"), "utf8"), + ) as any + const intent = (data?.active_intents || []).find((i: any) => i.id === intentId) + if (!intent) throw new Error(`Scope Enforcement: unknown intent ${intentId}`) + const allowed = (intent.owned_scope || []).some((pattern: string) => minimatch(target, pattern)) + if (!allowed) { + throw new Error( + `Scope Violation: ${intentId} is not authorized to edit ${target}. Request scope expansion.`, + ) + } + }, + } +} diff --git a/src/hooks/preToolUse.ts b/src/hooks/preToolUse.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/hooks/utils/contentHash.ts b/src/hooks/utils/contentHash.ts new file mode 100644 index 00000000000..e9b54dc2ea6 --- /dev/null +++ b/src/hooks/utils/contentHash.ts @@ -0,0 +1,4 @@ +import crypto from "crypto" +export function sha256(content: string) { + return "sha256:" + crypto.createHash("sha256").update(content).digest("hex") +} diff --git a/src/hooks/utils/yaml.ts b/src/hooks/utils/yaml.ts new file mode 100644 index 00000000000..d45c5bdcbcc --- /dev/null +++ b/src/hooks/utils/yaml.ts @@ -0,0 +1,5 @@ +import * as fs from "fs" +import yaml from "js-yaml" +export function readYaml(file: string) { + return yaml.load(fs.readFileSync(file, "utf8")) +} diff --git a/src/package.json b/src/package.json index 236bfe04acc..9843a66134b 100644 --- a/src/package.json +++ b/src/package.json @@ -11,7 +11,7 @@ }, "engines": { "vscode": "^1.84.0", - "node": "20.19.2" + "node": ">=20.19.2" }, "author": { "name": "Roo Code" @@ -492,10 +492,12 @@ "i18next": "^25.0.0", "ignore": "^7.0.3", "isbinaryfile": "^5.0.2", + "js-yaml": "^4.1.1", "json-stream-stringify": "^3.1.6", "jwt-decode": "^4.0.0", "lodash.debounce": "^4.0.8", "mammoth": "^1.9.1", + "minimatch": "^10.2.2", "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-cache": "^5.1.2", "node-ipc": "^12.0.0", @@ -551,7 +553,9 @@ "@types/diff": "^5.2.1", "@types/diff-match-patch": "^1.0.36", "@types/glob": "^8.1.0", + "@types/js-yaml": "^4.0.9", "@types/lodash.debounce": "^4.0.9", + "@types/minimatch": "^6.0.0", "@types/mocha": "^10.0.10", "@types/node": "20.x", "@types/node-cache": "^4.1.3", diff --git a/src/prompt/PromptBuilder.ts b/src/prompt/PromptBuilder.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/tools/fileTool.ts b/src/tools/fileTool.ts new file mode 100644 index 00000000000..7e8461f573b --- /dev/null +++ b/src/tools/fileTool.ts @@ -0,0 +1,9 @@ +import { HookEngine } from "../hooks/hookEngine" +// Utility to be used within actual file tool implementations +export async function withWriteHook(workspaceRoot: string, args: any, logic: () => Promise) { + const hook = new HookEngine(workspaceRoot) + await hook.onPreWrite(args) + const result = await logic() + await hook.onPostWrite({ ...args, content: args.content }) // Example + return result +} diff --git a/src/tools/intentTools.ts b/src/tools/intentTools.ts new file mode 100644 index 00000000000..56acc8bde6f --- /dev/null +++ b/src/tools/intentTools.ts @@ -0,0 +1,7 @@ +// src/tools/intentTools.ts +import { HookEngine } from "../hooks/hookEngine" + +export const selectActiveIntent = (args: { intent_id: string }) => { + const hook = new HookEngine("") + return hook.preHook("select_active_intent", args) +} diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts new file mode 100644 index 00000000000..e69de29bb2d