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/mcp.js CHANGED
@@ -3,7 +3,6 @@
3
3
  // and their AIs are doing, coordinate, and even join or start a session by
4
4
  // itself. It forwards to the local `quilt join` process for the project, or
5
5
  // runs the session itself when the agent joins on its own.
6
- import { syncStatusText, SYNC_STATUS_DESCRIPTION } from './sync-words.js'
7
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
8
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
9
8
  import { z } from 'zod'
@@ -18,7 +17,6 @@ import os from 'node:os'
18
17
  import { findDaemon, call as rawCall } from './control.js'
19
18
  import { parentPids } from './hooks.js'
20
19
  import { renderMessage, renderStatus } from './status.js'
21
- import { branchesMarkdown, upstreamLine, describeBranchSync } from './branches.js'
22
20
  import { DEFAULT_KEY } from './branchdocs.js'
23
21
  import { formatTasks, columnName, assigneeLabel, renderNextTask, COLUMNS } from './tasks.js'
24
22
  import { formatTaskDetails, MAX_COMMENT } from './task-comments.js'
@@ -33,16 +31,12 @@ import { quiltHome } from './legacy.js'
33
31
  import { TASK_WORKFLOW, pickupBrief, doneRefusal, verifiedEnough, verifiedLine, qaRefusal, qaNotesEnough, qaNotesLine, MAX_VERIFIED } from './agent-task-workflow.js'
34
32
  import { formatHistory } from './history.js'
35
33
  import { releaseWords } from './release-words.js'
36
- import { describeCommitRequests, REQUEST_COMMIT_DESCRIPTION } from './commit.js'
37
- import { POLICY_WORDS, unpublishedRefusal } from './relay-commit.js'
38
34
  import { renderInbox, describeEvent, INBOX_HOW } from './inbox.js'
39
35
  import { renderConversation, renderContext, CONVERSATION_DESCRIPTION } from './conversation.js'
40
36
  import { renderChatAbout, renderUnanswered, heldRefusal, renderQueueNotice, renderQueued, CHAT_RULES, RE_HELP, PERSON_CHAT } from './duties.js'
41
37
  import { describeSubscription, describeWebhookTest, COALESCE_HELP, WEBHOOK_EVENTS } from './webhooks.js'
42
38
  import { UpdateCheck } from './update-check.js'
43
39
  import { getSettings } from './settings.js'
44
- import { mergeAction } from './merges.js'
45
- import { CLASH_DESCRIPTION, clashSchema } from './relay-clash.js'
46
40
  import { describeSecretNames, SECRET_NAMES_DESCRIPTION, RUN_GUIDE } from './secret-names.js'
47
41
  import { appWorkspaces } from './app-workspaces.js'
48
42
  import { TRANSCRIPT_DESCRIPTION, renderLines } from './voice.js'
@@ -85,19 +79,17 @@ export const MCP_INSTRUCTIONS =
85
79
  '(open tasks assigned to you are listed first), add work with quilt_add_task, assign it with quilt_assign_task, ' +
86
80
  'and move a task with quilt_move_task when you start or finish it; quilt_task reads one in full with its comments, and quilt_comment_task leaves a note or handoff on it. ' +
87
81
  'quilt_history tells you who changed which file, when, with the diff: read it for the files you are about to touch. ' +
88
- 'If git refuses to pull because untracked files would be overwritten, those files came from the session: quilt_status lists them under Pulling (and whether they match); make way and pull with rm <files> && git pull --autostash, and Quilt keeps them for everyone. ' +
89
82
  'If the person you work for lets their AI pick up work by itself (their cloud AI, on the cloud machine they connected), finishing (quilt_set_work done, a task to QA or Done) hands you your next task from the board: start it. ' +
90
83
  'Before you change files, call quilt_before_edit with their paths: it tells you whether each is yours to edit (claiming free ones for you, so partners are refused instead of overwriting you), ' +
91
84
  'and shows what people said about those files in chat, so you know what was asked or planned before you change them. ' +
92
85
  'Do not edit a file it refuses: ask for it in its file queue with quilt_request_file (a title like "Working on <what> for <task>" and up to 300 characters on your plan) and carry on with other work; you are woken when it is handed to you, with the holder\'s context. ' +
93
86
  'Claims also follow your edits: a file you change that nobody holds is claimed for you until you finish. ' +
