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.
- package/RELEASES.md +10 -0
- package/bin/quilt.js +7 -143
- package/package.json +1 -1
- package/src/account.js +0 -29
- package/src/api/memory-store.js +0 -29
- package/src/api/routes/teams.js +0 -23
- package/src/api/server.js +5 -19
- package/src/api/supabase-store.js +0 -52
- package/src/branch-view.js +6 -131
- package/src/control.js +2 -33
- package/src/duties.js +1 -1
- package/src/github-commit.js +3 -315
- package/src/mcp.js +8 -157
- package/src/relay-ask.js +3 -66
- package/src/relay-cluster.js +3 -5
- package/src/relay-mcp.js +6 -418
- package/src/relay-prune.js +6 -145
- package/src/relay-turn-sync.js +1 -1
- package/src/server.js +33 -1331
- package/src/session-follow.js +18 -5
- package/src/session.js +58 -3526
- package/src/status.js +2 -58
- package/src/turn-sync.js +15 -2
- package/src/ui/changes.js +5 -14
- package/src/ui/chat.js +0 -1
- package/src/ui/common.js +0 -2
- package/src/ui/git-rule.js +2 -30
- package/src/ui/home.js +3 -76
- package/src/ui/session.js +34 -528
- package/src/ui/tree.js +2 -8
- package/src/ui/workspaces.js +2 -122
- package/src/ui-server.js +1 -38
- package/src/api/routes/github.js +0 -380
- package/src/branches.js +0 -194
- package/src/clash.js +0 -128
- package/src/commit-request.js +0 -59
- package/src/commit.js +0 -213
- package/src/git-hook.js +0 -48
- package/src/gitignore.js +0 -49
- package/src/gitstate.js +0 -1069
- package/src/relay-ci.js +0 -111
- package/src/relay-clash.js +0 -214
- package/src/relay-commit.js +0 -923
- package/src/relay-filekey.js +0 -46
- package/src/relay-git.js +0 -274
- package/src/relay-githttp.js +0 -166
- package/src/relay-github.js +0 -109
- package/src/relay-outbound.js +0 -1295
- package/src/relay-repo-moves.js +0 -43
- package/src/relay-sync.js +0 -795
- package/src/relay-upstream.js +0 -889
- package/src/sync-words.js +0 -42
- 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
|
-
}
|