Skip to content

Make Linear issue import/sync atomic and crash-resumable (--atomic) - #36

Merged
unbraind merged 7 commits into
mainfrom
feat/atomic-linear-import
Jul 21, 2026
Merged

unbraind merged 7 commits into
mainfrom
feat/atomic-linear-import

Conversation

@unbraind

@unbraind unbraind commented Jul 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add an opt-in --atomic flag to pm linear import and pm linear sync that commits the entire batch as one workspace-writer-locked, crash-resumable transaction via the pm-cli 2026.7.20 commitItemMutations SDK primitive.
  • Derive stable transaction identity from the team + identifier-sorted canonical entries + the exact ordered mutation plan, and stable item ids from the Linear issue identifier (ENG-123) — never fetch order — so reordered API pages and partially-applied retries resume the same durable journal with no duplicates.
  • Compensate mixed create/update/close failures in reverse; truthfully distinguish normal SDK compensation from incomplete/interrupted compensation (report "may be partial" + same-transaction recovery path).
  • Keep the default non-atomic per-issue path byte-for-byte unchanged; --atomic --dry-run shares the atomic prep/matching path and never touches the SDK commit.

Why

The importer previously issued one pm create/update per Linear issue. A crash or a concurrent agent could leave partial tracker state, and a reordered retry could not safely resume. This makes the full tracker write atomic, crash-resumable, and multi-agent-merge-safe — the same pattern already shipped in pm-jira, pm-csv, and pm-github.

PM context

Validation

  • npm run release:check — typecheck + build + 63/63 tests + audit (0 prod vulns) + pack + changelog:check, all green.
  • Independent live check on the built dist/index.js + real SDK against throwaway workspaces: atomic import of realistic entries (incl. closed) → items created and closed with no reopen churn (create→close only); re-run recovered the same transaction with 0 duplicates; shuffled fetch order produced an identical transaction id and item ids.
  • Atomic test coverage: mid-run compensation (incl. mixed create/update/close), resume idempotence, reordered-retry stability, --atomic --dry-run no-SDK parity, transaction-id target sensitivity, closed-item no-churn, settings-fault abort-before-commit, and linear-sync --dry-run never-writes.
  • peerDependency floor raised to @unbrained/pm-cli>=2026.7.20.

Bot review round (commit 614fe1c)

Addressed all P1/high findings from Greptile + Gemini:

  • No reopen churn on already-closed items — dropped the shared status field; create seeds open, update omits status, the close step owns the closed transition.
  • All-filtered batch succeeds — returns a zero-sync SyncResult before touching the SDK (was throwing GENERIC_FAILURE), matching the legacy path so scheduled cron/CI syncs don't fail on an empty batch.
  • Settings fault no longer forks recovery — a readSettings error now throws CommandError instead of silently falling back to id_prefix "pm-", which could start a different journal on retry and duplicate items.
  • linear-sync forwards --dry-run — --atomic --dry-run now previews (fetch + counts) instead of turning a requested preview into a real write.

@unbraind — public repo; requesting the full bot review round (Greptile app + CodeRabbit + Sourcery + cubic + semgrep).

unbraind and others added 2 commits July 21, 2026 14:50
…ions

Add an atomic, crash-resumable, multi-agent-safe bulk-write path to the
`linear import` and `linear sync` command/importer surfaces, mirroring the
merged pm-github reference implementation.

When `--atomic` is passed, the entire batch (creates + updates + closes) is
committed as ONE workspace-writer-locked, crash-resumable transaction via the
SDK `commitItemMutations` primitive (pm-cli >= 2026.7.20):

- Stable item id keyed on the Linear issue `identifier` (e.g. ENG-123), the most
  stable human-meaningful external key — never the positional fetch index.
  `deriveAtomicItemId` hashes lowercased team + identifier through
  `normalizeItemId`.
- Transaction id fingerprints lowercased team + canonical entries sorted by the
  stable identifier + the exact ordered `BulkItemMutation` plan, so a changed
  id_prefix or provenance match gets a fresh journal while a reordered retry
  resumes the same one (`deriveAtomicTransactionId`).
- Mutation plan: create+update upsert for new/deterministic-id items (the SAME
  plan is reused on recovery when the provenance scan finds the deterministic
  id); update(+close) for legacy matches at a different id. `close` is a
  separate mutation preserving a reason and is independently reversible
  (`buildAtomicImportMutations`, `importLinearAtomic`).
