From 7e64ab6f7c4c6b55a994643ec17afb369756d81f Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Wed, 15 Apr 2026 22:32:23 +0200 Subject: [PATCH 1/5] chore: add QA workflow and stabilize release gates --- .github/agents/changelog-drafter.agent.md | 54 +++++++++++++++++++ .github/instructions/testing.instructions.md | 55 ++++++++++++++++++++ .gitignore | 2 + package.json | 5 +- scripts/pre-commit-gates.mjs | 43 +++++++++++++++ src/cli.ts | 5 +- vitest.config.ts | 3 +- 7 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 .github/agents/changelog-drafter.agent.md create mode 100644 .github/instructions/testing.instructions.md create mode 100644 scripts/pre-commit-gates.mjs diff --git a/.github/agents/changelog-drafter.agent.md b/.github/agents/changelog-drafter.agent.md new file mode 100644 index 0000000..9e61825 --- /dev/null +++ b/.github/agents/changelog-drafter.agent.md @@ -0,0 +1,54 @@ +--- +description: "Use when: drafting release notes, generating changelog entries, summarizing commits for a release, writing CHANGELOG.md updates, preparing version bump notes. Generates structured release notes from conventional commits." +tools: [read, search, execute] +argument-hint: "Describe the version or commit range (e.g. 'draft notes for v1.2.0' or 'changelog since v1.1.4')" +--- + +You are a **Changelog Drafter** — you generate precise, well-structured release notes from conventional commits. You read git history, categorize changes, and produce a changelog entry that matches the project's existing format. + +## Approach + +1. **Detect the changelog format**: Read the existing `CHANGELOG.md` to understand the project's style — heading levels, categories, date format, linking conventions, and tone. +2. **Determine the commit range**: + - If the user specifies a version or tag range, use it: `git log v1.1.4..HEAD --oneline` + - If no range is given, find the latest tag (`git describe --tags --abbrev=0`) and log from there to HEAD + - Always use `--no-merges` to skip merge commits unless they carry meaningful info +3. **Categorize commits** by conventional commit prefix: + - `feat:` → **Added** + - `fix:` → **Fixed** + - `docs:` → **Documentation** + - `test:` → **Tests** + - `refactor:` → **Changed** + - `perf:` → **Performance** + - `chore:` / `ci:` / `build:` → **Maintenance** (collapse into one section) + - `BREAKING CHANGE` or `!` suffix → **Breaking Changes** (always listed first) +4. **Enrich entries**: + - Cross-reference commit messages with actual file changes (`git diff --stat`) to add context + - If a commit references an issue or PR (`#123`, `PR #45`), include the reference + - Write entries in past tense, user-facing language — explain *what changed* for the user, not implementation details + - Group related commits into single entries when they address the same feature/fix +5. **Determine the version number**: + - If the user specifies it, use that + - Otherwise, suggest based on changes: breaking → major bump, feat → minor, fix-only → patch +6. **Output the entry** in the exact format of the existing changelog + +## Constraints + +- DO NOT modify `CHANGELOG.md` directly — output the draft for the user to review and approve +- DO NOT invent changes that aren't in the commit log +- DO NOT include internal refactors in user-facing sections unless they affect behavior +- DO NOT include commit hashes in the changelog entries (unless the project's existing format does) +- ONLY use information from git history and file diffs — do not guess + +## Output Format + +Output the changelog entry as a markdown block ready to paste, preceded by a brief summary: + +``` +Version: {version} +Type: {major|minor|patch} +Commits analyzed: {count} +Range: {from_ref}..{to_ref} +``` + +Then the formatted entry matching the project's changelog style. diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md new file mode 100644 index 0000000..e434fa5 --- /dev/null +++ b/.github/instructions/testing.instructions.md @@ -0,0 +1,55 @@ +--- +description: "Use when: writing tests, modifying test files, adding test cases, reviewing test coverage. Covers Vitest conventions, harness patterns, and quality requirements for MCP Comet." +applyTo: "tests/**" +--- + +# Testing Conventions + +## General Rules + +- NEVER use `.only`, `.skip`, or `.todo` on tests — all tests must run in CI +- NEVER use `any` type assertions in tests — use proper typing or `as unknown as Type` +- Import from `vitest` explicitly: `describe`, `it`, `expect`, `vi`, `beforeEach`, `beforeAll`, `afterEach` +- Use `.js` extension in all import paths (TypeScript ESM requirement) +- Reset mocks/singletons in `beforeEach` to prevent test coupling +- Test names should describe behavior: `it('returns error when connection is refused')` + +## Directory Structure + +- `tests/unit/` — Isolated component tests with mocked dependencies. Mirror `src/` folder structure. +- `tests/integration/tools/` — End-to-end tool handler tests using the shared harness. +- UI script tests validate that `build*Script()` returns valid JS strings — no browser needed. + +## Unit Test Patterns + +- Mock external modules with `vi.mock()` at the top of the file +- CDPClient tests: mock `chrome-remote-interface` and `globalThis.fetch` +- Always call `CDPClient.resetInstance()` in `beforeEach` to clear singleton state +- Use `vi.fn()` for mock functions, assert with `toHaveBeenCalled()` / `toHaveBeenCalledWith()` + +## Integration Test Harness + +Always use the shared harness from `tests/integration/tools/harness.ts`: + +```ts +import { getHandler, mocks, registerHandlers, resetHarness } from './harness.js' + +beforeAll(async () => { await registerHandlers() }) +beforeEach(() => { resetHarness() }) +``` + +- Override `mocks.*` per-test to control CDPClient behavior +- Call `getHandler('tool_name')` to get the registered handler function +- DO NOT create alternative harness setups — extend the existing one + +## Coverage Requirements + +Thresholds (enforced in `vitest.config.ts`): +- Statements: 75%, Lines: 75%, Branches: 70%, Functions: 80% + +When adding new source code, add corresponding tests that maintain these thresholds. + +## Console Output + +- Use `// biome-ignore lint/suspicious/noConsole:` with a justification when `console.*` is intentionally used in test utilities +- Prefer `vi.spyOn(console, 'error')` to assert on console output without polluting test logs diff --git a/.gitignore b/.gitignore index c4432f8..2d3056b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ mcp-comet.config.json .mcp.json .claude/settings.local.json .claude/plans +.claude/scheduled_tasks.lock +.claude/worktrees/ tools/ # Internal Documentation & Plans (local only) diff --git a/package.json b/package.json index de740d8..9eb04d1 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,10 @@ "test:ci": "vitest run --coverage", "test:watch": "vitest", "lint": "biome check .", - "format": "biome write .", - "format:check": "biome check --formatter-enabled .", + "format": "biome format --write .", + "format:check": "biome check --formatter-enabled=true .", "typecheck": "tsc --noEmit", + "precommit": "node scripts/pre-commit-gates.mjs", "prepublishOnly": "npm run build && npm test" }, "engines": { diff --git a/scripts/pre-commit-gates.mjs b/scripts/pre-commit-gates.mjs new file mode 100644 index 0000000..80b94f9 --- /dev/null +++ b/scripts/pre-commit-gates.mjs @@ -0,0 +1,43 @@ +/** + * Pre-commit quality gates (Gates 1–3 from the review-qa agent). + * Runs: build → lint + typecheck → test suite. + * Exits with code 2 (blocking) on any failure, 0 on success. + * + * Invoked by the Copilot hook system (.github/hooks/pre-commit-gates.json) + * and can also be run manually: node scripts/pre-commit-gates.mjs + */ + +import { execSync } from 'node:child_process' + +const gates = [ + { name: 'Gate 1: Build', cmd: 'npm run build' }, + { name: 'Gate 2a: Lint', cmd: 'npm run lint' }, + { name: 'Gate 2b: Typecheck', cmd: 'npm run typecheck' }, + { name: 'Gate 3: Tests', cmd: 'npm test' }, +] + +let failed = false + +for (const gate of gates) { + process.stderr.write(`\n▶ ${gate.name}...\n`) + try { + execSync(gate.cmd, { stdio: 'inherit', timeout: 90_000 }) + process.stderr.write(`✅ ${gate.name} passed\n`) + } catch { + process.stderr.write(`❌ ${gate.name} FAILED\n`) + failed = true + break // fail fast — don't run remaining gates + } +} + +if (failed) { + const output = JSON.stringify({ + decision: 'block', + stopReason: 'Pre-commit quality gates failed. Fix the issues before committing.', + }) + process.stdout.write(output) + process.exit(2) +} + +process.stderr.write('\n✅ All pre-commit gates passed.\n') +process.exit(0) diff --git a/src/cli.ts b/src/cli.ts index 183ae58..6a68bd8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -220,20 +220,21 @@ async function runCall(args: string[]): Promise { try { child.stdin.end() } catch {} - setTimeout(() => process.exit(code), 300) + process.exit(code) } // Start handshake child.stdin.write(`${initMsg}\n`) // Timeout after 3 minutes - setTimeout(() => { + const responseTimeout = setTimeout(() => { if (!responded) { // biome-ignore lint/suspicious/noConsole: CLI output console.error('Timeout: no response from server (180s)') shutdown(1) } }, 180000) + responseTimeout.unref?.() } async function main(): Promise { diff --git a/vitest.config.ts b/vitest.config.ts index 46a37e0..4869109 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,13 +8,14 @@ export default defineConfig({ provider: 'v8', reporter: ['text', 'lcov', 'html'], reportsDirectory: 'coverage', + include: ['src/**/*.ts'], thresholds: { statements: 75, branches: 70, functions: 80, lines: 75, }, - exclude: ['tests/**', 'dist/**', 'node_modules/**', 'src/selectors/types.ts'], + exclude: ['src/index.ts', 'src/selectors/types.ts'], }, }, }) From 2288a9e3baa73983113245faac67bf990c6091b4 Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Wed, 15 Apr 2026 22:58:58 +0200 Subject: [PATCH 2/5] docs: add changelog for v1.1.4 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cba7c6..2f2c92c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.1.4] - 2026-04-13 + +### Documentation + +- **Product Documentation:** Refreshed README with improved product messaging and updated documentation files with clearer examples and consistent naming conventions. +- **Cleanup:** Removed obsolete UAT planning artifacts. + +### Changed + +- **Project Naming:** Standardized mcp-comet naming conventions across core server, CLI, logging, and test suite for consistency. +- **Configuration:** Aligned configuration file naming and maintenance scripts with project standards. + +### Maintenance + +- **Agent Workspace Integration:** Added shared agent workspace metadata and IDE configuration files for enhanced development experience. + ## [1.1.2] - 2026-04-11 ### Fixed @@ -79,3 +95,4 @@ TypeScript, Vitest, Chrome DevTools Protocol, Biome. 295 tests, 30 files. - Tab management with categorization - CLI with --help, --version, detect, snapshot commands - Comet version auto-detection and selector registry + From e945ca6872806d3fff9203a66eed0733a92d9b03 Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Wed, 15 Apr 2026 23:42:24 +0200 Subject: [PATCH 3/5] fix(uat): align smoke checks with ask/wait behavior --- scripts/run-uat.ts | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/scripts/run-uat.ts b/scripts/run-uat.ts index 78e67d6..6af0360 100644 --- a/scripts/run-uat.ts +++ b/scripts/run-uat.ts @@ -1,4 +1,5 @@ -import { writeFileSync } from 'node:fs' +import { mkdirSync, writeFileSync } from 'node:fs' +import { dirname } from 'node:path' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' @@ -6,6 +7,13 @@ const REPORT_FILE = 'docs/uat/uat-report.md' type ToolCallResult = Awaited> +function getTextContent(result: ToolCallResult): string { + return result.content + .filter((item): item is { type: 'text'; text: string } => item.type === 'text') + .map((item) => item.text) + .join('\n') +} + async function logResult( uatId: string, name: string, @@ -40,11 +48,14 @@ async function runUAT(): Promise { name: 'comet_ask', arguments: { prompt: 'What is 10+10?', newChat: true }, }) + const askSimpleText = getTextContent(res) reportMd += await logResult( 'UAT-002', 'Ask Simple Query', - res.content[0].text.includes('20') ? 'Pass' : 'Fail', - 'Got 20', + !res.isError && askSimpleText.includes('Prompt submitted successfully') ? 'Pass' : 'Fail', + !res.isError + ? 'Prompt accepted (fire-and-forget behavior)' + : askSimpleText || 'Tool returned error', ) res = await client.callTool({ name: 'comet_poll', arguments: {} }) @@ -123,18 +134,26 @@ async function runUAT(): Promise { 'Content parsed', ) - res = await client.callTool({ + await client.callTool({ name: 'comet_ask', - arguments: { prompt: 'Write a complete 100 page essay on AI', timeout: 2000 }, + arguments: { prompt: 'Write a complete 100 page essay on AI' }, + }) + res = await client.callTool({ + name: 'comet_wait', + arguments: { timeout: 2000 }, }) + const waitTimeoutText = getTextContent(res) reportMd += await logResult( 'UAT-020', 'Timeout returns partial', - res.content[0].text.includes('still working') || - res.content[0].text.includes('Partial response') + waitTimeoutText.includes('still working after timeout') || + waitTimeoutText.includes('Partial response') ? 'Pass' : 'Fail', - 'Handled timeout gracefully', + waitTimeoutText.includes('still working after timeout') || + waitTimeoutText.includes('Partial response') + ? 'Handled timeout gracefully' + : waitTimeoutText || 'Unexpected wait output', ) res = await client.callTool({ name: 'comet_mode', arguments: { mode: 'learn' } }) @@ -157,6 +176,7 @@ async function runUAT(): Promise { console.error('Test execution aborted due to error:', err) } finally { await client.close() + mkdirSync(dirname(REPORT_FILE), { recursive: true }) writeFileSync(REPORT_FILE, reportMd, 'utf8') // biome-ignore lint/suspicious/noConsole: UAT script intentionally prints report path console.log(`\nReport written to ${REPORT_FILE}`) From a594f64181d4b17315f75d3995f63333e18f24c9 Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Wed, 15 Apr 2026 23:54:06 +0200 Subject: [PATCH 4/5] chore(release): prepare v1.1.5 --- CHANGELOG.md | 18 ++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/server.ts | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f2c92c..23af428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.1.5] - 2026-04-15 + +### Fixed + +- **UAT Reliability:** Updated smoke checks to validate current comet_ask fire-and-forget behavior and timeout handling through comet_wait, resolving previously failing UAT scenarios. +- **UAT Reporting:** Ensured UAT report output directory is created automatically before writing docs/uat/uat-report.md. + +### Changed + +- **Release Metadata:** Bumped package version metadata to 1.1.5. +- **QA Workflow:** Stabilized release-gate execution and quality checks used during certification. + +### Documentation + +- Refreshed README presentation and supporting docs for clearer onboarding and release readiness guidance. + ## [1.1.4] - 2026-04-13 ### Documentation @@ -96,3 +112,5 @@ TypeScript, Vitest, Chrome DevTools Protocol, Biome. 295 tests, 30 files. - CLI with --help, --version, detect, snapshot commands - Comet version auto-detection and selector registry + + diff --git a/package-lock.json b/package-lock.json index 15f63ff..e1125f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@onestepat4time/mcp-comet", - "version": "1.1.4", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@onestepat4time/mcp-comet", - "version": "1.1.4", + "version": "1.1.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/package.json b/package.json index 9eb04d1..54e4b39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@onestepat4time/mcp-comet", - "version": "1.1.4", + "version": "1.1.5", "description": "The definitive MCP server for Perplexity Comet browser management", "type": "module", "main": "dist/index.js", diff --git a/src/server.ts b/src/server.ts index be4cc3a..ba756ff 100644 --- a/src/server.ts +++ b/src/server.ts @@ -301,7 +301,7 @@ export async function startServer(): Promise { const server = new McpServer({ name: 'mcp-comet', - version: '0.1.0', + version: '1.1.5', }) // 1. comet_connect From 9a1fd9154bbedbef81ae5dd48984eef2c0e49c26 Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Thu, 16 Apr 2026 00:33:47 +0200 Subject: [PATCH 5/5] docs: align ask/wait behavior across guides --- README.md | 2 +- docs/integration.md | 21 +++++++++++++++--- docs/tools.md | 54 ++++++++++++++++++--------------------------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 7d6706d..502b06a 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ Most teams only tune these three: | Variable | Default | Change It When | | ------------------------ | ------------- | ---------------------------------------- | -| `COMET_RESPONSE_TIMEOUT` | `180000` | Queries are long and timing out | +| `COMET_RESPONSE_TIMEOUT` | `180000` | `comet_wait` calls are timing out | | `COMET_PATH` | auto-detect | Comet is in a non-standard install path | | `COMET_LOG_LEVEL` | `info` | You need debug logs | diff --git a/docs/integration.md b/docs/integration.md index cb51a5a..ce42aed 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -108,10 +108,10 @@ MCP Comet provides a `call` subcommand for invoking tools directly from the term # Connect to Comet mcp-comet call comet_connect -# Ask a question +# Submit a question mcp-comet call comet_ask '{"prompt": "What is 2+2?"}' -# Wait for completion after a timeout +# Wait for completion mcp-comet call comet_wait # Check the current status @@ -206,8 +206,23 @@ child.stdout.on('data', (chunk) => { ) } - // Handle tool response + // comet_ask is fire-and-forget; then block with comet_wait if (msg.id === 1 && msg.result) { + child.stdin.write( + JSON.stringify({ + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { + name: 'comet_wait', + arguments: {}, + }, + }) + '\n', + ) + } + + // Handle final response + if (msg.id === 2 && msg.result) { console.log(msg.result.content[0].text) } } diff --git a/docs/tools.md b/docs/tools.md index 11de2fa..ec2c0f4 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -70,9 +70,9 @@ Connected to Comet on port {port} (Chrome/{version}), target {targetId} ## 2. comet_ask -Send a prompt to Perplexity Comet and poll until the agent responds or times out. +Send a prompt to Perplexity Comet and return immediately. -This is the primary interaction tool. It types the prompt into the Comet input field, submits it, and polls for the response using a non-blocking loop with stall detection. The response includes any agent steps and the final answer text. +This tool submits the prompt and does not block for the final answer. Use `comet_poll` for status snapshots or `comet_wait` to block until completion. ### Parameters @@ -80,31 +80,15 @@ This is the primary interaction tool. It types the prompt into the Comet input f |-----------|---------|----------|----------------------------------|--------------------------------------------------| | `prompt` | string | Yes | | The question or instruction to send | | `newChat` | boolean | No | `false` | Start a fresh chat before sending the prompt | -| `timeout` | number | No | `180000` (`COMET_RESPONSE_TIMEOUT`) | Maximum wait time in ms for the agent response | ### Response -**Completed (within timeout):** -``` -{response text} - -Steps: - - {step 1} - - {step 2} -``` - -**Timeout (agent still working):** +**Success:** ``` -Agent is still working. Use comet_poll to check status. - -Steps so far: - - {step 1} - -Partial response: -{partial text} +Prompt submitted successfully. Use comet_poll to track status or comet_wait to block until completion. ``` -**Error codes:** `CDP_CONNECTION_FAILED`, `EVALUATION_FAILED`, `TIMEOUT` +**Error codes:** `CDP_CONNECTION_FAILED`, `EVALUATION_FAILED` ### CLI Example @@ -115,18 +99,16 @@ Partial response: ```json { "prompt": "Compare GPT-4 and Claude 3.5 on reasoning benchmarks", - "newChat": true, - "timeout": 300000 + "newChat": true } ``` ### Notes -- **Stall detection:** If the response length does not grow for 10 consecutive polls, the tool breaks out of the polling loop and returns whatever has been collected so far. -- **Response stabilization:** After the agent transitions to `idle` or `completed`, the tool performs up to 5 additional settle polls (1 second apart) to ensure the response text has finished rendering. - **Pre-send state capture:** Before typing, the tool captures the current prose count and last prose text to accurately detect new responses versus pre-existing content. - **newChat behavior:** When `true`, closes all extra tabs, disconnects, reconnects, and navigates to the Perplexity home page before sending the prompt. - If `newChat` is `false` and the main tab differs from the current target, the tool automatically switches to the main tab. +- This tool is intentionally non-blocking. Pair it with `comet_poll` or `comet_wait` to collect progress and final output. --- @@ -134,7 +116,7 @@ Partial response: Poll the current agent status, steps, and response content. -Returns a snapshot of the Comet agent state. Use this to check progress after `comet_ask` times out, or to implement custom polling logic in your own agent loop. +Returns a snapshot of the Comet agent state. Use this after `comet_ask` to check progress, or in a custom polling loop. ### Parameters @@ -183,7 +165,7 @@ None. Poll until the current agent finishes responding and return the full response. -Designed to be used after `comet_ask` times out. It continues polling the agent status until the response completes, another timeout is reached, or stall detection triggers. +Designed to be used after `comet_ask` submission when you want a blocking wait. It continues polling the agent status until the response completes, another timeout is reached, or stall detection triggers. ### Parameters @@ -227,7 +209,7 @@ Partial response: - **Stall detection:** Breaks out of the polling loop if the response length does not grow for 10 consecutive polls. - **Response stabilization:** After the agent transitions to `idle` or `completed`, performs up to 5 settle polls (1 second apart) to ensure the response has finished rendering. -- Default timeout is 120 seconds (2 minutes), shorter than `comet_ask` default of 180 seconds. +- Default timeout is 120 seconds (2 minutes). - Returns `"Agent completed with no visible response."` if the agent finishes but no response text was found. --- @@ -649,18 +631,24 @@ Title: {page title} ### 1. Ask and Wait (Simple) -Use `comet_ask` with the default timeout (180 seconds). This handles most queries in a single call. +Submit with `comet_ask`, then call `comet_wait` to block until completion. +Step 1: ```json { "prompt": "What is the current state of fusion energy research?" } ``` +Step 2: +```json +{} +``` + ### 2. Ask + Poll (Custom Loop) -Use `comet_ask` with a short timeout, then loop `comet_poll` in your agent to implement custom progress reporting or conditional logic. +Submit with `comet_ask`, then loop `comet_poll` in your agent to implement custom progress reporting or conditional logic. ```json -{ "prompt": "Deep research on AI safety", "timeout": 30000 } +{ "prompt": "Deep research on AI safety" } ``` Then poll: @@ -671,14 +659,14 @@ Then poll: ### 3. Ask + Wait (Two-Phase) -Use `comet_ask` (which may timeout for long-running queries), then `comet_wait` to collect the full result. This is useful when you want to start a query and check back later. +Use `comet_ask`, continue with other work, then call `comet_wait` later to collect the full result. Step 1: ```json { "prompt": "Write a comprehensive analysis of global semiconductor supply chains" } ``` -Step 2 (if Step 1 times out): +Step 2 (when ready to block for completion): ```json { "timeout": 300000 } ```