diff --git a/.orchestration/active_intents.yaml b/.orchestration/active_intents.yaml new file mode 100644 index 00000000000..10df75b8e1a --- /dev/null +++ b/.orchestration/active_intents.yaml @@ -0,0 +1,35 @@ +active_intents: + - id: INT-001 + name: testing PRD + description: testing PRD + status: IN_PROGRESS + owned_scope: + - /tests + constraints: [] + acceptance_criteria: [] + - id: INT-002 + name: project dependencies + description: Updating project dependencies + status: CREATED + owned_scope: + - requirements.txt + - pyproject.toml + constraints: [] + acceptance_criteria: [] + - id: INT-003 + name: project documentation + description: Updating project documentation + status: IN_PROGRESS + owned_scope: + - README.md + constraints: [] + acceptance_criteria: [] + - id: INT-004 + name: project bot + description: Updating project bot + status: CREATED + owned_scope: + - bot.py + constraints: [] + acceptance_criteria: [] +last_updated: 2026-02-21T19:01:30.984Z diff --git a/.orchestration/agent_trace.jsonl b/.orchestration/agent_trace.jsonl new file mode 100644 index 00000000000..2945578f700 --- /dev/null +++ b/.orchestration/agent_trace.jsonl @@ -0,0 +1,57 @@ +{"timestamp":"2026-02-20T13:43:47.352Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"aba8052c-6025-4e79-94b2-5c98030ebdf8","input_hash":"","endpoint":"list_files","response_hash":"","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T13:44:00.822Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"b0ddc7d9-0062-4b8e-8dea-1010336577c7","input_hash":"","endpoint":"read_file","response_hash":"","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T13:44:09.386Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"8b0b9b61-7e9d-4d5c-a3cb-18ec7e1af5b7","input_hash":"","endpoint":"read_file","response_hash":"","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T13:44:16.656Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"9c9980ca-e399-4e43-b549-28d3009f85fb","input_hash":"","endpoint":"read_file","response_hash":"","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T13:44:51.171Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"d032aba9-e0e1-402d-a25c-e5695f3dfe25","input_hash":"","endpoint":"write_to_file","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:615ddf86dc5fec0f495ab6f00e5a49220108ea9a8ea2662070dd8265a7f11582"}],"related":[]}]}],"response_hash":"","outcome":"FAILURE","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T13:45:01.508Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"c4a2a262-b211-4b5c-ba1a-93d2c1c2b774","input_hash":"","endpoint":"ask_followup_question","response_hash":"","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T13:45:52.180Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"18904b14-2fc3-4170-856a-f5f2fba7a49b","input_hash":"","endpoint":"write_to_file","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:615ddf86dc5fec0f495ab6f00e5a49220108ea9a8ea2662070dd8265a7f11582"}],"related":[]}]}],"response_hash":"","outcome":"FAILURE","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T15:08:41.798Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"1dd5efe9-01a0-40ab-a429-74afbb8ccf51","input_hash":"800c2462366d2b6dc6ff495063855cb331ed762e28a523676969bbad975f6e69","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:de4a30fc212f55886586ea10f2233ce30ff02f0656067606ce2bc06217aad182"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T15:29:18.654Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"6312bda6-1b16-4aff-8f8f-62e4b7eb8c63","input_hash":"47c65533448e29e36e0f2ea0024a43ea08f7e7b510fcdb8c4751faad13360830","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:bb317deb33ac41efb5586ca97ed7abaf4c1348f6cedbfbf7e8681b736c4b5213"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T15:35:46.000Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"f71d929d-e73e-4010-9c7d-8af3b9eb9422","input_hash":"22f56cf6c0ff03bca028e4e4856d7d4ecb2b0aff808327807a5a9fc9efbe2a52","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:ce92f035b23da5029ff77de19b75f5f0bb0cbbe6aad94a44422a34b372662adb"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T15:45:52.711Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"b5c52db4-0e49-462f-828e-c3c235bf295d","input_hash":"67f3d18fa30cada3bb9218dadafdce743804fdf11d11955a63484b6d6a253f11","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:8f6c73ffac755c22f28958156f8872b2ca7b32561ecb85228223d6ef2c53b581"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T17:51:04.426Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"b4d941f7-164c-42b1-80b3-a5a0915a88a5","input_hash":"71fb7b40f9d833583fe456829622c9ea43054c0d9e290aebde623ca12cf2cca6","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:befcc1bfcba40d3ed9f775b06d759fa6553d81fa2b99854196dc798a06e50d81"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T18:00:19.505Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"fd9443d0-12cb-49ed-a82f-94e2476f6375","input_hash":"7bc689fc0a2c45e05ef5e20ae63400cfa3cfdd663b24eb9812ebf3971fae59dd","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:4b8c0a57095c39a1314f9dc35b5a6bc1c5700fd709c9aceb987856dd7d8d9e97"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T18:00:42.038Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-002","call_id":"2502e46c-7a4c-4262-97bb-b511da4d2471","input_hash":"7bc689fc0a2c45e05ef5e20ae63400cfa3cfdd663b24eb9812ebf3971fae59dd","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:4b8c0a57095c39a1314f9dc35b5a6bc1c5700fd709c9aceb987856dd7d8d9e97"}],"related":[{"type":"specification","value":"INT-002"}]}]}],"response_hash":"69e3f7dd881130537cf9377efbb51f55459c027b5197877b7e88d56423eea08a","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T18:00:59.567Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"225ff46d-9870-4565-95bd-2d464f5ddf37","input_hash":"7bc689fc0a2c45e05ef5e20ae63400cfa3cfdd663b24eb9812ebf3971fae59dd","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:4b8c0a57095c39a1314f9dc35b5a6bc1c5700fd709c9aceb987856dd7d8d9e97"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"72fb3593fe71767c998123c05ba5db416c848b77235a1e242d9ac4737a724a42","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T18:01:16.095Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"e5a20c27-5959-46df-96b2-736bce8d384c","input_hash":"7bc689fc0a2c45e05ef5e20ae63400cfa3cfdd663b24eb9812ebf3971fae59dd","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:4b8c0a57095c39a1314f9dc35b5a6bc1c5700fd709c9aceb987856dd7d8d9e97"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"72fb3593fe71767c998123c05ba5db416c848b77235a1e242d9ac4737a724a42","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T18:01:28.953Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"a8efd730-3f65-4da7-9a78-b81068800d94","input_hash":"7bc689fc0a2c45e05ef5e20ae63400cfa3cfdd663b24eb9812ebf3971fae59dd","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:4b8c0a57095c39a1314f9dc35b5a6bc1c5700fd709c9aceb987856dd7d8d9e97"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"72fb3593fe71767c998123c05ba5db416c848b77235a1e242d9ac4737a724a42","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T19:19:44.180Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"3e9a872b-d30b-4af2-ae00-a8c1709f1241","input_hash":"4be32b68dc0f5ecf594c03ce0465cd40fc28996542fac184055bcb0440945f24","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:026db4ac9ea2bf64648c0dcfff3cc01cc255bd0bb26eb48333c83fb7b924bb50"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T19:20:01.947Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-002","call_id":"e0b303a1-ed11-4dc7-97c0-bf168c1e3f8f","input_hash":"4be32b68dc0f5ecf594c03ce0465cd40fc28996542fac184055bcb0440945f24","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:026db4ac9ea2bf64648c0dcfff3cc01cc255bd0bb26eb48333c83fb7b924bb50"}],"related":[{"type":"specification","value":"INT-002"}]}]}],"response_hash":"69e3f7dd881130537cf9377efbb51f55459c027b5197877b7e88d56423eea08a","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T19:20:26.628Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"64f602a5-8c65-42fd-b2ee-384924894808","input_hash":"4be32b68dc0f5ecf594c03ce0465cd40fc28996542fac184055bcb0440945f24","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:026db4ac9ea2bf64648c0dcfff3cc01cc255bd0bb26eb48333c83fb7b924bb50"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"f49b7cd27a00d0322239a3f43fb275babbe3aaa6ac7abea2b02ed3662897f245","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T19:20:36.785Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"73afc3d9-0ce3-4149-b440-619e4e00c59b","input_hash":"adfdf13b2917e57a2d27c8372e4f370a04fdd8e69395b93b0a5a48340ba6203e","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:a6890daefddd5bb68cb02b6338427de28ec2cead492be55a195da59490be16a7"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"087b6d2cd4e1ba5381e04659eed610501d36e597f8061dfdc7c6575ec3983fcb","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T19:20:44.210Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"ae149adc-e729-4469-a90e-dbcde932017d","input_hash":"adfdf13b2917e57a2d27c8372e4f370a04fdd8e69395b93b0a5a48340ba6203e","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:a6890daefddd5bb68cb02b6338427de28ec2cead492be55a195da59490be16a7"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"087b6d2cd4e1ba5381e04659eed610501d36e597f8061dfdc7c6575ec3983fcb","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-20T19:21:37.248Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"4239b519-f2fb-491e-8d8e-ba7c927f2ec9","input_hash":"adfdf13b2917e57a2d27c8372e4f370a04fdd8e69395b93b0a5a48340ba6203e","endpoint":"apply_diff","files":[{"relative_path":"README.md","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:a6890daefddd5bb68cb02b6338427de28ec2cead492be55a195da59490be16a7"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"087b6d2cd4e1ba5381e04659eed610501d36e597f8061dfdc7c6575ec3983fcb","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:14:58.619Z","intentId":"NONE","tool":"read_file","path":"bot.py","success":true,"error":null} +{"timestamp":"2026-02-21T16:15:15.997Z","intentId":"NONE","tool":"apply_diff","path":"bot.py","success":true,"error":null} +{"timestamp":"2026-02-21T16:21:33.066Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"bd0d6f1f-27dd-4a72-b639-51e86674a387","input_hash":"d5039bd7c0a838d41fe37790e44f2cf6d2cf56896331de2d6665d88f14b701cc","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:55ee3a28846a19133b352021db84dcb2e20af290927268a3539de92e0b653c0c"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:21:39.927Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-001","call_id":"8a077b0c-449b-4c0f-81ab-36567c239630","input_hash":"d5039bd7c0a838d41fe37790e44f2cf6d2cf56896331de2d6665d88f14b701cc","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:55ee3a28846a19133b352021db84dcb2e20af290927268a3539de92e0b653c0c"}],"related":[{"type":"specification","value":"INT-001"}]}]}],"response_hash":"8485b90c8cdeaa687c3dfae42a490f17ab2e53084872eb22ad5d696d892aa783","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:24:09.570Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"3bce6be7-e4f7-4099-9efc-2cb0a6dc9c0c","input_hash":"2ada26f80d182a50ef53143be4770ce4eb2b06b659942995df3112c957af99a9","endpoint":"apply_diff","files":[{"relative_path":".orchestration/active_intents.yaml","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:1e387ace4f03c611e9052a5d715fe61dbb03bd850773f15f8c1fc7a98c271aef"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"2e156fb5294e33c8cf2da4ce8656b3261df0ac1c7f40502d407a4f1dc5b8f092","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:24:47.223Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-003","call_id":"4ec40712-a12d-48cb-bc0b-7ec47a5e9dab","input_hash":"7d20676009cafc44f747c820ed526070d9748fc0054396cf90b9dc6f4953fc58","endpoint":"apply_diff","files":[{"relative_path":".orchestration/active_intents.yaml","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:e12ca3ab0eb308e7bafc0134d5a3209b0899d6225954a4a6a267459b47c0877f"}],"related":[{"type":"specification","value":"INT-003"}]}]}],"response_hash":"2e156fb5294e33c8cf2da4ce8656b3261df0ac1c7f40502d407a4f1dc5b8f092","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:30:12.810Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"a55133f7-b81c-44e5-8192-05d1141f4919","input_hash":"d5039bd7c0a838d41fe37790e44f2cf6d2cf56896331de2d6665d88f14b701cc","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:55ee3a28846a19133b352021db84dcb2e20af290927268a3539de92e0b653c0c"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:30:21.590Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-001","call_id":"ac4fc981-986e-4f07-9226-0905d0ee4813","input_hash":"d5039bd7c0a838d41fe37790e44f2cf6d2cf56896331de2d6665d88f14b701cc","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:55ee3a28846a19133b352021db84dcb2e20af290927268a3539de92e0b653c0c"}],"related":[{"type":"specification","value":"INT-001"}]}]}],"response_hash":"8485b90c8cdeaa687c3dfae42a490f17ab2e53084872eb22ad5d696d892aa783","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:30:36.822Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"b0e93ea6-3893-40da-9759-e9480ea592ff","input_hash":"d5039bd7c0a838d41fe37790e44f2cf6d2cf56896331de2d6665d88f14b701cc","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:55ee3a28846a19133b352021db84dcb2e20af290927268a3539de92e0b653c0c"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"74ea0bff9980efcbf06b8aba4ab56d3422896e15ff7e9ecfa8b25abbe36521de","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:30:44.695Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"52dcc8e4-8140-4d4a-a17d-83a6b7a4bbb3","input_hash":"d5039bd7c0a838d41fe37790e44f2cf6d2cf56896331de2d6665d88f14b701cc","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:55ee3a28846a19133b352021db84dcb2e20af290927268a3539de92e0b653c0c"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"74ea0bff9980efcbf06b8aba4ab56d3422896e15ff7e9ecfa8b25abbe36521de","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:32:31.104Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"3e7ccc99-a6cd-4c7c-a85f-79da83d871ca","input_hash":"d5039bd7c0a838d41fe37790e44f2cf6d2cf56896331de2d6665d88f14b701cc","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:55ee3a28846a19133b352021db84dcb2e20af290927268a3539de92e0b653c0c"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"74ea0bff9980efcbf06b8aba4ab56d3422896e15ff7e9ecfa8b25abbe36521de","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:41:07.862Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"f30d57e2-6088-485c-aef1-ee360f571dfd","input_hash":"9d676cf338a82885552eb8947a1a2ed4f0fd5db78de973a8547b9f782c37d5a5","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:ab2d88b04ea74f2d4f899ca0477b84a8de4e42ac52bffd7e2d0737fd1615ad4f"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"34b86c0400b58616f460181ce7b076439adc99637601bedceef0552d9009fd4a","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:42:54.785Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"e690c84f-47df-48c5-b971-b9d09c972330","input_hash":"961e335fb0ef55ee4d549922885e40259008d695a50b0fb05562bd0a57954b55","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:24fad635564267014f5b93b2245ea51e5fe90e27dfb46604449fe4fdfd6caf5e"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"34b86c0400b58616f460181ce7b076439adc99637601bedceef0552d9009fd4a","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:45:39.360Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"b2d659fb-1c1e-4c73-bfeb-ae44fc9a7fed","input_hash":"227bea794dca0741922dedc8987bbbd11c5e7216d9f70d1bdd19daa96c1294e6","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:a459aef9567c69c8b076d9de48944bdf867363e0cdb717526f97b4a5e63e20ef"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"34b86c0400b58616f460181ce7b076439adc99637601bedceef0552d9009fd4a","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:48:03.735Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"bf5ea2bb-d93d-4203-a757-83ffecb66e8c","input_hash":"5369e074243f0a442795f3cf9841e5a6e2e891269326d5fd42eb14790c2a92ff","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:69d0ffe7744bc812778317c98c5f3d860692f4d701e1103e83f092e1884495c1"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:48:22.327Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-001","call_id":"13c4a449-4601-40b2-b767-e46604dfd206","input_hash":"5369e074243f0a442795f3cf9841e5a6e2e891269326d5fd42eb14790c2a92ff","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:69d0ffe7744bc812778317c98c5f3d860692f4d701e1103e83f092e1884495c1"}],"related":[{"type":"specification","value":"INT-001"}]}]}],"response_hash":"8485b90c8cdeaa687c3dfae42a490f17ab2e53084872eb22ad5d696d892aa783","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:58:40.892Z","actor_id":"claude-3-5-sonnet","spec_id":"UNSPECIFIED","call_id":"cb7ad52e-d4e4-41e5-b11d-f90f0966ca62","input_hash":"30fb5b02b653498f6fd0d52f7d685abdff3b4e794974704022e8e71eb849a070","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:ec3059550d88c864d2f1a32771aec62381a2597d1c9ff473505a596a6b67dfef"}],"related":[]}]}],"response_hash":"52762d5198d67e55c76ed01fd9e2ca35a230530feda8845695301c7e9157a582","outcome":"PERMISSION_DENIED","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:59:18.912Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-001","call_id":"31657c1a-b727-4456-8067-630dcf2b1792","input_hash":"30fb5b02b653498f6fd0d52f7d685abdff3b4e794974704022e8e71eb849a070","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:ec3059550d88c864d2f1a32771aec62381a2597d1c9ff473505a596a6b67dfef"}],"related":[{"type":"specification","value":"INT-001"}]}]}],"response_hash":"8485b90c8cdeaa687c3dfae42a490f17ab2e53084872eb22ad5d696d892aa783","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T16:59:45.535Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"350536ad-0bbb-46d5-a4bb-ff069fad9352","input_hash":"30fb5b02b653498f6fd0d52f7d685abdff3b4e794974704022e8e71eb849a070","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:ec3059550d88c864d2f1a32771aec62381a2597d1c9ff473505a596a6b67dfef"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"4609106fbde72f9b6d61648fbba8283036f989385a678b7838f739cafa03c609","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T17:00:23.831Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"e51762dc-b8a2-4842-b1a6-eb37c35071e2","input_hash":"f97ee872ce36ee15a7d8e671840a45e137d12d003ade06570680855fdef0a13c","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:d9953b07972e00e764fe70c298d6abd727ef256172e9b3cd6fcb7034a4df033c"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"36589748650cf2c49682309cd87061feed3462a53bf09e4bfe557b80e0995dd3","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"timestamp":"2026-02-21T17:01:00.180Z","actor_id":"claude-3-5-sonnet","spec_id":"INT-004","call_id":"6eb9e316-0707-4a65-a135-ce8e129754e3","input_hash":"db5e01beca140f08d4e7c13cd52e40b92c71ba753cee565a2ed5762a10252cab","endpoint":"apply_diff","files":[{"relative_path":"bot.py","conversations":[{"url":"session_log_id","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"content_hash":"sha256:de32b585e75d87d2d021da6877eceb463b8d43cb272e7e9798eb477a67398320"}],"related":[{"type":"specification","value":"INT-004"}]}]}],"response_hash":"b3820f88265a8787bb91719a9a652e7ad1999b62e1917cddde1346faadce5668","outcome":"SUCCESS","vcs":{"revision_id":"UNKNOWN"}} +{"id":"2cccabea-31ee-4289-851b-72720f780d72","timestamp":"2026-02-21T17:27:43.641Z","vcs":{"revision_id":"UNKNOWN"}} +{"id":"94cc041b-bcca-4d9b-9838-5586f08a1f11","timestamp":"2026-02-21T17:28:20.217Z","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"tests/test_bot.py","conversations":[{"url":"019c813d-8488-729a-b994-0698037ba856","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"start_line":1,"end_line":1,"content_hash":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"related":[{"type":"specification","value":"INT-001"}]}]}]} +{"id":"ce3a6ad3-9fdd-40ad-a023-342b41889649","timestamp":"2026-02-21T17:28:56.520Z","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"tests/test_bot.py","conversations":[{"url":"019c813d-8488-729a-b994-0698037ba856","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"start_line":1,"end_line":36,"content_hash":"sha256:4793b2eb1bdd3c3f31404891363b2f715447de66d8c0230eea34a1595ca1844a"}],"related":[{"type":"specification","value":"INT-001"}]}]}]} +{"id":"d0401272-1de1-4250-aa53-4271233763ff","timestamp":"2026-02-21T17:57:49.911Z","vcs":{"revision_id":"UNKNOWN"}} +{"id":"df8057db-54b0-41da-a8b0-00c747bc3063","timestamp":"2026-02-21T17:58:03.261Z","vcs":{"revision_id":"UNKNOWN"}} +{"id":"5bee1c84-b451-4e12-b8ee-99ba58058792","timestamp":"2026-02-21T18:02:21.660Z","vcs":{"revision_id":"UNKNOWN"}} +{"id":"5cd7882f-d2b2-43c8-9c6d-a336c6a4500e","timestamp":"2026-02-21T18:28:05.701Z","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"tests/__init__.py","conversations":[{"url":"019c8174-31e7-7509-9240-223fb63b537a","contributor":{"entity_type":"AI","model_identifier":"arcee-ai/trinity-large-preview:free"},"ranges":[{"start_line":1,"end_line":1,"content_hash":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"related":[{"type":"specification","value":"INT-001"}]}]}]} +{"id":"1e663a70-84d8-4bb8-a994-9f833edd7aa9","timestamp":"2026-02-21T18:28:47.524Z","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"tests/test_bot.py","conversations":[{"url":"019c8174-31e7-7509-9240-223fb63b537a","contributor":{"entity_type":"AI","model_identifier":"arcee-ai/trinity-large-preview:free"},"ranges":[{"start_line":1,"end_line":53,"content_hash":"sha256:2378a0c561aaace510374b093e66621c83381fe872872110ba78d798a837c68b"}],"related":[{"type":"specification","value":"INT-001"}]}]}]} +{"id":"b45a79df-8ce7-4f3f-8658-fe90c7f8464e","timestamp":"2026-02-21T18:30:01.823Z","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"tests/test_integration.py","conversations":[{"url":"019c8174-31e7-7509-9240-223fb63b537a","contributor":{"entity_type":"AI","model_identifier":"arcee-ai/trinity-large-preview:free"},"ranges":[{"start_line":1,"end_line":54,"content_hash":"sha256:e9503fe2948194b83d1e31741ffc5922b9d6f942283ef0ae69a51e7cda3a7cbc"}],"related":[{"type":"specification","value":"INT-001"}]}]}]} +{"id":"91f54836-7263-44f9-94c4-893007e66003","timestamp":"2026-02-21T18:32:18.433Z","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"requirements.txt","conversations":[{"url":"019c8174-31e7-7509-9240-223fb63b537a","contributor":{"entity_type":"AI","model_identifier":"arcee-ai/trinity-large-preview:free"},"ranges":[{"start_line":1,"end_line":9,"content_hash":"sha256:e1c39b040435a1fc0823c31d66abb8d4ade1ec0c83975759703eea58c9c7be19"}],"related":[{"type":"specification","value":"INT-001"}]}]}]} +{"id":"3682518d-bb6e-4e40-b839-ce78f642f088","timestamp":"2026-02-21T19:04:32.744Z","classification":"FILE_WRITE","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"bot.py","conversations":[{"url":"019c8193-d377-72f5-b93e-84b78b91248b","contributor":{"entity_type":"AI","model_identifier":"arcee-ai/trinity-large-preview:free"},"ranges":[{"start_line":1,"end_line":38,"content_hash":"sha256:7da9ca2565989636789cb41c248542f36b9abc364fc644922ccbc29686a6fa9d"}],"related":[{"type":"specification","value":"INT-003"}]}]}]} +{"id":"ee619aec-ea09-456c-8f73-f834062100d1","timestamp":"2026-02-21T19:05:42.953Z","classification":"FILE_WRITE","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"bot.py","conversations":[{"url":"019c8193-d377-72f5-b93e-84b78b91248b","contributor":{"entity_type":"AI","model_identifier":"arcee-ai/trinity-large-preview:free"},"ranges":[{"start_line":1,"end_line":38,"content_hash":"sha256:ec27afb4240b3fd5e7f6683ec830b5248ad806279223b08ff49c33bbb0f9de2a"}],"related":[{"type":"specification","value":"INT-003"}]}]}]} +{"id":"1b057636-20ff-4ac3-a159-39392c64819f","timestamp":"2026-02-21T19:06:53.414Z","classification":"FILE_WRITE","vcs":{"revision_id":"UNKNOWN"},"files":[{"relative_path":"bot.py","conversations":[{"url":"019c8193-d377-72f5-b93e-84b78b91248b","contributor":{"entity_type":"AI","model_identifier":"arcee-ai/trinity-large-preview:free"},"ranges":[{"start_line":1,"end_line":38,"content_hash":"sha256:ec27afb4240b3fd5e7f6683ec830b5248ad806279223b08ff49c33bbb0f9de2a"}],"related":[{"type":"specification","value":"INT-003"}]}]}]} diff --git a/.orchestration/intent_map.md b/.orchestration/intent_map.md new file mode 100644 index 00000000000..be81983aff1 --- /dev/null +++ b/.orchestration/intent_map.md @@ -0,0 +1,14 @@ +intents: + +- id: "INT-001" + name: "testing skills" + description: "testing skills" + owned_scope: ["/tests/"] +- id: "INT-002" + name: "project dependencies" + description: "Updating project dependencies" + owned_scope: ["requirements.txt", "pyproject.toml"] +- id: "INT-003" + name: "project documentation" + description: "create or update project documentation" + owned_scope: ["README.md"] diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 4f90b63e9fc..95ebc0b6a55 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -45,6 +45,8 @@ export const toolNames = [ "run_slash_command", "skill", "generate_image", + "select_active_intent", + "record_lesson_learned", "custom_tool", ] as const diff --git a/refactor_test.ts b/refactor_test.ts new file mode 100644 index 00000000000..2112d767ff1 --- /dev/null +++ b/refactor_test.ts @@ -0,0 +1,3 @@ +function test() { + console.log("Original content") +} diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index e0ea1383f17..fff25d51126 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -636,6 +636,42 @@ export class NativeToolCallParser { } break + case "select_active_intent": + if (partialArgs.intent_id !== undefined) { + nativeArgs = { + intent_id: partialArgs.intent_id, + } + } + break + + case "record_lesson_learned": + if (partialArgs.lesson !== undefined) { + nativeArgs = { + lesson: partialArgs.lesson, + } + } + break + + case "access_mcp_resource": + if (partialArgs.server_name !== undefined || partialArgs.uri !== undefined) { + nativeArgs = { + server_name: partialArgs.server_name, + uri: partialArgs.uri, + } + } + break + + case "read_command_output": + if (partialArgs.artifact_id !== undefined) { + nativeArgs = { + artifact_id: partialArgs.artifact_id, + search: partialArgs.search, + offset: this.coerceOptionalNumber(partialArgs.offset), + limit: this.coerceOptionalNumber(partialArgs.limit), + } + } + break + default: break } @@ -965,6 +1001,14 @@ export class NativeToolCallParser { } break + case "record_lesson_learned": + if (args.lesson !== undefined) { + nativeArgs = { + lesson: args.lesson, + } as NativeArgsFor + } + break + case "list_files": if (args.path !== undefined) { nativeArgs = { @@ -984,6 +1028,14 @@ export class NativeToolCallParser { } break + case "select_active_intent": + if (args.intent_id !== undefined) { + nativeArgs = { + intent_id: args.intent_id, + } as NativeArgsFor + } + break + default: if (customToolRegistry.has(resolvedName)) { nativeArgs = args as NativeArgsFor diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 7f5862be154..74b02e96953 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -34,12 +34,15 @@ import { updateTodoListTool } from "../tools/UpdateTodoListTool" import { runSlashCommandTool } from "../tools/RunSlashCommandTool" import { skillTool } from "../tools/SkillTool" import { generateImageTool } from "../tools/GenerateImageTool" +import { selectActiveIntentTool } from "../tools/SelectActiveIntentTool" +import { recordLessonTool } from "../tools/RecordLessonTool" import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool" import { isValidToolName, validateToolUse } from "../tools/validateToolUse" import { codebaseSearchTool } from "../tools/CodebaseSearchTool" import { formatResponse } from "../prompts/responses" import { sanitizeToolUseId } from "../../utils/tool-id" +import { HookContext } from "../../hooks/types" /** * Processes and presents assistant message content to the user interface. @@ -445,6 +448,8 @@ export async function presentAssistantMessage(cline: Task) { // Store approval feedback to merge into tool result (GitHub #10465) let approvalFeedback: { text: string; images?: string[] } | undefined + let toolResult: any + let toolError: any const pushToolResult = (content: ToolResponse) => { // Native tool calling: only allow ONE tool_result per tool call @@ -488,6 +493,7 @@ export async function presentAssistantMessage(cline: Task) { cline.userMessageContent.push(...imageBlocks) } + toolResult = resultContent hasToolResult = true } @@ -675,245 +681,305 @@ export async function presentAssistantMessage(cline: Task) { } } - switch (block.name) { - case "write_to_file": - await checkpointSaveAndMark(cline) - await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "update_todo_list": - await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "apply_diff": - await checkpointSaveAndMark(cline) - await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "edit": - case "search_and_replace": - await checkpointSaveAndMark(cline) - await editTool.handle(cline, block as ToolUse<"edit">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "search_replace": - await checkpointSaveAndMark(cline) - await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "edit_file": - await checkpointSaveAndMark(cline) - await editFileTool.handle(cline, block as ToolUse<"edit_file">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "apply_patch": - await checkpointSaveAndMark(cline) - await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "read_file": - // Type assertion is safe here because we're in the "read_file" case - await readFileTool.handle(cline, block as ToolUse<"read_file">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "list_files": - await listFilesTool.handle(cline, block as ToolUse<"list_files">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "codebase_search": - await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "search_files": - await searchFilesTool.handle(cline, block as ToolUse<"search_files">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "execute_command": - await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "read_command_output": - await readCommandOutputTool.handle(cline, block as ToolUse<"read_command_output">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "use_mcp_tool": - await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "access_mcp_resource": - await accessMcpResourceTool.handle(cline, block as ToolUse<"access_mcp_resource">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "ask_followup_question": - await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "switch_mode": - await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "new_task": - await checkpointSaveAndMark(cline) - await newTaskTool.handle(cline, block as ToolUse<"new_task">, { - askApproval, - handleError, - pushToolResult, - toolCallId: block.id, - }) - break - case "attempt_completion": { - const completionCallbacks: AttemptCompletionCallbacks = { - askApproval, - handleError, - pushToolResult, - askFinishSubTaskApproval, - toolDescription, + // 4. Hook Engine Orchestration + await cline.initializationPromise + + const hookContext: HookContext = { + task: cline, + toolName: block.name, + arguments: block.nativeArgs || block.params || {}, + intentId: cline.intentController.getActiveIntentId(), + timestamp: new Date().toISOString(), + } + + try { + if (!block.partial) { + const preHookResponse = await cline.hookEngine.executePreHooks(hookContext) + if (!preHookResponse.allow) { + const errorMessage = preHookResponse.reason || "Access Denied by Hook Engine" + const errorContent = formatResponse.toolError(errorMessage) + await cline.say("error", errorMessage) + cline.pushToolResultToUserContent({ + type: "tool_result", + tool_use_id: sanitizeToolUseId(toolCallId), + content: typeof errorContent === "string" ? errorContent : "Access Denied", + is_error: true, + }) + toolResult = errorContent + hasToolResult = true + cline.didAlreadyUseTool = true // Interrupt the stream + break // Exit the while loop } - await attemptCompletionTool.handle( - cline, - block as ToolUse<"attempt_completion">, - completionCallbacks, - ) - break } - case "run_slash_command": - await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "skill": - await skillTool.handle(cline, block as ToolUse<"skill">, { - askApproval, - handleError, - pushToolResult, - }) - break - case "generate_image": - await checkpointSaveAndMark(cline) - await generateImageTool.handle(cline, block as ToolUse<"generate_image">, { - askApproval, - handleError, - pushToolResult, - }) - break - default: { - // Handle unknown/invalid tool names OR custom tools - // This is critical for native tool calling where every tool_use MUST have a tool_result - - // CRITICAL: Don't process partial blocks for unknown tools - just let them stream in. - // If we try to show errors for partial blocks, we'd show the error on every streaming chunk, - // creating a loop that appears to freeze the extension. Only handle complete blocks. - if (block.partial) { + + switch (block.name) { + case "write_to_file": + await checkpointSaveAndMark(cline) + await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "update_todo_list": + await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "apply_diff": + await checkpointSaveAndMark(cline) + await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "edit": + case "search_and_replace": + await checkpointSaveAndMark(cline) + await editTool.handle(cline, block as ToolUse<"edit">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "search_replace": + await checkpointSaveAndMark(cline) + await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "edit_file": + await checkpointSaveAndMark(cline) + await editFileTool.handle(cline, block as ToolUse<"edit_file">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "apply_patch": + await checkpointSaveAndMark(cline) + await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "read_file": + // Type assertion is safe here because we're in the "read_file" case + await readFileTool.handle(cline, block as ToolUse<"read_file">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "list_files": + await listFilesTool.handle(cline, block as ToolUse<"list_files">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "codebase_search": + await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "search_files": + await searchFilesTool.handle(cline, block as ToolUse<"search_files">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "execute_command": + await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "read_command_output": + await readCommandOutputTool.handle(cline, block as ToolUse<"read_command_output">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "use_mcp_tool": + await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "record_lesson_learned": + await recordLessonTool.handle(cline, block as ToolUse<"record_lesson_learned">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "access_mcp_resource": + await accessMcpResourceTool.handle(cline, block as ToolUse<"access_mcp_resource">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "ask_followup_question": + await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "switch_mode": + await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "new_task": + await checkpointSaveAndMark(cline) + await newTaskTool.handle(cline, block as ToolUse<"new_task">, { + askApproval, + handleError, + pushToolResult, + toolCallId: block.id, + }) + break + case "attempt_completion": { + const completionCallbacks: AttemptCompletionCallbacks = { + askApproval, + handleError, + pushToolResult, + askFinishSubTaskApproval, + toolDescription, + } + await attemptCompletionTool.handle( + cline, + block as ToolUse<"attempt_completion">, + completionCallbacks, + ) break } + case "run_slash_command": + await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "skill": + await skillTool.handle(cline, block as ToolUse<"skill">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "select_active_intent": + await selectActiveIntentTool.handle(cline, block as ToolUse<"select_active_intent">, { + askApproval, + handleError, + pushToolResult, + }) + break + case "generate_image": + await checkpointSaveAndMark(cline) + await generateImageTool.handle(cline, block as ToolUse<"generate_image">, { + askApproval, + handleError, + pushToolResult, + }) + break + default: { + // Handle unknown/invalid tool names OR custom tools + // This is critical for native tool calling where every tool_use MUST have a tool_result + + // CRITICAL: Don't process partial blocks for unknown tools - just let them stream in. + // If we try to show errors for partial blocks, we'd show the error on every streaming chunk, + // creating a loop that appears to freeze the extension. Only handle complete blocks. + if (block.partial) { + break + } - const customTool = stateExperiments?.customTools ? customToolRegistry.get(block.name) : undefined - - if (customTool) { - try { - let customToolArgs - - if (customTool.parameters) { - try { - customToolArgs = customTool.parameters.parse(block.nativeArgs || block.params || {}) - } catch (parseParamsError) { - const message = `Custom tool "${block.name}" argument validation failed: ${parseParamsError.message}` - console.error(message) - cline.consecutiveMistakeCount++ - await cline.say("error", message) - pushToolResult(formatResponse.toolError(message)) - break + const customTool = stateExperiments?.customTools + ? customToolRegistry.get(block.name) + : undefined + + if (customTool) { + try { + let customToolArgs + + if (customTool.parameters) { + try { + customToolArgs = customTool.parameters.parse( + block.nativeArgs || block.params || {}, + ) + } catch (parseParamsError) { + const message = `Custom tool "${block.name}" argument validation failed: ${parseParamsError.message}` + console.error(message) + cline.consecutiveMistakeCount++ + await cline.say("error", message) + pushToolResult(formatResponse.toolError(message)) + break + } } + + const result = await customTool.execute(customToolArgs, { + mode: mode ?? defaultModeSlug, + task: cline, + }) + + console.log( + `${customTool.name}.execute(): ${JSON.stringify(customToolArgs)} -> ${JSON.stringify(result)}`, + ) + + pushToolResult(result) + cline.consecutiveMistakeCount = 0 + } catch (executionError: any) { + cline.consecutiveMistakeCount++ + // Record custom tool error with static name + cline.recordToolError("custom_tool", executionError.message) + await handleError(`executing custom tool "${block.name}"`, executionError) } - const result = await customTool.execute(customToolArgs, { - mode: mode ?? defaultModeSlug, - task: cline, - }) - - console.log( - `${customTool.name}.execute(): ${JSON.stringify(customToolArgs)} -> ${JSON.stringify(result)}`, - ) - - pushToolResult(result) - cline.consecutiveMistakeCount = 0 - } catch (executionError: any) { - cline.consecutiveMistakeCount++ - // Record custom tool error with static name - cline.recordToolError("custom_tool", executionError.message) - await handleError(`executing custom tool "${block.name}"`, executionError) + break } + // Not a custom tool - handle as unknown tool error + const errorMessage = `Unknown tool "${block.name}". This tool does not exist. Please use one of the available tools.` + cline.consecutiveMistakeCount++ + cline.recordToolError(block.name as ToolName, errorMessage) + await cline.say("error", t("tools:unknownToolError", { toolName: block.name })) + // Push tool_result directly WITHOUT setting didAlreadyUseTool + // This prevents the stream from being interrupted with "Response interrupted by tool use result" + cline.pushToolResultToUserContent({ + type: "tool_result", + tool_use_id: sanitizeToolUseId(toolCallId), + content: formatResponse.toolError(errorMessage), + is_error: true, + }) break } - - // Not a custom tool - handle as unknown tool error - const errorMessage = `Unknown tool "${block.name}". This tool does not exist. Please use one of the available tools.` - cline.consecutiveMistakeCount++ - cline.recordToolError(block.name as ToolName, errorMessage) - await cline.say("error", t("tools:unknownToolError", { toolName: block.name })) - // Push tool_result directly WITHOUT setting didAlreadyUseTool - // This prevents the stream from being interrupted with "Response interrupted by tool use result" - cline.pushToolResultToUserContent({ - type: "tool_result", - tool_use_id: sanitizeToolUseId(toolCallId), - content: formatResponse.toolError(errorMessage), - is_error: true, - }) - break + } + } catch (error) { + toolError = error + throw error + } finally { + // 5. Post-Hook Execution + if (!block.partial) { + hookContext.result = toolResult + hookContext.error = toolError + await cline.hookEngine.executePostHooks(hookContext) } } diff --git a/src/core/intent/IntentController.ts b/src/core/intent/IntentController.ts new file mode 100644 index 00000000000..da863bdd02a --- /dev/null +++ b/src/core/intent/IntentController.ts @@ -0,0 +1,212 @@ +import path from "path" +import fs from "fs/promises" +import crypto from "crypto" +import * as vscode from "vscode" +import YAML from "yaml" +import { fileExistsAtPath } from "../../utils/fs" + +import ignore, { Ignore } from "ignore" +import "../../utils/path" // For toPosix + +export interface Intent { + id: string + name: string + status: "CREATED" | "IN_PROGRESS" | "COMPLETED" | string + description: string + constraints: string[] + owned_scope: string[] + acceptance_criteria?: string[] +} + +export interface ActiveIntentsConfig { + active_intents: Intent[] +} + +/** + * Manages active intents and enforces architectural constraints. + */ +export class IntentController { + private cwd: string + private disposables: vscode.Disposable[] = [] + private activeIntents: Intent[] = [] + private ignoreInstances: Map = new Map() + private activeIntentId?: string + private readHashes: Map = new Map() + + constructor(cwd: string) { + this.cwd = cwd + } + + async initialize(): Promise { + await this.loadActiveIntents() + this.setupFileWatcher() + } + + getActiveIntentId(): string | undefined { + return this.activeIntentId + } + + setActiveIntentId(intentId: string | undefined): void { + this.activeIntentId = intentId + } + + private setupFileWatcher(): void { + const pattern = new vscode.RelativePattern(path.join(this.cwd, ".orchestration"), "active_intents.yaml") + const fileWatcher = vscode.workspace.createFileSystemWatcher(pattern) + + this.disposables.push( + fileWatcher.onDidChange(() => this.loadActiveIntents()), + fileWatcher.onDidCreate(() => this.loadActiveIntents()), + fileWatcher.onDidDelete(() => { + this.activeIntents = [] + this.ignoreInstances.clear() + }), + ) + + this.disposables.push(fileWatcher) + } + + private async loadActiveIntents(): Promise { + try { + const activePath = path.join(this.cwd, ".orchestration", "active_intents.yaml") + if (await fileExistsAtPath(activePath)) { + const content = await fs.readFile(activePath, "utf8") + const config = YAML.parse(content) as any + this.activeIntents = (config.active_intents || config.intents || []).map((intent: any) => ({ + ...intent, + status: (intent.status || "CREATED").toUpperCase(), + constraints: Array.isArray(intent.constraints) ? intent.constraints : [], + owned_scope: Array.isArray(intent.owned_scope) ? intent.owned_scope : [], + acceptance_criteria: Array.isArray(intent.acceptance_criteria) ? intent.acceptance_criteria : [], + })) + + // Refresh ignore instances for scope enforcement + this.ignoreInstances.clear() + for (const intent of this.activeIntents) { + if (intent.owned_scope.length > 0) { + const ig = ignore().add(intent.owned_scope) + this.ignoreInstances.set(intent.id, ig) + } + } + } else { + // Legacy support or fallback to optional map + const mapPath = path.join(this.cwd, ".orchestration", "intent_map.yaml") + if (await fileExistsAtPath(mapPath)) { + const content = await fs.readFile(mapPath, "utf8") + const config = YAML.parse(content) as any + this.activeIntents = config.active_intents || config.intents || [] + } else { + this.activeIntents = [] + } + this.ignoreInstances.clear() + } + } catch (error) { + console.error("Error loading active_intents.yaml:", error) + } + } + + getAllActiveIntents(): Intent[] { + return this.activeIntents + } + + getIntent(intentId: string): Intent | undefined { + return this.activeIntents.find((i) => i.id === intentId) + } + + async updateIntentStatus(intentId: string, status: Intent["status"]): Promise { + const intent = this.getIntent(intentId) + if (intent) { + intent.status = status + await this.saveActiveIntents() + } + } + + async addActiveIntent(intent: Intent): Promise { + this.activeIntents.push(intent) + await this.saveActiveIntents() + } + + private async saveActiveIntents(): Promise { + try { + const activePath = path.join(this.cwd, ".orchestration", "active_intents.yaml") + const content = YAML.stringify({ + active_intents: this.activeIntents, + last_updated: new Date().toISOString(), + }) + await fs.writeFile(activePath, content, "utf8") + } catch (error) { + console.error("Error persisting active intents:", error) + } + } + + /** + * Validates if a file path is within the owned scope of ANY active intent. + * If no intents are active, this typically returns true (governance is opt-in per intent). + * However, if projects strictly require an intent, that is handled by the pre-hook. + */ + validateScope(filePath: string, intentId?: string): boolean { + // Normalize path to be relative to cwd for the ignore library + // This handles absolute paths, ./ prefixes, and platform-specific separators + const absolutePath = path.resolve(this.cwd, filePath) + const posixPath = path.relative(this.cwd, absolutePath).toPosix() + + if (intentId) { + const ig = this.ignoreInstances.get(intentId) + return ig ? ig.ignores(posixPath) : true + } + + // Check if it matches ANY active intent's scope + if (this.ignoreInstances.size === 0) { + return true + } + + for (const ig of this.ignoreInstances.values()) { + if (ig.ignores(posixPath)) { + return true + } + } + + return false + } + + isDestructiveCommand(command: string): boolean { + const safeCommands = [ + "ls", + "dir", + "pwd", + "git status", + "git log", + "git diff", + "cat", + "type", + "git branch", + "git show", + "find", + "grep", + "pnpm test", + "npm test", + ] + + const trimmedCommand = command.trim() + if (safeCommands.some((c) => trimmedCommand.startsWith(c))) { + return false + } + + return true + } + + recordReadHash(filePath: string, hash: string): void { + const absolutePath = path.resolve(this.cwd, filePath) + this.readHashes.set(absolutePath, hash) + } + + getReadHash(filePath: string): string | undefined { + const absolutePath = path.resolve(this.cwd, filePath) + return this.readHashes.get(absolutePath) + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()) + this.disposables = [] + } +} diff --git a/src/core/prompts/sections/index.ts b/src/core/prompts/sections/index.ts index 318cd47bc9d..64127bd6b4b 100644 --- a/src/core/prompts/sections/index.ts +++ b/src/core/prompts/sections/index.ts @@ -8,3 +8,4 @@ export { getCapabilitiesSection } from "./capabilities" export { getModesSection } from "./modes" export { markdownFormattingSection } from "./markdown-formatting" export { getSkillsSection } from "./skills" +export { getIntentProtocolSection } from "./intent-protocol" diff --git a/src/core/prompts/sections/intent-protocol.ts b/src/core/prompts/sections/intent-protocol.ts new file mode 100644 index 00000000000..9d162bacff4 --- /dev/null +++ b/src/core/prompts/sections/intent-protocol.ts @@ -0,0 +1,24 @@ +import { Intent } from "../../intent/IntentController" + +export function getIntentProtocolSection(intents: Intent[]): string { + const intentCatalog = + intents.length > 0 + ? intents.map((i) => `- **${i.id}** (${i.name}): ${i.description}`).join("\n") + : "No active intents. If this is a governed project, check `.orchestration/intent_map.md` for a spatial map and initialize `.orchestration/active_intents.yaml` with your target intent." + + return `## Intent-Driven Development Protocol + +You are an Intent-Driven Architect. To ensure architectural governance and traceability, you MUST follow this protocol: + +### Active Intent Catalog +${intentCatalog} + +### Protocol Rules +1. **Analyze First**: Before making any changes, analyze your task against the business intents defined in \`.orchestration/active_intents.yaml\` and the spatial map in \`.orchestration/intent_map.md\`. +2. **Select Intent**: You MUST call \`select_active_intent\` to activate an ID from the specification for the **CURRENT SESSION**. This is required even if the intent status in the YAML is already "IN_PROGRESS", as the system needs to explicitly load session-level filters and constraints. +3. **Paths**: Use root-relative paths for all governance files in \`.orchestration/\`. +4. **Initialization**: If governance is not set up, your first task is to help the user initialize the \`.orchestration/\` folder with an \`intent_map.md\` and \`active_intents.yaml\`. +5. **Rejection Recovery**: If a tool call is rejected with "Access Denied" or "Security Violation", your **IMMEDIATE NEXT RESPONSE** must be a call to \`select_active_intent\` or \`ask_followup_question\`. + +**CRITICAL**: You CANNOT write code or execute destructive commands without an active intent in 'IN_PROGRESS' status that has been explicitly activated in this session.` +} diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 0d6071644a9..b74b38e7bdb 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -23,7 +23,9 @@ import { addCustomInstructions, markdownFormattingSection, getSkillsSection, + getIntentProtocolSection, } from "./sections" +import { Intent } from "../intent/IntentController" // Helper function to get prompt component, filtering out empty objects export function getPromptComponent( @@ -55,6 +57,7 @@ async function generatePrompt( todoList?: TodoItem[], modelId?: string, skillsManager?: SkillsManager, + intents?: Intent[], ): Promise { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -85,6 +88,8 @@ async function generatePrompt( const basePrompt = `${roleDefinition} ${markdownFormattingSection()} + +${getIntentProtocolSection(intents || [])} ${getSharedToolUseSection()}${toolsCatalog} @@ -126,6 +131,7 @@ export const SYSTEM_PROMPT = async ( todoList?: TodoItem[], modelId?: string, skillsManager?: SkillsManager, + intents?: Intent[], ): Promise => { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -154,5 +160,6 @@ export const SYSTEM_PROMPT = async ( todoList, modelId, skillsManager, + intents, ) } diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 758914d2d65..adbb294ecb2 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -20,6 +20,7 @@ import searchFiles from "./search_files" import switchMode from "./switch_mode" import updateTodoList from "./update_todo_list" import writeToFile from "./write_to_file" +import selectActiveIntent from "./select_active_intent" export { getMcpServerTools } from "./mcp_server" export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters" @@ -68,6 +69,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch switchMode, updateTodoList, writeToFile, + selectActiveIntent, ] satisfies OpenAI.Chat.ChatCompletionTool[] } diff --git a/src/core/prompts/tools/native-tools/select_active_intent.ts b/src/core/prompts/tools/native-tools/select_active_intent.ts new file mode 100644 index 00000000000..f7eb7d46380 --- /dev/null +++ b/src/core/prompts/tools/native-tools/select_active_intent.ts @@ -0,0 +1,25 @@ +import type OpenAI from "openai" + +const SELECT_ACTIVE_INTENT_DESCRIPTION = `Activate a specific intent for the current session to load its architectural constraints, filters, and owned scope. You MUST call this tool at the start of your task (even if the intent is already marked "IN_PROGRESS" in the file) before using any destructive tools.` + +const INTENT_ID_PARAMETER_DESCRIPTION = `The ID of the active intent to load (e.g., 'INT-XXX').` + +export default { + type: "function", + function: { + name: "select_active_intent", + description: SELECT_ACTIVE_INTENT_DESCRIPTION, + strict: true, + parameters: { + type: "object", + properties: { + intent_id: { + type: "string", + description: INTENT_ID_PARAMETER_DESCRIPTION, + }, + }, + required: ["intent_id"], + additionalProperties: false, + }, + }, +} satisfies OpenAI.Chat.ChatCompletionTool diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 6ba57e98ac3..1bcaae2071e 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -102,7 +102,15 @@ import { restoreTodoListForTask } from "../tools/UpdateTodoListTool" import { FileContextTracker } from "../context-tracking/FileContextTracker" import { RooIgnoreController } from "../ignore/RooIgnoreController" import { RooProtectedController } from "../protect/RooProtectedController" -import { type AssistantMessageContent, presentAssistantMessage } from "../assistant-message" +import { IntentController } from "../intent/IntentController" +import { HookEngine } from "../../hooks/HookEngine" +import { IntentValidationHook } from "../../hooks/pre/IntentValidationHook" +import { ScopeEnforcementHook } from "../../hooks/pre/ScopeEnforcementHook" +import { TraceLoggingHook } from "../../hooks/post/TraceLoggingHook" +import { ConcurrencyPreHook } from "../../hooks/pre/ConcurrencyPreHook" +import { ConcurrencyPostHook } from "../../hooks/post/ConcurrencyPostHook" +import { VerificationLessonHook } from "../../hooks/post/VerificationLessonHook" +import { AssistantMessageContent, presentAssistantMessage } from "../assistant-message" import { NativeToolCallParser } from "../assistant-message/NativeToolCallParser" import { manageContext, willManageContext } from "../context-management" import { ClineProvider } from "../webview/ClineProvider" @@ -220,6 +228,11 @@ export class Task extends EventEmitter implements TaskLike { * @see {@link waitForModeInitialization} - Public method to await this promise */ private taskModeReady: Promise + /** + * Promise that resolves when all core controllers (Ignore, Protected, Intent) + * have been fully initialized. Ensures governance hooks have required state. + */ + public readonly initializationPromise: Promise /** * The API configuration name (provider profile) associated with this task. @@ -298,6 +311,8 @@ export class Task extends EventEmitter implements TaskLike { toolRepetitionDetector: ToolRepetitionDetector rooIgnoreController?: RooIgnoreController rooProtectedController?: RooProtectedController + intentController: IntentController + hookEngine: HookEngine fileContextTracker: FileContextTracker terminalProcess?: RooTerminalProcess @@ -483,9 +498,24 @@ export class Task extends EventEmitter implements TaskLike { this.rooProtectedController = new RooProtectedController(this.cwd) this.fileContextTracker = new FileContextTracker(provider, this.taskId) - this.rooIgnoreController.initialize().catch((error) => { - console.error("Failed to initialize RooIgnoreController:", error) - }) + // Controllers must be initialized before hooks run to prevent race conditions + this.initializationPromise = Promise.all([ + this.rooIgnoreController.initialize().catch((error) => { + console.error("Failed to initialize RooIgnoreController:", error) + }), + (this.intentController = new IntentController(this.cwd)), + this.intentController.initialize().catch((error) => { + console.error("Failed to initialize IntentController:", error) + }), + ]).then(() => {}) + + this.hookEngine = new HookEngine() + this.hookEngine.registerHook(new IntentValidationHook()) + this.hookEngine.registerHook(new ScopeEnforcementHook()) + this.hookEngine.registerHook(new ConcurrencyPreHook()) + this.hookEngine.registerHook(new TraceLoggingHook()) + this.hookEngine.registerHook(new ConcurrencyPostHook()) + this.hookEngine.registerHook(new VerificationLessonHook()) this.apiConfiguration = apiConfiguration this.api = buildApiHandler(this.apiConfiguration) @@ -3815,6 +3845,7 @@ export class Task extends EventEmitter implements TaskLike { undefined, // todoList this.api.getModel().id, provider.getSkillsManager(), + this.intentController.getAllActiveIntents(), ) })() } diff --git a/src/core/tools/RecordLessonTool.ts b/src/core/tools/RecordLessonTool.ts new file mode 100644 index 00000000000..d5b6feef27a --- /dev/null +++ b/src/core/tools/RecordLessonTool.ts @@ -0,0 +1,34 @@ +import fs from "fs/promises" +import path from "path" +import { Task } from "../task/Task" +import { BaseTool, ToolCallbacks } from "./BaseTool" + +interface RecordLessonParams { + lesson: string +} + +export class RecordLessonTool extends BaseTool<"record_lesson_learned"> { + readonly name = "record_lesson_learned" as const + + async execute(params: RecordLessonParams, task: Task, callbacks: ToolCallbacks): Promise { + const { pushToolResult, handleError } = callbacks + const { lesson } = params + + try { + const agentsPath = path.join(task.cwd, "AGENTS.md") + + // Format the lesson entry + const timestamp = new Date().toISOString() + const lessonEntry = `\n\n### Lessons Learned (${timestamp})\n- ${lesson}\n` + + // Append to AGENTS.md (create if not exists) + await fs.appendFile(agentsPath, lessonEntry, "utf8") + + pushToolResult(`Successfully recorded lesson to AGENTS.md`) + } catch (error) { + await handleError("recording lesson", error as Error) + } + } +} + +export const recordLessonTool = new RecordLessonTool() diff --git a/src/core/tools/SelectActiveIntentTool.ts b/src/core/tools/SelectActiveIntentTool.ts new file mode 100644 index 00000000000..8d4c6d73b22 --- /dev/null +++ b/src/core/tools/SelectActiveIntentTool.ts @@ -0,0 +1,60 @@ +import { Task } from "../task/Task" +import { BaseTool, ToolCallbacks } from "./BaseTool" + +interface SelectActiveIntentParams { + intent_id: string +} + +export class SelectActiveIntentTool extends BaseTool<"select_active_intent"> { + readonly name = "select_active_intent" as const + + async execute(params: SelectActiveIntentParams, task: Task, callbacks: ToolCallbacks): Promise { + const { pushToolResult, handleError } = callbacks + const { intent_id } = params + + try { + if (!task.intentController) { + throw new Error("IntentController not initialized") + } + + // In this advanced model, search for the intent in the active list first + let intent = task.intentController.getIntent(intent_id) + + if (!intent) { + pushToolResult( + `Error: Intent ID '${intent_id}' not found in .orchestration/active_intents.yaml. You may need to ask the user to add it or use an existing one from .orchestration/intent_map.md.`, + ) + return + } + + // Update status if it's not already in progress + if (intent.status !== "IN_PROGRESS") { + await task.intentController.updateIntentStatus(intent_id, "IN_PROGRESS") + } + + // Essential: Set the session-level active intent ID for the Hook Engine + task.intentController.setActiveIntentId(intent_id) + + const contextBlock = [ + ``, + `ID: ${intent.id}`, + `Name: ${intent.name}`, + `Status: ${intent.status}`, + `Description: ${intent.description}`, + `Constraints:`, + ...(intent.constraints || []).map((c) => `- ${c}`), + `Owned Scope:`, + ...(intent.owned_scope || []).map((s) => `- ${s}`), + `Acceptance Criteria:`, + ...(intent.acceptance_criteria || []).map((ac) => `- ${ac}`), + ``, + ].join("\n") + + pushToolResult(contextBlock) + } catch (error) { + await handleError("selecting active intent", error as Error) + } + } +} + +export const selectActiveIntentTool = new SelectActiveIntentTool() diff --git a/src/core/tools/UpdateTodoListTool.ts b/src/core/tools/UpdateTodoListTool.ts index 7414b713cf4..6b58c7eb71a 100644 --- a/src/core/tools/UpdateTodoListTool.ts +++ b/src/core/tools/UpdateTodoListTool.ts @@ -188,10 +188,7 @@ export function parseMarkdownChecklist(md: string): TodoItem[] { let status: TodoStatus = "pending" if (match[1] === "x" || match[1] === "X") status = "completed" else if (match[1] === "-" || match[1] === "~") status = "in_progress" - const id = crypto - .createHash("md5") - .update(match[2] + status) - .digest("hex") + const id = crypto.createHash("md5").update(match[2]).digest("hex") todos.push({ id, content: match[2], diff --git a/src/hooks/HookContext.ts b/src/hooks/HookContext.ts new file mode 100644 index 00000000000..a33565b6ad4 --- /dev/null +++ b/src/hooks/HookContext.ts @@ -0,0 +1,6 @@ +/** + * Shared context for hook execution. + */ +export class HookContext { + // Skeleton for context state +} diff --git a/src/hooks/HookEngine.ts b/src/hooks/HookEngine.ts new file mode 100644 index 00000000000..c63b53816ad --- /dev/null +++ b/src/hooks/HookEngine.ts @@ -0,0 +1,33 @@ +import { IToolHook, HookType, HookContext, HookResponse } from "./types" + +/** + * HookEngine manages the execution of pre and post tool hooks. + */ +export class HookEngine { + private preHooks: IToolHook[] = [] + private postHooks: IToolHook[] = [] + + registerHook(hook: IToolHook) { + if (hook.type === HookType.PRE) { + this.preHooks.push(hook) + } else { + this.postHooks.push(hook) + } + } + + async executePreHooks(context: HookContext): Promise { + for (const hook of this.preHooks) { + const response = await hook.execute(context) + if (response && !response.allow) { + return response + } + } + return { allow: true } + } + + async executePostHooks(context: HookContext): Promise { + for (const hook of this.postHooks) { + await hook.execute(context) + } + } +} diff --git a/src/hooks/post/ConcurrencyPostHook.ts b/src/hooks/post/ConcurrencyPostHook.ts new file mode 100644 index 00000000000..8e7a0a179f3 --- /dev/null +++ b/src/hooks/post/ConcurrencyPostHook.ts @@ -0,0 +1,26 @@ +import crypto from "crypto" +import { IToolHook, HookType, HookContext } from "../types" + +/** + * Records the hash of files when they are read by the agent to enable optimistic locking. + */ +export class ConcurrencyPostHook implements IToolHook { + name = "ConcurrencyPostHook" + type = HookType.POST + + async execute(context: HookContext): Promise { + const { task, toolName, arguments: toolArgs, result, error } = context + + // If the tool failed, don't record anything + if (error) return + + if (toolName === "read_file") { + const filePath = toolArgs.path as string + const content = result as string + if (!filePath || typeof content !== "string") return + + const hash = crypto.createHash("sha256").update(content).digest("hex") + task.intentController.recordReadHash(filePath, hash) + } + } +} diff --git a/src/hooks/post/TraceLoggingHook.ts b/src/hooks/post/TraceLoggingHook.ts new file mode 100644 index 00000000000..b4a3855c0c3 --- /dev/null +++ b/src/hooks/post/TraceLoggingHook.ts @@ -0,0 +1,103 @@ +import path from "path" +import fs from "fs/promises" +import crypto from "crypto" +import { v4 as uuidv4 } from "uuid" +import { execa } from "execa" +import { IToolHook, HookType, HookContext } from "../types" + +import { getModelId } from "@roo-code/types" + +/** + * Automates project governance by logging all intent-driven changes with high fidelity. + */ +export class TraceLoggingHook implements IToolHook { + name = "TraceLoggingHook" + type = HookType.POST + + async execute(context: HookContext): Promise { + const { task, toolName, intentId, arguments: toolArgs, timestamp, result, error } = context + + // Only log mutating actions for the advanced ledger + const isMutating = toolName === "write_to_file" || toolName === "apply_diff" || toolName === "execute_command" + if (!isMutating) { + return + } + + try { + const logPath = path.join(task.cwd, ".orchestration", "agent_trace.jsonl") + + // Get current Git revision + let revisionId = "UNKNOWN" + try { + const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd: task.cwd }) + revisionId = stdout.trim() + } catch (gitErr) { + // Fallback if not a git repo + } + + // Generate content hash and ranges for file writes + const files: any[] = [] + if (toolName === "write_to_file" || toolName === "apply_diff") { + const filePath = (toolArgs.path || toolArgs.file_path) as string + const absolutePath = path.resolve(task.cwd, filePath) + + try { + const content = await fs.readFile(absolutePath, "utf8") + const hash = crypto.createHash("sha256").update(content).digest("hex") + const lineCount = content.split("\n").length + + // Heuristic for range: for write_to_file it's the whole file + // For apply_diff, we could parse the diff, but for now we'll use the whole file + // or specific lines if we can extract them. Let's use 1 to lineCount as a safe default. + let startLine = 1 + let endLine = lineCount + + // Try to extract range from diff if it's apply_diff + if (toolName === "apply_diff" && toolArgs.diff) { + const match = toolArgs.diff.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/) + if (match) { + startLine = parseInt(match[2], 10) + // Fallback endLine to lineCount or estimate from diff + } + } + + files.push({ + relative_path: filePath, + conversations: [ + { + url: task.taskId, + contributor: { + entity_type: "AI", + model_identifier: getModelId(task.apiConfiguration) || "claude-3-5-sonnet", + }, + ranges: [ + { + start_line: startLine, + end_line: endLine, + content_hash: `sha256:${hash}`, + }, + ], + related: intentId ? [{ type: "specification", value: intentId }] : [], + }, + ], + }) + } catch (fileErr) { + console.error(`Error reading file for trace logging: ${filePath}`, fileErr) + } + } + + const entry = { + id: uuidv4(), + timestamp, + classification: toolName === "apply_diff" ? "AST_REFACTOR" : "FILE_WRITE", + vcs: { revision_id: revisionId }, + files: files.length > 0 ? files : undefined, + } + + const logLine = JSON.stringify(entry) + "\n" + await fs.appendFile(logPath, logLine, "utf8") + } catch (err) { + console.error("Error writing to agent_trace.jsonl:", err) + } + } +} diff --git a/src/hooks/post/VerificationLessonHook.ts b/src/hooks/post/VerificationLessonHook.ts new file mode 100644 index 00000000000..fc54462591f --- /dev/null +++ b/src/hooks/post/VerificationLessonHook.ts @@ -0,0 +1,41 @@ +import { IToolHook, HookType, HookContext } from "../types" + +/** + * Automatically records "Lessons Learned" to AGENTS.md when a verification step fails. + */ +export class VerificationLessonHook implements IToolHook { + name = "VerificationLessonHook" + type = HookType.POST + + async execute(context: HookContext): Promise { + const { task, toolName, arguments: toolArgs, result, error } = context + + // We only care about failures + if (!error) return + + // Identify verification tools + // Usually execute_command for npm test, etc. + if (toolName === "execute_command") { + const command = (toolArgs.command || "").toLowerCase() + const verificationKeywords = ["test", "lint", "check", "verify", "tsc", "build"] + + const isVerification = verificationKeywords.some((kw) => command.includes(kw)) + + if (isVerification) { + const lesson = `Verification failed for command: '${toolArgs.command}'. Error: ${error.message || error}` + + // Use the tool's logic without a real tool call to avoid loops + const agentsPath = require("path").join(task.cwd, "AGENTS.md") + const timestamp = new Date().toISOString() + const lessonEntry = `\n\n### Lessons Learned (${timestamp})\n- ${lesson}\n` + + try { + await require("fs/promises").appendFile(agentsPath, lessonEntry, "utf8") + console.log(`[VerificationLessonHook] Auto-recorded lesson for failed command: ${command}`) + } catch (err) { + console.error(`[VerificationLessonHook] Failed to record lesson:`, err) + } + } + } + } +} diff --git a/src/hooks/pre/ConcurrencyPreHook.ts b/src/hooks/pre/ConcurrencyPreHook.ts new file mode 100644 index 00000000000..e807e5e7541 --- /dev/null +++ b/src/hooks/pre/ConcurrencyPreHook.ts @@ -0,0 +1,46 @@ +import path from "path" +import fs from "fs/promises" +import crypto from "crypto" +import { IToolHook, HookType, HookContext, HookResponse } from "../types" + +/** + * Enforces optimistic locking by blocking writes if the file has changed on disk + * since it was last read by this agent session. + */ +export class ConcurrencyPreHook implements IToolHook { + name = "ConcurrencyPreHook" + type = HookType.PRE + + async execute(context: HookContext): Promise { + const { task, toolName, arguments: toolArgs } = context + + if (toolName === "write_to_file" || toolName === "apply_diff") { + const filePath = (toolArgs.path || toolArgs.file_path) as string + if (!filePath) return + + const absolutePath = path.resolve(task.cwd, filePath) + const storedHash = task.intentController.getReadHash(absolutePath) + + // If we haven't read this file yet in this session, we don't have a "read hash" + // This might happen if the agent assumes the file content or it's a new file. + if (!storedHash) { + return + } + + try { + const currentContent = await fs.readFile(absolutePath, "utf8") + const currentHash = crypto.createHash("sha256").update(currentContent).digest("hex") + + if (currentHash !== storedHash) { + return { + allow: false, + reason: `Stale File Error: File '${filePath}' has been modified by another agent or user since you last read it. You MUST re-read the file using 'read_file' to synchronize your state before attempting to write.`, + } + } + } catch (error) { + // If file doesn't exist, it's not a collision (it might be a new file being created) + // or some other error happened - let the original tool handle it. + } + } + } +} diff --git a/src/hooks/pre/IntentValidationHook.ts b/src/hooks/pre/IntentValidationHook.ts new file mode 100644 index 00000000000..71d44e44a10 --- /dev/null +++ b/src/hooks/pre/IntentValidationHook.ts @@ -0,0 +1,45 @@ +import { IToolHook, HookType, HookContext, HookResponse } from "../types" + +/** + * Validates that an active intent is selected before executing destructive tools. + */ +export class IntentValidationHook implements IToolHook { + name = "IntentValidationHook" + type = HookType.PRE + + private destructiveTools = [ + "write_to_file", + "execute_command", + "apply_diff", + "edit", + "search_and_replace", + "search_replace", + "edit_file", + "apply_patch", + ] + + async execute(context: HookContext): Promise { + const { task, toolName, intentId, arguments: toolArgs } = context + + if (this.destructiveTools.includes(toolName)) { + let shouldBlock = !intentId + + // If it's a command, check if it's actually destructive + if (toolName === "execute_command") { + const command = toolArgs.command || "" + shouldBlock = shouldBlock && task.intentController.isDestructiveCommand(command) + } + + if (shouldBlock) { + // Only block if the project actually has intents defined (governance is active) + const hasIntents = task.intentController.getAllActiveIntents().length > 0 + if (hasIntents) { + return { + allow: false, + reason: `Access Denied: This project is under architectural governance. You MUST select an active intent from the catalog using 'select_active_intent' BEFORE you can use destructive tools like '${toolName}'. Your very next action MUST be to call 'select_active_intent'.`, + } + } + } + } + } +} diff --git a/src/hooks/pre/ScopeEnforcementHook.ts b/src/hooks/pre/ScopeEnforcementHook.ts new file mode 100644 index 00000000000..39ea7a0de18 --- /dev/null +++ b/src/hooks/pre/ScopeEnforcementHook.ts @@ -0,0 +1,28 @@ +import { IToolHook, HookType, HookContext, HookResponse } from "../types" + +/** + * Enforces that file modifications stay within the intent's owned scope. + */ +export class ScopeEnforcementHook implements IToolHook { + name = "ScopeEnforcementHook" + type = HookType.PRE + + async execute(context: HookContext): Promise { + const { task, toolName, intentId, arguments: toolArgs } = context + + if (!intentId) return + + if (toolName === "write_to_file" || toolName === "apply_diff") { + const filePath = (toolArgs.path || "") as string + if (filePath) { + const isScoped = task.intentController.validateScope(filePath, intentId) + if (!isScoped) { + return { + allow: false, + reason: `Security Violation: File '${filePath}' is outside the owned scope of intent '${intentId}'. You MUST either select a different intent that covers this file or ask the user to adjust the scope in '.orchestration/active_intents.yaml'.`, + } + } + } + } + } +} diff --git a/src/hooks/types.ts b/src/hooks/types.ts new file mode 100644 index 00000000000..e7827ed11f3 --- /dev/null +++ b/src/hooks/types.ts @@ -0,0 +1,27 @@ +import { Task } from "../core/task/Task" + +export enum HookType { + PRE = "pre", + POST = "post", +} + +export interface HookResponse { + allow: boolean + reason?: string +} + +export interface IToolHook { + name: string + type: HookType + execute(context: HookContext): Promise +} + +export interface HookContext { + task: Task + toolName: string + arguments: any + intentId?: string + result?: any + error?: any + timestamp: string +} diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 491ba693611..35cd6e3996e 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -80,6 +80,8 @@ export const toolParamNames = [ // read_file legacy format parameter (backward compatibility) "files", "line_ranges", + "intent_id", + "lesson", ] as const export type ToolParamName = (typeof toolParamNames)[number] @@ -115,6 +117,8 @@ export type NativeToolArgs = { update_todo_list: { todos: string } use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record } write_to_file: { path: string; content: string } + select_active_intent: { intent_id: string } + record_lesson_learned: { lesson: string } // Add more tools as they are migrated to native protocol } @@ -257,6 +261,11 @@ export interface GenerateImageToolUse extends ToolUse<"generate_image"> { params: Partial, "prompt" | "path" | "image">> } +export interface SelectActiveIntentToolUse extends ToolUse<"select_active_intent"> { + name: "select_active_intent" + params: Partial, "intent_id">> +} + // Define tool group configuration export type ToolGroupConfig = { tools: readonly string[] @@ -288,6 +297,8 @@ export const TOOL_DISPLAY_NAMES: Record = { run_slash_command: "run slash command", skill: "load skill", generate_image: "generate images", + select_active_intent: "select active intent", + record_lesson_learned: "record lesson learned", custom_tool: "use custom tools", } as const @@ -321,6 +332,8 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ "update_todo_list", "run_slash_command", "skill", + "select_active_intent", + "record_lesson_learned", ] as const /** diff --git a/tsconfig.json b/tsconfig.json index 44f734c197a..14ea9d8cf5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,5 @@ "types": ["node"] }, "exclude": ["node_modules"], - "include": ["scripts/*.ts"] + "include": ["scripts/*.ts", "*.ts"] }