heyquilt 0.3.121 → 0.3.122

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 (48) hide show
  1. package/RELEASES.md +19 -0
  2. package/bin/quilt.js +23 -2
  3. package/package.json +2 -1
  4. package/src/api/memory-store.js +5 -1
  5. package/src/api/routes/seam-connect.js +103 -0
  6. package/src/api/routes/session-migrate.js +36 -0
  7. package/src/api/server.js +37 -9
  8. package/src/api/supabase-store.js +10 -0
  9. package/src/connection.js +4 -1
  10. package/src/control.js +12 -0
  11. package/src/history.js +15 -1
  12. package/src/mcp.js +12 -1
  13. package/src/quilts/accounts.js +136 -0
  14. package/src/quilts/affected.js +132 -0
  15. package/src/quilts/checks.js +308 -0
  16. package/src/quilts/cli.js +97 -10
  17. package/src/quilts/client.js +9 -0
  18. package/src/quilts/describe.js +31 -1
  19. package/src/quilts/events.js +13 -2
  20. package/src/quilts/export-remote.js +156 -0
  21. package/src/quilts/import-follow.js +236 -0
  22. package/src/quilts/import-git.js +6 -3
  23. package/src/quilts/import.js +67 -5
  24. package/src/quilts/index.js +28 -6
  25. package/src/quilts/keys.js +186 -0
  26. package/src/quilts/merge.js +16 -1
  27. package/src/quilts/oauth.js +202 -0
  28. package/src/quilts/reviews.js +18 -5
  29. package/src/quilts/routes.js +61 -1
  30. package/src/quilts/runner.js +95 -0
  31. package/src/quilts/server.js +144 -0
  32. package/src/quilts/service.js +41 -1
  33. package/src/quilts/setup.js +9 -2
  34. package/src/quilts/store-memory.js +164 -3
  35. package/src/quilts/store-rpc.js +12 -1
  36. package/src/quilts/tools.js +39 -11
  37. package/src/quilts/window.js +2 -0
  38. package/src/relay-mcp.js +33 -3
  39. package/src/relay-prune.js +4 -1
  40. package/src/server.js +317 -14
  41. package/src/session-follow.js +81 -0
  42. package/src/session-layout3.js +60 -0
  43. package/src/session-migrate.js +119 -0
  44. package/src/session-quilt.js +145 -0
  45. package/src/session-seam.js +71 -0
  46. package/src/session.js +74 -2
  47. package/src/source-live/live-block.js +4 -3
  48. package/src/source-live/live-square.js +1 -1
@@ -8,11 +8,11 @@
8
8
  // quilt and square, and seam_commit takes the files' new contents.
9
9
  import path from 'node:path'
10
10
  import { z } from 'zod'
11
- import { describeLog, describeShow, describeDiff, describeBlame, describeStatus, describeMerge, describeWrite, describeSquare, describeSquareStatus, describeEvents, describeTag, describeRelease, describeImport, describeRestore, describeReset, describeClean, describeLive, describeClaims, describeResolutions, describeLiveSet, describeMergeBase, describeGrep, describeBisect, describeCheck, describeReviews, describeReviewed, describeSquareCheck, describePolicy, describeKeys, describeTokens, describeNewToken } from './describe.js'
11
+ import { describeLog, describeShow, describeDiff, describeBlame, describeStatus, describeMerge, describeWrite, describeSquare, describeSquareStatus, describeEvents, describeTag, describeRelease, describeImport, describeImportRecord, describeRestore, describeReset, describeClean, describeLive, describeClaims, describeResolutions, describeLiveSet, describeMergeBase, describeGrep, describeBisect, describeCheck, describeReviews, describeReviewed, describeSquareCheck, describePolicy, describeKeys, describeTokens, describeNewToken, describeCheckRuns, describeRerun } from './describe.js'
12
12
  import { signWithLocalKey, localKey, makeLocalKey, signingKeyFile } from './signing.js'
13
13
  import { status as blockStatus, commitFolder, amendFolder, syncTo, restore, reset, clean, localChanges } from './block.js'
14
14
 
