Skip to content

ADE CLI / PR workflow fixes and integration lane ownership#185

Merged
arul28 merged 8 commits intomainfrom
ade/cli-prs-fixes-747d7096
Apr 23, 2026
Merged

ADE CLI / PR workflow fixes and integration lane ownership#185
arul28 merged 8 commits intomainfrom
ade/cli-prs-fixes-747d7096

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Apr 23, 2026

ADE CLI RPC/bootstrap improvements, shared adeCliGuidance for agents, orchestrator/chat/CTO wiring, and safer integration lane ownership for cleanup (scratch lanes vs merge-into adoption).

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features

    • Expanded ADE actions with new domains and enhanced method allowlists for automation workflows.
    • Automated PR description drafting when title/body are omitted.
    • Role-based access controls for restricted actions.
  • Improvements

    • Enhanced help system with detailed command and argument documentation.
    • Improved project and workspace root discovery with environment overrides.
    • Better PR detection and duplicate handling.
    • Refined agent guidance with inline CLI documentation.

Greptile Summary

This PR consolidates ADE CLI guidance into a single shared constant (ADE_CLI_AGENT_GUIDANCE), fixes several PR duplicate-recovery bugs (cascade-delete scoping, findExistingPrForBranch, #123 locator parsing), expands the action allowlist with ~15 new domains and role-based access enforcement, and improves worktree-aware project root discovery in the CLI.

All previously flagged issues appear resolved: startupCommandPreview now logs the augmented prompt, duplicate guidance in promptInspector.ts is removed, the || Boolean(candidate?.draft) closed-draft matching is gone, --text is absent from VALUE_CARRIER_FLAGS, and graph_state is correctly scoped to runtime.projectId.

Confidence Score: 5/5

Safe to merge; all P0/P1 concerns from prior reviews are addressed and the two new findings are minor style inconsistencies.

Every previously flagged P0/P1 issue has a clear fix in this diff. The two new findings are P2: a null-vs-empty-object inconsistency in layout.set and a hardcoded provider string gate. Neither causes data loss or incorrect behavior in the current provider set.

apps/desktop/src/main/services/adeActions/registry.ts — layout.set(null) inconsistency; apps/desktop/src/main/services/chat/agentChatService.ts — hardcoded "claude" provider check.

Important Files Changed

Filename Overview
apps/ade-cli/src/cli.ts Major expansion of help text, hasHelpFlag/VALUE_CARRIER_FLAGS routing, worktree-aware root discovery, and command aliases. --text no longer in VALUE_CARRIER_FLAGS, fixing the prior help-swallowing bug.
apps/desktop/src/main/services/adeActions/registry.ts New domains (graph_state, layout, tiling_tree, keybindings, github, budget, etc.), role-based access control helpers (isCtoOnlyAdeAction/callerHasRoleAtLeast), and inline domain-service builders. layout.set(null) stores {} instead of clearing, inconsistent with tiling_tree.set and graph_state.set.
apps/desktop/src/main/services/prs/prService.ts Adds getRowByLocator, repoPrKey, findExistingPrForBranch for safer duplicate-PR recovery; upsertRow returns the upserted ID; cascade deletes gated behind allowRepoPrAdoption; #123 PR locator parsing; getIntegrationLaneOrigin null-safe guard; listAll now accepts laneId filter.
apps/desktop/src/main/services/orchestrator/promptInspector.ts Replaces hardcoded guidance strings with ADE_CLI_AGENT_GUIDANCE constant; removes the duplicate "ADE TOOLING" push that previously resulted in two copies of the guidance in every worker prompt.
apps/desktop/src/main/services/orchestrator/baseOrchestratorAdapter.ts Injects ADE_CLI_AGENT_GUIDANCE into startup-command override prompt; startupCommandPreview now logs the augmented prompt (not the bare override), fixing prior debug-visibility bug.
apps/desktop/src/main/services/orchestrator/orchestratorService.ts Adds comment clarifying that ADE_CLI_AGENT_GUIDANCE must NOT be prepended to the worker prompt file because buildFullPrompt already injects it via the system prompt for Claude workers.
apps/desktop/src/shared/adeCliGuidance.ts New shared constant file exporting ADE_CLI_AGENT_GUIDANCE (multi-line) and ADE_CLI_INLINE_GUIDANCE (single-line) to consolidate ADE CLI guidance strings across agents, inspector, and chat service.
apps/desktop/src/main/services/chat/agentChatService.ts Switches ADE tooling guidance to the shared constant; adds capability-based guidance injection guard (providerHasPersistentGuidance) using a hardcoded "claude" provider name comparison that may miss future providers.
apps/desktop/src/main/main.ts Moves autoUpdateService creation to before initContextForProjectRoot so all RPC runtimes capture a live reference; exposes many additional services on rpcRuntime to match the expanded ADE_ACTION_ALLOWLIST.
apps/ade-cli/src/adeRpcServer.ts Uses ADE_ACTION_DOMAIN_NAMES const instead of hardcoded enum arrays; wires isCtoOnlyAdeAction into both list_ade_actions (filter) and run_ade_action (enforcement) handlers; create_pr_from_lane title no longer required.
apps/desktop/src/shared/types/prs.ts Minor type addition to the PR types shared module.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ade CLI invocation] --> B{hasHelpFlag?}
    B -- yes --> C[Return HELP_BY_COMMAND text]
    B -- no --> D{Primary command}
    D --> E[prs create / link]
    D --> F[actions run]
    D --> G[git / lanes / files / ...]

    E --> H[prService.createFromLane]
    H --> I{GitHub API create PR}
    I -- success --> J[upsertRow - returns prId]
    I -- duplicate error --> K[findExistingPrForBranch]
    K --> L[Adopt existing PR row
allowRepoPrAdoption:true]
    L --> J
    J --> M[markHotRefresh + refreshOne]

    F --> N{isCtoOnlyAdeAction?}
    N -- yes, caller not CTO --> O[Throw permission error]
    N -- no / caller is CTO --> P[Invoke domain service method]

    subgraph ADE_CLI_GUIDANCE [Shared guidance injection]
        Q[ADE_CLI_AGENT_GUIDANCE constant]
        Q --> R[baseOrchestratorAdapter - startup override prompt]
        Q --> S[promptInspector - worker base guidance]
        Q --> T[agentChatService - non-Claude providers only]
        Q --> U[ctoStateService - CTO doctrine]
    end
Loading

Comments Outside Diff (5)

  1. apps/desktop/src/main/services/orchestrator/promptInspector.ts, line 178-225 (link)

    P1 Duplicate ADE_CLI_AGENT_GUIDANCE block in every worker prompt

    buildWorkerBaseGuidance unconditionally calls sections.push(...) with ADE_CLI_AGENT_GUIDANCE at two separate points (lines 178 and 213). Both execute for every worker, so the resulting prompt contains the ## ADE CLI heading and its two-line body verbatim twice.

    Before this PR the two slots held distinct strings:

    • slot 1 → "ADE CLI: In terminal-capable sessions, use the bundled \ade` command…"`
    • slot 2 → "ADE TOOLING: Use ADE's action surface or the \ade` CLI for team collaboration commands when available."`

    After the consolidation both slots emit the same constant. The worker identity / key-actions block in the second sections.push is still unique and correct, but it is now prefixed with a redundant ## ADE CLI section that will appear a second time in the prompt.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/orchestrator/promptInspector.ts
    Line: 178-225
    
    Comment:
    **Duplicate `ADE_CLI_AGENT_GUIDANCE` block in every worker prompt**
    
    `buildWorkerBaseGuidance` unconditionally calls `sections.push(...)` with `ADE_CLI_AGENT_GUIDANCE` at two separate points (lines 178 and 213). Both execute for every worker, so the resulting prompt contains the `## ADE CLI` heading and its two-line body verbatim twice.
    
    Before this PR the two slots held *distinct* strings:
    - slot 1 → `"ADE CLI: In terminal-capable sessions, use the bundled \`ade\` command…"`
    - slot 2 → `"ADE TOOLING: Use ADE's action surface or the \`ade\` CLI for team collaboration commands when available."`
    
    After the consolidation both slots emit the same constant. The worker identity / key-actions block in the second `sections.push` is still unique and correct, but it is now prefixed with a redundant `## ADE CLI` section that will appear a second time in the prompt.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/desktop/src/main/services/prs/prService.ts, line 2541-2559 (link)

    P2 Regex heuristic for duplicate-PR detection may over-match

    The condition used to trigger findExistingPrForBranch is:

    /pull request already exists|already exists|validation failed/i.test(msg)

    "validation failed" is a broad phrase that GitHub also returns for unrelated schema violations (e.g. invalid base branch, missing required field). If such an error is misidentified as a duplicate-PR error, the code will silently look up and adopt an existing PR for the branch — potentially mapping the wrong PR to the lane. A narrower pattern such as /pull request already exists/i (or checking the GitHub 422 status code) would reduce the false-positive surface.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/prs/prService.ts
    Line: 2541-2559
    
    Comment:
    **Regex heuristic for duplicate-PR detection may over-match**
    
    The condition used to trigger `findExistingPrForBranch` is:
    
    ```typescript
    /pull request already exists|already exists|validation failed/i.test(msg)
    ```
    
    `"validation failed"` is a broad phrase that GitHub also returns for unrelated schema violations (e.g. invalid base branch, missing required field). If such an error is misidentified as a duplicate-PR error, the code will silently look up and adopt an existing PR for the branch — potentially mapping the wrong PR to the lane. A narrower pattern such as `/pull request already exists/i` (or checking the GitHub `422` status code) would reduce the false-positive surface.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  3. apps/desktop/src/main/services/prs/prService.ts, line 1308-1330 (link)

    P2 findExistingPrForBranch typed as Promise<any | null>

    The function and its fetchAllPages<any> call use any, which drops type safety on the returned GitHub PR object. The rest of prService.ts accesses .state, .draft, .number, etc. from the result with runtime asString/Number guards — that pattern works — but the signature makes it impossible for callers to know what shape they're consuming without reading the implementation.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/prs/prService.ts
    Line: 1308-1330
    
    Comment:
    **`findExistingPrForBranch` typed as `Promise<any | null>`**
    
    The function and its `fetchAllPages<any>` call use `any`, which drops type safety on the returned GitHub PR object. The rest of `prService.ts` accesses `.state`, `.draft`, `.number`, etc. from the result with runtime `asString`/`Number` guards — that pattern works — but the signature makes it impossible for callers to know what shape they're consuming without reading the implementation.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  4. apps/desktop/src/main/services/orchestrator/promptInspector.ts, line 178-225 (link)

    P1 Duplicate ADE_CLI_AGENT_GUIDANCE in buildWorkerBaseGuidance

    ADE_CLI_AGENT_GUIDANCE is pushed into sections at two separate points in this function (lines 178 and 213). Both calls execute unconditionally for every worker, so the inspector output contains the ## ADE CLI heading and body verbatim twice. The inspector therefore shows a longer prompt than what buildFullPrompt in baseOrchestratorAdapter.ts actually sends to workers (where the guidance only appears once), making the inspector inaccurate for debugging.

    The second push (lines 213–225) is what contains the unique "worker identity" / key-actions text; ADE_CLI_AGENT_GUIDANCE should not prefix that block as well.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/orchestrator/promptInspector.ts
    Line: 178-225
    
    Comment:
    **Duplicate `ADE_CLI_AGENT_GUIDANCE` in `buildWorkerBaseGuidance`**
    
    `ADE_CLI_AGENT_GUIDANCE` is pushed into `sections` at two separate points in this function (lines 178 and 213). Both calls execute unconditionally for every worker, so the inspector output contains the `## ADE CLI` heading and body verbatim twice. The inspector therefore shows a longer prompt than what `buildFullPrompt` in `baseOrchestratorAdapter.ts` actually sends to workers (where the guidance only appears once), making the inspector inaccurate for debugging.
    
    The second push (lines 213–225) is what contains the unique "worker identity" / key-actions text; `ADE_CLI_AGENT_GUIDANCE` should not prefix that block as well.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  5. apps/desktop/src/main/services/prs/prService.ts, line 1154-1215 (link)

    P1 Cascade deletes silently strip group memberships on cross-lane reassignment

    upsertRow now falls back to getRowForRepoPr when getRowForLane finds nothing, and when it does match a PR that belongs to a different lane it immediately deletes that PR's pr_group_members rows and nullifies integration_proposals references. This path fires from both createFromLane (duplicate-recovery) and linkToLane.

    The linkToLane case is the risky one: if PR #X was already in a merge queue under lane A and a caller links it to lane B, the queue membership is silently destroyed with no notification or rollback. The caller has no signal that anything was cleared.

    Consider guarding the cascade deletes with a check that verifies neither pr_group_members nor active integration_proposals exist before proceeding, or at minimum logging a warning when rows are removed.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/prs/prService.ts
    Line: 1154-1215
    
    Comment:
    **Cascade deletes silently strip group memberships on cross-lane reassignment**
    
    `upsertRow` now falls back to `getRowForRepoPr` when `getRowForLane` finds nothing, and when it does match a PR that belongs to a *different* lane it immediately deletes that PR's `pr_group_members` rows and nullifies `integration_proposals` references. This path fires from both `createFromLane` (duplicate-recovery) and `linkToLane`.
    
    The `linkToLane` case is the risky one: if PR #X was already in a merge queue under lane A and a caller links it to lane B, the queue membership is silently destroyed with no notification or rollback. The caller has no signal that anything was cleared.
    
    Consider guarding the cascade deletes with a check that verifies neither `pr_group_members` nor active `integration_proposals` exist before proceeding, or at minimum logging a warning when rows are removed.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/adeActions/registry.ts
Line: 473-482

Comment:
**`layout.set(null)` stores `{}` instead of clearing — inconsistent with sibling services**

The error message on line 466 says "Pass an explicit null to clear", but when `rawLayout === null`, the code stores `{}` (empty object) rather than `null`. The two sibling services (`tiling_tree.set` and `graph_state.set`) both store `null` directly via `runtime.db.setJson(key, null)`, making the behavior of "clearing" consistent. For `layout`, calling `.set({ layoutId, layout: null })` leaves a `{}` record behind rather than clearing the key, which contradicts the documented intent.

