@gajae-code/coding-agent 0.6.3 → 0.6.5

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +73 -1
  3. package/dist/types/cli/migrate-cli.d.ts +20 -0
  4. package/dist/types/commands/migrate.d.ts +33 -0
  5. package/dist/types/config/keybindings.d.ts +4 -0
  6. package/dist/types/config/settings-schema.d.ts +27 -0
  7. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  8. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  9. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  10. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  11. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  12. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  14. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  19. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  20. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  21. package/dist/types/hooks/skill-state.d.ts +12 -4
  22. package/dist/types/migrate/action-planner.d.ts +11 -0
  23. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  24. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  25. package/dist/types/migrate/adapters/index.d.ts +45 -0
  26. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  27. package/dist/types/migrate/executor.d.ts +2 -0
  28. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  29. package/dist/types/migrate/report.d.ts +18 -0
  30. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  31. package/dist/types/migrate/types.d.ts +126 -0
  32. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  33. package/dist/types/modes/components/welcome.d.ts +3 -1
  34. package/dist/types/modes/interactive-mode.d.ts +3 -0
  35. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  37. package/dist/types/research-plan/index.d.ts +1 -0
  38. package/dist/types/research-plan/ledger.d.ts +33 -0
  39. package/dist/types/rlm/artifacts.d.ts +1 -1
  40. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  41. package/dist/types/skill-state/active-state.d.ts +6 -11
  42. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  43. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  44. package/dist/types/task/spawn-gate.d.ts +1 -10
  45. package/package.json +7 -7
  46. package/src/cli/migrate-cli.ts +106 -0
  47. package/src/cli/setup-cli.ts +14 -1
  48. package/src/cli.ts +1 -0
  49. package/src/commands/deep-interview.ts +2 -2
  50. package/src/commands/launch.ts +1 -1
  51. package/src/commands/migrate.ts +46 -0
  52. package/src/commands/state.ts +2 -1
  53. package/src/commands/team.ts +7 -3
  54. package/src/config/model-registry.ts +9 -2
  55. package/src/config/model-resolver.ts +13 -2
  56. package/src/config/settings-schema.ts +17 -0
  57. package/src/coordinator-mcp/policy.ts +10 -2
  58. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  61. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
  63. package/src/exec/bash-executor.ts +3 -1
  64. package/src/extensibility/custom-commands/loader.ts +0 -7
  65. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  66. package/src/extensibility/gjc-plugins/state.ts +16 -1
  67. package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
  68. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  69. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  70. package/src/gjc-runtime/launch-tmux.ts +68 -15
  71. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  72. package/src/gjc-runtime/session-layout.ts +180 -0
  73. package/src/gjc-runtime/session-resolution.ts +217 -0
  74. package/src/gjc-runtime/state-graph.ts +1 -2
  75. package/src/gjc-runtime/state-migrations.ts +1 -0
  76. package/src/gjc-runtime/state-runtime.ts +230 -121
  77. package/src/gjc-runtime/state-schema.ts +2 -0
  78. package/src/gjc-runtime/state-writer.ts +289 -41
  79. package/src/gjc-runtime/team-runtime.ts +43 -19
  80. package/src/gjc-runtime/tmux-sessions.ts +43 -2
  81. package/src/gjc-runtime/ultragoal-guard.ts +45 -2
  82. package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
  83. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  84. package/src/gjc-runtime/workflow-manifest.ts +1 -2
  85. package/src/harness-control-plane/storage.ts +14 -4
  86. package/src/hooks/native-skill-hook.ts +38 -12
  87. package/src/hooks/skill-state.ts +178 -83
  88. package/src/internal-urls/docs-index.generated.ts +9 -6
  89. package/src/migrate/action-planner.ts +318 -0
  90. package/src/migrate/adapters/claude-code.ts +39 -0
  91. package/src/migrate/adapters/codex.ts +70 -0
  92. package/src/migrate/adapters/index.ts +277 -0
  93. package/src/migrate/adapters/opencode.ts +52 -0
  94. package/src/migrate/executor.ts +81 -0
  95. package/src/migrate/mcp-mapper.ts +152 -0
  96. package/src/migrate/report.ts +104 -0
  97. package/src/migrate/skill-normalizer.ts +80 -0
  98. package/src/migrate/types.ts +163 -0
  99. package/src/modes/bridge/bridge-mode.ts +2 -2
  100. package/src/modes/components/custom-editor.ts +30 -20
  101. package/src/modes/components/welcome.ts +42 -9
  102. package/src/modes/controllers/input-controller.ts +21 -3
  103. package/src/modes/interactive-mode.ts +22 -1
  104. package/src/modes/prompt-action-autocomplete.ts +11 -1
  105. package/src/modes/rpc/rpc-mode.ts +2 -2
  106. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  107. package/src/prompts/agents/init.md +1 -1
  108. package/src/prompts/system/plan-mode-active.md +1 -1
  109. package/src/prompts/tools/ast-grep.md +1 -1
  110. package/src/prompts/tools/search.md +1 -1
  111. package/src/prompts/tools/task.md +1 -2
  112. package/src/research-plan/index.ts +1 -0
  113. package/src/research-plan/ledger.ts +177 -0
  114. package/src/rlm/artifacts.ts +12 -3
  115. package/src/rlm/index.ts +7 -0
  116. package/src/runtime-mcp/config-writer.ts +46 -0
  117. package/src/session/agent-session.ts +15 -21
  118. package/src/session/session-manager.ts +19 -2
  119. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  120. package/src/setup/hermes-setup.ts +1 -1
  121. package/src/skill-state/active-state.ts +72 -108
  122. package/src/skill-state/canonical-skills.ts +4 -0
  123. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  124. package/src/skill-state/workflow-hud.ts +4 -2
  125. package/src/skill-state/workflow-state-contract.ts +3 -3
  126. package/src/slash-commands/builtin-registry.ts +8 -4
  127. package/src/system-prompt.ts +11 -9
  128. package/src/task/agents.ts +1 -22
  129. package/src/task/index.ts +1 -41
  130. package/src/task/spawn-gate.ts +1 -38
  131. package/src/task/types.ts +1 -1
  132. package/src/tools/ask.ts +34 -12
  133. package/src/tools/computer.ts +58 -4
  134. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  135. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  136. package/src/prompts/agents/explore.md +0 -58
  137. package/src/prompts/agents/plan.md +0 -49
  138. package/src/prompts/agents/reviewer.md +0 -141
  139. package/src/prompts/agents/task.md +0 -16
  140. package/src/prompts/review-request.md +0 -70
