heyquilt 0.3.138 → 0.3.140

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 (52) hide show
  1. package/RELEASES.md +6 -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.js +52 -3526
  21. package/src/status.js +2 -58
  22. package/src/turn-sync.js +15 -2
  23. package/src/ui/changes.js +5 -14
  24. package/src/ui/chat.js +0 -1
  25. package/src/ui/common.js +0 -2
  26. package/src/ui/git-rule.js +2 -30
  27. package/src/ui/home.js +3 -76
  28. package/src/ui/session.js +34 -528
  29. package/src/ui/tree.js +2 -8
  30. package/src/ui/workspaces.js +2 -122
  31. package/src/ui-server.js +1 -38
  32. package/src/api/routes/github.js +0 -380
  33. package/src/branches.js +0 -194
  34. package/src/clash.js +0 -128
  35. package/src/commit-request.js +0 -59
  36. package/src/commit.js +0 -213
  37. package/src/git-hook.js +0 -48
  38. package/src/gitignore.js +0 -49
  39. package/src/gitstate.js +0 -1069
  40. package/src/relay-ci.js +0 -111
  41. package/src/relay-clash.js +0 -214
  42. package/src/relay-commit.js +0 -923
  43. package/src/relay-filekey.js +0 -46
  44. package/src/relay-git.js +0 -274
  45. package/src/relay-githttp.js +0 -166
  46. package/src/relay-github.js +0 -109
  47. package/src/relay-outbound.js +0 -1295
  48. package/src/relay-repo-moves.js +0 -43
  49. package/src/relay-sync.js +0 -795
  50. package/src/relay-upstream.js +0 -889
  51. package/src/sync-words.js +0 -42
  52. package/src/upstream.js +0 -192
