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/relay-mcp.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
// The older link-based server (handleAgentMcp) is kept for relays without sign-in.
|
|
11
11
|
import { authorOf } from './ui/chat.js'
|
|
12
12
|
import { projectRules, renderRules } from './project-rules.js'
|
|
13
|
-
import { cleanGit, branchBoard, branchesMarkdown } from './branches.js'
|
|
14
13
|
import crypto from 'node:crypto'
|
|
15
14
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
16
15
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
@@ -27,13 +26,6 @@ import { applyTextDiff } from './textdiff.js'
|
|
|
27
26
|
import { parseInvite } from './ui/invite.js'
|
|
28
27
|
import { scanInbox, renderInbox } from './inbox.js'
|
|
29
28
|
import { withContext, queryConversation, renderConversation, CONVERSATION_DESCRIPTION } from './conversation.js'
|
|
30
|
-
import { cleanFiles, describeCommitRequests, REQUEST_COMMIT_DESCRIPTION, MAX_REQUEST_FILES } from './commit.js'
|
|
31
|
-
import { commitRequestMessage } from './commit-request.js'
|
|
32
|
-
import { GIT_COMMIT_DESCRIPTION, gitCommitSchema, agentPrefix, slug } from './github-commit.js'
|
|
33
|
-
import { liveSyncOn, agentCommitRefusal, ownFiles } from './relay-outbound.js'
|
|
34
|
-
import { syncStatusText, SYNC_STATUS_DESCRIPTION } from './sync-words.js'
|
|
35
|
-
import { clashAction, CLASH_DESCRIPTION, clashSchema } from './relay-clash.js'
|
|
36
|
-
import { agentCommitsOf, repoRecordFor, POLICY_WORDS, pullRequestsFor, mergeForMember, describePulls, describeMerge, PULLS_DESCRIPTION, MERGE_DESCRIPTION, mergeSchema, agentsCanCommit, unpublishedFor, unpublishedRefusal, finishTaskFor, gitCommitFor, describeGitCommit, gitStatusFor, describeGitStatus, gitLogFor, describeGitLog, gitDiffFor, describeGitDiff, GIT_STATUS_DESCRIPTION, GIT_LOG_DESCRIPTION, GIT_DIFF_DESCRIPTION, GIT_REVERT_DESCRIPTION } from './relay-commit.js'
|
|
37
29
|
import { UpdateCheck } from './update-check.js'
|
|
38
30
|
import { TASK_WORKFLOW, pickupBrief, doneRefusal, verifiedEnough, verifiedLine, qaRefusal, qaNotesEnough, qaNotesLine, pickChecklist, MAX_VERIFIED } from './agent-task-workflow.js'
|
|
39
31
|
import { HistoryLog, queryHistory, parseSince, formatHistory, currentTask, lineDiff, legacyTail } from './history.js'
|
|
@@ -44,7 +36,6 @@ import { chatAbout, renderChatAbout, waitingOn, needsNoReply, renderUnanswered,
|
|
|
44
36
|
import { describeSubscription, describeWebhookTest, COALESCE_HELP, WEBHOOK_EVENTS } from './webhooks.js'
|
|
45
37
|
import { registerWorkspaceTools, bytesFetcher, WORKSPACE_GUIDE_HOSTED } from './workspace-tools.js'
|
|
46
38
|
import { DEFAULT_KEY, validBranchKey } from './branchdocs.js'
|
|
47
|
-
import { LOAD_FILES } from './relay-upstream.js'
|
|
48
39
|
import { isDiff, hasEntry, listPaths, readPath, readPaths, textOf, notePresent, removePath, baseOf, gitForSync } from './branch-view.js'
|
|
49
40
|
import { hostedRooms, hostedIn, newHosted } from './hosted-agents.js'
|
|
50
41
|
import { turnSyncStep, ROOM_KEY } from './relay-turn-sync.js'
|
|
@@ -100,8 +91,6 @@ export const HOSTED_INSTRUCTIONS =
|
|
|
100
91
|
'Do not edit files someone else has claimed: a refused write tells you who holds the file; ask for it in its file queue with quilt_request_file (a title, and up to 300 characters on what you plan) and carry on with other work: you are woken when it is handed to you. ' +
|
|
101
92
|
'When someone waits in the queue for a file you hold, every answer says so: finish your change, then hand it off with quilt_handoff and your context (what you changed, what is left). You cannot finish a task or release the file before. A claim whose holder does nothing for 20 minutes goes to the next in its queue. ' +
|
|
102
93
|
'Read what people said about a file before you change it: quilt_read_messages, and each write tells you what was said about that file. ' +
|
|
103
|
-
'On a branch that follows a GitHub repository, what you write stays in a box until you commit it (quilt_git_commit): everyone sees the change, but it is in nobody else\'s folder, so nobody else\'s git can commit it. Committing opens the box by itself; releasing everything does not. ' +
|
|
104
|
-
'quilt_unpack shares a box early when someone needs it: decide yourself, and ask your person first for a large, risky or critical change or when the project\'s rules say so. A file boxed by someone else (quilt_status lists Boxed work, and quilt_read_file says so) is their unfinished work: do not build on it; ask for it with quilt_request_file. ' +
|
|
105
94
|
'This rule is enforced for agents: while someone who messaged or mentioned you waits for an answer, writes, claims and task changes are refused until you answer with quilt_message. ' +
|
|
106
95
|
PERSON_CHAT + ' ' +
|
|
107
96
|
'Everyone sees your changes on their own disk within moments. ' +
|
|
@@ -111,11 +100,7 @@ export const HOSTED_INSTRUCTIONS =
|
|
|
111
100
|
'You can be in several sessions at once (quilt_join_session each): every tool then takes session: <id> (quilt_session_info lists them; a webhook event or inbox entry says which session it is from), and a call that names none is refused, so an answer never lands in the wrong session. ' +
|
|
112
101
|
'You are connected over HTTP, so you show as online for 30 minutes after each tool call; while idle, people reach you in the way you chose with quilt_wake. ' +
|
|
113
102
|
'HTTP is only for an agent with no computer. If you can run shell commands anywhere (a computer, a cloud machine, a sandbox), you must use the Quilt CLI there instead (`quilt agent join`, then `quilt join <invite> --agent <your name>`): files sync to disk and you show as live. ' +
|
|
114
|
-
'A session has a branch for each
|
|
115
|
-
'Commits pushed or merged on GitHub come into your branch by themselves: through a member\'s folder on it when one is online and can fetch, otherwise the relay brings them in from GitHub about every 10 minutes. quilt_sync_branch asks for that now (at most once a minute per branch), for example after a PR merged. ' +
|
|
116
|
-
'When those commits and the session\'s uncommitted work changed the same files, Quilt merges them on its own (where both changed the same lines, the newer edit stays, and whoever wrote the other lines is told): there is never a merge for you to do. ' +
|
|
117
|
-
'A private repository needs a read-only GitHub token on the relay: when quilt_status says the relay can\'t read it, ask the session owner (quilt_github_token sets it, owner only). You have no folder here, so git is the quilt_git_* tools: taking a task (quilt_move_task to doing) puts you on a branch of your own, quilt/<you>/<task>; quilt_git_commit commits your changed files there and opens the pull request tied to the task; quilt_git_status says where you stand, quilt_git_diff what differs from GitHub, quilt_git_log what was committed, quilt_git_revert puts GitHub\'s version of a file back. The session\'s branch itself is open to agents only when the owner lets agents push to any branch. Work only in the session has not shipped: commit before you move a task to QA. Done merges the task\'s pull request once its checks pass: a person moves it to Done, or you when the owner lets agents commit to any branch. quilt_git_pull_requests lists open pull requests with their checks; quilt_git_merge merges one (agents: when the owner lets agents commit to any branch). When agents may not commit, ask a person with quilt_request_commit. Pushes and merges on GitHub reach the session within seconds. ' +
|
|
118
|
-
'When a branch syncs with GitHub (quilt_git_status and quilt_branches say so), pushes and merges on GitHub come into the session by themselves, and nothing goes to GitHub until someone commits: quilt_git_commit commits your own changes (with_others: true takes others\' changes in the same files too, as co-authors). A file GitHub and the session both changed is held, and its merger proposes the merge with quilt_clash (the reviewer accepts). Releasing a file only lets go of your claim: it doesn\'t publish anything by itself. ' +
|
|
103
|
+
'A session has a branch for each branch its members work on, each with its own files: your first call on files puts you on the busiest one. A session in Seam keeps its work in squares: the seam_* tools commit, merge and read history. ' +
|
|
119
104
|
MAIL_GUIDE + ' ' +
|
|
120
105
|
TASK_WORKFLOW
|
|
121
106
|
|
|
@@ -132,21 +117,6 @@ const REMOVED = 'You are no longer in that session. Ask for a new invite and cal
|
|
|
132
117
|
const NEEDS_ROOM_PASS = 'Reconnecting you to the session. Call the same tool again.'
|
|
133
118
|
|
|
134
119
|
const id = () => crypto.randomBytes(8).toString('hex')
|
|
135
|
-
const BRING_IN_NOTE = 'Commits pushed or merged elsewhere come into the session by themselves: the folder of a member on that branch fetches about once a minute and brings them in. While no folder on a branch can (none online, or only ones that can\'t fetch, are held or diverged, or are a viewer\'s), the relay brings them in from GitHub itself, about every 10 minutes; quilt_sync_branch asks it to look now.'
|
|
136
|
-
|
|
137
|
-
/** For a hosted agent: what the relay does about new commits on its branch, and when it last looked; or ''. */
|
|
138
|
-
function relayStatus (room, key) {
|
|
139
|
-
const rec = room.meta && room.meta.upstreams ? room.meta.upstreams[room.resolveKey ? room.resolveKey(key) : key] : null
|
|
140
|
-
if (!rec || !rec.repo) return ''
|
|
141
|
-
const up = rec.name || 'its upstream'
|
|
142
|
-
if (!rec.checkedAt) return `The relay brings commits from ${up} into \`${key}\` while no folder on it is online; it hasn't looked yet.`
|
|
143
|
-
const bits = [`The relay last checked ${up} for new commits ${ago(rec.checkedAt)}.`]
|
|
144
|
-
if (rec.brought && rec.brought.at) bits.push(`It brought in ${rec.brought.count} commit${rec.brought.count === 1 ? '' : 's'} (${rec.brought.files} file${rec.brought.files === 1 ? '' : 's'}) ${ago(rec.brought.at)}.`)
|
|
145
|
-
if (rec.clash) bits.push(`Its clash with ${up} is task ${rec.clash.task} on the board.`)
|
|
146
|
-
if (rec.problem) bits.push(`Note: ${rec.problem}.`)
|
|
147
|
-
return bits.join(' ')
|
|
148
|
-
}
|
|
149
|
-
|
|
150
120
|
/** The chat `me` can see: public messages, and direct ones to or from them. */
|
|
151
121
|
const chatFor = (doc, me) => doc.getArray('chat').toArray().filter((m) => m && m.id && (!m.to || m.to === me || m.by === me))
|
|
152
122
|
|
|
@@ -205,25 +175,6 @@ function sessionTools (server, ctx) {
|
|
|
205
175
|
}
|
|
206
176
|
// Files, claims and history are a branch's: the agent's own (hosted agents can choose one), or the session's active branch.
|
|
207
177
|
const branchOf = (room) => ctx.branch ? ctx.branch(room) : room.activeBranch ? room.activeBranch() : null
|
|
208
|
-
/**
|
|
209
|
-
* An agent without a folder taking a task goes onto a branch of its own for it (quilt/<agent>/<task>,
|
|
210
|
-
* a copy of the default branch's files), as a folder agent's checkout does. Stays on a branch of its own
|
|
211
|
-
* that still has an open task; nothing when agents may not commit, or this relay has no branch documents.
|
|
212
|
-
*/
|
|
213
|
-
const branchForHostedTask = (room, task) => {
|
|
214
|
-
if (ctx.kind !== 'agent' || !ctx.setBranch || !task || !task.title || room.meta.layout !== 2) return null
|
|
215
|
-
if (!agentsCanCommit(room, room.defaultKey)) return null
|
|
216
|
-
const prefix = agentPrefix(me)
|
|
217
|
-
const here = branchOf(room) || room.defaultKey
|
|
218
|
-
if (here.startsWith(prefix) && readTasks(room.doc.getMap('tasks')).some((t) => t && !t.archived && t.column !== 'done' && t.pr && t.pr.branch === here)) return null
|
|
219
|
-
let name = `${prefix}${slug(task.title)}`
|
|
220
|
-
for (let n = 2; room.meta.branches[room.resolveKey(name)] && n < 50; n++) name = `${prefix}${slug(task.title)}-${n}`
|
|
221
|
-
try {
|
|
222
|
-
if (here !== room.defaultKey) ctx.setBranch(room, room.defaultKey, {})
|
|
223
|
-
ctx.setBranch(room, name, { create: true, by: me, editor: true })
|
|
224
|
-
return name
|
|
225
|
-
} catch { return null }
|
|
226
|
-
}
|
|
227
178
|
// The branch document the agent works on (its store entry), or null for a room without branch documents.
|
|
228
179
|
// The session's default branch may always start; any other new branch only for someone who may change files.
|
|
229
180
|
const branchEntry = (room) => {
|
|
@@ -292,13 +243,6 @@ function sessionTools (server, ctx) {
|
|
|
292
243
|
for (const h of room.hostedOnline ? room.hostedOnline() : []) if (h.name !== me && !out.some((p) => p.name === h.name)) out.push({ name: h.name, kind: h.kind, tool: 'hosted', hosted: true })
|
|
293
244
|
return out
|
|
294
245
|
}
|
|
295
|
-
// Each member's folder tells the room its git (branch, upstream, the repository's branches): one list.
|
|
296
|
-
const branchesOf = (room) => {
|
|
297
|
-
const members = []
|
|
298
|
-
for (const st of room.awareness.getStates().values()) if (st && st.name && st.git) members.push({ name: st.name, git: cleanGit(st.git) })
|
|
299
|
-
for (const p of peers(room)) if (p.persona) members.push({ name: p.name, git: null, persona: true })
|
|
300
|
-
return branchBoard(members, room.branchList ? room.branchList() : [])
|
|
301
|
-
}
|
|
302
246
|
// Claims as people see them: an older app's AI session ("Dana · login", of: Dana) is its person.
|
|
303
247
|
const claimsOf = (room) => (room.claimList ? room.claimList(branchOf(room)) : []).map((c) => (c.of || (c.queue || []).some((r) => r && r.of) ? { ...c, by: c.of || c.by, queue: (c.queue || []).map((r) => (r && r.of ? { ...r, by: r.of } : r)) } : c))
|
|
304
248
|
// Claims are per branch: who the agent is, on the branch it works on.
|
|
@@ -330,7 +274,8 @@ function sessionTools (server, ctx) {
|
|
|
330
274
|
// Claims follow this agent's writes: a file it changes that nobody holds is claimed for it, and let
|
|
331
275
|
// go when it hasn't written the file for a while (it has no end of turn Quilt can see).
|
|
332
276
|
const autoHeld = new Map() // `${roomId}\0${rel}` -> timer
|
|
333
|
-
|
|
277
|
+
// Boxes opened when a commit to the branch's repository landed: no session commits to git now.
|
|
278
|
+
const boxable = () => false
|
|
334
279
|
const roomKey = (room) => `${room.id || room.name || ''}\0${branchOf(room)}` // claims held per branch
|
|
335
280
|
const autoClaim = (room, rel) => {
|
|
336
281
|
const key = `${roomKey(room)}\0${rel}`
|
|
@@ -418,41 +363,18 @@ function sessionTools (server, ctx) {
|
|
|
418
363
|
if (away.length) lines.push('', '## Away (agents that work in turns, between turns: they read what you write at their next turn)', ...away.map((m) => `- ${m.name} (agent) · last here ${ago(m.lastSeen)}`))
|
|
419
364
|
const all = claimsOf(room)
|
|
420
365
|
const bxs = all.filter((c) => c.box)
|
|
421
|
-
if (bxs.length) lines.push('', '## Boxed work', ...bxs.map((c) => `- ${c.pattern}: ${c.by === me ? 'your unfinished work, in nobody else\'s folder until you commit it
|
|
366
|
+
if (bxs.length) lines.push('', '## Boxed work', ...bxs.map((c) => `- ${c.pattern}: ${c.by === me ? 'your unfinished work, in nobody else\'s folder until you commit it or quilt_unpack it' : `${c.by}'s unfinished work: not in anyone else's folder until they commit or unpack it`}`))
|
|
422
367
|
const cl = all.filter((c) => !c.box)
|
|
423
368
|
lines.push('', '## Claimed files', ...(cl.length ? cl.map((c) => `- ${c.pattern} by ${c.by}${c.note ? ` (${c.note})` : ''}${c.queue.length ? ` · waiting: ${c.queue.map((r) => `${r.by} ("${r.title}")`).join(', ')}` : ''}`) : ['- None.']))
|
|
424
369
|
const acts = activity.toArray().filter(Boolean).slice(-12).reverse()
|
|
425
370
|
lines.push('', '## Recent file changes', ...(acts.length ? acts.map((x) => x.kind === 'pulled' ? `- ${x.by} pulled ${x.detail || 'commits'} (${ago(x.ts)})` : x.kind === 'brought' ? `- ${x.by} brought in ${x.detail || 'commits'}${x.branch ? ` on ${x.branch}` : ''} (${ago(x.ts)})` : x.kind === 'switched' ? `- ${x.by} switched to ${x.branch} (${ago(x.ts)})` : `- ${x.by} ${x.kind} ${x.path} (${ago(x.ts)})`) : ['- None yet.']))
|
|
426
371
|
const msgs = chat.toArray().filter(visible).slice(-8)
|
|
427
372
|
lines.push('', '## Recent messages', ...(msgs.length ? msgs.map(fmtMsg) : ['- None.']))
|
|
428
|
-
const asked = [...doc.getMap('commitRequests').values()].filter((r) => r && r.id && r.state === 'open').sort((x, y) => x.ts - y.ts)
|
|
429
|
-
if (asked.length) lines.push('', '## Commit requests', ...asked.map((r) => `- [${r.id}] ${r.by === me ? 'you' : r.by} (${ago(r.ts)}): ${r.message}${Array.isArray(r.files) ? ` (${r.files.length} files)` : ''}${r.error ? `. Last try: ${r.error}` : ''}`))
|
|
430
373
|
lines.push('', '## Tasks', taskMarkdown(readTasks(doc.getMap('tasks')), me, { tool: ctx.tool(), asAi: false, mentionYours: true }))
|
|
431
|
-
const br = branchesOf(room)
|
|
432
|
-
if (br.length) {
|
|
433
|
-
lines.push('', '## Branches', branchesMarkdown(br, { limit: 6, mine: branchOf(room) }))
|
|
434
|
-
if (ctx.branch) lines.push('Work on another branch with quilt_git_branch.')
|
|
435
|
-
// Hosted agents learn when the relay last looked for new commits on their branch (relay-upstream.js).
|
|
436
|
-
const said = relayStatus(room, branchOf(room))
|
|
437
|
-
if (said) lines.push(said)
|
|
438
|
-
}
|
|
439
374
|
if (ctx.webhook) { const w = ctx.webhook.get(); lines.push('', w ? (w.email ? 'Quilt wakes you by email (quilt_wake changes it).' : `Webhook: Quilt POSTs to ${w.url} on ${w.events.join(', ')}.`) : 'No webhook or email wake: choose how Quilt wakes you with quilt_wake.') }
|
|
440
375
|
return text(lines.join('\n') + ctx.warn(room))
|
|
441
376
|
})
|
|
442
377
|
|
|
443
|
-
tool('quilt_sync_status', {
|
|
444
|
-
description: SYNC_STATUS_DESCRIPTION,
|
|
445
|
-
inputSchema: {}
|
|
446
|
-
}, (_, { room, branch }) => text(syncStatusText(room.branchList ? room.branchList() : [], { mine: branch ? branch.key : '' })))
|
|
447
|
-
|
|
448
|
-
tool('quilt_branches', {
|
|
449
|
-
description: 'The session\'s git branches: which branch each member\'s folder is on, which AI sessions and worktrees work on which branch, how each stands against its upstream (behind, ahead, diverged), and who committed last.',
|
|
450
|
-
inputSchema: {}
|
|
451
|
-
}, (_, { room }) => {
|
|
452
|
-
const br = branchesOf(room)
|
|
453
|
-
return text(`${branchesMarkdown(br, { limit: 40 })}\n\n${BRING_IN_NOTE}`)
|
|
454
|
-
})
|
|
455
|
-
|
|
456
378
|
// Seam (Seam's plans/quilt-source-control/16), behind QUILT_SOURCE_URL: what making this session a
|
|
457
379
|
// quilt would do. Only the dry run: the move itself is the owner's, from the app.
|
|
458
380
|
if (process.env.QUILT_SOURCE_URL) {
|
|
@@ -555,11 +477,6 @@ function sessionTools (server, ctx) {
|
|
|
555
477
|
}
|
|
556
478
|
}, async ({ id, column, qaNotes, verified }, { room, doc, files, branch }) => {
|
|
557
479
|
{ const w = waitRefusal(doc, 'quilt_move_task', room); if (w) return fail(w) }
|
|
558
|
-
// An agent's finished work goes to git before QA, when it can commit itself (relay-commit.js); Done merges the task's pull request.
|
|
559
|
-
if (column === 'qa' && ctx.kind === 'agent' && !Object.values(room.meta && room.meta.upstreams || {}).some((rec) => rec && rec.clash && rec.clash.task === id)) {
|
|
560
|
-
const u = await unpublishedFor(room, { key: branch ? branch.key : DEFAULT_KEY, who: me, task: id, column }, { fetch: room.githubFetch })
|
|
561
|
-
if (u.unpublished.length) return fail(unpublishedRefusal(readTasks(taskMap(doc)).find((t) => t.id === id), u.unpublished, column, []))
|
|
562
|
-
}
|
|
563
480
|
const err = writable(room)
|
|
564
481
|
if (err) return fail(err)
|
|
565
482
|
try {
|
|
@@ -568,11 +485,6 @@ function sessionTools (server, ctx) {
|
|
|
568
485
|
if (column === 'qa' && !qaNotesEnough(qaNotes)) return fail(qaRefusal({ task: cur, checklist: await checklistOf(files, room, branch) }))
|
|
569
486
|
if (column === 'done' && !verifiedEnough(verified)) return fail(doneRefusal({ task: cur, checklist: await checklistOf(files, room, branch) }))
|
|
570
487
|
{ const q = (column === 'done' || column === 'qa') && renderQueued(queuedFor(claimsOf(room), me), 'move the task again'); if (q) return fail(q) }
|
|
571
|
-
let merged = null
|
|
572
|
-
if (column === 'done' && cur.column !== 'done') {
|
|
573
|
-
const f = await finishTaskFor(room, { who: { name: me, kind: ctx.kind || 'human', access: ctx.access(room) }, key: branch ? branch.key : DEFAULT_KEY, task: id }, { fetch: room.githubFetch })
|
|
574
|
-
merged = f.merged
|
|
575
|
-
}
|
|
576
488
|
const patch = { id, column }
|
|
577
489
|
if (column === 'qa') patch.qaNotes = qaNotes
|
|
578
490
|
if (column === 'done') patch.verified = verified
|
|
@@ -583,11 +495,10 @@ function sessionTools (server, ctx) {
|
|
|
583
495
|
const history = (tf.length ? all.filter((e) => tf.includes(e.path)) : all).slice(-8)
|
|
584
496
|
const claims = claimsOf(room).filter((c) => !tf.length || tf.some((f) => globMatcher(c.pattern)(f)))
|
|
585
497
|
if (quiltOf(room.meta)) return text(pickupBrief({ task: { ...task, comments: readComments(doc.getMap('taskComments'), task.id) }, history, claims, checklist: await checklistOf(files, room, null), me, workflow: seamWorkflow(TASK_WORKFLOW, { hosted: true }) }))
|
|
586
|
-
|
|
587
|
-
return text(pickupBrief({ task: { ...task, comments: readComments(doc.getMap('taskComments'), task.id) }, history, claims, checklist: await checklistOf(files, room, branchEntry(room)), me }) + (onto ? `\nYou are on ${onto} now (Quilt made it for this task, a copy of ${room.defaultKey}'s files): write your files here and quilt_git_commit them; the pull request opens by itself. Move the task to QA when it is done and tested.` : ''))
|
|
498
|
+
return text(pickupBrief({ task: { ...task, comments: readComments(doc.getMap('taskComments'), task.id) }, history, claims, checklist: await checklistOf(files, room, branchEntry(room)), me }))
|
|
588
499
|
}
|
|
589
500
|
if (column === 'qa') return text(`Moved "${task.title}" to QA. Notes: ${qaNotesLine(task)}`)
|
|
590
|
-
if (column === 'done') return text(`Moved "${task.title}" to Done
|
|
501
|
+
if (column === 'done') return text(`Moved "${task.title}" to Done. Verified: ${verifiedLine(task)}`)
|
|
591
502
|
return text(`Moved "${task.title}" to ${columnName(task.column)}.`)
|
|
592
503
|
} catch (e) { return fail(e.message) }
|
|
593
504
|
})
|
|
@@ -839,247 +750,6 @@ function sessionTools (server, ctx) {
|
|
|
839
750
|
return text(msgs.length ? msgs.map(fmtMsg).join('\n') : 'No messages yet.')
|
|
840
751
|
})
|
|
841
752
|
|
|
842
|
-
// ------------------------------------------------------ commit timing --
|
|
843
|
-
// The same requests as a folder's quilt_request_commit (Session.requestCommit): a message to
|
|
844
|
-
// whoever commits, so viewers and members who may not post are refused.
|
|
845
|
-
const commitMap = (doc) => doc.getMap('commitRequests')
|
|
846
|
-
const commitState = (room, doc) => {
|
|
847
|
-
const busy = []
|
|
848
|
-
for (const st of room.awareness.getStates().values()) {
|
|
849
|
-
if (!st || !st.name || st.name === me) continue
|
|
850
|
-
const a = st.agent
|
|
851
|
-
if (a && a.sharing !== false && a.status === 'working') busy.push(`${st.name}'s ${a.tool || 'AI'} is working`)
|
|
852
|
-
else if (st.work && st.work.state === 'working') busy.push(`${st.name} is working${st.work.note ? `: ${String(st.work.note).slice(0, 200)}` : ''}`)
|
|
853
|
-
}
|
|
854
|
-
const requests = [...commitMap(doc).values()].filter((r) => r && r.id).sort((a, b) => a.ts - b.ts)
|
|
855
|
-
return { busy, open: requests.filter((r) => r.state === 'open'), recent: requests.filter((r) => r.state === 'done').slice(-5) }
|
|
856
|
-
}
|
|
857
|
-
const describeCommits = (c) => [
|
|
858
|
-
c.busy.length ? `⏳ Still working: ${c.busy.join('; ')}` : '✅ Everyone else\'s AI is idle: a good moment to commit.',
|
|
859
|
-
describeCommitRequests(c)
|
|
860
|
-
].join('\n')
|
|
861
|
-
|
|
862
|
-
tool('quilt_request_commit', {
|
|
863
|
-
description: REQUEST_COMMIT_DESCRIPTION,
|
|
864
|
-
inputSchema: {
|
|
865
|
-
message: z.string().max(500).optional().describe('The commit message: what the change does (a task\'s title by default)'),
|
|
866
|
-
files: z.array(z.string().max(1024)).max(500).optional().describe('The files you changed, relative to the project\'s top folder (deleted ones too)'),
|
|
867
|
-
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')
|
|
868
|
-
}
|
|
869
|
-
}, ({ message, files, task }, { room, doc, fdoc }) => {
|
|
870
|
-
{ const w = waitRefusal(doc, 'quilt_request_commit', room); if (w) return fail(w) }
|
|
871
|
-
const err = writable(room)
|
|
872
|
-
if (err) return fail(err)
|
|
873
|
-
const a = ctx.access(room)
|
|
874
|
-
if (a && a.role === 'viewer') return fail('Viewers can\'t ask for commits.')
|
|
875
|
-
if (ctx.kind === 'agent' && agentsCanCommit(room, branchOf(room) || DEFAULT_KEY)) return fail('Commit it yourself: quilt_git_commit (the files you changed and a message) commits them to the branch you are on, with nobody else needed; on a branch of your own the pull request opens by itself. Asking a person is for when agents may not commit here.')
|
|
876
|
-
if (a && a.talk === false) return fail(TALK_REFUSED)
|
|
877
|
-
const t = task ? readTasks(taskMap(doc)).find((x) => x.id === String(task)) : null
|
|
878
|
-
if (task && !t) return fail(`No task ${task} on the board.`)
|
|
879
|
-
const msg = String(message || (t ? t.title : '')).trim().slice(0, 500)
|
|
880
|
-
if (!msg) return fail('Say what the commit is for.')
|
|
881
|
-
let list = cleanFiles(files || [])
|
|
882
|
-
if (files && files.length && !list.length) return fail('None of those are files in the project (give paths relative to its top folder).')
|
|
883
|
-
if (!list.length) {
|
|
884
|
-
// What was changed for the task, or by this agent, on record in the branch's history.
|
|
885
|
-
for (const e of fdoc.getArray('history').toArray()) {
|
|
886
|
-
if (!e || e.pulled || typeof e.path !== 'string') continue
|
|
887
|
-
if (t ? e.task && e.task.id === t.id : e.by === me) { if (!list.includes(e.path)) list.push(e.path) }
|
|
888
|
-
}
|
|
889
|
-
if (t) for (const f of t.files || []) if (!list.includes(f)) list.push(f)
|
|
890
|
-
list = cleanFiles(list.slice(-MAX_REQUEST_FILES))
|
|
891
|
-
}
|
|
892
|
-
if (!list.length) return fail(t ? `Task ${t.id} has no changed files on record: give them in files.` : 'Quilt has no changes of yours on record: give the files in files.')
|
|
893
|
-
const key = branchOf(room)
|
|
894
|
-
const r = { id: crypto.randomBytes(6).toString('hex'), by: me, message: msg, ...(key ? { branch: key } : {}), ts: Date.now(), state: 'open', files: list, ...(t ? { task: { id: t.id, title: t.title } } : {}) }
|
|
895
|
-
// The session owner is told with a direct message, as from a folder (commit-request.js).
|
|
896
|
-
const note = commitRequestMessage(r, { owner: room.meta && room.meta.ownerName, id: id(), defaultBranch: DEFAULT_KEY })
|
|
897
|
-
doc.transact(() => {
|
|
898
|
-
const map = commitMap(doc)
|
|
899
|
-
map.set(r.id, r)
|
|
900
|
-
if (note) {
|
|
901
|
-
const chat = doc.getArray('chat')
|
|
902
|
-
chat.push([note])
|
|
903
|
-
if (chat.length > 500) chat.delete(0, chat.length - 500)
|
|
904
|
-
}
|
|
905
|
-
const done = [...map.values()].filter((x) => x && x.state === 'done').sort((x, y) => x.ts - y.ts)
|
|
906
|
-
for (const x of done.slice(0, Math.max(0, done.length - 100))) map.delete(x.id)
|
|
907
|
-
}, AGENT)
|
|
908
|
-
return text(`Asked for a commit [${r.id}] of ${list.length} file${list.length === 1 ? '' : 's'}: ${list.slice(0, 12).join(', ')}${list.length > 12 ? ', …' : ''}. ` +
|
|
909
|
-
'A person with git on this branch commits it from their Quilt; you are told the commit when it is made.')
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
if (ctx.kind) {
|
|
913
|
-
// Git, for an agent with no folder: the quilt_git_* tools work on the branch the agent is on (its branch document).
|
|
914
|
-
const whoAmI = (room) => ({ name: me, kind: ctx.kind, access: ctx.access(room) })
|
|
915
|
-
const keyOf = (branch) => branch ? branch.key : DEFAULT_KEY
|
|
916
|
-
|
|
917
|
-
tool('quilt_git_commit', {
|
|
918
|
-
description: GIT_COMMIT_DESCRIPTION,
|
|
919
|
-
inputSchema: gitCommitSchema(z)
|
|
920
|
-
}, async (args, { room, doc, branch }) => {
|
|
921
|
-
{ const w = waitRefusal(doc, 'quilt_git_commit', room); if (w) return fail(w) }
|
|
922
|
-
// A branch that syncs with GitHub through the relay's mirror: your own changes (or the files named, or the task's)
|
|
923
|
-
// become one commit now, pushed forward only. Others' changes in those files go in only with with_others.
|
|
924
|
-
// An agent the owner's policy keeps off this branch commits to a branch of its own instead (below, with its pull request).
|
|
925
|
-
if (room.outbound && liveSyncOn(room, keyOf(branch)) && !(ctx.kind === 'agent' && agentCommitRefusal(room, keyOf(branch), me))) {
|
|
926
|
-
const key = keyOf(branch)
|
|
927
|
-
const own = ownFiles(room, key, { who: me, files: args.files, task: args.task })
|
|
928
|
-
if (own.refused) return fail(own.refused)
|
|
929
|
-
const t = args.task ? room.doc.getMap('tasks').get(String(args.task)) : null
|
|
930
|
-
const message = String(args.message || (t && t.title) || '').trim().slice(0, 2000)
|
|
931
|
-
if (!message) return fail('Say what the commit does (message).')
|
|
932
|
-
const r = await room.outbound.flush(key, { message, by: me, only: own.list, own: { who: me, withOthers: args.with_others === true } })
|
|
933
|
-
if (r.state === 'others') return fail(`These files also hold changes by others that are not in git yet: ${[...r.others].map(([rel, names]) => `${rel} (${names.join(', ')})`).join('; ')}. Leave them out (files), ask those people to commit first, or commit them together with with_others: true (they are named as co-authors).`)
|
|
934
|
-
const rec = (room.meta.upstreams || {})[key] || {}
|
|
935
|
-
if (r.state === 'pushed') return text(`Pushed ${r.sha.slice(0, 7)} to ${rec.repo || 'GitHub'} ${rec.ref || key}: "${r.subject}" (${r.changed.length} file${r.changed.length === 1 ? '' : 's'}: ${r.changed.slice(0, 10).join(', ')}${r.changed.length > 10 ? ', …' : ''})${r.live ? `, through pull request work on ${r.live}` : ''}.`)
|
|
936
|
-
if (r.state === 'nothing') return text(`Nothing to commit: ${own.list.length === 1 ? own.list[0] : 'those files'} on ${key} ${own.list.length === 1 ? 'is' : 'are'} already on GitHub as the session has ${own.list.length === 1 ? 'it' : 'them'}${r.skipped && r.skipped.claimed.length ? ` (held by someone: ${r.skipped.claimed.join(', ')})` : ''}.`)
|
|
937
|
-
if (r.state === 'paused') return fail(`GitHub can't take the commit right now (${rec.problem || r.why}): Quilt keeps it and makes it when GitHub answers.`)
|
|
938
|
-
if (r.state === 'protected') return fail(`${rec.ref || key} is protected on GitHub: ${r.why}`)
|
|
939
|
-
if (r.state === 'refused') return fail(`Not committed: ${r.why}.`)
|
|
940
|
-
return fail(`Could not push ${key} right now (${r.state}${r.why ? `: ${r.why}` : ''}): try quilt_git_commit again in a moment.`)
|
|
941
|
-
}
|
|
942
|
-
if (!room.commitFor) return fail('This relay can\'t commit yet.')
|
|
943
|
-
try {
|
|
944
|
-
const r = await gitCommitFor(room, { who: whoAmI(room), key: keyOf(branch), message: args.message, files: args.files, task: args.task, withOthers: args.with_others === true }, { fetch: room.githubFetch, log: room.log || (() => {}) })
|
|
945
|
-
return text(describeGitCommit(r, { policy: agentCommitsOf(room) }))
|
|
946
|
-
} catch (err) { return fail(err.message) }
|
|
947
|
-
})
|
|
948
|
-
|
|
949
|
-
tool('quilt_clash', {
|
|
950
|
-
description: CLASH_DESCRIPTION,
|
|
951
|
-
inputSchema: clashSchema(z)
|
|
952
|
-
}, async (args, { room, branch }) => {
|
|
953
|
-
try {
|
|
954
|
-
const r = await clashAction(room, { who: { name: me, kind: ctx.kind === 'agent' ? 'agent' : 'human', access: ctx.access(room) }, key: keyOf(branch), action: args.action, path: args.path || '', content: args.content ?? null, checks: args.checks || null, force: args.force === true })
|
|
955
|
-
return r.ok ? text(r.text) : fail(r.text)
|
|
956
|
-
} catch (err) { return fail(err.message) }
|
|
957
|
-
})
|
|
958
|
-
|
|
959
|
-
tool('quilt_git_status', {
|
|
960
|
-
description: GIT_STATUS_DESCRIPTION,
|
|
961
|
-
inputSchema: {}
|
|
962
|
-
}, async (_, { room, branch }) => {
|
|
963
|
-
try { return text(describeGitStatus(await gitStatusFor(room, { who: whoAmI(room), key: keyOf(branch) }, { fetch: room.githubFetch }))) } catch (err) { return fail(err.message) }
|
|
964
|
-
})
|
|
965
|
-
|
|
966
|
-
tool('quilt_git_log', {
|
|
967
|
-
description: GIT_LOG_DESCRIPTION,
|
|
968
|
-
inputSchema: { n: z.number().int().min(1).max(50).optional().describe('How many commits (default 10)') }
|
|
969
|
-
}, async ({ n }, { room, branch }) => {
|
|
970
|
-
try { return text(describeGitLog(await gitLogFor(room, { key: keyOf(branch), n: n || 10 }, { fetch: room.githubFetch }))) } catch (err) { return fail(err.message) }
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
tool('quilt_git_diff', {
|
|
974
|
-
description: GIT_DIFF_DESCRIPTION,
|
|
975
|
-
inputSchema: { files: z.array(z.string().min(1).max(500)).max(20).optional().describe('The files to compare; default: your uncommitted changes') }
|
|
976
|
-
}, async ({ files: list }, { room, branch }) => {
|
|
977
|
-
try { return text(describeGitDiff(await gitDiffFor(room, { who: whoAmI(room), key: keyOf(branch), files: list }, { fetch: room.githubFetch, lineDiff }))) } catch (err) { return fail(err.message) }
|
|
978
|
-
})
|
|
979
|
-
|
|
980
|
-
tool('quilt_git_revert', {
|
|
981
|
-
description: GIT_REVERT_DESCRIPTION,
|
|
982
|
-
inputSchema: { files: z.array(z.string().min(1).max(500)).min(1).max(20).describe('The files to put back as GitHub has them') }
|
|
983
|
-
}, async ({ files: list }, { room, doc, fdoc, branch, files, blobs, activity }) => {
|
|
984
|
-
{ const w = waitRefusal(doc, 'quilt_git_revert', room); if (w) return fail(w) }
|
|
985
|
-
const err = writable(room) || branchWritable(room, branch)
|
|
986
|
-
if (err) return fail(err)
|
|
987
|
-
const a = ctx.access(room)
|
|
988
|
-
if (a && a.role === 'viewer') return fail('You can only view this session; file changes are refused.')
|
|
989
|
-
try {
|
|
990
|
-
const d = await gitDiffFor(room, { who: whoAmI(room), key: keyOf(branch), files: list }, { fetch: room.githubFetch, lineDiff })
|
|
991
|
-
const { githubRepoOf, credentialsFor } = await import('./relay-commit.js')
|
|
992
|
-
const { githubWriter } = await import('./github-commit.js')
|
|
993
|
-
const { repo } = githubRepoOf(room, keyOf(branch))
|
|
994
|
-
const gh = githubWriter({ fetch: room.githubFetch, token: await credentialsFor(room, repo), owner: repo.owner, name: repo.name })
|
|
995
|
-
const done = []; const skipped = []
|
|
996
|
-
for (const f of d.files) {
|
|
997
|
-
const rel = cleanPath(f.path)
|
|
998
|
-
if (f.note === 'same') { skipped.push(`${rel} (already as on GitHub)`); continue }
|
|
999
|
-
if (f.note) { skipped.push(`${rel} (${f.note})`); continue }
|
|
1000
|
-
const refusal = a && changeRefusal(a, rel)
|
|
1001
|
-
if (refusal) { skipped.push(`${rel} (${refusal})`); continue }
|
|
1002
|
-
const claim = claimsOf(room).find((c) => c.by !== me && globMatcher(c.pattern)(rel))
|
|
1003
|
-
if (claim) { skipped.push(`${rel} (held by ${claim.by})`); continue }
|
|
1004
|
-
const was = d.head ? await gh.content(d.head, rel) : null
|
|
1005
|
-
const diff = branch && isDiff(branch)
|
|
1006
|
-
let fromGit = null
|
|
1007
|
-
if (diff && !hasEntry(branch, rel) && !branch.gone.has(rel)) { const r = await readPath(room, branch.key, branch, rel); fromGit = r.kind === 'text' ? r.text : null }
|
|
1008
|
-
fdoc.transact(() => doc.transact(() => {
|
|
1009
|
-
const existed = files.has(rel) || blobs.has(rel) || fromGit !== null
|
|
1010
|
-
let before = fromGit || ''
|
|
1011
|
-
if (was === null || was === undefined) {
|
|
1012
|
-
const y = files.get(rel); if (y) before = y.toString()
|
|
1013
|
-
if (diff) removePath(branch, rel, { by: me, base: baseOf(room, branch.key, branch) })
|
|
1014
|
-
else { files.delete(rel); blobs.delete(rel) }
|
|
1015
|
-
} else {
|
|
1016
|
-
blobs.delete(rel)
|
|
1017
|
-
let ytext = files.get(rel)
|
|
1018
|
-
if (!ytext) { ytext = new Y.Text(); if (fromGit) ytext.insert(0, fromGit); files.set(rel, ytext) }
|
|
1019
|
-
if (diff) notePresent(branch, rel, { base: baseOf(room, branch.key, branch) })
|
|
1020
|
-
before = ytext.toString()
|
|
1021
|
-
applyTextDiff(ytext, was.toString('utf8'))
|
|
1022
|
-
}
|
|
1023
|
-
const kind = was == null ? 'deleted' : existed ? 'edited' : 'created'
|
|
1024
|
-
activity.push([{ by: me, path: rel, kind, detail: `reverted to GitHub (${d.against})`, ...(branch ? { branch: branch.key } : {}), ts: Date.now() }])
|
|
1025
|
-
if (activity.length > ACTIVITY_CAP) activity.delete(0, activity.length - ACTIVITY_CAP)
|
|
1026
|
-
historyOf(room).record({ by: me, path: rel, kind, before, after: was == null ? '' : was.toString('utf8'), task: currentTask(readTasks(taskMap(doc)), me) })
|
|
1027
|
-
}, AGENT), AGENT)
|
|
1028
|
-
done.push(rel)
|
|
1029
|
-
}
|
|
1030
|
-
return text(`${done.length ? `Put back as GitHub's ${d.against} has ${done.length === 1 ? 'it' : 'them'}: ${done.join(', ')}.` : 'Nothing reverted.'}${skipped.length ? ` Left alone: ${skipped.join('; ')}.` : ''}`)
|
|
1031
|
-
} catch (err) { return fail(err.message) }
|
|
1032
|
-
})
|
|
1033
|
-
|
|
1034
|
-
tool('quilt_git_pull_requests', {
|
|
1035
|
-
description: PULLS_DESCRIPTION,
|
|
1036
|
-
inputSchema: {}
|
|
1037
|
-
}, async (_, { room, branch }) => {
|
|
1038
|
-
try { return text(describePulls(await pullRequestsFor(room, { key: keyOf(branch) }, { fetch: room.githubFetch }))) } catch (err) { return fail(err.message) }
|
|
1039
|
-
})
|
|
1040
|
-
|
|
1041
|
-
tool('quilt_git_merge', {
|
|
1042
|
-
description: MERGE_DESCRIPTION,
|
|
1043
|
-
inputSchema: mergeSchema(z)
|
|
1044
|
-
}, async (args, { room, doc, branch }) => {
|
|
1045
|
-
{ const w = waitRefusal(doc, 'quilt_git_merge', room); if (w) return fail(w) }
|
|
1046
|
-
try {
|
|
1047
|
-
const r = await mergeForMember(room, { who: whoAmI(room), key: keyOf(branch), number: args.number, branch: args.branch, method: args.method, deleteBranch: args.delete_branch, force: args.force === true }, { fetch: room.githubFetch, log: () => {} })
|
|
1048
|
-
return text(describeMerge(r))
|
|
1049
|
-
} catch (err) { return fail(err.message) }
|
|
1050
|
-
})
|
|
1051
|
-
|
|
1052
|
-
tool('quilt_agent_commits', {
|
|
1053
|
-
description: 'Session owner only: git for this session, for everyone in it (people and agents push with git through one GitHub connection, nothing for them to set up). "off": off for everyone (nobody pushes through Quilt; agents ask a person with quilt_request_commit, who commits from the app); "branches" (the default): on, agents push to branches of their own, quilt/<agent>/…, with pull requests for people to merge; "any": on, agents push to any branch, the session\'s own included. People push any branch while it is on. A rule its org, team or workspace sets is a ceiling: you may be stricter, never looser. For a hard limit on main, protect the branch on GitHub.',
|
|
1054
|
-
inputSchema: { mode: z.enum(['off', 'branches', 'any']) }
|
|
1055
|
-
}, ({ mode }, { room }) => {
|
|
1056
|
-
if (!ctx.access(room)?.owner) return fail('Only the session owner decides whether git is on here.')
|
|
1057
|
-
try { return text(`Now ${POLICY_WORDS[room.setAgentCommits ? room.setAgentCommits(mode) : mode]}.`) } catch (err) { return fail(err.message) }
|
|
1058
|
-
})
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
tool('quilt_commit_status', {
|
|
1062
|
-
description: 'Is it a good moment to commit? Lists open commit requests and whose AI is still working (not counting yours).',
|
|
1063
|
-
inputSchema: {}
|
|
1064
|
-
}, (_, { room, doc }) => text(describeCommits(commitState(room, doc))))
|
|
1065
|
-
|
|
1066
|
-
tool('quilt_commit_request_done', {
|
|
1067
|
-
description: 'Close commit requests whose work was committed with git by hand (or isn\'t needed any more). Without an id, every open request is closed.',
|
|
1068
|
-
inputSchema: {
|
|
1069
|
-
id: z.string().max(40).optional().describe('One request id; omit for all open ones'),
|
|
1070
|
-
hash: z.string().max(64).optional().describe('The commit that did it, if you know it')
|
|
1071
|
-
}
|
|
1072
|
-
}, ({ id: one, hash }, { room, doc }) => {
|
|
1073
|
-
{ const w = waitRefusal(doc, 'quilt_commit_request_done', room); if (w) return fail(w) }
|
|
1074
|
-
const err = writable(room)
|
|
1075
|
-
if (err) return fail(err)
|
|
1076
|
-
const open = [...commitMap(doc).values()].filter((r) => r && r.state === 'open' && (!one || r.id === one))
|
|
1077
|
-
if (!open.length) return text(one ? `No open commit request ${one}.` : 'No open commit requests.')
|
|
1078
|
-
const h = typeof hash === 'string' && /^[0-9a-f]{7,64}$/.test(hash) ? hash : ''
|
|
1079
|
-
doc.transact(() => { for (const r of open) commitMap(doc).set(r.id, { ...r, state: 'done', doneBy: me, hash: h, doneAt: Date.now() }) }, AGENT)
|
|
1080
|
-
return text(`Marked ${open.length} commit request${open.length === 1 ? '' : 's'} done.`)
|
|
1081
|
-
})
|
|
1082
|
-
|
|
1083
753
|
tool('quilt_list_files', {
|
|
1084
754
|
description: 'List the shared project files with who is working on them (claims).',
|
|
1085
755
|
inputSchema: { under: z.string().optional().describe('Only files under this folder') }
|
|
@@ -1770,88 +1440,6 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
|
|
|
1770
1440
|
return text(`Left room ${name}.${left.length ? ` You are still in ${left.join(', ')}.` : ''}`)
|
|
1771
1441
|
})
|
|
1772
1442
|
|
|
1773
|
-
mcp.registerTool('quilt_git_branch', {
|
|
1774
|
-
description: 'Work on another branch of the session: your file tools, claims and history then read and write that branch. quilt_status lists the branches and who is on each. create: true adds a branch that isn\'t in the session yet: one that is on GitHub is loaded at its latest commit (and the relay brings later commits on it in); any other starts from a copy of the files on the one you are on.',
|
|
1775
|
-
inputSchema: {
|
|
1776
|
-
branch: z.string().min(1).max(200).optional().describe('The branch to work on, e.g. quilt/<you>/<topic> or feature/login; without it, the session\'s branches are listed'),
|
|
1777
|
-
create: z.boolean().optional().describe('Add it to the session: from GitHub when it is there, otherwise a copy of your current branch\'s files'),
|
|
1778
|
-
session: SESSION_ARG
|
|
1779
|
-
}
|
|
1780
|
-
}, async ({ branch, create, session }) => {
|
|
1781
|
-
const c = current(session)
|
|
1782
|
-
if (c.error) return fail(c.error)
|
|
1783
|
-
{ const no = seamRefusal('quilt_git_branch', quiltName(c.room), { hosted: true }); if (no) return fail(no) }
|
|
1784
|
-
const room = c.room
|
|
1785
|
-
if (!branch) {
|
|
1786
|
-
const members = []
|
|
1787
|
-
for (const st of room.awareness.getStates().values()) if (st && st.name && st.git) members.push({ name: st.name, git: cleanGit(st.git) })
|
|
1788
|
-
const br = branchBoard(members, room.branchList ? room.branchList() : [])
|
|
1789
|
-
return text(br.length ? `${branchesMarkdown(br, { limit: 20, mine: room.hostedBranch(account) })}\nSwitch with quilt_git_branch and a branch name; create: true adds one.` : 'This session has one branch, and you are on it.')
|
|
1790
|
-
}
|
|
1791
|
-
const key = String(branch).trim()
|
|
1792
|
-
const editor = ctx.access(room)?.role !== 'viewer'
|
|
1793
|
-
let gh = null
|
|
1794
|
-
try {
|
|
1795
|
-
if (create && validBranchKey(key) && key !== DEFAULT_KEY && !room.meta.branches[room.resolveKey(key)]) {
|
|
1796
|
-
room.assertMigrated()
|
|
1797
|
-
room.assertBranchRoom(editor, 0) // refused before asking GitHub anything
|
|
1798
|
-
gh = await room.branchFromGitHub(account, key)
|
|
1799
|
-
if (!(gh && gh.state === 'loaded') && room.readyToCut) await room.readyToCut(account)
|
|
1800
|
-
}
|
|
1801
|
-
const r = room.setHostedBranch(account, key, { create: !!create, by: me, editor, github: gh && gh.state === 'loaded' ? gh : null })
|
|
1802
|
-
if (r.created && gh && gh.state === 'loaded' && gh.diff) return text(`Started ${r.branch} from GitHub (${gh.repo} at ${gh.head.slice(0, 7)}). Your file tools, claims and history use ${r.branch} now: files you haven't changed are read from git. People work on it with \`git checkout ${r.branch}\`, and their folder follows. The relay brings new commits on ${gh.upstream.name} in while no folder on it can; quilt_sync_branch asks for them now.`)
|
|
1803
|
-
if (r.created && gh && gh.state === 'loaded') {
|
|
1804
|
-
const n = gh.texts.size + gh.blobs.size
|
|
1805
|
-
const left = gh.skipped.length ? ` ${gh.skipped.length} file${gh.skipped.length === 1 ? ' was' : 's were'} too large for the relay and left out (${gh.skipped.slice(0, 5).join(', ')}${gh.skipped.length > 5 ? ', …' : ''}): a member's folder on it brings ${gh.skipped.length === 1 ? 'it' : 'them'} in.` : ''
|
|
1806
|
-
return text(`Loaded ${r.branch} from GitHub (${gh.repo} at ${gh.head.slice(0, 7)}, ${n} file${n === 1 ? '' : 's'}).${left} Your file tools, claims and history use ${r.branch} now. People work on it with \`git checkout ${r.branch}\`, and their folder follows. The relay brings new commits on ${gh.upstream.name} in while no folder on it can; quilt_sync_branch asks for them now.`)
|
|
1807
|
-
}
|
|
1808
|
-
if (r.created) {
|
|
1809
|
-
const why = !gh ? '' : {
|
|
1810
|
-
'not-github': `The session's repository (${gh.repo}) isn't on GitHub, so the relay can't load ${r.branch} from it: `,
|
|
1811
|
-
'no-branch': `${r.branch} isn't a branch on GitHub (${gh.repo}), so `,
|
|
1812
|
-
private: `The relay can't read ${gh.repo} on GitHub (a private repository needs a read-only token: ask the session owner to add one), so `,
|
|
1813
|
-
'token-refused': `The relay's GitHub token can't read ${gh.repo} (ask the session owner to give it read access), so `,
|
|
1814
|
-
'too-many': `${r.branch} on GitHub has more files than the relay loads at once (${LOAD_FILES}), so `,
|
|
1815
|
-
backoff: 'GitHub asked the relay to slow down, so ',
|
|
1816
|
-
error: `The relay couldn't read ${r.branch} from GitHub (${gh.message || 'no answer'}), so `
|
|
1817
|
-
}[gh.state] || ''
|
|
1818
|
-
const said = `Started ${r.branch} from ${r.from}, with a copy of its files. Your file tools, claims and history use ${r.branch} now. It isn't in git yet: people work on it with \`git checkout -b ${r.branch}\` (or \`git checkout ${r.branch}\` once it is pushed), and their folder follows.`
|
|
1819
|
-
return text(why ? `${why}${said[0].toLowerCase()}${said.slice(1)}` : said)
|
|
1820
|
-
}
|
|
1821
|
-
return text(`You are on ${r.branch} now: your file tools, claims and history use it.`)
|
|
1822
|
-
} catch (e) { return fail(e.message) }
|
|
1823
|
-
})
|
|
1824
|
-
|
|
1825
|
-
mcp.registerTool('quilt_sync_branch', {
|
|
1826
|
-
description: 'Bring commits made outside the session (a PR merged on GitHub, a push) into your branch now. A member\'s folder on the branch that can fetch does this about once a minute; otherwise the relay asks GitHub itself, about every 10 minutes, and this asks it now (at most once a minute per branch). It never merges git history: files that clash with the session\'s work become one task on the board for one agent to merge.',
|
|
1827
|
-
inputSchema: { session: SESSION_ARG }
|
|
1828
|
-
}, async ({ session }) => {
|
|
1829
|
-
const c = current(session)
|
|
1830
|
-
if (c.error) return fail(c.error)
|
|
1831
|
-
{ const no = seamRefusal('quilt_sync_branch', quiltName(c.room), { hosted: true }); if (no) return fail(no) }
|
|
1832
|
-
const room = c.room
|
|
1833
|
-
if (room.meta.layout !== 2 || !room.syncUpstreamNow) return fail('This session has no branches on the relay.')
|
|
1834
|
-
try {
|
|
1835
|
-
const key = room.hostedBranch(account)
|
|
1836
|
-
const r = await room.syncUpstreamNow(key)
|
|
1837
|
-
return text(r.said)
|
|
1838
|
-
} catch (e) { return fail(e.message) }
|
|
1839
|
-
})
|
|
1840
|
-
|
|
1841
|
-
mcp.registerTool('quilt_github_token', {
|
|
1842
|
-
description: 'Session owner only: set (or clear, with an empty token) the GitHub token the relay uses to bring commits in from a private repository while no folder can, to load branches from it, and, when it can write, to commit members\' work (quilt_commit). A fine-grained token for the repository with "Contents: Read" brings commits in; "Contents: Read and write" and "Pull requests: Read and write" let agents commit and open pull requests, within what quilt_agent_commits allows. It stays on the relay: it is never shown again, in any answer, status or log.',
|
|
1843
|
-
inputSchema: { token: z.string().max(300).describe('The token (github_pat_… or ghp_…), or "" to remove it'), session: SESSION_ARG }
|
|
1844
|
-
}, ({ token, session }) => {
|
|
1845
|
-
const c = current(session)
|
|
1846
|
-
if (c.error) return fail(c.error)
|
|
1847
|
-
{ const no = seamRefusal('quilt_github_token', quiltName(c.room), { hosted: true }); if (no) return fail(no) }
|
|
1848
|
-
if (!c.access || !c.access.owner) return fail('Only the session owner can set the GitHub token: ask them.')
|
|
1849
|
-
try {
|
|
1850
|
-
const r = c.room.setGithubToken(token)
|
|
1851
|
-
return text(r.githubToken ? 'Saved the GitHub token on the relay. It looks at the session\'s branches again now.' : 'Removed the GitHub token.')
|
|
1852
|
-
} catch (e) { return fail(e.message) }
|
|
1853
|
-
})
|
|
1854
|
-
|
|
1855
1443
|
// Secrets: names only. A hosted agent runs no commands, so it uses none (the answer says so).
|
|
1856
1444
|
mcp.registerTool('quilt_secret_names', {
|
|
1857
1445
|
description: `${SECRET_NAMES_DESCRIPTION} You are a hosted agent: you run no commands, so you can't use secrets yourself; this tells you which exist.`,
|