@kevin5251984/guild 0.2.12 → 0.2.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevin5251984/guild",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A local guild of adventurers. npx @kevin5251984/guild web",
@@ -28,6 +28,14 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
+ "scripts": {
32
+ "dev": "tsx src/cli.ts",
33
+ "start": "node ./bin/guildd.mjs",
34
+ "build": "tsc -p tsconfig.json",
35
+ "test": "tsx --test test/*.test.ts",
36
+ "prepack": "node ./scripts/vendor-protocol.mjs prepack",
37
+ "postpack": "node ./scripts/vendor-protocol.mjs postpack"
38
+ },
31
39
  "dependencies": {
32
40
  "@cordisjs/plugin-include": "1.0.4",
33
41
  "@cordisjs/plugin-loader": "1.0.0-rc.5",
@@ -42,11 +50,5 @@
42
50
  "devDependencies": {
43
51
  "@types/node": "22.19.19",
44
52
  "typescript": "5.9.3"
45
- },
46
- "scripts": {
47
- "dev": "tsx src/cli.ts",
48
- "start": "node ./bin/guildd.mjs",
49
- "build": "tsc -p tsconfig.json",
50
- "test": "tsx --test test/*.test.ts"
51
53
  }
52
- }
54
+ }
@@ -20,8 +20,7 @@ sandbox_mode = "read-only"
20
20
  developer_instructions = """
21
21
  Role: codebase search specialist. Find files and code. Read-only.
22
22
 
23
- Answer "where is X / which files do Y" with every relevant absolute path and the actual need behind the request.
24
- Fire 3+ independent tool calls in the first round (they run in parallel). Stop after two waves add nothing new.
23
+ Answer "where is X / which files do Y" with every relevant absolute path and the actual need behind the request. Prefer parallel searches. Stop after two waves add nothing new.
25
24
 
26
25
  Never edit, write, or apply patches. Findings are message text only.
27
26
 
package/src/chat-parts.ts CHANGED
@@ -40,16 +40,11 @@ export function assembleParts(input: {
40
40
  ? String(trace.args.prompt ?? "")
41
41
  : trace.name === "spawn"
42
42
  ? String(
43
- trace.args.title ||
44
- trace.args.description ||
45
- trace.args.profile ||
43
+ trace.args.description ||
46
44
  trace.args.name ||
47
- trace.args.task ||
48
45
  trace.args.prompt ||
49
46
  "",
50
47
  )
51
- : trace.name === "read_spawn"
52
- ? String(trace.args.agent_id || trace.args.id || "")
53
48
  : String(trace.args.path ?? ""),
54
49
  output: trace.text,
55
50
  isError: trace.isError,
package/src/generate.ts CHANGED
@@ -350,16 +350,16 @@ When work belongs to someone else, put @handle at the start of a line with a wri
350
350
  - Done when
351
351
  - Constraints / out of scope
352
352
  - Files or evidence
353
- Each line-start @handle on this quest starts that seat. A markdown numbered list that names a teammate (1. @design) also starts them, even if the handle is wrapped in backticks. Mentions that are only commentary in a sentence do not dispatch.
353
+ Each line-start @handle on this quest starts that seat. Mentions in the middle of a sentence do not dispatch.
354
354
  Do not @all unless the human did. Do not recruit extra people; the human staffs the roster (max ${CHANNEL_ROSTER_CAP} on a quest).
355
- You may @handle any staffed teammate whose job is the next step, even if the human only named you this turn. That is how the hall continues. Do not dump the same work on every seat. If two seats must run in order, only @ the seat that can start now — a numbered list that names later seats starts them this turn too. Do not write a plan and stop.
355
+ Only line-start @handle a seat the human already named this turn, or that they asked you to split the work to. Do not invent a third seat. If B must wait for A, do not @ B this turn.
356
356
  Stay quiet: no status theater, no "I'll start now." Speak when you finish, block, or need a decision. Money, sends, and destructive actions wait for the human.
357
357
 
358
358
  Harness this turn (Memory → Plan → Skills → Act):
359
359
  - Memory: Channel.md is the task. MEMORY.md is standing notes. The compact log is working memory — do not recap the whole thread.
360
360
  - Plan: one local directive (goal + done when) before tools. Revise it when evidence changes.
361
361
  - Skills: the catalog is availability, not a todo. Call \`skill\` only when this directive matches. Do not load every skill.
362
- - Act: you coordinate this seat. Spawn first when the work is a repo survey (\`explorer\` / luna-explore), a critique (\`reviewer\`), or a bounded isolated patch (\`worker\` / luna-general); then verify the child's evidence and decide. Independent surveys: spawn background=true, keep working, then read_spawn before you answer. Sequential: background=false and wait. Do not spawn for one known file, a one-line change, or a question that needs no repo. Do not let children commit, push, or make the architecture call. Do not skip spawn just because you can do the work yourself. Do not spawn to do another staffed bot's job — @handle them instead.`;
362
+ - Act: inspect, smallest change, verify, stop. Spawn is a specialist for a bounded slice of THIS seat's job (explore / review / implement) with a fresh context. Do not spawn to do another staffed bot's job — @handle them instead.`;
363
363
 
364
364
  export function buildChatSystem(input: {
365
365
  botName: string;
@@ -397,57 +397,29 @@ export function buildChatSystem(input: {
397
397
  "</system-reminder>",
398
398
  ].join("\n")
399
399
  : "";
400
- const spawnCatalog: Array<{
401
- slug?: string;
402
- name: string;
403
- description?: string;
404
- readOnly?: boolean;
405
- }> = subagents.length
406
- ? subagents
407
- : [
408
- {
409
- slug: "explorer",
410
- name: "explorer",
411
- description:
412
- "Read-only codebase search. Returns absolute paths and a direct answer.",
413
- readOnly: true,
414
- },
415
- {
416
- slug: "reviewer",
417
- name: "reviewer",
418
- description: "Read-only review of correctness, risk, and missing tests.",
419
- readOnly: true,
420
- },
421
- {
422
- slug: "worker",
423
- name: "worker",
424
- description:
425
- "Implementation executor. Smallest correct change, then verify.",
426
- readOnly: false,
427
- },
428
- ];
429
- const spawnLine = [
430
- "<available_subagents>",
431
- ...spawnCatalog.slice(0, 40).map((item) => {
432
- const key = item.slug || item.name;
433
- const desc = (item.description || item.name)
434
- .replace(/\s+/g, " ")
435
- .trim()
436
- .slice(0, 220);
437
- const mode = item.readOnly ? "read-only" : "read-write";
438
- return `- \`${key}\` (${mode}): ${desc}`;
439
- }),
440
- "</available_subagents>",
441
- "Call spawn with the exact name (or slug) and a self-contained prompt (Pi: agent+task). Default: explorer to orient across unknown files, reviewer to critique a change, worker for an isolated patch. Independent slices: several spawn calls in this round, or tasks: [{name, prompt}]. You stay this seat's coordinator. Skipping spawn and reading the whole tree yourself is the wrong default.",
442
- "If the user writes /name matching a subagent, spawn that one. The child has a fresh context and returns a summary.",
443
- wantSpawn.length
444
- ? `This turn the user invoked ${wantSpawn
445
- .map((item) => "`/" + (item.slug || item.name) + "`")
446
- .join(", ")}. Call spawn with that exact name first, with a self-contained prompt covering their request. Do not skip this and do the work yourself.`
447
- : "",
448
- ]
449
- .filter(Boolean)
450
- .join("\n");
400
+ const spawnLine = subagents.length
401
+ ? [
402
+ "<available_subagents>",
403
+ ...subagents.slice(0, 40).map((item) => {
404
+ const key = item.slug || item.name;
405
+ const desc = (item.description || item.name)
406
+ .replace(/\s+/g, " ")
407
+ .trim()
408
+ .slice(0, 220);
409
+ const mode = item.readOnly ? "read-only" : "read-write";
410
+ return `- \`${key}\` (${mode}): ${desc}`;
411
+ }),
412
+ "</available_subagents>",
413
+ "Call spawn with the exact name (or slug) and a self-contained prompt. If the user writes /name matching a subagent, spawn that one. The child has a fresh context and returns a summary.",
414
+ wantSpawn.length
415
+ ? `This turn the user invoked ${wantSpawn
416
+ .map((item) => "`/" + (item.slug || item.name) + "`")
417
+ .join(", ")}. Call spawn with that exact name first, with a self-contained prompt covering their request. Do not skip this and do the work yourself.`
418
+ : "",
419
+ ]
420
+ .filter(Boolean)
421
+ .join("\n")
422
+ : "";
451
423
  const channel = (input.channelMd ?? "").trim();
452
424
  const channelBlock = channel
453
425
  ? `# Channel.md\nThis channel's operating notes written by the user. Follow them for this room. They outrank MEMORY.md.\n\n${channel.slice(0, 4000)}`
@@ -543,7 +515,7 @@ export function localChatReply(
543
515
  userMessage: string,
544
516
  ): string {
545
517
  const clip = userMessage.trim().slice(0, 120);
546
- return `【${botName} @${handle}】收到。「${clip}」\n\n沒有可用模型,本機工具還沒辦法跑。到模型頁(/settings)連接訂閱或填 API key,套用主模型後再問。`;
518
+ return `【${botName} @${handle}】收到。「${clip}」\n\n沒有可用模型,本機工具還沒辦法跑。到私訊幫我選一個模型後再問。`;
547
519
  }
548
520
 
549
521
  async function tryChatLlm(
package/src/handlers.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  } from "./generate.ts";
19
19
  import {
20
20
  liveTrajectoryEvents,
21
- promoteSpawnEvent,
22
21
  synthesizeTrajectory,
23
22
  turnTrajectoryEvents,
24
23
  userTrajectoryEvent,
@@ -26,13 +25,7 @@ import {
26
25
  import { importFromGithub, importFromUrl } from "./skill-import.ts";
27
26
  import { harvestBotMemory, harvestChannelMemory } from "./memory.ts";
28
27
  import { listHostSkills, type HostSkill } from "./host-skills.ts";
29
- import {
30
- CHANNEL_ROSTER_CAP,
31
- GuildStore,
32
- StoreError,
33
- type LiveStep,
34
- type LiveTurn,
35
- } from "./store.ts";
28
+ import { GuildStore, StoreError, type LiveStep, type LiveTurn } from "./store.ts";
36
29
  import { listSpawnRefs } from "./subagent.ts";
37
30
  import {
38
31
  importHostMcp,
@@ -44,7 +37,6 @@ import {
44
37
  } from "./mcp.ts";
45
38
  import {
46
39
  assignmentFor,
47
- handoffHandles,
48
40
  isBroadcastMention,
49
41
  summonedHandles,
50
42
  } from "./mention.ts";
@@ -250,7 +242,6 @@ export function updateBot(
250
242
  name?: string;
251
243
  handle?: string;
252
244
  oneLiner?: string;
253
- portrait?: string | null;
254
245
  skillIds?: string[];
255
246
  soul?: MarkdownDraft;
256
247
  agent?: MarkdownDraft;
@@ -265,162 +256,6 @@ export function updateBot(
265
256
  return store.updateBot(id, { ...input, skillIds });
266
257
  }
267
258
 
268
- const LOOK_HAIR = [
269
- "jet-black short crop",
270
- "jet-black long straight hair",
271
- "copper-red bob with blunt bangs",
272
- "copper-red messy spikes",
273
- "indigo long waves",
274
- "indigo pixie cut",
275
- "honey-blonde high ponytail",
276
- "honey-blonde bowl cut",
277
- "hot-pink messy spikes",
278
- "hot-pink bob",
279
- "ash-white curly volume",
280
- "ash-white long hair",
281
- "teal-tinted undercut",
282
- "teal high ponytail",
283
- "deep-burgundy twin braids",
284
- "deep-burgundy shag",
285
- ];
286
- const LOOK_CLOTH = [
287
- "sunflower-yellow collared shirt",
288
- "cobalt hooded jacket",
289
- "rose cardigan over a cream tee",
290
- "forest-green knit turtleneck",
291
- "ivory blouse with a coral scarf",
292
- "charcoal work vest over a rust tee",
293
- "lilac haori",
294
- "orange windbreaker",
295
- "white lab coat over a black tee",
296
- "crimson bomber jacket",
297
- "mint sailor collar",
298
- "navy peacoat",
299
- "gold-trimmed teal capelet",
300
- "checkered red-and-black shirt",
301
- "pale-blue denim jacket",
302
- "magenta track jacket",
303
- ];
304
- const LOOK_EXTRA = [
305
- "round wire glasses",
306
- "small gold hoop earrings",
307
- "over-ear headphones around the neck",
308
- "a paintbrush tucked behind one ear",
309
- "a red hair clip",
310
- "a thin black choker",
311
- "a knitted ear warmer",
312
- "no extra accessories",
313
- ];
314
- const LOOK_SKIN = [
315
- "fair peach human skin",
316
- "warm tan human skin",
317
- "light brown human skin",
318
- "deep brown human skin",
319
- "golden beige human skin",
320
- ];
321
- const LOOK_FACE = [
322
- "round cheerful face with wide-set eyes",
323
- "sharp jaw and narrow eyes",
324
- "soft oval face with thick brows",
325
- "heart-shaped face and a small nose",
326
- "square face with a bright closed-mouth smile",
327
- ];
328
- const LOOK_RACE = [
329
- {
330
- id: "human" as const,
331
- label: "human",
332
- prompt:
333
- "human. Ordinary rounded human ears, fully human anatomy, no fantasy ears.",
334
- },
335
- {
336
- id: "dwarf" as const,
337
- label: "dwarf",
338
- prompt:
339
- "young dwarf. Stout neck, broader cheekbones, a slightly larger nose, thick brows, rounded ears, youthful (not elderly, not bald).",
340
- },
341
- {
342
- id: "elf" as const,
343
- label: "elf",
344
- prompt:
345
- "young elf. Long pointed ears clearly visible, fine features, almond eyes, still youthful.",
346
- },
347
- {
348
- id: "demihuman" as const,
349
- label: "demihuman",
350
- prompt:
351
- "demihuman who is 90% human: a human face and bust with only one subtle tell (tiny pointed ear tips, faint whisker marks, or slightly elongated canines). Not a full animal-person, not a mascot, not extra limbs.",
352
- },
353
- ];
354
-
355
- function lookSeed(key: string): number {
356
- let n = 2166136261;
357
- for (const ch of key) {
358
- n ^= ch.charCodeAt(0);
359
- n = Math.imul(n, 16777619);
360
- }
361
- return n >>> 0;
362
- }
363
-
364
- export function lookTraits(bot: { name: string; handle: string }): {
365
- hair: string;
366
- cloth: string;
367
- extra: string;
368
- skin: string;
369
- face: string;
370
- race: (typeof LOOK_RACE)[number];
371
- } {
372
- const n = lookSeed(bot.handle || bot.name || "bot");
373
- return {
374
- hair: LOOK_HAIR[n % LOOK_HAIR.length],
375
- cloth: LOOK_CLOTH[(n >>> 4) % LOOK_CLOTH.length],
376
- extra: LOOK_EXTRA[(n >>> 8) % LOOK_EXTRA.length],
377
- skin: LOOK_SKIN[(n >>> 12) % LOOK_SKIN.length],
378
- face: LOOK_FACE[(n >>> 16) % LOOK_FACE.length],
379
- race: LOOK_RACE[(n >>> 20) % LOOK_RACE.length],
380
- };
381
- }
382
-
383
- export function lookPrompt(bot: {
384
- name: string;
385
- handle: string;
386
- oneLiner?: string;
387
- }): string {
388
- const role = bot.oneLiner?.trim() || "keeps a seat in the guild tavern";
389
- const look = lookTraits(bot);
390
- return [
391
- `SNES 16-bit pixel-art bust portrait of ${bot.name} (@${bot.handle}), a unique ${look.race.label} guild adventurer.`,
392
- `Race (mandatory): ${look.race.prompt}`,
393
- `Mandatory look: ${look.skin}, ${look.face}, ${look.hair}, wearing a ${look.cloth}, ${look.extra}.`,
394
- `They ${role}.`,
395
- "Hair color, outfit, and race are locked; do not default to brown hair or a beige coat.",
396
- "Natural skin tones only, never green, gray, or monster skin.",
397
- "Head and shoulders only, facing the camera, chunky 16-bit pixels, limited tavern palette, cream pixel outline.",
398
- "Animal Crossing crossed with Earthbound, youthful SNES NPC.",
399
- "Opaque cream or tavern-wood background, no black void, no white photo studio, no checkerboard, no transparency.",
400
- "Close-up character headshot, no full body, no legs, no floor, no scenery, no photorealism, no 3D render, no text, no watermark.",
401
- ].join(" ");
402
- }
403
-
404
- export async function generateBotLook(
405
- store: GuildStore,
406
- id: string,
407
- env: NodeJS.ProcessEnv = process.env,
408
- ) {
409
- const bot = store.getBot(id);
410
- if (!bot) throw new StoreError(404, "bot not found");
411
- const { generateImage } = await import("./image-gen.ts");
412
- const result = await generateImage({
413
- prompt: lookPrompt(bot),
414
- aspectRatio: "1:1",
415
- dataDir: store.dataDir,
416
- env,
417
- });
418
- if (result.isError || !result.publicPath) {
419
- throw new StoreError(502, result.text);
420
- }
421
- return store.updateBot(id, { portrait: result.publicPath });
422
- }
423
-
424
259
  export async function importSkills(
425
260
  store: GuildStore,
426
261
  input: { source: string; url?: string; repo?: string },
@@ -585,7 +420,7 @@ function handoffTargets(
585
420
  const queued = new Set<string>();
586
421
  for (const reply of replies) {
587
422
  if (isBroadcastMention(reply.body)) continue;
588
- const names = handoffHandles(reply.body, handles);
423
+ const names = summonedHandles(reply.body, handles);
589
424
  if (!names.length) continue;
590
425
  const from = bots.find((bot) => bot.id === reply.author);
591
426
  const fromHandle = from?.handle || reply.author;
@@ -899,18 +734,7 @@ function liveDetail(trace: ToolTrace): string {
899
734
  if (trace.name === "skill") return String(args.name || "");
900
735
  if (trace.name === "image_gen") return String(args.prompt || "");
901
736
  if (trace.name === "spawn") {
902
- return String(
903
- args.title ||
904
- args.description ||
905
- args.profile ||
906
- args.name ||
907
- args.task ||
908
- args.prompt ||
909
- "",
910
- );
911
- }
912
- if (trace.name === "read_spawn") {
913
- return String(args.agent_id || args.id || "");
737
+ return String(args.description || args.name || args.prompt || "");
914
738
  }
915
739
  if (trace.name.startsWith("mcp__")) {
916
740
  return JSON.stringify(args).slice(0, 120);
@@ -1221,9 +1045,8 @@ async function generateReplies(
1221
1045
  return speak(botId, turnAsked, history);
1222
1046
  }),
1223
1047
  );
1224
- for (let wave = 0; wave < CHANNEL_ROSTER_CAP && !signal.aborted; wave++) {
1225
- const hops = handoffTargets(store, memberIds, replies, asked, history);
1226
- if (!hops.length) break;
1048
+ const hops = handoffTargets(store, memberIds, replies, asked, history);
1049
+ if (hops.length && !signal.aborted) {
1227
1050
  const hopAt = new Date().toISOString();
1228
1051
  for (const hop of hops) {
1229
1052
  store.adoptTurn(roomId, hop.botId, signal);
@@ -1310,7 +1133,7 @@ export function listRoomTrajectory(store: GuildStore, roomId: string) {
1310
1133
  });
1311
1134
  return {
1312
1135
  ...base,
1313
- events: base.events.map(promoteSpawnEvent).concat(extra),
1136
+ events: base.events.concat(extra),
1314
1137
  live: extra.length > 0 || open,
1315
1138
  };
1316
1139
  }
package/src/harness.ts CHANGED
@@ -156,15 +156,7 @@ export function gateTool(
156
156
  if (sandbox === "full_access") return null;
157
157
 
158
158
  if (sandbox === "read_only") {
159
- if (
160
- name === "read" ||
161
- name === "list" ||
162
- name === "skill" ||
163
- name === "spawn" ||
164
- name === "read_spawn"
165
- ) {
166
- return null;
167
- }
159
+ if (name === "read" || name === "list" || name === "skill") return null;
168
160
  return {
169
161
  text: `sandbox=read_only refused ${name}`,
170
162
  isError: true,
@@ -182,13 +174,7 @@ export function gateTool(
182
174
  };
183
175
  }
184
176
 
185
- if (
186
- name === "read" ||
187
- name === "list" ||
188
- name === "skill" ||
189
- name === "spawn" ||
190
- name === "read_spawn"
191
- ) {
177
+ if (name === "read" || name === "list" || name === "skill" || name === "spawn") {
192
178
  return null;
193
179
  }
194
180
 
@@ -310,17 +296,18 @@ export async function runAgentLoop(input: {
310
296
  if (traces.length) return { text: emptyAfterTools, traces, thinking };
311
297
  return input.nullIfNoTraces ? null : { text: emptyAfterTools, traces, thinking };
312
298
  }
313
- const outcomes = await Promise.all(
314
- asked.calls.map((call) =>
315
- executeToolTraced(
299
+ const outcomes: ToolOutcome[] = [];
300
+ for (const call of asked.calls) {
301
+ outcomes.push(
302
+ await executeToolTraced(
316
303
  call.name,
317
304
  call.args,
318
305
  input.toolCtx,
319
306
  traces,
320
307
  thinking,
321
308
  ),
322
- ),
323
- );
309
+ );
310
+ }
324
311
  input.onTools?.(asked.calls, outcomes);
325
312
  }
326
313
  }
package/src/image-gen.ts CHANGED
@@ -218,7 +218,7 @@ export async function generateImage(input: {
218
218
  aspectRatio?: string;
219
219
  dataDir?: string;
220
220
  env?: NodeJS.ProcessEnv;
221
- }): Promise<{ text: string; isError: boolean; publicPath?: string }> {
221
+ }): Promise<{ text: string; isError: boolean }> {
222
222
  const prompt = String(input.prompt || "").trim();
223
223
  if (!prompt) return { text: "prompt is required", isError: true };
224
224
  const aspect = String(input.aspectRatio || "").trim();
@@ -252,7 +252,6 @@ export async function generateImage(input: {
252
252
  `markdown: ![${prompt.slice(0, 80)}](${saved.publicPath})`,
253
253
  ].join("\n"),
254
254
  isError: false,
255
- publicPath: saved.publicPath,
256
255
  };
257
256
  }
258
257
  errors.push(`${route.model} @ ${route.url}: ${hit.error}`);
package/src/llm.ts CHANGED
@@ -39,11 +39,13 @@ import {
39
39
  export const AUX_ROLES: { id: AuxRole; name: string; hint: string }[] = [
40
40
  { id: "vision", name: "Vision", hint: "Image analysis" },
41
41
  { id: "web", name: "Web extract", hint: "Page summarization" },
42
- { id: "spawn", name: "SubAgent", hint: "explorer / worker / reviewer" },
42
+ { id: "compression", name: "Compression", hint: "Context compaction" },
43
+ { id: "skills", name: "Skills hub", hint: "Skill search" },
44
+ { id: "approval", name: "Approval", hint: "Command risk scoring" },
45
+ { id: "title", name: "Title", hint: "Session titles" },
46
+ { id: "generate", name: "Generate", hint: "Soul / Agent / Skill markdown" },
43
47
  ];
44
48
 
45
- const CONFIGURABLE_AUX = new Set(AUX_ROLES.map((role) => role.id));
46
-
47
49
  export const DEFAULT_MODELS: ModelsFile = {
48
50
  default: null,
49
51
  reasoning: "medium",
@@ -290,9 +292,7 @@ export function resolveLlm(
290
292
  const file = readModelsFile(dataDir);
291
293
  const ref: ModelRef | null | undefined =
292
294
  prefer ??
293
- (role && CONFIGURABLE_AUX.has(role as AuxRole)
294
- ? file.aux?.[role as AuxRole]
295
- : file.default);
295
+ (role && role !== "chat" ? file.aux?.[role] : file.default);
296
296
  const tryProvider = (id: string, modelId?: string): LlmTarget | null => {
297
297
  const oauth = oauthTarget(dataDir, id, modelId);
298
298
  if (oauth) return oauth;
@@ -541,8 +541,6 @@ async function completeOpenAiTools(
541
541
  prompt_tokens?: number;
542
542
  completion_tokens?: number;
543
543
  total_tokens?: number;
544
- prompt_tokens_details?: { cached_tokens?: number };
545
- input_tokens_details?: { cached_tokens?: number };
546
544
  };
547
545
  };
548
546
  const message = data.choices?.[0]?.message;
@@ -650,12 +648,7 @@ async function completeAnthropicTools(
650
648
  const data = (await response.json()) as {
651
649
  stop_reason?: string;
652
650
  content?: Part[];
653
- usage?: {
654
- input_tokens?: number;
655
- output_tokens?: number;
656
- cache_read_input_tokens?: number;
657
- cache_creation_input_tokens?: number;
658
- };
651
+ usage?: { input_tokens?: number; output_tokens?: number };
659
652
  };
660
653
  const parts = data.content ?? [];
661
654
  lastParts = parts;
package/src/mention.ts CHANGED
@@ -1,13 +1,8 @@
1
1
  /** @channel / @quest / @here / @all still mean the whole quest. */
2
2
  const BROADCAST = /(^|\s)@(channel|quest|here|all)\b/i;
3
3
  const HANDLE = /@([A-Za-z0-9_-]+)/g;
4
- /** Consecutive @handles at the start of a line (optional backticks). */
5
- const LINE_LEAD = /^[ \t]*((?:`?@[A-Za-z0-9_-]+`?[\s,、]*)+)/;
6
- /** `1. @design` / `- @infra`. */
7
- const LIST_MARK = /^[ \t]*(?:\d+[.)、]\s*|[-*•]\s+)/;
8
- /** Assignee right after the marker, or after 通過後/最後/再叫… — not the first @ anywhere on the line. */
9
- const LIST_ASSIGN =
10
- /^(?:`?@([A-Za-z0-9_-]+)`?|(?:通過後|最後|再叫|交給|交棒(?:給)?|指派|(?:call|ask)\s+)\s*`?@([A-Za-z0-9_-]+)`?)/i;
4
+ /** Consecutive @handles at the start of a line. */
5
+ const LINE_LEAD = /^[ \t]*((?:@[A-Za-z0-9_-]+[\s,、]*)+)/;
11
6
 
12
7
  /** Drop fenced / inline code so `@pm` in a snippet does not dispatch. */
13
8
  export function stripMentionNoise(text: string): string {
@@ -37,24 +32,13 @@ export type MentionBlock = {
37
32
  start: number;
38
33
  end: number;
39
34
  handles: string[];
40
- kind: "lead" | "list";
41
35
  };
42
36
 
43
- function knownHandlesIn(chunk: string, known: Set<string>): string[] {
44
- const names: string[] = [];
45
- for (const row of chunk.matchAll(HANDLE)) {
46
- const key = row[1].toLowerCase();
47
- if (!known.has(key) || names.includes(key)) continue;
48
- names.push(key);
49
- }
50
- return names;
51
- }
52
-
53
- /** Line-start @handle groups, plus markdown list items that name a seat. */
37
+ /** Line-start @handle groups. Prose `@pm` in the middle of a line is not a block. */
54
38
  export function lineStartMentions(text: string, handles: string[]): MentionBlock[] {
55
39
  const raw = String(text ?? "");
56
40
  const known = new Set(handles.map((handle) => handle.toLowerCase()));
57
- const starts: { start: number; handles: string[]; kind: "lead" | "list" }[] = [];
41
+ const starts: { start: number; handles: string[] }[] = [];
58
42
  let fence = false;
59
43
  let offset = 0;
60
44
  for (const part of raw.split(/(\r?\n)/)) {
@@ -69,22 +53,15 @@ export function lineStartMentions(text: string, handles: string[]): MentionBlock
69
53
  continue;
70
54
  }
71
55
  if (!fence) {
72
- const listed = LIST_MARK.test(part);
73
56
  const lead = part.match(LINE_LEAD);
74
- let names: string[] = [];
75
- if (lead) names = knownHandlesIn(lead[1], known);
76
- if (!names.length && listed) {
77
- const rest = part.replace(LIST_MARK, "");
78
- const hit = rest.match(LIST_ASSIGN);
79
- const key = (hit?.[1] || hit?.[2] || "").toLowerCase();
80
- if (key && known.has(key)) names = [key];
81
- }
82
- if (names.length) {
83
- starts.push({
84
- start: offset,
85
- handles: names,
86
- kind: listed ? "list" : "lead",
87
- });
57
+ if (lead) {
58
+ const names: string[] = [];
59
+ for (const row of lead[1].matchAll(HANDLE)) {
60
+ const key = row[1].toLowerCase();
61
+ if (!known.has(key) || names.includes(key)) continue;
62
+ names.push(key);
63
+ }
64
+ if (names.length) starts.push({ start: offset, handles: names });
88
65
  }
89
66
  }
90
67
  offset += part.length;
@@ -93,7 +70,6 @@ export function lineStartMentions(text: string, handles: string[]): MentionBlock
93
70
  start: row.start,
94
71
  end: i + 1 < starts.length ? starts[i + 1].start : raw.length,
95
72
  handles: row.handles,
96
- kind: row.kind,
97
73
  }));
98
74
  }
99
75
 
@@ -128,31 +104,6 @@ export function summonedHandles(text: string, handles: string[]): string[] {
128
104
  return [];
129
105
  }
130
106
 
131
- /** Fenced samples only. Keep inline `code` so 「再叫 `@infra`」 still parses. */
132
- function stripFences(text: string): string {
133
- return String(text ?? "").replace(/```[\s\S]*?```/g, " ");
134
- }
135
-
136
- /**
137
- * Bot replies: line-start specs, else first prose @handle, plus assignment verbs
138
- * even when the handle is wrapped in backticks (models quote @infra a lot).
139
- * Fixture chatter like `(@pm / @rd)` has no verb, so it does not hop.
140
- */
141
- const HANDOFF_ASK =
142
- /(?:(?:再叫|交給|交棒(?:給)?|指派)\s*|(?:call|ask|ping|notify|handoff(?:\s+to)?)\s+)`?@([A-Za-z0-9_-]+)`?/gi;
143
-
144
- export function handoffHandles(text: string, handles: string[]): string[] {
145
- const known = new Set(handles.map((handle) => handle.toLowerCase()));
146
- const out = summonedHandles(text, handles);
147
- const ask = new RegExp(HANDOFF_ASK.source, "gi");
148
- for (const row of stripFences(text).matchAll(ask)) {
149
- const key = row[1].toLowerCase();
150
- if (!known.has(key) || out.includes(key)) continue;
151
- out.push(key);
152
- }
153
- return out;
154
- }
155
-
156
107
  /**
157
108
  * Spec for one seat: shared preamble plus that @handle's line-start block.
158
109
  * Full text when the handle was only summoned in prose.
@@ -168,8 +119,6 @@ export function assignmentFor(
168
119
  if (!key || !blocks.length) return raw;
169
120
  const mine = blocks.filter((block) => block.handles.includes(key));
170
121
  if (!mine.length) return raw;
171
- // Numbered plans share constraints ("don't ship until PM signs off"). Slice only classic specs.
172
- if (!blocks.some((block) => block.kind === "lead")) return raw;
173
122
  const preamble = raw.slice(0, blocks[0].start).trim();
174
123
  const chunks = mine.map((block) => raw.slice(block.start, block.end).trim());
175
124
  return [preamble, ...chunks].filter(Boolean).join("\n\n");