@@ -6,6 +6,7 @@ import type { ImageContent } from "@gajae-code/ai";
6
6
  import { prompt } from "@gajae-code/utils";
7
7
  import * as z from "zod/v4";
8
8
  import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
9
+ import { resizeImage } from "../utils/image-resize";
9
10
  import type { ToolSession } from "./index";
10
11
  import type { OutputMeta } from "./output-meta";
11
12
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
@@ -174,6 +175,11 @@ let platformOverrideForTests: NodeJS.Platform | undefined;
174
175
  let archOverrideForTests: NodeJS.Architecture | undefined;
175
176
  const screenshotFallbackDirs = new WeakMap<ToolSession, Promise<string>>();
176
177
 
178
+ const COMPUTER_INLINE_SCREENSHOT_MAX_WIDTH = 1568;
179
+ const COMPUTER_INLINE_SCREENSHOT_MAX_HEIGHT = 1568;
180
+ const COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES = 5 * 1024 * 1024;
181
+ const COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY = 70;
182
+
177
183
  export function setComputerControllerFactoryForTests(factory: ComputerControllerFactory | undefined): void {
178
184
  controllerFactory = factory ?? createNativeComputerController;
179
185
  }
@@ -278,6 +284,9 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
278
284
  if (batchResult.failedStep) {
279
285
  details.code = batchResult.failedStep.code;
280
286
  details.message = batchResult.failedStep.message;
287
+ if (batchResult.screenshotSource !== undefined) {
288
+ await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
289
+ }
281
290
  await writeComputerAuditLog(this.session, details);
282
291
  return {
283
292
  ...toolResult(details).text(`${details.code}: ${details.message}`).done(),
@@ -285,11 +294,11 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
285
294
  };
286
295
  }
287
296
  details.message = describeComputerSuccess(details);
288
- const image = imageContentFromNativeResult(batchResult.screenshotSource);
289
297
  if (batchResult.screenshotSource !== undefined) {
290
298
  await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
291
299
  details.message = describeComputerSuccess(details);
292
300
  }
301
+ const image = await inlineImageContentFromNativeResult(batchResult.screenshotSource, details, this.session);
293
302
  await writeComputerAuditLog(this.session, details);
294
303
  return image
295
304
  ? toolResult(details)
@@ -302,11 +311,11 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
302
311
  if (screenshot) details.screenshot = screenshot;
303
312
  details.status = "success";
304
313
  details.message = describeComputerSuccess(details);
305
- const image = imageContentFromNativeResult(result);
306
314
  if (screenshot) {
307
315
  await persistScreenshotFallback(result, details.screenshot, this.session);
308
316
  details.message = describeComputerSuccess(details);
309
317
  }
318
+ const image = await inlineImageContentFromNativeResult(result, details, this.session);
310
319
  await writeComputerAuditLog(this.session, details);
311
320
  return image
312
321
  ? toolResult(details)
@@ -472,7 +481,7 @@ function normalizeScreenshot(value: unknown): ComputerScreenshotDetails | undefi
472
481
  };
473
482
  }
474
483
 
475
- function imageContentFromNativeResult(value: unknown): ImageContent | undefined {
484
+ function fullResolutionImageContentFromNativeResult(value: unknown): ImageContent | undefined {
476
485
  const candidate =
477
486
  value && typeof value === "object" && "screenshot" in value
478
487
  ? (value as { screenshot?: unknown }).screenshot
@@ -483,13 +492,42 @@ function imageContentFromNativeResult(value: unknown): ImageContent | undefined
483
492
  return data ? { type: "image", data, mimeType: "image/png" } : undefined;
484
493
  }
485
494
 
495
+ async function inlineImageContentFromNativeResult(
496
+ value: unknown,
497
+ details: ComputerToolDetails,
498
+ session: ToolSession,
499
+ ): Promise<ImageContent | undefined> {
500
+ const image = fullResolutionImageContentFromNativeResult(value);
501
+ if (!image) return undefined;
502
+ const maxBytes = getInlineScreenshotMaxBytes(session);
503
+ const originalBytes = Buffer.byteLength(image.data, "base64");
504
+ if (originalBytes <= maxBytes) return image;
505
+
506
+ try {
507
+ const resized = await resizeImage(image, {
508
+ maxWidth: COMPUTER_INLINE_SCREENSHOT_MAX_WIDTH,
509
+ maxHeight: COMPUTER_INLINE_SCREENSHOT_MAX_HEIGHT,
510
+ maxBytes,
511
+ jpegQuality: COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY,
512
+ });
513
+ if (resized.buffer.length <= maxBytes) {
514
+ return { type: "image", data: resized.data, mimeType: resized.mimeType };
515
+ }
516
+ } catch {
517
+ // Keep the action successful and rely on the full-resolution artifact path below.
518
+ }
519
+
520
+ details.message = `${details.message} Inline screenshot omitted because it could not be bounded below ${formatByteCount(maxBytes)}; use the saved screenshot artifact instead.`;
521
+ return undefined;
522
+ }
523
+
486
524
  async function persistScreenshotFallback(
487
525
  value: unknown,
488
526
  screenshot: ComputerScreenshotDetails | undefined,
489
527
  session: ToolSession,
490
528
  ): Promise<void> {
491
529
  if (!screenshot || screenshot.path) return;
492
- const image = imageContentFromNativeResult(value);
530
+ const image = fullResolutionImageContentFromNativeResult(value);
493
531
  if (!image) return;
494
532
  const dir = await getScreenshotFallbackDir(session);
495
533
  const filePath = path.join(dir, `computer-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
@@ -526,6 +564,22 @@ function getPngByteLength(png: NativeScreenshot["png"]): number | undefined {
526
564
  return png.byteLength;
527
565
  }
528
566
 
567
+ function getInlineScreenshotMaxBytes(session: Pick<ToolSession, "settings">): number {
568
+ const configured = Number(session.settings.get("computer.screenshotMaxBytes"));
569
+ const finiteConfigured =
570
+ Number.isFinite(configured) && configured > 0
571
+ ? Math.floor(configured)
572
+ : COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES;
573
+ return Math.min(finiteConfigured, COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES);
574
+ }
575
+
576
+ function formatByteCount(bytes: number): string {
577
+ if (bytes < 1024) return `${bytes} bytes`;
578
+ const kib = bytes / 1024;
579
+ if (kib < 1024) return `${Math.round(kib)} KiB`;
580
+ return `${(kib / 1024).toFixed(1)} MiB`;
581
+ }
582
+
529
583
  function mapComputerError(error: unknown, hotkey?: string): { code: string; message: string } {
530
584
  if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
531
585
  return {
@@ -1,10 +0,0 @@
1
- import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
2
- import type { HookCommandContext } from "../../../../extensibility/hooks/types";
3
- export declare class ReviewCommand implements CustomCommand {
4
- private api;
5
- name: string;
6
- description: string;
7
- constructor(api: CustomCommandAPI);
8
- execute(args: string[], ctx: HookCommandContext): Promise<string | undefined>;
9
- }
10
- export default ReviewCommand;
@@ -1,456 +0,0 @@
1
- /**
2
- * /review command - Interactive code review launcher
3
- *
4
- * Provides a menu to select review mode:
5
- * 1. Review against a base branch (PR style)
6
- * 2. Review uncommitted changes
7
- * 3. Review a specific commit
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.
13
- */
14
- import { prompt } from "@gajae-code/utils";
15
- import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
16
- import type { HookCommandContext } from "../../../../extensibility/hooks/types";
17
- import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
18
- import * as git from "../../../../utils/git";
19
-
20
- // ─────────────────────────────────────────────────────────────────────────────
21
- // Types
22
- // ─────────────────────────────────────────────────────────────────────────────
23
-
24
- interface FileDiff {
25
- path: string;
26
- linesAdded: number;
27
- linesRemoved: number;
28
- hunks: string;
29
- }
30
-
31
- interface DiffStats {
32
- files: FileDiff[];
33
- totalAdded: number;
34
- totalRemoved: number;
35
- excluded: { path: string; reason: string; linesAdded: number; linesRemoved: number }[];
36
- }
37
-
38
- // ─────────────────────────────────────────────────────────────────────────────
39
- // Exclusion patterns for noise files
40
- // ─────────────────────────────────────────────────────────────────────────────
41
-
42
- const EXCLUDED_PATTERNS: { pattern: RegExp; reason: string }[] = [
43
- // Lock files
44
- { pattern: /\.lock$/, reason: "lock file" },
45
- { pattern: /-lock\.(json|yaml|yml)$/, reason: "lock file" },
46
- { pattern: /package-lock\.json$/, reason: "lock file" },
47
- { pattern: /yarn\.lock$/, reason: "lock file" },
48
- { pattern: /pnpm-lock\.yaml$/, reason: "lock file" },
49
- { pattern: /Cargo\.lock$/, reason: "lock file" },
50
- { pattern: /Gemfile\.lock$/, reason: "lock file" },
51
- { pattern: /poetry\.lock$/, reason: "lock file" },
52
- { pattern: /composer\.lock$/, reason: "lock file" },
53
- { pattern: /flake\.lock$/, reason: "lock file" },
54
-
55
- // Generated/build artifacts
56
- { pattern: /\.min\.(js|css)$/, reason: "minified" },
57
- { pattern: /\.generated\./, reason: "generated" },
58
- { pattern: /\.snap$/, reason: "snapshot" },
59
- { pattern: /\.map$/, reason: "source map" },
60
- { pattern: /^dist\//, reason: "build output" },
61
- { pattern: /^build\//, reason: "build output" },
62
- { pattern: /^out\//, reason: "build output" },
63
- { pattern: /node_modules\//, reason: "vendor" },
64
- { pattern: /vendor\//, reason: "vendor" },
65
-
66
- // Binary/assets (usually shown as binary in diff anyway)
67
- { pattern: /\.(png|jpg|jpeg|gif|ico|webp|avif)$/i, reason: "image" },
68
- { pattern: /\.(woff|woff2|ttf|eot|otf)$/i, reason: "font" },
69
- { pattern: /\.(pdf|zip|tar|gz|rar|7z)$/i, reason: "binary" },
70
- ];
71
-
72
- // ─────────────────────────────────────────────────────────────────────────────
73
- // Diff parsing
74
- // ─────────────────────────────────────────────────────────────────────────────
75
-
76
- /**
77
- * Check if a file path should be excluded from review.
78
- * Returns the exclusion reason if excluded, undefined otherwise.
79
- */
80
- function getExclusionReason(path: string): string | undefined {
81
- for (const { pattern, reason } of EXCLUDED_PATTERNS) {
82
- if (pattern.test(path)) return reason;
83
- }
84
- return undefined;
85
- }
86
-
87
- /**
88
- * Parse unified diff output into per-file stats.
89
- * Splits on file boundaries, counts +/- lines, and filters excluded files.
90
- */
91
- function parseDiff(diffOutput: string): DiffStats {
92
- const files: FileDiff[] = [];
93
- const excluded: DiffStats["excluded"] = [];
94
- let totalAdded = 0;
95
- let totalRemoved = 0;
96
-
97
- // Split by file boundary: "diff --git a/... b/..."
98
- const fileChunks = diffOutput.split(/^diff --git /m).filter(Boolean);
99
-
100
- for (const chunk of fileChunks) {
101
- // Extract file path from "a/path b/path" line
102
- const headerMatch = chunk.match(/^a\/(.+?) b\/(.+)/);
103
- if (!headerMatch) continue;
104
-
105
- const path = headerMatch[2];
106
-
107
- // Count added/removed lines (lines starting with + or - but not ++ or --)
108
- let linesAdded = 0;
109
- let linesRemoved = 0;
110
-
111
- const lines = chunk.split("\n");
112
- for (const line of lines) {
113
- if (line.startsWith("+") && !line.startsWith("+++")) {
114
- linesAdded++;
115
- } else if (line.startsWith("-") && !line.startsWith("---")) {
116
- linesRemoved++;
117
- }
118
- }
119
-
120
- const exclusionReason = getExclusionReason(path);
121
- if (exclusionReason) {
122
- excluded.push({ path, reason: exclusionReason, linesAdded, linesRemoved });
123
- } else {
124
- files.push({
125
- path,
126
- linesAdded,
127
- linesRemoved,
128
- hunks: `diff --git ${chunk}`,
129
- });
130
- totalAdded += linesAdded;
131
- totalRemoved += linesRemoved;
132
- }
133
- }
134
-
135
- return { files, totalAdded, totalRemoved, excluded };
136
- }
137
-
138
- /**
139
- * Get file extension for display purposes.
140
- */
141
- function getFileExt(path: string): string {
142
- const match = path.match(/\.([^.]+)$/);
143
- return match ? match[1] : "";
144
- }
145
-
146
- /**
147
- * Determine recommended number of reviewer agents based on diff weight.
148
- * Uses total lines changed as the primary metric.
149
- */
150
- function getRecommendedAgentCount(stats: DiffStats): number {
151
- const totalLines = stats.totalAdded + stats.totalRemoved;
152
- const fileCount = stats.files.length;
153
-
154
- // Heuristics:
155
- // - Tiny (<100 lines or 1-2 files): 1 agent
156
- // - Small (<500 lines): 1-2 agents
157
- // - Medium (<2000 lines): 2-4 agents
158
- // - Large (<5000 lines): 4-8 agents
159
- // - Huge (>5000 lines): 8-16 agents
160
-
161
- if (totalLines < 100 || fileCount <= 2) return 1;
162
- if (totalLines < 500) return Math.min(2, fileCount);
163
- if (totalLines < 2000) return Math.min(4, Math.ceil(fileCount / 3));
164
- if (totalLines < 5000) return Math.min(8, Math.ceil(fileCount / 2));
165
- return Math.min(16, fileCount);
166
- }
167
-
168
- /**
169
- * Extract first N lines of actual diff content (excluding headers) for preview.
170
- */
171
- function getDiffPreview(hunks: string, maxLines: number): string {
172
- const lines = hunks.split("\n");
173
- const contentLines: string[] = [];
174
-
175
- for (const line of lines) {
176
- // Skip diff headers, keep actual content
177
- if (
178
- line.startsWith("diff --git") ||
179
- line.startsWith("index ") ||
180
- line.startsWith("---") ||
181
- line.startsWith("+++") ||
182
- line.startsWith("@@")
183
- ) {
184
- continue;
185
- }
186
- contentLines.push(line);
187
- if (contentLines.length >= maxLines) break;
188
- }
189
-
190
- return contentLines.join("\n");
191
- }
192
-
193
- // Thresholds for diff inclusion
194
- const MAX_DIFF_CHARS = 50_000; // Don't include diff above this
195
- const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than this
196
-
197
- /**
198
- * Build the full review prompt with diff stats and distribution guidance.
199
- */
200
- function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, additionalInstructions?: string): string {
201
- const agentCount = getRecommendedAgentCount(stats);
202
- const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
203
- const totalLines = stats.totalAdded + stats.totalRemoved;
204
- const linesPerFile = skipDiff ? Math.max(5, Math.floor(100 / stats.files.length)) : 0;
205
-
206
- const filesWithExt = stats.files.map(f => ({
207
- ...f,
208
- ext: getFileExt(f.path),
209
- hunksPreview: skipDiff ? getDiffPreview(f.hunks, linesPerFile) : "",
210
- }));
211
-
212
- return prompt.render(reviewRequestTemplate, {
213
- mode,
214
- files: filesWithExt,
215
- excluded: stats.excluded,
216
- totalAdded: stats.totalAdded,
217
- totalRemoved: stats.totalRemoved,
218
- totalLines,
219
- agentCount,
220
- multiAgent: agentCount > 1,
221
- skipDiff,
222
- rawDiff: rawDiff.trim(),
223
- linesPerFile,
224
- additionalInstructions,
225
- });
226
- }
227
-
228
- export class ReviewCommand implements CustomCommand {
229
- name = "review";
230
- description = "Launch interactive code review";
231
-
232
- constructor(private api: CustomCommandAPI) {}
233
-
234
- async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
235
- if (!ctx.hasUI) {
236
- const base = "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
237
- return args.length > 0 ? `${base} Focus: ${args.join(" ")}` : base;
238
- }
239
-
240
- // Inline args act as additional instructions appended to the generated prompt.
241
- // When present, skip option 4 (editor) — the args already provide the instructions.
242
- const extraInstructions = args.length > 0 ? args.join(" ") : undefined;
243
-
244
- const menuItems = extraInstructions
245
- ? [
246
- "1. Review against a base branch (PR Style)",
247
- "2. Review uncommitted changes",
248
- "3. Review a specific commit",
249
- ]
250
- : [
251
- "1. Review against a base branch (PR Style)",
252
- "2. Review uncommitted changes",
253
- "3. Review a specific commit",
254
- "4. Custom review instructions",
255
- ];
256
-
257
- const mode = await ctx.ui.select("Review Mode", menuItems);
258
-
259
- if (!mode) return undefined;
260
-
261
- const modeNum = parseInt(mode[0], 10);
262
-
263
- switch (modeNum) {
264
- case 1: {
265
- // PR-style review against base branch
266
- const branches = await getGitBranches(this.api);
267
- if (branches.length === 0) {
268
- ctx.ui.notify("No git branches found", "error");
269
- return undefined;
270
- }
271
-
272
- const baseBranch = await ctx.ui.select("Select base branch to compare against", branches);
273
- if (!baseBranch) return undefined;
274
-
275
- const currentBranch = await getCurrentBranch(this.api);
276
- let diffText: string;
277
- try {
278
- diffText = await git.diff(this.api.cwd, { base: `${baseBranch}...${currentBranch}` });
279
- } catch (err) {
280
- ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
281
- return undefined;
282
- }
283
-
284
- if (!diffText.trim()) {
285
- ctx.ui.notify(`No changes between ${baseBranch} and ${currentBranch}`, "warning");
286
- return undefined;
287
- }
288
-
289
- const stats = parseDiff(diffText);
290
- if (stats.files.length === 0) {
291
- ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
292
- return undefined;
293
- }
294
-
295
- return buildReviewPrompt(
296
- `Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
297
- stats,
298
- diffText,
299
- extraInstructions,
300
- );
301
- }
302
-
303
- case 2: {
304
- // Uncommitted changes - combine staged and unstaged
305
- const status = await getGitStatus(this.api);
306
- if (!status.trim()) {
307
- ctx.ui.notify("No uncommitted changes found", "warning");
308
- return undefined;
309
- }
310
-
311
- let unstagedDiff: string;
312
- let stagedDiff: string;
313
- try {
314
- [unstagedDiff, stagedDiff] = await Promise.all([
315
- git.diff(this.api.cwd),
316
- git.diff(this.api.cwd, { cached: true }),
317
- ]);
318
- } catch (err) {
319
- ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
320
- return undefined;
321
- }
322
-
323
- const combinedDiff = [unstagedDiff, stagedDiff].filter(Boolean).join("\n");
324
-
325
- if (!combinedDiff.trim()) {
326
- ctx.ui.notify("No diff content found", "warning");
327
- return undefined;
328
- }
329
-
330
- const stats = parseDiff(combinedDiff);
331
- if (stats.files.length === 0) {
332
- ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
333
- return undefined;
334
- }
335
-
336
- return buildReviewPrompt(
337
- "Reviewing uncommitted changes (staged + unstaged)",
338
- stats,
339
- combinedDiff,
340
- extraInstructions,
341
- );
342
- }
343
-
344
- case 3: {
345
- // Specific commit
346
- const commits = await getRecentCommits(this.api, 20);
347
- if (commits.length === 0) {
348
- ctx.ui.notify("No commits found", "error");
349
- return undefined;
350
- }
351
-
352
- const selected = await ctx.ui.select("Select commit to review", commits);
353
- if (!selected) return undefined;
354
-
355
- // Extract commit hash from selection (format: "abc1234 message")
356
- const hash = selected.split(" ")[0];
357
-
358
- let diffText: string;
359
- try {
360
- diffText = await git.show(this.api.cwd, hash, { format: "" });
361
- } catch (err) {
362
- ctx.ui.notify(`Failed to get commit: ${err instanceof Error ? err.message : String(err)}`, "error");
363
- return undefined;
364
- }
365
-
366
- if (!diffText.trim()) {
367
- ctx.ui.notify("Commit has no diff content", "warning");
368
- return undefined;
369
- }
370
-
371
- const stats = parseDiff(diffText);
372
- if (stats.files.length === 0) {
373
- ctx.ui.notify("No reviewable files in commit (all changes filtered out)", "warning");
374
- return undefined;
375
- }
376
-
377
- return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText, extraInstructions);
378
- }
379
-
380
- case 4: {
381
- // Custom instructions - still uses the old approach since user provides context
382
- const instructions = await ctx.ui.editor("Enter custom review instructions", "Review the following:\n\n");
383
- if (!instructions?.trim()) return undefined;
384
-
385
- // For custom, we still try to get current diff for context
386
- let diffText: string | undefined;
387
- try {
388
- diffText = await git.diff(this.api.cwd, { base: "HEAD" });
389
- } catch {
390
- diffText = undefined;
391
- }
392
- const reviewDiff = diffText?.trim();
393
-
394
- if (reviewDiff) {
395
- const stats = parseDiff(reviewDiff);
396
- // Even if all files filtered, include the custom instructions
397
- return buildReviewPrompt(
398
- `Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
399
- stats,
400
- reviewDiff,
401
- instructions,
402
- );
403
- }
404
-
405
- // No diff available, just pass instructions
406
- return `## Code Review Request
407
-
408
- ### Mode
409
- Custom review instructions
410
-
411
- ### Instructions
412
-
413
- ${instructions}
414
-
415
- Use the Task tool with \`agent: "reviewer"\` to execute this review.`;
416
- }
417
-
418
- default:
419
- return undefined;
420
- }
421
- }
422
- }
423
-
424
- async function getGitBranches(api: CustomCommandAPI): Promise<string[]> {
425
- try {
426
- return await git.branch.list(api.cwd, { all: true });
427
- } catch {
428
- return [];
429
- }
430
- }
431
-
432
- async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
433
- try {
434
- return (await git.branch.current(api.cwd)) ?? "HEAD";
435
- } catch {
436
- return "HEAD";
437
- }
438
- }
439
-
440
- async function getGitStatus(api: CustomCommandAPI): Promise<string> {
441
- try {
442
- return await git.status(api.cwd);
443
- } catch {
444
- return "";
445
- }
446
- }
447
-
448
- async function getRecentCommits(api: CustomCommandAPI, count: number): Promise<string[]> {
449
- try {
450
- return await git.log.onelines(api.cwd, count);
451
- } catch {
452
- return [];
453
- }
454
- }
455
-
456
- export default ReviewCommand;
@@ -1,58 +0,0 @@
1
- ---
2
- name: explore
3
- description: Fast read-only codebase scout returning compressed context for handoff
4
- tools: read, search, find, web_search
5
- model: pi/default
6
- thinking-level: med
7
- output:
8
- properties:
9
- summary:
10
- metadata:
11
- description: Brief summary of findings and conclusions
12
- type: string
13
- files:
14
- metadata:
15
- description: Files examined with relevant code references
16
- elements:
17
- properties:
18
- path:
19
- metadata:
20
- description: Project-relative path or paths to the most relevant code reference(s), optionally suffixed with line ranges like `:12-34` when relevant
21
- type: string
22
- description:
23
- metadata:
24
- description: Section contents
25
- type: string
26
- architecture:
27
- metadata:
28
- description: Brief explanation of how pieces connect
29
- type: string
30
- hide: true
31
- ---
32
-
33
- Investigate the codebase rapidly. Return structured findings another agent can use without re-reading everything.
34
-
35
- <directives>
36
- - You MUST use tools for broad pattern matching / code search as much as possible.
37
- - You SHOULD invoke tools in parallel—this is a short investigation, and you are supposed to finish in a few seconds.
38
- - If a search returns empty results, you MUST try at least one alternate strategy (different pattern, broader path, or AST search) before concluding the target doesn't exist.
39
- </directives>
40
-
41
- <thoroughness>
42
- You MUST infer the thoroughness from the task; default to medium:
43
- - **Quick**: Targeted lookups, key files only
44
- - **Medium**: Follow imports, read critical sections
45
- - **Thorough**: Trace all dependencies, check tests/types.
46
- </thoroughness>
47
-
48
- <procedure>
49
- 1. Locate relevant code using tools.
50
- 2. Read key sections (You NEVER read full files unless they're tiny)
51
- 3. Identify types/interfaces/key functions.
52
- 4. Note dependencies between files.
53
- </procedure>
54
-
55
- <critical>
56
- You MUST operate as read-only. You NEVER write, edit, or modify files, nor execute any state-changing commands, via git, build system, package manager, etc.
57
- You MUST keep going until complete.
58
- </critical>