@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +152 -76
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/web-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/web-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/web-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/web-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.31.0] - 2026-01-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added temporary model selection: `Ctrl+Y` opens model selector for session-only model switching (not persisted to settings)
|
|
10
|
+
- Added `setModelTemporary()` method to AgentSession for ephemeral model changes
|
|
11
|
+
- Added empty Enter to flush queued messages: pressing Enter with empty editor while streaming aborts current stream
|
|
12
|
+
- Added auto-chdir to temp directories when starting in home unless `--allow-home` is set
|
|
13
|
+
- Added upfront diff parsing and filtering for code review command to exclude lock files, generated code, and binary assets
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed auto-chdir to only use existing directories and fall back to `tmpdir()`
|
|
18
|
+
- Added automatic reviewer agent count recommendation based on diff weight and file count
|
|
19
|
+
- Added file grouping guidance for parallel review distribution across multiple agents
|
|
20
|
+
- Added diff preview mode for large changesets that exceed size thresholds
|
|
21
|
+
- Added in-memory session storage implementation for testing and ephemeral sessions
|
|
22
|
+
- Added `createToolUIKit` helper to consolidate common UI formatting utilities across tool renderers
|
|
23
|
+
- Added configurable bash interceptor rules via `bashInterceptor.patterns` setting for custom command blocking
|
|
24
|
+
- Added `bashInterceptor.simpleLs` setting to control interception of bare ls commands
|
|
25
|
+
- Added LSP server configuration via external JSON defaults file for easier customization
|
|
26
|
+
- Added abort signal propagation to web scrapers for improved cancellation handling
|
|
27
|
+
- Added `diagnosticsVersion` tracking to LSP client for more reliable diagnostic polling
|
|
28
|
+
- Added 80+ specialized web scrapers for structured content extraction from popular sites including GitHub, GitLab, npm, PyPI, crates.io, Wikipedia, YouTube, Stack Overflow, Hacker News, Reddit, arXiv, PubMed, and many more
|
|
29
|
+
- Added site-specific API integrations for package registries (npm, PyPI, crates.io, Hex, Hackage, NuGet, Maven, RubyGems, Packagist, pub.dev, Go packages)
|
|
30
|
+
- Added scrapers for social platforms (Mastodon, Bluesky, Lemmy, Lobsters, Dev.to, Discourse)
|
|
31
|
+
- Added scrapers for academic sources (arXiv, bioRxiv, PubMed, Semantic Scholar, ORCID, CrossRef, IACR)
|
|
32
|
+
- Added scrapers for security databases (NVD, OSV, CISA KEV)
|
|
33
|
+
- Added scrapers for documentation sites (MDN, Read the Docs, RFC Editor, W3C, SPDX, tldr, cheat.sh)
|
|
34
|
+
- Added scrapers for media platforms (YouTube, Vimeo, Spotify, Discogs, MusicBrainz)
|
|
35
|
+
- Added scrapers for AI/ML platforms (Hugging Face, Ollama)
|
|
36
|
+
- Added scrapers for app stores and marketplaces (VS Code Marketplace, JetBrains Marketplace, Firefox Add-ons, Open VSX, Flathub, F-Droid, Snapcraft)
|
|
37
|
+
- Added scrapers for business data (SEC EDGAR, OpenCorporates, CoinGecko)
|
|
38
|
+
- Added scrapers for reference sources (Wikipedia, Wikidata, OpenLibrary, Choose a License)
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Changed `Ctrl+P` to cycle through role models (slow → default → smol) instead of all available models
|
|
43
|
+
- Changed `Shift+Ctrl+P` to cycle role models temporarily (not persisted)
|
|
44
|
+
- Changed Extension Control Center to scale with terminal height instead of fixed 25-line limit
|
|
45
|
+
- Changed review command to parse git diff upfront and provide structured context to reviewer agents
|
|
46
|
+
- Changed session persistence to use structured logging instead of console.error for persistence failures
|
|
47
|
+
- Changed find tool to use fd command for .gitignore discovery instead of Bun.Glob for better abort handling
|
|
48
|
+
- Changed LSP config loading to only mark overrides when servers are actually defined
|
|
49
|
+
- Changed task tool to require explicit task `id` field instead of auto-generating names from agent type
|
|
50
|
+
- Changed grep and find tools to use native Bun file APIs instead of Node.js fs module for improved performance
|
|
51
|
+
- Changed YouTube scraper to use async command execution with proper stream handling
|
|
52
|
+
- Improved rust-analyzer diagnostic polling to use version-based stability detection instead of time-based delays
|
|
53
|
+
- Changed theme icons for extension types to use Unicode symbols (✧, ⚒) instead of text abbreviations (SK, TL, MCP)
|
|
54
|
+
- Changed task tool to use short CamelCase task IDs instead of agent-based naming (e.g., 'SessionStore' instead of 'explore_0')
|
|
55
|
+
- Changed task tool to accept single `agent` parameter at top level instead of per-task agent specification
|
|
56
|
+
- Changed reviewer agent to use `complete` tool instead of `submit_review` for finishing reviews
|
|
57
|
+
- Changed theme icons for extensions to use Unicode symbols instead of text abbreviations
|
|
58
|
+
- Changed LSP file type matching to support exact filename matches in addition to extensions
|
|
59
|
+
- Improved rust-analyzer diagnostic polling to use version-based stability detection
|
|
60
|
+
- Refactored web-fetch tool to use modular scraper architecture for improved maintainability
|
|
61
|
+
|
|
62
|
+
### Removed
|
|
63
|
+
|
|
64
|
+
- Removed `submit_review` tool - reviewers now finish via `complete` tool with structured output
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Fixed session persistence to call fsync before renaming temp file for durability
|
|
69
|
+
- Fixed duplicate persistence error logging by tracking whether error was already reported
|
|
70
|
+
- Fixed byte counting in task output truncation to correctly handle multi-byte Unicode characters
|
|
71
|
+
- Fixed parallel task execution to propagate abort signals and fail fast on first error
|
|
72
|
+
- Fixed task worker abort handling to properly clean up on cancellation
|
|
73
|
+
- Fixed parallel task execution to fail fast on first error instead of waiting for all workers
|
|
74
|
+
- Fixed byte counting in task output truncation to handle multi-byte Unicode characters correctly
|
|
75
|
+
|
|
76
|
+
## [3.30.0] - 2026-01-07
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- Added environment variable configuration for task limits: `OMP_TASK_MAX_PARALLEL`, `OMP_TASK_MAX_CONCURRENCY`, `OMP_TASK_MAX_OUTPUT_BYTES`, `OMP_TASK_MAX_OUTPUT_LINES`, and `OMP_TASK_MAX_AGENTS_IN_DESCRIPTION`
|
|
80
|
+
- Added specialized web-fetch handlers for 50+ platforms including GitHub, GitLab, npm, PyPI, crates.io, Stack Overflow, Wikipedia, arXiv, PubMed, Hacker News, Reddit, Mastodon, Bluesky, and many more
|
|
81
|
+
- Added automatic yt-dlp installation for YouTube transcript extraction
|
|
82
|
+
- Added YouTube video support with automatic transcript extraction via yt-dlp
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- Changed task executor to gracefully handle worker termination with proper cleanup and timeout handling
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- Fixed Lobsters front page handler to use correct API endpoint (`/hottest.json` instead of invalid `.json`)
|
|
91
|
+
- Fixed task worker error handling to prevent hanging on worker crashes, uncaught errors, and unhandled rejections
|
|
92
|
+
- Fixed double-stringified JSON output from subagents being returned as escaped strings instead of parsed objects
|
|
93
|
+
- Fixed markitdown tool installation to use automatic tool installer instead of requiring manual installation
|
|
94
|
+
|
|
5
95
|
## [3.25.0] - 2026-01-07
|
|
6
96
|
### Added
|
|
7
97
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.31.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@mariozechner/pi-ai": "^0.37.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "3.
|
|
45
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@mariozechner/pi-ai": "^0.37.8",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "3.31.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "3.31.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "3.31.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
package/src/cli/args.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type Mode = "text" | "json" | "rpc";
|
|
|
11
11
|
|
|
12
12
|
export interface Args {
|
|
13
13
|
cwd?: string;
|
|
14
|
+
allowHome?: boolean;
|
|
14
15
|
provider?: string;
|
|
15
16
|
model?: string;
|
|
16
17
|
smol?: string;
|
|
@@ -62,6 +63,8 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
|
|
62
63
|
result.help = true;
|
|
63
64
|
} else if (arg === "--version" || arg === "-v") {
|
|
64
65
|
result.version = true;
|
|
66
|
+
} else if (arg === "--allow-home") {
|
|
67
|
+
result.allowHome = true;
|
|
65
68
|
} else if (arg === "--mode" && i + 1 < args.length) {
|
|
66
69
|
const mode = args[++i];
|
|
67
70
|
if (mode === "text" || mode === "json" || mode === "rpc") {
|
|
@@ -176,6 +179,7 @@ ${chalk.bold("Options:")}
|
|
|
176
179
|
--api-key <key> API key (defaults to env vars)
|
|
177
180
|
--system-prompt <text> System prompt (default: coding assistant prompt)
|
|
178
181
|
--append-system-prompt <text> Append text or file contents to the system prompt
|
|
182
|
+
--allow-home Allow starting in ~ without auto-switching to a temp dir
|
|
179
183
|
--mode <mode> Output mode: text (default), json, or rpc
|
|
180
184
|
--print, -p Non-interactive mode: process prompt and exit
|
|
181
185
|
--continue, -c Continue previous session
|
|
@@ -1128,6 +1128,24 @@ export class AgentSession {
|
|
|
1128
1128
|
this.setThinkingLevel(this.thinkingLevel);
|
|
1129
1129
|
}
|
|
1130
1130
|
|
|
1131
|
+
/**
|
|
1132
|
+
* Set model temporarily (for this session only).
|
|
1133
|
+
* Validates API key, saves to session log but NOT to settings.
|
|
1134
|
+
* @throws Error if no API key available for the model
|
|
1135
|
+
*/
|
|
1136
|
+
async setModelTemporary(model: Model<any>): Promise<void> {
|
|
1137
|
+
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
1138
|
+
if (!apiKey) {
|
|
1139
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
this.agent.setModel(model);
|
|
1143
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
1144
|
+
|
|
1145
|
+
// Re-clamp thinking level for new model's capabilities
|
|
1146
|
+
this.setThinkingLevel(this.thinkingLevel);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1131
1149
|
/**
|
|
1132
1150
|
* Cycle to next/previous model.
|
|
1133
1151
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
@@ -1144,8 +1162,13 @@ export class AgentSession {
|
|
|
1144
1162
|
/**
|
|
1145
1163
|
* Cycle through configured role models in a fixed order.
|
|
1146
1164
|
* Skips missing roles and deduplicates models.
|
|
1165
|
+
* @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
|
|
1166
|
+
* @param options - Optional settings: `temporary` to not persist to settings
|
|
1147
1167
|
*/
|
|
1148
|
-
async cycleRoleModels(
|
|
1168
|
+
async cycleRoleModels(
|
|
1169
|
+
roleOrder: string[],
|
|
1170
|
+
options?: { temporary?: boolean },
|
|
1171
|
+
): Promise<RoleModelCycleResult | undefined> {
|
|
1149
1172
|
const availableModels = this._modelRegistry.getAvailable();
|
|
1150
1173
|
if (availableModels.length === 0) return undefined;
|
|
1151
1174
|
|
|
@@ -1185,7 +1208,11 @@ export class AgentSession {
|
|
|
1185
1208
|
const nextIndex = (currentIndex + 1) % roleModels.length;
|
|
1186
1209
|
const next = roleModels[nextIndex];
|
|
1187
1210
|
|
|
1188
|
-
|
|
1211
|
+
if (options?.temporary) {
|
|
1212
|
+
await this.setModelTemporary(next.model);
|
|
1213
|
+
} else {
|
|
1214
|
+
await this.setModel(next.model, next.role);
|
|
1215
|
+
}
|
|
1189
1216
|
|
|
1190
1217
|
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
|
1191
1218
|
}
|
|
@@ -10,6 +10,7 @@ import { createWriteStream, type WriteStream } from "node:fs";
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { Subprocess } from "bun";
|
|
13
|
+
import { nanoid } from "nanoid";
|
|
13
14
|
import stripAnsi from "strip-ansi";
|
|
14
15
|
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell";
|
|
15
16
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
@@ -77,7 +78,7 @@ function createOutputSink(
|
|
|
77
78
|
|
|
78
79
|
// Spill to temp file if needed
|
|
79
80
|
if (totalBytes > spillThreshold && !fullOutputPath) {
|
|
80
|
-
fullOutputPath = join(tmpdir(), `omp-${
|
|
81
|
+
fullOutputPath = join(tmpdir(), `omp-${nanoid()}.buffer`);
|
|
81
82
|
const ts = createWriteStream(fullOutputPath);
|
|
82
83
|
chunks.forEach((c) => {
|
|
83
84
|
ts.write(c);
|
|
@@ -6,11 +6,296 @@
|
|
|
6
6
|
* 2. Review uncommitted changes
|
|
7
7
|
* 3. Review a specific commit
|
|
8
8
|
* 4. Custom review instructions
|
|
9
|
+
*
|
|
10
|
+
* Runs git diff upfront, parses results, filters noise, and provides
|
|
11
|
+
* rich context for the orchestrating agent to distribute work across
|
|
12
|
+
* multiple reviewer agents based on diff weight and locality.
|
|
9
13
|
*/
|
|
10
14
|
|
|
15
|
+
import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
|
|
11
16
|
import type { HookCommandContext } from "../../../hooks/types";
|
|
12
17
|
import type { CustomCommand, CustomCommandAPI } from "../../types";
|
|
13
18
|
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Types
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
interface FileDiff {
|
|
24
|
+
path: string;
|
|
25
|
+
linesAdded: number;
|
|
26
|
+
linesRemoved: number;
|
|
27
|
+
hunks: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface DiffStats {
|
|
31
|
+
files: FileDiff[];
|
|
32
|
+
totalAdded: number;
|
|
33
|
+
totalRemoved: number;
|
|
34
|
+
excluded: { path: string; reason: string; linesAdded: number; linesRemoved: number }[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Exclusion patterns for noise files
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const EXCLUDED_PATTERNS: { pattern: RegExp; reason: string }[] = [
|
|
42
|
+
// Lock files
|
|
43
|
+
{ pattern: /\.lock$/, reason: "lock file" },
|
|
44
|
+
{ pattern: /-lock\.(json|yaml|yml)$/, reason: "lock file" },
|
|
45
|
+
{ pattern: /package-lock\.json$/, reason: "lock file" },
|
|
46
|
+
{ pattern: /yarn\.lock$/, reason: "lock file" },
|
|
47
|
+
{ pattern: /pnpm-lock\.yaml$/, reason: "lock file" },
|
|
48
|
+
{ pattern: /Cargo\.lock$/, reason: "lock file" },
|
|
49
|
+
{ pattern: /Gemfile\.lock$/, reason: "lock file" },
|
|
50
|
+
{ pattern: /poetry\.lock$/, reason: "lock file" },
|
|
51
|
+
{ pattern: /composer\.lock$/, reason: "lock file" },
|
|
52
|
+
{ pattern: /flake\.lock$/, reason: "lock file" },
|
|
53
|
+
|
|
54
|
+
// Generated/build artifacts
|
|
55
|
+
{ pattern: /\.min\.(js|css)$/, reason: "minified" },
|
|
56
|
+
{ pattern: /\.generated\./, reason: "generated" },
|
|
57
|
+
{ pattern: /\.snap$/, reason: "snapshot" },
|
|
58
|
+
{ pattern: /\.map$/, reason: "source map" },
|
|
59
|
+
{ pattern: /^dist\//, reason: "build output" },
|
|
60
|
+
{ pattern: /^build\//, reason: "build output" },
|
|
61
|
+
{ pattern: /^out\//, reason: "build output" },
|
|
62
|
+
{ pattern: /node_modules\//, reason: "vendor" },
|
|
63
|
+
{ pattern: /vendor\//, reason: "vendor" },
|
|
64
|
+
|
|
65
|
+
// Binary/assets (usually shown as binary in diff anyway)
|
|
66
|
+
{ pattern: /\.(png|jpg|jpeg|gif|ico|webp|avif)$/i, reason: "image" },
|
|
67
|
+
{ pattern: /\.(woff|woff2|ttf|eot|otf)$/i, reason: "font" },
|
|
68
|
+
{ pattern: /\.(pdf|zip|tar|gz|rar|7z)$/i, reason: "binary" },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
// Diff parsing
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a file path should be excluded from review.
|
|
77
|
+
* Returns the exclusion reason if excluded, undefined otherwise.
|
|
78
|
+
*/
|
|
79
|
+
function getExclusionReason(path: string): string | undefined {
|
|
80
|
+
for (const { pattern, reason } of EXCLUDED_PATTERNS) {
|
|
81
|
+
if (pattern.test(path)) return reason;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse unified diff output into per-file stats.
|
|
88
|
+
* Splits on file boundaries, counts +/- lines, and filters excluded files.
|
|
89
|
+
*/
|
|
90
|
+
function parseDiff(diffOutput: string): DiffStats {
|
|
91
|
+
const files: FileDiff[] = [];
|
|
92
|
+
const excluded: DiffStats["excluded"] = [];
|
|
93
|
+
let totalAdded = 0;
|
|
94
|
+
let totalRemoved = 0;
|
|
95
|
+
|
|
96
|
+
// Split by file boundary: "diff --git a/... b/..."
|
|
97
|
+
const fileChunks = diffOutput.split(/^diff --git /m).filter(Boolean);
|
|
98
|
+
|
|
99
|
+
for (const chunk of fileChunks) {
|
|
100
|
+
// Extract file path from "a/path b/path" line
|
|
101
|
+
const headerMatch = chunk.match(/^a\/(.+?) b\/(.+)/);
|
|
102
|
+
if (!headerMatch) continue;
|
|
103
|
+
|
|
104
|
+
const path = headerMatch[2];
|
|
105
|
+
|
|
106
|
+
// Count added/removed lines (lines starting with + or - but not ++ or --)
|
|
107
|
+
let linesAdded = 0;
|
|
108
|
+
let linesRemoved = 0;
|
|
109
|
+
|
|
110
|
+
const lines = chunk.split("\n");
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
113
|
+
linesAdded++;
|
|
114
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
115
|
+
linesRemoved++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const exclusionReason = getExclusionReason(path);
|
|
120
|
+
if (exclusionReason) {
|
|
121
|
+
excluded.push({ path, reason: exclusionReason, linesAdded, linesRemoved });
|
|
122
|
+
} else {
|
|
123
|
+
files.push({
|
|
124
|
+
path,
|
|
125
|
+
linesAdded,
|
|
126
|
+
linesRemoved,
|
|
127
|
+
hunks: `diff --git ${chunk}`,
|
|
128
|
+
});
|
|
129
|
+
totalAdded += linesAdded;
|
|
130
|
+
totalRemoved += linesRemoved;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { files, totalAdded, totalRemoved, excluded };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get file extension for display purposes.
|
|
139
|
+
*/
|
|
140
|
+
function getFileExt(path: string): string {
|
|
141
|
+
const match = path.match(/\.([^.]+)$/);
|
|
142
|
+
return match ? match[1] : "";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Determine recommended number of reviewer agents based on diff weight.
|
|
147
|
+
* Uses total lines changed as the primary metric.
|
|
148
|
+
*/
|
|
149
|
+
function getRecommendedAgentCount(stats: DiffStats): number {
|
|
150
|
+
const totalLines = stats.totalAdded + stats.totalRemoved;
|
|
151
|
+
const fileCount = stats.files.length;
|
|
152
|
+
|
|
153
|
+
// Heuristics:
|
|
154
|
+
// - Tiny (<100 lines or 1-2 files): 1 agent
|
|
155
|
+
// - Small (<500 lines): 1-2 agents
|
|
156
|
+
// - Medium (<2000 lines): 2-4 agents
|
|
157
|
+
// - Large (<5000 lines): 4-8 agents
|
|
158
|
+
// - Huge (>5000 lines): 8-16 agents
|
|
159
|
+
|
|
160
|
+
if (totalLines < 100 || fileCount <= 2) return 1;
|
|
161
|
+
if (totalLines < 500) return Math.min(2, fileCount);
|
|
162
|
+
if (totalLines < 2000) return Math.min(4, Math.ceil(fileCount / 3));
|
|
163
|
+
if (totalLines < 5000) return Math.min(8, Math.ceil(fileCount / 2));
|
|
164
|
+
return Math.min(16, fileCount);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Format diff stats as a markdown table for the prompt.
|
|
169
|
+
*/
|
|
170
|
+
function formatFileTable(files: FileDiff[]): string {
|
|
171
|
+
if (files.length === 0) return "_No files to review._";
|
|
172
|
+
|
|
173
|
+
const rows = files.map((f) => {
|
|
174
|
+
const ext = getFileExt(f.path);
|
|
175
|
+
return `| ${f.path} | +${f.linesAdded}/-${f.linesRemoved} | ${ext} |`;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return `| File | +/- | Type |\n|------|-----|------|\n${rows.join("\n")}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract first N lines of actual diff content (excluding headers) for preview.
|
|
183
|
+
*/
|
|
184
|
+
function getDiffPreview(hunks: string, maxLines: number): string {
|
|
185
|
+
const lines = hunks.split("\n");
|
|
186
|
+
const contentLines: string[] = [];
|
|
187
|
+
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
// Skip diff headers, keep actual content
|
|
190
|
+
if (
|
|
191
|
+
line.startsWith("diff --git") ||
|
|
192
|
+
line.startsWith("index ") ||
|
|
193
|
+
line.startsWith("---") ||
|
|
194
|
+
line.startsWith("+++") ||
|
|
195
|
+
line.startsWith("@@")
|
|
196
|
+
) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
contentLines.push(line);
|
|
200
|
+
if (contentLines.length >= maxLines) break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return contentLines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Format condensed diff previews for large changesets.
|
|
208
|
+
*/
|
|
209
|
+
function formatDiffPreviews(files: FileDiff[], linesPerFile: number): string {
|
|
210
|
+
const parts: string[] = [];
|
|
211
|
+
|
|
212
|
+
for (const f of files) {
|
|
213
|
+
const preview = getDiffPreview(f.hunks, linesPerFile);
|
|
214
|
+
if (preview.trim()) {
|
|
215
|
+
parts.push(`#### ${f.path}\n\`\`\`diff\n${preview}\n\`\`\``);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return parts.join("\n\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Format excluded files list for the prompt.
|
|
224
|
+
*/
|
|
225
|
+
function formatExcluded(excluded: DiffStats["excluded"]): string {
|
|
226
|
+
if (excluded.length === 0) return "";
|
|
227
|
+
|
|
228
|
+
const items = excluded.map((e) => `- \`${e.path}\` (+${e.linesAdded}/-${e.linesRemoved}) — ${e.reason}`);
|
|
229
|
+
|
|
230
|
+
return `### Excluded Files (${excluded.length})\n\n${items.join("\n")}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Thresholds for diff inclusion
|
|
234
|
+
const MAX_DIFF_CHARS = 50_000; // Don't include diff above this
|
|
235
|
+
const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than this
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build the full review prompt with diff stats and distribution guidance.
|
|
239
|
+
*/
|
|
240
|
+
function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): string {
|
|
241
|
+
const agentCount = getRecommendedAgentCount(stats);
|
|
242
|
+
const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
|
|
243
|
+
const totalLines = stats.totalAdded + stats.totalRemoved;
|
|
244
|
+
|
|
245
|
+
// Build distribution guidance
|
|
246
|
+
const distributionGuidance =
|
|
247
|
+
`Based on the diff weight (~${totalLines} lines across ${stats.files.length} files), ` +
|
|
248
|
+
(agentCount === 1 ? `use **1 reviewer agent**.` : `spawn **${agentCount} reviewer agents** in parallel.`);
|
|
249
|
+
|
|
250
|
+
// Build grouping guidance (only for multi-agent)
|
|
251
|
+
const groupingGuidance =
|
|
252
|
+
agentCount > 1
|
|
253
|
+
? `Group files by locality (related changes together). For example:
|
|
254
|
+
- Files in the same directory or module → same agent
|
|
255
|
+
- Files that implement related functionality → same agent
|
|
256
|
+
- Test files with their implementation files → same agent
|
|
257
|
+
|
|
258
|
+
Use the Task tool with \`agent: "reviewer"\` and the batch \`tasks\` array to run reviews in parallel.`
|
|
259
|
+
: "";
|
|
260
|
+
|
|
261
|
+
// Build diff section
|
|
262
|
+
let diffSection: string;
|
|
263
|
+
if (!skipDiff) {
|
|
264
|
+
diffSection = `### Diff
|
|
265
|
+
|
|
266
|
+
<diff>
|
|
267
|
+
${rawDiff.trim()}
|
|
268
|
+
</diff>`;
|
|
269
|
+
} else {
|
|
270
|
+
const linesPerFile = Math.max(5, Math.floor(100 / stats.files.length));
|
|
271
|
+
diffSection = `### Diff Previews
|
|
272
|
+
|
|
273
|
+
_Full diff too large (${stats.files.length} files). Showing first ~${linesPerFile} lines per file. Reviewers should fetch full diffs for assigned files._
|
|
274
|
+
|
|
275
|
+
${formatDiffPreviews(stats.files, linesPerFile)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Build diff instruction
|
|
279
|
+
const diffInstruction = skipDiff
|
|
280
|
+
? "Run `git diff` or `git show` to get the diff for assigned files"
|
|
281
|
+
: "Use the diff hunks provided below (don't re-run git diff)";
|
|
282
|
+
|
|
283
|
+
// Replace template variables
|
|
284
|
+
return reviewRequestTemplate
|
|
285
|
+
.replace("{MODE}", mode)
|
|
286
|
+
.replace("{FILE_COUNT}", String(stats.files.length))
|
|
287
|
+
.replace("{LINES_ADDED}", String(stats.totalAdded))
|
|
288
|
+
.replace("{LINES_REMOVED}", String(stats.totalRemoved))
|
|
289
|
+
.replace("{FILE_TABLE}", formatFileTable(stats.files))
|
|
290
|
+
.replace("{EXCLUDED_SECTION}", stats.excluded.length > 0 ? formatExcluded(stats.excluded) : "")
|
|
291
|
+
.replace("{DISTRIBUTION_GUIDANCE}", distributionGuidance)
|
|
292
|
+
.replace("{GROUPING_GUIDANCE}", groupingGuidance)
|
|
293
|
+
.replace("{DIFF_INSTRUCTION}", diffInstruction)
|
|
294
|
+
.replace("{DIFF_SECTION}", diffSection)
|
|
295
|
+
.replace(/\n{3,}/g, "\n\n") // Collapse multiple blank lines
|
|
296
|
+
.trim();
|
|
297
|
+
}
|
|
298
|
+
|
|
14
299
|
export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
|
|
15
300
|
return {
|
|
16
301
|
name: "review",
|
|
@@ -21,7 +306,6 @@ export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
|
|
|
21
306
|
return "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
|
|
22
307
|
}
|
|
23
308
|
|
|
24
|
-
// Main menu
|
|
25
309
|
const mode = await ctx.ui.select("Review Mode", [
|
|
26
310
|
"1. Review against a base branch (PR Style)",
|
|
27
311
|
"2. Review uncommitted changes",
|
|
@@ -46,26 +330,59 @@ export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
|
|
|
46
330
|
if (!baseBranch) return undefined;
|
|
47
331
|
|
|
48
332
|
const currentBranch = await getCurrentBranch(api);
|
|
49
|
-
|
|
333
|
+
const diffResult = await api.exec("git", ["diff", `${baseBranch}...${currentBranch}`], {
|
|
334
|
+
timeout: 30000,
|
|
335
|
+
});
|
|
336
|
+
if (diffResult.code !== 0) {
|
|
337
|
+
ctx.ui.notify(`Failed to get diff: ${diffResult.stderr}`, "error");
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
50
340
|
|
|
51
|
-
|
|
341
|
+
if (!diffResult.stdout.trim()) {
|
|
342
|
+
ctx.ui.notify(`No changes between ${baseBranch} and ${currentBranch}`, "warning");
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
52
345
|
|
|
53
|
-
|
|
346
|
+
const stats = parseDiff(diffResult.stdout);
|
|
347
|
+
if (stats.files.length === 0) {
|
|
348
|
+
ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return buildReviewPrompt(
|
|
353
|
+
`Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
|
|
354
|
+
stats,
|
|
355
|
+
diffResult.stdout,
|
|
356
|
+
);
|
|
54
357
|
}
|
|
55
358
|
|
|
56
359
|
case 2: {
|
|
57
|
-
// Uncommitted changes
|
|
360
|
+
// Uncommitted changes - combine staged and unstaged
|
|
58
361
|
const status = await getGitStatus(api);
|
|
59
362
|
if (!status.trim()) {
|
|
60
363
|
ctx.ui.notify("No uncommitted changes found", "warning");
|
|
61
364
|
return undefined;
|
|
62
365
|
}
|
|
63
366
|
|
|
64
|
-
|
|
367
|
+
const [unstagedResult, stagedResult] = await Promise.all([
|
|
368
|
+
api.exec("git", ["diff"], { timeout: 30000 }),
|
|
369
|
+
api.exec("git", ["diff", "--cached"], { timeout: 30000 }),
|
|
370
|
+
]);
|
|
65
371
|
|
|
66
|
-
|
|
372
|
+
const combinedDiff = [unstagedResult.stdout, stagedResult.stdout].filter(Boolean).join("\n");
|
|
373
|
+
|
|
374
|
+
if (!combinedDiff.trim()) {
|
|
375
|
+
ctx.ui.notify("No diff content found", "warning");
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const stats = parseDiff(combinedDiff);
|
|
380
|
+
if (stats.files.length === 0) {
|
|
381
|
+
ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
67
384
|
|
|
68
|
-
|
|
385
|
+
return buildReviewPrompt("Reviewing uncommitted changes (staged + unstaged)", stats, combinedDiff);
|
|
69
386
|
}
|
|
70
387
|
|
|
71
388
|
case 3: {
|
|
@@ -82,24 +399,62 @@ Run \`git diff\` for unstaged changes and \`git diff --cached\` for staged chang
|
|
|
82
399
|
// Extract commit hash from selection (format: "abc1234 message")
|
|
83
400
|
const hash = selected.split(" ")[0];
|
|
84
401
|
|
|
85
|
-
|
|
402
|
+
// Get the commit diff (with timeout)
|
|
403
|
+
const showResult = await api.exec("git", ["show", "--format=", hash], { timeout: 30000 });
|
|
404
|
+
if (showResult.code !== 0) {
|
|
405
|
+
ctx.ui.notify(`Failed to get commit: ${showResult.stderr}`, "error");
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
86
408
|
|
|
87
|
-
|
|
409
|
+
if (!showResult.stdout.trim()) {
|
|
410
|
+
ctx.ui.notify("Commit has no diff content", "warning");
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const stats = parseDiff(showResult.stdout);
|
|
415
|
+
if (stats.files.length === 0) {
|
|
416
|
+
ctx.ui.notify("No reviewable files in commit (all changes filtered out)", "warning");
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
88
419
|
|
|
89
|
-
|
|
420
|
+
return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, showResult.stdout);
|
|
90
421
|
}
|
|
91
422
|
|
|
92
423
|
case 4: {
|
|
93
|
-
// Custom instructions
|
|
424
|
+
// Custom instructions - still uses the old approach since user provides context
|
|
94
425
|
const instructions = await ctx.ui.editor(
|
|
95
426
|
"Enter custom review instructions",
|
|
96
427
|
"Review the following:\n\n",
|
|
97
428
|
);
|
|
98
429
|
if (!instructions?.trim()) return undefined;
|
|
99
430
|
|
|
100
|
-
|
|
431
|
+
// For custom, we still try to get current diff for context
|
|
432
|
+
const diffResult = await api.exec("git", ["diff", "HEAD"], { timeout: 30000 });
|
|
433
|
+
const hasDiff = diffResult.code === 0 && diffResult.stdout.trim();
|
|
434
|
+
|
|
435
|
+
if (hasDiff) {
|
|
436
|
+
const stats = parseDiff(diffResult.stdout);
|
|
437
|
+
// Even if all files filtered, include the custom instructions
|
|
438
|
+
return (
|
|
439
|
+
buildReviewPrompt(
|
|
440
|
+
`Custom review: ${instructions.split("\n")[0].slice(0, 60)}...`,
|
|
441
|
+
stats,
|
|
442
|
+
diffResult.stdout,
|
|
443
|
+
) + `\n\n### Additional Instructions\n\n${instructions}`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// No diff available, just pass instructions
|
|
448
|
+
return `## Code Review Request
|
|
449
|
+
|
|
450
|
+
### Mode
|
|
451
|
+
Custom review instructions
|
|
452
|
+
|
|
453
|
+
### Instructions
|
|
454
|
+
|
|
455
|
+
${instructions}
|
|
101
456
|
|
|
102
|
-
|
|
457
|
+
Use the Task tool with \`agent: "reviewer"\` to execute this review.`;
|
|
103
458
|
}
|
|
104
459
|
|
|
105
460
|
default:
|
|
@@ -300,7 +300,7 @@ async function handleSpawn(args: SpawnArgs, ctx: HookCommandContext): Promise<st
|
|
|
300
300
|
async function handleParallel(args: ParallelTask[], ctx: HookCommandContext): Promise<string> {
|
|
301
301
|
validateDisjointScopes(args.map((t) => t.scope));
|
|
302
302
|
|
|
303
|
-
const sessionId = `parallel-${
|
|
303
|
+
const sessionId = `parallel-${nanoid()}`;
|
|
304
304
|
const agent = await pickAgent(ctx.cwd);
|
|
305
305
|
|
|
306
306
|
const worktrees: Array<{ task: ParallelTask; wt: worktree.Worktree; session: worktree.WorktreeSession }> = [];
|