Task Breakdown — github-storage LFS + dual repo modes (EW-644)
Spec: ./spec.md · Plan: ./plan.md · Status: Draft · Updated: 2026-05-21
Tasks are numbered T1…Tn. Each commit on the PR references its T-IDs in the body.
Phase 1 — Contract additions (1 commit)
- T1. Add optional
workId?: stringtoStoragePutInputinpackages/plugin/src/contracts/capabilities/storage.interface.ts. JSDoc the meaning and "ignored by backends that don't need it" semantics. - T2. Rebuild
@ever-works/plugindeclarations and confirm no consumer breaks (pnpm build --filter @ever-works/plugin && pnpm type-check).
Phase 2 — Plugin core: helpers and transports (1 commit)
- T3. New
packages/plugins/github-storage/src/lfs-pointer.ts:formatPointer(oid, size)returns the 3-line pointer payload;parsePointer(content)returns{oid, size}(used bygetObjectto detect pointer files). Vitest unit covers happy path + malformed input. - T4. New
lfs-batch.ts:lfsBatch({owner, repo, token, oid, size, operation})POSTs tohttps://github.com/<owner>/<repo>.git/info/lfs/objects/batchand returns{href, header, alreadyExists}.lfsUpload({href, headers, body})PUTs the bytes. Uses Nodefetch(Node 22 globalThis). Vitest covers: happy path, already-exists response (actions: {}), error responses. - T5. New
transport/contents-api.ts:class ContentsApiTransport implements TransportwithcommitFile({owner, repo, branch, path, content, message, sha?}). Pulls the existing Octokit logic out ofgithub-storage.plugin.tsverbatim. Vitest exercises 404→create vs sha→update. - T6. New
transport/clone-and-push.ts:class CloneAndPushTransport implements Transport. Uses@ever-works/plugin/gitGitOperationsto clone shallowly into a tmpdir, write the file, commit, push, clean up. Vitest mocksGitOperations. - T7. New
transport/git-cli.ts:class GitCliTransport implements Transport. Usesexecato rungit clone --depth 1,git lfs install --local,git lfs track,git commit,git push. Vitest mocksexecaand asserts argv shape. - T8. New
work-repo-resolver.ts:interface WorkRepoResolver { resolve(workId: string): Promise<{owner: string; repo: string; branch: string; token: string}> }. Plugin reads it from(context as any).workRepoResolver.
Phase 3 — Plugin core: orchestrator (1 commit)
- T9. Rewrite
github-storage.plugin.ts:- Read full settings (mode, transport, lfs*, owner/repo/branch/pathPrefix, token) from env + settings store.
- In
putObject({buffer, filename, ownerId, workId}):- Resolve destination via
mode. Ifdata-repo, requireworkId; otherwise resolve from settings. - Compute
oid,size,ext,path = <pathPrefix>/<ownerId>/<oid><ext>. - Pick
transport: explicit value, orauto→clone-and-pushfordata-repo,contents-apiforseparate-repo. - If
lfsEnabled:await lfsBatch(...)for upload action.- If
actions.upload.hrefpresent,await lfsUpload(...). - Ensure
.gitattributesline exists forpathPrefix(read-then-write via the chosen transport, idempotent). - Write pointer via transport.
- Else: write raw bytes via transport (same shape as today).
- Resolve destination via
- T10.
getObject: if file content matches the pointer regex, fetch via LFS (use the batch API withoperation: download); otherwise base64-decode the file content as today. - T11.
deleteObject: unchanged for direct blobs. For LFS pointer files, delete the pointer commit and best-effort delete the LFS object (best-effort because the LFS purge endpoint is not always available; log a warning). - T12. Update
package.jsoneverworks.pluginblock: bump version to1.1.0, add new env vars, add"lfs"tocapabilities. - T13. Update
README.mdfor the plugin: new modes, LFS, transport choices, migration note from §8 of the spec.
Phase 4 — API uploads integration (1 commit)
- T14.
apps/api/src/uploads/work-repo-resolver.service.ts: NestJS service implementingWorkRepoResolver. Resolves owner/repo/branch fromWork.owner, the Work's storage metadata, and the user's GitHub OAuth token from theIntegrationAccounttable. Includes a small TTL cache to avoid hammering the DB on every upload. - T15.
storage-backend.factory.ts: whenwanted === 'github-storage', attach the resolver to the stubPluginContextbeforeonLoad(). - T16.
uploads.controller.ts: accept optional?workId=(validated UUID) on the dashboard upload route. Anonymous route stays unchanged. - T17.
uploads.service.ts: threadworkIdintoStoragePutInput. - T18. Jest spec for
storage-backend.factory.spec.ts: ensures the resolver gets attached only forgithub-storage. Other backends keep working with no extra context fields. - T19. Jest spec for
uploads.service.workid.spec.ts: posting an upload withworkIdreaches the mocked backend withworkIdset; withoutworkId, backend receivesundefined.
Phase 5 — Web UI (1 commit)
- T20. Extract
OwnerFilter(and the per-org repo fetch) fromRepositorySelector.tsxinto a small reusable hook:useGitHubOwners(providerId)returning{ owners, loading, refresh }. The existing component keeps working; the hook is shared. - T21. Add
GithubOwnerWidget.tsx: usesuseGitHubOwners, renders the same Select asOwnerFilter, persists the owner login as the field value. - T22. Add
GithubRepoWidget.tsx: lists repos for the chosen owner, persists the fullowner/repostring. Default selection: if the user already has a Work, suggest its data repo. - T23.
PluginSettingsField.tsx: routex-widget === 'github-owner' | 'github-repo'to the new widgets. - T24. i18n strings for the new UI copy (mode labels, LFS toggle, transport options) under
apps/web/messages/en.jsonand the existing locale set.
Phase 6 — End-to-end tests (1 commit)
- T25. Playwright spec
apps/web/e2e/github-storage-settings.spec.ts:- Open
/dashboard/plugins/github-storage. - Toggle mode
separate-repo→data-repoand assert owner/repo/branch/pathPrefix fields disappear. - Toggle LFS on/off and assert the
lfsTransportfield shows/hides. - Save, reload, assert persistence.
- Open
- T26. Playwright spec
apps/web/e2e/github-storage-upload.spec.ts:- Mock the Octokit + LFS Batch API endpoints with
page.route(...). - Post a small upload via the dashboard upload form.
- Assert: (a) LFS batch POST happened, (b) blob PUT to signed URL happened, (c) pointer commit PUT happened, (d) the file appears in the uploads list under the expected key.
- Mock the Octokit + LFS Batch API endpoints with
Phase 7 — PR + bot loop (NN #14 / #18 / #19)
- T27. Push branch
session/github-storage-lfs-ew644, open PR. Title:feat(github-storage): LFS + dual repo modes (EW-644). Body links spec/plan/tasks + JIRA. - T28. Poll
gh pr checksuntil CI green. Triage failures, fix, push, re-poll. - T29. Poll
gh pr view --commentsfor CodeRabbit / Greptile / Codex / Copilot review. Apply P0/P1/P2 fixes or annotate with rationale. Re-poll until the second pass is clean. - T30. Report PR URL back to the operator with a one-line summary. Do not merge — the operator drives merge per NN #18.