94
87
  'If an edit of yours was undone because someone else holds the file, your next quilt answer says so: do not retry; ask for it with quilt_request_file and carry on with other work. ' +
95
- 'In a folder that is a git repository, your edits to a file stay in a box until you commit them: everyone sees the change, but it is only in your folder, so nobody else\'s git can commit it. Committing the file (or putting it back as it was) opens the box by itself; finishing or releasing does not. ' +
96
- '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) is not in your folder yet: do not build on it; ask for it with quilt_request_file. ' +
88
+ 'A boxed file\'s edits are only in its holder\'s folder; quilt_unpack shares a box 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) is not in your folder yet: do not build on it; ask for it with quilt_request_file. ' +
97
89
  'When someone waits in the file queue for a file you hold, every quilt answer says so: finish the change you are making, then hand it off with quilt_handoff and your context (what you changed, what is left, gotchas). ' +
98
90
  'Until you do, you cannot finish (quilt_set_work done, a task to Done) or release that file. A claim whose holder does nothing in the session for 20 minutes goes to the next one waiting, or is released. ' +
99
91
  'For an agent of its own (not a person\'s AI session), every quilt answer starts with anything new for it (direct messages, mentions, tasks handed to it): act on it. ' +
100
- 'These rules are enforced for agents: while someone who messaged or mentioned you is waiting for an answer, the tools that move work on (claims, tasks, focus, merges, commits, quilt_set_work) refuse until you answer them with quilt_message; ' +
92
+ 'These rules are enforced for agents: while someone who messaged or mentioned you is waiting for an answer, the tools that move work on (claims, tasks, focus, quilt_set_work) refuse until you answer them with quilt_message; ' +
101
93
  'an edit to a file someone else holds is undone. ' +
102
94
  'When you start a piece of work, call quilt_set_work with "working" and a note of what it is (with quilt_set_focus), so the loom shows you stitching; when you finish, call it with "done": it lets go of the files claimed for you. ' +
103
95
  'Claim ahead (quilt_claim) only for a larger change across several files. ' +
@@ -105,15 +97,12 @@ export const MCP_INSTRUCTIONS =
105
97
  'If quilt_status starts with While you were away, the session changed since this folder last synced: read quilt_history for those files before editing them. ' +
106
98
  'For an agent of its own, mentions of it (@yourname) in chat, direct messages to it and tasks handed to it wait in quilt_inbox: read it when you start, and act on each one. ' +
107
99
  'Each comes with the conversation before it with its sender; when a message refers to something earlier you do not have, read back with quilt_conversation before you answer, never guess. ' +
108
- 'Git works here as usual: commit only the files you changed and push yourself. When the relay serves the session\'s git, your folder\'s origin is the relay: git signs in with your Quilt pass, and the relay checks each push (the session owner\'s policy, never a force-push, no ignored files) and puts it on GitHub, replayed onto GitHub\'s head if it moved (your folder follows). Otherwise your folder signs in to GitHub through Quilt (a short-lived token for this repository). Either way pushes stay within the owner\'s policy: by default branches of your own. Taking a task (quilt_move_task to doing) puts your folder on quilt/<you>/<task>; push there and Quilt opens the pull request for the task; move the task to QA when it is done and tested; Done merges the pull request once its checks pass (a person moves it to Done unless the owner lets agents push to any branch). When agents may not commit, ask a person with quilt_request_commit. ' +
109
- 'When a branch syncs with GitHub (quilt_branches says so), pushes and merges on GitHub come into the session by themselves; nothing goes to GitHub until someone commits and pushes. A file GitHub and the session both changed is merged by Quilt on its own (the newer edit stays where both changed the same lines). Releasing a file only lets go of your claim: it publishes nothing by itself. ' +
110
- 'Work only in the session has not shipped: push before you move a task to QA. Pushes and merges on GitHub reach the session within seconds: never pull by hand. ' +
111
100
  'When you are about to answer someone in chat, quilt_typing shows them you are typing; sending clears it. ' +
112
101
  'Chat: ' + CHAT_RULES.replace(/^Send a chat message\. /, '') + ' A message that needs nothing back is settled with quilt_inbox (no_reply: [its id]), not answered. ' +
113
102
  WAKE_STEP + ' On a computer the ways include a command Quilt runs on each event (`quilt join --on-mention`), `quilt inbox --wait`, the quilt://inbox resource (MCP push), a webhook, email and a schedule. ' +
