Skip to content

Gate Automations and VM in production builds#523

Merged
arul28 merged 1 commit into
mainfrom
ade-75-automations-graph-ux-polish-false-warning-simulate-dead-end-lane-required-run-now-reparent-undo-double-click-rollback-order
Jun 3, 2026
Merged

Gate Automations and VM in production builds#523
arul28 merged 1 commit into
mainfrom
ade-75-automations-graph-ux-polish-false-warning-simulate-dead-end-lane-required-run-now-reparent-undo-double-click-rollback-order

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Jun 3, 2026

Summary\n- hide Automations behind a production coming-soon gate in packaged desktop builds while keeping source/dev builds available\n- block production CLI/runtime wiring for Automations, Linear ingress, and macOS VM unless internal env overrides are set\n- remove production-facing webhook settings affordances and update agent CLI guidance so agents do not treat these surfaces as usable in production\n\n## Validation\n- npm --prefix apps/ade-cli run typecheck -- --pretty false\n- npm --prefix apps/ade-cli run test\n- npm --prefix apps/desktop run typecheck -- --pretty false\n- npm --prefix apps/desktop exec -- vitest run src/main/services/adeActions/registry.test.ts src/main/services/automations/automationService.test.ts src/main/services/automations/automationIngressService.test.ts\n- npm --prefix apps/desktop run build\n- git diff --check\n- packaged CLI gate smoke: copied built cli.cjs outside source checkout and confirmed automations, linear ingress, and macos-vm are hidden/blocked without internal env overrides\n\n## Notes\nThis intentionally keeps Automations and macOS VM available for source/dev builds while packaged production builds present them as coming soon or unavailable.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added webhook gateway configuration for external event automations
    • GitHub webhooks can now trigger automations directly
    • New CLI commands for managing automation ingress webhooks
    • Automations feature now gated appropriately for production vs. dev builds
  • UI Updates

    • Improved automation rule editor with webhook gateway status indicators
    • Added "Coming soon" messaging for automations in production builds
    • Updated design tokens and styling for automations interface

Greptile Summary

This PR gates Automations, Linear webhook ingress, and macOS VM behind a production-build wall: packaged desktop builds show a "coming soon" screen and have all automation services nulled out, while source/dev builds retain full access. It also adds a webhook gateway abstraction (Tailscale Funnel or custom HTTPS URL) for directing external GitHub/Linear events to the local ADE runtime, plus a /github-webhooks endpoint with HMAC verification.

  • Feature gates: automationAvailability.ts centralises env-override logic; main.ts, bootstrap.ts, and registry.ts all use it to conditionally create/expose services. AutomationsProductionGate in the renderer mirrors this for the UI.
  • Webhook gateway: automationService.ts gains setWebhookGatewayPublicUrl, refreshWebhookGatewayStatus (Tailscale probe via execFile), and assertWebhookGatewayReadyForRule gating; automationIngressService.ts adds a /github-webhooks route with HMAC verification; linearIngressService.ts grows a direct-webhook registration path bypassing the relay when a gateway URL is configured.
  • Bug fixes in WorkspaceGraphPage.tsx: in-place completed.reverse() mutations replaced with [...completed].reverse(), and an undoToastPending guard prevents double-click undo races.

Confidence Score: 3/5

The core feature-flag plumbing is solid, but two issues in the new webhook gateway path could cause incorrect data at runtime for users who enable the feature.

The gating logic in automationAvailability.ts, main.ts, bootstrap.ts, and registry.ts is straightforward and well-tested. The bug fixes in WorkspaceGraphPage.tsx are correct. The concern is concentrated in the new webhook integration code: linearIngressService.ts can accumulate duplicate Linear webhook registrations when the gateway public URL is changed, leading to doubled event delivery. Separately, automationService.ts getIngressStatus() silently overwrites ingressStatusRef on every call, potentially discarding freshly-refreshed Tailscale probe data.

apps/desktop/src/main/services/cto/linearIngressService.ts (direct-webhook deduplication on URL change) and apps/desktop/src/main/services/automations/automationService.ts (getIngressStatus side-effectful mutation of ingressStatusRef).

Important Files Changed

Filename Overview
apps/desktop/src/shared/automationAvailability.ts New shared module centralizing env-override and packaged-state checks for Automations and macOS VM; logic is clean and well-tested.
apps/ade-cli/src/bootstrap.ts Adds feature-flag gating for automationService, automationIngressService, automationPlannerService, and macosVmService in the headless runtime; dist/bootstrap.cjs is treated as a source-checkout build.
apps/ade-cli/src/cli.ts Adds assertAutomationsCliEnabled / assertMacosVmCliEnabled guards, new automations ingress subcommands and linear ingress start / start-local routes, and dynamic help-text stripping for hidden features.
apps/desktop/src/main/main.ts Gates automationService, automationIngressService, githubPollingService, linearIngressService, automationPlannerService, and both macosVm service instances behind areAutomationsEnabledForPackagedState / isMacosVmEnabledForPackagedState.
apps/desktop/src/main/services/automations/automationService.ts Adds webhook gateway status tracking (Tailscale probe via execFile, publicUrl persistence in project config), assertWebhookGatewayReadyForRule gating, and new public methods; getIngressStatus() mutates ingressStatusRef and normalizePublicWebhookUrl is duplicated vs. linearIngressService.ts.
apps/desktop/src/main/services/cto/linearIngressService.ts Adds direct-webhook fallback path, env-var relay config overrides, and startLocalWebhook as a public method; orphaned webhook accumulation when the gateway URL changes is not handled.
apps/desktop/src/main/services/automations/automationIngressService.ts Adds a /github-webhooks endpoint with HMAC verification and full GitHub event → AutomationTriggerType mapping; path normalization under /ade-webhooks/ is correctly handled.
apps/desktop/src/main/services/adeActions/registry.ts Adds automations/macosVm/linearIngress domain nullification based on packaged state and new startIngress / refreshWebhookGatewayStatus / setWebhookGatewayPublicUrl action entries.
apps/desktop/src/renderer/components/automations/AutomationsPage.tsx Wraps Automations UI in AutomationsProductionGate; defaults to dev on IPC error, which could briefly show the full UI to packaged users.
apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx Fixes in-place mutation of completed[] in rollback loops and adds undoToastPending guard to prevent double-click undo race; both are correct bug fixes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[App Start / CLI Invoked] --> B{isPackaged?}
    B -- Yes / packaged --> C[automationsEnabled = false\nmacosVmEnabled = false]
    B -- No / dev --> D[automationsEnabled = true\nmacosVmEnabled = true]
    B -- ENV override set --> E[readAutomationsEnvOverride\nreadMacosVmEnvOverride]
    E --> C
    E --> D
    C --> F[automationService = null\nlinearIngressService = null\nmacosVmService = null]
    C --> G[UI: ProductionAutomationsComingSoon]
    C --> H[ADE action domains: automations/linear_ingress/macos_vm → null]
    D --> I[automationService created]
    I --> J[refreshWebhookGatewayStatus\n— Tailscale probe via execFile]
    J --> K{publicUrl configured?}
    K -- Custom HTTPS URL --> L[Gateway ready]
    K -- Tailscale .ts.net URL --> M{Funnel active?}
    M -- Yes --> L
    M -- No --> N[pending-approval]
    K -- None --> O[needs-public-url]
    I --> P[automationIngressService\n/automation-webhooks/:id\n/github-webhooks — HMAC verified]
    I --> Q[linearIngressService\ndirect webhook or relay path]
Loading

Comments Outside Diff (3)

  1. apps/desktop/src/main/services/automations/automationService.ts, line 2779-2807 (link)

    P1 getIngressStatus() silently mutates ingressStatusRef

    getIngressStatus() directly assigns to ingressStatusRef without going through updateIngressStatus(), which means it bypasses the emit({ type: "ingress-updated" }) side-channel. Callers reading status through subscribed events will not be notified of the re-computed values, while callers using the return value will see the updated data — two different pictures of the same state until the next explicit update. More importantly, every call to this method silently overwrites the current ingressStatusRef, including any freshly-refreshed Tailscale data written by refreshWebhookGatewayStatus(), because the block unconditionally overwrites webhookGateway.status with a fallback derivation that may lag the async refresh result.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/automations/automationService.ts
    Line: 2779-2807
    
    Comment:
    **`getIngressStatus()` silently mutates `ingressStatusRef`**
    
    `getIngressStatus()` directly assigns to `ingressStatusRef` without going through `updateIngressStatus()`, which means it bypasses the `emit({ type: "ingress-updated" })` side-channel. Callers reading status through subscribed events will not be notified of the re-computed values, while callers using the return value will see the updated data — two different pictures of the same state until the next explicit update. More importantly, every call to this method silently overwrites the current `ingressStatusRef`, including any freshly-refreshed Tailscale data written by `refreshWebhookGatewayStatus()`, because the block unconditionally overwrites `webhookGateway.status` with a fallback derivation that may lag the async refresh result.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/desktop/src/main/services/automations/automationService.ts, line 2520-2531 (link)

    P2 Duplicate normalizePublicWebhookUrl with diverging behavior

    linearIngressService.ts defines its own normalizePublicWebhookUrl with two differences: it also clears hash and search from the URL, and it strips all trailing slashes (/\/+$/) instead of just one (/\/$/). A URL like https://host.example.com/ade-webhooks// normalizes to https://host.example.com/ade-webhooks/ in automationService.ts but to https://host.example.com/ade-webhooks in linearIngressService.ts. The webhook URL appended by appendWebhookPath in linearIngressService.ts would then not match the publicUrl stored by automationService, breaking the isReady checks that compare the two values.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/automations/automationService.ts
    Line: 2520-2531
    
    Comment:
    **Duplicate `normalizePublicWebhookUrl` with diverging behavior**
    
    `linearIngressService.ts` defines its own `normalizePublicWebhookUrl` with two differences: it also clears `hash` and `search` from the URL, and it strips all trailing slashes (`/\/+$/`) instead of just one (`/\/$/`). A URL like `https://host.example.com/ade-webhooks//` normalizes to `https://host.example.com/ade-webhooks/` in `automationService.ts` but to `https://host.example.com/ade-webhooks` in `linearIngressService.ts`. The webhook URL appended by `appendWebhookPath` in `linearIngressService.ts` would then not match the `publicUrl` stored by `automationService`, breaking the `isReady` checks that compare the two values.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  3. apps/desktop/src/renderer/components/automations/AutomationsPage.tsx, line 144-176 (link)

    P2 UI gate defaults to "dev" on IPC failure

    When window.ade.app.getInfo() rejects (e.g., transient IPC error on startup), the useAppPackagingState hook resolves to "dev", rendering the full Automations UI to packaged users. The backend gate (automationsEnabled in main.ts and getAdeActionDomainServices) still prevents actual execution, so data won't be exposed, but users will briefly see the Automations interface. Defaulting to "packaged" on error would be more conservative for a production gate.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/components/automations/AutomationsPage.tsx
    Line: 144-176
    
    Comment:
    **UI gate defaults to "dev" on IPC failure**
    
    When `window.ade.app.getInfo()` rejects (e.g., transient IPC error on startup), the `useAppPackagingState` hook resolves to `"dev"`, rendering the full Automations UI to packaged users. The backend gate (`automationsEnabled` in `main.ts` and `getAdeActionDomainServices`) still prevents actual execution, so data won't be exposed, but users will briefly see the Automations interface. Defaulting to `"packaged"` on error would be more conservative for a production gate.
    
    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
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
apps/desktop/src/main/services/cto/linearIngressService.ts:350-382
**Orphaned Linear webhooks on gateway URL change**

`ensureRelayEndpoint` creates a new Linear webhook when `direct.webhookUrl` doesn't already exist in the list of registered webhooks. But when `webhookGatewayPublicUrl` is changed (via `setWebhookGatewayPublicUrl`), the new URL produces a different `direct.webhookUrl`. The check `!existing.some((entry) => entry.url === direct.webhookUrl)` now misses the old entry and registers a second webhook. Both old and new URLs remain active on Linear's side, causing every event to be delivered twice (duplicate `handleIngressEvent` calls) until the stale registration is manually removed.

The same issue applies in reverse: if the gateway URL is cleared, the previously registered webhook is abandoned on Linear but continues to receive deliveries that will now 404 or go unprocessed.

### Issue 2 of 5
apps/desktop/src/main/services/automations/automationService.ts:2779-2807
**`getIngressStatus()` silently mutates `ingressStatusRef`**

`getIngressStatus()` directly assigns to `ingressStatusRef` without going through `updateIngressStatus()`, which means it bypasses the `emit({ type: "ingress-updated" })` side-channel. Callers reading status through subscribed events will not be notified of the re-computed values, while callers using the return value will see the updated data — two different pictures of the same state until the next explicit update. More importantly, every call to this method silently overwrites the current `ingressStatusRef`, including any freshly-refreshed Tailscale data written by `refreshWebhookGatewayStatus()`, because the block unconditionally overwrites `webhookGateway.status` with a fallback derivation that may lag the async refresh result.

### Issue 3 of 5
apps/desktop/src/main/services/automations/automationService.ts:2520-2531
**Duplicate `normalizePublicWebhookUrl` with diverging behavior**

`linearIngressService.ts` defines its own `normalizePublicWebhookUrl` with two differences: it also clears `hash` and `search` from the URL, and it strips all trailing slashes (`/\/+$/`) instead of just one (`/\/$/`). A URL like `https://host.example.com/ade-webhooks//` normalizes to `https://host.example.com/ade-webhooks/` in `automationService.ts` but to `https://host.example.com/ade-webhooks` in `linearIngressService.ts`. The webhook URL appended by `appendWebhookPath` in `linearIngressService.ts` would then not match the `publicUrl` stored by `automationService`, breaking the `isReady` checks that compare the two values.

### Issue 4 of 5
apps/ade-cli/src/bootstrap.ts:271-290
**`isSourceCheckoutRuntimeModule` regex matches `dist/bootstrap.cjs` in the source tree**

The pattern matches both `src/bootstrap.ts` (dev) and `dist/bootstrap.cjs` (local build). A developer who builds the CLI and runs it outside the source tree would still be gated correctly, but this asymmetry is worth keeping in mind if the `dist/` artifacts are ever distributed separately from the packaged build.

### Issue 5 of 5
apps/desktop/src/renderer/components/automations/AutomationsPage.tsx:144-176
**UI gate defaults to "dev" on IPC failure**

When `window.ade.app.getInfo()` rejects (e.g., transient IPC error on startup), the `useAppPackagingState` hook resolves to `"dev"`, rendering the full Automations UI to packaged users. The backend gate (`automationsEnabled` in `main.ts` and `getAdeActionDomainServices`) still prevents actual execution, so data won't be exposed, but users will briefly see the Automations interface. Defaulting to `"packaged"` on error would be more conservative for a production gate.

Reviews (1): Last reviewed commit: "Gate automations and VM in production bu..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 3, 2026

ADE-75

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 3, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Jun 3, 2026 1:26am

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Jun 3, 2026

@copilot review but do not make fixes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements production-ready feature gating for automations and macOS VM, introduces webhook gateway readiness detection with Tailscale Funnel support, adds GitHub webhook ingress handling, and extends CLI/IPC surfaces for gateway management while simplifying the automation rule editor UI.

Changes

Automations & Linear Webhook Gateway Production Gating

Layer / File(s) Summary
Feature Availability Infrastructure
apps/desktop/src/shared/automationAvailability.ts
New shared utilities to determine automation/macOS VM feature availability based on packaged state and environment variable overrides (ADE_ENABLE_AUTOMATIONS / ADE_DISABLE_AUTOMATIONS, etc.).
CLI & Desktop Feature Gating
apps/ade-cli/src/bootstrap.ts, apps/ade-cli/src/cli.ts, apps/desktop/src/main/main.ts
CLI detects packaged state and gates automation/macOS-VM command routing with error assertions; desktop main process conditionally creates automation/ingress services based on computed feature flags; bootstrap sets isPackaged on runtime objects.
Webhook Gateway Status & Readiness Detection
apps/desktop/src/main/services/automations/automationService.ts, apps/desktop/src/shared/types/automations.ts, apps/desktop/src/shared/types/config.ts
Introduces webhook gateway status type and enforcement: reads effective public HTTPS URL from config, normalizes URLs, infers provider (custom vs Tailscale), runs background Tailscale CLI refresh routine to detect Funnel status, and throws when enabling external-event rules on unprepared gateways.
GitHub Webhook Support in Automation Ingress
apps/desktop/src/main/services/automations/automationIngressService.ts, apps/desktop/src/main/services/automations/automationIngressService.test.ts
Automation ingress service now accepts GitHub webhooks at /github-webhooks, verifies HMAC-SHA256 signatures, maps issue/PR events to internal automation triggers, and dispatches them; local webhook support remains at /automation-webhooks/:automationId.
Linear Ingress Direct Webhook Fallback
apps/desktop/src/main/services/cto/linearIngressService.ts, apps/desktop/src/main/services/cto/linearIntake.test.ts
Linear service supports direct webhook registration to Linear when relay API/project/token are unavailable, including HTTPS URL normalization, signing-secret selection from secrets or env, and HTTP server routing for optional /ade-webhooks/ prefix.
Automation Planner & Service Integration
apps/desktop/src/main/services/automations/automationPlannerService.ts, apps/desktop/src/main/services/automations/automationPlannerService.test.ts
Planner checks webhook gateway readiness before enabling external-event rules via new assertWebhookGatewayReadyForRule callback in automation service dependency.
ADE Action Registry & IPC Wiring
apps/desktop/src/main/services/adeActions/registry.ts, apps/desktop/src/main/services/adeActions/registry.test.ts, apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/preload/global.d.ts, apps/desktop/src/preload/preload.ts, apps/desktop/src/shared/ipc.ts
Action registry conditionally gates automation/linear/macOS-VM domains on packaged state; extends action allowlist with ingress/gateway operations; IPC handlers bridge refresh/set-url operations to preload; new webhook gateway methods exposed on window.ade.automations.
Headless Linear Ingress & Automation Dispatch
apps/ade-cli/src/headlessLinearServices.ts
Headless Linear ingress now wires events into automation dispatch by building triggers from Linear issue updates and calling dispatchIngressTrigger when automation service is available.
CLI Ingress Subcommands & Formatting
apps/ade-cli/src/cli.ts, apps/ade-cli/src/cli.test.ts
CLI adds automations ingress (status/start/refresh/set-url/clear-url) and linear ingress (start/start-local) subcommands; implements dynamic help filtering via topLevelHelpText() / commandHelpText() based on feature gates; new formatAutomationIngress formatter structures gateway/webhook/relay status into readable text.
Renderer Production Gating & Editor Simplification
apps/desktop/src/renderer/components/automations/AutomationsPage.tsx, apps/desktop/src/renderer/components/automations/AutomationsComingSoon.tsx, apps/desktop/src/renderer/components/automations/RulesTab.tsx, apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx, apps/desktop/src/renderer/components/automations/ActionList.tsx, apps/desktop/src/renderer/components/automations/ActionRow.tsx, apps/desktop/src/renderer/components/automations/designTokens.ts, apps/desktop/src/renderer/components/automations/AdeActionEditor.tsx, apps/desktop/src/renderer/components/app/TabNav.tsx, apps/desktop/src/renderer/browserMock.ts
Automations page gates UI behind packaging state check; rule editor removes brains/context/model/limits/output sections and simplifies action row/list prop contracts; new WebhookGatewayCallout and SuccessNotice components; design tokens relocated from CTO shared module to local automations with updated dark theme.
Project Config & CLI Guidance Updates
apps/desktop/src/main/services/config/projectConfigService.ts, apps/desktop/src/shared/adeCliGuidance.ts
Project UI config now includes webhookGatewayPublicUrl storage (shared/local); CLI guidance clarifies that automations/linear-ingress/macOS-VM are internal/coming-soon in production.
Graph Page Undo Toast State Fix
apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx
Reparent undo flow now uses non-mutating array reversal and adds undoToastPending state to prevent auto-dismiss and disable button during undo operation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • arul28/ADE#183: Automations dispatch system depends on the Linear ingress event integration pathway refactored in this PR.

Suggested labels

desktop

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade-75-automations-graph-ux-polish-false-warning-simulate-dead-end-lane-required-run-now-reparent-undo-double-click-rollback-order

Comment on lines 350 to +382
const ensureRelayEndpoint = async (force = false): Promise<RelayEnsureWebhookResponse | null> => {
const config = buildRelayConfig();
if (!config.configured) {
updateStatus({
relay: {
configured: false,
healthy: false,
status: "disabled",
webhookUrl: null,
endpointId: null,
},
});
return null;
const direct = buildDirectWebhookConfig();
if (!direct.configured) {
updateStatus({
relay: {
configured: false,
healthy: false,
status: "disabled",
webhookUrl: direct.webhookUrl,
endpointId: null,
lastError: direct.webhookUrl
? `Missing ${DIRECT_WEBHOOK_SECRET_REF}.`
: "Missing webhook gateway public URL.",
},
});
return null;
}
try {
localSigningSecret = direct.signingSecret!;
const existing = await args.linearClient.listWebhooks();
if (!existing.some((entry) => entry.url === direct.webhookUrl)) {
await args.linearClient.createWebhook({
url: direct.webhookUrl!,
secret: direct.signingSecret!,
label: `ADE ${args.projectId} workflow ingress`,
resourceTypes: ["Issue", "IssueLabel"],
allPublicTeams: true,
});
}
const payload: RelayEnsureWebhookResponse = {
endpointId: "direct",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Orphaned Linear webhooks on gateway URL change

ensureRelayEndpoint creates a new Linear webhook when direct.webhookUrl doesn't already exist in the list of registered webhooks. But when webhookGatewayPublicUrl is changed (via setWebhookGatewayPublicUrl), the new URL produces a different direct.webhookUrl. The check !existing.some((entry) => entry.url === direct.webhookUrl) now misses the old entry and registers a second webhook. Both old and new URLs remain active on Linear's side, causing every event to be delivered twice (duplicate handleIngressEvent calls) until the stale registration is manually removed.

The same issue applies in reverse: if the gateway URL is cleared, the previously registered webhook is abandoned on Linear but continues to receive deliveries that will now 404 or go unprocessed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/cto/linearIngressService.ts
Line: 350-382

Comment:
**Orphaned Linear webhooks on gateway URL change**

`ensureRelayEndpoint` creates a new Linear webhook when `direct.webhookUrl` doesn't already exist in the list of registered webhooks. But when `webhookGatewayPublicUrl` is changed (via `setWebhookGatewayPublicUrl`), the new URL produces a different `direct.webhookUrl`. The check `!existing.some((entry) => entry.url === direct.webhookUrl)` now misses the old entry and registers a second webhook. Both old and new URLs remain active on Linear's side, causing every event to be delivered twice (duplicate `handleIngressEvent` calls) until the stale registration is manually removed.

The same issue applies in reverse: if the gateway URL is cleared, the previously registered webhook is abandoned on Linear but continues to receive deliveries that will now 404 or go unprocessed.

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

Fix in Claude Code

Comment on lines 271 to 290
};
}

function isSourceCheckoutRuntimeModule(modulePath: string): boolean {
return /[/\\]apps[/\\]ade-cli[/\\](?:src|dist)[/\\]bootstrap\.(?:ts|js|cjs)$/i.test(modulePath);
}

function automationsEnabledForHeadlessRuntime(): boolean {
const override = readAutomationsEnvOverride(process.env);
if (override !== null) return override;
return isSourceCheckoutRuntimeModule(__filename);
}

function macosVmEnabledForHeadlessRuntime(): boolean {
const override = readMacosVmEnvOverride(process.env);
if (override !== null) return override;
return isSourceCheckoutRuntimeModule(__filename);
}

function resolveCurrentAdeCliEntry(): string | 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.

P2 isSourceCheckoutRuntimeModule regex matches dist/bootstrap.cjs in the source tree

The pattern matches both src/bootstrap.ts (dev) and dist/bootstrap.cjs (local build). A developer who builds the CLI and runs it outside the source tree would still be gated correctly, but this asymmetry is worth keeping in mind if the dist/ artifacts are ever distributed separately from the packaged build.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ade-cli/src/bootstrap.ts
Line: 271-290

Comment:
**`isSourceCheckoutRuntimeModule` regex matches `dist/bootstrap.cjs` in the source tree**

The pattern matches both `src/bootstrap.ts` (dev) and `dist/bootstrap.cjs` (local build). A developer who builds the CLI and runs it outside the source tree would still be gated correctly, but this asymmetry is worth keeping in mind if the `dist/` artifacts are ever distributed separately from the packaged build.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

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: 11

Caution

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

⚠️ Outside diff range comments (3)
apps/desktop/src/renderer/browserMock.ts (1)

3936-4003: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the webhook gateway mock stateful.

setWebhookGatewayPublicUrl() ignores its input and always returns the hard-coded URL, and refreshWebhookGatewayStatus() never reflects any prior change. That makes the new gateway-management UI look broken in browser preview and hides renderer/IPC contract regressions in the exact flow this PR adds.

Keep a mutable mock ingress-status object and update webhookGateway.publicUrl/lastCheckedAt before returning it from these methods.

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

In `@apps/desktop/src/renderer/browserMock.ts` around lines 3936 - 4003, The
current mocks (getIngressStatus, refreshWebhookGatewayStatus,
setWebhookGatewayPublicUrl) always return hard-coded objects and ignore prior
changes; make the mock stateful by creating a shared mutable ingressStatus
object used by getIngressStatus, refreshWebhookGatewayStatus, and
setWebhookGatewayPublicUrl, have setWebhookGatewayPublicUrl update
ingressStatus.webhookGateway.publicUrl and
ingressStatus.webhookGateway.lastCheckedAt from the input, and have
refreshWebhookGatewayStatus return the current ingressStatus (updating
lastCheckedAt when simulating a refresh) so subsequent calls and the UI/IPC flow
reflect prior changes.
apps/desktop/src/renderer/components/automations/ActionRow.tsx (1)

69-87: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't strand existing per-step runtime/model settings.

ActionRowValue still carries targetLaneId, condition, continueOnFailure, timeoutMs, retry, modelConfig, and permissionConfig, and the save path still preserves them, but this UI no longer lets users inspect, edit, or clear any of those fields. Existing rules can now show a custom model badge while giving the user no way to change the underlying config. Either keep the editors or migrate/strip unsupported fields explicitly on load/save.

Also applies to: 152-251

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

In `@apps/desktop/src/renderer/components/automations/ActionRow.tsx` around lines
69 - 87, ActionRow currently accepts and preserves fields on ActionRowValue
(targetLaneId, condition, continueOnFailure, timeoutMs, retry, modelConfig,
permissionConfig) but the UI no longer exposes editors for them; either
re-introduce editors for those fields in ActionRow (or linked subcomponents) so
users can inspect/edit/clear them, or explicitly migrate/strip unsupported
fields when loading/saving: update the component logic that consumes/returns
ActionRowValue to transform incoming value (removing or mapping
modelConfig/permissionConfig and other runtime fields) and ensure
onChange/onRemove handlers emit the cleaned shape, and add a clear migration
function (e.g., normalizeActionRowValue) used in ActionRow and any save path to
avoid stranded configs.
apps/ade-cli/src/bootstrap.ts (1)

927-947: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Packaged runtimes still wire linearIngressService.

createHeadlessLinearServices() is unconditional here, so packaged/headless runtimes still construct and expose Linear ingress even though this PR is supposed to block production runtime wiring for it. Hiding the CLI commands alone won't cover the socket-backed/runtime path if callers can still reach this service directly. Based on learnings: For ADE CLI changes, verify both headless mode and the desktop socket-backed ADE RPC path.

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

In `@apps/ade-cli/src/bootstrap.ts` around lines 927 - 947, The code
unconditionally calls createHeadlessLinearServices (producing
headlessLinearServices and wiring linearIngressService) which causes
packaged/headless runtimes to expose Linear ingress; guard this creation with
the same runtime/feature flag used to block production wiring (e.g., check
isPackagedRuntime or enableLinearIngress) so createHeadlessLinearServices is
only invoked when Linear should be available; update the caller site where
headlessLinearServices is used and any socket-backed ADE RPC path to respect the
same condition so neither headless nor desktop socket-backed paths
construct/expose the Linear ingress service.
🧹 Nitpick comments (1)
apps/desktop/src/main/services/adeActions/registry.test.ts (1)

98-147: ⚡ Quick win

Add override-path coverage for packaged builds.

These tests only cover the default packaged/dev states. The load-bearing behavior in this PR is that packaged builds can be re-enabled via internal env overrides, so regressions in ADE_ENABLE_AUTOMATIONS / ADE_ENABLE_MACOS_VM would still pass here.

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

In `@apps/desktop/src/main/services/adeActions/registry.test.ts` around lines 98 -
147, Update the test suite for getAdeActionDomainServices to add cases that
verify packaged builds respect the ADE_ENABLE_AUTOMATIONS / ADE_ENABLE_MACOS_VM
override envs (and their corresponding ADE_DISABLE_* counterparts) by using
withEnv to set override variables and asserting that
automations/automation_planner and macos_vm domains are present when enabled and
absent when explicitly disabled; reference the existing tests around
getAdeActionDomainServices and withEnv to add these packaged-override scenarios
so regressions in ADE_ENABLE_AUTOMATIONS and ADE_ENABLE_MACOS_VM behavior are
covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 248-264: The path-based gate is using the raw CLI_ENTRY_PATH (from
process.argv[1]) which can be a symlink and cause false negatives; update
isSourceCheckoutCliEntryPath to resolve the path before testing (e.g., call a
safe realpath resolver like realpathSync in try/catch or a realpathSyncSafe
utility inside isSourceCheckoutCliEntryPath or at the call sites
automationsCliEnabled/macosVmCliEnabled) and then run the existing regex checks
against the resolved target (so the /apps/ade-cli/dist/cli.cjs and source paths
match actual file targets rather than symlinked argv values).

In `@apps/ade-cli/src/headlessLinearServices.ts`:
- Around line 1897-1908: The current try/catch around the whole loop allows the
first failing dispatch to abort remaining dispatches; update
buildLinearAutomationDispatches handling so each dispatched item is handled
independently: either map the dispatches to promises and use Promise.allSettled
to run them concurrently and log any failures, or wrap each await
automationService.dispatchIngressTrigger(dispatched) in its own try/catch so a
single failure (inside the loop over buildLinearAutomationDispatches(event))
only logs via args.logger.warn (include issueId and the specific dispatched info
and error) and continues processing the rest; reference
buildLinearAutomationDispatches, automationService.dispatchIngressTrigger, and
args.logger.warn (and issueId) when making the change.

In `@apps/desktop/src/main/services/adeActions/registry.test.ts`:
- Around line 12-34: The helper withEnv currently accepts a synchronous callback
and restores process.env immediately because it doesn't await async callbacks;
change withEnv to be asynchronous by updating its signature to async function
withEnv<T>(updates: Record<string, string | undefined>, run: () => Promise<T> |
T): Promise<T>, call and await the result of run() inside the try block (e.g.,
const result = await run()), then return that result; keep the existing env
save/restore logic in the finally block so environment variables remain set for
the entire duration of the awaited callback — reference the withEnv function to
make these edits.

In `@apps/desktop/src/main/services/automations/automationIngressService.ts`:
- Around line 164-180: The PR comment webhook path in
automationIngressService.ts fabricates a PR context without
headBranch/baseBranch or branch/targetBranch, so branch-filtered automations in
automationService.ts won't match; update the issue_comment handling (the block
that builds issue, computes issueIsPr and returns triggerType/pr) to either (A)
hydrate full PR details when issueIsPr is true by fetching the PR via the GitHub
API and populating headBranch/baseBranch and branch/targetBranch on the returned
object before returning, or (B) explicitly mark PR comments as unsupported by
returning null for PR triggers and/or returning a distinct unsupported flag so
callers skip branch-matching; target the code paths using readNested,
buildIssueContext, the issueIsPr boolean, and the returned pr object so the
dispatched trigger includes the branch information automationService.ts expects.

In `@apps/desktop/src/main/services/automations/automationService.ts`:
- Around line 1196-1261: refreshWebhookGatewayStatus currently updates
ingressStatusRef but does not re-schedule gateway-backed automations when the
webhook gateway flips ready or its publicUrl changes; detect changes to
readiness/publicUrl by comparing the newly-computed next (from
refreshWebhookGatewayStatus) with the previous ingressStatusRef.webhookGateway
(or store the previous ready/publicUrl at start of the function) and if ready or
publicUrl changed from false/null to true/non-null, call syncFromConfig() to
re-sync gateway-backed rules. Keep the existing updateIngressStatus({
webhookGateway: next }) behavior but invoke syncFromConfig() after
updateIngressStatus when the readiness/publicUrl transition is detected;
reference refreshWebhookGatewayStatus, ingressStatusRef.webhookGateway,
updateIngressStatus, and syncFromConfig in the implementation.
- Around line 65-75: normalizePublicWebhookUrl currently only checks the scheme;
update it to reject non-public hosts so internal addresses (localhost,
127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, IPv6 ::1, fc00::/7,
link-local fe80::/10, and literal "localhost" or ".local" names) return null. In
normalizePublicWebhookUrl(), after creating the URL and verifying protocol ===
"https:", extract url.hostname and if it's an IP use a standard IP check
(net.isIP or equivalent) to detect private/loopback ranges and reject them, or
if it's a hostname reject explicit "localhost" and local mDNS patterns (e.g.
ending with ".local"); only allow otherwise and then trim trailing slash as
before so defaultWebhookGatewayStatus()/getIngressStatus() won't treat
non-public endpoints as ready. Ensure error handling still returns null on parse
failures.