```typescript
// Suggested fix: pass null through to the db directly (like tiling_tree/graph_state do)
if (rawLayout === null) {
  runtime.db.setJson(`dock_layout:${layoutId}`, null);
  return { layoutId, layout: {} };
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/main/services/chat/agentChatService.ts
Line: 10912-10915

Comment:
**Hardcoded provider string for guidance-injection gate**

`providerHasPersistentGuidance` is determined by `managed.session.provider === "claude"`. If a new provider with a persistent system prompt is added (e.g., Gemini with a native system-prompt slot), this gate will silently inject `ADE_CLI_AGENT_GUIDANCE` into its first user turn as well, creating the same redundancy that this code was written to prevent. Consider abstracting this into a provider-capability flag rather than a string comparison.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (9): Last reviewed commit: "ship: iteration 4 (post-rebate) — post-m..." | Re-trigger Greptile

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 23, 2026

@copilot review but do not make fixes

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Apr 23, 2026 11:41pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@arul28 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 43 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 31 minutes and 43 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e7213e76-9c39-4170-8b04-844171d218ee

📥 Commits

Reviewing files that changed from the base of the PR and between 7f88111 and e4d44e3.

📒 Files selected for processing (7)
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/orchestrator/orchestratorService.ts
  • apps/desktop/src/main/services/orchestrator/providerOrchestratorAdapter.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
📝 Walkthrough

Walkthrough

Introduces centralized ADE CLI guidance constants and substantially expands the dynamic ADE action framework with role-based access control, new domains (layout, tiling_tree, graph_state), and enhanced action metadata. Multiple components adopt this shared guidance instead of hardcoding prompts. PR service gains draft description support with optional base branch context. Runtime bootstrapping extends to wire 20+ new optional services. CLI system is refactored with improved help documentation, project root discovery, and PR linking. Comprehensive test coverage added across action execution, CLI validation, and agent prompt expectations.

Changes

Cohort / File(s) Summary
ADE CLI Guidance Centralization
apps/desktop/src/shared/adeCliGuidance.ts, apps/desktop/src/main/services/ai/tools/systemPrompt.ts, apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts, apps/desktop/src/main/services/chat/agentChatService.ts, apps/desktop/src/main/services/chat/agentChatService.test.ts, apps/desktop/src/main/services/cto/ctoStateService.ts, apps/desktop/src/main/services/cto/workerAdapterRuntimeService.ts, apps/desktop/src/main/services/cto/workerAdapterRuntimeService.test.ts, apps/desktop/src/main/services/orchestrator/baseOrchestratorAdapter.ts, apps/desktop/src/main/services/orchestrator/coordinatorAgent.ts, apps/desktop/src/main/services/orchestrator/orchestratorService.ts, apps/desktop/src/main/services/orchestrator/promptInspector.ts
New shared guidance constants replace hardcoded prompts across agent, CTO, orchestrator, and worker services; Claude guidance deduplication logic added to prevent duplicate injection.
Dynamic ADE Action Framework
apps/desktop/src/main/services/adeActions/registry.ts, apps/ade-cli/src/adeRpcServer.ts, apps/ade-cli/src/adeRpcServer.test.ts
Introduces ADE_ACTION_DOMAIN_NAMES and role-based access (AdeActionRole, isCtoOnlyAdeAction, callerHasRoleAtLeast); expands allowlist with new domains (layout, tiling_tree, graph_state, linear_credentials, github, feedback, usage, budget, update); adds domain services with input validation and persistent storage backing; enriched action metadata (name, usage hints); tests validate role enforcement and action metadata.
Runtime Service Wiring
apps/ade-cli/src/bootstrap.ts, apps/desktop/src/main/main.ts
Extended AdeRuntime type with 20+ new optional service properties (keybindings, onboarding, AI integration, PR summary, worker services, Linear credentials, sync/automation/budget services); wired multiple services in bootstrap and rpcRuntime construction.
PR Service Enhancements
apps/desktop/src/main/services/prs/prService.ts, apps/desktop/src/shared/types/prs.ts, apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
Added baseBranch support to draft PR descriptions; improved repo-scoped PR lookups; enhanced PR upsert to handle duplicate PRs on GitHub failures and normalize by both lane ID and repo+number; added hot-refresh signaling; updated listAll to filter by lane ID when specified.
CLI System Refactoring
apps/ade-cli/src/cli.ts, apps/ade-cli/src/cli.test.ts
Expanded help system with new commands and detailed argument documentation; rewrote project root discovery (findAdeManagedWorktreeRoot, findProjectRoots, resolveRoots); added support for help flag in value contexts via VALUE_CARRIER_FLAGS; enhanced PR linking to accept multiple identifier formats; added usage hint output and GitHub PR field preference; extensive test coverage for JSON contract validation, root discovery, and help rendering.
Test Infrastructure
apps/desktop/src/renderer/components/terminals/TerminalView.test.tsx
Refactored DOM-fallback test to use real timers instead of fake timers for dynamic import settlement; replaced iterative timer flushing with explicit waitFor assertion.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

desktop

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: ADE CLI guidance consolidation, PR workflow improvements (duplicate recovery, auto-draft), and integration lane ownership fixes.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/cli-prs-fixes-747d7096

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 and usage tips.

Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 3 comments

Comment thread apps/ade-cli/src/cli.ts Outdated
const projectRoot = options.projectRoot
?? (process.env.ADE_PROJECT_ROOT?.trim() ? path.resolve(process.env.ADE_PROJECT_ROOT.trim()) : findProjectRoot(process.cwd()));
?? (process.env.ADE_PROJECT_ROOT?.trim() ? path.resolve(process.env.ADE_PROJECT_ROOT.trim()) : discovered.projectRoot);
const workspaceRoot = options.workspaceRoot
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

This change stops workspaceRoot from following an explicit --project-root / ADE_PROJECT_ROOT override:

// apps/ade-cli/src/cli.ts
const projectRoot = options.projectRoot
  ?? (process.env.ADE_PROJECT_ROOT?.trim() ? path.resolve(process.env.ADE_PROJECT_ROOT.trim()) : discovered.projectRoot);
const workspaceRoot = options.workspaceRoot
  ?? (process.env.ADE_WORKSPACE_ROOT?.trim() ? path.resolve(process.env.ADE_WORKSPACE_ROOT.trim()) : discovered.workspaceRoot);

I verified that createConnection() passes both values into createAdeRuntime(), and adeRpcServer later uses runtime.workspaceRoot as the default filesystem/process workspace when no lane-specific worktree is selected. That means running ade --project-root /repoA ... from inside /repoB/.ade/worktrees/... will persist state and socket/layout lookups against repo A while file/process/agent operations default to repo B. Default workspaceRoot back to the resolved projectRoot whenever the project root was explicitly overridden, and only use discovered.workspaceRoot for pure auto-discovery.

`
update pull_requests
set repo_owner = ?,
set lane_id = ?,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When createFromLane()/linkToLane() reuses an existing PR row by repo_owner/repo_name/github_pr_number, this new update also rewrites that row's lane_id, but nothing updates the matching pr_group_members.lane_id rows. ts // apps/desktop/src/main/services/prs/prService.ts update pull_requests set lane_id = ?, repo_owner = ?, repo_name = ?, github_pr_number = ?, That leaves the PR record attached to the new lane while queue/integration membership, archive guards, and lane deletion cleanup still key off the old lane id (resolveQueueRebaseOverride() queries m.lane_id, and laneService.archive/delete also read/delete pr_group_members by lane_id). The result is inconsistent workflow state: the adopted lane loses its queue/integration context, the old lane can stay falsely blocked from archive/delete, and deleting the new lane can leave stale group-member rows behind. Fix by updating pr_group_members.lane_id for the reused PR in the same transaction, or by avoiding lane_id reassignment when reusing an existing PR row.

memoryService,
ctoStateService,
workerAgentService,
workerBudgetService,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The expanded AdeRuntime contract now includes workerHeartbeatService, workerTaskSessionService, and linearCredentialService, and createHeadlessLinearServices() already constructs all three, but this return object still stops at workerBudgetService before jumping straight to prService:

// apps/ade-cli/src/bootstrap.ts
workerAgentService,
workerBudgetService,
prService: headlessLinearServices.prService,
fileService: headlessLinearServices.fileService,
flowPolicyService: headlessLinearServices.flowPolicyService,

