heyquilt 0.3.138 → 0.3.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/RELEASES.md +10 -0
  2. package/bin/quilt.js +7 -143
  3. package/package.json +1 -1
  4. package/src/account.js +0 -29
  5. package/src/api/memory-store.js +0 -29
  6. package/src/api/routes/teams.js +0 -23
  7. package/src/api/server.js +5 -19
  8. package/src/api/supabase-store.js +0 -52
  9. package/src/branch-view.js +6 -131
  10. package/src/control.js +2 -33
  11. package/src/duties.js +1 -1
  12. package/src/github-commit.js +3 -315
  13. package/src/mcp.js +8 -157
  14. package/src/relay-ask.js +3 -66
  15. package/src/relay-cluster.js +3 -5
  16. package/src/relay-mcp.js +6 -418
  17. package/src/relay-prune.js +6 -145
  18. package/src/relay-turn-sync.js +1 -1
  19. package/src/server.js +33 -1331
  20. package/src/session-follow.js +18 -5
  21. package/src/session.js +58 -3526
  22. package/src/status.js +2 -58
  23. package/src/turn-sync.js +15 -2
  24. package/src/ui/changes.js +5 -14
  25. package/src/ui/chat.js +0 -1
  26. package/src/ui/common.js +0 -2
  27. package/src/ui/git-rule.js +2 -30
  28. package/src/ui/home.js +3 -76
  29. package/src/ui/session.js +34 -528
  30. package/src/ui/tree.js +2 -8
  31. package/src/ui/workspaces.js +2 -122
  32. package/src/ui-server.js +1 -38
  33. package/src/api/routes/github.js +0 -380
  34. package/src/branches.js +0 -194
  35. package/src/clash.js +0 -128
  36. package/src/commit-request.js +0 -59
  37. package/src/commit.js +0 -213
  38. package/src/git-hook.js +0 -48
  39. package/src/gitignore.js +0 -49
  40. package/src/gitstate.js +0 -1069
  41. package/src/relay-ci.js +0 -111
  42. package/src/relay-clash.js +0 -214
  43. package/src/relay-commit.js +0 -923
  44. package/src/relay-filekey.js +0 -46
  45. package/src/relay-git.js +0 -274
  46. package/src/relay-githttp.js +0 -166
  47. package/src/relay-github.js +0 -109
  48. package/src/relay-outbound.js +0 -1295
  49. package/src/relay-repo-moves.js +0 -43
  50. package/src/relay-sync.js +0 -795
  51. package/src/relay-upstream.js +0 -889
  52. package/src/sync-words.js +0 -42
  53. package/src/upstream.js +0 -192
package/RELEASES.md CHANGED
@@ -11,6 +11,16 @@ Format: `## <version> — <YYYY-MM-DD>`, an optional one-line summary, then bull
11
11
  Lead each bullet with a short bold phrase. Inline `code` and **bold** are rendered;
12
12
  nothing else is.
13
13
 
14
+ ## 0.3.141 — 2026-11-01
15
+
16
+ - **A new, empty folder joining a session that is already in Seam takes the square as it is.** Its first files come from the session's old copy of the branch, and before, a folder without git could start following its square while that old copy was still being written to it, so every older file went into the square as its own edits (an agent's new folder put 166 stale files into main's square). Now the folder's own syncing stops before it follows the square, and nothing it got from the session is ever counted as its edits.
17
+
18
+ ## 0.3.140 — 2026-11-01
19
+
20
+ 0.3.139 was tagged but never published; everything meant for it is here.
21
+
22
+ - **Quilt no longer has git mode.** Every session's code lives in Seam: sessions are Seam repositories, committed and merged there, and Quilt no longer syncs with GitHub, pushes, commits for you or opens merge tasks. A session with a GitHub repository that never moved into Seam keeps its chat, tasks, agent feed and audit trail (its files stay as they were and are no longer shared live), and its owner can still make it a quilt from Settings, Session.
23
+
14
24
  ## 0.3.138 — 2026-11-01
15
25
 
16
26
  0.3.137 was tagged but never published; everything meant for it is here.
package/bin/quilt.js CHANGED
@@ -28,7 +28,7 @@ Usage:
28
28
  quilt sync [--dir <folder>] Agents in a sandbox: sync this folder with the session once and exit
29
29
  (needs QUILT_AGENT_KEY; run it at the start and end of every turn).
30
30
  With the key, status, messages, say, tasks, task, history, claim,
31
- release, focus, commits, commit-request, tool and mcp work too
31
+ release, focus, tool and mcp work too
32
32
  quilt setup Connect Claude Code / Cursor / others via MCP
33
33
  quilt status Show collaborators, claims, activity, chat
34
34
  quilt history [path] [--by name] [--since 2h] [--task id] [--diff] [-n 30]
@@ -47,16 +47,11 @@ Usage:
47
47
  NAME's base URL (STRIPE_BASE_URL), and its requests go through
48
48
  Quilt's vault; it never sees a value
49
49
  quilt commit <message> In a session that is a quilt: commit the square this folder follows
50
- quilt commit-request <message> [--files a,b] [--task id]
51
- Ask a person to commit for you (when agents may not push here)
52
- quilt commits Open commit requests and recent commits
53
- quilt github Whether agents can push here through GitHub, and the link that connects it
54
50
  quilt focus <what you're doing> Tell collaborators what you're working on
55
51
  quilt claim <path|glob> [reason] Mark files as yours for now
56
52
  quilt release <path|glob|*> Release a claim
57
53
  quilt chat-link [name] [--minutes N] Owner: a link for a chat-only AI (ChatGPT, claude.ai, Grok); 10 minutes
58
54
  quilt chat-link extend <name> <minutes> Owner: keep a chat link working for that long from now
59
- quilt github-token [--clear] Owner: a read-only GitHub token (read from stdin) so the relay brings in commits while everyone's offline
60
55
  quilt invite Print this session's invite code
61
56
  quilt workspace list|files|get|put|write|mkdir|mv|rm The workspace library (quilt workspace for help)
62
57
  quilt stop Shut down everything quilt is running (relay, app, syncs)