In `@apps/desktop/src/main/services/config/projectConfigService.ts`:
- Around line 1919-1923: Validate webhookGatewayPublicUrl as a proper HTTPS
origin before assigning it to out.webhookGatewayPublicUrl: after computing
webhookGatewayPublicUrl = asString(... )?.trim(), attempt to parse it with the
URL constructor and ensure url.protocol === 'https:' and that the URL represents
a base/origin (e.g. url.pathname === '/' or url.pathname === ''), and only then
set out.webhookGatewayPublicUrl = webhookGatewayPublicUrl; if the value fails
validation, skip assigning it (and optionally surface/record a validation error
elsewhere). This change targets the webhookGatewayPublicUrl handling in
projectConfigService (the webhookGatewayPublicUrl local variable and the out.ui
assignment).

In `@apps/desktop/src/main/services/cto/linearIngressService.ts`:
- Around line 369-399: The code only checks webhook.url when ensuring a webhook,
so if direct.signingSecret rotates the existing Linear webhook remains with the
old secret; modify the ensure logic around localSigningSecret /
args.linearClient.listWebhooks() to also compare the stored signing secret
(direct.signingSecret) against the existing webhook.secret and, if different,
call the appropriate update path (use args.linearClient.updateWebhook(...) if
available or delete + args.linearClient.createWebhook(...)) to replace the
secret for the matching direct.webhookUrl; update the
RelayEnsureWebhookResponse/relayWebhook and call updateStatus(...) after the
update so state reflects the new signing secret and health.