114
103
  'In the session you are your person: your messages, claims and tasks are theirs, and people see you as them. "<person>\'s AI" ("Daniel\'s AI") is only that person\'s cloud AI, when they connected one (Quilt runs their own AI tool for them on their own cloud machine): write to it by that name. Nothing written or handed to "<person>\'s AI" comes to you; when your person asks you to work on tasks, take them from the board (quilt_tasks). ' +
115
104
  PERSON_CHAT + ' ' +
116
- 'Share what you are doing with quilt_share: when you start on a request (request and your plan) and when you finish (what you did and the files you changed). Partners see it in their feed, it puts your work on the task board, and it tells the host not to commit under you. ' +
105
+ 'Share what you are doing with quilt_share: when you start on a request (request and your plan) and when you finish (what you did and the files you changed). Partners see it in their feed, and it puts your work on the task board. ' +
117
106
  'When Claude Code is started with the quilt channel, they arrive on their own as <channel source="quilt"> events while you work: treat each like a request from that person, answer with quilt_message, and take a task with quilt_move_task. ' +
118
107
  RUN_GUIDE + ' ' +
119
108
  MAIL_GUIDE + ' ' +
@@ -335,21 +324,15 @@ export async function runMcp () {
335
324
  const brief = await call(d, 'POST', '/tasks/brief', { id })
336
325
  if (column === 'qa' && !qaNotesEnough(qaNotes)) return qaRefusal({ task: brief.task, checklist: brief.checklist })
337
326
  if (column === 'done' && !verifiedEnough(verified)) return doneRefusal({ task: brief.task, checklist: brief.checklist })
338
- // An agent's finished work goes to git before QA, when it can commit itself (relay-commit.js); Done merges its pull request (the daemon asks the relay).
339
- if (column === 'qa') {
340
- const u = await call(d, 'POST', '/unpublished', { task: id, column }).catch(() => null)
341
- if (u && u.unpublished && u.unpublished.length) return unpublishedRefusal(brief.task, u.unpublished, column, [])
342
- }
343
327
  const patch = { id, column }
344
328
  if (column === 'qa') patch.qaNotes = qaNotes
345
329
  if (column === 'done') patch.verified = verified
346
- const { task, merged, branch } = await call(d, 'POST', '/tasks/update', patch)
330
+ const { task } = await call(d, 'POST', '/tasks/update', patch)
347
331
  const q = column === 'doing' || column === 'qa' ? await quiltNow() : null
348
332
  if (q && column === 'doing') return pickupBrief({ ...brief, task: { ...task, comments: brief.task.comments }, workflow: seamWorkflow(TASK_WORKFLOW) })
349
- if (column === 'doing') return pickupBrief({ ...brief, task: { ...task, comments: brief.task.comments } }) + (branch ? `\nYour folder is on ${branch} now (Quilt made it for this task): commit only the files you change and push there; the pull request opens by itself. Move the task to QA when it is done and tested.` : '')
350
- // In a quilt a pull request from before the switch is git's: the task's work lands with seam_merge.
351
- if (column === 'qa') return `Moved "${task.title}" to QA. Notes: ${qaNotesLine(task)}` + (task.pr && !q ? ` Pull request #${task.pr.number} waits for a person to move the task to Done, which merges it.` : '') + await nextUp(d)
352
- if (column === 'done') return `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)}` + await nextUp(d)
333
+ if (column === 'doing') return pickupBrief({ ...brief, task: { ...task, comments: brief.task.comments } })
334
+ if (column === 'qa') return `Moved "${task.title}" to QA. Notes: ${qaNotesLine(task)}` + await nextUp(d)
335
+ if (column === 'done') return `Moved "${task.title}" to Done. Verified: ${verifiedLine(task)}` + await nextUp(d)
353
336
  return `Moved "${task.title}" to ${columnName(task.column)}.`
354
337
  }, { gate: gates(gateFor('quilt_move_task'), column === 'done' && queueGate('move the task again')) }))
355
338
 
@@ -503,39 +486,6 @@ export async function runMcp () {
503
486
  return n ? `Withdrew your request for ${rel}.` : `You had not asked for ${rel}.`
504
487
  }))
505
488
 
