Records feat-014 (GitHub GraphQL batch push/delete, commit 114a575) and
feat-015 (avoid stale remote-tree read after push, commit 7676325) as
done. Also archived feat-011/012/013 and other completed narratives out
of progress.md (had grown past its own 80-line cleanup threshold) into
archive/2026-07.md, and trimmed feature_list.json evidence strings under
300 chars — harness self-audit was 96/100, now 100/100.
User reported push-all was still slow on GitHub after the one-
commit-per-run batching landed. Root cause: GitHubService/
GiteaService.pushBatch still created each file's blob with a
sequential for loop -- one POST .../git/blobs awaited fully before
the next started -- so wall-clock time was still O(N) round trips of
pure latency even though only one commit came out the other end.
- Add BaseGitService.mapWithConcurrency<T,R>: an order-preserving,
bounded-concurrency mapper (worker-pool over a shared cursor).
- Add BLOB_CREATE_CONCURRENCY = 8 alongside MAX_BATCH_PUSH_SIZE.
- GitHubService/GiteaService.pushBatch now create blobs via
mapWithConcurrency instead of a for loop; the tree/commit/ref
sequence after it is unchanged since each step genuinely depends on
the previous one's result.
- GitLab is unaffected -- its pushBatch already sends the whole batch
in one Commits API request, no per-file blob step to parallelize.
Added a regression-guard test: deferred blob-response promises that
only resolve after every blob POST has been dispatched, so a
reintroduced sequential loop deadlocks the test instead of silently
passing.
Evidence: npx eslint . -> 0 errors; npm run build -> clean;
npx vitest run -> 340/340 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up to the push-all batching work: batch-deleting remote-only
files via the sync status panel's checkbox multi-select was still N
separate commits (SyncStatusView.performRemoteDeletion looped
gitService.deleteFile once per file).
- Add optional GitServiceInterface.deleteBatch, mirroring pushBatch.
GitHub/Gitea implement it via the git blob->tree->commit->ref Data
API, reusing resolveGitHubStyleBaseTree/resolveBaseTree and
commitGitHubStyleTree (widened its tree-item sha type to
string | null -- a null sha removes that path from the resulting
tree, GitHub's way of expressing a delete at the tree level).
GitLab implements it via the same Commits API endpoint pushBatch
already uses, with action: 'delete' entries.
- SyncStatusView.performRemoteDeletion now calls deleteBatch once per
chunk (MAX_BATCH_PUSH_SIZE, reused from the push work) when the
provider supports it; a failed chunk marks every path in it as
failed rather than dropping results silently. The original per-file
loop is preserved verbatim as performRemoteDeletionSequential, the
fallback for providers without deleteBatch.
Local deletion is unaffected -- it's a local vault operation, not a
git commit.
Evidence: npx eslint . -> 0 errors; npm run build -> clean;
npx vitest run -> 339/339 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Push-all was up to 3N sequential HTTP calls and N separate commits for
N files (getFile check -> pushFile PUT -> optional extra getFile),
plus two redundant full remote-tree fetches per run (one discarded in
main.ts, one inside GitignoreManager).
- Add optional GitServiceInterface.pushBatch. GitHub/Gitea implement it
via the git blob->tree->commit->ref Data API, generalizing
pushSymlink's existing pattern into shared
resolveGitHubStyleBaseTree/commitGitHubStyleTree helpers in
git-service-base.ts. GitLab implements it via its native multi-file
Commits API (actions array), with a follow-up listFilesDetailed call
to recover each file's new blob sha since that endpoint doesn't
return them. Symlinks stay on the existing per-file pushSymlink path.
- sync-manager.ts's push-all flow now classifies each file by comparing
a locally-computed git blob sha (utils/git-blob-sha.ts, already used
by the feat-006 status refresh) against a pre-fetched remote tree's
per-entry sha, eliminating the per-file getFile call for the common
case. Queued files are committed in one grouped pushBatch call,
chunked at MAX_BATCH_PUSH_SIZE=200; a failed chunk marks every file
in it as failed rather than dropping results silently. Providers
without pushBatch fall back to the original sequential path.
- main.ts fetches the remote tree once per push/pull-all run and
threads it into both GitignoreManager.loadGitignores(tree) and
SyncManager.pushAllFiles(files, onProgress, tree), replacing the
previously-discarded listFiles() call and gitignore-manager's
separate fetch with one shared call.
Rename detection and the pull-all path are unchanged (out of scope).
Evidence: npx eslint . -> 0 errors; npm run build -> clean;
npx vitest run -> 330/330 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds minimal ItemView/WorkspaceLeaf mocks to tests/setup.ts (the shared
'obsidian' module mock) -- SyncStatusView had no test file before this.
New tests/ui/SyncStatusView.test.ts covers:
- performRemoteDeletion strips the vaultFolder prefix before calling
gitService.deleteFile (with and without vaultFolder configured), and
records the real error message instead of swallowing it.
- identifyExtraFiles classifies a local folder colliding with a stale
remote record as remote-only rather than a readable file, while still
treating a genuine local file as checkable.
performRemoteDeletion passed the vault-relative path (carrying the
vaultFolder prefix) straight to gitService.deleteFile(), but
getFullPath() expects a path relative to rootPath only -- vaultFolder is
stripped before every other gitService call site (see sync-manager.ts's
getNormalizedPath(file.path) pattern). With a non-empty vaultFolder
setting this built the wrong repo path, so deleteFile's pre-delete
getFile() lookup 404'd -- surfacing as "file was not found on branch
main" for a file the UI still listed as remote-only, since status
refresh already normalizes the path correctly.
Also hardens SyncStatusView against a local directory (or symlink to
one) colliding with a stale remote record at the same path: a new
isLocalFile() helper (adapter.stat().type === 'file') gates
identifyExtraFiles and the hidden-file recursiveScan, and
readFileContent's string-path branch falls back to
readLocalSymlinkTarget() on read failure.
Resolves conflicts between issue #78 (delete-remote-only-file) fixes and
the i18n work landed on this branch in parallel: the delete-result Notice
now uses the i18n t() helper with a new 'partialWithMessage' key (en +
zh-tw) that carries the real error text.
- Root path setting's folder picker now suggests folders from the remote
repo tree (RemoteFolderSuggest) instead of the local vault, since Root
path is a repo-side setting unrelated to local vault structure.
- GitHub/Gitea deleteFile: URL-encode path segments (matching GitLab's
existing behavior) so paths with spaces/non-ASCII don't 404 on the
pre-delete sha lookup; throw a clear error instead of silently sending
a DELETE with an empty sha when that lookup 404s.
- SyncStatusView delete flow now surfaces the real error message instead
of swallowing it into a bare "N failed" notice.
- SyncStatusView.readFileContent: guard against EISDIR when a hidden
local-only symlinked folder (not yet known to the remote) is read as a
file, falling back to the raw symlink target.
Closesfirstsun-dev/blog#78 (misfiled; this is the actual fix location).
Records this session's completed work (#33, #36, #31, #39 consolidated
onto PR #51 alongside the prior session's #40/#41/#42/#48) and the
paused state of feat-009 (#38 i18n), which is blocked on a scope
decision from the user before any code is written.
Adds the missing harness subsystems so future agent sessions can start,
stay in scope, verify work, and resume reliably:
- feature_list.json: local mirror of active/next-up work (GitHub Issues
on Project #6 remains the source of truth for the full backlog)
- progress.md / session-handoff.md / archive/2026-07.md: session state,
restart point, and monthly archive of finished work
- init.sh: install + lint + test + build verification entrypoint
- AGENTS.md: added Startup Workflow, Definition of Done, Stay in Scope,
and End of Session sections (existing agent-tier content kept as-is)
- CLAUDE.md: added a short Agent Workflow section routing to the above
Validated with the firstsun-harness audit script: 100/100 across all
five subsystems (instructions, state, verification, scope, lifecycle).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYCTyZw7gUmJ7oh1VTmAqh