Because @apps/ade-cli/src/cli.ts uses createAdeRuntime() for headless ade actions calls, and @apps/ade-cli/src/adeRpcServer.ts only lists/calls domains when the corresponding runtime service is present, the worker_heartbeat, worker_task_session, and linear_credentials domains remain unavailable in CLI mode even though this PR is wiring them into the desktop socket runtime. Forward those already-created services from headlessLinearServices into the returned AdeRuntime so headless and embedded RPC stay in sync.

Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 7 comments

Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
},
set(args?: Record<string, unknown>): { layoutId: string; layout: DockLayout } {
const layoutId = assertNonEmptyString(args?.layoutId, "layoutId");
const layout = clampDockLayout(safeObject(args?.layout));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new synthetic layout/tiling_tree/graph_state ADE action services do not enforce the same required payload contract as the existing IPC APIs. In this file, layout.set turns a missing layout into {}, while tiling_tree.set and graph_state.set turn missing tree/state into null, then persist those values immediately. That means a malformed CLI call like run_ade_action layout.set --input-json '{"layoutId":"main"}' now erases the saved UI state instead of failing fast. The existing desktop contract requires these fields (preload exposes set(layoutId, layout), set(layoutId, tree), set(projectId, state)), so the CLI wrapper should reject absent payloads rather than defaulting to destructive writes. ts // apps/ade-cli/src/adeRpcServer.ts const layout = clampDockLayout(safeObject(args?.layout)); runtime.db.setJson(`dock_layout:${layoutId}`, layout); const tree = Object.prototype.hasOwnProperty.call(args ?? {}, "tree") ? args?.tree : null; runtime.db.setJson(`tiling_tree:${layoutId}`, tree); Validate layout, tree, and state explicitly before persisting so bad input returns invalidParams instead of wiping saved state.

Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
let title = asOptionalTrimmedString(toolArgs.title);
let body = typeof toolArgs.body === "string" ? toolArgs.body : null;
if (!title || body == null) {
const draft = await prSvc.draftDescription({ laneId });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When create_pr_from_lane is called with a non-default baseBranch and the caller omits title or body, this handler drafts the PR text without passing that override into the drafting path:

// apps/ade-cli/src/adeRpcServer.ts
if (!title || body == null) {
  const draft = await prSvc.draftDescription({ laneId });
  title = title || asOptionalTrimmedString(draft.title) || `PR for ${laneId}`;
  body = body ?? asOptionalTrimmedString(draft.body) ?? "";
}

prService.draftDescription only accepts { laneId } and computes its summary from lane.baseRef...HEAD, while createFromLane later honors the explicit baseBranch override. The result is a PR whose generated title/body describe a different diff than the actual target branch, which is user-visible and can mislead review/merge decisions for non-default-base workflows. Fix by threading baseBranch through DraftPrDescriptionArgs/draftDescription, or by skipping auto-drafting when an override base is supplied.

Comment thread apps/ade-cli/src/cli.ts
work: "chat",
agents: "agent",
test: "tests",
computer: "proof",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new alias table advertises computer, artifact, and setting, but buildCliPlan only uses that table for help lookup; the execution dispatch below still routes only proof | computer-use | artifacts and settings | config. That means commands like ade computer status, ade artifact list, and ade setting get now fail even though the diff exposes them as supported aliases. ```ts
// apps/ade-cli/src/cli.ts
test: "tests",
computer: "proof",
"computer-use": "proof",
artifact: "proof",
artifacts: "proof",
setting: "settings",

Comment thread apps/ade-cli/src/cli.ts Outdated
?? (process.env.ADE_PROJECT_ROOT?.trim() ? path.resolve(process.env.ADE_PROJECT_ROOT.trim()) : discovered.projectRoot);
const workspaceRoot = options.workspaceRoot
?? (process.env.ADE_WORKSPACE_ROOT?.trim() ? path.resolve(process.env.ADE_WORKSPACE_ROOT.trim()) : projectRoot);
?? (process.env.ADE_WORKSPACE_ROOT?.trim() ? path.resolve(process.env.ADE_WORKSPACE_ROOT.trim()) : discovered.workspaceRoot);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When the CLI is invoked from inside .ade/worktrees/<lane> and the caller overrides only --project-root, resolveRoots() now mixes two different defaults: it takes the requested project root, but still keeps the discovered worktree from the current cwd as workspaceRoot.

// apps/ade-cli/src/cli.ts
const discovered = findProjectRoots(process.cwd());
const projectRoot = options.projectRoot
  ?? (process.env.ADE_PROJECT_ROOT?.trim() ? path.resolve(process.env.ADE_PROJECT_ROOT.trim()) : discovered.projectRoot);
const workspaceRoot = options.workspaceRoot
  ?? (process.env.ADE_WORKSPACE_ROOT?.trim() ? path.resolve(process.env.ADE_WORKSPACE_ROOT.trim()) : discovered.workspaceRoot);

createConnection() passes both values into createAdeRuntime(), and the RPC layer falls back to runtime.workspaceRoot for lane-less operations, so commands can read/write the current lane worktree while talking to a different project's ADE database/socket. Default workspaceRoot to the resolved projectRoot whenever --project-root/ADE_PROJECT_ROOT is supplied and there is no explicit workspace override.

Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
"savePolicy",
"validatePolicy",
],
linear_credentials: [
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

These new allowlist entries widen run_ade_action from project-scoped automation into account/app administration. run_ade_action is still part of the base tool set returned to agent sessions, and the updated tests explicitly initialize an agent caller and assert that github and update are visible, so a worker can now invoke methods like linear_credentials.setToken, github.clearToken, or update.quitAndInstall. That lets an LLM session revoke stored credentials or trigger an app restart/install, which is outside lane/PR workflow scope and can disrupt the operator’s environment. Restrict these domains/actions to trusted roles (external/cto) or add a per-domain authorization check before dispatch.

// apps/ade-cli/src/adeRpcServer.ts
linear_credentials: [
  "clearOAuthClientCredentials",
  "clearToken",
  "getStatus",
  "setOAuthClientCredentials",
  "setOAuthToken",

const now = nowIso();
const existing = getRowForLane(summary.laneId);
const existing = getRowForLane(summary.laneId)
?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

upsertRow now reuses an existing PR row by repo/PR number and reassigns it to the new lane, but it never clears or rewrites that row's pr_group_members entries.

// apps/desktop/src/main/services/prs/prService.ts
const existing = getRowForLane(summary.laneId)
  ?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber);
...
update pull_requests
   set lane_id = ?,

That leaves the reused pr_id attached to the old queue/integration group, and later code still resolves workflow membership by pr_group_members.pr_id plus the cached member lane_id (getMergeContext, landQueueNext, and the GitHub snapshot). In practice, adopting/linking a PR that was previously tracked on lane A makes the new lane B PR still appear in—and potentially be acted on from—lane A's old group. When reusing a row for a different lane, clear or migrate its pr_group_members rows atomically before returning the reused pr_id.

throw new Error(
`Failed to create pull request for "${headBranch}" → "${baseBranch}": ${msg}`
);
const existingPr = /pull request already exists|already exists|validation failed/i.test(msg)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

The duplicate-PR recovery path is keyed off a generic message match instead of a duplicate-specific error.

// apps/desktop/src/main/services/prs/prService.ts
const msg = err instanceof Error ? err.message : String(err);
const existingPr = /pull request already exists|already exists|validation failed/i.test(msg)
  ? await findExistingPrForBranch(repo, headBranch, baseBranch).catch((lookupError) => {

GitHub returns 422 Validation Failed for unrelated create-PR errors too (for example invalid head/base data), so this branch can silently convert a real create failure into "success" by adopting whatever existing PR matches the branch. That leaves the lane bound to the wrong PR and hides the actual user error. Only trigger fallback for the duplicate-PR case, or parse the structured 422 error payload/status before adopting an existing PR.

Suggested change
const existingPr = /pull request already exists|already exists|validation failed/i.test(msg)
const existingPr = /pull request already exists/i.test(msg)

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 23, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 7 comments

Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
"startQueue",
],
pr_summary: ["ensureSummary", "getSummary", "regenerateSummary"],
flow_policy: [
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

This allowlist now exposes privileged flow-policy and Linear sync controls through run_ade_action, even though the same operations are already guarded behind callerCtx.role === "cto" in the dedicated tool handlers I verified at lines 4904-4983. ts // apps/ade-cli/src/adeRpcServer.ts flow_policy: [ "diffPolicyPaths", "getPolicy", "listRevisions", "normalizePolicy", "rollbackRevision", "savePolicy", The diff also adds linear_sync.runSyncNow/resolveQueueItem, linear_ingress.ensureRelayWebhook, and linear_routing.simulateRoute, but run_ade_action itself has no role check and is already callable from agent sessions (covered by the existing agent-path tests in @apps/ade-cli/src/adeRpcServer.test.ts). That means worker/agent callers can now mutate ADE's flow policy, drive sync runs, resolve queue items, and register the webhook through the generic action bridge, bypassing the intended CTO-only boundary. Remove these privileged methods from the dynamic allowlist or add equivalent role enforcement in run_ade_action for the affected domains.

sections.push(
[
"ADE TOOLING: Use ADE's action surface or the `ade` CLI for team collaboration commands when available.",
ADE_CLI_AGENT_GUIDANCE,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

buildWorkerBaseGuidance() now pushes ADE_CLI_AGENT_GUIDANCE in two unconditional sections, so the inspector prompt contains two identical ## ADE CLI blocks even though the actual worker prompt builder only injects that guidance once. That makes the inspector inaccurate for debugging and wastes prompt budget in the preview. Keep the first shared guidance block and remove it from the later worker-identity section. ```ts
// apps/desktop/src/main/services/orchestrator/promptInspector.ts
[
ADE_CLI_AGENT_GUIDANCE,
"Your worker identity (mission, run, step, attempt IDs) is automatically resolved — you don't need to pass IDs to observation tools.",
"Key actions available:",

: null,
buildExecutionModeDirective(executionMode, managed.session.provider),
buildClaudeInteractionModeDirective(managed.session.interactionMode, managed.session.provider),
shouldInjectLaneDirective ? ADE_CLI_AGENT_GUIDANCE : null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

For normal Claude sessions, buildClaudeV2SessionOpts() already appends ADE_CLI_AGENT_GUIDANCE to the session system prompt, and this new branch prepends the same block again into the first user message whenever the lane directive is injected. The result is a duplicated ## ADE CLI section on every first Claude turn in a lane-backed chat, which wastes context and over-weights the same instruction in a path that previously had only one copy. Limit this injection to providers that lack an equivalent persistent system prompt, or skip it for Claude. ```ts
// apps/desktop/src/main/services/chat/agentChatService.ts
composeLaunchDirectives(trimmed, [
shouldInjectLaneDirective ? buildLaneWorktreeDirective(...) : null,
buildExecutionModeDirective(executionMode, managed.session.provider),
buildClaudeInteractionModeDirective(managed.session.interactionMode, managed.session.provider),
shouldInjectLaneDirective ? ADE_CLI_AGENT_GUIDANCE : null,
])

Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
linear_sync: ["getDashboard", "getRunDetail", "listQueue", "resolveQueueItem", "runSyncNow"],
linear_ingress: ["ensureRelayWebhook", "getStatus", "listRecentEvents"],
linear_routing: ["simulateRoute"],
github: ["clearToken", "getRepoOrThrow", "getStatus", "setToken"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

This diff adds sensitive mutators to the generic ADE action allowlist, for example:

// apps/ade-cli/src/adeRpcServer.ts
github: ["clearToken", "getRepoOrThrow", "getStatus", "setToken"],
feedback: ["list", "prepareDraft", "submitPreparedDraft"],
usage: ["forceRefresh", "getUsageSnapshot", "poll", "start", "stop"],
budget: ["checkBudget", "getConfig", "getCumulativeUsage", "getGlobalCumulativeUsage", "recordUsage", "updateConfig"],
update: ["checkForUpdates", "dismissInstalledNotice", "getSnapshot", "quitAndInstall"],

run_ade_action is a base tool with no role-specific gate, and listToolSpecsForSession includes all base tools for agent and even external sessions, so these newly allowlisted entries let untrusted callers overwrite stored GitHub/Linear credentials, run the packaged CLI installer / mutate process.env via ade_cli, or force quitAndInstall() from the generic RPC surface. That turns settings/update operations into agent-callable actions and violates the project requirement to enforce policy in code. Remove privileged mutators from the generic allowlist or add explicit role checks inside run_ade_action for sensitive domains/actions so only trusted UI/CTO paths can invoke them.

Comment thread apps/ade-cli/src/cli.ts Outdated
function hasHelpFlag(args: string[]): boolean {
const terminatorIndex = args.indexOf("--");
const searchable = terminatorIndex >= 0 ? args.slice(0, terminatorIndex) : args;
return searchable.includes("--help") || searchable.includes("-h");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

hasHelpFlag() now short-circuits on any raw -h/--help token before the command-specific parser can consume it as a value. That breaks legitimate inputs such as ade chat send <session> --text --help or ade files write <path> --text --help, which previously would pass the literal string through readValue() but now return help instead of executing. Fix by making help detection aware of option arity/value positions, or by handling --help inside each command parser rather than scanning the raw argv up front.

// apps/ade-cli/src/cli.ts
function hasHelpFlag(args: string[]): boolean {
  const terminatorIndex = args.indexOf("--");
  const searchable = terminatorIndex >= 0 ? args.slice(0, terminatorIndex) : args;
  return searchable.includes("--help") || searchable.includes("-h");
}

ctoStateService,
workerAgentService,
workerBudgetService,
prService: headlessLinearServices.prService,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

createAdeRuntime() widens AdeRuntime for new headless-only domains, but this return block still forwards only a subset of headlessLinearServices, so linearCredentialService, workerTaskSessionService, and workerHeartbeatService are dropped on the floor even though createHeadlessLinearServices() constructs them. adeRpcServer.ts publishes linear_credentials, worker_task_session, and worker_heartbeat actions directly from runtime.*Service and filters out null services, so these CLI capabilities remain unavailable in headless mode despite this PR's registration work. Wire the already-created services through the returned runtime object (and any other intended headless services) so the RPC layer can actually expose them.

// apps/ade-cli/src/bootstrap.ts
workerBudgetService,
prService: headlessLinearServices.prService,
fileService: headlessLinearServices.fileService,
flowPolicyService: headlessLinearServices.flowPolicyService,
linearDispatcherService: headlessLinearServices.linearDispatcherService,

// lane, so `preferred_integration_lane_id` stays unset. When merging into an existing lane, that
// lane id is stored here (and normally matches `integration_lane_id`). Inferring "adopted" from
// `preferred === integration` alone is unsafe because merge-into and integration ids can differ.
if (!preferredIntegrationLaneId) return "ade-created";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

// apps/desktop/src/main/services/prs/prService.ts
const preferredIntegrationLaneId = asString(row.preferred_integration_lane_id).trim() || null;
if (!preferredIntegrationLaneId) return "ade-created";
return "adopted";

This heuristic now treats any non-null preferred_integration_lane_id as proof that the integration lane was adopted, but commitIntegration() can persist a new preferredIntegrationLaneId even when it keeps an existing scratch integration_lane_id (existingIntegrationLaneId is used at lines 3844-3848, then the override is still written back at lines 3908-3911). After that row shape, cleanupIntegrationWorkflow() and deleteIntegrationProposal() call isAdeOwnedIntegrationLane() and will skip archiving/deleting the ADE-created scratch lane because origin is misreported as "adopted". Record explicit provenance, or at minimum avoid treating mismatched preferred/integration ids as adopted.

arul28 added 3 commits April 23, 2026 18:19
- Expand ADE CLI RPC, bootstrap, and command surface; add shared adeCliGuidance for agent prompts.

- Wire desktop orchestrator, chat, CTO, and main for PR/CLI behavior.

- Derive integration scratch lanes from missing merge-into id; avoid misclassifying adopted lanes in cleanup.

Made-with: Cursor
When --project-root or ADE_PROJECT_ROOT fixes the project root, default workspaceRoot to that path unless ADE_WORKSPACE_ROOT/--workspace-root is set (fixes capy-ai review thread).

Made-with: Cursor
- Resolve adeRpcServer/bootstrap/cli conflicts with adeActions registry + headless services.

- prService: clear pr_group_members when PR row moves lanes; tighter duplicate-PR message match.

- CLI: route computer/artifact/setting aliases; safer hasHelpFlag after value-taking flags.

- Orchestrator: log augmented startup preview when ADE CLI guidance is prepended.

- main.ts: drop duplicate AdeRuntime passthrough keys.

- TerminalView test: real timers + waitFor for webgl fallback flake.

Made-with: Cursor
Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 4 comments

Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
"pr",
"tests",
"chat",
"keybindings",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This change expands the list_ade_actions / run_ade_action schemas with domains such as:

// apps/ade-cli/src/adeRpcServer.ts
"keybindings",
"agent_tools",
"ade_cli",
"layout",
"tiling_tree",
"graph_state",

but the actual ADE action source of truth in apps/desktop/src/main/services/adeActions/registry.ts still defines the older AdeActionDomain union, ADE_ACTION_ALLOWLIST, and getAdeActionDomainServices() map only through automations/issue. I verified that list_ade_actions and run_ade_action both read from that registry, so these newly documented domains will either return no actions or fail with Domain '...' is unavailable in this runtime. That means the main feature of this group is nonfunctional for every newly listed domain. Either register each new domain in the shared action registry (plus tests) or remove it from both CLI schemas until backend support exists.

Comment thread apps/ade-cli/src/cli.ts Outdated
function hasHelpFlag(args: string[]): boolean {
const terminatorIndex = args.indexOf("--");
const searchable = terminatorIndex >= 0 ? args.slice(0, terminatorIndex) : args;
const valueCarrierFlags = new Set([
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

hasHelpFlag() now short-circuits to command help before any subcommand parser runs, but it only treats a small hard-coded set of preceding flags as value carriers. In this same file, several handlers consume other value-bearing flags that are missing from the exemption list — for example -q in files search/memory search, --command in tests run/shell start, --workspace, --thread, --stack, and --app — so inputs like ade files search -q --help or ade tests run --lane lane-1 --command --help will now print help instead of passing the literal value through.

// apps/ade-cli/src/cli.ts
const valueCarrierFlags = new Set([
  "--text",
  "--body",
  "--title",
  "--question",

That is a behavioral regression introduced by the new pre-dispatch help scan. The fix is to derive help detection from the same option readers the command handlers use, or at minimum expand the exemption list to every value-taking flag supported by the CLI surface.

paths: AdeRuntimePaths;
logger: Logger;
db: AdeDb;
keybindingsService?: ReturnType<typeof createKeybindingsService> | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

@apps/ade-cli/src/bootstrap.ts widens AdeRuntime with new headless surfaces such as keybindingsService, onboardingService, usageTrackingService, and autoUpdateService, but createAdeRuntime() still never instantiates or assigns those services when it builds runtime, so a real CLI runtime lacks the objects that the new RPC/action surface expects. I verified that @apps/ade-cli/src/adeRpcServer.ts:4191-4198 resolves run_ade_action directly from the runtime and throws Domain '${domain}' is unavailable in this runtime. when the service is missing, while @apps/ade-cli/src/adeRpcServer.test.ts:3208-3213 and :3242-3248 now expect headless sessions to expose and execute domains like keybindings, onboarding, usage, and update; outside the mocked test fixture those capabilities stay unavailable. ```ts
// apps/ade-cli/src/bootstrap.ts
keybindingsService?: ReturnType | null;
onboardingService?: ReturnType | null;
usageTrackingService?: ReturnType | null;
autoUpdateService?: ReturnType | null;

?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber);
if (existing) {
if (existing.lane_id !== summary.laneId) {
db.run(`delete from pr_group_members where pr_id = ?`, [existing.id]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

upsertRow now reuses an existing pull_requests row for the same GitHub PR and rewrites its lane_id, but on a lane handoff it only clears pr_group_members:

// apps/desktop/src/main/services/prs/prService.ts
const existing = getRowForLane(summary.laneId)
  ?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber);
if (existing.lane_id !== summary.laneId) {
  db.run(`delete from pr_group_members where pr_id = ?`, [existing.id]);
}

That is incomplete because integration_proposals.linked_pr_id and other workflow state still point at the reused pr_id; loadIntegrationWorkflowRows() later dereferences that linked_pr_id through getRow(...) to compute cleanup/history state. If lane B adopts lane A’s PR row, the old integration workflow silently starts following lane B’s PR status, which can mark the wrong workflow merged/closed and drive incorrect cleanup decisions. When reusing a PR row across lanes, clear or migrate the other pr_id-backed workflow rows as well, or avoid mutating a row that is already attached to another workflow.

- adeRpcServer / adeActions registry: role-gate CTO-only methods (linear_credentials
  set/clear, github set/clear token, update.quitAndInstall, flow_policy.save/rollback,
  linear_sync.runSyncNow/resolveQueueItem, linear_ingress.ensureRelayWebhook,
  budget.updateConfig, feedback.submitPreparedDraft, usage.start/stop/forceRefresh)
  so agent-role callers cannot invoke them via run_ade_action.
- layout.set / tiling_tree.set / graph_state.set reject calls with missing payload
  key instead of persisting default {}/null, preventing accidental UI state wipe.
- create_pr_from_lane threads baseBranch through prService.draftDescription so
  auto-drafted title/body describe the actual target branch.
- Integration lane provenance: only flag as "adopted" when
  preferred_integration_lane_id === integration_lane_id; mismatched scratch lanes
  stay "ade-created" so cleanup/delete works.
- promptInspector: dedup ADE_CLI_AGENT_GUIDANCE block.
- agentChatService: skip first-user-message guidance injection for Claude
  sessions (system prompt already carries it).
- cli hasHelpFlag: expand valueCarrierFlags to cover all ~120 value-taking
  long-flags so --help after a pending value flag is treated as the value.

Addresses review comments 3134159150, 3134159153, 3134159164, 3134196314,
3134196316, 3134196319, 3134196322, 3134196340. Fixes 3 failing adeRpcServer
tests around dynamic ADE actions listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 23, 2026

@copilot review but do not make fixes

Comment thread apps/desktop/src/main/services/prs/prService.ts Outdated
Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
"tests",
"chat",
"keybindings",
"agent_tools",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new list_ade_actions/run_ade_action schemas advertise domains such as agent_tools, ade_cli, sync, mission_preflight, worker_budget, and lane_env, but the shared ADE action registry still does not include those domains in AdeActionDomain or getAdeActionDomainServices (verified in @apps/desktop/src/main/services/adeActions/registry.ts). That means calls like ade actions list agent_tools --text are accepted by the tool schema yet return no actions, and run_ade_action on the same domain fails with Domain 'agent_tools' is unavailable, so the CLI now claims support for functionality it cannot actually expose. Align the schema enums with the registry/runtime map, or finish wiring those domains into the shared registry before documenting them here.

// apps/ade-cli/src/adeRpcServer.ts
"keybindings",
"agent_tools",
"ade_cli",
"dev_tools",
"ai",

Comment thread apps/ade-cli/src/cli.ts
return { kind: "execute", label: `coordinator ${toolName}`, steps: [actionCallStep("result", toolName, collectGenericObjectArgs(args))] };
}

const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

hasHelpFlag() now relies on a hard-coded carrier list, but that list still does not match the actual parsers. In the new table

// apps/ade-cli/src/cli.ts
const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
  "-b", "-t",
  "--additional-instructions", "--app", "--arg", "--arg-json", "--arg-value",
  "--arg-value-json", "--args-list-json", "--attempt", "--attempt-id",

several live next-token carriers are missing even though the command handlers consume them via readValue()/readIntOption(): -m (git commit, lane merge, git stash), -q (files search, memory search), --max-rounds / --rounds (prs path-to-merge, prs pipeline), --max-log-bytes, --max-prompt-chars, and --recent-limit. Because runCli() passes those tokens through parseCliArgs() unchanged into buildCliPlan(), inputs like ade git commit -m --help, ade files search -q --help, or ade prs path-to-merge pr-1 --max-rounds --help will still be misclassified as help requests instead of treating --help as the value for that option. Fix by deriving the carrier set from the same option metadata used by readValue() / readIntOption(), or at minimum add every missing value-taking alias and cover them with negative tests.

Comment thread apps/ade-cli/src/cli.ts Outdated
Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 8 comments

linear_ingress: ["ensureRelayWebhook"],
budget: ["updateConfig"],
feedback: ["submitPreparedDraft"],
usage: ["start", "stop", "forceRefresh"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

ADE_ACTION_CTO_ONLY is meant to cover anything that “drives privileged polling”, but the new usage allowlist still exposes poll to every caller role. ts // apps/desktop/src/main/services/adeActions/registry.ts github: ["clearToken", "getRepoOrThrow", "getStatus", "setToken"], feedback: ["list", "prepareDraft", "submitPreparedDraft"], usage: ["forceRefresh", "getUsageSnapshot", "poll", "start", "stop"], I verified in @apps/desktop/src/main/services/usage/usageTrackingService.ts that poll() is the live collector which reads usage windows/costs and is the same routine called by start() and forceRefresh(). Because run_ade_action only checks ADE_ACTION_CTO_ONLY, an agent or orchestrator caller can still invoke usage.poll and trigger the privileged polling path the new gate was supposed to reserve for CTO callers. Add poll to ADE_ACTION_CTO_ONLY.usage (or stop exposing it) so all polling entrypoints are consistently gated.

Suggested change
usage: ["start", "stop", "forceRefresh"],
usage: ["start", "stop", "forceRefresh", "poll"],

Comment on lines +486 to +488
const layout = rawLayout && typeof rawLayout === "object" && !Array.isArray(rawLayout)
? clampDockLayout(rawLayout as Record<string, unknown>)
: {};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new layout ADE action says callers should pass explicit null to clear, but the implementation currently treats any non-object payload as {} and writes that empty layout to the DB. ts // apps/desktop/src/main/services/adeActions/registry.ts const rawLayout = args.layout; const layout = rawLayout && typeof rawLayout === "object" && !Array.isArray(rawLayout) ? clampDockLayout(rawLayout as Record<string, unknown>) : {}; Since run_ade_action accepts untyped JSON, a malformed CLI call like layout: true, layout: 1, or an array will silently wipe the saved layout instead of rejecting invalid input. That is destructive behavior introduced by this bridge, not just numeric clamping. Reject non-object/non-null payloads before setJson, and keep null as the only clear signal.

Suggested change
const layout = rawLayout && typeof rawLayout === "object" && !Array.isArray(rawLayout)
? clampDockLayout(rawLayout as Record<string, unknown>)
: {};
if (rawLayout !== null && (typeof rawLayout !== "object" || Array.isArray(rawLayout))) throw new Error("Expected 'layout' to be an object or null.");
const layout = rawLayout === null ? {}
: clampDockLayout(rawLayout as Record<string, unknown>);

: null,
buildExecutionModeDirective(executionMode, managed.session.provider),
buildClaudeInteractionModeDirective(managed.session.interactionMode, managed.session.provider),
shouldInjectLaneDirective && !providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

ADE_CLI_AGENT_GUIDANCE is now coupled to lane-directive freshness instead of provider capability:

// apps/desktop/src/main/services/chat/agentChatService.ts
shouldInjectLaneDirective
  ? buildLaneWorktreeDirective({ ... })
  : null,
...
shouldInjectLaneDirective && !providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,

For Codex/OpenCode/Cursor, promptText is the only place this guidance is supplied: runTurn(), runCursorTurn(), and sendCodexMessage() forward args.promptText directly, and only Claude gets a persistent system prompt from buildClaudeV2SessionOpts(). After lastLaneDirectiveKey is persisted, subsequent non-Claude turns stop receiving the ADE CLI instructions entirely, which regresses the intended always-available guidance and makes those sessions more likely to report ADE tasks as blocked instead of trying ade first. Inject the shared guidance whenever the provider lacks a persistent copy, independent of whether a lane directive is being re-sent.

Suggested change
shouldInjectLaneDirective && !providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,
!providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,

memoryService,
ctoStateService,
workerAgentService,
workerBudgetService,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

createAdeRuntime() adds many new optional AdeRuntime slots (keybindingsService, onboardingService, feedbackReporterService, usageTrackingService, budgetCapService, autoUpdateService, etc.), but this return object still only wires a handful of additions. getAdeActionDomainServices() reads those runtime fields directly, and list_ade_actions / run_ade_action drop falsy services or throw Domain '<name>' is unavailable in this runtime, so the new CLI domains never become callable in the real headless bootstrap path.

// apps/ade-cli/src/bootstrap.ts
    workerBudgetService,
    githubService: headlessLinearServices.githubService as never,
    workerTaskSessionService: headlessLinearServices.workerTaskSessionService,
    workerHeartbeatService: headlessLinearServices.workerHeartbeatService,
    linearCredentialService: headlessLinearServices.linearCredentialService as never,

Either construct and assign the new headless-safe services here (for example createKeybindingsService({ db }) plus the other intended registrations) or remove their domains from the CLI action surface until they are actually wired.

let title = asOptionalTrimmedString(toolArgs.title);
let body = typeof toolArgs.body === "string" ? toolArgs.body : null;
if (!title || body == null) {
const draft = await prSvc.draftDescription({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When create_pr_from_lane is called without an explicit baseBranch, the new draft flow asks prService.draftDescription to infer the PR body from lane.baseRef, but prService.createFromLane creates the actual PR against resolveStableLaneBaseBranch(...). Those are not equivalent for stacked lanes — @apps/desktop/src/shared/laneBaseResolution.test.ts documents a child lane whose stored baseRef is main while the stable PR base is the parent branch. That means this new path can generate a title/body describing changes against main and then immediately open the PR against the parent lane instead. Use the same stable-base resolution before drafting and pass that branch into draftDescription so the generated content matches the PR target.

// apps/ade-cli/src/adeRpcServer.ts
    if (!title || body == null) {
      const draft = await prSvc.draftDescription({
        laneId,
        ...(baseBranch ? { baseBranch } : {}),
      });

const now = nowIso();
const existing = getRowForLane(summary.laneId);
const existing = getRowForLane(summary.laneId)
?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

upsertRow() now adopts any existing row for the same repo/PR number before insert, then immediately strips its queue/workflow links if that row belongs to another lane.

// apps/desktop/src/main/services/prs/prService.ts
const existing = getRowForLane(summary.laneId)
  ?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber);
if (existing.lane_id !== summary.laneId) {
  db.run(`delete from pr_group_members where pr_id = ?`, [existing.id]);
  db.run(`update integration_proposals set linked_pr_id = null where linked_pr_id = ?`, [existing.id]);
}

Because linkToLane() always flows through upsertRow(), linking a PR that is already attached to another active lane now silently reassigns lane_id, drops its pr_group_members, and detaches any integration workflow that referenced it. This is a behavior change from “link this lane to that PR” to “move that PR record away from the other lane,” which corrupts the original lane’s state. Restrict repo+number adoption to the duplicate-create recovery path, or require an explicit adoption flag plus source-lane safety checks before mutating lane_id.

if (candidates.length === 0) return null;
const open = candidates.find((candidate) => {
const state = asString(candidate?.state).toLowerCase();
return state === "open" || Boolean(candidate?.draft);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

findExistingPrForBranch() still treats draft: true as equivalent to an open PR.

// apps/desktop/src/main/services/prs/prService.ts
const open = candidates.find((candidate) => {
  const state = asString(candidate?.state).toLowerCase();
  return state === "open" || Boolean(candidate?.draft);
});

GitHub leaves draft: true on closed draft PRs, so the duplicate-create recovery path can bind the lane to an abandoned closed draft instead of the newer open PR for the same branch. That makes later refresh/land operations target the wrong GitHub PR. Match only state === "open" here; open drafts already satisfy that condition.

Suggested change
return state === "open" || Boolean(candidate?.draft);
return state === "open";

};

const getRow = (prIdOrLocator: string): PullRequestRow | null =>
getRowById(prIdOrLocator) ?? getRowByLocator(prIdOrLocator);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new lookup fallback makes every prId-taking read API accept PR locators, but the snapshot cache still persists the caller’s original string instead of the canonical row id.

// apps/desktop/src/main/services/prs/prService.ts
const getRow = (prIdOrLocator: string): PullRequestRow | null =>
  getRowById(prIdOrLocator) ?? getRowByLocator(prIdOrLocator);

const requireRow = (prId: string): PullRequestRow => {

Methods such as getStatus, getChecks, getComments, getReviews, getDetail, and getFiles resolve #123 / PR URLs through getRow(), then call upsertSnapshotRow({ prId, ... }) with that raw locator. pull_request_snapshots.pr_id references pull_requests(id), so the newly supported locator form succeeds for the read and then fails or miskeys the snapshot write because it is not a real PR row id. Canonicalize snapshot writes to row.id anywhere locator lookup is allowed, or keep locator support out of snapshot-producing methods.

- prService.findExistingPrForBranch: drop `|| Boolean(candidate?.draft)`.
  GitHub keeps draft=true on closed drafts, so the OR could prefer a closed
  draft over a later open PR. state==="open" is sufficient.
- cli.ts VALUE_CARRIER_FLAGS: add -m, --max-rounds / --rounds, --max-log-bytes,
  --max-prompt-chars, --recent-limit; drop --text (global output flag, broke
  `lanes list --text --help`).
- cli.test.ts: regression for `--text --help` ordering.
- cli.ts findAdeManagedWorktreeRoot / findProjectRoots: canonicalize with
  fs.realpathSync.native so symlinked worktree paths still match.
- coordinatorAgent.ts: drop ADE_CLI_AGENT_GUIDANCE injection — coordinator
  delegates to workers and has no terminal tool, so the guidance misled it.
- main.ts buildAdeActionRuntimeForAutomations: forward keybindingsService,
  onboardingService, feedbackReporterService, usageTrackingService,
  budgetCapService, autoUpdateService so desktop automations can dispatch
  the new action domains.

Addresses review comments 3134349765, 3134365534, 3134365536, 3134365659,
3134365660, 3134365668. 3134365535 already addressed in iter 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 23, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 5 comments

ctoStateService,
workerAgentService,
workerBudgetService,
githubService: headlessLinearServices.githubService as never,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

@apps/ade-cli/src/bootstrap.ts now wires a few additional headless services into the returned runtime, but this assembly still never sets several newly advertised action services such as keybindingsService, onboardingService, feedbackReporterService, usageTrackingService, and budgetCapService.

// apps/ade-cli/src/bootstrap.ts
workerAgentService,
workerBudgetService,
githubService: headlessLinearServices.githubService as never,
workerTaskSessionService: headlessLinearServices.workerTaskSessionService,
workerHeartbeatService: headlessLinearServices.workerHeartbeatService,

That is a real behavior bug in headless mode: when the desktop socket is absent, cli.ts falls back to createAdeRuntime(), getAdeActionDomainServices(runtime) reads these fields directly, and run_ade_action throws Domain '<x>' is unavailable in this runtime. for missing services. Desktop already constructs keybindings, onboarding, feedback, usage, and budget from dependencies this function already has (db, logger, projectConfigService, laneService, aiIntegrationService, and the headless GitHub service), so the fix is to instantiate and attach the headless-safe services here as well, or stop exposing those domains from the headless runtime until they are actually wired.

`select ${PR_COLUMNS}
from pull_requests
where project_id = ?
and github_pr_number = ?
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

The new numeric locator path resolves #123 against pull_requests by project_id + github_pr_number only, even though this service now stores PRs from multiple repositories (linkToLane accepts full PR URLs, and the GitHub snapshot code explicitly tracks externalPullRequests). ts // apps/desktop/src/main/services/prs/prService.ts const getRowByNumber = (prNumber: number): PullRequestRow | null => db.get<PullRequestRow>( `select ${PR_COLUMNS} from pull_requests where project_id = ? and github_pr_number = ? If a project has both ownerA/repo1#123 and ownerB/repo2#123, any API that now accepts prId="#123" can pick whichever row was updated last and then operate on the wrong PR (open, land, retarget, comment, etc.). Scope bare-number lookups to the current repo, or reject them when more than one repo matches, instead of doing a project-wide lookup.

Comment thread apps/ade-cli/src/cli.ts
const segments = resolved.split(path.sep);
for (let index = segments.length - 2; index >= 0; index -= 1) {
if (segments[index] !== ".ade" || segments[index + 1] !== "worktrees") continue;
const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When the ADE project itself lives at a Windows drive root (for example C:\), splitting the resolved path and rejoining segments.slice(0, index) produces C: instead of C:\. The next line then runs path.resolve(projectRoot), and Node documents that path.resolve('C:') is drive-relative rather than the drive root. That makes the CLI derive the socket path and headless runtime from the caller’s current-directory-on-drive instead of the actual ADE project, so invocations from C:\.ade\worktrees\<lane> can fail to connect or open the wrong database. Use the parsed root when .ade is immediately under the filesystem root.

// apps/ade-cli/src/cli.ts
const segments = resolved.split(path.sep);
for (let index = segments.length - 2; index >= 0; index -= 1) {
  if (segments[index] !== ".ade" || segments[index + 1] !== "worktrees") continue;
  const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;
Suggested change
const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;
const projectRoot = index === 1 ? path.parse(resolved).root : segments.slice(0, index).join(path.sep) || path.sep;

usageTrackingService,
budgetCapService,
sessionDeltaService,
autoUpdateService,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

@apps/desktop/src/main/main.ts adds autoUpdateService to the rpcRuntime object before the global updater is initialized, so the embedded socket RPC server permanently captures null for the new update ADE-action domain. ts // apps/desktop/src/main/main.ts feedbackReporterService, usageTrackingService, budgetCapService, sessionDeltaService, autoUpdateService, issueInventoryService, rpcRuntime is passed to createAdeRpcRequestHandler() immediately afterward, but autoUpdateService is only assigned later (let autoUpdateService = null near line 4154, then autoUpdateService = createAutoUpdateService(...) near line 4409). Since getAdeActionDomainServices() exposes update from runtime.autoUpdateService, desktop socket clients will never see or be able to call update.getSnapshot, update.checkForUpdates, etc. even after startup finishes. Initialize the updater before constructing rpcRuntime, or patch rpcRuntime.autoUpdateService after creating the service so the embedded RPC runtime sees the live instance.

// 3) second timer flush lets the post-import code run
for (let i = 0; i < 100; i++) {
// `await import("@xterm/addon-webgl")` may not settle under Vi's fake timers on CI shards.
vi.useRealTimers();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This test now exits as soon as the renderer snapshot flips to "dom", but that is earlier than TerminalView's parked-runtime background work finishing. In @apps/desktop/src/renderer/components/terminals/TerminalView.tsx, unmounting a live terminal only parks it; it does not cancel settleTimer1, settleTimer2, hydrateTimer, or hydrateRetryTimer, and startHydration() will still call window.ade.sessions.readTranscriptTail(...) later. Because this case switches to real timers, those native timers can fire during the next test and increment that next test's fresh mocks, making the suite flaky again. The previous runAllTimersAsync loop drained this background work before the test ended. Either wait for the hydration/settle timers to complete as part of this test, or explicitly force the runtime to dispose before restoring fake timers.

// apps/desktop/src/renderer/components/terminals/TerminalView.test.tsx
vi.useRealTimers();
...
await waitFor(
  () => {
    const runtime = getTerminalRuntimeSnapshot("session-dom");

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 23, 2026

@copilot review but do not make fixes

Comment thread apps/desktop/src/main/services/orchestrator/orchestratorService.ts
Comment thread apps/desktop/src/main/services/adeActions/registry.ts
Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 7 comments

throw new Error("Missing required 'layout' object. Pass an explicit null to clear.");
}
const rawLayout = args.layout;
const layout = rawLayout && typeof rawLayout === "object" && !Array.isArray(rawLayout)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

layout.set claims callers can pass null to clear, but the new adapter coerces every non-object payload into {} and writes it to the DB instead of rejecting it. In @apps/desktop/src/main/services/adeActions/registry.ts:

const rawLayout = args.layout;
const layout = rawLayout && typeof rawLayout === "object" && !Array.isArray(rawLayout)
  ? clampDockLayout(rawLayout as Record<string, unknown>)
  : {};
runtime.db.setJson(`dock_layout:${layoutId}`, layout);

That matters because useDockLayout treats any truthy saved value as authoritative and does setLayout(saved), so a malformed CLI call like layout.set --input-json '{"layoutId":"main","layout":null}' replaces the user's persisted panel arrangement with an empty layout on the next load. Reject non-object payloads here (and handle explicit clears deliberately) instead of silently persisting {}.

: null,
buildExecutionModeDirective(executionMode, managed.session.provider),
buildClaudeInteractionModeDirective(managed.session.interactionMode, managed.session.provider),
shouldInjectLaneDirective && !providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new guidance injection is coupled to shouldInjectLaneDirective, but lastLaneDirectiveKey is persisted and restored across session resume, so resumed Codex/OpenCode/Cursor sessions in the same lane will evaluate that flag to false and never receive ADE_CLI_AGENT_GUIDANCE at all. I verified that only Claude has a persistent prompt path (buildClaudeV2SessionOpts), while OpenCode/Codex send whatever lands in promptText for each turn. That means the exact resumed-session case this PR is trying to standardize still misses the ade instructions for every non-Claude provider.

// apps/desktop/src/main/services/chat/agentChatService.ts
const laneDirectiveKey = executionContext.laneDirectiveKey;
const shouldInjectLaneDirective = laneDirectiveKey != null && managed.lastLaneDirectiveKey !== laneDirectiveKey;
const providerHasPersistentGuidance = managed.session.provider === "claude";
...
shouldInjectLaneDirective && !providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,

Decouple ADE guidance delivery from lane-directive churn: either track a separate "guidance delivered" bit for non-Claude sessions, or inject the guidance on the first non-Claude user turn after runtime/session resume even when the lane binding itself does not need reinjection.

expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "worker_agent")).toBe(true);
expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "computer_use_artifacts")).toBe(true);
expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "operation")).toBe(true);
expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "keybindings")).toBe(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

These new assertions assume the headless ade runtime exposes desktop-only domains such as keybindings, onboarding, usage, and update, but createAdeRuntime() is the actual runtime used by ade headless mode and it still does not populate keybindingsService, onboardingService, usageTrackingService, or autoUpdateService. As a result, list_ade_actions in headless mode will not return these domains and run_ade_action will fail with Domain '<name>' is unavailable in this runtime. even though this patch now treats them as part of the expanded CLI surface. The test therefore gives false confidence that the feature works end-to-end. Either wire those services into apps/ade-cli/src/bootstrap.ts for headless mode, or scope these expectations to the desktop-socket runtime only.

// apps/ade-cli/src/adeRpcServer.test.ts
expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "keybindings")).toBe(true);
expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "onboarding")).toBe(true);
expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "usage")).toBe(true);
expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "update")).toBe(true);