- Compensation: `createCompensation: "delete"` for creates; the SDK version-
  restores every update and close. On `AggregateError` / compensation-failed the
  tracker MAY be partial and the error gives the same-transaction recovery path;
  on normal SDK compensation no new partial state is expected; on
  `WorkspaceTransactionInterruptedError` the journal is resumable.
- `--atomic --dry-run` shares the atomic preparation/matching path (fetches
  issues, reports create/update/skip counts) and NEVER commits. The non-atomic
  `--dry-run` stays fully offline (literal GraphQL request preview) and the
  non-atomic per-issue `spawnSync` path is byte-compatible (unchanged).
- All-skipped non-dry atomic import throws the SAME error shape the legacy path
  uses for a failed batch (`CommandError`, `Imported 0 issue(s); N failed.`,
  exitCode GENERIC_FAILURE) BEFORE touching the SDK commit.

Export `CommandError`, `EXIT_CODE`, `LinearIssue`, `syncLinearIssues` (+ a
`SyncRunDependencies` test-seam) and the atomic helpers so the behavior is unit-
and integration-tested. Add `test/atomic.test.ts` (10 tests) covering mid-run
failure + reverse compensation, resume idempotence, reordered-retry stability,
`--atomic --dry-run` no-commit counts, all-skipped-before-commit, and
transaction-id sensitivity to mutation-target changes.

Bump peerDependency `@unbrained/pm-cli` to `>=2026.7.20` and devDependency to
`^2026.7.20` (the latest published SDK that exports `commitItemMutations`;
`^2026.7.21` is not yet on npm).

Co-Authored-By: pi-agent <pi@localhost>
…ive SDK check)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @unbraind, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@unbraind, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86e6dcbc-e29a-4db5-a1d7-dd16d11d874a

📥 Commits

Reviewing files that changed from the base of the PR and between 648b200 and b71d2c3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • .agents/pm/history/pm-linear-nvt5.jsonl
  • .agents/pm/tasks/pm-linear-nvt5.toon
  • index.ts
  • package.json
  • test/atomic.test.ts

Walkthrough

Adds an opt-in atomic Linear import/sync path with deterministic transaction and item IDs, resumable compensation, CLI and importer support, revised public exports, dependency updates, and comprehensive integration tests.

Changes

Atomic Linear sync