15
- export const SEAM_TOOLS = ['seam_quilts', 'seam_open_square', 'seam_squares', 'seam_log', 'seam_show', 'seam_diff', 'seam_blame', 'seam_cat', 'seam_ls', 'seam_commit', 'seam_merge', 'seam_rebase', 'seam_revert', 'seam_cherry_pick', 'seam_resolve', 'seam_square_status', 'seam_square_handoff', 'seam_abandon', 'seam_reopen', 'seam_fold', 'seam_split', 'seam_events', 'seam_events_webhook', 'seam_line_list', 'seam_line_new', 'seam_line_freeze', 'seam_line_unfreeze', 'seam_line_require', 'seam_reviews', 'seam_review', 'seam_tag_list', 'seam_tag', 'seam_release_list', 'seam_release', 'seam_import_git', 'seam_restore', 'seam_reset', 'seam_clean', 'seam_live', 'seam_live_set', 'seam_bisect', 'seam_grep', 'seam_search', 'seam_describe', 'seam_merge_base', 'seam_claims', 'seam_claim', 'seam_unclaim', 'seam_check', 'seam_resolutions', 'seam_forget_resolution', 'seam_sign', 'seam_keys', 'seam_key_add', 'seam_key_remove', 'seam_token_list', 'seam_token_new', 'seam_token_revoke']
15
+ export const SEAM_TOOLS = ['seam_quilts', 'seam_open_square', 'seam_squares', 'seam_log', 'seam_show', 'seam_diff', 'seam_blame', 'seam_cat', 'seam_ls', 'seam_commit', 'seam_merge', 'seam_rebase', 'seam_revert', 'seam_cherry_pick', 'seam_resolve', 'seam_square_status', 'seam_square_handoff', 'seam_abandon', 'seam_square_merged', 'seam_reopen', 'seam_fold', 'seam_split', 'seam_events', 'seam_events_webhook', 'seam_line_list', 'seam_line_new', 'seam_line_freeze', 'seam_line_unfreeze', 'seam_line_require', 'seam_reviews', 'seam_review', 'seam_tag_list', 'seam_tag', 'seam_release_list', 'seam_release', 'seam_import_git', 'seam_import_remote', 'seam_import_status', 'seam_import_poke', 'seam_import_cutover', 'seam_export_remote', 'seam_export_status', 'seam_restore', 'seam_reset', 'seam_clean', 'seam_live', 'seam_live_set', 'seam_bisect', 'seam_grep', 'seam_search', 'seam_describe', 'seam_merge_base', 'seam_claims', 'seam_claim', 'seam_unclaim', 'seam_check', 'seam_resolutions', 'seam_forget_resolution', 'seam_sign', 'seam_keys', 'seam_key_add', 'seam_key_remove', 'seam_token_list', 'seam_token_new', 'seam_token_revoke', 'seam_checks', 'seam_check_rerun']
16
16
 
17
17
  /** What every agent is told about source control, in each MCP's instructions. */
18
18
  export const SEAM_GUIDE = 'Quilt source control (quilts): a quilt is a repository on the Quilt server; you work in a square (your own branch and working copy), commit save points in it, and merge it into main, where it becomes a patch. ' +