Comment thread apps/ade-cli/src/cli.ts Outdated
"--branch-name", "--branch-ref", "--category", "--color", "--cols",
"--command", "--comment", "--comment-id", "--commit", "--compare-ref",
"--compare-to", "--content", "--context-file", "--cwd", "--data",
"--delete-source", "--delete-source-lane", "--depth", "--desc",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

VALUE_CARRIER_FLAGS is now used by hasHelpFlag() to decide whether a --help token is really a help request or just the value for the previous flag, but this set now includes flags that are consumed with readFlag() rather than readValue(). For example, --delete-source, --include-ignored, and --unresolved are boolean switches in the lane/files/PR builders, so commands like ade lanes merge lane-1 --delete-source --help and ade files tree --include-ignored --help will skip help and fall through into normal execution. Remove boolean-only switches from VALUE_CARRIER_FLAGS (or derive this list from actual readValue consumers) so --help is only suppressed after flags that really take a value.

// apps/ade-cli/src/cli.ts
"--delete-source", "--delete-source-lane", "--depth", "--desc",
"--description", "--domain", "--duration-sec", "--enabled", "--event",
"--from-file", "--group", "--group-id", "--head", "--icon", "--id",
"--include-ignored", "--input", "--input-json", "--instructions",

gitService,
diffService,
missionService,
missionBudgetService,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This hunk only wires a subset of the newly-added AdeRuntime service slots, and createAdeRuntime never assigns keybindingsService, onboardingService, usageTrackingService, or autoUpdateService anywhere. ```ts
// apps/ade-cli/src/bootstrap.ts
missionBudgetService,
ptyService,
testService,
aiIntegrationService,
agentChatService,

That matters because `getAdeActionDomainServices()` maps those domains directly from the runtime object, and `run_ade_action` throws `Domain '<x>' is unavailable in this runtime` when a slot is missing. This PR also adds those domains to `ADE_ACTION_DOMAIN_NAMES`, and the new RPC tests expect them to be callable via a handcrafted fixture, so real headless sessions created through `createAdeRuntime` will fail for the newly advertised actions instead of matching the desktop/socket runtime. Wire the same services into headless bootstrap (or stop advertising those domains there) so the CLI contract stays in sync with the actual runtime.

if (parsed.owner && parsed.repo) {
return getRowForRepoPr(parsed.owner, parsed.repo, parsed.number);
}
return getRowByNumber(parsed.number);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new locator resolver treats "123"/"#123" as a project-wide lookup and falls back to getRowByNumber, which has no repo filter and simply returns the most recently updated matching row. This service already stores PRs for multiple repositories (repo_owner/repo_name are first-class fields, and the GitHub snapshot logic handles external repos), so in a project tracking foo/api#123 and foo/web#123, any prId API that now accepts a numeric locator can act on the wrong PR. Reject repo-less numeric locators when more than one matching repo exists, or require owner/repo context for number-based lookup.

// apps/desktop/src/main/services/prs/prService.ts
if (parsed.owner && parsed.repo) {
  return getRowForRepoPr(parsed.owner, parsed.repo, parsed.number);
}
return getRowByNumber(parsed.number);

Comment thread apps/ade-cli/src/cli.ts
const segments = resolved.split(path.sep);
for (let index = segments.length - 2; index >= 0; index -= 1) {
if (segments[index] !== ".ade" || segments[index + 1] !== "worktrees") continue;
const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

findAdeManagedWorktreeRoot() rebuilds the inferred root by split(path.sep) / join(path.sep), which drops the trailing separator for drive-root projects. In a checkout rooted at C:\, segments.slice(0, index).join(path.sep) becomes "C:", and the later path.resolve(projectRoot) is volume-relative on Windows rather than the absolute drive root. That means invoking ade from C:\.ade\worktrees\<lane>\... can resolve projectRoot to the current directory on drive C: instead of C:\, so socket/runtime initialization points at the wrong .ade tree and the new auto-discovery fails for exactly the drive-root case this change is meant to support. Rebuild the path with the parsed absolute root (path.parse(resolved).root) or another root-preserving join strategy, and add a regression test for a C:\.ade\worktrees\... worktree.

// apps/ade-cli/src/cli.ts
const segments = resolved.split(path.sep);
for (let index = segments.length - 2; index >= 0; index -= 1) {
  if (segments[index] !== ".ade" || segments[index + 1] !== "worktrees") continue;
  const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/main/main.ts (1)

3369-3407: ⚠️ Potential issue | 🟠 Major

Wire linearCredentialService into the automation ADE runtime.

getAdeActionDomainServices() resolves the linear_credentials domain from runtime.linearCredentialService, but buildAdeActionRuntimeForAutomations() never supplies it. That leaves automation ade-action steps unable to use linear_credentials even though the desktop RPC runtime does expose it.

Suggested fix
         flowPolicyService,
+        linearCredentialService,
         linearDispatcherService,
         linearIssueTracker,
         linearSyncService,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/main.ts` around lines 3369 - 3407,
