Skip to content

fix(quarry): external-sync now propagates closed task state to remote#108

Open
fnnzzz wants to merge 1 commit into
stoneforge-ai:masterfrom
fnnzzz:fix/sync-closed-tasks-propagate-close
Open

fix(quarry): external-sync now propagates closed task state to remote#108
fnnzzz wants to merge 1 commit into
stoneforge-ai:masterfrom
fnnzzz:fix/sync-closed-tasks-propagate-close

Conversation

@fnnzzz
Copy link
Copy Markdown
Contributor

@fnnzzz fnnzzz commented May 6, 2026

Problem

pushSingleElement in packages/quarry/src/external-sync/sync-engine.ts skipped every element whose status was closed, with a comment claiming "they're done and shouldn't sync". That's wrong — the close transition itself is what needs to be pushed. As a result, GitHub issues linked via auto-link stayed OPEN forever after the corresponding Stoneforge task was closed, and their sf:status:open label was never replaced with sf:status:closed.

Root cause

packages/quarry/src/external-sync/sync-engine.ts (pre-fix), inside pushSingleElement:

// Skip closed/tombstone tasks and archived documents — they're done
// and shouldn't sync. If an element is later reopened/reactivated,
// it will be picked up again naturally since the filter no longer applies.
const elementStatus = (element as unknown as { status: string }).status;
if (elementStatus === 'closed' || elementStatus === 'tombstone' || elementStatus === 'archived') {
  return 'skipped';
}

'closed' short-circuited the push before pushElement could compare hashes or query events, so the close transition never reached the adapter.

Fix

Remove closed from the early-skip guard. The downstream lastPushedHash comparison and listEvents({ eventType: ['updated', 'closed', 'reopened'], after: lastPushedAt }) query in pushElement/pushDocument already dedupe subsequent pushes, so the change is safe and self-throttling — a closed task is pushed exactly once per close transition, not on every subsequent sync. Tombstones (deleted tasks) and archived documents stay in the skip guard as terminal states that shouldn't generate any further sync traffic. The surrounding comment is updated to explain why closed tasks must still push.

Test coverage

packages/quarry/src/external-sync/sync-engine.bun.test.ts:

  • Regression — closed task is pushed: push propagates closed tasks so the close-event reaches the remote — replaces the previous test that asserted the buggy behavior. Verifies the adapter's updateIssue is called and that the captured payload's state is 'closed', so the linked GitHub issue closes and its sf:status:* label updates.
  • Tombstone still skipped: push skips tombstone tasks — unchanged. Verifies tombstone tasks are still excluded from push.
  • Archived doc still skipped: skips archived documents on push — unchanged (in the document push describe block). Verifies archived documents are still excluded.
  • Idempotency / hash dedupe: push closed task: idempotent — skipped when lastPushedHash matches current hash — new. Computes the actual content hash of a closed task, sets lastPushedHash to that value, asserts the push is skipped via the downstream hash check (proving dedupe still works after the early-skip removal).
  • Event-based skip: push closed task: skipped when no updated/closed/reopened events since lastPushedAt — new. Verifies that a closed task with no relevant events since the last push is skipped via the downstream listEvents guard.
  • Reopen path: push resumes syncing when task is reopened (status no longer closed/tombstone) — unchanged. Verifies that reopening a previously-closed task pushes again.

Full quarry suite: bun test src4262 pass, 1 skip, 0 fail (95 files). pnpm run typecheck and pnpm run build clean.

Manual verification

End-to-end on a temporary workspace:

  1. sf init a fresh workspace; auto-link a Stoneforge task to a GitHub issue (sf external-sync link <task-id> <issue-url>).
  2. Confirm the issue is OPEN and labeled sf:status:open.
  3. sf task complete <task-id> (closes the task).
  4. sf external-sync push <task-id>.
  5. Observe: the GitHub issue is now CLOSED, the sf:status:open label has been replaced with sf:status:closed, and the local task's _externalSync.lastPushedAt/lastPushedHash are updated.
  6. Re-run sf external-sync push <task-id> immediately. Observe: the task is reported as skipped (downstream hash dedupe), confirming the fix is self-throttling.

pushSingleElement was unconditionally skipping every element whose status
was 'closed', leaving linked GitHub issues OPEN forever after the
Stoneforge task was closed and never updating their sf:status:* labels.

Remove 'closed' from the early-skip guard so the close transition reaches
the remote. Downstream hash and event checks in pushElement/pushDocument
already dedupe subsequent pushes, so this is safe and self-throttling.

Tombstones and archived documents remain in the skip guard as terminal
states.
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