Skip to content

feat: Linear status sync during loop execution#274

Open
rubenmarcus wants to merge 1 commit intomainfrom
feat/linear-status-sync
Open

feat: Linear status sync during loop execution#274
rubenmarcus wants to merge 1 commit intomainfrom
feat/linear-status-sync

Conversation

@rubenmarcus
Copy link
Copy Markdown
Member

Summary

  • Adds --linear-sync <issue-id> flag to ralph run that syncs loop status to a Linear issue in real-time
  • Loop start → "In Progress", success → "Done" + summary comment, failure → "In Review" + error comment
  • Non-blocking: gracefully skips if no LINEAR_API_KEY or issue not found
  • Exported as SDK API (createLinearSync) for programmatic use

Usage

ralph run "Fix auth bug" --from linear --linear-sync ENG-42

Files Changed

  • NEW src/loop/linear-sync.ts — Linear sync handler with event-driven architecture
  • NEW src/loop/__tests__/linear-sync.test.ts — 5 tests covering all transitions
  • src/loop/executor.ts — Wire linearSync into loop lifecycle
  • src/commands/run.ts — Add linearSync option passthrough
  • src/cli.ts — Add --linear-sync CLI flag
  • src/index.ts — Export types and factory for SDK users

Test plan

  • All 218 tests pass (5 new + 213 existing)
  • Build passes clean
  • Manual: ralph run "task" --linear-sync ENG-XXX moves ticket through states

Closes ENG-1472

🤖 Generated with Claude Code

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>
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 8, 2026

Issue Linking Reminder

This PR doesn't appear to have a linked issue. Consider linking to:

  • This repo: Closes #123
  • ralph-ideas: Closes multivmlabs/ralph-ideas#123

Using Closes, Fixes, or Resolves will auto-close the issue when this PR is merged.


If this PR doesn't need an issue, you can ignore this message.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 8, 2026

✔️ Bundle Size Analysis

Metric Value
Base 2442.17 KB
PR 2459.37 KB
Diff 17.20 KB (0%)
Bundle breakdown
156K	dist/auth
32K	dist/automation
4.0K	dist/cli.d.ts
4.0K	dist/cli.d.ts.map
20K	dist/cli.js
12K	dist/cli.js.map
568K	dist/commands
28K	dist/config
4.0K	dist/index.d.ts
4.0K	dist/index.d.ts.map
4.0K	dist/index.js
4.0K	dist/index.js.map
896K	dist/integrations
84K	dist/llm
944K	dist/loop
188K	dist/mcp
32K	dist/presets
92K	dist/setup
40K	dist/skills
392K	dist/sources
76K	dist/ui
144K	dist/utils
336K	dist/wizard

} else {
await linearSyncHandler({
type: 'failed',
error: exitReason || 'unknown',

Check warning

Code scanning / CodeQL

Useless conditional Warning

This use of variable 'exitReason' always evaluates to true.

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.

Suggested changeset 1
src/loop/executor.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/loop/executor.ts b/src/loop/executor.ts
--- a/src/loop/executor.ts
+++ b/src/loop/executor.ts
@@ -1701,7 +1701,7 @@
     } else {
       await linearSyncHandler({
         type: 'failed',
-        error: exitReason || 'unknown',
+        error: exitReason,
         iterations: finalIteration,
       });
     }
EOF
@@ -1701,7 +1701,7 @@
} else {
await linearSyncHandler({
type: 'failed',
error: exitReason || 'unknown',
error: exitReason,
iterations: finalIteration,
});
}
Copilot is powered by AI and may make mistakes. Always verify output.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR adds real-time Linear issue status synchronization to the ralph run loop lifecycle — moving a ticket through In Progress → Done / In Review as the loop starts, completes, or fails. The feature is well-scoped and non-blocking (errors are swallowed gracefully), with a clean CLI flag, SDK export, and 5 new tests.

Key points:

  • The LinearIntegration adapter is instantiated directly inside createLinearSync rather than being injected, violating the project's DI pattern and requiring vi.mock to work around in tests
  • Two event variants (start, iteration) are exported as SDK API but are never emitted and have explicit no-op handlers — they should be removed from the exported type
  • Each sync event triggers two independent resolveIssueId GraphQL calls (updateTask + addComment each resolve the ID separately); the UUID returned by updateTask could be reused

Confidence Score: 3/5

  • Safe to merge with code quality concerns — no runtime-breaking bugs, but DI pattern violation and dead exported API should be addressed before wider SDK adoption.
  • The core logic is sound and non-blocking errors prevent any user-facing breakage. However, three substantive architectural issues affect code quality and SDK ergonomics: (1) LinearIntegration instantiated inline rather than injected, violating the documented DI pattern; (2) two event type variants exported in the SDK surface but never actually emitted with no-op handlers, misleading consumers; (3) redundant GraphQL resolveIssueId calls on each sync event that could be optimized. These are not correctness bugs but represent style/architecture debt worth fixing before this stabilizes as public API.
  • src/loop/linear-sync.ts requires the most attention for DI refactor, dead event type cleanup, and redundant ID resolution optimization.

Sequence Diagram

sequenceDiagram
    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
Loading

Last reviewed commit: 92ed33a

export async function createLinearSync(
config: LinearSyncConfig
): Promise<((event: LinearSyncEvent) => Promise<void>) | null> {
const linear = new LinearIntegration();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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!

Comment on lines +18 to +22
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 };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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.

Comment on lines +66 to +67
await linear.updateTask(config.issueId, { status: 'Done' });
await linear.addComment(config.issueId, lines.join('\n'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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).

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant