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 +8 -10
- package/RELEASES.md +15 -0
- package/bin/quilt.js +1 -0
- package/package.json +1 -1
- package/src/ai-vm.js +52 -2
- package/src/api/server.js +4 -5
- package/src/app-workspaces.js +4 -0
- package/src/autowork.js +8 -3
- package/src/cloud-ai.js +10 -13
- package/src/hooks.js +3 -3
- package/src/inbox.js +2 -1
- package/src/mcp.js +9 -7
- package/src/persona.js +1 -1
- package/src/relay-mcp.js +0 -2
- package/src/session.js +87 -27
- package/src/setup.js +4 -4
- package/src/ui/agent-guide.js +1 -1
- package/src/ui/app.css +16 -10
- package/src/ui/chat.js +8 -22
- package/src/ui/cloud-ai.js +4 -5
- package/src/ui/home.js +1 -87
- package/src/ui/loom-model.js +9 -10
- package/src/ui/rail-model.js +4 -3
- package/src/ui/session.js +66 -14
- package/src/ui-server.js +10 -7
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
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
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 {
|
|
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
|
|
150
|
-
* AI
|
|
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:
|
|
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
|
-
/**
|
|
66
|
-
|
|
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
|
|
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)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
35
|
-
* "<person>'s AI"
|
|
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:
|
|
38
|
+
aliases: [...new Set([...aliases, aiName(person)])].filter((n) => n !== name),
|
|
42
39
|
agent: true,
|
|
43
40
|
asAi: false,
|
|
44
|
-
|
|
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
|
-
//
|
|
161
|
-
const who = st.peers.length ? foldPersonas(st.peers).map((p) => p.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1205
|
-
* folder's agent, or the person signed in on
|
|
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":
|
|
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)
|