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.
- package/RELEASES.md +279 -17
- package/bin/quilt.js +62 -152
- package/desktop/main.js +16 -2
- package/package.json +3 -2
- package/src/account.js +0 -29
- package/src/agent-task-workflow.js +2 -2
- package/src/agents/common.js +13 -0
- package/src/ai-vm.js +9 -1
- package/src/api/activity.js +5 -0
- package/src/api/memory-store.js +16 -38
- package/src/api/notify.js +134 -0
- package/src/api/routes/browsers.js +29 -0
- package/src/api/routes/relay.js +26 -3
- package/src/api/routes/session-migrate.js +7 -4
- package/src/api/routes/sessions.js +73 -1
- package/src/api/routes/teams.js +0 -23
- package/src/api/server.js +17 -25
- package/src/api/supabase-store.js +43 -56
- package/src/autowork.js +182 -16
- package/src/branch-view.js +6 -131
- package/src/connection.js +36 -9
- package/src/control.js +131 -48
- package/src/duties.js +1 -1
- package/src/force-date.js +27 -0
- package/src/fsutil.js +13 -3
- package/src/github-commit.js +3 -315
- package/src/history.js +13 -3
- package/src/mcp.js +93 -167
- package/src/merge3.js +11 -3
- package/src/now-summary.js +113 -0
- package/src/pathrules.js +12 -0
- package/src/protocol.js +10 -2
- package/src/quilt-default.js +84 -0
- package/src/quilt-history.js +106 -0
- package/src/quilt-remote.js +78 -0
- package/src/relay-ask.js +3 -66
- package/src/relay-cluster.js +3 -5
- package/src/relay-lease.js +6 -2
- package/src/relay-mcp.js +154 -432
- package/src/relay-prune.js +6 -145
- package/src/relay-turn-sync.js +1 -1
- package/src/release-words.js +2 -2
- package/src/runner.js +20 -7
- package/src/seam-access.js +97 -0
- package/src/seam-claims.js +53 -0
- package/src/seam-url.js +15 -0
- package/src/server.js +1516 -1383
- package/src/session-follow.js +461 -27
- package/src/session-layout3.js +410 -4
- package/src/session-migrate.js +15 -11
- package/src/session-quilt.js +1 -1
- package/src/session-seam.js +233 -20
- package/src/session.js +745 -3438
- package/src/source-live/bring-in.js +283 -0
- package/src/source-live/live-block.js +504 -33
- package/src/source-live/live-square.js +16 -5
- package/src/status.js +7 -58
- package/src/turn-sync.js +15 -2
- package/src/ui/agent-guide.js +1 -1
- package/src/ui/app.css +85 -20
- package/src/ui/app.js +4 -1
- package/src/ui/changes.js +5 -14
- package/src/ui/chat.js +9 -4
- package/src/ui/cloud-ai-model.js +13 -0
- package/src/ui/cloud-ai.js +10 -2
- package/src/ui/common.js +6 -4
- package/src/ui/fileview.js +20 -7
- package/src/ui/force-words.js +19 -0
- package/src/ui/git-rule.js +9 -28
- package/src/ui/home.js +3 -223
- package/src/ui/loom-model.js +15 -1
- package/src/ui/quiltview.js +213 -0
- package/src/ui/seam-model.js +251 -0
- package/src/ui/session.js +377 -489
- package/src/ui/tree.js +2 -8
- package/src/ui/voice-call.js +83 -14
- package/src/ui/voice-model.js +39 -0
- package/src/ui/voice.js +167 -17
- package/src/ui/workspaces.js +2 -122
- package/src/ui-server.js +81 -66
- package/src/update-check.js +25 -1
- package/src/voice.js +13 -6
- package/src/api/routes/github.js +0 -393
- 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/git.js +0 -147
- package/src/gitignore.js +0 -49
- package/src/gitstate.js +0 -1033
- package/src/quilts/AGENTS.md +0 -1
- package/src/quilts/CLAUDE.md +0 -1
- package/src/quilts/accounts.js +0 -136
- package/src/quilts/affected.js +0 -132
- package/src/quilts/archiver.js +0 -175
- package/src/quilts/autorevert.js +0 -49
- package/src/quilts/bisect-run.js +0 -31
- package/src/quilts/blame.js +0 -103
- package/src/quilts/block.js +0 -347
- package/src/quilts/bucket.js +0 -25
- package/src/quilts/cbor.js +0 -92
- package/src/quilts/checks.js +0 -308
- package/src/quilts/claims.js +0 -144
- package/src/quilts/cli.js +0 -536
- package/src/quilts/client.js +0 -137
- package/src/quilts/conflict-format.js +0 -190
- package/src/quilts/describe.js +0 -296
- package/src/quilts/dogfood.js +0 -82
- package/src/quilts/errors.js +0 -8
- package/src/quilts/events.js +0 -171
- package/src/quilts/export-git.js +0 -130
- package/src/quilts/export-remote.js +0 -156
- package/src/quilts/export.js +0 -81
- package/src/quilts/fastmerge.js +0 -41
- package/src/quilts/hash.js +0 -44
- package/src/quilts/history.js +0 -266
- package/src/quilts/ids.js +0 -12
- package/src/quilts/import-follow.js +0 -236
- package/src/quilts/import-git.js +0 -294
- package/src/quilts/import.js +0 -196
- package/src/quilts/index.js +0 -94
- package/src/quilts/keys.js +0 -186
- package/src/quilts/limits.js +0 -67
- package/src/quilts/lines.js +0 -49
- package/src/quilts/live.js +0 -50
- package/src/quilts/merge-smart.js +0 -114
- package/src/quilts/merge.js +0 -501
- package/src/quilts/oauth.js +0 -202
- package/src/quilts/objects.js +0 -144
- package/src/quilts/pack.js +0 -118
- package/src/quilts/records.js +0 -211
- package/src/quilts/resolutions.js +0 -69
- package/src/quilts/reviews.js +0 -208
- package/src/quilts/routes.js +0 -186
- package/src/quilts/runner.js +0 -95
- package/src/quilts/server.js +0 -144
- package/src/quilts/service.js +0 -963
- package/src/quilts/setup.js +0 -41
- package/src/quilts/signing.js +0 -149
- package/src/quilts/squares.js +0 -239
- package/src/quilts/store-memory.js +0 -844
- package/src/quilts/store-rpc.js +0 -90
- package/src/quilts/store-supabase.js +0 -13
- package/src/quilts/tags.js +0 -64
- package/src/quilts/textdiff.js +0 -96
- package/src/quilts/tokens.js +0 -66
- package/src/quilts/tools.js +0 -403
- package/src/quilts/tree.js +0 -351
- package/src/quilts/webhook.js +0 -116
- package/src/quilts/window.js +0 -121
- package/src/quilts/xdiff.js +0 -435
- package/src/relay-ci.js +0 -111
- package/src/relay-clash.js +0 -214
- package/src/relay-commit.js +0 -918
- 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/ui/branches.js +0 -111
- package/src/upstream.js +0 -192
package/src/github-commit.js
CHANGED
|
@@ -1,320 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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}]` : ''
|