buildAdeActionRuntimeForAutomations is not including linearCredentialService so
getAdeActionDomainServices (which reads runtime.linearCredentialService for the
linear_credentials domain) gets undefined; fix by adding linearCredentialService
to the returned object in buildAdeActionRuntimeForAutomations so the runtime
includes runtime.linearCredentialService (i.e., include a property named
linearCredentialService alongside the other services in the returned AdeRuntime
object) to ensure ade-action automation steps can access linear_credentials.
♻️ Duplicate comments (11)
apps/desktop/src/main/services/adeActions/registry.ts (2)

67-83: ⚠️ Potential issue | 🟠 Major

Role gate bypass: usage.poll is allowlisted but not CTO-only.

ADE_ACTION_CTO_ONLY.usage covers start, stop, forceRefresh, but usage.poll remains in ADE_ACTION_ALLOWLIST.usage at Line 321 without a matching CTO gate. Since run_ade_action enforces privilege via ADE_ACTION_CTO_ONLY only, any agent/orchestrator caller can invoke usage.poll and trigger the same privileged collection path the gate was introduced to protect. Previously verified that poll() is the live collector shared with start()/forceRefresh().

🛠️ Proposed fix
-  usage: ["start", "stop", "forceRefresh"],
+  usage: ["start", "stop", "forceRefresh", "poll"],

