heyquilt 0.3.107 → 0.3.109

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/README.md CHANGED
@@ -378,16 +378,14 @@ the way the agent listens for it, and mentions are marked in every message.
378
378
 
379
379
  ### Your AI on its own cloud machine
380
380
 
381
- Quilt runs a person's AI only when they turn "Work on its own" on in Settings → Your AI, and
382
- then only their own AI tool with their own account: on their own computer, or on their own
383
- cloud VM they connected. **Connect Claude**, **Connect Codex** or **Connect Cursor** there
384
- starts a small machine for that person alone (never the relay) and signs the tool in with its
385
- own login: the app shows the link and code, and Quilt never asks for, keeps or passes on the
386
- tool's token, or pays for its use. From then on it is one of the person's AI sessions,
387
- "Daniel · cloud" under "Daniel's AI": it joins the sessions they join and, while none of
388
- their AI sessions on a computer is live, takes tasks for their AI and answers what is written
389
- to it, with its own model, instructions and daily cap. It sleeps when idle and wakes when it
390
- is needed. Disconnect deletes the machine and its disk.
381
+ Quilt runs a person's AI only on their own cloud machine they connected, with their own AI
382
+ tool and their own account. **Connect Claude**, **Connect Codex** or **Connect Cursor** in
383
+ Settings → Your AI starts a small machine for that person alone (never the relay) and signs the
384
+ tool in with its own login: the app shows the link and code, and Quilt never asks for, keeps or
385
+ passes on the tool's token, or pays for its use. From then on it is "Daniel's AI", the only
386
+ member by that name: it joins the sessions they join, takes tasks for their AI and answers what
387
+ is written to it, with its own model, instructions and daily cap. It sleeps when idle and wakes
388
+ when it is needed. Disconnect deletes the machine and its disk.
391
389
 
392
390
  ### Webhooks: an agent sets up its own push
393
391
 
package/RELEASES.md CHANGED
@@ -11,6 +11,21 @@ Format: `## <version> — <YYYY-MM-DD>`, an optional one-line summary, then bull
11
11
  Lead each bullet with a short bold phrase. Inline `code` and **bold** are rendered;
12
12
  nothing else is.
13
13
 
14
+ ## 0.3.109 — 2026-10-20
15
+
16
+ "Your AI" is your cloud AI, and only that.
17
+
18
+ - **Each AI session is shown as itself.** The member list, chat, file locks and the Loom show each of your AI sessions by its own name ("Daniel · file-queue"), instead of folding them all into "Daniel's AI".
19
+ - **"Daniel's AI" is your cloud AI.** Connected, your cloud AI is named "Daniel's AI": what anyone writes or hands to your AI goes to it, whether or not you have AI sessions open. Without one, nobody is "Daniel's AI".
20
+ - **Nothing is pushed to the AI sessions on your computer.** Messages, mentions and tasks for your AI no longer land in whichever session you used last, and your computer never starts a run on its own: work for your AI waits on the board until you ask your AI for it. The "When work comes to your AI" settings are gone with it.
21
+ - **Your cloud AI talks caveman.** It answers in short, plain sentences (the caveman skill, for Claude Code, Codex and Cursor), so its notes cost fewer tokens. Code, commands, paths and commit messages stay exact.
22
+ - **Workspaces are off for now.** Home lists your sessions again, and workspaces, workspace invites, the Agents page and your AI's workspace library tools (and `quilt workspace`) are gone from the app. Nothing in them is deleted; they come back if they turn out to be missed.
23
+ - **The git panel shows only your work not committed.** It used to count every file anyone in the session had changed (14, say), with nothing about whose they were. Now it lists just yours and your AIs', in two lines: boxed (only in your folder so far) and shared (everyone has them); click either to see its files. The count in the top bar is yours too, and Commit and push commits only your files, leaving everyone else's for them.
24
+
25
+ ## 0.3.108 — 2026-10-19
26
+
27
+ - **A git reset over a held file isn't taken as your change.** Moving your branch to a commit from elsewhere (a release made in another clone, say) with `git reset` leaves your files as they were. For a file someone else held, Quilt then measured your copy from the new commit, so it looked as if you had taken that commit's change out: a merge appeared under your name, and the change was briefly removed when they let go. Your copy is now measured from the commit it came from, so nothing is recorded, and the commit's change comes in once they let go.
28
+
14
29
  ## 0.3.107 — 2026-10-19