506
- // Quilt merges on its own (session.js autoSettle): these say what it did, and what waits for a holder.
507
- const mergeLine = (m, me) => {
508
- const who = m.by === me ? 'you' : m.by
509
- const what = m.state !== 'done' ? `${who} ${mergeAction(m)}; ${m.claimedBy || 'someone'} has it claimed, and it goes in, merged by Quilt, when they let go`
510
- : m.reason || 'merged by Quilt'
511
- return `- \`${m.path}\`: ${what}`
512
- }
513
-
514
- server.registerTool('quilt_merges', {
515
- description: 'What Quilt merged on its own recently: files changed in two places (offline, by a pull or on GitHub, and in the session). Quilt combines them itself; where both changed the same lines the newer edit stays and the other lines are kept in the file\'s history. Nothing here is for you to settle.',
516
- inputSchema: {}
517
- }, () => withDaemon(async (d) => {
518
- const { merges } = await call(d, 'GET', '/merges')
519
- const me = (await call(d, 'GET', '/info')).name
520
- if (!merges.length) return 'Quilt has merged nothing lately.'
521
- return `Quilt merges on its own; nothing to do:\n${merges.map((m) => mergeLine(m, me)).join('\n')}`
522
- }))
523
-
524
- // Kept for guides and prompts from before Quilt merged on its own.
525
- server.registerTool('quilt_resolve_merge', {
526
- description: 'Not needed any more: Quilt merges on its own. Kept so older instructions get an answer.',
527
- inputSchema: { id: z.string().optional(), how: z.string().optional() }
528
- }, () => ({ content: [{ type: 'text', text: 'Quilt merges on its own now: there is nothing to settle.' }] }))
529
-
530
- server.registerTool('quilt_clash', {
531
- description: CLASH_DESCRIPTION,
532
- inputSchema: clashSchema(z)
533
- }, (args) => withDaemon(async (d) => {
534
- const r = await call(d, 'POST', '/clash', args)
535
- if (r && r.ok === false) throw new Error(r.text)
536
- return r && r.text ? r.text : 'Done.'
537
- }))
538
-
539
489
  server.registerTool('quilt_message', {
540
490
  description: CHAT_RULES + ' Set "to" to message one person directly.',
541
491
  inputSchema: {
@@ -888,18 +838,8 @@ export async function runMcp () {
888
838
  return { content: [{ type: 'text', text: left ? 'Left the session. The files stay where they are.' : 'You have not joined a session from here.' }] }
889
839
  })
890
840
 
891
- // ------------------------------------------------------ commit timing --
892
-
893
- const describeCommits = (c) => {
894
- const lines = []
895
- lines.push(c.ready ? '✅ Everyone else\'s AI is idle: a good moment to commit.' : `⏳ Still working: ${c.busy.map((b) => b.why).join('; ')}`)
896
- lines.push(describeCommitRequests(c, { canCommit: !!c.canCommit }))
897
- if (c.open.length) lines.push('A request committed outside Quilt (with git by hand) is closed with quilt_commit_request_done.')
898
- return lines.join('\n')
899
- }
900
-
901
841
  server.registerTool('quilt_set_work', {
902
- description: 'Tell everyone whether you (this agent) are working or done, so people know when it is safe to commit. Set "working" when you start a task and "done" when you finish: ' +
842
+ description: 'Tell everyone whether you (this agent) are working or done, so people know what is in progress. Set "working" when you start a task and "done" when you finish: ' +
903
843
  '"done" lets go of the files claimed for you while you edited. Like every tool that moves work on, it is refused while someone is waiting for an answer from you.',
904
844
  inputSchema: {
905
845
  state: z.enum(['working', 'done']),
@@ -983,95 +923,6 @@ export async function runMcp () {
983
923
  return lines.join('\n') + (said ? `\n\n${said}` : '') + '\n\nRe-read each file right before you edit it.'
984
924
  }))
985
925
 
986
- server.registerTool('quilt_request_commit', {
987
- description: REQUEST_COMMIT_DESCRIPTION,
988
- inputSchema: {
989
- message: z.string().max(500).optional().describe('The commit message: what the change does (a task\'s title by default)'),
990
- 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)'),
991
- 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')
992
- }
993
- }, ({ message, files, task }) => withDaemon(async (d) => {
994
- const r = await call(d, 'POST', '/commit-request', { message, files, task })
995
- return `Asked for a commit [${r.id}] of ${r.files.length} file${r.files.length === 1 ? '' : 's'}: ${r.files.slice(0, 12).join(', ')}${r.files.length > 12 ? ', …' : ''}.\n${r.committer}`
996
- }, { gate: gateFor('quilt_request_commit') }))
997
-
998
- server.registerTool('quilt_agent_commits', {
999
- 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.',
1000
- inputSchema: { mode: z.enum(['off', 'branches', 'any']) }
1001
- }, ({ mode }) => withDaemon(async (d) => {
1002
- const r = await call(d, 'POST', '/agent-commits', { mode })
1003
- return `Now ${POLICY_WORDS[r.agentCommits]}.`
1004
- }))
1005
-
1006
- server.registerTool('quilt_github_sync', {
1007
- description: 'Session owner only: whether this session syncs with GitHub through the relay (on by default once a GitHub connection covers the repository): pushes and merges on GitHub come into the session within seconds, and commits made in Quilt (quilt_git_commit) go through the relay. Nothing reaches GitHub without a commit. Off: the session keeps its files to itself, and people commit with git as before.',
1008
- inputSchema: { on: z.boolean() }
1009
- }, ({ on }) => withDaemon(async (d) => {
1010
- const r = await call(d, 'POST', '/github-sync', { on })
1011
- return r.githubSync ? 'This session syncs with GitHub.' : 'Sync with GitHub is off for this session.'
1012
- }))
1013
-
1014
- server.registerTool('quilt_adopt_work', {
1015
- description: 'For people (and their AI): take the work on an agent\'s branch (quilt/<agent>/…, everything it changed since it started) into the branch you are on, as your edits, pushed to GitHub at once as one commit. This is how agents\' work reaches GitHub when the owner keeps agents off it ("off"), and a way to take a branch\'s work without a pull request.',
1016
- inputSchema: { from: z.string().min(1).max(200).describe('The agent\'s branch, e.g. quilt/grok/login-page') }
1017
- }, ({ from }) => withDaemon(async (d) => {
1018
- const r = await call(d, 'POST', '/adopt', { from })
1019
- return r.state === 'pushed' ? `Adopted ${from}: pushed as ${String(r.sha).slice(0, 7)} (${(r.changed || []).length} files).` : r.state === 'nothing' ? `Nothing to adopt: ${r.why || 'no changes'}.` : `Adopted into the session; not pushed yet (${r.state}${r.why ? `: ${r.why}` : ''}).`
1020
- }))
1021
-
1022
- server.registerTool('quilt_branches', {
1023
- 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. Read it before you branch, merge or push.',
1024
- inputSchema: {}
1025
- }, () => withDaemon(async (d) => {
1026
- const { branches, git } = await call(d, 'GET', '/branches')
1027
- const here = git && git.branch ? `This folder is on \`${git.branch}\`${git.upstream ? ` (${upstreamLine(git.upstream)})` : ''}.\n\n` : ''
1028
- return here + branchesMarkdown(branches || [], { limit: 40 })
1029
- }))
1030
-
1031
- server.registerTool('quilt_sync_status', {
1032
- description: SYNC_STATUS_DESCRIPTION,
1033
- inputSchema: {}
1034
- }, () => withDaemon(async (d) => { const r = await call(d, 'GET', '/sync-status'); return syncStatusText(r.branches || [], { mine: r.mine || '' }) }))
1035
-
1036
- server.registerTool('quilt_sync_branch', {
1037
- description: 'Bring commits made outside the session into it now: fetches this folder\'s upstream (say origin/main after a PR merged, or a push from a worktree) and moves the branch forward, with the session\'s uncommitted work merged in. Quilt also does this by itself about once a minute; call it right after you push or merge elsewhere. It says when the fetch fails (git has no credentials here, say). A folder that is not a git clone, or can\'t fetch, asks the relay to bring the commits in from GitHub instead. Files that both the commits and the session changed are merged by Quilt on its own.',
1038
- inputSchema: {}
1039
- }, () => withDaemon(async (d) => describeBranchSync(await call(d, 'POST', '/branches/sync'))))
1040
-
1041
- server.registerTool('quilt_github_token', {
1042
- 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 for hosted agents, and, when it can write, to commit the work of agents that have no folder (hosted agents\' quilt_git_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 hosted agents commit and open pull requests, within what quilt_agent_commits allows. It goes to the relay and stays there: never shown again. Agents with a folder don\'t need it: their git signs in through the session\'s GitHub connection.',
1043
- inputSchema: { token: z.string().max(300).describe('The token (github_pat_… or ghp_…), or "" to remove it') }
1044
- }, ({ token }) => withDaemon(async (d) => {
1045
- const r = await call(d, 'POST', '/github-token', { token })
1046
- return r.githubToken ? 'Saved the GitHub token on the relay. It looks at the session\'s branches again now.' : 'Removed the GitHub token.'
1047
- }))
1048
-
1049
- server.registerTool('quilt_commit_status', {
1050
- description: 'Is it a good moment to commit? Lists open commit requests and whose AI is still working (not counting yours).',
1051
- inputSchema: {}
1052
- }, () => withDaemon(async (d) => describeCommits(await call(d, 'GET', '/commits'))))
1053
-
1054
- server.registerTool('quilt_wait_until_idle', {
1055
- description: 'Wait until every other AI in the session is idle (or the timeout passes), then report. Use it before committing, or when you need everyone else to finish first.',
1056
- inputSchema: { timeout_seconds: z.number().int().min(5).max(1800).optional().describe('How long to wait at most (default 300)') }
1057
- }, ({ timeout_seconds: timeout = 300 }) => withDaemon(async (d) => {
1058
- const until = Date.now() + timeout * 1000
1059
- let c = await call(d, 'GET', '/commits')
1060
- while (!c.ready && Date.now() < until) {
1061
- await new Promise((r) => setTimeout(r, 3000))
1062
- c = await call(d, 'GET', '/commits')
1063
- }
1064
- return `${c.ready ? '' : `Stopped waiting after ${timeout}s.\n`}${describeCommits(c)}`
1065
- }, { gate: gateFor('quilt_wait_until_idle') }))
1066
-
1067
- server.registerTool('quilt_commit_request_done', {
1068
- description: 'Close commit requests whose work was committed with git (or isn\'t needed any more). Without an id, every open request is closed.',
1069
- inputSchema: { id: z.string().optional().describe('One request id; omit for all open ones') }
1070
- }, ({ id }) => withDaemon(async (d) => {
1071
- const { done } = await call(d, 'POST', '/commit-request/done', { id })
1072
- return done ? `Marked ${done} commit request${done === 1 ? '' : 's'} done.` : 'No open commit requests.'
1073
- }, { gate: gateFor('quilt_commit_request_done') }))
1074
-
1075
926
  server.registerTool('quilt_project_rules', {
1076
927
  description: 'The project\'s rules for AI and its design system, the same whatever tool you run in: AGENTS.md (root and folders, else CLAUDE.md), Cursor rules that always apply, and DESIGN.md (or the file AGENTS.md names). Read it before your first change in a session.',
1077
928
  inputSchema: {}
package/src/relay-ask.js CHANGED
@@ -1,13 +1,10 @@
1
1
  // Questions for the relay, "Quilt" in chat. Anyone in a session can @mention @Quilt (or message it directly)
2
- // and it answers from what it knows: how the session's branches stand against git, who is
3
- // connected, which branches it keeps, and (for the owner) trimming branches nobody uses. No
2
+ // and it answers from what it knows: who is connected, which branches it keeps, and (for the owner) trimming branches nobody uses. No
4
3
  // model runs here: each question is matched by its words and answered by code. Pure but for
5
4
  // `room`, which the relay passes in (server.js answerRelay).
6
5
 
7
- import { RELAY_BY, isRelay } from './branches.js'
8
6
  import { mentioned } from './inbox.js'
9
- import { chosenBranches } from './relay-prune.js'
10
- import { RELAY_QUESTIONS, RELAY_OLD_NAMES } from './ui/chat.js'
7
+ import { RELAY_QUESTIONS, RELAY_OLD_NAMES, RELAY_NAME as RELAY_BY, isRelay } from './ui/chat.js'
11
8
 
12
9
  export { RELAY_QUESTIONS }
13
10
 
@@ -25,7 +22,6 @@ export function asksRelay (m) {
25
22
  export function questionOf (text) {
26
23
  const t = String(text || '').replace(/@(the relay|quilt)\b/gi, ' ').toLowerCase()
27
24
  if (/\b(trim|prune|clean ?up|remove)\b/.test(t)) return 'trim'
28
- if (/\b(git|github|upstream|commits?|behind|synced?)\b/.test(t)) return 'git'
29
25
  if (/\b(members?|connected|online|who|people|agents?)\b/.test(t)) return 'members'
30
26
  if (/\bbranch(es)?\b/.test(t)) return 'branches'
31
27
  return 'help'
@@ -53,32 +49,6 @@ function whoIsOn (room) {
53
49
  return on
54
50
  }
55
51
 
56
- function gitAnswer (room, now) {
57
- const ups = room.meta.upstreams || {}
58
- const lines = []
59
- for (const key of Object.keys(room.meta.branches)) {
60
- const r = ups[key]
61
- if (!r || !r.name) continue
62
- const parts = [`${key} follows ${r.name}${r.repo ? ` (${r.repo})` : ''}`]
63
- if (r.sha) parts.push(`at ${r.sha.slice(0, 7)}${r.checkedAt ? `, checked ${ago(r.checkedAt, now)}` : ''}`)
64
- if (r.brought && r.brought.at) parts.push(`brought in ${plural(r.brought.count || 0, 'commit')} ${ago(r.brought.at, now)}`)
65
- if (r.clash && r.clash.task) parts.push(`waiting on merge task ${r.clash.task}`)
66
- if (r.problem) parts.push(`problem: ${typeof r.problem === 'string' ? r.problem : r.problem.message || 'see the branch menu'}`)
67
- const why = r.github && GITHUB_BLOCKED[r.github.state]
68
- if (why) parts.push(`agents can't commit: ${why}`)
69
- lines.push(parts.join('; '))
70
- }
71
- if (!lines.length) return 'No branch here follows a GitHub repository yet. It starts when a member with a git folder joins.'
72
- return lines.length === 1 ? `${lines[0]}.` : `Against git:\n${lines.map((l) => `- ${l}`).join('\n')}`
73
- }
74
-
75
- // Why agents can't commit to a branch's repository, by its GitHub connection's state (relay-upstream.js noteConnection).
76
- const GITHUB_BLOCKED = {
77
- 'not-connected': 'nobody here has connected GitHub',
78
- 'not-installed': 'the Quilt app isn\'t installed on the repository',
79
- 'no-access': 'the connected GitHub account can\'t write to it'
80
- }
81
-
82
52
  function membersAnswer (room) {
83
53
  const people = new Set()
84
54
  for (const ws of room.conns.keys()) { const n = room.names.get(ws); if (n) people.add(n) }
@@ -116,38 +86,6 @@ export function unusedBranches (room, now = Date.now()) {
116
86
  }
117
87
 
118
88
  /** Branch names that are never trimmed for being merged: the trunk itself, whatever the session calls its default. */
119
- const TRUNKS = new Set(['main', 'master'])
120
-
121
- /**
122
- * The branches whose work is merged and can leave the session (Room.trimMergedBranches), so a
123
- * session never fills its MAX_BRANCHES with finished work. A branch counts when its pull request
124
- * was merged on GitHub (its upstream record has `merged`, from notePull, reviewPulls or a merge
125
- * made in Quilt) and nothing was written on it in the session since (`changed` no later than the
126
- * merge). Never the default branch, main or master, the branch it merged into, a branch someone
127
- * is on, holds a file on, or a hosted agent chose, nor one with session edits still waiting to go
128
- * to GitHub. Git keeps every branch: only the relay's copy goes.
129
- */
130
- export function mergedBranches (room, now = Date.now()) {
131
- const on = whoIsOn(room)
132
- const claimed = new Set(Object.values(room.meta.claims || {}).map((c) => room.branchOf(c)))
133
- const chosen = chosenBranches(room, now) // a hosted agent's choice holds a branch worked on this week
134
- const ups = room.meta.upstreams || {}
135
- const bases = new Set()
136
- for (const rec of Object.values(ups)) if (rec && rec.merged && rec.merged.into) bases.add(room.resolveKey ? room.resolveKey(rec.merged.into) : rec.merged.into)
137
- return Object.entries(room.meta.branches)
138
- .filter(([k, b]) => {
139
- const rec = ups[k]
140
- const m = rec && rec.merged
141
- if (!m || !m.into || !(m.at > 0)) return false
142
- if (k === room.defaultKey || TRUNKS.has(k) || TRUNKS.has(rec.ref) || bases.has(k) || rec.ref === m.into) return false
143
- if (on.has(k) || claimed.has(k) || chosen.has(k)) return false
144
- const pending = room.meta.sync && room.meta.sync[k] && room.meta.sync[k].pending
145
- if (pending && Object.keys(pending).length) return false // session edits not on GitHub yet (relay-outbound.js)
146
- return !(b && b.changed > m.at) // written on after the merge: that work isn't merged
147
- })
148
- .map(([k]) => k)
149
- }
150
-
151
89
  function trimAnswer (room, asker, now) {
152
90
  if (!asker.mayTrim) return 'Only the session owner can trim branches.'
153
91
  const gone = unusedBranches(room, now)
@@ -156,7 +94,7 @@ function trimAnswer (room, asker, now) {
156
94
  room.saveMeta()
157
95
  room.broadcastBranches()
158
96
  room.log(`[${room.name}] ${asker.name} trimmed ${plural(gone.length, 'unused branch')}: ${gone.join(', ')}`)
159
- return `Trimmed ${plural(gone.length, 'branch')} nobody used for a day: ${list(gone)}. Git keeps them, and a branch comes back when someone checks it out.`
97
+ return `Trimmed ${plural(gone.length, 'branch')} nobody used for a day: ${list(gone)}. A branch comes back when someone checks it out.`
160
98
  }
161
99
 
162
100
  const HELP = `I can answer:\n${RELAY_QUESTIONS.map((q) => `- ${q.text}${q.owner ? ' (owner only)' : ''}`).join('\n')}`
@@ -166,7 +104,6 @@ const HELP = `I can answer:\n${RELAY_QUESTIONS.map((q) => `- ${q.text}${q.owner
166
104
  */
167
105
  export function relayAnswer (room, m, asker, now = Date.now()) {
168
106
  switch (questionOf(m.text)) {
169
- case 'git': return gitAnswer(room, now)
170
107
  case 'members': return membersAnswer(room)
171
108
  case 'branches': return branchesAnswer(room, now)
172
109
  case 'trim': return trimAnswer(room, asker, now)
@@ -50,14 +50,13 @@ export function clusterConfig (opts = {}, env = process.env) {
50
50
  }
51
51
 
52
52
  /** What cluster mode is missing; [] when it can run. */
53
- export function clusterProblems (cc, cfg, { fileKey = '' } = {}) {
53
+ export function clusterProblems (cc, cfg) {
54
54
  if (!cc.on) return []
55
55
  const out = []
56
56
  if (!MACHINE_RE.test(cc.machineId)) out.push('FLY_MACHINE_ID (this machine\'s id)')
57
57
  if (!cfg.apiUrl) out.push('QUILT_API_URL')
58
58
  if (!cfg.relayApiSecret) out.push('RELAY_API_SECRET')
59
59
  if (!cfg.passPublicKey) out.push('QUILT_PASS_PUBLIC_KEY (sign-in)')
60
- if (cfg.gitSync && !fileKey) out.push('QUILT_RELAY_FILE_KEY (the file key every machine shares)')
61
60
  if (!cc.peerUrl) out.push('QUILT_PEER_URL or FLY_APP_NAME (how machines reach each other)')
62
61
  return out
63
62
  }
@@ -75,10 +74,9 @@ export function storedRooms (dataDir) {
75
74
  return [...out].sort()
76
75
  }
77
76
 
78
- /** The room a request's path is about ('' for none): git, chat files, large files, chat links. */
77
+ /** The room a request's path is about ('' for none): chat files, large files, chat links. */
79
78
  export function roomOfPath (pathname) {
80
- const m = /^\/git\/([A-Za-z0-9_-]{1,64})\.git\//.exec(pathname) ||
81
- /^\/files\/([A-Za-z0-9_-]{1,64})(?:\/|$)/.exec(pathname) ||
79
+ const m = /^\/files\/([A-Za-z0-9_-]{1,64})(?:\/|$)/.exec(pathname) ||
82
80
  /^\/blobs\/([A-Za-z0-9_-]{1,64})\//.exec(pathname) ||
83
81
  /^\/c\/([A-Za-z0-9_-]{1,64})\//.exec(pathname)
84
82
  return m ? m[1] : ''