heyquilt 0.3.122 → 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.
- package/RELEASES.md +273 -17
- package/bin/quilt.js +55 -9
- package/desktop/main.js +16 -2
- package/package.json +3 -2
- package/src/agent-task-workflow.js +2 -2
- package/src/agents/common.js +13 -0
- package/src/ai-vm.js +9 -1
- package/src/api/activity.js +5 -0
- package/src/api/memory-store.js +16 -9
- package/src/api/notify.js +134 -0
- package/src/api/routes/browsers.js +29 -0
- package/src/api/routes/github.js +0 -13
- package/src/api/routes/relay.js +26 -3
- package/src/api/routes/session-migrate.js +7 -4
- package/src/api/routes/sessions.js +73 -1
- package/src/api/server.js +13 -7
- package/src/api/supabase-store.js +43 -4
- package/src/autowork.js +182 -16
- package/src/connection.js +36 -9
- package/src/control.js +129 -15
- package/src/force-date.js +27 -0
- package/src/fsutil.js +13 -3
- package/src/gitstate.js +38 -2
- package/src/history.js +13 -3
- package/src/mcp.js +87 -12
- package/src/merge3.js +11 -3
- package/src/now-summary.js +113 -0
- package/src/pathrules.js +12 -0
- package/src/protocol.js +10 -2
- package/src/quilt-default.js +84 -0
- package/src/quilt-history.js +106 -0
- package/src/quilt-remote.js +78 -0
- package/src/relay-commit.js +5 -0
- package/src/relay-lease.js +6 -2
- package/src/relay-mcp.js +151 -17
- package/src/relay-outbound.js +1 -1
- package/src/release-words.js +2 -2
- package/src/runner.js +20 -7
- package/src/seam-access.js +97 -0
- package/src/seam-claims.js +53 -0
- package/src/seam-url.js +15 -0
- package/src/server.js +1517 -86
- package/src/session-follow.js +461 -27
- package/src/session-layout3.js +410 -4
- package/src/session-migrate.js +15 -11
- package/src/session-quilt.js +1 -1
- package/src/session-seam.js +233 -20
- package/src/session.js +848 -67
- package/src/source-live/bring-in.js +283 -0
- package/src/source-live/live-block.js +504 -33
- package/src/source-live/live-square.js +16 -5
- package/src/status.js +5 -0
- package/src/ui/agent-guide.js +1 -1
- package/src/ui/app.css +85 -20
- package/src/ui/app.js +4 -1
- package/src/ui/chat.js +9 -3
- package/src/ui/cloud-ai-model.js +13 -0
- package/src/ui/cloud-ai.js +10 -2
- package/src/ui/common.js +6 -2
- package/src/ui/fileview.js +20 -7
- package/src/ui/force-words.js +19 -0
- package/src/ui/git-rule.js +9 -0
- package/src/ui/home.js +0 -147
- package/src/ui/loom-model.js +15 -1
- package/src/ui/quiltview.js +213 -0
- package/src/ui/seam-model.js +251 -0
- package/src/ui/session.js +413 -31
- package/src/ui/voice-call.js +83 -14
- package/src/ui/voice-model.js +39 -0
- package/src/ui/voice.js +167 -17
- package/src/ui-server.js +80 -28
- package/src/update-check.js +25 -1
- package/src/voice.js +13 -6
- package/src/git.js +0 -147
- package/src/quilts/AGENTS.md +0 -1
- package/src/quilts/CLAUDE.md +0 -1
- package/src/quilts/accounts.js +0 -136
- package/src/quilts/affected.js +0 -132
- package/src/quilts/archiver.js +0 -175
- package/src/quilts/autorevert.js +0 -49
- package/src/quilts/bisect-run.js +0 -31
- package/src/quilts/blame.js +0 -103
- package/src/quilts/block.js +0 -347
- package/src/quilts/bucket.js +0 -25
- package/src/quilts/cbor.js +0 -92
- package/src/quilts/checks.js +0 -308
- package/src/quilts/claims.js +0 -144
- package/src/quilts/cli.js +0 -536
- package/src/quilts/client.js +0 -137
- package/src/quilts/conflict-format.js +0 -190
- package/src/quilts/describe.js +0 -296
- package/src/quilts/dogfood.js +0 -82
- package/src/quilts/errors.js +0 -8
- package/src/quilts/events.js +0 -171
- package/src/quilts/export-git.js +0 -130
- package/src/quilts/export-remote.js +0 -156
- package/src/quilts/export.js +0 -81
- package/src/quilts/fastmerge.js +0 -41
- package/src/quilts/hash.js +0 -44
- package/src/quilts/history.js +0 -266
- package/src/quilts/ids.js +0 -12
- package/src/quilts/import-follow.js +0 -236
- package/src/quilts/import-git.js +0 -294
- package/src/quilts/import.js +0 -196
- package/src/quilts/index.js +0 -94
- package/src/quilts/keys.js +0 -186
- package/src/quilts/limits.js +0 -67
- package/src/quilts/lines.js +0 -49
- package/src/quilts/live.js +0 -50
- package/src/quilts/merge-smart.js +0 -114
- package/src/quilts/merge.js +0 -501
- package/src/quilts/oauth.js +0 -202
- package/src/quilts/objects.js +0 -144
- package/src/quilts/pack.js +0 -118
- package/src/quilts/records.js +0 -211
- package/src/quilts/resolutions.js +0 -69
- package/src/quilts/reviews.js +0 -208
- package/src/quilts/routes.js +0 -186
- package/src/quilts/runner.js +0 -95
- package/src/quilts/server.js +0 -144
- package/src/quilts/service.js +0 -963
- package/src/quilts/setup.js +0 -41
- package/src/quilts/signing.js +0 -149
- package/src/quilts/squares.js +0 -239
- package/src/quilts/store-memory.js +0 -844
- package/src/quilts/store-rpc.js +0 -90
- package/src/quilts/store-supabase.js +0 -13
- package/src/quilts/tags.js +0 -64
- package/src/quilts/textdiff.js +0 -96
- package/src/quilts/tokens.js +0 -66
- package/src/quilts/tools.js +0 -403
- package/src/quilts/tree.js +0 -351
- package/src/quilts/webhook.js +0 -116
- package/src/quilts/window.js +0 -121
- package/src/quilts/xdiff.js +0 -435
- package/src/ui/branches.js +0 -111
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// "Make this a quilt" for a session with a git remote (Seam's plans/quilt-source-control/16, flag 9, and
|
|
2
|
+
// "An opt-in team's migration"): the steps the owner's one button runs, in order, each time it is
|
|
3
|
+
// clicked, from where the session stands. Pure: no I/O (test/unit/quilt-remote.test.js).
|
|
4
|
+
//
|
|
5
|
+
// 1. import Seam imports the GitHub repository and follows it (the owner's Seam GitHub App
|
|
6
|
+
// installation is the read credential; 16 step 1). The session stays git-mode.
|
|
7
|
+
// 2. link Once the import follows, the session reads the quilt's log (the shadow link).
|
|
8
|
+
// 3. cut-over In the owner's sitting: main must take pushes from Seam's export only (a ruleset only
|
|
9
|
+
// the repository's GitHub owner can add: plain steps), then the import is cut over
|
|
10
|
+
// with export to the same remote on, then the session moves in with its history.
|
|
11
|
+
|
|
12
|
+
export const SEAM_APP = { id: 5475977, slug: 'seam-by-heyquilt' }
|
|
13
|
+
|
|
14
|
+
/** owner/name of the session's GitHub remote, from its upstreams (the default branch's first), or null. */
|
|
15
|
+
export function githubRepoOf (upstreams, defaultKey) {
|
|
16
|
+
const all = Object.entries(upstreams || {})
|
|
17
|
+
const pick = all.find(([k]) => k === defaultKey) || all[0]
|
|
18
|
+
const m = pick && /^github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(\.git)?$/.exec(String((pick[1] && pick[1].repo) || ''))
|
|
19
|
+
return m ? m[1] : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Which of the owner's Seam GitHub installations reaches `repo`, or null. */
|
|
23
|
+
export function installationFor (gh, repo) {
|
|
24
|
+
const want = String(repo).toLowerCase()
|
|
25
|
+
return ((gh && gh.installations) || []).find((i) => (i.repos || []).some((r) => String(r.full_name || '').toLowerCase() === want)) || null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The step the session is at: 'import' (nothing yet), 'link' (importing), 'cutover' (linked to the quilt that follows). */
|
|
29
|
+
export function remoteStage ({ quiltImport = null, quiltShadow = null }) {
|
|
30
|
+
if (quiltShadow) return 'cutover'
|
|
31
|
+
if (quiltImport) return 'link'
|
|
32
|
+
return 'import'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether GitHub's rules for main (GET /repos/:o/:r/rules/branches/main) keep everyone but an app from pushing to it. */
|
|
36
|
+
export function mainGuarded (rules) {
|
|
37
|
+
return Array.isArray(rules) && rules.some((r) => r && r.type === 'update')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the repository's GitHub owner does so only Seam pushes to main (16's cut-over checklist, step 5). */
|
|
41
|
+
export function rulesetSteps (repo) {
|
|
42
|
+
return [
|
|
43
|
+
`On GitHub, open https://github.com/${repo}/settings/rules and choose New ruleset, then New branch ruleset.`,
|
|
44
|
+
'Name it "Seam is the authority on main" and set Enforcement status to Active.',
|
|
45
|
+
`Under Bypass list, add the app ${SEAM_APP.slug} (Seam), set to Always allow.`,
|
|
46
|
+
'Under Target branches, add the default branch and the pattern release/**.',
|
|
47
|
+
'Turn on Restrict updates, Restrict deletions and Block force pushes, then Create.',
|
|
48
|
+
'Ask everyone to stop pushing to main and wait for CI\'s last run there. Then click Move into Seam now.'
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The ruleset as GitHub's API takes it (for an owner who prefers `gh api -X POST repos/<repo>/rulesets --input`). */
|
|
53
|
+
export function rulesetBody () {
|
|
54
|
+
return {
|
|
55
|
+
name: 'Seam is the authority on main',
|
|
56
|
+
target: 'branch',
|
|
57
|
+
enforcement: 'active',
|
|
58
|
+
bypass_actors: [{ actor_id: SEAM_APP.id, actor_type: 'Integration', bypass_mode: 'always' }],
|
|
59
|
+
conditions: { ref_name: { include: ['~DEFAULT_BRANCH', 'refs/heads/release/**'], exclude: [] } },
|
|
60
|
+
rules: [{ type: 'update' }, { type: 'deletion' }, { type: 'non_fast_forward' }]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What the owner does when no Seam GitHub installation of theirs reaches the repository. */
|
|
65
|
+
export function installSteps (repo, installUrl = `https://github.com/apps/${SEAM_APP.slug}/installations/new`) {
|
|
66
|
+
return [
|
|
67
|
+
'Sign in to Seam at https://seam.heyquilt.com with the same account you use for Quilt.',
|
|
68
|
+
`Connect GitHub there and install the Seam app on ${repo} (${installUrl}). Seam reads the repository with it, and later pushes main to it.`,
|
|
69
|
+
'Then click Make this a quilt again.'
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One error whose message is the plain steps, with the steps alone in `steps` (the app lists them). */
|
|
74
|
+
export function stepsError (lead, steps) {
|
|
75
|
+
const e = new Error(`${lead}\n${steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`)
|
|
76
|
+
e.steps = steps
|
|
77
|
+
return e
|
|
78
|
+
}
|
package/src/relay-commit.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from './github-commit.js'
|
|
18
18
|
import { readTasks, addTask, updateTask } from './tasks.js'
|
|
19
19
|
import { addComment } from './task-comments.js'
|
|
20
|
+
import { quiltOf } from './session-seam.js'
|
|
20
21
|
|
|
21
22
|
export { CommitRefused }
|
|
22
23
|
|
|
@@ -566,6 +567,8 @@ export const mergeSchema = (z) => ({
|
|
|
566
567
|
* connection (or the owner's token) covers the repository. Cheap: from the relay's record.
|
|
567
568
|
*/
|
|
568
569
|
export function agentsCanCommit (room, key) {
|
|
570
|
+
// A session that is a quilt has no GitHub path for anyone's work: it lands with Seam (session-seam.js).
|
|
571
|
+
if (quiltOf(room.meta)) return false
|
|
569
572
|
if (agentCommitsOf(room) === 'off') return false
|
|
570
573
|
const found = repoRecordFor(room, key)
|
|
571
574
|
if (!found) return false
|
|
@@ -676,6 +679,8 @@ export async function finishTaskFor (room, { who, key, task: taskId }, { fetch =
|
|
|
676
679
|
const map = room.doc.getMap('tasks')
|
|
677
680
|
const task = readTasks(map).find((t) => t.id === taskId)
|
|
678
681
|
if (!task) return { merged: null }
|
|
682
|
+
// In a quilt a task's work lands with seam_merge: a pull request from before the switch is git's, and stays as it is.
|
|
683
|
+
if (quiltOf(room.meta)) return { merged: null }
|
|
679
684
|
// A clash task (clash.js) is the merge itself.
|
|
680
685
|
if (Object.values((room.meta && room.meta.upstreams) || {}).some((rec) => rec && rec.clash && rec.clash.task === taskId)) return { merged: null }
|
|
681
686
|
if (!agentsCanCommit(room, key) && !task.pr) return { merged: null }
|
package/src/relay-lease.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// One live session per repository and branch (plan §8, addendum 16.6). The session that pushes
|
|
2
|
-
// repo@branch holds its lease in the accounts API (
|
|
2
|
+
// repo@branch holds its lease in the accounts API (relay_leases), which works across relay
|
|
3
3
|
// machines; the relay renews every lease it holds every LEASE_HEARTBEAT_MS, for every session active
|
|
4
4
|
// in the last 24 hours (loaded or not), and keeps the list on its disk so a restart picks them up.
|
|
5
5
|
// A lease lapses LEASE_TTL_MS after its last heartbeat (two missed: a crashed relay's).
|
|
@@ -52,9 +52,13 @@ function localStore (now) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** A lease's key in the accounts API (relay_leases): "<repo> <branch>". */
|
|
56
|
+
export const leaseKeyOf = (repo, branch) => `${repo} ${branch}`
|
|
57
|
+
|
|
55
58
|
function apiStore ({ apiUrl, secret, fetch }) {
|
|
56
59
|
const call = async (what, body) => {
|
|
57
|
-
|
|
60
|
+
// `key` (relay_leases, 16 R2) beside repo and branch, which an API from before R2 reads instead: the same lease either way.
|
|
61
|
+
const res = await fetch(`${String(apiUrl).replace(/\/+$/, '')}/v1/relay/leases/${what}`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${secret}` }, body: JSON.stringify({ key: leaseKeyOf(body.repo, body.branch), ...body }) })
|
|
58
62
|
if (!res.ok) throw new Error(`the accounts API answered ${res.status}`)
|
|
59
63
|
return res.json()
|
|
60
64
|
}
|
package/src/relay-mcp.js
CHANGED
|
@@ -37,6 +37,7 @@ import { agentCommitsOf, repoRecordFor, POLICY_WORDS, pullRequestsFor, mergeForM
|
|
|
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
39
|
import { HistoryLog, queryHistory, parseSince, formatHistory, currentTask, lineDiff, legacyTail } from './history.js'
|
|
40
|
+
import { linkedHistory } from './quilt-history.js'
|
|
40
41
|
import { changeRefusal, TALK_REFUSED } from './session-access.js'
|
|
41
42
|
import { MIGRATE_SESSION_DESCRIPTION, describeQuiltPlan } from './session-quilt.js'
|
|
42
43
|
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'
|
|
@@ -48,7 +49,11 @@ import { isDiff, hasEntry, listPaths, readPath, readPaths, textOf, notePresent,
|
|
|
48
49
|
import { hostedRooms, hostedIn, newHosted } from './hosted-agents.js'
|
|
49
50
|
import { turnSyncStep, ROOM_KEY } from './relay-turn-sync.js'
|
|
50
51
|
import { describeSecretNames, SECRET_NAMES_DESCRIPTION } from './secret-names.js'
|
|
51
|
-
import { quiltOf, seamRefusal, seamNote } from './session-seam.js'
|
|
52
|
+
import { quiltOf, seamRefusal, seamNote, seamStatus, seamFileRefusal, seamToolSpec, runSessionSeamTool, seamOnly, seamWorkflow, SEAM_SESSION_TOOLS, SEAM_GUIDE_HOSTED, GIT_TOOLS } from './session-seam.js'
|
|
53
|
+
import { seamAccess } from './seam-access.js'
|
|
54
|
+
import { releaseInSeam } from './seam-claims.js'
|
|
55
|
+
import { releaseWords } from './release-words.js'
|
|
56
|
+
import { LAYOUT_QUILT } from './session-layout3.js'
|
|
52
57
|
import { TRANSCRIPT_DESCRIPTION, renderLines, readLines, inVoice, transcribeSetting, voiceStatusLines } from './voice.js'
|
|
53
58
|
|
|
54
59
|
const FEED_CAP = 300
|
|
@@ -154,10 +159,17 @@ function takeStock (doc, me) {
|
|
|
154
159
|
const linkInboxes = new WeakMap()
|
|
155
160
|
const text = (t) => ({ content: [{ type: 'text', text: t }] })
|
|
156
161
|
const fail = (t) => ({ content: [{ type: 'text', text: t }], isError: true })
|
|
162
|
+
// A session that is a quilt, by its quilt's name (its slug; its id before the relay knows one), or null.
|
|
163
|
+
const quiltName = (room) => { const q = quiltOf(room.meta); return q ? ((room.meta.quilt && room.meta.quilt.slug) || q) : null }
|
|
157
164
|
// quilt_status in a session that is a quilt starts with how work goes there (session-seam.js).
|
|
165
|
+
// Any other answer, the first in a session that is a quilt for this agent: its instructions may be from before the move.
|
|
166
|
+
const withSeamLead = (r, quilt) => {
|
|
167
|
+
if (r && typeof r.then === 'function') return r.then((x) => withSeamLead(x, quilt))
|
|
168
|
+
return r && Array.isArray(r.content) ? { ...r, content: [{ type: 'text', text: `🧵 ${seamNote(quilt, { hosted: true })} This is how work lands here, whatever your instructions said before.` }, ...r.content] } : r
|
|
169
|
+
}
|
|
158
170
|
const withSeamNote = (r, quilt) => {
|
|
159
171
|
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
|
|
172
|
+
return r && Array.isArray(r.content) ? { ...r, content: [{ type: 'text', text: seamNote(quilt, { hosted: true }) }, ...r.content.map((c) => c && c.type === 'text' ? { ...c, text: seamStatus(c.text) } : c)] } : r
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
function ago (ts) {
|
|
@@ -215,7 +227,8 @@ function sessionTools (server, ctx) {
|
|
|
215
227
|
// The branch document the agent works on (its store entry), or null for a room without branch documents.
|
|
216
228
|
// The session's default branch may always start; any other new branch only for someone who may change files.
|
|
217
229
|
const branchEntry = (room) => {
|
|
218
|
-
|
|
230
|
+
// At layout 3 there are no branch documents to work on: every tool but the file tools works on the room.
|
|
231
|
+
if (!room.branchDoc || room.meta.layout === LAYOUT_QUILT) return null
|
|
219
232
|
const key = branchOf(room)
|
|
220
233
|
const editor = room.resolveKey(key) === room.defaultKey || ctx.access(room)?.role !== 'viewer'
|
|
221
234
|
return room.branchDoc(key, { by: me, editor })
|
|
@@ -224,13 +237,16 @@ function sessionTools (server, ctx) {
|
|
|
224
237
|
// Hosted agents are in several sessions at once: `session` says which this call is about.
|
|
225
238
|
const { session, ...args } = given || {}
|
|
226
239
|
return stale(await ctx.withSession((room) => {
|
|
227
|
-
// A hosted agent with no branch of its own is pinned to the active branch by its first call
|
|
228
|
-
// on files, so a read and the write after it never land on two different branches.
|
|
229
|
-
if (ctx.pin && FILE_TOOLS.has(name)) ctx.pin(room)
|
|
230
240
|
// A session that is a quilt has no GitHub path for agents: each GitHub tool names its Seam tool.
|
|
231
|
-
|
|
232
|
-
const
|
|
241
|
+
// At layout 3 its files are in squares, not branches: the file tools name Seam's (quilt_history reads Seam's log, quilt_project_rules main's rules).
|
|
242
|
+
const quilt = quiltName(room)
|
|
243
|
+
// quilt_release lets go of the agent's claims in Seam then (seam-claims.js), when it reaches Seam.
|
|
244
|
+
const seamRelease = name === 'quilt_release' && !!(ctx.seam && ctx.seam())
|
|
245
|
+
const refused = seamRefusal(name, quilt, { hosted: true }) || (quilt && room.meta.layout === LAYOUT_QUILT && FILE_TOOLS.has(name) && name !== 'quilt_history' && name !== 'quilt_project_rules' && !seamRelease ? seamFileRefusal(quilt) : null)
|
|
233
246
|
if (refused) return fail(refused)
|
|
247
|
+
// A hosted agent with no branch of its own is pinned to the active branch by its first call
|
|
248
|
+
// on files, so a read and the write after it never land on two different branches.
|
|
249
|
+
if (ctx.pin && FILE_TOOLS.has(name) && room.meta.layout !== LAYOUT_QUILT) ctx.pin(room)
|
|
234
250
|
// The audit trail: which tool, on what (a path, a pattern or a task id; never contents).
|
|
235
251
|
const caller = ctx.who(room)
|
|
236
252
|
if (caller && caller.id && room.audit) {
|
|
@@ -247,7 +263,9 @@ function sessionTools (server, ctx) {
|
|
|
247
263
|
room.auditAs = caller && caller.id
|
|
248
264
|
try {
|
|
249
265
|
const r = queueNote(room, fn(args || {}, parts))
|
|
250
|
-
|
|
266
|
+
// The first answer in a session that is a quilt says how work lands there (quilt_status always does).
|
|
267
|
+
const once = quilt && ctx.seamOnce ? ctx.seamOnce(room) : false
|
|
268
|
+
return name === 'quilt_status' && quilt ? withSeamNote(r, quilt) : once ? withSeamLead(r, quilt) : r
|
|
251
269
|
} finally { room.auditAs = prev }
|
|
252
270
|
}, session))
|
|
253
271
|
})
|
|
@@ -356,11 +374,18 @@ function sessionTools (server, ctx) {
|
|
|
356
374
|
with_diff: z.boolean().optional().describe('Include each change\'s diff (capped per change)'),
|
|
357
375
|
limit: z.number().int().min(1).max(200).optional().describe('How many of the newest matching changes (default 30)')
|
|
358
376
|
}
|
|
359
|
-
}, ({ path: p, by, since, task, with_diff, limit }, { room }) => {
|
|
377
|
+
}, async ({ path: p, by, since, task, with_diff, limit }, { room }) => {
|
|
360
378
|
const from = parseSince(since)
|
|
361
379
|
if (from === undefined) return fail('since: use a duration like 2h or 3d, "today", "yesterday", or a date.')
|
|
362
380
|
const q = process.env.QUILT_SOURCE_URL && room.meta && room.meta.quilt_id ? room.meta.quilt : null
|
|
363
|
-
const
|
|
381
|
+
const own = legacyTail(historyOf(room).entries(), { at: q && q.at, history: !!(q && q.history) })
|
|
382
|
+
// Linked to a quilt (16: the session became one, or shadows one in step 1): Seam's log plus this record.
|
|
383
|
+
const link = room.quiltHistoryLink ? room.quiltHistoryLink() : null
|
|
384
|
+
if (link) {
|
|
385
|
+
const r = await linkedHistory({ seam: link.seam, quilt: link.quilt, entries: own, query: { path: p ? cleanPath(p) : '', by, since: from ?? undefined, task, limit: limit || 30, withDiff: !!with_diff } })
|
|
386
|
+
return text(`${formatHistory(r.list, { withDiff: !!with_diff })}${r.note ? `\n\n${r.note}` : ''}`)
|
|
387
|
+
}
|
|
388
|
+
const list = queryHistory(own, { path: p ? cleanPath(p) : '', by, since: from ?? undefined, task, limit: limit || 30 })
|
|
364
389
|
return text(formatHistory(list, { withDiff: !!with_diff }))
|
|
365
390
|
})
|
|
366
391
|
|
|
@@ -428,7 +453,7 @@ function sessionTools (server, ctx) {
|
|
|
428
453
|
return text(`${branchesMarkdown(br, { limit: 40 })}\n\n${BRING_IN_NOTE}`)
|
|
429
454
|
})
|
|
430
455
|
|
|
431
|
-
// Seam (plans/quilt-source-control/16), behind QUILT_SOURCE_URL: what making this session a
|
|
456
|
+
// Seam (Seam's plans/quilt-source-control/16), behind QUILT_SOURCE_URL: what making this session a
|
|
432
457
|
// quilt would do. Only the dry run: the move itself is the owner's, from the app.
|
|
433
458
|
if (process.env.QUILT_SOURCE_URL) {
|
|
434
459
|
tool('quilt_migrate_session', {
|
|
@@ -490,6 +515,8 @@ function sessionTools (server, ctx) {
|
|
|
490
515
|
: pickChecklist(files.get('AGENTS.md')?.toString(), files.get('CLAUDE.md')?.toString())
|
|
491
516
|
/** The project's rules (project-rules.js) on the agent's branch, a diff branch's from git where the session left them. */
|
|
492
517
|
const rulesFor = async (room, branch, files) => {
|
|
518
|
+
// In squares (layout 3): main's rules on Seam, read with the room's read token (the branch documents are as they were at the move).
|
|
519
|
+
if (room.meta.layout === LAYOUT_QUILT && quiltOf(room.meta)) return rulesOnSeam(room)
|
|
493
520
|
if (!branch || !isDiff(branch)) return projectRules({ list: [...files.keys()], read: (p) => files.get(p)?.toString() ?? null })
|
|
494
521
|
const list = [...(await listPaths(room, branch.key, branch)).keys()]
|
|
495
522
|
const got = new Map()
|
|
@@ -502,6 +529,20 @@ function sessionTools (server, ctx) {
|
|
|
502
529
|
}
|
|
503
530
|
return projectRules({ list, read: (p) => got.get(p) ?? null })
|
|
504
531
|
}
|
|
532
|
+
const rulesOnSeam = async (room) => {
|
|
533
|
+
const seam = room.quiltReadClient ? room.quiltReadClient() : null
|
|
534
|
+
if (!seam) throw new Error(`The session has no read token for ${quiltName(room)}, so its rules can't be read here: its owner links it to Seam again. Meanwhile read AGENTS.md with seam_cat.`)
|
|
535
|
+
const q = room.meta.quilt_id
|
|
536
|
+
const list = ((await seam.tree(q, 'main')).entries || []).filter((e) => e && e.path && e.mode !== 'dir').map((e) => e.path)
|
|
537
|
+
const got = new Map()
|
|
538
|
+
for (let round = 0; round < 4; round++) {
|
|
539
|
+
const asked = new Set()
|
|
540
|
+
const r = projectRules({ list, read: (p) => { if (got.has(p)) return got.get(p); asked.add(p); return null } })
|
|
541
|
+
if (!asked.size) return r
|
|
542
|
+
for (const p of asked) got.set(p, await seam.cat(q, 'main', p).then((x) => Buffer.from(x.content || '', 'base64').toString('utf8'), () => null))
|
|
543
|
+
}
|
|
544
|
+
return projectRules({ list, read: (p) => got.get(p) ?? null })
|
|
545
|
+
}
|
|
505
546
|
tool('quilt_move_task', {
|
|
506
547
|
description: 'Move a task on the shared board. "doing" when you start it: you get a briefing (its files, recent changes to them, claims, the project\'s checks). ' +
|
|
507
548
|
'"qa" when you finish implementing and testing: requires `qaNotes` (what changed and how you self-validated); without it the move is refused. ' +
|
|
@@ -541,6 +582,7 @@ function sessionTools (server, ctx) {
|
|
|
541
582
|
const all = historyOf(room).entries()
|
|
542
583
|
const history = (tf.length ? all.filter((e) => tf.includes(e.path)) : all).slice(-8)
|
|
543
584
|
const claims = claimsOf(room).filter((c) => !tf.length || tf.some((f) => globMatcher(c.pattern)(f)))
|
|
585
|
+
if (quiltOf(room.meta)) return text(pickupBrief({ task: { ...task, comments: readComments(doc.getMap('taskComments'), task.id) }, history, claims, checklist: await checklistOf(files, room, null), me, workflow: seamWorkflow(TASK_WORKFLOW, { hosted: true }) }))
|
|
544
586
|
const onto = branchForHostedTask(room, task)
|
|
545
587
|
return text(pickupBrief({ task: { ...task, comments: readComments(doc.getMap('taskComments'), task.id) }, history, claims, checklist: await checklistOf(files, room, branchEntry(room)), me }) + (onto ? `\nYou are on ${onto} now (Quilt made it for this task, a copy of ${room.defaultKey}'s files): write your files here and quilt_git_commit them; the pull request opens by itself. Move the task to QA when it is done and tested.` : ''))
|
|
546
588
|
}
|
|
@@ -1166,10 +1208,24 @@ function sessionTools (server, ctx) {
|
|
|
1166
1208
|
return text(`Claimed ${pattern.trim()}. Release it with quilt_release when done.`)
|
|
1167
1209
|
})
|
|
1168
1210
|
|
|
1211
|
+
const releaseHostedInSeam = async (room, pattern) => {
|
|
1212
|
+
const quilt = quiltName(room) && room.meta.quilt_id
|
|
1213
|
+
const access = ctx.seam && ctx.seam()
|
|
1214
|
+
if (!quilt || !access) return fail(seamFileRefusal(quiltName(room)))
|
|
1215
|
+
try {
|
|
1216
|
+
const asked = room.claimRequest(who(room), { op: 'quilt-claims' })
|
|
1217
|
+
const { gone, ...r } = await releaseInSeam({ access, api: process.env.QUILT_SOURCE_URL, quilt, asked, pattern: pattern ? pattern.trim() : '*' })
|
|
1218
|
+
if (gone.length) room.claimRequest(who(room), { op: 'quilt-released', ids: gone })
|
|
1219
|
+
return text(releaseWords(r, { cli: 'quilt_release' }))
|
|
1220
|
+
} catch (err) { return fail(err.message) }
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1169
1223
|
tool('quilt_release', {
|
|
1170
1224
|
description: 'Release a claim (or all of your claims when no pattern is given).',
|
|
1171
1225
|
inputSchema: { pattern: z.string().optional() }
|
|
1172
1226
|
}, ({ pattern }, { room }) => {
|
|
1227
|
+
// At layout 3 the claims are Seam's: the agent lets go of its own there, as itself.
|
|
1228
|
+
if (room.meta.layout === LAYOUT_QUILT) return releaseHostedInSeam(room, pattern)
|
|
1173
1229
|
// Releasing everything keeps boxes: a box opens when it's committed (quilt_git_commit) or unpacked (quilt_unpack).
|
|
1174
1230
|
const mine = claimsOf(room).filter((c) => c.by === me && (pattern ? c.pattern === pattern.trim() : !c.box))
|
|
1175
1231
|
// A file someone waits for is handed off, not released.
|
|
@@ -1430,6 +1486,13 @@ export function hostedWorkspaceAccess ({ apiUrl, pass, fetch: fetchImpl = global
|
|
|
1430
1486
|
return { call, fetchBytes: bytesFetcher(fetchImpl, { timeoutMs: WORKSPACE_TRANSFER_TIMEOUT_MS }), put, saveDir: null, readLocal: null }
|
|
1431
1487
|
}
|
|
1432
1488
|
|
|
1489
|
+
/** A request's JSON body, read here (the transport takes it parsed). */
|
|
1490
|
+
async function readJsonBody (req) {
|
|
1491
|
+
const chunks = []
|
|
1492
|
+
for await (const c of req) chunks.push(c)
|
|
1493
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1433
1496
|
async function serve (mcp, req, res, body) {
|
|
1434
1497
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true })
|
|
1435
1498
|
res.on('close', () => { transport.close().catch(() => {}); mcp.close().catch(() => {}) })
|
|
@@ -1446,11 +1509,30 @@ async function serve (mcp, req, res, body) {
|
|
|
1446
1509
|
*/
|
|
1447
1510
|
export async function handleHostedMcp ({ req, res, pass, relay, workspaces = null, secrets = null, body = undefined }) {
|
|
1448
1511
|
const image = String(req.headers['x-quilt-image'] || '').trim()
|
|
1449
|
-
// The library's guide is part of the instructions only when its tools are offered.
|
|
1450
|
-
const mcp = new McpServer({ name: 'quilt', version: '0.2.0' }, { instructions: (workspaces ? `${HOSTED_INSTRUCTIONS}\n\n${WORKSPACE_GUIDE_HOSTED}` : HOSTED_INSTRUCTIONS) })
|
|
1451
1512
|
const me = pass.name
|
|
1452
1513
|
const account = `${pass.kind}:${pass.sub}`
|
|
1453
1514
|
const touched = new Set()
|
|
1515
|
+
// Whether the agent's sessions on this machine are quilts (some, all): an agent whose sessions all
|
|
1516
|
+
// are reads only Seam's way, with no git in its instructions or tool list (session-seam.js).
|
|
1517
|
+
const quilts = (() => {
|
|
1518
|
+
let any = false; let all = true; let seen = 0
|
|
1519
|
+
for (const name of hostedRooms(relay.hosted.get(account))) {
|
|
1520
|
+
if (relay.roomEnded(name)) continue
|
|
1521
|
+
const room = relay.getRoom(name)
|
|
1522
|
+
if (!room || !room.exists) continue
|
|
1523
|
+
touched.add(room)
|
|
1524
|
+
seen++
|
|
1525
|
+
if (quiltOf(room.meta)) any = true; else all = false
|
|
1526
|
+
}
|
|
1527
|
+
return { any, all: any && all && seen > 0 }
|
|
1528
|
+
})()
|
|
1529
|
+
// The library's guide is part of the instructions only when its tools are offered.
|
|
1530
|
+
const base = quilts.all ? `${seamOnly(HOSTED_INSTRUCTIONS)} In a session that is a quilt: ${SEAM_GUIDE_HOSTED}` : HOSTED_INSTRUCTIONS
|
|
1531
|
+
const mcp = new McpServer({ name: 'quilt', version: '0.2.0' }, { instructions: (workspaces ? `${base}\n\n${WORKSPACE_GUIDE_HOSTED}` : base) })
|
|
1532
|
+
// Every tool it registers, so the git-era ones can be left out for an agent whose sessions are all quilts.
|
|
1533
|
+
const registered = new Map()
|
|
1534
|
+
const register = mcp.registerTool.bind(mcp)
|
|
1535
|
+
mcp.registerTool = (name, spec, handler) => { const t = register(name, spec, handler); registered.set(name, t); return t }
|
|
1454
1536
|
const done = () => { for (const room of touched) if (room && !room.conns.size && room.onEmpty) room.onEmpty() }
|
|
1455
1537
|
res.on('close', done)
|
|
1456
1538
|
// Which sessions the agent is in, for the accounts API (x-quilt-rooms), and which one this
|
|
@@ -1548,6 +1630,8 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
|
|
|
1548
1630
|
multi: true, // every tool takes `session`
|
|
1549
1631
|
who: () => ({ name: me, id: account }),
|
|
1550
1632
|
kind: pass.kind === 'agent' ? 'agent' : 'human',
|
|
1633
|
+
// This agent to Seam (its pass, traded for an agent token), for what it does there as itself.
|
|
1634
|
+
seam: () => process.env.QUILT_SOURCE_URL && pass.kind === 'agent' && req.headers['x-quilt-pass'] ? hostedSeam(account, String(req.headers['x-quilt-pass'])) : null,
|
|
1551
1635
|
access: (room) => room.hostedAccess(pass),
|
|
1552
1636
|
branch: (room) => room.hostedBranch(account),
|
|
1553
1637
|
setBranch: (room, key, opts) => room.setHostedBranch(account, key, opts),
|
|
@@ -1588,6 +1672,14 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
|
|
|
1588
1672
|
} : null,
|
|
1589
1673
|
updates: relay.updates || null,
|
|
1590
1674
|
image: () => image,
|
|
1675
|
+
// Whether this agent is yet to be told the Seam way in `room` (once per quilt, kept with its record there).
|
|
1676
|
+
seamOnce: (room) => {
|
|
1677
|
+
const r = hostedIn(relay.hosted.get(account), room.name)
|
|
1678
|
+
if (!r || !room.meta.quilt_id || r.seamTold === room.meta.quilt_id) return false
|
|
1679
|
+
r.seamTold = room.meta.quilt_id
|
|
1680
|
+
relay.saveHosted()
|
|
1681
|
+
return true
|
|
1682
|
+
},
|
|
1591
1683
|
withSession: (fn, session) => {
|
|
1592
1684
|
const c = current(session)
|
|
1593
1685
|
return c.error ? fail(c.error) : fn(c.room)
|
|
@@ -1688,7 +1780,7 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
|
|
|
1688
1780
|
}, async ({ branch, create, session }) => {
|
|
1689
1781
|
const c = current(session)
|
|
1690
1782
|
if (c.error) return fail(c.error)
|
|
1691
|
-
{ const no = seamRefusal('quilt_git_branch',
|
|
1783
|
+
{ const no = seamRefusal('quilt_git_branch', quiltName(c.room), { hosted: true }); if (no) return fail(no) }
|
|
1692
1784
|
const room = c.room
|
|
1693
1785
|
if (!branch) {
|
|
1694
1786
|
const members = []
|
|
@@ -1736,7 +1828,7 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
|
|
|
1736
1828
|
}, async ({ session }) => {
|
|
1737
1829
|
const c = current(session)
|
|
1738
1830
|
if (c.error) return fail(c.error)
|
|
1739
|
-
{ const no = seamRefusal('quilt_sync_branch',
|
|
1831
|
+
{ const no = seamRefusal('quilt_sync_branch', quiltName(c.room), { hosted: true }); if (no) return fail(no) }
|
|
1740
1832
|
const room = c.room
|
|
1741
1833
|
if (room.meta.layout !== 2 || !room.syncUpstreamNow) return fail('This session has no branches on the relay.')
|
|
1742
1834
|
try {
|
|
@@ -1752,7 +1844,7 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
|
|
|
1752
1844
|
}, ({ token, session }) => {
|
|
1753
1845
|
const c = current(session)
|
|
1754
1846
|
if (c.error) return fail(c.error)
|
|
1755
|
-
{ const no = seamRefusal('quilt_github_token',
|
|
1847
|
+
{ const no = seamRefusal('quilt_github_token', quiltName(c.room), { hosted: true }); if (no) return fail(no) }
|
|
1756
1848
|
if (!c.access || !c.access.owner) return fail('Only the session owner can set the GitHub token: ask them.')
|
|
1757
1849
|
try {
|
|
1758
1850
|
const r = c.room.setGithubToken(token)
|
|
@@ -1808,11 +1900,53 @@ export async function handleHostedMcp ({ req, res, pass, relay, workspaces = nul
|
|
|
1808
1900
|
}
|
|
1809
1901
|
|
|
1810
1902
|
sessionTools(mcp, ctx)
|
|
1903
|
+
// Seam's tools for an agent in a session that is a quilt (session-seam.js), as itself: its
|
|
1904
|
+
// pass, traded once for an agent token of its own (seam-access.js). Only while one of its
|
|
1905
|
+
// sessions here is a quilt, so every other agent's tool list stays as it was.
|
|
1906
|
+
const rawPass = String(req.headers['x-quilt-pass'] || '')
|
|
1907
|
+
// Hidden from the list, but a call still answers with the Seam way: an agent's client keeps the
|
|
1908
|
+
// list it read before its session moved into Seam, and a hidden tool would answer only "disabled".
|
|
1909
|
+
if (quilts.all) {
|
|
1910
|
+
if (body === undefined && req.method === 'POST') {
|
|
1911
|
+
try { body = await readJsonBody(req) } catch { res.writeHead(400, { 'content-type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null })); return }
|
|
1912
|
+
}
|
|
1913
|
+
const calls = [].concat(body || []).some((m) => m && m.method === 'tools/call')
|
|
1914
|
+
if (!calls) for (const [name, t] of registered) if (GIT_TOOLS.has(name)) t.disable()
|
|
1915
|
+
}
|
|
1916
|
+
if (process.env.QUILT_SOURCE_URL && pass.kind === 'agent' && rawPass && quilts.any) {
|
|
1917
|
+
const seam = hostedSeam(account, rawPass)
|
|
1918
|
+
for (const name of SEAM_SESSION_TOOLS) {
|
|
1919
|
+
const spec = seamToolSpec(name, { hosted: true })
|
|
1920
|
+
mcp.registerTool(name, { ...spec, inputSchema: spec.inputSchema.extend({ session: SESSION_ARG }) }, async ({ session, ...args } = {}) => {
|
|
1921
|
+
const c = current(session)
|
|
1922
|
+
if (c.error) return fail(c.error)
|
|
1923
|
+
const quilt = quiltOf(c.room.meta)
|
|
1924
|
+
if (!quilt) return fail('This session is not a quilt: its source control is git, as quilt_status says.')
|
|
1925
|
+
let client
|
|
1926
|
+
try { client = (await seam.client(quilt)).client } catch (e) { return fail(e.message) }
|
|
1927
|
+
return runSessionSeamTool(name, args, { client, quilt, hosted: true })
|
|
1928
|
+
})
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1811
1931
|
// The library works whether or not the agent is in a session (it never touches relay.hosted).
|
|
1812
1932
|
if (workspaces) registerWorkspaceTools(mcp, { ...hostedWorkspaceAccess(workspaces), webhook: false })
|
|
1813
1933
|
await serve(mcp, req, res, body)
|
|
1814
1934
|
}
|
|
1815
1935
|
|
|
1936
|
+
// Each hosted agent's way into Seam, kept between its requests (each is a new MCP server): the
|
|
1937
|
+
// agent token it traded its pass for lasts hours, while passes last minutes.
|
|
1938
|
+
const hostedSeams = new Map() // account -> { access, pass }
|
|
1939
|
+
function hostedSeam (account, pass) {
|
|
1940
|
+
let e = hostedSeams.get(account)
|
|
1941
|
+
if (!e) {
|
|
1942
|
+
e = { pass }
|
|
1943
|
+
e.access = seamAccess({ api: () => process.env.QUILT_SOURCE_URL, credential: async () => `QuiltPass ${e.pass}` })
|
|
1944
|
+
hostedSeams.set(account, e)
|
|
1945
|
+
}
|
|
1946
|
+
e.pass = pass
|
|
1947
|
+
return e.access
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1816
1950
|
/**
|
|
1817
1951
|
* Handles one MCP request (stateless: a fresh server per request) for the older
|
|
1818
1952
|
* link-based flow: a browser's token tied to a room by /agent/link.
|
package/src/relay-outbound.js
CHANGED
|
@@ -50,7 +50,7 @@ const plural = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`
|
|
|
50
50
|
|
|
51
51
|
/** Whether branch `key` of the room syncs live to GitHub: the mirror is on, the owner hasn't switched it off, and the relay can write to the repository. */
|
|
52
52
|
export function liveSyncOn (room, key) {
|
|
53
|
-
if (!room.cfg || !room.cfg.gitSync || !room.mirrors || room.meta.githubSync === false) return false
|
|
53
|
+
if (!room.cfg || !room.cfg.gitSync || !room.mirrors || room.meta.githubSync === false || room.meta.syncStopped) return false
|
|
54
54
|
const rec = upstreamsOf(room)[key]
|
|
55
55
|
if (!rec || !rec.repo || !rec.ref) return false
|
|
56
56
|
// Agents' policy "off" (addendum 16.7): an agent's own branch stays in the session; a person adopts its work (adoptBranch).
|
package/src/release-words.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const list = (xs) => xs.length === 1 ? xs[0] : `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`
|
|
6
6
|
|
|
7
|
-
/** r: { released, held?, branches?, coveredBy?, holder?, yours? }; `cli`: the command to name (quilt release, or quilt_release). */
|
|
7
|
+
/** r: { released, held?, seam? (claims are Seam's: layout 3), branches?, coveredBy?, holder?, yours? }; `cli`: the command to name (quilt release, or quilt_release). */
|
|
8
8
|
export function releaseWords (r, { cli = 'quilt release' } = {}) {
|
|
9
9
|
const n = (r && r.released) || 0
|
|
10
10
|
let s = `Released ${n} claim(s)${r && r.branches && r.branches.length ? ` (on ${list([...new Set(r.branches)])})` : ''}.`
|
|
@@ -13,6 +13,6 @@ export function releaseWords (r, { cli = 'quilt release' } = {}) {
|
|
|
13
13
|
? ` That path is not a claim of its own: your claim on ${r.coveredBy} covers it. Let go of that with ${cli} ${r.coveredBy}.`
|
|
14
14
|
: ` That path is not a claim of its own: ${r.holder || 'someone'}'s claim on ${r.coveredBy} covers it; only they (or the session owner) can let go of it.`
|
|
15
15
|
}
|
|
16
|
-
if (r && r.held && r.held.length) s += ` Kept ${r.held.join(', ')}: someone is waiting for ${r.held.length === 1 ? 'it' : 'them'}; hand off with quilt_handoff.`
|
|
16
|
+
if (r && r.held && r.held.length) s += ` Kept ${r.held.join(', ')}: someone is waiting for ${r.held.length === 1 ? 'it' : 'them'}; ${r.seam ? `hand it on in Seam with a note (seam claim handoff ${r.held[0]} --note "…")` : 'hand off with quilt_handoff'}.`
|
|
17
17
|
return s
|
|
18
18
|
}
|
package/src/runner.js
CHANGED
|
@@ -22,6 +22,8 @@ import { keptInProject } from './agent-join.js'
|
|
|
22
22
|
|
|
23
23
|
export { JOIN_HOST }
|
|
24
24
|
|
|
25
|
+
const STATUS_FILE_MS = 1500 // .quilt/STATUS.md is written at most this often
|
|
26
|
+
|
|
25
27
|
/**
|
|
26
28
|
* An invite link: https://join.heyquilt.com/<room>#<secret> for sessions on Quilt's relay,
|
|
27
29
|
* or https://<relay>/join/<room>#<secret> for any other relay (development relays).
|
|
@@ -101,7 +103,7 @@ export async function describeOtherSync (dir) {
|
|
|
101
103
|
* optionally overrides the relay address given out in invites (e.g. a public
|
|
102
104
|
* tunnel URL when the relay runs on this machine).
|
|
103
105
|
*/
|
|
104
|
-
export async function runSession ({ dir, conn, name, tool,
|
|
106
|
+
export async function runSession ({ dir, conn, name, tool, color = null, shareByDefault = true, summarizeByDefault = false, joined = false, prefer = 'remote', inviteServer, onLog, onFatal, onDebug, kind = 'human', agentFeed = true, readerOptions = {}, passes = null, identity = null, startName = '', startAgentCommits = '', newSession = false, workspace = '', apiAuth = null }) {
|
|
105
107
|
dir = path.resolve(dir)
|
|
106
108
|
if (!/^wss?:\/\//.test(conn.server)) throw new Error('The relay address must start with ws:// or wss://')
|
|
107
109
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
@@ -139,7 +141,7 @@ export async function runSession ({ dir, conn, name, tool, toolGuessed = false,
|
|
|
139
141
|
|
|
140
142
|
// An agent whose keys are kept in its project (quilt agent join --here) is in a sandbox that is cleaned up after every run: it works in turns.
|
|
141
143
|
const turns = kind === 'agent' && keptInProject(name)
|
|
142
|
-
const session = new Session({ dir, ...conn, name, tool,
|
|
144
|
+
const session = new Session({ dir, ...conn, name, tool, color, prefer, kind, shareAgent, identity, passes, startName, startAgentCommits, newSession, workspace: savedWorkspace, turns, apiAuth })
|
|
143
145
|
const summarizer = () => createSummarizer({ onWarn: (msg) => session.log(`✂️ ${msg}`) })
|
|
144
146
|
if (summarize) session.summarizer = summarizer()
|
|
145
147
|
session.taskTitler = createTaskTitler({ onWarn: (msg) => session.log(`📋 ${msg}`) })
|
|
@@ -147,12 +149,24 @@ export async function runSession ({ dir, conn, name, tool, toolGuessed = false,
|
|
|
147
149
|
if (onDebug) session.on('debug', onDebug)
|
|
148
150
|
session.on('fatal', (err) => onFatal && onFatal(err))
|
|
149
151
|
const statusFile = path.join(dir, '.quilt', 'STATUS.md')
|
|
152
|
+
// Written at most every STATUS_FILE_MS: each write works out the whole status again.
|
|
153
|
+
let statusWrite = null
|
|
150
154
|
session.on('status-changed', () => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
if (statusWrite) return
|
|
156
|
+
statusWrite = setTimeout(() => {
|
|
157
|
+
statusWrite = null
|
|
158
|
+
if (session.stopped) return
|
|
159
|
+
// Read config fresh: the workspace can change after the session started (the app's
|
|
160
|
+
// "move to workspace", or a later quilt_start_session call writing it here).
|
|
161
|
+
try { fs.writeFileSync(statusFile, renderStatus({ ...session.status(), workspace: readConfig(dir)?.workspace })) } catch {}
|
|
162
|
+
}, STATUS_FILE_MS)
|
|
163
|
+
statusWrite.unref?.()
|
|
154
164
|
})
|
|
155
165
|
|
|
166
|
+
// Work for this person's AI can run on its own here, when they turned that on (autowork.js). It
|
|
167
|
+
// listens before the session starts: what was written to their cloud AI while it was away arrives
|
|
168
|
+
// as the session syncs, before start() returns, and is its work.
|
|
169
|
+
autowork().attach(session)
|
|
156
170
|
try {
|
|
157
171
|
await session.start({ waitTimeoutMs: 15000 })
|
|
158
172
|
} catch (err) {
|
|
@@ -164,8 +178,6 @@ export async function runSession ({ dir, conn, name, tool, toolGuessed = false,
|
|
|
164
178
|
session.on('identity', ({ name }) => saveName(name))
|
|
165
179
|
// apiAuth: { api, token } for the accounts API as this session's person or agent (secrets, `quilt run`).
|
|
166
180
|
const control = await startControl(session, { invite, viewInvite, joined, apiAuth })
|
|
167
|
-
// Work for this person's AI can run on its own here, when they turned that on (autowork.js).
|
|
168
|
-
autowork().attach(session)
|
|
169
181
|
remember({ dir, room: conn.room, server: conn.server, name: session.name, tool, kind, workspace: savedWorkspace })
|
|
170
182
|
|
|
171
183
|
// Every AI tool on this computer can reach Quilt (its MCP server, by absolute path), with nothing to set up.
|
|
@@ -198,6 +210,7 @@ export async function runSession ({ dir, conn, name, tool, toolGuessed = false,
|
|
|
198
210
|
if (stopped) return
|
|
199
211
|
stopped = true
|
|
200
212
|
if (readers) readers.stop()
|
|
213
|
+
clearTimeout(statusWrite)
|
|
201
214
|
await control.close()
|
|
202
215
|
await session.stop()
|
|
203
216
|
}
|