Skip to content

Draft: Autonomous Orchestrator CLI#1026

Draft
taltas wants to merge 4 commits into
mainfrom
fm/zoocode-cli-draft
Draft

Draft: Autonomous Orchestrator CLI#1026
taltas wants to merge 4 commits into
mainfrom
fm/zoocode-cli-draft

Conversation

@taltas

@taltas taltas commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Status

DRAFT ONLY. Do not mark ready or merge. This is a dangerous autonomous CLI working draft for review and follow-up extraction.

Implemented

  • Adds roo --autonomous --print --workspace <path> --timeout <seconds> on the existing apps/cli plus @roo-code/vscode-shim path. It runs the real bundled extension in one process, with no daemon and no second agent loop.
  • Canonicalizes an explicit workspace and forces every new autonomous root into the effective orchestrator slug. Project .roomodes overrides global custom modes and built-ins with normal Zoo Code precedence; --mode and --require-approval are rejected.
  • Scopes dangerous unrestricted approval to this autonomous CLI process for supported read, write, outside-workspace, protected-file, command, MCP, mode-switch, subtask, and completion classes. Interactive VS Code behavior is unchanged.
  • Reuses portable .roomodes, .roo/mcp.json, .roo/rules*, AGENTS.md/AGENTS.local.md, and supported global equivalents. Credentials remain explicit flags or provider environment variables. Native VS Code settings databases, profiles, OAuth state, and SecretStorage are not read or imported.
  • Tracks the generated root task ID through authoritative extension events. switch_mode preserves one conversation; new_task creates a fresh child, persists the parent, and resumes it with the child result. Child completion is progress; only accepted and persisted root completion exits 0.
  • Emits exactly one terminal result for completed, input-required, provider-failed, cancelled, timed-out, configuration-error, tool-failed, or crashed outcomes. First SIGINT/SIGTERM cancels cooperatively and settles persistence; a second signal force-terminates.
  • Adds backward-compatible ripgrep discovery for legacy layouts and @vscode/ripgrep 1.18 platform packages.

Test Evidence

No real model credentials or billable requests were used.

  • pnpm --filter @roo-code/cli test: 575 tests passed after no-mistakes coverage fixes.
  • pnpm --filter @roo-code/vscode-shim test: 407 tests passed.
  • Focused extension suites: 79 tests passed for auto-approval, ripgrep, custom modes, accepted completion, and nested delegation/resumption.
  • pnpm --filter @roo-code/cli test:autonomous-process: passed repeatedly against a local fake OpenRouter-compatible stream. It asserts project/global config precedence, root and child IDs, parent linkage, root Orchestrator and child mode, same-task mode switching, parent resumption, accepted root completion, input required, provider failure, timeout, SIGINT cancellation, second-signal force behavior, parseable output, and one terminal outcome. Six consecutive full runs were clean during implementation.
  • pnpm --filter ./src bundle, pnpm --filter @roo-code/cli build, and ./apps/cli/scripts/build.sh --skip-verify: passed; the release tarball found and packaged the 1.18 platform ripgrep binary.
  • Type checking and lint passed for the CLI, extension, and shim.
  • GitHub CI: 14/14 checks green, including Linux and Windows unit tests, compile, e2e-mock, knip, CodeQL, dependency review, CodeRabbit, and Codecov patch.

Unsupported Headless Capabilities

  • Native VS Code settings/profile resolution, encrypted SecretStorage, VS Code OAuth/URI callbacks, and VS Code Language Models.
  • Editor selections, tabs/focus, live diagnostics, visual diff UI, clipboard UI, and VS Code terminal shell integration.
  • Reliable live file-watcher reloads in a long-running shim process; restart after changing portable configuration.
  • This draft keeps checkpoints disabled, matching the existing CLI.

Known Risks

Risk: Medium. The mode intentionally grants unrestricted filesystem, command, and MCP authority, including outside-workspace and protected-file mutations. Run it only in a fully trusted environment.

  • The extension remains VS Code-centered and the shim is intentionally partial; unsupported APIs can still reveal additional capability gaps.
  • Task histories and shim state can contain sensitive prompts, outputs, and settings.
  • run.ts is exercised by the built-process smoke rather than unit instrumentation; the no-mistakes CI fix excludes that process entrypoint from Codecov patch accounting while moving pure autonomous validation into a fully covered module.
  • The explicit internal-crash terminal path is unit/review covered but is not a dedicated process-smoke fixture.
  • Same-workspace concurrent autonomous processes are not coordinated by a workspace mutation lock.

Follow-Up Extraction

  • Extract host-neutral task lifecycle, capability, secret, and configuration adapters incrementally from Task/ClineProvider, retaining parity tests against this shim path.
  • Add a first-class versioned root/child lifecycle event protocol rather than adapting webview presentation messages.
  • Add real filesystem watchers or explicit reload commands, headless checkpoint storage, workspace leasing, and stronger mutation journaling before considering production readiness.
  • Add deliberate credential helpers or an authenticated extension bridge if native profile/SecretStorage reuse is ever required; never scrape VS Code storage.

