feat: Linear status sync during loop execution#274
Conversation
Syncs loop execution status to a Linear issue during the run: - Loop start → moves issue to "In Progress" - Loop success → moves to "Done" + adds summary comment - Loop failure → moves to "In Review" + adds error comment Non-blocking: gracefully skips if no API key or issue not found. Exposed as SDK export (createLinearSync) for programmatic use. Closes ENG-1472 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Issue Linking ReminderThis PR doesn't appear to have a linked issue. Consider linking to:
Using If this PR doesn't need an issue, you can ignore this message. |
✔️ Bundle Size Analysis
Bundle breakdown |
| } else { | ||
| await linearSyncHandler({ | ||
| type: 'failed', | ||
| error: exitReason || 'unknown', |
Check warning
Code scanning / CodeQL
Useless conditional Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 21 days ago
General fix: remove the redundant boolean‑OR with a constant default value when the left operand is proven always truthy. That avoids misleading “fallback” code that never actually runs and silences the static analysis warning.
Concrete change here: on line 1704, replace error: exitReason || 'unknown', with error: exitReason,. This keeps the same effective behavior on all paths where CodeQL has determined exitReason is always truthy, and removes the useless conditional. No imports, helper methods, or other structural changes are needed. All modifications are within src/loop/executor.ts in the shown snippet.
| @@ -1701,7 +1701,7 @@ | ||
| } else { | ||
| await linearSyncHandler({ | ||
| type: 'failed', | ||
| error: exitReason || 'unknown', | ||
| error: exitReason, | ||
| iterations: finalIteration, | ||
| }); | ||
| } |
Greptile SummaryThis PR adds real-time Linear issue status synchronization to the Key points:
Confidence Score: 3/5
Sequence DiagramsequenceDiagram
participant CLI as ralph run CLI
participant Executor as executor.ts (runLoop)
participant Sync as createLinearSync
participant Linear as LinearIntegration
participant API as Linear GraphQL API
CLI->>Executor: runLoop({ linearSync: "ENG-42" })
Executor->>Sync: createLinearSync({ issueId: "ENG-42" })
Sync->>Linear: updateTask("ENG-42", { status: "In Progress" })
Linear->>API: resolveIssueId + resolveStateId + issueUpdate
API-->>Linear: success
Linear-->>Sync: TaskReference
Sync-->>Executor: handler fn (or null on auth failure)
alt Loop completes successfully
Executor->>Sync: handler({ type: "complete", ... })
Sync->>Linear: updateTask("ENG-42", { status: "Done" })
Linear->>API: resolveIssueId + resolveStateId + issueUpdate
Sync->>Linear: addComment("ENG-42", summary)
Linear->>API: resolveIssueId + commentCreate
API-->>Linear: success
else Loop blocked / failed
Executor->>Sync: handler({ type: "failed", error, iterations })
Sync->>Linear: updateTask("ENG-42", { status: "In Review" })
Linear->>API: resolveIssueId + resolveStateId + issueUpdate
Sync->>Linear: addComment("ENG-42", error details)
Linear->>API: resolveIssueId + commentCreate
API-->>Linear: success
end
Last reviewed commit: 92ed33a |
| export async function createLinearSync( | ||
| config: LinearSyncConfig | ||
| ): Promise<((event: LinearSyncEvent) => Promise<void>) | null> { | ||
| const linear = new LinearIntegration(); |
There was a problem hiding this comment.
LinearIntegration is instantiated directly inside createLinearSync rather than being passed as a dependency. Per the codebase's DI pattern, adapters should be pre-initialized at application startup and injected, not created inline. This also makes the function harder to test without vi.mock (which this test file already requires).
LinearSyncConfig should accept an optional pre-initialized adapter:
| const linear = new LinearIntegration(); | |
| export type LinearSyncConfig = { | |
| issueId: string; | |
| headless?: boolean; | |
| linearIntegration?: LinearIntegration; // injected from app startup | |
| }; | |
| // In createLinearSync: | |
| const linear = config.linearIntegration ?? new LinearIntegration(); |
And the callsite in executor.ts should pass a shared instance initialized once at startup.
Rule Used: Use dependency injection pattern for adapters - pa... (source)
Learnt From
cytonic-network/mcp-server#10
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/loop/linear-sync.ts
Line: 32
Comment:
`LinearIntegration` is instantiated directly inside `createLinearSync` rather than being passed as a dependency. Per the codebase's DI pattern, adapters should be pre-initialized at application startup and injected, not created inline. This also makes the function harder to test without `vi.mock` (which this test file already requires).
`LinearSyncConfig` should accept an optional pre-initialized adapter:
```suggestion
export type LinearSyncConfig = {
issueId: string;
headless?: boolean;
linearIntegration?: LinearIntegration; // injected from app startup
};
// In createLinearSync:
const linear = config.linearIntegration ?? new LinearIntegration();
```
And the callsite in `executor.ts` should pass a shared instance initialized once at startup.
**Rule Used:** Use dependency injection pattern for adapters - pa... ([source](https://app.greptile.com/review/custom-context?memory=6670b6eb-8407-440b-8c79-4fb7a0b326ea))
**Learnt From**
[cytonic-network/mcp-server#10](https://github.com/cytonic-network/mcp-server/pull/10)
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!
| export type LinearSyncEvent = | ||
| | { type: 'start' } | ||
| | { type: 'iteration'; iteration: number; totalIterations: number; success: boolean } | ||
| | { type: 'complete'; summary: string; commits: number; iterations: number; cost?: string } | ||
| | { type: 'failed'; error: string; iterations: number }; |
There was a problem hiding this comment.
The start and iteration variants of LinearSyncEvent are exported as public SDK API but are never emitted anywhere in executor.ts, and their handlers in linear-sync.ts are explicit no-ops (with comments "Already moved to In Progress during init" and "No status change per iteration"). Publishing them as part of the SDK surface will mislead consumers who expect these events to actually trigger behaviour.
Either remove them from the exported type if they are intentionally unsupported, or document clearly that emitting them is a no-op:
| export type LinearSyncEvent = | |
| | { type: 'start' } | |
| | { type: 'iteration'; iteration: number; totalIterations: number; success: boolean } | |
| | { type: 'complete'; summary: string; commits: number; iterations: number; cost?: string } | |
| | { type: 'failed'; error: string; iterations: number }; | |
| export type LinearSyncEvent = | |
| | { type: 'complete'; summary: string; commits: number; iterations: number; cost?: string } | |
| | { type: 'failed'; error: string; iterations: number }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/loop/linear-sync.ts
Line: 18-22
Comment:
The `start` and `iteration` variants of `LinearSyncEvent` are exported as public SDK API but are never emitted anywhere in `executor.ts`, and their handlers in `linear-sync.ts` are explicit no-ops (with comments "Already moved to In Progress during init" and "No status change per iteration"). Publishing them as part of the SDK surface will mislead consumers who expect these events to actually trigger behaviour.
Either remove them from the exported type if they are intentionally unsupported, or document clearly that emitting them is a no-op:
```suggestion
export type LinearSyncEvent =
| { type: 'complete'; summary: string; commits: number; iterations: number; cost?: string }
| { type: 'failed'; error: string; iterations: number };
```
How can I resolve this? If you propose a fix, please make it concise.| await linear.updateTask(config.issueId, { status: 'Done' }); | ||
| await linear.addComment(config.issueId, lines.join('\n')); |
There was a problem hiding this comment.
Both updateTask (line 66) and addComment (line 67) independently call resolveIssueId inside LinearIntegration, resulting in two extra issue(id: $id) { id } GraphQL round-trips per event. updateTask already returns a TaskReference with the resolved UUID (ref.id), which could be passed directly to addComment to skip the second resolution. The same applies to the failed block (lines 79–80).
| await linear.updateTask(config.issueId, { status: 'Done' }); | |
| await linear.addComment(config.issueId, lines.join('\n')); | |
| const ref = await linear.updateTask(config.issueId, { status: 'Done' }); | |
| await linear.addComment(ref.id, lines.join('\n')); |
Apply the same pattern for the failed case below.
Rule Used: Avoid unnecessary preliminary database calls when ... (source)
Learnt From
cytonic-network/ai-backend#90
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/loop/linear-sync.ts
Line: 66-67
Comment:
Both `updateTask` (line 66) and `addComment` (line 67) independently call `resolveIssueId` inside `LinearIntegration`, resulting in two extra `issue(id: $id) { id }` GraphQL round-trips per event. `updateTask` already returns a `TaskReference` with the resolved UUID (`ref.id`), which could be passed directly to `addComment` to skip the second resolution. The same applies to the `failed` block (lines 79–80).
```suggestion
const ref = await linear.updateTask(config.issueId, { status: 'Done' });
await linear.addComment(ref.id, lines.join('\n'));
```
Apply the same pattern for the `failed` case below.
**Rule Used:** Avoid unnecessary preliminary database calls when ... ([source](https://app.greptile.com/review/custom-context?memory=4d69b9a9-01a9-46fd-8812-52a8f33dd131))
**Learnt From**
[cytonic-network/ai-backend#90](https://github.com/cytonic-network/ai-backend/pull/90)
How can I resolve this? If you propose a fix, please make it concise.
Summary
--linear-sync <issue-id>flag toralph runthat syncs loop status to a Linear issue in real-timecreateLinearSync) for programmatic useUsage
ralph run "Fix auth bug" --from linear --linear-sync ENG-42Files Changed
src/loop/linear-sync.ts— Linear sync handler with event-driven architecturesrc/loop/__tests__/linear-sync.test.ts— 5 tests covering all transitionssrc/loop/executor.ts— Wire linearSync into loop lifecyclesrc/commands/run.ts— Add linearSync option passthroughsrc/cli.ts— Add--linear-syncCLI flagsrc/index.ts— Export types and factory for SDK usersTest plan
ralph run "task" --linear-sync ENG-XXXmoves ticket through statesCloses ENG-1472
🤖 Generated with Claude Code