Also applies to: 321-321

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/adeActions/registry.ts` around lines 67 - 83,
The allowlist contains usage.poll without CTO gating, so update
ADE_ACTION_CTO_ONLY to include "poll" under the usage key to match the
privileged actions; specifically, add "poll" alongside "start", "stop", and
"forceRefresh" in ADE_ACTION_CTO_ONLY (so run_ade_action will properly enforce
CTO-only callers for poll(), which shares the collector with
start()/forceRefresh()); ensure ADE_ACTION_ALLOWLIST.usage still contains "poll"
if needed but the CTO-only gate must be added in ADE_ACTION_CTO_ONLY to close
the privilege gap.

480-492: ⚠️ Potential issue | 🟠 Major

layout.set silently destroys saved layout on malformed payloads.

If args.layout is anything other than a plain object (e.g., true, 42, "foo", or an array), the ternary falls through to {} and runtime.db.setJson("dock_layout:" + layoutId, {}) persists an empty layout. Because run_ade_action accepts untyped JSON, a single bad CLI/RPC call silently wipes a real user’s saved dock layout instead of failing loudly. The adjacent tiling_tree and graph_state setters explicitly reject a missing key; layout should similarly reject invalid shapes and reserve null as the only clear signal.

🛠️ Proposed fix
       const rawLayout = args.layout;
-      const layout = rawLayout && typeof rawLayout === "object" && !Array.isArray(rawLayout)
-        ? clampDockLayout(rawLayout as Record<string, unknown>)
-        : {};
+      if (rawLayout !== null && (typeof rawLayout !== "object" || Array.isArray(rawLayout))) {
+        throw new Error("Expected 'layout' to be an object or null.");
+      }
+      const layout = rawLayout === null
+        ? {}
+        : clampDockLayout(rawLayout as Record<string, unknown>);
       runtime.db.setJson(`dock_layout:${layoutId}`, layout);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/adeActions/registry.ts` around lines 480 -