@@ -110,13 +105,7 @@ async function main () {
110
105
  case 'wake': return wakeCmd()
111
106
  case 'get': return getFile()
112
107
  case 'commit': if (!argv.join(' ').trim()) fail('Usage: quilt commit <message>'); return simple('/square-commit', { message: argv.join(' ') }, (r) => r.text)
113
- case 'commit-request': return commitRequest()
114
- case 'commits': return commits()
115
- case 'github': return githubStatusCmd()
116
- case 'git-token': return gitToken()
117
108
  case 'run': return runCmd()
118
- case 'git-credential': return gitCredential()
119
- case 'git-pre-push': return gitPrePush()
120
109
  case 'chat': return chat()
121
110
  case 'focus': return simple('/focus', { text: argv.join(' ') }, () => 'focus updated')
122
111
  case 'claim': return simple('/claim', { pattern: argv[0], note: argv.slice(1).join(' ') }, (r) =>
@@ -132,20 +121,6 @@ async function main () {
132
121
  const name = argv.filter((a, k) => !(i >= 0 && (k === i || k === i + 1))).join(' ')
133
122
  return simple('/chat-link', { name, minutes }, (r) => `Chat link for ${r.name}, until ${new Date(r.expiresAt).toLocaleTimeString()}:\n\n ${r.url}\n\nPaste it into ChatGPT, claude.ai or Grok with "Open this link and follow it to join our Quilt session."\nAnyone with it can act as ${r.name}. Extend it with \`quilt chat-link extend ${r.name} <minutes>\`; once it runs out, make a new one.`)
134
123
  }
135
- case 'github-token': {
136
- // Read from stdin, never the command line (where shell history and ps would keep it).
137
- if (argv[0] === '--clear') return simple('/github-token', { token: '' }, () => 'The relay no longer has a GitHub token for this session.')
138
- if (argv.length) fail('usage: quilt github-token [--clear] (the token is read from stdin: pbpaste | quilt github-token)')
139
- if (process.stdin.isTTY) process.stderr.write('Paste a read-only GitHub token (contents: read) and press Enter: ')
140
- const token = await new Promise((resolve) => {
141
- let buf = ''
142
- process.stdin.setEncoding('utf8')
143
- process.stdin.on('data', (d) => { buf += d; if (process.stdin.isTTY && buf.includes('\n')) { process.stdin.pause(); resolve(buf) } })
144
- process.stdin.on('end', () => resolve(buf))
145
- })
146
- if (!token.trim()) fail('no token given')
147
- return simple('/github-token', { token: token.trim() }, () => 'The relay has the token: it brings in commits from your private repository while nobody\'s folder is online.')
148
- }
149
124
  case 'session': return sessionCmd()
150
125
  case 'invite': return invite()
151
126
  case 'workspace': case 'workspaces': return workspaceCmd()
@@ -478,6 +453,12 @@ async function withAgentKey () {
478
453
  return true
479
454
  }
480
455
 
456
+ /** All of stdin, for `quilt tool <name> -` (the tool's arguments as JSON). */
457
+ const readStdin = () => new Promise((resolve) => {
458
+ let s = ''
459
+ process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { s += c }); process.stdin.on('end', () => resolve(s)); process.stdin.on('error', () => resolve(s))
460
+ })
461
+
481
462
  /** The sandbox's QUILT_AGENT_KEY (src/ui/agent-env-key.js), or null; a malformed one stops here. */
482
463
  async function envKey () {
483
464
  const { agentEnvKeyFromEnv } = await import('../src/ui/agent-env-key.js')
@@ -779,99 +760,6 @@ async function wakeCmd () {
779
760
  return simple('/wake', { args }, (r) => r.text)
780
761
  }
781
762
 
782
- async function commitRequest () {
783
- const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { files: { type: 'string' }, task: { type: 'string' } } })
784
- const files = values.files ? values.files.split(',').map((f) => f.trim()).filter(Boolean) : undefined
785
- await simple('/commit-request', { message: positionals.join(' '), files, task: values.task }, (r) =>
786
- `asked for a commit [${r.id}] of ${r.files.length} file${r.files.length === 1 ? '' : 's'}: ${r.files.slice(0, 12).join(', ')}${r.files.length > 12 ? ', …' : ''}\n${r.committer}`)
787
- }
788
-
789
- // git in an agent's folder runs these (installGitHelpers, gitstate.js); `--dir` is the folder, whose
790
- // running session answers. They never print anything but what git expects, and never fail a git
791
- // command when Quilt isn't running: git then behaves as it would without Quilt. Nor do they make git
792
- // wait: a session that doesn't answer within HELPER_WAIT is taken as no session, and the whole helper
793
- // exits by HELPER_LIMIT whatever is still pending (the env overrides are for tests).
794
- const HELPER_WAIT = Number(process.env.QUILT_HELPER_WAIT_MS) || 4000
795
- const HELPER_LIMIT = Number(process.env.QUILT_HELPER_LIMIT_MS) || 10000
796
- function helperDeadline () { setTimeout(() => process.exit(0), HELPER_LIMIT).unref() }
797
- function helperDir () {
798
- const i = argv.indexOf('--dir')
799
- const dir = i >= 0 ? argv[i + 1] : process.cwd()
800
- if (i >= 0) argv.splice(i, 2)
801
- return dir
802
- }
803
- async function helperDaemon (dir) {
804
- const { findDaemon, call } = await import('../src/control.js')
805
- const d = findDaemon(dir)
806
- return d ? { d, call } : null
807
- }
808
- // git closes stdin after what it sends; one that stays open is read for HELPER_WAIT at most.
809
- const readStdin = () => new Promise((resolve) => {
810
- let s = ''
811
- const done = () => { clearTimeout(t); process.stdin.pause(); resolve(s) }
812
- const t = setTimeout(done, HELPER_WAIT)
813
- process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { s += c }); process.stdin.on('end', done); process.stdin.on('error', done)
814
- })
815
-
816
- /**
817
- * git's credential helper protocol: `get` answers with the session's token for the host; store and erase
818
- * are nothing to do. `--fallback` (a person's folder, git for everyone): when no Quilt session answers
819
- * (the app is closed) or it has no token, the person's own helpers are asked instead, as git would have
820
- * without Quilt: `git credential fill` from outside the folder, so only their global and system config count.
821
- */
822
- async function gitCredential () {
823
- const dir = helperDir()
824
- const fallback = argv.includes('--fallback')
825
- if (fallback) argv.splice(argv.indexOf('--fallback'), 1)
826
- if (argv[0] !== 'get') return
827
- helperDeadline()
828
- const input = await readStdin()
829
- const fields = Object.fromEntries(input.split('\n').filter((l) => l.includes('=')).map((l) => { const i = l.indexOf('='); return [l.slice(0, i), l.slice(i + 1)] }))
830
- try {
831
- const h = await helperDaemon(dir)
832
- if (h) {
833
- const r = await h.call(h.d, 'POST', '/git/credential', { host: fields.host || '', protocol: fields.protocol || '' }, { timeoutMs: HELPER_WAIT })
834
- if (r && r.username && r.password) return process.stdout.write(`username=${r.username}\npassword=${r.password}\n`)
835
- }
836
- } catch {}
837
- if (fallback) await ownCredential(input)
838
- }
839
-
840
- /** The person's own credentials for what git asked (`input`, git's credential fields), or nothing: never a prompt. */
841
- async function ownCredential (input) {
842
- const os = await import('node:os')
843
- const { execFile } = await import('node:child_process')
844
- const away = os.tmpdir()
845
- const out = await new Promise((resolve) => {
846
- const c = execFile('git', ['credential', 'fill'], { cwd: away, encoding: 'utf8', timeout: Math.max(1000, HELPER_LIMIT - HELPER_WAIT - 1000), killSignal: 'SIGKILL', env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '', SSH_ASKPASS: '', GIT_CEILING_DIRECTORIES: path.dirname(away) } }, (err, stdout) => resolve(err ? '' : stdout))
847
- c.stdin.end(input.endsWith('\n\n') ? input : `${input.replace(/\n*$/, '')}\n\n`)
848
- })
849
- const lines = out.split('\n').filter((l) => /^(username|password)=/.test(l))
850
- if (lines.length === 2) process.stdout.write(`${lines.join('\n')}\n`)
851
- }
852
-
853
- /** The pre-push hook: refs on stdin (local ref, local sha, remote ref, remote sha), the remote's name and URL as arguments. Exits 1 with the reason when the push may not go. */
854
- async function gitPrePush () {
855
- const dir = helperDir()
856
- const [remote = 'origin'] = argv
857
- helperDeadline()
858
- const input = await readStdin()
859
- const refs = input.split('\n').filter(Boolean).map((l) => { const [localRef, localSha, remoteRef, remoteSha] = l.trim().split(/\s+/); return { localRef, localSha, remoteRef, remoteSha } })
860
- try {
861
- const h = await helperDaemon(dir)
862
- if (!h) return
863
- const r = await h.call(h.d, 'POST', '/git/push-check', { remote, refs }, { timeoutMs: HELPER_WAIT })
864
- if (r && r.ok === false) { process.stderr.write(`Quilt: ${r.why || 'this push is not allowed here'}\n`); process.exitCode = 1 }
865
- } catch {}
866
- }
867
-
868
- /** A short-lived token for this folder's repository (agents), for gh or GitHub's API. */
869
- /** `quilt git-token` no longer prints a token (a value in a terminal is a value in an AI's context). */
870
- async function gitToken () {
871
- const { GIT_TOKEN_GONE } = await import('../src/secret-run.js')
872
- fail(GIT_TOKEN_GONE)
873
- }
874
-
875
763
  /** `quilt run -- <command>`: the command gets its secrets' stand-ins and gateways (src/secret-run.js). */