Validation Commands

pnpm --filter @roo-code/cli check-types
pnpm --filter @roo-code/cli lint
pnpm --filter @roo-code/cli test
pnpm --filter @roo-code/vscode-shim check-types
pnpm --filter @roo-code/vscode-shim lint
pnpm --filter @roo-code/vscode-shim test
pnpm --filter ./src check-types
pnpm --filter ./src lint
pnpm --filter ./src bundle
pnpm --filter @roo-code/cli build
pnpm --filter @roo-code/cli test:autonomous-process
./apps/cli/scripts/build.sh --skip-verify

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds an autonomous CLI profile with task orchestration, terminal-state reporting, cancellation, persistence, provider configuration, validation, documentation, and smoke tests. It also consolidates agent guidance and adds platform-specific ripgrep binary resolution with integration coverage.

Changes

Autonomous CLI execution

Layer / File(s) Summary
Autonomous contracts and configuration
apps/cli/src/agent/autonomous-run.ts, apps/cli/src/types/*, apps/cli/src/index.ts, apps/cli/src/lib/utils/provider.ts, apps/cli/src/agent/ask-dispatcher.ts
Defines autonomous terminal states, exit codes, task results, CLI flags, JSON result metadata, provider base URL support, and input-required handling.
Extension host task orchestration
apps/cli/src/agent/extension-host.ts
Adds autonomous initialization, environment management, root-task tracking, completion detection, timeout cancellation, persistence, and task accessors.
CLI validation and shutdown flow
apps/cli/src/commands/cli/run.ts
Validates autonomous options, configures execution, emits terminal states, handles signals and crashes, cancels tasks, flushes output, and maps failures to exit codes.
Authoritative terminal output
apps/cli/src/agent/json-event-emitter.ts, apps/cli/src/agent/__tests__/json-event-emitter-result.test.ts
Adds single-emission terminal results for JSON and NDJSON output and tests autonomous result fields.
Autonomous integration coverage
apps/cli/scripts/autonomous-smoke.ts, apps/cli/src/agent/__tests__/extension-host.test.ts, src/core/auto-approval/*, apps/cli/package.json
Covers delegation, mode switching, persistence, input requirements, failures, timeouts, cancellation, forced signals, auto-approval, and process-level output.
Autonomous CLI documentation
apps/cli/README.md
Documents autonomous execution, restrictions, supported capabilities, terminal output, exit codes, signal behavior, persistence, and smoke testing.
CLI host wiring
apps/cli/src/commands/cli/list.ts, apps/cli/src/ui/hooks/useExtensionHost.ts
Disables ask handling for list and UI-created extension hosts.

Agent guidance consolidation

Layer / File(s) Summary
Shared agent guidance
AGENTS.md, CLAUDE.md
Adds maintenance instructions to AGENTS.md and makes CLAUDE.md reference it.

Platform-specific ripgrep resolution

Layer / File(s) Summary
Platform ripgrep fallback
src/services/ripgrep/index.ts, src/services/ripgrep/__tests__/index.spec.ts
Resolves platform-specific ripgrep packages through package metadata and tests successful and unavailable layouts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ExtensionHost
  participant RooCodeAPI
  participant JsonEventEmitter
  CLI->>ExtensionHost: start autonomous task
  ExtensionHost->>RooCodeAPI: dispatch task and monitor events
  RooCodeAPI-->>ExtensionHost: completion, input, timeout, or cancellation event
  ExtensionHost-->>CLI: task result or terminal error state
  CLI->>JsonEventEmitter: emit authoritative terminal result
  JsonEventEmitter-->>CLI: JSON or NDJSON output
Loading

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant, edelauna

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is missing the required issue link, structured Description/Test Procedure sections, and the pre-submission checklist. Add the approved GitHub issue, a brief how/why description, concrete test steps, and the checklist items required by the template.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the new autonomous orchestrator CLI draft and matches the main change set.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/zoocode-cli-draft

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
src/core/auto-approval/__tests__/autonomous-cli.spec.ts (1)

11-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider testing that the env var alone (without alwaysAllowMcp) does not bypass approval.

The current test only verifies the env var toggles approval when alwaysAllowMcp: true. Given this is a security-sensitive auto-approval bypass, an additional case asserting ROO_CLI_AUTONOMOUS=1 with alwaysAllowMcp: false still returns { decision: "ask" } would guard against a future regression weakening the AND-condition in checkAutoApproval.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/auto-approval/__tests__/autonomous-cli.spec.ts` around lines 11 -
22, Extend the test for checkAutoApproval to cover ROO_CLI_AUTONOMOUS=1 with
alwaysAllowMcp set to false, asserting the result remains { decision: "ask" };
retain the existing enabled-case assertions and ensure the environment variable
is cleaned up afterward.
apps/cli/src/agent/json-event-emitter.ts (1)

215-256: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

resumable is hardcoded false for every terminal state, including needs_input.

needs_input conceptually represents a paused task awaiting more input, not necessarily an unresumable failure, and the payload already surfaces rootTaskId explicitly "for lineage-aware consumers." Hardcoding resumable: false regardless of state may under-report actual resumability once/if autonomous session resumption is wired up, or it may simply be an intentional placeholder for this draft. Worth confirming intended semantics and, if needs_input sessions are in fact resumable via --session-id, deriving resumable from state instead of a constant.

♻️ Possible direction (pending confirmation of intended semantics)
-			resumable: false,
+			resumable: event.state === "needs_input",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/agent/json-event-emitter.ts` around lines 215 - 256, Update
emitTerminal so the terminal payload’s resumable value reflects the intended
resumability of each AutonomousTerminalState, particularly treating needs_input
as resumable when session continuation via --session-id is supported. Apply the
same derived value to both the JsonEvent terminal object and JsonFinalOutput,
preserving false for states that cannot be resumed.
apps/cli/src/agent/__tests__/json-event-emitter-result.test.ts (1)

70-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a dedicated first-call "crashed" terminal test.

PR objectives flag missing explicit crash-outcome coverage. The only state: "crashed" case exercised here is the second, suppressed call (used to prove the single-emission guard), so there's no assertion that a crash emitted as the first terminal event produces the correct payload (success: false, exitCode: 70, etc.). Based on learnings, pure-logic/serialization tests for terminal-state handling belong at this package-local unit-test layer rather than only in an e2e/smoke test.

✅ Suggested additional test
+	it("serializes a crashed terminal outcome correctly", () => {
+		const { stdout, lines } = createMockStdout()
+		const emitter = new JsonEventEmitter({ mode: "stream-json", stdout, authoritativeCompletion: true })
+
+		emitter.emitTerminal({ state: "crashed", exitCode: 70, content: "boom" })
+
+		expect(lines()).toEqual([
+			expect.objectContaining({
+				type: "result",
+				subtype: "terminal",
+				state: "crashed",
+				exitCode: 70,
+				success: false,
+				done: true,
+			}),
+		])
+	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/agent/__tests__/json-event-emitter-result.test.ts` around lines
70 - 88, Add a dedicated test alongside the existing JsonEventEmitter terminal
tests that creates an authoritative stream-json emitter, emits a first terminal
event with state "crashed" and exitCode 70, and asserts the single result
payload includes subtype "terminal", done true, success false, and the crash
exit code. Keep the existing duplicate-emission guard test unchanged.

Source: Learnings

apps/cli/src/agent/__tests__/extension-host.test.ts (1)

713-726: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Attach the rejection assertion before triggering the ask.

taskPromise can reject while await dispatcher.handleAsk(...) is in flight, i.e. before expect(...).rejects subscribes. Setting up the assertion first removes any unhandled-rejection flakiness.

♻️ Suggested change
-			const taskPromise = host.runTask("ask something")
+			const taskAssertion = expect(host.runTask("ask something")).rejects.toMatchObject({ state: "needs_input" })
 			const dispatcher = getPrivate<{ handleAsk: (message: ClineMessage) => Promise<unknown> }>(
 				host,
 				"askDispatcher",
 			)
@@
-			await expect(taskPromise).rejects.toMatchObject({ state: "needs_input" })
+			await taskAssertion
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/agent/__tests__/extension-host.test.ts` around lines 713 - 726,
Attach the rejects assertion to taskPromise immediately after host.runTask("ask
something") and before invoking dispatcher.handleAsk. Await that assertion after
the ask completes, preserving the expected { state: "needs_input" } rejection
while ensuring the rejection is observed during handleAsk.
apps/cli/src/agent/extension-host.ts (1)

638-640: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid the "unknown" sentinel leaking through getRootTaskId()/getLastTaskResult().

For non-autonomous runs without an explicit taskId, the host stores the literal string "unknown" as the root task id even though no taskId is sent to the extension. Any consumer reading getRootTaskId() in interactive/TUI mode gets a fake id rather than "not known yet".

♻️ Suggested change
-		const rootTaskId = taskId ?? (this.options.autonomous ? randomUUID() : "unknown")
-		this.rootTaskId = rootTaskId
+		const rootTaskId = taskId ?? (this.options.autonomous ? randomUUID() : undefined)
+		this.rootTaskId = rootTaskId

with waitForTaskCompletion accepting string | undefined and TaskRunResult.rootTaskId typed accordingly (the autonomous path always has a concrete id).

Also applies to: 662-664

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/agent/extension-host.ts` around lines 638 - 640, Update the root
task initialization around waitForTaskCompletion to preserve undefined for
non-autonomous runs without an explicit taskId instead of using the "unknown"
sentinel. Allow waitForTaskCompletion and TaskRunResult.rootTaskId to use string
| undefined, while retaining the concrete generated or supplied ID for
autonomous and explicitly identified runs so getRootTaskId() and
getLastTaskResult() report the correct unknown state.
apps/cli/scripts/autonomous-smoke.ts (1)

168-180: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Throwing inside the async request handler hangs the request instead of failing the smoke test.

createServer(async (request, response) => ...) — Node does not await or catch the returned promise, so the throw at Lines 176 and 179 produces an unhandled rejection while the CLI's HTTP request never gets a response and stalls until the 30s execa timeout. Respond with a diagnostic status and record the failure instead.

♻️ Suggested change
-				for (const marker of [
-					"PROJECT_ORCHESTRATOR_OVERRIDE",
-					"PROJECT_MODE_RULE",
-					"PROJECT_AGENTS_INSTRUCTION",
-					"GLOBAL_PORTABLE_RULE",
-				]) {
-					if (!body.includes(marker)) throw new Error(`provider request did not include ${marker}`)
-				}
-				if (body.includes("GLOBAL_ORCHESTRATOR_SHOULD_LOSE")) {
-					throw new Error("project orchestrator did not override the global mode")
-				}
+				const missing = [
+					"PROJECT_ORCHESTRATOR_OVERRIDE",
+					"PROJECT_MODE_RULE",
+					"PROJECT_AGENTS_INSTRUCTION",
+					"GLOBAL_PORTABLE_RULE",
+				].filter((marker) => !body.includes(marker))
+				if (missing.length > 0 || body.includes("GLOBAL_ORCHESTRATOR_SHOULD_LOSE")) {
+					fixtureFailures.push(
+						missing.length > 0
+							? `provider request did not include ${missing.join(", ")}`
+							: "project orchestrator did not override the global mode",
+					)
+					response.writeHead(400)
+					response.end("fixture precondition failed")
+					return
+				}

Then assert fixtureFailures is empty alongside the other scenario checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/scripts/autonomous-smoke.ts` around lines 168 - 180, Replace the
throws in the async request handler’s delegation-marker validation with a
diagnostic HTTP response and recorded fixture failure, ensuring every failed
validation ends the request without hanging. Update the smoke-test assertions
alongside the existing scenario checks to require fixtureFailures to be empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/scripts/autonomous-smoke.ts`:
- Around line 193-195: Ensure the fake provider created in the autonomous smoke
scenario is always closed by moving server cleanup into a finally block that
surrounds the assertion/scenario flow. Hoist the server and its listen setup
outside the try if necessary, while preserving the existing server.close
behavior and handling the intentionally hung request sockets during cleanup.

In `@apps/cli/src/agent/extension-host.ts`:
- Around line 490-495: Attach a rejection handler immediately when creating
initializationPromise in markWebviewReady, while preserving the promise for
activate() to await and surface the original failure. Ensure rejected
updateSettings or webviewDidLaunch dispatches are observed before isReady allows
activate() to continue.
- Around line 237-247: Update the autonomous-mode error handling around the
onInputRequired callback and waitForTaskCompletion lifecycle so the client’s
"error" event always has a listener before AutonomousRunError is emitted,
including before task activation and after cleanup. Add a persistent terminal
listener or otherwise route these errors safely without changing the existing
state and message construction.

In `@src/services/ripgrep/__tests__/index.spec.ts`:
- Around line 143-144: Update the fallback test around getBinPath to stop
mocking fileExistsAtPath and exercise the real filesystem probe through
resolvePlatformRipgrepPath. Preserve mocks only for candidate-order unit tests,
and keep the assertion that getBinPath(appRoot) resolves to resolvedRg using the
fixture’s actual filesystem state.

In `@src/services/ripgrep/index.ts`:
- Around line 109-117: The resolver in resolvePlatformRipgrepPath must use the
wrapper-exported rgPath from `@vscode/ripgrep` instead of constructing an optional
package name from process.arch; update the related test expectations and setup
in src/services/ripgrep/index.ts lines 109-117 and
src/services/ripgrep/__tests__/index.spec.ts lines 114-144 to cover the
wrapper’s platform-specific resolution behavior.

---

Nitpick comments:
In `@apps/cli/scripts/autonomous-smoke.ts`:
- Around line 168-180: Replace the throws in the async request handler’s
delegation-marker validation with a diagnostic HTTP response and recorded
fixture failure, ensuring every failed validation ends the request without
hanging. Update the smoke-test assertions alongside the existing scenario checks
to require fixtureFailures to be empty.

In `@apps/cli/src/agent/__tests__/extension-host.test.ts`:
- Around line 713-726: Attach the rejects assertion to taskPromise immediately
after host.runTask("ask something") and before invoking dispatcher.handleAsk.
Await that assertion after the ask completes, preserving the expected { state:
"needs_input" } rejection while ensuring the rejection is observed during
handleAsk.

In `@apps/cli/src/agent/__tests__/json-event-emitter-result.test.ts`:
- Around line 70-88: Add a dedicated test alongside the existing
JsonEventEmitter terminal tests that creates an authoritative stream-json
emitter, emits a first terminal event with state "crashed" and exitCode 70, and
asserts the single result payload includes subtype "terminal", done true,
success false, and the crash exit code. Keep the existing duplicate-emission
guard test unchanged.

In `@apps/cli/src/agent/extension-host.ts`:
- Around line 638-640: Update the root task initialization around
waitForTaskCompletion to preserve undefined for non-autonomous runs without an
explicit taskId instead of using the "unknown" sentinel. Allow
waitForTaskCompletion and TaskRunResult.rootTaskId to use string | undefined,
while retaining the concrete generated or supplied ID for autonomous and
explicitly identified runs so getRootTaskId() and getLastTaskResult() report the
correct unknown state.

In `@apps/cli/src/agent/json-event-emitter.ts`:
- Around line 215-256: Update emitTerminal so the terminal payload’s resumable
value reflects the intended resumability of each AutonomousTerminalState,
particularly treating needs_input as resumable when session continuation via
--session-id is supported. Apply the same derived value to both the JsonEvent
terminal object and JsonFinalOutput, preserving false for states that cannot be
resumed.

In `@src/core/auto-approval/__tests__/autonomous-cli.spec.ts`:
- Around line 11-22: Extend the test for checkAutoApproval to cover
ROO_CLI_AUTONOMOUS=1 with alwaysAllowMcp set to false, asserting the result
remains { decision: "ask" }; retain the existing enabled-case assertions and
ensure the environment variable is cleaned up afterward.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 70782dfe-ce10-440a-a7bf-4e532bb6abc8

📥 Commits

Reviewing files that changed from the base of the PR and between 1ceb4e6 and ea1f06a.

📒 Files selected for processing (22)
  • AGENTS.md
  • CLAUDE.md
  • apps/cli/README.md
  • apps/cli/package.json
  • apps/cli/scripts/autonomous-smoke.ts
  • apps/cli/src/agent/__tests__/extension-host.test.ts
  • apps/cli/src/agent/__tests__/json-event-emitter-result.test.ts
  • apps/cli/src/agent/ask-dispatcher.ts
  • apps/cli/src/agent/autonomous-run.ts
  • apps/cli/src/agent/extension-host.ts
  • apps/cli/src/agent/json-event-emitter.ts
  • apps/cli/src/commands/cli/list.ts
  • apps/cli/src/commands/cli/run.ts
  • apps/cli/src/index.ts
  • apps/cli/src/lib/utils/provider.ts
  • apps/cli/src/types/json-events.ts
  • apps/cli/src/types/types.ts
  • apps/cli/src/ui/hooks/useExtensionHost.ts
  • src/core/auto-approval/__tests__/autonomous-cli.spec.ts
  • src/core/auto-approval/index.ts
  • src/services/ripgrep/__tests__/index.spec.ts
  • src/services/ripgrep/index.ts

Comment on lines +193 to +195
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
const address = server.address()
if (!address || typeof address === "string") throw new Error("failed to bind fake provider")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close the fake provider in finally, not only on the success path.

server.close() at Line 336 is skipped whenever any assertion between Lines 240–335 throws, so the listening socket (plus the intentionally hung request sockets) keeps the event loop alive and the smoke run hangs instead of failing fast in CI.

🛡️ Suggested fix
-		server.close()
 		process.stdout.write("autonomous process smoke passed\n")
 	} finally {
+		server.closeAllConnections?.()
+		await new Promise<void>((resolve) => server.close(() => resolve()))
 		await rm(root, { recursive: true, force: true })
 	}

This requires hoisting server (and its listen) outside the try, or moving the cleanup into a nested try/finally around the scenario block.

Also applies to: 336-340

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/scripts/autonomous-smoke.ts` around lines 193 - 195, Ensure the fake
provider created in the autonomous smoke scenario is always closed by moving
server cleanup into a finally block that surrounds the assertion/scenario flow.
Hoist the server and its listen setup outside the try if necessary, while
preserving the existing server.close behavior and handling the intentionally
hung request sockets during cleanup.

Comment on lines +237 to +247
onInputRequired: options.autonomous
? (ask, text) => {
const state = ask === "api_req_failed" ? "provider_failed" : "needs_input"
this.client
.getEmitter()
.emit(
"error",
new AutonomousRunError(state, `${ask}: ${text || "human input is required"}`),
)
}
: undefined,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the emitter used by ExtensionClient.getEmitter and its base class.
fd -t f 'extension-client.ts' apps/cli/src | head
ast-grep run --pattern 'getEmitter() { $$$ }' --lang typescript apps/cli/src || true
rg -nP -C4 'class ExtensionClient|getEmitter\s*\(' apps/cli/src --type=ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 5515


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate TypedEventEmitter and event emitter implementation =="
rg -n "class TypedEventEmitter|interface TypedEventEmitter|type TypedEventEmitter|EventEmitter|emit\\(|on\\(" apps/cli/src/agent apps/cli/src -g '*.ts' -g '!*.d.ts' | head -200

echo
echo "== extension-client relevant sections =="
sed -n '1,180p' apps/cli/src/agent/extension-client.ts
sed -n '480,545p' apps/cli/src/agent/extension-client.ts

echo
echo "== extension-host relevant cleanup/error handling sections =="
sed -n '220,260p' apps/cli/src/agent/extension-host.ts
rg -n -C4 "waitForTaskCompletion|cleanup\\(|on\\(\"error\"|on\\('error\"|useExtensionHost" apps/cli/src/agent/extension-host.ts

echo
echo "== dependency/event-emitter source if published inside repo =="
fd -t f 'event-emitter|emitter' apps . | head -50

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 32547


Ensure the client "error" event has listeners before emitting.

TypedEventEmitter delegates to Node’s EventEmitter, and emitting "error" with zero listeners throws. In autonomous runs the error handler is attached inside waitForTaskCompletion and removed by cleanup, so an ask arriving before runTask()/resumeTask() is active or after teardown will crash the CLI. Add a persistent/terminal "error" listener for autonomous mode, or avoid emitting directly; otherwise emit(new AutonomousRunError(...)) can bring down the process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/agent/extension-host.ts` around lines 237 - 247, Update the
autonomous-mode error handling around the onInputRequired callback and
waitForTaskCompletion lifecycle so the client’s "error" event always has a
listener before AutonomousRunError is emitted, including before task activation
and after cleanup. Add a persistent terminal listener or otherwise route these
errors safely without changing the existing state and message construction.

Comment on lines +490 to +495
// Serialize initial settings and launch. In particular, webviewDidLaunch
// loads project custom modes before activate() allows the first task.
this.initializationPromise = (async () => {
await this.dispatchToExtension({ type: "updateSettings", updatedSettings: this.initialSettings })
await this.dispatchToExtension({ type: "webviewDidLaunch" })
})()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Attach a rejection handler when initializationPromise is created.

markWebviewReady() sets isReady = true first, but activate() only awaits initializationPromise after pWaitFor(() => this.isReady, { interval: 100 }) resolves — up to ~100ms later. If updateSettings/webviewDidLaunch dispatch rejects in that window the promise is unhandled, and Node's default --unhandled-rejections=throw will terminate the process instead of surfacing the error through activate().

🛡️ Suggested fix
-		this.initializationPromise = (async () => {
-			await this.dispatchToExtension({ type: "updateSettings", updatedSettings: this.initialSettings })
-			await this.dispatchToExtension({ type: "webviewDidLaunch" })
-		})()
+		const initialization = (async () => {
+			await this.dispatchToExtension({ type: "updateSettings", updatedSettings: this.initialSettings })
+			await this.dispatchToExtension({ type: "webviewDidLaunch" })
+		})()
+		// Keep a settled handler attached so a rejection before activate() awaits
+		// it does not surface as an unhandled rejection.
+		initialization.catch(() => undefined)
+		this.initializationPromise = initialization
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Serialize initial settings and launch. In particular, webviewDidLaunch
// loads project custom modes before activate() allows the first task.
this.initializationPromise = (async () => {
await this.dispatchToExtension({ type: "updateSettings", updatedSettings: this.initialSettings })
await this.dispatchToExtension({ type: "webviewDidLaunch" })
})()
// Serialize initial settings and launch. In particular, webviewDidLaunch
// loads project custom modes before activate() allows the first task.
const initialization = (async () => {
await this.dispatchToExtension({ type: "updateSettings", updatedSettings: this.initialSettings })
await this.dispatchToExtension({ type: "webviewDidLaunch" })
})()
// Keep a settled handler attached so a rejection before activate() awaits
// it does not surface as an unhandled rejection.
initialization.catch(() => undefined)
this.initializationPromise = initialization
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/agent/extension-host.ts` around lines 490 - 495, Attach a
rejection handler immediately when creating initializationPromise in
markWebviewReady, while preserving the promise for activate() to await and
surface the original failure. Ensure rejected updateSettings or webviewDidLaunch
dispatches are observed before isReady allows activate() to continue.

Comment on lines +143 to +144
mockFileExists.mockImplementation(async (candidate: string) => candidate === resolvedRg)
await expect(getBinPath(appRoot)).resolves.toBe(resolvedRg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Exercise the real filesystem probe here.

This behavior crosses getBinPath, resolvePlatformRipgrepPath, and fileExistsAtPath; mocking the latter makes the fallback assertion unit-style rather than filesystem-backed. Keep mocks for candidate-order unit tests, but use the real helper in this fixture.

As per coding guidelines, “Use integration tests when behavior depends on multiple internal modules but does not require the real VS Code extension host or browser/webview runtime.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ripgrep/__tests__/index.spec.ts` around lines 143 - 144, Update
the fallback test around getBinPath to stop mocking fileExistsAtPath and
exercise the real filesystem probe through resolvePlatformRipgrepPath. Preserve
mocks only for candidate-order unit tests, and keep the assertion that
getBinPath(appRoot) resolves to resolvedRg using the fixture’s actual filesystem
state.

Source: Coding guidelines

Comment on lines +109 to +117
/** Resolve @vscode/ripgrep >=1.18, which ships rg in a platform-specific optional package. */
export function resolvePlatformRipgrepPath(vscodeAppRoot: string): string | undefined {
try {
const wrapperManifest = path.join(vscodeAppRoot, "node_modules", "@vscode", "ripgrep", "package.json")
if (!fs.existsSync(wrapperManifest)) return undefined
const requireFromApp = createRequire(path.join(vscodeAppRoot, "package.json"))
const wrapperEntry = requireFromApp.resolve("@vscode/ripgrep")
const requireFromWrapper = createRequire(wrapperEntry)
return requireFromWrapper.resolve(`@vscode/ripgrep-${process.platform}-${process.arch}/bin/${binName}`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'src/services/ripgrep/(index|.*spec)\.ts$|package\.json$|pnpm-lock|package-lock|yarn.lock' || true

echo "== ripgrep implementation outline =="
f="$(git ls-files | rg 'src/services/ripgrep/index.ts$' | head -n1 || true)"
if [ -n "${f:-}" ]; then
  wc -l "$f"
  sed -n '1,180p' "$f" | cat -n
fi

echo "== ripgrep tests relevant section =="
tf="$(git ls-files | rg 'src/services/ripgrep/__tests__/index.ser\.ts$|src/services/ripgrep/__tests__/index.spec.ts$' | head -n1 || true)"
if [ -n "${tf:-}" ]; then
  wc -l "$tf"
  sed -n '1,220p' "$tf" | cat -n
fi

echo "== package mentions =="
rg -n '"`@vscode/ripgrep`"|ripgrep|binName|resolvePlatformRipgrepPath|rgPath' . -S --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 44036


🌐 Web query:

npm @vscode/ripgrep 1.18.0 package files bin rgPath

💡 Result:

In version 1.18.0 of the @vscode/ripgrep npm package, the architecture and distribution model underwent a significant change from previous versions [1][2]. The package is now a lightweight wrapper that resolves the rgPath to a sibling package [3][2]. Specifically, the wrapper uses optionalDependencies to install the correct per-platform package (such as @vscode/ripgrep-linux-x64 or @vscode/ripgrep-win32-x64) based on the user's platform and architecture [3][2]. Regarding the bin file and rgPath: - Files are located within the platform-specific sub-packages [2]. When installed, these packages place the ripgrep binary at a path structured as packages/ripgrep-<os>-<arch>/bin/rg (or rg.exe on Windows) [4][3]. - At runtime, the main @vscode/ripgrep package exports the rgPath constant, which is resolved dynamically in lib/index.js to point to the correct binary location within the installed platform-specific sibling package [4][3][2]. Users should access the path using the standard exported constant: const { rgPath } = require('@vscode/ripgrep'); // child_process.spawn(rgPath,...); Note that older versions of the package (prior to the migration to per-platform packages) relied on a postinstall script to download binaries; this is no longer the case in 1.18.0, which ships the binaries directly within the npm tarball for each platform-specific package [4][3][5][6]. If you require access to binaries for multiple platforms from a single install, the @vscode/ripgrep-universal package is available, which provides both rgPath (for the current host) and a binPathFor({ os, arch }) function [3][7].

Citations:


🌐 Web query:

npm @vscode/ripgrep-universal``

💡 Result:

@vscode/ripgrep-universal is an npm package that bundles ripgrep binaries for all supported platforms into a single package [1][2]. It was introduced in version 1.18.0 of the microsoft/vscode-ripgrep repository [3]. Key characteristics and usage: - Purpose: It is designed for use cases where access to binaries for multiple platforms is required from a single installation, such as when repackaging a Node.js application into cross-platform artifacts (e.g., installers or archives) from a single build host [1][2]. - Size: Because it includes binaries for all 12 supported platforms, the package is significantly larger than the standard @vscode/ripgrep package, approximately 60 MB [1][4]. - Comparison: For general application use, the standard @vscode/ripgrep package is preferred, as it only installs the binary for the current platform via optionalDependencies [1][2]. - API: The package exports rgPath (for the current platform) and a helper function binPathFor({ os, arch }) to retrieve the path to any platform's specific binary [1][4]. Example usage: import { rgPath, binPathFor } from '@vscode/ripgrep-universal'; // Path to the binary for the current platform console.log(rgPath); // Path to a specific platform binary const winPath = binPathFor({ os: 'win32', arch: 'x64' }); [1]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dependency declarations =="
for f in package.json apps/cli/package.json apps/vscode-e2e/package.json apps/vscode-nightly/package.json; do
  [ -f "$f" ] || continue
  echo "-- $f --"
  php -r "echo json_encode(json_decode(file_get_contents('$f'), true), JSON_PRETTY_PRINT)." 2>/dev/null | rg -n '"`@vscode/ripgrep`"|"`@vscode/ripgrep-universal`"|"vscode-ripgrep"|version|dependencies|devDependencies|optionalDependencies' -C 2 || true
done

echo "== lockfile entries around `@vscode/ripgrep` packages =="
python3 - <<'PY'
from pathlib import Path
p=Path('pnpm-lock.yaml')
lines=p.read_text().splitlines()
targets=['`@vscode/ripgrep`@1.18.0','`@vscode/ripgrep-universal`','`@vscode/ripgrep-darwin-arm64`@1.18.0']
for t in targets:
    print(f'-- {t} --')
    matches=[i for i,l in enumerate(lines) if l.strip().startswith(t + '@') or l.strip().startswith(t + ' ')]
    for i in matches[:5]:
        for j in range(i, min(len(lines), i+18)):
            print(f'{j+1}: {lines[j]}')
        print()
PY

echo "== inspect `@vscode/ripgrep` lib index in repository lock contents if present =="
tmpdir="$(mktemp -d)"
cd "$tmpdir"
if command -v npm >/dev/null 2>&1; then
  npm pack `@vscode/ripgrep`@1.18.0 --silent
  tar -tvf vscode-ripgrep-1.18.0.tgz | grep -E 'package/(lib/index|package/bin|packages/ripgrep-\w+-\w+/bin)' | head -n 40 || true
  echo "-- package/index.js excerpt --"
  tar -xzf vscode-ripgrep-1.18.0.tgz
  sed -n '1,220p' package/index.js 2>/dev/null || true
else
  echo "npm not available"
fi
rm -rf "$tmpdir"

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 744


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
for p in ['package.json','apps/cli/package.json','apps/vscode-e2e/package.json','apps/vscode-nightly/package.json']:
    try:
        data=json.load(open(p))
    except Exception as e:
        print(f"### {p}: {type(e).__name__}: {e}")
        continue
    print(f"### {p}")
    for sec in ["dependencies","devDependencies","peerDependencies","optionalDependencies"]:
        if sec in data:
            entries={k:v for k,v in data[sec].items() if "ripgrep" in k.lower()}
            if entries:
                print(sec, entries)
            else:
                print(sec, '<ripgrep entries>')
PY

echo "== package manifest from lock =="
python3 - <<'PY'
from pathlib import Path
text=Path('pnpm-lock.yaml').read_text()
for marker in ['`@vscode/ripgrep-universal`@', '`@vscode/ripgrep-darwin-arm64`@1.18.0']:
    idx=text.find(marker)
    print(f"\n-- {marker} at {idx} --")
    if idx!=-1:
        start=text.rfind("\n", 0, idx-50)
        end=text.find("\n\n", idx+100)
        print(text[start:end if end!=-1 else idx+500])

# find package.json of `@vscode/ripgrep`@1.18.0 in snapshot
idx=text.find("`@vscode/ripgrep`@1.18.0:")
if idx!=-1:
    start=text.find("snapshot:", idx)
    if start!=-1:
        end=text.find("\n\n", start+8)
        print("\n-- `@vscode/ripgrep`@1.18.0 snapshot --")
        print(text[start:end])
PY

echo "== npm pack index/package paths =="
tmpdir="$(mktemp -d)"
cd "$tmpdir"
npm pack `@vscode/ripgrep`@1.18.0 --silent 2>/dev/null || true
tar -tvf vscode-ripgrep-1.18.0.tgz | rg -E 'package/(package\.json|lib/index\.js|bin/|packages/ripgrep-[^\s]+/package\.json|packages/ripgrep-[^\s]+/bin/)' | head -n 80
rm -rf "$tmpdir"

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
cd "$tmpdir"
npm pack `@vscode/ripgrep`@1.18.0 --silent
tar -tf vscode-ripgrep-1.18.0.tgz | sort | sed -n '1,120p'
echo "== package.json =="
tar -xzf vscode-ripgrep-1.18.0.tgz
cat package/package.json
echo "== lib/index.js =="
cat package/lib/index.js
echo "== platform package dirs/files excerpt =="
find package -maxdepth 4 -path '*/ripgrep-*-*/package.json' -o -path '*/ripgrep-*-*/lib/index.js' | sort | sed -n '1,80p'
for p in $(find package -path '*/ripgrep-*-*/package.json' | sort | head -n 4); do
  echo "-- $p --"
  cat "$p"
done
rm -rf "$tmpdir"

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2036


Use rgPath from @vscode/ripgrep instead of constructing a package from process.arch.

@vscode/ripgrep 1.18.0 is a wrapper whose optional platform packages use the CPU target (darwin-x64, linux-arm64, etc.), not process.arch (x64, arm64). That means this resolver can construct names like @vscode/ripgrep-x64 instead of @vscode/ripgrep-darwin-x64 and return undefined; model the actual optional package dependency or use the wrapper-exported rgPath.

  • src/services/ripgrep/index.ts#L109-L120
  • src/services/ripgrep/__tests__/index.spec.ts#L114-L125
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import * as childProcess from "child_process"
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

📍 Affects 2 files
  • src/services/ripgrep/index.ts#L109-L117 (this comment)
  • src/services/ripgrep/__tests__/index.spec.ts#L114-L144
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ripgrep/index.ts` around lines 109 - 117, The resolver in
resolvePlatformRipgrepPath must use the wrapper-exported rgPath from
`@vscode/ripgrep` instead of constructing an optional package name from
process.arch; update the related test expectations and setup in
src/services/ripgrep/index.ts lines 109-117 and
src/services/ripgrep/__tests__/index.spec.ts lines 114-144 to cover the
wrapper’s platform-specific resolution behavior.

Source: Learnings

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 26, 2026
@taltas
taltas marked this pull request as draft July 26, 2026 04:49
@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 26, 2026
@taltas taltas changed the title feat(cli): add autonomous orchestrator draft Draft: Autonomous Orchestrator CLI Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant