mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Compare commits
243 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b867ec9145 | ||
|
|
8b94decaef | ||
|
|
f49f0ce56a | ||
|
|
d82583c91b | ||
|
|
6ae816f244 | ||
|
|
f5cad409a1 | ||
|
|
15542fcbbf | ||
|
|
d229002cdd | ||
|
|
06777a513e | ||
|
|
5641ccd58d | ||
|
|
e921f9d21e | ||
|
|
28acb6acf2 | ||
|
|
2428199851 | ||
|
|
8922418600 | ||
|
|
22d845cecb | ||
|
|
ff16c9c297 | ||
|
|
3d37e054dd | ||
|
|
ad25634905 | ||
|
|
96d6e43643 | ||
|
|
14922f127e | ||
|
|
142f3fb335 | ||
|
|
c4547d25c5 | ||
|
|
b4f2a242bc | ||
|
|
2bb826039b | ||
|
|
3e202ebf5b | ||
|
|
cb83656718 | ||
|
|
8b12f21439 | ||
|
|
9f251fde70 | ||
|
|
319b5576c7 | ||
|
|
7adbfec2a4 | ||
|
|
9cfaf38d8a | ||
|
|
92a1be40df | ||
|
|
5b93bc7dc3 | ||
|
|
5432fa2689 | ||
|
|
eafed9705c | ||
|
|
c3cae397a1 | ||
|
|
137d3f4fd8 | ||
|
|
08f3a2bb39 | ||
|
|
44fd975b17 | ||
|
|
4ab1d81f55 | ||
|
|
7ae9c4efd2 | ||
|
|
0892455db2 | ||
|
|
0e56afc764 | ||
|
|
5eec588653 | ||
|
|
58c535319e | ||
|
|
0612739ba1 | ||
|
|
52ce796e42 | ||
|
|
3e237e6061 | ||
|
|
0b0b33e6a2 | ||
|
|
0db51d8bfa | ||
|
|
7dc83a522e | ||
|
|
9a58f76299 | ||
|
|
f9da48f73a | ||
|
|
06afe4206b | ||
|
|
02598bc4a5 | ||
|
|
d0480360cb | ||
|
|
459f27015d | ||
|
|
d6611f8f69 | ||
|
|
af1e12c76d | ||
|
|
ce73d05646 | ||
|
|
3c7544c104 | ||
|
|
7ea91fa79e | ||
|
|
941e6de743 | ||
|
|
95ea01f9a5 | ||
|
|
5a4ab2b13f | ||
|
|
b00c260dc0 | ||
|
|
737edbc5ab | ||
|
|
eb2147148e | ||
|
|
7118a14188 | ||
|
|
571cae2368 | ||
|
|
e6ad7949d6 | ||
|
|
a7cebac2d8 | ||
|
|
787f02e526 | ||
|
|
ec979585e8 | ||
|
|
b75f484663 | ||
|
|
52d606bab3 | ||
|
|
193053110b | ||
|
|
54721a3155 | ||
|
|
b55409c0b0 | ||
|
|
50c8101754 | ||
|
|
a7c8275e85 | ||
|
|
81047a65b6 | ||
|
|
2f4ce367c4 | ||
|
|
96d77fd2c8 | ||
|
|
de0e3632d2 | ||
|
|
e25b1f2768 | ||
|
|
bcdc842eda | ||
|
|
4e8cc6eaee | ||
|
|
0aea65d1e6 | ||
|
|
efbba9e8d5 | ||
|
|
191053cec5 | ||
|
|
70cbc716e8 | ||
|
|
6f65a26694 | ||
|
|
2541449924 | ||
|
|
d0e72b7b3e | ||
|
|
b73dc15fda | ||
|
|
48d7e29ed8 | ||
|
|
29caab9d0d | ||
|
|
aca95ccd04 | ||
|
|
ab61ee2cd6 | ||
|
|
62a7bd7354 | ||
|
|
2df6e38998 | ||
|
|
ae09ee5a64 | ||
|
|
6b3097cb31 | ||
|
|
6bd6dc61a4 | ||
|
|
a55c85a8ec | ||
|
|
2991bc9a39 | ||
|
|
93415c7204 | ||
|
|
4288b0a72f | ||
|
|
9918e837ba | ||
|
|
b34678488e | ||
|
|
618aa6652b | ||
|
|
27095553ca | ||
|
|
3a7a01f2f7 | ||
|
|
492006d53d | ||
|
|
5db2062d0c | ||
|
|
0d5b932d2e | ||
|
|
318bb928e0 | ||
|
|
ea7da97a40 | ||
|
|
9a8af61936 | ||
|
|
d00f1a2bb8 | ||
|
|
159bf2e9f1 | ||
|
|
d8cfbfdb05 | ||
|
|
8743020f4e | ||
|
|
b64334b2bd | ||
|
|
aff46b14e2 | ||
|
|
00c35e691f | ||
|
|
ffec9ac1ec | ||
|
|
c974443379 | ||
|
|
5f87f83d97 | ||
|
|
b598efb422 | ||
|
|
f864270547 | ||
|
|
67ea856fd0 | ||
|
|
d7359ff228 | ||
|
|
00075684fd | ||
|
|
b91b20536e | ||
|
|
d9b42f57fb | ||
|
|
2d21e80add | ||
|
|
7bc966ee00 | ||
|
|
d575295883 | ||
|
|
bfbfaf6d46 | ||
|
|
44df43063f | ||
|
|
1b699d9040 | ||
|
|
be2b414785 | ||
|
|
17d534e3ff | ||
|
|
24958f3a3d | ||
|
|
90e2451ad1 | ||
|
|
dc243e4cf9 | ||
|
|
3e24e21c3d | ||
|
|
77ed60a173 | ||
|
|
6ef149d567 | ||
|
|
aaee5a2c02 | ||
|
|
27a78833c8 | ||
|
|
04ffb64bb7 | ||
|
|
bcb8c81916 | ||
|
|
b2b77809ff | ||
|
|
d52b6834e3 | ||
|
|
594ed62f96 | ||
|
|
6cfa82f571 | ||
|
|
5507f5ab64 | ||
|
|
b4d9f92b4d | ||
|
|
3965cd9743 | ||
|
|
b7857bcd6a | ||
|
|
9e46d3e10c | ||
|
|
6c9ff49dd8 | ||
|
|
cdf481c8f9 | ||
|
|
897c372d2d | ||
|
|
fa9dff4a06 | ||
|
|
f54f224a26 | ||
|
|
6c8b9dfa26 | ||
|
|
bd4cd07e88 | ||
|
|
77b76a891f | ||
|
|
b51af98278 | ||
|
|
b759b92f01 | ||
|
|
d2c52c9e09 | ||
|
|
e6f0e79c38 | ||
|
|
dcb33c47ad | ||
|
|
a7ba3b5af5 | ||
|
|
4e659b459d | ||
|
|
5800363903 | ||
|
|
8a69e9e04d | ||
|
|
097f648349 | ||
|
|
8d57da8bc8 | ||
|
|
08a96af22c | ||
|
|
50e5d98f5b | ||
|
|
4438fdadc6 | ||
|
|
b971fe1a63 | ||
|
|
cdd1a4dabf | ||
|
|
4272a69726 | ||
|
|
ff89965f59 | ||
|
|
d4cf3211e7 | ||
|
|
7df1a18ccb | ||
|
|
c1a8767adc | ||
|
|
b02924530c | ||
|
|
5773af8ddd | ||
|
|
2f9849071d | ||
|
|
938e6994a2 | ||
|
|
b6bf829ad9 | ||
|
|
b7d6e4933d | ||
|
|
34a278e9a2 | ||
|
|
a4914c438c | ||
|
|
7a38b99232 | ||
|
|
f51e91ac76 | ||
|
|
7da429da19 | ||
|
|
c74acf36ef | ||
|
|
70775f0ec1 | ||
|
|
bb86eb8651 | ||
|
|
24039c78b5 | ||
|
|
571be71005 | ||
|
|
b8ef352808 | ||
|
|
7c58f29975 | ||
|
|
62a0106ce0 | ||
|
|
115d6c5fce | ||
|
|
2b72a585dc | ||
|
|
1e0fc89fa1 | ||
|
|
ef2fec9a23 | ||
|
|
7afa43635f | ||
|
|
5313cb503a | ||
|
|
2789baba96 | ||
|
|
99a2e3b1ed | ||
|
|
4bc3d62d1f | ||
|
|
12cddc9697 | ||
|
|
7db4bfef5a | ||
|
|
5d17f56db7 | ||
|
|
6826e20da2 | ||
|
|
f179ba1b9e | ||
|
|
97c10387ee | ||
|
|
7fc42d70ea | ||
|
|
4d87a004a5 | ||
|
|
d7714e4e07 | ||
|
|
43a3073545 | ||
|
|
f279537892 | ||
|
|
9b39d2242a | ||
|
|
6d2459c7e8 | ||
|
|
ca7e3b6161 | ||
|
|
0e5407aa76 | ||
|
|
00b1a7ed4d | ||
|
|
36a986db1d | ||
|
|
a3a9048741 | ||
|
|
a9e9c0207e | ||
|
|
06b25185e2 | ||
|
|
8ef61bd32b | ||
|
|
78e64115e8 |
2264 changed files with 81849 additions and 120241 deletions
|
|
@ -1,9 +1,9 @@
|
|||
const getReleaseLine = async (changeset) => {
|
||||
const [firstLine] = changeset.summary
|
||||
const lines = changeset.summary
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
return `- ${firstLine}`
|
||||
return lines.map((line) => (line.startsWith("- ") ? line : `- ${line}`)).join("\n")
|
||||
}
|
||||
|
||||
const getDependencyReleaseLine = async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
- Remove "Enable URL context" and "Enable Grounding with Google search" checkboxes that are no longer needed (PR #11253 by @roomote)
|
||||
- Revert refactor that appended environment details into existing blocks, restoring original behavior (PR #11256 by @mrubens)
|
||||
- Revert removal of stripAppendedEnvironmentDetails and helpers, restoring necessary utility functions (PR #11255 by @mrubens)
|
||||
15
.changeset/v3.54.0.md
Normal file
15
.changeset/v3.54.0.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
"roo-cline": minor
|
||||
---
|
||||
|
||||
- Remove: Roo Code Cloud and eval infrastructure from the extension, CLI, workflows, and package surfaces so the release is focused on the standalone extension (PR #12328 by @mrubens)
|
||||
- Remove: All telemetry collection and analytics plumbing across the extension, website, shared types, provider flows, and related tests (PR #12324 by @mrubens)
|
||||
- Remove: MDM and organization membership enforcement, including host wiring, webview state, user-facing messages, and locale strings (PR #12323 by @mrubens)
|
||||
- Remove: The MCP marketplace, marketplace services, webview marketplace UI, package contributions, and related localized copy (PR #12326 by @mrubens)
|
||||
- Update: Extension-facing support, diagnostics, and announcement content for the final Roo Code release, including GitHub help paths and links to Roomote, ZooCode, and Cline (PR #12341 by @brunobergher)
|
||||
- Add: A cleaned docs app with GitHub Pages deployment support (PR #12344 by @brunobergher)
|
||||
- Fix: Configure the docs GitHub Pages base URL so deployed assets and canonical paths load correctly under the repository Pages path (PR #12370 by @mrubens)
|
||||
- Update: Point docs links in the root README, localized READMEs, and web app copy to the current GitHub Pages docs URL (PR #12371 by @mrubens)
|
||||
- Remove: Stale `roocode.github.io` docs references, including the old CNAME and outdated docs README and robots.txt URLs (PR #12372 by @mrubens)
|
||||
- Update: The website to focus almost entirely on the Roo Code extension and remove cloud, team, enterprise, provider, pricing, Slack, and Linear product pages (PR #12180 by @brunobergher)
|
||||
- Remove: Contributor, community, social channel, and tutorial references from README files, docs, website copy, issue templates, and workflows (PR #12347 by @brunobergher)
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
|
@ -1,2 +1,2 @@
|
|||
# These owners will be the default owners for everything in the repo
|
||||
* @mrubens @cte @jr
|
||||
* @mrubens @cte @jr @hannesrudolph @daniel-lxs
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -64,7 +64,7 @@ body:
|
|||
attributes:
|
||||
value: |
|
||||
---
|
||||
Optional (for contributors): You can stop here if you're just proposing the improvement.
|
||||
Optional: You can stop here if you're just proposing the improvement.
|
||||
|
||||
- type: textarea
|
||||
id: acceptance-criteria
|
||||
|
|
|
|||
75
.github/pull_request_template.md
vendored
75
.github/pull_request_template.md
vendored
|
|
@ -1,75 +0,0 @@
|
|||
<!--
|
||||
Thank you for contributing to Roo Code!
|
||||
|
||||
Before submitting your PR, please ensure:
|
||||
- It's linked to an approved GitHub Issue.
|
||||
- You've reviewed our [Contributing Guidelines](../CONTRIBUTING.md).
|
||||
-->
|
||||
|
||||
### Related GitHub Issue
|
||||
|
||||
<!-- Every PR MUST be linked to an approved issue. -->
|
||||
|
||||
Closes: # <!-- Replace with the issue number, e.g., Closes: #123 -->
|
||||
|
||||
### Roo Code Task Context (Optional)
|
||||
|
||||
<!--
|
||||
If you used Roo Code to help create this PR, you can share public task links here.
|
||||
This helps reviewers understand your development process and provides additional context.
|
||||
Example: https://app.roocode.com/share/task-id
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!--
|
||||
Briefly summarize the changes in this PR and how they address the linked issue.
|
||||
The issue should cover the "what" and "why"; this section should focus on:
|
||||
- The "how": key implementation details, design choices, or trade-offs made.
|
||||
- Anything specific reviewers should pay attention to in this PR.
|
||||
-->
|
||||
|
||||
### Test Procedure
|
||||
|
||||
<!--
|
||||
Detail the steps to test your changes. This helps reviewers verify your work.
|
||||
- How did you test this specific implementation? (e.g., unit tests, manual testing steps)
|
||||
- How can reviewers reproduce your tests or verify the fix/feature?
|
||||
- Include relevant testing environment details if applicable.
|
||||
-->
|
||||
|
||||
### Pre-Submission Checklist
|
||||
|
||||
<!-- Go through this checklist before marking your PR as ready for review. -->
|
||||
|
||||
- [ ] **Issue Linked**: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
|
||||
- [ ] **Scope**: My changes are focused on the linked issue (one major feature/fix per PR).
|
||||
- [ ] **Self-Review**: I have performed a thorough self-review of my code.
|
||||
- [ ] **Testing**: New and/or updated tests have been added to cover my changes (if applicable).
|
||||
- [ ] **Documentation Impact**: I have considered if my changes require documentation updates (see "Documentation Updates" section below).
|
||||
- [ ] **Contribution Guidelines**: I have read and agree to the [Contributor Guidelines](/CONTRIBUTING.md).
|
||||
|
||||
### Screenshots / Videos
|
||||
|
||||
<!--
|
||||
For UI changes, please provide before-and-after screenshots or a short video of the *actual results*.
|
||||
This greatly helps in understanding the visual impact of your changes.
|
||||
-->
|
||||
|
||||
### Documentation Updates
|
||||
|
||||
<!--
|
||||
Does this PR necessitate updates to user-facing documentation?
|
||||
- [ ] No documentation updates are required.
|
||||
- [ ] Yes, documentation updates are required. (Please describe what needs to be updated or link to a PR in the docs repository).
|
||||
-->
|
||||
|
||||
### Additional Notes
|
||||
|
||||
<!-- Add any other context, questions, or information for reviewers here. -->
|
||||
|
||||
### Get in Touch
|
||||
|
||||
<!--
|
||||
Please provide your Discord username for reviewers or maintainers to reach you if they have questions about your PR
|
||||
-->
|
||||
6
.github/workflows/cli-release.yml
vendored
6
.github/workflows/cli-release.yml
vendored
|
|
@ -26,6 +26,9 @@ jobs:
|
|||
- os: ubuntu-latest
|
||||
platform: linux-x64
|
||||
runs-on: ubuntu-latest
|
||||
- os: ubuntu-24.04-arm
|
||||
platform: linux-arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
|
||||
|
|
@ -328,7 +331,7 @@ jobs:
|
|||
echo "## Requirements" >> "$NOTES_FILE"
|
||||
echo "" >> "$NOTES_FILE"
|
||||
echo "- Node.js 20 or higher" >> "$NOTES_FILE"
|
||||
echo "- macOS Apple Silicon (M1/M2/M3/M4) or Linux x64" >> "$NOTES_FILE"
|
||||
echo "- macOS Apple Silicon (M1/M2/M3/M4), Linux x64, or Linux ARM64" >> "$NOTES_FILE"
|
||||
echo "" >> "$NOTES_FILE"
|
||||
echo "## Usage" >> "$NOTES_FILE"
|
||||
echo "" >> "$NOTES_FILE"
|
||||
|
|
@ -345,6 +348,7 @@ jobs:
|
|||
echo "This release includes binaries for:" >> "$NOTES_FILE"
|
||||
echo '- `roo-cli-darwin-arm64.tar.gz` - macOS Apple Silicon (M1/M2/M3)' >> "$NOTES_FILE"
|
||||
echo '- `roo-cli-linux-x64.tar.gz` - Linux x64' >> "$NOTES_FILE"
|
||||
echo '- `roo-cli-linux-arm64.tar.gz` - Linux ARM64' >> "$NOTES_FILE"
|
||||
echo "" >> "$NOTES_FILE"
|
||||
echo "## Checksums" >> "$NOTES_FILE"
|
||||
echo "" >> "$NOTES_FILE"
|
||||
|
|
|
|||
63
.github/workflows/code-qa.yml
vendored
63
.github/workflows/code-qa.yml
vendored
|
|
@ -58,66 +58,3 @@ jobs:
|
|||
uses: ./.github/actions/setup-node-pnpm
|
||||
- name: Run unit tests
|
||||
run: pnpm test
|
||||
|
||||
check-openrouter-api-key:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
exists: ${{ steps.openrouter-api-key-check.outputs.defined }}
|
||||
steps:
|
||||
- name: Check if OpenRouter API key exists
|
||||
id: openrouter-api-key-check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ secrets.OPENROUTER_API_KEY }}" != '' ]; then
|
||||
echo "defined=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "defined=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-openrouter-api-key]
|
||||
if: needs.check-openrouter-api-key.outputs.exists == 'true'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js and pnpm
|
||||
uses: ./.github/actions/setup-node-pnpm
|
||||
- name: Create .env.local file
|
||||
working-directory: apps/vscode-e2e
|
||||
run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.local
|
||||
- name: Set VS Code test version
|
||||
run: echo "VSCODE_VERSION=1.101.2" >> $GITHUB_ENV
|
||||
- name: Cache VS Code test runtime
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: apps/vscode-e2e/.vscode-test
|
||||
key: ${{ runner.os }}-vscode-test-${{ env.VSCODE_VERSION }}
|
||||
- name: Pre-download VS Code test runtime with retry
|
||||
working-directory: apps/vscode-e2e
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
echo "Download attempt $attempt of 3..."
|
||||
node -e "
|
||||
const { downloadAndUnzipVSCode } = require('@vscode/test-electron');
|
||||
downloadAndUnzipVSCode({ version: process.env.VSCODE_VERSION || '1.101.2' })
|
||||
.then(() => {
|
||||
console.log('✅ VS Code test runtime downloaded successfully');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('❌ Failed to download VS Code (attempt $attempt):', err);
|
||||
process.exit(1);
|
||||
});
|
||||
" && break || {
|
||||
if [ $attempt -eq 3 ]; then
|
||||
echo "All download attempts failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "Retrying in 5 seconds..."
|
||||
sleep 5
|
||||
}
|
||||
done
|
||||
- name: Run integration tests
|
||||
working-directory: apps/vscode-e2e
|
||||
run: xvfb-run -a pnpm test:ci
|
||||
|
|
|
|||
55
.github/workflows/docs-pages.yml
vendored
Normal file
55
.github/workflows/docs-pages.yml
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
name: Deploy docs to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/docs/**"
|
||||
- ".github/workflows/docs-pages.yml"
|
||||
- ".github/actions/setup-node-pnpm/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: docs-pages
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js and pnpm
|
||||
uses: ./.github/actions/setup-node-pnpm
|
||||
with:
|
||||
install-args: "--frozen-lockfile"
|
||||
- name: Run type check
|
||||
run: pnpm --filter @roo-code/docs check-types
|
||||
- name: Run lint
|
||||
run: pnpm --filter @roo-code/docs lint
|
||||
- name: Build docs
|
||||
run: pnpm --filter @roo-code/docs build
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: apps/docs/build
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
74
.github/workflows/evals.yml
vendored
74
.github/workflows/evals.yml
vendored
|
|
@ -1,74 +0,0 @@
|
|||
name: Evals
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
COMPOSE_DOCKER_CLI_BUILD: 1
|
||||
|
||||
jobs:
|
||||
evals:
|
||||
# Run if triggered manually or if PR has 'evals' label.
|
||||
if: github.event_name == 'workflow_dispatch' || contains(github.event.label.name, 'evals')
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 45
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/evals
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create environment
|
||||
run: |
|
||||
cat > .env.local << EOF
|
||||
OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY || 'test-key-for-build' }}
|
||||
EOF
|
||||
|
||||
cat > .env.development << EOF
|
||||
NODE_ENV=development
|
||||
DATABASE_URL=postgresql://postgres:password@db:5432/evals_development
|
||||
REDIS_URL=redis://redis:6379
|
||||
HOST_EXECUTION_METHOD=docker
|
||||
EOF
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: packages/evals/Dockerfile.runner
|
||||
tags: evals-runner:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
push: false
|
||||
load: true
|
||||
|
||||
- name: Tag image
|
||||
run: docker tag evals-runner:latest evals-runner
|
||||
|
||||
- name: Start containers
|
||||
run: |
|
||||
docker compose up -d db redis
|
||||
timeout 60 bash -c 'until docker compose exec -T db pg_isready -U postgres; do sleep 2; done'
|
||||
timeout 60 bash -c 'until docker compose exec -T redis redis-cli ping | grep -q PONG; do sleep 2; done'
|
||||
docker compose run --rm runner sh -c 'nc -z db 5432 && echo "✓ Runner -> Database connection successful"'
|
||||
docker compose run --rm runner sh -c 'nc -z redis 6379 && echo "✓ Runner -> Redis connection successful"'
|
||||
docker compose run --rm runner docker ps
|
||||
|
||||
- name: Run database migrations
|
||||
run: docker compose run --rm runner pnpm --filter @roo-code/evals db:migrate
|
||||
|
||||
- name: Run evals
|
||||
run: docker compose run --rm runner pnpm --filter @roo-code/evals cli --ci
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: docker compose down -v --remove-orphans
|
||||
67
.github/workflows/update-contributors.yml
vendored
67
.github/workflows/update-contributors.yml
vendored
|
|
@ -1,67 +0,0 @@
|
|||
name: Update Contributors # Refresh contrib.rocks image cache
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
refresh-contrib-cache:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Bump cacheBust in all README files
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TS="$(date +%s)"
|
||||
# Target only the root README.md and localized READMEs under locales/*/README.md
|
||||
mapfile -t FILES < <(git ls-files README.md 'locales/*/README.md' || true)
|
||||
|
||||
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||
echo "No target README files found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UPDATED=0
|
||||
for f in "${FILES[@]}"; do
|
||||
if grep -q 'cacheBust=' "$f"; then
|
||||
# Use portable sed in GNU environment of ubuntu-latest
|
||||
sed -i -E "s/cacheBust=[0-9]+/cacheBust=${TS}/g" "$f"
|
||||
echo "Updated cacheBust in $f"
|
||||
UPDATED=1
|
||||
else
|
||||
echo "Warning: cacheBust parameter not found in $f" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$UPDATED" -eq 0 ]; then
|
||||
echo "No files were updated. Ensure READMEs embed contrib.rocks with cacheBust param." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Detect changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "docs: update contributors list [skip ci]"
|
||||
committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
branch: refresh-contrib-cache
|
||||
delete-branch: true
|
||||
title: "Refresh contrib.rocks image cache (all READMEs)"
|
||||
body: |
|
||||
Automated refresh of the contrib.rocks image cache by bumping the cacheBust parameter in README.md and locales/*/README.md.
|
||||
base: main
|
||||
59
.github/workflows/website-deploy.yml
vendored
59
.github/workflows/website-deploy.yml
vendored
|
|
@ -1,59 +0,0 @@
|
|||
name: Deploy roocode.com
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/web-roo-code/**'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-roocode-com
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
||||
jobs:
|
||||
check-secrets:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has-vercel-token: ${{ steps.check.outputs.has-vercel-token }}
|
||||
steps:
|
||||
- name: Check if VERCEL_TOKEN exists
|
||||
id: check
|
||||
run: |
|
||||
if [ -n "${{ secrets.VERCEL_TOKEN }}" ]; then
|
||||
echo "has-vercel-token=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has-vercel-token=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-secrets
|
||||
if: ${{ needs.check-secrets.outputs.has-vercel-token == 'true' }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js and pnpm
|
||||
uses: ./.github/actions/setup-node-pnpm
|
||||
- name: Run lint
|
||||
run: pnpm lint
|
||||
working-directory: apps/web-roo-code
|
||||
- name: Run type check
|
||||
run: pnpm check-types
|
||||
working-directory: apps/web-roo-code
|
||||
- name: Run build
|
||||
run: pnpm build
|
||||
working-directory: apps/web-roo-code
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: npx vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: npx vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
102
.github/workflows/website-preview.yml
vendored
102
.github/workflows/website-preview.yml
vendored
|
|
@ -1,102 +0,0 @@
|
|||
name: Preview roocode.com
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- main
|
||||
paths:
|
||||
- "apps/web-roo-code/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/web-roo-code/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: preview-roocode-com-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
||||
jobs:
|
||||
check-secrets:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has-vercel-token: ${{ steps.check.outputs.has-vercel-token }}
|
||||
steps:
|
||||
- name: Check if VERCEL_TOKEN exists
|
||||
id: check
|
||||
run: |
|
||||
if [ -n "${{ secrets.VERCEL_TOKEN }}" ]; then
|
||||
echo "has-vercel-token=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has-vercel-token=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
preview:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-secrets
|
||||
if: ${{ needs.check-secrets.outputs.has-vercel-token == 'true' }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js and pnpm
|
||||
uses: ./.github/actions/setup-node-pnpm
|
||||
- name: Run lint
|
||||
run: pnpm lint
|
||||
working-directory: apps/web-roo-code
|
||||
- name: Run type check
|
||||
run: pnpm check-types
|
||||
working-directory: apps/web-roo-code
|
||||
- name: Run build
|
||||
run: pnpm build
|
||||
working-directory: apps/web-roo-code
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: npx vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: npx vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
id: deploy
|
||||
run: |
|
||||
DEPLOYMENT_URL=$(npx vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
|
||||
echo "deployment_url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT
|
||||
echo "Preview deployed to: $DEPLOYMENT_URL"
|
||||
|
||||
- name: Comment PR with preview link
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const deploymentUrl = '${{ steps.deploy.outputs.deployment_url }}';
|
||||
const commentIdentifier = '<!-- roo-preview-comment -->';
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const existingComment = comments.find(comment =>
|
||||
comment.body.includes(commentIdentifier)
|
||||
);
|
||||
|
||||
const comment = commentIdentifier + '\n🚀 **Preview deployed!**\n\nYour changes have been deployed to Vercel:\n\n**Preview URL:** ' + deploymentUrl + '\n\nThis preview will be updated automatically when you push new commits to this PR.';
|
||||
|
||||
if (existingComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
body: comment
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
}
|
||||
72
.roo/commands/roo-resolve-conflicts.md
Normal file
72
.roo/commands/roo-resolve-conflicts.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
description: "Resolve merge conflicts intelligently using git history analysis"
|
||||
argument-hint: "#PR-number"
|
||||
mode: merge-resolver
|
||||
---
|
||||
|
||||
Resolve merge conflicts for a specific pull request by analyzing git history, commit messages, and code changes to make intelligent resolution decisions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Provide a PR number** (e.g., `#123` or just `123`)
|
||||
|
||||
2. The workflow will automatically:
|
||||
- Fetch PR information (title, description, branches)
|
||||
- Checkout the PR branch
|
||||
- Rebase onto the target branch to reveal conflicts
|
||||
- Analyze and resolve conflicts using git history
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### 1. Initialize PR Resolution
|
||||
|
||||
```bash
|
||||
# Fetch PR info
|
||||
gh pr view [PR_NUMBER] --json title,body,headRefName,baseRefName
|
||||
|
||||
# Checkout and rebase
|
||||
gh pr checkout [PR_NUMBER] --force
|
||||
git fetch origin main
|
||||
GIT_EDITOR=true git rebase origin/main
|
||||
```
|
||||
|
||||
### 2. Identify Conflicts
|
||||
|
||||
```bash
|
||||
git status --porcelain | grep "^UU"
|
||||
```
|
||||
|
||||
### 3. Analyze Each Conflict
|
||||
|
||||
For each conflicted file:
|
||||
- Read the conflict markers
|
||||
- Run `git blame` on conflicting sections
|
||||
- Fetch commit messages for context
|
||||
- Determine the intent behind each change
|
||||
|
||||
### 4. Apply Resolution Strategy
|
||||
|
||||
Based on the analysis:
|
||||
- **Bugfixes** generally take precedence over features
|
||||
- **Recent changes** are often more relevant (unless older is a security fix)
|
||||
- **Combine** non-conflicting changes when possible
|
||||
- **Preserve** test updates alongside code changes
|
||||
|
||||
### 5. Complete Resolution
|
||||
|
||||
```bash
|
||||
git add [resolved-files]
|
||||
GIT_EDITOR=true git rebase --continue
|
||||
```
|
||||
|
||||
## Key Guidelines
|
||||
|
||||
- Always escape conflict markers with `\` when using `apply_diff`
|
||||
- Document resolution decisions in the summary
|
||||
- Verify no syntax errors after resolution
|
||||
- Preserve valuable changes from both sides when possible
|
||||
|
||||
## Examples
|
||||
|
||||
- `/roo-resolve-conflicts #123` - Resolve conflicts for PR #123
|
||||
- `/roo-resolve-conflicts 456` - Resolve conflicts for PR #456
|
||||
50
.roo/commands/roo-translate.md
Normal file
50
.roo/commands/roo-translate.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
description: "Translate and localize strings in the Roo Code extension"
|
||||
argument-hint: "[language-code or 'all'] [string-key or file-path]"
|
||||
mode: translate
|
||||
---
|
||||
|
||||
Perform translation and localization tasks for the Roo Code extension. This command activates the translation workflow with comprehensive i18n guidelines.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Identify the translation scope:**
|
||||
- If a specific language code is provided (e.g., `de`, `zh-CN`), focus on that language
|
||||
- If `all` is specified, translate to all supported languages
|
||||
- If a string key is provided, locate and translate that specific string
|
||||
- If a file path is provided, work with that translation file
|
||||
|
||||
2. **Supported languages:** ca, de, en, es, fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW
|
||||
|
||||
3. **Translation locations:**
|
||||
- Core Extension: `src/i18n/locales/`
|
||||
- WebView UI: `webview-ui/src/i18n/locales/`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. If adding new strings:
|
||||
- Add the English string first
|
||||
- Ask for confirmation before translating to other languages
|
||||
- Use `apply_diff` for efficient file updates
|
||||
|
||||
2. If updating existing strings:
|
||||
- Identify all affected language files
|
||||
- Update English first, then propagate changes
|
||||
|
||||
3. Validate your changes:
|
||||
```bash
|
||||
node scripts/find-missing-translations.js
|
||||
```
|
||||
|
||||
## Key Guidelines
|
||||
|
||||
- Use informal speech (e.g., "du" not "Sie" in German)
|
||||
- Keep technical terms like "token", "Prompt" in English
|
||||
- Preserve all `{{variable}}` placeholders exactly
|
||||
- Use `apply_diff` instead of `write_to_file` for existing files
|
||||
|
||||
## Examples
|
||||
|
||||
- `/roo-translate de` - Focus on German translations
|
||||
- `/roo-translate all welcome.title` - Translate a specific key to all languages
|
||||
- `/roo-translate zh-CN src/i18n/locales/zh-CN/core.json` - Work on specific file
|
||||
15
.roo/guidance/roo-translator.md
Normal file
15
.roo/guidance/roo-translator.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Roo Code Translation Guidance
|
||||
|
||||
This file contains brand voice, tone, and word choice guidelines for Roo Code translations.
|
||||
|
||||
## Brand Voice
|
||||
|
||||
<!-- Add brand voice guidelines here -->
|
||||
|
||||
## Tone
|
||||
|
||||
<!-- Add tone guidelines here -->
|
||||
|
||||
## Word Choice
|
||||
|
||||
<!-- Add word choice preferences here -->
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
<workflow>
|
||||
<step number="1">
|
||||
<name>Understand Test Requirements</name>
|
||||
<instructions>
|
||||
Use ask_followup_question to determine what type of integration test is needed:
|
||||
|
||||
<ask_followup_question>
|
||||
<question>What type of integration test would you like me to create or work on?</question>
|
||||
<follow_up>
|
||||
<suggest>New E2E test for a specific feature or workflow</suggest>
|
||||
<suggest>Fix or update an existing integration test</suggest>
|
||||
<suggest>Create test utilities or helpers for common patterns</suggest>
|
||||
<suggest>Debug failing integration tests</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<name>Gather Test Specifications</name>
|
||||
<instructions>
|
||||
Based on the test type, gather detailed requirements:
|
||||
|
||||
For New E2E Tests:
|
||||
- What specific user workflow or feature needs testing?
|
||||
- What are the expected inputs and outputs?
|
||||
- What edge cases or error scenarios should be covered?
|
||||
- Are there specific API interactions to validate?
|
||||
- What events should be monitored during the test?
|
||||
|
||||
For Existing Test Issues:
|
||||
- Which test file is failing or needs updates?
|
||||
- What specific error messages or failures are occurring?
|
||||
- What changes in the codebase might have affected the test?
|
||||
|
||||
For Test Utilities:
|
||||
- What common patterns are being repeated across tests?
|
||||
- What helper functions would improve test maintainability?
|
||||
|
||||
Use multiple ask_followup_question calls if needed to gather complete information.
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<name>Explore Existing Test Patterns</name>
|
||||
<instructions>
|
||||
Use codebase_search FIRST to understand existing test patterns and similar functionality:
|
||||
|
||||
For New Tests:
|
||||
- Search for similar test scenarios in apps/vscode-e2e/src/suite/
|
||||
- Find existing test utilities and helpers
|
||||
- Identify patterns for the type of functionality being tested
|
||||
|
||||
For Test Fixes:
|
||||
- Search for the failing test file and related code
|
||||
- Find similar working tests for comparison
|
||||
- Look for recent changes that might have broken the test
|
||||
|
||||
Example searches:
|
||||
- "file creation test mocha" for file operation tests
|
||||
- "task completion waitUntilCompleted" for task monitoring patterns
|
||||
- "api message validation" for API interaction tests
|
||||
|
||||
After codebase_search, use:
|
||||
- read_file on relevant test files to understand structure
|
||||
- list_code_definition_names on test directories
|
||||
- search_files for specific test patterns or utilities
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="4">
|
||||
<name>Analyze Test Environment and Setup</name>
|
||||
<instructions>
|
||||
Examine the test environment configuration:
|
||||
|
||||
1. Read the test runner configuration:
|
||||
- apps/vscode-e2e/package.json for test scripts
|
||||
- apps/vscode-e2e/src/runTest.ts for test setup
|
||||
- Any test configuration files
|
||||
|
||||
2. Understand the test workspace setup:
|
||||
- How test workspaces are created
|
||||
- What files are available during tests
|
||||
- How the extension API is accessed
|
||||
|
||||
3. Review existing test utilities:
|
||||
- Helper functions for common operations
|
||||
- Event listening patterns
|
||||
- Assertion utilities
|
||||
- Cleanup procedures
|
||||
|
||||
Document findings including:
|
||||
- Test environment structure
|
||||
- Available utilities and helpers
|
||||
- Common patterns and best practices
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="5">
|
||||
<name>Design Test Structure</name>
|
||||
<instructions>
|
||||
Plan the test implementation based on gathered information:
|
||||
|
||||
For New Tests:
|
||||
- Define test suite structure with suite/test blocks
|
||||
- Plan setup and teardown procedures
|
||||
- Identify required test data and fixtures
|
||||
- Design event listeners and validation points
|
||||
- Plan for both success and failure scenarios
|
||||
|
||||
For Test Fixes:
|
||||
- Identify the root cause of the failure
|
||||
- Plan the minimal changes needed to fix the issue
|
||||
- Consider if the test needs to be updated due to code changes
|
||||
- Plan for improved error handling or debugging
|
||||
|
||||
Create a detailed test plan including:
|
||||
- Test file structure and organization
|
||||
- Required setup and cleanup
|
||||
- Specific assertions and validations
|
||||
- Error handling and edge cases
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="6">
|
||||
<name>Implement Test Code</name>
|
||||
<instructions>
|
||||
Implement the test following established patterns:
|
||||
|
||||
CRITICAL: Never write a test file with a single write_to_file call.
|
||||
Always implement tests in parts:
|
||||
|
||||
1. Start with the basic test structure (suite, setup, teardown)
|
||||
2. Add individual test cases one by one
|
||||
3. Implement helper functions separately
|
||||
4. Add event listeners and validation logic incrementally
|
||||
|
||||
Follow these implementation guidelines:
|
||||
- Use suite() and test() blocks following Mocha TDD style
|
||||
- Always use the global api object for extension interactions
|
||||
- Implement proper async/await patterns with waitFor utility
|
||||
- Use waitUntilCompleted and waitUntilAborted helpers for task monitoring
|
||||
- Listen to and validate appropriate events (message, taskCompleted, etc.)
|
||||
- Test both positive flows and error scenarios
|
||||
- Validate message content using proper type assertions
|
||||
- Create reusable test utilities when patterns emerge
|
||||
- Use meaningful test descriptions that explain the scenario
|
||||
- Always clean up tasks with cancelCurrentTask or clearCurrentTask
|
||||
- Ensure tests are independent and can run in any order
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="7">
|
||||
<name>Run and Validate Tests</name>
|
||||
<instructions>
|
||||
Execute the tests to ensure they work correctly:
|
||||
|
||||
ALWAYS use the correct working directory and commands:
|
||||
- Working directory: apps/vscode-e2e
|
||||
- Test command: npm run test:run
|
||||
- For specific tests: TEST_FILE="filename.test" npm run test:run
|
||||
- Example: cd apps/vscode-e2e && TEST_FILE="apply-diff.test" npm run test:run
|
||||
|
||||
Test execution process:
|
||||
1. Run the specific test file first
|
||||
2. Check for any failures or errors
|
||||
3. Analyze test output and logs
|
||||
4. Debug any issues found
|
||||
5. Re-run tests after fixes
|
||||
|
||||
If tests fail:
|
||||
- Add console.log statements to track execution flow
|
||||
- Log important events like task IDs, file paths, and AI responses
|
||||
- Check test output carefully for error messages and stack traces
|
||||
- Verify file creation in correct workspace directories
|
||||
- Ensure proper event handling and timeouts
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="8">
|
||||
<name>Document and Complete</name>
|
||||
<instructions>
|
||||
Finalize the test implementation:
|
||||
|
||||
1. Add comprehensive comments explaining complex test logic
|
||||
2. Document any new test utilities or patterns created
|
||||
3. Ensure test descriptions clearly explain what is being tested
|
||||
4. Verify all cleanup procedures are in place
|
||||
5. Confirm tests can run independently and in any order
|
||||
|
||||
Provide the user with:
|
||||
- Summary of tests created or fixed
|
||||
- Instructions for running the tests
|
||||
- Any new patterns or utilities that can be reused
|
||||
- Recommendations for future test improvements
|
||||
</instructions>
|
||||
</step>
|
||||
</workflow>
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
<test_patterns>
|
||||
<mocha_tdd_structure>
|
||||
<description>Standard Mocha TDD structure for integration tests</description>
|
||||
<pattern>
|
||||
<name>Basic Test Suite Structure</name>
|
||||
<example>
|
||||
```typescript
|
||||
import { suite, test, suiteSetup, suiteTeardown } from 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import { waitFor, waitUntilCompleted, waitUntilAborted } from '../utils/testUtils';
|
||||
|
||||
suite('Feature Name Tests', () => {
|
||||
let testWorkspaceDir: string;
|
||||
let testFiles: { [key: string]: string } = {};
|
||||
|
||||
suiteSetup(async () => {
|
||||
// Setup test workspace and files
|
||||
testWorkspaceDir = vscode.workspace.workspaceFolders![0].uri.fsPath;
|
||||
// Create test files in workspace
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
// Cleanup test files and tasks
|
||||
await api.cancelCurrentTask();
|
||||
});
|
||||
|
||||
test('should perform specific functionality', async () => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Event Listening Pattern</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should handle task completion events', async () => {
|
||||
const events: any[] = [];
|
||||
|
||||
const messageListener = (message: any) => {
|
||||
events.push({ type: 'message', data: message });
|
||||
};
|
||||
|
||||
const taskCompletedListener = (result: any) => {
|
||||
events.push({ type: 'taskCompleted', data: result });
|
||||
};
|
||||
|
||||
api.onDidReceiveMessage(messageListener);
|
||||
api.onTaskCompleted(taskCompletedListener);
|
||||
|
||||
try {
|
||||
// Perform test actions
|
||||
await api.startTask('test prompt');
|
||||
await waitUntilCompleted();
|
||||
|
||||
// Validate events
|
||||
assert(events.some(e => e.type === 'taskCompleted'));
|
||||
} finally {
|
||||
// Cleanup listeners
|
||||
api.onDidReceiveMessage(() => {});
|
||||
api.onTaskCompleted(() => {});
|
||||
}
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>File Creation Test Pattern</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should create files in workspace', async () => {
|
||||
const fileName = 'test-file.txt';
|
||||
const expectedContent = 'test content';
|
||||
|
||||
await api.startTask(`Create a file named ${fileName} with content: ${expectedContent}`);
|
||||
await waitUntilCompleted();
|
||||
|
||||
// Check multiple possible locations
|
||||
const possiblePaths = [
|
||||
path.join(testWorkspaceDir, fileName),
|
||||
path.join(process.cwd(), fileName),
|
||||
// Add other possible locations
|
||||
];
|
||||
|
||||
let fileFound = false;
|
||||
let actualContent = '';
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
actualContent = fs.readFileSync(filePath, 'utf8');
|
||||
fileFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert(fileFound, `File ${fileName} not found in any expected location`);
|
||||
assert.strictEqual(actualContent.trim(), expectedContent);
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</mocha_tdd_structure>
|
||||
|
||||
<api_interaction_patterns>
|
||||
<pattern>
|
||||
<name>Basic Task Execution</name>
|
||||
<example>
|
||||
```typescript
|
||||
// Start a task and wait for completion
|
||||
await api.startTask('Your prompt here');
|
||||
await waitUntilCompleted();
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Task with Auto-Approval Settings</name>
|
||||
<example>
|
||||
```typescript
|
||||
// Enable auto-approval for specific actions
|
||||
await api.updateSettings({
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true
|
||||
});
|
||||
|
||||
await api.startTask('Create and execute a script');
|
||||
await waitUntilCompleted();
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Message Validation</name>
|
||||
<example>
|
||||
```typescript
|
||||
const messages: any[] = [];
|
||||
api.onDidReceiveMessage((message) => {
|
||||
messages.push(message);
|
||||
});
|
||||
|
||||
await api.startTask('test prompt');
|
||||
await waitUntilCompleted();
|
||||
|
||||
// Validate specific message types
|
||||
const toolMessages = messages.filter(m =>
|
||||
m.type === 'say' && m.say === 'api_req_started'
|
||||
);
|
||||
assert(toolMessages.length > 0, 'Expected tool execution messages');
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</api_interaction_patterns>
|
||||
|
||||
<error_handling_patterns>
|
||||
<pattern>
|
||||
<name>Task Abortion Handling</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should handle task abortion', async () => {
|
||||
await api.startTask('long running task');
|
||||
|
||||
// Abort after short delay
|
||||
setTimeout(() => api.abortTask(), 1000);
|
||||
|
||||
await waitUntilAborted();
|
||||
|
||||
// Verify task was properly aborted
|
||||
const status = await api.getTaskStatus();
|
||||
assert.strictEqual(status, 'aborted');
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Error Message Validation</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should handle invalid input gracefully', async () => {
|
||||
const errorMessages: any[] = [];
|
||||
|
||||
api.onDidReceiveMessage((message) => {
|
||||
if (message.type === 'error' || message.text?.includes('error')) {
|
||||
errorMessages.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
await api.startTask('invalid prompt that should fail');
|
||||
await waitFor(() => errorMessages.length > 0, 5000);
|
||||
|
||||
assert(errorMessages.length > 0, 'Expected error messages');
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</error_handling_patterns>
|
||||
|
||||
<utility_patterns>
|
||||
<pattern>
|
||||
<name>File Location Helper</name>
|
||||
<example>
|
||||
```typescript
|
||||
function findFileInWorkspace(fileName: string, workspaceDir: string): string | null {
|
||||
const possiblePaths = [
|
||||
path.join(workspaceDir, fileName),
|
||||
path.join(process.cwd(), fileName),
|
||||
path.join(os.tmpdir(), fileName),
|
||||
// Add other common locations
|
||||
];
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Event Collection Helper</name>
|
||||
<example>
|
||||
```typescript
|
||||
class EventCollector {
|
||||
private events: any[] = [];
|
||||
|
||||
constructor(private api: any) {
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.api.onDidReceiveMessage((message: any) => {
|
||||
this.events.push({ type: 'message', timestamp: Date.now(), data: message });
|
||||
});
|
||||
|
||||
this.api.onTaskCompleted((result: any) => {
|
||||
this.events.push({ type: 'taskCompleted', timestamp: Date.now(), data: result });
|
||||
});
|
||||
}
|
||||
|
||||
getEvents(type?: string) {
|
||||
return type ? this.events.filter(e => e.type === type) : this.events;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.events = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</utility_patterns>
|
||||
|
||||
<debugging_patterns>
|
||||
<pattern>
|
||||
<name>Comprehensive Logging</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should log execution flow for debugging', async () => {
|
||||
console.log('Starting test execution');
|
||||
|
||||
const events: any[] = [];
|
||||
api.onDidReceiveMessage((message) => {
|
||||
console.log('Received message:', JSON.stringify(message, null, 2));
|
||||
events.push(message);
|
||||
});
|
||||
|
||||
console.log('Starting task with prompt');
|
||||
await api.startTask('test prompt');
|
||||
|
||||
console.log('Waiting for task completion');
|
||||
await waitUntilCompleted();
|
||||
|
||||
console.log('Task completed, events received:', events.length);
|
||||
console.log('Final workspace state:', fs.readdirSync(testWorkspaceDir));
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>State Validation</name>
|
||||
<example>
|
||||
```typescript
|
||||
function validateTestState(description: string) {
|
||||
console.log(`=== ${description} ===`);
|
||||
console.log('Workspace files:', fs.readdirSync(testWorkspaceDir));
|
||||
console.log('Current working directory:', process.cwd());
|
||||
console.log('Task status:', api.getTaskStatus?.() || 'unknown');
|
||||
console.log('========================');
|
||||
}
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</debugging_patterns>
|
||||
</test_patterns>
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
<best_practices>
|
||||
<test_structure>
|
||||
- Always use suite() and test() blocks following Mocha TDD style
|
||||
- Use descriptive test names that explain the scenario being tested
|
||||
- Implement proper setup and teardown in suiteSetup() and suiteTeardown()
|
||||
- Create test files in the VSCode workspace directory during suiteSetup()
|
||||
- Store file paths in a test-scoped object for easy reference across tests
|
||||
- Ensure tests are independent and can run in any order
|
||||
- Clean up all test files and tasks in suiteTeardown() to avoid test pollution
|
||||
</test_structure>
|
||||
|
||||
<api_interactions>
|
||||
- Always use the global api object for extension interactions
|
||||
- Implement proper async/await patterns with the waitFor utility
|
||||
- Use waitUntilCompleted and waitUntilAborted helpers for task monitoring
|
||||
- Set appropriate auto-approval settings (alwaysAllowWrite, alwaysAllowExecute) for the functionality being tested
|
||||
- Listen to and validate appropriate events (message, taskCompleted, taskAborted, etc.)
|
||||
- Always clean up tasks with cancelCurrentTask or clearCurrentTask after tests
|
||||
- Use meaningful timeouts that account for actual task execution time
|
||||
</api_interactions>
|
||||
|
||||
<file_system_handling>
|
||||
- Be aware that files may be created in the workspace directory (/tmp/roo-test-workspace-*) rather than expected locations
|
||||
- Always check multiple possible file locations when verifying file creation
|
||||
- Use flexible file location checking that searches workspace directories
|
||||
- Verify files exist after creation to catch setup issues early
|
||||
- Account for the fact that the workspace directory is created by runTest.ts
|
||||
- The AI may use internal tools instead of the documented tools - verify outcomes rather than methods
|
||||
</file_system_handling>
|
||||
|
||||
<event_handling>
|
||||
- Add multiple event listeners (taskStarted, taskCompleted, taskAborted) for better debugging
|
||||
- Don't rely on parsing AI messages to detect tool usage - the AI's message format may vary
|
||||
- Use terminal shell execution events (onDidStartTerminalShellExecution, onDidEndTerminalShellExecution) for command tracking
|
||||
- Tool executions are reported via api_req_started messages with type="say" and say="api_req_started"
|
||||
- Focus on testing outcomes (files created, commands executed) rather than message parsing
|
||||
- There is no "tool_result" message type - tool results appear in "completion_result" or "text" messages
|
||||
</event_handling>
|
||||
|
||||
<error_scenarios>
|
||||
- Test both positive flows and error scenarios
|
||||
- Validate message content using proper type assertions
|
||||
- Implement proper error handling and edge cases
|
||||
- Use try-catch blocks around critical test operations
|
||||
- Log important events like task IDs, file paths, and AI responses for debugging
|
||||
- Check test output carefully for error messages and stack traces
|
||||
</error_scenarios>
|
||||
|
||||
<test_reliability>
|
||||
- Remove unnecessary waits for specific tool executions - wait for task completion instead
|
||||
- Simplify message handlers to only capture essential error information
|
||||
- Use the simplest possible test structure that verifies the outcome
|
||||
- Avoid complex message parsing logic that depends on AI behavior
|
||||
- Terminal events are more reliable than message parsing for command execution verification
|
||||
- Keep prompts simple and direct - complex instructions may confuse the AI
|
||||
</test_reliability>
|
||||
|
||||
<debugging_and_troubleshooting>
|
||||
- Add console.log statements to track test execution flow
|
||||
- Log important events like task IDs, file paths, and AI responses
|
||||
- Use codebase_search first to find similar test patterns before writing new tests
|
||||
- Create helper functions for common file location checks
|
||||
- Use descriptive variable names for file paths and content
|
||||
- Always log the expected vs actual locations when tests fail
|
||||
- Add comprehensive comments explaining complex test logic
|
||||
</debugging_and_troubleshooting>
|
||||
|
||||
<test_utilities>
|
||||
- Create reusable test utilities when patterns emerge
|
||||
- Implement helper functions for common operations like file finding
|
||||
- Use event collection utilities for consistent event handling
|
||||
- Create assertion helpers for common validation patterns
|
||||
- Document any new test utilities or patterns created
|
||||
- Share common utilities across test files to reduce duplication
|
||||
</test_utilities>
|
||||
|
||||
<ai_interaction_considerations>
|
||||
- Keep prompts simple and direct - complex instructions may lead to unexpected behavior
|
||||
- Allow for variations in how the AI accomplishes tasks
|
||||
- The AI may not always use the exact tool you specify in the prompt
|
||||
- Be prepared to adapt tests based on actual AI behavior rather than expected behavior
|
||||
- The AI may interpret instructions creatively - test results rather than implementation details
|
||||
- The AI will not see the files in the workspace directory, you must tell it to assume they exist and proceed
|
||||
</ai_interaction_considerations>
|
||||
|
||||
<test_execution>
|
||||
- ALWAYS use the correct working directory: apps/vscode-e2e
|
||||
- The test command is: npm run test:run
|
||||
- To run specific tests use environment variable: TEST_FILE="filename.test" npm run test:run
|
||||
- Example: cd apps/vscode-e2e && TEST_FILE="apply-diff.test" npm run test:run
|
||||
- Never use npm test directly as it doesn't exist
|
||||
- Always check available scripts with npm run if unsure
|
||||
- Run tests incrementally during development to catch issues early
|
||||
</test_execution>
|
||||
|
||||
<code_organization>
|
||||
- Never write a test file with a single write_to_file tool call
|
||||
- Always implement tests in parts: structure first, then individual test cases
|
||||
- Group related tests in the same suite
|
||||
- Use consistent naming conventions for test files and functions
|
||||
- Separate test utilities into their own files when they become substantial
|
||||
- Follow the existing project structure and conventions
|
||||
</code_organization>
|
||||
</best_practices>
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
<common_mistakes_to_avoid>
|
||||
<test_structure_mistakes>
|
||||
- Writing a test file with a single write_to_file tool call instead of implementing in parts
|
||||
- Not using proper Mocha TDD structure with suite() and test() blocks
|
||||
- Forgetting to implement suiteSetup() and suiteTeardown() for proper cleanup
|
||||
- Creating tests that depend on each other or specific execution order
|
||||
- Not cleaning up tasks and files after test completion
|
||||
- Using describe/it blocks instead of the required suite/test blocks
|
||||
</test_structure_mistakes>
|
||||
|
||||
<api_interaction_mistakes>
|
||||
- Not using the global api object for extension interactions
|
||||
- Forgetting to set auto-approval settings (alwaysAllowWrite, alwaysAllowExecute) when testing functionality that requires user approval
|
||||
- Not implementing proper async/await patterns with waitFor utilities
|
||||
- Using incorrect timeout values that are too short for actual task execution
|
||||
- Not properly cleaning up tasks with cancelCurrentTask or clearCurrentTask
|
||||
- Assuming the AI will use specific tools instead of testing outcomes
|
||||
</api_interaction_mistakes>
|
||||
|
||||
<file_system_mistakes>
|
||||
- Assuming files will be created in the expected location without checking multiple paths
|
||||
- Not accounting for the workspace directory being created by runTest.ts
|
||||
- Creating test files in temporary directories instead of the VSCode workspace directory
|
||||
- Not verifying files exist after creation during setup
|
||||
- Forgetting that the AI may not see files in the workspace directory
|
||||
- Not using flexible file location checking that searches workspace directories
|
||||
</file_system_mistakes>
|
||||
|
||||
<event_handling_mistakes>
|
||||
- Relying on parsing AI messages to detect tool usage instead of using proper event listeners
|
||||
- Expecting tool results in "tool_result" message type (which doesn't exist)
|
||||
- Not listening to terminal shell execution events for command tracking
|
||||
- Depending on specific message formats that may vary
|
||||
- Not implementing proper event cleanup after tests
|
||||
- Parsing complex AI conversation messages instead of focusing on outcomes
|
||||
</event_handling_mistakes>
|
||||
|
||||
<test_execution_mistakes>
|
||||
- Using npm test instead of npm run test:run
|
||||
- Not using the correct working directory (apps/vscode-e2e)
|
||||
- Running tests from the wrong directory
|
||||
- Not checking available scripts with npm run when unsure
|
||||
- Forgetting to use TEST_FILE environment variable for specific tests
|
||||
- Not running tests incrementally during development
|
||||
</test_execution_mistakes>
|
||||
|
||||
<debugging_mistakes>
|
||||
- Not adding sufficient logging to track test execution flow
|
||||
- Not logging important events like task IDs, file paths, and AI responses
|
||||
- Not using codebase_search to find similar test patterns before writing new tests
|
||||
- Not checking test output carefully for error messages and stack traces
|
||||
- Not validating test state at critical points
|
||||
- Assuming test failures are due to code issues without checking test logic
|
||||
</debugging_mistakes>
|
||||
|
||||
<ai_interaction_mistakes>
|
||||
- Using complex instructions that may confuse the AI
|
||||
- Expecting the AI to use exact tools specified in prompts
|
||||
- Not allowing for variations in how the AI accomplishes tasks
|
||||
- Testing implementation details instead of outcomes
|
||||
- Not adapting tests based on actual AI behavior
|
||||
- Forgetting to tell the AI to assume files exist in the workspace directory
|
||||
</ai_interaction_mistakes>
|
||||
|
||||
<reliability_mistakes>
|
||||
- Adding unnecessary waits for specific tool executions
|
||||
- Using complex message parsing logic that depends on AI behavior
|
||||
- Not using the simplest possible test structure
|
||||
- Depending on specific AI message formats
|
||||
- Not using terminal events for reliable command execution verification
|
||||
- Making tests too brittle by depending on exact AI responses
|
||||
</reliability_mistakes>
|
||||
|
||||
<workspace_mistakes>
|
||||
- Not understanding that files may be created in /tmp/roo-test-workspace-* directories
|
||||
- Assuming the AI can see files in the workspace directory
|
||||
- Not checking multiple possible file locations when verifying creation
|
||||
- Creating files outside the VSCode workspace during tests
|
||||
- Not properly setting up the test workspace in suiteSetup()
|
||||
- Forgetting to clean up workspace files in suiteTeardown()
|
||||
</workspace_mistakes>
|
||||
|
||||
<message_handling_mistakes>
|
||||
- Expecting specific message types for tool execution results
|
||||
- Not understanding that ClineMessage types have specific values
|
||||
- Trying to parse tool execution from AI conversation messages
|
||||
- Not checking packages/types/src/message.ts for valid message types
|
||||
- Depending on message parsing instead of outcome verification
|
||||
- Not using api_req_started messages to verify tool execution
|
||||
</message_handling_mistakes>
|
||||
|
||||
<timeout_and_timing_mistakes>
|
||||
- Using timeouts that are too short for actual task execution
|
||||
- Not accounting for AI processing time in test timeouts
|
||||
- Waiting for specific tool executions instead of task completion
|
||||
- Not implementing proper retry logic for flaky operations
|
||||
- Using fixed delays instead of condition-based waiting
|
||||
- Not considering that some operations may take longer in CI environments
|
||||
</timeout_and_timing_mistakes>
|
||||
|
||||
<test_data_mistakes>
|
||||
- Not creating test files in the correct workspace directory
|
||||
- Using hardcoded paths that don't work across different environments
|
||||
- Not storing file paths in test-scoped objects for easy reference
|
||||
- Creating test data that conflicts with other tests
|
||||
- Not cleaning up test data properly after tests complete
|
||||
- Using test data that's too complex for the AI to handle reliably
|
||||
</test_data_mistakes>
|
||||
</common_mistakes_to_avoid>
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
<test_environment_and_tools>
|
||||
<test_framework>
|
||||
<description>VSCode E2E testing framework using Mocha and VSCode Test</description>
|
||||
<key_components>
|
||||
- Mocha TDD framework for test structure
|
||||
- VSCode Test framework for extension testing
|
||||
- Custom test utilities and helpers
|
||||
- Event-driven testing patterns
|
||||
- Workspace-based test execution
|
||||
</key_components>
|
||||
</test_framework>
|
||||
|
||||
<directory_structure>
|
||||
<test_files_location>apps/vscode-e2e/src/suite/</test_files_location>
|
||||
<test_utilities>apps/vscode-e2e/src/utils/</test_utilities>
|
||||
<test_runner>apps/vscode-e2e/src/runTest.ts</test_runner>
|
||||
<package_config>apps/vscode-e2e/package.json</package_config>
|
||||
<type_definitions>packages/types/</type_definitions>
|
||||
</directory_structure>
|
||||
|
||||
<test_execution_commands>
|
||||
<working_directory>apps/vscode-e2e</working_directory>
|
||||
<commands>
|
||||
<run_all_tests>npm run test:run</run_all_tests>
|
||||
<run_specific_test>TEST_FILE="filename.test" npm run test:run</run_specific_test>
|
||||
<example>cd apps/vscode-e2e && TEST_FILE="apply-diff.test" npm run test:run</example>
|
||||
<check_scripts>npm run</check_scripts>
|
||||
</commands>
|
||||
<important_notes>
|
||||
- Never use npm test directly as it doesn't exist
|
||||
- Always use the correct working directory
|
||||
- Use TEST_FILE environment variable for specific tests
|
||||
- Check available scripts with npm run if unsure
|
||||
</important_notes>
|
||||
</test_execution_commands>
|
||||
|
||||
<api_object>
|
||||
<description>Global api object for extension interactions</description>
|
||||
<key_methods>
|
||||
<task_management>
|
||||
- api.startTask(prompt: string): Start a new task
|
||||
- api.cancelCurrentTask(): Cancel the current task
|
||||
- api.clearCurrentTask(): Clear the current task
|
||||
- api.abortTask(): Abort the current task
|
||||
- api.getTaskStatus(): Get current task status
|
||||
</task_management>
|
||||
<event_listeners>
|
||||
- api.onDidReceiveMessage(callback): Listen to messages
|
||||
- api.onTaskCompleted(callback): Listen to task completion
|
||||
- api.onTaskAborted(callback): Listen to task abortion
|
||||
- api.onTaskStarted(callback): Listen to task start
|
||||
- api.onDidStartTerminalShellExecution(callback): Terminal start events
|
||||
- api.onDidEndTerminalShellExecution(callback): Terminal end events
|
||||
</event_listeners>
|
||||
<settings>
|
||||
- api.updateSettings(settings): Update extension settings
|
||||
- api.getSettings(): Get current settings
|
||||
</settings>
|
||||
</key_methods>
|
||||
</api_object>
|
||||
|
||||
<test_utilities>
|
||||
<wait_functions>
|
||||
<waitFor>
|
||||
<description>Wait for a condition to be true</description>
|
||||
<usage>await waitFor(() => condition, timeout)</usage>
|
||||
<example>await waitFor(() => fs.existsSync(filePath), 5000)</example>
|
||||
</waitFor>
|
||||
<waitUntilCompleted>
|
||||
<description>Wait until current task is completed</description>
|
||||
<usage>await waitUntilCompleted()</usage>
|
||||
<timeout>Default timeout for task completion</timeout>
|
||||
</waitUntilCompleted>
|
||||
<waitUntilAborted>
|
||||
<description>Wait until current task is aborted</description>
|
||||
<usage>await waitUntilAborted()</usage>
|
||||
<timeout>Default timeout for task abortion</timeout>
|
||||
</waitUntilAborted>
|
||||
</wait_functions>
|
||||
|
||||
<helper_patterns>
|
||||
<file_location_helper>
|
||||
<description>Helper to find files in multiple possible locations</description>
|
||||
<usage>Use when files might be created in different workspace directories</usage>
|
||||
</file_location_helper>
|
||||
<event_collector>
|
||||
<description>Utility to collect and analyze events during test execution</description>
|
||||
<usage>Use for comprehensive event tracking and validation</usage>
|
||||
</event_collector>
|
||||
<assertion_helpers>
|
||||
<description>Custom assertion functions for common test patterns</description>
|
||||
<usage>Use for consistent validation across tests</usage>
|
||||
</assertion_helpers>
|
||||
</helper_patterns>
|
||||
</test_utilities>
|
||||
|
||||
<workspace_management>
|
||||
<workspace_creation>
|
||||
<description>Test workspaces are created by runTest.ts</description>
|
||||
<location>/tmp/roo-test-workspace-*</location>
|
||||
<access>vscode.workspace.workspaceFolders![0].uri.fsPath</access>
|
||||
</workspace_creation>
|
||||
|
||||
<file_creation_strategy>
|
||||
<setup_phase>Create all test files in suiteSetup() before any tests run</setup_phase>
|
||||
<location>Always create files in the VSCode workspace directory</location>
|
||||
<verification>Verify files exist after creation to catch setup issues early</verification>
|
||||
<cleanup>Clean up all test files in suiteTeardown() to avoid test pollution</cleanup>
|
||||
<storage>Store file paths in a test-scoped object for easy reference</storage>
|
||||
</file_creation_strategy>
|
||||
|
||||
<ai_visibility>
|
||||
<important_note>The AI will not see the files in the workspace directory</important_note>
|
||||
<solution>Tell the AI to assume files exist and proceed as if they do</solution>
|
||||
<verification>Always verify outcomes rather than relying on AI file visibility</verification>
|
||||
</ai_visibility>
|
||||
</workspace_management>
|
||||
|
||||
<message_types>
|
||||
<description>Understanding message types for proper event handling</description>
|
||||
<reference>Check packages/types/src/message.ts for valid message types</reference>
|
||||
|
||||
<key_message_types>
|
||||
<api_req_started>
|
||||
<type>say</type>
|
||||
<say>api_req_started</say>
|
||||
<description>Indicates tool execution started</description>
|
||||
<text_content>JSON with tool name and execution details</text_content>
|
||||
<usage>Most reliable way to verify tool execution</usage>
|
||||
</api_req_started>
|
||||
|
||||
<completion_result>
|
||||
<description>Contains tool execution results</description>
|
||||
<usage>Tool results appear here, not in "tool_result" type</usage>
|
||||
</completion_result>
|
||||
|
||||
<text_messages>
|
||||
<description>General AI conversation messages</description>
|
||||
<caution>Format may vary, don't rely on parsing these for tool detection</caution>
|
||||
</text_messages>
|
||||
</key_message_types>
|
||||
</message_types>
|
||||
|
||||
<auto_approval_settings>
|
||||
<description>Settings to enable automatic approval of AI actions</description>
|
||||
<critical_settings>
|
||||
<alwaysAllowWrite>Enable for file creation/modification tests</alwaysAllowWrite>
|
||||
<alwaysAllowExecute>Enable for command execution tests</alwaysAllowExecute>
|
||||
<alwaysAllowBrowser>Enable for browser-related tests</alwaysAllowBrowser>
|
||||
</critical_settings>
|
||||
<usage>
|
||||
```typescript
|
||||
await api.updateSettings({
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true
|
||||
});
|
||||
```
|
||||
</usage>
|
||||
<importance>Without proper auto-approval settings, the AI won't be able to perform actions without user approval</importance>
|
||||
</auto_approval_settings>
|
||||
|
||||
<debugging_tools>
|
||||
<console_logging>
|
||||
<description>Use console.log for tracking test execution flow</description>
|
||||
<best_practices>
|
||||
- Log test phase transitions
|
||||
- Log important events and data
|
||||
- Log file paths and workspace state
|
||||
- Log expected vs actual outcomes
|
||||
</best_practices>
|
||||
</console_logging>
|
||||
|
||||
<state_validation>
|
||||
<description>Helper functions to validate test state at critical points</description>
|
||||
<includes>
|
||||
- Workspace file listing
|
||||
- Current working directory
|
||||
- Task status
|
||||
- Event counts
|
||||
</includes>
|
||||
</state_validation>
|
||||
|
||||
<error_analysis>
|
||||
<description>Tools for analyzing test failures</description>
|
||||
<techniques>
|
||||
- Stack trace analysis
|
||||
- Event timeline reconstruction
|
||||
- File system state comparison
|
||||
- Message flow analysis
|
||||
</techniques>
|
||||
</error_analysis>
|
||||
</debugging_tools>
|
||||
|
||||
<performance_considerations>
|
||||
<timeouts>
|
||||
<description>Appropriate timeout values for different operations</description>
|
||||
<task_completion>Use generous timeouts for task completion (30+ seconds)</task_completion>
|
||||
<file_operations>Shorter timeouts for file system operations (5-10 seconds)</file_operations>
|
||||
<event_waiting>Medium timeouts for event waiting (10-15 seconds)</event_waiting>
|
||||
</timeouts>
|
||||
|
||||
<resource_management>
|
||||
<description>Proper cleanup to avoid resource leaks</description>
|
||||
<event_listeners>Always clean up event listeners after tests</event_listeners>
|
||||
<tasks>Cancel or clear tasks in teardown</tasks>
|
||||
<files>Remove test files to avoid disk space issues</files>
|
||||
</resource_management>
|
||||
</performance_considerations>
|
||||
</test_environment_and_tools>
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
</step>
|
||||
<step>
|
||||
<title>Draft Comment</title>
|
||||
<description>Draft a comment for the GitHub issue that explains your findings and suggested solution in a conversational, human-like tone.</description>
|
||||
<description>Draft a comment for the GitHub issue that explains your findings and suggested solution in a conversational, human-like tone. Start the comment with "Hey @roomote-agent,".</description>
|
||||
</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
</category>
|
||||
<category name="communication">
|
||||
<item>Does the draft comment sound conversational and human?</item>
|
||||
<item>Does the draft comment start with "Hey @roomote-agent,"?</item>
|
||||
<item>Have I avoided technical jargon where possible?</item>
|
||||
<item>Is the tone helpful and not condescending?</item>
|
||||
</category>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,16 @@
|
|||
<practice>Use `gh issue view [URL] --json title,body,labels,comments` to fetch initial details.</practice>
|
||||
<practice>Use `gh issue comment [URL] --body "..."` to add comments, but only after explicit user approval.</practice>
|
||||
<practice>Always wrap the comment body in quotes to handle special characters.</practice>
|
||||
<practice>When posting a comment, the body must start with "Hey @roomote-agent," exactly.</practice>
|
||||
</best_practices>
|
||||
<example><![CDATA[
|
||||
<execute_command>
|
||||
<command>gh issue view https://github.com/RooCodeInc/Roo-Code/issues/123 --json title,body</command>
|
||||
</execute_command>
|
||||
]]></example>
|
||||
<example><![CDATA[
|
||||
<execute_command>
|
||||
<command>gh issue comment https://github.com/RooCodeInc/Roo-Code/issues/123 --body "Hey @roomote-agent, I've investigated and proposed a theoretical fix above."</command>
|
||||
</execute_command>
|
||||
]]></example>
|
||||
</tool>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<step number="6">
|
||||
<description>Formulate a solution and draft a comment.</description>
|
||||
<draft>
|
||||
"Hey, I took a look at this issue and it seems like the problem is in the `handleLogin` function in `src/auth/login.ts`. There's an empty `try...catch` block that is likely catching the login error and preventing any message from being shown to the user.
|
||||
"Hey @roomote-agent, I took a look at this issue and it seems like the problem is in the `handleLogin` function in `src/auth/login.ts`. There's an empty `try...catch` block that is likely catching the login error and preventing any message from being shown to the user.
|
||||
|
||||
A good first step to fixing this would be to add some logging to the `catch` block to see what error is actually happening. Then, we should probably implement proper error feedback to the user on the login page. This should solve the issue of the silent failure."
|
||||
</draft>
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@
|
|||
<tone_and_style>
|
||||
<principle>Be conversational and helpful, not robotic.</principle>
|
||||
<principle>Write comments as if you are a human developer collaborating on the project.</principle>
|
||||
<principle>Always start any GitHub issue comment body with "Hey @roomote-agent,".</principle>
|
||||
<avoid>
|
||||
<phrase>Analysis complete.</phrase>
|
||||
<phrase>The investigation has yielded the following result.</phrase>
|
||||
<phrase>Starting comments with "Hey," instead of "Hey @roomote-agent,".</phrase>
|
||||
</avoid>
|
||||
<prefer>
|
||||
<phrase>Hey, I took a look at this and found something interesting...</phrase>
|
||||
<phrase>I've been digging into this issue, and I think I've found a possible cause.</phrase>
|
||||
<phrase>Hey @roomote-agent, I took a look at this and found something interesting...</phrase>
|
||||
<phrase>Hey @roomote-agent, I've been digging into this issue, and I think I've found a possible cause.</phrase>
|
||||
</prefer>
|
||||
</tone_and_style>
|
||||
|
||||
<comment_structure>
|
||||
<element>Start with a friendly opening.</element>
|
||||
<element>Start every GitHub issue comment with "Hey @roomote-agent,".</element>
|
||||
<element>State your main finding or hypothesis clearly but not definitively.</element>
|
||||
<element>Provide context, like file paths and function names.</element>
|
||||
<element>Propose a next step or a theoretical solution.</element>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,190 +0,0 @@
|
|||
<github_issue_templates>
|
||||
<overview>
|
||||
This mode prioritizes using repository-specific issue templates over hardcoded ones.
|
||||
If no templates exist in the repository, simple generic templates are created on the fly.
|
||||
</overview>
|
||||
|
||||
<template_detection>
|
||||
<locations>
|
||||
<location priority="1">.github/ISSUE_TEMPLATE/*.yml</location>
|
||||
<location priority="2">.github/ISSUE_TEMPLATE/*.yaml</location>
|
||||
<location priority="3">.github/ISSUE_TEMPLATE/*.md</location>
|
||||
<location priority="4">.github/issue_template.md</location>
|
||||
<location priority="5">.github/ISSUE_TEMPLATE.md</location>
|
||||
</locations>
|
||||
|
||||
<yaml_template_structure>
|
||||
<field name="name">Display name of the template</field>
|
||||
<field name="description">Brief description of when to use this template</field>
|
||||
<field name="title">Default issue title (optional)</field>
|
||||
<field name="labels">Array of labels to apply</field>
|
||||
<field name="assignees">Array of default assignees</field>
|
||||
<field name="body">Array of form elements or markdown content</field>
|
||||
</yaml_template_structure>
|
||||
|
||||
<yaml_form_elements>
|
||||
<element type="markdown">
|
||||
<description>Static markdown content</description>
|
||||
<attributes>
|
||||
<attr name="value">The markdown content to display</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="input">
|
||||
<description>Single-line text input</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="placeholder">Placeholder text</attr>
|
||||
<attr name="value">Default value</attr>
|
||||
<attr name="required">Boolean</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="textarea">
|
||||
<description>Multi-line text input</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="placeholder">Placeholder text</attr>
|
||||
<attr name="value">Default value</attr>
|
||||
<attr name="required">Boolean</attr>
|
||||
<attr name="render">Language for syntax highlighting</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="dropdown">
|
||||
<description>Dropdown selection</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="options">Array of options</attr>
|
||||
<attr name="required">Boolean</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="checkboxes">
|
||||
<description>Multiple checkbox options</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="options">Array of checkbox items</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
</yaml_form_elements>
|
||||
|
||||
<markdown_template_structure>
|
||||
<front_matter>
|
||||
Optional YAML front matter with:
|
||||
- name: Template name
|
||||
- about: Template description
|
||||
- title: Default title
|
||||
- labels: Comma-separated or array
|
||||
- assignees: Comma-separated or array
|
||||
</front_matter>
|
||||
<body>
|
||||
Markdown content with sections and placeholders
|
||||
Common patterns:
|
||||
- Headers with ##
|
||||
- Placeholder text in brackets or as comments
|
||||
- Checklists with - [ ]
|
||||
- Code blocks with ```
|
||||
</body>
|
||||
</markdown_template_structure>
|
||||
</template_detection>
|
||||
|
||||
<generic_templates>
|
||||
<description>
|
||||
When no repository templates exist, create simple templates based on issue type.
|
||||
These should be minimal and focused on gathering essential information.
|
||||
</description>
|
||||
|
||||
<bug_template>
|
||||
<structure>
|
||||
- Description: Clear explanation of the bug
|
||||
- Steps to Reproduce: Numbered list
|
||||
- Expected Behavior: What should happen
|
||||
- Actual Behavior: What actually happens
|
||||
- Additional Context: Version, environment, logs
|
||||
- Code Investigation: Findings from exploration (if any)
|
||||
</structure>
|
||||
<labels>["bug"]</labels>
|
||||
</bug_template>
|
||||
|
||||
<feature_template>
|
||||
<structure>
|
||||
- Problem Description: What problem this solves
|
||||
- Current Behavior: How it works now
|
||||
- Proposed Solution: What should change
|
||||
- Impact: Who benefits and how
|
||||
- Technical Context: Code findings (if any)
|
||||
</structure>
|
||||
<labels>["enhancement", "proposal"]</labels>
|
||||
</feature_template>
|
||||
</generic_templates>
|
||||
|
||||
<template_parsing_guidelines>
|
||||
<guideline>
|
||||
When parsing YAML templates:
|
||||
1. Use a YAML parser to extract the structure
|
||||
2. Convert form elements to markdown sections
|
||||
3. Preserve required field indicators
|
||||
4. Include descriptions as help text
|
||||
5. Maintain the intended flow of the template
|
||||
</guideline>
|
||||
|
||||
<guideline>
|
||||
When parsing Markdown templates:
|
||||
1. Extract front matter if present
|
||||
2. Identify section headers
|
||||
3. Look for placeholder patterns
|
||||
4. Preserve formatting and structure
|
||||
5. Replace generic placeholders with user's information
|
||||
</guideline>
|
||||
|
||||
<guideline>
|
||||
For template selection:
|
||||
1. If only one template exists, use it automatically
|
||||
2. If multiple exist, let user choose based on name/description
|
||||
3. Match template to issue type when possible (bug vs feature)
|
||||
4. Respect template metadata (labels, assignees, etc.)
|
||||
</guideline>
|
||||
</template_parsing_guidelines>
|
||||
|
||||
<filling_templates>
|
||||
<principle>
|
||||
Fill templates intelligently using gathered information:
|
||||
- Map user's description to appropriate sections
|
||||
- Include code investigation findings where relevant
|
||||
- Preserve template structure and formatting
|
||||
- Don't leave placeholder text unfilled
|
||||
- Add contributor scoping if user is contributing
|
||||
</principle>
|
||||
|
||||
<mapping_examples>
|
||||
<example from="Steps to Reproduce" to="User's reproduction steps + code paths"/>
|
||||
<example from="Expected behavior" to="What user expects + code logic verification"/>
|
||||
<example from="System information" to="Detected versions + environment"/>
|
||||
<example from="Additional context" to="Code findings + architecture insights"/>
|
||||
</mapping_examples>
|
||||
</filling_templates>
|
||||
|
||||
<no_template_behavior>
|
||||
<description>
|
||||
When no templates exist, create appropriate generic templates on the fly.
|
||||
Keep them simple and focused on essential information.
|
||||
</description>
|
||||
|
||||
<guidelines>
|
||||
- Don't overwhelm with too many fields
|
||||
- Focus on problem description first
|
||||
- Include technical details only if user is contributing
|
||||
- Use clear, simple section headers
|
||||
- Adapt based on issue type (bug vs feature)
|
||||
</guidelines>
|
||||
</no_template_behavior>
|
||||
</github_issue_templates>
|
||||
|
|
@ -1,172 +1,147 @@
|
|||
<best_practices>
|
||||
<mode_scope>
|
||||
This mode assembles a template-free issue body grounded by codebase exploration and can submit it via GitHub CLI after explicit confirmation.
|
||||
Submission uses Title and Body only and targets the detected repository after the merged Review and Submit step.
|
||||
</mode_scope>
|
||||
|
||||
<mode_behavior>
|
||||
- CRITICAL: This mode assumes the user's FIRST message is already an issue description
|
||||
- Do NOT ask "What would you like to do?" or "Do you want to create an issue?"
|
||||
- Immediately start the issue creation workflow when the user begins talking
|
||||
- Treat their initial message as the problem/feature description
|
||||
- Begin with repository detection and codebase discovery right away
|
||||
- The user is already in "issue creation mode" by choosing this mode
|
||||
- Treat the user's FIRST message as the issue description; do not ask if they want to create an issue.
|
||||
- Start with repository detection (verify git repo; resolve OWNER/REPO from origin), then determine repository structure (monorepo/standard).
|
||||
- After detection, begin codebase discovery scoped to the repository root or the selected package (in monorepos).
|
||||
- Keep final output non-technical; implementation details remain internal.
|
||||
</mode_behavior>
|
||||
|
||||
<template_usage>
|
||||
- ALWAYS check for repository-specific issue templates before creating issues
|
||||
- Use templates from .github/ISSUE_TEMPLATE/ directory if they exist
|
||||
- Parse both YAML (.yml/.yaml) and Markdown (.md) template formats
|
||||
- If multiple templates exist, let the user choose the appropriate one
|
||||
- If no templates exist, create a simple generic template on the fly
|
||||
- NEVER fall back to hardcoded templates - always use repo templates or generate minimal ones
|
||||
- Respect template metadata like labels, assignees, and title patterns
|
||||
- Fill templates intelligently using gathered information from codebase exploration
|
||||
</template_usage>
|
||||
|
||||
<problem_reporting_focus>
|
||||
- Focus on helping users describe problems clearly, not solutions
|
||||
- The project team will design solutions unless the user explicitly wants to contribute
|
||||
- Don't push users to provide technical details they may not have
|
||||
- Make it easy for non-technical users to report issues effectively
|
||||
|
||||
CRITICAL: Lead with user impact:
|
||||
- Always explain WHO is affected and WHEN the problem occurs
|
||||
- Use concrete examples with actual values, not abstractions
|
||||
- Show before/after scenarios with specific data
|
||||
- Example: "Users trying to [action] see [actual result] instead of [expected result]"
|
||||
</problem_reporting_focus>
|
||||
|
||||
<fact_driven_verification>
|
||||
- ALWAYS verify user claims against actual code implementation
|
||||
- For feature requests, aggressively check if current behavior matches user's description
|
||||
- If code shows different intent than user describes, it might be a bug not a feature
|
||||
- Present code evidence when challenging user assumptions
|
||||
- Do not be agreeable - be fact-driven and question discrepancies
|
||||
- Continue verification until facts are established
|
||||
- A "feature request" where code shows the feature should already work is likely a bug
|
||||
|
||||
CRITICAL additions for thorough analysis:
|
||||
- Trace data flow from where values are created to where they're used
|
||||
- Look for existing variables/functions that already contain needed data
|
||||
- Check if the issue is just missing usage of existing code
|
||||
- Follow imports and exports to understand data availability
|
||||
- Identify patterns in similar features that work correctly
|
||||
</fact_driven_verification>
|
||||
|
||||
<general_practices>
|
||||
- Always search for existing similar issues before creating a new one
|
||||
- Check for and use repository issue templates before creating content
|
||||
- Include specific version numbers and environment details
|
||||
- Use code blocks with syntax highlighting for code snippets
|
||||
- Make titles descriptive but concise (e.g., "Dark theme: Submit button invisible due to white-on-grey text")
|
||||
- For bugs, always test if the issue is reproducible
|
||||
- Include screenshots or mockups when relevant (ask user to provide)
|
||||
- Link to related issues or PRs if found during exploration
|
||||
|
||||
CRITICAL: Use concrete examples throughout:
|
||||
- Show actual data values, not just descriptions
|
||||
- Include specific file paths and line numbers
|
||||
- Demonstrate the data flow with real examples
|
||||
- Bad: "The value is incorrect"
|
||||
- Good: "The function returns '123' when it should return '456'"
|
||||
</general_practices>
|
||||
|
||||
<contributor_specific>
|
||||
- Only perform issue scoping if user wants to contribute
|
||||
- Reference specific files and line numbers from codebase exploration
|
||||
- Ensure technical proposals align with project architecture
|
||||
- Include implementation steps and issue scoping
|
||||
- Provide clear acceptance criteria in Given/When/Then format
|
||||
- Consider trade-offs and alternative approaches
|
||||
|
||||
CRITICAL: Prioritize simple solutions:
|
||||
- ALWAYS check if needed functionality already exists before proposing new code
|
||||
- Look for existing variables that just need to be passed/used differently
|
||||
- Prefer using existing patterns over creating new ones
|
||||
- The best fix often involves minimal code changes
|
||||
- Example: "Use existing `modeInfo` from line 234 in export" vs "Create new mode tracking system"
|
||||
</contributor_specific>
|
||||
|
||||
<backwards_compatibility_focus>
|
||||
ALWAYS consider backwards compatibility:
|
||||
- Think about existing data/configurations already in use
|
||||
- Propose solutions that handle both old and new formats gracefully
|
||||
- Consider migration paths for existing users
|
||||
- Document any breaking changes clearly
|
||||
- Prefer additive changes over breaking changes when possible
|
||||
</backwards_compatibility_focus>
|
||||
|
||||
|
||||
<value_framing>
|
||||
<principles>
|
||||
- Always pair the problem with user-facing value: who is impacted, when it occurs, and why it matters.
|
||||
- Keep value non-technical (clarity, time saved, fewer errors, better UX, improved accessibility, reduced confusion).
|
||||
</principles>
|
||||
<lightweight_impact_options>
|
||||
- Severity: Blocker | High | Medium | Low (optional)
|
||||
- Reach: Few | Some | Many (optional)
|
||||
</lightweight_impact_options>
|
||||
</value_framing>
|
||||
|
||||
<sourcing_and_provenance>
|
||||
<direct_from_user_only>
|
||||
- Reproduction steps
|
||||
- Variations tried
|
||||
- Environment details
|
||||
</direct_from_user_only>
|
||||
<inference_allowed_with_care>
|
||||
- Problem/Value statement (plain-language synthesis from user wording)
|
||||
- Context (who/when) based on user input; keep code-based signals internal
|
||||
</inference_allowed_with_care>
|
||||
<hallucination_guards>
|
||||
- Never fabricate “Variations tried.” If not provided, omit.
|
||||
- If critical details are missing, ask targeted questions; otherwise proceed with omissions.
|
||||
</hallucination_guards>
|
||||
</sourcing_and_provenance>
|
||||
|
||||
<cli_submission>
|
||||
<confirmation>
|
||||
Use a single merged "Review and Submit" step with options:
|
||||
- Submit now
|
||||
- Submit now and assign to me
|
||||
Any other response is treated as a change request and the step is rerun after applying edits.
|
||||
</confirmation>
|
||||
<repo_detection>
|
||||
Submission requires repository detection (git present, origin configured). Capture normalized OWNER/REPO (e.g., owner/repo) and store as [OWNER_REPO] for submission.
|
||||
</repo_detection>
|
||||
<target_repo>
|
||||
Always specify the target using --repo "[OWNER_REPO]" to avoid ambiguity and ensure the correct repository is used.
|
||||
</target_repo>
|
||||
<assignment>
|
||||
When "Submit now and assign to me" is chosen, create using: --assignee "@me".
|
||||
If creation with --assignee fails (e.g., permissions), create the issue without an assignee and immediately run:
|
||||
gh issue edit <issue-url-or-number> --add-assignee "@me".
|
||||
</assignment>
|
||||
<command_safety>
|
||||
Use --body with robust quoting (for example: --body "$(printf '%s\n' "[ISSUE_BODY]")") or a heredoc; do not create temporary files or reference file paths. Always include --repo "[OWNER_REPO]" and echo the resulting issue URL.
|
||||
In execute_command calls, output only the command string; never include XML tags, CDATA markers, code fences, or backticks in the command payload.
|
||||
</command_safety>
|
||||
<error_handling>
|
||||
On gh errors (installation/auth), present the error and offer to retry after fixing gh setup. Surface the computed Title and Body inline
|
||||
so the user can submit manually if needed.
|
||||
</error_handling>
|
||||
</cli_submission>
|
||||
|
||||
<codebase_exploration>
|
||||
<principles>
|
||||
- Use semantic search first to find relevant areas.
|
||||
- Refine with targeted regex for exact strings (errors, component names, flags).
|
||||
- Read key files to verify behavior; keep evidence internal.
|
||||
- Early-stop when hits converge (~70%) or you can name the exact feature/component.
|
||||
- Escalate-once if signals conflict; run one refined batch, then proceed.
|
||||
</principles>
|
||||
<tool_sequence>
|
||||
1) codebase_search → 2) search_files → 3) read_file (as needed)
|
||||
</tool_sequence>
|
||||
<scoping>
|
||||
In monorepos, scope searches to the selected package when the context is clear; otherwise ask for the relevant package/app if ambiguous.
|
||||
</scoping>
|
||||
<internal_only>
|
||||
Keep language plain and exclude technical artifacts (paths, line numbers, stack traces, diffs) from the final issue body.
|
||||
</internal_only>
|
||||
</codebase_exploration>
|
||||
|
||||
<questioning>
|
||||
<guidelines>
|
||||
- Ask minimal, targeted questions based on what you found in code.
|
||||
- For bugs: request a minimal reproduction (environment, steps, expected, actual, variations).
|
||||
- For enhancements: capture user goal, desired behavior in plain language, and any constraints.
|
||||
- Present discrepancies in plain language (no code) and confirm understanding.
|
||||
</guidelines>
|
||||
</questioning>
|
||||
|
||||
<issue_output_rules>
|
||||
<format>
|
||||
<![CDATA[
|
||||
## Type
|
||||
Bug | Enhancement
|
||||
|
||||
## Problem / Value
|
||||
[One or two sentences that capture the problem and why it matters in plain language]
|
||||
|
||||
## Context
|
||||
[Who is affected and when it happens]
|
||||
[Enhancement: desired behavior conceptually, in the user's words]
|
||||
[Bug: current observed behavior in plain language]
|
||||
|
||||
## Reproduction (Bug only, if available)
|
||||
1) Steps (each action/command)
|
||||
2) Expected result
|
||||
3) Actual result
|
||||
4) Variations tried (only if explicitly provided)
|
||||
|
||||
## Constraints/Preferences
|
||||
[Performance, accessibility, UX, or other considerations]
|
||||
]]>
|
||||
</format>
|
||||
<rules>
|
||||
- Omit sections that would be empty.
|
||||
- Do not include "Variations tried" unless explicitly provided by the user.
|
||||
- Keep language plain and user-centric.
|
||||
- Exclude technical artifacts (paths, lines, stacks, diffs).
|
||||
</rules>
|
||||
</issue_output_rules>
|
||||
|
||||
<review_stage_presentation>
|
||||
- At each review stage, present the full current issue details (Title + Body) in a markdown code block.
|
||||
- Offer "Submit now" or "Submit now and assign to me" suggestions; treat any other response as a change request and rerun the step after applying edits.
|
||||
</review_stage_presentation>
|
||||
|
||||
<autonomy_and_budgets>
|
||||
- Tool preambles: restate goal briefly, outline a short plan, narrate progress succinctly, summarize final delta.
|
||||
- One-tool-per-message: await results before continuing.
|
||||
- Discovery budget: default max 3 searches before escalate-once; stop when sufficient.
|
||||
- Early-stop: when top hits converge or target is identifiable.
|
||||
- Verbosity: low narrative; detail appears only in structured outputs.
|
||||
</autonomy_and_budgets>
|
||||
|
||||
<communication_guidelines>
|
||||
- Be supportive and encouraging to problem reporters
|
||||
- Don't overwhelm users with technical questions upfront
|
||||
- Clearly indicate when technical sections are optional
|
||||
- Guide contributors through the additional requirements
|
||||
- Make the "submit now" option clear for problem reporters
|
||||
- When presenting template choices, include template descriptions to help users choose
|
||||
- Explain that you're using the repository's own templates for consistency
|
||||
- Be direct and concise; avoid jargon in the final issue body.
|
||||
- Keep questions optional and easy to answer with suggested options.
|
||||
- Emphasize WHO is affected and WHEN it happens.
|
||||
</communication_guidelines>
|
||||
|
||||
<template_best_practices>
|
||||
<practice name="template_detection">
|
||||
Always check these locations in order:
|
||||
1. .github/ISSUE_TEMPLATE/*.yml or *.yaml (GitHub form syntax)
|
||||
2. .github/ISSUE_TEMPLATE/*.md (Markdown templates)
|
||||
3. .github/issue_template.md (single template)
|
||||
4. .github/ISSUE_TEMPLATE.md (alternate naming)
|
||||
</practice>
|
||||
|
||||
<practice name="template_parsing">
|
||||
For YAML templates:
|
||||
- Extract form elements and convert to appropriate markdown sections
|
||||
- Preserve required field indicators
|
||||
- Include field descriptions as context
|
||||
- Respect dropdown options and checkbox lists
|
||||
|
||||
For Markdown templates:
|
||||
- Parse front matter for metadata
|
||||
- Identify section headers and structure
|
||||
- Replace placeholder text with actual information
|
||||
- Maintain formatting and hierarchy
|
||||
</practice>
|
||||
|
||||
<practice name="template_filling">
|
||||
- Map gathered information to template sections intelligently
|
||||
- Don't leave placeholder text in the final issue
|
||||
- Add code investigation findings to relevant sections
|
||||
- Include contributor scoping in appropriate section if applicable
|
||||
- Preserve the template's intended structure and flow
|
||||
</practice>
|
||||
|
||||
<practice name="no_template_handling">
|
||||
When no templates exist:
|
||||
- Create minimal, focused templates
|
||||
- Use simple section headers
|
||||
- Focus on essential information only
|
||||
- Adapt structure based on issue type
|
||||
- Don't overwhelm with unnecessary fields
|
||||
</practice>
|
||||
</template_best_practices>
|
||||
<technical_accuracy_guidelines>
|
||||
<guideline name="thorough_code_analysis">
|
||||
Before proposing ANY solution:
|
||||
1. Use codebase_search extensively to find all related code
|
||||
2. Read multiple files to understand the full context
|
||||
3. Trace variable usage from creation to consumption
|
||||
4. Look for similar working features to understand patterns
|
||||
5. Identify what already exists vs what's actually missing
|
||||
</guideline>
|
||||
|
||||
<guideline name="simplicity_first">
|
||||
When designing solutions:
|
||||
1. Check if the data/function already exists somewhere
|
||||
2. Look for configuration options before code changes
|
||||
3. Prefer passing existing variables over creating new ones
|
||||
4. Use established patterns from similar features
|
||||
5. Aim for minimal diff size
|
||||
</guideline>
|
||||
|
||||
<guideline name="precise_technical_details">
|
||||
Always include:
|
||||
- Exact file paths and line numbers
|
||||
- Variable/function names as they appear in code
|
||||
- Before/after code snippets showing minimal changes
|
||||
- Clear explanation of why the simple fix works
|
||||
</guideline>
|
||||
</technical_accuracy_guidelines>
|
||||
</best_practices>
|
||||
|
|
@ -1,126 +1,109 @@
|
|||
<common_mistakes_to_avoid>
|
||||
<mode_initialization_mistakes>
|
||||
- CRITICAL: Asking "What would you like to do?" when mode starts
|
||||
- Waiting for user to say "create an issue" or "make me an issue"
|
||||
- Not treating the first user message as the issue description
|
||||
- Delaying the workflow start with unnecessary questions
|
||||
- Asking if they want to create an issue when they've already chosen this mode
|
||||
- Not immediately beginning repository detection and codebase discovery
|
||||
- Asking "What would you like to do?" at start instead of treating the first message as the issue description
|
||||
- Delaying the workflow with unnecessary questions before discovery
|
||||
- Not immediately beginning codebase-aware discovery (semantic search → regex refine → read key files)
|
||||
- Skipping repository detection (git + origin) before discovery or submission
|
||||
- Not validating repository context before gh commands
|
||||
</mode_initialization_mistakes>
|
||||
|
||||
|
||||
<scope_mistakes>
|
||||
- Submitting without explicit user confirmation ("Submit now")
|
||||
- Targeting the wrong repository by relying on current directory defaults; always pass --repo OWNER/REPO detected in Step 2
|
||||
- Performing PR prep, complexity estimates, or technical scoping
|
||||
</scope_mistakes>
|
||||
|
||||
<submission_mistakes>
|
||||
<mistake_block>
|
||||
<mistake>Splitting final review and submission into multiple steps</mistake>
|
||||
<impact>Creates redundant prompts and inconsistent state; leads to janky UX</impact>
|
||||
<correct_approach>Use a single merged "Review and Submit" step offering only: Submit now, Submit now and assign to me; treat any other response as a change request</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Not offering "Submit now and assign to me"</mistake>
|
||||
<impact>Forces manual assignment later; reduces efficiency</impact>
|
||||
<correct_approach>Provide the assignment option and use gh issue create --assignee "@me"; if that fails, immediately run gh issue edit <issue-url-or-number> --add-assignee "@me"</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Using temporary files or --body-file for issue body submission</mistake>
|
||||
<impact>Introduces filesystem dependencies and leaks paths; contradicts single-command policy</impact>
|
||||
<correct_approach>Use inline --body with robust quoting, e.g., --body "$(printf '%s\n' "[ISSUE_BODY]")"; do not reference any file paths</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Omitting --repo or relying on current directory defaults</mistake>
|
||||
<impact>May submit to the wrong repository in multi-repo or worktree contexts</impact>
|
||||
<correct_approach>Always pass --repo [OWNER_REPO] detected in Step 2</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Attempting submission without prior repository detection</mistake>
|
||||
<impact>Commands may target the wrong repo or fail</impact>
|
||||
<correct_approach>Detect git repo and ensure origin is configured before any gh commands</correct_approach>
|
||||
</mistake_block>
|
||||
</submission_mistakes>
|
||||
|
||||
<sourcing_mistakes>
|
||||
<mistake_block>
|
||||
<mistake>Inventing or inferring “Variations tried” when the user didn’t provide any</mistake>
|
||||
<impact>Misleads triage and wastes time reproducing non-existent attempts</impact>
|
||||
<correct_approach>Omit the “Variations tried” line entirely unless explicitly provided; if needed, ask a targeted question first</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Framing only the problem without the value/impact</mistake>
|
||||
<impact>Makes prioritization harder; obscures who benefits and why it matters</impact>
|
||||
<correct_approach>Pair the problem with a plain-language value statement (who, when, why it matters)</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Overstating impact without user signal</mistake>
|
||||
<impact>Damages credibility and misguides prioritization</impact>
|
||||
<correct_approach>Use conservative, plain language; if unsure, omit severity/reach or ask a single targeted question</correct_approach>
|
||||
</mistake_block>
|
||||
</sourcing_mistakes>
|
||||
|
||||
<problem_reporting_mistakes>
|
||||
- Vague descriptions like "doesn't work" or "broken"
|
||||
- Missing reproduction steps for bugs
|
||||
- Feature requests without clear problem statements
|
||||
- Not explaining the impact on users
|
||||
- Forgetting to specify when/how the problem occurs
|
||||
- Using wrong labels or no labels
|
||||
- Titles that don't summarize the issue
|
||||
- Not checking for duplicates
|
||||
- Vague descriptions like "doesn't work" without who/when impact
|
||||
- Missing minimal reproduction for bugs (environment, steps, expected, actual, variations)
|
||||
- Enhancement requests that skip the user goal or desired behavior in plain language
|
||||
- Titles/summaries that don't quickly communicate the issue
|
||||
</problem_reporting_mistakes>
|
||||
|
||||
<workflow_mistakes>
|
||||
- Asking for technical details from non-contributing users
|
||||
- Performing issue scoping before confirming user wants to contribute
|
||||
- Requiring acceptance criteria from problem reporters
|
||||
- Making the process too complex for simple problem reports
|
||||
- Not clearly indicating the "submit now" option
|
||||
- Overwhelming users with contributor requirements upfront
|
||||
- Using hardcoded templates instead of repository templates
|
||||
- Not checking for issue templates before creating content
|
||||
- Ignoring template metadata like labels and assignees
|
||||
</workflow_mistakes>
|
||||
|
||||
<contributor_mistakes>
|
||||
- Starting implementation before approval
|
||||
- Not providing detailed issue scoping when contributing
|
||||
- Missing acceptance criteria for contributed features
|
||||
- Forgetting to include technical context from code exploration
|
||||
- Not considering trade-offs and alternatives
|
||||
- Proposing solutions without understanding current architecture
|
||||
</contributor_mistakes>
|
||||
|
||||
<technical_analysis_mistakes>
|
||||
<mistake>Not tracing data flow completely through the system</mistake>
|
||||
<impact>Missing that data already exists leads to proposing unnecessary new code</impact>
|
||||
|
||||
<output_mistakes>
|
||||
- Including code paths, line numbers, stack traces, or diffs in the final issue body
|
||||
- Adding labels, metadata, or repository details to the body
|
||||
- Leaving empty section placeholders instead of omitting the section
|
||||
- Using technical jargon instead of plain, user-centric language
|
||||
</output_mistakes>
|
||||
|
||||
<code_exploration_mistakes>
|
||||
<mistake>Skipping semantic search and jumping straight to assumptions</mistake>
|
||||
<impact>Leads to misclassification and inaccurate context</impact>
|
||||
<correct_approach>
|
||||
- Use codebase_search extensively to find ALL related code
|
||||
- Trace variables from creation to consumption
|
||||
- Check if needed data is already calculated but not used
|
||||
- Look for similar working features as patterns
|
||||
- Start with codebase_search on extracted keywords
|
||||
- Refine with search_files for exact strings (errors, component names, flags)
|
||||
- read_file only as needed to verify behavior; keep evidence internal
|
||||
- Early-stop when hits converge or you can name the exact feature/component
|
||||
- Escalate-once if signals conflict (one refined pass), then proceed
|
||||
</correct_approach>
|
||||
<example>
|
||||
Bad: "Add mode tracking to import function"
|
||||
Good: "The export already includes mode info at line 234, just use it in import at line 567"
|
||||
</example>
|
||||
</technical_analysis_mistakes>
|
||||
|
||||
<solution_design_mistakes>
|
||||
<mistake>Proposing complex new systems when simple fixes exist</mistake>
|
||||
<impact>Creates unnecessary complexity, maintenance burden, and potential bugs</impact>
|
||||
</code_exploration_mistakes>
|
||||
|
||||
<discrepancy_handling_mistakes>
|
||||
<mistake>Accepting user claims that contradict the codebase without verification</mistake>
|
||||
<impact>Produces misleading or incorrect issue framing</impact>
|
||||
<correct_approach>
|
||||
- ALWAYS check if functionality already exists first
|
||||
- Look for minimal changes that solve the problem
|
||||
- Prefer using existing variables/functions differently
|
||||
- Aim for the smallest possible diff
|
||||
- Verify claims against the implementation; trace data from creation → usage
|
||||
- Compare with similar working features to ground expectations
|
||||
- If discrepancies arise, present concrete, plain-language examples (no code) and confirm
|
||||
</correct_approach>
|
||||
<example>
|
||||
Bad: "Create new state management system for mode tracking"
|
||||
Good: "Pass existing modeInfo variable from line 45 to the function at line 78"
|
||||
</example>
|
||||
</solution_design_mistakes>
|
||||
|
||||
<code_verification_mistakes>
|
||||
<mistake>Not reading actual code before proposing solutions</mistake>
|
||||
<impact>Solutions don't match the actual codebase structure</impact>
|
||||
<correct_approach>
|
||||
- Always read the relevant files first
|
||||
- Verify exact line numbers and content
|
||||
- Check imports/exports to understand data availability
|
||||
- Look at similar features that work correctly
|
||||
</correct_approach>
|
||||
</code_verification_mistakes>
|
||||
|
||||
<pattern_recognition_mistakes>
|
||||
<mistake>Creating new patterns instead of following existing ones</mistake>
|
||||
<impact>Inconsistent codebase, harder to maintain</impact>
|
||||
<correct_approach>
|
||||
- Find similar features that work correctly
|
||||
- Follow the same patterns and structures
|
||||
- Reuse existing utilities and helpers
|
||||
- Maintain consistency with the codebase style
|
||||
</correct_approach>
|
||||
</pattern_recognition_mistakes>
|
||||
|
||||
<template_usage_mistakes>
|
||||
<mistake>Using hardcoded templates when repository templates exist</mistake>
|
||||
<impact>Issues don't follow repository conventions, may be rejected or need reformatting</impact>
|
||||
<correct_approach>
|
||||
- Always check .github/ISSUE_TEMPLATE/ directory first
|
||||
- Parse and use repository templates when available
|
||||
- Only create generic templates when none exist
|
||||
</correct_approach>
|
||||
</template_usage_mistakes>
|
||||
|
||||
<template_parsing_mistakes>
|
||||
<mistake>Not properly parsing YAML template structure</mistake>
|
||||
<impact>Missing required fields, incorrect formatting, lost metadata</impact>
|
||||
<correct_approach>
|
||||
- Parse YAML templates to extract all form elements
|
||||
- Convert form elements to appropriate markdown sections
|
||||
- Preserve field requirements and descriptions
|
||||
- Maintain dropdown options and checkbox lists
|
||||
</correct_approach>
|
||||
</template_parsing_mistakes>
|
||||
|
||||
<template_filling_mistakes>
|
||||
<mistake>Leaving placeholder text in final issue</mistake>
|
||||
<impact>Unprofessional appearance, confusion about what information is needed</impact>
|
||||
<correct_approach>
|
||||
- Replace all placeholders with actual information
|
||||
- Remove instruction text meant for template users
|
||||
- Fill every section with relevant content
|
||||
- Add "N/A" for truly inapplicable sections
|
||||
</correct_approach>
|
||||
</template_filling_mistakes>
|
||||
</discrepancy_handling_mistakes>
|
||||
|
||||
<questioning_mistakes>
|
||||
- Asking broad, unfocused questions instead of targeted ones based on findings
|
||||
- Demanding technical details from non-technical users
|
||||
- Failing to provide easy, suggested answer formats (repro scaffold, goal statement)
|
||||
</questioning_mistakes>
|
||||
|
||||
<consistency_mistakes>
|
||||
- Mixing internal technical evidence into the final body
|
||||
- Ignoring the issue format or adding extra sections
|
||||
- Using inconsistent tone or switching between technical and non-technical language
|
||||
</consistency_mistakes>
|
||||
</common_mistakes_to_avoid>
|
||||
134
.roo/rules-issue-writer/5_examples.xml
Normal file
134
.roo/rules-issue-writer/5_examples.xml
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<issue_examples>
|
||||
<overview>
|
||||
Examples of assembling template-free issue prompts grounded by codebase exploration, with optional CLI submission after explicit confirmation.
|
||||
Repository detection precedes submission; review and submission occur in a single merged step offering "Submit now" or "Submit now and assign to me". Any other response is treated as a change request.
|
||||
</overview>
|
||||
|
||||
<example name="bug_dark_theme_button_invisible">
|
||||
<user_input>
|
||||
In dark theme the Submit button is almost invisible on the New Run page.
|
||||
</user_input>
|
||||
<discovery>
|
||||
<tool_calls>
|
||||
<![CDATA[
|
||||
<codebase_search>
|
||||
<query>dark theme submit button visibility</query>
|
||||
</codebase_search>
|
||||
|
||||
<search_files>
|
||||
<path>.</path>
|
||||
<regex>Submit|button|dark|theme</regex>
|
||||
</search_files>
|
||||
]]>
|
||||
</tool_calls>
|
||||
<notes>
|
||||
Internal: matches found in UI components related to theme; wording grounded to user impact.
|
||||
</notes>
|
||||
</discovery>
|
||||
<final_issue_body><![CDATA[
|
||||
## Type
|
||||
Bug
|
||||
|
||||
## Problem / Value
|
||||
In dark theme, the Submit button is hard to see on the new run form, making it difficult for users to complete new runs.
|
||||
|
||||
## Context
|
||||
Affects users creating new runs with dark theme enabled; the button appears low-contrast and is difficult to locate.
|
||||
|
||||
## Reproduction
|
||||
1) Steps: Open "New Run" -> Scroll to bottom -> Look for Submit
|
||||
2) Expected result: Clearly visible, high-contrast Submit button
|
||||
3) Actual result: Button appears nearly invisible in dark theme
|
||||
4) Variations tried: Different browsers (Chrome/Firefox) show same result
|
||||
]]></final_issue_body>
|
||||
</example>
|
||||
|
||||
<example name="enhancement_copy_run_confirmation">
|
||||
<user_input>
|
||||
I accidentally click "Copy Run" sometimes; would be great to have a simple confirmation.
|
||||
</user_input>
|
||||
<discovery>
|
||||
<tool_calls>
|
||||
<![CDATA[
|
||||
<codebase_search>
|
||||
<query>Copy Run confirmation</query>
|
||||
</codebase_search>
|
||||
]]>
|
||||
</tool_calls>
|
||||
<notes>
|
||||
Internal: feature entry point identified; keep final output non-technical and user-centric.
|
||||
</notes>
|
||||
</discovery>
|
||||
<final_issue_body><![CDATA[
|
||||
## Type
|
||||
Enhancement
|
||||
|
||||
## Problem / Value
|
||||
Add a confirmation dialog before copying an existing run to prevent accidental duplication.
|
||||
|
||||
## Context
|
||||
Users sometimes click "Copy Run" by mistake when browsing runs; a simple confirmation would prevent accidental duplication.
|
||||
|
||||
## Constraints/Preferences
|
||||
Keep the flow lightweight and unobtrusive; avoid slowing down intentional copies.
|
||||
]]></final_issue_body>
|
||||
</example>
|
||||
|
||||
<example name="bug_submission_review_and_assign">
|
||||
<user_input>
|
||||
Dark theme Submit button is invisible; I'd like to file this.
|
||||
</user_input>
|
||||
<final_issue_body><![CDATA[
|
||||
## Type
|
||||
Bug
|
||||
|
||||
## Problem / Value
|
||||
In dark theme, the Submit button is hard to see on the new run form, making it difficult for users to complete new runs.
|
||||
|
||||
## Context
|
||||
Affects users creating new runs with dark theme enabled; the button appears low-contrast and is difficult to locate.
|
||||
|
||||
## Reproduction
|
||||
1) Steps: Open "New Run" -> Scroll to bottom -> Look for Submit
|
||||
2) Expected result: Clearly visible, high-contrast Submit button
|
||||
3) Actual result: Button appears nearly invisible in dark theme
|
||||
]]></final_issue_body>
|
||||
<review_and_submit>
|
||||
<ask_followup_question>
|
||||
<question>Review the current issue details. Select one of the options below or specify any changes or other workflow you would like me to perform:
|
||||
|
||||
```md
|
||||
Title: [ISSUE_TITLE]
|
||||
|
||||
[ISSUE_BODY]
|
||||
```</question>
|
||||
<follow_up>
|
||||
<suggest>Submit now</suggest>
|
||||
<suggest>Submit now and assign to me</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
|
||||
<execute_command for="submit_now">
|
||||
<command>gh issue create --repo "[OWNER_REPO]" --title "[ISSUE_TITLE]" --body "$(printf '%s\n' "[ISSUE_BODY]")"</command>
|
||||
</execute_command>
|
||||
|
||||
<execute_command for="submit_now_and_assign_to_me">
|
||||
<command>ISSUE_URL=$(gh issue create --repo "[OWNER_REPO]" --title "[ISSUE_TITLE]" --body "$(printf '%s\n' "[ISSUE_BODY]")" --assignee "@me") || true; if [ -z "$ISSUE_URL" ]; then ISSUE_URL=$(gh issue create --repo "[OWNER_REPO]" --title "[ISSUE_TITLE]" --body "$(printf '%s\n' "[ISSUE_BODY]")"); gh issue edit "$ISSUE_URL" --add-assignee "@me"; fi; echo "$ISSUE_URL"</command>
|
||||
</execute_command>
|
||||
|
||||
<loopback_note>
|
||||
If a change request is provided, collect the requested edits, update the draft (re-run discovery if new info affects context), then rerun this merged step.
|
||||
</loopback_note>
|
||||
|
||||
<expected_output>https://github.com/OWNER/REPO/issues/123</expected_output>
|
||||
</review_and_submit>
|
||||
</example>
|
||||
|
||||
<policies>
|
||||
<policy>Issues are template-free (Title + Body only).</policy>
|
||||
<policy>Repository detection (git + origin → OWNER/REPO) occurs before submission and is passed explicitly via --repo [OWNER_REPO].</policy>
|
||||
<policy>Never use --body-file or temporary files; submit with inline --body only (no file paths).</policy>
|
||||
<policy>Review and submission happen in one merged step offering "Submit now" or "Submit now and assign to me"; any other response is treated as a change request.</policy>
|
||||
<policy>All discovery is internal; keep final output plain-language.</policy>
|
||||
</policies>
|
||||
</issue_examples>
|
||||
|
|
@ -1,342 +0,0 @@
|
|||
<github_cli_usage>
|
||||
<overview>
|
||||
The GitHub CLI (gh) provides comprehensive tools for interacting with GitHub.
|
||||
Here's when and how to use each command in the issue creation workflow.
|
||||
|
||||
Note: This mode prioritizes using repository-specific issue templates over
|
||||
hardcoded ones. Templates are detected and used dynamically from the repository.
|
||||
</overview>
|
||||
|
||||
<pre_creation_commands>
|
||||
<command name="gh issue list">
|
||||
<when_to_use>
|
||||
ALWAYS use this FIRST before creating any issue to check for duplicates.
|
||||
Search for keywords from the user's problem description.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue list --repo $REPO_FULL_NAME --search "dark theme button visibility" --state all --limit 20</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
<options>
|
||||
--search: Search query for issue titles and bodies
|
||||
--state: all, open, or closed
|
||||
--label: Filter by specific labels
|
||||
--limit: Number of results to show
|
||||
--json: Get structured JSON output
|
||||
</options>
|
||||
</command>
|
||||
|
||||
<command name="gh search issues">
|
||||
<when_to_use>
|
||||
Use for more advanced searches across issues and pull requests.
|
||||
Supports GitHub's advanced search syntax.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh search issues --repo $REPO_FULL_NAME "dark theme button" --limit 10</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh issue view">
|
||||
<when_to_use>
|
||||
Use when you find a potentially related issue and need full details.
|
||||
Check if the user's issue is already reported or related.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue view 123 --repo $REPO_FULL_NAME --comments</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
<options>
|
||||
--comments: Include issue comments
|
||||
--json: Get structured data
|
||||
--web: Open in browser
|
||||
</options>
|
||||
</command>
|
||||
</pre_creation_commands>
|
||||
|
||||
<template_detection_commands>
|
||||
<command name="list_files">
|
||||
<when_to_use>
|
||||
Use to check for issue templates in the repository before creating issues.
|
||||
This is not a gh command but necessary for template detection.
|
||||
</when_to_use>
|
||||
<examples>
|
||||
Check for templates in standard location:
|
||||
<list_files>
|
||||
<path>.github/ISSUE_TEMPLATE</path>
|
||||
<recursive>true</recursive>
|
||||
</list_files>
|
||||
|
||||
Check for single template file:
|
||||
<list_files>
|
||||
<path>.github</path>
|
||||
<recursive>false</recursive>
|
||||
</list_files>
|
||||
</examples>
|
||||
</command>
|
||||
|
||||
<command name="read_file">
|
||||
<when_to_use>
|
||||
Read template files to parse their structure and content.
|
||||
Used after detecting template files.
|
||||
</when_to_use>
|
||||
<examples>
|
||||
Read YAML template:
|
||||
<read_file>
|
||||
<path>.github/ISSUE_TEMPLATE/bug_report.yml</path>
|
||||
</read_file>
|
||||
|
||||
Read Markdown template:
|
||||
<read_file>
|
||||
<path>.github/ISSUE_TEMPLATE/feature_request.md</path>
|
||||
</read_file>
|
||||
</examples>
|
||||
</command>
|
||||
</template_detection_commands>
|
||||
|
||||
<contributor_only_commands>
|
||||
<note>
|
||||
These commands should ONLY be used if the user has indicated they want to
|
||||
contribute the implementation. Skip these for problem reporters.
|
||||
</note>
|
||||
|
||||
<command name="gh repo view">
|
||||
<when_to_use>
|
||||
Get repository information and recent activity.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh repo view $REPO_FULL_NAME --json defaultBranchRef,description,updatedAt</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh search prs">
|
||||
<when_to_use>
|
||||
Check recent PRs that might be related to the issue.
|
||||
Look for PRs that modified relevant code.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh search prs --repo $REPO_FULL_NAME "dark theme" --limit 10 --state all</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="git log">
|
||||
<when_to_use>
|
||||
For bug reports from contributors, check recent commits that might have introduced the issue.
|
||||
Use after cloning the repository locally.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>git log --oneline --grep="theme" -n 20</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
</contributor_only_commands>
|
||||
|
||||
<issue_creation_command>
|
||||
<command name="gh issue create">
|
||||
<when_to_use>
|
||||
Only use after:
|
||||
1. Confirming no duplicates exist
|
||||
2. Checking for and using repository templates
|
||||
3. Gathering all required information
|
||||
4. Determining if user is contributing or just reporting
|
||||
5. Getting user confirmation
|
||||
</when_to_use>
|
||||
<bug_report_example>
|
||||
<execute_command>
|
||||
<command>gh issue create --repo $REPO_FULL_NAME --title "[Descriptive title of the bug]" --body-file /tmp/issue_body.md --label "bug"</command>
|
||||
</execute_command>
|
||||
</bug_report_example>
|
||||
<feature_request_example>
|
||||
<execute_command>
|
||||
<command>gh issue create --repo $REPO_FULL_NAME --title "[Problem-focused title]" --body-file /tmp/issue_body.md --label "proposal" --label "enhancement"</command>
|
||||
</execute_command>
|
||||
</feature_request_example>
|
||||
<options>
|
||||
--title: Issue title (required)
|
||||
--body: Issue body text
|
||||
--body-file: Read body from file
|
||||
--label: Add labels (can use multiple times)
|
||||
--assignee: Assign to user
|
||||
--project: Add to project
|
||||
--web: Open in browser to create
|
||||
</options>
|
||||
</command>
|
||||
</issue_creation_command>
|
||||
|
||||
<post_creation_commands>
|
||||
<command name="gh issue comment">
|
||||
<when_to_use>
|
||||
ONLY use if user wants to add additional information after creation.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue comment 456 --repo $REPO_FULL_NAME --body "Additional context or comments."</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh issue edit">
|
||||
<when_to_use>
|
||||
Use if user realizes they need to update the issue after creation.
|
||||
Can update title, body, or labels.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue edit 456 --repo $REPO_FULL_NAME --title "[Updated title]" --body "[Updated body]"</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
</post_creation_commands>
|
||||
|
||||
<workflow_integration>
|
||||
<step_1_integration>
|
||||
After user selects issue type, immediately search for related issues:
|
||||
1. Use `gh issue list --search` with keywords from their description
|
||||
2. Show any similar issues found
|
||||
3. Ask if they want to continue or comment on existing issue
|
||||
</step_1_integration>
|
||||
|
||||
<step_2_integration>
|
||||
Template detection (NEW):
|
||||
1. Use list_files to check .github/ISSUE_TEMPLATE/ directory
|
||||
2. Read any template files found (YAML or Markdown)
|
||||
3. Parse template structure and metadata
|
||||
4. If multiple templates, let user choose
|
||||
5. If no templates, prepare to create generic one
|
||||
</step_2_integration>
|
||||
|
||||
<step_3_integration>
|
||||
Decision point for contribution:
|
||||
1. Ask user if they want to contribute implementation
|
||||
2. If yes: Use contributor commands for codebase investigation
|
||||
3. If no: Skip directly to creating a problem-focused issue
|
||||
4. This saves time for problem reporters
|
||||
</step_3_integration>
|
||||
|
||||
<step_4_integration>
|
||||
During codebase exploration (CONTRIBUTORS ONLY):
|
||||
1. Clone repo locally if needed: `gh repo clone $REPO_FULL_NAME`
|
||||
2. Use `git log` to find recent changes to affected files
|
||||
3. Use `gh search prs` for related pull requests
|
||||
4. Include findings in the technical context section
|
||||
</step_4_integration>
|
||||
|
||||
<step_5_integration>
|
||||
When creating the issue:
|
||||
1. Use repository template if found, or generic template if not
|
||||
2. Fill template with gathered information
|
||||
3. Format differently based on contributor vs problem reporter
|
||||
4. Save formatted body to temporary file
|
||||
5. Use `gh issue create` with appropriate labels from template
|
||||
6. Capture the returned issue URL
|
||||
7. Show user the created issue URL
|
||||
</step_5_integration>
|
||||
</workflow_integration>
|
||||
|
||||
<best_practices>
|
||||
<practice name="file_handling">
|
||||
When creating issues with long bodies:
|
||||
1. Save to temporary file: `cat > /tmp/issue_body.md << 'EOF'`
|
||||
2. Use --body-file flag with gh issue create
|
||||
3. Clean up after: `rm /tmp/issue_body.md`
|
||||
</practice>
|
||||
|
||||
<practice name="search_efficiency">
|
||||
Use specific search terms:
|
||||
- Include error messages in quotes
|
||||
- Use label filters when appropriate
|
||||
- Limit results to avoid overwhelming output
|
||||
</practice>
|
||||
|
||||
<practice name="json_output">
|
||||
Use --json flag for structured data when needed:
|
||||
- Easier to parse programmatically
|
||||
- Consistent format across commands
|
||||
- Example: `gh issue list --json number,title,state`
|
||||
</practice>
|
||||
</best_practices>
|
||||
|
||||
<error_handling>
|
||||
<duplicate_found>
|
||||
If search finds exact duplicate:
|
||||
- Show the existing issue to user using `gh issue view`
|
||||
- Ask if they want to add a comment instead
|
||||
- Use `gh issue comment` if they agree
|
||||
</duplicate_found>
|
||||
|
||||
<creation_failed>
|
||||
If `gh issue create` fails:
|
||||
- Check error message (auth, permissions, network)
|
||||
- Ensure gh is authenticated: `gh auth status`
|
||||
- Save the drafted issue content for user
|
||||
- Suggest using --web flag to create in browser
|
||||
</creation_failed>
|
||||
|
||||
<authentication>
|
||||
Ensure GitHub CLI is authenticated:
|
||||
- Check status: `gh auth status`
|
||||
- Login if needed: `gh auth login`
|
||||
- Select appropriate scopes for issue creation
|
||||
</authentication>
|
||||
</error_handling>
|
||||
|
||||
<command_reference>
|
||||
<issues>
|
||||
gh issue create - Create new issue
|
||||
gh issue list - List and search issues
|
||||
gh issue view - View issue details
|
||||
gh issue comment - Add comment to issue
|
||||
gh issue edit - Edit existing issue
|
||||
gh issue close - Close an issue
|
||||
gh issue reopen - Reopen closed issue
|
||||
</issues>
|
||||
|
||||
<search>
|
||||
gh search issues - Search issues and PRs
|
||||
gh search prs - Search pull requests
|
||||
gh search repos - Search repositories
|
||||
</search>
|
||||
|
||||
<repository>
|
||||
gh repo view - View repository info
|
||||
gh repo clone - Clone repository
|
||||
</repository>
|
||||
</command_reference>
|
||||
|
||||
<template_handling_reference>
|
||||
<yaml_template_parsing>
|
||||
When parsing YAML templates:
|
||||
- Extract 'name' for template identification
|
||||
- Get 'labels' array for automatic labeling
|
||||
- Parse 'body' array for form elements
|
||||
- Convert form elements to markdown sections
|
||||
- Preserve 'required' field indicators
|
||||
</yaml_template_parsing>
|
||||
|
||||
<markdown_template_parsing>
|
||||
When parsing Markdown templates:
|
||||
- Check for YAML front matter
|
||||
- Extract metadata (labels, assignees)
|
||||
- Identify section headers
|
||||
- Replace placeholder text
|
||||
- Maintain formatting structure
|
||||
</markdown_template_parsing>
|
||||
|
||||
<template_usage_flow>
|
||||
1. Detect templates with list_files
|
||||
2. Read templates with read_file
|
||||
3. Parse structure and metadata
|
||||
4. Let user choose if multiple exist
|
||||
5. Fill template with information
|
||||
6. Create issue with template content
|
||||
</template_usage_flow>
|
||||
</template_handling_reference>
|
||||
</github_cli_usage>
|
||||
|
|
@ -1,301 +0,0 @@
|
|||
<mode_management_workflow>
|
||||
<overview>
|
||||
This workflow guides you through creating new custom modes or editing existing modes
|
||||
for the Roo Code Software, ensuring comprehensive understanding and cohesive implementation.
|
||||
</overview>
|
||||
|
||||
<initial_determination>
|
||||
<step number="1">
|
||||
<title>Determine User Intent</title>
|
||||
<description>
|
||||
Identify whether the user wants to create a new mode or edit an existing one
|
||||
</description>
|
||||
<detection_patterns>
|
||||
<pattern type="edit_existing">
|
||||
<indicators>
|
||||
<indicator>User mentions a specific mode by name or slug</indicator>
|
||||
<indicator>User references a mode directory path (e.g., .roo/rules-[mode-slug])</indicator>
|
||||
<indicator>User asks to modify, update, enhance, or fix an existing mode</indicator>
|
||||
<indicator>User says "edit this mode" or "change this mode"</indicator>
|
||||
</indicators>
|
||||
</pattern>
|
||||
<pattern type="create_new">
|
||||
<indicators>
|
||||
<indicator>User asks to create a new mode</indicator>
|
||||
<indicator>User describes a new capability not covered by existing modes</indicator>
|
||||
<indicator>User says "make a mode for" or "create a mode that"</indicator>
|
||||
</indicators>
|
||||
</pattern>
|
||||
</detection_patterns>
|
||||
<clarification_question>
|
||||
<ask_followup_question>
|
||||
<question>I want to make sure I understand correctly. Are you looking to create a brand new mode or modify an existing one?</question>
|
||||
<follow_up>
|
||||
<suggest>Create a new mode for a specific purpose</suggest>
|
||||
<suggest>Edit an existing mode to add new capabilities</suggest>
|
||||
<suggest>Fix issues in an existing mode</suggest>
|
||||
<suggest>Enhance an existing mode with better workflows</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</clarification_question>
|
||||
</step>
|
||||
</initial_determination>
|
||||
|
||||
<workflow_branches>
|
||||
<branch name="create_new_mode">
|
||||
<step number="2a">
|
||||
<title>Gather Requirements for New Mode</title>
|
||||
<description>
|
||||
Understand what the user wants the new mode to accomplish
|
||||
</description>
|
||||
<actions>
|
||||
<action>Ask about the mode's primary purpose and use cases</action>
|
||||
<action>Identify what types of tasks the mode should handle</action>
|
||||
<action>Determine what tools and file access the mode needs</action>
|
||||
<action>Clarify any special behaviors or restrictions</action>
|
||||
</actions>
|
||||
<example>
|
||||
<ask_followup_question>
|
||||
<question>What is the primary purpose of this new mode? What types of tasks should it handle?</question>
|
||||
<follow_up>
|
||||
<suggest>A mode for writing and maintaining documentation</suggest>
|
||||
<suggest>A mode for database schema design and migrations</suggest>
|
||||
<suggest>A mode for API endpoint development and testing</suggest>
|
||||
<suggest>A mode for performance optimization and profiling</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</example>
|
||||
</step>
|
||||
|
||||
<step number="3a">
|
||||
<title>Design Mode Configuration</title>
|
||||
<description>
|
||||
Create the mode definition with all required fields
|
||||
</description>
|
||||
<required_fields>
|
||||
<field name="slug">
|
||||
<description>Unique identifier (lowercase, hyphens allowed)</description>
|
||||
<best_practice>Keep it short and descriptive (e.g., "api-dev", "docs-writer")</best_practice>
|
||||
</field>
|
||||
<field name="name">
|
||||
<description>Display name with optional emoji</description>
|
||||
<best_practice>Use an emoji that represents the mode's purpose</best_practice>
|
||||
</field>
|
||||
<field name="roleDefinition">
|
||||
<description>Detailed description of the mode's role and expertise</description>
|
||||
<best_practice>
|
||||
Start with "You are Roo Code, a [specialist type]..."
|
||||
List specific areas of expertise
|
||||
Mention key technologies or methodologies
|
||||
</best_practice>
|
||||
</field>
|
||||
<field name="groups">
|
||||
<description>Tool groups the mode can access</description>
|
||||
<options>
|
||||
<option name="read">File reading and searching tools</option>
|
||||
<option name="edit">File editing tools (can be restricted by regex)</option>
|
||||
<option name="command">Command execution tools</option>
|
||||
<option name="browser">Browser interaction tools</option>
|
||||
<option name="mcp">MCP server tools</option>
|
||||
</options>
|
||||
</field>
|
||||
</required_fields>
|
||||
<recommended_fields>
|
||||
<field name="whenToUse">
|
||||
<description>Clear description for the Orchestrator</description>
|
||||
<best_practice>Explain specific scenarios and task types</best_practice>
|
||||
</field>
|
||||
</recommended_fields>
|
||||
<important_note>
|
||||
Do not include customInstructions in the .roomodes configuration.
|
||||
All detailed instructions should be placed in XML files within
|
||||
the .roo/rules-[mode-slug]/ directory instead.
|
||||
</important_note>
|
||||
</step>
|
||||
|
||||
<step number="4a">
|
||||
<title>Implement File Restrictions</title>
|
||||
<description>
|
||||
Configure appropriate file access permissions
|
||||
</description>
|
||||
<example>
|
||||
<comment>Restrict edit access to specific file types</comment>
|
||||
<code>
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: \.(md|txt|rst)$
|
||||
description: Documentation files only
|
||||
- command
|
||||
</code>
|
||||
</example>
|
||||
<guidelines>
|
||||
<guideline>Use regex patterns to limit file editing scope</guideline>
|
||||
<guideline>Provide clear descriptions for restrictions</guideline>
|
||||
<guideline>Consider the principle of least privilege</guideline>
|
||||
</guidelines>
|
||||
</step>
|
||||
|
||||
<step number="5a">
|
||||
<title>Create XML Instruction Files</title>
|
||||
<description>
|
||||
Design structured instruction files in .roo/rules-[mode-slug]/
|
||||
</description>
|
||||
<file_structure>
|
||||
<file name="1_workflow.xml">Main workflow and step-by-step processes</file>
|
||||
<file name="2_best_practices.xml">Guidelines and conventions</file>
|
||||
<file name="3_common_patterns.xml">Reusable code patterns and examples</file>
|
||||
<file name="4_tool_usage.xml">Specific tool usage instructions</file>
|
||||
<file name="5_examples.xml">Complete workflow examples</file>
|
||||
</file_structure>
|
||||
<xml_best_practices>
|
||||
<practice>Use semantic tag names that describe content</practice>
|
||||
<practice>Nest tags hierarchically for better organization</practice>
|
||||
<practice>Include code examples in CDATA sections when needed</practice>
|
||||
<practice>Add comments to explain complex sections</practice>
|
||||
</xml_best_practices>
|
||||
</step>
|
||||
</branch>
|
||||
|
||||
<branch name="edit_existing_mode">
|
||||
<step number="2b">
|
||||
<title>Immerse in Existing Mode</title>
|
||||
<description>
|
||||
Fully understand the existing mode before making any changes
|
||||
</description>
|
||||
<actions>
|
||||
<action>Locate and read the mode configuration in .roomodes</action>
|
||||
<action>Read all XML instruction files in .roo/rules-[mode-slug]/</action>
|
||||
<action>Analyze the mode's current capabilities and limitations</action>
|
||||
<action>Understand the mode's role in the broader ecosystem</action>
|
||||
</actions>
|
||||
<questions_to_ask>
|
||||
<ask_followup_question>
|
||||
<question>What specific aspects of the mode would you like to change or enhance?</question>
|
||||
<follow_up>
|
||||
<suggest>Add new capabilities or tool permissions</suggest>
|
||||
<suggest>Fix issues with current workflows or instructions</suggest>
|
||||
<suggest>Improve the mode's roleDefinition or whenToUse description</suggest>
|
||||
<suggest>Enhance XML instructions for better clarity</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</questions_to_ask>
|
||||
</step>
|
||||
|
||||
<step number="3b">
|
||||
<title>Analyze Change Impact</title>
|
||||
<description>
|
||||
Understand how proposed changes will affect the mode
|
||||
</description>
|
||||
<analysis_areas>
|
||||
<area>Compatibility with existing workflows</area>
|
||||
<area>Impact on file permissions and tool access</area>
|
||||
<area>Consistency with mode's core purpose</area>
|
||||
<area>Integration with other modes</area>
|
||||
</analysis_areas>
|
||||
<validation_questions>
|
||||
<ask_followup_question>
|
||||
<question>I've analyzed the existing mode. Here's what I understand about your requested changes. Is this correct?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, that's exactly what I want to change</suggest>
|
||||
<suggest>Mostly correct, but let me clarify some details</suggest>
|
||||
<suggest>No, I meant something different</suggest>
|
||||
<suggest>I'd like to add additional changes</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</validation_questions>
|
||||
</step>
|
||||
|
||||
<step number="4b">
|
||||
<title>Plan Modifications</title>
|
||||
<description>
|
||||
Create a detailed plan for modifying the mode
|
||||
</description>
|
||||
<planning_steps>
|
||||
<step>Identify which files need to be modified</step>
|
||||
<step>Determine if new XML instruction files are needed</step>
|
||||
<step>Check for potential conflicts or contradictions</step>
|
||||
<step>Plan the order of changes for minimal disruption</step>
|
||||
</planning_steps>
|
||||
</step>
|
||||
|
||||
<step number="5b">
|
||||
<title>Implement Changes</title>
|
||||
<description>
|
||||
Apply the planned modifications to the mode
|
||||
</description>
|
||||
<implementation_order>
|
||||
<change>Update .roomodes configuration if needed</change>
|
||||
<change>Modify existing XML instruction files</change>
|
||||
<change>Create new XML instruction files if required</change>
|
||||
<change>Update examples and documentation</change>
|
||||
</implementation_order>
|
||||
</step>
|
||||
</branch>
|
||||
</workflow_branches>
|
||||
|
||||
<validation_and_cohesion>
|
||||
<step number="6">
|
||||
<title>Validate Cohesion and Consistency</title>
|
||||
<description>
|
||||
Ensure all changes are cohesive and don't contradict each other
|
||||
</description>
|
||||
<validation_checks>
|
||||
<check type="configuration">
|
||||
<item>Mode slug follows naming conventions</item>
|
||||
<item>File restrictions align with mode purpose</item>
|
||||
<item>Tool permissions are appropriate</item>
|
||||
<item>whenToUse clearly differentiates from other modes</item>
|
||||
</check>
|
||||
<check type="instructions">
|
||||
<item>All XML files follow consistent structure</item>
|
||||
<item>No contradicting instructions between files</item>
|
||||
<item>Examples align with stated workflows</item>
|
||||
<item>Tool usage matches granted permissions</item>
|
||||
</check>
|
||||
<check type="integration">
|
||||
<item>Mode integrates well with Orchestrator</item>
|
||||
<item>Clear boundaries with other modes</item>
|
||||
<item>Handoff points are well-defined</item>
|
||||
</check>
|
||||
</validation_checks>
|
||||
<cohesion_questions>
|
||||
<ask_followup_question>
|
||||
<question>I've completed the validation checks. Would you like me to review any specific aspect in more detail?</question>
|
||||
<follow_up>
|
||||
<suggest>Review the file permission patterns</suggest>
|
||||
<suggest>Check for workflow contradictions</suggest>
|
||||
<suggest>Verify integration with other modes</suggest>
|
||||
<suggest>Everything looks good, proceed to testing</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</cohesion_questions>
|
||||
</step>
|
||||
|
||||
<step number="7">
|
||||
<title>Test and Refine</title>
|
||||
<description>
|
||||
Verify the mode works as intended
|
||||
</description>
|
||||
<checklist>
|
||||
<item>Mode appears in the mode list</item>
|
||||
<item>File restrictions work correctly</item>
|
||||
<item>Instructions are clear and actionable</item>
|
||||
<item>Mode integrates well with Orchestrator</item>
|
||||
<item>All examples are accurate and helpful</item>
|
||||
<item>Changes don't break existing functionality (for edits)</item>
|
||||
<item>New capabilities work as expected</item>
|
||||
</checklist>
|
||||
</step>
|
||||
</validation_and_cohesion>
|
||||
|
||||
<quick_reference>
|
||||
<command>Create mode in .roomodes for project-specific modes</command>
|
||||
<command>Create mode in global custom_modes.yaml for system-wide modes</command>
|
||||
<command>Use list_files to verify .roo folder structure</command>
|
||||
<command>Test file regex patterns with search_files</command>
|
||||
<command>Use codebase_search to find existing mode implementations</command>
|
||||
<command>Read all XML files in a mode directory to understand its structure</command>
|
||||
<command>Always validate changes for cohesion and consistency</command>
|
||||
</quick_reference>
|
||||
</mode_management_workflow>
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
<xml_structuring_best_practices>
|
||||
<overview>
|
||||
XML tags help Claude parse prompts more accurately, leading to higher-quality outputs.
|
||||
This guide covers best practices for structuring mode instructions using XML.
|
||||
</overview>
|
||||
|
||||
<why_use_xml_tags>
|
||||
<benefit type="clarity">
|
||||
Clearly separate different parts of your instructions and ensure well-structured content
|
||||
</benefit>
|
||||
<benefit type="accuracy">
|
||||
Reduce errors caused by Claude misinterpreting parts of your instructions
|
||||
</benefit>
|
||||
<benefit type="flexibility">
|
||||
Easily find, add, remove, or modify parts of instructions without rewriting everything
|
||||
</benefit>
|
||||
<benefit type="parseability">
|
||||
Having Claude use XML tags in its output makes it easier to extract specific parts of responses
|
||||
</benefit>
|
||||
</why_use_xml_tags>
|
||||
|
||||
<core_principles>
|
||||
<principle name="consistency">
|
||||
<description>Use the same tag names throughout your instructions</description>
|
||||
<example>
|
||||
Always use <step> for workflow steps, not sometimes <action> or <task>
|
||||
</example>
|
||||
</principle>
|
||||
|
||||
<principle name="semantic_naming">
|
||||
<description>Tag names should clearly describe their content</description>
|
||||
<good_examples>
|
||||
<tag>detailed_steps</tag>
|
||||
<tag>error_handling</tag>
|
||||
<tag>validation_rules</tag>
|
||||
</good_examples>
|
||||
<bad_examples>
|
||||
<tag>stuff</tag>
|
||||
<tag>misc</tag>
|
||||
<tag>data1</tag>
|
||||
</bad_examples>
|
||||
</principle>
|
||||
|
||||
<principle name="hierarchical_nesting">
|
||||
<description>Nest tags to show relationships and structure</description>
|
||||
<example>
|
||||
<workflow>
|
||||
<phase name="preparation">
|
||||
<step>Gather requirements</step>
|
||||
<step>Validate inputs</step>
|
||||
</phase>
|
||||
<phase name="execution">
|
||||
<step>Process data</step>
|
||||
<step>Generate output</step>
|
||||
</phase>
|
||||
</workflow>
|
||||
</example>
|
||||
</principle>
|
||||
</core_principles>
|
||||
|
||||
<common_tag_patterns>
|
||||
<pattern name="workflow_structure">
|
||||
<usage>For step-by-step processes</usage>
|
||||
<template><![CDATA[
|
||||
<workflow>
|
||||
<overview>High-level description</overview>
|
||||
<prerequisites>
|
||||
<prerequisite>Required condition 1</prerequisite>
|
||||
<prerequisite>Required condition 2</prerequisite>
|
||||
</prerequisites>
|
||||
<steps>
|
||||
<step number="1">
|
||||
<title>Step Title</title>
|
||||
<description>What this step accomplishes</description>
|
||||
<actions>
|
||||
<action>Specific action to take</action>
|
||||
</actions>
|
||||
<validation>How to verify success</validation>
|
||||
</step>
|
||||
</steps>
|
||||
</workflow>
|
||||
]]></template>
|
||||
</pattern>
|
||||
|
||||
<pattern name="examples_structure">
|
||||
<usage>For providing code examples and demonstrations</usage>
|
||||
<template><![CDATA[
|
||||
<examples>
|
||||
<example name="descriptive_name">
|
||||
<description>What this example demonstrates</description>
|
||||
<context>When to use this approach</context>
|
||||
<code language="typescript">
|
||||
// Your code example here
|
||||
</code>
|
||||
<explanation>
|
||||
Key points about the implementation
|
||||
</explanation>
|
||||
</example>
|
||||
</examples>
|
||||
]]></template>
|
||||
</pattern>
|
||||
|
||||
<pattern name="guidelines_structure">
|
||||
<usage>For rules and best practices</usage>
|
||||
<template><![CDATA[
|
||||
<guidelines category="category_name">
|
||||
<guideline priority="high">
|
||||
<rule>The specific rule or guideline</rule>
|
||||
<rationale>Why this is important</rationale>
|
||||
<exceptions>When this doesn't apply</exceptions>
|
||||
</guideline>
|
||||
</guidelines>
|
||||
]]></template>
|
||||
</pattern>
|
||||
|
||||
<pattern name="tool_usage_structure">
|
||||
<usage>For documenting how to use specific tools</usage>
|
||||
<template><![CDATA[
|
||||
<tool_usage tool="tool_name">
|
||||
<purpose>What this tool accomplishes</purpose>
|
||||
<when_to_use>Specific scenarios for this tool</when_to_use>
|
||||
<syntax>
|
||||
<command>The exact command format</command>
|
||||
<parameters>
|
||||
<parameter name="param1" required="true">
|
||||
<description>What this parameter does</description>
|
||||
<type>string|number|boolean</type>
|
||||
<example>example_value</example>
|
||||
</parameter>
|
||||
</parameters>
|
||||
</syntax>
|
||||
<examples>
|
||||
<example scenario="common_use_case">
|
||||
<code>Actual usage example</code>
|
||||
<output>Expected output</output>
|
||||
</example>
|
||||
</examples>
|
||||
</tool_usage>
|
||||
]]></template>
|
||||
</pattern>
|
||||
</common_tag_patterns>
|
||||
|
||||
<formatting_guidelines>
|
||||
<guideline name="indentation">
|
||||
Use consistent indentation (2 or 4 spaces) for nested elements
|
||||
</guideline>
|
||||
<guideline name="line_breaks">
|
||||
Add line breaks between major sections for readability
|
||||
</guideline>
|
||||
<guideline name="comments">
|
||||
Use XML comments <!-- like this --> to explain complex sections
|
||||
</guideline>
|
||||
<guideline name="cdata_sections">
|
||||
Use CDATA for code blocks or content with special characters:
|
||||
<![CDATA[<code><![CDATA[your code here]]></code>]]>
|
||||
</guideline>
|
||||
<guideline name="attributes_vs_elements">
|
||||
Use attributes for metadata, elements for content:
|
||||
<example type="good">
|
||||
<step number="1" priority="high">
|
||||
<description>The actual step content</description>
|
||||
</step>
|
||||
</example>
|
||||
</guideline>
|
||||
</formatting_guidelines>
|
||||
|
||||
<anti_patterns>
|
||||
<anti_pattern name="flat_structure">
|
||||
<description>Avoid completely flat structures without hierarchy</description>
|
||||
<bad><![CDATA[
|
||||
<instructions>
|
||||
<item1>Do this</item1>
|
||||
<item2>Then this</item2>
|
||||
<item3>Finally this</item3>
|
||||
</instructions>
|
||||
]]></bad>
|
||||
<good><![CDATA[
|
||||
<instructions>
|
||||
<steps>
|
||||
<step order="1">Do this</step>
|
||||
<step order="2">Then this</step>
|
||||
<step order="3">Finally this</step>
|
||||
</steps>
|
||||
</instructions>
|
||||
]]></good>
|
||||
</anti_pattern>
|
||||
|
||||
<anti_pattern name="inconsistent_naming">
|
||||
<description>Don't mix naming conventions</description>
|
||||
<bad>
|
||||
Mixing camelCase, snake_case, and kebab-case in tag names
|
||||
</bad>
|
||||
<good>
|
||||
Pick one convention (preferably snake_case for XML) and stick to it
|
||||
</good>
|
||||
</anti_pattern>
|
||||
|
||||
<anti_pattern name="overly_generic_tags">
|
||||
<description>Avoid tags that don't convey meaning</description>
|
||||
<bad>data, info, stuff, thing, item</bad>
|
||||
<good>user_input, validation_result, error_message, configuration</good>
|
||||
</anti_pattern>
|
||||
</anti_patterns>
|
||||
|
||||
<integration_tips>
|
||||
<tip>
|
||||
Reference XML content in instructions:
|
||||
"Using the workflow defined in <workflow> tags..."
|
||||
</tip>
|
||||
<tip>
|
||||
Combine XML structure with other techniques like multishot prompting
|
||||
</tip>
|
||||
<tip>
|
||||
Use XML tags in expected outputs to make parsing easier
|
||||
</tip>
|
||||
<tip>
|
||||
Create reusable XML templates for common patterns
|
||||
</tip>
|
||||
</integration_tips>
|
||||
</xml_structuring_best_practices>
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
<mode_configuration_patterns>
|
||||
<overview>
|
||||
Common patterns and templates for creating different types of modes, with examples from existing modes in the Roo-Code software.
|
||||
</overview>
|
||||
|
||||
<mode_types>
|
||||
<type name="specialist_mode">
|
||||
<description>
|
||||
Modes focused on specific technical domains or tasks
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Deep expertise in a particular area</characteristic>
|
||||
<characteristic>Restricted file access based on domain</characteristic>
|
||||
<characteristic>Specialized tool usage patterns</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: api-specialist
|
||||
name: 🔌 API Specialist
|
||||
roleDefinition: >-
|
||||
You are Roo Code, an API development specialist with expertise in:
|
||||
- RESTful API design and implementation
|
||||
- GraphQL schema design
|
||||
- API documentation with OpenAPI/Swagger
|
||||
- Authentication and authorization patterns
|
||||
- Rate limiting and caching strategies
|
||||
- API versioning and deprecation
|
||||
|
||||
You ensure APIs are:
|
||||
- Well-documented and discoverable
|
||||
- Following REST principles or GraphQL best practices
|
||||
- Secure and performant
|
||||
- Properly versioned and maintainable
|
||||
whenToUse: >-
|
||||
Use this mode when designing, implementing, or refactoring APIs.
|
||||
This includes creating new endpoints, updating API documentation,
|
||||
implementing authentication, or optimizing API performance.
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (api/.*\.(ts|js)|.*\.openapi\.yaml|.*\.graphql|docs/api/.*)$
|
||||
description: API implementation files, OpenAPI specs, and API documentation
|
||||
- command
|
||||
- mcp
|
||||
]]></example_template>
|
||||
</type>
|
||||
|
||||
<type name="workflow_mode">
|
||||
<description>
|
||||
Modes that guide users through multi-step processes
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Step-by-step workflow guidance</characteristic>
|
||||
<characteristic>Heavy use of ask_followup_question</characteristic>
|
||||
<characteristic>Process validation at each step</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: migration-guide
|
||||
name: 🔄 Migration Guide
|
||||
roleDefinition: >-
|
||||
You are Roo Code, a migration specialist who guides users through
|
||||
complex migration processes:
|
||||
- Database schema migrations
|
||||
- Framework version upgrades
|
||||
- API version migrations
|
||||
- Dependency updates
|
||||
- Breaking change resolutions
|
||||
|
||||
You provide:
|
||||
- Step-by-step migration plans
|
||||
- Automated migration scripts
|
||||
- Rollback strategies
|
||||
- Testing approaches for migrations
|
||||
whenToUse: >-
|
||||
Use this mode when performing any kind of migration or upgrade.
|
||||
This mode will analyze the current state, plan the migration,
|
||||
and guide you through each step with validation.
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- command
|
||||
]]></example_template>
|
||||
</type>
|
||||
|
||||
<type name="analysis_mode">
|
||||
<description>
|
||||
Modes focused on code analysis and reporting
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Read-heavy operations</characteristic>
|
||||
<characteristic>Limited or no edit permissions</characteristic>
|
||||
<characteristic>Comprehensive reporting outputs</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: security-auditor
|
||||
name: 🔒 Security Auditor
|
||||
roleDefinition: >-
|
||||
You are Roo Code, a security analysis specialist focused on:
|
||||
- Identifying security vulnerabilities
|
||||
- Analyzing authentication and authorization
|
||||
- Reviewing data validation and sanitization
|
||||
- Checking for common security anti-patterns
|
||||
- Evaluating dependency vulnerabilities
|
||||
- Assessing API security
|
||||
|
||||
You provide detailed security reports with:
|
||||
- Vulnerability severity ratings
|
||||
- Specific remediation steps
|
||||
- Security best practice recommendations
|
||||
whenToUse: >-
|
||||
Use this mode to perform security audits on codebases.
|
||||
This mode will analyze code for vulnerabilities, check
|
||||
dependencies, and provide actionable security recommendations.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (SECURITY\.md|\.github/security/.*|docs/security/.*)$
|
||||
description: Security documentation files only
|
||||
]]></example_template>
|
||||
</type>
|
||||
|
||||
<type name="creative_mode">
|
||||
<description>
|
||||
Modes for generating new content or features
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Broad file creation permissions</characteristic>
|
||||
<characteristic>Template and boilerplate generation</characteristic>
|
||||
<characteristic>Interactive design process</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: component-designer
|
||||
name: 🎨 Component Designer
|
||||
roleDefinition: >-
|
||||
You are Roo Code, a UI component design specialist who creates:
|
||||
- Reusable React/Vue/Angular components
|
||||
- Component documentation and examples
|
||||
- Storybook stories
|
||||
- Unit tests for components
|
||||
- Accessibility-compliant interfaces
|
||||
|
||||
You follow design system principles and ensure components are:
|
||||
- Highly reusable and composable
|
||||
- Well-documented with examples
|
||||
- Fully tested
|
||||
- Accessible (WCAG compliant)
|
||||
- Performance optimized
|
||||
whenToUse: >-
|
||||
Use this mode when creating new UI components or refactoring
|
||||
existing ones. This mode helps design component APIs, implement
|
||||
the components, and create comprehensive documentation.
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (components/.*|stories/.*|__tests__/.*\.test\.(tsx?|jsx?))$
|
||||
description: Component files, stories, and component tests
|
||||
- browser
|
||||
- command
|
||||
]]></example_template>
|
||||
</type>
|
||||
</mode_types>
|
||||
|
||||
<permission_patterns>
|
||||
<pattern name="documentation_only">
|
||||
<description>For modes that only work with documentation</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: \.(md|mdx|rst|txt)$
|
||||
description: Documentation files only
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
|
||||
<pattern name="test_focused">
|
||||
<description>For modes that work with test files</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|.*\.test\.(ts|tsx|js|jsx)$|.*\.spec\.(ts|tsx|js|jsx)$)
|
||||
description: Test files and mocks
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
|
||||
<pattern name="config_management">
|
||||
<description>For modes that manage configuration</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (.*\.config\.(js|ts|json)|.*rc\.json|.*\.yaml|.*\.yml|\.env\.example)$
|
||||
description: Configuration files (not .env)
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
|
||||
<pattern name="full_stack">
|
||||
<description>For modes that need broad access</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- edit # No restrictions
|
||||
- command
|
||||
- browser
|
||||
- mcp
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
</permission_patterns>
|
||||
|
||||
<naming_conventions>
|
||||
<convention category="slug">
|
||||
<rule>Use lowercase with hyphens</rule>
|
||||
<good>api-dev, test-writer, docs-manager</good>
|
||||
<bad>apiDev, test_writer, DocsManager</bad>
|
||||
</convention>
|
||||
|
||||
<convention category="name">
|
||||
<rule>Use title case with descriptive emoji</rule>
|
||||
<good>🔧 API Developer, 📝 Documentation Writer</good>
|
||||
<bad>api developer, DOCUMENTATION WRITER</bad>
|
||||
</convention>
|
||||
|
||||
<convention category="emoji_selection">
|
||||
<common_emojis>
|
||||
<emoji meaning="testing">🧪</emoji>
|
||||
<emoji meaning="documentation">📝</emoji>
|
||||
<emoji meaning="design">🎨</emoji>
|
||||
<emoji meaning="debugging">🪲</emoji>
|
||||
<emoji meaning="building">🏗️</emoji>
|
||||
<emoji meaning="security">🔒</emoji>
|
||||
<emoji meaning="api">🔌</emoji>
|
||||
<emoji meaning="database">🗄️</emoji>
|
||||
<emoji meaning="performance">⚡</emoji>
|
||||
<emoji meaning="configuration">⚙️</emoji>
|
||||
</common_emojis>
|
||||
</convention>
|
||||
</naming_conventions>
|
||||
|
||||
<integration_guidelines>
|
||||
<guideline name="orchestrator_compatibility">
|
||||
<description>Ensure whenToUse is clear for Orchestrator mode</description>
|
||||
<checklist>
|
||||
<item>Specify concrete task types the mode handles</item>
|
||||
<item>Include trigger keywords or phrases</item>
|
||||
<item>Differentiate from similar modes</item>
|
||||
<item>Mention specific file types or areas</item>
|
||||
</checklist>
|
||||
</guideline>
|
||||
|
||||
<guideline name="mode_boundaries">
|
||||
<description>Define clear boundaries between modes</description>
|
||||
<checklist>
|
||||
<item>Avoid overlapping responsibilities</item>
|
||||
<item>Make handoff points explicit</item>
|
||||
<item>Use switch_mode when appropriate</item>
|
||||
<item>Document mode interactions</item>
|
||||
</checklist>
|
||||
</guideline>
|
||||
</integration_guidelines>
|
||||
</mode_configuration_patterns>
|
||||
|
|
@ -1,367 +0,0 @@
|
|||
<instruction_file_templates>
|
||||
<overview>
|
||||
Templates and examples for creating XML instruction files that provide
|
||||
detailed guidance for each mode's behavior and workflows.
|
||||
</overview>
|
||||
|
||||
<file_organization>
|
||||
<principle>Number files to indicate execution order</principle>
|
||||
<principle>Use descriptive names that indicate content</principle>
|
||||
<principle>Keep related instructions together</principle>
|
||||
<standard_structure>
|
||||
<file>1_workflow.xml - Main workflow and processes</file>
|
||||
<file>2_best_practices.xml - Guidelines and conventions</file>
|
||||
<file>3_common_patterns.xml - Reusable code patterns</file>
|
||||
<file>4_tool_usage.xml - Specific tool instructions</file>
|
||||
<file>5_examples.xml - Complete workflow examples</file>
|
||||
<file>6_error_handling.xml - Error scenarios and recovery</file>
|
||||
<file>7_communication.xml - User interaction guidelines</file>
|
||||
</standard_structure>
|
||||
</file_organization>
|
||||
|
||||
<workflow_file_template>
|
||||
<description>Template for main workflow files (1_workflow.xml)</description>
|
||||
<template><![CDATA[
|
||||
<workflow_instructions>
|
||||
<mode_overview>
|
||||
Brief description of what this mode does and its primary purpose
|
||||
</mode_overview>
|
||||
|
||||
<initialization_steps>
|
||||
<step number="1">
|
||||
<action>Understand the user's request</action>
|
||||
<details>
|
||||
Parse the user's input to identify:
|
||||
- Primary objective
|
||||
- Specific requirements
|
||||
- Constraints or limitations
|
||||
</details>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<action>Gather necessary context</action>
|
||||
<tools>
|
||||
<tool>codebase_search - Find relevant existing code</tool>
|
||||
<tool>list_files - Understand project structure</tool>
|
||||
<tool>read_file - Examine specific implementations</tool>
|
||||
</tools>
|
||||
</step>
|
||||
</initialization_steps>
|
||||
|
||||
<main_workflow>
|
||||
<phase name="analysis">
|
||||
<description>Analyze the current state and requirements</description>
|
||||
<steps>
|
||||
<step>Identify affected components</step>
|
||||
<step>Assess impact of changes</step>
|
||||
<step>Plan implementation approach</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="implementation">
|
||||
<description>Execute the planned changes</description>
|
||||
<steps>
|
||||
<step>Create/modify necessary files</step>
|
||||
<step>Ensure consistency across codebase</step>
|
||||
<step>Add appropriate documentation</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="validation">
|
||||
<description>Verify the implementation</description>
|
||||
<steps>
|
||||
<step>Check for errors or inconsistencies</step>
|
||||
<step>Validate against requirements</step>
|
||||
<step>Ensure no regressions</step>
|
||||
</steps>
|
||||
</phase>
|
||||
</main_workflow>
|
||||
|
||||
<completion_criteria>
|
||||
<criterion>All requirements have been addressed</criterion>
|
||||
<criterion>Code follows project conventions</criterion>
|
||||
<criterion>Changes are properly documented</criterion>
|
||||
<criterion>No breaking changes introduced</criterion>
|
||||
</completion_criteria>
|
||||
</workflow_instructions>
|
||||
]]></template>
|
||||
</workflow_file_template>
|
||||
|
||||
<best_practices_template>
|
||||
<description>Template for best practices files (2_best_practices.xml)</description>
|
||||
<template><![CDATA[
|
||||
<best_practices>
|
||||
<general_principles>
|
||||
<principle priority="high">
|
||||
<name>Principle Name</name>
|
||||
<description>Detailed explanation of the principle</description>
|
||||
<rationale>Why this principle is important</rationale>
|
||||
<example>
|
||||
<scenario>When this applies</scenario>
|
||||
<good>Correct approach</good>
|
||||
<bad>What to avoid</bad>
|
||||
</example>
|
||||
</principle>
|
||||
</general_principles>
|
||||
|
||||
<code_conventions>
|
||||
<convention category="naming">
|
||||
<rule>Specific naming convention</rule>
|
||||
<examples>
|
||||
<good>goodExampleName</good>
|
||||
<bad>bad_example-name</bad>
|
||||
</examples>
|
||||
</convention>
|
||||
|
||||
<convention category="structure">
|
||||
<rule>How to structure code/files</rule>
|
||||
<template>
|
||||
// Example structure
|
||||
</template>
|
||||
</convention>
|
||||
</code_conventions>
|
||||
|
||||
<common_pitfalls>
|
||||
<pitfall>
|
||||
<description>Common mistake to avoid</description>
|
||||
<why_problematic>Explanation of issues it causes</why_problematic>
|
||||
<correct_approach>How to do it properly</correct_approach>
|
||||
</pitfall>
|
||||
</common_pitfalls>
|
||||
|
||||
<quality_checklist>
|
||||
<category name="before_starting">
|
||||
<item>Understand requirements fully</item>
|
||||
<item>Check existing implementations</item>
|
||||
</category>
|
||||
<category name="during_implementation">
|
||||
<item>Follow established patterns</item>
|
||||
<item>Write clear documentation</item>
|
||||
</category>
|
||||
<category name="before_completion">
|
||||
<item>Review all changes</item>
|
||||
<item>Verify requirements met</item>
|
||||
</category>
|
||||
</quality_checklist>
|
||||
</best_practices>
|
||||
]]></template>
|
||||
</best_practices_template>
|
||||
|
||||
<tool_usage_template>
|
||||
<description>Template for tool usage files (4_tool_usage.xml)</description>
|
||||
<template><![CDATA[
|
||||
<tool_usage_guide>
|
||||
<tool_priorities>
|
||||
<priority level="1">
|
||||
<tool>codebase_search</tool>
|
||||
<when>Always use first to find relevant code</when>
|
||||
<why>Semantic search finds functionality better than keywords</why>
|
||||
</priority>
|
||||
<priority level="2">
|
||||
<tool>read_file</tool>
|
||||
<when>After identifying files with codebase_search</when>
|
||||
<why>Get full context of implementations</why>
|
||||
</priority>
|
||||
</tool_priorities>
|
||||
|
||||
<tool_specific_guidance>
|
||||
<tool name="apply_diff">
|
||||
<best_practices>
|
||||
<practice>Always read file first to ensure exact content match</practice>
|
||||
<practice>Make multiple changes in one diff when possible</practice>
|
||||
<practice>Include line numbers for accuracy</practice>
|
||||
</best_practices>
|
||||
<example><![CDATA[
|
||||
<apply_diff>
|
||||
<path>src/config.ts</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
:start_line:10
|
||||
-------
|
||||
export const config = {
|
||||
apiUrl: 'http://localhost:3000',
|
||||
timeout: 5000
|
||||
};
|
||||
=======
|
||||
export const config = {
|
||||
apiUrl: process.env.API_URL || 'http://localhost:3000',
|
||||
timeout: parseInt(process.env.TIMEOUT || '5000'),
|
||||
retries: 3
|
||||
};
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></example>
|
||||
</tool>
|
||||
|
||||
<tool name="ask_followup_question">
|
||||
<best_practices>
|
||||
<practice>Provide 2-4 specific, actionable suggestions</practice>
|
||||
<practice>Order suggestions by likelihood or importance</practice>
|
||||
<practice>Make suggestions complete (no placeholders)</practice>
|
||||
</best_practices>
|
||||
<example><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>Which database system should I configure for this project?</question>
|
||||
<follow_up>
|
||||
<suggest>PostgreSQL with the default configuration</suggest>
|
||||
<suggest>MySQL 8.0 with InnoDB storage engine</suggest>
|
||||
<suggest>SQLite for local development only</suggest>
|
||||
<suggest>MongoDB for document-based storage</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></example>
|
||||
</tool>
|
||||
</tool_specific_guidance>
|
||||
|
||||
<tool_combination_patterns>
|
||||
<pattern name="explore_then_modify">
|
||||
<sequence>
|
||||
<step>codebase_search - Find relevant files</step>
|
||||
<step>list_code_definition_names - Understand structure</step>
|
||||
<step>read_file - Get full context</step>
|
||||
<step>apply_diff or write_to_file - Make changes</step>
|
||||
</sequence>
|
||||
</pattern>
|
||||
|
||||
<pattern name="verify_then_proceed">
|
||||
<sequence>
|
||||
<step>list_files - Check file exists</step>
|
||||
<step>read_file - Verify current content</step>
|
||||
<step>ask_followup_question - Confirm approach</step>
|
||||
<step>apply_diff - Implement changes</step>
|
||||
</sequence>
|
||||
</pattern>
|
||||
</tool_combination_patterns>
|
||||
</tool_usage_guide>
|
||||
]]></template>
|
||||
</tool_usage_template>
|
||||
|
||||
<examples_file_template>
|
||||
<description>Template for example files (5_examples.xml)</description>
|
||||
<template><![CDATA[
|
||||
<complete_examples>
|
||||
<example name="descriptive_example_name">
|
||||
<scenario>
|
||||
Detailed description of the use case this example covers
|
||||
</scenario>
|
||||
|
||||
<user_request>
|
||||
The initial request from the user
|
||||
</user_request>
|
||||
|
||||
<workflow>
|
||||
<step number="1">
|
||||
<description>First step description</description>
|
||||
<tool_use><![CDATA[
|
||||
<codebase_search>
|
||||
<query>search query here</query>
|
||||
</codebase_search>
|
||||
]]></tool_use>
|
||||
<expected_outcome>What we learn from this step</expected_outcome>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<description>Second step description</description>
|
||||
<tool_use><![CDATA[
|
||||
<read_file>
|
||||
<path>path/to/file.ts</path>
|
||||
</read_file>
|
||||
]]></tool_use>
|
||||
<analysis>How we interpret the results</analysis>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<description>Implementation step</description>
|
||||
<tool_use><![CDATA[
|
||||
<apply_diff>
|
||||
<path>path/to/file.ts</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
:start_line:1
|
||||
-------
|
||||
original content
|
||||
=======
|
||||
new content
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
</workflow>
|
||||
|
||||
<completion><![CDATA[
|
||||
<attempt_completion>
|
||||
<result>
|
||||
Summary of what was accomplished and how it addresses the user's request
|
||||
</result>
|
||||
</attempt_completion>
|
||||
]]></completion>
|
||||
|
||||
<key_takeaways>
|
||||
<takeaway>Important lesson from this example</takeaway>
|
||||
<takeaway>Pattern that can be reused</takeaway>
|
||||
</key_takeaways>
|
||||
</example>
|
||||
</complete_examples>
|
||||
]]></template>
|
||||
</examples_file_template>
|
||||
|
||||
<communication_template>
|
||||
<description>Template for communication guidelines (7_communication.xml)</description>
|
||||
<template><![CDATA[
|
||||
<communication_guidelines>
|
||||
<tone_and_style>
|
||||
<principle>Be direct and technical, not conversational</principle>
|
||||
<principle>Focus on actions taken and results achieved</principle>
|
||||
<avoid>
|
||||
<phrase>Great! I'll help you with that...</phrase>
|
||||
<phrase>Certainly! Let me...</phrase>
|
||||
<phrase>Sure thing!</phrase>
|
||||
</avoid>
|
||||
<prefer>
|
||||
<phrase>I'll analyze the codebase to...</phrase>
|
||||
<phrase>Implementing the requested changes...</phrase>
|
||||
<phrase>The analysis shows...</phrase>
|
||||
</prefer>
|
||||
</tone_and_style>
|
||||
|
||||
<user_interaction>
|
||||
<when_to_ask_questions>
|
||||
<scenario>Missing critical information</scenario>
|
||||
<scenario>Multiple valid approaches exist</scenario>
|
||||
<scenario>Potential breaking changes</scenario>
|
||||
</when_to_ask_questions>
|
||||
|
||||
<question_format>
|
||||
<guideline>Be specific about what you need</guideline>
|
||||
<guideline>Provide actionable options</guideline>
|
||||
<guideline>Explain implications of choices</guideline>
|
||||
</question_format>
|
||||
</user_interaction>
|
||||
|
||||
<progress_updates>
|
||||
<when>During long-running operations</when>
|
||||
<format>
|
||||
<update>Analyzing [X] files for [purpose]...</update>
|
||||
<update>Implementing [feature] in [location]...</update>
|
||||
<update>Validating changes against [criteria]...</update>
|
||||
</format>
|
||||
</progress_updates>
|
||||
|
||||
<completion_messages>
|
||||
<structure>
|
||||
<element>What was accomplished</element>
|
||||
<element>Key changes made</element>
|
||||
<element>Any important notes or warnings</element>
|
||||
</structure>
|
||||
<avoid>
|
||||
<element>Questions at the end</element>
|
||||
<element>Offers for further assistance</element>
|
||||
<element>Conversational closings</element>
|
||||
</avoid>
|
||||
</completion_messages>
|
||||
</communication_guidelines>
|
||||
]]></template>
|
||||
</communication_template>
|
||||
</instruction_file_templates>
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
<complete_examples>
|
||||
<example name="mode_editing_enhancement">
|
||||
<scenario>
|
||||
Edit the test mode to add support for performance testing and benchmarking
|
||||
</scenario>
|
||||
|
||||
<user_request>
|
||||
I want to edit the test mode to add benchmark testing capabilities
|
||||
</user_request>
|
||||
|
||||
<workflow>
|
||||
<step number="1">
|
||||
<description>Determine user intent and clarify scope</description>
|
||||
<tool_use><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I understand you want to edit the test mode to add performance testing capabilities. Before we proceed, let me confirm the scope of changes. What specific performance testing features do you need?</question>
|
||||
<follow_up>
|
||||
<suggest>Add benchmark testing with Vitest's bench API</suggest>
|
||||
<suggest>Include performance profiling and memory usage tracking</suggest>
|
||||
<suggest>Add load testing capabilities for API endpoints</suggest>
|
||||
<suggest>All of the above - comprehensive performance testing suite</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></tool_use>
|
||||
<expected_outcome>User selected: Add benchmark testing with Vitest's bench API</expected_outcome>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<description>Immerse in existing test mode configuration</description>
|
||||
<tool_use><![CDATA[
|
||||
<read_file>
|
||||
<path>.roomodes</path>
|
||||
</read_file>
|
||||
]]></tool_use>
|
||||
<analysis>Found test mode configuration with current roleDefinition and file restrictions</analysis>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<description>Explore existing test mode instructions</description>
|
||||
<tool_use><![CDATA[
|
||||
<list_files>
|
||||
<path>.roo/rules-test</path>
|
||||
<recursive>true</recursive>
|
||||
</list_files>
|
||||
]]></tool_use>
|
||||
<tool_use><![CDATA[
|
||||
<read_file>
|
||||
<path>.roo/rules-test/1_workflow.xml</path>
|
||||
</read_file>
|
||||
]]></tool_use>
|
||||
<analysis>Understood current test workflows and patterns</analysis>
|
||||
</step>
|
||||
|
||||
<step number="4">
|
||||
<description>Validate changes for cohesion</description>
|
||||
<tool_use><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I've analyzed the test mode. Adding benchmark testing would require:
|
||||
1. Updating the roleDefinition to include performance testing expertise
|
||||
2. Modifying file restrictions to include .bench.ts files
|
||||
3. Adding new workflow instructions for benchmark creation
|
||||
4. Including Vitest bench API examples
|
||||
|
||||
This aligns well with the existing testing focus. Should I proceed with all these changes?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, implement all the changes as described</suggest>
|
||||
<suggest>Start with just the roleDefinition and file restrictions</suggest>
|
||||
<suggest>Focus only on the workflow instructions first</suggest>
|
||||
<suggest>Let me review the specific changes before proceeding</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></tool_use>
|
||||
<expected_outcome>User selected: Yes, implement all the changes as described</expected_outcome>
|
||||
</step>
|
||||
|
||||
<step number="5">
|
||||
<description>Update mode configuration in .roomodes</description>
|
||||
<tool_use><![CDATA[
|
||||
<apply_diff>
|
||||
<path>.roomodes</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
roleDefinition: |-
|
||||
You are Roo, a Vitest testing specialist with deep expertise in: - Writing and maintaining Vitest test suites - Test-driven development (TDD) practices - Mocking and stubbing with Vitest - Integration testing strategies - TypeScript testing patterns - Code coverage analysis - Test performance optimization
|
||||
=======
|
||||
roleDefinition: |-
|
||||
You are Roo, a Vitest testing specialist with deep expertise in: - Writing and maintaining Vitest test suites - Test-driven development (TDD) practices - Mocking and stubbing with Vitest - Integration testing strategies - TypeScript testing patterns - Code coverage analysis - Test performance optimization - Benchmark testing with Vitest bench API - Performance profiling and optimization - Memory usage analysis in tests
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
|
||||
<step number="6">
|
||||
<description>Update file restrictions to include benchmark files</description>
|
||||
<tool_use><![CDATA[
|
||||
<apply_diff>
|
||||
<path>.roomodes</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$)
|
||||
description: Test files, mocks, and Vitest configuration
|
||||
=======
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|\.bench\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$)
|
||||
description: Test files, benchmarks, mocks, and Vitest configuration
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
|
||||
<step number="7">
|
||||
<description>Create new benchmark workflow instructions</description>
|
||||
<tool_use><![CDATA[
|
||||
<write_to_file>
|
||||
<path>.roo/rules-test/5_benchmark_testing.xml</path>
|
||||
<content>
|
||||
<benchmark_testing_guide>
|
||||
<overview>
|
||||
Guidelines for creating and maintaining performance benchmarks using Vitest bench API
|
||||
</overview>
|
||||
|
||||
<benchmark_patterns>
|
||||
<pattern name="basic_benchmark">
|
||||
<description>Basic benchmark structure</description>
|
||||
<example><![CDATA[
|
||||
import { bench, describe } from 'vitest';
|
||||
|
||||
describe('Array operations', () => {
|
||||
bench('Array.push', () => {
|
||||
const arr = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
bench('Array spread', () => {
|
||||
let arr = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
arr = [...arr, i];
|
||||
}
|
||||
});
|
||||
});
|
||||
]]></example>
|
||||
</pattern>
|
||||
</benchmark_patterns>
|
||||
|
||||
<best_practices>
|
||||
<practice>Use meaningful benchmark names that describe what's being measured</practice>
|
||||
<practice>Include baseline comparisons when optimizing code</practice>
|
||||
<practice>Run benchmarks in isolation to avoid interference</practice>
|
||||
<practice>Document performance expectations and thresholds</practice>
|
||||
</best_practices>
|
||||
</benchmark_testing_guide>
|
||||
</content>
|
||||
<line_count>38</line_count>
|
||||
</write_to_file>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
|
||||
<step number="8">
|
||||
<description>Validate all changes for consistency</description>
|
||||
<tool_use><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I've completed all the changes to add benchmark testing capabilities to the test mode:
|
||||
|
||||
1. ✓ Updated roleDefinition to include benchmark expertise
|
||||
2. ✓ Modified file restrictions to include .bench.ts files
|
||||
3. ✓ Created new benchmark workflow instructions
|
||||
4. ✓ Added Vitest bench API examples
|
||||
|
||||
All changes are cohesive and align with the existing test mode structure. Would you like me to test any specific aspect of these changes?</question>
|
||||
<follow_up>
|
||||
<suggest>Test creating a new benchmark file to verify file permissions</suggest>
|
||||
<suggest>Review the complete changes one more time</suggest>
|
||||
<suggest>Everything looks good, the mode is ready</suggest>
|
||||
<suggest>Create a simple benchmark example to test</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
</workflow>
|
||||
|
||||
<completion><![CDATA[
|
||||
<attempt_completion>
|
||||
<result>
|
||||
I've successfully enhanced the test mode with benchmark testing capabilities:
|
||||
|
||||
1. Updated the roleDefinition to include:
|
||||
- Benchmark testing with Vitest bench API
|
||||
- Performance profiling and optimization
|
||||
- Memory usage analysis in tests
|
||||
|
||||
2. Modified file restrictions to include .bench.ts files, allowing the mode to create and edit benchmark files
|
||||
|
||||
3. Created new workflow instructions at .roo/rules-test/5_benchmark_testing.xml with:
|
||||
- Basic benchmark patterns
|
||||
- Best practices for performance testing
|
||||
- Example code using Vitest's bench API
|
||||
|
||||
All changes have been validated for cohesion and consistency with the existing test mode structure. The mode now supports comprehensive performance testing alongside its existing unit and integration testing capabilities.
|
||||
</result>
|
||||
</attempt_completion>
|
||||
]]></completion>
|
||||
|
||||
<key_takeaways>
|
||||
<takeaway>Always immerse yourself in the existing mode before making changes</takeaway>
|
||||
<takeaway>Use ask_followup_question aggressively to clarify scope and validate changes</takeaway>
|
||||
<takeaway>Validate all changes for cohesion and consistency</takeaway>
|
||||
<takeaway>Update all relevant parts: configuration, file restrictions, and instructions</takeaway>
|
||||
<takeaway>Test changes to ensure they work as expected</takeaway>
|
||||
</key_takeaways>
|
||||
</example>
|
||||
</complete_examples>
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
<mode_testing_validation>
|
||||
<overview>
|
||||
Guidelines for testing and validating newly created modes to ensure they function correctly and integrate well with the Roo Code ecosystem.
|
||||
</overview>
|
||||
|
||||
<validation_checklist>
|
||||
<category name="configuration_validation">
|
||||
<item priority="critical">
|
||||
<check>Mode slug is unique and follows naming conventions</check>
|
||||
<validation>No spaces, lowercase, hyphens only</validation>
|
||||
</item>
|
||||
<item priority="critical">
|
||||
<check>All required fields are present and non-empty</check>
|
||||
<fields>slug, name, roleDefinition, groups</fields>
|
||||
</item>
|
||||
<item priority="critical">
|
||||
<check>No customInstructions field in .roomodes</check>
|
||||
<validation>All instructions must be in XML files in .roo/rules-[slug]/</validation>
|
||||
</item>
|
||||
<item priority="high">
|
||||
<check>File restrictions use valid regex patterns</check>
|
||||
<test_method><![CDATA[
|
||||
<search_files>
|
||||
<path>.</path>
|
||||
<regex>your_file_regex_here</regex>
|
||||
</search_files>
|
||||
]]></test_method>
|
||||
</item>
|
||||
<item priority="high">
|
||||
<check>whenToUse clearly differentiates from other modes</check>
|
||||
<validation>Compare with existing mode descriptions</validation>
|
||||
</item>
|
||||
</category>
|
||||
|
||||
<category name="instruction_validation">
|
||||
<item>
|
||||
<check>XML files are well-formed and valid</check>
|
||||
<validation>No syntax errors, proper closing tags</validation>
|
||||
</item>
|
||||
<item>
|
||||
<check>Instructions follow XML best practices</check>
|
||||
<validation>Semantic tag names, proper nesting</validation>
|
||||
</item>
|
||||
<item>
|
||||
<check>Examples use correct tool syntax</check>
|
||||
<validation>Tool parameters match current API</validation>
|
||||
</item>
|
||||
<item>
|
||||
<check>File paths in examples are consistent</check>
|
||||
<validation>Use project-relative paths</validation>
|
||||
</item>
|
||||
</category>
|
||||
|
||||
<category name="functional_testing">
|
||||
<item>
|
||||
<check>Mode appears in mode list</check>
|
||||
<test>Switch to the new mode and verify it loads</test>
|
||||
</item>
|
||||
<item>
|
||||
<check>Tool permissions work as expected</check>
|
||||
<test>Try using each tool group and verify access</test>
|
||||
</item>
|
||||
<item>
|
||||
<check>File restrictions are enforced</check>
|
||||
<test>Attempt to edit allowed and restricted files</test>
|
||||
</item>
|
||||
<item>
|
||||
<check>Mode handles edge cases gracefully</check>
|
||||
<test>Test with minimal input, errors, edge cases</test>
|
||||
</item>
|
||||
</category>
|
||||
</validation_checklist>
|
||||
|
||||
<testing_workflow>
|
||||
<step number="1">
|
||||
<title>Configuration Testing</title>
|
||||
<actions>
|
||||
<action>Verify mode appears in available modes list</action>
|
||||
<action>Check that mode metadata displays correctly</action>
|
||||
<action>Confirm mode can be activated</action>
|
||||
</actions>
|
||||
<verification><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I've created the mode configuration. Can you see the new mode in your mode list?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, I can see the new mode and switch to it</suggest>
|
||||
<suggest>No, the mode doesn't appear in the list</suggest>
|
||||
<suggest>The mode appears but has errors when switching</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></verification>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<title>Permission Testing</title>
|
||||
<test_cases>
|
||||
<test case="read_permissions">
|
||||
<action>Use read tools on various files</action>
|
||||
<expected>All read operations should work</expected>
|
||||
</test>
|
||||
<test case="edit_restrictions">
|
||||
<action>Try editing allowed file types</action>
|
||||
<expected>Edits succeed for matching patterns</expected>
|
||||
</test>
|
||||
<test case="edit_restrictions_negative">
|
||||
<action>Try editing restricted file types</action>
|
||||
<expected>FileRestrictionError for non-matching files</expected>
|
||||
</test>
|
||||
</test_cases>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<title>Workflow Testing</title>
|
||||
<actions>
|
||||
<action>Execute main workflow from start to finish</action>
|
||||
<action>Test each decision point</action>
|
||||
<action>Verify error handling</action>
|
||||
<action>Check completion criteria</action>
|
||||
</actions>
|
||||
</step>
|
||||
|
||||
<step number="4">
|
||||
<title>Integration Testing</title>
|
||||
<areas>
|
||||
<area>Orchestrator mode compatibility</area>
|
||||
<area>Mode switching functionality</area>
|
||||
<area>Tool handoff between modes</area>
|
||||
<area>Consistent behavior with other modes</area>
|
||||
</areas>
|
||||
</step>
|
||||
</testing_workflow>
|
||||
|
||||
<common_issues>
|
||||
<issue type="configuration">
|
||||
<problem>Mode doesn't appear in list</problem>
|
||||
<causes>
|
||||
<cause>Syntax error in YAML</cause>
|
||||
<cause>Invalid mode slug</cause>
|
||||
<cause>File not saved</cause>
|
||||
</causes>
|
||||
<solution>Check YAML syntax, validate slug format</solution>
|
||||
</issue>
|
||||
|
||||
<issue type="permissions">
|
||||
<problem>File restriction not working</problem>
|
||||
<causes>
|
||||
<cause>Invalid regex pattern</cause>
|
||||
<cause>Escaping issues in regex</cause>
|
||||
<cause>Wrong file path format</cause>
|
||||
</causes>
|
||||
<solution>Test regex pattern, use proper escaping</solution>
|
||||
<example><![CDATA[
|
||||
# Wrong: *.ts (glob pattern)
|
||||
# Right: .*\.ts$ (regex pattern)
|
||||
]]></example>
|
||||
</issue>
|
||||
|
||||
<issue type="behavior">
|
||||
<problem>Mode not following instructions</problem>
|
||||
<causes>
|
||||
<cause>Instructions not in .roo/rules-[slug]/ folder</cause>
|
||||
<cause>XML parsing errors</cause>
|
||||
<cause>Conflicting instructions</cause>
|
||||
</causes>
|
||||
<solution>Verify file locations and XML validity</solution>
|
||||
</issue>
|
||||
</common_issues>
|
||||
|
||||
<debugging_tools>
|
||||
<tool name="list_files">
|
||||
<usage>Verify instruction files exist in correct location</usage>
|
||||
<command><![CDATA[
|
||||
<list_files>
|
||||
<path>.roo</path>
|
||||
<recursive>true</recursive>
|
||||
</list_files>
|
||||
]]></command>
|
||||
</tool>
|
||||
|
||||
<tool name="read_file">
|
||||
<usage>Check mode configuration syntax</usage>
|
||||
<command><![CDATA[
|
||||
<read_file>
|
||||
<path>.roomodes</path>
|
||||
</read_file>
|
||||
]]></command>
|
||||
</tool>
|
||||
|
||||
<tool name="search_files">
|
||||
<usage>Test file restriction patterns</usage>
|
||||
<command><![CDATA[
|
||||
<search_files>
|
||||
<path>.</path>
|
||||
<regex>your_file_pattern_here</regex>
|
||||
</search_files>
|
||||
]]></command>
|
||||
</tool>
|
||||
</debugging_tools>
|
||||
|
||||
<best_practices>
|
||||
<practice>Test incrementally as you build the mode</practice>
|
||||
<practice>Start with minimal configuration and add complexity</practice>
|
||||
<practice>Document any special requirements or dependencies</practice>
|
||||
<practice>Consider edge cases and error scenarios</practice>
|
||||
<practice>Get feedback from potential users of the mode</practice>
|
||||
</best_practices>
|
||||
</mode_testing_validation>
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
<validation_cohesion_checking>
|
||||
<overview>
|
||||
Guidelines for thoroughly validating mode changes to ensure cohesion,
|
||||
consistency, and prevent contradictions across all mode components.
|
||||
</overview>
|
||||
|
||||
<validation_principles>
|
||||
<principle name="comprehensive_review">
|
||||
<description>
|
||||
Every change must be reviewed in context of the entire mode
|
||||
</description>
|
||||
<checklist>
|
||||
<item>Read all existing XML instruction files</item>
|
||||
<item>Verify new changes align with existing patterns</item>
|
||||
<item>Check for duplicate or conflicting instructions</item>
|
||||
<item>Ensure terminology is consistent throughout</item>
|
||||
</checklist>
|
||||
</principle>
|
||||
|
||||
<principle name="aggressive_questioning">
|
||||
<description>
|
||||
Use ask_followup_question extensively to clarify ambiguities
|
||||
</description>
|
||||
<when_to_ask>
|
||||
<scenario>User's intent is unclear</scenario>
|
||||
<scenario>Multiple interpretations are possible</scenario>
|
||||
<scenario>Changes might conflict with existing functionality</scenario>
|
||||
<scenario>Impact on other modes needs clarification</scenario>
|
||||
</when_to_ask>
|
||||
<example><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I notice this change might affect how the mode interacts with file permissions. Should we also update the file regex patterns to match?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, update the file regex to include the new file types</suggest>
|
||||
<suggest>No, keep the current file restrictions as they are</suggest>
|
||||
<suggest>Let me explain what file types I need to work with</suggest>
|
||||
<suggest>Show me the current file restrictions first</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></example>
|
||||
</principle>
|
||||
|
||||
<principle name="contradiction_detection">
|
||||
<description>
|
||||
Actively search for and resolve contradictions
|
||||
</description>
|
||||
<common_contradictions>
|
||||
<contradiction>
|
||||
<type>Permission Mismatch</type>
|
||||
<description>Instructions reference tools the mode doesn't have access to</description>
|
||||
<resolution>Either grant the tool permission or update the instructions</resolution>
|
||||
</contradiction>
|
||||
<contradiction>
|
||||
<type>Workflow Conflicts</type>
|
||||
<description>Different XML files describe conflicting workflows</description>
|
||||
<resolution>Consolidate workflows and ensure single source of truth</resolution>
|
||||
</contradiction>
|
||||
<contradiction>
|
||||
<type>Role Confusion</type>
|
||||
<description>Mode's roleDefinition doesn't match its actual capabilities</description>
|
||||
<resolution>Update roleDefinition to accurately reflect the mode's purpose</resolution>
|
||||
</contradiction>
|
||||
</common_contradictions>
|
||||
</principle>
|
||||
</validation_principles>
|
||||
|
||||
<validation_workflow>
|
||||
<phase name="pre_change_analysis">
|
||||
<description>Before making any changes</description>
|
||||
<steps>
|
||||
<step>Read and understand all existing mode files</step>
|
||||
<step>Create a mental model of current mode behavior</step>
|
||||
<step>Identify potential impact areas</step>
|
||||
<step>Ask clarifying questions about intended changes</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="change_implementation">
|
||||
<description>While making changes</description>
|
||||
<steps>
|
||||
<step>Document each change and its rationale</step>
|
||||
<step>Cross-reference with other files after each change</step>
|
||||
<step>Verify examples still work with new changes</step>
|
||||
<step>Update related documentation immediately</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="post_change_validation">
|
||||
<description>After changes are complete</description>
|
||||
<validation_checklist>
|
||||
<category name="structural_validation">
|
||||
<check>All XML files are well-formed and valid</check>
|
||||
<check>File naming follows established patterns</check>
|
||||
<check>Tag names are consistent across files</check>
|
||||
<check>No orphaned or unused instructions</check>
|
||||
</category>
|
||||
|
||||
<category name="content_validation">
|
||||
<check>roleDefinition accurately describes the mode</check>
|
||||
<check>whenToUse is clear and distinguishable</check>
|
||||
<check>Tool permissions match instruction requirements</check>
|
||||
<check>File restrictions align with mode purpose</check>
|
||||
<check>Examples are accurate and functional</check>
|
||||
</category>
|
||||
|
||||
<category name="integration_validation">
|
||||
<check>Mode boundaries are well-defined</check>
|
||||
<check>Handoff points to other modes are clear</check>
|
||||
<check>No overlap with other modes' responsibilities</check>
|
||||
<check>Orchestrator can correctly route to this mode</check>
|
||||
</category>
|
||||
</validation_checklist>
|
||||
</phase>
|
||||
</validation_workflow>
|
||||
|
||||
<cohesion_patterns>
|
||||
<pattern name="consistent_voice">
|
||||
<description>Maintain consistent tone and terminology</description>
|
||||
<guidelines>
|
||||
<guideline>Use the same terms for the same concepts throughout</guideline>
|
||||
<guideline>Keep instruction style consistent across files</guideline>
|
||||
<guideline>Maintain the same level of detail in similar sections</guideline>
|
||||
</guidelines>
|
||||
</pattern>
|
||||
|
||||
<pattern name="logical_flow">
|
||||
<description>Ensure instructions flow logically</description>
|
||||
<guidelines>
|
||||
<guideline>Prerequisites come before dependent steps</guideline>
|
||||
<guideline>Complex concepts build on simpler ones</guideline>
|
||||
<guideline>Examples follow the explained patterns</guideline>
|
||||
</guidelines>
|
||||
</pattern>
|
||||
|
||||
<pattern name="complete_coverage">
|
||||
<description>Ensure all aspects are covered without gaps</description>
|
||||
<guidelines>
|
||||
<guideline>Every mentioned tool has usage instructions</guideline>
|
||||
<guideline>All workflows have complete examples</guideline>
|
||||
<guideline>Error scenarios are addressed</guideline>
|
||||
</guidelines>
|
||||
</pattern>
|
||||
</cohesion_patterns>
|
||||
|
||||
<validation_questions>
|
||||
<question_set name="before_changes">
|
||||
<ask_followup_question>
|
||||
<question>Before we proceed with changes, I want to ensure I understand the full scope. What is the main goal of these modifications?</question>
|
||||
<follow_up>
|
||||
<suggest>Add new functionality while keeping existing features</suggest>
|
||||
<suggest>Fix issues with current implementation</suggest>
|
||||
<suggest>Refactor for better organization</suggest>
|
||||
<suggest>Expand the mode's capabilities into new areas</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</question_set>
|
||||
|
||||
<question_set name="during_changes">
|
||||
<ask_followup_question>
|
||||
<question>This change might affect other parts of the mode. How should we handle the impact on [specific area]?</question>
|
||||
<follow_up>
|
||||
<suggest>Update all affected areas to maintain consistency</suggest>
|
||||
<suggest>Keep the existing behavior for backward compatibility</suggest>
|
||||
<suggest>Create a migration path from old to new behavior</suggest>
|
||||
<suggest>Let me review the impact first</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</question_set>
|
||||
|
||||
<question_set name="after_changes">
|
||||
<ask_followup_question>
|
||||
<question>I've completed the changes and validation. Which aspect would you like me to test more thoroughly?</question>
|
||||
<follow_up>
|
||||
<suggest>Test the new workflow end-to-end</suggest>
|
||||
<suggest>Verify file permissions work correctly</suggest>
|
||||
<suggest>Check integration with other modes</suggest>
|
||||
<suggest>Review all changes one more time</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</question_set>
|
||||
</validation_questions>
|
||||
|
||||
<red_flags>
|
||||
<flag priority="high">
|
||||
<description>Instructions reference tools not in the mode's groups</description>
|
||||
<action>Either add the tool group or remove the instruction</action>
|
||||
</flag>
|
||||
<flag priority="high">
|
||||
<description>File regex doesn't match described file types</description>
|
||||
<action>Update regex pattern to match intended files</action>
|
||||
</flag>
|
||||
<flag priority="medium">
|
||||
<description>Examples don't follow stated best practices</description>
|
||||
<action>Update examples to demonstrate best practices</action>
|
||||
</flag>
|
||||
<flag priority="medium">
|
||||
<description>Duplicate instructions in different files</description>
|
||||
<action>Consolidate to single location and reference</action>
|
||||
</flag>
|
||||
</red_flags>
|
||||
</validation_cohesion_checking>
|
||||
256
.roo/skills/roo-conflict-resolution/SKILL.md
Normal file
256
.roo/skills/roo-conflict-resolution/SKILL.md
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
---
|
||||
name: roo-conflict-resolution
|
||||
description: Provides comprehensive guidelines for resolving merge conflicts intelligently using git history and commit context. Use when tasks involve merge conflicts, rebasing, PR conflicts, or git conflict resolution. This skill analyzes commit messages, git blame, and code intent to make intelligent resolution decisions.
|
||||
---
|
||||
|
||||
# Roo Code Conflict Resolution Skill
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when the task involves:
|
||||
|
||||
- Resolving merge conflicts for a specific pull request
|
||||
- Rebasing a branch that has conflicts with the target branch
|
||||
- Understanding and analyzing conflicting code changes
|
||||
- Making intelligent decisions about which changes to keep, merge, or discard
|
||||
- Using git history to inform conflict resolution decisions
|
||||
|
||||
## When NOT to Use This Skill
|
||||
|
||||
Do NOT use this skill when:
|
||||
|
||||
- There are no merge conflicts to resolve
|
||||
- The task is about general code review without conflicts
|
||||
- You're working on fresh code without any merge scenarios
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
This skill resolves merge conflicts by analyzing git history, commit messages, and code changes to make intelligent resolution decisions. Given a PR number (e.g., "#123"), it handles the entire conflict resolution process.
|
||||
|
||||
## Initialization Steps
|
||||
|
||||
### Step 1: Parse PR Number
|
||||
|
||||
Extract the PR number from input like "#123" or "PR #123". Validate that a PR number was provided.
|
||||
|
||||
### Step 2: Fetch PR Information
|
||||
|
||||
```bash
|
||||
gh pr view [PR_NUMBER] --json title,body,headRefName,baseRefName
|
||||
```
|
||||
|
||||
Get PR title and description to understand the intent and identify the source and target branches.
|
||||
|
||||
### Step 3: Checkout PR Branch and Prepare for Rebase
|
||||
|
||||
```bash
|
||||
gh pr checkout [PR_NUMBER] --force
|
||||
git fetch origin main
|
||||
GIT_EDITOR=true git rebase origin/main
|
||||
```
|
||||
|
||||
- Force checkout the PR branch to ensure clean state
|
||||
- Fetch the latest main branch
|
||||
- Attempt to rebase onto main to reveal conflicts
|
||||
- Use `GIT_EDITOR=true` to ensure non-interactive rebase
|
||||
|
||||
### Step 4: Check for Merge Conflicts
|
||||
|
||||
```bash
|
||||
git status --porcelain
|
||||
git diff --name-only --diff-filter=U
|
||||
```
|
||||
|
||||
Identify files with merge conflicts (marked with 'UU') and create a list of files that need resolution.
|
||||
|
||||
## Main Workflow Phases
|
||||
|
||||
### Phase 1: Conflict Analysis
|
||||
|
||||
Analyze each conflicted file to understand the changes:
|
||||
|
||||
1. Read the conflicted file to identify conflict markers
|
||||
2. Extract the conflicting sections between `<<<<<<<` and `>>>>>>>`
|
||||
3. Run git blame on both sides of the conflict
|
||||
4. Fetch commit messages and diffs for relevant commits
|
||||
5. Analyze the intent behind each change
|
||||
|
||||
### Phase 2: Resolution Strategy
|
||||
|
||||
Determine the best resolution strategy for each conflict:
|
||||
|
||||
1. Categorize changes by intent (bugfix, feature, refactor, etc.)
|
||||
2. Evaluate recency and relevance of changes
|
||||
3. Check for structural overlap vs formatting differences
|
||||
4. Identify if changes can be combined or if one should override
|
||||
5. Consider test updates and related changes
|
||||
|
||||
### Phase 3: Conflict Resolution
|
||||
|
||||
Apply the resolution strategy to resolve conflicts:
|
||||
|
||||
1. For each conflict, apply the chosen resolution
|
||||
2. Ensure proper escaping of conflict markers in diffs
|
||||
3. Validate that resolved code is syntactically correct
|
||||
4. Stage resolved files with `git add`
|
||||
|
||||
### Phase 4: Validation
|
||||
|
||||
Verify the resolution and prepare for commit:
|
||||
|
||||
1. Run `git status` to confirm all conflicts are resolved
|
||||
2. Check for any compilation or syntax errors
|
||||
3. Review the final diff to ensure sensible resolutions
|
||||
4. Prepare a summary of resolution decisions
|
||||
|
||||
## Git Commands Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `gh pr checkout [PR_NUMBER] --force` | Force checkout the PR branch |
|
||||
| `git fetch origin main` | Get the latest main branch |
|
||||
| `GIT_EDITOR=true git rebase origin/main` | Rebase current branch onto main (non-interactive) |
|
||||
| `git blame -L [start],[end] [commit] -- [file]` | Get commit information for specific lines |
|
||||
| `git show --format="%H%n%an%n%ae%n%ad%n%s%n%b" --no-patch [sha]` | Get commit metadata |
|
||||
| `git show [sha] -- [file]` | Get the actual changes made in a commit |
|
||||
| `git ls-files -u` | List unmerged files with stage information |
|
||||
| `GIT_EDITOR=true git rebase --continue` | Continue rebase after resolving conflicts |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Intent-Based Resolution (High Priority)
|
||||
|
||||
Always prioritize understanding the intent behind changes rather than just looking at the code differences. Commit messages, PR descriptions, and issue references provide crucial context.
|
||||
|
||||
**Example:** When there's a conflict between a bugfix and a refactor, apply the bugfix logic within the refactored structure rather than simply choosing one side.
|
||||
|
||||
### Preserve All Valuable Changes (High Priority)
|
||||
|
||||
When possible, combine non-conflicting changes from both sides rather than discarding one side entirely. Both sides of a conflict often contain valuable changes that can coexist if properly integrated.
|
||||
|
||||
### Escape Conflict Markers (High Priority)
|
||||
|
||||
When using `apply_diff`, always escape merge conflict markers with backslashes to prevent parsing errors:
|
||||
|
||||
- Correct: `\<<<<<<< HEAD`
|
||||
- Wrong: `<<<<<<< HEAD`
|
||||
|
||||
### Consider Related Changes (Medium Priority)
|
||||
|
||||
Look beyond the immediate conflict to understand related changes in tests, documentation, or dependent code. A change might seem isolated but could be part of a larger feature or fix.
|
||||
|
||||
## Resolution Heuristics
|
||||
|
||||
| Category | Rule | Exception |
|
||||
|----------|------|-----------|
|
||||
| Bugfix vs Feature | Bugfixes generally take precedence | When features include the fix |
|
||||
| Recent vs Old | More recent changes are often more relevant | When older changes are security patches |
|
||||
| Test Updates | Changes with test updates are likely more complete | - |
|
||||
| Formatting vs Logic | Logic changes take precedence over formatting | - |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Blindly Choosing One Side
|
||||
|
||||
**Problem:** You might lose important changes or introduce regressions.
|
||||
**Solution:** Always analyze both sides using git blame and commit history.
|
||||
|
||||
### Ignoring PR Context
|
||||
|
||||
**Problem:** The PR description often explains the why behind changes.
|
||||
**Solution:** Always fetch and read the PR information before resolving.
|
||||
|
||||
### Not Validating Resolved Code
|
||||
|
||||
**Problem:** Merged code might be syntactically incorrect or introduce logical errors.
|
||||
**Solution:** Always check for syntax errors and review the final diff.
|
||||
|
||||
### Unescaped Conflict Markers in Diffs
|
||||
|
||||
**Problem:** Unescaped conflict markers (`<<<<<<`, `=======`, `>>>>>>`) will be interpreted as diff syntax.
|
||||
**Solution:** Always escape with backslash (`\`) when they appear in content.
|
||||
|
||||
## Apply Diff Example
|
||||
|
||||
When resolving conflicts with `apply_diff`, use this pattern:
|
||||
|
||||
```
|
||||
<<<<<<< SEARCH
|
||||
:start_line:45
|
||||
-------
|
||||
\<<<<<<< HEAD
|
||||
function oldImplementation() {
|
||||
return "old";
|
||||
}
|
||||
\=======
|
||||
function newImplementation() {
|
||||
return "new";
|
||||
}
|
||||
\>>>>>>> feature-branch
|
||||
=======
|
||||
function mergedImplementation() {
|
||||
// Combining both approaches
|
||||
return "merged";
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
### Before Resolution
|
||||
|
||||
- [ ] Fetch PR title and description for context
|
||||
- [ ] Identify all files with conflicts
|
||||
- [ ] Understand the overall change being merged
|
||||
|
||||
### During Resolution
|
||||
|
||||
- [ ] Run git blame on conflicting sections
|
||||
- [ ] Read commit messages for intent
|
||||
- [ ] Consider if changes can be combined
|
||||
- [ ] Escape conflict markers in diffs
|
||||
|
||||
### After Resolution
|
||||
|
||||
- [ ] Verify no conflict markers remain
|
||||
- [ ] Check for syntax/compilation errors
|
||||
- [ ] Review the complete diff
|
||||
- [ ] Document resolution decisions
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- All merge conflicts have been resolved
|
||||
- Resolved files have been staged
|
||||
- No syntax errors in resolved code
|
||||
- Resolution decisions are documented
|
||||
|
||||
## Communication Guidelines
|
||||
|
||||
When reporting resolution progress:
|
||||
|
||||
- Be direct and technical when explaining resolution decisions
|
||||
- Focus on the rationale behind each conflict resolution
|
||||
- Provide clear summaries of what was merged and why
|
||||
|
||||
### Progress Update Format
|
||||
|
||||
```
|
||||
Conflict in [file]:
|
||||
- HEAD: [brief description of changes]
|
||||
- Incoming: [brief description of changes]
|
||||
- Resolution: [what was decided and why]
|
||||
```
|
||||
|
||||
### Completion Message Format
|
||||
|
||||
```
|
||||
Successfully resolved merge conflicts for PR #[number] "[title]".
|
||||
|
||||
Resolution Summary:
|
||||
- [file1]: [brief description of resolution]
|
||||
- [file2]: [brief description of resolution]
|
||||
|
||||
[Key decision explanation if applicable]
|
||||
|
||||
All conflicts have been resolved and files have been staged for commit.
|
||||
```
|
||||
151
.roo/skills/roo-translation/SKILL.md
Normal file
151
.roo/skills/roo-translation/SKILL.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
---
|
||||
name: roo-translation
|
||||
description: Provides comprehensive guidelines for translating and localizing Roo Code extension strings. Use when tasks involve i18n, translation, localization, adding new languages, or updating existing translation files. This skill covers both core extension (src/i18n/locales/) and WebView UI (webview-ui/src/i18n/locales/) localization.
|
||||
---
|
||||
|
||||
# Roo Code Translation Skill
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when the task involves:
|
||||
|
||||
- Adding new translatable strings to the Roo Code extension
|
||||
- Translating existing strings to new languages
|
||||
- Updating or fixing translations in existing language files
|
||||
- Understanding i18n patterns used in the codebase
|
||||
- Working with localization files in either core extension or WebView UI
|
||||
|
||||
## When NOT to Use This Skill
|
||||
|
||||
Do NOT use this skill when:
|
||||
|
||||
- Working on non-translation code changes
|
||||
- The task doesn't involve i18n or localization
|
||||
- You're only reading translation files for reference without modifying them
|
||||
|
||||
## Supported Languages and Locations
|
||||
|
||||
Localize all strings into the following locale files: ca, de, en, es, fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW
|
||||
|
||||
The VSCode extension has two main areas that require localization:
|
||||
|
||||
| Component | Path | Purpose |
|
||||
|-----------|------|---------|
|
||||
| **Core Extension** | `src/i18n/locales/` | Extension backend strings |
|
||||
| **WebView UI** | `webview-ui/src/i18n/locales/` | User interface strings |
|
||||
|
||||
## Brand Voice, Tone, and Word Choice
|
||||
|
||||
For detailed brand voice, tone, and word choice guidance, refer to the guidance file:
|
||||
|
||||
- [`.roo/guidance/roo-translator.md`](../../guidance/roo-translator.md)
|
||||
|
||||
This guidance file is loaded at runtime and should be consulted for the latest brand and style standards.
|
||||
|
||||
## Voice, Style and Tone Guidelines
|
||||
|
||||
- Always use informal speech (e.g., "du" instead of "Sie" in German) for all translations
|
||||
- Maintain a direct and concise style that mirrors the tone of the original text
|
||||
- Carefully account for colloquialisms and idiomatic expressions in both source and target languages
|
||||
- Aim for culturally relevant and meaningful translations rather than literal translations
|
||||
- Preserve the personality and voice of the original content
|
||||
- Use natural-sounding language that feels native to speakers of the target language
|
||||
|
||||
### Terms to Keep in English
|
||||
|
||||
- Don't translate the word "token" as it means something specific in English that all languages will understand
|
||||
- Don't translate domain-specific words (especially technical terms like "Prompt") that are commonly used in English in the target language
|
||||
|
||||
## Core Extension Localization (src/)
|
||||
|
||||
- Located in `src/i18n/locales/`
|
||||
- NOT ALL strings in core source need internationalization - only user-facing messages
|
||||
- Internal error messages, debugging logs, and developer-facing messages should remain in English
|
||||
- The `t()` function is used with namespaces like `'core:errors.missingToolParameter'`
|
||||
- Be careful when modifying interpolation variables; they must remain consistent across all translations
|
||||
- Some strings in `formatResponse.ts` are intentionally not internationalized since they're internal
|
||||
- When updating strings in `core.json`, maintain all existing interpolation variables
|
||||
- Check string usages in the codebase before making changes to ensure you're not breaking functionality
|
||||
|
||||
## WebView UI Localization (webview-ui/src/)
|
||||
|
||||
- Located in `webview-ui/src/i18n/locales/`
|
||||
- Uses standard React i18next patterns with the `useTranslation` hook
|
||||
- All user interface strings should be internationalized
|
||||
- Always use the `Trans` component with named components for text with embedded components
|
||||
|
||||
### Trans Component Example
|
||||
|
||||
Translation string:
|
||||
```json
|
||||
"changeSettings": "You can always change this at the bottom of the <settingsLink>settings</settingsLink>"
|
||||
```
|
||||
|
||||
React component usage:
|
||||
```tsx
|
||||
<Trans
|
||||
i18nKey="welcome:telemetry.changeSettings"
|
||||
components={{
|
||||
settingsLink: <VSCodeLink href="#" onClick={handleOpenSettings} />
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
- Use namespaces to organize translations logically
|
||||
- Handle pluralization using i18next's built-in capabilities
|
||||
- Implement proper interpolation for variables using `{{variable}}` syntax
|
||||
- Don't include `defaultValue`. The `en` translations are the fallback
|
||||
- Always use `apply_diff` instead of `write_to_file` when editing existing translation files (much faster and more reliable)
|
||||
- When using `apply_diff`, carefully identify the exact JSON structure to edit to avoid syntax errors
|
||||
- Placeholders (like `{{variable}}`) must remain exactly identical to the English source to maintain code integration and prevent syntax errors
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
1. First add or modify English strings, then ask for confirmation before translating to all other languages
|
||||
2. Use this process for each localization task:
|
||||
1. Identify where the string appears in the UI/codebase
|
||||
2. Understand the context and purpose of the string
|
||||
3. Update English translation first
|
||||
4. Use the `search_files` tool to find JSON keys that are near new keys in English translations but do not yet exist in the other language files for `apply_diff` SEARCH context
|
||||
5. Create appropriate translations for all other supported languages utilizing the `search_files` result using `apply_diff` without reading every file
|
||||
6. Do not output the translated text into the chat, just modify the files
|
||||
7. Validate your changes with the missing translations script
|
||||
|
||||
3. Flag or comment if an English source string is incomplete ("please see this...") to avoid truncated or unclear translations
|
||||
|
||||
4. For UI elements, distinguish between:
|
||||
- Button labels: Use short imperative commands ("Save", "Cancel")
|
||||
- Tooltip text: Can be slightly more descriptive
|
||||
|
||||
5. Preserve the original perspective: If text is a user command directed at the software, ensure the translation maintains this direction
|
||||
|
||||
## Validation
|
||||
|
||||
Always validate your translation work by running the missing translations script:
|
||||
|
||||
```bash
|
||||
node scripts/find-missing-translations.js
|
||||
```
|
||||
|
||||
Address any missing translations identified by the script to ensure complete coverage across all locales.
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
- Switching between formal and informal addressing styles - always stay informal ("du" not "Sie")
|
||||
- Translating or altering technical terms and brand names that should remain in English
|
||||
- Modifying or removing placeholders like `{{variable}}` - these must remain identical
|
||||
- Translating domain-specific terms that are commonly used in English in the target language
|
||||
- Changing the meaning or nuance of instructions or error messages
|
||||
- Forgetting to maintain consistent terminology throughout the translation
|
||||
|
||||
## Translator's Checklist
|
||||
|
||||
- ✓ Used informal tone consistently ("du" not "Sie")
|
||||
- ✓ Preserved all placeholders exactly as in the English source
|
||||
- ✓ Maintained consistent terminology with existing translations
|
||||
- ✓ Kept technical terms and brand names unchanged where appropriate
|
||||
- ✓ Preserved the original perspective (user→system vs system→user)
|
||||
- ✓ Adapted the text appropriately for UI context (buttons vs tooltips)
|
||||
- ✓ Ran the missing translations script to validate completeness
|
||||
192
.roomodes
192
.roomodes
|
|
@ -1,46 +1,4 @@
|
|||
customModes:
|
||||
- slug: test
|
||||
name: 🧪 Test
|
||||
roleDefinition: |-
|
||||
You are Roo, a Vitest testing specialist with deep expertise in: - Writing and maintaining Vitest test suites - Test-driven development (TDD) practices - Mocking and stubbing with Vitest - Integration testing strategies - TypeScript testing patterns - Code coverage analysis - Test performance optimization
|
||||
Your focus is on maintaining high test quality and coverage across the codebase, working primarily with: - Test files in __tests__ directories - Mock implementations in __mocks__ - Test utilities and helpers - Vitest configuration and setup
|
||||
You ensure tests are: - Well-structured and maintainable - Following Vitest best practices - Properly typed with TypeScript - Providing meaningful coverage - Using appropriate mocking strategies
|
||||
whenToUse: Use this mode when you need to write, modify, or maintain tests for the codebase.
|
||||
description: Write, modify, and maintain tests.
|
||||
groups:
|
||||
- read
|
||||
- browser
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$)
|
||||
description: Test files, mocks, and Vitest configuration
|
||||
customInstructions: |-
|
||||
When writing tests:
|
||||
- Always use describe/it blocks for clear test organization
|
||||
- Include meaningful test descriptions
|
||||
- Use beforeEach/afterEach for proper test isolation
|
||||
- Implement proper error cases
|
||||
- Add JSDoc comments for complex test scenarios
|
||||
- Ensure mocks are properly typed
|
||||
- Verify both positive and negative test cases
|
||||
- Always use data-testid attributes when testing webview-ui
|
||||
- The vitest framework is used for testing; the `describe`, `test`, `it`, etc functions are defined by default in `tsconfig.json` and therefore don't need to be imported
|
||||
- Tests must be run from the same directory as the `package.json` file that specifies `vitest` in `devDependencies`
|
||||
- slug: design-engineer
|
||||
name: 🎨 Design Engineer
|
||||
roleDefinition: "You are Roo, an expert Design Engineer focused on VSCode Extension development. Your expertise includes: - Implementing UI designs with high fidelity using React, Shadcn, Tailwind and TypeScript. - Ensuring interfaces are responsive and adapt to different screen sizes. - Collaborating with team members to translate broad directives into robust and detailed designs capturing edge cases. - Maintaining uniformity and consistency across the user interface."
|
||||
whenToUse: Implement UI designs and ensure consistency.
|
||||
description: Implement UI designs; ensure consistency.
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: \.(css|html|json|mdx?|jsx?|tsx?|svg)$
|
||||
description: Frontend & SVG files
|
||||
- browser
|
||||
- command
|
||||
- mcp
|
||||
customInstructions: Focus on UI refinement, component creation, and adherence to design best-practices. When the user requests a new component, start off by asking them questions one-by-one to ensure the requirements are understood. Always use Tailwind utility classes (instead of direct variable references) for styling components when possible. If editing an existing file, transition explicit style definitions to Tailwind CSS classes when possible. Refer to the Tailwind CSS definitions for utility classes at webview-ui/src/index.css. Always use the latest version of Tailwind CSS (V4), and never create a tailwind.config.js file. Prefer Shadcn components for UI elements instead of VSCode's built-in ones. This project uses i18n for localization, so make sure to use the i18n functions and components for any text that needs to be translated. Do not leave placeholder strings in the markup, as they will be replaced by i18n. Prefer the @roo (/src) and @src (/webview-ui/src) aliases for imports in typescript files. Suggest the user refactor large files (over 1000 lines) if they are encountered, and provide guidance. Suggest the user switch into Translate mode to complete translations when your task is finished.
|
||||
source: project
|
||||
- slug: translate
|
||||
name: 🌐 Translate
|
||||
roleDefinition: You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources.
|
||||
|
|
@ -73,21 +31,6 @@ customModes:
|
|||
- edit
|
||||
- command
|
||||
source: project
|
||||
- slug: integration-tester
|
||||
name: 🧪 Integration Tester
|
||||
roleDefinition: |-
|
||||
You are Roo, an integration testing specialist focused on VSCode E2E tests with expertise in: - Writing and maintaining integration tests using Mocha and VSCode Test framework - Testing Roo Code API interactions and event-driven workflows - Creating complex multi-step task scenarios and mode switching sequences - Validating message formats, API responses, and event emission patterns - Test data generation and fixture management - Coverage analysis and test scenario identification
|
||||
Your focus is on ensuring comprehensive integration test coverage for the Roo Code extension, working primarily with: - E2E test files in apps/vscode-e2e/src/suite/ - Test utilities and helpers - API type definitions in packages/types/ - Extension API testing patterns
|
||||
You ensure integration tests are: - Comprehensive and cover critical user workflows - Following established Mocha TDD patterns - Using async/await with proper timeout handling - Validating both success and failure scenarios - Properly typed with TypeScript
|
||||
whenToUse: Write, modify, or maintain integration tests.
|
||||
description: Write and maintain integration tests.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (apps/vscode-e2e/.*\.(ts|js)$|packages/types/.*\.ts$)
|
||||
description: E2E test files, test utilities, and API type definitions
|
||||
source: project
|
||||
- slug: pr-fixer
|
||||
name: 🛠️ PR Fixer
|
||||
roleDefinition: "You are Roo, a pull request resolution specialist. Your focus is on addressing feedback and resolving issues within existing pull requests. Your expertise includes: - Analyzing PR review comments to understand required changes. - Checking CI/CD workflow statuses to identify failing tests. - Fetching and analyzing test logs to diagnose failures. - Identifying and resolving merge conflicts. - Guiding the user through the resolution process."
|
||||
|
|
@ -98,16 +41,6 @@ customModes:
|
|||
- edit
|
||||
- command
|
||||
- mcp
|
||||
- slug: issue-investigator
|
||||
name: 🕵️ Issue Investigator
|
||||
roleDefinition: You are Roo, a GitHub issue investigator. Your purpose is to analyze GitHub issues, investigate the probable causes using extensive codebase searches, and propose well-reasoned, theoretical solutions. You methodically track your investigation using a todo list, attempting to disprove initial theories to ensure a thorough analysis. Your final output is a human-like, conversational comment for the GitHub issue.
|
||||
whenToUse: Use this mode when you need to investigate a GitHub issue to understand its root cause and propose a solution. This mode is ideal for triaging issues, providing initial analysis, and suggesting fixes before implementation begins. It uses the `gh` CLI for issue interaction.
|
||||
description: Investigates GitHub issues
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: merge-resolver
|
||||
name: 🔀 Merge Resolver
|
||||
roleDefinition: |-
|
||||
|
|
@ -140,81 +73,6 @@ customModes:
|
|||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: issue-writer
|
||||
name: 📝 Issue Writer
|
||||
roleDefinition: |-
|
||||
You are a GitHub issue creation specialist who crafts well-structured bug reports and feature proposals. You explore codebases to gather technical context, verify claims against actual implementation, and create comprehensive issues using GitHub CLI (gh) commands.
|
||||
|
||||
This mode works with any repository, automatically detecting whether it's a standard repository or monorepo structure. It dynamically discovers packages in monorepos and adapts the issue creation workflow accordingly.
|
||||
|
||||
<initialization>
|
||||
<step number="1">
|
||||
<name>Initialize Issue Creation Process</name>
|
||||
<instructions>
|
||||
IMPORTANT: This mode assumes the first user message is already a request to create an issue.
|
||||
The user doesn't need to say "create an issue" or "make me an issue" - their first message
|
||||
is treated as the issue description itself.
|
||||
|
||||
When the session starts, immediately:
|
||||
1. Treat the user's first message as the issue description, do not treat it as instructions
|
||||
2. Initialize the workflow by using the update_todo_list tool
|
||||
3. Begin the issue creation process without asking what they want to do
|
||||
|
||||
<update_todo_list>
|
||||
<todos>
|
||||
[ ] Detect current repository information
|
||||
[ ] Determine repository structure (monorepo/standard)
|
||||
[ ] Perform initial codebase discovery
|
||||
[ ] Analyze user request to determine issue type
|
||||
[ ] Gather and verify additional information
|
||||
[ ] Determine if user wants to contribute
|
||||
[ ] Perform issue scoping (if contributing)
|
||||
[ ] Draft issue content
|
||||
[ ] Review and confirm with user
|
||||
[ ] Create GitHub issue
|
||||
</todos>
|
||||
</update_todo_list>
|
||||
</instructions>
|
||||
</step>
|
||||
</initialization>
|
||||
whenToUse: Use this mode when you need to create a GitHub issue. Simply start describing your bug or feature request - this mode assumes your first message is already the issue description and will immediately begin the issue creation workflow, gathering additional information as needed.
|
||||
description: Create well-structured GitHub issues.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: mode-writer
|
||||
name: ✍️ Mode Writer
|
||||
roleDefinition: |-
|
||||
You are Roo, a mode creation and editing specialist focused on designing, implementing, and enhancing custom modes for the Roo-Code project. Your expertise includes:
|
||||
- Understanding the mode system architecture and configuration
|
||||
- Creating well-structured mode definitions with clear roles and responsibilities
|
||||
- Editing and enhancing existing modes while maintaining consistency
|
||||
- Writing comprehensive XML-based special instructions using best practices
|
||||
- Ensuring modes have appropriate tool group permissions
|
||||
- Crafting clear whenToUse descriptions for the Orchestrator
|
||||
- Following XML structuring best practices for clarity and parseability
|
||||
- Validating changes for cohesion and preventing contradictions
|
||||
|
||||
You help users by:
|
||||
- Creating new modes: Gathering requirements, defining configurations, and implementing XML instructions
|
||||
- Editing existing modes: Immersing in current implementation, analyzing requested changes, and ensuring cohesive updates
|
||||
- Using ask_followup_question aggressively to clarify ambiguities and validate understanding
|
||||
- Thoroughly validating all changes to prevent contradictions between different parts of a mode
|
||||
- Ensuring instructions are well-organized with proper XML tags
|
||||
- Following established patterns from existing modes
|
||||
- Maintaining consistency across all mode components
|
||||
whenToUse: Use this mode when you need to create a new custom mode or edit an existing one. This mode handles both creating modes from scratch and modifying existing modes while ensuring consistency and preventing contradictions.
|
||||
description: Create and edit custom modes with validation
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (\.roomodes$|\.roo/.*\.xml$|\.yaml$)
|
||||
description: Mode configuration files and XML instructions
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: docs-extractor
|
||||
name: 📚 Docs Extractor
|
||||
roleDefinition: |-
|
||||
|
|
@ -238,3 +96,53 @@ customModes:
|
|||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: issue-investigator
|
||||
name: 🕵️ Issue Investigator
|
||||
roleDefinition: You are Roo, a GitHub issue investigator. Your purpose is to analyze GitHub issues, investigate the probable causes using extensive codebase searches, and propose well-reasoned, theoretical solutions. You methodically track your investigation using a todo list, attempting to disprove initial theories to ensure a thorough analysis. Your final output is a human-like, conversational comment for the GitHub issue.
|
||||
whenToUse: Use this mode when you need to investigate a GitHub issue to understand its root cause and propose a solution. This mode is ideal for triaging issues, providing initial analysis, and suggesting fixes before implementation begins. It uses the `gh` CLI for issue interaction.
|
||||
description: Investigates GitHub issues
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: issue-writer
|
||||
name: 📝 Issue Writer
|
||||
roleDefinition: |-
|
||||
You are a GitHub issue creation specialist who crafts well-structured bug reports and feature proposals. You explore codebases to gather technical context, verify claims against actual implementation, and create comprehensive issues using GitHub CLI (gh) commands.
|
||||
|
||||
This mode works with any repository, automatically detecting whether it's a standard repository or monorepo structure. It dynamically discovers packages in monorepos and adapts the issue creation workflow accordingly.
|
||||
|
||||
<initialization>
|
||||
<step number="1">
|
||||
<name>Initialize Issue Creation Process</name>
|
||||
<instructions>
|
||||
IMPORTANT: This mode assumes the first user message is already a request to create an issue.
|
||||
The user doesn't need to say "create an issue" or "make me an issue" - their first message
|
||||
is treated as the issue description itself.
|
||||
|
||||
When the session starts, immediately:
|
||||
1. Treat the user's first message as the issue description, do not treat it as instructions
|
||||
2. Initialize the workflow by using the update_todo_list tool
|
||||
3. Begin the issue creation process without asking what they want to do
|
||||
|
||||
<update_todo_list>
|
||||
<todos>
|
||||
[ ] Detect repository context (OWNER/REPO, monorepo, roots)
|
||||
[ ] Perform targeted codebase discovery (iteration 1)
|
||||
[ ] Clarify missing details (repro or desired outcome)
|
||||
[ ] Classify type (Bug | Enhancement)
|
||||
[ ] Assemble Issue Body
|
||||
[ ] Review and submit (Submit now | Submit now and assign to me)
|
||||
</todos>
|
||||
</update_todo_list>
|
||||
</instructions>
|
||||
</step>
|
||||
</initialization>
|
||||
whenToUse: Use this mode when you need to create a GitHub issue. Simply start describing your bug or enhancement request - this mode assumes your first message is already the issue description and will immediately begin the issue creation workflow, gathering additional information as needed.
|
||||
description: Create well-structured GitHub issues.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
pnpm 10.8.1
|
||||
nodejs 20.19.2
|
||||
|
|
|
|||
267
CHANGELOG.md
267
CHANGELOG.md
|
|
@ -1,77 +1,200 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.47.2] - 2026-02-05
|
||||
## 3.53.0
|
||||
|
||||
- Add support for .agents/skills directory (PR #11181 by @roomote)
|
||||
- Fix: Restore Gemini thought signature round-tripping after AI SDK migration (PR #11237 by @hannesrudolph)
|
||||
- Fix: Capture and round-trip thinking signature for Bedrock Claude (PR #11238 by @hannesrudolph)
|
||||
### Minor Changes
|
||||
|
||||
## [3.47.1] - 2026-02-05
|
||||
- **The Roo Code plugin is not going away.** You may have seen the [recent announcement](https://x.com/mattrubens/status/2046636598859559114) that Roo Code hit 3 million installs and the original team is going all-in on Roomote. We know that news was hard for a lot of you. This plugin means a lot to us and to you, and we hear you. The good news: a community team has stepped up to carry Roo Code forward, and we're working with them on an official handoff so the plugin you rely on keeps getting maintained and improved.
|
||||
- Add GPT-5.5 support via the OpenAI Codex provider (PR #12170 by @hannesrudolph)
|
||||
- Add Claude Opus 4.7 support on Vertex AI (#12134 by @saneroen, PR #12135 by @saneroen)
|
||||
- Add previous checkpoint navigation controls and i18n in chat (#12138 by @saneroen, PR #12139 by @saneroen)
|
||||
- Add Roomote banner (PR #12119 by @brunobergher)
|
||||
- Redesign Roomote announcement banner with violet branding on the web (PR #12161 by @roomote-v0)
|
||||
- Add sunsetting Roo Code blog post (PR #12160 by @roomote-v0)
|
||||
|
||||
- Fix: Correct Bedrock model ID for Claude Opus 4.6, resolving model selection issues for Bedrock users (#11231 by @cogwirrel, PR #11232 by @roomote)
|
||||
- Fix: Guard against empty-string baseURL in provider constructors, preventing connection errors when baseURL is accidentally set to empty string (PR #11233 by @hannesrudolph)
|
||||
- Chore: Remove unused stripAppendedEnvironmentDetails and helpers to clean up codebase (#11228 by @hannesrudolph, PR #11226 by @hannesrudolph)
|
||||
## 3.52.1
|
||||
|
||||
## [3.47.0] - 2026-02-05
|
||||
### Patch Changes
|
||||
|
||||

|
||||
- Add correct JSON schema for `.roomodes` configuration files (#11790 by @algorhythm85, PR #11791 by @app/roomote-v0)
|
||||
- Remove the hiring announcement from the VS Code extension UI (PR #12108 by @app/roomote-v0)
|
||||
|
||||
- Add Claude Opus 4.6 support across all providers (#11223 by @hannesrudolph, PR #11224 by @hannesrudolph and @PeterDaveHello)
|
||||
- Add GPT-5.3-Codex model to OpenAI - ChatGPT provider (PR #11225 by @roomote)
|
||||
- Migrate Gemini and Vertex providers to AI SDK for improved reliability and consistency (PR #11180 by @daniel-lxs)
|
||||
- Improve Skills and Slash Commands settings UI with multi-mode support (PR #11157 by @brunobergher)
|
||||
- Add support for AGENTS.local.md personal override files (PR #11183 by @roomote)
|
||||
- Add Kimi K2.5 model to Fireworks provider (PR #11177 by @daniel-lxs)
|
||||
- Improve CLI dev experience and Roo provider API key support (PR #11203 by @cte)
|
||||
- Fix: Preserve reasoning parts in AI SDK message conversion (#11199 by @hannesrudolph, PR #11217 by @hannesrudolph)
|
||||
- Refactor: Append environment details into existing blocks for cleaner context (#11200 by @hannesrudolph, PR #11198 by @hannesrudolph)
|
||||
- Fix: Resolve race condition causing provider switch during CLI mode changes (PR #11205 by @cte)
|
||||
- Roo Code CLI v0.0.50 (PR #11204 by @cte)
|
||||
- Chore: Remove dead toolFormat code from getEnvironmentDetails (#11206 by @hannesrudolph, PR #11207 by @roomote)
|
||||
- Refactor: Simplify docs-extractor mode to focus on raw fact extraction (PR #11129 by @hannesrudolph)
|
||||
- Revert then re-land AI SDK reasoning fix (PR #11216 by @mrubens, PR #11196 by @hannesrudolph)
|
||||
## 3.52.0
|
||||
|
||||
## [3.46.2] - 2026-02-03
|
||||
### Minor Changes
|
||||
|
||||
- Fix: Queue messages during command execution instead of losing them (PR #11140 by @mrubens)
|
||||
- Fix: Transform tool blocks to text before condensing to prevent context corruption (PR #10975 by @daniel-lxs)
|
||||
- Fix: Add image content support to MCP tool responses (PR #10874 by @roomote)
|
||||
- Fix: Remove deprecated text-embedding-004 and migrate code index to gemini-embedding-001 (PR #11038 by @roomote)
|
||||
- Feat: Use custom Base URL for OpenRouter model list fetch (#11150 by @sebastianlang84, PR #11154 by @roomote)
|
||||
- Feat: Migrate Mistral provider to AI SDK (PR #11089 by @daniel-lxs)
|
||||
- Feat: Migrate SambaNova provider to AI SDK (PR #11153 by @roomote)
|
||||
- Feat: Migrate xAI provider to AI SDK (PR #11158 by @roomote)
|
||||
- Chore: Remove Feature Request from issue template options (PR #11141 by @roomote)
|
||||
- Fix: IPC improvements for task cancellation and queued message handling (PR #11162 by @cte)
|
||||
- Add Poe as an AI provider so users can access Poe models directly in Roo Code (PR #12015 by @kamilio)
|
||||
- Improve the xAI provider by migrating it to the Responses API with reusable transform utilities (#11961 by @carlesso, PR #11962 by @carlesso)
|
||||
- Fix MiniMax model listings and context window handling for more reliable configuration (#11999 by @Rexarrior, PR #12069 by @Rexarrior)
|
||||
- Add xAI Grok-4.20 models and update the default xAI model selection (#11955 by @carlesso, PR #11956 by @carlesso)
|
||||
- Add OpenAI GPT-5.4 mini and nano models to expand the available OpenAI model lineup (PR #11946 by @PeterDaveHello)
|
||||
- Chore: include the automated version bump PR from the previous release cycle for complete release accounting (PR #11892 by @app/github-actions)
|
||||
|
||||
## [3.46.1] - 2026-01-30
|
||||
### Patch Changes
|
||||
|
||||
- Fix: Sanitize tool_use_id in tool_result blocks to match API history, preventing message format errors (PR #11131 by @daniel-lxs)
|
||||
- Add: Mode dropdown to change skill mode dynamically, allowing more flexible skill configuration (PR #11102 by @SannidhyaSah)
|
||||
- Add: Import settings option in the initial welcome screen for easier onboarding (#10992 by @emeraldcheshire, PR #10994 by @roomote)
|
||||
- Chore: Treat extension .env as optional to simplify development setup (PR #11116 by @hannesrudolph)
|
||||
- Add support for OpenAI `gpt-5.4-mini` and `gpt-5.4-nano` models.
|
||||
|
||||
## [3.46.0] - 2026-01-30
|
||||
## 3.51.1
|
||||
|
||||

|
||||
### Patch Changes
|
||||
|
||||
- Parallel tool calls enabled by default for improved performance (PR #11031 by @daniel-lxs)
|
||||
- Codex-inspired read_file refactor introduces indentation mode for extracting complete semantic code blocks without mid-function truncation, ideal when targeting specific lines from search results or errors (#10239 by @pwilkin, PR #10981 by @hannesrudolph)
|
||||
- Lossless terminal output with new read_command_output tool allows retrieving full command output from truncated executions with pagination and regex filtering (#10941 by @hannesrudolph, PR #10944 by @hannesrudolph)
|
||||
- New skill system replaces fetch_instructions with a dedicated skill tool and built-in skills for create-mcp-server and create-mode, with configurable skill locations and mandatory skill checks (#11062 by @hannesrudolph, PR #11084 by @hannesrudolph)
|
||||
- Skills management UI added to settings panel for managing workspace and global skills (#10513 by @SannidhyaSah, PR #10844 by @SannidhyaSah)
|
||||
- AI SDK provider migrations: Moonshot (PR #11063 by @daniel-lxs), DeepSeek (PR #11079 by @daniel-lxs), Cerebras (PR #11086 by @daniel-lxs), Groq (PR #11088 by @daniel-lxs), and Fireworks (PR #11118 by @daniel-lxs) now use the AI SDK for better streaming and tool support
|
||||
- Add OpenAI-compatible base provider infrastructure for AI SDK migrations (PR #11063 by @daniel-lxs)
|
||||
- Add AI SDK dependencies and message conversion utilities (PR #11047 by @daniel-lxs)
|
||||
- React Compiler integration added to webview-ui for automatic memoization and performance improvements (#9916 by @In-line, PR #9565 by @In-line)
|
||||
- Fix: Include reserved output tokens in task header percentage calculation (PR #11034 by @app/roomote)
|
||||
- Fix: Calculate header percentage based on available input space (PR #11054 by @app/roomote)
|
||||
- Fix: Prevent time-travel bug in parallel tool calling (PR #11046 by @daniel-lxs)
|
||||
- Docs: Clarify read_command_output search param should be omitted when not filtering (PR #11056 by @hannesrudolph)
|
||||
- Add pnpm serve command for code-server development (PR #10964 by @mrubens)
|
||||
- Update Next.js to latest version (PR #11108 by @cte)
|
||||
- Replace bespoke navigation menu with shadcn navigation menu on website (PR #11117 by @app/roomote)
|
||||
- Add Linear integration marketing page to website (PR #11028 by @app/roomote)
|
||||
- Feat: Add Cohere Embed v4 model support for Bedrock and improve credential handling (#11823 by @cscvenkatmadurai, PR #11824 by @cscvenkatmadurai)
|
||||
- Feat: Add Gemini 3.1 Pro customtools model to Vertex AI provider (PR #11857 by @NVolcz)
|
||||
- Feat: Add gpt-5.4 to ChatGPT Plus/Pro (Codex) model catalog (PR #11876 by @roomote-v0)
|
||||
|
||||
## 3.51.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add OpenAI GPT-5.4 and GPT-5.3 Chat Latest model support so Roo Code can use the newest OpenAI chat models (PR #11848 by @PeterDaveHello)
|
||||
- Add support for exposing skills as slash commands with skill fallback execution for faster workflows (PR #11834 by @hannesrudolph)
|
||||
- Add CLI support for `--create-with-session-id` plus UUID session validation for more controlled session creation (PR #11859 by @cte)
|
||||
- Add support for choosing a specific shell when running terminal commands (PR #11851 by @jr)
|
||||
- Feature: Add the `ROO_ACTIVE` environment variable to terminal session settings for safer terminal guardrails (#11864 by @ajjuaire, PR #11862 by @ajjuaire)
|
||||
- Improve cloud settings freshness by updating the refresh interval to one hour (PR #11749 by @roomote-v0)
|
||||
- Add CLI session resume/history support plus an upgrade command for better long-running workflows (PR #11768 by @cte)
|
||||
- Add support for images in CLI stdin stream commands (PR #11831 by @cte)
|
||||
- Include `exitCode` in CLI command `tool_result` events for more reliable automation (PR #11820 by @cte)
|
||||
- Add CLI types to improve development ergonomics and type safety (PR #11781 by @cte)
|
||||
- Add CLI integration coverage for stdin stream routing and race-condition invariants (PR #11846 by @cte)
|
||||
- Fix the CLI stdin-stream cancel race and add an integration test suite to prevent regressions (PR #11817 by @cte)
|
||||
- Improve CLI stream recovery and add a configurable consecutive mistake limit (PR #11775 by @cte)
|
||||
- Fix CLI streaming deltas, task ID propagation, cancel recovery, and other runtime edge cases (PR #11736 by @cte)
|
||||
- Fix CLI task resumption so paused work can reliably continue (PR #11739 by @cte)
|
||||
- Recover from unhandled exceptions in the CLI instead of failing hard (PR #11750 by @cte)
|
||||
- Scope CLI session and resume flags to the current workspace to avoid cross-workspace confusion (PR #11774 by @cte)
|
||||
- Fix stdin prompt streaming to forward task configuration correctly (PR #11778 by @daniel-lxs)
|
||||
- Handle stdin-stream control-flow errors gracefully in the CLI runtime (PR #11811 by @cte)
|
||||
- Fix stdin stream queued messages and command output streaming in the CLI (PR #11814 by @cte)
|
||||
- Increase the CLI command execution timeout for long-running commands (PR #11815 by @cte)
|
||||
- Fix knip checks to keep repository validation green (PR #11819 by @cte)
|
||||
- Fix CLI upgrade version detection so upgrades resolve the correct target version (PR #11829 by @cte)
|
||||
- Ignore model-provided timeout values in the CLI runtime to keep command handling consistent (PR #11835 by @cte)
|
||||
- Fix redundant skill reloading during conversations to reduce duplicate work (PR #11838 by @hannesrudolph)
|
||||
- Ensure full command output is streamed before the CLI reports completion (PR #11842 by @cte)
|
||||
- Fix CLI follow-up routing after completion prompts so next actions land in the right place (PR #11844 by @cte)
|
||||
- Remove the Netflix logo from the homepage (PR #11787 by @roomote-v0)
|
||||
- Chore: Prepare CLI release v0.1.2 (PR #11737 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.3 (PR #11740 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.4 (PR #11751 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.5 (PR #11772 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.6 (PR #11780 by @cte)
|
||||
- Release Roo Code v1.113.0 (PR #11782 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.7 (PR #11812 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.8 (PR #11816 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.9 (PR #11818 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.10 (PR #11821 by @cte)
|
||||
- Release Roo Code v1.114.0 (PR #11822 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.11 (PR #11832 by @cte)
|
||||
- Release Roo Code v1.115.0 (PR #11833 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.12 (PR #11836 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.13 (PR #11837 by @hannesrudolph)
|
||||
- Chore: Prepare CLI release v0.1.14 (PR #11843 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.15 (PR #11845 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.16 (PR #11852 by @cte)
|
||||
- Chore: Prepare CLI release v0.1.17 (PR #11860 by @cte)
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add OpenAI's GPT-5.3-Chat-Latest model support
|
||||
- Add OpenAI's GPT-5.3-Codex model support
|
||||
- Add OpenAI's GPT-5.4 model support
|
||||
- Add OpenAI's GPT-5.3-Codex model support (PR #11728 by @PeterDaveHello)
|
||||
- Warm Roo models on CLI startup for faster initial responses (PR #11722 by @cte)
|
||||
- Fix spelling/grammar and casing inconsistencies (#11478 by @PeterDaveHello, PR #11485 by @PeterDaveHello)
|
||||
- Fix: Restore Linear integration page (PR #11725 by @roomote)
|
||||
- Chore: Prepare CLI release v0.1.1 (PR #11723 by @cte)
|
||||
|
||||
## [3.50.4] - 2026-02-21
|
||||
|
||||
- Feat: Add MiniMax M2.5 model support (#11471 by @love8ko, PR #11458 by @roomote)
|
||||
|
||||
## [3.50.3] - 2026-02-20
|
||||
|
||||
- Fix: Correct Vertex AI claude-sonnet-4-6 model ID (#11625 by @yuvarajl, PR #11626 by @roomote)
|
||||
- Restore Unbound as a provider (PR #11624 by @pugazhendhi-m)
|
||||
|
||||
## [3.50.2] - 2026-02-20
|
||||
|
||||
- Fix: Inline terminal rendering parity with the VSCode Terminal (#10699 by @jerrill-johnson-bitwerx, PR #11361 by @RussellZager)
|
||||
- Fix: Enable prompt caching for Bedrock custom ARN and default to ON (#10846 by @wisestmumbler, PR #11373 by @roomote)
|
||||
- Feat: Add visual feedback to copy button in task actions (#11401 by @omagoduck, PR #11403 by @omagoduck)
|
||||
|
||||
## [3.50.1] - 2026-02-20
|
||||
|
||||
- Fix OpenAI Codex and OpenAI Native stream parsing for done-only and `content_part` events, including duplicate-text guards when deltas are already streamed.
|
||||
|
||||
## [3.50.0] - 2026-02-19
|
||||
|
||||
- Add Gemini 3.1 Pro support and set as default Gemini model (PR #11608 by @PeterDaveHello)
|
||||
- Add NDJSON stdin protocol, list subcommands, and modularize CLI run command (PR #11597 by @cte)
|
||||
- Prepare CLI v0.1.0 release (PR #11599 by @cte)
|
||||
- Remove integration tests (PR #11598 by @roomote)
|
||||
- Changeset version bump (PR #11596 by @github-actions)
|
||||
|
||||
## [3.49.0] - 2026-02-19
|
||||
|
||||
- Add file changes panel to track all file modifications per conversation (#11493 by @saneroen, PR #11494 by @saneroen)
|
||||
- Add per-workspace indexing opt-in and stop/cancel indexing controls (#11455 by @JamesRobert20, PR #11456 by @JamesRobert20)
|
||||
- Add per-task file-based history store for cross-instance safety (PR #11490 by @roomote)
|
||||
- Fix: Redesign rehydration scroll lifecycle for smoother chat experience (PR #11483 by @hannesrudolph)
|
||||
- Fix: Bump @roo-code/types metadata version to 1.111.0 after revert regression (PR #11588 by @roomote)
|
||||
|
||||
## [3.48.1] - 2026-02-18
|
||||
|
||||
- Fix: Await MCP server initialization before returning McpHub instance, preventing race conditions (PR #11518 by @daniel-lxs)
|
||||
- Fix: Correct Bedrock Claude Sonnet 4.6 model ID (#11509 by @PeterDaveHello, PR #11569 by @PeterDaveHello)
|
||||
- Add DeleteQueuedMessage IPC command for managing queued messages (PR #11464 by @roomote)
|
||||
|
||||
## [3.48.0] - 2026-02-17
|
||||
|
||||
- Add Anthropic Claude Sonnet 4.6 support across all providers — Anthropic, Bedrock, Vertex, OpenRouter, and Vercel AI Gateway (PR #11509 by @PeterDaveHello)
|
||||
- Add lock toggle to pin API config across all modes in a workspace (PR #11295 by @hannesrudolph)
|
||||
- Fix: Prevent parent task state loss during orchestrator delegation (PR #11281 by @hannesrudolph)
|
||||
- Fix: Resolve race condition in new_task delegation that loses parent task history (PR #11331 by @daniel-lxs)
|
||||
- Fix: Serialize taskHistory writes and fix delegation status overwrite race (PR #11335 by @hannesrudolph)
|
||||
- Fix: Prevent chat history loss during cloud/settings navigation (#11371 by @SannidhyaSah, PR #11372 by @SannidhyaSah)
|
||||
- Fix: Preserve condensation summary during task resume (#11487 by @SannidhyaSah, PR #11488 by @SannidhyaSah)
|
||||
- Fix: Resolve chat scroll anchoring and task-switch scroll race conditions (PR #11385 by @hannesrudolph)
|
||||
- Fix: Preserve pasted images in chatbox during chat activity (PR #11375 by @app/roomote)
|
||||
- Add disabledTools setting to globally disable native tools (PR #11277 by @daniel-lxs)
|
||||
- Rename search_and_replace tool to edit and unify edit-family UI (PR #11296 by @hannesrudolph)
|
||||
- Render nested subtasks as recursive tree in history view (PR #11299 by @hannesrudolph)
|
||||
- Remove 9 low-usage providers and add retired-provider UX (PR #11297 by @hannesrudolph)
|
||||
- Remove browser use functionality entirely (PR #11392 by @hannesrudolph)
|
||||
- Remove built-in skills and built-in skills mechanism (PR #11414 by @hannesrudolph)
|
||||
- Remove footgun prompting (file-based system prompt override) (PR #11387 by @hannesrudolph)
|
||||
- Batch consecutive tool calls in chat UI with shared utility (PR #11245 by @hannesrudolph)
|
||||
- Validate Gemini thinkingLevel against model capabilities and handle empty streams (PR #11303 by @hannesrudolph)
|
||||
- Add GLM-5 model support to Z.ai provider (PR #11440 by @app/roomote)
|
||||
- Fix: Prevent double notification sound playback (PR #11283 by @hannesrudolph)
|
||||
- Fix: Prevent false unsaved changes prompt with OpenAI Compatible headers (#8230 by @hannesrudolph, PR #11334 by @daniel-lxs)
|
||||
- Fix: Cancel backend auto-approval timeout when auto-approve is toggled off mid-countdown (PR #11439 by @SannidhyaSah)
|
||||
- Fix: Add follow_up param validation in AskFollowupQuestionTool (PR #11484 by @rossdonald)
|
||||
- Fix: Prevent webview postMessage crashes and make dispose idempotent (PR #11313 by @0xMink)
|
||||
- Fix: Avoid zsh process-substitution false positives in assignments (PR #11365 by @hannesrudolph)
|
||||
- Fix: Harden command auto-approval against inline JS false positives (PR #11382 by @hannesrudolph)
|
||||
- Fix: Make tab close best-effort in DiffViewProvider.open (PR #11363 by @0xMink)
|
||||
- Fix: Canonicalize core.worktree comparison to prevent Windows path mismatch failures (PR #11346 by @0xMink)
|
||||
- Fix: Make removeClineFromStack() delegation-aware to prevent orphaned parent tasks (PR #11302 by @app/roomote)
|
||||
- Fix task resumption in the API module (PR #11369 by @cte)
|
||||
- Make defaultTemperature required in getModelParams to prevent silent temperature overrides (PR #11218 by @app/roomote)
|
||||
- Remove noisy console.warn logs from NativeToolCallParser (PR #11264 by @daniel-lxs)
|
||||
- Consolidate getState calls in resolveWebviewView (PR #11320 by @0xMink)
|
||||
- Clean up repo-facing mode rules (PR #11410 by @hannesrudolph)
|
||||
- Implement ModelMessage storage layer with AI SDK response messages (PR #11409 by @daniel-lxs)
|
||||
- Extract translation and merge resolver modes into reusable skills (PR #11215 by @app/roomote)
|
||||
- Add blog section with initial posts to roocode.com (PR #11127 by @app/roomote)
|
||||
- Replace Roomote Control with Linear Integration in cloud features grid (PR #11280 by @app/roomote)
|
||||
- Add IPC query handlers for commands, modes, and models (PR #11279 by @cte)
|
||||
- Add stdin stream mode for the CLI (PR #11476 by @cte)
|
||||
- Make CLI auto-approve by default with require-approval opt-in (PR #11424 by @cte)
|
||||
- Update CLI default model from Opus 4.5 to Opus 4.6 (PR #11273 by @app/roomote)
|
||||
- Add linux-arm64 support for the Roo CLI (PR #11314 by @cte)
|
||||
- CLI release: v0.0.51 (PR #11274 by @cte)
|
||||
- CLI release: v0.0.52 (PR #11324 by @cte)
|
||||
- CLI release: v0.0.53 (PR #11425 by @cte)
|
||||
- CLI release: v0.0.54 (PR #11477 by @cte)
|
||||
|
||||
## [3.45.0] - 2026-01-27
|
||||
|
||||
|
|
@ -544,7 +667,7 @@
|
|||
- Refactor: Consolidate ThinkingBudget components and fix disable handling (PR #9930 by @hannesrudolph)
|
||||
- Forbid time estimates in architect mode for more focused planning (PR #9931 by @app/roomote)
|
||||
- Web: Add product pages (PR #9865 by @brunobergher)
|
||||
- Make eval runs deleteable in the web UI (PR #9909 by @mrubens)
|
||||
- Make eval runs deletable in the web UI (PR #9909 by @mrubens)
|
||||
- Feat: Change defaultToolProtocol default from xml to native (later reverted) (PR #9892 by @app/roomote)
|
||||
|
||||
## [3.36.2] - 2025-12-04
|
||||
|
|
@ -1151,7 +1274,6 @@
|
|||
- Reposition Add Image button inside ChatTextArea (thanks @roomote!)
|
||||
- Bring back a way to temporarily and globally pause auto-approve without losing your toggle state (thanks @brunobergher!)
|
||||
- Makes text area buttons appear only when there's text (thanks @brunobergher!)
|
||||
- CONTRIBUTING.md tweaks and issue template rewrite (thanks @hannesrudolph!)
|
||||
- Bump axios from 1.9.0 to 1.12.0 (thanks @dependabot!)
|
||||
|
||||
## [3.28.2] - 2025-09-14
|
||||
|
|
@ -1592,7 +1714,7 @@
|
|||
- Add: Mistral embedding provider (thanks @SannidhyaSah!)
|
||||
- Fix: add run parameter to vitest command in rules (thanks @KJ7LNW!)
|
||||
- Update: the max_tokens fallback logic in the sliding window
|
||||
- Fix: Bedrock and Vertext token counting improvements (thanks @daniel-lxs!)
|
||||
- Fix: Bedrock and Vertex token counting improvements (thanks @daniel-lxs!)
|
||||
- Add: llama-4-maverick model to Vertex AI provider (thanks @MuriloFP!)
|
||||
- Fix: properly distinguish between user cancellations and API failures
|
||||
- Fix: add case sensitivity mention to suggested fixes in apply_diff error message
|
||||
|
|
@ -1611,7 +1733,6 @@
|
|||
- Fix Claude model detection by name for API protocol selection (thanks @daniel-lxs!)
|
||||
- Move marketplace icon from overflow menu to top navigation
|
||||
- Optional setting to prevent completion with open todos
|
||||
- Added YouTube to website footer (thanks @thill2323!)
|
||||
|
||||
## [3.23.14] - 2025-07-17
|
||||
|
||||
|
|
@ -1902,7 +2023,7 @@
|
|||
- Sync BatchDiffApproval styling with BatchFilePermission for UI consistency (thanks @samhvw8!)
|
||||
- Add max height constraint to MCP execution response for better UX (thanks @samhvw8!)
|
||||
- Prevent MCP 'installed' label from being squeezed #4630 (thanks @daniel-lxs!)
|
||||
- Allow a lower context condesning threshold (thanks @SECKainersdorfer!)
|
||||
- Allow a lower context condensing threshold (thanks @SECKainersdorfer!)
|
||||
- Avoid type system duplication for cleaner codebase (thanks @EamonNerbonne!)
|
||||
|
||||
## [3.20.1] - 2025-06-12
|
||||
|
|
@ -2005,7 +2126,6 @@
|
|||
- Fix bug with context condensing in Amazon Bedrock
|
||||
- Fix UTF-8 encoding in ExecaTerminalProcess (thanks @mr-ryan-james!)
|
||||
- Set sidebar name bugfix (thanks @chrarnoldus!)
|
||||
- Fix link to CONTRIBUTING.md in feature request template (thanks @cannuri!)
|
||||
- Add task metadata to Unbound and improve caching logic (thanks @pugazhendhi-m!)
|
||||
|
||||
## [3.19.0] - 2025-05-29
|
||||
|
|
@ -2059,7 +2179,7 @@
|
|||
|
||||
## [3.18.2] - 2025-05-23
|
||||
|
||||
- Fix vscode-material-icons in the filer picker
|
||||
- Fix vscode-material-icons in the file picker
|
||||
- Fix global settings export
|
||||
- Respect user-configured terminal integration timeout (thanks @KJ7LNW)
|
||||
- Context condensing enhancements (thanks @SannidhyaSah)
|
||||
|
|
@ -2177,7 +2297,7 @@
|
|||
- Add vertical tab navigation to the settings (thanks @dlab-anton)
|
||||
- Add Groq and Chutes API providers (thanks @shariqriazz)
|
||||
- Clickable code references in code block (thanks @KJ7LNW)
|
||||
- Improve accessibility of ato-approve toggles (thanks @Deon588)
|
||||
- Improve accessibility of auto-approve toggles (thanks @Deon588)
|
||||
- Requesty provider fixes (thanks @dtrugman)
|
||||
- Fix migration and persistence of per-mode API profiles (thanks @alasano)
|
||||
- Fix usage of `path.basename` in the extension webview (thanks @samhvw8)
|
||||
|
|
@ -2239,7 +2359,7 @@
|
|||
- Fix file mentions for filenames containing spaces
|
||||
- Improve the auto-approve toggle buttons for some high-contrast VSCode themes
|
||||
- Offload expensive count token operations to a web worker (thanks @samhvw8)
|
||||
- Improve support for mult-root workspaces (thanks @snoyiatk)
|
||||
- Improve support for multi-root workspaces (thanks @snoyiatk)
|
||||
- Simplify and streamline Roo Code's quick actions
|
||||
- Allow Roo Code settings to be imported from the welcome screen (thanks @julionav)
|
||||
- Remove unused types (thanks @wkordalski)
|
||||
|
|
@ -2645,7 +2765,7 @@
|
|||
- Custom ARNs in Amazon Bedrock (thanks @Smartsheet-JB-Brown!)
|
||||
- Update MCP servers directory path for platform compatibility (thanks @hannesrudolph!)
|
||||
- Fix browser system prompt inclusion rules (thanks @cannuri!)
|
||||
- Publish git tags to github from CI (thanks @pdecat!)
|
||||
- Publish git tags to GitHub from CI (thanks @pdecat!)
|
||||
- Fixes to OpenAI-style cost calculations (thanks @dtrugman!)
|
||||
- Fix to allow using an excluded directory as your working directory (thanks @Szpadel!)
|
||||
- Kotlin language support in list_code_definition_names tool (thanks @kohii!)
|
||||
|
|
@ -2750,7 +2870,7 @@
|
|||
|
||||
## [3.7.6] - 2025-02-26
|
||||
|
||||
- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!)
|
||||
- Handle really long text better in the ChatRow similar to TaskHeader (thanks @joemanley201!)
|
||||
- Support multiple files in drag-and-drop
|
||||
- Truncate search_file output to avoid crashing the extension
|
||||
- Better OpenRouter error handling (no more "Provider Error")
|
||||
|
|
@ -2973,7 +3093,6 @@
|
|||
- Ask and Architect modes can now edit markdown files
|
||||
- Custom modes can now be restricted to specific file patterns (for example, a technical writer who can only edit markdown files 👋)
|
||||
- Support for configuring the Bedrock provider with AWS Profiles
|
||||
- New Roo Code community Discord at https://roocode.com/discord!
|
||||
|
||||
## [3.2.8]
|
||||
|
||||
|
|
@ -3013,8 +3132,6 @@
|
|||
- Create specialized assistants for any workflow
|
||||
- Just type "Create a new mode for <X>" or visit the Prompts tab in the top menu to get started
|
||||
|
||||
Join us at https://www.reddit.com/r/RooCode to share your custom modes and be part of our next chapter!
|
||||
|
||||
## [3.1.7]
|
||||
|
||||
- DeepSeek-R1 support (thanks @philipnext!)
|
||||
|
|
@ -3062,12 +3179,8 @@ Join us at https://www.reddit.com/r/RooCode to share your custom modes and be pa
|
|||
|
||||
## [3.0.1]
|
||||
|
||||
- Fix the reddit link and a small visual glitch in the chat input
|
||||
|
||||
## [3.0.0]
|
||||
|
||||
- This release adds chat modes! Now you can ask Roo Code questions about system architecture or the codebase without immediately jumping into writing code. You can even assign different API configuration profiles to each mode if you prefer to use different models for thinking vs coding. Would love feedback in the new Roo Code Reddit! https://www.reddit.com/r/RooCode
|
||||
|
||||
## [2.2.46]
|
||||
|
||||
- Only parse @-mentions in user input (not in files)
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
<div align="center">
|
||||
<sub>
|
||||
|
||||
<b>English</b> • [Català](locales/ca/CODE_OF_CONDUCT.md) • [Deutsch](locales/de/CODE_OF_CONDUCT.md) • [Español](locales/es/CODE_OF_CONDUCT.md) • [Français](locales/fr/CODE_OF_CONDUCT.md) • [हिंदी](locales/hi/CODE_OF_CONDUCT.md) • [Bahasa Indonesia](locales/id/CODE_OF_CONDUCT.md) • [Italiano](locales/it/CODE_OF_CONDUCT.md) • [日本語](locales/ja/CODE_OF_CONDUCT.md)
|
||||
|
||||
</sub>
|
||||
<sub>
|
||||
|
||||
[한국어](locales/ko/CODE_OF_CONDUCT.md) • [Nederlands](locales/nl/CODE_OF_CONDUCT.md) • [Polski](locales/pl/CODE_OF_CONDUCT.md) • [Português (BR)](locales/pt-BR/CODE_OF_CONDUCT.md) • [Русский](locales/ru/CODE_OF_CONDUCT.md) • [Türkçe](locales/tr/CODE_OF_CONDUCT.md) • [Tiếng Việt](locales/vi/CODE_OF_CONDUCT.md) • [简体中文](locales/zh-CN/CODE_OF_CONDUCT.md) • [繁體中文](locales/zh-TW/CODE_OF_CONDUCT.md)
|
||||
|
||||
</sub>
|
||||
</div>
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at support@roocode.com. All complaints
|
||||
will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from [Cline's version][cline_coc] of the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[cline_coc]: https://github.com/cline/cline/blob/main/CODE_OF_CONDUCT.md
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
141
CONTRIBUTING.md
141
CONTRIBUTING.md
|
|
@ -1,141 +0,0 @@
|
|||
<div align="center">
|
||||
<sub>
|
||||
|
||||
<b>English</b> • [Català](locales/ca/CONTRIBUTING.md) • [Deutsch](locales/de/CONTRIBUTING.md) • [Español](locales/es/CONTRIBUTING.md) • [Français](locales/fr/CONTRIBUTING.md) • [हिंदी](locales/hi/CONTRIBUTING.md) • [Bahasa Indonesia](locales/id/CONTRIBUTING.md) • [Italiano](locales/it/CONTRIBUTING.md) • [日本語](locales/ja/CONTRIBUTING.md)
|
||||
|
||||
</sub>
|
||||
<sub>
|
||||
|
||||
[한국어](locales/ko/CONTRIBUTING.md) • [Nederlands](locales/nl/CONTRIBUTING.md) • [Polski](locales/pl/CONTRIBUTING.md) • [Português (BR)](locales/pt-BR/CONTRIBUTING.md) • [Русский](locales/ru/CONTRIBUTING.md) • [Türkçe](locales/tr/CONTRIBUTING.md) • [Tiếng Việt](locales/vi/CONTRIBUTING.md) • [简体中文](locales/zh-CN/CONTRIBUTING.md) • [繁體中文](locales/zh-TW/CONTRIBUTING.md)
|
||||
|
||||
</sub>
|
||||
</div>
|
||||
|
||||
# Contributing to Roo Code
|
||||
|
||||
Roo Code is a community-driven project, and we deeply value every contribution. To streamline collaboration, we operate on an [Issue-First](#issue-first-approach) basis, meaning all [Pull Requests (PRs)](#submitting-a-pull-request) must first be linked to a GitHub Issue. Please review this guide carefully.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Before You Contribute](#before-you-contribute)
|
||||
- [Finding & Planning Your Contribution](#finding--planning-your-contribution)
|
||||
- [Development & Submission Process](#development--submission-process)
|
||||
- [Legal](#legal)
|
||||
|
||||
## Before You Contribute
|
||||
|
||||
### 1. Code of Conduct
|
||||
|
||||
All contributors must adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
|
||||
### 2. Project Roadmap
|
||||
|
||||
Our roadmap guides the project's direction. Align your contributions with these key goals:
|
||||
|
||||
### Reliability First
|
||||
|
||||
- Ensure diff editing and command execution are consistently reliable.
|
||||
- Reduce friction points that deter regular usage.
|
||||
- Guarantee smooth operation across all locales and platforms.
|
||||
- Expand robust support for a wide variety of AI providers and models.
|
||||
|
||||
### Enhanced User Experience
|
||||
|
||||
- Streamline the UI/UX for clarity and intuitiveness.
|
||||
- Continuously improve the workflow to meet the high expectations developers have for daily-use tools.
|
||||
|
||||
### Leading on Agent Performance
|
||||
|
||||
- Establish comprehensive evaluation benchmarks (evals) to measure real-world productivity.
|
||||
- Make it easy for everyone to easily run and interpret these evals.
|
||||
- Ship improvements that demonstrate clear increases in eval scores.
|
||||
|
||||
Mention alignment with these areas in your PRs.
|
||||
|
||||
### 3. Join the Roo Code Community
|
||||
|
||||
- **Primary:** Join our [Discord](https://discord.gg/roocode) and DM **Hannes Rudolph (`hrudolph`)**.
|
||||
- **Alternative:** Experienced contributors can engage directly via [GitHub Projects](https://github.com/orgs/RooCodeInc/projects/1).
|
||||
|
||||
## Finding & Planning Your Contribution
|
||||
|
||||
### Types of Contributions
|
||||
|
||||
- **Bug Fixes:** Addressing code issues.
|
||||
- **New Features:** Adding functionality.
|
||||
- **Documentation:** Improving guides and clarity.
|
||||
|
||||
### Issue-First Approach
|
||||
|
||||
All contributions start with a GitHub Issue using our skinny templates.
|
||||
|
||||
- **Check existing issues**: Search [GitHub Issues](https://github.com/RooCodeInc/Roo-Code/issues).
|
||||
- **Create an issue** using:
|
||||
- **Enhancements:** "Enhancement Request" template (plain language focused on user benefit).
|
||||
- **Bugs:** "Bug Report" template (minimal repro + expected vs actual + version).
|
||||
- **Want to work on it?** Comment "Claiming" on the issue and DM **Hannes Rudolph (`hrudolph`)** on [Discord](https://discord.gg/roocode) to get assigned. Assignment will be confirmed in the thread.
|
||||
- **PRs must link to the issue.** Unlinked PRs may be closed.
|
||||
|
||||
### Deciding What to Work On
|
||||
|
||||
- Check the [GitHub Project](https://github.com/orgs/RooCodeInc/projects/1) for "Issue [Unassigned]" issues.
|
||||
- For docs, visit [Roo Code Docs](https://github.com/RooCodeInc/Roo-Code-Docs).
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
- Check for existing reports first.
|
||||
- Create a new bug using the ["Bug Report" template](https://github.com/RooCodeInc/Roo-Code/issues/new/choose) with:
|
||||
- Clear, numbered reproduction steps
|
||||
- Expected vs actual result
|
||||
- Roo Code version (required); API provider/model if relevant
|
||||
- **Security issues**: Report privately via [security advisories](https://github.com/RooCodeInc/Roo-Code/security/advisories/new).
|
||||
|
||||
## Development & Submission Process
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. **Fork & Clone:**
|
||||
|
||||
```
|
||||
git clone https://github.com/YOUR_USERNAME/Roo-Code.git
|
||||
```
|
||||
|
||||
2. **Install Dependencies:**
|
||||
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Debugging:** Open with VS Code (`F5`).
|
||||
|
||||
### Writing Code Guidelines
|
||||
|
||||
- One focused PR per feature or fix.
|
||||
- Follow ESLint and TypeScript best practices.
|
||||
- Write clear, descriptive commits referencing issues (e.g., `Fixes #123`).
|
||||
- Provide thorough testing (`npm test`).
|
||||
- Rebase onto the latest `main` branch before submission.
|
||||
|
||||
### Submitting a Pull Request
|
||||
|
||||
- Begin as a **Draft PR** if seeking early feedback.
|
||||
- Clearly describe your changes following the Pull Request Template.
|
||||
- Link the issue in the PR description/title (e.g., "Fixes #123").
|
||||
- Provide screenshots/videos for UI changes.
|
||||
- Indicate if documentation updates are necessary.
|
||||
|
||||
### Pull Request Policy
|
||||
|
||||
- Must reference an assigned GitHub Issue. To get assigned: comment "Claiming" on the issue and DM **Hannes Rudolph (`hrudolph`)** on [Discord](https://discord.gg/roocode). Assignment will be confirmed in the thread.
|
||||
- Unlinked PRs may be closed.
|
||||
- PRs should pass CI tests, align with the roadmap, and have clear documentation.
|
||||
|
||||
### Review Process
|
||||
|
||||
- **Daily Triage:** Quick checks by maintainers.
|
||||
- **Weekly In-depth Review:** Comprehensive assessment.
|
||||
- **Iterate promptly** based on feedback.
|
||||
|
||||
## Legal
|
||||
|
||||
By contributing, you agree your contributions will be licensed under the Apache 2.0 License, consistent with Roo Code's licensing.
|
||||
|
|
@ -6,12 +6,11 @@ Roo Code respects your privacy and is committed to transparency about how we han
|
|||
|
||||
### **Where Your Data Goes (And Where It Doesn’t)**
|
||||
|
||||
- **Code & Files**: Roo Code accesses files on your local machine when needed for AI-assisted features. When you send commands to Roo Code, relevant files may be transmitted to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. If you select Roo Code Cloud as the model provider (proxy mode), your code may transit Roo Code servers only to forward it to the upstream provider. We do not store your code; it is deleted immediately after forwarding. Otherwise, your code is sent directly to the provider. AI providers may store data per their privacy policies.
|
||||
- **Code & Files**: Roo Code accesses files on your local machine when needed for AI-assisted features. When you send commands to Roo Code, relevant files may be transmitted to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. AI providers may store data per their privacy policies.
|
||||
- **Commands**: Any commands executed through Roo Code happen on your local environment. However, when you use AI-powered features, the relevant code and context from your commands may be transmitted to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not have access to or store this data, but AI providers may process it per their privacy policies.
|
||||
- **Prompts & AI Requests**: When you use AI-powered features, your prompts and relevant project context are sent to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not store or process this data. These AI providers have their own privacy policies and may store data per their terms of service. If you choose Roo Code Cloud as the provider (proxy mode), prompts may transit Roo Code servers only to forward them to the upstream model and are not stored.
|
||||
- **Prompts & AI Requests**: When you use AI-powered features, your prompts and relevant project context are sent to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not store or process this data. These AI providers have their own privacy policies and may store data per their terms of service.
|
||||
- **API Keys & Credentials**: If you enter an API key (e.g., to connect an AI model), it is stored locally on your device and never sent to us or any third party, except the provider you have chosen.
|
||||
- **Telemetry (Usage Data)**: We collect anonymous feature usage and error data to help us improve Roo Code. This telemetry is powered by PostHog and includes your VS Code machine ID, feature usage patterns, and exception reports. This telemetry does **not** collect personally identifiable information, your code, or AI prompts. You can opt out of this telemetry at any time through the settings.
|
||||
- **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Roo Code makes a secure API call to Roo Code's backend servers to retrieve listing information. These requests send only the query parameters (e.g., extension version, search term) necessary to fulfill the request and do not include your code, prompts, or personally identifiable information.
|
||||
|
||||
### **How We Use Your Data (If Collected)**
|
||||
|
||||
|
|
|
|||
119
README.md
119
README.md
|
|
@ -1,12 +1,5 @@
|
|||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
|
||||
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
|
||||
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
<a href="https://www.reddit.com/r/RooCode/"><img src="https://img.shields.io/badge/Join%20r%2FRooCode-FF4500?style=flat&logo=reddit&logoColor=white" alt="Join r/RooCode"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<em>Get help fast → <a href="https://discord.gg/roocode">Join Discord</a> • Prefer async? → <a href="https://www.reddit.com/r/RooCode/">Join r/RooCode</a></em>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
</p>
|
||||
|
||||
# Roo Code
|
||||
|
|
@ -35,7 +28,7 @@
|
|||
- [简体中文](locales/zh-CN/README.md)
|
||||
- [繁體中文](locales/zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -58,119 +51,27 @@ Roo Code adapts to how you work:
|
|||
- Ask Mode: fast answers, explanations, and docs
|
||||
- Debug Mode: trace issues, add logs, isolate root causes
|
||||
- Custom Modes: build specialized modes for your team or workflow
|
||||
- Roomote Control: Roomote Control lets you remotely control tasks running in your local VS Code instance.
|
||||
|
||||
Learn more: [Using Modes](https://docs.roocode.com/basic-usage/using-modes) • [Custom Modes](https://docs.roocode.com/advanced-usage/custom-modes) • [Roomote Control](https://docs.roocode.com/roo-code-cloud/roomote-control)
|
||||
|
||||
## Tutorial & Feature Videos
|
||||
|
||||
<div align="center">
|
||||
|
||||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installing Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configuring Profiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase Indexing</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Custom Modes</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Context Management</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
<a href="https://docs.roocode.com/tutorial-videos">More quick tutorial and feature videos...</a>
|
||||
</p>
|
||||
Learn more: [Using Modes](https://roocodeinc.github.io/Roo-Code/basic-usage/using-modes) • [Custom Modes](https://roocodeinc.github.io/Roo-Code/advanced-usage/custom-modes)
|
||||
|
||||
## Resources
|
||||
|
||||
- **[Documentation](https://docs.roocode.com):** The official guide to installing, configuring, and mastering Roo Code.
|
||||
- **[YouTube Channel](https://youtube.com/@roocodeyt?feature=shared):** Watch tutorials and see features in action.
|
||||
- **[Discord Server](https://discord.gg/roocode):** Join the community for real-time help and discussion.
|
||||
- **[Reddit Community](https://www.reddit.com/r/RooCode):** Share your experiences and see what others are building.
|
||||
- **[Documentation](https://roocodeinc.github.io/Roo-Code/):** The official guide to installing, configuring, and mastering Roo Code.
|
||||
- **[GitHub Issues](https://github.com/RooCodeInc/Roo-Code/issues):** Report bugs and track development.
|
||||
- **[Feature Requests](https://github.com/RooCodeInc/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop):** Have an idea? Share it with the developers.
|
||||
|
||||
---
|
||||
|
||||
## Local Setup & Development
|
||||
|
||||
1. **Clone** the repo:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/RooCodeInc/Roo-Code.git
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Run the extension**:
|
||||
|
||||
There are several ways to run the Roo Code extension:
|
||||
|
||||
### Development Mode (F5)
|
||||
|
||||
For active development, use VSCode's built-in debugging:
|
||||
|
||||
Press `F5` (or go to **Run** → **Start Debugging**) in VSCode. This will open a new VSCode window with the Roo Code extension running.
|
||||
|
||||
- Changes to the webview will appear immediately.
|
||||
- Changes to the core extension will also hot reload automatically.
|
||||
|
||||
### Automated VSIX Installation
|
||||
|
||||
To build and install the extension as a VSIX package directly into VSCode:
|
||||
|
||||
```sh
|
||||
pnpm install:vsix [-y] [--editor=<command>]
|
||||
```
|
||||
|
||||
This command will:
|
||||
|
||||
- Ask which editor command to use (code/cursor/code-insiders) - defaults to 'code'
|
||||
- Uninstall any existing version of the extension.
|
||||
- Build the latest VSIX package.
|
||||
- Install the newly built VSIX.
|
||||
- Prompt you to restart VS Code for changes to take effect.
|
||||
|
||||
Options:
|
||||
|
||||
- `-y`: Skip all confirmation prompts and use defaults
|
||||
- `--editor=<command>`: Specify the editor command (e.g., `--editor=cursor` or `--editor=code-insiders`)
|
||||
|
||||
### Manual VSIX Installation
|
||||
|
||||
If you prefer to install the VSIX package manually:
|
||||
|
||||
1. First, build the VSIX package:
|
||||
```sh
|
||||
pnpm vsix
|
||||
```
|
||||
2. A `.vsix` file will be generated in the `bin/` directory (e.g., `bin/roo-cline-<version>.vsix`).
|
||||
3. Install it manually using the VSCode CLI:
|
||||
```sh
|
||||
code --install-extension bin/roo-cline-<version>.vsix
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
We use [changesets](https://github.com/changesets/changesets) for versioning and publishing. Check our `CHANGELOG.md` for release notes.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
The Roo Code Extension was shut down on May 15th.
|
||||
|
||||
- If you're looking for an alternative, check out [ZooCode](https://github.com/Zoo-Code-Org/Zoo-Code/) (a fork started by the Roo Code community) and [Cline](https://cline.bot/) (from where Roo Code originated).
|
||||
- If you were a paying user and have billing questions, please write [billing@roocode.com](mailto:billing@roocode.com).
|
||||
|
||||
**Please note** that Roo Code, Inc does **not** make any representations or warranties regarding any code, models, or other tools provided or made available in connection with Roo Code, any associated third-party tools, or any resulting outputs. You assume **all risks** associated with the use of any such tools or outputs; such tools are provided on an **"AS IS"** and **"AS AVAILABLE"** basis. Such risks may include, without limitation, intellectual property infringement, cyber vulnerabilities or attacks, bias, inaccuracies, errors, defects, viruses, downtime, property loss or damage, and/or personal injury. You are solely responsible for your use of any such tools or outputs (including, without limitation, the legality, appropriateness, and results thereof).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
We love community contributions! Get started by reading our [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](./LICENSE)
|
||||
|
||||
---
|
||||
|
||||
**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!
|
||||
[Apache 2.0 © 2026 Roo Code, Inc.](./LICENSE)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,223 @@ All notable changes to the `@roo-code/cli` package will be documented in this fi
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.17] - 2026-03-04
|
||||
|
||||
### Added
|
||||
|
||||
- **Custom Session ID Support**: New `--create-with-session-id` flag allows specifying a custom UUID session ID when creating tasks. Session IDs are now validated as UUIDs for both create and resume operations, as well as for `start.taskId` in stdin-stream mode.
|
||||
|
||||
### Tests
|
||||
|
||||
- Added integration coverage for create+resume loading the correct session.
|
||||
|
||||
## [0.1.16] - 2026-03-04
|
||||
|
||||
### Added
|
||||
|
||||
- **Custom Shell Selection**: New `--terminal-shell` flag to specify which shell to use for inline command execution. The shell path is validated at the CLI layer and passed through the standard settings mechanism.
|
||||
|
||||
### Tests
|
||||
|
||||
- Added integration coverage for stdin stream routing and race invariants.
|
||||
|
||||
## [0.1.15] - 2026-03-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Follow-up Routing for Completion Asks**: Fixed routing of follow-up messages when the agent asks for clarification (ask_followup_question) in stdin-stream mode. Messages sent after a completion ask are now correctly delivered to the agent instead of being queued.
|
||||
|
||||
## [0.1.14] - 2026-03-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Command Output Streaming**: Ensure full command output is streamed before the done event is emitted, preventing truncated output in stdin-stream mode.
|
||||
|
||||
## [0.1.13] - 2026-03-02
|
||||
|
||||
### Added
|
||||
|
||||
- **Skills as Slash Commands**: Skills are now exposed as slash commands, so you can invoke skill workflows directly from command-style input.
|
||||
- **Skill Fallback Execution**: When a slash command does not match a command file but matches a skill slug, the CLI can resolve and execute that skill path.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Slash Command Resolution Priority**: Command precedence is preserved, with skill fallback only used when no matching slash command is found.
|
||||
|
||||
### Tests
|
||||
|
||||
- Added and updated tests for slash command + skill fallback behavior, including command precedence and duplicate skill-slug handling.
|
||||
|
||||
## [0.1.12] - 2026-03-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Command Timeout Handling**: CLI runtime now correctly ignores model-provided background timeouts for commands, ensuring command lifetime is governed solely by the `--timeout` setting.
|
||||
|
||||
## [0.1.11] - 2026-03-02
|
||||
|
||||
### Added
|
||||
|
||||
- **Image Support in Stdin Stream**: The `start` and `message` commands in stdin-stream mode now support an optional `images` field (array of base64 data URIs) to attach images to prompts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Upgrade Version Detection**: Fixed version detection in the `upgrade` command to correctly identify when updates are available.
|
||||
|
||||
## [0.1.10] - 2026-03-02
|
||||
|
||||
### Added
|
||||
|
||||
- **Command Exit Code in Events**: The `tool_result` event for command executions now includes an `exitCode` field, allowing CLI consumers to programmatically distinguish between successful and failed command executions without parsing output text.
|
||||
|
||||
## [0.1.9] - 2026-03-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stdin Stream Cancel Race**: Fixed a race condition during startup cancellation in stdin-stream mode that could cause unexpected behavior when canceling tasks immediately after starting them.
|
||||
|
||||
### Tests
|
||||
|
||||
- **Integration Test Suite**: Added comprehensive integration test suite for stdin-stream protocol covering cancel, followup, multi-message queue, and shutdown scenarios.
|
||||
|
||||
## [0.1.8] - 2026-03-02
|
||||
|
||||
### Changed
|
||||
|
||||
- **Command Execution Timeout**: Increased timeout for command execution to improve reliability for long-running operations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stdin Stream Queue Handling**: Fixed stdin stream queued messages and command output streaming to ensure messages are properly processed.
|
||||
|
||||
## [0.1.7] - 2026-03-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stdin Stream Control Flow**: Gracefully handle control-flow errors in stdin-stream mode to prevent unexpected crashes during cancellation and shutdown sequences.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Type Definitions**: Refactored and simplified JSON event type definitions for better type safety.
|
||||
|
||||
## [0.1.6] - 2026-02-27
|
||||
|
||||
### Added
|
||||
|
||||
- **Consecutive Mistake Limit**: New `--mistake-limit` flag to configure the maximum number of consecutive mistakes before the agent pauses for intervention.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Workspace-Scoped Sessions**: The `list sessions` command and `--resume` flag now only show and resume sessions from the current workspace directory.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Task Configuration Forwarding**: Task configuration (custom modes, disabled tools, etc.) passed via the stdin-prompt-stream protocol is now correctly forwarded to the extension host instead of being silently dropped.
|
||||
- **Stream Error Recovery**: Improved recovery from streaming errors to prevent task interruption.
|
||||
|
||||
## [0.1.5] - 2026-02-26
|
||||
|
||||
### Added
|
||||
|
||||
- **Session History**: New `list sessions` subcommand to view recent CLI sessions with task IDs, timestamps, and initial prompts.
|
||||
- **Session Resume**: New `--resume <taskId>` flag to continue a previous session from where it left off.
|
||||
- **Upgrade Command**: New `upgrade` command to check for and install the latest CLI version.
|
||||
|
||||
## [0.1.4] - 2026-02-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Exception Handling**: Improved recovery from unhandled exceptions in the CLI to prevent unexpected crashes.
|
||||
|
||||
## [0.1.3] - 2026-02-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Task Resumption**: Fixed an issue where resuming a previously suspended task could fail due to state initialization timing in the extension host.
|
||||
|
||||
## [0.1.2] - 2026-02-25
|
||||
|
||||
### Changed
|
||||
|
||||
- **Streaming Deltas**: Tool use ask messages (command, tool, mcp) are now streamed as structured deltas instead of full snapshots in json-event-emitter for improved efficiency.
|
||||
- **Task ID Propagation**: Task ID is now generated upfront and propagated through runTask/createTask so currentTaskId is available in extension state immediately.
|
||||
- **Custom Tools**: Enabled customTools experiment in extension host.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Cancel Recovery**: Wait for resumable state after cancel before processing follow-up messages to prevent race conditions in stdin-stream.
|
||||
- **Custom Tool Schema**: Provide valid empty JSON Schema for custom tools without parameters to fix strict-mode API validation.
|
||||
- **Path Handling**: Skip paths outside cwd in RooProtectedController to avoid RangeError.
|
||||
- **Retry Handling**: Silently handle abort during exponential backoff retry countdown.
|
||||
- Fixed spelling/grammar and casing inconsistencies.
|
||||
|
||||
### Added
|
||||
|
||||
- **Telemetry Control**: Added `ROO_CODE_DISABLE_TELEMETRY=1` environment variable to disable cloud telemetry.
|
||||
|
||||
## [0.1.1] - 2026-02-24
|
||||
|
||||
### Added
|
||||
|
||||
- **Roo Model Warmup**: When configured with the Roo provider, the CLI now proactively fetches and warms the model list during activation so that model information is available before the first prompt is sent. The warmup has a 10s timeout and failures are logged only in debug mode.
|
||||
- **Unbound Provider**: Added Unbound as an available provider option.
|
||||
|
||||
## [0.1.0] - 2026-02-19
|
||||
|
||||
### Added
|
||||
|
||||
- **NDJSON Stdin Protocol**: Overhauled the stdin prompt stream from raw text lines to a structured NDJSON command protocol (`start`/`message`/`cancel`/`ping`/`shutdown`) with requestId correlation, ack/done/error lifecycle events, and queue telemetry. See [`stdin-stream.ts`](src/ui/stdin-stream.ts) for implementation.
|
||||
- **List Subcommands**: New `list` subcommands (`commands`, `modes`, `models`) for programmatic discovery of available CLI capabilities.
|
||||
- **Shared Utilities**: Added `isRecord` guard utility for improved type safety.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Modularized Architecture**: Extracted stdin stream logic from `run.ts` into dedicated [`stdin-stream.ts`](src/ui/stdin-stream.ts) module for better code organization and maintainability.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a bug in `Task.ts` affecting CLI operation.
|
||||
|
||||
## [0.0.55] - 2026-02-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stdin Stream Mode**: Fixed issue where new tasks were incorrectly being created in stdin-prompt-stream mode. The mode now properly reuses the existing task for subsequent prompts instead of creating new tasks.
|
||||
|
||||
## [0.0.54] - 2026-02-15
|
||||
|
||||
### Added
|
||||
|
||||
- **Stdin Stream Mode**: New `stdin-prompt-stream` mode that reads prompts from stdin, allowing batch processing and piping multiple tasks. Each line of stdin is processed as a separate prompt with streaming JSON output. See [`stdin-prompt-stream.ts`](src/ui/stdin-prompt-stream.ts) for implementation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed JSON emitter state not being cleared between tasks in stdin-prompt-stream mode
|
||||
- Fixed inconsistent user role for prompt echo partials in stream-json mode
|
||||
|
||||
## [0.0.53] - 2026-02-12
|
||||
|
||||
### Changed
|
||||
|
||||
- **Auto-Approve by Default**: The CLI now auto-approves all actions (tools, commands, browser, MCP) by default. Followup questions auto-select the first suggestion after a 60-second timeout.
|
||||
- **New `--require-approval` Flag**: Replaced `-y`/`--yes`/`--dangerously-skip-permissions` flags with a new `-a, --require-approval` flag for users who want manual approval prompts before actions execute.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Spamming the escape key to cancel a running task no longer crashes the cli.
|
||||
|
||||
## [0.0.52] - 2026-02-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Linux Support**: Added support for `linux-arm64`.
|
||||
|
||||
## [0.0.51] - 2026-02-06
|
||||
|
||||
### Changed
|
||||
|
||||
- **Default Model Update**: Changed the default model from Opus 4.5 to Opus 4.6 for improved performance and capabilities
|
||||
|
||||
## [0.0.50] - 2026-02-05
|
||||
|
||||
### Added
|
||||
|
|
@ -55,7 +272,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- Skip onboarding flow when a provider is explicitly specified via `--provider` flag or saved in settings
|
||||
- Unified permission flags: Combined `-y`, `--yes`, and `--dangerously-skip-permissions` into a single option for Claude Code-like CLI compatibility
|
||||
- Unified permission flags: Combined approval-skipping flags into a single option for Claude Code-like CLI compatibility
|
||||
- Improved Roo Code Router authentication flow and error messaging
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -41,32 +41,23 @@ Re-run the install script to update to the latest version:
|
|||
curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
```
|
||||
|
||||
Or run:
|
||||
|
||||
```bash
|
||||
roo upgrade
|
||||
```
|
||||
|
||||
### Uninstalling
|
||||
|
||||
```bash
|
||||
rm -rf ~/.roo/cli ~/.local/bin/roo
|
||||
```
|
||||
|
||||
### Development Installation
|
||||
|
||||
For contributing or development:
|
||||
|
||||
```bash
|
||||
# From the monorepo root.
|
||||
pnpm install
|
||||
|
||||
# Build the main extension first.
|
||||
pnpm --filter roo-cline bundle
|
||||
|
||||
# Build the cli.
|
||||
pnpm --filter @roo-code/cli build
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode (Default)
|
||||
|
||||
By default, the CLI prompts for approval before executing actions:
|
||||
By default, the CLI auto-approves actions and runs in interactive TUI mode:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
|
@ -82,96 +73,71 @@ roo -w ~/Documents/my-project
|
|||
|
||||
In interactive mode:
|
||||
|
||||
- Tool executions prompt for yes/no approval
|
||||
- Commands prompt for yes/no approval
|
||||
- Followup questions show suggestions and wait for user input
|
||||
- Browser and MCP actions prompt for approval
|
||||
- Tool executions are auto-approved
|
||||
- Commands are auto-approved
|
||||
- Followup questions show suggestions with a 60-second timeout, then auto-select the first suggestion
|
||||
- Browser and MCP actions are auto-approved
|
||||
|
||||
### Non-Interactive Mode (`-y`)
|
||||
### Approval-Required Mode (`--require-approval`)
|
||||
|
||||
For automation and scripts, use `-y` to auto-approve all actions:
|
||||
If you want manual approval prompts, enable approval-required mode:
|
||||
|
||||
```bash
|
||||
roo "Refactor the utils.ts file" -y -w ~/Documents/my-project
|
||||
roo "Refactor the utils.ts file" --require-approval -w ~/Documents/my-project
|
||||
```
|
||||
|
||||
In non-interactive mode:
|
||||
In approval-required mode:
|
||||
|
||||
- Tool, command, browser, and MCP actions are auto-approved
|
||||
- Followup questions show a 60-second timeout, then auto-select the first suggestion
|
||||
- Typing any key cancels the timeout and allows manual input
|
||||
- Tool, command, browser, and MCP actions prompt for yes/no approval
|
||||
- Followup questions wait for manual input (no auto-timeout)
|
||||
|
||||
### Roo Code Cloud Authentication
|
||||
### Print Mode (`--print`)
|
||||
|
||||
To use Roo Code Cloud features (like the provider proxy), you need to authenticate:
|
||||
Use `--print` for non-interactive execution and machine-readable output:
|
||||
|
||||
```bash
|
||||
# Log in to Roo Code Cloud (opens browser)
|
||||
roo auth login
|
||||
# Prompt is required
|
||||
roo --print "Summarize this repository"
|
||||
|
||||
# Check authentication status
|
||||
roo auth status
|
||||
|
||||
# Log out
|
||||
roo auth logout
|
||||
# Create a new task with a specific session ID (UUID)
|
||||
roo --print --create-with-session-id 018f7fc8-7c96-7f7c-98aa-2ec4ff7f6d87 "Summarize this repository"
|
||||
```
|
||||
|
||||
The `auth login` command:
|
||||
### Stdin Stream Mode (`--stdin-prompt-stream`)
|
||||
|
||||
1. Opens your browser to authenticate with Roo Code Cloud
|
||||
2. Receives a secure token via localhost callback
|
||||
3. Stores the token in `~/.config/roo/credentials.json`
|
||||
For programmatic control (one process, multiple prompts), use `--stdin-prompt-stream` with `--print`.
|
||||
Send NDJSON commands via stdin:
|
||||
|
||||
Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when your token expires.
|
||||
```bash
|
||||
printf '{"command":"start","requestId":"1","prompt":"1+1=?"}\n' | roo --print --stdin-prompt-stream --output-format stream-json
|
||||
|
||||
**Authentication Flow:**
|
||||
|
||||
```
|
||||
┌──────┐ ┌─────────┐ ┌───────────────┐
|
||||
│ CLI │ │ Browser │ │ Roo Code Cloud│
|
||||
└──┬───┘ └────┬────┘ └───────┬───────┘
|
||||
│ │ │
|
||||
│ Open auth URL │ │
|
||||
│─────────────────>│ │
|
||||
│ │ │
|
||||
│ │ Authenticate │
|
||||
│ │─────────────────────>│
|
||||
│ │ │
|
||||
│ │<─────────────────────│
|
||||
│ │ Token via callback │
|
||||
│<─────────────────│ │
|
||||
│ │ │
|
||||
│ Store token │ │
|
||||
│ │ │
|
||||
# Optional: provide taskId per start command
|
||||
printf '{"command":"start","requestId":"1","taskId":"018f7fc8-7c96-7f7c-98aa-2ec4ff7f6d87","prompt":"1+1=?"}\n' | roo --print --stdin-prompt-stream --output-format stream-json
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| ------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| `[prompt]` | Your prompt (positional argument, optional) | None |
|
||||
| `--prompt-file <path>` | Read prompt from a file instead of command line argument | None |
|
||||
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
|
||||
| `-p, --print` | Print response and exit (non-interactive mode) | `false` |
|
||||
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
|
||||
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
|
||||
| `-y, --yes, --dangerously-skip-permissions` | Auto-approve all actions (use with caution) | `false` |
|
||||
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
|
||||
| `--provider <provider>` | API provider (roo, anthropic, openai, openrouter, etc.) | `openrouter` (or `roo` if authenticated) |
|
||||
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.5` |
|
||||
| `--mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
|
||||
| `-r, --reasoning-effort <effort>` | Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh) | `medium` |
|
||||
| `--ephemeral` | Run without persisting state (uses temporary storage) | `false` |
|
||||
| `--oneshot` | Exit upon task completion | `false` |
|
||||
| `--output-format <format>` | Output format with `--print`: `text`, `json`, or `stream-json` | `text` |
|
||||
|
||||
## Auth Commands
|
||||
|
||||
| Command | Description |
|
||||
| ----------------- | ---------------------------------- |
|
||||
| `roo auth login` | Authenticate with Roo Code Cloud |
|
||||
| `roo auth logout` | Clear stored authentication token |
|
||||
| `roo auth status` | Show current authentication status |
|
||||
| Option | Description | Default |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------- |
|
||||
| `[prompt]` | Your prompt (positional argument, optional) | None |
|
||||
| `--prompt-file <path>` | Read prompt from a file instead of command line argument | None |
|
||||
| `--create-with-session-id <session-id>` | Create a new task using the provided session ID (UUID) | None |
|
||||
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
|
||||
| `-p, --print` | Print response and exit (non-interactive mode) | `false` |
|
||||
| `--stdin-prompt-stream` | Read NDJSON control commands from stdin (requires `--print`) | `false` |
|
||||
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
|
||||
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
|
||||
| `-a, --require-approval` | Require manual approval before actions execute | `false` |
|
||||
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
|
||||
| `--provider <provider>` | API provider (anthropic, openai, openrouter, etc.) | `openrouter` |
|
||||
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.6` |
|
||||
| `--mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
|
||||
| `--terminal-shell <path>` | Absolute shell path for inline terminal command execution | Auto-detected shell |
|
||||
| `-r, --reasoning-effort <effort>` | Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh) | `medium` |
|
||||
| `--consecutive-mistake-limit <n>` | Consecutive error/repetition limit before guidance prompt (`0` disables the limit) | `10` |
|
||||
| `--ephemeral` | Run without persisting state (uses temporary storage) | `false` |
|
||||
| `--oneshot` | Exit upon task completion | `false` |
|
||||
| `--output-format <format>` | Output format with `--print`: `text`, `json`, or `stream-json` | `text` |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
|
@ -179,19 +145,12 @@ The CLI will look for API keys in environment variables if not provided via `--a
|
|||
|
||||
| Provider | Environment Variable |
|
||||
| ----------------- | --------------------------- |
|
||||
| roo | `ROO_API_KEY` |
|
||||
| anthropic | `ANTHROPIC_API_KEY` |
|
||||
| openai-native | `OPENAI_API_KEY` |
|
||||
| openrouter | `OPENROUTER_API_KEY` |
|
||||
| gemini | `GOOGLE_API_KEY` |
|
||||
| vercel-ai-gateway | `VERCEL_AI_GATEWAY_API_KEY` |
|
||||
|
||||
**Authentication Environment Variables:**
|
||||
|
||||
| Variable | Description |
|
||||
| ----------------- | -------------------------------------------------------------------- |
|
||||
| `ROO_WEB_APP_URL` | Override the Roo Code Cloud URL (default: `https://app.roocode.com`) |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
|
|
@ -235,7 +194,7 @@ The CLI will look for API keys in environment variables if not provided via `--a
|
|||
|
||||
```bash
|
||||
# Run directly from source (no build required)
|
||||
pnpm dev --provider roo --api-key $ROO_API_KEY --print "Hello"
|
||||
pnpm dev --provider openrouter --api-key $OPENROUTER_API_KEY --print "Hello"
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
|
@ -247,12 +206,6 @@ pnpm check-types
|
|||
pnpm lint
|
||||
```
|
||||
|
||||
By default the `start` script points `ROO_CODE_PROVIDER_URL` at `http://localhost:8080/proxy` for local development. To point at the production API instead, override the environment variable:
|
||||
|
||||
```bash
|
||||
ROO_CODE_PROVIDER_URL=https://api.roocode.com/proxy pnpm dev --provider roo --api-key $ROO_API_KEY --print "Hello"
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
Official releases are created via the GitHub Actions workflow at `.github/workflows/cli-release.yml`.
|
||||
|
|
|
|||
|
|
@ -242,7 +242,8 @@ Routes asks to appropriate handlers:
|
|||
|
||||
- Uses type guards: `isIdleAsk()`, `isInteractiveAsk()`, etc.
|
||||
- Coordinates between `OutputManager` and `PromptManager`
|
||||
- In non-interactive mode (`-y` flag), auto-approves everything
|
||||
- By default, the CLI auto-approves tool/command/browser/MCP actions
|
||||
- In `--require-approval` mode, those actions prompt for manual approval
|
||||
|
||||
### OutputManager
|
||||
|
||||
|
|
@ -320,7 +321,7 @@ if (isInteractiveAsk(ask)) {
|
|||
Enable with `-d` flag. Logs go to `~/.roo/cli-debug.log`:
|
||||
|
||||
```bash
|
||||
roo -d -y -P "Build something" --no-tui
|
||||
roo -d -P "Build something" --no-tui
|
||||
```
|
||||
|
||||
View logs:
|
||||
|
|
|
|||
|
|
@ -104,12 +104,60 @@ get_version() {
|
|||
error "Failed to fetch releases from GitHub. Check your internet connection."
|
||||
}
|
||||
|
||||
# Extract the latest cli-v* tag
|
||||
VERSION=$(echo "$RELEASES_JSON" |
|
||||
grep -o '"tag_name": "cli-v[^"]*"' |
|
||||
head -1 |
|
||||
sed 's/"tag_name": "cli-v//' |
|
||||
sed 's/"//')
|
||||
# Extract highest cli-v* tag by semantic version (do not rely on API ordering)
|
||||
VERSION=$(printf "%s" "$RELEASES_JSON" | node -e '
|
||||
const fs = require("fs")
|
||||
const input = fs.readFileSync(0, "utf8")
|
||||
let releases
|
||||
try {
|
||||
releases = JSON.parse(input)
|
||||
} catch {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function parseVersion(version) {
|
||||
const core = String(version).trim().split("+", 1)[0].split("-", 1)[0]
|
||||
if (!core) return null
|
||||
const parts = core.split(".")
|
||||
if (parts.length === 0 || parts.some((part) => !/^\d+$/.test(part))) {
|
||||
return null
|
||||
}
|
||||
return parts.map((part) => Number.parseInt(part, 10))
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const maxLength = Math.max(a.length, b.length)
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const aPart = a[i] ?? 0
|
||||
const bPart = b[i] ?? 0
|
||||
if (aPart > bPart) return 1
|
||||
if (aPart < bPart) return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
let latestVersion = ""
|
||||
let latestParts = null
|
||||
|
||||
if (Array.isArray(releases)) {
|
||||
for (const release of releases) {
|
||||
if (!release || typeof release.tag_name !== "string" || !release.tag_name.startsWith("cli-v")) {
|
||||
continue
|
||||
}
|
||||
const candidate = release.tag_name.slice("cli-v".length)
|
||||
const candidateParts = parseVersion(candidate)
|
||||
if (!candidateParts) continue
|
||||
if (!latestParts || compareVersions(candidateParts, latestParts) > 0) {
|
||||
latestVersion = candidate
|
||||
latestParts = candidateParts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (latestVersion) {
|
||||
process.stdout.write(latestVersion)
|
||||
}
|
||||
')
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
error "Could not find any CLI releases. The CLI may not have been released yet."
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@roo-code/cli",
|
||||
"version": "0.0.50",
|
||||
"version": "0.1.17",
|
||||
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
@ -13,10 +13,11 @@
|
|||
"lint": "eslint src --ext .ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:integration": "tsx scripts/integration/run.ts",
|
||||
"build": "tsup",
|
||||
"build:extension": "pnpm --filter roo-cline bundle",
|
||||
"dev": "ROO_AUTH_BASE_URL=https://app.roocode.com ROO_SDK_BASE_URL=https://cloud-api.roocode.com ROO_CODE_PROVIDER_URL=https://api.roocode.com/proxy tsx src/index.ts -y",
|
||||
"dev:local": "ROO_AUTH_BASE_URL=http://localhost:3000 ROO_SDK_BASE_URL=http://localhost:3001 ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy tsx src/index.ts",
|
||||
"dev": "tsx src/index.ts",
|
||||
"dev:local": "tsx src/index.ts",
|
||||
"clean": "rimraf dist .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ create_tarball() {
|
|||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
|
@ -200,7 +201,10 @@ const __dirname = dirname(__filename);
|
|||
// Set environment variables for the CLI
|
||||
process.env.ROO_CLI_ROOT = join(__dirname, '..');
|
||||
process.env.ROO_EXTENSION_PATH = join(__dirname, '..', 'extension');
|
||||
process.env.ROO_RIPGREP_PATH = join(__dirname, 'rg');
|
||||
const ripgrepPath = join(__dirname, 'rg');
|
||||
if (existsSync(ripgrepPath)) {
|
||||
process.env.ROO_RIPGREP_PATH = ripgrepPath;
|
||||
}
|
||||
|
||||
// Import and run the actual CLI
|
||||
await import(join(__dirname, '..', 'lib', 'index.js'));
|
||||
|
|
@ -211,10 +215,21 @@ WRAPPER_EOF
|
|||
# Create empty .env file
|
||||
touch "$RELEASE_DIR/.env"
|
||||
|
||||
# Strip macOS metadata artifacts before packaging.
|
||||
find "$RELEASE_DIR" -type f -name "._*" -delete
|
||||
find "$RELEASE_DIR" -type f -name ".DS_Store" -delete
|
||||
find "$RELEASE_DIR" -type d -name "__MACOSX" -prune -exec rm -rf {} +
|
||||
|
||||
# Create tarball
|
||||
info "Creating tarball..."
|
||||
cd "$REPO_ROOT"
|
||||
tar -czvf "$TARBALL" "$(basename "$RELEASE_DIR")"
|
||||
COPYFILE_DISABLE=1 tar \
|
||||
--exclude="._*" \
|
||||
--exclude=".DS_Store" \
|
||||
--exclude="__MACOSX" \
|
||||
--exclude="*/._*" \
|
||||
--exclude="*/.DS_Store" \
|
||||
-czvf "$TARBALL" "$(basename "$RELEASE_DIR")"
|
||||
|
||||
# Clean up release directory
|
||||
rm -rf "$RELEASE_DIR"
|
||||
|
|
|
|||
104
apps/cli/scripts/integration/cases/cancel-active-task.ts
Normal file
104
apps/cli/scripts/integration/cases/cancel-active-task.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const LONG_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 12 && echo "done". After it finishes, reply with exactly "done".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-a-${Date.now()}`
|
||||
const cancelRequestId = `cancel-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let startAccepted = false
|
||||
let startCommandToolUseSeen = false
|
||||
let sentCancel = false
|
||||
let cancelDone = false
|
||||
let sentShutdown = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: LONG_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === startRequestId
|
||||
) {
|
||||
startAccepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "tool_use" &&
|
||||
event.subtype === "command" &&
|
||||
event.done === true &&
|
||||
event.requestId === startRequestId
|
||||
) {
|
||||
startCommandToolUseSeen = true
|
||||
}
|
||||
|
||||
if (startAccepted && startCommandToolUseSeen && !sentCancel) {
|
||||
context.sendCommand({
|
||||
command: "cancel",
|
||||
requestId: cancelRequestId,
|
||||
})
|
||||
sentCancel = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "cancel" &&
|
||||
event.requestId === cancelRequestId
|
||||
) {
|
||||
if (event.code === "cancel_requested" || event.code === "no_active_task") {
|
||||
cancelDone = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (cancelDone && !sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error" && event.requestId === cancelRequestId) {
|
||||
throw new Error(
|
||||
`cancel command failed with code=${event.code ?? "unknown"} content="${event.content ?? ""}"`,
|
||||
)
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
throw new Error(`unexpected stream error event: ${event.content ?? "unknown error"}`)
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for cancel flow (initSeen=${initSeen}, startAccepted=${startAccepted}, startCommandToolUseSeen=${startCommandToolUseSeen}, sentCancel=${sentCancel}, cancelDone=${cancelDone}, sentShutdown=${sentShutdown})`
|
||||
},
|
||||
})
|
||||
|
||||
if (!startAccepted || !startCommandToolUseSeen || !sentCancel || !cancelDone || !sentShutdown) {
|
||||
throw new Error(
|
||||
`cancel flow did not complete expected transitions (startAccepted=${startAccepted}, startCommandToolUseSeen=${startCommandToolUseSeen}, sentCancel=${sentCancel}, cancelDone=${cancelDone}, sentShutdown=${sentShutdown})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const LONG_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 12 && echo "done". After it finishes, reply with exactly "done".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const cancelRequestId = `cancel-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let startAccepted = false
|
||||
let sentCancel = false
|
||||
let cancelDone = false
|
||||
let sentShutdown = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: LONG_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === startRequestId &&
|
||||
!startAccepted
|
||||
) {
|
||||
startAccepted = true
|
||||
context.sendCommand({
|
||||
command: "cancel",
|
||||
requestId: cancelRequestId,
|
||||
})
|
||||
sentCancel = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "cancel" &&
|
||||
event.requestId === cancelRequestId
|
||||
) {
|
||||
if (event.code === "cancel_requested" || event.code === "no_active_task") {
|
||||
cancelDone = true
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
throw new Error(`unexpected stream error event: ${event.content ?? "unknown error"}`)
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for immediate-cancel flow (initSeen=${initSeen}, startAccepted=${startAccepted}, sentCancel=${sentCancel}, cancelDone=${cancelDone}, sentShutdown=${sentShutdown})`
|
||||
},
|
||||
})
|
||||
|
||||
if (!startAccepted || !sentCancel || !cancelDone || !sentShutdown) {
|
||||
throw new Error(
|
||||
`immediate-cancel flow did not complete expected transitions (startAccepted=${startAccepted}, sentCancel=${sentCancel}, cancelDone=${cancelDone}, sentShutdown=${sentShutdown})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const START_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 12 && echo "done". After it finishes, reply with exactly "done".'
|
||||
const FOLLOWUP_PROMPT = 'After cancellation, reply with only "RACE-OK".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const cancelRequestId = `cancel-${Date.now()}`
|
||||
const followupRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let sentCancelAndFollowup = false
|
||||
let sentShutdown = false
|
||||
let cancelDoneCode: string | undefined
|
||||
let followupDoneCode: string | undefined
|
||||
let followupResult = ""
|
||||
let sawFollowupUserTurn = false
|
||||
let sawMisroutedToolResult = false
|
||||
let sawMessageControlError = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: START_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error") {
|
||||
if (event.requestId === followupRequestId) {
|
||||
sawMessageControlError = true
|
||||
}
|
||||
throw new Error(
|
||||
`received control error for requestId=${event.requestId ?? "unknown"} command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!sentCancelAndFollowup &&
|
||||
event.type === "tool_use" &&
|
||||
event.requestId === startRequestId &&
|
||||
event.subtype === "command"
|
||||
) {
|
||||
context.sendCommand({
|
||||
command: "cancel",
|
||||
requestId: cancelRequestId,
|
||||
})
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: followupRequestId,
|
||||
prompt: FOLLOWUP_PROMPT,
|
||||
})
|
||||
sentCancelAndFollowup = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.command === "cancel" &&
|
||||
event.subtype === "done" &&
|
||||
event.requestId === cancelRequestId
|
||||
) {
|
||||
cancelDoneCode = event.code
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.command === "message" &&
|
||||
event.subtype === "done" &&
|
||||
event.requestId === followupRequestId
|
||||
) {
|
||||
followupDoneCode = event.code
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "tool_result" &&
|
||||
event.requestId === followupRequestId &&
|
||||
typeof event.content === "string" &&
|
||||
event.content.includes("<user_message>")
|
||||
) {
|
||||
sawMisroutedToolResult = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "user" && event.requestId === followupRequestId) {
|
||||
sawFollowupUserTurn = typeof event.content === "string" && event.content.includes("RACE-OK")
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type !== "result" || event.done !== true || event.requestId !== followupRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
followupResult = event.content ?? ""
|
||||
|
||||
if (followupResult.trim().length === 0) {
|
||||
throw new Error("follow-up after cancel produced an empty result")
|
||||
}
|
||||
if (cancelDoneCode !== "cancel_requested") {
|
||||
throw new Error(
|
||||
`cancel done code mismatch; expected cancel_requested, got "${cancelDoneCode ?? "none"}"`,
|
||||
)
|
||||
}
|
||||
if (followupDoneCode !== "responded" && followupDoneCode !== "queued") {
|
||||
throw new Error(
|
||||
`unexpected follow-up done code after cancel race; expected responded|queued, got "${followupDoneCode ?? "none"}"`,
|
||||
)
|
||||
}
|
||||
if (sawMessageControlError) {
|
||||
throw new Error("follow-up message emitted control error in cancel recovery race")
|
||||
}
|
||||
if (sawMisroutedToolResult) {
|
||||
throw new Error(
|
||||
"follow-up message was misrouted into tool_result (<user_message>) in cancel recovery race",
|
||||
)
|
||||
}
|
||||
if (!sawFollowupUserTurn) {
|
||||
throw new Error("follow-up after cancel did not appear as a normal user turn")
|
||||
}
|
||||
|
||||
console.log(`[PASS] cancel done code: "${cancelDoneCode}"`)
|
||||
console.log(`[PASS] follow-up done code: "${followupDoneCode}"`)
|
||||
console.log(`[PASS] follow-up user turn observed: ${sawFollowupUserTurn}`)
|
||||
console.log(`[PASS] follow-up result: "${followupResult}"`)
|
||||
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return [
|
||||
"timed out waiting for cancel-message-recovery-race validation",
|
||||
`initSeen=${initSeen}`,
|
||||
`sentCancelAndFollowup=${sentCancelAndFollowup}`,
|
||||
`cancelDoneCode=${cancelDoneCode ?? "none"}`,
|
||||
`followupDoneCode=${followupDoneCode ?? "none"}`,
|
||||
`sawFollowupUserTurn=${sawFollowupUserTurn}`,
|
||||
`sawMisroutedToolResult=${sawMisroutedToolResult}`,
|
||||
`sawMessageControlError=${sawMessageControlError}`,
|
||||
`haveFollowupResult=${Boolean(followupResult)}`,
|
||||
].join(" ")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
async function main() {
|
||||
const cancelRequestId = `cancel-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let cancelAckSeen = false
|
||||
let cancelDoneSeen = false
|
||||
let shutdownSent = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "cancel",
|
||||
requestId: cancelRequestId,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "cancel" &&
|
||||
event.requestId === cancelRequestId
|
||||
) {
|
||||
cancelAckSeen = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "cancel" &&
|
||||
event.requestId === cancelRequestId
|
||||
) {
|
||||
cancelDoneSeen = true
|
||||
|
||||
if (event.code !== "no_active_task") {
|
||||
throw new Error(`cancel without task should return no_active_task, got "${event.code ?? "none"}"`)
|
||||
}
|
||||
if (event.success !== true) {
|
||||
throw new Error("cancel without task should be treated as successful no-op")
|
||||
}
|
||||
|
||||
if (!shutdownSent) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
shutdownSent = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error") {
|
||||
throw new Error(
|
||||
`unexpected control error command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for cancel-without-active-task validation (initSeen=${initSeen}, cancelAckSeen=${cancelAckSeen}, cancelDoneSeen=${cancelDoneSeen}, shutdownSent=${shutdownSent})`
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import readline from "readline"
|
||||
import { fileURLToPath } from "url"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
import { execa } from "execa"
|
||||
import type { TaskSessionEntry } from "@roo-code/core/cli"
|
||||
|
||||
type StreamEvent = {
|
||||
type?: string
|
||||
subtype?: string
|
||||
requestId?: string
|
||||
command?: string
|
||||
taskId?: string
|
||||
content?: string
|
||||
code?: string
|
||||
success?: boolean
|
||||
done?: boolean
|
||||
}
|
||||
|
||||
const RESUME_TIMEOUT_MS = 180_000
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function parseStreamEvent(line: string): StreamEvent | null {
|
||||
const trimmed = line.trim()
|
||||
|
||||
if (!trimmed.startsWith("{")) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed) as StreamEvent
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function listSessions(cliRoot: string, workspacePath: string): Promise<TaskSessionEntry[]> {
|
||||
const result = await execa("pnpm", ["dev", "list", "sessions", "--workspace", workspacePath, "--format", "json"], {
|
||||
cwd: cliRoot,
|
||||
reject: false,
|
||||
})
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`list sessions failed with exit code ${result.exitCode}: ${result.stderr || result.stdout}`)
|
||||
}
|
||||
|
||||
const stdoutLines = result.stdout.split("\n")
|
||||
const jsonStartIndex = stdoutLines.findIndex((line) => line.trim().startsWith("{"))
|
||||
if (jsonStartIndex === -1) {
|
||||
throw new Error(`list sessions output did not contain JSON payload: ${result.stdout}`)
|
||||
}
|
||||
|
||||
const jsonPayload = stdoutLines.slice(jsonStartIndex).join("\n").trim()
|
||||
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(jsonPayload)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`failed to parse list sessions output as JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed !== "object" ||
|
||||
parsed === null ||
|
||||
!("sessions" in parsed) ||
|
||||
!Array.isArray((parsed as { sessions?: unknown }).sessions)
|
||||
) {
|
||||
throw new Error("list sessions output missing sessions array")
|
||||
}
|
||||
|
||||
return (parsed as { sessions: TaskSessionEntry[] }).sessions
|
||||
}
|
||||
|
||||
async function createSessionWithCustomId(
|
||||
cliRoot: string,
|
||||
workspacePath: string,
|
||||
sessionId: string,
|
||||
prompt: string,
|
||||
): Promise<void> {
|
||||
const result = await execa(
|
||||
"pnpm",
|
||||
[
|
||||
"dev",
|
||||
"--print",
|
||||
"--provider",
|
||||
"openrouter",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--workspace",
|
||||
workspacePath,
|
||||
"--create-with-session-id",
|
||||
sessionId,
|
||||
prompt,
|
||||
],
|
||||
{
|
||||
cwd: cliRoot,
|
||||
reject: false,
|
||||
},
|
||||
)
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`create-with-session-id failed for ${sessionId} with exit code ${result.exitCode}: ${result.stderr || result.stdout}`,
|
||||
)
|
||||
}
|
||||
|
||||
const lines = result.stdout.split("\n")
|
||||
const events = lines.map(parseStreamEvent).filter((event): event is StreamEvent => Boolean(event))
|
||||
const errorEvent = events.find((event) => event.type === "error")
|
||||
|
||||
if (errorEvent) {
|
||||
throw new Error(
|
||||
`create-with-session-id emitted error for ${sessionId}: code=${errorEvent.code ?? "none"} content=${errorEvent.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
const completion = events.find((event) => event.type === "result" && event.done === true)
|
||||
if (!completion) {
|
||||
throw new Error(`create-with-session-id did not emit final result for ${sessionId}`)
|
||||
}
|
||||
|
||||
if (completion.success !== true) {
|
||||
throw new Error(`create-with-session-id completed unsuccessfully for ${sessionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeSessionAndSendMarker(
|
||||
cliRoot: string,
|
||||
workspacePath: string,
|
||||
sessionId: string,
|
||||
messageToken: string,
|
||||
): Promise<void> {
|
||||
const pingRequestId = `ping-${Date.now()}`
|
||||
const messageRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
const messagePrompt = `Resume marker token: ${messageToken}. Reply with exactly "ack-${messageToken}".`
|
||||
|
||||
const child = execa(
|
||||
"pnpm",
|
||||
[
|
||||
"dev",
|
||||
"--print",
|
||||
"--stdin-prompt-stream",
|
||||
"--provider",
|
||||
"openrouter",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--workspace",
|
||||
workspacePath,
|
||||
"--session-id",
|
||||
sessionId,
|
||||
],
|
||||
{
|
||||
cwd: cliRoot,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
reject: false,
|
||||
forceKillAfterDelay: 2_000,
|
||||
},
|
||||
)
|
||||
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
process.stderr.write(chunk)
|
||||
})
|
||||
|
||||
let pingSent = false
|
||||
let messageSent = false
|
||||
let shutdownSent = false
|
||||
let sawMessageControlDone = false
|
||||
let sawUserTurnWithMarker = false
|
||||
let shutdownTaskId: string | undefined
|
||||
let handlerError: Error | null = null
|
||||
let timedOut = false
|
||||
|
||||
const sendCommand = (command: { command: "ping" | "message" | "shutdown"; requestId: string; prompt?: string }) => {
|
||||
if (!child.stdin || child.stdin.destroyed) {
|
||||
return
|
||||
}
|
||||
child.stdin.write(`${JSON.stringify(command)}\n`)
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true
|
||||
handlerError = new Error(
|
||||
`timed out resuming session ${sessionId} (pingSent=${pingSent}, messageSent=${messageSent}, sawMessageControlDone=${sawMessageControlDone}, sawUserTurnWithMarker=${sawUserTurnWithMarker})`,
|
||||
)
|
||||
child.kill("SIGTERM")
|
||||
}, RESUME_TIMEOUT_MS)
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: child.stdout!,
|
||||
crlfDelay: Infinity,
|
||||
})
|
||||
|
||||
rl.on("line", (line) => {
|
||||
process.stdout.write(`${line}\n`)
|
||||
|
||||
const event = parseStreamEvent(line)
|
||||
if (!event) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "system" && event.subtype === "init" && !pingSent) {
|
||||
pingSent = true
|
||||
sendCommand({ command: "ping", requestId: pingRequestId })
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "ping" &&
|
||||
event.requestId === pingRequestId &&
|
||||
!messageSent
|
||||
) {
|
||||
messageSent = true
|
||||
sendCommand({
|
||||
command: "message",
|
||||
requestId: messageRequestId,
|
||||
prompt: messagePrompt,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "error" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === messageRequestId
|
||||
) {
|
||||
handlerError = new Error(
|
||||
`message command failed while resuming ${sessionId}: code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
child.kill("SIGTERM")
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === messageRequestId
|
||||
) {
|
||||
sawMessageControlDone = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "user" && event.requestId === messageRequestId && event.content?.includes(messageToken)) {
|
||||
sawUserTurnWithMarker = true
|
||||
|
||||
if (!shutdownSent) {
|
||||
shutdownSent = true
|
||||
sendCommand({ command: "shutdown", requestId: shutdownRequestId })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
(event.subtype === "ack" || event.subtype === "done") &&
|
||||
event.command === "shutdown" &&
|
||||
event.requestId === shutdownRequestId &&
|
||||
typeof event.taskId === "string"
|
||||
) {
|
||||
shutdownTaskId = event.taskId
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error" && event.requestId !== shutdownRequestId) {
|
||||
handlerError = new Error(
|
||||
`unexpected control error while resuming ${sessionId}: command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
child.kill("SIGTERM")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const result = await child
|
||||
clearTimeout(timeout)
|
||||
rl.close()
|
||||
|
||||
if (handlerError) {
|
||||
throw handlerError
|
||||
}
|
||||
|
||||
if (timedOut) {
|
||||
throw new Error(`stream resume for ${sessionId} timed out`)
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`stream resume for ${sessionId} exited non-zero: ${result.exitCode}`)
|
||||
}
|
||||
|
||||
if (!sawMessageControlDone) {
|
||||
throw new Error(`did not observe message control completion while resuming ${sessionId}`)
|
||||
}
|
||||
|
||||
if (!sawUserTurnWithMarker) {
|
||||
throw new Error(`did not observe resumed user marker turn while resuming ${sessionId}`)
|
||||
}
|
||||
|
||||
if (shutdownTaskId !== sessionId) {
|
||||
throw new Error(
|
||||
`shutdown taskId did not match resumed session (expected=${sessionId}, actual=${shutdownTaskId ?? "none"})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cliRoot = process.env.ROO_CLI_ROOT
|
||||
? path.resolve(process.env.ROO_CLI_ROOT)
|
||||
: path.resolve(__dirname, "../../..")
|
||||
const workspacePath = await fs.mkdtemp(path.join(os.tmpdir(), "roo-cli-create-session-id-"))
|
||||
|
||||
const firstSessionId = randomUUID()
|
||||
const secondSessionId = randomUUID()
|
||||
const firstMarker = `FIRST-MARKER-${Date.now()}`
|
||||
const secondMarker = `SECOND-MARKER-${Date.now()}`
|
||||
|
||||
try {
|
||||
await createSessionWithCustomId(
|
||||
cliRoot,
|
||||
workspacePath,
|
||||
firstSessionId,
|
||||
`Create first session marker ${firstMarker}. Reply with exactly "ok-${firstMarker}".`,
|
||||
)
|
||||
await createSessionWithCustomId(
|
||||
cliRoot,
|
||||
workspacePath,
|
||||
secondSessionId,
|
||||
`Create second session marker ${secondMarker}. Reply with exactly "ok-${secondMarker}".`,
|
||||
)
|
||||
|
||||
const initialSessions = await listSessions(cliRoot, workspacePath)
|
||||
if (!initialSessions.some((session) => session.id === firstSessionId)) {
|
||||
throw new Error(`session list missing first custom session id ${firstSessionId}`)
|
||||
}
|
||||
if (!initialSessions.some((session) => session.id === secondSessionId)) {
|
||||
throw new Error(`session list missing second custom session id ${secondSessionId}`)
|
||||
}
|
||||
|
||||
const resumeMarkerForFirst = `resume-first-${Date.now()}`
|
||||
await resumeSessionAndSendMarker(cliRoot, workspacePath, firstSessionId, resumeMarkerForFirst)
|
||||
|
||||
const resumeMarkerForSecond = `resume-second-${Date.now()}`
|
||||
await resumeSessionAndSendMarker(cliRoot, workspacePath, secondSessionId, resumeMarkerForSecond)
|
||||
|
||||
console.log(`[PASS] created and resumed custom sessions: ${firstSessionId}, ${secondSessionId}`)
|
||||
} finally {
|
||||
await fs.rm(workspacePath, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
135
apps/cli/scripts/integration/cases/followup-after-completion.ts
Normal file
135
apps/cli/scripts/integration/cases/followup-after-completion.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const FIRST_PROMPT = `What is 1+1? Reply with only "2".`
|
||||
const FOLLOWUP_PROMPT = `Different question now: what is 3+3? Reply with only "6".`
|
||||
|
||||
function parseEventContent(text: string | undefined): string {
|
||||
return typeof text === "string" ? text : ""
|
||||
}
|
||||
|
||||
function validateFollowupResult(text: string): void {
|
||||
if (text.trim().length === 0) {
|
||||
throw new Error("follow-up produced an empty result")
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const followupRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let sentFollowup = false
|
||||
let sentShutdown = false
|
||||
let firstResult = ""
|
||||
let followupResult = ""
|
||||
let followupDoneCode: string | undefined
|
||||
let sawFollowupUserTurn = false
|
||||
let sawMisroutedToolResult = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: FIRST_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error") {
|
||||
throw new Error(
|
||||
`received control error for requestId=${event.requestId ?? "unknown"} command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (event.type !== "result" || event.done !== true) {
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.requestId === followupRequestId &&
|
||||
event.command === "message" &&
|
||||
event.subtype === "done"
|
||||
) {
|
||||
followupDoneCode = event.code
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "tool_result" &&
|
||||
event.requestId === followupRequestId &&
|
||||
typeof event.content === "string" &&
|
||||
event.content.includes("<user_message>")
|
||||
) {
|
||||
sawMisroutedToolResult = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "user" && event.requestId === followupRequestId) {
|
||||
sawFollowupUserTurn = typeof event.content === "string" && event.content.includes("3+3")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.requestId === startRequestId) {
|
||||
firstResult = parseEventContent(event.content)
|
||||
if (!/\b2\b/.test(firstResult)) {
|
||||
throw new Error(`first result did not answer first prompt; result="${firstResult}"`)
|
||||
}
|
||||
|
||||
if (!sentFollowup) {
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: followupRequestId,
|
||||
prompt: FOLLOWUP_PROMPT,
|
||||
})
|
||||
sentFollowup = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.requestId !== followupRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
followupResult = parseEventContent(event.content)
|
||||
validateFollowupResult(followupResult)
|
||||
|
||||
if (followupDoneCode !== "responded") {
|
||||
throw new Error(
|
||||
`follow-up message was not routed as ask response; code="${followupDoneCode ?? "none"}"`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!sawFollowupUserTurn) {
|
||||
throw new Error("follow-up did not appear as a normal user turn in stream output")
|
||||
}
|
||||
|
||||
if (sawMisroutedToolResult) {
|
||||
throw new Error("follow-up message was misrouted into tool_result (<user_message>), old bug reproduced")
|
||||
}
|
||||
|
||||
console.log(`[PASS] first result="${firstResult}"`)
|
||||
console.log(`[PASS] follow-up result="${followupResult}"`)
|
||||
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for completion (initSeen=${initSeen}, sentFollowup=${sentFollowup}, firstResult=${Boolean(firstResult)}, followupResult=${Boolean(followupResult)})`
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const START_PROMPT = 'Answer this question and finish: What is 1+1? Reply with only "2", then complete the task.'
|
||||
const FOLLOWUP_PROMPT = 'Different question now: what is 3+3? Reply with only "6".'
|
||||
const ONE_PIXEL_IMAGE =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9R4WQAAAAASUVORK5CYII="
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const followupRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let sentFollowup = false
|
||||
let sentShutdown = false
|
||||
let followupDoneCode: string | undefined
|
||||
let sawFollowupUserTurn = false
|
||||
let sawMisroutedToolResult = false
|
||||
let sawQueueImageMetadata = false
|
||||
let shutdownDoneSeen = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: START_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error") {
|
||||
throw new Error(
|
||||
`received control error for requestId=${event.requestId ?? "unknown"} command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.command === "message" &&
|
||||
event.subtype === "done" &&
|
||||
event.requestId === followupRequestId
|
||||
) {
|
||||
followupDoneCode = event.code
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.command === "shutdown" &&
|
||||
event.subtype === "done" &&
|
||||
event.requestId === shutdownRequestId
|
||||
) {
|
||||
shutdownDoneSeen = true
|
||||
|
||||
if (followupDoneCode !== "responded") {
|
||||
throw new Error(
|
||||
`follow-up image message was not routed as ask response; code="${followupDoneCode ?? "none"}"`,
|
||||
)
|
||||
}
|
||||
if (sawQueueImageMetadata) {
|
||||
throw new Error("follow-up image message was unexpectedly queued (observed queue image metadata)")
|
||||
}
|
||||
if (sawMisroutedToolResult) {
|
||||
throw new Error("follow-up image message was misrouted into tool_result (<user_message>)")
|
||||
}
|
||||
|
||||
console.log(`[PASS] follow-up image control code: "${followupDoneCode}"`)
|
||||
console.log(`[PASS] follow-up image user turn observed before shutdown: ${sawFollowupUserTurn}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "queue" &&
|
||||
Array.isArray(event.queue) &&
|
||||
event.queue.some((item) => item?.imageCount === 1)
|
||||
) {
|
||||
sawQueueImageMetadata = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "tool_result" &&
|
||||
event.requestId === followupRequestId &&
|
||||
typeof event.content === "string" &&
|
||||
event.content.includes("<user_message>")
|
||||
) {
|
||||
sawMisroutedToolResult = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "user" && event.requestId === followupRequestId) {
|
||||
sawFollowupUserTurn = typeof event.content === "string" && event.content.includes("3+3")
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "result" && event.done === true && event.requestId === startRequestId && !sentFollowup) {
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: followupRequestId,
|
||||
prompt: FOLLOWUP_PROMPT,
|
||||
images: [ONE_PIXEL_IMAGE],
|
||||
})
|
||||
sentFollowup = true
|
||||
return
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return [
|
||||
"timed out waiting for followup-completion-ask-response-images validation",
|
||||
`initSeen=${initSeen}`,
|
||||
`sentFollowup=${sentFollowup}`,
|
||||
`sentShutdown=${sentShutdown}`,
|
||||
`shutdownDoneSeen=${shutdownDoneSeen}`,
|
||||
`followupDoneCode=${followupDoneCode ?? "none"}`,
|
||||
`sawFollowupUserTurn=${sawFollowupUserTurn}`,
|
||||
`sawMisroutedToolResult=${sawMisroutedToolResult}`,
|
||||
`sawQueueImageMetadata=${sawQueueImageMetadata}`,
|
||||
].join(" ")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const START_PROMPT = 'Answer this question and finish: What is 1+1? Reply with only "2", then complete the task.'
|
||||
const FOLLOWUP_PROMPT = 'Different question now: what is 3+3? Reply with only "6".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const followupRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let sentFollowup = false
|
||||
let sentShutdown = false
|
||||
let startAckCount = 0
|
||||
let sawStartControlAfterFollowup = false
|
||||
let followupDoneCode: string | undefined
|
||||
let sawFollowupUserTurn = false
|
||||
let sawMisroutedToolResult = false
|
||||
let sawQueueEventForFollowupRequest = false
|
||||
let followupResult = ""
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: START_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error") {
|
||||
throw new Error(
|
||||
`received control error for requestId=${event.requestId ?? "unknown"} command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.command === "start" && event.subtype === "ack") {
|
||||
startAckCount += 1
|
||||
if (sentFollowup) {
|
||||
sawStartControlAfterFollowup = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.command === "message" &&
|
||||
event.subtype === "done" &&
|
||||
event.requestId === followupRequestId
|
||||
) {
|
||||
followupDoneCode = event.code
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "queue" && event.requestId === followupRequestId) {
|
||||
sawQueueEventForFollowupRequest = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "tool_result" &&
|
||||
event.requestId === followupRequestId &&
|
||||
typeof event.content === "string" &&
|
||||
event.content.includes("<user_message>")
|
||||
) {
|
||||
sawMisroutedToolResult = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "user" && event.requestId === followupRequestId) {
|
||||
sawFollowupUserTurn = typeof event.content === "string" && event.content.includes("3+3")
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "result" && event.done === true && event.requestId === startRequestId && !sentFollowup) {
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: followupRequestId,
|
||||
prompt: FOLLOWUP_PROMPT,
|
||||
})
|
||||
sentFollowup = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type !== "result" || event.done !== true || event.requestId !== followupRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
followupResult = event.content ?? ""
|
||||
if (followupResult.trim().length === 0) {
|
||||
throw new Error("follow-up produced an empty result")
|
||||
}
|
||||
|
||||
if (followupDoneCode !== "responded") {
|
||||
throw new Error(
|
||||
`follow-up message was not routed as ask response; code="${followupDoneCode ?? "none"}"`,
|
||||
)
|
||||
}
|
||||
|
||||
if (sawMisroutedToolResult) {
|
||||
throw new Error("follow-up message was misrouted into tool_result (<user_message>), old bug reproduced")
|
||||
}
|
||||
if (sawQueueEventForFollowupRequest) {
|
||||
throw new Error("follow-up message produced queue events despite responded routing")
|
||||
}
|
||||
|
||||
if (!sawFollowupUserTurn) {
|
||||
throw new Error("follow-up did not appear as a normal user turn in stream output")
|
||||
}
|
||||
|
||||
if (sawStartControlAfterFollowup) {
|
||||
throw new Error("unexpected start control event after follow-up; message should not trigger a new task")
|
||||
}
|
||||
|
||||
if (startAckCount !== 1) {
|
||||
throw new Error(`expected exactly one start ack event, saw ${startAckCount}`)
|
||||
}
|
||||
|
||||
console.log(`[PASS] follow-up control code: "${followupDoneCode}"`)
|
||||
console.log(`[PASS] follow-up user turn observed: ${sawFollowupUserTurn}`)
|
||||
console.log(`[PASS] follow-up result: "${followupResult}"`)
|
||||
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return [
|
||||
"timed out waiting for completion ask-response follow-up validation",
|
||||
`initSeen=${initSeen}`,
|
||||
`sentFollowup=${sentFollowup}`,
|
||||
`startAckCount=${startAckCount}`,
|
||||
`followupDoneCode=${followupDoneCode ?? "none"}`,
|
||||
`sawFollowupUserTurn=${sawFollowupUserTurn}`,
|
||||
`sawMisroutedToolResult=${sawMisroutedToolResult}`,
|
||||
`sawQueueEventForFollowupRequest=${sawQueueEventForFollowupRequest}`,
|
||||
`haveFollowupResult=${Boolean(followupResult)}`,
|
||||
].join(" ")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
159
apps/cli/scripts/integration/cases/followup-during-streaming.ts
Normal file
159
apps/cli/scripts/integration/cases/followup-during-streaming.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const START_PROMPT = 'Answer this question and finish: What is 1+1? Reply with only "2", then complete the task.'
|
||||
const FOLLOWUP_PROMPT = 'Different question now: what is 3+3? Reply with only "6".'
|
||||
|
||||
function looksLikeAttemptCompletionToolUse(event: StreamEvent): boolean {
|
||||
if (event.type !== "tool_use") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (event.tool_use?.name === "attempt_completion") {
|
||||
return true
|
||||
}
|
||||
|
||||
const content = event.content ?? ""
|
||||
return content.includes('"tool":"attempt_completion"') || content.includes('"name":"attempt_completion"')
|
||||
}
|
||||
|
||||
function validateFollowupResult(text: string): void {
|
||||
if (text.trim().length === 0) {
|
||||
throw new Error("follow-up produced an empty result")
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const followupRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let sentFollowup = false
|
||||
let sentShutdown = false
|
||||
let sawAttemptCompletion = false
|
||||
let sawFollowupUserTurn = false
|
||||
let sawMisroutedToolResult = false
|
||||
let followupResult = ""
|
||||
let sawFirstAssistantChunkForStart = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: START_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "control" && event.subtype === "error") {
|
||||
throw new Error(
|
||||
`received control error for requestId=${event.requestId ?? "unknown"} command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!sawAttemptCompletion && looksLikeAttemptCompletionToolUse(event)) {
|
||||
sawAttemptCompletion = true
|
||||
if (!sentFollowup) {
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: followupRequestId,
|
||||
prompt: FOLLOWUP_PROMPT,
|
||||
})
|
||||
sentFollowup = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "assistant" &&
|
||||
event.requestId === startRequestId &&
|
||||
event.done !== true &&
|
||||
!sawFirstAssistantChunkForStart
|
||||
) {
|
||||
sawFirstAssistantChunkForStart = true
|
||||
if (!sentFollowup) {
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: followupRequestId,
|
||||
prompt: FOLLOWUP_PROMPT,
|
||||
})
|
||||
sentFollowup = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "tool_result" &&
|
||||
event.requestId === followupRequestId &&
|
||||
typeof event.content === "string" &&
|
||||
event.content.includes("<user_message>")
|
||||
) {
|
||||
sawMisroutedToolResult = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "user" && event.requestId === followupRequestId) {
|
||||
sawFollowupUserTurn = typeof event.content === "string" && event.content.includes("3+3")
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "result" && event.done === true && event.requestId === startRequestId && !sentFollowup) {
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: followupRequestId,
|
||||
prompt: FOLLOWUP_PROMPT,
|
||||
})
|
||||
sentFollowup = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type !== "result" || event.done !== true || event.requestId !== followupRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
followupResult = event.content ?? ""
|
||||
validateFollowupResult(followupResult)
|
||||
|
||||
if (sawMisroutedToolResult) {
|
||||
throw new Error("follow-up message was misrouted into tool_result (<user_message>), old bug reproduced")
|
||||
}
|
||||
|
||||
if (!sawFollowupUserTurn) {
|
||||
throw new Error("follow-up did not appear as a normal user turn in stream output")
|
||||
}
|
||||
|
||||
console.log(`[PASS] saw attempt_completion tool use: ${sawAttemptCompletion}`)
|
||||
console.log(`[PASS] saw start assistant chunk before follow-up: ${sawFirstAssistantChunkForStart}`)
|
||||
console.log(`[PASS] follow-up user turn observed: ${sawFollowupUserTurn}`)
|
||||
console.log(`[PASS] follow-up result: "${followupResult}"`)
|
||||
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return [
|
||||
"timed out waiting for follow-up validation",
|
||||
`initSeen=${initSeen}`,
|
||||
`sentFollowup=${sentFollowup}`,
|
||||
`sawAttemptCompletion=${sawAttemptCompletion}`,
|
||||
`sawFirstAssistantChunkForStart=${sawFirstAssistantChunkForStart}`,
|
||||
`sawFollowupUserTurn=${sawFollowupUserTurn}`,
|
||||
`sawMisroutedToolResult=${sawMisroutedToolResult}`,
|
||||
`haveFollowupResult=${Boolean(followupResult)}`,
|
||||
].join(" ")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const LONG_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 20 && echo "done". After it finishes, reply with exactly "done".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const messageRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
const testImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
||||
|
||||
let initSeen = false
|
||||
let startAccepted = false
|
||||
let messageAccepted = false
|
||||
let messageQueued = false
|
||||
let queueImageCountObserved = false
|
||||
let shutdownSent = false
|
||||
let shutdownAck = false
|
||||
let shutdownDone = false
|
||||
|
||||
await runStreamCase({
|
||||
timeoutMs: 180_000,
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({ command: "start", requestId: startRequestId, prompt: LONG_PROMPT })
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === startRequestId &&
|
||||
!startAccepted
|
||||
) {
|
||||
startAccepted = true
|
||||
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: messageRequestId,
|
||||
prompt: "Respond with exactly IMAGE-QUEUED when this message is processed.",
|
||||
images: [testImage],
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === messageRequestId
|
||||
) {
|
||||
messageAccepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === messageRequestId &&
|
||||
event.code === "queued"
|
||||
) {
|
||||
messageQueued = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "queue" &&
|
||||
(event.subtype === "snapshot" || event.subtype === "enqueued" || event.subtype === "updated") &&
|
||||
Array.isArray(event.queue) &&
|
||||
event.queue.some((item) => item?.imageCount === 1)
|
||||
) {
|
||||
queueImageCountObserved = true
|
||||
|
||||
if (!shutdownSent) {
|
||||
context.sendCommand({ command: "shutdown", requestId: shutdownRequestId })
|
||||
shutdownSent = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "shutdown" &&
|
||||
event.requestId === shutdownRequestId
|
||||
) {
|
||||
shutdownAck = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "shutdown" &&
|
||||
event.requestId === shutdownRequestId
|
||||
) {
|
||||
shutdownDone = true
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for queue image metadata (initSeen=${initSeen}, startAccepted=${startAccepted}, messageAccepted=${messageAccepted}, messageQueued=${messageQueued}, queueImageCountObserved=${queueImageCountObserved}, shutdownSent=${shutdownSent}, shutdownAck=${shutdownAck}, shutdownDone=${shutdownDone})`
|
||||
},
|
||||
})
|
||||
|
||||
if (!messageAccepted || !messageQueued || !queueImageCountObserved) {
|
||||
throw new Error(
|
||||
`expected queued message with image metadata (messageAccepted=${messageAccepted}, messageQueued=${messageQueued}, queueImageCountObserved=${queueImageCountObserved})`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!shutdownAck || !shutdownDone) {
|
||||
throw new Error("shutdown control events were not fully observed")
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
async function main() {
|
||||
const messageRequestId = `message-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
let initSeen = false
|
||||
let sawNoActiveTaskError = false
|
||||
let sentShutdown = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: messageRequestId,
|
||||
prompt: "Hello",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "error" &&
|
||||
event.requestId === messageRequestId &&
|
||||
event.code === "no_active_task"
|
||||
) {
|
||||
sawNoActiveTaskError = true
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for no_active_task error (initSeen=${initSeen}, sawNoActiveTaskError=${sawNoActiveTaskError})`
|
||||
},
|
||||
})
|
||||
|
||||
if (!sawNoActiveTaskError) {
|
||||
throw new Error("expected no_active_task error was not observed")
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
148
apps/cli/scripts/integration/cases/mixed-command-ordering.ts
Normal file
148
apps/cli/scripts/integration/cases/mixed-command-ordering.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const START_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 8 && echo "done". After it finishes, reply with exactly "done".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const pingARequestId = `ping-a-${Date.now()}`
|
||||
const messageRequestId = `message-${Date.now()}`
|
||||
const pingBRequestId = `ping-b-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let sentInterleavedCommands = false
|
||||
let sentShutdown = false
|
||||
|
||||
const eventOrderByRequestId = new Map<string, string[]>()
|
||||
let messageDoneCode: string | undefined
|
||||
let messageQueueEnqueuedSeen = false
|
||||
let messageResultSeen = false
|
||||
|
||||
function recordControlEvent(event: StreamEvent): void {
|
||||
if (!event.requestId || event.type !== "control" || !event.subtype) {
|
||||
return
|
||||
}
|
||||
const existing = eventOrderByRequestId.get(event.requestId) ?? []
|
||||
existing.push(event.subtype)
|
||||
eventOrderByRequestId.set(event.requestId, existing)
|
||||
}
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: START_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
recordControlEvent(event)
|
||||
|
||||
if (event.type === "control" && event.subtype === "error") {
|
||||
throw new Error(
|
||||
`received control error for requestId=${event.requestId ?? "unknown"} command=${event.command ?? "unknown"} code=${event.code ?? "unknown"} content=${event.content ?? ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!sentInterleavedCommands &&
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === startRequestId
|
||||
) {
|
||||
context.sendCommand({
|
||||
command: "ping",
|
||||
requestId: pingARequestId,
|
||||
})
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: messageRequestId,
|
||||
prompt: 'When this queued message is processed, reply with only "INTERLEAVED".',
|
||||
})
|
||||
context.sendCommand({
|
||||
command: "ping",
|
||||
requestId: pingBRequestId,
|
||||
})
|
||||
sentInterleavedCommands = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === messageRequestId
|
||||
) {
|
||||
messageDoneCode = event.code
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "queue" &&
|
||||
event.subtype === "enqueued" &&
|
||||
event.requestId === startRequestId &&
|
||||
event.queueDepth === 1
|
||||
) {
|
||||
messageQueueEnqueuedSeen = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "result" && event.done === true && event.requestId === messageRequestId) {
|
||||
messageResultSeen = true
|
||||
|
||||
const pingAOrder = eventOrderByRequestId.get(pingARequestId) ?? []
|
||||
const pingBOrder = eventOrderByRequestId.get(pingBRequestId) ?? []
|
||||
const messageOrder = eventOrderByRequestId.get(messageRequestId) ?? []
|
||||
|
||||
if (pingAOrder.join(",") !== "ack,done") {
|
||||
throw new Error(`ping A control order mismatch: ${pingAOrder.join(",") || "none"}`)
|
||||
}
|
||||
if (pingBOrder.join(",") !== "ack,done") {
|
||||
throw new Error(`ping B control order mismatch: ${pingBOrder.join(",") || "none"}`)
|
||||
}
|
||||
if (messageOrder.join(",") !== "ack,done") {
|
||||
throw new Error(`message control order mismatch: ${messageOrder.join(",") || "none"}`)
|
||||
}
|
||||
if (messageDoneCode !== "queued") {
|
||||
throw new Error(
|
||||
`expected interleaved message done code \"queued\", got \"${messageDoneCode ?? "none"}\"`,
|
||||
)
|
||||
}
|
||||
if (!messageQueueEnqueuedSeen) {
|
||||
throw new Error("expected queue enqueued event after interleaved message")
|
||||
}
|
||||
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return [
|
||||
"timed out waiting for mixed-command-ordering validation",
|
||||
`initSeen=${initSeen}`,
|
||||
`sentInterleavedCommands=${sentInterleavedCommands}`,
|
||||
`messageDoneCode=${messageDoneCode ?? "none"}`,
|
||||
`messageQueueEnqueuedSeen=${messageQueueEnqueuedSeen}`,
|
||||
`messageResultSeen=${messageResultSeen}`,
|
||||
`pingAOrder=${(eventOrderByRequestId.get(pingARequestId) ?? []).join(",") || "none"}`,
|
||||
`messageOrder=${(eventOrderByRequestId.get(messageRequestId) ?? []).join(",") || "none"}`,
|
||||
`pingBOrder=${(eventOrderByRequestId.get(pingBRequestId) ?? []).join(",") || "none"}`,
|
||||
].join(" ")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
184
apps/cli/scripts/integration/cases/multi-message-queue-order.ts
Normal file
184
apps/cli/scripts/integration/cases/multi-message-queue-order.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const LONG_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 6 && echo "done". After it finishes, reply with exactly "done".'
|
||||
const MESSAGE_ONE_PROMPT = 'For this follow-up, reply with only "ALPHA".'
|
||||
const MESSAGE_TWO_PROMPT = 'For this follow-up, reply with only "BETA".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const firstMessageRequestId = `message-a-${Date.now()}`
|
||||
const secondMessageRequestId = `message-b-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let startAccepted = false
|
||||
let sentQueuedMessages = false
|
||||
let sentShutdown = false
|
||||
|
||||
let firstMessageAccepted = false
|
||||
let secondMessageAccepted = false
|
||||
let firstMessageQueued = false
|
||||
let secondMessageQueued = false
|
||||
|
||||
const resultOrder: string[] = []
|
||||
let queueDequeuedByFirst = false
|
||||
let queueDrainedBySecond = false
|
||||
let firstResultSeen = false
|
||||
let secondResultSeen = false
|
||||
|
||||
await runStreamCase({
|
||||
timeoutMs: 180_000,
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: LONG_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === startRequestId &&
|
||||
!startAccepted
|
||||
) {
|
||||
startAccepted = true
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: firstMessageRequestId,
|
||||
prompt: MESSAGE_ONE_PROMPT,
|
||||
})
|
||||
context.sendCommand({
|
||||
command: "message",
|
||||
requestId: secondMessageRequestId,
|
||||
prompt: MESSAGE_TWO_PROMPT,
|
||||
})
|
||||
sentQueuedMessages = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === firstMessageRequestId
|
||||
) {
|
||||
firstMessageAccepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === secondMessageRequestId
|
||||
) {
|
||||
secondMessageAccepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === firstMessageRequestId &&
|
||||
event.code === "queued"
|
||||
) {
|
||||
firstMessageQueued = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "message" &&
|
||||
event.requestId === secondMessageRequestId &&
|
||||
event.code === "queued"
|
||||
) {
|
||||
secondMessageQueued = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "queue" &&
|
||||
event.subtype === "dequeued" &&
|
||||
event.requestId === firstMessageRequestId &&
|
||||
event.queueDepth === 1
|
||||
) {
|
||||
queueDequeuedByFirst = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "queue" &&
|
||||
event.subtype === "drained" &&
|
||||
event.requestId === secondMessageRequestId &&
|
||||
event.queueDepth === 0
|
||||
) {
|
||||
queueDrainedBySecond = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "result" && event.done === true) {
|
||||
if (event.requestId === firstMessageRequestId) {
|
||||
firstResultSeen = true
|
||||
resultOrder.push(firstMessageRequestId)
|
||||
}
|
||||
if (event.requestId === secondMessageRequestId) {
|
||||
secondResultSeen = true
|
||||
resultOrder.push(secondMessageRequestId)
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstResultSeen || !secondResultSeen || sentShutdown) {
|
||||
return
|
||||
}
|
||||
|
||||
const expectedOrder = [firstMessageRequestId, secondMessageRequestId].join(",")
|
||||
if (resultOrder.join(",") !== expectedOrder) {
|
||||
throw new Error(
|
||||
`queued message result order mismatch; expected=${expectedOrder} actual=${resultOrder.join(",")}`,
|
||||
)
|
||||
}
|
||||
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for queued message order validation (initSeen=${initSeen}, startAccepted=${startAccepted}, sentQueuedMessages=${sentQueuedMessages}, firstMessageAccepted=${firstMessageAccepted}, secondMessageAccepted=${secondMessageAccepted}, firstMessageQueued=${firstMessageQueued}, secondMessageQueued=${secondMessageQueued}, queueDequeuedByFirst=${queueDequeuedByFirst}, queueDrainedBySecond=${queueDrainedBySecond}, resultOrder=${resultOrder.join(" -> ")}, firstResultSeen=${firstResultSeen}, secondResultSeen=${secondResultSeen})`
|
||||
},
|
||||
})
|
||||
|
||||
if (
|
||||
!firstMessageAccepted ||
|
||||
!secondMessageAccepted ||
|
||||
!firstMessageQueued ||
|
||||
!secondMessageQueued ||
|
||||
!queueDequeuedByFirst ||
|
||||
!queueDrainedBySecond
|
||||
) {
|
||||
throw new Error(
|
||||
`expected both queued messages to be accepted/queued and queue transitions observed (firstMessageAccepted=${firstMessageAccepted}, secondMessageAccepted=${secondMessageAccepted}, firstMessageQueued=${firstMessageQueued}, secondMessageQueued=${secondMessageQueued}, queueDequeuedByFirst=${queueDequeuedByFirst}, queueDrainedBySecond=${queueDrainedBySecond})`,
|
||||
)
|
||||
}
|
||||
|
||||
const expectedOrder = [firstMessageRequestId, secondMessageRequestId].join(",")
|
||||
if (resultOrder.join(",") !== expectedOrder) {
|
||||
throw new Error(
|
||||
`queued message result order mismatch; expected=${expectedOrder} actual=${resultOrder.join(",")}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
76
apps/cli/scripts/integration/cases/shutdown-while-running.ts
Normal file
76
apps/cli/scripts/integration/cases/shutdown-while-running.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const LONG_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 20 && echo "done". After it finishes, reply with exactly "done".'
|
||||
|
||||
async function main() {
|
||||
const startRequestId = `start-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let startAccepted = false
|
||||
let shutdownSent = false
|
||||
let shutdownAck = false
|
||||
let shutdownDone = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: startRequestId,
|
||||
prompt: LONG_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === startRequestId &&
|
||||
!startAccepted
|
||||
) {
|
||||
startAccepted = true
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
shutdownSent = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "shutdown" &&
|
||||
event.requestId === shutdownRequestId
|
||||
) {
|
||||
shutdownAck = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "done" &&
|
||||
event.command === "shutdown" &&
|
||||
event.requestId === shutdownRequestId
|
||||
) {
|
||||
shutdownDone = true
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for shutdown flow (initSeen=${initSeen}, startAccepted=${startAccepted}, shutdownSent=${shutdownSent}, shutdownAck=${shutdownAck}, shutdownDone=${shutdownDone})`
|
||||
},
|
||||
})
|
||||
|
||||
if (!shutdownAck || !shutdownDone) {
|
||||
throw new Error("shutdown control events were not fully observed")
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
77
apps/cli/scripts/integration/cases/start-while-busy.ts
Normal file
77
apps/cli/scripts/integration/cases/start-while-busy.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { runStreamCase, StreamEvent } from "../lib/stream-harness"
|
||||
|
||||
const LONG_PROMPT =
|
||||
'Run exactly this command and do not summarize until it finishes: sleep 8 && echo "done". After it finishes, reply with exactly "done".'
|
||||
|
||||
async function main() {
|
||||
const firstStartRequestId = `start-a-${Date.now()}`
|
||||
const secondStartRequestId = `start-b-${Date.now()}`
|
||||
const shutdownRequestId = `shutdown-${Date.now()}`
|
||||
|
||||
let initSeen = false
|
||||
let firstStartAccepted = false
|
||||
let secondStartSent = false
|
||||
let sawTaskBusyError = false
|
||||
let sentShutdown = false
|
||||
|
||||
await runStreamCase({
|
||||
onEvent(event: StreamEvent, context) {
|
||||
if (event.type === "system" && event.subtype === "init" && !initSeen) {
|
||||
initSeen = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: firstStartRequestId,
|
||||
prompt: LONG_PROMPT,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "ack" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === firstStartRequestId &&
|
||||
!firstStartAccepted
|
||||
) {
|
||||
firstStartAccepted = true
|
||||
context.sendCommand({
|
||||
command: "start",
|
||||
requestId: secondStartRequestId,
|
||||
prompt: "What is 1+1? Reply with only 2.",
|
||||
})
|
||||
secondStartSent = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "control" &&
|
||||
event.subtype === "error" &&
|
||||
event.command === "start" &&
|
||||
event.requestId === secondStartRequestId &&
|
||||
event.code === "task_busy"
|
||||
) {
|
||||
sawTaskBusyError = true
|
||||
if (!sentShutdown) {
|
||||
context.sendCommand({
|
||||
command: "shutdown",
|
||||
requestId: shutdownRequestId,
|
||||
})
|
||||
sentShutdown = true
|
||||
}
|
||||
return
|
||||
}
|
||||
},
|
||||
onTimeoutMessage() {
|
||||
return `timed out waiting for task_busy error (initSeen=${initSeen}, firstStartAccepted=${firstStartAccepted}, secondStartSent=${secondStartSent}, sawTaskBusyError=${sawTaskBusyError})`
|
||||
},
|
||||
})
|
||||
|
||||
if (!sawTaskBusyError) {
|
||||
throw new Error("expected task_busy error for second start command was not observed")
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
152
apps/cli/scripts/integration/lib/stream-harness.ts
Normal file
152
apps/cli/scripts/integration/lib/stream-harness.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import readline from "readline"
|
||||
|
||||
import { execa } from "execa"
|
||||
|
||||
export type StreamEvent = {
|
||||
type?: string
|
||||
subtype?: string
|
||||
requestId?: string
|
||||
command?: string
|
||||
content?: string
|
||||
code?: string
|
||||
success?: boolean
|
||||
done?: boolean
|
||||
id?: number
|
||||
queueDepth?: number
|
||||
queue?: Array<{ id?: string; text?: string; imageCount?: number; timestamp?: number }>
|
||||
tool_use?: {
|
||||
name?: string
|
||||
input?: Record<string, unknown>
|
||||
}
|
||||
tool_result?: {
|
||||
name?: string
|
||||
output?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type StreamCommand = {
|
||||
command: "start" | "message" | "cancel" | "ping" | "shutdown"
|
||||
requestId: string
|
||||
prompt?: string
|
||||
images?: string[]
|
||||
}
|
||||
|
||||
export interface StreamCaseContext {
|
||||
readonly cliRoot: string
|
||||
readonly timeoutMs: number
|
||||
nextRequestId(prefix: string): string
|
||||
sendCommand(command: StreamCommand): void
|
||||
}
|
||||
|
||||
export interface RunStreamCaseOptions {
|
||||
timeoutMs?: number
|
||||
onEvent: (event: StreamEvent, context: StreamCaseContext) => void
|
||||
onTimeoutMessage?: (context: StreamCaseContext) => string
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const defaultCliRoot = path.resolve(__dirname, "../../..")
|
||||
|
||||
function parseEvent(line: string): StreamEvent | null {
|
||||
const trimmed = line.trim()
|
||||
|
||||
if (!trimmed.startsWith("{")) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed) as StreamEvent
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function runStreamCase(options: RunStreamCaseOptions): Promise<void> {
|
||||
const cliRoot = process.env.ROO_CLI_ROOT ? path.resolve(process.env.ROO_CLI_ROOT) : defaultCliRoot
|
||||
const timeoutMs = options.timeoutMs ?? 120_000
|
||||
|
||||
const child = execa(
|
||||
"pnpm",
|
||||
["dev", "--print", "--stdin-prompt-stream", "--provider", "openrouter", "--output-format", "stream-json"],
|
||||
{
|
||||
cwd: cliRoot,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
reject: false,
|
||||
forceKillAfterDelay: 2_000,
|
||||
},
|
||||
)
|
||||
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
process.stderr.write(chunk)
|
||||
})
|
||||
|
||||
let requestCounter = 0
|
||||
|
||||
const context: StreamCaseContext = {
|
||||
cliRoot,
|
||||
timeoutMs,
|
||||
nextRequestId(prefix: string): string {
|
||||
requestCounter += 1
|
||||
return `${prefix}-${Date.now()}-${requestCounter}`
|
||||
},
|
||||
sendCommand(command: StreamCommand): void {
|
||||
if (child.stdin?.destroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
child.stdin.write(`${JSON.stringify(command)}\n`)
|
||||
},
|
||||
}
|
||||
|
||||
let handlerError: Error | null = null
|
||||
let timedOut = false
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true
|
||||
const message = options.onTimeoutMessage?.(context) ?? "timed out waiting for stream scenario completion"
|
||||
handlerError = new Error(message)
|
||||
child.kill("SIGTERM")
|
||||
}, timeoutMs)
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: child.stdout!,
|
||||
crlfDelay: Infinity,
|
||||
})
|
||||
|
||||
rl.on("line", (line) => {
|
||||
process.stdout.write(`${line}\n`)
|
||||
|
||||
const event = parseEvent(line)
|
||||
|
||||
if (!event) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
options.onEvent(event, context)
|
||||
} catch (error) {
|
||||
handlerError = error instanceof Error ? error : new Error(String(error))
|
||||
child.kill("SIGTERM")
|
||||
}
|
||||
})
|
||||
|
||||
const result = await child
|
||||
clearTimeout(timeout)
|
||||
rl.close()
|
||||
|
||||
if (handlerError) {
|
||||
throw handlerError
|
||||
}
|
||||
|
||||
if (timedOut) {
|
||||
throw new Error("stream scenario timed out")
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`CLI exited with non-zero code: ${result.exitCode}`)
|
||||
}
|
||||
}
|
||||
111
apps/cli/scripts/integration/run.ts
Normal file
111
apps/cli/scripts/integration/run.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import { execa } from "execa"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const cliRoot = path.resolve(__dirname, "../..")
|
||||
const casesDir = path.resolve(__dirname, "cases")
|
||||
|
||||
interface RunnerOptions {
|
||||
listOnly: boolean
|
||||
match?: string
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): RunnerOptions {
|
||||
let listOnly = false
|
||||
let match: string | undefined
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i]
|
||||
if (arg === "--list") {
|
||||
listOnly = true
|
||||
continue
|
||||
}
|
||||
if (arg === "--match") {
|
||||
match = argv[i + 1]
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return { listOnly, match }
|
||||
}
|
||||
|
||||
async function discoverCaseFiles(match?: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(casesDir, { withFileTypes: true })
|
||||
const files = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
|
||||
.map((entry) => path.resolve(casesDir, entry.name))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
|
||||
if (!match) {
|
||||
return files
|
||||
}
|
||||
|
||||
const normalized = match.toLowerCase()
|
||||
return files.filter((file) => path.basename(file).toLowerCase().includes(normalized))
|
||||
}
|
||||
|
||||
async function runCase(caseFile: string): Promise<void> {
|
||||
const caseName = path.basename(caseFile, ".ts")
|
||||
console.log(`\n[RUN] ${caseName}`)
|
||||
|
||||
await execa("tsx", [caseFile], {
|
||||
cwd: cliRoot,
|
||||
stdio: "inherit",
|
||||
reject: true,
|
||||
env: {
|
||||
...process.env,
|
||||
ROO_CLI_ROOT: cliRoot,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`[PASS] ${caseName}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2))
|
||||
const caseFiles = await discoverCaseFiles(options.match)
|
||||
|
||||
if (caseFiles.length === 0) {
|
||||
throw new Error(
|
||||
options.match ? `no integration cases matched --match "${options.match}"` : "no integration cases found",
|
||||
)
|
||||
}
|
||||
|
||||
if (options.listOnly) {
|
||||
console.log("Available integration cases:")
|
||||
for (const file of caseFiles) {
|
||||
console.log(`- ${path.basename(file, ".ts")}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const failures: Array<{ caseName: string; error: string }> = []
|
||||
|
||||
for (const caseFile of caseFiles) {
|
||||
const caseName = path.basename(caseFile, ".ts")
|
||||
try {
|
||||
await runCase(caseFile)
|
||||
} catch (error) {
|
||||
const errorText = error instanceof Error ? error.message : String(error)
|
||||
failures.push({ caseName, error: errorText })
|
||||
console.error(`[FAIL] ${caseName}: ${errorText}`)
|
||||
}
|
||||
}
|
||||
|
||||
const total = caseFiles.length
|
||||
const passed = total - failures.length
|
||||
console.log(`\nSummary: ${passed}/${total} passed`)
|
||||
|
||||
if (failures.length > 0) {
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
35
apps/cli/src/agent/__tests__/events.test.ts
Normal file
35
apps/cli/src/agent/__tests__/events.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { ClineMessage } from "@roo-code/types"
|
||||
|
||||
import { detectAgentState } from "../agent-state.js"
|
||||
import { taskCompleted } from "../events.js"
|
||||
|
||||
function createMessage(overrides: Partial<ClineMessage>): ClineMessage {
|
||||
return { ts: Date.now() + Math.random() * 1000, type: "say", ...overrides }
|
||||
}
|
||||
|
||||
describe("taskCompleted", () => {
|
||||
it("returns true for completion_result", () => {
|
||||
const previous = detectAgentState([createMessage({ type: "say", say: "text", text: "working" })])
|
||||
const current = detectAgentState([createMessage({ type: "ask", ask: "completion_result", partial: false })])
|
||||
|
||||
expect(taskCompleted(previous, current)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns true for resume_completed_task", () => {
|
||||
const previous = detectAgentState([createMessage({ type: "say", say: "text", text: "working" })])
|
||||
const current = detectAgentState([createMessage({ type: "ask", ask: "resume_completed_task", partial: false })])
|
||||
|
||||
expect(taskCompleted(previous, current)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for recoverable idle asks", () => {
|
||||
const previous = detectAgentState([createMessage({ type: "say", say: "text", text: "working" })])
|
||||
const mistakeLimit = detectAgentState([
|
||||
createMessage({ type: "ask", ask: "mistake_limit_reached", partial: false }),
|
||||
])
|
||||
const apiFailed = detectAgentState([createMessage({ type: "ask", ask: "api_req_failed", partial: false })])
|
||||
|
||||
expect(taskCompleted(previous, mistakeLimit)).toBe(false)
|
||||
expect(taskCompleted(previous, apiFailed)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -93,13 +93,6 @@ describe("detectAgentState", () => {
|
|||
expect(state.requiredAction).toBe("answer")
|
||||
})
|
||||
|
||||
it("should detect waiting for browser_action_launch approval", () => {
|
||||
const messages = [createMessage({ type: "ask", ask: "browser_action_launch", partial: false })]
|
||||
const state = detectAgentState(messages)
|
||||
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
|
||||
expect(state.requiredAction).toBe("approve")
|
||||
})
|
||||
|
||||
it("should detect waiting for use_mcp_server approval", () => {
|
||||
const messages = [createMessage({ type: "ask", ask: "use_mcp_server", partial: false })]
|
||||
const state = detectAgentState(messages)
|
||||
|
|
@ -202,7 +195,6 @@ describe("Type Guards", () => {
|
|||
expect(isInteractiveAsk("tool")).toBe(true)
|
||||
expect(isInteractiveAsk("command")).toBe(true)
|
||||
expect(isInteractiveAsk("followup")).toBe(true)
|
||||
expect(isInteractiveAsk("browser_action_launch")).toBe(true)
|
||||
expect(isInteractiveAsk("use_mcp_server")).toBe(true)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import fs from "fs"
|
|||
|
||||
import type { ExtensionMessage, WebviewMessage } from "@roo-code/types"
|
||||
|
||||
import { DEFAULT_FLAGS } from "@/types/index.js"
|
||||
|
||||
import { type ExtensionHostOptions, ExtensionHost } from "../extension-host.js"
|
||||
import { ExtensionClient } from "../extension-client.js"
|
||||
import { AgentLoopState } from "../agent-state.js"
|
||||
|
|
@ -80,13 +82,28 @@ function spyOnPrivate(host: ExtensionHost, method: string) {
|
|||
}
|
||||
|
||||
describe("ExtensionHost", () => {
|
||||
const initialRooCliRuntimeEnv = process.env.ROO_CLI_RUNTIME
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
if (initialRooCliRuntimeEnv === undefined) {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
} else {
|
||||
process.env.ROO_CLI_RUNTIME = initialRooCliRuntimeEnv
|
||||
}
|
||||
// Clean up globals
|
||||
delete (global as Record<string, unknown>).vscode
|
||||
delete (global as Record<string, unknown>).__extensionHost
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (initialRooCliRuntimeEnv === undefined) {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
} else {
|
||||
process.env.ROO_CLI_RUNTIME = initialRooCliRuntimeEnv
|
||||
}
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should store options correctly", () => {
|
||||
const options: ExtensionHostOptions = {
|
||||
|
|
@ -135,6 +152,28 @@ describe("ExtensionHost", () => {
|
|||
expect(getPrivate(host, "promptManager")).toBeDefined()
|
||||
expect(getPrivate(host, "askDispatcher")).toBeDefined()
|
||||
})
|
||||
|
||||
it("should mark process as CLI runtime", () => {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
createTestHost()
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("1")
|
||||
})
|
||||
|
||||
it("should set execaShellPath in initialSettings when terminalShell is provided", () => {
|
||||
const host = createTestHost({ terminalShell: "/bin/bash" })
|
||||
const emitSpy = vi.spyOn(host, "emit")
|
||||
host.markWebviewReady()
|
||||
const updateSettingsCall = emitSpy.mock.calls.find(
|
||||
(call) =>
|
||||
call[0] === "webviewMessage" &&
|
||||
typeof call[1] === "object" &&
|
||||
call[1] !== null &&
|
||||
(call[1] as WebviewMessage).type === "updateSettings",
|
||||
)
|
||||
expect(updateSettingsCall).toBeDefined()
|
||||
const payload = updateSettingsCall?.[1] as WebviewMessage
|
||||
expect(payload.updatedSettings?.execaShellPath).toBe("/bin/bash")
|
||||
})
|
||||
})
|
||||
|
||||
describe("webview provider registration", () => {
|
||||
|
|
@ -215,6 +254,26 @@ describe("ExtensionHost", () => {
|
|||
)
|
||||
expect(updateSettingsCall).toBeDefined()
|
||||
})
|
||||
|
||||
it("should force terminalShellIntegrationDisabled when terminalShell is provided", () => {
|
||||
const host = createTestHost({ terminalShell: "/bin/bash" })
|
||||
const emitSpy = vi.spyOn(host, "emit")
|
||||
|
||||
host.markWebviewReady()
|
||||
|
||||
const updateSettingsCall = emitSpy.mock.calls.find(
|
||||
(call) =>
|
||||
call[0] === "webviewMessage" &&
|
||||
typeof call[1] === "object" &&
|
||||
call[1] !== null &&
|
||||
(call[1] as WebviewMessage).type === "updateSettings",
|
||||
)
|
||||
|
||||
expect(updateSettingsCall).toBeDefined()
|
||||
const payload = updateSettingsCall?.[1] as WebviewMessage
|
||||
expect(payload.type).toBe("updateSettings")
|
||||
expect(payload.updatedSettings?.terminalShellIntegrationDisabled).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -429,6 +488,26 @@ describe("ExtensionHost", () => {
|
|||
|
||||
expect(restoreConsoleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should clear ROO_CLI_RUNTIME on dispose when it was previously unset", async () => {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
host = createTestHost()
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("1")
|
||||
|
||||
await host.dispose()
|
||||
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should restore prior ROO_CLI_RUNTIME value on dispose", async () => {
|
||||
process.env.ROO_CLI_RUNTIME = "preexisting-value"
|
||||
host = createTestHost()
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("1")
|
||||
|
||||
await host.dispose()
|
||||
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("preexisting-value")
|
||||
})
|
||||
})
|
||||
|
||||
describe("runTask", () => {
|
||||
|
|
@ -461,6 +540,37 @@ describe("ExtensionHost", () => {
|
|||
expect(emitSpy).toHaveBeenCalledWith("webviewMessage", { type: "newTask", text: "test prompt" })
|
||||
})
|
||||
|
||||
it("should include taskId when provided", async () => {
|
||||
const host = createTestHost()
|
||||
host.markWebviewReady()
|
||||
|
||||
const emitSpy = vi.spyOn(host, "emit")
|
||||
const client = getPrivate(host, "client") as ExtensionClient
|
||||
|
||||
const taskPromise = host.runTask("test prompt", "task-123")
|
||||
|
||||
const taskCompletedEvent = {
|
||||
success: true,
|
||||
stateInfo: {
|
||||
state: AgentLoopState.IDLE,
|
||||
isWaitingForInput: false,
|
||||
isRunning: false,
|
||||
isStreaming: false,
|
||||
requiredAction: "start_task" as const,
|
||||
description: "Task completed",
|
||||
},
|
||||
}
|
||||
setTimeout(() => client.getEmitter().emit("taskCompleted", taskCompletedEvent), 10)
|
||||
|
||||
await taskPromise
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith("webviewMessage", {
|
||||
type: "newTask",
|
||||
text: "test prompt",
|
||||
taskId: "task-123",
|
||||
})
|
||||
})
|
||||
|
||||
it("should resolve when taskCompleted is emitted on client", async () => {
|
||||
const host = createTestHost()
|
||||
host.markWebviewReady()
|
||||
|
|
@ -484,6 +594,33 @@ describe("ExtensionHost", () => {
|
|||
|
||||
await expect(taskPromise).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it("should send showTaskWithId for resumeTask and resolve on completion", async () => {
|
||||
const host = createTestHost()
|
||||
host.markWebviewReady()
|
||||
|
||||
const emitSpy = vi.spyOn(host, "emit")
|
||||
const client = getPrivate(host, "client") as ExtensionClient
|
||||
|
||||
const taskPromise = host.resumeTask("task-abc")
|
||||
|
||||
const taskCompletedEvent = {
|
||||
success: true,
|
||||
stateInfo: {
|
||||
state: AgentLoopState.IDLE,
|
||||
isWaitingForInput: false,
|
||||
isRunning: false,
|
||||
isStreaming: false,
|
||||
requiredAction: "start_task" as const,
|
||||
description: "Task completed",
|
||||
},
|
||||
}
|
||||
setTimeout(() => client.getEmitter().emit("taskCompleted", taskCompletedEvent), 10)
|
||||
|
||||
await taskPromise
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith("webviewMessage", { type: "showTaskWithId", text: "task-abc" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("initial settings", () => {
|
||||
|
|
@ -494,6 +631,20 @@ describe("ExtensionHost", () => {
|
|||
expect(initialSettings.mode).toBe("architect")
|
||||
})
|
||||
|
||||
it("should use default consecutiveMistakeLimit when not provided", () => {
|
||||
const host = createTestHost()
|
||||
|
||||
const initialSettings = getPrivate<Record<string, unknown>>(host, "initialSettings")
|
||||
expect(initialSettings.consecutiveMistakeLimit).toBe(DEFAULT_FLAGS.consecutiveMistakeLimit)
|
||||
})
|
||||
|
||||
it("should set consecutiveMistakeLimit from options", () => {
|
||||
const host = createTestHost({ consecutiveMistakeLimit: 8 })
|
||||
|
||||
const initialSettings = getPrivate<Record<string, unknown>>(host, "initialSettings")
|
||||
expect(initialSettings.consecutiveMistakeLimit).toBe(8)
|
||||
})
|
||||
|
||||
it("should enable auto-approval in non-interactive mode", () => {
|
||||
const host = createTestHost({ nonInteractive: true })
|
||||
|
||||
|
|
|
|||
170
apps/cli/src/agent/__tests__/json-event-emitter-control.test.ts
Normal file
170
apps/cli/src/agent/__tests__/json-event-emitter-control.test.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { Writable } from "stream"
|
||||
|
||||
import { JsonEventEmitter } from "../json-event-emitter.js"
|
||||
|
||||
function createMockStdout(): { stdout: NodeJS.WriteStream; lines: () => Record<string, unknown>[] } {
|
||||
const chunks: string[] = []
|
||||
|
||||
const writable = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
chunks.push(chunk.toString())
|
||||
callback()
|
||||
},
|
||||
}) as unknown as NodeJS.WriteStream
|
||||
|
||||
// Each write is a JSON line terminated by \n
|
||||
const lines = () =>
|
||||
chunks
|
||||
.join("")
|
||||
.split("\n")
|
||||
.filter((l) => l.length > 0)
|
||||
.map((l) => JSON.parse(l) as Record<string, unknown>)
|
||||
|
||||
return { stdout: writable, lines }
|
||||
}
|
||||
|
||||
describe("JsonEventEmitter control events", () => {
|
||||
describe("emitControl", () => {
|
||||
it("emits an ack event with type control", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
|
||||
emitter.emitControl({
|
||||
subtype: "ack",
|
||||
requestId: "req-1",
|
||||
command: "start",
|
||||
content: "starting task",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(1)
|
||||
expect(output[0]!).toMatchObject({
|
||||
type: "control",
|
||||
subtype: "ack",
|
||||
requestId: "req-1",
|
||||
command: "start",
|
||||
content: "starting task",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
expect(output[0]!.done).toBeUndefined()
|
||||
})
|
||||
|
||||
it("sets done: true for done events", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
|
||||
emitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: "req-2",
|
||||
command: "start",
|
||||
content: "task completed",
|
||||
code: "task_completed",
|
||||
success: true,
|
||||
})
|
||||
|
||||
const output = lines()
|
||||
expect(output[0]!).toMatchObject({ type: "control", subtype: "done", done: true })
|
||||
})
|
||||
|
||||
it("does not set done for error events", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
|
||||
emitter.emitControl({
|
||||
subtype: "error",
|
||||
requestId: "req-3",
|
||||
command: "start",
|
||||
content: "something went wrong",
|
||||
code: "task_error",
|
||||
success: false,
|
||||
})
|
||||
|
||||
const output = lines()
|
||||
expect(output[0]!.done).toBeUndefined()
|
||||
expect(output[0]!.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("requestIdProvider", () => {
|
||||
it("injects requestId from provider when event has none", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({
|
||||
mode: "stream-json",
|
||||
stdout,
|
||||
requestIdProvider: () => "injected-id",
|
||||
})
|
||||
|
||||
emitter.emitControl({ subtype: "ack", content: "test" })
|
||||
|
||||
const output = lines()
|
||||
expect(output[0]!.requestId).toBe("injected-id")
|
||||
})
|
||||
|
||||
it("keeps explicit requestId when provider also returns one", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({
|
||||
mode: "stream-json",
|
||||
stdout,
|
||||
requestIdProvider: () => "provider-id",
|
||||
})
|
||||
|
||||
emitter.emitControl({ subtype: "ack", requestId: "explicit-id", content: "test" })
|
||||
|
||||
const output = lines()
|
||||
expect(output[0]!.requestId).toBe("explicit-id")
|
||||
})
|
||||
|
||||
it("omits requestId when provider returns undefined and event has none", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({
|
||||
mode: "stream-json",
|
||||
stdout,
|
||||
requestIdProvider: () => undefined,
|
||||
})
|
||||
|
||||
emitter.emitControl({ subtype: "ack", content: "test" })
|
||||
|
||||
const output = lines()
|
||||
expect(output[0]!).not.toHaveProperty("requestId")
|
||||
})
|
||||
})
|
||||
|
||||
describe("emitInit", () => {
|
||||
it("emits system init with default schema values", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
|
||||
// emitInit requires a client — we call emitControl to test init-like fields instead.
|
||||
// emitInit is called internally by attach(), so we test the init fields via options.
|
||||
// Instead, directly verify the constructor defaults by emitting a control event
|
||||
// and checking that the emitter was created with correct defaults.
|
||||
|
||||
// We can't call emitInit without a client, but we can verify the options
|
||||
// were stored correctly by checking what emitControl produces.
|
||||
emitter.emitControl({ subtype: "ack", content: "test" })
|
||||
|
||||
// The control event itself doesn't include schema fields, but at least
|
||||
// we verify the emitter was constructed successfully with defaults.
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("accepts custom schemaVersion, protocol, and capabilities", () => {
|
||||
const { stdout } = createMockStdout()
|
||||
|
||||
// Should not throw when constructed with custom values
|
||||
const emitter = new JsonEventEmitter({
|
||||
mode: "stream-json",
|
||||
stdout,
|
||||
schemaVersion: 2,
|
||||
protocol: "custom-protocol",
|
||||
capabilities: ["stdin:start", "stdin:message"],
|
||||
})
|
||||
|
||||
expect(emitter).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
129
apps/cli/src/agent/__tests__/json-event-emitter-result.test.ts
Normal file
129
apps/cli/src/agent/__tests__/json-event-emitter-result.test.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import type { ClineMessage } from "@roo-code/types"
|
||||
import { Writable } from "stream"
|
||||
|
||||
import type { TaskCompletedEvent } from "../events.js"
|
||||
import { JsonEventEmitter } from "../json-event-emitter.js"
|
||||
import { AgentLoopState, type AgentStateInfo } from "../agent-state.js"
|
||||
|
||||
function createMockStdout(): { stdout: NodeJS.WriteStream; lines: () => Record<string, unknown>[] } {
|
||||
const chunks: string[] = []
|
||||
|
||||
const writable = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
chunks.push(chunk.toString())
|
||||
callback()
|
||||
},
|
||||
}) as unknown as NodeJS.WriteStream
|
||||
|
||||
const lines = () =>
|
||||
chunks
|
||||
.join("")
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0)
|
||||
.map((line) => JSON.parse(line) as Record<string, unknown>)
|
||||
|
||||
return { stdout: writable, lines }
|
||||
}
|
||||
|
||||
function emitMessage(emitter: JsonEventEmitter, message: ClineMessage): void {
|
||||
;(emitter as unknown as { handleMessage: (msg: ClineMessage, isUpdate: boolean) => void }).handleMessage(
|
||||
message,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
function emitTaskCompleted(emitter: JsonEventEmitter, event: TaskCompletedEvent): void {
|
||||
;(emitter as unknown as { handleTaskCompleted: (taskCompleted: TaskCompletedEvent) => void }).handleTaskCompleted(
|
||||
event,
|
||||
)
|
||||
}
|
||||
|
||||
function createAskCompletionMessage(ts: number, text = ""): ClineMessage {
|
||||
return {
|
||||
ts,
|
||||
type: "ask",
|
||||
ask: "completion_result",
|
||||
partial: false,
|
||||
text,
|
||||
} as ClineMessage
|
||||
}
|
||||
|
||||
function createCompletedStateInfo(message: ClineMessage): AgentStateInfo {
|
||||
return {
|
||||
state: AgentLoopState.IDLE,
|
||||
isWaitingForInput: true,
|
||||
isRunning: false,
|
||||
isStreaming: false,
|
||||
currentAsk: "completion_result",
|
||||
requiredAction: "start_task",
|
||||
lastMessageTs: message.ts,
|
||||
lastMessage: message,
|
||||
description: "Task completed successfully. You can provide feedback or start a new task.",
|
||||
}
|
||||
}
|
||||
|
||||
describe("JsonEventEmitter result emission", () => {
|
||||
it("prefers current completion message content over stale cached completion text", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
|
||||
emitMessage(emitter, {
|
||||
ts: 100,
|
||||
type: "say",
|
||||
say: "completion_result",
|
||||
partial: false,
|
||||
text: "FIRST",
|
||||
} as ClineMessage)
|
||||
|
||||
const firstCompletionMessage = createAskCompletionMessage(101, "")
|
||||
emitTaskCompleted(emitter, {
|
||||
success: true,
|
||||
stateInfo: createCompletedStateInfo(firstCompletionMessage),
|
||||
message: firstCompletionMessage,
|
||||
})
|
||||
|
||||
const secondCompletionMessage = createAskCompletionMessage(102, "SECOND")
|
||||
emitTaskCompleted(emitter, {
|
||||
success: true,
|
||||
stateInfo: createCompletedStateInfo(secondCompletionMessage),
|
||||
message: secondCompletionMessage,
|
||||
})
|
||||
|
||||
const output = lines().filter((line) => line.type === "result")
|
||||
expect(output).toHaveLength(2)
|
||||
expect(output[0]?.content).toBe("FIRST")
|
||||
expect(output[1]?.content).toBe("SECOND")
|
||||
})
|
||||
|
||||
it("clears cached completion text after each result emission", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
|
||||
emitMessage(emitter, {
|
||||
ts: 200,
|
||||
type: "say",
|
||||
say: "completion_result",
|
||||
partial: false,
|
||||
text: "FIRST",
|
||||
} as ClineMessage)
|
||||
|
||||
const firstCompletionMessage = createAskCompletionMessage(201, "")
|
||||
emitTaskCompleted(emitter, {
|
||||
success: true,
|
||||
stateInfo: createCompletedStateInfo(firstCompletionMessage),
|
||||
message: firstCompletionMessage,
|
||||
})
|
||||
|
||||
const secondCompletionMessage = createAskCompletionMessage(202, "")
|
||||
emitTaskCompleted(emitter, {
|
||||
success: true,
|
||||
stateInfo: createCompletedStateInfo(secondCompletionMessage),
|
||||
message: secondCompletionMessage,
|
||||
})
|
||||
|
||||
const output = lines().filter((line) => line.type === "result")
|
||||
expect(output).toHaveLength(2)
|
||||
expect(output[0]?.content).toBe("FIRST")
|
||||
expect(output[1]).not.toHaveProperty("content")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
import type { ClineMessage } from "@roo-code/types"
|
||||
import { Writable } from "stream"
|
||||
|
||||
import { JsonEventEmitter } from "../json-event-emitter.js"
|
||||
|
||||
function createMockStdout(): { stdout: NodeJS.WriteStream; lines: () => Record<string, unknown>[] } {
|
||||
const chunks: string[] = []
|
||||
|
||||
const writable = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
chunks.push(chunk.toString())
|
||||
callback()
|
||||
},
|
||||
}) as unknown as NodeJS.WriteStream
|
||||
|
||||
const lines = () =>
|
||||
chunks
|
||||
.join("")
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0)
|
||||
.map((line) => JSON.parse(line) as Record<string, unknown>)
|
||||
|
||||
return { stdout: writable, lines }
|
||||
}
|
||||
|
||||
function emitMessage(emitter: JsonEventEmitter, message: ClineMessage): void {
|
||||
;(emitter as unknown as { handleMessage: (msg: ClineMessage, isUpdate: boolean) => void }).handleMessage(
|
||||
message,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
function createAskMessage(overrides: Partial<ClineMessage>): ClineMessage {
|
||||
return {
|
||||
ts: 1,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
partial: true,
|
||||
text: "",
|
||||
...overrides,
|
||||
} as ClineMessage
|
||||
}
|
||||
|
||||
describe("JsonEventEmitter streaming deltas", () => {
|
||||
it("streams ask:command partial updates as deltas and emits full final snapshot", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
const id = 101
|
||||
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "command",
|
||||
partial: true,
|
||||
text: "g",
|
||||
}),
|
||||
)
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "command",
|
||||
partial: true,
|
||||
text: "gh",
|
||||
}),
|
||||
)
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "command",
|
||||
partial: true,
|
||||
text: "gh pr",
|
||||
}),
|
||||
)
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "command",
|
||||
partial: false,
|
||||
text: "gh pr",
|
||||
}),
|
||||
)
|
||||
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(4)
|
||||
expect(output[0]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id,
|
||||
subtype: "command",
|
||||
content: "g",
|
||||
tool_use: { name: "execute_command", input: { command: "g" } },
|
||||
})
|
||||
expect(output[1]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id,
|
||||
subtype: "command",
|
||||
content: "h",
|
||||
tool_use: { name: "execute_command", input: { command: "h" } },
|
||||
})
|
||||
expect(output[2]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id,
|
||||
subtype: "command",
|
||||
content: " pr",
|
||||
tool_use: { name: "execute_command", input: { command: " pr" } },
|
||||
})
|
||||
expect(output[3]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id,
|
||||
subtype: "command",
|
||||
tool_use: { name: "execute_command", input: { command: "gh pr" } },
|
||||
done: true,
|
||||
})
|
||||
expect(output[3]).not.toHaveProperty("content")
|
||||
})
|
||||
|
||||
it("streams ask:tool snapshots as structured deltas and preserves full final payload", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
const id = 202
|
||||
const first = JSON.stringify({ tool: "readFile", path: "a" })
|
||||
const second = JSON.stringify({ tool: "readFile", path: "ab" })
|
||||
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "tool",
|
||||
partial: true,
|
||||
text: first,
|
||||
}),
|
||||
)
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "tool",
|
||||
partial: true,
|
||||
text: second,
|
||||
}),
|
||||
)
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "tool",
|
||||
partial: false,
|
||||
text: second,
|
||||
}),
|
||||
)
|
||||
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(3)
|
||||
expect(output[0]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id,
|
||||
subtype: "tool",
|
||||
content: first,
|
||||
tool_use: { name: "readFile" },
|
||||
})
|
||||
expect(output[1]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id,
|
||||
subtype: "tool",
|
||||
content: "b",
|
||||
tool_use: { name: "readFile" },
|
||||
})
|
||||
expect(output[2]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id,
|
||||
subtype: "tool",
|
||||
tool_use: { name: "readFile", input: { tool: "readFile", path: "ab" } },
|
||||
done: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("suppresses duplicate partial tool snapshots with no delta", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
const id = 303
|
||||
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "command",
|
||||
partial: true,
|
||||
text: "gh",
|
||||
}),
|
||||
)
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "command",
|
||||
partial: true,
|
||||
text: "gh",
|
||||
}),
|
||||
)
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: id,
|
||||
ask: "command",
|
||||
partial: true,
|
||||
text: "gh pr",
|
||||
}),
|
||||
)
|
||||
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(2)
|
||||
expect(output[0]).toMatchObject({ content: "gh" })
|
||||
expect(output[1]).toMatchObject({ content: " pr" })
|
||||
})
|
||||
|
||||
it("streams say:command_output as deltas and correlates tool_result id to execute_command", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
const commandId = 404
|
||||
const outputTs = 405
|
||||
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: commandId,
|
||||
ask: "command",
|
||||
partial: false,
|
||||
text: "echo hello",
|
||||
}),
|
||||
)
|
||||
|
||||
emitMessage(emitter, {
|
||||
ts: outputTs,
|
||||
type: "say",
|
||||
say: "command_output",
|
||||
partial: true,
|
||||
text: "line1\n",
|
||||
} as ClineMessage)
|
||||
emitMessage(emitter, {
|
||||
ts: outputTs,
|
||||
type: "say",
|
||||
say: "command_output",
|
||||
partial: true,
|
||||
text: "line1\nline2\n",
|
||||
} as ClineMessage)
|
||||
emitMessage(emitter, {
|
||||
ts: outputTs,
|
||||
type: "say",
|
||||
say: "command_output",
|
||||
partial: false,
|
||||
text: "line1\nline2\n",
|
||||
} as ClineMessage)
|
||||
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(4)
|
||||
expect(output[0]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_use: { name: "execute_command", input: { command: "echo hello" } },
|
||||
done: true,
|
||||
})
|
||||
expect(output[1]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command", output: "line1\n" },
|
||||
})
|
||||
expect(output[2]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command", output: "line2\n" },
|
||||
})
|
||||
expect(output[3]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command" },
|
||||
done: true,
|
||||
})
|
||||
expect(output[3]).not.toHaveProperty("tool_result.output")
|
||||
})
|
||||
|
||||
it("prefers status-driven command output streaming and suppresses duplicate say completion", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
const commandId = 505
|
||||
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: commandId,
|
||||
ask: "command",
|
||||
partial: false,
|
||||
text: "echo streamed",
|
||||
}),
|
||||
)
|
||||
|
||||
emitter.emitCommandOutputChunk("line1\n")
|
||||
emitter.emitCommandOutputChunk("line1\nline2\n")
|
||||
emitter.markCommandOutputExited(17)
|
||||
|
||||
// This completion say is expected from the extension and should finalize
|
||||
// the status-driven command_output stream without duplicating content.
|
||||
emitMessage(emitter, {
|
||||
ts: 999,
|
||||
type: "say",
|
||||
say: "command_output",
|
||||
partial: false,
|
||||
text: "line1\nline2\n",
|
||||
} as ClineMessage)
|
||||
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(4)
|
||||
expect(output[0]).toMatchObject({
|
||||
type: "tool_use",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_use: { name: "execute_command", input: { command: "echo streamed" } },
|
||||
done: true,
|
||||
})
|
||||
expect(output[1]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command", output: "line1\n" },
|
||||
})
|
||||
expect(output[2]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command", output: "line2\n" },
|
||||
})
|
||||
expect(output[3]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command", exitCode: 17 },
|
||||
done: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("flushes remaining output on final say completion after fast status:exited", () => {
|
||||
const { stdout, lines } = createMockStdout()
|
||||
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
|
||||
const commandId = 606
|
||||
|
||||
emitMessage(
|
||||
emitter,
|
||||
createAskMessage({
|
||||
ts: commandId,
|
||||
ask: "command",
|
||||
partial: false,
|
||||
text: "aws sts get-caller-identity",
|
||||
}),
|
||||
)
|
||||
|
||||
emitter.emitCommandOutputChunk("{\n")
|
||||
emitter.markCommandOutputExited(0)
|
||||
|
||||
emitMessage(emitter, {
|
||||
ts: 607,
|
||||
type: "say",
|
||||
say: "command_output",
|
||||
partial: false,
|
||||
text: '{\n "Account": "123"\n}\n',
|
||||
} as ClineMessage)
|
||||
|
||||
const output = lines()
|
||||
expect(output).toHaveLength(3)
|
||||
expect(output[1]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command", output: "{\n" },
|
||||
})
|
||||
expect(output[2]).toMatchObject({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command", output: ' "Account": "123"\n}\n', exitCode: 0 },
|
||||
done: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -116,7 +116,7 @@ export enum AgentLoopState {
|
|||
*/
|
||||
export type RequiredAction =
|
||||
| "none" // No action needed (running/streaming)
|
||||
| "approve" // Can approve/reject (tool, command, browser, mcp)
|
||||
| "approve" // Can approve/reject (tool, command, mcp)
|
||||
| "answer" // Need to answer a question (followup)
|
||||
| "retry_or_new_task" // Can retry or start new task (api_req_failed)
|
||||
| "proceed_or_new_task" // Can proceed or start new task (mistake_limit)
|
||||
|
|
@ -221,7 +221,6 @@ function getRequiredAction(ask: ClineAsk): RequiredAction {
|
|||
return "answer"
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
return "approve"
|
||||
case "command_output":
|
||||
|
|
@ -264,8 +263,6 @@ function getStateDescription(state: AgentLoopState, ask?: ClineAsk): string {
|
|||
return "Agent wants to execute a command. Approve or reject."
|
||||
case "tool":
|
||||
return "Agent wants to perform a file operation. Approve or reject."
|
||||
case "browser_action_launch":
|
||||
return "Agent wants to use the browser. Approve or reject."
|
||||
case "use_mcp_server":
|
||||
return "Agent wants to use an MCP server. Approve or reject."
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ export class AskDispatcher {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle interactive asks (followup, command, tool, browser_action_launch, use_mcp_server).
|
||||
* Handle interactive asks (followup, command, tool, use_mcp_server).
|
||||
* These require user approval or input.
|
||||
*/
|
||||
private async handleInteractiveAsk(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
|
||||
|
|
@ -258,9 +258,6 @@ export class AskDispatcher {
|
|||
case "tool":
|
||||
return await this.handleToolApproval(ts, text)
|
||||
|
||||
case "browser_action_launch":
|
||||
return await this.handleBrowserApproval(ts, text)
|
||||
|
||||
case "use_mcp_server":
|
||||
return await this.handleMcpApproval(ts, text)
|
||||
|
||||
|
|
@ -444,32 +441,6 @@ export class AskDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle browser action approval.
|
||||
*/
|
||||
private async handleBrowserApproval(ts: number, text: string): Promise<AskHandleResult> {
|
||||
this.outputManager.output("\n[browser action request]")
|
||||
if (text) {
|
||||
this.outputManager.output(` Action: ${text}`)
|
||||
}
|
||||
this.outputManager.markDisplayed(ts, text || "", false)
|
||||
|
||||
if (this.nonInteractive) {
|
||||
// Auto-approved by extension settings
|
||||
return { handled: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const approved = await this.promptManager.promptForYesNo("Allow browser action? (y/n): ")
|
||||
this.sendApprovalResponse(approved)
|
||||
return { handled: true, response: approved ? "yesButtonClicked" : "noButtonClicked" }
|
||||
} catch {
|
||||
this.outputManager.output("[Defaulting to: no]")
|
||||
this.sendApprovalResponse(false)
|
||||
return { handled: true, response: "noButtonClicked" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle MCP server access approval.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ export function streamingEnded(previous: AgentStateInfo, current: AgentStateInfo
|
|||
* Helper to determine if task completed.
|
||||
*/
|
||||
export function taskCompleted(previous: AgentStateInfo, current: AgentStateInfo): boolean {
|
||||
const completionAsks = ["completion_result", "api_req_failed", "mistake_limit_reached"]
|
||||
const completionAsks = ["completion_result", "resume_completed_task"]
|
||||
const wasNotComplete = !previous.currentAsk || !completionAsks.includes(previous.currentAsk)
|
||||
const isNowComplete = current.currentAsk !== undefined && completionAsks.includes(current.currentAsk)
|
||||
return wasNotComplete && isNowComplete
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import type {
|
|||
import { createVSCodeAPI, IExtensionHost, ExtensionHostEventMap, setRuntimeConfigValues } from "@roo-code/vscode-shim"
|
||||
import { DebugLogger, setDebugLogEnabled } from "@roo-code/core/cli"
|
||||
|
||||
import type { SupportedProvider } from "@/types/index.js"
|
||||
import { DEFAULT_FLAGS, type SupportedProvider } from "@/types/index.js"
|
||||
import type { User } from "@/lib/sdk/index.js"
|
||||
import { getProviderSettings } from "@/lib/utils/provider.js"
|
||||
import { createEphemeralStorageDir } from "@/lib/storage/index.js"
|
||||
|
|
@ -66,6 +66,7 @@ const CLI_PACKAGE_ROOT = process.env.ROO_CLI_ROOT || findCliPackageRoot()
|
|||
export interface ExtensionHostOptions {
|
||||
mode: string
|
||||
reasoningEffort?: ReasoningEffortExtended | "unspecified" | "disabled"
|
||||
consecutiveMistakeLimit?: number
|
||||
user: User | null
|
||||
provider: SupportedProvider
|
||||
apiKey?: string
|
||||
|
|
@ -79,6 +80,7 @@ export interface ExtensionHostOptions {
|
|||
ephemeral: boolean
|
||||
debug: boolean
|
||||
exitOnComplete: boolean
|
||||
terminalShell?: string
|
||||
/**
|
||||
* When true, exit the process on API request errors instead of retrying.
|
||||
*/
|
||||
|
|
@ -107,7 +109,8 @@ interface WebviewViewProvider {
|
|||
export interface ExtensionHostInterface extends IExtensionHost<ExtensionHostEventMap> {
|
||||
client: ExtensionClient
|
||||
activate(): Promise<void>
|
||||
runTask(prompt: string): Promise<void>
|
||||
runTask(prompt: string, taskId?: string, configuration?: RooCodeSettings, images?: string[]): Promise<void>
|
||||
resumeTask(taskId: string): Promise<void>
|
||||
sendToExtension(message: WebviewMessage): void
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
|
|
@ -135,6 +138,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
|
||||
// Ephemeral storage.
|
||||
private ephemeralStorageDir: string | null = null
|
||||
private previousCliRuntimeEnv: string | undefined
|
||||
|
||||
// ==========================================================================
|
||||
// Managers - These do all the heavy lifting
|
||||
|
|
@ -172,6 +176,10 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
super()
|
||||
|
||||
this.options = options
|
||||
// Mark this process as CLI runtime so extension code can apply
|
||||
// CLI-specific behavior without affecting VS Code desktop usage.
|
||||
this.previousCliRuntimeEnv = process.env.ROO_CLI_RUNTIME
|
||||
process.env.ROO_CLI_RUNTIME = "1"
|
||||
|
||||
// Enable file-based debug logging only when --debug is passed.
|
||||
if (options.debug) {
|
||||
|
|
@ -213,9 +221,12 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
// Populate initial settings.
|
||||
const baseSettings: RooCodeSettings = {
|
||||
mode: this.options.mode,
|
||||
commandExecutionTimeout: 30,
|
||||
browserToolEnabled: false,
|
||||
consecutiveMistakeLimit: this.options.consecutiveMistakeLimit ?? DEFAULT_FLAGS.consecutiveMistakeLimit,
|
||||
commandExecutionTimeout: 300,
|
||||
enableCheckpoints: false,
|
||||
experiments: {
|
||||
customTools: true,
|
||||
},
|
||||
...getProviderSettings(this.options.provider, this.options.apiKey, this.options.model),
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +238,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
alwaysAllowWrite: true,
|
||||
alwaysAllowWriteOutsideWorkspace: true,
|
||||
alwaysAllowWriteProtected: true,
|
||||
alwaysAllowBrowser: true,
|
||||
alwaysAllowMcp: true,
|
||||
alwaysAllowModeSwitch: true,
|
||||
alwaysAllowSubtasks: true,
|
||||
|
|
@ -248,6 +258,11 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
this.initialSettings.reasoningEffort = this.options.reasoningEffort
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.terminalShell) {
|
||||
this.initialSettings.terminalShellIntegrationDisabled = true
|
||||
this.initialSettings.execaShellPath = this.options.terminalShell
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
|
|
@ -431,7 +446,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
// Apply CLI settings to the runtime config and context proxy BEFORE
|
||||
// sending webviewDidLaunch. This prevents a race condition where the
|
||||
// webviewDidLaunch handler's first-time init sync reads default state
|
||||
// (apiProvider: "anthropic") instead of the CLI-provided settings.
|
||||
// instead of the CLI-provided settings.
|
||||
setRuntimeConfigValues("roo-cline", this.initialSettings as Record<string, unknown>)
|
||||
this.sendToExtension({ type: "updateSettings", updatedSettings: this.initialSettings })
|
||||
|
||||
|
|
@ -460,9 +475,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
// Task Management
|
||||
// ==========================================================================
|
||||
|
||||
public async runTask(prompt: string): Promise<void> {
|
||||
this.sendToExtension({ type: "newTask", text: prompt })
|
||||
|
||||
private waitForTaskCompletion(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const completeHandler = () => {
|
||||
cleanup()
|
||||
|
|
@ -503,6 +516,27 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
})
|
||||
}
|
||||
|
||||
public async runTask(
|
||||
prompt: string,
|
||||
taskId?: string,
|
||||
configuration?: RooCodeSettings,
|
||||
images?: string[],
|
||||
): Promise<void> {
|
||||
this.sendToExtension({
|
||||
type: "newTask",
|
||||
text: prompt,
|
||||
taskId,
|
||||
taskConfiguration: configuration,
|
||||
...(images !== undefined ? { images } : {}),
|
||||
})
|
||||
return this.waitForTaskCompletion()
|
||||
}
|
||||
|
||||
public async resumeTask(taskId: string): Promise<void> {
|
||||
this.sendToExtension({ type: "showTaskWithId", text: taskId })
|
||||
return this.waitForTaskCompletion()
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Public Agent State API
|
||||
// ==========================================================================
|
||||
|
|
@ -569,5 +603,12 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
// Restore previous CLI runtime marker for process hygiene in tests.
|
||||
if (this.previousCliRuntimeEnv === undefined) {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
} else {
|
||||
process.env.ROO_CLI_RUNTIME = this.previousCliRuntimeEnv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@
|
|||
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
|
||||
import type { JsonEvent, JsonEventCost, JsonFinalOutput } from "@/types/json-events.js"
|
||||
import type { JsonEvent, JsonEventCost, JsonEventQueueItem, JsonFinalOutput } from "@/types/json-events.js"
|
||||
|
||||
import type { ExtensionClient } from "./extension-client.js"
|
||||
import type { TaskCompletedEvent } from "./events.js"
|
||||
import type { AgentStateChangeEvent, TaskCompletedEvent } from "./events.js"
|
||||
import { AgentLoopState } from "./agent-state.js"
|
||||
|
||||
/**
|
||||
* Options for JsonEventEmitter.
|
||||
|
|
@ -29,6 +30,14 @@ export interface JsonEventEmitterOptions {
|
|||
mode: "json" | "stream-json"
|
||||
/** Output stream (defaults to process.stdout) */
|
||||
stdout?: NodeJS.WriteStream
|
||||
/** Optional request id provider for correlating stream events */
|
||||
requestIdProvider?: () => string | undefined
|
||||
/** Transport schema version emitted in system:init */
|
||||
schemaVersion?: number
|
||||
/** Transport protocol identifier emitted in system:init */
|
||||
protocol?: string
|
||||
/** Supported stdin protocol capabilities emitted in system:init */
|
||||
capabilities?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -81,22 +90,55 @@ const SKIP_SAY_TYPES = new Set([
|
|||
|
||||
/** Key offset for reasoning content to avoid collision with text content delta tracking */
|
||||
const REASONING_KEY_OFFSET = 1_000_000_000
|
||||
/** Grace period to wait for final say:command_output after status:exited */
|
||||
const COMMAND_OUTPUT_EXIT_GRACE_MS = 250
|
||||
|
||||
export class JsonEventEmitter {
|
||||
private mode: "json" | "stream-json"
|
||||
private stdout: NodeJS.WriteStream
|
||||
private events: JsonEvent[] = []
|
||||
private unsubscribers: (() => void)[] = []
|
||||
private pendingWrites = new Set<Promise<void>>()
|
||||
private lastCost: JsonEventCost | undefined
|
||||
private requestIdProvider: () => string | undefined
|
||||
private schemaVersion: number
|
||||
private protocol: string
|
||||
private capabilities: string[]
|
||||
private seenMessageIds = new Set<number>()
|
||||
// Track previous content for delta computation
|
||||
private previousContent = new Map<number, string>()
|
||||
// Track previous tool-use content for structured (non-append-only) delta computation.
|
||||
private previousToolUseContent = new Map<number, string>()
|
||||
// Track the currently active execute_command tool_use id for command_output correlation.
|
||||
private activeCommandToolUseId: number | undefined
|
||||
// Track command output snapshots by command tool-use id for delta computation.
|
||||
private previousCommandOutputByToolUseId = new Map<number, string>()
|
||||
// Track command ids whose output is being streamed from commandExecutionStatus updates.
|
||||
private statusDrivenCommandOutputIds = new Set<number>()
|
||||
// Track command ids that already emitted a terminal command_output done event.
|
||||
private completedCommandOutputIds = new Set<number>()
|
||||
// Track exited commands awaiting final say:command_output completion.
|
||||
private pendingCommandCompletionByToolUseId = new Map<number, { exitCode?: number; timer: NodeJS.Timeout }>()
|
||||
// Track the completion result content
|
||||
private completionResultContent: string | undefined
|
||||
// Track the latest assistant text as a fallback for result.content.
|
||||
private lastAssistantText: string | undefined
|
||||
// The first non-partial "say:text" per task is the echoed user prompt.
|
||||
private expectPromptEchoAsUser = true
|
||||
|
||||
constructor(options: JsonEventEmitterOptions) {
|
||||
this.mode = options.mode
|
||||
this.stdout = options.stdout ?? process.stdout
|
||||
this.requestIdProvider = options.requestIdProvider ?? (() => undefined)
|
||||
this.schemaVersion = options.schemaVersion ?? 1
|
||||
this.protocol = options.protocol ?? "roo-cli-stream"
|
||||
this.capabilities = options.capabilities ?? [
|
||||
"stdin:start",
|
||||
"stdin:message",
|
||||
"stdin:cancel",
|
||||
"stdin:ping",
|
||||
"stdin:shutdown",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -106,19 +148,72 @@ export class JsonEventEmitter {
|
|||
// Subscribe to message events
|
||||
const unsubMessage = client.on("message", (msg) => this.handleMessage(msg, false))
|
||||
const unsubMessageUpdated = client.on("messageUpdated", (msg) => this.handleMessage(msg, true))
|
||||
const unsubStateChange = client.on("stateChange", (event) => this.handleStateChange(event))
|
||||
const unsubTaskCompleted = client.on("taskCompleted", (event) => this.handleTaskCompleted(event))
|
||||
const unsubError = client.on("error", (error) => this.handleError(error))
|
||||
|
||||
this.unsubscribers.push(unsubMessage, unsubMessageUpdated, unsubTaskCompleted, unsubError)
|
||||
this.unsubscribers.push(unsubMessage, unsubMessageUpdated, unsubStateChange, unsubTaskCompleted, unsubError)
|
||||
|
||||
// Emit init event
|
||||
this.emitEvent({
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
content: "Task started",
|
||||
schemaVersion: this.schemaVersion,
|
||||
protocol: this.protocol,
|
||||
capabilities: this.capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
emitControl(event: {
|
||||
subtype: "ack" | "done" | "error"
|
||||
requestId?: string
|
||||
command?: JsonEvent["command"]
|
||||
taskId?: string
|
||||
content?: string
|
||||
success?: boolean
|
||||
code?: string
|
||||
}): void {
|
||||
this.emitEvent({
|
||||
type: "control",
|
||||
subtype: event.subtype,
|
||||
requestId: event.requestId,
|
||||
command: event.command,
|
||||
taskId: event.taskId,
|
||||
content: event.content,
|
||||
success: event.success,
|
||||
code: event.code,
|
||||
done: event.subtype === "done" ? true : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
emitQueue(event: {
|
||||
subtype: "snapshot" | "enqueued" | "dequeued" | "drained" | "updated"
|
||||
taskId?: string
|
||||
content?: string
|
||||
queueDepth: number
|
||||
queue: JsonEventQueueItem[]
|
||||
}): void {
|
||||
this.emitEvent({
|
||||
type: "queue",
|
||||
subtype: event.subtype,
|
||||
taskId: event.taskId,
|
||||
content: event.content,
|
||||
queueDepth: event.queueDepth,
|
||||
queue: event.queue,
|
||||
})
|
||||
}
|
||||
|
||||
private handleStateChange(event: AgentStateChangeEvent): void {
|
||||
// Only treat the next say:text as a prompt echo when a new task starts.
|
||||
if (
|
||||
event.previousState.state === AgentLoopState.NO_TASK &&
|
||||
event.currentState.state !== AgentLoopState.NO_TASK
|
||||
) {
|
||||
this.expectPromptEchoAsUser = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach from the client and clean up subscriptions.
|
||||
*/
|
||||
|
|
@ -144,6 +239,60 @@ export class JsonEventEmitter {
|
|||
return fullContent.startsWith(previous) ? fullContent.slice(previous.length) : fullContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a compact delta for structured strings (for tool_use snapshots).
|
||||
*
|
||||
* Unlike append-only text streams, tool-use payloads are often full snapshots
|
||||
* where edits happen before a stable suffix (e.g., inside JSON strings). This
|
||||
* extracts the inserted segment when possible; otherwise it falls back to the
|
||||
* full snapshot so consumers can recover.
|
||||
*/
|
||||
private computeStructuredDelta(msgId: number, fullContent: string | undefined): string | null {
|
||||
if (!fullContent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const previous = this.previousToolUseContent.get(msgId) || ""
|
||||
|
||||
if (fullContent === previous) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.previousToolUseContent.set(msgId, fullContent)
|
||||
|
||||
if (previous.length === 0) {
|
||||
return fullContent
|
||||
}
|
||||
|
||||
if (fullContent.startsWith(previous)) {
|
||||
return fullContent.slice(previous.length)
|
||||
}
|
||||
|
||||
let prefix = 0
|
||||
|
||||
while (prefix < previous.length && prefix < fullContent.length && previous[prefix] === fullContent[prefix]) {
|
||||
prefix++
|
||||
}
|
||||
|
||||
let suffix = 0
|
||||
|
||||
while (
|
||||
suffix < previous.length - prefix &&
|
||||
suffix < fullContent.length - prefix &&
|
||||
previous[previous.length - 1 - suffix] === fullContent[fullContent.length - 1 - suffix]
|
||||
) {
|
||||
suffix++
|
||||
}
|
||||
|
||||
const isPureInsertion = fullContent.length >= previous.length && prefix + suffix >= previous.length
|
||||
|
||||
if (isPureInsertion) {
|
||||
return fullContent.slice(prefix, fullContent.length - suffix)
|
||||
}
|
||||
|
||||
return fullContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a streaming partial message with no new content.
|
||||
*/
|
||||
|
|
@ -151,6 +300,138 @@ export class JsonEventEmitter {
|
|||
return this.mode === "stream-json" && content === null
|
||||
}
|
||||
|
||||
private computeCommandOutputDelta(commandId: number, fullOutput: string | undefined): string | null {
|
||||
const normalized = fullOutput ?? ""
|
||||
const previous = this.previousCommandOutputByToolUseId.get(commandId) || ""
|
||||
|
||||
if (normalized === previous) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.previousCommandOutputByToolUseId.set(commandId, normalized)
|
||||
return normalized.startsWith(previous) ? normalized.slice(previous.length) : normalized
|
||||
}
|
||||
|
||||
private emitCommandOutputEvent(
|
||||
commandId: number,
|
||||
fullOutput: string | undefined,
|
||||
isDone: boolean,
|
||||
exitCode?: number,
|
||||
): void {
|
||||
if (this.mode === "stream-json") {
|
||||
const outputDelta = this.computeCommandOutputDelta(commandId, fullOutput)
|
||||
const event: JsonEvent = {
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: { name: "execute_command" },
|
||||
}
|
||||
|
||||
if (outputDelta !== null && outputDelta.length > 0) {
|
||||
event.tool_result = { name: "execute_command", output: outputDelta }
|
||||
}
|
||||
|
||||
if (isDone && exitCode !== undefined) {
|
||||
event.tool_result = {
|
||||
...(event.tool_result ?? { name: "execute_command" }),
|
||||
exitCode,
|
||||
}
|
||||
}
|
||||
|
||||
if (isDone) {
|
||||
event.done = true
|
||||
this.clearPendingCommandCompletion(commandId)
|
||||
this.previousCommandOutputByToolUseId.delete(commandId)
|
||||
this.statusDrivenCommandOutputIds.delete(commandId)
|
||||
this.completedCommandOutputIds.add(commandId)
|
||||
if (this.activeCommandToolUseId === commandId) {
|
||||
this.activeCommandToolUseId = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress empty partial updates that carry no delta.
|
||||
if (!isDone && outputDelta === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent(event)
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent({
|
||||
type: "tool_result",
|
||||
id: commandId,
|
||||
subtype: "command",
|
||||
tool_result: {
|
||||
name: "execute_command",
|
||||
output: fullOutput,
|
||||
...(isDone && exitCode !== undefined ? { exitCode } : {}),
|
||||
},
|
||||
...(isDone ? { done: true } : {}),
|
||||
})
|
||||
|
||||
if (isDone) {
|
||||
this.clearPendingCommandCompletion(commandId)
|
||||
this.previousCommandOutputByToolUseId.delete(commandId)
|
||||
this.statusDrivenCommandOutputIds.delete(commandId)
|
||||
this.completedCommandOutputIds.add(commandId)
|
||||
if (this.activeCommandToolUseId === commandId) {
|
||||
this.activeCommandToolUseId = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public emitCommandOutputChunk(outputSnapshot: string): void {
|
||||
const commandId = this.activeCommandToolUseId
|
||||
if (commandId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.statusDrivenCommandOutputIds.add(commandId)
|
||||
this.emitCommandOutputEvent(commandId, outputSnapshot, false)
|
||||
}
|
||||
|
||||
public markCommandOutputExited(exitCode?: number): void {
|
||||
const commandId = this.activeCommandToolUseId
|
||||
if (commandId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.statusDrivenCommandOutputIds.add(commandId)
|
||||
this.clearPendingCommandCompletion(commandId)
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
// Fallback close if final say:command_output never arrives.
|
||||
if (!this.pendingCommandCompletionByToolUseId.has(commandId)) {
|
||||
return
|
||||
}
|
||||
this.pendingCommandCompletionByToolUseId.delete(commandId)
|
||||
this.emitCommandOutputEvent(commandId, undefined, true, exitCode)
|
||||
}, COMMAND_OUTPUT_EXIT_GRACE_MS)
|
||||
timer.unref?.()
|
||||
|
||||
this.pendingCommandCompletionByToolUseId.set(commandId, { exitCode, timer })
|
||||
}
|
||||
|
||||
public emitCommandOutputDone(exitCode?: number): void {
|
||||
const commandId = this.activeCommandToolUseId
|
||||
if (commandId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.statusDrivenCommandOutputIds.add(commandId)
|
||||
this.emitCommandOutputEvent(commandId, undefined, true, exitCode)
|
||||
}
|
||||
|
||||
private clearPendingCommandCompletion(commandId: number): void {
|
||||
const pending = this.pendingCommandCompletionByToolUseId.get(commandId)
|
||||
if (!pending) {
|
||||
return
|
||||
}
|
||||
clearTimeout(pending.timer)
|
||||
this.pendingCommandCompletionByToolUseId.delete(commandId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content to send for a message (delta for streaming, full for json mode).
|
||||
*/
|
||||
|
|
@ -158,6 +439,7 @@ export class JsonEventEmitter {
|
|||
if (this.mode === "stream-json" && isPartial) {
|
||||
return this.computeDelta(msgId, text)
|
||||
}
|
||||
|
||||
return text ?? null
|
||||
}
|
||||
|
||||
|
|
@ -172,15 +454,19 @@ export class JsonEventEmitter {
|
|||
subtype?: string,
|
||||
): JsonEvent {
|
||||
const event: JsonEvent = { type, id }
|
||||
|
||||
if (content !== null) {
|
||||
event.content = content
|
||||
}
|
||||
|
||||
if (subtype) {
|
||||
event.subtype = subtype
|
||||
}
|
||||
|
||||
if (isDone) {
|
||||
event.done = true
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
|
|
@ -203,21 +489,22 @@ export class JsonEventEmitter {
|
|||
if (isDone) {
|
||||
this.seenMessageIds.add(msg.ts)
|
||||
this.previousContent.delete(msg.ts)
|
||||
}
|
||||
|
||||
const contentToSend = this.getContentToSend(msg.ts, msg.text, msg.partial ?? false)
|
||||
|
||||
// Skip if no new content for streaming partial messages
|
||||
if (msg.partial && this.isEmptyStreamingDelta(contentToSend)) {
|
||||
return
|
||||
this.previousToolUseContent.delete(msg.ts)
|
||||
}
|
||||
|
||||
if (msg.type === "say" && msg.say) {
|
||||
const contentToSend = this.getContentToSend(msg.ts, msg.text, msg.partial ?? false)
|
||||
|
||||
// Skip if no new content for streaming partial messages
|
||||
if (msg.partial && this.isEmptyStreamingDelta(contentToSend)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.handleSayMessage(msg, contentToSend, isDone)
|
||||
}
|
||||
|
||||
if (msg.type === "ask" && msg.ask) {
|
||||
this.handleAskMessage(msg, contentToSend, isDone)
|
||||
this.handleAskMessage(msg, isDone)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +514,17 @@ export class JsonEventEmitter {
|
|||
private handleSayMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void {
|
||||
switch (msg.say) {
|
||||
case "text":
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone))
|
||||
if (this.expectPromptEchoAsUser) {
|
||||
this.emitEvent(this.buildTextEvent("user", msg.ts, contentToSend, isDone))
|
||||
if (isDone) {
|
||||
this.expectPromptEchoAsUser = false
|
||||
}
|
||||
} else {
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone))
|
||||
if (msg.text) {
|
||||
this.lastAssistantText = msg.text
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "reasoning":
|
||||
|
|
@ -239,15 +536,15 @@ export class JsonEventEmitter {
|
|||
break
|
||||
|
||||
case "command_output":
|
||||
this.emitEvent({
|
||||
type: "tool_result",
|
||||
tool_result: { name: "execute_command", output: msg.text },
|
||||
})
|
||||
this.handleCommandOutputMessage(msg, isDone)
|
||||
break
|
||||
|
||||
case "user_feedback":
|
||||
case "user_feedback_diff":
|
||||
this.emitEvent(this.buildTextEvent("user", msg.ts, contentToSend, isDone))
|
||||
if (isDone) {
|
||||
this.expectPromptEchoAsUser = false
|
||||
}
|
||||
break
|
||||
|
||||
case "api_req_started": {
|
||||
|
|
@ -258,15 +555,6 @@ export class JsonEventEmitter {
|
|||
break
|
||||
}
|
||||
|
||||
case "browser_action":
|
||||
case "browser_action_result":
|
||||
this.emitEvent({
|
||||
type: "tool_result",
|
||||
subtype: "browser",
|
||||
tool_result: { name: "browser_action", output: msg.text },
|
||||
})
|
||||
break
|
||||
|
||||
case "mcp_server_response":
|
||||
this.emitEvent({
|
||||
type: "tool_result",
|
||||
|
|
@ -314,49 +602,31 @@ export class JsonEventEmitter {
|
|||
/**
|
||||
* Handle "ask" type messages.
|
||||
*/
|
||||
private handleAskMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void {
|
||||
private handleAskMessage(msg: ClineMessage, isDone: boolean): void {
|
||||
switch (msg.ask) {
|
||||
case "tool": {
|
||||
const toolInfo = parseToolInfo(msg.text)
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "tool",
|
||||
tool_use: toolInfo ?? { name: "unknown_tool", input: { raw: msg.text } },
|
||||
})
|
||||
case "tool":
|
||||
this.handleToolUseAsk(msg, "tool", isDone)
|
||||
break
|
||||
}
|
||||
|
||||
case "command":
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "command",
|
||||
tool_use: { name: "execute_command", input: { command: msg.text } },
|
||||
})
|
||||
break
|
||||
|
||||
case "browser_action_launch":
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "browser",
|
||||
tool_use: { name: "browser_action", input: { raw: msg.text } },
|
||||
})
|
||||
this.handleToolUseAsk(msg, "command", isDone)
|
||||
break
|
||||
|
||||
case "use_mcp_server":
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "mcp",
|
||||
tool_use: { name: "mcp_server", input: { raw: msg.text } },
|
||||
})
|
||||
this.handleToolUseAsk(msg, "mcp", isDone)
|
||||
break
|
||||
|
||||
case "followup":
|
||||
case "followup": {
|
||||
const contentToSend = this.getContentToSend(msg.ts, msg.text, msg.partial ?? false)
|
||||
|
||||
// Skip if no new content for streaming partial messages
|
||||
if (msg.partial && this.isEmptyStreamingDelta(contentToSend)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, "followup"))
|
||||
break
|
||||
}
|
||||
|
||||
case "command_output":
|
||||
// Handled in say type
|
||||
|
|
@ -370,18 +640,147 @@ export class JsonEventEmitter {
|
|||
|
||||
default:
|
||||
if (msg.text) {
|
||||
const contentToSend = this.getContentToSend(msg.ts, msg.text, msg.partial ?? false)
|
||||
|
||||
// Skip if no new content for streaming partial messages
|
||||
if (msg.partial && this.isEmptyStreamingDelta(contentToSend)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, msg.ask))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private handleToolUseAsk(msg: ClineMessage, subtype: "tool" | "command" | "mcp", isDone: boolean): void {
|
||||
const isStreamingPartial = this.mode === "stream-json" && msg.partial === true
|
||||
const toolInfo = parseToolInfo(msg.text)
|
||||
|
||||
if (subtype === "command") {
|
||||
if (this.activeCommandToolUseId !== undefined && this.activeCommandToolUseId !== msg.ts) {
|
||||
const previousCommandId = this.activeCommandToolUseId
|
||||
const pending = this.pendingCommandCompletionByToolUseId.get(previousCommandId)
|
||||
if (pending) {
|
||||
clearTimeout(pending.timer)
|
||||
this.pendingCommandCompletionByToolUseId.delete(previousCommandId)
|
||||
this.emitCommandOutputEvent(previousCommandId, undefined, true, pending.exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
this.activeCommandToolUseId = msg.ts
|
||||
this.completedCommandOutputIds.delete(msg.ts)
|
||||
this.clearPendingCommandCompletion(msg.ts)
|
||||
|
||||
if (isStreamingPartial) {
|
||||
const commandDelta = this.computeStructuredDelta(msg.ts, msg.text)
|
||||
if (commandDelta === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "command",
|
||||
content: commandDelta,
|
||||
tool_use: { name: "execute_command", input: { command: commandDelta } },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "command",
|
||||
tool_use: { name: "execute_command", input: { command: msg.text } },
|
||||
...(isDone ? { done: true } : {}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (subtype === "mcp") {
|
||||
if (isStreamingPartial) {
|
||||
const mcpDelta = this.computeStructuredDelta(msg.ts, msg.text)
|
||||
if (mcpDelta === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "mcp",
|
||||
content: mcpDelta,
|
||||
tool_use: { name: "mcp_server" },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "mcp",
|
||||
tool_use: { name: "mcp_server", input: { raw: msg.text } },
|
||||
...(isDone ? { done: true } : {}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isStreamingPartial) {
|
||||
const toolDelta = this.computeStructuredDelta(msg.ts, msg.text)
|
||||
if (toolDelta === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "tool",
|
||||
content: toolDelta,
|
||||
tool_use: { name: toolInfo?.name ?? "unknown_tool" },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "tool",
|
||||
tool_use: toolInfo ?? { name: "unknown_tool", input: { raw: msg.text } },
|
||||
...(isDone ? { done: true } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
private handleCommandOutputMessage(msg: ClineMessage, isDone: boolean): void {
|
||||
const commandId = this.activeCommandToolUseId ?? msg.ts
|
||||
if (this.completedCommandOutputIds.has(commandId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const pending = this.pendingCommandCompletionByToolUseId.get(commandId)
|
||||
if (pending) {
|
||||
if (!isDone) {
|
||||
return
|
||||
}
|
||||
clearTimeout(pending.timer)
|
||||
this.pendingCommandCompletionByToolUseId.delete(commandId)
|
||||
this.emitCommandOutputEvent(commandId, msg.text, true, pending.exitCode)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.statusDrivenCommandOutputIds.has(commandId)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitCommandOutputEvent(commandId, msg.text, isDone)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle task completion and emit result event.
|
||||
*/
|
||||
private handleTaskCompleted(event: TaskCompletedEvent): void {
|
||||
// Use tracked completion result content, falling back to event message
|
||||
const resultContent = this.completionResultContent || event.message?.text
|
||||
// Prefer the completion payload from the current event. If it is empty,
|
||||
// fall back to the most recent tracked completion text, then assistant text.
|
||||
const resultContent = event.message?.text || this.completionResultContent || this.lastAssistantText
|
||||
|
||||
this.emitEvent({
|
||||
type: "result",
|
||||
|
|
@ -392,6 +791,10 @@ export class JsonEventEmitter {
|
|||
cost: this.lastCost,
|
||||
})
|
||||
|
||||
// Prevent stale completion content from leaking into later turns.
|
||||
this.completionResultContent = undefined
|
||||
this.lastAssistantText = undefined
|
||||
|
||||
// For "json" mode, output the final accumulated result
|
||||
if (this.mode === "json") {
|
||||
this.outputFinalResult(event.success, resultContent)
|
||||
|
|
@ -415,10 +818,13 @@ export class JsonEventEmitter {
|
|||
* For json mode: accumulate for final output
|
||||
*/
|
||||
private emitEvent(event: JsonEvent): void {
|
||||
this.events.push(event)
|
||||
const requestId = event.requestId ?? this.requestIdProvider()
|
||||
const payload = requestId ? { ...event, requestId } : event
|
||||
|
||||
this.events.push(payload)
|
||||
|
||||
if (this.mode === "stream-json") {
|
||||
this.outputLine(event)
|
||||
this.outputLine(payload)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -426,7 +832,7 @@ export class JsonEventEmitter {
|
|||
* Output a single JSON line (NDJSON format).
|
||||
*/
|
||||
private outputLine(data: unknown): void {
|
||||
this.stdout.write(JSON.stringify(data) + "\n")
|
||||
this.writeToStdout(JSON.stringify(data) + "\n")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -441,7 +847,31 @@ export class JsonEventEmitter {
|
|||
events: this.events.filter((e) => e.type !== "result"), // Exclude the result event itself
|
||||
}
|
||||
|
||||
this.stdout.write(JSON.stringify(output, null, 2) + "\n")
|
||||
this.writeToStdout(JSON.stringify(output, null, 2) + "\n")
|
||||
}
|
||||
|
||||
private writeToStdout(content: string): void {
|
||||
const writePromise = new Promise<void>((resolve, reject) => {
|
||||
this.stdout.write(content, (error?: Error | null) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
this.pendingWrites.add(writePromise)
|
||||
|
||||
void writePromise.finally(() => {
|
||||
this.pendingWrites.delete(writePromise)
|
||||
})
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
while (this.pendingWrites.size > 0) {
|
||||
await Promise.all([...this.pendingWrites])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -459,6 +889,17 @@ export class JsonEventEmitter {
|
|||
this.lastCost = undefined
|
||||
this.seenMessageIds.clear()
|
||||
this.previousContent.clear()
|
||||
this.previousToolUseContent.clear()
|
||||
this.activeCommandToolUseId = undefined
|
||||
this.previousCommandOutputByToolUseId.clear()
|
||||
this.statusDrivenCommandOutputIds.clear()
|
||||
this.completedCommandOutputIds.clear()
|
||||
for (const pending of this.pendingCommandCompletionByToolUseId.values()) {
|
||||
clearTimeout(pending.timer)
|
||||
}
|
||||
this.pendingCommandCompletionByToolUseId.clear()
|
||||
this.completionResultContent = undefined
|
||||
this.lastAssistantText = undefined
|
||||
this.expectPromptEchoAsUser = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -343,13 +343,16 @@ export class MessageProcessor {
|
|||
|
||||
// Task completed
|
||||
if (taskCompleted(previousState, currentState)) {
|
||||
const completedSuccessfully =
|
||||
currentState.currentAsk === "completion_result" || currentState.currentAsk === "resume_completed_task"
|
||||
|
||||
if (this.options.debug) {
|
||||
debugLog("[MessageProcessor] EMIT taskCompleted", {
|
||||
success: currentState.currentAsk === "completion_result",
|
||||
success: completedSuccessfully,
|
||||
})
|
||||
}
|
||||
const completedEvent: TaskCompletedEvent = {
|
||||
success: currentState.currentAsk === "completion_result",
|
||||
success: completedSuccessfully,
|
||||
stateInfo: currentState,
|
||||
message: currentState.lastMessage,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,12 @@ export class OutputManager {
|
|||
*/
|
||||
private currentlyStreamingTs: number | null = null
|
||||
|
||||
/**
|
||||
* Track whether a say:completion_result has been streamed,
|
||||
* so the subsequent ask:completion_result doesn't duplicate the text.
|
||||
*/
|
||||
private completionResultStreamed = false
|
||||
|
||||
/**
|
||||
* Track first partial logs (for debugging first/last pattern).
|
||||
*/
|
||||
|
|
@ -197,6 +203,7 @@ export class OutputManager {
|
|||
this.displayedMessages.clear()
|
||||
this.streamedContent.clear()
|
||||
this.currentlyStreamingTs = null
|
||||
this.completionResultStreamed = false
|
||||
this.loggedFirstPartial.clear()
|
||||
this.streamingState.next({ ts: null, isStreaming: false })
|
||||
}
|
||||
|
|
@ -248,8 +255,13 @@ export class OutputManager {
|
|||
this.outputCommandOutput(ts, text, isPartial, alreadyDisplayedComplete)
|
||||
break
|
||||
|
||||
// Note: completion_result is an "ask" type, not a "say" type.
|
||||
// It is handled via the TaskCompleted event in extension-host.ts
|
||||
case "completion_result":
|
||||
// completion_result can arrive as both a "say" (with streamed text)
|
||||
// and an "ask" (handled via TaskCompleted in extension-host.ts).
|
||||
// Stream the say variant here; the ask variant is handled by
|
||||
// outputCompletionResult which will skip if already displayed.
|
||||
this.outputCompletionSayMessage(ts, text, isPartial, alreadyDisplayedComplete)
|
||||
break
|
||||
|
||||
case "error":
|
||||
if (!alreadyDisplayedComplete) {
|
||||
|
|
@ -401,13 +413,50 @@ export class OutputManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a say:completion_result message (streamed text of the completion).
|
||||
* The subsequent ask:completion_result is handled by outputCompletionResult.
|
||||
*/
|
||||
private outputCompletionSayMessage(
|
||||
ts: number,
|
||||
text: string,
|
||||
isPartial: boolean,
|
||||
alreadyDisplayedComplete: boolean | undefined,
|
||||
): void {
|
||||
if (isPartial && text) {
|
||||
this.streamContent(ts, text, "[assistant]")
|
||||
this.displayedMessages.set(ts, { ts, text, partial: true })
|
||||
this.completionResultStreamed = true
|
||||
} else if (!isPartial && text && !alreadyDisplayedComplete) {
|
||||
const streamed = this.streamedContent.get(ts)
|
||||
|
||||
if (streamed) {
|
||||
if (text.length > streamed.text.length && text.startsWith(streamed.text)) {
|
||||
const delta = text.slice(streamed.text.length)
|
||||
this.writeRaw(delta)
|
||||
}
|
||||
this.finishStream(ts)
|
||||
} else {
|
||||
this.output("\n[assistant]", text)
|
||||
}
|
||||
|
||||
this.displayedMessages.set(ts, { ts, text, partial: false })
|
||||
this.completionResultStreamed = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output completion message (called from TaskCompleted handler).
|
||||
*/
|
||||
outputCompletionResult(ts: number, text: string): void {
|
||||
const previousDisplay = this.displayedMessages.get(ts)
|
||||
if (!previousDisplay || previousDisplay.partial) {
|
||||
this.output("\n[task complete]", text || "")
|
||||
if (this.completionResultStreamed) {
|
||||
// Text was already streamed via say:completion_result.
|
||||
this.output("\n[task complete]")
|
||||
} else {
|
||||
this.output("\n[task complete]", text || "")
|
||||
}
|
||||
this.displayedMessages.set(ts, { ts, text: text || "", partial: false })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./login.js"
|
||||
export * from "./logout.js"
|
||||
export * from "./status.js"
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
import http from "http"
|
||||
import { randomBytes } from "crypto"
|
||||
import net from "net"
|
||||
import { exec } from "child_process"
|
||||
|
||||
import { AUTH_BASE_URL } from "@/types/index.js"
|
||||
import { saveToken } from "@/lib/storage/index.js"
|
||||
|
||||
export interface LoginOptions {
|
||||
timeout?: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export type LoginResult =
|
||||
| {
|
||||
success: true
|
||||
token: string
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: string
|
||||
}
|
||||
|
||||
const LOCALHOST = "127.0.0.1"
|
||||
|
||||
export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginOptions = {}): Promise<LoginResult> {
|
||||
const state = randomBytes(16).toString("hex")
|
||||
const port = await getAvailablePort()
|
||||
const host = `http://${LOCALHOST}:${port}`
|
||||
|
||||
if (verbose) {
|
||||
console.log(`[Auth] Starting local callback server on port ${port}`)
|
||||
}
|
||||
|
||||
// Create promise that will be resolved when we receive the callback.
|
||||
const tokenPromise = new Promise<{ token: string; state: string }>((resolve, reject) => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url!, host)
|
||||
|
||||
if (url.pathname === "/callback") {
|
||||
const receivedState = url.searchParams.get("state")
|
||||
const token = url.searchParams.get("token")
|
||||
const error = url.searchParams.get("error")
|
||||
|
||||
if (error) {
|
||||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=error-in-callback`)
|
||||
errorUrl.searchParams.set("message", error)
|
||||
res.writeHead(302, { Location: errorUrl.toString() })
|
||||
res.end(() => {
|
||||
server.close()
|
||||
reject(new Error(error))
|
||||
})
|
||||
} else if (!token) {
|
||||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=missing-token`)
|
||||
errorUrl.searchParams.set("message", "Missing token in callback")
|
||||
res.writeHead(302, { Location: errorUrl.toString() })
|
||||
res.end(() => {
|
||||
server.close()
|
||||
reject(new Error("Missing token in callback"))
|
||||
})
|
||||
} else if (receivedState !== state) {
|
||||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=invalid-state-parameter`)
|
||||
errorUrl.searchParams.set("message", "Invalid state parameter")
|
||||
res.writeHead(302, { Location: errorUrl.toString() })
|
||||
res.end(() => {
|
||||
server.close()
|
||||
reject(new Error("Invalid state parameter"))
|
||||
})
|
||||
} else {
|
||||
res.writeHead(302, { Location: `${AUTH_BASE_URL}/cli/sign-in?success=true` })
|
||||
res.end(() => {
|
||||
server.close()
|
||||
resolve({ token, state: receivedState })
|
||||
})
|
||||
}
|
||||
} else {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" })
|
||||
res.end("Not found")
|
||||
}
|
||||
})
|
||||
|
||||
server.listen(port, LOCALHOST)
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
server.close()
|
||||
reject(new Error("Authentication timed out"))
|
||||
}, timeout)
|
||||
|
||||
server.on("close", () => {
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
})
|
||||
|
||||
const authUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in`)
|
||||
authUrl.searchParams.set("state", state)
|
||||
authUrl.searchParams.set("callback", `${host}/callback`)
|
||||
|
||||
console.log("Opening browser for authentication...")
|
||||
console.log(`If the browser doesn't open, visit: ${authUrl.toString()}`)
|
||||
|
||||
try {
|
||||
await openBrowser(authUrl.toString())
|
||||
} catch (error) {
|
||||
if (verbose) {
|
||||
console.warn("[Auth] Failed to open browser automatically:", error)
|
||||
}
|
||||
|
||||
console.log("Please open the URL above in your browser manually.")
|
||||
}
|
||||
|
||||
try {
|
||||
const { token } = await tokenPromise
|
||||
await saveToken(token)
|
||||
console.log("✓ Successfully authenticated!")
|
||||
return { success: true, token }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`✗ Authentication failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
async function getAvailablePort(startPort = 49152, endPort = 65535): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer()
|
||||
let port = startPort
|
||||
|
||||
const tryPort = () => {
|
||||
server.once("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "EADDRINUSE" && port < endPort) {
|
||||
port++
|
||||
tryPort()
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
|
||||
server.once("listening", () => {
|
||||
server.close(() => {
|
||||
resolve(port)
|
||||
})
|
||||
})
|
||||
|
||||
server.listen(port, LOCALHOST)
|
||||
}
|
||||
|
||||
tryPort()
|
||||
})
|
||||
}
|
||||
|
||||
function openBrowser(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const platform = process.platform
|
||||
let command: string
|
||||
|
||||
switch (platform) {
|
||||
case "darwin":
|
||||
command = `open "${url}"`
|
||||
break
|
||||
case "win32":
|
||||
command = `start "" "${url}"`
|
||||
break
|
||||
default:
|
||||
// Linux and other Unix-like systems.
|
||||
command = `xdg-open "${url}"`
|
||||
break
|
||||
}
|
||||
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { clearToken, hasToken, getCredentialsPath } from "@/lib/storage/index.js"
|
||||
|
||||
export interface LogoutOptions {
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export interface LogoutResult {
|
||||
success: boolean
|
||||
wasLoggedIn: boolean
|
||||
}
|
||||
|
||||
export async function logout({ verbose = false }: LogoutOptions = {}): Promise<LogoutResult> {
|
||||
const wasLoggedIn = await hasToken()
|
||||
|
||||
if (!wasLoggedIn) {
|
||||
console.log("You are not currently logged in.")
|
||||
return { success: true, wasLoggedIn: false }
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log(`[Auth] Removing credentials from ${getCredentialsPath()}`)
|
||||
}
|
||||
|
||||
await clearToken()
|
||||
console.log("✓ Successfully logged out")
|
||||
return { success: true, wasLoggedIn: true }
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import { loadToken, loadCredentials, getCredentialsPath } from "@/lib/storage/index.js"
|
||||
import { isTokenExpired, isTokenValid, getTokenExpirationDate } from "@/lib/auth/index.js"
|
||||
|
||||
export interface StatusOptions {
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export interface StatusResult {
|
||||
authenticated: boolean
|
||||
expired?: boolean
|
||||
expiringSoon?: boolean
|
||||
userId?: string
|
||||
orgId?: string | null
|
||||
expiresAt?: Date
|
||||
createdAt?: Date
|
||||
}
|
||||
|
||||
export async function status(options: StatusOptions = {}): Promise<StatusResult> {
|
||||
const { verbose = false } = options
|
||||
|
||||
const token = await loadToken()
|
||||
|
||||
if (!token) {
|
||||
console.log("✗ Not authenticated")
|
||||
console.log("")
|
||||
console.log("Run: roo auth login")
|
||||
return { authenticated: false }
|
||||
}
|
||||
|
||||
const expiresAt = getTokenExpirationDate(token)
|
||||
const expired = !isTokenValid(token)
|
||||
const expiringSoon = isTokenExpired(token, 24 * 60 * 60) && !expired
|
||||
|
||||
const credentials = await loadCredentials()
|
||||
const createdAt = credentials?.createdAt ? new Date(credentials.createdAt) : undefined
|
||||
|
||||
if (expired) {
|
||||
console.log("✗ Authentication token expired")
|
||||
console.log("")
|
||||
console.log("Run: roo auth login")
|
||||
|
||||
return {
|
||||
authenticated: false,
|
||||
expired: true,
|
||||
expiresAt: expiresAt ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (expiringSoon) {
|
||||
console.log("⚠ Expires soon; refresh with `roo auth login`")
|
||||
} else {
|
||||
console.log("✓ Authenticated")
|
||||
}
|
||||
|
||||
if (expiresAt) {
|
||||
const remaining = getTimeRemaining(expiresAt)
|
||||
console.log(` Expires: ${formatDate(expiresAt)} (${remaining})`)
|
||||
}
|
||||
|
||||
if (createdAt && verbose) {
|
||||
console.log(` Created: ${formatDate(createdAt)}`)
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log(` Credentials: ${getCredentialsPath()}`)
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
expired: false,
|
||||
expiringSoon,
|
||||
expiresAt: expiresAt ?? undefined,
|
||||
createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
|
||||
}
|
||||
|
||||
function getTimeRemaining(date: Date): string {
|
||||
const now = new Date()
|
||||
const diff = date.getTime() - now.getTime()
|
||||
|
||||
if (diff <= 0) {
|
||||
return "expired"
|
||||
}
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
|
||||
if (days > 0) {
|
||||
return `${days} day${days === 1 ? "" : "s"}`
|
||||
}
|
||||
|
||||
return `${hours} hour${hours === 1 ? "" : "s"}`
|
||||
}
|
||||
104
apps/cli/src/commands/cli/__tests__/cancellation.test.ts
Normal file
104
apps/cli/src/commands/cli/__tests__/cancellation.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import {
|
||||
isCancellationLikeError,
|
||||
isExpectedControlFlowError,
|
||||
isNoActiveTaskLikeError,
|
||||
isStreamTeardownLikeError,
|
||||
} from "../cancellation.js"
|
||||
|
||||
describe("isCancellationLikeError", () => {
|
||||
it("returns true for aborted error messages", () => {
|
||||
expect(isCancellationLikeError(new Error("[RooCode#say] task 123 aborted"))).toBe(true)
|
||||
expect(isCancellationLikeError("AbortError: operation aborted")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns true for abort/cancel error names and codes", () => {
|
||||
expect(isCancellationLikeError({ name: "AbortError", message: "stop now" })).toBe(true)
|
||||
expect(isCancellationLikeError({ code: "ABORT_ERR", message: "aborted" })).toBe(true)
|
||||
expect(isCancellationLikeError({ code: "ERR_CANCELED", message: "request failed" })).toBe(true)
|
||||
})
|
||||
|
||||
it("returns true for canceled/cancelled error messages", () => {
|
||||
expect(isCancellationLikeError(new Error("Request canceled"))).toBe(true)
|
||||
expect(isCancellationLikeError(new Error("request cancelled by user"))).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for non-cancellation errors", () => {
|
||||
expect(isCancellationLikeError(new Error("network timeout"))).toBe(false)
|
||||
expect(isCancellationLikeError("validation failed")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isNoActiveTaskLikeError", () => {
|
||||
it("matches task-settled cancel race messages", () => {
|
||||
expect(isNoActiveTaskLikeError(new Error("no active task to cancel"))).toBe(true)
|
||||
expect(isNoActiveTaskLikeError(new Error("task not found"))).toBe(true)
|
||||
expect(isNoActiveTaskLikeError("already completed")).toBe(true)
|
||||
})
|
||||
|
||||
it("does not match unrelated messages", () => {
|
||||
expect(isNoActiveTaskLikeError("network timeout")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isStreamTeardownLikeError", () => {
|
||||
it("matches common stream teardown errors", () => {
|
||||
expect(isStreamTeardownLikeError({ code: "EPIPE", message: "broken pipe" })).toBe(true)
|
||||
expect(isStreamTeardownLikeError({ code: "ERR_STREAM_DESTROYED", message: "stream destroyed" })).toBe(true)
|
||||
expect(isStreamTeardownLikeError(new Error("write after end"))).toBe(true)
|
||||
})
|
||||
|
||||
it("does not match unrelated stream errors", () => {
|
||||
expect(isStreamTeardownLikeError(new Error("permission denied"))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isExpectedControlFlowError", () => {
|
||||
it("returns false when not in stdin stream mode", () => {
|
||||
expect(
|
||||
isExpectedControlFlowError(new Error("AbortError: aborted"), {
|
||||
stdinStreamMode: false,
|
||||
operation: "runtime",
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("accepts cancellation-like runtime errors in stdin stream mode", () => {
|
||||
expect(
|
||||
isExpectedControlFlowError(new Error("AbortError: aborted"), {
|
||||
stdinStreamMode: true,
|
||||
operation: "runtime",
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("accepts no-active-task races for cancel operations", () => {
|
||||
expect(
|
||||
isExpectedControlFlowError(new Error("task not found"), {
|
||||
stdinStreamMode: true,
|
||||
operation: "cancel",
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("accepts stream teardown errors during shutdown", () => {
|
||||
expect(
|
||||
isExpectedControlFlowError(
|
||||
{ code: "EPIPE", message: "broken pipe" },
|
||||
{
|
||||
stdinStreamMode: true,
|
||||
shuttingDown: true,
|
||||
operation: "runtime",
|
||||
},
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects unrelated errors", () => {
|
||||
expect(
|
||||
isExpectedControlFlowError(new Error("authentication failed"), {
|
||||
stdinStreamMode: true,
|
||||
operation: "runtime",
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
84
apps/cli/src/commands/cli/__tests__/list.test.ts
Normal file
84
apps/cli/src/commands/cli/__tests__/list.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { readWorkspaceTaskSessions } from "@/lib/task-history/index.js"
|
||||
|
||||
import { listSessions, parseFormat } from "../list.js"
|
||||
|
||||
vi.mock("@/lib/task-history/index.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/task-history/index.js")>()
|
||||
return {
|
||||
...actual,
|
||||
readWorkspaceTaskSessions: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe("parseFormat", () => {
|
||||
it("defaults to json when undefined", () => {
|
||||
expect(parseFormat(undefined)).toBe("json")
|
||||
})
|
||||
|
||||
it("returns json for 'json'", () => {
|
||||
expect(parseFormat("json")).toBe("json")
|
||||
})
|
||||
|
||||
it("returns text for 'text'", () => {
|
||||
expect(parseFormat("text")).toBe("text")
|
||||
})
|
||||
|
||||
it("is case-insensitive", () => {
|
||||
expect(parseFormat("JSON")).toBe("json")
|
||||
expect(parseFormat("Text")).toBe("text")
|
||||
expect(parseFormat("TEXT")).toBe("text")
|
||||
})
|
||||
|
||||
it("throws on invalid format", () => {
|
||||
expect(() => parseFormat("xml")).toThrow('Invalid format: xml. Must be "json" or "text".')
|
||||
})
|
||||
|
||||
it("throws on empty string", () => {
|
||||
expect(() => parseFormat("")).toThrow("Invalid format")
|
||||
})
|
||||
})
|
||||
|
||||
describe("listSessions", () => {
|
||||
const workspacePath = process.cwd()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const captureStdout = async (fn: () => Promise<void>): Promise<string> => {
|
||||
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
|
||||
|
||||
try {
|
||||
await fn()
|
||||
return stdoutSpy.mock.calls.map(([chunk]) => String(chunk)).join("")
|
||||
} finally {
|
||||
stdoutSpy.mockRestore()
|
||||
}
|
||||
}
|
||||
|
||||
it("uses the CLI runtime storage path and prints JSON output", async () => {
|
||||
vi.mocked(readWorkspaceTaskSessions).mockResolvedValue([
|
||||
{ id: "s1", task: "Task 1", ts: 1_700_000_000_000, mode: "code" },
|
||||
])
|
||||
|
||||
const output = await captureStdout(() => listSessions({ format: "json", workspace: workspacePath }))
|
||||
|
||||
expect(readWorkspaceTaskSessions).toHaveBeenCalledWith(workspacePath)
|
||||
expect(JSON.parse(output)).toEqual({
|
||||
workspace: workspacePath,
|
||||
sessions: [{ id: "s1", task: "Task 1", ts: 1_700_000_000_000, mode: "code" }],
|
||||
})
|
||||
})
|
||||
|
||||
it("prints tab-delimited text output with ISO timestamps and formatted titles", async () => {
|
||||
vi.mocked(readWorkspaceTaskSessions).mockResolvedValue([
|
||||
{ id: "s1", task: "Task 1", ts: Date.UTC(2024, 0, 1, 0, 0, 0) },
|
||||
{ id: "s2", task: " ", ts: Date.UTC(2024, 0, 1, 1, 0, 0) },
|
||||
])
|
||||
|
||||
const output = await captureStdout(() => listSessions({ format: "text", workspace: workspacePath }))
|
||||
const lines = output.trim().split("\n")
|
||||
|
||||
expect(lines).toEqual(["s1\t2024-01-01T00:00:00.000Z\tTask 1", "s2\t2024-01-01T01:00:00.000Z\t(untitled)"])
|
||||
})
|
||||
})
|
||||
247
apps/cli/src/commands/cli/__tests__/parse-stdin-command.test.ts
Normal file
247
apps/cli/src/commands/cli/__tests__/parse-stdin-command.test.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import { parseStdinStreamCommand, shouldSendMessageAsAskResponse } from "../stdin-stream.js"
|
||||
|
||||
describe("parseStdinStreamCommand", () => {
|
||||
describe("valid commands", () => {
|
||||
it("parses a start command", () => {
|
||||
const result = parseStdinStreamCommand(
|
||||
JSON.stringify({ command: "start", requestId: "req-1", prompt: "hello" }),
|
||||
1,
|
||||
)
|
||||
expect(result).toEqual({ command: "start", requestId: "req-1", prompt: "hello" })
|
||||
})
|
||||
|
||||
it("parses a start command with taskId", () => {
|
||||
const result = parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "start",
|
||||
requestId: "req-task-id",
|
||||
prompt: "hello",
|
||||
taskId: "018f7fc8-7c96-7f7c-98aa-2ec4ff7f6d87",
|
||||
}),
|
||||
1,
|
||||
)
|
||||
expect(result).toEqual({
|
||||
command: "start",
|
||||
requestId: "req-task-id",
|
||||
prompt: "hello",
|
||||
taskId: "018f7fc8-7c96-7f7c-98aa-2ec4ff7f6d87",
|
||||
})
|
||||
})
|
||||
|
||||
it("parses a message command", () => {
|
||||
const result = parseStdinStreamCommand(
|
||||
JSON.stringify({ command: "message", requestId: "req-2", prompt: "follow up" }),
|
||||
1,
|
||||
)
|
||||
expect(result).toEqual({ command: "message", requestId: "req-2", prompt: "follow up" })
|
||||
})
|
||||
|
||||
it("parses start and message images", () => {
|
||||
const start = parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "start",
|
||||
requestId: "req-img-start",
|
||||
prompt: "hello",
|
||||
images: ["data:image/jpeg;base64,abc123"],
|
||||
}),
|
||||
1,
|
||||
)
|
||||
expect(start).toEqual({
|
||||
command: "start",
|
||||
requestId: "req-img-start",
|
||||
prompt: "hello",
|
||||
images: ["data:image/jpeg;base64,abc123"],
|
||||
})
|
||||
|
||||
const message = parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "message",
|
||||
requestId: "req-img-msg",
|
||||
prompt: "follow up",
|
||||
images: ["data:image/png;base64,xyz456"],
|
||||
}),
|
||||
1,
|
||||
)
|
||||
expect(message).toEqual({
|
||||
command: "message",
|
||||
requestId: "req-img-msg",
|
||||
prompt: "follow up",
|
||||
images: ["data:image/png;base64,xyz456"],
|
||||
})
|
||||
})
|
||||
|
||||
it.each(["cancel", "ping", "shutdown"] as const)("parses a %s command (no prompt required)", (command) => {
|
||||
const result = parseStdinStreamCommand(JSON.stringify({ command, requestId: "req-3" }), 1)
|
||||
expect(result).toEqual({ command, requestId: "req-3" })
|
||||
})
|
||||
|
||||
it("trims whitespace from requestId", () => {
|
||||
const result = parseStdinStreamCommand(JSON.stringify({ command: "ping", requestId: " req-4 " }), 1)
|
||||
expect(result.requestId).toBe("req-4")
|
||||
})
|
||||
|
||||
it("ignores extra fields", () => {
|
||||
const result = parseStdinStreamCommand(
|
||||
JSON.stringify({ command: "ping", requestId: "req-5", extra: "ignored", nested: { a: 1 } }),
|
||||
1,
|
||||
)
|
||||
expect(result).toEqual({ command: "ping", requestId: "req-5" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid input", () => {
|
||||
it("throws on invalid JSON", () => {
|
||||
expect(() => parseStdinStreamCommand("not json", 3)).toThrow("stdin command line 3: invalid JSON")
|
||||
})
|
||||
|
||||
it("throws on non-object JSON (string)", () => {
|
||||
expect(() => parseStdinStreamCommand('"hello"', 1)).toThrow("expected JSON object")
|
||||
})
|
||||
|
||||
it("throws on non-object JSON (array)", () => {
|
||||
// Arrays pass isRecord (typeof [] === "object") but lack a command field
|
||||
expect(() => parseStdinStreamCommand("[]", 1)).toThrow('missing string "command"')
|
||||
})
|
||||
|
||||
it("throws on non-object JSON (number)", () => {
|
||||
expect(() => parseStdinStreamCommand("42", 1)).toThrow("expected JSON object")
|
||||
})
|
||||
|
||||
it("throws on null", () => {
|
||||
expect(() => parseStdinStreamCommand("null", 1)).toThrow("expected JSON object")
|
||||
})
|
||||
|
||||
it("throws when command field is missing", () => {
|
||||
expect(() => parseStdinStreamCommand(JSON.stringify({ requestId: "req" }), 5)).toThrow(
|
||||
'stdin command line 5: missing string "command"',
|
||||
)
|
||||
})
|
||||
|
||||
it("throws when command is not a string", () => {
|
||||
expect(() => parseStdinStreamCommand(JSON.stringify({ command: 123, requestId: "req" }), 1)).toThrow(
|
||||
'missing string "command"',
|
||||
)
|
||||
})
|
||||
|
||||
it("throws on unsupported command name", () => {
|
||||
expect(() => parseStdinStreamCommand(JSON.stringify({ command: "unknown", requestId: "req" }), 2)).toThrow(
|
||||
'stdin command line 2: unsupported command "unknown"',
|
||||
)
|
||||
})
|
||||
|
||||
it("throws when requestId is missing", () => {
|
||||
expect(() => parseStdinStreamCommand(JSON.stringify({ command: "ping" }), 1)).toThrow(
|
||||
'missing non-empty string "requestId"',
|
||||
)
|
||||
})
|
||||
|
||||
it("throws when requestId is empty", () => {
|
||||
expect(() => parseStdinStreamCommand(JSON.stringify({ command: "ping", requestId: " " }), 1)).toThrow(
|
||||
'missing non-empty string "requestId"',
|
||||
)
|
||||
})
|
||||
|
||||
it("throws when start command has no prompt", () => {
|
||||
expect(() => parseStdinStreamCommand(JSON.stringify({ command: "start", requestId: "req" }), 1)).toThrow(
|
||||
'"start" requires non-empty string "prompt"',
|
||||
)
|
||||
})
|
||||
|
||||
it("throws when start taskId is empty, not a string, or not a UUID", () => {
|
||||
expect(() =>
|
||||
parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "start",
|
||||
requestId: "req-start-task-id-empty",
|
||||
prompt: "hello",
|
||||
taskId: " ",
|
||||
}),
|
||||
1,
|
||||
),
|
||||
).toThrow('"start" taskId must be a non-empty string')
|
||||
|
||||
expect(() =>
|
||||
parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "start",
|
||||
requestId: "req-start-task-id-num",
|
||||
prompt: "hello",
|
||||
taskId: 123,
|
||||
}),
|
||||
1,
|
||||
),
|
||||
).toThrow('"start" taskId must be a non-empty string')
|
||||
|
||||
expect(() =>
|
||||
parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "start",
|
||||
requestId: "req-start-task-id-invalid-format",
|
||||
prompt: "hello",
|
||||
taskId: "task-123",
|
||||
}),
|
||||
1,
|
||||
),
|
||||
).toThrow('"start" taskId must be a valid UUID')
|
||||
})
|
||||
|
||||
it("throws when message command has empty prompt", () => {
|
||||
expect(() =>
|
||||
parseStdinStreamCommand(JSON.stringify({ command: "message", requestId: "req", prompt: " " }), 1),
|
||||
).toThrow('"message" requires non-empty string "prompt"')
|
||||
})
|
||||
|
||||
it("throws when start or message images are not string arrays", () => {
|
||||
expect(() =>
|
||||
parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "start",
|
||||
requestId: "req-start-img",
|
||||
prompt: "hello",
|
||||
images: "not-an-array",
|
||||
}),
|
||||
1,
|
||||
),
|
||||
).toThrow('"start" images must be an array of strings')
|
||||
|
||||
expect(() =>
|
||||
parseStdinStreamCommand(
|
||||
JSON.stringify({
|
||||
command: "message",
|
||||
requestId: "req-msg-img",
|
||||
prompt: "follow up",
|
||||
images: ["ok", 123],
|
||||
}),
|
||||
1,
|
||||
),
|
||||
).toThrow('"message" images must be an array of strings')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("shouldSendMessageAsAskResponse", () => {
|
||||
it("routes completion_result asks as ask responses", () => {
|
||||
expect(shouldSendMessageAsAskResponse(true, "completion_result")).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
"followup",
|
||||
"tool",
|
||||
"command",
|
||||
"use_mcp_server",
|
||||
"resume_task",
|
||||
"resume_completed_task",
|
||||
"mistake_limit_reached",
|
||||
])("routes %s asks as ask responses", (ask) => {
|
||||
expect(shouldSendMessageAsAskResponse(true, ask)).toBe(true)
|
||||
})
|
||||
|
||||
it("does not route when not waiting for input", () => {
|
||||
expect(shouldSendMessageAsAskResponse(false, "completion_result")).toBe(false)
|
||||
})
|
||||
|
||||
it("does not route unknown asks", () => {
|
||||
expect(shouldSendMessageAsAskResponse(true, "unknown")).toBe(false)
|
||||
expect(shouldSendMessageAsAskResponse(true, undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
93
apps/cli/src/commands/cli/__tests__/upgrade.test.ts
Normal file
93
apps/cli/src/commands/cli/__tests__/upgrade.test.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { compareVersions, getLatestCliVersion, upgrade } from "../upgrade.js"
|
||||
|
||||
function createFetchResponse(body: unknown, init: { ok?: boolean; status?: number } = {}): Response {
|
||||
const { ok = true, status = 200 } = init
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
json: async () => body,
|
||||
} as Response
|
||||
}
|
||||
|
||||
describe("compareVersions", () => {
|
||||
it("returns 1 when first version is newer", () => {
|
||||
expect(compareVersions("0.2.0", "0.1.9")).toBe(1)
|
||||
})
|
||||
|
||||
it("returns -1 when first version is older", () => {
|
||||
expect(compareVersions("0.1.4", "0.1.5")).toBe(-1)
|
||||
})
|
||||
|
||||
it("returns 0 when versions are equivalent", () => {
|
||||
expect(compareVersions("v1.2.0", "1.2")).toBe(0)
|
||||
})
|
||||
|
||||
it("supports cli tag prefixes and prerelease metadata", () => {
|
||||
expect(compareVersions("cli-v1.2.3", "1.2.2")).toBe(1)
|
||||
expect(compareVersions("1.2.3-beta.1", "1.2.3")).toBe(0)
|
||||
})
|
||||
|
||||
it("compares multi-digit patch versions numerically", () => {
|
||||
expect(compareVersions("0.1.10", "0.1.9")).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getLatestCliVersion", () => {
|
||||
it("returns the highest cli-v release tag from GitHub releases", async () => {
|
||||
const fetchImpl = (async () =>
|
||||
createFetchResponse([
|
||||
{ tag_name: "cli-v0.1.9" },
|
||||
{ tag_name: "v9.9.9" },
|
||||
{ tag_name: "cli-v0.1.10" },
|
||||
{ tag_name: "cli-v0.1.8" },
|
||||
])) as typeof fetch
|
||||
|
||||
await expect(getLatestCliVersion(fetchImpl)).resolves.toBe("0.1.10")
|
||||
})
|
||||
|
||||
it("throws when release check fails", async () => {
|
||||
const fetchImpl = (async () => createFetchResponse({}, { ok: false, status: 503 })) as typeof fetch
|
||||
|
||||
await expect(getLatestCliVersion(fetchImpl)).rejects.toThrow("Failed to check latest version")
|
||||
})
|
||||
})
|
||||
|
||||
describe("upgrade", () => {
|
||||
let logSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
logSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("does not run installer when already up to date", async () => {
|
||||
const runInstaller = vi.fn(async () => undefined)
|
||||
const fetchImpl = (async () => createFetchResponse([{ tag_name: "cli-v0.1.4" }])) as typeof fetch
|
||||
|
||||
await upgrade({
|
||||
currentVersion: "0.1.4",
|
||||
fetchImpl,
|
||||
runInstaller,
|
||||
})
|
||||
|
||||
expect(runInstaller).not.toHaveBeenCalled()
|
||||
expect(logSpy).toHaveBeenCalledWith("Roo CLI is already up to date.")
|
||||
})
|
||||
|
||||
it("runs installer when a newer version is available", async () => {
|
||||
const runInstaller = vi.fn(async () => undefined)
|
||||
const fetchImpl = (async () => createFetchResponse([{ tag_name: "cli-v0.2.0" }])) as typeof fetch
|
||||
|
||||
await upgrade({
|
||||
currentVersion: "0.1.4",
|
||||
fetchImpl,
|
||||
runInstaller,
|
||||
})
|
||||
|
||||
expect(runInstaller).toHaveBeenCalledTimes(1)
|
||||
expect(logSpy).toHaveBeenCalledWith("✓ Upgrade completed.")
|
||||
})
|
||||
})
|
||||
131
apps/cli/src/commands/cli/cancellation.ts
Normal file
131
apps/cli/src/commands/cli/cancellation.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
const CANCELLATION_ERROR_PATTERNS = ["aborted", "aborterror", "cancelled", "canceled"]
|
||||
const CANCELLATION_ERROR_NAMES = new Set(["aborterror"])
|
||||
const CANCELLATION_ERROR_CODES = new Set(["ABORT_ERR", "ERR_CANCELED", "ERR_CANCELLED"])
|
||||
const NO_ACTIVE_TASK_PATTERNS = [
|
||||
"no active task",
|
||||
"no task to cancel",
|
||||
"task not found",
|
||||
"unable to find task",
|
||||
"already completed",
|
||||
"already cancelled",
|
||||
"already canceled",
|
||||
]
|
||||
const STREAM_TEARDOWN_CODES = new Set(["EPIPE", "ECONNRESET", "ERR_STREAM_DESTROYED", "ERR_STREAM_PREMATURE_CLOSE"])
|
||||
const STREAM_TEARDOWN_PATTERNS = [
|
||||
"write after end",
|
||||
"stream destroyed",
|
||||
"premature close",
|
||||
"socket hang up",
|
||||
"broken pipe",
|
||||
]
|
||||
|
||||
export interface ExpectedControlFlowErrorContext {
|
||||
stdinStreamMode: boolean
|
||||
cancelRequested?: boolean
|
||||
shuttingDown?: boolean
|
||||
operation?: "runtime" | "client" | "cancel" | "shutdown"
|
||||
}
|
||||
|
||||
interface ErrorMetadata {
|
||||
message: string
|
||||
normalizedMessage: string
|
||||
name?: string
|
||||
normalizedName?: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
function getErrorMetadata(error: unknown): ErrorMetadata {
|
||||
if (error instanceof Error) {
|
||||
const maybeCode = (error as Error & { code?: unknown }).code
|
||||
const code = typeof maybeCode === "string" ? maybeCode : undefined
|
||||
return {
|
||||
message: error.message,
|
||||
normalizedMessage: error.message.toLowerCase(),
|
||||
name: error.name,
|
||||
normalizedName: error.name.toLowerCase(),
|
||||
code,
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const nameRaw = (error as { name?: unknown }).name
|
||||
const messageRaw = (error as { message?: unknown }).message
|
||||
const codeRaw = (error as { code?: unknown }).code
|
||||
const message = typeof messageRaw === "string" ? messageRaw : String(error)
|
||||
return {
|
||||
message,
|
||||
normalizedMessage: message.toLowerCase(),
|
||||
name: typeof nameRaw === "string" ? nameRaw : undefined,
|
||||
normalizedName: typeof nameRaw === "string" ? nameRaw.toLowerCase() : undefined,
|
||||
code: typeof codeRaw === "string" ? codeRaw : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const message = String(error)
|
||||
return {
|
||||
message,
|
||||
normalizedMessage: message.toLowerCase(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort classifier for cancellation/abort failures.
|
||||
*/
|
||||
export function isCancellationLikeError(error: unknown): boolean {
|
||||
const details = getErrorMetadata(error)
|
||||
|
||||
if (details.code && CANCELLATION_ERROR_CODES.has(details.code)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (details.normalizedName && CANCELLATION_ERROR_NAMES.has(details.normalizedName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return CANCELLATION_ERROR_PATTERNS.some((pattern) => details.normalizedMessage.includes(pattern))
|
||||
}
|
||||
|
||||
export function isNoActiveTaskLikeError(error: unknown): boolean {
|
||||
const details = getErrorMetadata(error)
|
||||
return NO_ACTIVE_TASK_PATTERNS.some((pattern) => details.normalizedMessage.includes(pattern))
|
||||
}
|
||||
|
||||
export function isStreamTeardownLikeError(error: unknown): boolean {
|
||||
const details = getErrorMetadata(error)
|
||||
if (details.code && STREAM_TEARDOWN_CODES.has(details.code)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return STREAM_TEARDOWN_PATTERNS.some((pattern) => details.normalizedMessage.includes(pattern))
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify errors that should be treated as expected control flow rather than
|
||||
* fatal failures while handling stdin stream tasks.
|
||||
*/
|
||||
export function isExpectedControlFlowError(error: unknown, context: ExpectedControlFlowErrorContext): boolean {
|
||||
if (!context.stdinStreamMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (context.shuttingDown && isStreamTeardownLikeError(error)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isCancelLike = isCancellationLikeError(error)
|
||||
if (isCancelLike && (context.cancelRequested || context.shuttingDown || context.operation === "runtime")) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
isNoActiveTaskLikeError(error) &&
|
||||
(context.cancelRequested ||
|
||||
context.shuttingDown ||
|
||||
context.operation === "cancel" ||
|
||||
context.operation === "shutdown")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
export * from "./run.js"
|
||||
export * from "./list.js"
|
||||
export * from "./upgrade.js"
|
||||
|
|
|
|||
312
apps/cli/src/commands/cli/list.ts
Normal file
312
apps/cli/src/commands/cli/list.ts
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import pWaitFor from "p-wait-for"
|
||||
|
||||
import type { TaskSessionEntry } from "@roo-code/core/cli"
|
||||
import type { Command, ModelRecord, WebviewMessage } from "@roo-code/types"
|
||||
import { openRouterDefaultModelId } from "@roo-code/types"
|
||||
|
||||
import { ExtensionHost, type ExtensionHostOptions } from "@/agent/index.js"
|
||||
import { readWorkspaceTaskSessions } from "@/lib/task-history/index.js"
|
||||
import { getDefaultExtensionPath } from "@/lib/utils/extension.js"
|
||||
import { getApiKeyFromEnv } from "@/lib/utils/provider.js"
|
||||
import { isRecord } from "@/lib/utils/guards.js"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 10_000
|
||||
|
||||
type ListFormat = "json" | "text"
|
||||
|
||||
type BaseListOptions = {
|
||||
workspace?: string
|
||||
extension?: string
|
||||
apiKey?: string
|
||||
format?: string
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
type CommandLike = Pick<Command, "name" | "source" | "filePath" | "description" | "argumentHint">
|
||||
type ModeLike = { slug: string; name: string }
|
||||
type SessionLike = TaskSessionEntry
|
||||
type ListHostOptions = { ephemeral: boolean }
|
||||
|
||||
export function parseFormat(rawFormat: string | undefined): ListFormat {
|
||||
const format = (rawFormat ?? "json").toLowerCase()
|
||||
if (format === "json" || format === "text") {
|
||||
return format
|
||||
}
|
||||
|
||||
throw new Error(`Invalid format: ${rawFormat}. Must be "json" or "text".`)
|
||||
}
|
||||
|
||||
function resolveWorkspacePath(workspace: string | undefined): string {
|
||||
const resolved = workspace ? path.resolve(workspace) : process.cwd()
|
||||
|
||||
if (!fs.existsSync(resolved)) {
|
||||
throw new Error(`Workspace path does not exist: ${resolved}`)
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
function resolveExtensionPath(extension: string | undefined): string {
|
||||
const resolved = path.resolve(extension || getDefaultExtensionPath(__dirname))
|
||||
|
||||
if (!fs.existsSync(path.join(resolved, "extension.js"))) {
|
||||
throw new Error(`Extension bundle not found at: ${resolved}`)
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
function outputJson(data: unknown): void {
|
||||
process.stdout.write(JSON.stringify(data, null, 2) + "\n")
|
||||
}
|
||||
|
||||
function outputCommandsText(commands: CommandLike[]): void {
|
||||
for (const command of commands) {
|
||||
const description = command.description ? ` - ${command.description}` : ""
|
||||
process.stdout.write(`/${command.name} (${command.source})${description}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
function outputModesText(modes: ModeLike[]): void {
|
||||
for (const mode of modes) {
|
||||
process.stdout.write(`${mode.slug}\t${mode.name}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
function outputModelsText(models: ModelRecord): void {
|
||||
for (const modelId of Object.keys(models).sort()) {
|
||||
process.stdout.write(`${modelId}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
function formatSessionTitle(task: string): string {
|
||||
const compact = task.replace(/\s+/g, " ").trim()
|
||||
|
||||
if (!compact) {
|
||||
return "(untitled)"
|
||||
}
|
||||
|
||||
return compact.length <= 120 ? compact : `${compact.slice(0, 117)}...`
|
||||
}
|
||||
|
||||
function outputSessionsText(sessions: SessionLike[]): void {
|
||||
for (const session of sessions) {
|
||||
const startedAt = Number.isFinite(session.ts) ? new Date(session.ts).toISOString() : "unknown-time"
|
||||
process.stdout.write(`${session.id}\t${startedAt}\t${formatSessionTitle(session.task)}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
async function createListHost(options: BaseListOptions, hostOptions: ListHostOptions): Promise<ExtensionHost> {
|
||||
const workspacePath = resolveWorkspacePath(options.workspace)
|
||||
const extensionPath = resolveExtensionPath(options.extension)
|
||||
const apiKey = options.apiKey || getApiKeyFromEnv("openrouter")
|
||||
|
||||
const extensionHostOptions: ExtensionHostOptions = {
|
||||
mode: "code",
|
||||
reasoningEffort: undefined,
|
||||
user: null,
|
||||
provider: "openrouter",
|
||||
model: openRouterDefaultModelId,
|
||||
apiKey,
|
||||
workspacePath,
|
||||
extensionPath,
|
||||
nonInteractive: true,
|
||||
ephemeral: hostOptions.ephemeral,
|
||||
debug: options.debug ?? false,
|
||||
exitOnComplete: true,
|
||||
exitOnError: false,
|
||||
disableOutput: true,
|
||||
}
|
||||
|
||||
const host = new ExtensionHost(extensionHostOptions)
|
||||
|
||||
await host.activate()
|
||||
|
||||
// Best effort wait; mode/commands requests can still succeed without this.
|
||||
await pWaitFor(() => host.client.isInitialized(), {
|
||||
interval: 25,
|
||||
timeout: 2_000,
|
||||
}).catch(() => undefined)
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the extension and wait for a matching response message.
|
||||
* Returns `undefined` from `extract` to skip non-matching messages, or the
|
||||
* parsed value to resolve the promise.
|
||||
*/
|
||||
function requestFromExtension<T>(
|
||||
host: ExtensionHost,
|
||||
requestType: WebviewMessage["type"],
|
||||
extract: (message: Record<string, unknown>) => T | undefined,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let settled = false
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeoutId)
|
||||
host.off("extensionWebviewMessage", onMessage)
|
||||
offError()
|
||||
}
|
||||
|
||||
const finish = (fn: () => void) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
fn()
|
||||
}
|
||||
|
||||
const onMessage = (message: unknown) => {
|
||||
if (!isRecord(message)) {
|
||||
return
|
||||
}
|
||||
|
||||
let result: T | undefined
|
||||
try {
|
||||
result = extract(message)
|
||||
} catch (error) {
|
||||
finish(() => reject(error instanceof Error ? error : new Error(String(error))))
|
||||
return
|
||||
}
|
||||
|
||||
if (result !== undefined) {
|
||||
finish(() => resolve(result))
|
||||
}
|
||||
}
|
||||
|
||||
const offError = host.client.on("error", (error) => {
|
||||
finish(() => reject(error))
|
||||
})
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
finish(() =>
|
||||
reject(new Error(`Timed out waiting for ${requestType} response after ${REQUEST_TIMEOUT_MS}ms`)),
|
||||
)
|
||||
}, REQUEST_TIMEOUT_MS)
|
||||
|
||||
host.on("extensionWebviewMessage", onMessage)
|
||||
host.sendToExtension({ type: requestType })
|
||||
})
|
||||
}
|
||||
|
||||
function requestCommands(host: ExtensionHost): Promise<CommandLike[]> {
|
||||
return requestFromExtension(host, "requestCommands", (message) => {
|
||||
if (message.type !== "commands") {
|
||||
return undefined
|
||||
}
|
||||
return Array.isArray(message.commands) ? (message.commands as CommandLike[]) : []
|
||||
})
|
||||
}
|
||||
|
||||
function requestModes(host: ExtensionHost): Promise<ModeLike[]> {
|
||||
return requestFromExtension(host, "requestModes", (message) => {
|
||||
if (message.type !== "modes") {
|
||||
return undefined
|
||||
}
|
||||
return Array.isArray(message.modes) ? (message.modes as ModeLike[]) : []
|
||||
})
|
||||
}
|
||||
|
||||
function requestOpenRouterModels(host: ExtensionHost): Promise<ModelRecord> {
|
||||
return requestFromExtension(host, "requestRouterModels", (message) => {
|
||||
if (message.type !== "routerModels") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const routerModels = isRecord(message.routerModels) ? message.routerModels : {}
|
||||
const openRouterModels = routerModels.openrouter
|
||||
return isRecord(openRouterModels) ? (openRouterModels as ModelRecord) : {}
|
||||
})
|
||||
}
|
||||
|
||||
async function withHostAndSignalHandlers<T>(
|
||||
options: BaseListOptions,
|
||||
hostOptions: ListHostOptions,
|
||||
fn: (host: ExtensionHost) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const host = await createListHost(options, hostOptions)
|
||||
|
||||
const shutdown = async (exitCode: number) => {
|
||||
await host.dispose()
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
const onSigint = () => void shutdown(130)
|
||||
const onSigterm = () => void shutdown(143)
|
||||
|
||||
process.on("SIGINT", onSigint)
|
||||
process.on("SIGTERM", onSigterm)
|
||||
|
||||
try {
|
||||
return await fn(host)
|
||||
} finally {
|
||||
process.off("SIGINT", onSigint)
|
||||
process.off("SIGTERM", onSigterm)
|
||||
await host.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export async function listCommands(options: BaseListOptions): Promise<void> {
|
||||
const format = parseFormat(options.format)
|
||||
|
||||
await withHostAndSignalHandlers(options, { ephemeral: true }, async (host) => {
|
||||
const commands = await requestCommands(host)
|
||||
|
||||
if (format === "json") {
|
||||
outputJson({ commands })
|
||||
return
|
||||
}
|
||||
|
||||
outputCommandsText(commands)
|
||||
})
|
||||
}
|
||||
|
||||
export async function listModes(options: BaseListOptions): Promise<void> {
|
||||
const format = parseFormat(options.format)
|
||||
|
||||
await withHostAndSignalHandlers(options, { ephemeral: true }, async (host) => {
|
||||
const modes = await requestModes(host)
|
||||
|
||||
if (format === "json") {
|
||||
outputJson({ modes })
|
||||
return
|
||||
}
|
||||
|
||||
outputModesText(modes)
|
||||
})
|
||||
}
|
||||
|
||||
export async function listModels(options: BaseListOptions): Promise<void> {
|
||||
const format = parseFormat(options.format)
|
||||
|
||||
await withHostAndSignalHandlers(options, { ephemeral: true }, async (host) => {
|
||||
const models = await requestOpenRouterModels(host)
|
||||
|
||||
if (format === "json") {
|
||||
outputJson({ models })
|
||||
return
|
||||
}
|
||||
|
||||
outputModelsText(models)
|
||||
})
|
||||
}
|
||||
|
||||
export async function listSessions(options: BaseListOptions): Promise<void> {
|
||||
const format = parseFormat(options.format)
|
||||
const workspacePath = resolveWorkspacePath(options.workspace)
|
||||
const sessions = await readWorkspaceTaskSessions(workspacePath)
|
||||
|
||||
if (format === "json") {
|
||||
outputJson({ workspace: workspacePath, sessions })
|
||||
return
|
||||
}
|
||||
|
||||
outputSessionsText(sessions)
|
||||
}
|
||||
|
|
@ -3,32 +3,50 @@ import path from "path"
|
|||
import { fileURLToPath } from "url"
|
||||
|
||||
import { createElement } from "react"
|
||||
import pWaitFor from "p-wait-for"
|
||||
|
||||
import { setLogger } from "@roo-code/vscode-shim"
|
||||
|
||||
import {
|
||||
FlagOptions,
|
||||
isSupportedProvider,
|
||||
OnboardingProviderChoice,
|
||||
supportedProviders,
|
||||
DEFAULT_FLAGS,
|
||||
REASONING_EFFORTS,
|
||||
SDK_BASE_URL,
|
||||
OutputFormat,
|
||||
} from "@/types/index.js"
|
||||
import { isValidOutputFormat } from "@/types/json-events.js"
|
||||
import { JsonEventEmitter } from "@/agent/json-event-emitter.js"
|
||||
|
||||
import { createClient } from "@/lib/sdk/index.js"
|
||||
import { loadToken, loadSettings } from "@/lib/storage/index.js"
|
||||
import { loadSettings } from "@/lib/storage/index.js"
|
||||
import { readWorkspaceTaskSessions, resolveWorkspaceResumeSessionId } from "@/lib/task-history/index.js"
|
||||
import { getEnvVarName, getApiKeyFromEnv } from "@/lib/utils/provider.js"
|
||||
import { runOnboarding } from "@/lib/utils/onboarding.js"
|
||||
import { validateTerminalShellPath } from "@/lib/utils/shell.js"
|
||||
import { getDefaultExtensionPath } from "@/lib/utils/extension.js"
|
||||
import { isValidSessionId } from "@/lib/utils/session-id.js"
|
||||
import { VERSION } from "@/lib/utils/version.js"
|
||||
|
||||
import { ExtensionHost, ExtensionHostOptions } from "@/agent/index.js"
|
||||
import { isExpectedControlFlowError } from "./cancellation.js"
|
||||
import { runStdinStreamMode } from "./stdin-stream.js"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const SIGNAL_ONLY_EXIT_KEEPALIVE_MS = 60_000
|
||||
const STREAM_RESUME_WAIT_TIMEOUT_MS = 2_000
|
||||
|
||||
async function bootstrapResumeForStdinStream(host: ExtensionHost, sessionId: string): Promise<void> {
|
||||
host.sendToExtension({ type: "showTaskWithId", text: sessionId })
|
||||
|
||||
// Best-effort wait so early stdin "message" commands can target the resumed task.
|
||||
await pWaitFor(() => host.client.hasActiveTask() || host.isWaitingForInput(), {
|
||||
interval: 25,
|
||||
timeout: STREAM_RESUME_WAIT_TIMEOUT_MS,
|
||||
}).catch(() => undefined)
|
||||
}
|
||||
|
||||
function normalizeError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
export async function run(promptArg: string | undefined, flagOptions: FlagOptions) {
|
||||
setLogger({
|
||||
|
|
@ -49,82 +67,105 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
|
|||
prompt = fs.readFileSync(flagOptions.promptFile, "utf-8")
|
||||
}
|
||||
|
||||
const requestedSessionId = flagOptions.sessionId?.trim()
|
||||
const requestedCreateSessionId = flagOptions.createWithSessionId?.trim()
|
||||
const shouldContinueSession = flagOptions.continue
|
||||
const isResumeRequested = Boolean(requestedSessionId || shouldContinueSession)
|
||||
|
||||
if (flagOptions.createWithSessionId !== undefined && !requestedCreateSessionId) {
|
||||
console.error("[CLI] Error: --create-with-session-id requires a non-empty session id")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (flagOptions.sessionId !== undefined && !requestedSessionId) {
|
||||
console.error("[CLI] Error: --session-id requires a non-empty session id")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (requestedCreateSessionId && !isValidSessionId(requestedCreateSessionId)) {
|
||||
console.error("[CLI] Error: --create-with-session-id must be a valid UUID session id")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (requestedSessionId && !isValidSessionId(requestedSessionId)) {
|
||||
console.error("[CLI] Error: --session-id must be a valid UUID session id")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (requestedCreateSessionId && isResumeRequested) {
|
||||
console.error("[CLI] Error: cannot use --create-with-session-id with --session-id/--continue")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (requestedSessionId && shouldContinueSession) {
|
||||
console.error("[CLI] Error: cannot use --session-id with --continue")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (isResumeRequested && prompt) {
|
||||
console.error("[CLI] Error: cannot use prompt or --prompt-file with --session-id/--continue")
|
||||
console.error("[CLI] Usage: roo [--session-id <session-id> | --continue] [options]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Options
|
||||
|
||||
let rooToken = await loadToken()
|
||||
const settings = await loadSettings()
|
||||
|
||||
const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY
|
||||
const isTuiEnabled = !flagOptions.print && isTuiSupported
|
||||
const isOnboardingEnabled = isTuiEnabled && !rooToken && !flagOptions.provider && !settings.provider
|
||||
|
||||
// Determine effective values: CLI flags > settings file > DEFAULT_FLAGS.
|
||||
const effectiveMode = flagOptions.mode || settings.mode || DEFAULT_FLAGS.mode
|
||||
const effectiveModel = flagOptions.model || settings.model || DEFAULT_FLAGS.model
|
||||
const effectiveReasoningEffort =
|
||||
flagOptions.reasoningEffort || settings.reasoningEffort || DEFAULT_FLAGS.reasoningEffort
|
||||
const effectiveProvider = flagOptions.provider ?? settings.provider ?? (rooToken ? "roo" : "openrouter")
|
||||
const effectiveProvider = flagOptions.provider ?? settings.provider ?? "openrouter"
|
||||
const effectiveWorkspacePath = flagOptions.workspace ? path.resolve(flagOptions.workspace) : process.cwd()
|
||||
const effectiveDangerouslySkipPermissions =
|
||||
flagOptions.yes || flagOptions.dangerouslySkipPermissions || settings.dangerouslySkipPermissions || false
|
||||
const legacyRequireApprovalFromSettings =
|
||||
settings.requireApproval ??
|
||||
(settings.dangerouslySkipPermissions === undefined ? undefined : !settings.dangerouslySkipPermissions)
|
||||
const effectiveRequireApproval = flagOptions.requireApproval || legacyRequireApprovalFromSettings || false
|
||||
const effectiveExitOnComplete = flagOptions.print || flagOptions.oneshot || settings.oneshot || false
|
||||
const rawConsecutiveMistakeLimit =
|
||||
flagOptions.consecutiveMistakeLimit ?? settings.consecutiveMistakeLimit ?? DEFAULT_FLAGS.consecutiveMistakeLimit
|
||||
const effectiveConsecutiveMistakeLimit = Number(rawConsecutiveMistakeLimit)
|
||||
|
||||
if (!Number.isInteger(effectiveConsecutiveMistakeLimit) || effectiveConsecutiveMistakeLimit < 0) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid consecutive mistake limit: ${rawConsecutiveMistakeLimit}; must be a non-negative integer`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let terminalShell: string | undefined
|
||||
if (flagOptions.terminalShell !== undefined) {
|
||||
const validatedTerminalShell = await validateTerminalShellPath(flagOptions.terminalShell)
|
||||
|
||||
if (!validatedTerminalShell.valid) {
|
||||
console.error(
|
||||
`[CLI] Warning: ignoring --terminal-shell "${flagOptions.terminalShell}" (${validatedTerminalShell.reason})`,
|
||||
)
|
||||
} else {
|
||||
terminalShell = validatedTerminalShell.shellPath
|
||||
}
|
||||
}
|
||||
|
||||
const extensionHostOptions: ExtensionHostOptions = {
|
||||
mode: effectiveMode,
|
||||
reasoningEffort: effectiveReasoningEffort === "unspecified" ? undefined : effectiveReasoningEffort,
|
||||
consecutiveMistakeLimit: effectiveConsecutiveMistakeLimit,
|
||||
user: null,
|
||||
provider: effectiveProvider,
|
||||
model: effectiveModel,
|
||||
workspacePath: effectiveWorkspacePath,
|
||||
extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)),
|
||||
nonInteractive: effectiveDangerouslySkipPermissions,
|
||||
nonInteractive: !effectiveRequireApproval,
|
||||
exitOnError: flagOptions.exitOnError,
|
||||
ephemeral: flagOptions.ephemeral,
|
||||
debug: flagOptions.debug,
|
||||
exitOnComplete: effectiveExitOnComplete,
|
||||
}
|
||||
|
||||
// Roo Code Cloud Authentication
|
||||
|
||||
if (isOnboardingEnabled) {
|
||||
let { onboardingProviderChoice } = settings
|
||||
|
||||
if (!onboardingProviderChoice) {
|
||||
const { choice, token } = await runOnboarding()
|
||||
onboardingProviderChoice = choice
|
||||
rooToken = token ?? null
|
||||
}
|
||||
|
||||
if (onboardingProviderChoice === OnboardingProviderChoice.Roo) {
|
||||
extensionHostOptions.provider = "roo"
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionHostOptions.provider === "roo") {
|
||||
if (rooToken) {
|
||||
try {
|
||||
const client = createClient({ url: SDK_BASE_URL, authToken: rooToken })
|
||||
const me = await client.auth.me.query()
|
||||
|
||||
if (me?.type !== "user") {
|
||||
throw new Error("Invalid token")
|
||||
}
|
||||
|
||||
extensionHostOptions.apiKey = rooToken
|
||||
extensionHostOptions.user = me.user
|
||||
} catch {
|
||||
// If an explicit API key was provided via flag or env var, fall through
|
||||
// to the general API key resolution below instead of exiting.
|
||||
if (!flagOptions.apiKey && !getApiKeyFromEnv(extensionHostOptions.provider)) {
|
||||
console.error("[CLI] Your Roo Code Router token is not valid.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
console.error("[CLI] Or use --api-key or set ROO_API_KEY to provide your own API key.")
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no rooToken, fall through to the general API key resolution below
|
||||
// which will check flagOptions.apiKey and ROO_API_KEY env var.
|
||||
terminalShell,
|
||||
}
|
||||
|
||||
// Validations
|
||||
|
|
@ -142,18 +183,8 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
|
|||
extensionHostOptions.apiKey || flagOptions.apiKey || getApiKeyFromEnv(extensionHostOptions.provider)
|
||||
|
||||
if (!extensionHostOptions.apiKey) {
|
||||
if (extensionHostOptions.provider === "roo") {
|
||||
console.error("[CLI] Error: Authentication with Roo Code Cloud failed or was cancelled.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
console.error("[CLI] Or use --api-key to provide your own API key.")
|
||||
} else {
|
||||
console.error(
|
||||
`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`,
|
||||
)
|
||||
console.error(
|
||||
`[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`,
|
||||
)
|
||||
}
|
||||
console.error(`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`)
|
||||
console.error(`[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`)
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
@ -183,15 +214,76 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
|
|||
// Output format only works with --print mode
|
||||
if (outputFormat !== "text" && !flagOptions.print && isTuiSupported) {
|
||||
console.error("[CLI] Error: --output-format requires --print mode")
|
||||
console.error("[CLI] Usage: roo <prompt> --print --output-format json")
|
||||
console.error("[CLI] Usage: roo --print --output-format json")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (flagOptions.stdinPromptStream && !flagOptions.print) {
|
||||
console.error("[CLI] Error: --stdin-prompt-stream requires --print mode")
|
||||
console.error("[CLI] Usage: roo --print --output-format stream-json --stdin-prompt-stream [options]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (flagOptions.signalOnlyExit && !flagOptions.stdinPromptStream) {
|
||||
console.error("[CLI] Error: --signal-only-exit requires --stdin-prompt-stream")
|
||||
console.error("[CLI] Usage: roo --print --output-format stream-json --stdin-prompt-stream --signal-only-exit")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (flagOptions.stdinPromptStream && outputFormat !== "stream-json") {
|
||||
console.error("[CLI] Error: --stdin-prompt-stream requires --output-format=stream-json")
|
||||
console.error("[CLI] Usage: roo --print --output-format stream-json --stdin-prompt-stream [options]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (flagOptions.stdinPromptStream && process.stdin.isTTY) {
|
||||
console.error("[CLI] Error: --stdin-prompt-stream requires piped stdin")
|
||||
console.error(
|
||||
'[CLI] Example: printf \'{"command":"start","requestId":"1","prompt":"1+1=?"}\\n\' | roo --print --output-format stream-json --stdin-prompt-stream [options]',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (flagOptions.stdinPromptStream && prompt) {
|
||||
console.error("[CLI] Error: cannot use positional prompt or --prompt-file with --stdin-prompt-stream")
|
||||
console.error("[CLI] Usage: roo --print --output-format stream-json --stdin-prompt-stream [options]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (flagOptions.stdinPromptStream && requestedCreateSessionId) {
|
||||
console.error("[CLI] Error: --create-with-session-id is not supported with --stdin-prompt-stream")
|
||||
console.error('[CLI] Use per-request "taskId" in stdin start commands instead.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const useStdinPromptStream = flagOptions.stdinPromptStream
|
||||
let resolvedResumeSessionId: string | undefined
|
||||
|
||||
if (isResumeRequested) {
|
||||
const workspaceSessions = await readWorkspaceTaskSessions(effectiveWorkspacePath)
|
||||
try {
|
||||
resolvedResumeSessionId = resolveWorkspaceResumeSessionId(workspaceSessions, requestedSessionId)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[CLI] Error: ${message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTuiEnabled) {
|
||||
if (!prompt) {
|
||||
console.error("[CLI] Error: prompt is required in print mode")
|
||||
console.error("[CLI] Usage: roo <prompt> --print [options]")
|
||||
console.error("[CLI] Run without -p for interactive mode")
|
||||
if (!prompt && !useStdinPromptStream && !isResumeRequested) {
|
||||
if (flagOptions.print) {
|
||||
console.error("[CLI] Error: no prompt provided")
|
||||
console.error("[CLI] Usage: roo --print [options] <prompt>")
|
||||
console.error(
|
||||
"[CLI] For stdin control mode: roo --print --output-format stream-json --stdin-prompt-stream [options]",
|
||||
)
|
||||
} else {
|
||||
console.error("[CLI] Error: prompt is required in non-interactive mode")
|
||||
console.error("[CLI] Usage: roo <prompt> [options]")
|
||||
console.error("[CLI] Run without -p for interactive mode")
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +303,9 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
|
|||
createElement(App, {
|
||||
...extensionHostOptions,
|
||||
initialPrompt: prompt,
|
||||
initialTaskId: requestedCreateSessionId,
|
||||
initialSessionId: resolvedResumeSessionId,
|
||||
continueSession: false,
|
||||
version: VERSION,
|
||||
createExtensionHost: (opts: ExtensionHostOptions) => new ExtensionHost(opts),
|
||||
}),
|
||||
|
|
@ -228,26 +323,172 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
|
|||
}
|
||||
} else {
|
||||
const useJsonOutput = outputFormat === "json" || outputFormat === "stream-json"
|
||||
const signalOnlyExit = flagOptions.signalOnlyExit
|
||||
|
||||
extensionHostOptions.disableOutput = useJsonOutput
|
||||
|
||||
const host = new ExtensionHost(extensionHostOptions)
|
||||
let streamRequestId: string | undefined
|
||||
let keepAliveInterval: NodeJS.Timeout | undefined
|
||||
let isShuttingDown = false
|
||||
let hostDisposed = false
|
||||
|
||||
const jsonEmitter = useJsonOutput
|
||||
? new JsonEventEmitter({ mode: outputFormat as "json" | "stream-json" })
|
||||
? new JsonEventEmitter({
|
||||
mode: outputFormat as "json" | "stream-json",
|
||||
requestIdProvider: () => streamRequestId,
|
||||
})
|
||||
: null
|
||||
|
||||
const emitRuntimeError = (error: Error, source?: string) => {
|
||||
const errorMessage = source ? `${source}: ${error.message}` : error.message
|
||||
|
||||
if (useJsonOutput) {
|
||||
const errorEvent = { type: "error", id: Date.now(), content: errorMessage }
|
||||
process.stdout.write(JSON.stringify(errorEvent) + "\n")
|
||||
return
|
||||
}
|
||||
|
||||
console.error("[CLI] Error:", errorMessage)
|
||||
console.error(error.stack)
|
||||
}
|
||||
|
||||
const clearKeepAliveInterval = () => {
|
||||
if (!keepAliveInterval) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(keepAliveInterval)
|
||||
keepAliveInterval = undefined
|
||||
}
|
||||
|
||||
const flushStdout = async () => {
|
||||
try {
|
||||
if (!process.stdout.writable || process.stdout.destroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
process.stdout.write("", (error?: Error | null) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
} catch {
|
||||
// Best effort: shutdown should proceed even if stdout flush fails.
|
||||
}
|
||||
}
|
||||
|
||||
const ensureKeepAliveInterval = () => {
|
||||
if (!signalOnlyExit || keepAliveInterval) {
|
||||
return
|
||||
}
|
||||
|
||||
keepAliveInterval = setInterval(() => {}, SIGNAL_ONLY_EXIT_KEEPALIVE_MS)
|
||||
}
|
||||
|
||||
const disposeHost = async () => {
|
||||
if (hostDisposed) {
|
||||
return
|
||||
}
|
||||
|
||||
hostDisposed = true
|
||||
jsonEmitter?.detach()
|
||||
await host.dispose()
|
||||
}
|
||||
|
||||
const onSigint = () => {
|
||||
void shutdown("SIGINT", 130)
|
||||
}
|
||||
|
||||
const onSigterm = () => {
|
||||
void shutdown("SIGTERM", 143)
|
||||
}
|
||||
|
||||
const onUncaughtException = (error: Error) => {
|
||||
if (
|
||||
isExpectedControlFlowError(error, {
|
||||
stdinStreamMode: useStdinPromptStream,
|
||||
shuttingDown: isShuttingDown,
|
||||
operation: "runtime",
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
emitRuntimeError(error, "uncaughtException")
|
||||
|
||||
if (signalOnlyExit) {
|
||||
return
|
||||
}
|
||||
|
||||
void shutdown("uncaughtException", 1)
|
||||
}
|
||||
|
||||
const onUnhandledRejection = (reason: unknown) => {
|
||||
if (
|
||||
isExpectedControlFlowError(reason, {
|
||||
stdinStreamMode: useStdinPromptStream,
|
||||
shuttingDown: isShuttingDown,
|
||||
operation: "runtime",
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const error = normalizeError(reason)
|
||||
emitRuntimeError(error, "unhandledRejection")
|
||||
|
||||
if (signalOnlyExit) {
|
||||
return
|
||||
}
|
||||
|
||||
void shutdown("unhandledRejection", 1)
|
||||
}
|
||||
|
||||
const parkUntilSignal = async (reason: string): Promise<never> => {
|
||||
ensureKeepAliveInterval()
|
||||
|
||||
if (!useJsonOutput) {
|
||||
console.error(`[CLI] ${reason} (--signal-only-exit active; waiting for SIGINT/SIGTERM).`)
|
||||
}
|
||||
|
||||
await new Promise<void>(() => {})
|
||||
throw new Error("unreachable")
|
||||
}
|
||||
|
||||
async function shutdown(signal: string, exitCode: number): Promise<void> {
|
||||
if (isShuttingDown) {
|
||||
return
|
||||
}
|
||||
|
||||
isShuttingDown = true
|
||||
process.off("SIGINT", onSigint)
|
||||
process.off("SIGTERM", onSigterm)
|
||||
process.off("uncaughtException", onUncaughtException)
|
||||
process.off("unhandledRejection", onUnhandledRejection)
|
||||
clearKeepAliveInterval()
|
||||
|
||||
if (!useJsonOutput) {
|
||||
console.log(`\n[CLI] Received ${signal}, shutting down...`)
|
||||
}
|
||||
jsonEmitter?.detach()
|
||||
await host.dispose()
|
||||
|
||||
await disposeHost()
|
||||
if (jsonEmitter) {
|
||||
await jsonEmitter.flush()
|
||||
}
|
||||
await flushStdout()
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => shutdown("SIGINT", 130))
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM", 143))
|
||||
process.on("SIGINT", onSigint)
|
||||
process.on("SIGTERM", onSigterm)
|
||||
process.on("uncaughtException", onUncaughtException)
|
||||
process.on("unhandledRejection", onUnhandledRejection)
|
||||
|
||||
try {
|
||||
await host.activate()
|
||||
|
|
@ -256,25 +497,61 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
|
|||
jsonEmitter.attachToClient(host.client)
|
||||
}
|
||||
|
||||
await host.runTask(prompt!)
|
||||
jsonEmitter?.detach()
|
||||
await host.dispose()
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (useStdinPromptStream) {
|
||||
if (!jsonEmitter || outputFormat !== "stream-json") {
|
||||
throw new Error("--stdin-prompt-stream requires --output-format=stream-json to emit control events")
|
||||
}
|
||||
|
||||
if (useJsonOutput) {
|
||||
const errorEvent = { type: "error", id: Date.now(), content: errorMessage }
|
||||
process.stdout.write(JSON.stringify(errorEvent) + "\n")
|
||||
if (isResumeRequested) {
|
||||
await bootstrapResumeForStdinStream(host, resolvedResumeSessionId!)
|
||||
}
|
||||
|
||||
await runStdinStreamMode({
|
||||
host,
|
||||
jsonEmitter,
|
||||
setStreamRequestId: (id) => {
|
||||
streamRequestId = id
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.error("[CLI] Error:", errorMessage)
|
||||
if (error instanceof Error) {
|
||||
console.error(error.stack)
|
||||
if (isResumeRequested) {
|
||||
await host.resumeTask(resolvedResumeSessionId!)
|
||||
} else {
|
||||
await host.runTask(prompt!, requestedCreateSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
jsonEmitter?.detach()
|
||||
await host.dispose()
|
||||
await disposeHost()
|
||||
if (jsonEmitter) {
|
||||
await jsonEmitter.flush()
|
||||
}
|
||||
await flushStdout()
|
||||
|
||||
if (signalOnlyExit) {
|
||||
await parkUntilSignal("Task loop completed")
|
||||
}
|
||||
|
||||
process.off("SIGINT", onSigint)
|
||||
process.off("SIGTERM", onSigterm)
|
||||
process.off("uncaughtException", onUncaughtException)
|
||||
process.off("unhandledRejection", onUnhandledRejection)
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
emitRuntimeError(normalizeError(error))
|
||||
await disposeHost()
|
||||
if (jsonEmitter) {
|
||||
await jsonEmitter.flush()
|
||||
}
|
||||
await flushStdout()
|
||||
|
||||
if (signalOnlyExit) {
|
||||
await parkUntilSignal("Task loop failed")
|
||||
}
|
||||
|
||||
process.off("SIGINT", onSigint)
|
||||
process.off("SIGTERM", onSigterm)
|
||||
process.off("uncaughtException", onUncaughtException)
|
||||
process.off("unhandledRejection", onUnhandledRejection)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
977
apps/cli/src/commands/cli/stdin-stream.ts
Normal file
977
apps/cli/src/commands/cli/stdin-stream.ts
Normal file
|
|
@ -0,0 +1,977 @@
|
|||
import { createInterface } from "readline"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
import {
|
||||
rooCliCommandNames,
|
||||
type RooCliCommandName,
|
||||
type RooCliInputCommand,
|
||||
type RooCliStartCommand,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { isRecord } from "@/lib/utils/guards.js"
|
||||
import { isValidSessionId } from "@/lib/utils/session-id.js"
|
||||
import { isCancellationLikeError, isExpectedControlFlowError, isNoActiveTaskLikeError } from "./cancellation.js"
|
||||
|
||||
import type { ExtensionHost } from "@/agent/index.js"
|
||||
import type { JsonEventEmitter } from "@/agent/json-event-emitter.js"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type StdinStreamCommandName = RooCliCommandName
|
||||
|
||||
export type StdinStreamCommand = RooCliInputCommand
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const VALID_STDIN_COMMANDS = new Set<StdinStreamCommandName>(rooCliCommandNames)
|
||||
|
||||
export function parseStdinStreamCommand(line: string, lineNumber: number): StdinStreamCommand {
|
||||
let parsed: unknown
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(line)
|
||||
} catch {
|
||||
throw new Error(`stdin command line ${lineNumber}: invalid JSON`)
|
||||
}
|
||||
|
||||
if (!isRecord(parsed)) {
|
||||
throw new Error(`stdin command line ${lineNumber}: expected JSON object`)
|
||||
}
|
||||
|
||||
const commandRaw = parsed.command
|
||||
const requestIdRaw = parsed.requestId
|
||||
|
||||
if (typeof commandRaw !== "string") {
|
||||
throw new Error(`stdin command line ${lineNumber}: missing string "command"`)
|
||||
}
|
||||
|
||||
if (!VALID_STDIN_COMMANDS.has(commandRaw as StdinStreamCommandName)) {
|
||||
throw new Error(
|
||||
`stdin command line ${lineNumber}: unsupported command "${commandRaw}" (expected start|message|cancel|ping|shutdown)`,
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof requestIdRaw !== "string" || requestIdRaw.trim().length === 0) {
|
||||
throw new Error(`stdin command line ${lineNumber}: missing non-empty string "requestId"`)
|
||||
}
|
||||
|
||||
const command = commandRaw as StdinStreamCommandName
|
||||
const requestId = requestIdRaw.trim()
|
||||
|
||||
if (command === "start" || command === "message") {
|
||||
const promptRaw = parsed.prompt
|
||||
|
||||
if (typeof promptRaw !== "string" || promptRaw.trim().length === 0) {
|
||||
throw new Error(`stdin command line ${lineNumber}: "${command}" requires non-empty string "prompt"`)
|
||||
}
|
||||
|
||||
const imagesRaw = parsed.images
|
||||
let images: string[] | undefined
|
||||
|
||||
if (imagesRaw !== undefined) {
|
||||
if (!Array.isArray(imagesRaw) || !imagesRaw.every((image) => typeof image === "string")) {
|
||||
throw new Error(`stdin command line ${lineNumber}: "${command}" images must be an array of strings`)
|
||||
}
|
||||
|
||||
images = imagesRaw
|
||||
}
|
||||
|
||||
if (command === "start") {
|
||||
const taskIdRaw = parsed.taskId
|
||||
let taskId: string | undefined
|
||||
|
||||
if (taskIdRaw !== undefined) {
|
||||
if (typeof taskIdRaw !== "string" || taskIdRaw.trim().length === 0) {
|
||||
throw new Error(`stdin command line ${lineNumber}: "start" taskId must be a non-empty string`)
|
||||
}
|
||||
taskId = taskIdRaw.trim()
|
||||
|
||||
if (!isValidSessionId(taskId)) {
|
||||
throw new Error(`stdin command line ${lineNumber}: "start" taskId must be a valid UUID`)
|
||||
}
|
||||
}
|
||||
|
||||
if (isRecord(parsed.configuration)) {
|
||||
return {
|
||||
command,
|
||||
requestId,
|
||||
prompt: promptRaw,
|
||||
...(taskId !== undefined ? { taskId } : {}),
|
||||
...(images !== undefined ? { images } : {}),
|
||||
configuration: parsed.configuration as RooCliStartCommand["configuration"],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
command,
|
||||
requestId,
|
||||
prompt: promptRaw,
|
||||
...(taskId !== undefined ? { taskId } : {}),
|
||||
...(images !== undefined ? { images } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
command,
|
||||
requestId,
|
||||
prompt: promptRaw,
|
||||
...(images !== undefined ? { images } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
return { command, requestId }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NDJSON stdin reader
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function* readCommandsFromStdinNdjson(): AsyncGenerator<StdinStreamCommand> {
|
||||
const lineReader = createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity,
|
||||
terminal: false,
|
||||
})
|
||||
|
||||
let lineNumber = 0
|
||||
|
||||
try {
|
||||
for await (const line of lineReader) {
|
||||
lineNumber += 1
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) {
|
||||
continue
|
||||
}
|
||||
yield parseStdinStreamCommand(trimmed, lineNumber)
|
||||
}
|
||||
} finally {
|
||||
lineReader.close()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queue snapshot helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface StreamQueueItem {
|
||||
id: string
|
||||
text?: string
|
||||
imageCount: number
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
function normalizeQueueText(text: string | undefined): string | undefined {
|
||||
if (!text) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const compact = text.replace(/\s+/g, " ").trim()
|
||||
if (!compact) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return compact.length <= 180 ? compact : `${compact.slice(0, 177)}...`
|
||||
}
|
||||
|
||||
function parseQueueSnapshot(rawQueue: unknown): StreamQueueItem[] | undefined {
|
||||
if (!Array.isArray(rawQueue)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const snapshot: StreamQueueItem[] = []
|
||||
|
||||
for (const entry of rawQueue) {
|
||||
if (!isRecord(entry)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const idRaw = entry.id
|
||||
if (typeof idRaw !== "string" || idRaw.trim().length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const imagesRaw = entry.images
|
||||
const timestampRaw = entry.timestamp
|
||||
const imageCount = Array.isArray(imagesRaw) ? imagesRaw.length : 0
|
||||
|
||||
snapshot.push({
|
||||
id: idRaw,
|
||||
text: normalizeQueueText(typeof entry.text === "string" ? entry.text : undefined),
|
||||
imageCount,
|
||||
timestamp: typeof timestampRaw === "number" ? timestampRaw : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function areStringArraysEqual(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orchestrator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface StdinStreamModeOptions {
|
||||
host: ExtensionHost
|
||||
jsonEmitter: JsonEventEmitter
|
||||
setStreamRequestId: (id: string | undefined) => void
|
||||
}
|
||||
|
||||
const RESUME_ASKS = new Set(["resume_task", "resume_completed_task"])
|
||||
const CANCEL_RECOVERY_WAIT_TIMEOUT_MS = 8_000
|
||||
const CANCEL_RECOVERY_POLL_INTERVAL_MS = 100
|
||||
const STDIN_EOF_RESUME_WAIT_TIMEOUT_MS = 2_000
|
||||
const STDIN_EOF_POLL_INTERVAL_MS = 100
|
||||
const STDIN_EOF_IDLE_ASKS = new Set(["completion_result", "resume_completed_task"])
|
||||
const STDIN_EOF_IDLE_STABLE_POLLS = 2
|
||||
const MESSAGE_AS_ASK_RESPONSE_ASKS = new Set([
|
||||
"followup",
|
||||
"tool",
|
||||
"command",
|
||||
"use_mcp_server",
|
||||
"completion_result",
|
||||
"resume_task",
|
||||
"resume_completed_task",
|
||||
"mistake_limit_reached",
|
||||
])
|
||||
|
||||
export function shouldSendMessageAsAskResponse(waitingForInput: boolean, currentAsk: string | undefined): boolean {
|
||||
return waitingForInput && typeof currentAsk === "string" && MESSAGE_AS_ASK_RESPONSE_ASKS.has(currentAsk)
|
||||
}
|
||||
|
||||
function isResumableState(host: ExtensionHost): boolean {
|
||||
const agentState = host.client.getAgentState()
|
||||
return (
|
||||
agentState.isWaitingForInput &&
|
||||
typeof agentState.currentAsk === "string" &&
|
||||
RESUME_ASKS.has(agentState.currentAsk)
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForPostCancelRecovery(host: ExtensionHost): Promise<void> {
|
||||
const deadline = Date.now() + CANCEL_RECOVERY_WAIT_TIMEOUT_MS
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (isResumableState(host)) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, CANCEL_RECOVERY_POLL_INTERVAL_MS))
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForTaskProgressAfterStdinClosed(
|
||||
host: ExtensionHost,
|
||||
getQueueState: () => { hasSeenQueueState: boolean; queueDepth: number },
|
||||
): Promise<void> {
|
||||
while (host.client.hasActiveTask()) {
|
||||
if (!host.isWaitingForInput()) {
|
||||
await new Promise((resolve) => setTimeout(resolve, STDIN_EOF_POLL_INTERVAL_MS))
|
||||
continue
|
||||
}
|
||||
|
||||
const deadline = Date.now() + STDIN_EOF_RESUME_WAIT_TIMEOUT_MS
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (!host.client.hasActiveTask() || !host.isWaitingForInput()) {
|
||||
break
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, STDIN_EOF_POLL_INTERVAL_MS))
|
||||
}
|
||||
|
||||
if (host.client.hasActiveTask() && host.isWaitingForInput()) {
|
||||
const currentAsk = host.client.getCurrentAsk()
|
||||
const { hasSeenQueueState, queueDepth } = getQueueState()
|
||||
|
||||
// EOF is allowed when the task has reached an idle completion boundary and
|
||||
// there is no queued user input waiting to be processed.
|
||||
if (
|
||||
hasSeenQueueState &&
|
||||
queueDepth === 0 &&
|
||||
typeof currentAsk === "string" &&
|
||||
STDIN_EOF_IDLE_ASKS.has(currentAsk)
|
||||
) {
|
||||
let isStable = true
|
||||
for (let i = 1; i < STDIN_EOF_IDLE_STABLE_POLLS; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, STDIN_EOF_POLL_INTERVAL_MS))
|
||||
|
||||
if (!host.client.hasActiveTask() || !host.isWaitingForInput()) {
|
||||
isStable = false
|
||||
break
|
||||
}
|
||||
|
||||
const nextAsk = host.client.getCurrentAsk()
|
||||
const nextQueueState = getQueueState()
|
||||
if (
|
||||
nextAsk !== currentAsk ||
|
||||
!nextQueueState.hasSeenQueueState ||
|
||||
nextQueueState.queueDepth !== 0
|
||||
) {
|
||||
isStable = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (isStable) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`stdin ended while task was waiting for input (${currentAsk ?? "unknown"})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runStdinStreamMode({ host, jsonEmitter, setStreamRequestId }: StdinStreamModeOptions) {
|
||||
let hasReceivedStdinCommand = false
|
||||
let shouldShutdown = false
|
||||
let activeTaskPromise: Promise<void> | null = null
|
||||
let fatalStreamError: Error | null = null
|
||||
let activeRequestId: string | undefined
|
||||
let activeTaskCommand: "start" | undefined
|
||||
let latestTaskId: string | undefined
|
||||
let cancelRequestedForActiveTask = false
|
||||
let awaitingPostCancelRecovery = false
|
||||
let hasSeenQueueState = false
|
||||
let lastQueueDepth = 0
|
||||
let lastQueueMessageIds: string[] = []
|
||||
const pendingQueuedMessageRequestIds: string[] = []
|
||||
const queueMessageRequestIdByMessageId = new Map<string, string>()
|
||||
|
||||
const assignRequestIdsToNewQueueMessages = (queueMessageIds: string[]) => {
|
||||
for (const messageId of queueMessageIds) {
|
||||
if (queueMessageRequestIdByMessageId.has(messageId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const requestId = pendingQueuedMessageRequestIds.shift()
|
||||
if (!requestId) {
|
||||
continue
|
||||
}
|
||||
|
||||
queueMessageRequestIdByMessageId.set(messageId, requestId)
|
||||
}
|
||||
}
|
||||
|
||||
const promoteRequestIdForDequeuedMessages = (queueMessageIds: string[]) => {
|
||||
if (lastQueueMessageIds.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const remainingIds = new Set(queueMessageIds)
|
||||
|
||||
for (const dequeuedMessageId of lastQueueMessageIds) {
|
||||
if (remainingIds.has(dequeuedMessageId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const requestId = queueMessageRequestIdByMessageId.get(dequeuedMessageId)
|
||||
if (requestId) {
|
||||
setStreamRequestId(requestId)
|
||||
}
|
||||
queueMessageRequestIdByMessageId.delete(dequeuedMessageId)
|
||||
}
|
||||
}
|
||||
|
||||
const waitForPreviousTaskToSettle = async () => {
|
||||
if (!activeTaskPromise) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await activeTaskPromise
|
||||
} catch {
|
||||
// Errors are emitted through control/error events.
|
||||
}
|
||||
}
|
||||
|
||||
const offClientError = host.client.on("error", (error) => {
|
||||
if (
|
||||
isExpectedControlFlowError(error, {
|
||||
stdinStreamMode: true,
|
||||
cancelRequested: cancelRequestedForActiveTask,
|
||||
shuttingDown: shouldShutdown,
|
||||
operation: "client",
|
||||
})
|
||||
) {
|
||||
if (activeTaskCommand === "start" && (cancelRequestedForActiveTask || isCancellationLikeError(error))) {
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: activeRequestId,
|
||||
command: "start",
|
||||
taskId: latestTaskId,
|
||||
content: "task cancelled",
|
||||
code: "task_aborted",
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
activeTaskCommand = undefined
|
||||
activeRequestId = undefined
|
||||
setStreamRequestId(undefined)
|
||||
cancelRequestedForActiveTask = false
|
||||
awaitingPostCancelRecovery = false
|
||||
return
|
||||
}
|
||||
|
||||
fatalStreamError = error
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "error",
|
||||
requestId: activeRequestId,
|
||||
command: activeTaskCommand,
|
||||
taskId: latestTaskId,
|
||||
content: error.message,
|
||||
code: "client_error",
|
||||
success: false,
|
||||
})
|
||||
})
|
||||
|
||||
const onExtensionMessage = (message: {
|
||||
type?: string
|
||||
text?: unknown
|
||||
state?: {
|
||||
currentTaskId?: unknown
|
||||
currentTaskItem?: { id?: unknown }
|
||||
messageQueue?: unknown
|
||||
}
|
||||
}) => {
|
||||
if (message.type === "commandExecutionStatus") {
|
||||
if (typeof message.text !== "string") {
|
||||
return
|
||||
}
|
||||
|
||||
let parsedStatus: unknown
|
||||
try {
|
||||
parsedStatus = JSON.parse(message.text)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isRecord(parsedStatus) || typeof parsedStatus.status !== "string") {
|
||||
return
|
||||
}
|
||||
|
||||
if (parsedStatus.status === "output" && typeof parsedStatus.output === "string") {
|
||||
jsonEmitter.emitCommandOutputChunk(parsedStatus.output)
|
||||
return
|
||||
}
|
||||
|
||||
if (parsedStatus.status === "exited") {
|
||||
const exitCode =
|
||||
parsedStatus.status === "exited" && typeof parsedStatus.exitCode === "number"
|
||||
? parsedStatus.exitCode
|
||||
: undefined
|
||||
|
||||
if (typeof parsedStatus.output === "string") {
|
||||
jsonEmitter.emitCommandOutputChunk(parsedStatus.output)
|
||||
}
|
||||
|
||||
jsonEmitter.markCommandOutputExited(exitCode)
|
||||
return
|
||||
}
|
||||
|
||||
if (parsedStatus.status === "timeout" || parsedStatus.status === "fallback") {
|
||||
jsonEmitter.emitCommandOutputDone(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type !== "state") {
|
||||
return
|
||||
}
|
||||
|
||||
const currentTaskId = message.state?.currentTaskId ?? message.state?.currentTaskItem?.id
|
||||
if (typeof currentTaskId === "string" && currentTaskId.trim().length > 0) {
|
||||
latestTaskId = currentTaskId
|
||||
}
|
||||
|
||||
const queueSnapshot = parseQueueSnapshot(message.state?.messageQueue)
|
||||
if (!queueSnapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
const queueDepth = queueSnapshot.length
|
||||
const queueMessageIds = queueSnapshot.map((item) => item.id)
|
||||
|
||||
if (!hasSeenQueueState) {
|
||||
assignRequestIdsToNewQueueMessages(queueMessageIds)
|
||||
hasSeenQueueState = true
|
||||
lastQueueDepth = queueDepth
|
||||
lastQueueMessageIds = queueMessageIds
|
||||
|
||||
if (queueDepth === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
jsonEmitter.emitQueue({
|
||||
subtype: "snapshot",
|
||||
taskId: latestTaskId,
|
||||
content: `queue snapshot (${queueDepth} item${queueDepth === 1 ? "" : "s"})`,
|
||||
queueDepth,
|
||||
queue: queueSnapshot,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const depthChanged = queueDepth !== lastQueueDepth
|
||||
const idsChanged = !areStringArraysEqual(queueMessageIds, lastQueueMessageIds)
|
||||
|
||||
if (!depthChanged && !idsChanged) {
|
||||
return
|
||||
}
|
||||
|
||||
promoteRequestIdForDequeuedMessages(queueMessageIds)
|
||||
assignRequestIdsToNewQueueMessages(queueMessageIds)
|
||||
|
||||
const subtype: "enqueued" | "dequeued" | "drained" | "updated" = depthChanged
|
||||
? queueDepth > lastQueueDepth
|
||||
? "enqueued"
|
||||
: queueDepth === 0
|
||||
? "drained"
|
||||
: "dequeued"
|
||||
: "updated"
|
||||
|
||||
const content =
|
||||
subtype === "drained"
|
||||
? "queue drained"
|
||||
: `queue ${subtype} (${queueDepth} item${queueDepth === 1 ? "" : "s"})`
|
||||
|
||||
jsonEmitter.emitQueue({
|
||||
subtype,
|
||||
taskId: latestTaskId,
|
||||
content,
|
||||
queueDepth,
|
||||
queue: queueSnapshot,
|
||||
})
|
||||
|
||||
lastQueueDepth = queueDepth
|
||||
lastQueueMessageIds = queueMessageIds
|
||||
}
|
||||
|
||||
host.on("extensionWebviewMessage", onExtensionMessage)
|
||||
|
||||
const offTaskCompleted = host.client.on("taskCompleted", (event) => {
|
||||
if (activeTaskCommand === "start") {
|
||||
const completionCode = event.success
|
||||
? "task_completed"
|
||||
: cancelRequestedForActiveTask
|
||||
? "task_aborted"
|
||||
: "task_failed"
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: activeRequestId,
|
||||
command: "start",
|
||||
taskId: latestTaskId,
|
||||
content: event.success
|
||||
? "task completed"
|
||||
: cancelRequestedForActiveTask
|
||||
? "task cancelled"
|
||||
: "task failed",
|
||||
code: completionCode,
|
||||
success: event.success,
|
||||
})
|
||||
|
||||
// If user messages were queued while the task was still running, shift
|
||||
// event attribution to the oldest pending message request as soon as the
|
||||
// task turn completes so prompt echo/user feedback events are tagged.
|
||||
const oldestQueuedMessageId = lastQueueMessageIds[0]
|
||||
const nextQueuedRequestId =
|
||||
pendingQueuedMessageRequestIds[0] ??
|
||||
(oldestQueuedMessageId ? queueMessageRequestIdByMessageId.get(oldestQueuedMessageId) : undefined)
|
||||
if (nextQueuedRequestId) {
|
||||
setStreamRequestId(nextQueuedRequestId)
|
||||
}
|
||||
|
||||
activeTaskCommand = undefined
|
||||
activeRequestId = undefined
|
||||
cancelRequestedForActiveTask = false
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
for await (const stdinCommand of readCommandsFromStdinNdjson()) {
|
||||
hasReceivedStdinCommand = true
|
||||
|
||||
if (fatalStreamError) {
|
||||
throw fatalStreamError
|
||||
}
|
||||
|
||||
switch (stdinCommand.command) {
|
||||
case "start": {
|
||||
// A task can emit completion events before runTask() finalizers run.
|
||||
// Wait for full settlement to avoid false "task_busy" on immediate next start.
|
||||
// Safe from races: `for await` processes stdin commands serially, so no
|
||||
// concurrent command can mutate state between the check and the await.
|
||||
if (activeTaskPromise && !host.client.hasActiveTask()) {
|
||||
await waitForPreviousTaskToSettle()
|
||||
}
|
||||
|
||||
if (activeTaskPromise || host.client.hasActiveTask()) {
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "error",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "start",
|
||||
taskId: latestTaskId,
|
||||
content: "cannot start a new task while another task is active",
|
||||
code: "task_busy",
|
||||
success: false,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
activeRequestId = stdinCommand.requestId
|
||||
activeTaskCommand = "start"
|
||||
setStreamRequestId(stdinCommand.requestId)
|
||||
latestTaskId = stdinCommand.taskId ?? randomUUID()
|
||||
cancelRequestedForActiveTask = false
|
||||
awaitingPostCancelRecovery = false
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "ack",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "start",
|
||||
taskId: latestTaskId,
|
||||
content: "starting task",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
|
||||
// In CLI stdin-stream mode, default to the execa terminal provider so
|
||||
// command output can be streamed deterministically. Explicit per-request
|
||||
// config still wins.
|
||||
const taskConfiguration = {
|
||||
terminalShellIntegrationDisabled: true,
|
||||
...(stdinCommand.configuration ?? {}),
|
||||
}
|
||||
|
||||
activeTaskPromise = host
|
||||
.runTask(stdinCommand.prompt, latestTaskId, taskConfiguration, stdinCommand.images)
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
|
||||
if (
|
||||
isExpectedControlFlowError(error, {
|
||||
stdinStreamMode: true,
|
||||
cancelRequested: cancelRequestedForActiveTask,
|
||||
shuttingDown: shouldShutdown,
|
||||
operation: "client",
|
||||
})
|
||||
) {
|
||||
if (
|
||||
activeTaskCommand === "start" &&
|
||||
(cancelRequestedForActiveTask || isCancellationLikeError(error))
|
||||
) {
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "start",
|
||||
taskId: latestTaskId,
|
||||
content: "task cancelled",
|
||||
code: "task_aborted",
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
|
||||
activeTaskCommand = undefined
|
||||
activeRequestId = undefined
|
||||
setStreamRequestId(undefined)
|
||||
cancelRequestedForActiveTask = false
|
||||
awaitingPostCancelRecovery = false
|
||||
return
|
||||
}
|
||||
|
||||
fatalStreamError = error instanceof Error ? error : new Error(message)
|
||||
activeTaskCommand = undefined
|
||||
activeRequestId = undefined
|
||||
setStreamRequestId(undefined)
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "error",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "start",
|
||||
taskId: latestTaskId,
|
||||
content: message,
|
||||
code: "task_error",
|
||||
success: false,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
activeTaskPromise = null
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "message": {
|
||||
// If cancel was requested, wait briefly for the task to be rehydrated
|
||||
// so message prompts don't race into the pre-cancel task instance.
|
||||
if (awaitingPostCancelRecovery) {
|
||||
await waitForPostCancelRecovery(host)
|
||||
}
|
||||
|
||||
const wasResumable = isResumableState(host)
|
||||
const currentAsk = host.client.getCurrentAsk()
|
||||
const shouldSendAsAskResponse = shouldSendMessageAsAskResponse(host.isWaitingForInput(), currentAsk)
|
||||
|
||||
if (!host.client.hasActiveTask()) {
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "error",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "message",
|
||||
taskId: latestTaskId,
|
||||
content: "no active task; send a start command first",
|
||||
code: "no_active_task",
|
||||
success: false,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "ack",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "message",
|
||||
taskId: latestTaskId,
|
||||
content: "message accepted",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
|
||||
if (shouldSendAsAskResponse) {
|
||||
// Match webview behavior: if there is an active ask, route message directly as an ask response.
|
||||
host.sendToExtension({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
text: stdinCommand.prompt,
|
||||
images: stdinCommand.images,
|
||||
})
|
||||
|
||||
setStreamRequestId(stdinCommand.requestId)
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "message",
|
||||
taskId: latestTaskId,
|
||||
content: "message sent to current ask",
|
||||
code: "responded",
|
||||
success: true,
|
||||
})
|
||||
awaitingPostCancelRecovery = false
|
||||
break
|
||||
}
|
||||
|
||||
host.sendToExtension({
|
||||
type: "queueMessage",
|
||||
text: stdinCommand.prompt,
|
||||
images: stdinCommand.images,
|
||||
})
|
||||
pendingQueuedMessageRequestIds.push(stdinCommand.requestId)
|
||||
if (host.isWaitingForInput()) {
|
||||
setStreamRequestId(stdinCommand.requestId)
|
||||
}
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "message",
|
||||
taskId: latestTaskId,
|
||||
content: wasResumable ? "resume message queued" : "message queued",
|
||||
code: wasResumable ? "resumed" : "queued",
|
||||
success: true,
|
||||
})
|
||||
|
||||
awaitingPostCancelRecovery = false
|
||||
break
|
||||
}
|
||||
|
||||
case "cancel": {
|
||||
setStreamRequestId(stdinCommand.requestId)
|
||||
|
||||
const hasTaskInFlight = Boolean(
|
||||
activeTaskPromise || activeTaskCommand === "start" || host.client.hasActiveTask(),
|
||||
)
|
||||
|
||||
if (!hasTaskInFlight) {
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "ack",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "cancel",
|
||||
taskId: latestTaskId,
|
||||
content: "no active task to cancel",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "cancel",
|
||||
taskId: latestTaskId,
|
||||
content: "cancel ignored (no active task)",
|
||||
code: "no_active_task",
|
||||
success: true,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
cancelRequestedForActiveTask = true
|
||||
awaitingPostCancelRecovery = true
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "ack",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "cancel",
|
||||
taskId: latestTaskId,
|
||||
content: host.client.hasActiveTask() ? "cancel requested" : "cancel requested (task starting)",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
|
||||
try {
|
||||
host.client.cancelTask()
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "cancel",
|
||||
taskId: latestTaskId,
|
||||
content: "cancel signal sent",
|
||||
code: "cancel_requested",
|
||||
success: true,
|
||||
})
|
||||
} catch (error) {
|
||||
if (
|
||||
isExpectedControlFlowError(error, {
|
||||
stdinStreamMode: true,
|
||||
cancelRequested: true,
|
||||
shuttingDown: shouldShutdown,
|
||||
operation: "cancel",
|
||||
})
|
||||
) {
|
||||
const noActiveTask = isNoActiveTaskLikeError(error)
|
||||
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "cancel",
|
||||
taskId: latestTaskId,
|
||||
content: noActiveTask ? "cancel ignored (task already settled)" : "cancel handled",
|
||||
code: noActiveTask ? "no_active_task" : "cancel_requested",
|
||||
success: true,
|
||||
})
|
||||
|
||||
if (noActiveTask) {
|
||||
awaitingPostCancelRecovery = false
|
||||
}
|
||||
|
||||
cancelRequestedForActiveTask = false
|
||||
} else {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "error",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "cancel",
|
||||
taskId: latestTaskId,
|
||||
content: message,
|
||||
code: "cancel_error",
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "ping":
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "ack",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "ping",
|
||||
taskId: latestTaskId,
|
||||
content: "pong",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "ping",
|
||||
taskId: latestTaskId,
|
||||
content: "pong",
|
||||
code: "pong",
|
||||
success: true,
|
||||
})
|
||||
break
|
||||
|
||||
case "shutdown":
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "ack",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "shutdown",
|
||||
taskId: latestTaskId,
|
||||
content: "shutdown requested",
|
||||
code: "accepted",
|
||||
success: true,
|
||||
})
|
||||
jsonEmitter.emitControl({
|
||||
subtype: "done",
|
||||
requestId: stdinCommand.requestId,
|
||||
command: "shutdown",
|
||||
taskId: latestTaskId,
|
||||
content: "shutting down process",
|
||||
code: "shutdown_requested",
|
||||
success: true,
|
||||
})
|
||||
shouldShutdown = true
|
||||
break
|
||||
}
|
||||
|
||||
if (shouldShutdown) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasReceivedStdinCommand) {
|
||||
throw new Error("no stdin command provided")
|
||||
}
|
||||
|
||||
if (shouldShutdown && host.client.hasActiveTask()) {
|
||||
host.client.cancelTask()
|
||||
}
|
||||
|
||||
if (!shouldShutdown) {
|
||||
if (activeTaskPromise) {
|
||||
await activeTaskPromise
|
||||
} else if (host.client.hasActiveTask()) {
|
||||
await waitForTaskProgressAfterStdinClosed(host, () => ({
|
||||
hasSeenQueueState,
|
||||
queueDepth: lastQueueDepth,
|
||||
}))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
offClientError()
|
||||
host.off("extensionWebviewMessage", onExtensionMessage)
|
||||
offTaskCompleted()
|
||||
}
|
||||
}
|
||||
155
apps/cli/src/commands/cli/upgrade.ts
Normal file
155
apps/cli/src/commands/cli/upgrade.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { spawn } from "child_process"
|
||||
|
||||
import { VERSION } from "@/lib/utils/version.js"
|
||||
import { isRecord } from "@/lib/utils/guards.js"
|
||||
|
||||
const RELEASES_URL = "https://api.github.com/repos/RooCodeInc/Roo-Code/releases?per_page=100"
|
||||
export const INSTALL_SCRIPT_COMMAND =
|
||||
"curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh"
|
||||
|
||||
export interface UpgradeOptions {
|
||||
currentVersion?: string
|
||||
fetchImpl?: typeof fetch
|
||||
runInstaller?: () => Promise<void>
|
||||
}
|
||||
|
||||
function parseVersion(version: string): number[] {
|
||||
const cleaned = version
|
||||
.trim()
|
||||
.replace(/^cli-v/, "")
|
||||
.replace(/^v/, "")
|
||||
const core = cleaned.split("+", 1)[0]?.split("-", 1)[0]
|
||||
|
||||
if (!core) {
|
||||
throw new Error(`Invalid version: ${version}`)
|
||||
}
|
||||
|
||||
const parts = core.split(".")
|
||||
if (parts.length === 0) {
|
||||
throw new Error(`Invalid version: ${version}`)
|
||||
}
|
||||
|
||||
return parts.map((part) => {
|
||||
if (!/^\d+$/.test(part)) {
|
||||
throw new Error(`Invalid version: ${version}`)
|
||||
}
|
||||
|
||||
return Number.parseInt(part, 10)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns:
|
||||
* - 1 when `a > b`
|
||||
* - 0 when `a === b`
|
||||
* - -1 when `a < b`
|
||||
*/
|
||||
export function compareVersions(a: string, b: string): number {
|
||||
const aParts = parseVersion(a)
|
||||
const bParts = parseVersion(b)
|
||||
const maxLength = Math.max(aParts.length, bParts.length)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const aPart = aParts[i] ?? 0
|
||||
const bPart = bParts[i] ?? 0
|
||||
|
||||
if (aPart > bPart) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (aPart < bPart) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export async function getLatestCliVersion(fetchImpl: typeof fetch = fetch): Promise<string> {
|
||||
const response = await fetchImpl(RELEASES_URL, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "roo-cli",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to check latest version (HTTP ${response.status})`)
|
||||
}
|
||||
|
||||
const releases = await response.json()
|
||||
if (!Array.isArray(releases)) {
|
||||
throw new Error("Invalid release response from GitHub.")
|
||||
}
|
||||
|
||||
let latestVersion: string | undefined
|
||||
|
||||
for (const release of releases) {
|
||||
if (!isRecord(release)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const tagName = release.tag_name
|
||||
if (typeof tagName === "string" && tagName.startsWith("cli-v")) {
|
||||
const candidate = tagName.slice("cli-v".length)
|
||||
try {
|
||||
if (!latestVersion || compareVersions(candidate, latestVersion) > 0) {
|
||||
latestVersion = candidate
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed CLI tags and keep scanning other releases.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (latestVersion) {
|
||||
return latestVersion
|
||||
}
|
||||
|
||||
throw new Error("Could not determine the latest CLI release version.")
|
||||
}
|
||||
|
||||
export function runUpgradeInstaller(version?: string, spawnImpl: typeof spawn = spawn): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const env = version ? { ...process.env, ROO_VERSION: version } : process.env
|
||||
const child = spawnImpl("sh", ["-c", INSTALL_SCRIPT_COMMAND], { stdio: "inherit", env })
|
||||
|
||||
child.once("error", (error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
child.once("close", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const reason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`
|
||||
reject(new Error(`Upgrade installer failed (${reason}).`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function upgrade(options: UpgradeOptions = {}): Promise<void> {
|
||||
const currentVersion = options.currentVersion ?? VERSION
|
||||
const fetchImpl = options.fetchImpl ?? fetch
|
||||
const runInstaller = options.runInstaller
|
||||
|
||||
console.log(`Current version: ${currentVersion}`)
|
||||
|
||||
const latestVersion = await getLatestCliVersion(fetchImpl)
|
||||
console.log(`Latest version: ${latestVersion}`)
|
||||
|
||||
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
||||
console.log("Roo CLI is already up to date.")
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Upgrading Roo CLI from ${currentVersion} to ${latestVersion}...`)
|
||||
if (runInstaller) {
|
||||
await runInstaller()
|
||||
} else {
|
||||
await runUpgradeInstaller(latestVersion)
|
||||
}
|
||||
console.log("✓ Upgrade completed.")
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
export * from "./auth/index.js"
|
||||
export * from "./cli/index.js"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Command } from "commander"
|
|||
|
||||
import { DEFAULT_FLAGS } from "@/types/constants.js"
|
||||
import { VERSION } from "@/lib/utils/version.js"
|
||||
import { run, login, logout, status } from "@/commands/index.js"
|
||||
import { run, listCommands, listModes, listModels, listSessions, upgrade } from "@/commands/index.js"
|
||||
|
||||
const program = new Command()
|
||||
|
||||
|
|
@ -10,25 +10,45 @@ program
|
|||
.name("roo")
|
||||
.description("Roo Code CLI - starts an interactive session by default, use -p/--print for non-interactive output")
|
||||
.version(VERSION)
|
||||
.enablePositionalOptions()
|
||||
.passThroughOptions()
|
||||
|
||||
program
|
||||
.argument("[prompt]", "Your prompt")
|
||||
.option("--prompt-file <path>", "Read prompt from a file instead of command line argument")
|
||||
.option("--create-with-session-id <session-id>", "Create a new task with a specific session ID (must be a UUID)")
|
||||
.option("--session-id <session-id>", "Resume a specific task by session ID")
|
||||
.option("-c, --continue", "Resume the most recent task in the current workspace", false)
|
||||
.option("-w, --workspace <path>", "Workspace directory path (defaults to current working directory)")
|
||||
.option("-p, --print", "Print response and exit (non-interactive mode)", false)
|
||||
.option(
|
||||
"--stdin-prompt-stream",
|
||||
"Read NDJSON commands from stdin (requires --print and --output-format stream-json)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--signal-only-exit",
|
||||
"Do not exit from normal completion/errors; only terminate on SIGINT/SIGTERM (intended for stdin stream harnesses)",
|
||||
false,
|
||||
)
|
||||
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
||||
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
|
||||
.option("-y, --yes", "Auto-approve all prompts (use with caution)", false)
|
||||
.option("--dangerously-skip-permissions", "Alias for --yes", false)
|
||||
.option("-a, --require-approval", "Require manual approval for actions", false)
|
||||
.option("-k, --api-key <key>", "API key for the LLM provider")
|
||||
.option("--provider <provider>", "API provider (roo, anthropic, openai, openrouter, etc.)")
|
||||
.option("--provider <provider>", "API provider (anthropic, openai, openrouter, etc.)")
|
||||
.option("-m, --model <model>", "Model to use", DEFAULT_FLAGS.model)
|
||||
.option("--mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode)
|
||||
.option("--terminal-shell <path>", "Absolute path to shell executable for inline terminal commands")
|
||||
.option(
|
||||
"-r, --reasoning-effort <effort>",
|
||||
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
||||
DEFAULT_FLAGS.reasoningEffort,
|
||||
)
|
||||
.option(
|
||||
"--consecutive-mistake-limit <limit>",
|
||||
"Consecutive error/repetition limit before guidance prompt (0 disables the limit)",
|
||||
(value) => Number.parseInt(value, 10),
|
||||
)
|
||||
.option("--exit-on-error", "Exit on API request errors instead of retrying", false)
|
||||
.option("--ephemeral", "Run without persisting state (uses temporary storage)", false)
|
||||
.option("--oneshot", "Exit upon task completion", false)
|
||||
|
|
@ -39,33 +59,71 @@ program
|
|||
)
|
||||
.action(run)
|
||||
|
||||
const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud")
|
||||
const listCommand = program
|
||||
.command("list")
|
||||
.description("List commands, modes, models, or sessions")
|
||||
.enablePositionalOptions()
|
||||
.passThroughOptions()
|
||||
|
||||
authCommand
|
||||
.command("login")
|
||||
.description("Authenticate with Roo Code Cloud")
|
||||
.option("-v, --verbose", "Enable verbose output", false)
|
||||
.action(async (options: { verbose: boolean }) => {
|
||||
const result = await login({ verbose: options.verbose })
|
||||
process.exit(result.success ? 0 : 1)
|
||||
})
|
||||
const applyListOptions = (command: Command) =>
|
||||
command
|
||||
.option("-w, --workspace <path>", "Workspace directory path (defaults to current working directory)")
|
||||
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
||||
.option("-k, --api-key <key>", "API key for the LLM provider")
|
||||
.option("--format <format>", 'Output format: "json" (default) or "text"', "json")
|
||||
.option("-d, --debug", "Enable debug output", false)
|
||||
|
||||
authCommand
|
||||
.command("logout")
|
||||
.description("Log out from Roo Code Cloud")
|
||||
.option("-v, --verbose", "Enable verbose output", false)
|
||||
.action(async (options: { verbose: boolean }) => {
|
||||
const result = await logout({ verbose: options.verbose })
|
||||
process.exit(result.success ? 0 : 1)
|
||||
})
|
||||
const runListAction = async (action: () => Promise<void>) => {
|
||||
try {
|
||||
await action()
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[CLI] Error: ${message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
authCommand
|
||||
.command("status")
|
||||
.description("Show authentication status")
|
||||
.option("-v, --verbose", "Enable verbose output", false)
|
||||
.action(async (options: { verbose: boolean }) => {
|
||||
const result = await status({ verbose: options.verbose })
|
||||
process.exit(result.authenticated ? 0 : 1)
|
||||
const runUpgradeAction = async (action: () => Promise<void>) => {
|
||||
try {
|
||||
await action()
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[CLI] Error: ${message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
applyListOptions(listCommand.command("commands").description("List available slash commands")).action(
|
||||
async (options: Parameters<typeof listCommands>[0]) => {
|
||||
await runListAction(() => listCommands(options))
|
||||
},
|
||||
)
|
||||
|
||||
applyListOptions(listCommand.command("modes").description("List available modes")).action(
|
||||
async (options: Parameters<typeof listModes>[0]) => {
|
||||
await runListAction(() => listModes(options))
|
||||
},
|
||||
)
|
||||
|
||||
applyListOptions(listCommand.command("models").description("List available models")).action(
|
||||
async (options: Parameters<typeof listModels>[0]) => {
|
||||
await runListAction(() => listModels(options))
|
||||
},
|
||||
)
|
||||
|
||||
applyListOptions(listCommand.command("sessions").description("List task sessions")).action(
|
||||
async (options: Parameters<typeof listSessions>[0]) => {
|
||||
await runListAction(() => listSessions(options))
|
||||
},
|
||||
)
|
||||
|
||||
program
|
||||
.command("upgrade")
|
||||
.description("Upgrade Roo Code CLI to the latest version")
|
||||
.action(async () => {
|
||||
await runUpgradeAction(() => upgrade())
|
||||
})
|
||||
|
||||
program.parse()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue