diff --git a/apps/hook/README.md b/apps/hook/README.md index b585a96..96c747a 100644 --- a/apps/hook/README.md +++ b/apps/hook/README.md @@ -52,7 +52,7 @@ If you prefer not to use the plugin system, add this to your `~/.claude/settings { "type": "command", "command": "plannotator", - "timeout": 1800 + "timeout": 345600 } ] } @@ -133,4 +133,3 @@ tags: [plan, authentication, typescript, sql] ``` image - diff --git a/apps/marketing/src/content/docs/getting-started/configuration.md b/apps/marketing/src/content/docs/getting-started/configuration.md index b8dde9c..e46d911 100644 --- a/apps/marketing/src/content/docs/getting-started/configuration.md +++ b/apps/marketing/src/content/docs/getting-started/configuration.md @@ -35,7 +35,7 @@ The hook is defined in `hooks.json` inside the plugin directory. When installed { "type": "command", "command": "plannotator", - "timeout": 1800 + "timeout": 345600 } ] } @@ -44,7 +44,7 @@ The hook is defined in `hooks.json` inside the plugin directory. When installed } ``` -The `matcher` targets the `ExitPlanMode` tool specifically. The `timeout` is in seconds (30 minutes) — plan reviews can take a while. +The `matcher` targets the `ExitPlanMode` tool specifically. The `timeout` is in seconds (`345600` = 96 hours) — long reviews can stay open without expiring. ## Plugin configuration (OpenCode) diff --git a/apps/marketing/src/content/docs/getting-started/installation.md b/apps/marketing/src/content/docs/getting-started/installation.md index 37e5efe..080f75e 100644 --- a/apps/marketing/src/content/docs/getting-started/installation.md +++ b/apps/marketing/src/content/docs/getting-started/installation.md @@ -57,7 +57,7 @@ If you prefer not to use the plugin system, add this to your `~/.claude/settings { "type": "command", "command": "plannotator", - "timeout": 1800 + "timeout": 345600 } ] } diff --git a/apps/marketing/src/content/docs/reference/environment-variables.md b/apps/marketing/src/content/docs/reference/environment-variables.md index 4fc284e..98b747d 100644 --- a/apps/marketing/src/content/docs/reference/environment-variables.md +++ b/apps/marketing/src/content/docs/reference/environment-variables.md @@ -18,6 +18,7 @@ All Plannotator environment variables and their defaults. | `BROWSER` | (none) | Standard env var for specifying a browser. VS Code sets this automatically in devcontainers. Used as fallback when `PLANNOTATOR_BROWSER` is not set. | | `PLANNOTATOR_SHARE` | enabled | Set to `disabled` to turn off sharing. Hides share UI and import options. | | `PLANNOTATOR_SHARE_URL` | `https://share.plannotator.ai` | Base URL for share links. Set this when self-hosting the share portal. | +| `PLANNOTATOR_PLAN_TIMEOUT_SECONDS` | `345600` | OpenCode only. `submit_plan` wait timeout in seconds. Set `0` to disable timeout. | ## Paste service variables diff --git a/apps/opencode-plugin/README.md b/apps/opencode-plugin/README.md index 40aba0e..f85e536 100644 --- a/apps/opencode-plugin/README.md +++ b/apps/opencode-plugin/README.md @@ -59,6 +59,7 @@ Restart OpenCode. The `submit_plan` tool is now available. | `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. | | `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. | | `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. | +| `PLANNOTATOR_PLAN_TIMEOUT_SECONDS` | Timeout for `submit_plan` review wait. Default: `345600` (96h). Set `0` to disable timeout. | ## Devcontainer / Docker diff --git a/apps/opencode-plugin/index.ts b/apps/opencode-plugin/index.ts index e4c4bf4..0ac9a2d 100644 --- a/apps/opencode-plugin/index.ts +++ b/apps/opencode-plugin/index.ts @@ -8,6 +8,7 @@ * Environment variables: * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (devcontainer, SSH) * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) + * PLANNOTATOR_PLAN_TIMEOUT_SECONDS - Max wait for submit_plan approval (default: 345600, set 0 to disable) * * @packageDocumentation */ @@ -36,6 +37,8 @@ const htmlContent = indexHtml as unknown as string; import reviewHtml from "./review-editor.html" with { type: "text" }; const reviewHtmlContent = reviewHtml as unknown as string; +const DEFAULT_PLAN_TIMEOUT_SECONDS = 345_600; // 96 hours + export const PlannotatorPlugin: Plugin = async (ctx) => { // Helper to determine if sharing is enabled (lazy evaluation) // Priority: OpenCode config > env var > default (enabled) @@ -60,6 +63,28 @@ export const PlannotatorPlugin: Plugin = async (ctx) => { return process.env.PLANNOTATOR_SHARE_URL || undefined; } + /** + * submit_plan wait timeout in seconds. + * - unset: default to 96h + * - 0: disable timeout + * - invalid/negative: fall back to default + */ + function getPlanTimeoutSeconds(): number | null { + const raw = process.env.PLANNOTATOR_PLAN_TIMEOUT_SECONDS?.trim(); + if (!raw) return DEFAULT_PLAN_TIMEOUT_SECONDS; + + const parsed = Number.parseInt(raw, 10); + if (!Number.isFinite(parsed) || parsed < 0) { + console.error( + `[Plannotator] Invalid PLANNOTATOR_PLAN_TIMEOUT_SECONDS="${raw}". Using default ${DEFAULT_PLAN_TIMEOUT_SECONDS}s.` + ); + return DEFAULT_PLAN_TIMEOUT_SECONDS; + } + + if (parsed === 0) return null; + return parsed; + } + return { // Register submit_plan as primary-only tool (hidden from sub-agents) config: async (opencodeConfig) => { @@ -321,17 +346,26 @@ Do NOT proceed with implementation until your plan is approved. }, }); - const PLANNOTATOR_TIMEOUT_MS = 10 * 60 * 1000; // 10min timeout - let timeoutId: ReturnType; - const result = await Promise.race([ - server.waitForDecision().then((r) => { clearTimeout(timeoutId); return r; }), - new Promise<{ approved: boolean; feedback?: string }>((resolve) => { - timeoutId = setTimeout( - () => resolve({ approved: false, feedback: "[Plannotator] No response within 10 minutes. Port released automatically. Please call submit_plan again." }), - PLANNOTATOR_TIMEOUT_MS - ); - }), - ]); + const timeoutSeconds = getPlanTimeoutSeconds(); + const timeoutMs = timeoutSeconds === null ? null : timeoutSeconds * 1000; + + const result = timeoutMs === null + ? await server.waitForDecision() + : await new Promise>>((resolve) => { + const timeoutId = setTimeout( + () => + resolve({ + approved: false, + feedback: `[Plannotator] No response within ${timeoutSeconds} seconds. Port released automatically. Please call submit_plan again.`, + }), + timeoutMs + ); + + server.waitForDecision().then((r) => { + clearTimeout(timeoutId); + resolve(r); + }); + }); await Bun.sleep(1500); server.stop();