Completed work is archived in archive/, one file per calendar month.
Last Updated: 2026-08-20
Active Feature: feat-026 / issue #105 — sync architecture refactor on refactor/sync-domain-pipeline. SyncPlanner is now the decision source for normal push, batch pull/preview, single pull, and moves. Edited tracked renames with a free destination plan one move instead of being auto-skipped; remote-only changes pull without false conflicts; real two-sided divergence and occupied move destinations remain conflicts. Post-push CI hardening is locally green; real provider CI plus Obsidian desktop/mobile manual verification remain before declaring the feature complete.
Parallel Work: PR #87 (4x Dependabot security alerts via npm overrides) and Issue #57 (live-credential smoke test).
0a. Re-enable the gitea leg in CI (.github/workflows/ci.yml, "Determine whether this provider leg should run" step) — disabled 2026-08-13 after two rounds of real-CI-only failures (host-port/127.0.0.1 unreachable from this self-hosted fleet's sibling-container topology, then a curl hang) got fixed but a third run wasn't attempted before the user asked to pause it; harness code (scripts/e2e-harness.sh's gitea path, e2e/suites/gitea.e2e.test.ts) is unchanged and passes locally every time (npm run test:e2e -- --provider gitea, most recently re-confirmed 14/14 twice this session). While disabled, fork PRs get zero E2E coverage (gitea is normally the only leg that needs no secrets). PR #124 is already open and green with this leg gated off; re-enabling is a follow-up, not a blocker.
- feat-025 manual verification — Tree view code is complete and all automated checks pass; manual Obsidian verification in a real vault remains for user to confirm functionality (tree hierarchy, folder expand/collapse, checkboxes, Show synced toggle).
- PR #87 — Dependabot security patches via npm overrides; awaiting review/merge.
- Issue #57 — Live-credential smoke test; pre-existing, relevant before pushing major sync work.
-
Issue #105 post-push CI hardening (2026-08-20), commit
948df28: diagnosed run 32336155736 as two exhausted transient-provider attempts rather than a planner regression (GitHub 503/socket close; GitLab deadline exceeded). Increased provider E2E attempts from 2 to 3. A duplicate matrix cancelled by the shared push/PR concurrency group now produces a neutral aggregate gate withrun-ci=false, so it neither creates a misleadingE2E gatefailure nor starts duplicate downstream CI; real failures still block. SyncManager E2E push preconditions now includesuccess,failed, and providererrorsin assertion diagnostics instead of surfacing only a secondary count mismatch. Added workflow contract and diagnostic unit tests and updated the E2E documentation. Verification:actionlint v1.7.12 .github/workflows/ci.yml— 0 errors;npx eslint .— 0 errors;npm run build— clean including Obsidian 1.11 compatibility;npx vitest run— 56 files / 613 tests;npm run test:e2e -- --provider gitea— 2 files / 14 tests and container cleanup;git diff --check— clean. Real CI run 32338116598 passed GitHub/GitLab production E2E, independent verification, cleanup, aggregate gate, Node 22/24 tests, lint, package, and build/release. The initial disabled-Gitea job landed on offline runnerheavenweb-runner-8; failed-only rerun completed its skip in 11s and the full run concluded success. Provider API checks found no remaininge2e/pr/127/**or branch-source E2E refs. AGENTS-required Haiku was unavailable, so verification ran locally and through real CI. -
Issue #105 unified sync decisions and move regression (2026-08-20): added operation-aware
SyncPlanner.planFor(push|pull),MoveFacts, and themovedomain action. Normal push, batch pull and preview, single pull, and tracked moves now consume planner decisions instead of reimplementing SHA conflict checks. RemovedPushCoordinator.queueMove's stale-metadata gate, so an edited tracked rename with a free destination appears under Moves and commits once; occupied destinations remain conflicts. Fixed the complementary pull false positive: a remote-only change now pulls, while real two-sided divergence still resolves as conflict. Content-fetched text/binary paths normalize equal bytes to the provider blob SHA before planning, preserving binary and GitLab legacy-baseline behavior. Added planner operation matrix, coordinator move regression, batch pull, and single pull coverage. Verification:npx eslint .— 0 errors;npm run build— clean including Obsidian 1.11 compatibility;npx vitest run— 54 files / 610 tests;git diff --check— clean. Manual Obsidian verification remains. -
Issue #105 architecture implementation (2026-08-19): extracted
SyncStatusRendererandSyncStatusComposition;SyncStatusView.tsis 11.5 KB / 251 lines. ExtractedPullCoordinatorandPushCoordinator;SyncManager.tsis 13.7 KB / 298 lines and retains its public compatibility API.SyncManagerWorkspacenow owns refresh/tree-snapshot reuse, push/pull, diff, local/remote deletion, move, metadata mutations, provider URLs and UI-safe workspace info; sync-status UI code no longer reaches provider/tree/settings/vault mutation helpers, andsrc/logic/**has no UI imports. Legacy refresh characterization cases now target the extracted service instead of private View delegates; legacy modal tests explicitly inject the Obsidian interaction adapter. Added real refresh integration plus focused push-coordinator/workspace regression tests. Independent verification:npx eslint .— 0 errors;npm run build— clean including Obsidian 1.11 compatibility;npx vitest run— 54 files / 598 tests;npm run test:e2e -- --provider gitea— 2 files / 14 tests with container cleanup;git diff --check— clean. Desktop/mobile Obsidian smoke remains manual. -
Issue #105 architecture slice 3 (2026-08-19): added tested
SyncDiffServiceandSyncStatusNavigator, so lazy blob loading/cache/content-kind projection is a domainFileDiffboundary. Extracted single-file, batch push/pull, local/remote delete, move revert, remote-tree reuse, progress/confirmation, and optimistic-status orchestration intoSyncStatusOperations; all View row/group events now enter throughSyncStatusController. The actual View is about 40 KB (down from 58 KB this slice and 80 KB initially). Independent verification:npx eslint .— 0 errors;npm run build— clean including Obsidian 1.11 compatibility;npx vitest run— 52 files / 594 tests;npm run test:e2e -- --provider gitea— 2 files / 14 tests with container cleanup;git diff --check— clean. Feature remains in progress because renderer composition and the ~50 KB manager facade are still oversized. -
Issue #105 architecture slice 2 (2026-08-19): extracted
SyncStatusRefreshServicefor local/remote discovery, hidden files, symlinks, SHA/content classification, out-of-band move reconciliation, and live modify/rename transitions. The actual View fell from about 80 KB to 58 KB while legacy characterization entrypoints remain thin delegates. AddedSyncInteractionPortplusObsidianSyncInteraction;logic/sync/SyncManager.tsno longer imports Modal or Notice classes. Independent verification:npx eslint .— 0 errors;npm run build— clean including Obsidian 1.11 compatibility;npx vitest run— 51 files / 585 tests;npm run test:e2e -- --provider gitea— 2 files / 14 tests with container cleanup;git diff --check— clean. Feature remains in progress: action orchestration is still View-owned, the manager core is about 50 KB, and desktop/mobile manual smoke tests remain pending. -
Issue #105 architecture slice (2026-08-19): moved compatibility entrypoints to thin re-exports; added presentation state, pure selectors, path-only controller commands, pure planner matrix, scanner, metadata store, push/pull/remote-delete/conflict executors,
SyncManagerWorkspace,FileDiff, and four workspace integration paths.npx eslint .— 0 errors;npm run build— clean including Obsidian 1.11 compatibility;npx vitest run— 51 files / 585 tests passed;npm run test:e2e -- --provider gitea— 2 files / 14 tests passed with sandbox cleanup. Feature remains in progress: the implementation files are still oversized (sync-status/SyncStatusView.ts~80 KB,sync/SyncManager.ts~50 KB), domain still imports modal adapters, and manual Obsidian desktop/mobile checks are pending. -
Real-provider E2E Phase 2, PR #124 fully green (2 more follow-up commits, same branch/PR): (1) NOSONAR placement fix — the previous commit's
# NOSONARcomments on the gitea-provisioningcurlcalls landed on the closing line of each multi-line statement, but SonarCloud attributes shell:S5332 to the openingcurlline, which can't carry a trailing comment while also ending in a\continuation; collapsed those two calls to single lines (payload JSON pulled into a local var first) so the marker lands correctly — confirmed via SonarCloud's issues API (2 of 7 findings were still OPEN after the first fix, both on the curl lines themselves; 0 after this one, Security Rating A). (2) Dedup push/pull_request races —provider-e2e's concurrency group keyed PR runs by PR number and branch-only runs by branch name, so a push to a branch with an open PR (this branch, since it has an open PR) fired both apushand apull_requestrun in different concurrency groups for the same commit, running fully concurrently against the same shared GitLab sandbox; reproduced twice (rerunning thepull_request-triggered run's GitLab leg failed both times — first with 3 different real-API errors including400: Deadline Exceeded, then with a plaintestConnection120s timeout — while thepush-triggered run for the identical commit passed cleanly both times). Fixed by keying the group by branch name alone (github.head_ref || github.ref_name, same expressionE2E_SOURCE_BRANCHalready used) regardless of trigger event, and updatede2e-pr-cleanup.yml/e2e-branch-cleanup.yml's groups to match (documented as sharingprovider-e2e's group so cleanup queues behind rather than races an active run). Verified end-to-end: pushed the fix, both apushand apull_requestrun fired again for the same commit as expected, and this time the concurrency group correctly cancelled one of them instead of letting them race — the surviving run passed 100% clean (GitHub/GitLab/ Gitea E2E, full CI, SonarCloud A). User explicitly chose "fix the dedup now" over deferring or just re-running until green, when asked. Verification:actionlint— 0 errors;npx eslint .— 0 errors;npm run build— clean;npx vitest run— 527 passed; real end-to-end Gitea sandbox run (npm run test:e2e -- --provider gitea) — 14/14 passed, twice, exercising the edited curl calls directly; real CI — PR #124's surviving run fully green including all three real-provider E2E legs and SonarCloud Security Rating A. -
Real-provider E2E Phase 2 follow-up fix (same branch/PR #124):
ci.yml'sprovider-e2ejob setE2E_WORKDIRin job-levelenv:using${{ runner.temp }}—runnerisn't an allowed context there (onlygithub/inputs/matrix/needs/secrets/strategy/varsare), which makes GitHub Actions reject the whole workflow file at parse time; confirmed viaactionlintand via the GitHub API (jobs_urlfor the c8382cb push run returnedtotal_count: 0— no job was ever created). Fixed by computingE2E_WORKDIRin an unconditional first step instead, exporting it through$GITHUB_ENV(uses$RUNNER_TEMP, the step-level equivalent). Also fixed the SonarCloud Quality Gate failure (Security Rating D on new code, required ≥ A):scripts/e2e-namespace.sh'se2e_branch_hashusedsha1sum/shasum(CRITICAL, shell:S4790 weak-hash — not a real security use, just a collision-avoidance digest, but Sonar flags SHA-1 regardless of context) — switched tosha256sum/shasum -a 256; fivecurl/log lines inscripts/e2e-harness.sh's gitea provisioning that talkhttp://to a per-run Docker-bridge-only container (shell:S5332 clear-text-protocol) — annotated# NOSONARwith an inline justification (address never leaves the run's own Docker network, credentials are freshly random and discarded at cleanup);.github/workflows/ci.yml's newnpm ci(githubactions:S6505, missing--ignore-scripts) andactions/checkout@v6/actions/setup-node@v6/dorny/paths-filter@v3in the two new jobs plus the three new standalone workflow files (githubactions:S7637, unpinned action refs) — pinned to full commit SHAs,npm ciin the new job got--ignore-scripts(husky'spreparehook isn't needed in CI). Left the pre-existingbuild-artifactjob's checkout/setup-node/npm ci untouched (not flagged, out of this fix's scope). Verification:actionlint(downloaded v1.7.12 binary) — 0 errors on all 4 workflow files (aside from an expected false-positive on the32gb-ramcustom self-hosted label, which actionlint can't know about);bash -non all 5 changed/touched shell scripts — all parse;npx eslint .— 0 errors;npm run build(incl. Obsidian 1.11.0 compat typecheck) — clean;npx vitest run— 527 passed. Not yet re-verified against real CI/SonarCloud (push pending). -
Real-provider E2E Phase 2 (multi-run isolation): added
scripts/e2e-namespace.sh(single canonicale2e/pr/<n>/<provider>/run-<id>-<attempt>/e2e/branch/<sanitized-id>/<provider>/ run-<id>-<attempt>identity generator, sourced by every other layer — no branch-naming logic duplicated anywhere else),scripts/e2e-namespace-cleanup.sh(layer 2: deletes a whole PR/branch namespace),scripts/e2e-janitor.sh(layer 3: TTL sweep, default 24h, of any leftovere2e/**branch, genericgit for-each-ref/push --delete, tolerant of already-deleted refs — no Node-based sweeper reintroduced). Removede2e-harness.sh's oldsweepsubcommand (superseded by the janitor) and its ad hocgfs-e2e-<provider>-<run>naming.ci.yml'sprovider-e2ejob now setsE2E_WORKDIRto$RUNNER_TEMP/git-files-sync-e2e/<run-id>/<run-attempt>/<provider>(was a sharede2e-<provider>dir), passesE2E_PR_NUMBER/E2E_SOURCE_BRANCHthrough forprovision, and carries a per-source/providerconcurrencygroup (e2e-pr-<n>-<provider>/e2e-branch-<branch>-<provider>,cancel-in-progress: true) so a repeated push/rerun cancels its own predecessor instead of both running. Added.github/workflows/e2e-pr-cleanup.yml(pull_request_target: [closed], noref:override on checkout so it only ever runs this repo's own trusted code/secrets, never the closing PR's branch) ande2e-branch-cleanup.yml(deleteevent) — both share the same concurrency-group naming asprovider-e2ewithcancel-in-progress: falseso cleanup queues behind rather than races an active run. Added.github/workflows/e2e-janitor.yml(schedule, every 6h, plusworkflow_dispatch). Rewrotedocs/testing/real-provider-e2e.md's "Isolation model" section (namespace scheme, concurrency/cancellation semantics, 3-layer cleanup hierarchy with a Mermaid diagram, self-hosted workdir isolation) and updated Layout/CI/Cleanup/Known-gaps to match. Verification:npx eslint .— 0 errors;npm run build(incl. Obsidian 1.11.0 compat typecheck) — clean;npx vitest run— 527 passed;python3 -c yaml.safe_load(...)on all 4 touched/new workflow YAML files — all parse;bash -non all 4 shell scripts — all parse; functional dry-runs against throwaway local git repos (not the real sandboxes) fore2e_test_branch/e2e_branch_idcollision resolution (feature/foo-barvsfeature-foo/barhash to different identities), the janitor's TTL sweep (old branch deleted, recent branch and an unrelatedfeature/keep-mebranch both left untouched), ande2e-namespace-cleanup.sh's prefix match (e2e/pr/123/**matches only that PR's two provider branches, not PR 456 or the branch-only namespace); real end-to-end run against a live local Gitea sandbox (npm run test:e2e -- --provider gitea) with the new harness/namespace code — 14/14 E2E tests passed including a real Docker provision/seed/cleanup cycle; confirmedE2E_PROVIDER=github scripts/e2e-harness.sh provisionstill hard-fails on missingE2E_GITHUB_OWNER(never a silent skip) with the new identity plumbing in place. Not yet exercised against live GitHub/GitLab sandboxes or the real self-hosted runner fleet from this checkout (no credentials/runner access here) — seedocs/testing/real-provider-e2e.md's "Known gaps". -
Real-provider E2E: pushed to
origin/test/real-provider-e2e, real CI run againstfirstsun-dev/git-files-sync's self-hosted fleet (run 31666859288) fully green:E2E / github(3m15s) andE2E / gitlab(3m54s) both passed for real against live sandboxes,E2E / github+gitlab+giteagate, and the full downstreamCI(lint, test Node 22/24, package, build/release) all green. Getting there took 3 fix-and-repush rounds off real CI failures the local-only verification hadn't caught: (1) the generatedGitVerifier's git calls had noGIT_ASKPASS/GIT_TERMINAL_PROMPTin the separate vitest-step process — fixed by persisting them (paths/flags only, not the token itself) intoe2e.env; (2) gitea provisioning timed out on127.0.0.1:<host-port>— this runner fleet is itself a sibling container of the Docker daemon, so a published host port isn't reachable from it; switched to the container's own bridge IP; (3) that same curl call could hang indefinitely with no--max-time, silently blowing past the health-check loop's own retry budget — added--max-timeeverywhere and a retry-with-backoff ondocker inspectreturning an empty IP. Gitea leg then temporarily disabled in CI per user request (still passes locally) — see Outstanding Items. -
Real-provider E2E Phase 1 (Shell/Git harness rewrite): replaced the Node-based
e2e/provision/e2e/verifier/e2e/providers/e2e/shim/{obsidian-request-url,window-timers}/scripts/run-e2e*.mjs(fetch/globalThis/node:child_process/node:crypto in committed.ts— the exact APIsdocs/obsidian-scanner-audit.mdflagged) withscripts/e2e-harness.sh(provision/seed/verify/cleanup/sweep — Shell + Git CLI:git push <sha>:refs/heads/<branch>for GitHub/GitLab branch isolation, plaindocker/curlfor Gitea's disposable container+repo,GIT_ASKPASSgenerated per-run under$RUNNER_TEMP/$E2E_WORKDIR, never persisted) plusscripts/run-e2e.sh(local orchestration wrapper). Node-only glue the suites still need at runtime (realrequestUrlshim,windowtimer alias, a git-CLI-backed verifier) is generated byprovisioninto$E2E_RUNTIME_DIRand loaded via runtime-computed dynamicimport()— never committed — soe2e/**/*.tswent back intotsconfig.json'sinclude/eslint.config.mts's scope clean. Ported all 4 suites (github/gitlab/gitea/sync-manager) to the newSyncManager.pushFilesAPI and the generated verifier.npx eslint .— 0 errors;npm run build— clean;npx vitest run— 527 passed; real end-to-end run against a live local Gitea sandbox (npm run test:e2e -- --provider gitea) — 14/14 E2E tests passed (gitea contract suite + SyncManager suite), including a real Docker container provision/seed/cleanup cycle. GitHub/GitLab E2E legs are written and typecheck/lint clean but weren't run live (no sandbox credentials in this environment) — same known gap the pre-Phase-1 harness had, documented indocs/testing/real-provider-e2e.md's "Known gaps". Self-audit ofdocs/obsidian-scanner-audit.md's grep method against the new tree: zero hits forfetch/globalThis/node:crypto/node:child_process/node:util/bare-timers ine2e/**orsrc/**. -
Real-provider E2E Phase 0 reconcile: merged
origin/main(scanner-driven E2E removal, v1.5.8) intotest/real-provider-e2e-work, keeping the olde2e/**tree temporarily (addede2e/**/vitest.e2e.config.tstoeslint.config.mtsglobalIgnoresas an interim measure — not intsconfig.jsonincludeeither, both to be resolved for real by the Phase 1 harness rewrite), then mergedorigin/claude/unify-push-pull-pipeline(new unifiedSyncManager.pushFilesAPI) cleanly (disjoint file sets, onlypackage-lock.jsonauto-merged).npx eslint .— 0 errors;npm run build(incl. Obsidian 1.11.0 compat typecheck) — clean;npx vitest run— 527 tests passed. -
fix(sync): ensure parent dirs exist when reverting file moves(issue #94): extractedensureParentDirs()tosrc/utils/vault-path.tsand called it before rename in bothrevertMoveandrevertMoveGroup, fixing "folder does not exist" error when reverting moves to deleted parent folders.npx eslint .— 0 errors;npm run build— clean;npx vitest run— 502 tests passed. -
fix(gitlab): fix sha/revision semantics for optimistic locking(issue #101, PR #113, merged):GitFile.shanow consistently represents blob identity across providers; addedGitFile.revisionfor provider-specific write control.
Full history of completed features (feat-001 through feat-024) archived to archive/2026-07.md. August work archived to archive/2026-08.md.