{ "_note": "GitHub Issues (firstsun-dev/git-files-sync, Project #6) is the source of truth for the full backlog and priority/estimate fields. This file mirrors only the active feature and the next few candidates so an agent session has a local, offline checkpoint — sync it against `gh issue list --repo firstsun-dev/git-files-sync --state open` at the start of a session rather than treating it as authoritative.", "_prPolicy": "User explicitly requested fewer PRs (2026-07-13): all issue work is consolidated onto branch claude/fix-directory-symlink-pull-260713 -> PR #51, not one PR per issue. Commit new work directly onto that branch unless told otherwise.", "features": [ { "id": "feat-001", "name": "Project Setup", "description": "Confirm the project can install dependencies, run verification, and start from a clean checkout", "dependencies": [], "status": "done", "evidence": "Commit 28f4f8e - npm install/lint/build/test all pass from a clean checkout" }, { "id": "feat-002", "name": "Settings UX bundle (issues #40, #41, #42)", "description": "Local ignore-pattern sync setting (#40), persistent connection status badge in settings (#41), and resized/tabbed conflict resolution modal (#42)", "dependencies": ["feat-001"], "status": "done", "evidence": "Consolidated onto PR #51 (branch claude/fix-directory-symlink-pull-260713); 302 tests pass" }, { "id": "feat-003", "name": "fix: symbolic link pull fails (issue #33)", "description": "Directory symlinks were walked as real trees by local scanning code, causing bogus remote .gitignore 404s and a permanently-stuck 'remote only' status; also fixed a missing Windows symlinkSync type hint", "dependencies": [], "status": "done", "evidence": "Commit 4c8896b, consolidated onto PR #51" }, { "id": "feat-004", "name": "fix: resolve sonarqube issues (issue #45)", "description": "Review current SonarQube findings and fix code smells/bugs/security hotspots where applicable", "dependencies": [], "status": "not-started", "evidence": "" }, { "id": "feat-005", "name": "feat(settings): folder picker for root path / vault folder (issue #48)", "description": "Searchable folder suggester for the Root path and Vault folder settings fields, replacing free-text entry", "dependencies": [], "status": "done", "evidence": "Commit c107979, consolidated onto PR #51" }, { "id": "feat-006", "name": "perf(refresh): use tree blob SHAs to avoid per-file content fetches (issue #36)", "description": "Status refresh compares a locally-computed git blob SHA against each remote tree entry's SHA instead of one getFile per file; diff content fetched on demand via new getBlob(sha,path); symlink-aware hashing per real/follow/skip modes", "dependencies": [], "status": "done", "evidence": "Commit 2ed5a43, consolidated onto PR #51; 302 tests pass" }, { "id": "feat-007", "name": "fix: clear error when requestUrl() rejects with HTML (issue #31)", "description": "Some Obsidian versions eagerly JSON-parse inside requestUrl() itself, so a login/proxy HTML page rejected the whole call with a raw SyntaxError bypassing the existing parseJson() HTML-detection wrapper; added the same detection to safeRequest's outer catch", "dependencies": [], "status": "done", "evidence": "Commit a867217, consolidated onto PR #51" }, { "id": "feat-008", "name": "feat: show new feature tips after update (issue #39)", "description": "Hand-curated src/changelog.ts (separate from auto-generated CHANGELOG.md) with a notable flag per entry, numeric compareVersions(), WhatsNewModal shown once per version bump via a new lastSeenVersion setting", "dependencies": [], "status": "done", "evidence": "Commit 4eebebc, consolidated onto PR #51" }, { "id": "feat-009", "name": "feat: add i18n (multi-language) support (issue #38)", "description": "Extract hardcoded UI strings (settings tab, ribbon/command labels, Notice messages, sync status view/modals) into an i18n key system with locale detection and English fallback", "dependencies": [], "status": "done", "evidence": "Commit 144eb28, merged into claude/fix-directory-symlink-pull-260713 - 159 i18n keys (en+zh-tw), ~130 strings replaced; lint/build clean, 308 tests pass; consolidated onto PR #51" }, { "id": "feat-010", "name": "feat: add bitbucket support (issue #37)", "description": "Add Bitbucket as a fourth GitServiceInterface provider; Bitbucket's API has no native blob SHA concept and is PR-based rather than direct-commit, so GitFile.sha semantics may need adjusting", "dependencies": ["feat-009"], "status": "not-started", "evidence": "" }, { "id": "feat-011", "name": "perf(push): batch-commit push-all + SHA-based diffing", "description": "Push-all was up to 3N sequential HTTP calls and N separate commits for N files, plus two redundant full-tree fetches per run. Added optional GitServiceInterface.pushBatch (GitHub/Gitea via blob->tree->commit->ref, generalizing pushSymlink's pattern; GitLab via the native multi-action Commits API + a follow-up tree read for blob shas); sync-manager now classifies each file via a locally-computed git blob sha against one pre-fetched remote tree (no getFile per file) and commits all queued files in one grouped call (chunked at MAX_BATCH_PUSH_SIZE=200), falling back to the old per-file path when a provider has no pushBatch; main.ts/GitignoreManager now share one tree fetch instead of two", "dependencies": [], "status": "done", "evidence": "npx eslint . -> 0 errors; npm run build -> clean; npx vitest run -> 330/330 passed; consolidated onto PR #51" }, { "id": "feat-012", "name": "perf(delete): batch-commit remote-only file deletion", "description": "Follow-up to feat-011: batch-deleting remote-only files via the sync status panel's checkbox multi-select was still N separate commits (SyncStatusView.performRemoteDeletion looped gitService.deleteFile per file). Added optional GitServiceInterface.deleteBatch, mirroring pushBatch: GitHub/Gitea via the git blob->tree->commit->ref Data API (a tree entry with sha:null removes that path); GitLab via the same Commits API actions array used for pushBatch, with action:'delete'. SyncStatusView.performRemoteDeletion now calls deleteBatch once (chunked at MAX_BATCH_PUSH_SIZE) when the provider supports it, falling back to the original sequential per-file performRemoteDeletionSequential otherwise", "dependencies": ["feat-011"], "status": "done", "evidence": "npx eslint . -> 0 errors; npm run build -> clean; npx vitest run -> 339/339 passed; consolidated onto PR #51" }, { "id": "feat-013", "name": "perf(push): parallelize blob creation within a batch commit", "description": "User reported push-all was still slow on GitHub after feat-011's one-commit-per-run batching landed. Root cause: GitHubService/GiteaService.pushBatch still created each file's blob with a sequential `for` loop (N sequential POST .../git/blobs round trips before the single tree/commit/ref sequence), so wall-clock time was still O(N) latency-bound. Added a shared BaseGitService.mapWithConcurrency helper (order-preserving, bounded concurrency) and a BLOB_CREATE_CONCURRENCY=8 cap in git-service-base.ts; both services' pushBatch now creates blobs concurrently instead of one at a time. GitLab unaffected (its Commits API already sends the whole batch in one request, no per-file blob step)", "dependencies": ["feat-011"], "status": "done", "evidence": "npx eslint . -> 0 errors; npm run build -> clean; npx vitest run -> 340/340 passed; consolidated onto PR #51" }, { "id": "feat-014", "name": "perf(push): GitHub batch push/delete via GraphQL createCommitOnBranch", "description": "User reported batch push was still slow on GitHub after feat-013's blob-concurrency fix. Root cause: GitHubService.pushBatch/deleteBatch still needed N/8 rounds of sequential REST blob-creation calls before the tree/commit/ref sequence. Replaced GitHub's pushBatch/deleteBatch with a single GraphQL createCommitOnBranch mutation carrying file content directly (additions/deletions), cutting an N-file batch to 2-3 HTTP calls total (ref lookup + mutation, plus one follow-up tree fetch for pushBatch's per-file shas, which the mutation doesn't return). Extracted BaseGitService.getLatestCommitSha out of resolveGitHubStyleBaseTree (still used by pushSymlink and Gitea's REST-only batch path, which has no GraphQL equivalent). Added explicit handling for GraphQL's 200-status-with-errors-array failure mode, which REST's status-code check can't catch.", "dependencies": ["feat-013"], "status": "done", "evidence": "Commit 114a575. npx eslint . -> 0 errors; npm run build -> clean; npx vitest run -> 341/341 passed; live smoke test against a real GitHub repo confirmed pushBatch/deleteBatch each produce one commit." }, { "id": "feat-015", "name": "fix(push): avoid stale remote-tree read right after a batch push", "description": "User reported: batch-pushing two empty files, then refreshing immediately, showed them as 'modified' even though the diff was identical; refreshing again a bit later showed them synced. Root cause (confirmed via live GitHub API test, not a content bug - the pushed blob was byte-for-byte correct): SyncStatusView re-fetched the remote tree via refreshAllStatuses() right after push, and GitHub's tree-by-branch-name read can lag a just-completed write by a moment. Fix: SyncManager.pushAllFiles now also returns syncedPaths (path + new sha) from data already known at each write site (batch chunk result, sequential fallback, immediate symlink/rename push); SyncStatusView marks those paths 'synced' directly instead of re-fetching the tree, avoiding the race entirely. Pull unaffected.", "dependencies": ["feat-014"], "status": "done", "evidence": "Commit 7676325. npx eslint . -> 0 errors; npm run build -> clean; npx vitest run -> 344/344 passed." } ], "_evidenceStyle": "Keep evidence to one line: commit hash + short pointer (e.g. 'Commit abc1234 - added X, tests pass'). Debugging narrative and design discussion belong in the commit message, not this file." }