heyquilt 0.3.121 → 0.3.138

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 (129) hide show
  1. package/RELEASES.md +292 -17
  2. package/bin/quilt.js +72 -5
  3. package/desktop/main.js +16 -2
  4. package/package.json +3 -1
  5. package/src/agent-task-workflow.js +2 -2
  6. package/src/agents/common.js +13 -0
  7. package/src/ai-vm.js +9 -1
  8. package/src/api/activity.js +5 -0
  9. package/src/api/memory-store.js +21 -10
  10. package/src/api/notify.js +134 -0
  11. package/src/api/routes/browsers.js +29 -0
  12. package/src/api/routes/github.js +0 -13
  13. package/src/api/routes/relay.js +26 -3
  14. package/src/api/routes/seam-connect.js +103 -0
  15. package/src/api/routes/session-migrate.js +39 -0
  16. package/src/api/routes/sessions.js +73 -1
  17. package/src/api/server.js +46 -12
  18. package/src/api/supabase-store.js +53 -4
  19. package/src/autowork.js +182 -16
  20. package/src/connection.js +39 -9
  21. package/src/control.js +139 -13
  22. package/src/force-date.js +27 -0
  23. package/src/fsutil.js +13 -3
  24. package/src/gitstate.js +38 -2
  25. package/src/history.js +25 -1
  26. package/src/mcp.js +93 -7
  27. package/src/merge3.js +11 -3
  28. package/src/now-summary.js +113 -0
  29. package/src/pathrules.js +12 -0
  30. package/src/protocol.js +10 -2
  31. package/src/quilt-default.js +84 -0
  32. package/src/quilt-history.js +106 -0
  33. package/src/quilt-remote.js +78 -0
  34. package/src/relay-commit.js +5 -0
  35. package/src/relay-lease.js +6 -2
  36. package/src/relay-mcp.js +172 -8
  37. package/src/relay-outbound.js +1 -1
  38. package/src/relay-prune.js +4 -1
  39. package/src/release-words.js +2 -2
  40. package/src/runner.js +20 -7
  41. package/src/seam-access.js +97 -0
  42. package/src/seam-claims.js +53 -0
  43. package/src/seam-url.js +15 -0
  44. package/src/server.js +1767 -33
  45. package/src/session-follow.js +515 -0
  46. package/src/session-layout3.js +466 -0
  47. package/src/session-migrate.js +123 -0
  48. package/src/session-quilt.js +145 -0
  49. package/src/session-seam.js +284 -0
  50. package/src/session.js +899 -46
  51. package/src/source-live/bring-in.js +283 -0
  52. package/src/source-live/live-block.js +507 -35
  53. package/src/source-live/live-square.js +17 -6
  54. package/src/status.js +5 -0
  55. package/src/ui/agent-guide.js +1 -1
  56. package/src/ui/app.css +85 -20
  57. package/src/ui/app.js +4 -1
  58. package/src/ui/chat.js +9 -3
  59. package/src/ui/cloud-ai-model.js +13 -0
  60. package/src/ui/cloud-ai.js +10 -2
  61. package/src/ui/common.js +6 -2
  62. package/src/ui/fileview.js +20 -7
  63. package/src/ui/force-words.js +19 -0
  64. package/src/ui/git-rule.js +9 -0
  65. package/src/ui/home.js +0 -147
  66. package/src/ui/loom-model.js +15 -1
  67. package/src/ui/quiltview.js +213 -0
  68. package/src/ui/seam-model.js +251 -0
  69. package/src/ui/session.js +413 -31
  70. package/src/ui/voice-call.js +83 -14
  71. package/src/ui/voice-model.js +39 -0
  72. package/src/ui/voice.js +167 -17
  73. package/src/ui-server.js +80 -28
  74. package/src/update-check.js +25 -1
  75. package/src/voice.js +13 -6
  76. package/src/git.js +0 -147
  77. package/src/quilts/AGENTS.md +0 -1
  78. package/src/quilts/CLAUDE.md +0 -1
  79. package/src/quilts/archiver.js +0 -175
  80. package/src/quilts/autorevert.js +0 -49
  81. package/src/quilts/bisect-run.js +0 -31
  82. package/src/quilts/blame.js +0 -103
  83. package/src/quilts/block.js +0 -347
  84. package/src/quilts/bucket.js +0 -25
  85. package/src/quilts/cbor.js +0 -92
  86. package/src/quilts/claims.js +0 -144
  87. package/src/quilts/cli.js +0 -449
  88. package/src/quilts/client.js +0 -128
  89. package/src/quilts/conflict-format.js +0 -190
  90. package/src/quilts/describe.js +0 -266
  91. package/src/quilts/dogfood.js +0 -82
  92. package/src/quilts/errors.js +0 -8
  93. package/src/quilts/events.js +0 -160
  94. package/src/quilts/export-git.js +0 -130
  95. package/src/quilts/export.js +0 -81
  96. package/src/quilts/fastmerge.js +0 -41
  97. package/src/quilts/hash.js +0 -44
  98. package/src/quilts/history.js +0 -266
  99. package/src/quilts/ids.js +0 -12
  100. package/src/quilts/import-git.js +0 -291
  101. package/src/quilts/import.js +0 -134
  102. package/src/quilts/index.js +0 -72
  103. package/src/quilts/limits.js +0 -67
  104. package/src/quilts/lines.js +0 -49
  105. package/src/quilts/live.js +0 -50
  106. package/src/quilts/merge-smart.js +0 -114
  107. package/src/quilts/merge.js +0 -486
  108. package/src/quilts/objects.js +0 -144
  109. package/src/quilts/pack.js +0 -118
  110. package/src/quilts/records.js +0 -211
  111. package/src/quilts/resolutions.js +0 -69
  112. package/src/quilts/reviews.js +0 -195
  113. package/src/quilts/routes.js +0 -126
  114. package/src/quilts/service.js +0 -923
  115. package/src/quilts/setup.js +0 -34
  116. package/src/quilts/signing.js +0 -149
  117. package/src/quilts/squares.js +0 -239
  118. package/src/quilts/store-memory.js +0 -683
  119. package/src/quilts/store-rpc.js +0 -79
  120. package/src/quilts/store-supabase.js +0 -13
  121. package/src/quilts/tags.js +0 -64
  122. package/src/quilts/textdiff.js +0 -96
  123. package/src/quilts/tokens.js +0 -66
  124. package/src/quilts/tools.js +0 -375
  125. package/src/quilts/tree.js +0 -351
  126. package/src/quilts/webhook.js +0 -116
  127. package/src/quilts/window.js +0 -119
  128. package/src/quilts/xdiff.js +0 -435
  129. package/src/ui/branches.js +0 -111
@@ -0,0 +1,145 @@
1
+ // Quilt's side of Seam: what making a session a quilt would do, without doing it
2
+ // (Seam's plans/quilt-source-control/16, "How a session becomes a quilt", "Dry run and check").
3
+ // Pure: it reads a snapshot of the room (Room.quiltSnapshot in server.js) and returns the
4
+ // report the owner reads before the real migration. Seam (src/quilts/) never imports this.
5
+ //
6
+ // Snapshot:
7
+ // { room, owner, defaultKey, hasRemote,
8
+ // branches: [{ key, by, files: { path: { hash, bytes } }, history: [entry], merges: [{ id, state }] }],
9
+ // claims: [{ pattern, branch, by, byId, queue }], tasks: [{ id, pr }], commitRequests: n }
10
+ // Options:
11
+ // tip: { path: hash } the imported main's tree, when the room has a remote and its import exists.
12
+ import { createHash } from 'node:crypto'
13
+
14
+ export const fileHash = (text) => createHash('sha256').update(String(text)).digest('hex')
15
+
16
+ const sumBytes = (files) => Object.values(files || {}).reduce((n, f) => n + (Number(f && f.bytes) || 0), 0)
17
+
18
+ /** The paths whose content differs between a branch document's files and a tree, both `{ path: hash }`-like. */
19
+ export function differingPaths (files, tree) {
20
+ const out = []
21
+ for (const [p, f] of Object.entries(files || {})) if (tree[p] !== (f && f.hash)) out.push(p)
22
+ for (const p of Object.keys(tree || {})) if (!(p in (files || {}))) out.push(p)
23
+ return out.sort()
24
+ }
25
+
26
+ /**
27
+ * The dry run's report. `mode` is `with_history` for a room with a git remote (the import and
28
+ * its follow come first; `needs_import` until a `tip` is given), else `without_history` (the
29
+ * default branch document is sealed as patch 1).
30
+ */
31
+ export function planSessionQuilt (snap, { tip = null } = {}) {
32
+ const withHistory = !!snap.hasRemote
33
+ const defaultKey = snap.defaultKey
34
+ const owner = snap.owner || ''
35
+ const branches = (snap.branches || []).filter((b) => b && typeof b.key === 'string')
36
+ const def = branches.find((b) => b.key === defaultKey) || null
37
+ const squares = []
38
+ const report = {
39
+ room: snap.room,
40
+ mode: withHistory ? 'with_history' : 'without_history',
41
+ needs_import: withHistory && !tip,
42
+ first_patch: null,
43
+ squares,
44
+ claims: { moved: 0, opened_for: [], without_square: [] },
45
+ tasks: { matched: [], unmatched: [] },
46
+ terms: 0,
47
+ history: { kept: 0, dropped: 0 },
48
+ commit_requests: Number(snap.commitRequests) || 0,
49
+ bytes: 0
50
+ }
51
+ if (def && !withHistory) {
52
+ report.first_patch = { from: def.key, message: `Session ${snap.room} as of <date>`, files: Object.keys(def.files || {}).length, bytes: sumBytes(def.files) }
53
+ }
54
+ if (def && withHistory && tip) {
55
+ const paths = differingPaths(def.files, tip)
56
+ if (paths.length) {
57
+ squares.push({ from: def.key, title: `Uncommitted work from ${snap.room}`, base: 'import:main', holder: owner, adoption: true, files: paths.length, bytes: paths.reduce((n, p) => n + (Number(def.files[p] && def.files[p].bytes) || 0), 0) })
58
+ }
59
+ }
60
+ const squareOf = new Map()
61
+ for (const b of branches) {
62
+ if (b.key === defaultKey) continue
63
+ const at = /^@([0-9a-f]{7,64})$/.exec(b.key)
64
+ const base = withHistory ? (at ? `commit:${at[1]}` : `import:${b.key}`) : 'patch:1'
65
+ const sq = { from: b.key, title: b.key, base, holder: b.by || owner, adoption: false, files: Object.keys(b.files || {}).length, bytes: sumBytes(b.files) }
66
+ squares.push(sq)
67
+ squareOf.set(b.key, sq)
68
+ }
69
+ const adoption = squares.find((s) => s.adoption) || null
70
+ const opened = new Set()
71
+ for (const c of snap.claims || []) {
72
+ const branch = c.branch || defaultKey
73
+ const holder = c.byId || c.by || ''
74
+ if (branch === defaultKey) {
75
+ if (adoption) { report.claims.moved++; continue }
76
+ if (!def) { report.claims.without_square.push({ pattern: c.pattern, branch, holder }); continue }
77
+ // No square holds the default branch's work: one is opened for the claim's holder.
78
+ if (!opened.has(holder)) { opened.add(holder); report.claims.opened_for.push(holder) }
79
+ report.claims.moved++
80
+ continue
81
+ }
82
+ if (squareOf.has(branch)) report.claims.moved++
83
+ else report.claims.without_square.push({ pattern: c.pattern, branch, holder })
84
+ }
85
+ for (const t of snap.tasks || []) {
86
+ if (!t || !t.pr) continue
87
+ const b = t.pr.branch
88
+ if (b && squareOf.has(b)) report.tasks.matched.push({ id: t.id, square_from: b })
89
+ else report.tasks.unmatched.push({ id: t.id, pr: t.pr.number || null })
90
+ }
91
+ for (const b of branches) {
92
+ report.terms += (b.merges || []).filter((m) => m && (m.state === 'open' || m.state === 'editing')).length
93
+ for (const h of b.history || []) {
94
+ if (withHistory && h && (h.pulled || h.seed)) report.history.dropped++
95
+ else report.history.kept++
96
+ }
97
+ }
98
+ report.bytes = squares.reduce((n, s) => n + s.bytes, 0) + (report.first_patch ? report.first_patch.bytes : 0)
99
+ report.problems = [
100
+ ...report.claims.without_square.map((c) => `claim on ${c.pattern} (${c.branch}) has no square`),
101
+ ...report.tasks.unmatched.map((t) => `task ${t.id}'s pull request${t.pr ? ` #${t.pr}` : ''} has no square; it becomes a task comment`)
102
+ ]
103
+ return report
104
+ }
105
+
106
+ export const MIGRATE_SESSION_DESCRIPTION =
107
+ 'What making this session a quilt (Quilt\'s own source control, Seam) would do, read only: the first patch or the import, ' +
108
+ 'which branches become squares, how claims and tasks with pull requests move, and anything that would not carry over. ' +
109
+ 'Only the session\'s owner (or, in a session without one, anyone who may change files) can ask. Nothing changes.'
110
+
111
+ /** Why `a` (the asker's access) may not see the dry run, or null when they may. */
112
+ export function quiltPlanRefusal ({ controlled, access }) {
113
+ if (controlled ? !(access && access.owner) : !!(access && access.role === 'viewer')) return 'only the session\'s owner can see what making it a quilt would do'
114
+ return null
115
+ }
116
+
117
+ const kb = (n) => n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.ceil(n / 1024)} KB`
118
+
119
+ /** The dry run's report as plain text, for an agent or a person. */
120
+ export function describeQuiltPlan (p) {
121
+ const out = [`Making session ${p.room} a quilt (dry run, nothing changed):`]
122
+ if (p.mode === 'with_history') {
123
+ out.push(p.needs_import
124
+ ? '- The session has a git remote: its history comes in by an import that follows the remote first (seam import), then the session moves.'
125
+ : '- History comes from the import of the session\'s git remote.')
126
+ } else if (p.first_patch) {
127
+ out.push(`- Patch 1 is the ${p.first_patch.from} branch as it is now: ${p.first_patch.files} file${p.first_patch.files === 1 ? '' : 's'}, ${kb(p.first_patch.bytes)}.`)
128
+ } else {
129
+ out.push('- The session has no default branch document: patch 1 starts empty.')
130
+ }
131
+ if (p.squares.length) {
132
+ out.push(`- ${p.squares.length} square${p.squares.length === 1 ? '' : 's'}:`)
133
+ for (const s of p.squares) out.push(` - ${s.title} from ${s.from}, on ${s.base}, held by ${s.holder || 'nobody'}: ${s.files} file${s.files === 1 ? '' : 's'}`)
134
+ } else out.push('- No squares: there is no other branch with work on it.')
135
+ out.push(`- Claims: ${p.claims.moved} move to their squares${p.claims.opened_for.length ? ` (a square is opened for ${p.claims.opened_for.join(', ')})` : ''}.`)
136
+ if (p.tasks.matched.length) out.push(`- ${p.tasks.matched.length} task${p.tasks.matched.length === 1 ? '' : 's'} with a pull request link${p.tasks.matched.length === 1 ? 's' : ''} to a square.`)
137
+ if (p.terms) out.push(`- ${p.terms} open merge${p.terms === 1 ? '' : 's'} become${p.terms === 1 ? 's' : ''} terms on their squares.`)
138
+ out.push(`- History: ${p.history.kept} entr${p.history.kept === 1 ? 'y' : 'ies'} kept${p.history.dropped ? `, ${p.history.dropped} dropped (they are in the import)` : ''}.`)
139
+ if (p.commit_requests) out.push(`- ${p.commit_requests} commit request${p.commit_requests === 1 ? '' : 's'} must be done or withdrawn first.`)
140
+ if (p.problems && p.problems.length) {
141
+ out.push('Would not carry over:')
142
+ for (const x of p.problems) out.push(`- ${x}`)
143
+ }
144
+ return out.join('\n')
145
+ }
@@ -0,0 +1,284 @@
1
+ // Quilt's side of Seam for agents (Seam's plans/quilt-source-control/12 and 16): in a session that is a
2
+ // quilt, an agent works through Seam (squares, commits, merges), never GitHub. The same for every
3
+ // AI tool: the session's own MCP server (`quilt mcp` in a folder, the relay's for hosted agents)
4
+ // serves Seam's everyday tools itself, acting on the session's quilt and the folder's square with
5
+ // the member's own Quilt sign-in (seam-access.js), so nothing is set up by hand. There, everything
6
+ // an AI is told names only Seam (Daniel, 2026-10-31): the git-era tools are hidden (GIT_TOOLS) or
7
+ // refuse with the Seam tool to use, the guidance leaves git out (seamOnly), a push that still
8
+ // reaches the hook is refused, and the relay gives no GitHub token to an agent's git there.
9
+ //
10
+ // A session is a quilt when the relay is on Seam (QUILT_SOURCE_URL) and its room names its quilt
11
+ // (meta.quilt_id, set by the migration with layout 3 and cleared when a failed check reverts it).
12
+ // Every other session keeps its GitHub path exactly as it is, and offers no Seam tools.
13
+ import { z } from 'zod'
14
+ // Seam's tools, their descriptions and answers come from Seam's client package (@heyquilt/seam).
15
+ import { registerSeamTools, SEAM_INPUTS, SEAM_DESCRIPTIONS } from '@heyquilt/seam/lib/quilts/tools.js'
16
+ import { describeWrite } from '@heyquilt/seam/lib/quilts/describe.js'
17
+ import { squareShort } from '@heyquilt/seam/lib/quilts/errors.js'
18
+
19
+ /** The quilt a room's session is, or null while it is a GitHub-backed (or plain) session. */
20
+ export function quiltOf (meta, env = process.env) {
21
+ if (!env || !env.QUILT_SOURCE_URL) return null
22
+ if (!meta) return null
23
+ return typeof meta.quilt_id === 'string' && meta.quilt_id ? meta.quilt_id : null
24
+ }
25
+
26
+ /**
27
+ * Seam's tools a session that is a quilt serves, the same for a folder's AI sessions and hosted
28
+ * agents: the everyday ones (Seam's 'core' profile) an agent in a session needs. A folder's
29
+ * square is the session's, so cloning, pulling, listing quilts and abandoning are left out.
30
+ */
31
+ export const SEAM_SESSION_TOOLS = ['seam_square_status', 'seam_commit', 'seam_merge', 'seam_rebase', 'seam_resolve', 'seam_diff', 'seam_log', 'seam_show', 'seam_cat', 'seam_ls', 'seam_grep', 'seam_blame', 'seam_checks', 'seam_events', 'seam_reviews', 'seam_review', 'seam_interdiff', 'seam_squares', 'seam_open_square']
32
+
33
+ // Each GitHub tool, and the Seam tool an agent uses for it in a session that is a quilt.
34
+ export const SEAM_FOR = {
35
+ quilt_git_commit: 'seam_commit (commits your square)',
36
+ quilt_git_status: 'seam_square_status (your square and what changed in it)',
37
+ quilt_git_log: 'seam_log',
38
+ quilt_git_diff: 'seam_diff',
39
+ quilt_git_revert: 'seam_cat (the line\'s copy of a file, to write back)',
40
+ quilt_git_pull_requests: 'seam_squares (the squares on a line) and seam_checks',
41
+ quilt_git_merge: 'seam_merge (merges your square onto main once its checks pass)',
42
+ quilt_git_branch: 'seam_square_status (your folder\'s square)',
43
+ quilt_sync_branch: 'seam_square_status (a square follows its line: seam_rebase moves it onto the tip)',
44
+ quilt_sync_status: 'seam_square_status',
45
+ quilt_github_token: null, // nothing to use instead: the owner's export to GitHub has its own credential
46
+ quilt_request_commit: 'seam_commit, then seam_merge once its checks pass (or ask a person to)',
47
+ quilt_agent_commits: 'seam_log',
48
+ quilt_commit_status: 'seam_square_status',
49
+ quilt_commit_request_done: 'seam_square_status',
50
+ quilt_github_sync: 'seam_square_status'
51
+ }
52
+
53
+ /** How work lands in a session that is a quilt, for an AI with a folder (its files are its square's live work). */
54
+ export const SEAM_GUIDE = 'Edit files in your folder as always: they are your square\'s live work, shared at once with everyone on it. ' +
55
+ 'Commit the square with seam_commit (it commits everything the square holds now, every writer\'s, so say why in the message), and merge it onto main with seam_merge once its checks pass (seam_checks; seam_events with wait: 30 waits for a verdict). ' +
56
+ 'A conflict comes back as terms in your square: read them with seam_square_status and settle each with seam_resolve (or edit the file and commit), then merge again. ' +
57
+ 'seam_diff, seam_log, seam_show and seam_blame read the history. Work lands only through these tools.'
58
+
59
+ /** The same for a hosted agent, which has no folder. */
60
+ export const SEAM_GUIDE_HOSTED = 'You have no folder: open a square of your own with seam_open_square, read files with seam_cat and seam_ls (rev: main or your square), and commit their new contents with seam_commit (files, square: your square). ' +
61
+ 'Merge it onto main with seam_merge once its checks pass (seam_checks; seam_events with wait: 30 waits for a verdict). ' +
62
+ 'A conflict comes back as terms in your square: read them with seam_square_status and settle each with seam_resolve (or commit the file without markers), then merge again. ' +
63
+ 'Work lands only through these tools.'
64
+
65
+ const guideFor = ({ hosted = false } = {}) => hosted ? SEAM_GUIDE_HOSTED : SEAM_GUIDE
66
+
67
+ // How an agent hears of Seam: only as where the session's code is committed and merged. Chat, tasks and
68
+ // the session itself stay in Quilt; nobody reaches a session through Seam.
69
+ const codeIn = (quiltId) => `This session's code is committed and merged in Seam (the quilt ${quiltId})`
70
+
71
+ /**
72
+ * Why a git-era tool is refused in a quilt session: just the Seam tool to use (or, for one with no
73
+ * counterpart, how work lands there); null when it isn't. An AI whose tool list is from before the
74
+ * move may still call one that is hidden now.
75
+ */
76
+ export function seamRefusal (tool, quiltId, { hosted = false } = {}) {
77
+ if (!quiltId) return null
78
+ if (Object.hasOwn(SEAM_FOR, tool)) return `${codeIn(quiltId)}: ${SEAM_FOR[tool] ? `use ${SEAM_FOR[tool]} instead.` : 'Seam\'s tools need nothing set up.'}`
79
+ return GIT_TOOLS.has(tool) ? `${codeIn(quiltId)}, where ${tool} is not used. ${guideFor({ hosted })}` : null
80
+ }
81
+
82
+ /**
83
+ * The git-era tools a session that is a quilt does not offer (each of SEAM_FOR, and those about
84
+ * branches, boxes and merges with git): hidden from the tool list while it is one, so nothing an
85
+ * AI reads there names git or GitHub.
86
+ */
87
+ export const GIT_TOOLS = new Set([...Object.keys(SEAM_FOR), 'quilt_git_status', 'quilt_git_log', 'quilt_git_diff', 'quilt_git_revert', 'quilt_git_pull_requests', 'quilt_git_merge', 'quilt_unpack', 'quilt_merges', 'quilt_resolve_merge', 'quilt_adopt_work', 'quilt_branches', 'quilt_clash', 'quilt_migrate_session'])
88
+
89
+ // A sentence of guidance about git: branches, boxes, pushes, pull requests or GitHub.
90
+ const GITTY = /\bgit\b|GitHub|pull request|\bpush(es|ed|ing)?\b|\bbox(es|ed)?\b|quilt_git_|quilt_unpack|quilt_branches|quilt_sync_|quilt_github|quilt_request_commit|quilt_commit_|quilt_agent_commits|quilt_merges|quilt_resolve_merge|quilt_adopt_work|quilt_clash|\bbranch(es)?\b/i
91
+
92
+ /** Guidance with every sentence about git left out, for a session that is a quilt. */
93
+ export function seamOnly (text) {
94
+ return String(text || '').split(/(?<=[.!?])\s+(?=[A-Z@"'(*0-9])/).filter((x) => !GITTY.test(x)).join(' ')
95
+ }
96
+
97
+ // Status sections that are git's alone (status.js), and the lines elsewhere that speak of git.
98
+ const GIT_SECTION = /^## (Branches|Commit requests|Pulling|Boxed work|Waiting to go in)\b/
99
+ const GIT_LINE = /\bgit\b|GitHub|pull request/i
100
+
101
+ /**
102
+ * A session's status (status.js markdown) for a session that is a quilt: git's sections left out,
103
+ * and lines about git elsewhere too; what people wrote (the messages and chat sections) stays as it is.
104
+ */
105
+ export function seamStatus (md) {
106
+ const out = []
107
+ let skip = false
108
+ let said = false
109
+ for (const line of String(md || '').split('\n')) {
110
+ if (line.startsWith('## ')) { skip = GIT_SECTION.test(line); said = /messages|chat/i.test(line) }
111
+ if (skip) continue
112
+ if (!said && GIT_LINE.test(line)) continue
113
+ out.push(line)
114
+ }
115
+ return out.join('\n')
116
+ }
117
+
118
+ /** Seam's own description of a tool, without its comparisons to git commands. */
119
+ const seamWords = (d) => String(d || '').replace(/\s*\(git [^)]*\)/g, '').replace(/,? as git [^.,;]* does/g, '').replace(/, git:<sha>/g, '')
120
+
121
+ /** The task workflow (agent-task-workflow.js) for a session that is a quilt: its git steps left out, the Seam one in. */
122
+ export function seamWorkflow (workflow, { hosted = false } = {}) {
123
+ return `${seamOnly(workflow)} Before QA, commit the task's work with seam_commit${hosted ? ' in your square' : ''}, saying what the task is; it reaches main with seam_merge once its checks pass.`
124
+ }
125
+
126
+ /** A hosted agent's file tool at layout 3, where the session's files are in squares, not branches. */
127
+ export function seamFileRefusal (quiltId) {
128
+ return quiltId ? `${codeIn(quiltId)}: its files are in squares, which Seam's tools read and write. ${SEAM_GUIDE_HOSTED}` : null
129
+ }
130
+
131
+ /** What a push that still reaches the folder's hook hears in a session that is a quilt: the Seam way, nothing else. */
132
+ export function pushRefusal (quiltId) {
133
+ return quiltId ? `${codeIn(quiltId)}, so nothing is sent from here. ${SEAM_GUIDE}` : null
134
+ }
135
+
136
+ /** The line a session's status answers with, in a quilt session; '' otherwise. */
137
+ export function seamNote (quiltId, { hosted = false } = {}) {
138
+ return quiltId ? `${codeIn(quiltId)}; chat, tasks and the session stay in Quilt. ${guideFor({ hosted })}` : ''
139
+ }
140
+
141
+ /** Why the relay gives no GitHub token to a member's git here; null when it may. People keep theirs. */
142
+ export function gitTokenRefusal (quiltId, kind) {
143
+ if (!quiltId || kind !== 'agent') return null
144
+ return `this session's code is committed and merged in Seam (the quilt ${quiltId}): agents use seam_commit and seam_merge`
145
+ }
146
+
147
+ /**
148
+ * A folder's MCP server (src/mcp.js), in a session that is a quilt: wraps `registerTool` so each
149
+ * GitHub tool refuses with the Seam tool to use. `quiltNow()` is the session's link as the relay
150
+ * last said ({ id, slug } or null), asked at call time, so a session that becomes a quilt (or
151
+ * stops being one) changes the answer without a restart. Every other tool is untouched.
152
+ */
153
+ export function seamGate (registerTool, quiltNow) {
154
+ return (name, spec, handler) => registerTool(name, spec, GIT_TOOLS.has(name)
155
+ ? async (...args) => {
156
+ const q = await quiltNow()
157
+ const no = q && seamRefusal(name, q.slug || q.id)
158
+ return no ? { content: [{ type: 'text', text: no }], isError: true } : handler(...args)
159
+ }
160
+ : handler)
161
+ }
162
+
163
+ // What the session adds to Seam's own description of a tool.
164
+ const IN_SESSION = {
165
+ seam_commit: 'In this Quilt session, with no files: commits your folder\'s square (everything its live work holds now). ',
166
+ seam_merge: 'In this Quilt session: merges your folder\'s square onto main (committing its live work first); the folder carries on in a new square. ',
167
+ seam_square_status: 'In this Quilt session: your folder\'s square, with its live work not committed yet. ',
168
+ seam_open_square: 'In this Quilt session your folder works in a square of its own already (seam_square_status names it): edit there and seam_commit. '
169
+ }
170
+ const IN_SESSION_HOSTED = { seam_commit: 'You have no folder: give the files\' new contents (files) and your square. ' }
171
+
172
+ /** An MCP tool spec for one of SEAM_SESSION_TOOLS: Seam's description and input, quilt and square optional (the session's). */
173
+ export function seamToolSpec (name, { hosted = false } = {}) {
174
+ const lead = (hosted ? IN_SESSION_HOSTED[name] : IN_SESSION[name]) || ''
175
+ const shape = Object.fromEntries(Object.entries(SEAM_INPUTS[name]).map(([k, v]) => [k, v && v.description && seamWords(v.description) !== v.description ? v.describe(seamWords(v.description)) : v]))
176
+ return { description: lead + seamWords(SEAM_DESCRIPTIONS[name]), inputSchema: z.looseObject(shape) }
177
+ }
178
+
179
+ const SQUARE_INPUT = SEAM_INPUTS.seam_square_status.square
180
+ // The tools whose answer can move the folder's square on Seam (its live document follows).
181
+ const MOVES_HEAD = new Set(['seam_rebase', 'seam_resolve', 'seam_merge', 'seam_commit'])
182
+
183
+ /** Seam's own handler for one tool (registerSeamTools on a stand-in server), with `client` for this call. */
184
+ function seamHandler (name, client, hosted) {
185
+ let handler = null
186
+ registerSeamTools({ registerTool: (n, spec, fn) => { handler = fn } }, { client: () => client, tools: [name], hosted })
187
+ return handler
188
+ }
189
+
190
+ const textOf = (r) => ((r && r.content) || []).map((c) => c.text || '').join('\n')
191
+ const answer = (text, extra = {}) => ({ content: [{ type: 'text', text }], ...extra })
192
+
193
+ /**
194
+ * Runs one of SEAM_SESSION_TOOLS for a member of a session that is a quilt: Seam's own handler,
195
+ * with the session's quilt (and the folder's square) filled in where the call names none.
196
+ * `live` is the folder's live block on that square (source-live/live-block.js), or null: with it,
197
+ * seam_commit commits the live work the relay holds, seam_merge commits it first, the status says
198
+ * what is uncommitted, and a square whose head Seam moved has its live document follow.
199
+ * `next()` is asked after the folder's square merged, to move the branch to its next square.
200
+ * Returns an MCP tool result.
201
+ */
202
+ export async function runSessionSeamTool (name, args = {}, { client, quilt, square = null, live = null, hosted = false, next = null, branch = '' }) {
203
+ if (!SEAM_SESSION_TOOLS.includes(name)) return answer(`${name} is not one of this session's Seam tools (${SEAM_SESSION_TOOLS.join(', ')}).`, { isError: true })
204
+ const shape = SEAM_INPUTS[name]
205
+ const a = { ...(args || {}) }
206
+ if (Object.hasOwn(shape, 'quilt') && !a.quilt) a.quilt = quilt
207
+ // A call that names a square other than the folder's acts on that one, on Seam alone.
208
+ const ref = String(a.square || '').replace(/^sq:/, '')
209
+ const mine = !ref || !!(square && ref.length >= 4 && String(square).startsWith(ref))
210
+ if (square && shape.square === SQUARE_INPUT && !a.square) a.square = square
211
+ if (square && name === 'seam_log' && !a.rev) a.rev = `sq:${square}`
212
+ if (square && name === 'seam_diff' && !a.from && !a.to) { a.from = 'main'; a.to = `sq:${square}` }
213
+ const onFolder = !!(live && square && mine && a.quilt === quilt)
214
+ const run = (x) => seamHandler(name, client, hosted)(x)
215
+ // A square opened beside the folder's is in no folder: its work could only go in as file contents
216
+ // (Seam's agent trial 2026-10-31, finding 10). The folder's square is where work goes.
217
+ if (live && square && name === 'seam_open_square') return answer(`This folder is on square ${squareShort(square)} already, and work goes there: edit the files in your folder and commit them with seam_commit, then seam_merge merges the square onto main (and the folder carries on in a new one).`, { isError: true })
218
+ try {
219
+ if (onFolder && name === 'seam_commit' && !a.files && !a.delete && !a.amend) {
220
+ if (!a.message) return answer('Say why: message.')
221
+ const r = await live.commit({ message: a.message })
222
+ return r && r.nothing ? answer('Nothing to commit: the square holds no live work since its last commit.') : answer(describeWrite(r, 'Committed'), { structuredContent: r })
223
+ }
224
+ if (onFolder && name === 'seam_merge' && !a.from && !a.dry_run) {
225
+ // The relay holds the square's live work: it is committed first, so the merge has all of it.
226
+ const c = await live.commit({ message: a.message || 'Work in progress' })
227
+ if (!a.message) { const t = await patchTitle(client, a.quilt, a.square, branch).catch(() => null); if (t) a.message = t }
228
+ const r = await run({ ...a, commit_live: false })
229
+ const rec = r.structuredContent || {}
230
+ const sealed = c && !c.nothing ? `Committed the square's live work first: ${describeWrite(c, 'committed')}\n` : ''
231
+ if (rec.ok && rec.merged && next) {
232
+ let moved = ''
233
+ try { const n = await next(); if (n && n.moved) moved = `\nThe folder carries on in a new square, ${squareShort(n.square)}, on main's new tip: keep editing as before.` } catch (err) { moved = `\nThe folder's branch did not move to a new square yet (${err.message}): ask again with seam_square_status.` }
234
+ return { ...r, content: [{ type: 'text', text: sealed + textOf(r) + moved }] }
235
+ }
236
+ if (rec.ok === false) await followHead(live, rec)
237
+ return { ...r, content: [{ type: 'text', text: sealed + textOf(r) + inChat(r) }] }
238
+ }
239
+ if (name === 'seam_merge' && !a.message && !a.from && !a.dry_run && mine) { const t = await patchTitle(client, a.quilt, a.square, branch).catch(() => null); if (t) a.message = t }
240
+ const r = await run(a)
241
+ if (name === 'seam_merge') return { ...r, content: [{ type: 'text', text: textOf(r) + inChat(r) }] }
242
+ if (onFolder && name === 'seam_square_status') {
243
+ const work = live.status ? live.status() : []
244
+ const lines = work.length ? `Live work not committed yet (seam_commit commits it):\n${work.map((w) => ` ${w.status} ${w.path}`).join('\n')}` : 'No live work since the last commit.'
245
+ return { ...r, content: [{ type: 'text', text: `${textOf(r)}\n${lines}` }] }
246
+ }
247
+ if (onFolder && MOVES_HEAD.has(name) && !r.isError) await followHead(live, r.structuredContent || {})
248
+ return r
249
+ } catch (err) {
250
+ return answer(err.message, { isError: true })
251
+ }
252
+ }
253
+
254
+ // Seam tells an agent whose merge waits for an approval to ask for a review (Seam's agent trial
255
+ // 2026-10-31, finding 5), which reaches people only in Seam: in a Quilt session they hear it in chat.
256
+ const ASKS_REVIEW = /waits for someone else[\s\S]*seam_review with kind request/
257
+ const inChat = (r) => ASKS_REVIEW.test(textOf(r)) ? '\nIn this Quilt session, also ask them in chat (quilt_message, naming the square): Seam\'s review request is not shown in Quilt, and they approve with seam_review (kind approve) from their own AI session. Then wait for their answer and merge again.' : ''
258
+
259
+ /**
260
+ * The title a merge gives its patch when the call names none: Seam takes the square's title, and a square
261
+ * the move into Seam opened for a branch is titled with the branch's name, so every patch from main was
262
+ * called "main". Then the square's own commits name it: its one message, or "N commits: first; second; …".
263
+ * Null (Seam's title stands) for a square named otherwise, or with no message of its own.
264
+ */
265
+ export async function patchTitle (client, quilt, square, branch) {
266
+ if (!branch || !square || !client || !client.square || !client.log) return null
267
+ const s = await client.square(quilt, square)
268
+ if (!s || String(s.title || '').trim() !== String(branch).trim()) return null
269
+ const log = await client.log(quilt, { rev: `sq:${s.id || square}`, limit: 50 })
270
+ const own = ((log && log.commits) || []).filter((c) => c && c.square_id === (s.id || square) && c.message)
271
+ .map((c) => String(c.message).split('\n')[0].trim()).filter((m) => m && m !== 'Work in progress').reverse()
272
+ if (!own.length) return null
273
+ if (own.length === 1) return own[0].slice(0, 200)
274
+ const all = `${own.length} commits: ${own.join('; ')}`
275
+ return all.length > 200 ? `${all.slice(0, 199)}…` : all
276
+ }
277
+
278
+ /** The square's head after a Seam answer that moved it, and the live document following it. */
279
+ async function followHead (live, rec) {
280
+ const own = (rec.square && rec.square.head) || rec.head || null
281
+ // A square with no commit of its own (a rebase moved its base only): its live work is relative to its base.
282
+ const base = !own && rec.square && rec.square.base ? rec.square.base : null
283
+ if ((own || base) && live.moveTo) await live.moveTo(own || base, { base: !!base }).catch(() => {})
284
+ }