heyquilt 0.3.122 → 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 (166) hide show
  1. package/RELEASES.md +279 -17
  2. package/bin/quilt.js +62 -152
  3. package/desktop/main.js +16 -2
  4. package/package.json +3 -2
  5. package/src/account.js +0 -29
  6. package/src/agent-task-workflow.js +2 -2
  7. package/src/agents/common.js +13 -0
  8. package/src/ai-vm.js +9 -1
  9. package/src/api/activity.js +5 -0
  10. package/src/api/memory-store.js +16 -38
  11. package/src/api/notify.js +134 -0
  12. package/src/api/routes/browsers.js +29 -0
  13. package/src/api/routes/relay.js +26 -3
  14. package/src/api/routes/session-migrate.js +7 -4
  15. package/src/api/routes/sessions.js +73 -1
  16. package/src/api/routes/teams.js +0 -23
  17. package/src/api/server.js +17 -25
  18. package/src/api/supabase-store.js +43 -56
  19. package/src/autowork.js +182 -16
  20. package/src/branch-view.js +6 -131
  21. package/src/connection.js +36 -9
  22. package/src/control.js +131 -48
  23. package/src/duties.js +1 -1
  24. package/src/force-date.js +27 -0
  25. package/src/fsutil.js +13 -3
  26. package/src/github-commit.js +3 -315
  27. package/src/history.js +13 -3
  28. package/src/mcp.js +93 -167
  29. package/src/merge3.js +11 -3
  30. package/src/now-summary.js +113 -0
  31. package/src/pathrules.js +12 -0
  32. package/src/protocol.js +10 -2
  33. package/src/quilt-default.js +84 -0
  34. package/src/quilt-history.js +106 -0
  35. package/src/quilt-remote.js +78 -0
  36. package/src/relay-ask.js +3 -66
  37. package/src/relay-cluster.js +3 -5
  38. package/src/relay-lease.js +6 -2
  39. package/src/relay-mcp.js +154 -432
  40. package/src/relay-prune.js +6 -145
  41. package/src/relay-turn-sync.js +1 -1
  42. package/src/release-words.js +2 -2
  43. package/src/runner.js +20 -7
  44. package/src/seam-access.js +97 -0
  45. package/src/seam-claims.js +53 -0
  46. package/src/seam-url.js +15 -0
  47. package/src/server.js +1516 -1383
  48. package/src/session-follow.js +461 -27
  49. package/src/session-layout3.js +410 -4
  50. package/src/session-migrate.js +15 -11
  51. package/src/session-quilt.js +1 -1
  52. package/src/session-seam.js +233 -20
  53. package/src/session.js +745 -3438
  54. package/src/source-live/bring-in.js +283 -0
  55. package/src/source-live/live-block.js +504 -33
  56. package/src/source-live/live-square.js +16 -5
  57. package/src/status.js +7 -58
  58. package/src/turn-sync.js +15 -2
  59. package/src/ui/agent-guide.js +1 -1
  60. package/src/ui/app.css +85 -20
  61. package/src/ui/app.js +4 -1
  62. package/src/ui/changes.js +5 -14
  63. package/src/ui/chat.js +9 -4
  64. package/src/ui/cloud-ai-model.js +13 -0
  65. package/src/ui/cloud-ai.js +10 -2
  66. package/src/ui/common.js +6 -4
  67. package/src/ui/fileview.js +20 -7
  68. package/src/ui/force-words.js +19 -0
  69. package/src/ui/git-rule.js +9 -28
  70. package/src/ui/home.js +3 -223
  71. package/src/ui/loom-model.js +15 -1
  72. package/src/ui/quiltview.js +213 -0
  73. package/src/ui/seam-model.js +251 -0
  74. package/src/ui/session.js +377 -489
  75. package/src/ui/tree.js +2 -8
  76. package/src/ui/voice-call.js +83 -14
  77. package/src/ui/voice-model.js +39 -0
  78. package/src/ui/voice.js +167 -17
  79. package/src/ui/workspaces.js +2 -122
  80. package/src/ui-server.js +81 -66
  81. package/src/update-check.js +25 -1
  82. package/src/voice.js +13 -6
  83. package/src/api/routes/github.js +0 -393
  84. package/src/branches.js +0 -194
  85. package/src/clash.js +0 -128
  86. package/src/commit-request.js +0 -59
  87. package/src/commit.js +0 -213
  88. package/src/git-hook.js +0 -48
  89. package/src/git.js +0 -147
  90. package/src/gitignore.js +0 -49
  91. package/src/gitstate.js +0 -1033
  92. package/src/quilts/AGENTS.md +0 -1
  93. package/src/quilts/CLAUDE.md +0 -1
  94. package/src/quilts/accounts.js +0 -136
  95. package/src/quilts/affected.js +0 -132
  96. package/src/quilts/archiver.js +0 -175
  97. package/src/quilts/autorevert.js +0 -49
  98. package/src/quilts/bisect-run.js +0 -31
  99. package/src/quilts/blame.js +0 -103
  100. package/src/quilts/block.js +0 -347
  101. package/src/quilts/bucket.js +0 -25
  102. package/src/quilts/cbor.js +0 -92
  103. package/src/quilts/checks.js +0 -308
  104. package/src/quilts/claims.js +0 -144
  105. package/src/quilts/cli.js +0 -536
  106. package/src/quilts/client.js +0 -137
  107. package/src/quilts/conflict-format.js +0 -190
  108. package/src/quilts/describe.js +0 -296
  109. package/src/quilts/dogfood.js +0 -82
  110. package/src/quilts/errors.js +0 -8
  111. package/src/quilts/events.js +0 -171
  112. package/src/quilts/export-git.js +0 -130
  113. package/src/quilts/export-remote.js +0 -156
  114. package/src/quilts/export.js +0 -81
  115. package/src/quilts/fastmerge.js +0 -41
  116. package/src/quilts/hash.js +0 -44
  117. package/src/quilts/history.js +0 -266
  118. package/src/quilts/ids.js +0 -12
  119. package/src/quilts/import-follow.js +0 -236
  120. package/src/quilts/import-git.js +0 -294
  121. package/src/quilts/import.js +0 -196
  122. package/src/quilts/index.js +0 -94
  123. package/src/quilts/keys.js +0 -186
  124. package/src/quilts/limits.js +0 -67
  125. package/src/quilts/lines.js +0 -49
  126. package/src/quilts/live.js +0 -50
  127. package/src/quilts/merge-smart.js +0 -114
  128. package/src/quilts/merge.js +0 -501
  129. package/src/quilts/oauth.js +0 -202
  130. package/src/quilts/objects.js +0 -144
  131. package/src/quilts/pack.js +0 -118
  132. package/src/quilts/records.js +0 -211
  133. package/src/quilts/resolutions.js +0 -69
  134. package/src/quilts/reviews.js +0 -208
  135. package/src/quilts/routes.js +0 -186
  136. package/src/quilts/runner.js +0 -95
  137. package/src/quilts/server.js +0 -144
  138. package/src/quilts/service.js +0 -963
  139. package/src/quilts/setup.js +0 -41
  140. package/src/quilts/signing.js +0 -149
  141. package/src/quilts/squares.js +0 -239
  142. package/src/quilts/store-memory.js +0 -844
  143. package/src/quilts/store-rpc.js +0 -90
  144. package/src/quilts/store-supabase.js +0 -13
  145. package/src/quilts/tags.js +0 -64
  146. package/src/quilts/textdiff.js +0 -96
  147. package/src/quilts/tokens.js +0 -66
  148. package/src/quilts/tools.js +0 -403
  149. package/src/quilts/tree.js +0 -351
  150. package/src/quilts/webhook.js +0 -116
  151. package/src/quilts/window.js +0 -121
  152. package/src/quilts/xdiff.js +0 -435
  153. package/src/relay-ci.js +0 -111
  154. package/src/relay-clash.js +0 -214
  155. package/src/relay-commit.js +0 -918
  156. package/src/relay-filekey.js +0 -46
  157. package/src/relay-git.js +0 -274
  158. package/src/relay-githttp.js +0 -166
  159. package/src/relay-github.js +0 -109
  160. package/src/relay-outbound.js +0 -1295
  161. package/src/relay-repo-moves.js +0 -43
  162. package/src/relay-sync.js +0 -795
  163. package/src/relay-upstream.js +0 -889
  164. package/src/sync-words.js +0 -42
  165. package/src/ui/branches.js +0 -111
  166. package/src/upstream.js +0 -192