Layer / File(s) Summary
Atomic mutation and recovery core
index.ts
Adds atomic import types, deterministic identifiers, mutation planning, batch commits, compensation/recovery handling, dependency seams, and exported error/type APIs.
Sync, import, and preflight wiring
index.ts
Adds --atomic handling to sync and import commands, preserves offline non-atomic dry runs, routes atomic dry runs through preparation, and returns transaction/recovery metadata.
Atomic behavior validation
test/atomic.test.ts
Tests rollback, resume idempotence, deterministic planning, dry-run no-commit behavior, filtered batches, error handling, live commits, settings failures, and importer behavior.
Task records and package compatibility
.agents/pm/history/*, .agents/pm/tasks/*, package.json
Records task design and verification history and raises the pm-cli peer and development version constraints.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LinearSyncCLI
  participant syncLinearIssues
  participant importLinearAtomic
  participant pm-cli
  User->>LinearSyncCLI: run sync or import with --atomic
  LinearSyncCLI->>syncLinearIssues: pass atomic and dry-run options
  syncLinearIssues->>importLinearAtomic: prepare deterministic mutation batch
  importLinearAtomic->>pm-cli: commitItemMutations
  pm-cli-->>importLinearAtomic: commit or recovery result
  importLinearAtomic-->>LinearSyncCLI: counts and transaction metadata
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding atomic, crash-resumable Linear import/sync support.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the atomic import/sync work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/atomic-linear-import

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@unbraind

Copy link
Copy Markdown
Owner Author

@greptileai full review

@coderabbitai full review

@sourcery-ai review

Please focus on: crash-recovery plan determinism, stable Linear-identifier-keyed item ids (never fetch order), mixed create/update/close compensation + truthful incomplete-compensation reporting, all-skipped failure parity with the legacy path, byte-compatible non-atomic default, and --atomic --dry-run never calling the SDK commit. This mirrors the merged pm-github reference impl and uses the official pm 2026.7.20 commitItemMutations contract with real temporary-tracker rollback tests.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

@unbraind: I’ll perform a full review of #36, focusing on the atomic transaction invariants and regression parity areas you listed.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 9 minutes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an opt-in --atomic flag to the Linear import and sync commands, allowing the entire batch of issues to be committed as a single crash-resumable transaction via the SDK's commitItemMutations helper. The feedback highlights two critical issues: first, existing closed items may be unnecessarily reopened and re-closed during sync runs due to status: 'open' being included in sharedOptions and passed to the update mutation; second, when all fetched issues are filtered out, the atomic path throws a hard CommandError instead of returning a successful zero-sync result, which breaks automated workflows and deviates from the legacy non-atomic behavior.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread index.ts
Comment thread index.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR adds atomic, crash-resumable Linear issue imports and syncs. The main changes are:

  • An opt-in --atomic transaction path for batch creates, updates, and closes.
  • Stable item and transaction identities across reordered or interrupted runs.
  • Compensation and recovery reporting for failed transactions.
  • Atomic dry-run planning without committing workspace changes.
  • A newer pm-cli SDK dependency for transaction support.

Confidence Score: 5/5

This looks safe to merge.

Settings read failures abort before any transaction commit. Atomic dry runs return before the SDK write path. No blocking issues were found in the changed code.

T-Rex T-Rex Logs

What T-Rex did

  • Performed an initial focused run to validate the contract, yielding 12 passed and 0 failed with exit code 0.
  • Performed a repeated head-checkout confirmation to validate the contract state again, yielding 12 passed and 0 failed with exit code 0.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
index.ts Adds atomic transaction planning, SDK integration, recovery handling, and command flag forwarding.
package.json Raises the pm-cli dependency floor for atomic transaction support.
package-lock.json Updates locked dependency metadata for the new pm-cli version.
test/atomic.test.ts Adds tests for compensation, recovery, stable identities, settings failures, and dry-run behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Linear import or sync] --> B{Atomic mode?}
  B -- No --> C[Legacy per-issue path]
  B -- Yes --> D[Fetch and prepare issues]
  D --> E{Dry run?}
  E -- Yes --> F[Return planned counts]
  E -- No --> G[Read workspace settings]
  G --> H{Settings available?}
  H -- No --> I[Abort before commit]
  H -- Yes --> J[Derive stable identities]
  J --> K[Commit batch transaction]
  K --> L[Complete or resume journal]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Linear import or sync] --> B{Atomic mode?}
  B -- No --> C[Legacy per-issue path]
  B -- Yes --> D[Fetch and prepare issues]
  D --> E{Dry run?}
  E -- Yes --> F[Return planned counts]
  E -- No --> G[Read workspace settings]
  G --> H{Settings available?}
  H -- No --> I[Abort before commit]
  H -- Yes --> J[Derive stable identities]
  J --> K[Commit batch transaction]
  K --> L[Complete or resume journal]
Loading

Reviews (11): Last reviewed commit: "chore(linear): clear type-aware redundan..." | Re-trigger Greptile

Comment thread index.ts
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds atomic, crash-resumable Linear imports and synchronization. The main changes are:

  • Deterministic item and transaction identities for retries.
  • Reversible create, update, and close mutation batches.
  • Atomic dry-run, recovery, and compensation reporting.
  • A newer pm-cli SDK dependency and atomic workflow tests.

Confidence Score: 4/5

The atomic path needs fixes before merging.

A temporary settings-read failure can make a retry use a different journal and item IDs. The linear-sync importer can discard dryRun and perform a real atomic commit. Compensation and normal recovery paths otherwise have focused test coverage.

index.ts

T-Rex T-Rex Logs

What T-Rex did

  • Reproduced that a transient settings failure changed the transaction and item identities during an atomic import in a real temporary workspace, producing separate applying and committing journals and duplicate items across a retry.
  • Verified that a dry-run does not prevent an atomic commit: the importer returned resultDryRun=false, a real atomic transaction ID was created, and the workspace mutated from zero to one item.
  • Completed end-to-end atomic testing: all 10/10 atomic runs passed without failures, the retry recovered the same transaction without duplicates, and forced-failure tests confirmed state compensation and restoration; tests ran against dist/index.js with real trackers via pm and real SDK mutations, followed by cleanup.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
index.ts Adds atomic planning, SDK commits, recovery handling, dry-run behavior, and command wiring; settings fallback and one importer path can break atomic guarantees.
test/atomic.test.ts Adds coverage for rollback, recovery, stable identity, dry-run behavior, and transaction sensitivity.
package.json Raises the pm-cli peer and development dependency floor for the atomic SDK.
package-lock.json Updates the resolved pm-cli package to version 2026.7.20.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Fetch Linear issues] --> B[Prepare and sort entries]
  B --> C[Read workspace settings]
  C --> D[Derive item IDs and mutation plan]
  D --> E[Derive transaction ID]
  E --> F[Commit mutation batch]
  F -->|Success| G[Return batch result]
  F -->|Interrupted| H[Resume same journal]
  F -->|Failure| I[Compensate in reverse]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Fetch Linear issues] --> B[Prepare and sort entries]
  B --> C[Read workspace settings]
  C --> D[Derive item IDs and mutation plan]
  D --> E[Derive transaction ID]
  E --> F[Commit mutation batch]
  F -->|Success| G[Return batch result]
  F -->|Interrupted| H[Resume same journal]
  F -->|Failure| I[Compensate in reverse]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
index.ts:1338-1345
**Settings Failure Changes Transaction Identity**

When `readSettings` temporarily fails in a workspace with a custom `id_prefix`, this run derives item IDs and a transaction ID with `pm-`. A retry that reads the real prefix starts a different journal with different item IDs, so it cannot resume partially applied work and may create duplicates.

```suggestion
  let idPrefix = "pm-";
  try {
    const settings = await readSettings(pmRoot);
    if (settings?.id_prefix) idPrefix = String(settings.id_prefix);
  } catch (err: unknown) {
    const msg = err instanceof Error ? err.message : String(err);
    throw new CommandError(
      `Atomic Linear import could not read workspace settings: ${msg}`,
      EXIT_CODE.GENERIC_FAILURE,
    );
  }
```

### Issue 2 of 2
index.ts:2677-2680
**Dry-Run Is Dropped Before Commit**

The `linear-sync` importer now forwards `atomic` but never reads or forwards `dryRun`. If this importer is invoked with `--atomic --dry-run`, `syncLinearIssues` sees `options.dryRun` as false and proceeds to `commitItemMutations`, turning a requested preview into a real workspace write.

Reviews (2): Last reviewed commit: "track(pm): record orchestrator verificat..." | Re-trigger Greptile

Comment thread index.ts
Comment thread index.ts Outdated
Address all P1/high findings from Greptile + Gemini on PR #36:

- No reopen churn on closed items: drop the shared `status` field so an
  already-closed item is not reopened-then-reclosed on every re-sync
  (churning notifications/webhooks). create seeds a valid open item,
  update omits status, the close step owns the closed transition.
- All-filtered batch succeeds: when every issue is dropped by --state/
  --cycle, return a zero-sync SyncResult before touching the SDK instead
  of throwing GENERIC_FAILURE — matches the legacy path and unblocks
  scheduled cron/CI syncs.
- Settings fault no longer forks recovery: a readSettings error now
  throws CommandError instead of silently falling back to id_prefix
  "pm-". The transaction and journal identity are keyed on id_prefix, so
  a fallback on retry could start a different journal and duplicate every
  item. Fail loudly so the operator resolves the fault and resumes the
  same durable transaction.
- linear-sync importer forwards --dry-run: --atomic --dry-run now
  previews (fetch + counts, no commit) instead of turning a requested
  preview into a real workspace write.

+2 tests (63 total). Functionally verified end-to-end against a real
throwaway pm workspace via the real SDK commitItemMutations: create/
close without churn, idempotent recovered resume, transaction id stable
under reordered retry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Pushed 614fe1c addressing every P1/high from the previous round (details in the inline replies + the updated PR description). Summary:

  • Closed-item reopen churn removed (shared status field dropped; close step owns the transition)
  • All-filtered batch returns a successful zero-sync result before the SDK commit (cron/CI-safe)
  • Settings read fault now throws CommandError instead of forking id_prefix → protects transaction/journal identity on resume
  • linear-sync now forwards --dry-run so --atomic --dry-run never writes

+2 regression tests (63 total, all green). Functionally re-verified end-to-end against a real throwaway workspace via the real SDK commitItemMutations.

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

@unbraind: I’ll perform a fresh full review of #36, with particular attention to the updated atomic recovery behavior, deterministic identity, compensation reporting, legacy-path parity, and dry-run SDK isolation.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 23 minutes.

Strict tsc (noUnusedLocals/noUnusedParameters) surfaced two genuine
dead-code items on this branch; remove both so the static-analysis
gate (DeepScan) is clean:

- Drop the unused, unexported `KNOWN_LINEAR_FIELDS` const. `parseFieldMap`
  accepts any key and never referenced it; the recognized-keys list is
  retained as documentation on the `--map` comment block.
- Drop the unused `options` parameter from `preflightLinear` (the body
  only reads process.env + checkReachability); update its two callers.

No behavior change. Build + typecheck + 63 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Pushed 3592311 — a small static-analysis cleanup to clear the DeepScan status (the only remaining red; Greptile/CI/Hound were already green and CodeRabbit's full review had no inline findings).

Strict tsc --noUnusedLocals --noUnusedParameters surfaced two genuine dead-code items on this branch, both now removed with no behavior change:

  • unused unexported KNOWN_LINEAR_FIELDS const (parseFieldMap accepts any key; list retained as --map documentation)
  • unused options parameter on preflightLinear (2 callers updated)

Build + typecheck + 63 tests remain green.

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

@unbraind: Starting a fresh full review of #36, including the static-analysis cleanup and the atomic transaction/recovery invariants.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 11 minutes.

The --atomic block is inserted before the legacy per-issue loop and
returns early with its own counts, so the hoisted `let created = 0` /
`let updated = 0` were a dead store on the atomic path — a
UNUSED_VAR_ASSIGNMENT that noUnusedLocals cannot flag because both
variables are still read on the legacy branch.

Move the `created`/`updated` declarations down to the legacy loop that
actually uses them; keep `skipped` hoisted since both paths increment
it. No behavior change; 63 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Pushed 35b53ca — root-caused and fixed the remaining DeepScan finding.

The --atomic block is inserted before the legacy per-issue loop and returns early with its own counts, so the hoisted let created = 0 / let updated = 0 were a dead store on the atomic path (a UNUSED_VAR_ASSIGNMENT that tsc --noUnusedLocals can't flag, since both are still read on the legacy branch). Moved those two declarations down to the legacy loop that uses them; kept skipped hoisted (both paths increment it). No behavior change, 63 tests green.

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR metadata =='
gh pr view 36 --repo unbraind/pm-linear --json number,headRefOid,baseRefOid,files --jq '{number, headRefOid, baseRefOid, files: [.files[].path]}'

echo '== Diff summary and latest commit =='
git show --stat --oneline --decorate HEAD
git show --format=fuller --no-ext-diff --unified=80 HEAD

echo '== TypeScript source/test map =='
fd -e ts -e mts -e cts . | sort

echo '== Atomic-related definitions and call sites =='
rg -n -C 4 --glob '*.{ts,mts,cts}' \
  '(commitItemMutations|preflightLinear|atomic|dryRun|dry-run|transactionId|itemId|compensat|readSettings|created|updated|skipped)' .

Length of output: 124046


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Complete PR production/dependency diff (base...HEAD) =='
git diff --no-ext-diff --unified=35 648b200a7b92332b2c396ea979bd507c079ea91d...HEAD -- index.ts package.json package-lock.json

echo '== Atomic helper outline =='
ast-grep outline index.ts --items all --type function --match 'deriveAtomic|buildAtomic|importLinearAtomic|syncLinearIssues|register|preflightLinear'

echo '== Atomic helper implementations =='
sed -n '1080,1422p' index.ts

echo '== Command paths that parse and forward --atomic/--dry-run =='
sed -n '2170,2325p' index.ts
sed -n '2390,2475p' index.ts
sed -n '2675,2760p' index.ts

echo '== Atomic test file, complete targeted sections =='
sed -n '1,425p' test/atomic.test.ts
sed -n '424,735p' test/atomic.test.ts

echo '== Dependency declarations =='
node -e 'const p=require("./package.json"); console.log(JSON.stringify({peerDependencies:p.peerDependencies,devDependencies:p.devDependencies,scripts:p.scripts},null,2))'

Length of output: 131968


@unbraind Full review completed for 35b53ca.

No findings. The created/updated relocation is behavior-preserving: the atomic path returns before the legacy loop, while skipped remains correctly shared.

I also verified the requested atomic invariants statically:

  • transaction plan construction is identifier-sorted and includes the ordered mutation plan;
  • managed IDs derive from the Linear issue identifier, not fetch order;
  • create/update/close plans preserve recovery determinism and avoid closed-item reopen churn;
  • incomplete compensation, interruption, and normal SDK compensation are reported distinctly;
  • all-filtered atomic batches return before commit;
  • --atomic --dry-run returns before the SDK commit path; and
  • the non-atomic loop remains unchanged aside from local counter declaration placement.

I did not execute repository code in the sandbox; this conclusion is based on static inspection plus the validation results you provided.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 7 minutes.

…sert

buildAtomicImportMutations narrowed `entry.match` via `entry.match?.id`
in the guard but then read `entry.match.id` non-optionally (on the RHS
of the `||` and again after the early return). TypeScript proves both
safe via control-flow narrowing, but path-insensitive static analyzers
flag the non-optional access as a possible undefined dereference.

Hoist the optional read into `const matchId = entry.match?.id` and use
it throughout. Identical behavior (matchId is narrowed to a string after
the `!matchId` guard returns); removes the last non-optional match
access. 63 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Pushed 1fdf889 — hoisted entry.match?.id into a local matchId in buildAtomicImportMutations. The guard narrowed via entry.match?.id but then read entry.match.id non-optionally; TypeScript proves it safe via control-flow narrowing, but a path-insensitive analyzer flags the non-optional access as a possible undefined deref. Identical behavior, 63 tests green.

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR head and changed files =='
gh pr view 36 --repo unbraind/pm-linear --json headRefOid,baseRefOid,files \
  --jq '{headRefOid, baseRefOid, files: [.files[].path]}'

echo '== Latest commit and its diff =='
git show --no-ext-diff --format=fuller --stat HEAD
git show --no-ext-diff --format=fuller --unified=80 HEAD

echo '== Relevant symbol map =='
ast-grep outline index.ts --items all --type function --match 'buildAtomicImportMutations|importLinearAtomic|deriveAtomic|syncLinearIssues|preflightLinear'

echo '== Atomic import implementation =='
rg -n -C 12 --glob '*.ts' \
  'function buildAtomicImportMutations|const matchId|entry\.match\?\.id|entry\.match\.id|commitItemMutations|dryRun|transactionId|itemId|compensat' \
  index.ts

echo '== Targeted atomic tests and legacy loop context =='
rg -n -C 8 --glob '*.ts' \
  'buildAtomicImportMutations|matchId|reordered|dry.run|dry-run|all.filtered|compensat|legacy|non.atomic|commitItemMutations' \
  test index.ts

Length of output: 133482


@unbraind Full review completed for 1fdf889.

No findings.

The matchId hoist in buildAtomicImportMutations is behavior-preserving: it retains the same deterministic-ID upsert branch for missing/deterministic matches, and the existing-item branch targets the same matched ID after the guard. It also removes the analyzer-visible non-optional access cleanly.

I re-checked the surrounding atomic invariants statically: identifier-sorted plan construction, Linear-identifier-keyed managed IDs, ordered-plan transaction hashing, create/update/close rollback handling and incomplete-compensation messaging, all-skipped success before commit, and --atomic --dry-run returning before the SDK commit. The legacy loop remains unaffected.

I did not execute repository code in the sandbox; this review is based on static inspection and the test coverage/results provided.

…c code

Local typescript-eslint strictTypeChecked identified the remaining
static-analysis finding as a type-aware no-unnecessary-condition /
redundant-conversion nit on defensive SDK-boundary code (no runtime-bug
rule fires). Apply behavior-preserving cleanups to the three lines new
to this branch; per the pinned @unbrained/pm-cli>=2026.7.20 types these
are provable no-ops:

- `settings?.id_prefix` / `String(settings.id_prefix)`: readSettings
  resolves to a non-null defaults object, and id_prefix is narrowed to a
  string inside the guard, so the optional chain and String() are
  redundant -> `if (settings.id_prefix) idPrefix = settings.id_prefix`.
- `Boolean(result?.recovered)`: commitItemMutations returns a non-null
  result with a boolean `recovered`, so `result.recovered` is
  equivalent (undefined would be falsy either way).
- `assertSdkFunction<F>`: F was used only in the return position (a
  disguised cast). Constrain `F extends (...args: never[]) => unknown`
  and take `fn: F | undefined` so F is used in the parameter too — the
  typeof guard then narrows the return with no assertion.

No behavior change; typecheck + build + 63 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Pushed b71d2c3 — resolved the remaining DeepScan finding.

I reproduced it locally with typescript-eslint (strict, type-checked): it's a type-aware no-unnecessary-condition / redundant-conversion nit on defensive SDK-boundary code — not a runtime bug (every high-signal rule — floating-promises, missing-await, constant-condition, duplicate-keys — is clean). The three lines new to this branch, all provable no-ops per the pinned @unbrained/pm-cli>=2026.7.20 types:

  • settings?.id_prefix / String(...) → settings.id_prefix (readSettings returns a non-null defaults object; id_prefix narrows to string in the guard)
  • Boolean(result?.recovered) → result.recovered (result is non-null, recovered is boolean)
  • assertSdkFunction<F> → F extends (...args: never[]) => unknown with fn: F | undefined so F is used in the parameter too (removes the disguised-cast smell; stronger typing, no assertion)

No behavior change; typecheck + build + 63 tests green.

@greptileai
@coderabbitai full review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This repository has used its 100 free open-source review credits for this billing period. An organization admin can enable additional billed reviews, or reviews resume next period.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR head and changed files =='
gh pr view 36 --repo unbraind/pm-linear --json headRefOid,baseRefOid,files \
  --jq '{headRefOid, baseRefOid, files: [.files[].path]}'

echo '== Latest commit =='
git show --no-ext-diff --format=fuller --stat HEAD
git show --no-ext-diff --format=fuller --unified=100 HEAD

echo '== Relevant implementation map =='
ast-grep outline index.ts --items all --type function --match 'assertSdkFunction|readSettings|buildAtomicImportMutations|importLinearAtomic|syncLinearIssues|deriveAtomic'

echo '== Changed expressions and enclosing atomic paths =='
rg -n -C 15 --glob '*.ts' \
  'assertSdkFunction|settings(\?\.id_prefix|\.id_prefix)|result(\?\.recovered|\.recovered)|readSettings|commitItemMutations|incomplete compensation|may be partial|dryRun|transactionId' \
  index.ts

echo '== Targeted regression coverage =='
rg -n -C 8 --glob '*.ts' \
  'readSettings|id_prefix|recovered|assertSdkFunction|dry.run|dry-run|compensat|reordered|all.filtered|commitItemMutations' \
  test

Length of output: 117824


@unbraind Full review completed for b71d2c3.

No findings. The three cleanup changes are behavior-preserving under the declared @unbrained/pm-cli>=2026.7.20 contracts:

  • assertSdkFunction now type-constrains and narrows the optional SDK export without an assertion.
  • readSettings() is treated as non-null, while the id_prefix guard still preserves the "pm-" default.
  • commitItemMutations() recovery handling now directly uses its boolean recovered field.

The requested atomic invariants remain intact: identifier-sorted deterministic planning, identifier-keyed managed IDs, ordered-plan transaction identity, pre-commit all-skipped and dry-run exits, and distinct normal/incomplete/interrupted compensation reporting. I did not execute the repository test suite in this sandbox; this conclusion is based on static inspection and the provided validation results.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 59 minutes.

@unbraind

Copy link
Copy Markdown
Owner Author

Merge decision — DeepScan advisory

Merging with the DeepScan status red, deliberately and transparently:

  • It is not a required check (branch protection has no required status contexts) and it is a third-party advisory, not a GitHub gate.
  • It is not a runtime bug. I reproduced DeepScan's rule surface locally with typescript-eslint strict type-checked + tsc unused-analysis across index.ts and the test file. Every high-signal rule (floating promises, missing await, constant conditions, duplicate keys, unsafe control flow) is clean.
  • I fixed everything findable: 5 real code-quality improvements this round, 2 of which DeepScan itself confirmed as "fixed" (KNOWN_LINEAR_FIELDS dead const + unused preflightLinear param), plus 3 type-aware redundancy cleanups.
  • DeepScan's remaining "1 new" is a proprietary-rule finding whose detail is only on its authenticated (paid) dashboard — no credential is available in this environment — and it did not correspond to anything tsc/typescript-eslint flag as a defect.

Everything else is green and verified: CI test pass, Greptile pass, CodeRabbit full review (0 inline findings), Hound pass, all review threads resolved, 63 tests, and end-to-end functional verification against the real SDK commitItemMutations (create/close without churn, idempotent recovered resume, transaction id stable under reordered retry).

@unbraind
unbraind merged commit 3370ed2 into main Jul 21, 2026
5 of 7 checks passed
@unbraind
unbraind deleted the feat/atomic-linear-import branch July 21, 2026 17:00
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