heyquilt 0.3.138 → 0.3.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/RELEASES.md +6 -0
  2. package/bin/quilt.js +7 -143
  3. package/package.json +1 -1
  4. package/src/account.js +0 -29
  5. package/src/api/memory-store.js +0 -29
  6. package/src/api/routes/teams.js +0 -23
  7. package/src/api/server.js +5 -19
  8. package/src/api/supabase-store.js +0 -52
  9. package/src/branch-view.js +6 -131
  10. package/src/control.js +2 -33
  11. package/src/duties.js +1 -1
  12. package/src/github-commit.js +3 -315
  13. package/src/mcp.js +8 -157
  14. package/src/relay-ask.js +3 -66
  15. package/src/relay-cluster.js +3 -5
  16. package/src/relay-mcp.js +6 -418
  17. package/src/relay-prune.js +6 -145
  18. package/src/relay-turn-sync.js +1 -1
  19. package/src/server.js +33 -1331
  20. package/src/session.js +52 -3526
  21. package/src/status.js +2 -58
  22. package/src/turn-sync.js +15 -2
  23. package/src/ui/changes.js +5 -14
  24. package/src/ui/chat.js +0 -1
  25. package/src/ui/common.js +0 -2
  26. package/src/ui/git-rule.js +2 -30
  27. package/src/ui/home.js +3 -76
  28. package/src/ui/session.js +34 -528
  29. package/src/ui/tree.js +2 -8
  30. package/src/ui/workspaces.js +2 -122
  31. package/src/ui-server.js +1 -38
  32. package/src/api/routes/github.js +0 -380
  33. package/src/branches.js +0 -194
  34. package/src/clash.js +0 -128
  35. package/src/commit-request.js +0 -59
  36. package/src/commit.js +0 -213
  37. package/src/git-hook.js +0 -48
  38. package/src/gitignore.js +0 -49
  39. package/src/gitstate.js +0 -1069
  40. package/src/relay-ci.js +0 -111
  41. package/src/relay-clash.js +0 -214
  42. package/src/relay-commit.js +0 -923
  43. package/src/relay-filekey.js +0 -46
  44. package/src/relay-git.js +0 -274
  45. package/src/relay-githttp.js +0 -166
  46. package/src/relay-github.js +0 -109
  47. package/src/relay-outbound.js +0 -1295
  48. package/src/relay-repo-moves.js +0 -43
  49. package/src/relay-sync.js +0 -795
  50. package/src/relay-upstream.js +0 -889
  51. package/src/sync-words.js +0 -42
  52. package/src/upstream.js +0 -192
package/src/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 git branch its members work on, each with its own files: quilt_status and quilt_branches list them and who is on each, and your first call on files puts you on the busiest one, where you stay until you pick another with quilt_git_branch (create: true loads a branch that is on GitHub but not yet in the session, at its latest commit; any other new branch starts from a copy of the files you have). ' +
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
- const boxable = (room, branch) => !!(room.meta && repoRecordFor(room, branch || room.defaultKey))
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 (quilt_git_commit) or quilt_unpack it' : `${c.by}'s unfinished work: not in anyone else's folder until they commit or unpack 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
- const onto = branchForHostedTask(room, task)
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.${merged ? ` Merged pull request #${merged.number} (${merged.head} → ${merged.base}) as ${String(merged.sha).slice(0, 7)}.` : ''} Verified: ${verifiedLine(task)}`)
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.`,