package/src/sync-words.js DELETED
@@ -1,42 +0,0 @@
1
- // Where each branch stands with GitHub (relay-sync.js syncStatus), in words for agents: the
2
- // quilt_sync_status tool, hosted (relay-mcp.js) and in a folder (mcp.js), from the branch list.
3
-
4
- const short = (sha) => (typeof sha === 'string' && sha ? sha.slice(0, 7) : '')
5
- const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`
6
-
7
- /** One line for branch entry `b` ({ key, relay: { repo, sync, ... } }), or null when it doesn't follow GitHub. */
8
- export function syncLine (b) {
9
- const r = b && b.relay
10
- const s = r && r.sync
11
- if (!s) return null
12
- const repo = String(r.repo || '').replace(/^github\.com\//, '')
13
- const at = s.revision ? ` at ${short(s.revision)}` : ''
14
- const head = `${b.key}${repo ? ` (${repo})` : ''}`
15
- switch (s.state) {
16
- case 'synced': return `${head}: in step with GitHub${at}; nothing uncommitted.`
17
- case 'github-pending': return `${head}: has GitHub's latest${at}; ${plural(s.pending || 0, 'file')} changed in the session and not committed (quilt_git_commit commits yours).`
18
- case 'conflict-held': return `${head}: ${s.why || `${plural(s.held || 0, 'file')} waiting to be merged`}${at}; Quilt merges ${s.held === 1 ? 'it' : 'them'} on its own, nothing to do.` // from a relay before 0.3.99
19
- case 'pr-pending': return `${head}: ${r.leased ? 'another session holds this branch' : 'protected on GitHub'}, so commits go through ${s.pr && s.pr.number ? `pull request #${s.pr.number}${s.pr.url ? ` (${s.pr.url})` : ''}` : 'a pull request (opened with the next commit)'}${at}.`
20
- case 'offline': return `${head}: GitHub unreachable${s.since ? ` since ${new Date(s.since).toISOString().slice(11, 16)} UTC` : ''}; the session keeps working, and commits asked for go when it answers.`
21
- case 'permission-held': return `${head}: ${s.why || 'GitHub refused the relay'}.`
22
- default: return `${head}: not syncing with GitHub (${s.why || 'paused'}).`
23
- }
24
- }
25
-
26
- /** The files of a branch that are GitHub's version, not edited in the session since: one line, or ''. */
27
- export function fromGithubLine (s) {
28
- const files = s && Array.isArray(s.fromGithubFiles) ? s.fromGithubFiles : []
29
- if (!files.length) return ''
30
- const more = (s.fromGithub || files.length) - files.length
31
- return ` From GitHub, not edited here since: ${files.map((f) => `${f.path} (${short(f.sha)}${f.by ? ` by ${f.by}` : ''})`).join(', ')}${more > 0 ? `, and ${more} more` : ''}.`
32
- }
33
-
34
- /** Every branch that follows GitHub, the caller's first: one line each. */
35
- export function syncStatusText (list, { mine = '' } = {}) {
36
- const rows = (Array.isArray(list) ? list : []).filter((b) => b && b.relay && b.relay.sync)
37
- if (!rows.length) return 'No branch of this session syncs with GitHub (none follows a GitHub repository, or the relay has no git sync).'
38
- rows.sort((a, b) => (a.key === mine ? -1 : b.key === mine ? 1 : 0))
39
- return rows.map((b) => [syncLine(b), fromGithubLine(b.relay.sync)].filter(Boolean).join('\n')).filter(Boolean).join('\n')
40
- }
41
-
42
- export const SYNC_STATUS_DESCRIPTION = 'Where each branch of the session stands with GitHub: in step (with the commit it is at), files changed and not committed, files held for a merge, commits going through a pull request (a protected branch, or another session holds it), GitHub unreachable, or sync paused and why. Pushes and merges on GitHub come in by themselves, and the files that are GitHub\'s version, not edited here since, are listed with the commit that brought them; nothing goes to GitHub until someone commits.'
package/src/upstream.js DELETED
@@ -1,192 +0,0 @@
1
- // Catching a folder's branch up with commits it doesn't have yet (pushed from
2
- // a worktree, merged on GitHub, moved by another worktree), without anyone
3
- // running git pull. Quilt never merges git history: the branch only ever moves
4
- // forward to a commit it is behind. What it merges is the session's
5
- // uncommitted work into those commits' files, file by file, the way
6
- // `git pull --autostash` would, except that where both changed the same lines
7
- // Quilt combines them itself (combine, merge3.js mergeAuto): nobody is asked to
8
- // merge (docs/superpowers/specs/2026-10-18-automatic-merges.md). Only a file git
9
- // can't read holds a catch-up up.
10
- //
11
- // planCatchUp is pure: session.js reads git and the disk, asks for a plan,
12
- // and applies it (bringIn there).
13
- import crypto from 'node:crypto'
14
- import { mergeAuto } from './merge3.js'
15
-
16
- const isText = (k) => typeof k === 'string' && !k.startsWith('bin:')
17
-
18
- /**
19
- * A file's content id as Quilt records what it published (`published` in the room document):
20
- * the sha1 of its bytes. A key here is text or "bin:<sha1>" (the sha1 of the bytes already).
21
- */
22
- export const contentId = (k) => typeof k !== 'string' ? null : k.startsWith('bin:') ? k.slice(4) : crypto.createHash('sha1').update(Buffer.from(k, 'utf8')).digest('hex')
23
-
24
- /**
25
- * Whether `key` is a version of `rel` the session itself published to GitHub (a commit the relay
26
- * made from the session's copy, on any branch). `published`: Map path -> [content ids], or null.
27
- */
28
- export const wasPublished = (published, rel, key) => {
29
- if (!published || key === null || key === undefined) return false
30
- const ids = published.get(rel)
31
- return !!ids && ids.includes(contentId(key))
32
- }
33
-
34
- /** What the session published (relay commits), read from the room document: Map path -> [content ids]. */
35
- export function publishedOf (doc) {
36
- const out = new Map()
37
- try { doc.getMap('published').forEach((v, k) => { if (Array.isArray(v)) out.set(k, v.filter((x) => typeof x === 'string')) }) } catch {}
38
- return out
39
- }
40
-
41
- /** Records that these files' contents (path -> bytes, null for deleted) were published; the last 20 per path are kept. */
42
- export function notePublished (doc, files, origin) {
43
- const entries = []
44
- for (const [rel, buf] of files) if (buf) entries.push([rel, crypto.createHash('sha1').update(buf).digest('hex')])
45
- notePublishedIds(doc, entries, origin)
46
- }
47
-
48
- /** The same, by content id (sha1 of the bytes): [[path, id]], as a folder reports what it pushed. */
49
- export function notePublishedIds (doc, entries, origin) {
50
- const map = doc.getMap('published')
51
- doc.transact(() => {
52
- for (const [rel, id] of entries) {
53
- if (typeof rel !== 'string' || !/^[0-9a-f]{40}$/.test(id || '')) continue
54
- const was = Array.isArray(map.get(rel)) ? map.get(rel) : []
55
- if (was.includes(id)) continue
56
- map.set(rel, [...was, id].slice(-20))
57
- }
58
- }, origin)
59
- }
60
-
61
- /**
62
- * Two versions of a file that both moved on from `base`, combined the way Quilt does it on its own
63
- * (keys as planCatchUp's: text, "bin:<sha1>", null when absent): lines only one side changed are
64
- * kept, lines both changed go to the newer side (`oursNewer`), an edit beats a deletion, and a binary
65
- * file (or one both created, differently) is the newer version. Returns { key, lost, how }: `lost` as mergeAuto's ({ side, lines, kept,
66
- * at }; a binary that lost has `binary: true` and no lines); `how` 'lines', 'kept-edit' or 'newer'.
67
- */
68
- export function combine (base, ours, theirs, { oursNewer = true, union = false } = {}) {
69
- if (ours === theirs || theirs === base) return { key: ours, lost: [], how: 'lines' }
70
- if (ours === base) return { key: theirs, lost: [], how: 'lines' }
71
- // Deleted on one side, edited on the other: the edit stays (a deletion is cheap to redo; lost work is not).
72
- if (ours === null) return { key: theirs, lost: [], how: 'kept-edit', deleted: 'ours' }
73
- if (theirs === null) return { key: ours, lost: [], how: 'kept-edit', deleted: 'theirs' }
74
- if (base === null && isText(ours) && isText(theirs) && !union) {
75
- // Created on both sides, differently: nothing to measure either from, so the newer file, and the other's lines it lacks are what lost.
76
- const [won, lose] = oursNewer ? [ours, theirs] : [theirs, ours]
77
- const left = new Map()
78
- for (const l of won.split('\n')) left.set(l, (left.get(l) || 0) + 1)
79
- const gone = lose.split('\n').filter((l) => { const n = left.get(l); if (n) { left.set(l, n - 1); return false } return l !== '' })
80
- return { key: won, lost: gone.length ? [{ side: oursNewer ? 'theirs' : 'ours', lines: gone, kept: [], at: 0 }] : [], how: 'newer' }
81
- }
82
- if ((base === null || isText(base)) && isText(ours) && isText(theirs)) {
83
- const r = mergeAuto(base ?? '', ours, theirs, { oursNewer, union })
84
- return { key: r.text, lost: r.lost, how: 'lines' }
85
- }
86
- // A binary file (or one that became one): the newer version.
87
- return { key: oursNewer ? ours : theirs, lost: [{ side: oursNewer ? 'theirs' : 'ours', lines: [], kept: [], at: 0, binary: true }], how: 'newer' }
88
- }
89
-
90
- /**
91
- * How the folder at commit `from` (its files on disk: the commit plus the
92
- * session's uncommitted work) becomes the folder at `to` with that work kept.
93
- *
94
- * - changes: Map path -> git status letter (A, M, D, T...) between the commits.
95
- * - base / theirs: Map path -> the file at `from` / at `to` (text, "bin:<sha1>",
96
- * null when absent, undefined when git couldn't read it).
97
- * - disk(rel): the file in the folder now (same keys).
98
- * - moved: candidates for a file the session moved: Map path -> its text, for
99
- * files on disk that `from` didn't have (new or moved here).
100
- * - emptied: folders the session emptied (had files at `from`, none on disk now).
101
- *
102
- * - oursNewer(rel): whether the session's (the folder's) edit of rel is newer than
103
- * the commits' (it keeps the lines both changed); union(rel): a merge=union file.
104
- *
105
- * Every file both sides changed is combined (combine): nothing clashes. Returns
106
- * { writes: Map path -> key (null: delete), moves: [{ from, to }], conflicts:
107
- * [{ path, why }] (only files git couldn't read), strays: [path], merged: [{ path,
108
- * how, lost, oursNewer, deleted? }] (the files both changed, for the history and
109
- * the notice to whoever's lines lost) }. With conflicts, writes is empty: a
110
- * catch-up is all or nothing.
111
- */
112
- export function planCatchUp ({ changes, base, theirs, disk, moved = new Map(), emptied = new Set(), published = null, oursNewer = () => true, union = () => false }) {
113
- const writes = new Map()
114
- const conflicts = []
115
- const moves = []
116
- const strays = []
117
- const merged = []
118
- const taken = new Set()
119
- for (const rel of changes.keys()) {
120
- const b = base.get(rel)
121
- const t = theirs.get(rel)
122
- const o = disk(rel)
123
- if (b === undefined || t === undefined || o === undefined) {
124
- conflicts.push({ path: rel, why: 'git could not read it (too large, or behind a filter such as Git LFS)' })
125
- continue
126
- }
127
- if (o === b) { // untouched here: the commits' version
128
- if (t !== o) writes.set(rel, t)
129
- if (b === null && t !== null && emptied.has(dirOf(rel))) strays.push(rel)
130
- continue
131
- }
132
- if (o === t || t === b) continue // the session already has it, or the commits didn't change it
133
- // The commits bring a version of this file the session itself published (an agent committed it
134
- // to a branch, and the branch was merged, squashed or rebased in): the session's copy is that
135
- // version or newer, so it stays as it is. Matched by content, so every merge strategy counts.
136
- if (o !== null && wasPublished(published, rel, t)) continue
137
- const newer = !!oursNewer(rel)
138
- if (o === null && isText(b) && isText(t)) {
139
- // Gone here, changed there: the session may have moved it. Followed by content, as git
140
- // follows a rename, into the moved copy, which takes the commits' change.
141
- const to = findMove(rel, b, moved, taken)
142
- if (to) {
143
- const r = mergeAuto(b, moved.get(to), t, { oursNewer: newer, union: !!union(to) })
144
- taken.add(to)
145
- moves.push({ from: rel, to })
146
- if (r.text !== moved.get(to)) writes.set(to, r.text)
147
- if (r.lost.length) merged.push({ path: to, how: 'lines', lost: r.lost, oursNewer: newer })
148
- continue
149
- }
150
- }
151
- // Both changed it: Quilt combines them (an edit beats a deletion; the newer edit keeps the lines both changed).
152
- const r = combine(b, o, t, { oursNewer: newer, union: !!union(rel) })
153
- if (r.key !== o) writes.set(rel, r.key)
154
- merged.push({ path: rel, how: r.how, lost: r.lost, oursNewer: newer, ...(r.deleted ? { deleted: r.deleted } : {}) })
155
- }
156
- if (conflicts.length) return { writes: new Map(), moves: [], conflicts, strays: [], merged: [] }
157
- return { writes, moves, conflicts, strays, merged }
158
- }
159
-
160
- const dirOf = (rel) => rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : ''
161
- const nameOf = (rel) => rel.slice(rel.lastIndexOf('/') + 1)
162
-
163
- /** The moved copy of a file: same name, and at least half its lines the same. */
164
- function findMove (rel, text, moved, taken) {
165
- let best = null
166
- let score = 0.5
167
- for (const [to, t] of moved) {
168
- if (taken.has(to) || nameOf(to) !== nameOf(rel) || !isText(t)) continue
169
- const s = similarity(text, t)
170
- if (s >= score) { best = to; score = s }
171
- }
172
- return best
173
- }
174
-
175
- /** Share of lines in common (multiset), against the longer file. */
176
- export function similarity (a, b) {
177
- const count = new Map()
178
- const la = a.split('\n')
179
- const lb = b.split('\n')
180
- for (const l of la) count.set(l, (count.get(l) || 0) + 1)
181
- let same = 0
182
- for (const l of lb) { const n = count.get(l); if (n) { same++; count.set(l, n - 1) } }
183
- return same / Math.max(la.length, lb.length, 1)
184
- }
185
-
186
- /** One line for the AI and the log about why a catch-up waits. Nobody is told to pull or merge: Quilt combines what it can read. */
187
- export function catchUpAdvice ({ branch, upstream, behind, conflicts = [], diverged, ahead }) {
188
- const n = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`
189
- if (diverged) return `${branch} and ${upstream} have both moved on (${n(ahead, 'commit')} here, ${n(behind, 'commit')} there). Quilt never rewrites git history in this folder: the session's files take in ${upstream}'s changes, combined with yours, and your commits go to GitHub through the session.`
190
- const list = conflicts.slice(0, 4).map((c) => `${c.path} (${c.why})`).join('; ') + (conflicts.length > 4 ? `; and ${conflicts.length - 4} more` : '')
191
- return `${branch} is ${n(behind, 'commit')} behind ${upstream}, and Quilt can't read ${list} to combine ${conflicts.length === 1 ? 'it' : 'them'} with the session's work, so it brought nothing in yet. A folder on ${branch} that can read ${conflicts.length === 1 ? 'it' : 'them'} brings the commits in; nothing to merge by hand.`
192
- }