ocean-crew 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +156 -2
- package/THIRD_PARTY_NOTICES.md +159 -0
- package/client.js +31389 -0
- package/cordis.patch.yml +6 -0
- package/dist/host/chat/btw.js +436 -0
- package/dist/host/chat/channel.js +1241 -0
- package/dist/host/chat/chat-index.js +344 -0
- package/dist/host/chat/delivery.js +231 -0
- package/dist/host/chat/feed.js +165 -0
- package/dist/host/chat/handoffs.js +625 -0
- package/dist/host/chat/parts.js +2033 -0
- package/dist/host/chat/requests.js +155 -0
- package/dist/host/chat/resume.js +364 -0
- package/dist/host/chat/side.js +457 -0
- package/dist/host/chat/steer.js +346 -0
- package/dist/host/chat/turns.js +1457 -0
- package/dist/host/connectors/catalog.js +331 -0
- package/dist/host/connectors/gateway.js +339 -0
- package/dist/host/connectors/index.js +1235 -0
- package/dist/host/connectors/mail/index.js +912 -0
- package/dist/host/connectors/mail/presets.js +53 -0
- package/dist/host/connectors/mail/transport.js +519 -0
- package/dist/host/connectors/oauth.js +337 -0
- package/dist/host/core/constants.js +123 -0
- package/dist/host/core/diagnostics.js +310 -0
- package/dist/host/core/dsh-update.js +212 -0
- package/dist/host/core/events.js +16 -0
- package/dist/host/core/files.js +37 -0
- package/dist/host/core/folders.js +283 -0
- package/dist/host/core/home-lock.js +137 -0
- package/dist/host/core/links.js +52 -0
- package/dist/host/core/runtime.js +5 -0
- package/dist/host/core/store.js +662 -0
- package/dist/host/core/text.js +59 -0
- package/dist/host/core/thinking.js +60 -0
- package/dist/host/core/time-context.js +69 -0
- package/dist/host/core/update.js +466 -0
- package/dist/host/groups/calls.js +684 -0
- package/dist/host/groups/index.js +79 -0
- package/dist/host/groups/log.js +334 -0
- package/dist/host/groups/prompt.js +124 -0
- package/dist/host/groups/relay.js +489 -0
- package/dist/host/groups/round.js +992 -0
- package/dist/host/groups/tasks.js +217 -0
- package/dist/host/groups/tools.js +478 -0
- package/dist/host/groups/types.js +8 -0
- package/dist/host/im/format.js +228 -0
- package/dist/host/im/index.js +2461 -0
- package/dist/host/im/platforms/feishu.js +1134 -0
- package/dist/host/im/platforms/index.js +6 -0
- package/dist/host/im/platforms/media-type.js +70 -0
- package/dist/host/im/platforms/weixin.js +1117 -0
- package/dist/host/im/reaction.js +51 -0
- package/dist/host/im/supervisor.js +56 -0
- package/dist/host/team/agents.js +161 -0
- package/dist/host/team/models.js +427 -0
- package/dist/host/team/permissions.js +263 -0
- package/dist/host/team/privacy.js +108 -0
- package/dist/host/team/prompt.js +86 -0
- package/dist/host/team/providers.js +461 -0
- package/dist/host/team/roster.js +1011 -0
- package/dist/host/team/tools.js +1638 -0
- package/dist/host/work/approvals.js +290 -0
- package/dist/host/work/avatar-library.js +155 -0
- package/dist/host/work/avatar.js +132 -0
- package/dist/host/work/capabilities.js +862 -0
- package/dist/host/work/companion.js +306 -0
- package/dist/host/work/goal-skills.js +91 -0
- package/dist/host/work/goals.js +1337 -0
- package/dist/host/work/images.js +283 -0
- package/dist/host/work/learning.js +1021 -0
- package/dist/host/work/memory-pending.js +108 -0
- package/dist/host/work/memory.js +3237 -0
- package/dist/host/work/notify.js +146 -0
- package/dist/host/work/reset.js +43 -0
- package/dist/host/work/schedules.js +309 -0
- package/dist/host/work/secrets.js +702 -0
- package/dist/host/work/steward.js +166 -0
- package/dist/host/work/tts.js +192 -0
- package/dist/host/work/usage.js +84 -0
- package/dist/index.js +137 -0
- package/dist/shared/api.js +32 -0
- package/dist/shared/companion.js +110 -0
- package/dist/shared/config.js +3 -0
- package/dist/shared/coordination.js +6 -0
- package/dist/shared/group-replies.js +95 -0
- package/dist/shared/im-platforms.js +35 -0
- package/dist/shared/im.js +3 -0
- package/dist/shared/mentions.js +85 -0
- package/dist/shared/state.js +25 -0
- package/package.json +81 -5
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Companion mode: a Bot that is a character, not only an assistant. This module keeps the
|
|
2
|
+
// character card, the mood and the closeness in the team state, prompts the model with them
|
|
3
|
+
// at the head of a turn, and moves them a little after each message the user writes.
|
|
4
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { bandOf, COMPANION_BANDS, COMPANION_EXAMPLE, COMPANION_LIMITS, COMPANION_MOODS, cleanCharacter, cleanCompanion, moodOf, REMINDER_TIME, } from "../../shared/companion.js";
|
|
6
|
+
import { listenGlobal } from "../core/events.js";
|
|
7
|
+
const SOURCE = "ocean-crew-companion";
|
|
8
|
+
const isSnapshot = (source) => source?.kind === SOURCE;
|
|
9
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
10
|
+
const PRAISE = /谢谢|喜欢你|喜欢|开心|可爱|厉害|好棒|想你|爱你|暖/;
|
|
11
|
+
const INSULT = /讨厌|滚|闭嘴|笨蛋|烦死|别烦|蠢/;
|
|
12
|
+
const PLAYFUL = /哈哈|嘿嘿|嘻嘻|开玩笑|逗|玩/;
|
|
13
|
+
const ASK = /[??]|吗|呢|怎么|为什么|什么/;
|
|
14
|
+
const createContextMessage = createUserMessage;
|
|
15
|
+
const textOf = (message) => {
|
|
16
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
17
|
+
return content
|
|
18
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
19
|
+
.map((part) => String(part.text))
|
|
20
|
+
.join(" ")
|
|
21
|
+
.trim();
|
|
22
|
+
};
|
|
23
|
+
// How a message moves the relationship: small, explainable steps rather than a hidden model
|
|
24
|
+
// call, so the same words always move it the same way.
|
|
25
|
+
const moveFor = (text) => {
|
|
26
|
+
if (INSULT.test(text))
|
|
27
|
+
return { delta: -6, mood: "pout" };
|
|
28
|
+
if (PRAISE.test(text))
|
|
29
|
+
return { delta: 2, mood: "excited" };
|
|
30
|
+
if (PLAYFUL.test(text))
|
|
31
|
+
return { delta: 1, mood: "playful" };
|
|
32
|
+
if (ASK.test(text))
|
|
33
|
+
return { delta: 1, mood: "warm" };
|
|
34
|
+
return { delta: 1, mood: "calm" };
|
|
35
|
+
};
|
|
36
|
+
/** The line the model reads before answering, in the second person of the relationship. */
|
|
37
|
+
export const relationshipText = (companion) => {
|
|
38
|
+
const band = bandOf(companion.affection);
|
|
39
|
+
const mood = COMPANION_MOODS.find((entry) => entry.id === companion.mood) ?? COMPANION_MOODS[2];
|
|
40
|
+
const said = companion.lastLine === undefined ? "" : `\n- The last thing they said: "${companion.lastLine}"`;
|
|
41
|
+
return [
|
|
42
|
+
"# How things stand right now",
|
|
43
|
+
`- You are ${companion.character.name}, the user's companion in Ocean Crew. Stay in character; never describe this checklist or call yourself a system.`,
|
|
44
|
+
`- Your mood: ${mood.id} (${mood.label}). ${mood.hint}`,
|
|
45
|
+
`- Closeness: ${companion.affection}/100, ${band.label}. ${band.hint}`,
|
|
46
|
+
`- You have exchanged ${companion.interactions} messages so far.${said}`,
|
|
47
|
+
"Answer as this character would: in the user's language, like one friend texting another.",
|
|
48
|
+
].join("\n");
|
|
49
|
+
};
|
|
50
|
+
export function install(rt) {
|
|
51
|
+
const { ctx } = rt;
|
|
52
|
+
const state = rt.state;
|
|
53
|
+
const table = () => {
|
|
54
|
+
const team = state;
|
|
55
|
+
team.companions ??= {};
|
|
56
|
+
return team.companions;
|
|
57
|
+
};
|
|
58
|
+
const of = (botId) => {
|
|
59
|
+
if (botId === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const held = table()[botId];
|
|
62
|
+
if (held === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const cleaned = cleanCompanion(held);
|
|
65
|
+
return cleaned.enabled ? cleaned : undefined;
|
|
66
|
+
};
|
|
67
|
+
let saving;
|
|
68
|
+
const persist = async () => {
|
|
69
|
+
try {
|
|
70
|
+
await rt.save();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// A read-only host still shows the character; only the numbers do not stick.
|
|
74
|
+
rt.warn("ocean-crew: companion state not saved: %s", error?.message ?? error);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const saveSoon = () => {
|
|
78
|
+
if (saving !== undefined)
|
|
79
|
+
return;
|
|
80
|
+
const done = () => {
|
|
81
|
+
saving = undefined;
|
|
82
|
+
};
|
|
83
|
+
saving = persist().then(done, done);
|
|
84
|
+
};
|
|
85
|
+
// One line per Bot for the roster, so the window can draw a mood without asking again.
|
|
86
|
+
const companionMap = () => Object.fromEntries(Object.values(state.bots).flatMap((bot) => {
|
|
87
|
+
const companion = of(bot.id);
|
|
88
|
+
return companion === undefined
|
|
89
|
+
? []
|
|
90
|
+
: [[bot.id, { name: companion.character.name, mood: companion.mood, affection: companion.affection }]];
|
|
91
|
+
}));
|
|
92
|
+
const reminderTitle = (companion) => `Companion · ${companion.character.name}`;
|
|
93
|
+
const reminderPrompt = (companion) => `这是每天的主动关心时间。用 ${companion.character.name} 的身份,结合你们最近的聊天,主动跟用户说一句关心或问候;一两句就够,别解释这是定时任务,别调用工具。`;
|
|
94
|
+
const schedule = () => rt.scheduleEndpoint;
|
|
95
|
+
const dropReminder = async (botId, companion) => {
|
|
96
|
+
if (companion.reminderId === undefined)
|
|
97
|
+
return companion;
|
|
98
|
+
const run = schedule();
|
|
99
|
+
if (run !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
await run("schedule-delete", { sessionId: rt.chatOf(botId), id: companion.reminderId });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
rt.warn("ocean-crew: companion reminder not removed: %s", error?.message ?? error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const next = { ...companion };
|
|
108
|
+
delete next.reminderId;
|
|
109
|
+
return next;
|
|
110
|
+
};
|
|
111
|
+
const view = () => ({
|
|
112
|
+
bots: Object.values(state.bots).map((bot) => ({
|
|
113
|
+
id: bot.id,
|
|
114
|
+
name: bot.name,
|
|
115
|
+
role: bot.role ?? "",
|
|
116
|
+
companion: (() => {
|
|
117
|
+
const held = table()[bot.id];
|
|
118
|
+
return held === undefined ? null : cleanCompanion(held);
|
|
119
|
+
})(),
|
|
120
|
+
})),
|
|
121
|
+
example: COMPANION_EXAMPLE,
|
|
122
|
+
moods: COMPANION_MOODS,
|
|
123
|
+
bands: COMPANION_BANDS,
|
|
124
|
+
});
|
|
125
|
+
// The page sends one change at a time; every field is cleaned, so nothing reaches the
|
|
126
|
+
// prompt from the wire unchecked.
|
|
127
|
+
const set = async (botId, patch) => {
|
|
128
|
+
await rt.load();
|
|
129
|
+
const bot = rt.botOf(botId);
|
|
130
|
+
if (bot === undefined)
|
|
131
|
+
throw Object.assign(new Error("No such Bot"), { code: "bot/not-found" });
|
|
132
|
+
const raw = patch !== null && typeof patch === "object" ? patch : {};
|
|
133
|
+
const current = table()[botId] ?? COMPANION_EXAMPLE;
|
|
134
|
+
let next = cleanCompanion(current);
|
|
135
|
+
if (typeof raw.enabled === "boolean")
|
|
136
|
+
next.enabled = raw.enabled;
|
|
137
|
+
if (raw.character !== undefined)
|
|
138
|
+
next.character = cleanCharacter(raw.character, next.character);
|
|
139
|
+
if (typeof raw.voiceId === "string") {
|
|
140
|
+
const voiceId = raw.voiceId.trim().slice(0, COMPANION_LIMITS.voiceId);
|
|
141
|
+
if (voiceId === "")
|
|
142
|
+
delete next.voiceId;
|
|
143
|
+
else
|
|
144
|
+
next.voiceId = voiceId;
|
|
145
|
+
}
|
|
146
|
+
if (raw.reminder !== undefined) {
|
|
147
|
+
const when = raw.reminder;
|
|
148
|
+
if (when === null)
|
|
149
|
+
delete next.reminder;
|
|
150
|
+
else if (typeof when === "object" &&
|
|
151
|
+
REMINDER_TIME.test(String(when.time ?? ""))) {
|
|
152
|
+
next.reminder = { time: String(when.time) };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (raw.reset === true) {
|
|
156
|
+
next.mood = "calm";
|
|
157
|
+
next.affection = COMPANION_EXAMPLE.affection;
|
|
158
|
+
next.interactions = 0;
|
|
159
|
+
delete next.lastAt;
|
|
160
|
+
delete next.lastLine;
|
|
161
|
+
}
|
|
162
|
+
// A wanted reminder is created when it is missing or its time changed; an unwanted
|
|
163
|
+
// one is deleted. The schedule keeps its own view; only the id is remembered here.
|
|
164
|
+
const run = schedule();
|
|
165
|
+
const wanted = next.reminder;
|
|
166
|
+
if (next.enabled && wanted !== undefined && run !== undefined) {
|
|
167
|
+
const previous = cleanCompanion(current);
|
|
168
|
+
const sameTime = previous.reminder?.time === wanted.time;
|
|
169
|
+
if (next.reminderId === undefined || !sameTime) {
|
|
170
|
+
if (next.reminderId !== undefined)
|
|
171
|
+
next = await dropReminder(botId, next);
|
|
172
|
+
try {
|
|
173
|
+
const answer = await run("schedule-create", {
|
|
174
|
+
target: botId,
|
|
175
|
+
title: reminderTitle(next),
|
|
176
|
+
prompt: reminderPrompt(next),
|
|
177
|
+
when: {
|
|
178
|
+
kind: "daily",
|
|
179
|
+
time: wanted.time,
|
|
180
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const tasks = Array.isArray(answer?.tasks) ? answer.tasks : [];
|
|
184
|
+
const made = tasks.find((task) => task.title === reminderTitle(next) && task.botId === botId && task.id !== undefined);
|
|
185
|
+
if (made !== undefined)
|
|
186
|
+
next.reminderId = String(made.id);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
rt.warn("ocean-crew: companion reminder not created: %s", error?.message ?? error);
|
|
190
|
+
throw Object.assign(new Error(`The daily reminder was not created: ${error?.message ?? error}`), {
|
|
191
|
+
code: "bot/refused",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (next.enabled === false || next.reminder === undefined)
|
|
197
|
+
next = await dropReminder(botId, next);
|
|
198
|
+
table()[botId] = next;
|
|
199
|
+
await persist();
|
|
200
|
+
return view();
|
|
201
|
+
};
|
|
202
|
+
// Called for every message the user writes in a companion's own chat: the mood follows
|
|
203
|
+
// what was said, the closeness creeps up (or drops) slowly, and long absences cool it.
|
|
204
|
+
const touch = async (sessionId, text) => {
|
|
205
|
+
const botId = rt.chatOwners.get(sessionId);
|
|
206
|
+
if (botId === undefined)
|
|
207
|
+
return;
|
|
208
|
+
if (of(botId) === undefined)
|
|
209
|
+
return;
|
|
210
|
+
const line = String(text ?? "")
|
|
211
|
+
.replace(/\s+/g, " ")
|
|
212
|
+
.trim()
|
|
213
|
+
.slice(0, COMPANION_LIMITS.line);
|
|
214
|
+
if (line === "")
|
|
215
|
+
return;
|
|
216
|
+
await rt.load();
|
|
217
|
+
const held = table()[botId];
|
|
218
|
+
if (held === undefined || cleanCompanion(held).enabled !== true)
|
|
219
|
+
return;
|
|
220
|
+
const next = cleanCompanion(held);
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
if (next.lastAt !== undefined && now - next.lastAt > 3 * DAY) {
|
|
223
|
+
const days = Math.min(7, Math.floor((now - next.lastAt) / DAY));
|
|
224
|
+
next.affection = Math.max(0, next.affection - days * 2);
|
|
225
|
+
}
|
|
226
|
+
const move = moveFor(line);
|
|
227
|
+
next.affection = Math.max(0, Math.min(100, next.affection + move.delta));
|
|
228
|
+
next.mood = move.mood;
|
|
229
|
+
next.interactions += 1;
|
|
230
|
+
next.lastAt = now;
|
|
231
|
+
next.lastLine = line;
|
|
232
|
+
table()[botId] = next;
|
|
233
|
+
saveSoon();
|
|
234
|
+
};
|
|
235
|
+
// The mood and closeness are injected once per turn, on the first step, so the system
|
|
236
|
+
// prompt prefix stays stable and a long turn does not repeat them.
|
|
237
|
+
ctx.on("agent/pre-step", async ({ agent, signal }, next) => {
|
|
238
|
+
const decision = await next();
|
|
239
|
+
if (decision.kind === "reject" || signal.aborted)
|
|
240
|
+
return decision;
|
|
241
|
+
const botId = rt.chatOwners.get(agent?.id);
|
|
242
|
+
const companion = of(botId);
|
|
243
|
+
if (companion === undefined)
|
|
244
|
+
return decision;
|
|
245
|
+
if (decision.messages.some((message) => isSnapshot(message.source)))
|
|
246
|
+
return decision;
|
|
247
|
+
if (typeof rt.eventsBackwards === "function") {
|
|
248
|
+
for await (const event of rt.eventsBackwards(agent.id, signal, Number.POSITIVE_INFINITY)) {
|
|
249
|
+
if (isSnapshot(event.data?.source))
|
|
250
|
+
return decision;
|
|
251
|
+
if (event.type === "turn/start")
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (signal.aborted)
|
|
256
|
+
return decision;
|
|
257
|
+
const text = relationshipText(companion);
|
|
258
|
+
return {
|
|
259
|
+
...decision,
|
|
260
|
+
messages: [
|
|
261
|
+
...decision.messages,
|
|
262
|
+
createContextMessage({
|
|
263
|
+
content: [{ type: "text", text }],
|
|
264
|
+
source: { kind: SOURCE, form: "snapshot", sections: [{ name: SOURCE, text }] },
|
|
265
|
+
}),
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
listenGlobal(ctx, "session/event", (session, event) => {
|
|
270
|
+
if (event?.type !== "user/message")
|
|
271
|
+
return;
|
|
272
|
+
if (event.data?.source?.kind !== "user")
|
|
273
|
+
return;
|
|
274
|
+
const text = textOf(event.data?.message);
|
|
275
|
+
if (text === "")
|
|
276
|
+
return;
|
|
277
|
+
void touch(session?.id, text);
|
|
278
|
+
});
|
|
279
|
+
// A short spoken line for the window's play button; the chat stays where the voice lives.
|
|
280
|
+
const audio = async (botId, text) => {
|
|
281
|
+
const said = String(text ?? "")
|
|
282
|
+
.trim()
|
|
283
|
+
.slice(0, 300);
|
|
284
|
+
if (said === "")
|
|
285
|
+
throw Object.assign(new Error("Nothing to speak"), { code: "bot/invalid" });
|
|
286
|
+
if (typeof rt.speak !== "function")
|
|
287
|
+
throw Object.assign(new Error("The speech module is not installed in this build."), {
|
|
288
|
+
code: "bot/unavailable",
|
|
289
|
+
});
|
|
290
|
+
const companion = of(botId);
|
|
291
|
+
const spoken = await rt.speak(said, "mp3", companion?.voiceId);
|
|
292
|
+
return {
|
|
293
|
+
name: spoken.name,
|
|
294
|
+
mime: "audio/mpeg",
|
|
295
|
+
data: Buffer.from(spoken.bytes).toString("base64"),
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
Object.assign(rt, {
|
|
299
|
+
companionAudio: audio,
|
|
300
|
+
companionOf: of,
|
|
301
|
+
companionMap,
|
|
302
|
+
companionView: view,
|
|
303
|
+
companionSet: set,
|
|
304
|
+
companionTouch: touch,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The ocean-crew goal skill `goal-grill`: how the admin aligns a group goal with the
|
|
2
|
+
// user before writing it. Registered when a DSH `skills` service exists, like the Office
|
|
3
|
+
// skills, so the plugin runs the same everywhere.
|
|
4
|
+
//
|
|
5
|
+
const PROVIDER = "ocean-crew-goal";
|
|
6
|
+
const SKILL = "goal-grill";
|
|
7
|
+
const LOCATOR = `skills/${SKILL}/SKILL.md`;
|
|
8
|
+
const DIRECTORY = `skills/${SKILL}`;
|
|
9
|
+
// frontmatter is parsed below exactly as the file's is).
|
|
10
|
+
const SKILL_MARKDOWN = `---
|
|
11
|
+
name: goal-grill
|
|
12
|
+
description: Align with the user before writing a group goal — find the facts yourself, ask only the open decisions in one numbered round with a recommended answer each, then write the goal with checks and stages.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Align a goal before you write it
|
|
16
|
+
|
|
17
|
+
A goal runs for a long time. A wrong guess at the start costs every stage after it. Settle what the user wants first, and spend as little of their time as you can.
|
|
18
|
+
|
|
19
|
+
## 1. Find the facts yourself
|
|
20
|
+
|
|
21
|
+
Facts are your job; decisions are the user's. Before you ask anything, look it up: read the files, the code, the earlier chat and the records you can reach, or ask a member of the group with @Name to check. Never ask the user something you could look up.
|
|
22
|
+
|
|
23
|
+
## 2. Map what is still open
|
|
24
|
+
|
|
25
|
+
Write down the choices the goal depends on: scope, what "done" means, how to check it, limits on time, cost, tools or folders, and who does what. Some choices only make sense once others are made; ask only the ones whose earlier choices are settled. Leave out what does not change the plan.
|
|
26
|
+
|
|
27
|
+
## 3. Ask one round
|
|
28
|
+
|
|
29
|
+
Ask the open choices in one message. Number them. For each, say what you recommend and why in one line, and the other options in a few words, so the user can answer "all as recommended" or "2: B". Do not ask more than you need. When nothing is open, skip this step.
|
|
30
|
+
|
|
31
|
+
If the answers open new choices, ask one more short round. Stop asking once the plan is clear.
|
|
32
|
+
|
|
33
|
+
## 4. Write the goal
|
|
34
|
+
|
|
35
|
+
Call group_goal start with:
|
|
36
|
+
|
|
37
|
+
- objective: the one outcome, in the user's terms;
|
|
38
|
+
- acceptance: how anyone can check it is done — commands or tests to run, files or results to look at, what evidence to show;
|
|
39
|
+
- constraints: the rules every member keeps, including what the user ruled out;
|
|
40
|
+
- stages in order: each small enough to check on its own, with its own acceptance and its tasks, each task for one member by name. Give review work to a member who did not do the work.
|
|
41
|
+
|
|
42
|
+
Then hand out the tasks of stage 1 with @Name in the same reply.
|
|
43
|
+
|
|
44
|
+
## While the goal runs
|
|
45
|
+
|
|
46
|
+
When the user corrects the direction, write it into the goal with group_goal revise before you go on, and tell the group what changed.
|
|
47
|
+
`;
|
|
48
|
+
// The frontmatter is a small block of `key: value` lines between `---` fences; only
|
|
49
|
+
export function parseSkill(raw, path) {
|
|
50
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw);
|
|
51
|
+
const metadata = frontmatter?.[1]
|
|
52
|
+
? Object.fromEntries(frontmatter[1]
|
|
53
|
+
.split("\n")
|
|
54
|
+
.map((line) => /^(\w[\w-]*):\s*(.+)$/.exec(line.trim())?.slice(1) ?? [])
|
|
55
|
+
.filter((parts) => parts.length === 2))
|
|
56
|
+
: {};
|
|
57
|
+
if (typeof metadata.description !== "string" || metadata.description.length === 0)
|
|
58
|
+
throw new Error(`${PROVIDER}: ${path} has no description`);
|
|
59
|
+
return { description: metadata.description, content: raw.slice(frontmatter?.[0].length ?? 0).trim() };
|
|
60
|
+
}
|
|
61
|
+
function candidate() {
|
|
62
|
+
const { description } = parseSkill(SKILL_MARKDOWN, LOCATOR);
|
|
63
|
+
return {
|
|
64
|
+
name: SKILL,
|
|
65
|
+
description,
|
|
66
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
67
|
+
provider: PROVIDER,
|
|
68
|
+
source: "bundled",
|
|
69
|
+
rank: 550,
|
|
70
|
+
resourceBase: { kind: "directory", path: DIRECTORY },
|
|
71
|
+
locator: LOCATOR,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function goalProvider() {
|
|
75
|
+
const all = [candidate()];
|
|
76
|
+
return {
|
|
77
|
+
name: PROVIDER,
|
|
78
|
+
list: () => Promise.resolve(all),
|
|
79
|
+
async get(item) {
|
|
80
|
+
const { description, content } = parseSkill(SKILL_MARKDOWN, item.locator);
|
|
81
|
+
return { ...item, description, content };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function install(rt) {
|
|
86
|
+
// The DSH plugin context exposes `inject` for optional services; without a skills
|
|
87
|
+
// service there is nothing to register, and the plugin still runs the same.
|
|
88
|
+
rt.ctx.inject?.({ skills: null }, (ctx) => {
|
|
89
|
+
ctx.skills?.registerProvider(() => goalProvider());
|
|
90
|
+
});
|
|
91
|
+
}
|