In `@apps/desktop/src/renderer/components/automations/RulesTab.tsx`:
- Around line 785-815: When switching selection to manualRunRule you must guard
against accidentally overwriting an in-memory draft and preserve the
created-lane draft; modify the onClick handlers that call
setSelectedRuleId(manualRunRule.id) (the "Create lanes for runs" and "Edit lane
mode" handlers) to first call confirmDiscardIfDirty() and, if it
returns/permits, set a one-shot override ref (e.g., draftOverrideRef) containing
the precomputed draftWithCreateLaneMode(toDraftFromRule(manualRunRule)) (for the
Create path) or the current draft (for Edit), then call
setSelectedRuleId(manualRunRule.id) and setDraft from that overrideRef instead
of relying on the selection effect to re-run toDraftFromRule(selected); also
clear the override ref after applying and keep savedSnapshotRef/current issue
state updates as before.

In `@apps/desktop/src/shared/automationAvailability.ts`:
- Around line 17-20: In readAutomationsEnvOverride, currently
envFlagEnabled(AUTOMATIONS_ENABLE_ENV) is checked before the disable flag so a
set disable flag can be overridden; change the logic so the disable flag takes
precedence (check envFlagEnabled(env[AUTOMATIONS_DISABLE_ENV]) first and return
false if set) or explicitly detect both flags set and return false (or throw if
you prefer strict behavior); apply the same change to the analogous function
around lines 32-35 (the other env-reading function) — reference symbols:
readAutomationsEnvOverride, AUTOMATIONS_ENABLE_ENV, AUTOMATIONS_DISABLE_ENV.

In `@apps/desktop/src/shared/ipc.ts`:
- Around line 446-447: The new IPC channels/APIs (constants
automationsRefreshWebhookGatewayStatus and automationsSetWebhookGatewayPublicUrl
in ipc.ts, the ipcMain.handle registrations in registerIpc.ts, and the preload
methods window.ade.automations.refreshWebhookGatewayStatus /
setWebhookGatewayPublicUrl plus typings) are not used anywhere in production
renderer code—only referenced in browserMock.ts; either wire them into the real
UI or remove them. To fix: if the UI needs them, add calls from the appropriate
renderer component(s) (search for automation UI pages/components) to invoke
window.ade.automations.refreshWebhookGatewayStatus() and
setWebhookGatewayPublicUrl() and update any component props/state to surface
results; otherwise remove the unused constants in ipc.ts, remove the
ipcMain.handle registrations in registerIpc.ts, strip the preload methods and
global.d.ts typings, and delete the mock usage in browserMock.ts so there are no
unused IPC entry points left.

---

Outside diff comments:
In `@apps/ade-cli/src/bootstrap.ts`:
- Around line 927-947: The code unconditionally calls
createHeadlessLinearServices (producing headlessLinearServices and wiring
linearIngressService) which causes packaged/headless runtimes to expose Linear
ingress; guard this creation with the same runtime/feature flag used to block
production wiring (e.g., check isPackagedRuntime or enableLinearIngress) so
createHeadlessLinearServices is only invoked when Linear should be available;
update the caller site where headlessLinearServices is used and any
socket-backed ADE RPC path to respect the same condition so neither headless nor
desktop socket-backed paths construct/expose the Linear ingress service.

In `@apps/desktop/src/renderer/browserMock.ts`:
- Around line 3936-4003: The current mocks (getIngressStatus,
refreshWebhookGatewayStatus, setWebhookGatewayPublicUrl) always return
hard-coded objects and ignore prior changes; make the mock stateful by creating
a shared mutable ingressStatus object used by getIngressStatus,
refreshWebhookGatewayStatus, and setWebhookGatewayPublicUrl, have
setWebhookGatewayPublicUrl update ingressStatus.webhookGateway.publicUrl and
ingressStatus.webhookGateway.lastCheckedAt from the input, and have
refreshWebhookGatewayStatus return the current ingressStatus (updating
lastCheckedAt when simulating a refresh) so subsequent calls and the UI/IPC flow
reflect prior changes.

In `@apps/desktop/src/renderer/components/automations/ActionRow.tsx`:
- Around line 69-87: ActionRow currently accepts and preserves fields on
ActionRowValue (targetLaneId, condition, continueOnFailure, timeoutMs, retry,
modelConfig, permissionConfig) but the UI no longer exposes editors for them;
either re-introduce editors for those fields in ActionRow (or linked
subcomponents) so users can inspect/edit/clear them, or explicitly migrate/strip
unsupported fields when loading/saving: update the component logic that
consumes/returns ActionRowValue to transform incoming value (removing or mapping
modelConfig/permissionConfig and other runtime fields) and ensure
onChange/onRemove handlers emit the cleaned shape, and add a clear migration
function (e.g., normalizeActionRowValue) used in ActionRow and any save path to
avoid stranded configs.

---

Nitpick comments:
In `@apps/desktop/src/main/services/adeActions/registry.test.ts`:
- Around line 98-147: Update the test suite for getAdeActionDomainServices to
add cases that verify packaged builds respect the ADE_ENABLE_AUTOMATIONS /
ADE_ENABLE_MACOS_VM override envs (and their corresponding ADE_DISABLE_*
counterparts) by using withEnv to set override variables and asserting that
automations/automation_planner and macos_vm domains are present when enabled and
absent when explicitly disabled; reference the existing tests around
getAdeActionDomainServices and withEnv to add these packaged-override scenarios
so regressions in ADE_ENABLE_AUTOMATIONS and ADE_ENABLE_MACOS_VM behavior are
covered.
🪄 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: a1696eb2-cb60-49bb-a823-c00285afb7fb

📥 Commits

Reviewing files that changed from the base of the PR and between e924ae8 and 1e4380c.

📒 Files selected for processing (38)
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/adeActions/registry.test.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/automations/automationPlannerService.test.ts
  • apps/desktop/src/main/services/automations/automationPlannerService.ts
  • apps/desktop/src/main/services/automations/automationService.test.ts
  • apps/desktop/src/main/services/automations/automationService.ts
  • apps/desktop/src/main/services/config/projectConfigService.test.ts
  • apps/desktop/src/main/services/config/projectConfigService.ts
  • apps/desktop/src/main/services/cto/linearIngressService.ts
  • apps/desktop/src/main/services/cto/linearIntake.test.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/TabNav.tsx
  • apps/desktop/src/renderer/components/automations/ActionList.tsx
  • apps/desktop/src/renderer/components/automations/ActionRow.tsx
  • apps/desktop/src/renderer/components/automations/AdeActionEditor.tsx
  • apps/desktop/src/renderer/components/automations/AutomationsComingSoon.tsx
  • apps/desktop/src/renderer/components/automations/AutomationsPage.tsx
  • apps/desktop/src/renderer/components/automations/AutomationsTemplatesPage.tsx
  • apps/desktop/src/renderer/components/automations/RulesTab.tsx
  • apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx
  • apps/desktop/src/renderer/components/automations/designTokens.ts
  • apps/desktop/src/renderer/components/automations/shared.ts
  • apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/automationAvailability.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/automations.ts
  • apps/desktop/src/shared/types/config.ts

Comment thread apps/ade-cli/src/cli.ts
Comment on lines +248 to +264
function isSourceCheckoutCliEntryPath(modulePath: string): boolean {
return (
isSourceCliEntryPath(modulePath) ||
/[/\\]apps[/\\]ade-cli[/\\]dist[/\\]cli\.cjs$/i.test(modulePath)
);
}

function automationsCliEnabled(): boolean {
const override = readAutomationsEnvOverride(process.env);
if (override !== null) return override;
return isSourceCheckoutCliEntryPath(CLI_ENTRY_PATH);
}

function macosVmCliEnabled(): boolean {
const override = readMacosVmEnvOverride(process.env);
if (override !== null) return override;
return isSourceCheckoutCliEntryPath(CLI_ENTRY_PATH);
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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate cli.ts and print relevant regions around the reported lines
FILE="apps/ade-cli/src/cli.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"; exit 1
fi

echo "== Context around lines 220-310 =="
nl -ba "$FILE" | sed -n '220,310p'

# Find where CLI_ENTRY_PATH and any dist/module path constants are defined in this file
echo "== Definitions/uses of CLI_ENTRY_PATH and dist constants in cli.ts =="
rg -n "CLI_ENTRY_PATH|CLI_DIST_PATH|process\.argv\[1\]|argv\[1\]|realpath|realpathSync|realpathSyncSafe|sourceCliEntryPath|isSourceCliEntryPath" "$FILE"

# Print the implementations of helpers used here (isSourceCliEntryPath, read*EnvOverride) if in this file
echo "== Helper implementations (isSourceCliEntryPath/readAutomationsEnvOverride/readMacosVmEnvOverride) =="
rg -n "function (isSourceCliEntryPath|readAutomationsEnvOverride|readMacosVmEnvOverride|read.*EnvOverride)|const (CLI_ENTRY_PATH|CLI_DIST_PATH)" "$FILE"
# If functions are found, show their bodies
for sym in isSourceCliEntryPath readAutomationsEnvOverride readMacosVmEnvOverride; do
  line=$(rg -n "function $sym" "$FILE" | head -n1 | cut -d: -f1 || true)
  if [ -n "${line:-}" ]; then
    start=$((line-20)); end=$((line+80))
    echo "== $sym around $line =="
    nl -ba "$FILE" | sed -n "${start},${end}p"
  fi
done

# Search for realpathSyncSafe in repo (to see if it's available/used elsewhere)
echo "== realpathSyncSafe usage in repo =="
rg -n "realpathSyncSafe" -S .

# Search for CLI_ENTRY_PATH derivation elsewhere (if imported)
echo "== Any other references to CLI_ENTRY_PATH (name-based) =="
rg -n "CLI_ENTRY_PATH" -S apps/ade-cli/src

# Search for gating command logic around automations/macOS-vm
echo "== References to automationsCliEnabled/macosVmCliEnabled in cli.ts =="
rg -n "automationsCliEnabled\(|macosVmCliEnabled\(|automations(?!CliEnabled)|macos-vm" "$FILE" || true

Repository: arul28/ADE

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/ade-cli/src/cli.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"; exit 1
fi

echo "== Context around lines 220-310 =="
cat -n "$FILE" | sed -n '220,310p'

echo "== Definitions/uses of CLI_ENTRY_PATH and dist constants in cli.ts =="
rg -n "CLI_ENTRY_PATH|CLI_DIST_PATH|process\.argv\[1\]|argv\[1\]|realpath|realpathSync|realpathSyncSafe|sourceCliEntryPath|isSourceCliEntryPath" "$FILE" || true

echo "== Helper implementations (isSourceCliEntryPath/readAutomationsEnvOverride/readMacosVmEnvOverride) =="
rg -n "function (isSourceCliEntryPath|readAutomationsEnvOverride|readMacosVmEnvOverride)|const (CLI_ENTRY_PATH|CLI_DIST_PATH)" "$FILE" || true

for sym in isSourceCliEntryPath readAutomationsEnvOverride readMacosVmEnvOverride; do
  line=$(rg -n "function $sym" "$FILE" | head -n1 | cut -d: -f1 || true)
  if [ -n "${line:-}" ]; then
    start=$((line-20)); end=$((line+100))
    echo "== $sym around $line =="
    cat -n "$FILE" | sed -n "${start},${end}p"
  fi
done

echo "== realpathSyncSafe usage in repo =="
rg -n "realpathSyncSafe" -S . || true

echo "== Any other references to CLI_ENTRY_PATH (name-based) =="
rg -n "CLI_ENTRY_PATH" -S apps/ade-cli/src || true

echo "== References to automationsCliEnabled/macosVmCliEnabled in cli.ts =="
rg -n "automationsCliEnabled\(|macosVmCliEnabled\(|automations(?!CliEnabled)|macos-vm" "$FILE" || true

Repository: arul28/ADE

Length of output: 11246


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/ade-cli/src/cli.ts"

echo "== CLI_ENTRY_PATH definition context =="
cat -n "$FILE" | sed -n '180,235p'

echo "== realpathSyncSafe implementation =="
cat -n "$FILE" | sed -n '12020,12090p'

echo "== realpathIfExists usage helper (if present) =="
# Show the import location around line ~1-60
cat -n "$FILE" | sed -n '1,80p'

echo "== bootstrap.ts where ADE_CLI_ENTRY_PATH is set =="
FILE2="apps/ade-cli/src/bootstrap.ts"
if [ -f "$FILE2" ]; then
  cat -n "$FILE2" | sed -n '360,430p'
fi

echo "== Where is CLI_ENTRY_PATH overridden / sourced from env/global =="
rg -n "ADE_CLI_ENTRY_PATH|CLI_ENTRY_PATH\s*=" "$FILE" apps/ade-cli/src/bootstrap.ts

Repository: arul28/ADE

Length of output: 11719


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/ade-cli/src/cli.ts"

echo "== Find automations/macOS-vm gating usage in cli.ts (help + command dispatch) =="
rg -n "automationsCliEnabled\(|macosVmCliEnabled\(|assertAutomationsCliEnabled\(|assertMacosVmCliEnabled\(|AUTOMATIONS_COMING_SOON_MESSAGE|MACOS_VM_COMING_SOON_MESSAGE|linear ingress|macos-vm" "$FILE"

echo "== Snippet around where automationsCliEnabled is called =="
# Since rg might not show context, show nearby lines for any occurrences
for pat in "automationsCliEnabled(" "macosVmCliEnabled(" "assertAutomationsCliEnabled" "assertMacosVmCliEnabled"; do
  echo "--- occurrences for $pat ---"
  rg -n "$pat" "$FILE" | head -n 20
done

echo "== Show a broader window around the first few matches for command gating/help =="
matches=$(rg -n "automationsCliEnabled\(|macosVmCliEnabled\(|assertAutomationsCliEnabled\(|assertMacosVmCliEnabled\(|linear ingress|macos-vm" "$FILE" | cut -d: -f1 | head -n 20)
i=0
for line in $matches; do
  i=$((i+1))
  start=$((line-15)); end=$((line+60))
  echo "=== window around line $line (set $i) ==="
  sed -n "${start},${end}p" "$FILE" | nl -ba -w2 -s' ' | head -n 110
done

Repository: arul28/ADE

Length of output: 5969


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/ade-cli/src/cli.ts"

echo "== automations/macOS-vm gating: windows around lines 480-560 =="
cat -n "$FILE" | sed -n '430,570p'

echo "== help command map entries: windows around lines 1260-1360 =="
cat -n "$FILE" | sed -n '1260,1360p'

echo "== where HELP_BY_COMMAND['macos-vm'] / automations filtering happens (around 7400-7600) =="
cat -n "$FILE" | sed -n '7400,7560p'

echo "== command dispatch area: around 10180-10520 =="
cat -n "$FILE" | sed -n '10080,10560p'

echo "== search: ADE_CLI_ENTRY_PATH usage in cli.ts =="
rg -n "ADE_CLI_ENTRY_PATH" "$FILE" || true

echo "== search: process.argv[1] usage elsewhere in cli.ts for gating expectations =="
rg -n "process\.argv\[1\]" "$FILE" | head -n 50

Repository: arul28/ADE

Length of output: 40334


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Search for ade-cli/bin / executable entrypoints =="
rg -n "ade-cli|dist/cli\.cjs|src/cli\.ts|bin/ade|#!/usr/bin/env node" -S apps/ade-cli . || true

echo "== List package.json entrypoints for ade-cli (bin/module) =="
fd -e package.json apps/ade-cli | xargs -I{} sh -c 'echo "--- {} ---"; cat "{}" | head -n 80' || true

echo "== Search for bin field or cli file references in package.json files =="
rg -n "\"bin\"|\"main\"|\"exports\"" apps/ade-cli -S || true

Repository: arul28/ADE

Length of output: 50739


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/ade-cli/README.md"
if [ ! -f "$FILE" ]; then echo "Missing $FILE"; exit 1; fi
cat -n "$FILE" | sed -n '60,100p'

Repository: arul28/ADE

Length of output: 2049


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n apps/ade-cli/package.json | sed -n '1,60p'

Repository: arul28/ADE

Length of output: 2182


Use a realpath-based check for source-checkout gating (avoid argv[1] path-string matches).

CLI_ENTRY_PATH is derived from process.argv[1] (only path.resolved) and isSourceCheckoutCliEntryPath() gates automations/macos-vm help + command dispatch purely via regexes on that path. When ade is invoked through npm link/global shims (symlinked executable path like .../bin/ade rather than .../apps/ade-cli/dist/cli.cjs), the gate can flip off and hide/block automations and macos-vm even from a source/dev build.

Fix by resolving the executable target path (e.g., realpathSyncSafe(modulePath)) before applying the /src/cli.ts or /apps/ade-cli/dist/cli.cjs checks, or by deriving the check from the resolved package/module root rather than the raw argv[1] string. [Major: apps/ade-cli/src/cli.ts isSourceCheckoutCliEntryPath + automationsCliEnabled/macosVmCliEnabled call sites]

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

In `@apps/ade-cli/src/cli.ts` around lines 248 - 264, The path-based gate is using
the raw CLI_ENTRY_PATH (from process.argv[1]) which can be a symlink and cause
false negatives; update isSourceCheckoutCliEntryPath to resolve the path before
testing (e.g., call a safe realpath resolver like realpathSync in try/catch or a
realpathSyncSafe utility inside isSourceCheckoutCliEntryPath or at the call
sites automationsCliEnabled/macosVmCliEnabled) and then run the existing regex
checks against the resolved target (so the /apps/ade-cli/dist/cli.cjs and source
paths match actual file targets rather than symlinked argv values).

Comment on lines +1897 to +1908
const automationService = args.getAutomationService?.() ?? null;
if (!automationService) return;
try {
for (const dispatched of buildLinearAutomationDispatches(event)) {
await automationService.dispatchIngressTrigger(dispatched);
}
} catch (error) {
args.logger.warn("linear.headless_automation_dispatch_failed", {
issueId,
error: error instanceof Error ? error.message : String(error),
});
}
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 | 🟠 Major | ⚡ Quick win

Don't let one automation dispatch abort the rest.

The try/catch wraps the whole loop, so the first dispatchIngressTrigger rejection stops every later dispatch generated from the same Linear event. One bad rule can suppress unrelated automations here. Catch per dispatch or use Promise.allSettled.

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

In `@apps/ade-cli/src/headlessLinearServices.ts` around lines 1897 - 1908, The
current try/catch around the whole loop allows the first failing dispatch to
abort remaining dispatches; update buildLinearAutomationDispatches handling so
each dispatched item is handled independently: either map the dispatches to
promises and use Promise.allSettled to run them concurrently and log any
failures, or wrap each await
automationService.dispatchIngressTrigger(dispatched) in its own try/catch so a
single failure (inside the loop over buildLinearAutomationDispatches(event))
only logs via args.logger.warn (include issueId and the specific dispatched info
and error) and continues processing the rest; reference
buildLinearAutomationDispatches, automationService.dispatchIngressTrigger, and
args.logger.warn (and issueId) when making the change.

Comment on lines +12 to +34
function withEnv<T>(updates: Record<string, string | undefined>, run: () => T): T {
const previous = new Map<string, string | undefined>();
for (const key of Object.keys(updates)) {
previous.set(key, process.env[key]);
const value = updates[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
try {
return run();
} finally {
for (const [key, value] of previous) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
}
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 | ⚡ Quick win

Make withEnv await async callbacks.

finally runs as soon as run() returns, so await withEnv(..., async () => ...) restores process.env before the async body finishes. The current signature makes that misuse look valid.

Suggested fix
-function withEnv<T>(updates: Record<string, string | undefined>, run: () => T): T {
+async function withEnv<T>(
+  updates: Record<string, string | undefined>,
+  run: () => T | Promise<T>,
+): Promise<T> {
   const previous = new Map<string, string | undefined>();
   for (const key of Object.keys(updates)) {
     previous.set(key, process.env[key]);
     const value = updates[key];
     if (value === undefined) {
@@
   }
   try {
-    return run();
+    return await run();
   } finally {
     for (const [key, value] of previous) {
       if (value === undefined) {
         delete process.env[key];
       } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/adeActions/registry.test.ts` around lines 12 -
34, The helper withEnv currently accepts a synchronous callback and restores
process.env immediately because it doesn't await async callbacks; change withEnv
to be asynchronous by updating its signature to async function
withEnv<T>(updates: Record<string, string | undefined>, run: () => Promise<T> |
T): Promise<T>, call and await the result of run() inside the try block (e.g.,
const result = await run()), then return that result; keep the existing env
save/restore logic in the finally block so environment variables remain set for
the entire duration of the awaited callback — reference the withEnv function to
make these edits.

Comment on lines +164 to +180
if (githubEvent === "issue_comment") {
if (action && action !== "created") return null;
const rawIssue = readNested(payload, "issue");
const comment = readNested(payload, "comment");
const issueIsPr = Boolean(readNested(rawIssue, "pull_request"));
const issue = buildIssueContext(rawIssue, repo);
if (!issue) return null;
return {
triggerType: issueIsPr ? "github.pr_commented" : "github.issue_commented",
summary: `GitHub ${issueIsPr ? "PR" : "issue"} #${issue.number} commented: ${issue.title}`,
author: readUserLogin(comment) ?? readLogin(readNested(payload, "sender")),
labels: issue.labels,
issue: issueIsPr ? null : issue,
pr: issueIsPr
? { ...issue, baseBranch: undefined, headBranch: undefined, draft: false, merged: false }
: 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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

PR comment webhooks lose the branch data needed for matching.

apps/desktop/src/main/services/automations/automationService.ts matches github.pr_commented branch filters against trigger.branch, but this issue_comment path fabricates a PR context from the issue payload without headBranch/baseBranch and never sets branch/targetBranch. Branch-filtered PR comment automations that work via apps/desktop/src/main/services/automations/githubPollingService.ts:240-310 will be silently missed here. Either hydrate the PR details before dispatch or treat PR comments as unsupported on the direct webhook path.

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

In `@apps/desktop/src/main/services/automations/automationIngressService.ts`
around lines 164 - 180, The PR comment webhook path in
automationIngressService.ts fabricates a PR context without
headBranch/baseBranch or branch/targetBranch, so branch-filtered automations in
automationService.ts won't match; update the issue_comment handling (the block
that builds issue, computes issueIsPr and returns triggerType/pr) to either (A)
hydrate full PR details when issueIsPr is true by fetching the PR via the GitHub
API and populating headBranch/baseBranch and branch/targetBranch on the returned
object before returning, or (B) explicitly mark PR comments as unsupported by
returning null for PR triggers and/or returning a distinct unsupported flag so
callers skip branch-matching; target the code paths using readNested,
buildIssueContext, the issueIsPr boolean, and the returned pr object so the
dispatched trigger includes the branch information automationService.ts expects.

Comment on lines +65 to +75
function normalizePublicWebhookUrl(value: string | null | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed) return null;
try {
const url = new URL(trimmed);
if (url.protocol !== "https:") return null;
return url.toString().replace(/\/$/, "");
} catch {
return 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-public HTTPS hosts here.

normalizePublicWebhookUrl() currently accepts any https: URL, so values like https://127.0.0.1/..., https://localhost/..., or RFC1918 hosts will flow through as “ready” via defaultWebhookGatewayStatus()/getIngressStatus() and let external-trigger automations be enabled even though GitHub/Linear can never reach them. This needs host-level validation, not just a scheme check.

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

In `@apps/desktop/src/main/services/automations/automationService.ts` around lines
65 - 75, normalizePublicWebhookUrl currently only checks the scheme; update it
to reject non-public hosts so internal addresses (localhost, 127.0.0.0/8,
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, IPv6 ::1, fc00::/7, link-local
fe80::/10, and literal "localhost" or ".local" names) return null. In
normalizePublicWebhookUrl(), after creating the URL and verifying protocol ===
"https:", extract url.hostname and if it's an IP use a standard IP check
(net.isIP or equivalent) to detect private/loopback ranges and reject them, or
if it's a hostname reject explicit "localhost" and local mDNS patterns (e.g.
ending with ".local"); only allow otherwise and then trim trailing slash as
before so defaultWebhookGatewayStatus()/getIngressStatus() won't treat
non-public endpoints as ready. Ensure error handling still returns null on parse
failures.

Comment on lines +1919 to +1923
const webhookGatewayPublicUrl = asString(value.webhookGatewayPublicUrl)?.trim();
const out: NonNullable<ProjectConfigFile["ui"]> = {};
if (linearBatchLaunchDefaultPrompt) out.linearBatchLaunchDefaultPrompt = linearBatchLaunchDefaultPrompt;
if (webhookGatewayPublicUrl) out.webhookGatewayPublicUrl = webhookGatewayPublicUrl;
return Object.keys(out).length ? out : undefined;
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 | 🟠 Major | ⚡ Quick win

Enforce HTTPS URL validation for webhookGatewayPublicUrl before persisting config.

Line 1919 currently accepts any trimmed string, which conflicts with the HTTPS base-URL contract and allows invalid/insecure values into effective config.

Suggested fix
 function coerceProjectUiConfig(value: unknown): ProjectConfigFile["ui"] {
   if (!isRecord(value)) return undefined;
   const linearBatchLaunchDefaultPrompt = asString(value.linearBatchLaunchDefaultPrompt)?.trim();
-  const webhookGatewayPublicUrl = asString(value.webhookGatewayPublicUrl)?.trim();
+  const webhookGatewayPublicUrlRaw = asString(value.webhookGatewayPublicUrl)?.trim();
+  let webhookGatewayPublicUrl: string | undefined;
+  if (webhookGatewayPublicUrlRaw) {
+    try {
+      const parsed = new URL(webhookGatewayPublicUrlRaw);
+      if (parsed.protocol === "https:") {
+        webhookGatewayPublicUrl = parsed.toString().replace(/\/$/, "");
+      }
+    } catch {
+      // ignore invalid URL
+    }
+  }
   const out: NonNullable<ProjectConfigFile["ui"]> = {};
   if (linearBatchLaunchDefaultPrompt) out.linearBatchLaunchDefaultPrompt = linearBatchLaunchDefaultPrompt;
   if (webhookGatewayPublicUrl) out.webhookGatewayPublicUrl = webhookGatewayPublicUrl;
   return Object.keys(out).length ? out : undefined;
 }
📝 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
const webhookGatewayPublicUrl = asString(value.webhookGatewayPublicUrl)?.trim();
const out: NonNullable<ProjectConfigFile["ui"]> = {};
if (linearBatchLaunchDefaultPrompt) out.linearBatchLaunchDefaultPrompt = linearBatchLaunchDefaultPrompt;
if (webhookGatewayPublicUrl) out.webhookGatewayPublicUrl = webhookGatewayPublicUrl;
return Object.keys(out).length ? out : undefined;
function coerceProjectUiConfig(value: unknown): ProjectConfigFile["ui"] {
if (!isRecord(value)) return undefined;
const linearBatchLaunchDefaultPrompt = asString(value.linearBatchLaunchDefaultPrompt)?.trim();
const webhookGatewayPublicUrlRaw = asString(value.webhookGatewayPublicUrl)?.trim();
let webhookGatewayPublicUrl: string | undefined;
if (webhookGatewayPublicUrlRaw) {
try {
const parsed = new URL(webhookGatewayPublicUrlRaw);
if (parsed.protocol === "https:") {
webhookGatewayPublicUrl = parsed.toString().replace(/\/$/, "");
}
} catch {
// ignore invalid URL
}
}
const out: NonNullable<ProjectConfigFile["ui"]> = {};
if (linearBatchLaunchDefaultPrompt) out.linearBatchLaunchDefaultPrompt = linearBatchLaunchDefaultPrompt;
if (webhookGatewayPublicUrl) out.webhookGatewayPublicUrl = webhookGatewayPublicUrl;
return Object.keys(out).length ? out : undefined;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/config/projectConfigService.ts` around lines
1919 - 1923, Validate webhookGatewayPublicUrl as a proper HTTPS origin before
assigning it to out.webhookGatewayPublicUrl: after computing
webhookGatewayPublicUrl = asString(... )?.trim(), attempt to parse it with the
URL constructor and ensure url.protocol === 'https:' and that the URL represents
a base/origin (e.g. url.pathname === '/' or url.pathname === ''), and only then
set out.webhookGatewayPublicUrl = webhookGatewayPublicUrl; if the value fails
validation, skip assigning it (and optionally surface/record a validation error
elsewhere). This change targets the webhookGatewayPublicUrl handling in
projectConfigService (the webhookGatewayPublicUrl local variable and the out.ui
assignment).

Comment on lines +369 to +399
try {
localSigningSecret = direct.signingSecret!;
const existing = await args.linearClient.listWebhooks();
if (!existing.some((entry) => entry.url === direct.webhookUrl)) {
await args.linearClient.createWebhook({
url: direct.webhookUrl!,
secret: direct.signingSecret!,
label: `ADE ${args.projectId} workflow ingress`,
resourceTypes: ["Issue", "IssueLabel"],
allPublicTeams: true,
});
}
const payload: RelayEnsureWebhookResponse = {
endpointId: "direct",
webhookUrl: direct.webhookUrl!,
signingSecret: direct.signingSecret!,
lastDeliveredAt: null,
};
relayWebhook = payload;
updateStatus({
relay: {
configured: true,
healthy: true,
status: "ready",
webhookUrl: payload.webhookUrl,
endpointId: payload.endpointId,
lastDeliveryAt: null,
lastError: null,
},
});
return payload;
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 | 🟠 Major | 🏗️ Heavy lift

Recreate or update the Linear webhook when the signing secret changes.

Both branches treat url as the only identity check. If the public URL stays stable but the secret rotates, ADE updates localSigningSecret locally and skips createWebhook, so the existing Linear webhook keeps signing with the old secret and every delivery starts failing HMAC validation until someone deletes the webhook manually.

Also applies to: 436-446

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

In `@apps/desktop/src/main/services/cto/linearIngressService.ts` around lines 369
- 399, The code only checks webhook.url when ensuring a webhook, so if
direct.signingSecret rotates the existing Linear webhook remains with the old
secret; modify the ensure logic around localSigningSecret /
args.linearClient.listWebhooks() to also compare the stored signing secret
(direct.signingSecret) against the existing webhook.secret and, if different,
call the appropriate update path (use args.linearClient.updateWebhook(...) if
available or delete + args.linearClient.createWebhook(...)) to replace the
secret for the matching direct.webhookUrl; update the
RelayEnsureWebhookResponse/relayWebhook and call updateStatus(...) after the
update so state reflects the new signing secret and health.

Comment on lines +785 to +815
<Button
size="sm"
variant="primary"
onClick={() => {
const nextDraft = draftWithCreateLaneMode(toDraftFromRule(manualRunRule));
setSelectedRuleId(manualRunRule.id);
setDraft(nextDraft);
savedSnapshotRef.current = JSON.stringify(toDraftFromRule(manualRunRule));
setIssues([]);
setSimulationNotes([]);
setRequiredConfirmations([]);
setAcceptedConfirmations(new Set());
setDetailView("editor");
setManualRunRule(null);
setManualRunLaneId("");
}}
>
Create lanes for runs
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedRuleId(manualRunRule.id);
setDetailView("editor");
setManualRunRule(null);
setManualRunLaneId("");
}}
>
Edit lane mode
</Button>
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 | 🟠 Major | ⚡ Quick win

Preserve the intended draft before switching rules.

If this modal is opened from a different rule than the one currently selected, both actions call setSelectedRuleId(manualRunRule.id), which triggers the effect on Lines 444-455 and reloads toDraftFromRule(selected). That wipes the draftWithCreateLaneMode(...) changes in the “Create lanes for runs” path, and it can also silently discard unsaved edits in the currently open draft.

Guard the rule switch with confirmDiscardIfDirty() and carry the intended draft through a one-shot override/ref so the selection effect does not immediately replace it.

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

In `@apps/desktop/src/renderer/components/automations/RulesTab.tsx` around lines
785 - 815, When switching selection to manualRunRule you must guard against
accidentally overwriting an in-memory draft and preserve the created-lane draft;
modify the onClick handlers that call setSelectedRuleId(manualRunRule.id) (the
"Create lanes for runs" and "Edit lane mode" handlers) to first call
confirmDiscardIfDirty() and, if it returns/permits, set a one-shot override ref
(e.g., draftOverrideRef) containing the precomputed
draftWithCreateLaneMode(toDraftFromRule(manualRunRule)) (for the Create path) or
the current draft (for Edit), then call setSelectedRuleId(manualRunRule.id) and
setDraft from that overrideRef instead of relying on the selection effect to
re-run toDraftFromRule(selected); also clear the override ref after applying and
keep savedSnapshotRef/current issue state updates as before.

Comment on lines +17 to +20
export function readAutomationsEnvOverride(env: EnvLike = process.env): boolean | null {
if (envFlagEnabled(env[AUTOMATIONS_ENABLE_ENV])) return true;
if (envFlagEnabled(env[AUTOMATIONS_DISABLE_ENV])) return false;
return 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the disable override win on conflicting env flags.

Both readers check ADE_ENABLE_* before ADE_DISABLE_*, so setting both truthy still enables the feature. That weakens the production kill switch: an operator cannot reliably shut the feature off if a stale enable var is still present. Fail closed here by giving *_DISABLE_* precedence, or by throwing on conflicts.

Suggested fix
 export function readAutomationsEnvOverride(env: EnvLike = process.env): boolean | null {
-  if (envFlagEnabled(env[AUTOMATIONS_ENABLE_ENV])) return true;
   if (envFlagEnabled(env[AUTOMATIONS_DISABLE_ENV])) return false;
+  if (envFlagEnabled(env[AUTOMATIONS_ENABLE_ENV])) return true;
   return null;
 }
@@
 export function readMacosVmEnvOverride(env: EnvLike = process.env): boolean | null {
-  if (envFlagEnabled(env[MACOS_VM_ENABLE_ENV])) return true;
   if (envFlagEnabled(env[MACOS_VM_DISABLE_ENV])) return false;
+  if (envFlagEnabled(env[MACOS_VM_ENABLE_ENV])) return true;
   return null;
 }

Also applies to: 32-35

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

In `@apps/desktop/src/shared/automationAvailability.ts` around lines 17 - 20, In
readAutomationsEnvOverride, currently envFlagEnabled(AUTOMATIONS_ENABLE_ENV) is
checked before the disable flag so a set disable flag can be overridden; change
the logic so the disable flag takes precedence (check
envFlagEnabled(env[AUTOMATIONS_DISABLE_ENV]) first and return false if set) or
explicitly detect both flags set and return false (or throw if you prefer strict
behavior); apply the same change to the analogous function around lines 32-35
(the other env-reading function) — reference symbols:
readAutomationsEnvOverride, AUTOMATIONS_ENABLE_ENV, AUTOMATIONS_DISABLE_ENV.

Comment on lines +446 to +447
automationsRefreshWebhookGatewayStatus: "ade.automations.refreshWebhookGatewayStatus",
automationsSetWebhookGatewayPublicUrl: "ade.automations.setWebhookGatewayPublicUrl",
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify both new channels are registered (main), bridged + typed (preload), and consumed (renderer).
for ch in automationsRefreshWebhookGatewayStatus automationsSetWebhookGatewayPublicUrl; do
  echo "===== $ch ====="
  echo "--- main IPC registration ---"
  rg -nP -C2 "\b$ch\b" -g 'apps/desktop/src/main/**'
  echo "--- preload bridge + types ---"
  rg -nP -C2 "\b$ch\b" -g 'apps/desktop/src/preload/**'
  echo "--- renderer usage ---"
  rg -nP -C2 "\b$ch\b" -g 'apps/desktop/src/renderer/**'
done

Repository: arul28/ADE

Length of output: 2328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search renderer for both IPC channel references and the preload-exposed method names.
rg -n --hidden --no-ignore \
  -S "IPC\.automationsRefreshWebhookGatewayStatus|IPC\.automationsSetWebhookGatewayPublicUrl" \
  apps/desktop/src/renderer || true

rg -n --hidden --no-ignore \
  -S "refreshWebhookGatewayStatus|setWebhookGatewayPublicUrl" \
  apps/desktop/src/renderer || true

# Broaden slightly in case renderer calls are elsewhere (still within apps/desktop/src).
rg -n --hidden --no-ignore \
  -S "refreshWebhookGatewayStatus|setWebhookGatewayPublicUrl" \
  apps/desktop/src | head -n 50 || true

Repository: arul28/ADE

Length of output: 4333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Renderer call sites (invocations) for preload-exposed methods
rg -n --hidden --no-ignore \
  "(^|[^A-Za-z0-9_])(refreshWebhookGatewayStatus|setWebhookGatewayPublicUrl)\s*\(" \
  apps/desktop/src/renderer || true

# Also search for dotted access patterns commonly used off preload/global objects
rg -n --hidden --no-ignore \
  "automations\.(refreshWebhookGatewayStatus|setWebhookGatewayPublicUrl)" \
  apps/desktop/src/renderer || true

# Search for usage via global/runtime namespaces (best-effort)
rg -n --hidden --no-ignore \
  "(refreshWebhookGatewayStatus|setWebhookGatewayPublicUrl)" \
  apps/desktop/src/renderer/browserMock.ts || true

Repository: arul28/ADE

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for raw string action names in real renderer code
rg -n --hidden --no-ignore \
  '"refreshWebhookGatewayStatus"|"setWebhookGatewayPublicUrl"' \
  apps/desktop/src/renderer || true

# Search for likely action-layer usage in renderer
rg -n --hidden --no-ignore \
  "adeActions|runtimeAction|callProjectRuntimeActionOr|projectRuntimeAction|automations\.|automations'" \
  apps/desktop/src/renderer || true

# As a last broaden: search for the module prefix used in preload/global for actions
rg -n --hidden --no-ignore \
  "automations|refreshWebhookGatewayStatus|setWebhookGatewayPublicUrl" \
  apps/desktop/src/renderer --glob '!**/browserMock.ts' || true

Repository: arul28/ADE

Length of output: 15540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- preload.ts context (around refresh/set) ----"
sed -n '3900,3990p' apps/desktop/src/preload/preload.ts

echo "---- global.d.ts context (around automations typing) ----"
sed -n '960,1035p' apps/desktop/src/preload/global.d.ts

Repository: arul28/ADE

Length of output: 7085


IPC channels are wired through main + preload; only renderer mock usage found.

  • apps/desktop/src/shared/ipc.ts: constants added for ade.automations.refreshWebhookGatewayStatus and ade.automations.setWebhookGatewayPublicUrl
  • apps/desktop/src/main/services/ipc/registerIpc.ts: both ipcMain.handle(...) handlers are registered
  • apps/desktop/src/preload/preload.ts: window.ade.automations.refreshWebhookGatewayStatus() / setWebhookGatewayPublicUrl() invoke the matching IPC.* channels
  • apps/desktop/src/preload/global.d.ts: corresponding window.ade.automations typings exist
  • Missing concern: no production renderer call sites found for window.ade.automations.refreshWebhookGatewayStatus / setWebhookGatewayPublicUrl—only apps/desktop/src/renderer/browserMock.ts references

If these are intended to be used by the UI, wire them into the relevant renderer component(s); otherwise remove/avoid keeping unused IPC channels.

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

In `@apps/desktop/src/shared/ipc.ts` around lines 446 - 447, The new IPC
channels/APIs (constants automationsRefreshWebhookGatewayStatus and
automationsSetWebhookGatewayPublicUrl in ipc.ts, the ipcMain.handle
registrations in registerIpc.ts, and the preload methods
window.ade.automations.refreshWebhookGatewayStatus / setWebhookGatewayPublicUrl
plus typings) are not used anywhere in production renderer code—only referenced
in browserMock.ts; either wire them into the real UI or remove them. To fix: if
the UI needs them, add calls from the appropriate renderer component(s) (search
for automation UI pages/components) to invoke
window.ade.automations.refreshWebhookGatewayStatus() and
setWebhookGatewayPublicUrl() and update any component props/state to surface
results; otherwise remove the unused constants in ipc.ts, remove the
ipcMain.handle registrations in registerIpc.ts, strip the preload methods and
global.d.ts typings, and delete the mock usage in browserMock.ts so there are no
unused IPC entry points left.

@arul28 arul28 merged commit 687a002 into main Jun 3, 2026
29 checks passed
@arul28 arul28 deleted the ade-75-automations-graph-ux-polish-false-warning-simulate-dead-end-lane-required-run-now-reparent-undo-double-click-rollback-order branch June 3, 2026 16:13
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