@@ -32,6 +32,8 @@ export const SEAM_DESCRIPTIONS = {
32
32
  seam_review: 'Reviews a square: kind approve, request_changes (say what in body), comment (body; path and line put it on a line of a file at the square\'s head) or dismiss (takes back your verdict). Nobody approves a square they or the person they work for opened or hold. An approval counts for the square\'s latest commit only.',
33
33
  seam_claims: 'The claims that hold now: which square is working on which paths, who, and why (square: only that square\'s).',
34
34
  seam_claim: 'Claims paths (patterns: files, folders or globs like src/api/**) for your square, so writes to them from other squares on its line are refused until your square merges or you let go. note says what for. Claim before a larger change across several files.',
35
+ seam_check_rerun: 'Asks a check\'s runners to run it again: with square, on that square\'s latest commit (pre_merge); with line, on the line\'s tip (post_merge); or on commit. A run of it still open there is cancelled (check.cancelled to its runners). Returns the new run_key.',
36
+ seam_checks: 'The quilt\'s check runs, newest first (filter by state, name, tier, line, square). A runner polls state "queued" for the checks its line\'s policy names it for (or gets check.requested events), runs each on its commit and reports with seam_check and the run\'s run_key.',
35
37
  seam_check: 'Reports a post-merge check on a line (status red, green or flaky; a red check halves the line\'s merge window, green restores it). With patch (the patch number it failed at, found for example with seam_bisect) a red check reverts that patch on the line by itself and opens a square holding the change again for its holder to fix. With square instead, the result (green or red) is for that square\'s latest commit, which its line may require green before a merge.',
36
38
  seam_unclaim: 'Lets go of claims on your square (patterns: the patterns or claim ids).',
37
39
  seam_live_set: 'Shares your square\'s uncommitted work so others can read it as sq:<id>:live before you commit: in a block folder the folder\'s changed files; elsewhere files: [{ path, content }] and delete: [paths]. Each call replaces what was shared; none clears it. It is not a commit: seam_commit keeps work.',
@@ -44,6 +46,12 @@ export const SEAM_DESCRIPTIONS = {
44
46
  seam_line_freeze: 'Freezes a line (admins): merges into it are refused with line_frozen until seam_line_unfreeze.',
45
47
  seam_line_unfreeze: 'Unfreezes a line (admins): it takes merges again.',
46
48
  seam_tag_list: 'The quilt\'s tags: name, commit, the patch it is, and whether it is a release.',
49
+ seam_import_remote: 'Imports a git remote (https:// or ssh) into a new quilt on the server. With follow, it keeps taking new commits on main and release/* every interval (and on seam_import_poke) and freezes those lines until cut-over, because git stays the authority. Returns the import id and its poke secret (shown once).',
50
+ seam_export_remote: 'Exports lines of a quilt to a git remote (admins) after each merge on them, as fast-forwards that are never forced; releases: true pushes release tags too. While an import follows git, only to a scratch remote (scratch: true).',
51
+ seam_export_status: 'The export to a git remote: its target, lines, state (idle, ok, retrying, refused), each line\'s last exported patch and sha, and the last error.',
52
+ seam_import_cutover: 'Ends a following import for good (admins): a last pass, a check that the remote\'s tips equal the imported ones, then main and release/* take merges in Seam and main exports to the source after each merge. Refuses with remote_moved if git moved.',
53
+ seam_import_status: 'An import from a git remote: its state (reading, following, done, failed), its lines with their git tips, and its last error.',
54
+ seam_import_poke: 'Makes a following import fetch its git remote now instead of at its next interval.',
47
55
  seam_import_git: 'Imports a git repository on this computer into a quilt: HEAD\'s branch as main, every other branch as a line of its name, every tag, submodules as gitlink entries. Run it again into the same quilt to add only the new commits.',
48
56
  seam_sign: 'Signs a commit you made (rev, default your square\'s head) with your Ed25519 key, so seam_log with show_signature shows it as yours. With no signature, signs with this computer\'s key (seam_key_add makes one); a hosted agent signs the text "seam-commit <commit id>\\n" itself and passes key and signature (base64).',
49
57
  seam_keys: 'Your signing keys (or member\'s): their ids, names, and when each was added or removed.',
@@ -59,9 +67,10 @@ export const SEAM_DESCRIPTIONS = {
59
67
  seam_squares: 'The open squares of a quilt, with holder, head, how far behind main each is and its conflicted paths. all: true for merged and abandoned ones too.',
60
68
  seam_square_status: 'One square as the server sees it (head, behind_by, conflicted paths), the patches merged under it that changed its files, and which files would conflict if merged now; in a block folder also the files changed since the last commit.',
61
69
  seam_square_handoff: 'Hands your square to another member (to), with a note on what is done and what is left. You stop being able to commit to it.',
70
+ seam_square_merged: 'Closes an open square whose work a person put into git by hand while an import follows git, when the follower could not match it (the diff was edited on the way). Admins; git_sha is the imported git commit that holds the work.',
62
71
  seam_abandon: 'Abandons a square whose work is not wanted. Its commits stay readable and seam_reopen brings it back.',
63
72
  seam_reopen: 'Reopens an abandoned square; you become its holder.',
64
- seam_events: 'What happened to your work since you last looked: patch.merged (a patch changed files your open square changed, and whether your merge would now conflict), conflict.added (a conflict was kept in your square), square.handoff. Pass after (the cursor from the last answer) to get only new ones.',
73
+ seam_events: 'What happened to your work since you last looked: patch.merged (a patch changed files your open square changed, and whether your merge would now conflict), conflict.added (a conflict was kept in your square), square.handoff, check.requested (you run a check a line names you for: run it on the commit and report with seam_check and its run_key). Pass after (the cursor from the last answer) to get only new ones.',
65
74
  seam_events_webhook: 'Has the quilt server POST your events to a URL (signed with HMAC-SHA256 over <timestamp>.<body>, retried 3 times), so you are woken instead of polling: url, events (default all), secret (made for you if left out). url: "off" removes it.',
66
75
  seam_fold: 'Folds a square (square) into another square you hold (into): its work becomes one commit there and the square closes. Overlapping changes become conflict markers in the target.',
67
76
  seam_split: 'Splits your square by paths: groups: [[paths], ...], each group moves to a new square (same base) as one commit; the largest group, and paths no group names, stay in this square.',
@@ -88,12 +97,12 @@ export const SEAM_DESCRIPTIONS = {
88
97
 
89
98
  export const SEAM_INPUTS = {
90
99
  seam_live: { quilt: QUILT, square: SQUARE },
91
- seam_line_require: { quilt: QUILT, line: z.string().max(120), approvals: z.number().int().min(0).max(20).optional(), checks: z.array(z.string().max(100)).max(20).optional().describe('Check names that must be green on a square\'s latest commit, e.g. ["ci"]; [] for none') },
100
+ seam_line_require: { quilt: QUILT, line: z.string().max(120), approvals: z.number().int().min(0).max(20).optional(), checks: z.array(z.string().max(100)).max(20).optional().describe('Check names that must be green on a square\'s latest commit, e.g. ["ci"]; [] for none'), runners: z.record(z.string().max(100), z.array(z.string().max(200)).max(20)).optional().describe('Who runs each check, e.g. {"ci": ["agent:ci"]}: they are asked to run it (check.requested) and only their verdicts count'), tiers: z.record(z.string().max(100), z.array(z.enum(['pre_merge', 'post_merge'])).max(2)).optional().describe('Where each check runs, e.g. {"e2e": ["post_merge"]}; default: a required check runs pre_merge on the square, a check with runners runs post_merge on the line'), tests: z.array(z.string().max(300)).max(50).optional().describe('Globs of the line\'s test files, e.g. ["test/**"]: a pre-merge check.requested names the tests a change reaches (affected)'), ci_config: z.array(z.string().max(300)).max(50).optional().describe('Globs of ci config: a change there makes affected confidence "all"') },
92
101
  seam_reviews: { quilt: QUILT, square: SQUARE },
93
102
  seam_review: { quilt: QUILT, square: SQUARE, kind: z.enum(['approve', 'request_changes', 'comment', 'dismiss']), body: z.string().max(20000).optional(), path: z.string().max(1000).optional().describe('A file, for a comment on it'), line: z.number().int().min(1).optional().describe('A line of that file at the square\'s head') },
94
103
  seam_claims: { quilt: QUILT, square: SQUARE },
95
104
  seam_claim: { quilt: QUILT, square: SQUARE, patterns: z.array(z.string().max(500)).min(1).max(50).describe('Files, folders or globs, e.g. src/api/** or README.md'), note: z.string().max(300).optional().describe('What you are doing with them') },
96
- seam_check: { quilt: QUILT, check: z.string().max(200).describe('The check\'s name, e.g. ci'), status: z.enum(['red', 'green', 'flaky']), square: z.string().max(100).optional().describe('A square (sq:…): the result is for its latest commit, which a line may require green before a merge'), line: z.string().max(110).optional().describe('Default main'), patch: z.number().int().min(1).optional().describe('The patch number the check failed at: reverts it') },
105
+ seam_check: { quilt: QUILT, check: z.string().max(200).describe('The check\'s name, e.g. ci'), status: z.enum(['red', 'green', 'flaky']), run_key: z.string().max(600).optional().describe('The run_key of the check.requested event or run being answered'), url: z.string().max(2000).optional().describe('A link to the run\'s log (http(s) or file://), kept on the run'), square: z.string().max(100).optional().describe('A square (sq:…): the result is for its latest commit, which a line may require green before a merge'), line: z.string().max(110).optional().describe('Default main'), patch: z.number().int().min(1).optional().describe('The patch number the check failed at: reverts it') },
97
106
  seam_unclaim: { quilt: QUILT, square: SQUARE, patterns: z.array(z.string().max(500)).min(1).max(50).describe('The claimed patterns (or claim ids)') },
98
107
  seam_live_set: { quilt: QUILT, square: SQUARE, files: z.array(z.object({ path: z.string().max(1000), content: z.string(), base64: z.boolean().optional(), mode: z.enum(['file', 'exec']).optional() })).max(2000).optional(), delete: z.array(z.string().max(1000)).max(2000).optional() },
99
108
  seam_restore: { paths: z.array(z.string().max(1000)).min(1).max(200).describe('Files or folders, relative to the block folder ("." for all)') },
@@ -105,6 +114,12 @@ export const SEAM_INPUTS = {
105
114
  seam_line_freeze: { quilt: QUILT, line: z.string().max(110) },
106
115
  seam_line_unfreeze: { quilt: QUILT, line: z.string().max(110) },
107
116
  seam_tag_list: { quilt: QUILT },
117
+ seam_import_remote: { url: z.string().max(2000).describe('The git remote: https://, ssh:// or user@host:path'), into: z.string().max(63).describe('The new quilt\'s slug'), follow: z.boolean().optional().describe('Keep following main and release/* until cut-over'), interval: z.number().int().min(60).optional().describe('Seconds between follow passes (default 600)'), credential: z.string().max(100).optional().describe('The name of the server secret that reads the remote (user:token or a token)') },
118
+ seam_export_remote: { quilt: z.string().max(100).optional().describe('The quilt'), target: z.string().max(2000).describe('The git remote'), lines: z.array(z.string().max(100)).max(20).optional().describe('Lines to export (default ["main"]); release/* for every release line'), releases: z.boolean().optional().describe('Push release tags too'), credential: z.string().max(100).optional().describe('The name of the server secret that may push'), scratch: z.boolean().optional().describe('A scratch remote nothing pins a sha from'), enabled: z.boolean().optional().describe('false turns the export off') },
119
+ seam_export_status: { quilt: z.string().max(100).optional().describe('The quilt') },
120
+ seam_import_cutover: { id: z.string().max(100).describe('The import id'), credential: z.string().max(100).optional().describe('The server secret that may push to the source (default the import\'s)') },
121
+ seam_import_status: { id: z.string().max(100).describe('The import id') },
122
+ seam_import_poke: { id: z.string().max(100).describe('The import id') },
108
123
  seam_import_git: { path: z.string().max(1000).describe('The git repository\'s folder'), into: z.string().max(63).optional().describe('The quilt\'s slug (default the folder\'s name); a quilt an import made takes only the new commits'), validate: z.boolean().optional().describe('Compare every patch written with git\'s tree') },
109
124
  seam_sign: { quilt: QUILT, rev: REV, key: z.string().max(40).optional().describe('Your key id (ed25519:…), with signature'), signature: z.string().max(200).optional().describe('Base64 Ed25519 signature of "seam-commit <commit id>\\n"') },
110
125
  seam_keys: { member: z.string().max(200).optional().describe('Whose keys (default yours)') },
@@ -140,9 +155,12 @@ export const SEAM_INPUTS = {
140
155
  },
141
156
  seam_square_handoff: { quilt: QUILT, square: SQUARE, to: z.string().max(200).describe('The member to hand it to'), note: z.string().max(2000).optional() },
142
157
  seam_abandon: { quilt: QUILT, square: SQUARE },
158
+ seam_square_merged: { quilt: QUILT, square: SQUARE, git_sha: z.string().max(64).describe('The git commit (full sha) that holds the square\'s work') },
143
159
  seam_reopen: { quilt: QUILT, square: SQUARE },
144
- seam_events: { quilt: QUILT, after: z.number().int().min(0).optional(), kinds: z.array(z.enum(['patch.merged', 'conflict.added', 'square.handoff'])).optional() },
145
- seam_events_webhook: { quilt: QUILT, url: z.string().max(2000), events: z.array(z.enum(['patch.merged', 'conflict.added', 'square.handoff'])).optional(), secret: z.string().min(16).max(200).optional() },
160
+ seam_check_rerun: { quilt: QUILT, check: z.string().max(200).describe('The check\'s name, e.g. ci'), square: z.string().max(100).optional().describe('A square (sq:…): re-run on its latest commit'), line: z.string().max(110).optional().describe('A line: re-run on its tip (default main when no square)'), commit: z.string().max(80).optional().describe('A commit instead of the head or tip') },
161
+ seam_checks: { quilt: QUILT, state: z.enum(['queued', 'running', 'done', 'cancelled']).optional(), name: z.string().max(100).optional(), tier: z.enum(['pre_merge', 'post_merge']).optional(), line: z.string().max(120).optional(), square: z.string().max(100).optional() },
162
+ seam_events: { quilt: QUILT, after: z.number().int().min(0).optional(), kinds: z.array(z.enum(['patch.merged', 'conflict.added', 'square.handoff', 'check.requested'])).optional() },
163
+ seam_events_webhook: { quilt: QUILT, url: z.string().max(2000), events: z.array(z.enum(['patch.merged', 'conflict.added', 'square.handoff', 'check.requested'])).optional(), secret: z.string().min(16).max(200).optional() },
146
164
  seam_fold: { quilt: QUILT, square: SQUARE, into: z.string().max(100).describe('The square (sq:…) that takes the work'), message: z.string().max(65536).optional() },
147
165
  seam_split: { quilt: QUILT, square: SQUARE, groups: z.array(z.array(z.string().max(1000)).min(1).max(2000)).min(1).max(20).describe('Paths (files or directories) per new square'), titles: z.array(z.string().max(200)).max(20).optional() },
148
166
  seam_merge: { quilt: QUILT, square: SQUARE, from: z.string().max(200).optional().describe('Merge this square (sq:…) or revision into your square instead of merging your square into a line'), into: z.string().max(100).optional().describe('The line, default main'), message: z.string().max(65536).optional(), dry_run: z.boolean().optional(), on_conflict: z.enum(['rebase', 'leave']).optional() },
@@ -169,8 +187,9 @@ export async function runSeamTool (name, args, { client, found = null }) {
169
187
  try {
170
188
  switch (name) {
171
189
  case 'seam_line_require': {
172
- if (args.approvals === undefined && args.checks === undefined) return describePolicy(await client.linePolicy(needQuilt(), args.line))
173
- return describePolicy(await client.setLinePolicy(needQuilt(), args.line, { ...(args.approvals !== undefined ? { approvals: args.approvals } : {}), ...(args.checks ? { checks: args.checks } : {}) }))
190
+ const extra = Object.fromEntries(['tiers', 'tests', 'ci_config'].filter((k) => args[k] !== undefined).map((k) => [k, args[k]]))
191
+ if (args.approvals === undefined && args.checks === undefined && args.runners === undefined && !Object.keys(extra).length) return describePolicy(await client.linePolicy(needQuilt(), args.line))
192
+ return describePolicy(await client.setLinePolicy(needQuilt(), args.line, { ...(args.approvals !== undefined ? { approvals: args.approvals } : {}), ...(args.checks ? { checks: args.checks } : {}), ...(args.runners ? { runners: args.runners } : {}), ...extra }))
174
193
  }
175
194
  case 'seam_reviews': return describeReviews(await client.reviews(needQuilt(), needSquare()))
176
195
  case 'seam_review': return describeReviewed(await client.review(needQuilt(), needSquare(), { kind: args.kind, body: args.body || '', ...(args.path ? { path: args.path } : {}), ...(args.line ? { line: args.line } : {}) }))
@@ -180,8 +199,10 @@ export async function runSeamTool (name, args, { client, found = null }) {
180
199
  for (const p of args.patterns) made.push(await client.claim(needQuilt(), needSquare(), { pattern: p, note: args.note }))
181
200
  return made.map((c) => `Claimed ${c.pattern} for ${c.square}: writes to it from other squares on ${c.line} are refused until the square merges or seam_unclaim.`).join('\n')
182
201
  }
183
- case 'seam_check': if (args.square) return describeSquareCheck(await client.reportSquareCheck(needQuilt(), args.square, { check: args.check, status: args.status }))
184
- return describeCheck(await client.reportCheck(needQuilt(), args.line || 'main', { check: args.check, status: args.status, ...(args.patch ? { patch: args.patch } : {}) }))
202
+ case 'seam_check': if (args.square) return describeSquareCheck(await client.reportSquareCheck(needQuilt(), args.square, { check: args.check, status: args.status, ...(args.run_key ? { run_key: args.run_key } : {}), ...(args.url ? { url: args.url } : {}) }))
203
+ return describeCheck(await client.reportCheck(needQuilt(), args.line || 'main', { check: args.check, status: args.status, ...(args.patch ? { patch: args.patch } : {}), ...(args.run_key ? { run_key: args.run_key } : {}), ...(args.url ? { url: args.url } : {}) }))
204
+ case 'seam_check_rerun': return describeRerun(await client.rerunCheck(needQuilt(), { check: args.check, ...(args.square ? { square: args.square } : { line: args.line || 'main' }), ...(args.commit ? { commit: args.commit } : {}) }))
205
+ case 'seam_checks': return describeCheckRuns(await client.checkRuns(needQuilt(), { state: args.state, name: args.name, tier: args.tier, line: args.line, square: args.square }))
185
206
  case 'seam_unclaim': {
186
207
  const out = []
187
208
  for (const p of args.patterns) out.push(await client.unclaim(needQuilt(), needSquare(), p))
@@ -260,6 +281,7 @@ export async function runSeamTool (name, args, { client, found = null }) {
260
281
  return signed(await client.commit(quilt, sq.id, { expect_head: sq.head, message: args.message, changes }), 'Committed')
261
282
  }
262
283
  case 'seam_square_handoff': { const sq = await client.handoff(needQuilt(), needSquare(), { to: args.to, note: args.note }); return `Handed ${sq.short} to ${sq.holder}.` }
284
+ case 'seam_square_merged': { const sq = await client.squareMerged(needQuilt(), needSquare(), { git_sha: args.git_sha }); return `${sq.short} is merged by hand: its work is patch ${sq.merged.patch_no} on ${sq.merged.line} (git ${sq.merged.git_sha.slice(0, 12)}).` }
263
285
  case 'seam_abandon': { const sq = await client.abandon(needQuilt(), needSquare(), inFolder ? { expect_head: block.head } : {}); return `Abandoned ${sq.short}; seam_reopen brings it back.` }
264
286
  case 'seam_reopen': { const sq = await client.reopen(needQuilt(), needSquare()); return `Reopened ${describeSquare(sq)}.` }
265
287
  case 'seam_events': return describeEvents(await client.events(needQuilt(), { after: args.after, kinds: args.kinds }))
@@ -288,6 +310,12 @@ export async function runSeamTool (name, args, { client, found = null }) {
288
310
  case 'seam_line_unfreeze': { const l = await client.unfreezeLine(needQuilt(), args.line); return `${l.name} takes merges again.` }
289
311
  case 'seam_tag_list': { const ts = await client.tags(needQuilt()); return ts.length ? ts.map(describeTag).join('\n') : 'No tags yet.' }
290
312
  case 'seam_release_list': { const rs = await client.releases(needQuilt()); return rs.length ? rs.map(describeRelease).join('\n') : 'No releases yet.' }
313
+ case 'seam_import_remote': { const r = await client.startImport({ url: args.url, slug: args.into, follow: !!args.follow, interval: args.interval, credential: args.credential }); return `Importing ${args.url} into ${args.into} (import ${r.id})${r.follow ? `, following every ${r.interval_s} s; poke secret ${r.poke_secret} (shown once)` : ''}.` }
314
+ case 'seam_export_remote': { const r = await client.setExport(needQuilt(), { target: args.target, lines: args.lines, releases: args.releases, credential: args.credential, scratch: args.scratch, enabled: args.enabled !== false }); return r.enabled ? `Exporting ${r.lines.join(', ')} to ${r.target} after each merge.` : `The export to ${r.target} is off.` }
315
+ case 'seam_export_status': { const r = await client.exportStatus(needQuilt()); return JSON.stringify({ target: r.target, lines: r.lines, releases: r.releases, enabled: r.enabled, status: r.status }, null, 2) }
316
+ case 'seam_import_cutover': { const r = await client.cutover(args.id, { credential: args.credential }); return `Cut over: import ${r.import.id} is done; main exports to ${r.export.target} after each merge.` }
317
+ case 'seam_import_status': return describeImportRecord(await client.importStatus(args.id))
318
+ case 'seam_import_poke': { const r = await client.pokeImport(args.id); return `Poked: import ${r.id} fetches the remote now.` }
291
319
  case 'seam_import_git': {
292
320
  const { importGit } = await import('./import-git.js')
293
321
  const { default: nodePath } = await import('node:path')
@@ -81,6 +81,8 @@ export function windowOps (svc, { max = DEFAULT_WINDOW, quiltMax = DEFAULT_WINDO
81
81
  const check = String(body.check || '').trim()
82
82
  if (!check || check.length > 200) throw new QuiltError(400, 'bad_request', 'check: the name of the check, up to 200 characters')
83
83
  if (!['red', 'green', 'flaky'].includes(body.status)) throw new QuiltError(400, 'bad_request', "status: 'red', 'green' or 'flaky'")
84
+ const noted = svc.internals.noteVerdict ? await svc.internals.noteVerdict(me, q, { name: check, tier: 'post_merge', commit: ln.tip, line, verdict: body.status, run_key: body.run_key, url: body.url }) : {}
85
+ if (noted.advisory) { const w0 = windowOf(ln); return { line, window: w0.window, window_max: w0.max, red: Object.keys(w0.red).sort(), advisory: true } }
84
86
  const w = windowOf(ln)
85
87
  const red = { ...w.red }
86
88
  let window = w.window
package/src/relay-mcp.js CHANGED
@@ -36,8 +36,9 @@ import { clashAction, CLASH_DESCRIPTION, clashSchema } from './relay-clash.js'
36
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
37
  import { UpdateCheck } from './update-check.js'
38
38
  import { TASK_WORKFLOW, pickupBrief, doneRefusal, verifiedEnough, verifiedLine, qaRefusal, qaNotesEnough, qaNotesLine, pickChecklist, MAX_VERIFIED } from './agent-task-workflow.js'
39
- import { HistoryLog, queryHistory, parseSince, formatHistory, currentTask, lineDiff } from './history.js'
39
+ import { HistoryLog, queryHistory, parseSince, formatHistory, currentTask, lineDiff, legacyTail } from './history.js'
40
40
  import { changeRefusal, TALK_REFUSED } from './session-access.js'
41
+ import { MIGRATE_SESSION_DESCRIPTION, describeQuiltPlan } from './session-quilt.js'
41
42
  import { chatAbout, renderChatAbout, waitingOn, needsNoReply, renderUnanswered, heldRefusal, queuedFor, renderQueueNotice, renderQueued, answered, unaddressed, CHAT_RULES, RE_HELP, replyFor, renderReplied, PERSON_CHAT, PERSON_INBOX } from './duties.js'
42
43
  import { describeSubscription, describeWebhookTest, COALESCE_HELP, WEBHOOK_EVENTS } from './webhooks.js'
43
44
  import { registerWorkspaceTools, bytesFetcher, WORKSPACE_GUIDE_HOSTED } from './workspace-tools.js'
@@ -47,6 +48,7 @@ import { isDiff, hasEntry, listPaths, readPath, readPaths, textOf, notePresent,
47
48
  import { hostedRooms, hostedIn, newHosted } from './hosted-agents.js'
48
49
  import { turnSyncStep, ROOM_KEY } from './relay-turn-sync.js'
49
50
  import { describeSecretNames, SECRET_NAMES_DESCRIPTION } from './secret-names.js'
51
+ import { quiltOf, seamRefusal, seamNote } from './session-seam.js'
50
52
  import { TRANSCRIPT_DESCRIPTION, renderLines, readLines, inVoice, transcribeSetting, voiceStatusLines } from './voice.js'
51
53
 
52
54
  const FEED_CAP = 300
@@ -152,6 +154,11 @@ function takeStock (doc, me) {
152
154
  const linkInboxes = new WeakMap()
153
155
  const text = (t) => ({ content: [{ type: 'text', text: t }] })
154
156
  const fail = (t) => ({ content: [{ type: 'text', text: t }], isError: true })
157
+ // quilt_status in a session that is a quilt starts with how work goes there (session-seam.js).
158
+ const withSeamNote = (r, quilt) => {
159
+ if (r && typeof r.then === 'function') return r.then((x) => withSeamNote(x, quilt))
160
+ return r && Array.isArray(r.content) ? { ...r, content: [{ type: 'text', text: seamNote(quilt) }, ...r.content] } : r
161
+ }
155
162
 
156
163
  function ago (ts) {
157
164
  const s = Math.max(0, Math.round((Date.now() - ts) / 1000))
@@ -220,6 +227,10 @@ function sessionTools (server, ctx) {
220
227
  // A hosted agent with no branch of its own is pinned to the active branch by its first call
221
228
  // on files, so a read and the write after it never land on two different branches.
222
229
  if (ctx.pin && FILE_TOOLS.has(name)) ctx.pin(room)
230
+ // A session that is a quilt has no GitHub path for agents: each GitHub tool names its Seam tool.
231
+ const quilt = quiltOf(room.meta)
232
+ const refused = seamRefusal(name, quilt)
233
+ if (refused) return fail(refused)
223
234
  // The audit trail: which tool, on what (a path, a pattern or a task id; never contents).
224
235
  const caller = ctx.who(room)
225
236
  if (caller && caller.id && room.audit) {
@@ -234,7 +245,10 @@ function sessionTools (server, ctx) {
234
245
  // Changes the tool makes to the session (files, chat, the board) go in this agent's audit trail.
235
246
  const prev = room.auditAs
236
247
  room.auditAs = caller && caller.id
237
- try { return queueNote(room, fn(args || {}, parts)) } finally { room.auditAs = prev }
248
+ try {
249
+ const r = queueNote(room, fn(args || {}, parts))
250
+ return name === 'quilt_status' && quilt ? withSeamNote(r, quilt) : r
251
+ } finally { room.auditAs = prev }
238
252
  }, session))
239
253
  })
240
254
  if (ctx.updates) {
@@ -345,7 +359,8 @@ function sessionTools (server, ctx) {
345
359
  }, ({ path: p, by, since, task, with_diff, limit }, { room }) => {
346
360
  const from = parseSince(since)
347
361
  if (from === undefined) return fail('since: use a duration like 2h or 3d, "today", "yesterday", or a date.')
348
- const list = queryHistory(historyOf(room).entries(), { path: p ? cleanPath(p) : '', by, since: from ?? undefined, task, limit: limit || 30 })
362
+ const q = process.env.QUILT_SOURCE_URL && room.meta && room.meta.quilt_id ? room.meta.quilt : null
363
+ const list = queryHistory(legacyTail(historyOf(room).entries(), { at: q && q.at }), { path: p ? cleanPath(p) : '', by, since: from ?? undefined, task, limit: limit || 30 })
349
364
  return text(formatHistory(list, { withDiff: !!with_diff }))
350
365
  })
351
366
 
@@ -413,6 +428,18 @@ function sessionTools (server, ctx) {
413
428
  return text(`${branchesMarkdown(br, { limit: 40 })}\n\n${BRING_IN_NOTE}`)
414
429
  })
415
430
 
431
+ // Seam (plans/quilt-source-control/16), behind QUILT_SOURCE_URL: what making this session a
432
+ // quilt would do. Only the dry run: the move itself is the owner's, from the app.
433
+ if (process.env.QUILT_SOURCE_URL) {
434
+ tool('quilt_migrate_session', {
435
+ description: MIGRATE_SESSION_DESCRIPTION,
436
+ inputSchema: { dry_run: z.literal(true).optional().describe('Always true: report what would happen, change nothing.') }
437
+ }, (_, { room }) => {
438
+ if (!room.quiltPlan) return fail('This session cannot become a quilt here.')
439
+ try { return text(describeQuiltPlan(room.quiltPlan(ctx.access(room)))) } catch (e) { return fail(e.message) }
440
+ })
441
+ }
442
+
416
443
  const taskMap = (doc) => doc.getMap('tasks')
417
444
 
418
445
  const reader = () => ({ name: me, tool: ctx.tool(), asAi: false, agent: true })
@@ -1661,6 +1688,7 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
1661
1688
  }, async ({ branch, create, session }) => {
1662
1689
  const c = current(session)
1663
1690
  if (c.error) return fail(c.error)
1691
+ { const no = seamRefusal('quilt_git_branch', quiltOf(c.room.meta)); if (no) return fail(no) }
1664
1692
  const room = c.room
1665
1693
  if (!branch) {
1666
1694
  const members = []
@@ -1708,6 +1736,7 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
1708
1736
  }, async ({ session }) => {
1709
1737
  const c = current(session)
1710
1738
  if (c.error) return fail(c.error)
1739
+ { const no = seamRefusal('quilt_sync_branch', quiltOf(c.room.meta)); if (no) return fail(no) }
1711
1740
  const room = c.room
1712
1741
  if (room.meta.layout !== 2 || !room.syncUpstreamNow) return fail('This session has no branches on the relay.')
1713
1742
  try {
@@ -1723,6 +1752,7 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
1723
1752
  }, ({ token, session }) => {
1724
1753
  const c = current(session)
1725
1754
  if (c.error) return fail(c.error)
1755
+ { const no = seamRefusal('quilt_github_token', quiltOf(c.room.meta)); if (no) return fail(no) }
1726
1756
  if (!c.access || !c.access.owner) return fail('Only the session owner can set the GitHub token: ask them.')
1727
1757
  try {
1728
1758
  const r = c.room.setGithubToken(token)
@@ -55,8 +55,11 @@ export const lastActive = (b) => Math.max(Number(b && (b.used || b.seen)) || 0,
55
55
  /** Branches nobody has worked on for INACTIVE_MS: not the default, not main or master, not in use. */
56
56
  export function inactiveBranches (room, now = Date.now(), inactiveMs = INACTIVE_MS) {
57
57
  const on = inUse(room, now)
58
+ // A quilt's squares' live documents (layout 3) stay: their squares decide when they go. Agents'
59
+ // own branches are named quilt/<agent>/… too, so only the keys the room lists are kept.
60
+ const live = new Set(Object.values(room.meta.quilt?.live || {}))
58
61
  return Object.entries(room.meta.branches)
59
- .filter(([k, b]) => k !== room.defaultKey && !TRUNKS.has(k) && !on.has(k) && now - lastActive(b) >= inactiveMs)
62
+ .filter(([k, b]) => k !== room.defaultKey && !TRUNKS.has(k) && !live.has(k) && !on.has(k) && now - lastActive(b) >= inactiveMs)
60
63
  .map(([k]) => k)
61
64
  }
62
65