@@ -1,320 +1,8 @@
1
- // Commits made by the relay, through GitHub's API, for any member of a session: an agent on any
2
- // tool (Codex, Cursor, Claude Code, a hosted agent over HTTP), with no git and no credentials on
3
- // its machine, and with nobody else online. The session's copy of the files is the content; the
4
- // branch's commit on GitHub is the parent; only the files named change. No folder, index or
5
- // working tree is involved, so nobody's uncommitted work is swept in.
6
- //
7
- // It never rewrites history: a ref is only moved forward (GitHub refuses anything else without
8
- // force, which is never sent), and a file that changed on GitHub since the session's copy was
9
- // taken is refused rather than overwritten. Public and private repositories work alike: writing
10
- // needs the session's GitHub token with write access to the repository's contents (and to pull
11
- // requests, to open one).
12
- //
13
- // Git for this session (AGENT_COMMITS; docs/superpowers/specs/2026-10-17-git-for-everyone.md): off for
14
- // everyone, or on with what agents may push: their own branches only (quilt/<agent>/…, the default),
15
- // or any branch, the session's own included. People may push any branch while it is on.
16
- import crypto from 'node:crypto'
1
+ // R3 removed GitHub commits; only this pure helper stays, because test/unit/history.test.js (claimed in
2
+ // another square, sq:09a100d0) still imports it from here. Delete this file with that test's import.
17
3
  import { blameChange } from './history.js'