876
764
  async function runCmd () {
877
765
  const { RUN_USAGE, runSource, runWithSecrets } = await import('../src/secret-run.js')
@@ -885,30 +773,6 @@ async function runCmd () {
885
773
  try { process.exitCode = await runWithSecrets({ command, source }) } catch (err) { fail(err.message) }
886
774
  }
887
775
 
888
- async function githubStatusCmd () {
889
- const { d, call } = await daemonOrFail()
890
- const st = await call(d, 'GET', '/status')
891
- const mine = (st.branches || []).find((b) => st.git && b.name === st.git.key) || (st.branches || []).find((b) => b.default)
892
- const g = mine && mine.relay && mine.relay.github
893
- const repo = mine && mine.relay && mine.relay.repo
894
- if (!repo) return console.log('This session does not follow a GitHub repository (no member\'s folder is a clone of one), so there is nothing to connect.')
895
- const words = { ok: `Git is on for everyone here, through @${g && g.login}'s connection: people and agents push with git, nothing to set up`, 'not-connected': 'not connected: nobody in the session has connected GitHub yet', 'not-installed': 'Quilt is not installed on this repository on GitHub yet', 'no-access': `no connected GitHub account (@${g && g.login}) may write to it`, 'not-github': 'not on GitHub' }
896
- console.log(`${repo}: ${g ? words[g.state] || g.state : 'not checked yet'}`)
897
- if (g && g.state === 'ok') return
898
- const { readAccount } = await import('../src/account.js')
899
- const token = readAccount()?.token
900
- if (!token) return console.log('Sign in (quilt login), then run this again for the link that connects GitHub.')
901
- const { githubConnectUrl } = await import('../src/account.js')
902
- try { console.log(`Connect it (one click, once, by someone whose GitHub account can write to the repository):\n ${await githubConnectUrl({ token, repo })}`) } catch (err) { console.log(`Could not get the link: ${err.message}`) }
903
- }
904
-
905
- async function commits () {
906
- const { d, call } = await daemonOrFail()
907
- const { describeCommitRequests } = await import('../src/commit.js')
908
- const c = await call(d, 'GET', '/commits')
909
- console.log(plain(describeCommitRequests(c, { canCommit: !!c.canCommit })))
910
- }
911
-
912
776
  async function getFile () {
913
777
  if (!argv[0]) fail('usage: quilt get <message-id> [dest]')
914
778
  await simple('/get', { id: argv[0], dest: argv[1] ? path.resolve(argv[1]) : undefined }, (r) => `saved to ${r.path}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyquilt",
3
- "version": "0.3.138",
3
+ "version": "0.3.141",
4
4
  "description": "Real-time, tool-agnostic pair vibe coding: sync a project folder live between collaborators using any AI coding tool.",
5
5
  "author": {
6
6
  "name": "Daniel Carmichael",
package/src/account.js CHANGED
@@ -224,35 +224,6 @@ export async function removeSandboxKey ({ token, id, keyId, api = apiUrl(), fetc
224
224
  return call(fetchImpl, api, 'DELETE', sandbox$(id, `/${encodeURIComponent(keyId)}`), null, token)
225
225
  }
226
226
 
227
- /** This account's GitHub connection (Quilt's GitHub App): { available, connected, login? }. */
228
- export async function githubStatus ({ token, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
229
- return call(fetchImpl, api, 'GET', '/v1/me/github', null, token)
230
- }
231
-
232
- /** Where to send this person to connect GitHub (install Quilt's app on their repositories). */
233
- export async function githubConnectUrl ({ token, repo = '', scope = '', id = '', api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
234
- const r = await call(fetchImpl, api, 'POST', '/v1/github/connect', { ...(repo ? { repo } : {}), ...(scope ? { scope, id } : {}) }, token)
235
- if (!r.url) throw new Error(BAD_REPLY)
236
- return r.url
237
- }
238
-
239
- /** A workspace's GitHub (plan §2): its connections, access mode and repositories, and its org's switch. */
240
- export async function workspaceGithub ({ token, id, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
241
- return call(fetchImpl, api, 'GET', `/v1/workspaces/${encodeURIComponent(id)}/github`, null, token)
242
- }
243
- export async function setWorkspaceGithub ({ token, id, access, repos, agentCommits, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
244
- // Only what is given changes: the access mode (with its repositories) and/or what agents may commit ('' for each session).
245
- const body = { ...(access !== undefined ? { access, repos } : {}), ...(agentCommits !== undefined ? { agentCommits } : {}) }
246
- return call(fetchImpl, api, 'PUT', `/v1/workspaces/${encodeURIComponent(id)}/github`, body, token)
247
- }
248
- export async function removeWorkspaceGithub ({ token, id, connectedBy = '', api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
249
- return call(fetchImpl, api, 'DELETE', `/v1/workspaces/${encodeURIComponent(id)}/github/connection`, connectedBy ? { connectedBy } : {}, token)
250
- }
251
- export async function setOrgGithub ({ token, slug, requireGithub, agentCommits, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
252
- const body = { ...(requireGithub !== undefined ? { requireGithub: !!requireGithub } : {}), ...(agentCommits !== undefined ? { agentCommits } : {}) }
253
- return call(fetchImpl, api, 'PUT', `/v1/orgs/${encodeURIComponent(slug)}/github`, body, token)
254
- }
255
-
256
227
  // Access types, grants and session invites, as this computer's account (see
257
228
  // docs/superpowers/specs/2026-10-02-access-types-and-invites-design.md). Each throws with
258
229
  // .status when the API says no (401 once the token is revoked).
@@ -399,35 +399,6 @@ export function createMemoryStore ({ now = Date.now } = {}) {
399
399
  addAuthSession (userId, s) { authSessions.set(s.id, { userId, createdAt: now(), lastActiveAt: now(), userAgent: '', ip: '', ...s }) },
400
400
  async authSessions (userId) { return all(authSessions, (s) => s.userId === userId).sort((a, b) => b.lastActiveAt - a.lastActiveAt).map(({ userId: _, ...s }) => ({ ...s })) },
401
401
  async endAuthSession (userId, id) { const s = authSessions.get(id); if (!s || s.userId !== userId) return false; authSessions.delete(id); return true },
402
- async githubLink (userId) { const g = githubLinks.get(userId); return g ? { ...g } : null },
403
- async setGithubLink (userId, { githubId, login }) { githubLinks.set(userId, { githubId, login, linkedAt: now() }) },
404
- async deleteGithubLink (userId) { githubLinks.delete(userId) },
405
- // GitHub connected for a scope (github_connections): an org, a workspace or a person; newest first.
406
- async githubConnections ({ scope, scopeId }) { return all(githubConnections, (c) => c.scope === scope && c.scopeId === scopeId).sort((a, b) => b.createdAt - a.createdAt).map(copy) },
407
- async addGithubConnection ({ scope, scopeId, connectedBy, installationId = null }) { githubConnections.set(`${scope}\0${scopeId}\0${connectedBy}`, { scope, scopeId, connectedBy, installationId, createdAt: now() }) },
408
- async removeGithubConnection ({ scope, scopeId, connectedBy }) { githubConnections.delete(`${scope}\0${scopeId}\0${connectedBy}`) },
409
- // A workspace's access mode (workspace_github): 'open', or 'repos' with the repositories it allows.
410
- async workspaceGithub (workspaceId) { const w = workspaceGithub.get(workspaceId); return w ? { access: w.access, repos: [...w.repos], agentCommits: w.agentCommits || '' } : { access: 'open', repos: [], agentCommits: '' } },
411
- // Only what is given changes: { access, repos } and/or { agentCommits } ('' for each session to decide).
412
- async setWorkspaceGithub (workspaceId, { access, repos, agentCommits }) {
413
- const w = workspaceGithub.get(workspaceId) || { access: 'open', repos: [], agentCommits: '' }
414
- workspaceGithub.set(workspaceId, { access: access ?? w.access, repos: repos ? [...repos] : w.repos, agentCommits: agentCommits ?? w.agentCommits ?? '', updatedAt: now() })
415
- },
416
- // An org's GitHub switch (org_github): whether it requires GitHub.
417
- async teamGithub (teamId) { const r = teamGithub.get(teamId); return { agentCommits: (r && r.agentCommits) || '' } },
418
- async setTeamGithub (teamId, { agentCommits }) { teamGithub.set(teamId, { agentCommits: agentCommits || '', updatedAt: now() }) },
419
- // The rules of the teams `memberId` is on: [{ teamId, name, agentCommits }], only those that set one.
420
- async teamGitRules (memberId) {
421
- return all(teamMembers, (tm) => tm.memberId === memberId)
422
- .map((tm) => ({ team: teams.get(tm.teamId), rule: teamGithub.get(tm.teamId) }))
423
- .filter((x) => x.team && x.rule && x.rule.agentCommits)
424
- .map((x) => ({ teamId: x.team.id, name: x.team.name, agentCommits: x.rule.agentCommits }))
425
- },
426
- async orgGithub (orgId) { const o = orgGithub.get(orgId); return { requireGithub: !!(o && o.requireGithub), agentCommits: (o && o.agentCommits) || '' } },
427
- async setOrgGithub (orgId, { requireGithub, agentCommits }) {
428
- const o = orgGithub.get(orgId) || { requireGithub: false, agentCommits: '' }
429
- orgGithub.set(orgId, { requireGithub: requireGithub ?? o.requireGithub, agentCommits: agentCommits ?? o.agentCommits ?? '', updatedAt: now() })
430
- },
431
402
  // The API's own settings (app_settings): a value it keeps across starts.
432
403
  async getSetting (key) { return settings.has(key) ? settings.get(key) : null },
433
404
  // Relay leases (relay_leases; mirrors the SQL functions). `key`: the relay's name for what it holds.
@@ -37,8 +37,6 @@ export function teamRoutes ({ store, user, policyChanged = null }) {
37
37
  name: t.name,
38
38
  createdAt: t.createdAt,
39
39
  access: mine.get(t.id) || null,
40
- // The team's git rule (git for everyone: '' sets nothing, off, branches or any).
41
- git: (await store.teamGithub(t.id)).agentCommits,
42
40
  members: a.can('team_members', 'r') || mine.has(t.id)
43
41
  ? (await store.listTeamMembers(t.id)).map((m) => ({ memberId: m.memberId, name: m.name, access: m.access, kind: m.kind, scopes: m.scopes || [] }))
44
42
  : null
@@ -98,27 +96,6 @@ export function teamRoutes ({ store, user, policyChanged = null }) {
98
96
  return { member: memberView(tm) }
99
97
  }],
100
98
 
101
- // The team's git rule (docs/superpowers/specs/2026-10-17-git-for-everyone.md): whether git is on for
102
- // everyone in the sessions its people own in the org's workspaces, and what agents may push there.
103
- // '' sets nothing; off, branches or any is a ceiling for the workspaces' and sessions' own choices.
104
- ['GET', /^\/v1\/orgs\/([^/]+)\/teams\/([^/]+)\/github$/, async (req, body, [slug, id]) => {
105
- const a = await orgFor(req, slug)
106
- const team = await teamIn(a, id)
107
- const mine = (await store.teamsOfMember(a.me.id)).some((x) => x.teamId === team.id)
108
- if (!a.can('teams', 'r') && !mine) throw new HttpError(404, 'no such team')
109
- return store.teamGithub(team.id)
110
- }],
111
- ['PUT', /^\/v1\/orgs\/([^/]+)\/teams\/([^/]+)\/github$/, async (req, body, [slug, id]) => {
112
- const a = await orgFor(req, slug)
113
- a.need('teams', 'u')
114
- const team = await teamIn(a, id)
115
- if (!('agentCommits' in body)) throw new HttpError(400, 'Give agentCommits: "" (the team sets nothing), off, branches or any.')
116
- const v = body.agentCommits === null ? '' : body.agentCommits
117
- if (!['', 'off', 'branches', 'any'].includes(v)) throw new HttpError(400, 'agentCommits is "" (the team sets nothing), off, branches or any.')
118
- await store.setTeamGithub(team.id, { agentCommits: v })
119
- if (policyChanged) policyChanged()
120
- return store.teamGithub(team.id)
121
- }],
122
99
 
123
100
  ['DELETE', /^\/v1\/orgs\/([^/]+)\/teams\/([^/]+)\/members\/([^/]+)$/, async (req, body, [slug, id, memberId]) => {
124
101
  const a = await orgFor(req, slug)
package/src/api/server.js CHANGED
@@ -25,8 +25,6 @@ import { relayRoutes } from './routes/relay.js'
25
25
  import { relayClusterRoutes } from './routes/relay-cluster.js'
26
26
  import { relayCluster } from './relay-cluster.js'
27
27
  import crypto from 'node:crypto'
28
- import { githubRoutes } from './routes/github.js'
29
- import { githubApp, ensureWebhookSecret, redeliverFailed } from './github-app.js'
30
28
  import { releaseSource } from './releases.js'
31
29
  import { sessionRoutes, nowRoutes, nowAsker, overviewOf } from './routes/sessions.js'
32
30
  import { notifier, NOTIFY_EVERY_MS } from './notify.js'
@@ -361,8 +359,6 @@ export function startApi ({ port = 0, host = '127.0.0.1', store, verifyUser, sit
361
359
  // The relay on many machines: where each room lives, and heavy rooms' own machines (relay-cluster.js).
362
360
  const cluster = relayCluster({ store, relayUrl: relay, relaySecret, fetch: relayFetch || githubFetch, now, log, fly: relayFly, ...(heavy ? { limits: heavy } : {}), ...(relayTargets ? { targets: relayTargets } : {}), ...clusterTiming })
363
361
  const ctx = { store, user, person, device, bearer, now, site, apiUrl: api, mailer, log, limit: limitInvites, limitSend: limitInviteSend, limitTokens, limitStarts, spendResume, resumeWindowMs: RESUME_WINDOW_MS, limitJoin, agentAuth, reportKey, limitReports, relaySecret, files, maxFileBytes, workspaceQuotaBytes, maxWorkspaceFiles }
364
- // GitHub's webhook secret: set on the app by this API at start (github-app.js), kept in the store when the environment gives none.
365
- const hookSecret = { value: (github && github.webhookSecret) || '' }
366
362
  // On several machines, one leads what lives in a machine's memory (leader.js); alone, this one does.
367
363
  const lead = leader({ store, machineId, log, ...leaderTiming })
368
364
  // Scaling down (autoscale.js): an idle machine shuts itself down cleanly when the others can carry
@@ -381,27 +377,17 @@ export function startApi ({ port = 0, host = '127.0.0.1', store, verifyUser, sit
381
377
  exit()
382
378
  }
383
379
  })
384
- const githubReady = (async () => {
385
- if (!github) return
386
- const app = githubApp(github, { fetch: githubFetch, now })
387
- await ensureWebhookSecret({ cfg: github, app, store, hook: hookSecret, log })
388
- // Asking GitHub for failed deliveries again is done once, by the leader, not by every machine.
389
- await lead.ready
390
- if (lead.leads()) await redeliverFailed({ app, log })
391
- })().catch((err) => log(`github at start: ${err.message}`))
380
+ // Sessions are quilts (Seam): nothing waits on GitHub at start.
381
+ const githubReady = Promise.resolve()
392
382
  // A person's AI on its own cloud machine (routes/ai-vm.js): off (503) without Fly set up. Only the leader runs it.
393
383
  const aiVm = aiVmRoutes({ ...ctx, relayUrl, fly, waitMs: aiVmWaitMs, spareSuspendMs: aiVmSpareSuspendMs, leads: lead.leads })
394
384
  routes.push(...aiVm.routes)
395
385
  // Email notices people turned on in the website's Settings › Notifications (notify.js): the leader sends them.
396
386
  const notices = notifier({ store, mailer, ask: nowAsker({ store, cluster, relayOn: !!(relay && relaySecret), log }), overview: (account, t) => overviewOf(store, account, 'UTC', t), now, log, site, leads: lead.leads, everyMs: notifyEveryMs })
397
387
  if (notifyEveryMs > 0) notices.start()
398
- // A team's git rule or who is on a team changed: the relay asks its sessions' rules again now (relay /github/policy).
399
- // With a room (a session put in or taken out of a workspace), only that session asks.
400
- const policyChanged = (room = '') => {
401
- if (!relay || !relaySecret) return
402
- cluster.tellRelays('/github/policy', room ? { room } : {}).catch((err) => log(`git rules to relay: ${err.message}`))
403
- }
404
- routes.push(...orgRoutes(ctx), ...memberRoutes(ctx), ...teamRoutes({ ...ctx, policyChanged }), ...inviteRoutes(ctx), ...agentRoutes({ ...ctx, passKey }), ...agentInviteRoutes({ ...ctx, workspaces }), ...joinRoutes(ctx), ...relayRoutes(ctx), ...githubRoutes({ ...ctx, github, githubFetch, relayHttp: relay, relayFetch: githubFetch, tellRelays: (path, body) => cluster.tellRelays(path, body), hookSecret, workspaces }), ...relayClusterRoutes({ ...ctx, cluster }), ...sessionRoutes(ctx), ...nowRoutes({ ...ctx, cluster, relayOn: !!(relay && relaySecret) }), ...browserRoutes(ctx), ...sessionMigrateRoutes({ ...ctx, cluster }), ...accessTypeRoutes(ctx), ...grantRoutes({ ...ctx, workspaces }), ...sessionInviteRoutes({ ...ctx, workspaces }), ...issueRoutes(ctx))
388
+ // Git rules went with git mode (R3): nothing to tell the relay.
389
+ const policyChanged = () => {}
390
+ routes.push(...orgRoutes(ctx), ...memberRoutes(ctx), ...teamRoutes({ ...ctx, policyChanged }), ...inviteRoutes(ctx), ...agentRoutes({ ...ctx, passKey }), ...agentInviteRoutes({ ...ctx, workspaces }), ...joinRoutes(ctx), ...relayRoutes(ctx), ...relayClusterRoutes({ ...ctx, cluster }), ...sessionRoutes(ctx), ...nowRoutes({ ...ctx, cluster, relayOn: !!(relay && relaySecret) }), ...browserRoutes(ctx), ...sessionMigrateRoutes({ ...ctx, cluster }), ...accessTypeRoutes(ctx), ...grantRoutes({ ...ctx, workspaces }), ...sessionInviteRoutes({ ...ctx, workspaces }), ...issueRoutes(ctx))
405
391
  // Always routed: with the flag off each answers a plain 404 of its own, so the app's
406
392
  // check at every launch isn't filed as a missing route.
407
393
  // Only these take a hosted agent's pass (workspaceReach), so they alone get the pass key.
@@ -298,58 +298,6 @@ export function createSupabaseStore ({ url, serviceKey, client }) {
298
298
  if (error) throw error
299
299
  },
300
300
 
301
- // The GitHub account a person connected (routes/github.js): { githubId, login, linkedAt } or null.
302
- async githubLink (userId) { return rowFrom(await one(db.from('github_links').select('github_id, login, linked_at').eq('user_id', userId).maybeSingle())) },
303
- async setGithubLink (userId, { githubId, login }) {
304
- await one(db.from('github_links').upsert({ user_id: userId, github_id: githubId, login, linked_at: new Date().toISOString() }, { onConflict: 'user_id' }))
305
- },
306
- async deleteGithubLink (userId) { await one(db.from('github_links').delete().eq('user_id', userId)) },
307
- // GitHub connected for a scope (github_connections), newest first.
308
- async githubConnections ({ scope, scopeId }) {
309
- const rows = await one(db.from('github_connections').select('scope, scope_id, connected_by, installation_id, created_at').eq('scope', scope).eq('scope_id', scopeId).order('created_at', { ascending: false }))
310
- return rows.map((r) => ({ scope: r.scope, scopeId: r.scope_id, connectedBy: r.connected_by, installationId: r.installation_id == null ? null : Number(r.installation_id), createdAt: Date.parse(r.created_at) }))
311
- },
312
- async addGithubConnection ({ scope, scopeId, connectedBy, installationId = null }) {
313
- await one(db.from('github_connections').upsert({ scope, scope_id: scopeId, connected_by: connectedBy, installation_id: installationId, created_at: new Date().toISOString() }, { onConflict: 'scope,scope_id,connected_by' }))
314
- },
315
- async removeGithubConnection ({ scope, scopeId, connectedBy }) { await one(db.from('github_connections').delete().eq('scope', scope).eq('scope_id', scopeId).eq('connected_by', connectedBy)) },
316
- async workspaceGithub (workspaceId) {
317
- const r = await one(db.from('workspace_github').select('access, repos, agent_commits').eq('workspace_id', workspaceId).maybeSingle())
318
- return r ? { access: r.access, repos: r.repos || [], agentCommits: r.agent_commits || '' } : { access: 'open', repos: [], agentCommits: '' }
319
- },
320
- // Only what is given changes: { access, repos } and/or { agentCommits } ('' for each session to decide).
321
- async setWorkspaceGithub (workspaceId, { access, repos, agentCommits }) {
322
- const row = { workspace_id: workspaceId, updated_at: new Date().toISOString() }
323
- if (access !== undefined) { row.access = access; row.repos = repos || [] }
324
- if (agentCommits !== undefined) row.agent_commits = agentCommits || null
325
- await one(db.from('workspace_github').upsert(row, { onConflict: 'workspace_id' }))
326
- },
327
- async teamGithub (teamId) {
328
- const r = await one(db.from('team_github').select('agent_commits').eq('team_id', teamId).maybeSingle())
329
- return { agentCommits: (r && r.agent_commits) || '' }
330
- },
331
- async setTeamGithub (teamId, { agentCommits }) {
332
- await one(db.from('team_github').upsert({ team_id: teamId, agent_commits: agentCommits || null, updated_at: new Date().toISOString() }, { onConflict: 'team_id' }))
333
- },
334
- // The rules of the teams `memberId` is on: [{ teamId, name, agentCommits }], only those that set one.
335
- async teamGitRules (memberId) {
336
- const rows = await one(db.from('team_members').select('team_id, teams (name, team_github (agent_commits))').eq('member_id', memberId))
337
- return rows.map((r) => {
338
- const g = r.teams && r.teams.team_github
339
- const mode = Array.isArray(g) ? (g[0] && g[0].agent_commits) : (g && g.agent_commits)
340
- return mode ? { teamId: r.team_id, name: (r.teams && r.teams.name) || '', agentCommits: mode } : null
341
- }).filter(Boolean)
342
- },
343
- async orgGithub (orgId) {
344
- const r = await one(db.from('org_github').select('require_github, agent_commits').eq('org_id', orgId).maybeSingle())
345
- return { requireGithub: !!(r && r.require_github), agentCommits: (r && r.agent_commits) || '' }
346
- },
347
- async setOrgGithub (orgId, { requireGithub, agentCommits }) {
348
- const row = { org_id: orgId, updated_at: new Date().toISOString() }
349
- if (requireGithub !== undefined) row.require_github = !!requireGithub
350
- if (agentCommits !== undefined) row.agent_commits = agentCommits || null
351
- await one(db.from('org_github').upsert(row, { onConflict: 'org_id' }))
352
- },
353
301
  // The API's own settings (app_settings): a value it keeps across starts, such as the webhook secret it set on GitHub.
354
302
  // Relay leases (relay_leases, through its locking functions; 16 R2). `key`: the relay's name for what it holds.
355
303
  // Before the migration that makes relay_leases is applied, a git branch's key ("<repo> <branch>") goes to the old functions.
@@ -12,9 +12,6 @@
12
12
  // Everything on the relay that reads a branch's files asks here, for either form.
13
13
  import path from 'node:path'
14
14
  import crypto from 'node:crypto'
15
- import { runGit, GitFailed, authEnv } from './relay-git.js'
16
- import { parseRepo } from './relay-upstream.js'
17
- import { looksBinary, sha1 } from './fsutil.js'
18
15
 
19
16
  export const DIFF = 'diff'
20
17
  /** docKey's answer for a path a diff document leaves to git: never null (a deletion), never undefined (unreadable). */
@@ -161,84 +158,15 @@ export function baseOf (room, key, e = null) {
161
158
 
162
159
  // --------------------------------------------------------------- git --
163
160
 
164
- const TREE_CACHE = 16
165
- /** The relay's mirror for branch `key`'s repository: { dir, repo, token } or null (not on GitHub, or no mirror). */
166
- export async function mirrorFor (room, key) {
167
- const ups = (room.meta && room.meta.upstreams) || {}
168
- const b = room.meta && room.meta.branches && room.meta.branches[key]
169
- // The branch's own record, the default branch's, or the repository the folder that started it named (a new session).
170
- const rec = (ups[key] && ups[key].repo && ups[key]) || (ups[room.defaultKey] && ups[room.defaultKey].repo && ups[room.defaultKey]) || (b && b.repo ? { repo: b.repo } : null)
171
- if (!rec || !room.mirrors) return null
172
- const repo = parseRepo(`https://${rec.repo}`)
173
- if (!repo || !repo.github) return null
174
- const token = room.gitToken ? await room.gitToken(repo) : null
175
- const { dir } = await room.mirrors.ensure(repo, { url: room.gitRemoteUrl(repo) })
176
- return { dir, repo, token }
177
- }
178
-
179
- /** Makes sure the mirror has commit `sha`, fetching it from GitHub when it hasn't (a shallow mirror, or a commit it never fetched). */
180
- export async function ensureCommit (dir, sha, { token = null } = {}) {
181
- if ((await runGit(dir, ['cat-file', '-e', `${sha}^{commit}`])).ok) return true
182
- const r = await runGit(dir, ['fetch', '-q', 'origin', sha], { env: authEnv(token) })
183
- return r.ok && (await runGit(dir, ['cat-file', '-e', `${sha}^{commit}`])).ok
184
- }
185
-
186
- /** Commit `sha`'s tree: Map path -> { mode, sha, size } (cached on the room, a few commits). */
187
- export async function treeAt (room, dir, sha) {
188
- if (!room.gitTrees) room.gitTrees = new Map()
189
- const k = `${dir}\0${sha}`
190
- if (room.gitTrees.has(k)) { const t = room.gitTrees.get(k); room.gitTrees.delete(k); room.gitTrees.set(k, t); return t }
191
- const r = await runGit(dir, ['ls-tree', '-r', '-z', '-l', sha])
192
- if (!r.ok) throw new GitFailed(`listing ${sha.slice(0, 7)}`, r.err)
193
- const out = new Map()
194
- for (const entry of r.out.split('\0')) {
195
- if (!entry) continue
196
- const tab = entry.indexOf('\t')
197
- const [mode, type, oid, size] = entry.slice(0, tab).split(/\s+/)
198
- if (type !== 'blob') continue // submodules have no bytes here
199
- out.set(entry.slice(tab + 1), { mode, sha: oid, size: Number(size) || 0 })
200
- }
201
- room.gitTrees.set(k, out)
202
- while (room.gitTrees.size > TREE_CACHE) room.gitTrees.delete(room.gitTrees.keys().next().value)
203
- return out
204
- }
205
-
206
- /** The bytes of `paths` at commit `sha`: Map path -> Buffer, or null when the commit hasn't it. */
207
- export async function bytesAt (dir, sha, paths) {
208
- const out = new Map()
209
- if (!paths.length) return out
210
- const r = await runGit(dir, ['cat-file', '--batch'], { input: paths.map((p) => `${sha}:${p}\n`).join(''), buffer: true })
211
- if (!r.ok) throw new GitFailed(`reading files at ${sha.slice(0, 7)}`, r.err)
212
- const buf = r.out
213
- let at = 0
214
- for (const rel of paths) {
215
- const nl = buf.indexOf(0x0a, at)
216
- if (nl < 0) break
217
- const line = buf.subarray(at, nl).toString('utf8')
218
- at = nl + 1
219
- if (/ missing$/.test(line) || / ambiguous$/.test(line)) { out.set(rel, null); continue }
220
- const size = Number(line.split(' ')[2])
221
- out.set(rel, Buffer.from(buf.subarray(at, at + size)))
222
- at += size + 1
223
- }
224
- return out
225
- }
226
-
227
161
  /** Why git can't answer for a diff branch, in words for whoever asked. */
228
- const noGit = (key) => `the session leaves ${key}'s unchanged files to git, and the relay can't read git for it right now (no GitHub mirror, or GitHub out of reach); try again in a minute`
162
+ const noGit = (key) => `the session left ${key}'s unchanged files to git, which the relay no longer reads (sessions are quilts now: open the branch's square)`
229
163
 
230
164
  /**
231
165
  * What git has of branch `key` at its base, for reading paths that follow git: { base, dir, tree }
232
166
  * or throws a plain-words error. Only for a diff document.
233
167
  */
234
- export async function gitOf (room, key, e) {
235
- const base = baseOf(room, key, e)
236
- if (!base) throw new Error(noGit(key))
237
- let m
238
- try { m = await mirrorFor(room, key) } catch { m = null }
239
- if (!m) throw new Error(noGit(key))
240
- if (!(await ensureCommit(m.dir, base, { token: m.token }))) throw new Error(noGit(key))
241
- return { base, dir: m.dir, tree: await treeAt(room, m.dir, base) }
168
+ export async function gitOf (room, key) {
169
+ throw new Error(noGit(key))
242
170
  }
243
171
 
244
172
  /**
@@ -280,17 +208,7 @@ export async function readPaths (room, key, e, rels) {
280
208
  else if (s.kind === 'git') fromGit.push(rel)
281
209
  else out.set(rel, { kind: 'missing' })
282
210
  }
283
- if (fromGit.length) {
284
- const g = await gitOf(room, key, e)
285
- const known = fromGit.filter((rel) => g.tree.has(rel))
286
- const bytes = await bytesAt(g.dir, g.base, known)
287
- for (const rel of fromGit) {
288
- const buf = bytes.get(rel)
289
- if (!buf) out.set(rel, { kind: 'missing' })
290
- else if (looksBinary(buf)) out.set(rel, { kind: 'binary', buf, git: true })
291
- else out.set(rel, { kind: 'text', text: buf.toString('utf8'), git: true })
292
- }
293
- }
211
+ if (fromGit.length) await gitOf(room, key, e)
294
212
  return out
295
213
  }
296
214
 
@@ -315,44 +233,8 @@ export async function textOf (room, key, e, rel) {
315
233
  * the contents of `want` (and of every .gitattributes), hashes only for the rest. The hashes of a
316
234
  * commit are read once and kept on the room (a few commits).
317
235
  */
318
- export async function gitForSync (room, key, e, want = []) {
319
- const g = await gitOf(room, key, e)
320
- if (!room.gitSha1s) room.gitSha1s = new Map()
321
- const ck = `${g.dir}\0${g.base}`
322
- let hashes = room.gitSha1s.get(ck)
323
- if (!hashes) {
324
- hashes = new Map()
325
- const rels = [...g.tree.keys()]
326
- for (let i = 0; i < rels.length; i += 400) {
327
- const part = rels.slice(i, i + 400)
328
- const at = await bytesAt(g.dir, g.base, part)
329
- for (const rel of part) { const b = at.get(rel); if (b) hashes.set(rel, { h: sha1(b), binary: looksBinary(b), size: b.length }) }
330
- }
331
- room.gitSha1s.set(ck, hashes)
332
- while (room.gitSha1s.size > 4) room.gitSha1s.delete(room.gitSha1s.keys().next().value)
333
- }
334
- const gone = goneOf(e)
335
- const files = new Map()
336
- for (const [rel, x] of hashes) if (!hasEntry(e, rel) && !gone.has(rel)) files.set(rel, { h: x.h, binary: x.binary, size: x.size })
337
- const read = [...new Set([...want, ...[...files.keys()].filter((rel) => path.posix.basename(rel) === '.gitattributes')])].filter((rel) => files.has(rel))
338
- const at = await bytesAt(g.dir, g.base, read)
339
- for (const rel of read) {
340
- const b = at.get(rel)
341
- if (!b) continue
342
- const f = files.get(rel)
343
- if (f.binary) f.buf = b; else f.text = b.toString('utf8')
344
- }
345
- return { base: g.base, files }
346
- }
347
-
348
- /** A diff branch's rule files that only git has (the session never changed them) at commit `sha`, matching `pattern`: [[path, text]]. */
349
- export async function gitRuleFiles (dir, sha, e, pattern) {
350
- if (!sha) return []
351
- const r = await runGit(dir, ['ls-tree', '-r', '-z', '--name-only', sha])
352
- if (!r.ok) return []
353
- const rels = r.out.split('\0').filter((rel) => rel && pattern.test(rel) && !hasEntry(e, rel) && !goneOf(e).has(rel))
354
- const at = await bytesAt(dir, sha, rels)
355
- return rels.map((rel) => [rel, at.get(rel)]).filter(([, b]) => b && b.length <= 256 * 1024).map(([rel, b]) => [rel, b.toString('utf8')])
236
+ export async function gitForSync (room, key, e) {
237
+ return gitOf(room, key, e)
356
238
  }
357
239
 
358
240
  /** The rule files (ignore, attributes) branch `key` has, with their text: Map path -> text, from the session and git. */
@@ -360,12 +242,5 @@ export async function ruleFiles (room, key, e, names) {
360
242
  const want = (rel) => names.includes(path.posix.basename(rel))
361
243
  const out = new Map()
362
244
  for (const [rel, t] of e.files) if (want(rel)) out.set(rel, t.toString())
363
- if (!isDiff(e)) return out
364
- let g
365
- try { g = await gitOf(room, key, e) } catch { return out }
366
- const gone = goneOf(e)
367
- const rels = [...g.tree.keys()].filter((rel) => want(rel) && !hasEntry(e, rel) && !gone.has(rel))
368
- const bytes = await bytesAt(g.dir, g.base, rels)
369
- for (const rel of rels) { const b = bytes.get(rel); if (b && b.length <= 256 * 1024) out.set(rel, b.toString('utf8')) }
370
245
  return out
371
246
  }