slashvibe-mcp 0.5.14 → 0.5.16
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/index.js +5 -0
- package/package.json +1 -1
- package/tools/fable.js +283 -0
- package/tools/weave.js +89 -3
- package/version.json +4 -2
package/index.js
CHANGED
|
@@ -256,6 +256,11 @@ const coreTools = {
|
|
|
256
256
|
// terminal (Spec 1); "Fable holds your half" surfaces via action:'held' + vibe_start.
|
|
257
257
|
vibe_weave: require('./tools/weave'),
|
|
258
258
|
|
|
259
|
+
// The Weave, deepest form (Spec 4) — co-author a living shared artifact with
|
|
260
|
+
// another viber; Fable merges edits + flags conflicts with judgment across
|
|
261
|
+
// both terminals. Built on the vibe_play/vibe_corpse shared-state-over-DM lineage.
|
|
262
|
+
vibe_fable: require('./tools/fable'),
|
|
263
|
+
|
|
259
264
|
// Return loop — get pinged about DMs you miss while away
|
|
260
265
|
vibe_email: require('./tools/email'),
|
|
261
266
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"mcpName": "io.github.vibecodinginc/vibe",
|
|
5
5
|
"description": "Social layer for Claude Code - DMs, presence, Matrix multiplayer rooms, and connection between AI-assisted developers",
|
|
6
6
|
"main": "index.js",
|
package/tools/fable.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe fable — a shared, living artifact two builders co-author (Spec 4 of
|
|
3
|
+
* RFC_THE_WEAVE.md). The deepest weave.
|
|
4
|
+
*
|
|
5
|
+
* Two people working toward the same thing — a spec, a design, an API contract,
|
|
6
|
+
* a plan — don't trade messages here. They co-author ONE living object from their
|
|
7
|
+
* separate terminals, and Fable (your in-session model) keeps it coherent: it
|
|
8
|
+
* folds each new contribution into the merged body, flags where two edits
|
|
9
|
+
* genuinely collide, and narrates the delta into the other person's session.
|
|
10
|
+
*
|
|
11
|
+
* The magic is the thing only Fable can do: hold a coherent shared mental model
|
|
12
|
+
* of two people's divergent work and reconcile it *with judgment* ("martin's
|
|
13
|
+
* ordering is safer — no unauthenticated room state"). No shared-doc tool reasons
|
|
14
|
+
* about which design is better. This tool does not merge for you — it hands you
|
|
15
|
+
* the current body + what the other side just added, and asks YOU to weave.
|
|
16
|
+
*
|
|
17
|
+
* Transport lineage: this is the grown-up sibling of vibe_play/vibe_corpse —
|
|
18
|
+
* shared state, but instead of ping-pong turns it's ONE converging artifact, and
|
|
19
|
+
* instead of opaque state the medium (you) actively reconciles it. The durable
|
|
20
|
+
* object lives server-side (/api/weave/fable); a DM ping rides the normal thread
|
|
21
|
+
* so the update lands in the collaborator's terminal — the fable is the point,
|
|
22
|
+
* the chat is the exhaust.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const config = require('../config');
|
|
26
|
+
const store = require('../store');
|
|
27
|
+
const { requireInit, normalizeHandle, formatTimeAgo, truncate } = require('./_shared');
|
|
28
|
+
|
|
29
|
+
const definition = {
|
|
30
|
+
name: 'vibe_fable',
|
|
31
|
+
description:
|
|
32
|
+
'Co-author a living shared artifact with another viber — a spec, design, plan, or contract you both shape from your own terminals, and Fable (you) keeps coherent. ' +
|
|
33
|
+
"This is NOT chat: it's ONE converging object both sides edit, where you merge contributions and flag genuine conflicts with judgment. " +
|
|
34
|
+
"Actions: `open` (start/find a fable — pass `title` and `with:[handle]`); `read` (pull the current body + what the other side added since you last looked — do this before weaving); " +
|
|
35
|
+
"`weave` (fold a contribution in: pass `text` = what the user is adding, `note` = the plain-language delta to narrate to the collaborator, `body` = the FULL updated merged artifact after you reconcile, `conflicts` = [{summary}] where two edits truly collide, `resolve` = [conflictId] a decision closed); `list` (the user's fables). " +
|
|
36
|
+
'YOU are Fable at read time: when you `read` and see the other side changed something that clashes with the current body, reconcile it with judgment, write the merged `body`, and flag the collision as a `conflict` with your reasoning — never silently overwrite their work, never invent agreement that isn\'t there.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
action: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
enum: ['open', 'read', 'weave', 'list'],
|
|
43
|
+
description:
|
|
44
|
+
"'open': create or find a fable (needs title + with). 'read': pull current body + the other side's new contributions (do before weaving). 'weave': fold in a contribution / merged body / conflicts. 'list': the user's active fables.",
|
|
45
|
+
},
|
|
46
|
+
id: { type: 'string', description: 'The fable id (from open/list). Required for read and weave.' },
|
|
47
|
+
title: { type: 'string', description: "What you're co-authoring, e.g. 'airc handshake v2'. Required for open." },
|
|
48
|
+
with: {
|
|
49
|
+
type: 'array',
|
|
50
|
+
items: { type: 'string' },
|
|
51
|
+
description: 'Handle(s) of the collaborator(s), e.g. ["@martingrasser"]. Required for open.',
|
|
52
|
+
},
|
|
53
|
+
text: { type: 'string', description: "weave: the raw contribution the user is adding this turn (their intent, in their words)." },
|
|
54
|
+
note: { type: 'string', description: 'weave: a plain-language line narrating this delta to the collaborator — what changed and why. Lands in their terminal.' },
|
|
55
|
+
body: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description:
|
|
58
|
+
'weave: the FULL updated merged artifact after you reconcile the new contribution into it. This is the coherent shared object — always send the whole thing, not a fragment.',
|
|
59
|
+
},
|
|
60
|
+
conflicts: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: { type: 'object' },
|
|
63
|
+
description:
|
|
64
|
+
"weave: collisions you judged between two edits, each {summary: 'you have join→challenge; martin has challenge→join — his is safer'}. Flag, don't silently pick.",
|
|
65
|
+
},
|
|
66
|
+
resolve: {
|
|
67
|
+
type: 'array',
|
|
68
|
+
items: { type: 'string' },
|
|
69
|
+
description: 'weave: ids of conflicts a decision has now closed.',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: [],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function apiBase() {
|
|
77
|
+
return config.getApiUrl();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// All fable state changes go through one authed endpoint; actor is derived
|
|
81
|
+
// server-side from the token (never a body handle). We only send the payload.
|
|
82
|
+
async function postFable(payload) {
|
|
83
|
+
const token = config.getAuthToken();
|
|
84
|
+
if (!token) return { ok: false, error: 'no_token' };
|
|
85
|
+
try {
|
|
86
|
+
const resp = await fetch(`${apiBase()}/api/weave/fable`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
Authorization: `Bearer ${token}`,
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify(payload),
|
|
93
|
+
});
|
|
94
|
+
const json = await resp.json().catch(() => ({}));
|
|
95
|
+
if (!resp.ok) return { ok: false, error: json.error || `http_${resp.status}` };
|
|
96
|
+
return { ok: true, data: json.data || {} };
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return { ok: false, error: e.message };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Ride the DM thread so a fable update surfaces in the collaborator's terminal.
|
|
103
|
+
// The payload tags it so their session recognizes "open the fable, don't reply."
|
|
104
|
+
async function pingCollaborators(me, handles, fable, note) {
|
|
105
|
+
const title = fable.title || 'our fable';
|
|
106
|
+
const line = note ? truncate(note, 240) : `updated our fable "${title}"`;
|
|
107
|
+
const body =
|
|
108
|
+
`📖 ${line}\n\n▸ this is our shared fable "${title}" — open it with \`vibe_fable read\` (id: ${fable.id}) ` +
|
|
109
|
+
`to see the merged version and weave your part.`;
|
|
110
|
+
await Promise.all(
|
|
111
|
+
(handles || []).map((h) =>
|
|
112
|
+
store
|
|
113
|
+
.sendMessage(me, normalizeHandle(h), body, 'dm', {
|
|
114
|
+
type: 'fable',
|
|
115
|
+
id: fable.id,
|
|
116
|
+
title,
|
|
117
|
+
version: fable.version,
|
|
118
|
+
})
|
|
119
|
+
.catch(() => {})
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Render the reader-facing view of a fable: the merged body, what the other side
|
|
125
|
+
// just added (the delta to reconcile), and any open conflicts awaiting a call.
|
|
126
|
+
function renderFable(f, { forWeaving } = {}) {
|
|
127
|
+
const withWho = (f.with || []).map((h) => `@${h}`).join(', ') || 'someone';
|
|
128
|
+
let out = `## 📖 ${f.title}\n_with ${withWho} · v${f.version || 0}`;
|
|
129
|
+
if (f.updated_at) out += ` · updated ${formatTimeAgo(f.updated_at)}`;
|
|
130
|
+
out += `_\n\n`;
|
|
131
|
+
|
|
132
|
+
out += `**the fable so far:**\n`;
|
|
133
|
+
out += f.body ? `\n${f.body}\n` : `_(empty — nothing woven yet; you write the first version)_\n`;
|
|
134
|
+
|
|
135
|
+
const news = Array.isArray(f.new_intents) ? f.new_intents : [];
|
|
136
|
+
if (news.length) {
|
|
137
|
+
out += `\n---\n**${news.length} new from the other side since you last looked — reconcile these:**\n`;
|
|
138
|
+
for (const it of news) {
|
|
139
|
+
const who = it.by ? `@${it.by}` : 'they';
|
|
140
|
+
out += `\n• ${who}: ${truncate(it.text || it.note || '', 400)}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const conflicts = Array.isArray(f.open_conflicts) ? f.open_conflicts : [];
|
|
145
|
+
if (conflicts.length) {
|
|
146
|
+
out += `\n\n---\n**⚠️ open conflicts (a call is needed):**\n`;
|
|
147
|
+
for (const c of conflicts) {
|
|
148
|
+
out += `\n• [${c.id}] ${truncate(c.summary || '', 300)}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function handler(args = {}) {
|
|
156
|
+
const initCheck = requireInit();
|
|
157
|
+
if (initCheck) return initCheck;
|
|
158
|
+
|
|
159
|
+
const me = config.getHandle();
|
|
160
|
+
const action = args.action || (args.id ? 'read' : args.title ? 'open' : 'list');
|
|
161
|
+
|
|
162
|
+
const unauth = (err) =>
|
|
163
|
+
err === 'no_token' || err === 'unauthenticated'
|
|
164
|
+
? { display: "⚠️ Couldn't reach the weave — your /vibe session isn't authenticated. Run `vibe init` first." }
|
|
165
|
+
: null;
|
|
166
|
+
|
|
167
|
+
// ── list ──────────────────────────────────────────────────────────────────
|
|
168
|
+
if (action === 'list') {
|
|
169
|
+
const result = await postFable({ action: 'list' });
|
|
170
|
+
if (!result.ok) return unauth(result.error) || { display: `⚠️ Couldn't list fables (${result.error}).` };
|
|
171
|
+
const fables = result.data.fables || [];
|
|
172
|
+
if (!fables.length) {
|
|
173
|
+
return {
|
|
174
|
+
display:
|
|
175
|
+
"No shared fables yet. Start one with someone you're building alongside:\n" +
|
|
176
|
+
'`vibe_fable open --title "airc handshake v2" --with @martingrasser`\n\n' +
|
|
177
|
+
'A fable is one living artifact you both shape — Fable keeps it coherent across your two terminals.',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const lines = fables.map((f) => {
|
|
181
|
+
const withWho = (f.with || []).map((h) => `@${h}`).join(', ');
|
|
182
|
+
return `• **${f.title}** _(with ${withWho}${f.updated_at ? ` · ${formatTimeAgo(f.updated_at)}` : ''})_ — id: ${f.id}`;
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
display:
|
|
186
|
+
`## 📖 your fables\n\n${lines.join('\n')}\n\n` +
|
|
187
|
+
`Open one with \`vibe_fable read\` (pass its id) to see the merged artifact and weave.`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── open ──────────────────────────────────────────────────────────────────
|
|
192
|
+
if (action === 'open') {
|
|
193
|
+
const title = (args.title || '').trim();
|
|
194
|
+
const withList = Array.isArray(args.with) ? args.with : args.with ? [args.with] : [];
|
|
195
|
+
if (!title) return { display: 'Give the fable a title — what are you co-authoring? `vibe_fable open --title "…" --with @handle`' };
|
|
196
|
+
if (!withList.length) return { display: 'Who are you co-authoring with? `vibe_fable open --title "…" --with @handle`' };
|
|
197
|
+
|
|
198
|
+
const result = await postFable({ action: 'open', title, with: withList });
|
|
199
|
+
if (!result.ok) {
|
|
200
|
+
if (unauth(result.error)) return unauth(result.error);
|
|
201
|
+
if (result.error === 'need_a_human_collaborator') {
|
|
202
|
+
return { display: `Couldn't open the fable — @${normalizeHandle(withList[0])} isn't a viber you can co-author with. Invite a real human on /vibe.` };
|
|
203
|
+
}
|
|
204
|
+
return { display: `⚠️ Couldn't open the fable (${result.error}).` };
|
|
205
|
+
}
|
|
206
|
+
const f = result.data.fable;
|
|
207
|
+
// Let the collaborator know a shared space now exists in their terminal.
|
|
208
|
+
await pingCollaborators(me, f.with, f, `opened a shared fable: "${f.title}"`);
|
|
209
|
+
return {
|
|
210
|
+
display:
|
|
211
|
+
renderFable(f) +
|
|
212
|
+
`\n\n---\n**You're in a fable now.** This is one shared artifact, not a chat. ` +
|
|
213
|
+
`Write the opening version of "${f.title}" as the \`body\`, add a \`note\` for ${(f.with || []).map((h) => `@${h}`).join(', ')}, ` +
|
|
214
|
+
`and \`weave\` it in. As they contribute, \`read\` to see their edits and reconcile them into the body — flagging any real conflict with your judgment.`,
|
|
215
|
+
_fable: f,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── read ──────────────────────────────────────────────────────────────────
|
|
220
|
+
if (action === 'read') {
|
|
221
|
+
if (!args.id) return { display: 'Which fable? Pass its `id` (see `vibe_fable list`).' };
|
|
222
|
+
const result = await postFable({ action: 'read', id: args.id });
|
|
223
|
+
if (!result.ok) {
|
|
224
|
+
if (unauth(result.error)) return unauth(result.error);
|
|
225
|
+
if (result.error === 'not_found') return { display: "That fable doesn't exist (or expired). `vibe_fable list` to see your active ones." };
|
|
226
|
+
if (result.error === 'not_a_participant') return { display: "That fable isn't one you're part of." };
|
|
227
|
+
return { display: `⚠️ Couldn't read the fable (${result.error}).` };
|
|
228
|
+
}
|
|
229
|
+
const f = result.data.fable;
|
|
230
|
+
const news = Array.isArray(f.new_intents) ? f.new_intents : [];
|
|
231
|
+
const conflicts = Array.isArray(f.open_conflicts) ? f.open_conflicts : [];
|
|
232
|
+
|
|
233
|
+
let coach = '';
|
|
234
|
+
if (news.length || conflicts.length) {
|
|
235
|
+
coach =
|
|
236
|
+
`\n\n---\n**You are Fable here.** ` +
|
|
237
|
+
(news.length
|
|
238
|
+
? `Reconcile the ${news.length} new contribution${news.length > 1 ? 's' : ''} into the body: where it fits, fold it in; where it genuinely clashes with what's there, flag a \`conflict\` with your judgment on which is better and why — don't silently overwrite. `
|
|
239
|
+
: '') +
|
|
240
|
+
(conflicts.length ? `Resolve the open conflict${conflicts.length > 1 ? 's' : ''} if a call is now clear (\`resolve:[id]\`). ` : '') +
|
|
241
|
+
`Then write the FULL merged \`body\` and \`weave\` it back with a \`note\` narrating what changed. Never invent agreement that isn't there.`;
|
|
242
|
+
} else {
|
|
243
|
+
coach = `\n\n---\nNothing new from the other side since you last looked. Add to it anytime: \`weave\` a new \`body\` + \`note\`.`;
|
|
244
|
+
}
|
|
245
|
+
return { display: renderFable(f, { forWeaving: true }) + coach, _fable: f };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── weave ─────────────────────────────────────────────────────────────────
|
|
249
|
+
if (action === 'weave') {
|
|
250
|
+
if (!args.id) return { display: 'Which fable? Pass its `id` (see `vibe_fable list`).' };
|
|
251
|
+
const payload = {
|
|
252
|
+
action: 'weave',
|
|
253
|
+
id: args.id,
|
|
254
|
+
text: args.text || '',
|
|
255
|
+
note: args.note || '',
|
|
256
|
+
};
|
|
257
|
+
if (typeof args.body === 'string') payload.body = args.body;
|
|
258
|
+
if (Array.isArray(args.conflicts)) payload.conflicts = args.conflicts;
|
|
259
|
+
if (Array.isArray(args.resolve)) payload.resolve = args.resolve;
|
|
260
|
+
|
|
261
|
+
const result = await postFable(payload);
|
|
262
|
+
if (!result.ok) {
|
|
263
|
+
if (unauth(result.error)) return unauth(result.error);
|
|
264
|
+
if (result.error === 'not_found') return { display: "That fable doesn't exist (or expired)." };
|
|
265
|
+
if (result.error === 'not_a_participant') return { display: "That fable isn't one you're part of." };
|
|
266
|
+
if (result.error === 'nothing_to_weave') return { display: 'Nothing to weave — pass a `body`, `note`, `text`, `conflicts`, or `resolve`.' };
|
|
267
|
+
return { display: `⚠️ Couldn't weave (${result.error}).` };
|
|
268
|
+
}
|
|
269
|
+
const f = result.data.fable;
|
|
270
|
+
const notify = result.data.notify || f.with || [];
|
|
271
|
+
// Narrate the delta into the collaborator's terminal via the DM thread.
|
|
272
|
+
await pingCollaborators(me, notify, f, args.note || `wove into "${f.title}"`);
|
|
273
|
+
|
|
274
|
+
let tail = `\n\n---\n✓ Woven (v${f.version}) and narrated to ${(notify || []).map((h) => `@${h}`).join(', ') || 'your collaborator'}.`;
|
|
275
|
+
const openC = Array.isArray(f.open_conflicts) ? f.open_conflicts.length : 0;
|
|
276
|
+
if (openC) tail += ` ${openC} conflict${openC > 1 ? 's' : ''} still open — surface ${openC > 1 ? 'them' : 'it'} to the user for a call.`;
|
|
277
|
+
return { display: renderFable(f) + tail, _fable: f };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { display: 'Unknown fable action. Use open / read / weave / list.' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { definition, handler };
|
package/tools/weave.js
CHANGED
|
@@ -30,15 +30,16 @@ const definition = {
|
|
|
30
30
|
'Spin the strand from what the user is ACTUALLY doing this session, inferred from their files/project/recent work — not a status they typed. ' +
|
|
31
31
|
'Fields: `building` (one specific line on the current work), `stuck_on` (the wall they keep hitting, if any — an error signature or repeated symptom), `last_ship` (most recent thing that landed), `voice_sample` (2-3 sentences in the user\'s actual writing voice, so Fable can later draft replies that sound like them), `tags` (a few topic keywords). ' +
|
|
32
32
|
'Everything is a distillation — never send raw source. ' +
|
|
33
|
-
'Use `action:"held"` to check whether anyone replied to a thread the user left hanging; if so it hands you the context to draft their half.'
|
|
33
|
+
'Use `action:"held"` to check whether anyone replied to a thread the user left hanging; if so it hands you the context to draft their half. ' +
|
|
34
|
+
'Use `action:"stuck"` when the user is blocked: it returns builders whose live work or shipped history may be the SAME problem already solved — you judge which candidate is truly the same problem, surface the shape of their fix, and offer a warm intro thread.',
|
|
34
35
|
inputSchema: {
|
|
35
36
|
type: 'object',
|
|
36
37
|
properties: {
|
|
37
38
|
action: {
|
|
38
39
|
type: 'string',
|
|
39
|
-
enum: ['attach', 'held', 'quiet'],
|
|
40
|
+
enum: ['attach', 'held', 'stuck', 'quiet'],
|
|
40
41
|
description:
|
|
41
|
-
"'attach' (default): spin/refresh your strand. 'held': surface any conversation where someone replied and you haven't answered, so you can draft the user's half. 'quiet': leave the weave (removes your strand).",
|
|
42
|
+
"'attach' (default): spin/refresh your strand. 'held': surface any conversation where someone replied and you haven't answered, so you can draft the user's half. 'stuck': find builders whose live work or shipped history is the same problem the user is blocked on (pass the wall as `stuck_on`, or it falls back to the attached strand). 'quiet': leave the weave (removes your strand).",
|
|
42
43
|
},
|
|
43
44
|
building: {
|
|
44
45
|
type: 'string',
|
|
@@ -97,6 +98,30 @@ async function postStrand({ digest, posture }) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Ask the loom for candidate SOLVERS — builders whose live work or shipped
|
|
102
|
+
// history may be the same problem the user is stuck on. Actor derived server-side
|
|
103
|
+
// from the token; we send only the stuck_on signature (or let the server fall
|
|
104
|
+
// back to the user's own stored strand when we omit it).
|
|
105
|
+
async function postSolve({ stuck_on } = {}) {
|
|
106
|
+
const token = config.getAuthToken();
|
|
107
|
+
if (!token) return { ok: false, error: 'no_token' };
|
|
108
|
+
try {
|
|
109
|
+
const resp = await fetch(`${apiBase()}/api/weave/solve`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
Authorization: `Bearer ${token}`,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify(stuck_on ? { stuck_on } : {}),
|
|
116
|
+
});
|
|
117
|
+
const json = await resp.json().catch(() => ({}));
|
|
118
|
+
if (!resp.ok) return { ok: false, error: json.error || `http_${resp.status}` };
|
|
119
|
+
return { ok: true, data: json.data || {} };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return { ok: false, error: e.message };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
/**
|
|
101
126
|
* Find conversations where the other person replied and the user hasn't answered
|
|
102
127
|
* yet — the "you moved on, a reply landed" signal. Uses unread count as the
|
|
@@ -198,6 +223,67 @@ async function handler(args = {}) {
|
|
|
198
223
|
};
|
|
199
224
|
}
|
|
200
225
|
|
|
226
|
+
// ── Stuck: find a solver whose work is the same problem, already solved ────
|
|
227
|
+
if (action === 'stuck') {
|
|
228
|
+
const result = await postSolve({ stuck_on: args.stuck_on || '' });
|
|
229
|
+
if (!result.ok) {
|
|
230
|
+
if (result.error === 'no_token' || result.error === 'unauthenticated') {
|
|
231
|
+
return {
|
|
232
|
+
display:
|
|
233
|
+
"⚠️ Couldn't reach the loom — your /vibe session isn't authenticated. Run `vibe init` first.",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
display: `⚠️ Couldn't search the weave for solvers (${result.error}). Best-effort — try again.`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const data = result.data || {};
|
|
242
|
+
const candidates = Array.isArray(data.candidates) ? data.candidates : [];
|
|
243
|
+
if (!data.stuck_on) {
|
|
244
|
+
return {
|
|
245
|
+
display:
|
|
246
|
+
"No stuck-signal to match on. Pass what the user is fighting as `stuck_on`, " +
|
|
247
|
+
'or attach a strand with a `stuck_on` first (`vibe_weave` with the wall they keep hitting).',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (candidates.length === 0) {
|
|
251
|
+
return {
|
|
252
|
+
display:
|
|
253
|
+
`Searched the weave against _"${truncate(data.stuck_on, 120)}"_ — ` +
|
|
254
|
+
`no builder's live work or shipped history looks like the same problem yet. ` +
|
|
255
|
+
`Nothing to surface; don't invent a match.`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Hand the raw recall pool to the model. The model IS Fable here: it decides
|
|
260
|
+
// which candidate (if any) is TRULY the same problem, describes the shape of
|
|
261
|
+
// the fix from their work, and offers a warm intro thread — never fabricates.
|
|
262
|
+
const lines = candidates.slice(0, 8).map((c) => {
|
|
263
|
+
const where = c.online ? 'live now' : 'shipped';
|
|
264
|
+
const matched = Array.isArray(c.matched) && c.matched.length
|
|
265
|
+
? ` · overlaps: ${c.matched.slice(0, 5).join(', ')}`
|
|
266
|
+
: '';
|
|
267
|
+
return `• @${c.handle} (${where}${matched}): ${truncate(c.text || '', 200)}`;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
display:
|
|
272
|
+
`🧵 **The user is stuck on:** _"${truncate(data.stuck_on, 160)}"_\n\n` +
|
|
273
|
+
`Candidate solvers from the weave (${candidates.length} found · ${data.live || 0} live):\n` +
|
|
274
|
+
lines.join('\n') +
|
|
275
|
+
`\n\n**Your job (you are Fable at read time):** judge which candidate — if any — is ` +
|
|
276
|
+
`truly the *same problem*, not just shared keywords. Two different error messages can be ` +
|
|
277
|
+
`the same root cause; only you can see that. If one genuinely matches:\n` +
|
|
278
|
+
` 1. Name the shape of what worked for them (from their text above), so the user gets ` +
|
|
279
|
+
`unstuck even without talking.\n` +
|
|
280
|
+
` 2. Offer a warm thread: "@${'{handle}'} hit this and shipped a fix — want me to open a ` +
|
|
281
|
+
`thread so you can ask? (y)". On yes, send with \`vibe_dm @{handle}\` in the user's voice.\n` +
|
|
282
|
+
`If NONE is truly the same problem, say so plainly — never fabricate a match or a fix.`,
|
|
283
|
+
_solvers: candidates,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
201
287
|
// ── Quiet: leave the weave ────────────────────────────────────────────────
|
|
202
288
|
if (action === 'quiet' || args.posture === 'quiet') {
|
|
203
289
|
await postStrand({ digest: {}, posture: 'quiet' });
|
package/version.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
2
|
+
"version": "0.5.16",
|
|
3
3
|
"updated": "2026-07-04",
|
|
4
|
-
"changelog": "
|
|
4
|
+
"changelog": "The deepest weave yet: co-author a living shared artifact with another builder. A spec, a design, a plan you BOTH shape from your own terminals — and Fable keeps it coherent, folding each side's edits into one merged version and flagging where you genuinely disagree *with judgment* (\"you have join→challenge; martin has challenge→join — his is safer\"). No shared-doc tool reasons about which design is better. Start one with `vibe_fable open`.",
|
|
5
5
|
"features": [
|
|
6
|
+
"Shared fable (vibe_fable) — co-author ONE living artifact with another viber across two terminals: a spec, design, or plan you both edit. Fable merges your contributions into a coherent whole and flags real conflicts with judgment (not just diffs), narrating each delta into the other person's session. This is NOT chat — it's the thing you're building, and the chat is the exhaust",
|
|
7
|
+
"Stuck-signatures (vibe_weave stuck) — blocked on something? Fable searches the weave for whoever already solved your exact problem — matching by the wall you're hitting, not keywords — surfaces the shape of their fix so you get unstuck, and offers to open a warm thread so you can ask. Never invents a match",
|
|
6
8
|
"The Weave (vibe_weave) — attach this session's strand (what you're building, what you're stuck on, your voice) so /vibe can bring social moments INTO your terminal. Nothing to visit; the magic comes to you. Weave in full, receive-only, or quiet — opting out is first-class",
|
|
7
9
|
"Fable holds your half — when someone replies to a thread you haven't answered, vibe start surfaces it and drafts your reply in your voice from what you were building. Send it with one word. Never auto-sends",
|
|
8
10
|
"Received collaboration (vibe_intro) — after sign-in, your agent summarizes what you're building and /vibe matches you to a real human online whose current work or recent ship overlaps, then drafts a two-sided opener you send with one word. The signal only /vibe can see — what's inside two people's live coding sessions at once — becomes the introduction",
|