492, The layout.set handler currently treats any non-plain-object as {} and
overwrites saved layouts; change set (in the layout action) to explicitly
validate args.layout: allow only null (to clear) or a plain object (non-array,
typeof "object"); if args.layout is missing or is any other type (primitive or
array) throw an Error (message: "Invalid 'layout' — pass an explicit null to
clear or a plain object") instead of falling through to {}. After validation,
call clampDockLayout on the plain object and persist via
runtime.db.setJson(`dock_layout:${layoutId}`, layout) as before; keep
requireNonEmptyString(layoutId) and the existing error text for missing layout
key but tighten the type check.
apps/ade-cli/src/cli.ts (2)

1860-1887: ⚠️ Potential issue | 🟡 Minor

-c is missing from VALUE_CARRIER_FLAGS.

Both buildShellPlan (Line 1458) and buildTestsPlan (Line 1502) consume -c via readValue(args, ["--command", "-c"]), but -c is absent from the new carrier list. As a result, ade shell start --lane l1 -c --help and ade tests run --lane l1 -c --help are short-circuited to command help instead of passing the literal --help through to --command. The -m, -q, -b, -t short-form peers are already listed, so this is a straightforward gap.

🛠️ Proposed fix
 const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
-  "-b", "-m", "-q", "-t",
+  "-b", "-c", "-m", "-q", "-t",

Related: the root cause (hard-coded carrier list that must stay in lockstep with every readValue/readIntOption call site) was flagged previously. Deriving the set from the same metadata the parsers use would avoid this recurring drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/cli.ts` around lines 1860 - 1887, VALUE_CARRIER_FLAGS is
missing the short flag "-c", which breaks buildShellPlan and buildTestsPlan
because they call readValue(args, ["--command", "-c"]); update the ReadonlySet
VALUE_CARRIER_FLAGS to include the string "-c" alongside the other short-form
flags (e.g., "-b","-m","-q","-t") so that readValue can forward "-c" values
correctly; after adding "-c", run the related shell/tests paths and consider
adding a TODO to derive this set from the parser metadata to avoid future drift.

2009-2017: ⚠️ Potential issue | 🟡 Minor

Windows drive-root join still yields a drive-relative path.

When the ADE project sits at a drive root (e.g., C:\.ade\worktrees\<lane>), resolved.split(path.sep) produces ["C:", ".ade", "worktrees", "<lane>", ...]. At index === 1, segments.slice(0, 1).join(path.sep) is "C:", which Node treats as drive-relative (current directory on drive C:), not the drive root "C:\". Downstream path.resolve(projectRoot) then anchors to the process cwd-on-drive rather than the actual project. The symlink canonicalization added via fs.realpathSync.native is a good fix for the other past concern, but this one remains.

🛠️ Proposed fix
-    const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;
+    const projectRoot = index === 1
+      ? path.parse(resolved).root
+      : segments.slice(0, index).join(path.sep) || path.sep;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/cli.ts` around lines 2009 - 2017, The loop builds
projectRoot/workspaceRoot from segments but on Windows a drive root becomes "C:"
(drive-relative) which causes path.resolve(projectRoot) to resolve relative to
the current directory on that drive; detect Windows drive-only tokens (e.g.,
projectRoot matching /^[A-Za-z]:$/) and append the path separator before using
projectRoot/workspaceRoot (or use path.join(projectRoot, path.sep)) so that
projectRoot becomes "C:\\" (true drive root) before calling fs.existsSync and
path.resolve; apply the same normalization for workspaceRoot and any checks that
use projectRoot.
apps/desktop/src/main/main.ts (1)

3176-3249: ⚠️ Potential issue | 🟠 Major

Make autoUpdateService live in the socket RPC runtime.

Line 3245 snapshots autoUpdateService while it is still null, and the RPC handler then keeps that stale value for the lifetime of the socket server. Desktop socket clients will never see the update ADE-action domain even after the updater is created later in startup.

Suggested fix
     const rpcRuntime: AdeRuntime = {
       projectRoot,
       workspaceRoot: projectRoot,
       projectId,
       project,
@@
-      autoUpdateService,
+      get autoUpdateService() {
+        return autoUpdateService;
+      },
       issueInventoryService,
       eventBuffer: rpcEventBuffer,
       dispose: () => {}, // desktop manages service lifecycle
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/main.ts` around lines 3176 - 3249, The RPC runtime
currently captures a null snapshot of autoUpdateService; instead ensure
rpcRuntime uses the real service instance by assigning
rpcRuntime.autoUpdateService = autoUpdateService after the updater is
initialized (or define a live getter on rpcRuntime that returns the current
autoUpdateService variable). Locate the rpcRuntime object and the
autoUpdateService variable and either (a) remove autoUpdateService from the
static literal and set rpcRuntime.autoUpdateService = autoUpdateService once the
updater is created, or (b) use Object.defineProperty(rpcRuntime,
"autoUpdateService", { get: () => autoUpdateService }) so socket RPC handlers
always see the live updater instance.
apps/ade-cli/src/bootstrap.ts (1)

526-582: ⚠️ Potential issue | 🟠 Major

Headless AdeRuntime is still missing several newly advertised action services.

This runtime still never assigns keybindingsService, onboardingService, feedbackReporterService, usageTrackingService, budgetCapService, or autoUpdateService. Since getAdeActionDomainServices(runtime) reads those slots directly, headless ade actions will keep rejecting those domains even though the desktop socket runtime now wires them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/bootstrap.ts` around lines 526 - 582, The headless
AdeRuntime object is missing the newly required action services
(keybindingsService, onboardingService, feedbackReporterService,
usageTrackingService, budgetCapService, autoUpdateService) which causes
getAdeActionDomainServices(runtime) to reject those domains; add those
properties to the runtime object and wire them to the appropriate headless
implementations (e.g., headlessLinearServices.keybindingsService,
headlessLinearServices.onboardingService, etc.) or provide lightweight/no-op
fallbacks from the same service factory, and ensure any added services are also
disposed in the dispose() swallow block (call their dispose/disposeAll if
available).
apps/desktop/src/main/services/chat/agentChatService.ts (1)

10918-10930: ⚠️ Potential issue | 🟠 Major

Non-Claude guidance is still incorrectly tied to lane-directive refresh.

On Line 10929, non-Claude providers only get ADE_CLI_AGENT_GUIDANCE when shouldInjectLaneDirective is true. After lane directive persistence, guidance disappears on subsequent turns.

Suggested fix
-          shouldInjectLaneDirective && !providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,
+          !providerHasPersistentGuidance ? ADE_CLI_AGENT_GUIDANCE : null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 10918 -
10930, The ADE_CLI_AGENT_GUIDANCE entry is currently gated by
shouldInjectLaneDirective so non-Claude providers lose guidance after lane
persistence; change the condition that adds ADE_CLI_AGENT_GUIDANCE in the
composeLaunchDirectives array so it does not depend on shouldInjectLaneDirective
but instead checks that providerHasPersistentGuidance is false and the provider
is not Claude (use managed.session.provider or existing
buildClaudeInteractionModeDirective/interactionMode to detect Claude); i.e., add
ADE_CLI_AGENT_GUIDANCE when !providerHasPersistentGuidance &&
managed.session.provider !== 'claude' (or equivalent), leaving the lane
directive inclusion unchanged.
apps/ade-cli/src/adeRpcServer.ts (1)

5413-5419: ⚠️ Potential issue | 🟠 Major

Draft the PR text against the same base branch that will actually be used.

When baseBranch is omitted, Line 5414 still asks draftDescription to infer the diff on its own, while createFromLane can resolve a different stable base for stacked lanes. That can generate a title/body for one diff and then open the PR against another branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/adeRpcServer.ts` around lines 5413 - 5419, When baseBranch
is omitted, prSvc.draftDescription is currently asked to infer the diff itself
which can mismatch the stable base resolution used later by createFromLane;
instead resolve the exact base branch that will be used (using the same
resolver/helper or logic createFromLane uses) into a variable (e.g.,
resolvedBaseBranch) and pass that resolvedBaseBranch into
prSvc.draftDescription({ laneId, ...(resolvedBaseBranch ? { baseBranch:
resolvedBaseBranch } : {})}) so the drafted title/body match the actual PR base;
update references around baseBranch, prSvc.draftDescription, createFromLane,
laneId, title and body accordingly.
apps/desktop/src/main/services/prs/prService.ts (3)

4917-4940: ⚠️ Potential issue | 🟠 Major

Canonicalize snapshot writes before accepting PR locators.

These methods now resolve prId through getRow()/requireRow(), so callers can pass #123 or a PR URL. But upsertSnapshotRow() still stores the raw argument as pull_request_snapshots.pr_id, which should reference pull_requests.id. Calls like getStatus("#123") will mis-key or fail the snapshot write. Use the resolved row id for every snapshot upsert.

💡 Minimal fix
    async getStatus(prId: string): Promise<PrStatus> {
      const row = getRow(prId);
      if (!row) throw new Error(`PR not found: ${prId}`);
      const status = await computeStatus(rowToSummary(row));
-      upsertSnapshotRow({ prId, status });
+      upsertSnapshotRow({ prId: row.id, status });
      return status;
    },

    async getChecks(prId: string): Promise<PrCheck[]> {
-      const checks = await getChecks(prId);
-      upsertSnapshotRow({ prId, checks });
+      const row = requireRow(prId);
+      const checks = await getChecks(row.id);
+      upsertSnapshotRow({ prId: row.id, checks });
      return checks;
    },

Apply the same canonicalization to getComments, getReviews, getDetail, and getFiles.

Also applies to: 5110-5119


732-755: ⚠️ Potential issue | 🟠 Major

Reject bare #123 locators when more than one repo can match.

getRowByNumber() is project-wide, but this service now tracks PRs from multiple repositories. If two repos both have PR 123, any API that accepts a bare number can refresh, comment on, or land the wrong PR based on recency. Scope numeric locators to the active repo, or fail when the lookup is ambiguous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/prs/prService.ts` around lines 732 - 755,
getRowByLocator currently resolves bare numeric locators via getRowByNumber
which searches the whole project and can return the wrong PR if multiple repos
in the project have the same PR number; change the logic so that when
parsePrLocator returns only a number (no owner/repo) you first check for
ambiguity by counting matching rows for that projectId and number (use the same
query shape as getRowByNumber), and if more than one match exists either scope
the lookup to the active repo (call getRowForRepoPr(activeOwner, activeRepo,
parsed.number)) or return null/fail to avoid ambiguous resolution; update
getRowByLocator to use parsePrLocator, getRowForRepoPr, and projectId
accordingly and ensure ambiguous cases are rejected instead of returning the
most-recent row.

1157-1210: ⚠️ Potential issue | 🔴 Critical

Don't let upsertRow() silently steal a PR from another lane.

The repo+PR fallback now runs for every upsert, not just duplicate-create recovery. If another lane already owns that PR row, this path deletes pr_group_members, nulls integration_proposals.linked_pr_id, and rewrites lane_id with no explicit handoff. That can break an active queue/integration workflow on the original lane. Keep repo+number adoption behind an explicit lane-transfer flow, or fail when the matched row belongs to a different lane.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/prs/prService.ts` around lines 1157 - 1210,
The upsertRow() currently uses a fallback getRowForRepoPr() and will
unconditionally reassign lane ownership (deleting pr_group_members and nulling
integration_proposals) which can steal an active PR from another lane; fix by
splitting the lookup into two variables (e.g. const laneRow =
getRowForLane(...); const repoRow = getRowForRepoPr(...); const existing =
laneRow ?? repoRow) and if existing came from the repoRow fallback (laneRow is
null) and existing.lane_id !== summary.laneId then do not perform the
destructive handoff—either return existing.id unchanged or throw/return an
error—so lane transfers only happen via an explicit transfer flow instead of
silently rewriting lane_id in upsertRow().
🧹 Nitpick comments (3)
apps/ade-cli/src/cli.test.ts (1)

387-401: Temp directory from mkdtempSync leaks after the test.

fs.mkdtempSync creates a real temp directory but there is no afterEach/try..finally that removes it. Over many local/CI runs this accumulates under os.tmpdir(). A small fs.rmSync(rawRoot, { recursive: true, force: true }) in a finally (or an afterEach scoped to this test) keeps the filesystem clean without changing behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/cli.test.ts` around lines 387 - 401, This test creates a
real temp directory via rawRoot = fs.mkdtempSync(...) but never removes it; wrap
the test body in a try/finally (or add an afterEach for this spec) and call
fs.rmSync(rawRoot, { recursive: true, force: true }) in the finally/afterEach to
ensure the temp directory is removed; reference the rawRoot variable created at
the top of the test (and you can use root = fs.realpathSync.native(rawRoot) as
before) so cleanup removes the actual on-disk directory created by mkdtempSync.
apps/desktop/src/main/services/orchestrator/baseOrchestratorAdapter.ts (1)

798-799: startupCommandPreview loses signal now that guidance is prepended.

ADE_CLI_AGENT_GUIDANCE is ~400+ characters, so overridePrompt.slice(0, 320) will now always return the same guidance prefix for every override, making the stored preview useless for distinguishing which startup command was actually applied. Similarly, promptLength now counts the guidance prefix, so values aren't comparable against historical rows.

Consider deriving the preview from startupCommandOverride (the actual override) while keeping promptLength on the combined overridePrompt, or vice-versa, so the metadata keeps its debug value.

♻️ Suggested adjustment
           metadata: {
             adapterKind: executorKind,
             startupCommandOverride: true,
             promptLength: overridePrompt.length,
-            startupCommandPreview: overridePrompt.slice(0, 320)
+            startupCommandPreview: startupCommandOverride.slice(0, 320)
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/orchestrator/baseOrchestratorAdapter.ts`
around lines 798 - 799, The stored preview currently slices overridePrompt
(which includes ADE_CLI_AGENT_GUIDANCE) causing every preview to show the same
guidance; change startupCommandPreview to be derived from startupCommandOverride
(e.g., use startupCommandOverride.slice(0, 320)) so the preview reflects the
actual override, while keeping promptLength calculated from overridePrompt (to
include the full combined length); alternatively, if you prefer promptLength to
exclude guidance, compute promptLength from startupCommandOverride and keep
startupCommandPreview from overridePrompt—update the assignments around
promptLength, startupCommandPreview, overridePrompt, and startupCommandOverride
to reflect the chosen approach.
apps/ade-cli/src/adeRpcServer.test.ts (1)

3270-3283: Consider also covering the baseBranch-omitted draft path.

Per the PR summary, create_pr_from_lane now treats both title and baseBranch as optional (with auto-draft/inference). This test only exercises the title/body-omitted case while still passing baseBranch: "main". Adding a second assertion that omits baseBranch (verifying it's either inferred by the server or forwarded as-is to draftDescription/createFromLane) would lock in the full optional-arg contract and prevent silent regressions on base-branch resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/adeRpcServer.test.ts` around lines 3270 - 3283, Add a second
test invocation of callTool(handler, "create_pr_from_lane", { laneId: "lane-1"
}) that omits baseBranch and assert the result and service calls mirror the
optional-arg contract: verify drafted?.isError is undefined, that
fixture.runtime.prService.draftDescription was called for the lane without a
baseBranch (e.g., calledWith({ laneId: "lane-1" }) or with baseBranch
undefined), and that fixture.runtime.prService.createFromLane was lastCalledWith
an object containing laneId "lane-1" and either an inferred baseBranch or
baseBranch undefined (match the server behavior you expect) so the test covers
the baseBranch-omitted path for create_pr_from_lane.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ade-cli/src/adeRpcServer.ts`:
- Around line 4098-4103: The usage string in the mapped object (the usage
property created in the .map callback for each action) is shell-specific by
embedding single-quoted JSON; update it to a shell-neutral example such as
showing file-based input or a documented cross-platform form. Replace the inline
'{"key":"value"}' example with something like --input-json-file
<path/to/input.json> or else show the JSON using an escaped form that you
document as platform-dependent; update the usage property in the map (the code
that builds name/usage for each action) so list_ade_actions returns a
shell-neutral, copy/paste-safe example.

---

Outside diff comments:
In `@apps/desktop/src/main/main.ts`:
- Around line 3369-3407: buildAdeActionRuntimeForAutomations is not including
linearCredentialService so getAdeActionDomainServices (which reads
runtime.linearCredentialService for the linear_credentials domain) gets
undefined; fix by adding linearCredentialService to the returned object in
buildAdeActionRuntimeForAutomations so the runtime includes
runtime.linearCredentialService (i.e., include a property named
linearCredentialService alongside the other services in the returned AdeRuntime
object) to ensure ade-action automation steps can access linear_credentials.

---

Duplicate comments:
In `@apps/ade-cli/src/adeRpcServer.ts`:
- Around line 5413-5419: When baseBranch is omitted, prSvc.draftDescription is
currently asked to infer the diff itself which can mismatch the stable base
resolution used later by createFromLane; instead resolve the exact base branch
that will be used (using the same resolver/helper or logic createFromLane uses)
into a variable (e.g., resolvedBaseBranch) and pass that resolvedBaseBranch into
prSvc.draftDescription({ laneId, ...(resolvedBaseBranch ? { baseBranch:
resolvedBaseBranch } : {})}) so the drafted title/body match the actual PR base;
update references around baseBranch, prSvc.draftDescription, createFromLane,
laneId, title and body accordingly.

In `@apps/ade-cli/src/bootstrap.ts`:
- Around line 526-582: The headless AdeRuntime object is missing the newly
required action services (keybindingsService, onboardingService,
feedbackReporterService, usageTrackingService, budgetCapService,
autoUpdateService) which causes getAdeActionDomainServices(runtime) to reject
those domains; add those properties to the runtime object and wire them to the
appropriate headless implementations (e.g.,
headlessLinearServices.keybindingsService,
headlessLinearServices.onboardingService, etc.) or provide lightweight/no-op
fallbacks from the same service factory, and ensure any added services are also
disposed in the dispose() swallow block (call their dispose/disposeAll if
available).

In `@apps/ade-cli/src/cli.ts`:
- Around line 1860-1887: VALUE_CARRIER_FLAGS is missing the short flag "-c",
which breaks buildShellPlan and buildTestsPlan because they call readValue(args,
["--command", "-c"]); update the ReadonlySet VALUE_CARRIER_FLAGS to include the
string "-c" alongside the other short-form flags (e.g., "-b","-m","-q","-t") so
that readValue can forward "-c" values correctly; after adding "-c", run the
related shell/tests paths and consider adding a TODO to derive this set from the
parser metadata to avoid future drift.
- Around line 2009-2017: The loop builds projectRoot/workspaceRoot from segments
but on Windows a drive root becomes "C:" (drive-relative) which causes
path.resolve(projectRoot) to resolve relative to the current directory on that
drive; detect Windows drive-only tokens (e.g., projectRoot matching
/^[A-Za-z]:$/) and append the path separator before using
projectRoot/workspaceRoot (or use path.join(projectRoot, path.sep)) so that
projectRoot becomes "C:\\" (true drive root) before calling fs.existsSync and
path.resolve; apply the same normalization for workspaceRoot and any checks that
use projectRoot.

In `@apps/desktop/src/main/main.ts`:
- Around line 3176-3249: The RPC runtime currently captures a null snapshot of
autoUpdateService; instead ensure rpcRuntime uses the real service instance by
assigning rpcRuntime.autoUpdateService = autoUpdateService after the updater is
initialized (or define a live getter on rpcRuntime that returns the current
autoUpdateService variable). Locate the rpcRuntime object and the
autoUpdateService variable and either (a) remove autoUpdateService from the
static literal and set rpcRuntime.autoUpdateService = autoUpdateService once the
updater is created, or (b) use Object.defineProperty(rpcRuntime,
"autoUpdateService", { get: () => autoUpdateService }) so socket RPC handlers
always see the live updater instance.

In `@apps/desktop/src/main/services/adeActions/registry.ts`:
- Around line 67-83: The allowlist contains usage.poll without CTO gating, so
update ADE_ACTION_CTO_ONLY to include "poll" under the usage key to match the
privileged actions; specifically, add "poll" alongside "start", "stop", and
"forceRefresh" in ADE_ACTION_CTO_ONLY (so run_ade_action will properly enforce
CTO-only callers for poll(), which shares the collector with
start()/forceRefresh()); ensure ADE_ACTION_ALLOWLIST.usage still contains "poll"
if needed but the CTO-only gate must be added in ADE_ACTION_CTO_ONLY to close
the privilege gap.
- Around line 480-492: The layout.set handler currently treats any
non-plain-object as {} and overwrites saved layouts; change set (in the layout
action) to explicitly validate args.layout: allow only null (to clear) or a
plain object (non-array, typeof "object"); if args.layout is missing or is any
other type (primitive or array) throw an Error (message: "Invalid 'layout' —
pass an explicit null to clear or a plain object") instead of falling through to
{}. After validation, call clampDockLayout on the plain object and persist via
runtime.db.setJson(`dock_layout:${layoutId}`, layout) as before; keep
requireNonEmptyString(layoutId) and the existing error text for missing layout
key but tighten the type check.

In `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 10918-10930: The ADE_CLI_AGENT_GUIDANCE entry is currently gated
by shouldInjectLaneDirective so non-Claude providers lose guidance after lane
persistence; change the condition that adds ADE_CLI_AGENT_GUIDANCE in the
composeLaunchDirectives array so it does not depend on shouldInjectLaneDirective
but instead checks that providerHasPersistentGuidance is false and the provider
is not Claude (use managed.session.provider or existing
buildClaudeInteractionModeDirective/interactionMode to detect Claude); i.e., add
ADE_CLI_AGENT_GUIDANCE when !providerHasPersistentGuidance &&
managed.session.provider !== 'claude' (or equivalent), leaving the lane
directive inclusion unchanged.

In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 732-755: getRowByLocator currently resolves bare numeric locators
via getRowByNumber which searches the whole project and can return the wrong PR
if multiple repos in the project have the same PR number; change the logic so
that when parsePrLocator returns only a number (no owner/repo) you first check
for ambiguity by counting matching rows for that projectId and number (use the
same query shape as getRowByNumber), and if more than one match exists either
scope the lookup to the active repo (call getRowForRepoPr(activeOwner,
activeRepo, parsed.number)) or return null/fail to avoid ambiguous resolution;
update getRowByLocator to use parsePrLocator, getRowForRepoPr, and projectId
accordingly and ensure ambiguous cases are rejected instead of returning the
most-recent row.
- Around line 1157-1210: The upsertRow() currently uses a fallback
getRowForRepoPr() and will unconditionally reassign lane ownership (deleting
pr_group_members and nulling integration_proposals) which can steal an active PR
from another lane; fix by splitting the lookup into two variables (e.g. const
laneRow = getRowForLane(...); const repoRow = getRowForRepoPr(...); const
existing = laneRow ?? repoRow) and if existing came from the repoRow fallback
(laneRow is null) and existing.lane_id !== summary.laneId then do not perform
the destructive handoff—either return existing.id unchanged or throw/return an
error—so lane transfers only happen via an explicit transfer flow instead of
silently rewriting lane_id in upsertRow().

---

Nitpick comments:
In `@apps/ade-cli/src/adeRpcServer.test.ts`:
- Around line 3270-3283: Add a second test invocation of callTool(handler,
"create_pr_from_lane", { laneId: "lane-1" }) that omits baseBranch and assert
the result and service calls mirror the optional-arg contract: verify
drafted?.isError is undefined, that fixture.runtime.prService.draftDescription
was called for the lane without a baseBranch (e.g., calledWith({ laneId:
"lane-1" }) or with baseBranch undefined), and that
fixture.runtime.prService.createFromLane was lastCalledWith an object containing
laneId "lane-1" and either an inferred baseBranch or baseBranch undefined (match
the server behavior you expect) so the test covers the baseBranch-omitted path
for create_pr_from_lane.

In `@apps/ade-cli/src/cli.test.ts`:
- Around line 387-401: This test creates a real temp directory via rawRoot =
fs.mkdtempSync(...) but never removes it; wrap the test body in a try/finally
(or add an afterEach for this spec) and call fs.rmSync(rawRoot, { recursive:
true, force: true }) in the finally/afterEach to ensure the temp directory is
removed; reference the rawRoot variable created at the top of the test (and you
can use root = fs.realpathSync.native(rawRoot) as before) so cleanup removes the
actual on-disk directory created by mkdtempSync.

In `@apps/desktop/src/main/services/orchestrator/baseOrchestratorAdapter.ts`:
- Around line 798-799: The stored preview currently slices overridePrompt (which
includes ADE_CLI_AGENT_GUIDANCE) causing every preview to show the same
guidance; change startupCommandPreview to be derived from startupCommandOverride
(e.g., use startupCommandOverride.slice(0, 320)) so the preview reflects the
actual override, while keeping promptLength calculated from overridePrompt (to
include the full combined length); alternatively, if you prefer promptLength to
exclude guidance, compute promptLength from startupCommandOverride and keep
startupCommandPreview from overridePrompt—update the assignments around
promptLength, startupCommandPreview, overridePrompt, and startupCommandOverride
to reflect the chosen approach.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11375018-1081-4863-b5eb-cfe9f1012b34

📥 Commits

Reviewing files that changed from the base of the PR and between 2499202 and 7f88111.

⛔ Files ignored due to path filters (1)
  • .claude/scheduled_tasks.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/cto/ctoStateService.ts
  • apps/desktop/src/main/services/cto/workerAdapterRuntimeService.test.ts
  • apps/desktop/src/main/services/cto/workerAdapterRuntimeService.ts
  • apps/desktop/src/main/services/orchestrator/baseOrchestratorAdapter.ts
  • apps/desktop/src/main/services/orchestrator/coordinatorAgent.ts
  • apps/desktop/src/main/services/orchestrator/orchestratorService.ts
  • apps/desktop/src/main/services/orchestrator/promptInspector.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
  • apps/desktop/src/renderer/components/terminals/TerminalView.test.tsx
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/types/prs.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/main/services/orchestrator/coordinatorAgent.ts

Comment on lines +4098 to +4103
.map((action) => ({
domain: entry,
action,
name: `${entry}.${action}`,
usage: `ade actions run ${entry}.${action} --input-json '{"key":"value"}' (or --scalar value / --args-list-json '[...]' for scalar or positional service methods)`,
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use shell-neutral usage examples here.

The new usage string hardcodes single-quoted JSON, which is copy/pasteable in POSIX shells but not in common Windows shells. Since list_ade_actions returns this directly to users, the example is now misleading on a supported platform.

💡 Suggested change
-          usage: `ade actions run ${entry}.${action} --input-json '{"key":"value"}' (or --scalar value / --args-list-json '[...]' for scalar or positional service methods)`,
+          usage: `ade actions run ${entry}.${action} --input-json <json> (or --scalar <value> / --args-list-json <json-array> for scalar or positional service methods)`,
📝 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
.map((action) => ({
domain: entry,
action,
name: `${entry}.${action}`,
usage: `ade actions run ${entry}.${action} --input-json '{"key":"value"}' (or --scalar value / --args-list-json '[...]' for scalar or positional service methods)`,
}));
.map((action) => ({
domain: entry,
action,
name: `${entry}.${action}`,
usage: `ade actions run ${entry}.${action} --input-json <json> (or --scalar <value> / --args-list-json <json-array> for scalar or positional service methods)`,
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/adeRpcServer.ts` around lines 4098 - 4103, The usage string
in the mapped object (the usage property created in the .map callback for each
action) is shell-specific by embedding single-quoted JSON; update it to a
shell-neutral example such as showing file-based input or a documented
cross-platform form. Replace the inline '{"key":"value"}' example with something
like --input-json-file <path/to/input.json> or else show the JSON using an
escaped form that you document as platform-dependent; update the usage property
in the map (the code that builds name/usage for each action) so list_ade_actions
returns a shell-neutral, copy/paste-safe example.

- orchestratorService: drop redundant ADE_CLI_AGENT_GUIDANCE prepend in
  augmentedPrompt; buildFullPrompt already injects it into the system prompt
  for mission-tooling workers, so the double-injection wasted context.
- adeActions registry.graph_state: ignore caller-supplied projectId and always
  key by runtime.projectId — closes the cross-project read/write escalation.
- adeActions registry.layout/tiling_tree/graph_state set: reject non-plain-
  object payloads in addition to missing keys (allow explicit null to clear).
- adeActions registry CTO-only: gate usage.poll alongside start/stop/forceRefresh.
- prService.upsertRow: no longer adopts rows matching by repo/PR number across
  lanes unless caller passes allowRepoPrAdoption. Only duplicate-PR recovery
  in createFromLane opts in; linkToLane + refresh stay lane-scoped.
- prService.getRowByNumber: accepts optional repo owner/name; throws on
  cross-repo ambiguity so bare "#123" locators surface conflicts instead of
  silently picking the most-recently-updated row.
- main.ts: autoUpdateService is initialized before rpcRuntime construction so
  the embedded socket RPC captures a live instance instead of null.
- cli.ts VALUE_CARRIER_FLAGS: prune boolean-only flags
  (--delete-source, --delete-source-lane, --include-ignored, --unresolved).
- agentChatService: decouple ADE_CLI_AGENT_GUIDANCE from shouldInjectLaneDirective.
  Providers without persistent system-prompt guidance (Codex/OpenCode/Cursor)
  now get the block on every launch, even resumed sessions.
- providerOrchestratorAdapter.test: accept the now-always-augmented worker
  prompt via expect.stringContaining("diagnose the failing check") instead of
  an exact-equality assertion.

Addresses 3134403033, 3134403054, 3134403060, 3134403076, 3134443870,
3134443879, 3134503369, 3134503423, 3134504179, 3134504183, 3134504190,
3134504196. Fixes the providerOrchestratorAdapter test that failed on CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 23, 2026

@copilot review but do not make fixes

Comment thread apps/ade-cli/src/cli.ts
const segments = resolved.split(path.sep);
for (let index = segments.length - 2; index >= 0; index -= 1) {
if (segments[index] !== ".ade" || segments[index + 1] !== "worktrees") continue;
const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new worktree-root discovery reconstructs absolute paths by splitting on path.sep and joining slices back together. On Windows, a worktree under a drive-root project like C:\.ade\worktrees\lane\... produces segments[0] === "C:", so segments.slice(0, index).join(path.sep) becomes C: instead of C:\. ```ts
// apps/ade-cli/src/cli.ts
const segments = resolved.split(path.sep);
for (let index = segments.length - 2; index >= 0; index -= 1) {
if (segments[index] !== ".ade" || segments[index + 1] !== "worktrees") continue;
const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;

That makes the `fs.existsSync(path.join(projectRoot, ".ade"))` check look at `C:.ade`, so managed-worktree detection fails and `findProjectRoots()` falls back to the worktree-local `.ade` directory instead of the parent project. I verified that `resolveAdeLayout(projectRoot)` keys the socket/db paths off this value, so CLI calls from that worktree will target the wrong ADE state. Fix by preserving the parsed filesystem root when rebuilding `projectRoot`/`workspaceRoot` rather than joining raw segments directly.

pr: toService(runtime.prService),
tests: toService(runtime.testService),
chat: toService(runtime.agentChatService),
keybindings: toService(runtime.keybindingsService),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

These new registry entries make domains like keybindings and onboarding part of the CLI action surface, but the actual headless runtime created in apps/ade-cli/src/bootstrap.ts still never instantiates keybindingsService, onboardingService, usageTrackingService, budgetCapService, feedbackReporterService, or autoUpdateService — it only added optional type slots for them. In practice that means the PR's "service expansion" is incomplete: list_ade_actions on the real CLI silently omits these domains and any direct run_ade_action call fails with Domain '...' is unavailable in this runtime. Wire the corresponding services in createAdeRuntime() or keep these domains out of the shared CLI registry until headless implementations exist.

// apps/desktop/src/main/services/adeActions/registry.ts
keybindings: toService(runtime.keybindingsService),
onboarding: toService(runtime.onboardingService),
automation_planner: toService(runtime.automationPlannerService),
...
usage: toService(runtime.usageTrackingService),
budget: toService(runtime.budgetCapService),

// prepended even on resumed sessions where `shouldInjectLaneDirective` is
// false (review 3134504183 / 3134403060).
const providerHasPersistentGuidance = managed.session.provider === "claude";
const shouldInjectGuidance = !providerHasPersistentGuidance;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

// apps/desktop/src/main/services/chat/agentChatService.ts
const providerHasPersistentGuidance = managed.session.provider === "claude";
const shouldInjectGuidance = !providerHasPersistentGuidance;
...
shouldInjectGuidance ? ADE_CLI_AGENT_GUIDANCE : null,

This changed the guidance gate from a first-launch/session-state check into an unconditional per-turn check for every non-Claude provider. composeLaunchDirectives() is used for normal OpenCode/Codex/Cursor turns, so every follow-up message now gets the full ## ADE CLI block again; that directly contradicts the existing regression expectation in agentChatService.test.ts that the second OpenCode turn omits the guidance. It also creates an immediate double-injection path for worker sessions, because createWorkerAdapterRuntimeService.run() already prefixes runSessionTurn() input with the same guidance before handing control to agentChatService. Track whether ADE CLI guidance has already been delivered for the session/continuation and inject it once, rather than on every non-Claude turn.

Comment thread apps/ade-cli/src/cli.ts
if (sub === "body" || sub === "update-body") return { kind: "execute", label: "PR update body", steps: [actionCallStep("result", "pr_update_body", withPr({ prId: prId ?? firstPositional(args), body: readValue(args, ["--body"]) ?? "" }))] };
if (sub === "link") return { kind: "execute", label: "PR link", steps: [actionStep("result", "pr", "linkToLane", collectGenericObjectArgs(args, { laneId: readLaneId(args) ?? firstPositional(args), url: readValue(args, ["--url"]) }))] };
if (sub === "link") {
const laneId = readLaneId(args) ?? firstPositional(args);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

buildPrPlan() now advertises support for linking by PR number, but the new link branch still consumes the first positional before stripping the PR locator flags. For ade prs link --number 123 lane-1, firstPositional(args) sees 123 (the value for --number) and assigns it to laneId, then readValue(... --number ...) reads the remaining positional lane-1 back as prUrlOrNumber. The call then reaches pr.linkToLane with swapped arguments and fails on the lane lookup instead of linking the PR. Read the explicit PR locator flags before falling back to a positional PR argument so --number/--pr-number work regardless of argument order.

// apps/ade-cli/src/cli.ts
const laneId = readLaneId(args) ?? firstPositional(args);
const prUrlOrNumber =
  readValue(args, ["--url", "--pr-url", "--number", "--pr-number"])
  ?? firstPositional(args);
Suggested change
const laneId = readLaneId(args) ?? firstPositional(args);
const explicitPrUrlOrNumber =
readValue(args, ["--url", "--pr-url", "--number", "--pr-number"]);
const laneId = readLaneId(args) ?? firstPositional(args);
const prUrlOrNumber = explicitPrUrlOrNumber ?? firstPositional(args);

providersSection,
"",
"ADE CLI: Terminal-capable workers can use the bundled `ade` command for internal ADE actions. Instruct them to run `ade doctor` for readiness, `ade actions list --text` for discovery, typed commands such as `ade lanes list --text` or `ade prs checks <pr> --text` first, and `ade actions run ...` as the escape hatch. Use `--json` for structured output and `--text` for readable output.",
ADE_CLI_AGENT_GUIDANCE,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

// apps/desktop/src/main/services/orchestrator/promptInspector.ts
text: [
  providersSection,
  "",
  ADE_CLI_AGENT_GUIDANCE,
].join("\n"),

The inspector now claims the coordinator receives ADE_CLI_AGENT_GUIDANCE, but the actual coordinator system prompt built in coordinatorAgent.buildSystemPrompt() no longer contains any equivalent ## ADE CLI section after this diff removed the old inline block. That makes the prompt inspector inaccurate again: operators inspecting a run will see ADE CLI guidance in the synthetic prompt even though the live coordinator never saw it, which is exactly the kind of prompt/runtime divergence this tool is supposed to prevent. Keep the inspector and the real coordinator prompt in lockstep by either restoring the same guidance in coordinatorAgent or removing it from the inspector layer.

// creation because a PR already exists for the head branch) is the only
// legitimate use of the repo/PR-number fallback; it opts in via
// `allowRepoPrAdoption: true`.
const existing = options?.allowRepoPrAdoption
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new upsertRow lookup only checks lane_id unless allowRepoPrAdoption is set, so linkToLane() will insert a second pull_requests row when the same GitHub PR is already linked to another lane. ts // apps/desktop/src/main/services/prs/prService.ts const existing = options?.allowRepoPrAdoption ? getRowForLane(summary.laneId) ?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber) : getRowForLane(summary.laneId); The table schema has no uniqueness constraint on (project_id, repo_owner, repo_name, github_pr_number), but downstream code still assumes repo/PR uniqueness: getRowForRepoPr(... limit 1) picks one row arbitrarily, and getGithubSnapshot() collapses rows into a single Map entry by repoPrKey, so after a relink the older lane can win and the newly linked lane disappears from snapshot/group metadata. Reject cross-lane relinks explicitly or perform a deliberate migration flow; do not silently insert duplicate rows for the same external PR.

return getRowByNumber(parsed.number);
};

const getRow = (prIdOrLocator: string): PullRequestRow | null =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This change makes every internal prId lookup accept #123/URL locators, but the snapshot-backed methods still persist snapshots under the raw argument instead of the resolved row ID. ts // apps/desktop/src/main/services/prs/prService.ts const getRow = (prIdOrLocator: string): PullRequestRow | null => getRowById(prIdOrLocator) ?? getRowByLocator(prIdOrLocator); getStatus, getChecks, getDetail, getFiles, and refreshSnapshotData all call upsertSnapshotRow({ prId, ... }), and pull_request_snapshots.pr_id is a foreign key to pull_requests(id) in @apps/desktop/src/main/services/state/kvDb.ts. So an IPC/sync call like prs.getStatus({ prId: "#123" }) now resolves the PR successfully and then writes "#123" as the snapshot key, which fails foreign-key validation (or at best creates an unusable orphan if FK enforcement is off). Normalize these code paths to the canonical row.id before persisting snapshots, or keep locator parsing scoped to APIs that only read by locator.

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