15
30
 
16
31
  - **Your cloud AI's sessions stay listed.** Settings → Your AI shows the sessions your cloud AI is in while its machine sleeps and after Quilt's servers restart, instead of "In no session yet". A session nobody named shows as your folder's name, as the rest of the app shows it.
package/bin/quilt.js CHANGED
@@ -558,6 +558,7 @@ async function say () {
558
558
  async function workspaceCmd () {
559
559
  const { WORKSPACE_USAGE, runWorkspaceCommand } = await import('../src/workspace-cli.js')
560
560
  const [sub, ...rest] = argv
561
+ if (!(await import('../src/app-workspaces.js')).appWorkspaces()) fail('Workspaces are turned off in this version of Quilt.')
561
562
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') return console.log(WORKSPACE_USAGE)
562
563
  const { localWorkspaceAccess } = await import('../src/mcp.js')
563
564
  const { findDaemon } = await import('../src/control.js')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyquilt",
3
- "version": "0.3.107",
3
+ "version": "0.3.109",
4
4
  "description": "Real-time, tool-agnostic pair vibe coding: sync a project folder live between collaborators using any AI coding tool.",
5
5
  "author": {
6
6
  "name": "Daniel Carmichael",
package/src/ai-vm.js CHANGED
@@ -74,6 +74,53 @@ export async function warmTool (exe, stop = () => false) {
74
74
  return warmFolder(root, BIG_FILE, stop)
75
75
  }
76
76
 
77
+ // What the image installed into a home folder of its own for every machine (vm/Dockerfile: caveman
78
+ // for Claude Code, Codex and Cursor); each machine's home (its volume) takes it in at start.
79
+ export const IMAGE_HOME = '/opt/cavehome'
80
+
81
+ /** Two settings files as one: `over`'s keys added to `base`, objects merged, lists joined, `base` kept where both say. */
82
+ function mergeJson (base, over) {
83
+ if (Array.isArray(base) && Array.isArray(over)) return [...base, ...over.filter((x) => !base.some((y) => JSON.stringify(y) === JSON.stringify(x)))]
84
+ if (!base || !over || typeof base !== 'object' || typeof over !== 'object' || Array.isArray(base) || Array.isArray(over)) return base
85
+ const out = { ...base }
86
+ for (const [k, v] of Object.entries(over)) out[k] = k in base ? mergeJson(base[k], v) : v
87
+ return out
88
+ }
89
+
90
+ /**
91
+ * Takes in what the image installed for every machine (`from`) into this machine's home (`to`):
92
+ * files it lacks are copied, JSON settings it has are merged (its own kept where both say), and
93
+ * nothing of its own is replaced. Returns how many files changed.
94
+ */
95
+ export function seedHome (from, to) {
96
+ let n = 0
97
+ const walk = (rel) => {
98
+ let entries = []
99
+ try { entries = fs.readdirSync(path.join(from, rel), { withFileTypes: true }) } catch { return }
100
+ for (const e of entries) {
101
+ const r = path.join(rel, e.name)
102
+ const src = path.join(from, r)
103
+ const dst = path.join(to, r)
104
+ if (e.isDirectory()) { walk(r); continue }
105
+ if (!e.isFile()) continue
106
+ try {
107
+ if (!fs.existsSync(dst)) {
108
+ fs.mkdirSync(path.dirname(dst), { recursive: true })
109
+ fs.copyFileSync(src, dst)
110
+ fs.chmodSync(dst, fs.statSync(src).mode & 0o777)
111
+ n++
112
+ } else if (e.name.endsWith('.json')) {
113
+ const have = JSON.parse(fs.readFileSync(dst, 'utf8'))
114
+ const merged = mergeJson(have, JSON.parse(fs.readFileSync(src, 'utf8')))
115
+ if (JSON.stringify(merged) !== JSON.stringify(have)) { fs.writeFileSync(dst, JSON.stringify(merged, null, 2) + '\n'); n++ }
116
+ }
117
+ } catch {}
118
+ }
119
+ }
120
+ walk('')
121
+ return n
122
+ }
123
+
77
124
  /** Reads every file of `min` bytes or more under `root` into memory, until `stop()`; returns how many bytes. */
78
125
  export async function warmFolder (root, min = 0, stop = () => false) {
79
126
  let bytes = 0
@@ -114,10 +161,11 @@ export class AiVm {
114
161
  * reaches outside is injectable for tests: `fetch`, `login` (spawnLogin), `status` (runQuiet),
115
162
  * `join` (runSession), `agentJoin`/`agentAccess` (agent-join.js), `autowork`, `which`.
116
163
  */
117
- constructor ({ api, joinLink = '', spareKey = '', home = process.env.HOME, fetch: fetchImpl = globalThis.fetch, login = spawnLogin, status = runQuiet, warm = warmTool, join = null, agent = null, autowork = null, which = null, now = Date.now, log = console.log, idleMs = IDLE_MS, reportMs = REPORT_MS, port = 8080, waitParam = '1', claimEveryMs = CLAIM_EVERY_MS, codeMs = CODE_MS }) {
164
+ constructor ({ api, joinLink = '', spareKey = '', imageHome = null, home = process.env.HOME, fetch: fetchImpl = globalThis.fetch, login = spawnLogin, status = runQuiet, warm = warmTool, join = null, agent = null, autowork = null, which = null, now = Date.now, log = console.log, idleMs = IDLE_MS, reportMs = REPORT_MS, port = 8080, waitParam = '1', claimEveryMs = CLAIM_EVERY_MS, codeMs = CODE_MS }) {
118
165
  this.api = String(api || '').replace(/\/+$/, '')
119
166
  this.joinLink = joinLink
120
167
  this.spareKey = spareKey
168
+ this.imageHome = imageHome
121
169
  this.warm = warm
122
170
  this.claimEveryMs = claimEveryMs
123
171
  this.codeMs = codeMs
@@ -235,6 +283,8 @@ export class AiVm {
235
283
 
236
284
  async start () {
237
285
  fs.mkdirSync(this.dir, { recursive: true })
286
+ // What the image installed for every machine (caveman for each AI tool) comes into this home.
287
+ if (this.imageHome) { const n = seedHome(this.imageHome, this.home); if (n) this.log(`took in ${n} file(s) of the image's tool settings`) }
238
288
  if (this.spareKey && !(await this.hasAgent())) await this.spare()
239
289
  if (this.stopped) return
240
290
  await this.ensureAgent()
@@ -512,7 +562,7 @@ export function onPath (names, { env = process.env } = {}) {
512
562
  /** `quilt ai-vm`, from the machine's environment. */
513
563
  export async function runAiVm (env = process.env) {
514
564
  if (!env.QUILT_API) throw new Error('QUILT_API is not set: this command runs on a cloud AI machine Quilt started.')
515
- const vm = new AiVm({ api: env.QUILT_API, joinLink: env.QUILT_AI_VM_JOIN || '', spareKey: env.QUILT_AI_VM_SPARE || '', home: env.HOME, which: (names) => onPath(names), port: Number(env.PORT || 8080) })
565
+ const vm = new AiVm({ api: env.QUILT_API, joinLink: env.QUILT_AI_VM_JOIN || '', spareKey: env.QUILT_AI_VM_SPARE || '', imageHome: fs.existsSync(IMAGE_HOME) ? IMAGE_HOME : null, home: env.HOME, which: (names) => onPath(names), port: Number(env.PORT || 8080) })
516
566
  const shutdown = async () => { await vm.stop(); process.exit(0) }
517
567
  process.on('SIGINT', shutdown)
518
568
  process.on('SIGTERM', shutdown)
package/src/api/server.js CHANGED
@@ -41,8 +41,7 @@ import { roomAccessFrom } from './access.js'
41
41
  import { joiningRoom, sessionArg } from '../mcp-room.js'
42
42
  import { issueRoutes } from './routes/issues.js'
43
43
  import { aiVmRoutes, cloudAiOf, WAIT_MS as AI_VM_WAIT_MS } from './routes/ai-vm.js'
44
- import { personaName } from '../persona.js'
45
- import { CLOUD_LABEL } from '../cloud-ai.js'
44
+ import { aiName } from '../persona.js'
46
45
  import { routeName, cleanEvent } from './issues.js'
47
46
  import { secretRoutes } from './routes/secrets.js'
48
47
 
@@ -146,8 +145,8 @@ export function startApi ({ port = 0, host = '127.0.0.1', store, verifyUser, sit
146
145
  }
147
146
 
148
147
  /**
149
- * Who a pass for an agent is for. A person's cloud AI (routes/ai-vm.js) is one of its person's
150
- * AI sessions: named like one ("Dan · cloud"), with its person's account (`of`) and name
148
+ * Who a pass for an agent is for. A person's cloud AI (routes/ai-vm.js) is that
149
+ * person's AI: named "<person>'s AI" ("Dan's AI"), with its person's account (`of`) and name
151
150
  * (`ofName`), which the relay lets in where its person stands.
152
151
  */
153
152
  async function agentHolder (agent) {
@@ -156,7 +155,7 @@ export function startApi ({ port = 0, host = '127.0.0.1', store, verifyUser, sit
156
155
  if (!vm) return holder
157
156
  const p = await store.profile(vm.userId)
158
157
  const person = String((p && p.name) || 'Quilt user').slice(0, 64)
159
- return { ...holder, name: personaName(person, CLOUD_LABEL).slice(0, 64), of: `person:${vm.userId}`, ofName: person }
158
+ return { ...holder, name: aiName(person.slice(0, 58)), of: `person:${vm.userId}`, ofName: person }
160
159
  }
161
160
 
162
161
  // Resume signatures seen in the last window, so a copied one can't be used again: here, and in
@@ -0,0 +1,4 @@
1
+ // Workspaces are off on this computer until after the MVP (the cofounders' call, 2026-10-19): the
2
+ // app, its agents' workspace tools and `quilt workspace` act as if the accounts API had them off,
3
+ // whatever the API says. QUILT_APP_WORKSPACES=1 brings them back (tests, and when they return).
4
+ export const appWorkspaces = (env = process.env) => /^(1|true)$/i.test(env.QUILT_APP_WORKSPACES || '')
package/src/autowork.js CHANGED
@@ -62,8 +62,12 @@ export function tokensOn (entries, ts) {
62
62
  return Object.values(d).reduce((n, t) => n + t.input + t.output, 0)
63
63
  }
64
64
 
65
- /** What a run is asked to do: the work, and how to talk while nobody watches. */
66
- export function runPrompt ({ person, job, changeFiles, prompt = '' }) {
65
+ /**
66
+ * What a run is asked to do: the work, and how to talk while nobody watches. `voice`: the
67
+ * machine's caveman mode (CAVEMAN_DEFAULT_MODE on a cloud AI machine, vm/Dockerfile), said
68
+ * here too so every tool talks that way, not only those whose own hooks turn it on.
69
+ */
70
+ export function runPrompt ({ person, job, changeFiles, prompt = '', voice = process.env.CAVEMAN_DEFAULT_MODE || '' }) {
67
71
  const lines = [
68
72
  `You are ${person}'s AI in the Quilt session in this folder, working on your own: nobody is watching this chat, and it isn't a conversation.`,
69
73
  ''
@@ -76,6 +80,7 @@ export function runPrompt ({ person, job, changeFiles, prompt = '' }) {
76
80
  `Answer ${job.by || 'them'} with quilt_message. If they ask for work, do it as ${person}'s AI would, and say in chat what you did.`)
77
81
  }
78
82
  if (prompt) lines.push('', `${person}'s own instructions for you:`, prompt)
83
+ if (/^[a-z]+$/.test(voice) && voice !== 'off') lines.push('', `Talk in ${voice} mode (the caveman skill): terse, like a smart caveman, in chat, comments and notes. Code, commands, paths, commit messages and error text stay exact.`)
79
84
  lines.push('', 'Before your first change, read the project\'s rules with quilt_project_rules.')
80
85
  if (!changeFiles) lines.push(`${person} hasn't let you change files: read, plan, comment on the task (quilt_comment_task) or ask, and leave the changes to a person or another session.`)
81
86
  lines.push(
@@ -291,7 +296,7 @@ export class AutoWork {
291
296
  fail (session, run, why) {
292
297
  const where = session.aiOf ? 'your cloud AI (Settings → Your AI)' : 'this computer'
293
298
  const text = SIGNED_OUT.test(String(why)) ? `${run.tool || 'your AI tool'} isn't signed in on ${where}` : String(why).replace(/[.\s]+$/, '').slice(0, 300)
294
- this.notice(session, `Couldn't run your AI for ${run.title || 'that work'}: ${text}. It's still waiting for your AI in your chat.`, run)
299
+ this.notice(session, `Couldn't run your AI for ${run.title || 'that work'}: ${text}. It's still waiting for your AI.`, run)
295
300
  session.dropAutoPersona(run.via)
296
301
  }
297
302
 
package/src/cloud-ai.js CHANGED
@@ -1,13 +1,10 @@
1
- // A person's AI on its own cloud machine (docs/superpowers/specs/2026-10-17-ai-vm.md) is one of
2
- // that person's AI sessions: its presence says whose (`aiOf`), and it answers for
3
- // "<person>'s AI" only while none of the person's AI sessions on a computer is live. Pure, so
4
- // the relay (which wakes a suspended one) and every app (which shows it and reads its inbox)
5
- // decide the same way.
1
+ // A person's AI on its own cloud machine (docs/superpowers/specs/2026-10-17-ai-vm.md): its
2
+ // presence says whose (`aiOf`), and it is the only "<person>'s AI": that is its name, and what
3
+ // is written to it or handed to the person's AI is its, whether or not they have AI sessions
4
+ // open on a computer (those answer only to their own names). Pure, so the relay (which wakes a
5
+ // suspended one) and every app (which shows it and reads its inbox) decide the same way.
6
6
  import { aiName } from './ui/chat.js'
7
7
 
8
- /** The label a cloud AI's name carries: "Daniel · cloud". */
9
- export const CLOUD_LABEL = 'cloud'
10
-
11
8
  /**
12
9
  * Whether one of `person`'s AI sessions on a computer is live: a presence of theirs (not a
13
10
  * cloud AI's) publishing an AI session that isn't a "work on its own" run.
@@ -31,17 +28,17 @@ export function localAiNames (states, person) {
31
28
  }
32
29
 
33
30
  /**
34
- * How a cloud AI named `name`, of `person`, reads the inbox (inbox.js): its own name always;
35
- * "<person>'s AI" and tasks for it only while no AI session of theirs on a computer is live.
31
+ * How a cloud AI named `name`, of `person`, reads the inbox (inbox.js): its own name and
32
+ * "<person>'s AI" (the same, once it has rejoined under it), and tasks for the person's AI.
33
+ * Its person's AI sessions on computers don't wake it: they work for the same person.
36
34
  */
37
35
  export function cloudReader ({ name, person, states = [], aliases = [] }) {
38
- const local = localAiLive(states, person)
39
36
  return {
40
37
  name,
41
- aliases: local ? [...aliases] : [...aliases, aiName(person)],
38
+ aliases: [...new Set([...aliases, aiName(person)])].filter((n) => n !== name),
42
39
  agent: true,
43
40
  asAi: false,
44
- ...(local ? {} : { of: person }),
41
+ of: person,
45
42
  own: localAiNames(states, person)
46
43
  }
47
44
  }
package/src/hooks.js CHANGED
@@ -157,8 +157,8 @@ async function sessionStart (api, state, say, name) {
157
157
  // Only what arrives from now on is for this Claude; what came before is for quilt_inbox.
158
158
  const { seq } = await api('POST', '/inbox', { after: 0 }).catch(() => ({ seq: 0 }))
159
159
  state.update((s) => { s.after = seq || 0 })
160
- // A person's AI sessions are one name to people: "Daniel's AI (3 sessions)".
161
- const who = st.peers.length ? foldPersonas(st.peers).map((p) => p.sessions ? `${p.name} (${p.sessions.length === 1 ? 'one AI session' : `${p.sessions.length} AI sessions`}${p.mine ? ', you among them' : ''})` : p.name + (p.kind === 'agent' ? ' (AI agent)' : '')).join(', ') : 'nobody else yet'
160
+ // Each AI session is a member by its own name.
161
+ const who = st.peers.length ? foldPersonas(st.peers).map((p) => p.name + (p.aiOf ? ` (${p.mine ? 'one of your person\'s AI sessions' : `an AI session of ${p.aiOf}`})` : p.kind === 'agent' ? ' (AI agent)' : '')).join(', ') : 'nobody else yet'
162
162
  const parts = [
163
163
  `This folder is in a live Quilt session (room ${st.room}) with ${who}. Files can change underneath you at any time; re-read a file right before editing it.`,
164
164
  'Quilt claims each file for you the moment you edit it, and releases those claims when you finish. ' +
@@ -167,7 +167,7 @@ async function sessionStart (api, state, say, name) {
167
167
  'When someone asks for a file you hold, finish your change, then hand it off with quilt_handoff and your context; you cannot finish before you do.',
168
168
  'Messages from collaborators, mentions of you and tasks handed to you are shown to you as you work; answer with quilt_message and take a task with quilt_move_task.',
169
169
  'You are a member of your own in the session, apart from your person and their other AI sessions, named after your work (your git branch, or the first thing you say you are doing); rename yourself with quilt_name_session. ' +
170
- 'People see all of a person\'s AI sessions as one, "<person>\'s AI": write to another person\'s AI by that name, and what is written to your person\'s AI reaches whichever of their sessions was active last (with none of them live, their cloud AI, if they connected one).'
170
+ 'People see each AI session by its own name. "<person>\'s AI" ("Daniel\'s AI") is only that person\'s cloud AI, when they connected one (Quilt runs their own AI tool for them on their own cloud machine): write to it by that name. Nothing written or handed to "<person>\'s AI" comes to you; when your person asks you to work on tasks, take them from the board (quilt_tasks).'
171
171
  ]
172
172
  const duties = await api('GET', '/duties').catch(() => ({}))
173
173
  if (duties.pickup && duties.pickup !== 'off') parts.push(`This person lets you pick up work from the task board by yourself (${duties.pickup === 'any' ? 'tasks assigned to you, then unassigned ones' : 'tasks assigned to you'}): when you finish, you are handed the next one.`)
package/src/inbox.js CHANGED
@@ -101,7 +101,8 @@ export function scanInbox ({ messages = [], tasks = [], reader, now = Date.now()
101
101
  else if (follows) events.push({ id: m.id, kind: 'mention', by: m.by, text, ts: m.ts, follows })
102
102
  }
103
103
  const mine = []
104
- for (const t of tasks) {
104
+ // `tasks: false`: a person's own inbox on a computer takes no tasks (their AI asks the board).
105
+ for (const t of reader && reader.tasks === false ? [] : tasks) {
105
106
  // An AI session's tasks: given to it by name (as a member), or to its person's AI.
106
107
  const its = assignedToReader(t, reader) || names.slice(1).some((n) => assignedToReader(t, { ...reader, name: n })) ||
107
108
  (reader && reader.of && (t.assignee === me || assignedToReader(t, { name: reader.of, asAi: true })))
package/src/mcp.js CHANGED
@@ -40,6 +40,7 @@ import { getSettings } from './settings.js'
40
40
  import { mergeAction } from './merges.js'
41
41
  import { CLASH_DESCRIPTION, clashSchema } from './relay-clash.js'
42
42
  import { describeSecretNames, SECRET_NAMES_DESCRIPTION, RUN_GUIDE } from './secret-names.js'
43
+ import { appWorkspaces } from './app-workspaces.js'
43
44
 
44
45
  // The board's columns, from tasks.js, so this and the hosted MCP (relay-mcp.js) never drift apart.
45
46
  const COLUMN_IDS_LIST = COLUMNS.map((c) => c.id)
@@ -79,7 +80,7 @@ export const MCP_INSTRUCTIONS =
79
80
  'and move a task with quilt_move_task when you start or finish it; quilt_task reads one in full with its comments, and quilt_comment_task leaves a note or handoff on it. ' +
80
81
  'quilt_history tells you who changed which file, when, with the diff: read it for the files you are about to touch. ' +
81
82
  'If git refuses to pull because untracked files would be overwritten, those files came from the session: quilt_status lists them under Pulling (and whether they match); make way and pull with rm <files> && git pull --autostash, and Quilt keeps them for everyone. ' +
82
- 'If the person you work for lets their AI pick up work by itself (their Quilt settings: on their own computer, or on their own cloud VM they connected), finishing (quilt_set_work done, a task to QA or Done) hands you your next task from the board: start it. ' +
83
+ 'If the person you work for lets their AI pick up work by itself (their cloud AI, on the cloud machine they connected), finishing (quilt_set_work done, a task to QA or Done) hands you your next task from the board: start it. ' +
83
84
  'Before you change files, call quilt_before_edit with their paths: it tells you whether each is yours to edit (claiming free ones for you, so partners are refused instead of overwriting you), ' +
84
85
  'and shows what people said about those files in chat, so you know what was asked or planned before you change them. ' +
85
86
  'Do not edit a file it refuses: ask for it in its file queue with quilt_request_file (a title like "Working on <what> for <task>" and up to 300 characters on your plan) and carry on with other work; you are woken when it is handed to you, with the holder\'s context. ' +
@@ -104,7 +105,7 @@ export const MCP_INSTRUCTIONS =
104
105
  'When you are about to answer someone in chat, quilt_typing shows them you are typing; sending clears it. ' +
105
106
  'Chat: ' + CHAT_RULES.replace(/^Send a chat message\. /, '') + ' A message that needs nothing back is settled with quilt_inbox (no_reply: [its id]), not answered. ' +
106
107
  'To be woken instead of polling, subscribe to the quilt://inbox resource (you are told when something new arrives), or quilt_webhook_subscribe POSTs each one to a URL of yours as it happens. ' +
107
- 'You are a member of your own in the session, apart from your person and their other AI sessions, named "<their first name> · <label>" after your work (your git branch, or what you first say you are doing); rename yourself with quilt_name_session. Your messages, inbox, claims and duties are your own. People see all of a person\'s AI sessions as one, "<person>\'s AI" ("Daniel\'s AI"): write to another person\'s AI by that name, and what is written to your person\'s AI reaches whichever of their sessions was active last (with none of them live, their cloud AI, if they connected one: Quilt runs their own AI tool for them on their own cloud VM). ' +
108
+ 'You are a member of your own in the session, apart from your person and their other AI sessions, named "<their first name> · <label>" after your work (your git branch, or what you first say you are doing); rename yourself with quilt_name_session. Your messages, inbox, claims and duties are your own. People see each AI session by its own name. "<person>\'s AI" ("Daniel\'s AI") is only that person\'s cloud AI, when they connected one (Quilt runs their own AI tool for them on their own cloud machine): write to it by that name. Nothing written or handed to "<person>\'s AI" comes to you; when your person asks you to work on tasks, take them from the board (quilt_tasks). ' +
108
109
  'Share what you are doing with quilt_share: when you start on a request (request and your plan) and when you finish (what you did and the files you changed). Partners see it in their feed, it puts your work on the task board, and it tells the host not to commit under you. ' +
109
110
  'When Claude Code is started with the quilt channel, they arrive on their own as <channel source="quilt"> events while you work: treat each like a request from that person, answer with quilt_message, and take a task with quilt_move_task. ' +
110
111
  RUN_GUIDE + ' ' +
@@ -237,7 +238,7 @@ export async function runMcp () {
237
238
  inputSchema: {
238
239
  title: z.string().describe('What needs doing, in a few words'),
239
240
  assignee: z.string().optional().describe('Who should do it: a person\'s name, or "me". Omit to leave it unassigned.'),
240
- to_ai: z.boolean().optional().describe('Assign it to that person\'s AI instead of the person. You are this person\'s AI unless you joined as your own agent: set assignee to "me" and to_ai to true to take the task yourself.'),
241
+ to_ai: z.boolean().optional().describe('Assign it to that person\'s AI (their cloud AI, when they connected one) instead of the person. To take a task yourself, set assignee to your own name.'),
241
242
  files: z.array(z.string()).max(20).optional().describe('Project files this task is about, relative paths such as src/app.js')
242
243
  }
243
244
  }, ({ title, assignee, to_ai, files }) => withDaemon(async (d) => {
@@ -281,7 +282,7 @@ export async function runMcp () {
281
282
  }, { gate: gates(gateFor('quilt_move_task'), column === 'done' && queueGate('move the task again')) }))
282
283
 
283
284
  server.registerTool('quilt_assign_task', {
284
- description: 'Assign a shared task to a person or to their AI, and optionally set the files it is about. assignee "" clears it. Set assignee to "me" and to_ai to true to take it yourself when you are that person\'s AI.',
285
+ description: 'Assign a shared task to a person, an AI session (by its own name) or a person\'s AI (their cloud AI), and optionally set the files it is about. assignee "" clears it. To take it yourself, set assignee to your own name.',
285
286
  inputSchema: {
286
287
  id: z.string().describe('Task id from quilt_tasks'),
287
288
  assignee: z.string().describe('A person\'s name, "me", or "" to unassign'),
@@ -1201,12 +1202,13 @@ export async function workspacesOn (api, fetchImpl = globalThis.fetch) {
1201
1202
  }
1202
1203
 
1203
1204
  /**
1204
- * Adds the workspace library tools when there is someone to act as (workspaceActor: this
1205
- * folder's agent, or the person signed in on this computer) and their API says workspaces are
1206
- * on. Anything else (nobody, the flag off, the API slow, unreachable or odd) adds nothing and
1205
+ * Adds the workspace library tools when workspaces are on on this computer (app-workspaces.js),
1206
+ * there is someone to act as (workspaceActor: this folder's agent, or the person signed in on
1207
+ * this computer) and their API says workspaces are on. Anything else (nobody, the flag off, the API slow, unreachable or odd) adds nothing and
1207
1208
  * never throws. Returns whether the tools were added.
1208
1209
  */
1209
1210
  export async function addWorkspaceTools (server, { fetch: fetchImpl = globalThis.fetch, projectDirs = () => [process.cwd()] } = {}) {
1211
+ if (!appWorkspaces()) return false // off on this computer (app-workspaces.js)
1210
1212
  const access = localWorkspaceAccess({ fetch: fetchImpl, projectDirs })
1211
1213
  let api
1212
1214
  try { api = access.api() } catch { return false }
package/src/persona.js CHANGED
@@ -97,5 +97,5 @@ export function labelFromFile (rel) {
97
97
  return cleanLabel(base.replace(/\.[A-Za-z0-9]+$/, ''))
98
98
  }
99
99
 
100
- // "Daniel's AI": all of a person's AI sessions as one name (pure, shared with the app).
100
+ // "Daniel's AI": a person's cloud AI's name, and only that (pure, shared with the app).
101
101
  export { AI_SUFFIX, aiName, foldPersonas } from './ui/chat.js'
package/src/relay-mcp.js CHANGED
@@ -34,7 +34,6 @@ import { UpdateCheck } from './update-check.js'
34
34
  import { TASK_WORKFLOW, pickupBrief, doneRefusal, verifiedEnough, verifiedLine, qaRefusal, qaNotesEnough, qaNotesLine, pickChecklist, MAX_VERIFIED } from './agent-task-workflow.js'
35
35
  import { HistoryLog, queryHistory, parseSince, formatHistory, currentTask, lineDiff } from './history.js'
36
36
  import { changeRefusal, TALK_REFUSED } from './session-access.js'
37
- import { aiName } from './persona.js'
38
37
  import { chatAbout, renderChatAbout, waitingOn, needsNoReply, renderUnanswered, heldRefusal, queuedFor, renderQueueNotice, renderQueued, answered, unaddressed, CHAT_RULES, RE_HELP, replyFor, renderReplied } from './duties.js'
39
38
  import { describeSubscription, WEBHOOK_EVENTS } from './webhooks.js'
40
39
  import { registerWorkspaceTools, bytesFetcher, WORKSPACE_GUIDE_HOSTED } from './workspace-tools.js'
@@ -268,7 +267,6 @@ function sessionTools (server, ctx) {
268
267
  // Everyone this agent could address: who is here and who has been in the chat.
269
268
  const memberNames = (room, doc) => {
270
269
  const names = new Set(peers(room).map((p) => p.name))
271
- for (const p of peers(room)) if (p.persona && p.of) names.add(aiName(p.of)) // "Daniel's AI"
272
270
  for (const m of seen(doc)) { if (m.by) names.add(m.by); if (m.to) names.add(m.to) }
273
271
  names.delete(me)
274
272
  return [...names].filter((n) => typeof n === 'string' && n)