18
4
 
19
- const API = 'https://api.github.com'
20
- export const AGENT_COMMITS = ['off', 'branches', 'any']
21
- export const DEFAULT_AGENT_COMMITS = 'branches'
22
- export const MAX_COMMIT_FILES = 300
23
- export const MAX_COMMIT_BYTES = 20 * 1024 * 1024
24
- const BRANCH = /^(?!\/)(?!.*\/\/)(?!.*\.\.)(?!.*\.lock(\/|$))(?!.*[\s~^:?*[\\\x00-\x1f\x7f])(?!.*@\{)[^]+(?<![/.])$/
25
-
26
- export class CommitRefused extends Error {}
27
-
28
- /** Who set the rule over a session, in words: "the workspace Platform", "the team Night shift", "the org Acme". */
29
- export const ruleSetter = (set) => `the ${set.by === 'org' ? 'org' : set.by === 'team' ? 'team' : 'workspace'}${set.name ? ` ${set.name}` : ''}`
30
-
31
- /**
32
- * Git for everyone, in one line, for whoever is in the session: `policy` ('off', 'branches', 'any'),
33
- * `by` (the rule over the session, { by, name }, when one set it) and `login` (the GitHub account
34
- * whose connection covers the repository, when one does).
35
- */
36
- export function gitForEveryoneWords ({ policy = DEFAULT_AGENT_COMMITS, by = null, login = '' } = {}) {
37
- if (policy === 'off') return `Git is off for everyone in this session: ${by ? ruleSetter(by) : 'the session owner'} turned it off.`
38
- const agents = policy === 'any' ? 'people and agents push to any branch' : 'people push to any branch, agents to branches of their own'
39
- if (login) return `Git is on for everyone here, through @${login}'s connection: ${agents}, with nothing to set up.`
40
- return `Git is on for everyone here once someone connects GitHub (one click in the commit panel, by anyone whose GitHub account can write to the repository): ${agents}.`
41
- }
42
-
43
- /** git's id for a file's bytes (what GitHub's trees list), so content is compared without fetching it. */
44
- export function gitBlobSha (buf) {
45
- return crypto.createHash('sha1').update(`blob ${buf.length}\0`).update(buf).digest('hex')
46
- }
47
-
48
- /** "duncan", "daniel-carmichael": a name as a branch path segment. */
49
- export function slug (s, max = 40) {
50
- return String(s || '').toLowerCase().normalize('NFKD').replace(/[^\w\s-]/g, '').trim().replace(/[\s_]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, max).replace(/-$/, '') || 'work'
51
- }
52
-
53
- /** The branches an agent owns: quilt/<its name>/… */
54
- export const agentPrefix = (name) => `quilt/${slug(name, 30)}/`
55
-
56
- /** Whether `name` is a branch name git and GitHub accept. */
57
- export const validBranchName = (name) => typeof name === 'string' && name.length > 0 && name.length <= 200 && BRANCH.test(name)
58
-
59
- /**
60
- * Where a member's commit goes, or why it may not: { branch, own } or throws CommitRefused.
61
- * `sessionBranch`: the GitHub branch the session's branch follows (main). `target`: what they
62
- * asked for ('' = the default). `kind`: 'agent' for agents, anything else for people.
63
- */
64
- export function commitTarget ({ policy = DEFAULT_AGENT_COMMITS, kind = 'human', name, sessionBranch, target = '', message = '' }) {
65
- const agent = kind === 'agent'
66
- const mode = AGENT_COMMITS.includes(policy) ? policy : DEFAULT_AGENT_COMMITS
67
- if (agent && mode === 'off') throw new CommitRefused('Git is off for everyone in this session. Ask a person for the commit with quilt_request_commit (your files and a message).')
68
- const prefix = agentPrefix(name)
69
- let branch = String(target || '').trim().replace(/^refs\/heads\//, '')
70
- if (!branch) branch = agent && mode === 'branches' ? `${prefix}${slug(message, 40)}` : sessionBranch
71
- if (!validBranchName(branch)) throw new CommitRefused(`"${branch}" is not a branch name git accepts.`)
72
- if (agent && mode === 'branches' && !branch.startsWith(prefix)) {
73
- throw new CommitRefused(`The session owner lets agents commit only to branches of their own: ${prefix}<topic>${branch === sessionBranch ? ` (not ${sessionBranch} itself)` : ''}. Leave branch out to get one, and open a pull request with pull_request: true.`)
74
- }
75
- return { branch, own: branch !== sessionBranch }
76
- }
77
-
78
- /**
79
- * What a commit of `files` would change: { changes: [{ path, sha?, deleted, bytes }], same: [paths], moved: [paths] }.
80
- * `files`: Map path -> Buffer (the session's content) or null (deleted in the session).
81
- * `parent`: Map path -> blob sha at the commit the new one goes on; `base`: at the commit the
82
- * session's copy is measured from. A file whose blob differs between the two changed on GitHub
83
- * since the session took its copy: it is `moved`, and committing the session's copy would undo
84
- * that change. Without `base` (a branch only its agent writes to) nothing counts as moved.
85
- */
86
- export function planCommit (files, { parent, base = null }) {
87
- const changes = []
88
- const same = []
89
- const moved = []
90
- for (const [rel, buf] of files) {
91
- const now = buf ? gitBlobSha(buf) : null
92
- const at = parent.get(rel) || null
93
- if (now === at) { same.push(rel); continue }
94
- if (base && (base.get(rel) || null) !== at) { moved.push(rel); continue }
95
- changes.push({ path: rel, sha: now, deleted: !buf, bytes: buf || null })
96
- }
97
- return { changes, same, moved }
98
- }
99
-
100
- /**
101
- * Who changed each file in the session since `since` (ms) besides `me`, from the branch's
102
- * history (entries oldest first): Map path -> [names]. Pulls from git are nobody's.
103
- */
104
- export function othersEditing (paths, history, { me, since = 0 } = {}) {
105
- const want = new Set(paths)
106
- const out = new Map()
107
- for (const e of history || []) {
108
- if (!e || !want.has(e.path) || e.pulled || e.seed || !e.by || e.by === me || (e.ts || 0) <= since) continue
109
- const list = out.get(e.path) || []
110
- if (!list.includes(e.by)) list.push(e.by)
111
- out.set(e.path, list)
112
- }
113
- return out
114
- }
115
-
116
- /** Who besides `me` made the changes a commit of one text file would carry (history.js blameChange). */
5
+ /** Who besides `me` made the changes in one text file's edit (history.js blameChange). */
117
6
  export function othersInChange ({ before = '', after = '', entries = [], me }) {
118
7
  return blameChange({ before, after, entries }).filter((n) => n !== me)
119
8
  }
120
-
121
- /** The commit message, and who made it: an agent is named as the author, the token's account commits it. */
122
- export function messageFor ({ message, by, task = null, coAuthors = [] }) {
123
- const lines = [String(message || '').trim() || 'Changes from the Quilt session']
124
- const trailer = [`Made in Quilt by ${by}${task ? ` for task ${task.id}: ${task.title}` : ''}.`]
125
- for (const n of coAuthors) trailer.push(`Co-authored-by: ${n} <${noreply(n)}>`)
126
- return `${lines.join('\n')}\n\n${trailer.join('\n')}`
127
- }
128
-
129
- export const noreply = (name) => `${slug(name, 40)}@agents.noreply.heyquilt.com`
130
-
131
- class GitHubWriteError extends Error {
132
- constructor (status, what, body) {
133
- super(`GitHub answered ${status} to ${what}${body && body.message ? `: ${body.message}` : ''}`)
134
- this.status = status
135
- this.body = body
136
- }
137
- }
138
-
139
- /** The GitHub calls a commit needs. The token goes in a header, never a URL. */
140
- export function githubWriter ({ fetch = globalThis.fetch, token, owner, name }) {
141
- const headers = { 'user-agent': 'quilt-relay', 'x-github-api-version': '2022-11-28', accept: 'application/vnd.github+json', authorization: `Bearer ${token}` }
142
- const repo = `${API}/repos/${owner}/${name}`
143
- const req = async (method, path, body, what) => {
144
- const res = await fetch(`${repo}${path}`, { method, headers: { ...headers, ...(body ? { 'content-type': 'application/json' } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}), redirect: 'follow' })
145
- let json = null
146
- try { json = await res.json() } catch {}
147
- if (!res.ok) throw new GitHubWriteError(res.status, what, json)
148
- return json
149
- }
150
- const enc = (ref) => ref.split('/').map(encodeURIComponent).join('/')
151
- return {
152
- /** The branch's head commit, or null when there is no such branch. */
153
- async ref (branch) {
154
- try { return (await req('GET', `/git/ref/heads/${enc(branch)}`, null, `branch ${branch}`)).object.sha } catch (err) { if (err.status === 404) return null; throw err }
155
- },
156
- /** A commit's tree sha and date. */
157
- async commit (sha) {
158
- const c = await req('GET', `/git/commits/${sha}`, null, `commit ${sha.slice(0, 7)}`)
159
- return { tree: c.tree.sha, ts: Date.parse(c.committer && c.committer.date) || 0 }
160
- },
161
- /** Every file's blob sha in a tree: Map path -> sha (null when GitHub cut the listing short). */
162
- async blobs (treeSha) {
163
- const t = await req('GET', `/git/trees/${treeSha}?recursive=1`, null, 'the file list')
164
- if (t.truncated) return null
165
- return new Map((t.tree || []).filter((x) => x.type === 'blob').map((x) => [x.path, x.sha]))
166
- },
167
- async modes (treeSha) {
168
- const t = await req('GET', `/git/trees/${treeSha}?recursive=1`, null, 'the file list')
169
- return new Map((t.tree || []).filter((x) => x.type === 'blob').map((x) => [x.path, x.mode]))
170
- },
171
- /** A file's bytes at a commit, or null when it isn't there. */
172
- async content (sha, rel) {
173
- const res = await fetch(`${repo}/contents/${enc(rel)}?ref=${sha}`, { headers: { ...headers, accept: 'application/vnd.github.raw' }, redirect: 'follow' })
174
- if (res.status === 404) return null
175
- if (!res.ok) throw new GitHubWriteError(res.status, rel, null)
176
- return Buffer.from(await res.arrayBuffer())
177
- },
178
- async blob (buf) { return (await req('POST', '/git/blobs', { content: buf.toString('base64'), encoding: 'base64' }, 'a file')).sha },
179
- async tree (baseTree, entries) { return (await req('POST', '/git/trees', { base_tree: baseTree, tree: entries }, 'the new file list')).sha },
180
- async newCommit ({ message, tree, parent, author }) {
181
- return (await req('POST', '/git/commits', { message, tree, parents: [parent], ...(author ? { author } : {}) }, 'the commit')).sha
182
- },
183
- /** Moves the branch forward to `sha` (never forced: GitHub refuses a move that would lose commits). */
184
- async move (branch, sha) { await req('PATCH', `/git/refs/heads/${enc(branch)}`, { sha, force: false }, `branch ${branch}`) },
185
- async create (branch, sha) { await req('POST', '/git/refs', { ref: `refs/heads/${branch}`, sha }, `new branch ${branch}`) },
186
- async pull ({ title, head, base, body }) {
187
- try {
188
- const p = await req('POST', '/pulls', { title, head, base, body }, 'the pull request')
189
- return { number: p.number, url: p.html_url }
190
- } catch (err) {
191
- // One is already open for this branch: say which.
192
- if (err.status === 422) {
193
- const open = await req('GET', `/pulls?head=${encodeURIComponent(`${owner}:${head}`)}&state=open`, null, 'pull requests')
194
- if (Array.isArray(open) && open[0]) return { number: open[0].number, url: open[0].html_url, existed: true }
195
- }
196
- throw err
197
- }
198
- },
199
- /** The branch's newest commits: [{ sha, message, author, date, url }]. */
200
- async log (branch, n = 10) {
201
- const list = await req('GET', `/commits?sha=${encodeURIComponent(branch)}&per_page=${Math.max(1, Math.min(50, n | 0))}`, null, `commits on ${branch}`)
202
- return (Array.isArray(list) ? list : []).map((c) => ({ sha: c.sha, message: (c.commit && c.commit.message) || '', author: (c.commit && c.commit.author && c.commit.author.name) || (c.author && c.author.login) || '', date: (c.commit && c.commit.author && c.commit.author.date) || '', url: c.html_url }))
203
- },
204
- /** Open pull requests, newest first: [{ number, title, head, base, url, draft, by, sha }]. */
205
- async pulls () {
206
- const list = await req('GET', '/pulls?state=open&per_page=50&sort=updated&direction=desc', null, 'pull requests')
207
- return (Array.isArray(list) ? list : []).map((p) => ({ number: p.number, title: p.title, head: p.head && p.head.ref, base: p.base && p.base.ref, url: p.html_url, draft: !!p.draft, by: p.user && p.user.login, sha: p.head && p.head.sha }))
208
- },
209
- /** One pull request: { number, title, head, base, url, sha, state, merged, mergeable, mergeableState }. */
210
- async pullRequest (number) {
211
- const p = await req('GET', `/pulls/${Number(number)}`, null, `pull request #${number}`)
212
- return { number: p.number, title: p.title, head: p.head && p.head.ref, base: p.base && p.base.ref, url: p.html_url, sha: p.head && p.head.sha, state: p.state, merged: !!p.merged, mergeable: p.mergeable, mergeableState: p.mergeable_state || '' }
213
- },
214
- /** How `head` stands to `base`: 'identical', 'ahead' (base is in head), 'behind' or 'diverged' (it isn't). */
215
- async compare (base, head) {
216
- const c = await req('GET', `/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`, null, 'a comparison')
217
- return (c && c.status) || 'diverged'
218
- },
219
- /** How a commit's checks stand: 'passing', 'failing', 'pending' or 'none', with the failing ones named. */
220
- async checks (sha) {
221
- const runs = await req('GET', `/commits/${sha}/check-runs?per_page=100`, null, 'checks')
222
- const st = await req('GET', `/commits/${sha}/status`, null, 'statuses')
223
- const items = (runs && Array.isArray(runs.check_runs) ? runs.check_runs : []).map((c) => ({ name: c.name, done: c.status === 'completed', ok: ['success', 'neutral', 'skipped'].includes(c.conclusion) }))
224
- for (const s of (st && Array.isArray(st.statuses) ? st.statuses : [])) items.push({ name: s.context, done: s.state !== 'pending', ok: s.state === 'success' })
225
- if (!items.length) return { state: 'none', failing: [] }
226
- const failing = items.filter((c) => c.done && !c.ok).map((c) => c.name)
227
- if (failing.length) return { state: 'failing', failing }
228
- return { state: items.every((c) => c.done) ? 'passing' : 'pending', failing: [] }
229
- },
230
- /** Merges a pull request: { sha }. GitHub refuses when it can't be merged as it is (conflicts, rules). */
231
- async merge (number, { method = 'squash', title = '' } = {}) {
232
- const r = await req('PUT', `/pulls/${Number(number)}/merge`, { merge_method: method, ...(title ? { commit_title: title } : {}) }, `merging #${number}`)
233
- return { sha: r.sha, merged: !!r.merged }
234
- },
235
- async deleteBranch (branch) {
236
- try { await req('DELETE', `/git/refs/heads/${enc(branch)}`, null, `deleting ${branch}`) } catch (err) { if (err.status !== 404 && err.status !== 422) throw err }
237
- }
238
- }
239
- }
240
-
241
- /** Why GitHub refused, in words for whoever asked (never the token). */
242
- export function writeProblem (err, repo) {
243
- const s = err && err.status
244
- if (s === 401) return `the session's GitHub token was refused by GitHub (expired or revoked): ask the session owner for a new one (quilt_github_token)`
245
- if (s === 403 || s === 404) return `the session's GitHub token can't write to ${repo}: the owner needs a token with "Contents: Read and write" on it (and "Pull requests: Read and write" to open pull requests), set with quilt_github_token`
246
- if (s === 409) return `${repo} is empty or being changed: try again`
247
- if (s === 422) return `GitHub refused: ${err.body && err.body.message ? err.body.message : 'the branch moved meanwhile'}`
248
- return err && err.message ? err.message : String(err)
249
- }
250
-
251
- /**
252
- * Makes the commit on GitHub: `files` (Map path -> Buffer|null) onto `branch`, which starts at
253
- * `from` when it doesn't exist yet (the session's commit), checked against `base` (the commit
254
- * the session's copy is measured from; null for a branch of the asker's own, which only they write to). Resolves { sha, branch, created, changed, same } or
255
- * throws CommitRefused (nothing to commit, files changed on GitHub) or a GitHub error.
256
- */
257
- export async function commitOnGitHub (gh, { files, branch, from, base, message, author, tries = 2 }) {
258
- for (let attempt = 1; ; attempt++) {
259
- const head = await gh.ref(branch)
260
- const parent = head || from
261
- const pc = await gh.commit(parent)
262
- const parentBlobs = await gh.blobs(pc.tree)
263
- if (!parentBlobs) throw new CommitRefused('The repository is too large for GitHub to list in one go; commit from a folder with git.')
264
- const baseBlobs = !base ? null : base === parent ? parentBlobs : await gh.blobs((await gh.commit(base)).tree)
265
- if (base && !baseBlobs) throw new CommitRefused('The repository is too large for GitHub to list in one go; commit from a folder with git.')
266
- const plan = planCommit(files, { parent: parentBlobs, base: baseBlobs })
267
- if (plan.moved.length) {
268
- throw new CommitRefused(`${plan.moved.join(', ')} changed on ${head ? branch : 'GitHub'} since the session's copy was taken, so committing the session's copy would undo that. ` +
269
- 'Bring those commits into the session first (quilt_sync_branch; the session then has both), then commit again.')
270
- }
271
- if (!plan.changes.length) return { sha: parent, branch, created: false, changed: [], same: plan.same, nothing: true, exists: !!head }
272
- const modes = await gh.modes(pc.tree)
273
- const entries = []
274
- for (const c of plan.changes) {
275
- if (c.deleted) entries.push({ path: c.path, mode: modes.get(c.path) || '100644', type: 'blob', sha: null })
276
- else entries.push({ path: c.path, mode: modes.get(c.path) || '100644', type: 'blob', sha: await gh.blob(c.bytes) })
277
- }
278
- const tree = await gh.tree(pc.tree, entries)
279
- const sha = await gh.newCommit({ message, tree, parent, author })
280
- try {
281
- if (head) await gh.move(branch, sha)
282
- else await gh.create(branch, sha)
283
- return { sha, parent, branch, created: !head, changed: plan.changes.map((c) => c.path), same: plan.same }
284
- } catch (err) {
285
- // Someone pushed to the branch meanwhile: start again from its new head.
286
- if ((err.status === 422 || err.status === 409) && attempt < tries) continue
287
- throw err
288
- }
289
- }
290
- }
291
-
292
- /** quilt_commit, the same for every agent (local MCP, hosted MCP, CLI). */
293
- export const GIT_COMMIT_DESCRIPTION = 'Commit your work to the branch you are on, on GitHub: the session\'s copy of exactly these files (by default every change of yours on record on this branch), with your message. ' +
294
- 'On a branch of your own (quilt/<you>/…, where taking a task puts you) the pull request to the session\'s branch opens by itself and is tied to your task. ' +
295
- 'The session\'s branch itself is open to agents only when the owner lets agents push to any branch; otherwise take a task or quilt_git_branch create first. ' +
296
- 'Refused for a file that changed on GitHub since the session\'s copy (quilt_sync_branch first) and for one holding others\' uncommitted changes (unless with_others). ' +
297
- 'Commit as soon as work is finished and tested: work that is only in the session has not shipped. When the owner keeps agents from committing, ask a person with quilt_request_commit.'
298
- export const gitCommitSchema = (z) => ({
299
- message: z.string().min(1).max(2000).describe('What the change does (the first line is the subject)'),
300
- files: z.array(z.string().min(1).max(500)).max(200).optional().describe('The files to commit, deleted ones too; default: every change of yours on record on this branch'),
301
- task: z.string().max(40).optional().describe('The task this is for: its title names the pull request, and the task follows it'),
302
- with_others: z.boolean().optional().describe('Also commit files that hold other people\'s uncommitted changes (they are named as co-authors)')
303
- })
304
-
305
- export const COMMIT_DESCRIPTION = 'For agents with no folder (hosted, over HTTP): commit your finished work to the repository on GitHub through the relay, since you have no git of your own; nobody else needs to be online. Agents with a folder push with git instead. ' +
306
- 'Quilt commits the session\'s copy of exactly these files (by default every change of yours on record on this branch) with your message, and everyone sees it in chat. ' +
307
- 'Where it goes is the session owner\'s call: by default a branch of your own (quilt/<you>/<topic>; pull_request: true opens a pull request to the session\'s branch); ' +
308
- 'when the owner lets agents commit to any branch, leaving branch out commits to the session\'s branch itself. ' +
309
- 'Refused for a file that changed on GitHub since the session\'s copy (bring it in with quilt_sync_branch first) and for one holding others\' uncommitted changes (unless with_others). ' +
310
- 'Commit as soon as work is finished and tested: work that is only in the session has not shipped. When the owner keeps agents from committing, ask a person with quilt_request_commit.'
311
-
312
- /** quilt_commit's arguments, the same for the local and the hosted tool (`z`: zod). */
313
- export const commitSchema = (z) => ({
314
- message: z.string().max(2000).optional().describe('The commit message: what the change does (a task\'s title by default)'),
315
- files: z.array(z.string().max(1024)).max(300).optional().describe('The files to commit, relative to the project\'s top folder (deleted ones too); default: every change of yours on record on this branch'),
316
- branch: z.string().max(200).optional().describe('The GitHub branch to commit to; default: a branch of your own (quilt/<you>/<topic>), or the session\'s branch when the owner lets agents commit there'),
317
- pull_request: z.boolean().optional().describe('Open a pull request from your branch to the session\'s branch'),
318
- with_others: z.boolean().optional().describe('Also commit files that hold others\' uncommitted changes (they are named as co-authors)'),
319
- task: z.string().max(40).optional().describe('The task this work was for: its title is the message and its changes the files, unless you give them')
320
- })
package/src/history.js CHANGED
@@ -256,14 +256,16 @@ export const LEGACY_DAYS = 90
256
256
 
257
257
  /**
258
258
  * The chronology once the session became a quilt at `at` (an ISO date, meta.quilt.at): entries from
259
- * before then are its legacy tail (plans/quilt-source-control/16), marked `legacy: true` for
259
+ * before then are its legacy tail (Seam's plans/quilt-source-control/16), marked `legacy: true` for
260
260
  * LEGACY_DAYS and then left out. Not a quilt (no `at`): the entries as they are.
261
261
  */
262
- export function legacyTail (entries, { at, now = Date.now() } = {}) {
262
+ export function legacyTail (entries, { at, now = Date.now(), history = false } = {}) {
263
263
  const t = at ? Date.parse(at) : NaN
264
264
  if (!Number.isFinite(t)) return entries
265
265
  if (now - t > LEGACY_DAYS * 24 * 60 * 60 * 1000) return entries.filter((e) => !(e && e.ts < t))
266
- return entries.map((e) => e && e.ts < t ? { ...e, legacy: true } : e)
266
+ // With history (the quilt is the session's imported git remote), git's own writes before then
267
+ // (pulls, first copies) are the log's imported patches: they are dropped, not shown twice.
268
+ return entries.filter((e) => !(history && e && e.ts < t && (e.pulled || e.seed))).map((e) => e && e.ts < t ? { ...e, legacy: true } : e)
267
269
  }
268
270
 
269
271
  function ago (ts, now) {
@@ -279,6 +281,14 @@ export function formatHistory (list, { withDiff = false, now = Date.now() } = {}
279
281
  if (!list.length) return 'No changes match.'
280
282
  const out = []
281
283
  for (const e of list) {
284
+ if (e.seam) {
285
+ // A commit in the quilt's log (quilt-history.js): who, which patch, its message, how many files.
286
+ const where = e.patch ? `patch ${e.patch.line && e.patch.line !== 'main' ? `${e.patch.line}:` : ''}${e.patch.no}` : `commit ${e.short || String(e.commit || '').slice(0, 12)}`
287
+ const n = e.files ? ` (${e.files} file${e.files === 1 ? '' : 's'})` : ''
288
+ out.push(`[${ago(e.ts, now)}] ${e.by} committed ${where}: ${e.message || '(no message)'}${n}${e.task ? ` [task ${e.task.id}]` : ''}`)
289
+ if (withDiff && e.diff) out.push(e.diff, '')
290
+ continue
291
+ }
282
292
  const span = e.from && e.ts - e.from > 60000 ? ` over ${Math.round((e.ts - e.from) / 60000)}m` : ''
283
293
  const what = e.detail ? ` (${e.detail})` : ''
284
294
  const task = e.task ? ` for "${e.task.title}" [${e.task.id}]` : ''