Fix Linear review bypass and headless usage tracking start#374
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Guard resolveRunAction approve/reject so they only succeed when the run is actively awaiting_human_review on a request_human_review step. Previously an agent or CLI call could pre-set reviewState=approved while the run was still in_progress, causing advanceRun to auto-skip the gate. Add regression test for early approve rejection. Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
createAdeRuntime constructed usageTrackingService but never called start(), so headless/daemon runs had empty usage snapshots and percent-based budget caps never blocked dispatch. Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
3b30915 to
a2787a6
Compare
|
@copilot review but do not make fixes |
Reviewed the latest commits in this PR and I don’t see any actionable correctness or security issues to fix. Per your request, I did not make code changes. |
There was a problem hiding this comment.
PR Review
Scope: 6 file(s), +90 / −18
Verdict: Looks good
This PR closes a Linear workflow correctness gap by requiring awaiting_human_review and an active request_human_review step before approve/reject mutate state, starts usageTrackingService in headless createAdeRuntime() after the ADE action registry is bound, and adds regression tests plus doc updates. The prior approve path could still call updateRun with reviewState: "approved" when the run was not on the review gate; that bypass is removed.
Notes
- Linear gate fix:
approvepreviously guarded onlyupdateStepwhile always runningupdateRun/appendEvent/ workpad sync — early API or automation calls could skip supervisor review. Reject had no equivalent guards. New throws align with UI gating inOperationsSidebar.tsx(approve/reject only whenreviewContextandawaiting_human_review). - Headless usage:
usageTrackingService.start()is idempotent (if (pollTimer) return), so the new bootstrap call is safe; placement afterbindAdeActionRegistrymatches the remote-runtime doc note about usage/budget actions. - Test-only RPC change:
adeRpcServer.test.tsnow expects read-onlylist_lanesnot to record operation metadata; no server behavior change in this diff. - Error surfacing: Thrown messages from
resolveRunActionpropagate to chat tools (ctoOperatorToolscatch) and the CTO UI (LinearSyncPanelactOnRuncatch); no new unhandled IPC gap spotted for invalid approve/reject timing.
Sent by Cursor Automation: BUGBOT in Versic
| const awaitingReview = await dispatcher.advanceRun(run.id, policy); | ||
| expect(awaitingReview?.status).toBe("awaiting_human_review"); | ||
| db.close(); |
There was a problem hiding this comment.
Second
advanceRun may not reach awaiting_human_review without a prior state change. After the blocked action, the run is still in whatever state the first advanceRun left it (e.g., awaiting_delegation or in_progress). If advanceRun is not idempotent for that intermediate state — or if additional external signals are required — the second call could return the same blocked status rather than awaiting_human_review, making the final assertion flaky. The mock returns listRuns: [{ status: "completed" }] which should drive progression, but explicitly asserting the intermediate state before the second advanceRun call would make the progression assumptions visible.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/cto/linearSync.test.ts
Line: 2321-2323
Comment:
**Second `advanceRun` may not reach `awaiting_human_review` without a prior state change.** After the blocked action, the run is still in whatever state the first `advanceRun` left it (e.g., `awaiting_delegation` or `in_progress`). If `advanceRun` is not idempotent for that intermediate state — or if additional external signals are required — the second call could return the same blocked status rather than `awaiting_human_review`, making the final assertion flaky. The mock returns `listRuns: [{ status: "completed" }]` which should drive progression, but explicitly asserting the intermediate state before the second `advanceRun` call would make the progression assumptions visible.
How can I resolve this? If you propose a fix, please make it concise.

Bug and impact
1. Linear supervisor review bypass (critical)
Impact: Human review gates could be silently skipped. An agent, CLI, or automation could call
resolveLinearRunAction({ action: "approve" })while a workflow run was stillin_progresson an earlier step. That pre-setreviewState: "approved". WhenadvanceRunlater reached therequest_human_reviewstep, it saw the pre-approved state and auto-completed the gate without any supervisor action.Concrete trigger: Supervised worker workflow with a
request_human_reviewstep → agent callsresolveLinearRunActionwithapproveduring worker delegation → worker finishes → review step is auto-approved.2. Headless usage tracking never started (high)
Impact:
ade serve/ daemon bootstrap createdusageTrackingServicebut never calledstart(), so usage snapshots stayed empty. Percent-based budget caps (weekly-percent,five-hour-percent) never blocked dispatch in headless mode becausebudgetCapService.checkBudget()returns{ exceeded: false }when no usage data is available.Root cause
resolveRunActionallowedapprove/rejectwithout checkingrun.status === "awaiting_human_review"or that the current step isrequest_human_review. The UI gated the Approve button correctly; service/tool/CLI paths did not.usageTrackingService.start()during project bind, butcreateAdeRuntimeinapps/ade-cli/src/bootstrap.tsomitted the same call.Fix
approve/rejectinlinearDispatcherService.resolveRunActionto require active supervisor review state.usageTrackingService.start()in ade-cli bootstrap after service construction.Validation
npx vitest run src/main/services/cto/linearSync.test.ts -t "supervisor approval|early approve"(2 tests pass)npm --prefix apps/ade-cli run typecheckDoes not overlap open draft PRs #363–#372 (sync/CRR/orchestration/PTY fixes).
Greptile Summary
This PR fixes two correctness bugs: a supervisor review bypass in the Linear dispatcher where
approve/rejectactions lacked run-status guards, and missingusageTrackingService.start()in the headless CLI bootstrap that caused percent-based budget caps to never trigger.resolveRunActionnow throws early ifrun.status !== "awaiting_human_review"or the current step is notrequest_human_reviewbefore mutating any state. A regression test covering bothapproveandrejectwas added.usageTrackingService.start()is called afterautomationService.bindAdeActionRegistry()and beforeruntimeCreated = true, fitting cleanly into the existing try/finally cleanup path.adeRpcServertest was updated to reflect that read-only action calls no longer trigger operation audit records.Confidence Score: 4/5
Both fixes are logically sound and well-scoped; the PR is safe to merge.
The dispatcher guard correctly prevents premature review state mutation, and the bootstrap change fits the existing try/finally cleanup lifecycle. The new regression test covers the core scenario, though its final assertion relies on implicit mock-driven state progression that could silently break if mocks change.
apps/desktop/src/main/services/cto/linearSync.test.ts — verify the second advanceRun reliably reaches awaiting_human_review under the mocked worker state
Important Files Changed
Sequence Diagram
sequenceDiagram participant Caller as Agent / CLI / UI participant Dispatcher as linearDispatcherService participant DB as KV Store Note over Caller,DB: Happy path Caller->>Dispatcher: advanceRun(runId) Dispatcher->>DB: "updateRun status=awaiting_human_review" Dispatcher-->>Caller: run awaiting_human_review Caller->>Dispatcher: resolveRunAction approve Dispatcher->>DB: check run.status OK Dispatcher->>DB: updateStep + updateRun approved Dispatcher-->>Caller: updated run Note over Caller,DB: Early approve fixed Caller->>Dispatcher: resolveRunAction approve [in_progress] Dispatcher-->>Caller: throws not awaiting supervisor review Note over DB: No mutationComments Outside Diff (1)
apps/desktop/src/main/services/cto/linearDispatcherService.ts, line 2442-2452 (link)!reviewContextthrows, the subsequent uses ofreviewContext?.rejectActionwith optional chaining are redundant —reviewContextis guaranteed non-null at this point. Using direct property access makes the intent clearer and avoids confusion about whethernullis still possible here.Prompt To Fix With AI
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!
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "Fix ADE RPC action audit test" | Re-trigger Greptile