@a-t-h-i/bot-lobby 0.6.5 → 0.6.7

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.
Files changed (44) hide show
  1. package/README.md +127 -6
  2. package/package.json +1 -1
  3. package/prompts/global.md +18 -0
  4. package/prompts/master.md +77 -2
  5. package/prompts/worker.md +7 -2
  6. package/src/ask/dialog.ts +15 -3
  7. package/src/ask/state.ts +10 -1
  8. package/src/ask/tool.ts +6 -1
  9. package/src/ask/view.ts +2 -1
  10. package/src/execution/agent-runner.ts +23 -1
  11. package/src/execution/fallback.ts +75 -0
  12. package/src/index.ts +1 -1
  13. package/src/lobby/feed.ts +8 -1
  14. package/src/lobby/keys.ts +0 -1
  15. package/src/lobby/layout.ts +22 -1
  16. package/src/lobby/mini.ts +159 -0
  17. package/src/lobby/planner.ts +10 -3
  18. package/src/lobby/quickfix.ts +17 -3
  19. package/src/lobby/runtime.ts +29 -26
  20. package/src/lobby/tabs/home.ts +6 -42
  21. package/src/lobby/tabs/tasks.ts +1 -1
  22. package/src/lobby/view.ts +31 -30
  23. package/src/master/master.ts +1 -1
  24. package/src/master/research.ts +1 -0
  25. package/src/pi/activity.ts +1 -33
  26. package/src/pi/events.ts +3 -16
  27. package/src/pi/master-fallback.ts +61 -0
  28. package/src/pi/model-support.ts +18 -3
  29. package/src/pi/plan-checklist.ts +305 -0
  30. package/src/pi/run-summary.ts +12 -3
  31. package/src/pi/settings-ui.ts +65 -12
  32. package/src/pi/tools.ts +5 -4
  33. package/src/pi/ui.ts +18 -284
  34. package/src/roles/worker.ts +1 -1
  35. package/src/schemas/configuration.ts +51 -15
  36. package/src/schemas/findings.ts +2 -0
  37. package/src/workflow/brief.ts +59 -0
  38. package/src/workflow/workflow.ts +15 -2
  39. package/src/pi/expressions.ts +0 -169
  40. package/src/pi/kaomoji.ts +0 -227
  41. package/src/pi/mascot-art.ts +0 -359
  42. package/src/pi/zen-large.ts +0 -699
  43. package/src/pi/zen-metrics.ts +0 -130
  44. package/src/pi/zen.ts +0 -659
@@ -49,6 +49,7 @@ import { assessReconnaissance, completionBlockers, decideReviewLoop, recordDecis
49
49
  import { detectSharedFiles, summarizeOutcomes } from "../master/synthesis.ts";
50
50
  import { tail, truncate } from "../text.ts";
51
51
  import { isAutoMode } from "../state/auto.ts";
52
+ import { briefRejection } from "./brief.ts";
52
53
  import { assertNoPendingApprovals, pendingApprovals, requestApproval, resolveApproval } from "./approvals.ts";
53
54
  import { pingApproval } from "../pi/notify.ts";
54
55
  import { describeRun, runLogEntry } from "../pi/run-summary.ts";
@@ -295,7 +296,7 @@ async function handleClarify(task: Task, params: OrchestrateParams, deps: Workfl
295
296
  }
296
297
  const answer = params.options?.length ? await deps.choose(question, params.options) : await deps.ask(question);
297
298
  if (answer === undefined) {
298
- return `No answer captured. Ask the user this in your reply, then continue.\n\nQuestion: ${question}`;
299
+ return `The user did not answer. Do not assume an answer or pick the recommended option: ask the user this again in your reply, end your turn, and continue only once they have answered.\n\nQuestion: ${question}`;
299
300
  }
300
301
  return `User answered: ${answer}`;
301
302
  }
@@ -680,6 +681,10 @@ function workerRequest(deps: WorkflowDeps, task: Task, domain: Domain, instructi
680
681
  };
681
682
  }
682
683
 
684
+ const ANSWER_NOW = "Answer the questions now";
685
+ /** Times an agent's abandoned questions are put to the user again before it decides. */
686
+ const MAX_NUDGES = 5;
687
+
683
688
  /** Agents that may ask the user themselves: the designer, whose choices are the user's to see. */
684
689
  const ASKING_DOMAINS: ReadonlySet<Domain> = new Set(["designer"]);
685
690
 
@@ -705,7 +710,13 @@ function askRelay(task: Task, deps: WorkflowDeps, domain: Domain): { onAsk: Rela
705
710
  if (isAutoMode(deps.root, deps.configDir, task.id)) {
706
711
  return { answers: [], cancelled: true, globalNote: "Auto mode is on, so nobody can answer: decide with the options you recommend and say in your report what you chose and why." };
707
712
  }
708
- const result = await askQuestions(questions, label, signal);
713
+ let result = await askQuestions(questions, label, signal);
714
+ // Leaving the questions must not quietly hand the choice to the agent: ask whether it may decide.
715
+ for (let nudges = 0; result.answers.length === 0 && result.cancelled && !signal.aborted && nudges < MAX_NUDGES; nudges++) {
716
+ const choice = await deps.choose(`${label} is waiting for your answer${questions.length === 1 ? "" : "s"} (${questions.map((question) => question.header).join(", ")}).`, [ANSWER_NOW, `Let ${label} decide with its recommendation`]);
717
+ if (choice !== ANSWER_NOW && choice !== undefined) break;
718
+ result = await askQuestions(questions, label, signal);
719
+ }
709
720
  recordDecision(task, askedDecision(label, questions, result), domain);
710
721
  return result;
711
722
  });
@@ -877,6 +888,8 @@ async function handleImplement(task: Task, params: OrchestrateParams, deps: Work
877
888
  }
878
889
  if (!starting) requireState(task, ["planning", "implementing", "reviewing"]);
879
890
  const assignments = parseAssignments(params);
891
+ const short = !deps.config.workflow.briefCheck ? [] : assignments.map((entry) => briefRejection(task.id, entry.domain, entry.instruction)).filter(Boolean);
892
+ if (short.length > 0) throw new Error(short.join("\n"));
880
893
  for (const { domain } of assignments) assertNoPendingApprovals(task, domain);
881
894
  for (const { domain } of assignments) if (!task.domains.includes(domain)) task.domains.push(domain);
882
895
  // Under a time budget every step is given its share before any starts; a spent budget starts none.
@@ -1,169 +0,0 @@
1
- /**
2
- * Caller-driven expression schedule for the zen sprites.
3
- *
4
- * Pure and deterministic: `now` and the random source are always injected, so
5
- * the art, layout and metrics stay free of `Date.now`/`Math.random`. Each slot
6
- * and the oracle keeps its own state, holds its rest frame between events and,
7
- * after a random 20-30 s gap, plays a short blink or a longer emote.
8
- */
9
-
10
- /** Resting gap before the next expression, milliseconds. */
11
- export const BLINK_MIN_MS = 20_000;
12
- export const BLINK_MAX_MS = 30_000;
13
- /** How long one blink and one emote are held. */
14
- export const BLINK_MS = 500;
15
- export const EMOTE_MS = 2000;
16
- /** Fast clock while an expression plays, short enough that a blink is never skipped. */
17
- export const FAST_TICK_MS = 120;
18
-
19
- /** Art frame indexes: 0 rests, 1 blinks, `2 ..` are emotes. */
20
- export const REST_FRAME = 0;
21
- export const BLINK_FRAME = 1;
22
- export const EMOTE_FRAME = 2;
23
- /**
24
- * Emote frame range: `EMOTE_FRAME .. EMOTE_FRAME + EMOTE_FRAMES - 1`; an emote
25
- * steps through them: the open face, a blink, then its action (see kaomoji.ts).
26
- */
27
- export const EMOTE_FRAMES = 4;
28
- /** Each emote frame is held this long, so an emote steps through its frames. */
29
- export const EMOTE_STEP_MS = Math.floor(EMOTE_MS / EMOTE_FRAMES);
30
-
31
- const BLINK_CHANCE = 0.65;
32
- /** Working agents emote as often as they blink. */
33
- export const WORKING_BLINK_CHANCE = 0.5;
34
- /** Upper bound (exclusive) of an expression's variant. */
35
- const VARIANT_SPAN = 2 ** 30;
36
-
37
- export interface ExpressionState {
38
- /** Earliest `now` the next expression may start. */
39
- nextAt: number;
40
- /** When the playing expression ends; not after `now` while resting. */
41
- until: number;
42
- /** When the current expression started; drives emote frame stepping. */
43
- startedAt: number;
44
- /** Frame index for the art: 0 rest, 1 blink, 2+ emote. */
45
- frame: number;
46
- /** Drawn once per expression; picks which face an emote shows (see kaomoji.ts). */
47
- variant: number;
48
- }
49
-
50
- /** A random draw clamped to `[0, 1]`, so a non-finite source value can never leak. */
51
- function unit(rng: () => number): number {
52
- const value = rng();
53
- return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
54
- }
55
-
56
- /** Resting window between expressions, milliseconds. */
57
- export interface ExpressionGap {
58
- min: number;
59
- max: number;
60
- }
61
-
62
- /** Idle agent slots rest 20-30 s between expressions. */
63
- export const SLOT_GAP: ExpressionGap = { min: BLINK_MIN_MS, max: BLINK_MAX_MS };
64
- /** Working agents are livelier: 8-15 s between expressions. */
65
- export const WORKING_GAP: ExpressionGap = { min: 8_000, max: 15_000 };
66
- /** The oracle is the scene's centrepiece: it blinks and emotes every 6-12 s. */
67
- export const ORACLE_GAP: ExpressionGap = { min: 6_000, max: 12_000 };
68
-
69
- /** Random resting gap before the next expression, inside `gap` (the slot window by default). */
70
- export function nextGap(rng: () => number, gap: ExpressionGap = SLOT_GAP): number {
71
- return gap.min + Math.round(unit(rng) * (gap.max - gap.min));
72
- }
73
-
74
- /** Whether the next expression blinks (~65% by default) or emotes. */
75
- export function pickEvent(rng: () => number, blinkChance = BLINK_CHANCE): "blink" | "emote" {
76
- return unit(rng) < blinkChance ? "blink" : "emote";
77
- }
78
-
79
- function drawVariant(rng: () => number): number {
80
- return Math.floor(unit(rng) * (VARIANT_SPAN - 1));
81
- }
82
-
83
- /** A freshly rested expression that fires for the first time after one random gap. */
84
- export function createExpression(now: number, rng: () => number, gap: ExpressionGap = SLOT_GAP): ExpressionState {
85
- const start = Number.isFinite(now) ? now : 0;
86
- return { nextAt: start + nextGap(rng, gap), until: start, startedAt: start, frame: REST_FRAME, variant: 0 };
87
- }
88
-
89
- /** True while `now` is inside a playing blink or emote. */
90
- export function isPlaying(state: ExpressionState, now: number): boolean {
91
- return Number.isFinite(now) && now < state.until;
92
- }
93
-
94
- /** True when any state is mid-expression; the caller uses it to retime its clock. */
95
- export function anyPlaying(states: readonly ExpressionState[], now: number): boolean {
96
- return states.some((state) => isPlaying(state, now));
97
- }
98
-
99
- /**
100
- * Advance one sprite to `now`: start a blink or emote once the gap has elapsed,
101
- * drop back to the rest frame when it ends, and change nothing in between.
102
- */
103
- export function advanceExpression(
104
- state: ExpressionState,
105
- now: number,
106
- rng: () => number,
107
- gap: ExpressionGap = SLOT_GAP,
108
- blinkChance = BLINK_CHANCE,
109
- ): ExpressionState {
110
- if (!Number.isFinite(now)) return state;
111
- if (now < state.until) return state.frame >= EMOTE_FRAME ? steppedEmote(state, now) : state;
112
- if (now < state.nextAt) return state.frame === REST_FRAME ? state : { ...state, frame: REST_FRAME };
113
- return play(now, rng, gap, blinkChance);
114
- }
115
-
116
- function play(now: number, rng: () => number, gap: ExpressionGap, blinkChance: number): ExpressionState {
117
- const blink = pickEvent(rng, blinkChance) === "blink";
118
- return blink ? startBlink(now, rng, gap) : triggerEmote(now, rng, gap);
119
- }
120
-
121
- function startBlink(now: number, rng: () => number, gap: ExpressionGap): ExpressionState {
122
- const until = now + BLINK_MS;
123
- return { nextAt: until + nextGap(rng, gap), until, startedAt: now, frame: BLINK_FRAME, variant: drawVariant(rng) };
124
- }
125
-
126
- /**
127
- * Start an emote right now, whatever the sprite was doing: the reaction when a
128
- * slot's situation changes (it starts, finishes, fails, stalls, waits or
129
- * receives a file). The next scheduled expression follows one gap later.
130
- */
131
- export function triggerEmote(now: number, rng: () => number, gap: ExpressionGap = SLOT_GAP): ExpressionState {
132
- const start = Number.isFinite(now) ? now : 0;
133
- const until = start + EMOTE_MS;
134
- return { nextAt: until + nextGap(rng, gap), until, startedAt: start, frame: EMOTE_FRAME, variant: drawVariant(rng) };
135
- }
136
-
137
- /** One sub-step of a playing expression; the oracle's blink and glance step through these. */
138
- export const PHASE_MS = FAST_TICK_MS;
139
-
140
- /** Sub-steps elapsed in the playing expression, 0 while resting or on a bad clock. */
141
- export function expressionPhase(state: ExpressionState, now: number): number {
142
- if (!isPlaying(state, now)) return 0;
143
- const elapsed = now - state.startedAt;
144
- return Number.isFinite(elapsed) && elapsed > 0 ? Math.floor(elapsed / PHASE_MS) : 0;
145
- }
146
-
147
- /** How long the oracle lip-syncs after it says something new, and one mouth shape's hold. */
148
- export const TALK_MS = 1800;
149
- export const TALK_STEP_MS = FAST_TICK_MS;
150
-
151
- /** Mouth-shape index while the oracle is still talking about what it said at `since`, else undefined. */
152
- export function talkFrame(since: number | undefined, now: number): number | undefined {
153
- if (since === undefined || !Number.isFinite(since) || !Number.isFinite(now)) return undefined;
154
- const elapsed = now - since;
155
- return elapsed >= 0 && elapsed < TALK_MS ? Math.floor(elapsed / TALK_STEP_MS) : undefined;
156
- }
157
-
158
- /** Emote frame for `now`: one step per EMOTE_STEP_MS, clamped to the last frame. */
159
- function emoteFrameAt(startedAt: number, now: number): number {
160
- const elapsed = now - startedAt;
161
- const step = Number.isFinite(elapsed) && elapsed > 0 ? Math.floor(elapsed / EMOTE_STEP_MS) : 0;
162
- return EMOTE_FRAME + Math.min(EMOTE_FRAMES - 1, Math.max(0, step));
163
- }
164
-
165
- /** Advance an in-flight emote's frame, keeping the same object when it has not changed. */
166
- function steppedEmote(state: ExpressionState, now: number): ExpressionState {
167
- const frame = emoteFrameAt(state.startedAt, now);
168
- return frame === state.frame ? state : { ...state, frame };
169
- }
package/src/pi/kaomoji.ts DELETED
@@ -1,227 +0,0 @@
1
- /**
2
- * The agents' kaomoji: a pure catalogue of emotes and the rules that pick one.
3
- *
4
- * Every emote is a tiny animation: the open face, the same face blinking (same
5
- * width, so nothing shifts), and an optional action frame (a table flip, a
6
- * sparkle, a bow). Each agent has its own personality set for every emotion,
7
- * and a shared pool of emotions is open to all of them:
8
- *
9
- * - DEV, the hacker: shades, flexing, coffee, and table flips on failure
10
- * - DESIGN, the artist: sparkles, flowers, hearts and little flourishes
11
- * - RESEARCH, the bookworm: note-taking, pointing, shrugging, deal-with-it glasses
12
- * - QA, the inspector: side-eyes and squints, then a flex and a victory dance
13
- *
14
- * Nothing here reads a clock or a random source: the caller passes a `variant`
15
- * drawn once per expression, so the same variant always picks the same face.
16
- * Every face fits a slot cell and avoids combining marks, right-to-left
17
- * scripts and emoji-presentation glyphs (enforced by tests).
18
- */
19
- import type { SlotId, SlotState } from "./mascot-art.ts";
20
-
21
- export const EMOTIONS = [
22
- "happy",
23
- "proud",
24
- "love",
25
- "excited",
26
- "focused",
27
- "curious",
28
- "thinking",
29
- "nervous",
30
- "confused",
31
- "sleepy",
32
- "sad",
33
- "angry",
34
- "waiting",
35
- "surprised",
36
- "grateful",
37
- ] as const;
38
- export type Emotion = (typeof EMOTIONS)[number];
39
-
40
- /** One emote: the open face, the same face blinking, and an optional action frame. */
41
- export interface Emote {
42
- open: string;
43
- blink: string;
44
- action?: string;
45
- }
46
-
47
- const e = (open: string, blink: string, action?: string): Emote => (action ? { open, blink, action } : { open, blink });
48
-
49
- /** Emotions every agent can draw on. */
50
- export const SHARED_EMOTES: Record<Emotion, readonly Emote[]> = {
51
- happy: [e("(^▽^)", "(-▽-)"), e("(^ω^)", "(-ω-)"), e("ヽ(•‿•)ノ", "ヽ(-‿-)ノ"), e("(*^‿^*)", "(*-‿-*)"), e("(◕‿◕)", "(-‿-)")],
52
- proud: [e("( ̄^ ̄)ゞ", "(-^-)ゞ"), e("(´▽`)b", "(-▽-)b"), e("(  ̄ー ̄)", "( -ー-)"), e("(•ᴗ•)ง", "(-ᴗ-)ง")],
53
- love: [e("(♡‿♡)", "(-‿-)", "(♡‿♡)♡"), e("(っ˘з(˘⌣˘ )", "(っ-з(-⌣- )"), e("(´ε` )♡", "(-ε- )♡"), e("(◍•ᴗ•◍)♡", "(◍-ᴗ-◍)♡")],
54
- excited: [e("\\(^o^)/", "\\(-o-)/"), e("ヽ(^▽^)ノ", "ヽ(-▽-)ノ"), e("(ノ◕ヮ◕)ノ", "(ノ-ヮ-)ノ", "(ノ◕ヮ◕)ノ*:・゚✧"), e("o(≧▽≦)o", "o(-▽-)o")],
55
- focused: [e("(•_•)", "(-_-)"), e("(ง •_•)ง", "(ง -_-)ง"), e("(`・ω・´)", "(`-ω-´)"), e("( ._.)φ", "( -_-)φ")],
56
- curious: [e("(・・ )?", "(-- )?"), e("(⊙_⊙)?", "(-_-)?"), e("(◔_◔)", "(-_-)"), e("(゚∀゚)?", "(-∀-)?")],
57
- thinking: [e("( ̄~ ̄;)", "(-~-;)"), e("( ˘_˘ )", "( -_- )"), e("(・・;)ゞ", "(--;)ゞ"), e("(¬‿¬ )", "(-‿- )")],
58
- nervous: [e("(;・∀・)", "(;-∀-)"), e("(⊙﹏⊙)", "(-﹏-)"), e("(●´⌓`●)", "(●-⌓-●)"), e("(°□°;)", "(-□-;)")],
59
- confused: [e("(・_・ヾ", "(-_-ヾ"), e("(⊙_☉)", "(-_-)"), e("(゜-゜)", "(---)"), e("(・・?)", "(--?)")],
60
- sleepy: [e("(-_-) zzZ", "(-.-) zzZ"), e("( ̄o ̄) zzZZ", "(-o-) zzZZ"), e("(∪。∪)。zz", "(-。-)。zz"), e("( ˘ω˘ )スヤァ", "( -ω- )スヤァ")],
61
- sad: [e("(ಥ_ಥ)", "(-_-)"), e("(╥﹏╥)", "(-﹏-)"), e("(´;ω;`)", "(-;ω;-)"), e("(〒﹏〒)", "(-﹏-)"), e("(T_T)", "(-_-)")],
62
- angry: [e("(╬ಠ益ಠ)", "(╬-益-)"), e("(ノಠ益ಠ)ノ", "(ノ-益-)ノ", "(ノಠ益ಠ)ノ彡┻━┻"), e("ヽ(`Д´)ノ", "ヽ(-Д-)ノ"), e("(¬_¬\")", "(-_-\")")],
63
- waiting: [e("(´・_・`)", "(´-_-`)"), e("( ˘・_・˘)", "( ˘-_-˘)"), e("(っ´ω`c)", "(っ-ω-c)"), e("(・_・)…", "(-_-)…")],
64
- surprised: [e("(°o°)", "(-o-)"), e("Σ(°△°|||)", "Σ(-△-|||)"), e("(゚Д゚)", "(-Д-)"), e("w(°o°)w", "w(-o-)w")],
65
- grateful: [e("(人´∀`)", "(人-∀-)"), e("(◍•ᴗ•◍)", "(◍-ᴗ-◍)"), e("m(_ _)m", "m(- -)m"), e("(ᵔᴥᵔ)", "(-ᴥ-)"), e("(*´∀`*)", "(*-∀-*)")],
66
- };
67
-
68
- /** DEV, the hacker: shades, flexes, coffee and code brackets; flips tables when things break. */
69
- const DEV: Record<Emotion, readonly Emote[]> = {
70
- happy: [e("(⌐■_■)", "(⌐-_-)"), e("(^_^)b", "(-_-)b"), e("</>(^▽^)", "</>(-▽-)"), e("( •_•)c[_]", "( -_-)c[_]")],
71
- proud: [e("(⌐■_■)b", "(⌐-_-)b"), e("ᕦ(ò_óˇ)ᕤ", "ᕦ(-_-ˇ)ᕤ"), e("( •_•)>⌐■-■", "( -_-)>⌐■-■", "(⌐■_■)"), e("b( ̄▽ ̄)d", "b(-▽-)d")],
72
- love: [e("(♡_♡)</>", "(-_-)</>"), e("(´∀`)♡{ }", "(-∀-)♡{ }"), e("(˘▽˘)c[_]", "(-▽-)c[_]"), e("(◍•_•◍)♡", "(◍-_-◍)♡")],
73
- excited: [e("ᕦ(ò_ó)ᕤ", "ᕦ(-_-)ᕤ"), e("(ノ≧∀≦)ノ", "(ノ-∀-)ノ", "(ノ≧∀≦)ノ</>"), e("\\(•_•)/", "\\(-_-)/"), e("(⌐■▽■)", "(⌐-▽-)")],
74
- focused: [e("(•_•)>_", "(-_-)>_"), e("[•_•]$_", "[-_-]$_"), e("(ง'-')ง", "(ง- -)ง"), e("(⌐■_■)>_", "(⌐-_-)>_")],
75
- curious: [e("(•_•)?</>", "(-_-)?</>"), e("(・_・)ノ{ }", "(-_-)ノ{ }"), e("(°_°)>_", "(-_-)>_"), e("( ⊙_⊙)</>", "( -_-)</>")],
76
- thinking: [e("( ̄_ ̄)c[_]", "(-_-)c[_]"), e("( •_•)…{ }", "( -_-)…{ }"), e("(¬_¬)>_", "(-_-)>_"), e("(°~°)</>", "(-~-)</>")],
77
- nervous: [e("(°_°;)>_", "(-_-;)>_"), e("(・_・;)$_", "(-_-;)$_"), e("(⊙_⊙;)", "(-_-;)"), e("(゚_゚;)c[_]", "(-_-;)c[_]")],
78
- confused: [e("(・_・)?{ }", "(-_-)?{ }"), e("(°_°)?>_", "(-_-)?>_"), e("(⊙_☉)</>", "(-_-)</>"), e("(゜_゜)$?", "(-_-)$?")],
79
- sleepy: [e("(-_-)c[_] z", "(-.-)c[_] z"), e("( ̄o ̄)>_ zz", "(-o-)>_ zz"), e("(∪_∪)</> z", "(-_-)</> z"), e("( ˘_˘ )$_ z", "( -_- )$_ z")],
80
- sad: [e("(╥_╥)>_", "(-_-)>_"), e("(T_T)</>", "(-_-)</>"), e("(ಥ_ಥ)c[_]", "(-_-)c[_]"), e("(´;_;`)$_", "(-;_;-)$_")],
81
- angry: [e("(╯°□°)╯", "(╯-□-)╯", "(╯°□°)╯︵ ┻━┻"), e("(ノಠ益ಠ)ノ", "(ノ-益-)ノ", "(ノಠ益ಠ)ノ彡┻━┻"), e("(╬ò_ó)>_", "(╬-_-)>_"), e("┻━┻ ︵ (°□°)", "┻━┻ ︵ (-□-)")],
82
- waiting: [e("(•_•)c[_]", "(-_-)c[_]"), e("(´・_・`)>_", "(´-_-`)>_"), e("( ˘_˘)</>…", "( -_-)</>…"), e("(・_・)$_…", "(-_-)$_…")],
83
- surprised: [e("(°□°)>_", "(-□-)>_"), e("Σ(°_°)</>", "Σ(-_-)</>"), e("(゚Д゚)$_", "(-Д-)$_"), e("(⊙_⊙)!{ }", "(-_-)!{ }")],
84
- grateful: [e("(⌐■_■)b", "(⌐-_-)b"), e("m(_ _)m</>", "m(- -)m</>"), e("(^_^)c[_]", "(-_-)c[_]"), e("(人•_•)", "(人-_-)")],
85
- };
86
-
87
- /** DESIGN, the artist: sparkles, flowers, hearts and a little flourish on most things. */
88
- const DESIGN: Record<Emotion, readonly Emote[]> = {
89
- happy: [e("✧(◕‿◕✿)", "✧(-‿-✿)"), e("(✿^‿^)", "(✿-‿-)"), e("(◕ᴗ◕✿)", "(-ᴗ-✿)"), e("❀(^▽^)❀", "❀(-▽-)❀")],
90
- proud: [e("(✿◠‿◠)", "(✿-‿-)"), e("✧(˘▽˘)✧", "✧(-▽-)✧"), e("( ̄▽ ̄)✿", "(-▽-)✿"), e("(◕‿◕)b✧", "(-‿-)b✧")],
91
- love: [e("(♡‿♡)✿", "(-‿-)✿"), e("(っ˘з˘)♡✧", "(っ-з-)♡✧"), e("(◍♡‿♡◍)", "(◍-‿-◍)"), e("✿(´ε`)♡", "✿(-ε-)♡")],
92
- excited: [e("(ノ◕ヮ◕)ノ", "(ノ-ヮ-)ノ", "(ノ◕ヮ◕)ノ*:・゚✧"), e("✧*(^▽^)*✧", "✧*(-▽-)*✧"), e("☆(≧▽≦)☆", "☆(-▽-)☆"), e("♪(^∇^)♪", "♪(-∇-)♪")],
93
- focused: [e("(•‿•)~✎", "(-‿-)~✎"), e("( ˘_˘)~✿", "( -_-)~✿"), e("(◕_◕)✎", "(-_-)✎"), e("(・ω・)~♪", "(-ω-)~♪")],
94
- curious: [e("(◕_◕)?✧", "(-_-)?✧"), e("(・・ )✿?", "(-- )✿?"), e("(°‿°)?", "(-‿-)?"), e("✧(◔_◔)", "✧(-_-)")],
95
- thinking: [e("( ˘‿˘)~✎", "( -‿-)~✎"), e("( ̄~ ̄)✿", "(-~-)✿"), e("(◕~◕)…", "(-~-)…"), e("(・・)~♪", "(--)~♪")],
96
- nervous: [e("(◕﹏◕✿)", "(-﹏-✿)"), e("(°‿°;)✎", "(-‿-;)✎"), e("(・_・;)✿", "(-_-;)✿"), e("(⊙‿⊙;)", "(-‿-;)")],
97
- confused: [e("(◕_◕)?~", "(-_-)?~"), e("(・・?)✿", "(--?)✿"), e("(゜‿゜)?", "(-‿-)?"), e("(°_°)✎?", "(-_-)✎?")],
98
- sleepy: [e("(-‿-)✿ zz", "(-.-)✿ zz"), e("( ˘ω˘ )✿z", "( -ω- )✿z"), e("(∪‿∪)♪ z", "(-‿-)♪ z"), e("( ̄‿ ̄)zzZ", "(-‿-)zzZ")],
99
- sad: [e("(╥﹏╥)✿", "(-﹏-)✿"), e("(ಥ‿ಥ)", "(-‿-)"), e("(´;ω;`)✎", "(-;ω;-)✎"), e("(◕︵◕)", "(-︵-)")],
100
- angry: [e("(╬◣_◢)", "(╬-_-)"), e("(ノ`Д´)ノ✎", "(ノ-Д-)ノ✎"), e("(¬_¬)✿", "(-_-)✿"), e("(ಠ‿ಠ)✎", "(-‿-)✎")],
101
- waiting: [e("(っ´ω`c)✿", "(っ-ω-c)✿"), e("( ˘‿˘)~♪", "( -‿-)~♪"), e("(・‿・)…✿", "(-‿-)…✿"), e("(´・‿・`)", "(´-‿-`)")],
102
- surprised: [e("(°o°)✿", "(-o-)✿"), e("✧(⊙‿⊙)✧", "✧(-‿-)✧"), e("(゚∀゚)!✎", "(-∀-)!✎"), e("Σ(◕o◕)", "Σ(-o-)")],
103
- grateful: [e("(人◕‿◕)", "(人-‿-)"), e("(◍•ᴗ•◍)✿", "(◍-ᴗ-◍)✿"), e("✿m(_ _)m", "✿m(- -)m"), e("(*´▽`*)✧", "(*-▽-*)✧")],
104
- };
105
-
106
- /** RESEARCH, the bookworm: takes notes, points at things, shrugs at the unknown. */
107
- const RESEARCH: Record<Emotion, readonly Emote[]> = {
108
- happy: [e("φ(^▽^)", "φ(-▽-)"), e("(^_^)[ ]", "(-_-)[ ]"), e("(•‿•)φ", "(-‿-)φ"), e("( ˘▽˘)ノ", "( -▽-)ノ")],
109
- proud: [e("( •_•)>⌐■-■", "( -_-)>⌐■-■", "(⌐■_■)"), e("( ̄^ ̄)φ", "(-^-)φ"), e("(•ᴗ•)b[ ]", "(-ᴗ-)b[ ]"), e("(  ̄ー ̄)φ", "( -ー-)φ")],
110
- love: [e("(♡_♡)[ ]", "(-_-)[ ]"), e("φ(´∀`)♡", "φ(-∀-)♡"), e("(◍•_•◍)φ", "(◍-_-◍)φ"), e("(˘ε˘)♡[ ]", "(-ε-)♡[ ]")],
111
- excited: [e("(σ・・)σ!", "(σ--)σ!"), e("φ(≧▽≦)", "φ(-▽-)"), e("\\(°o°)/", "\\(-o-)/"), e("(ノ°▽°)ノ[ ]", "(ノ-▽-)ノ[ ]")],
112
- focused: [e("φ(..)", "φ(--)", "φ(..;)"), e("( ._.)φ__", "( -_-)φ__"), e("(・_・)[ ]", "(-_-)[ ]"), e("( •_•)φ", "( -_-)φ")],
113
- curious: [e("(σ・・)σ", "(σ--)σ"), e("(・・ )?[ ]", "(-- )?[ ]"), e("(⊙_⊙)φ?", "(-_-)φ?"), e("(°_°)…?", "(-_-)…?")],
114
- thinking: [e("φ( ̄~ ̄)", "φ(-~-)"), e("( ˘_˘ )φ", "( -_- )φ"), e("(・・;)[ ]", "(--;)[ ]"), e("(¬_¬)φ…", "(-_-)φ…")],
115
- nervous: [e("φ(°_°;)", "φ(-_-;)"), e("(・_・;)[?]", "(-_-;)[?]"), e("(⊙﹏⊙)φ", "(-﹏-)φ"), e("(°□°;)[ ]", "(-□-;)[ ]")],
116
- confused: [e("¯\\_(ツ)_/¯", "¯\\_(ー)_/¯"), e("(・_・ヾ[?]", "(-_-ヾ[?]"), e("(⊙_☉)φ", "(-_-)φ"), e("(゜-゜)?[ ]", "(---)?[ ]")],
117
- sleepy: [e("(-_-)[ ] zz", "(-.-)[ ] zz"), e("φ( ̄o ̄) z", "φ(-o-) z"), e("(∪_∪)φ zz", "(-_-)φ zz"), e("( ˘ω˘ )[ ]z", "( -ω- )[ ]z")],
118
- sad: [e("(ಥ_ಥ)φ", "(-_-)φ"), e("(╥_╥)[ ]", "(-_-)[ ]"), e("(´;ω;`)φ", "(-;ω;-)φ"), e("(T_T)[?]", "(-_-)[?]")],
119
- angry: [e("(ಠ_ಠ)φ", "(-_-)φ"), e("(╬ಠ_ಠ)[ ]", "(╬-_-)[ ]"), e("ヽ(`Д´)ノφ", "ヽ(-Д-)ノφ"), e("(¬_¬\")[ ]", "(-_-\")[ ]")],
120
- waiting: [e("(´・_・`)φ", "(´-_-`)φ"), e("( ˘・_・˘)[ ]", "( ˘-_-˘)[ ]"), e("φ(・_・)…", "φ(-_-)…"), e("(っ´ω`c)φ", "(っ-ω-c)φ")],
121
- surprised: [e("Σ(°o°)φ", "Σ(-o-)φ"), e("(゚Д゚)[!]", "(-Д-)[!]"), e("(⊙_⊙)!φ", "(-_-)!φ"), e("w(°o°)w[ ]", "w(-o-)w[ ]")],
122
- grateful: [e("(人´∀`)φ", "(人-∀-)φ"), e("m(_ _)m[ ]", "m(- -)m[ ]"), e("(ᵔᴥᵔ)φ", "(-ᴥ-)φ"), e("(◍•ᴗ•◍)[ ]", "(◍-ᴗ-◍)[ ]")],
123
- };
124
-
125
- /** QA, the inspector: side-eyes and squints at everything, then flexes and dances on a pass. */
126
- const QA: Record<Emotion, readonly Emote[]> = {
127
- happy: [e("(•‿•)✓", "(-‿-)✓"), e("( ^_^)[✓]", "( -_-)[✓]"), e("(ᵔ.ᵔ)✓", "(-.-)✓"), e("(^_^)✓", "(-_-)✓")],
128
- proud: [e("ᕙ( • ‿ • )ᕗ", "ᕙ( - ‿ - )ᕗ"), e("ᕦ(•_•)ᕤ✓", "ᕦ(-_-)ᕤ✓"), e("( ̄^ ̄)✓", "(-^-)✓"), e("( •_•)b✓", "( -_-)b✓")],
129
- love: [e("(♡_♡)✓", "(-_-)✓"), e("(´∀`)♡[✓]", "(-∀-)♡[✓]"), e("(◍•_•◍)✓", "(◍-_-◍)✓"), e("(˘ε˘)♡✓", "(-ε-)♡✓")],
130
- excited: [e("ᕕ( ᐛ )ᕗ", "ᕕ( - )ᕗ"), e("\\(•_•)/✓", "\\(-_-)/✓"), e("(ノ≧▽≦)ノ✓", "(ノ-▽-)ノ✓"), e("ᕙ(⇀‸↼)ᕗ", "ᕙ(-‸-)ᕗ")],
131
- focused: [e("(ಠ_ಠ)", "(−_−)"), e("(¬_¬)", "(-_-)"), e("(눈_눈)", "(-_-)"), e("(•_•)⊙", "(-_-)⊙")],
132
- curious: [e("(ಠ_ಠ)?", "(−_−)?"), e("(¬_¬)?[ ]", "(-_-)?[ ]"), e("(⊙_⊙)⊙", "(-_-)⊙"), e("(・_・)?✓", "(-_-)?✓")],
133
- thinking: [e("(¬‿¬)[ ]", "(-‿-)[ ]"), e("(ಠ~ಠ)", "(−~−)"), e("( ̄_ ̄)[?]", "(-_-)[?]"), e("( ˘_˘ )✓?", "( -_- )✓?")],
134
- nervous: [e("(ಠ_ಠ;)", "(−_−;)"), e("(°_°;)[ ]", "(-_-;)[ ]"), e("(⊙﹏⊙)✓?", "(-﹏-)✓?"), e("(・_・;)✗?", "(-_-;)✗?")],
135
- confused: [e("(ಠ_ಠ)?!", "(−_−)?!"), e("(・_・ヾ[?]", "(-_-ヾ[?]"), e("(⊙_☉)✗", "(-_-)✗"), e("(゜-゜)[✗]", "(---)[✗]")],
136
- sleepy: [e("(-_-)[ ] zz", "(-.-)[ ] zz"), e("( ̄o ̄)✓ zz", "(-o-)✓ zz"), e("(∪_∪)⊙ z", "(-_-)⊙ z"), e("( ˘ω˘ )✓z", "( -ω- )✓z")],
137
- sad: [e("(ಥ_ಥ)✗", "(-_-)✗"), e("(╥﹏╥)[✗]", "(-﹏-)[✗]"), e("(´;ω;`)✗", "(-;ω;-)✗"), e("(T_T)✗", "(-_-)✗")],
138
- angry: [e("(ಠ益ಠ)", "(−益−)"), e("(╬ಠ_ಠ)✗", "(╬−_−)✗"), e("(ノಠ益ಠ)ノ", "(ノ−益−)ノ", "(ノಠ益ಠ)ノ[✗]"), e("(¬_¬\")✗", "(-_-\")✗")],
139
- waiting: [e("(¬_¬)…", "(-_-)…"), e("(´・_・`)[ ]", "(´-_-`)[ ]"), e("(ಠ_ಠ)…", "(−_−)…"), e("( ˘_˘)✓…", "( -_-)✓…")],
140
- surprised: [e("(ಠ_ಠ)!", "(−_−)!"), e("Σ(°_°)✗", "Σ(-_-)✗"), e("(⊙_⊙)[!]", "(-_-)[!]"), e("(゚Д゚)✓!", "(-Д-)✓!")],
141
- grateful: [e("(人•_•)✓", "(人-_-)✓"), e("m(_ _)m✓", "m(- -)m✓"), e("(ᵔ.ᵔ)b", "(-.-)b"), e("(◍•ᴗ•◍)✓", "(◍-ᴗ-◍)✓")],
142
- };
143
-
144
- /** Personality sets per agent column. */
145
- export const PERSONALITY_EMOTES: Record<SlotId, Record<Emotion, readonly Emote[]>> = {
146
- dev: DEV,
147
- design: DESIGN,
148
- research: RESEARCH,
149
- qa: QA,
150
- };
151
-
152
- /** Frames per emote: open, blink, then the action (or the open face again), held one step each. */
153
- export const EMOTE_STEPS = 4;
154
-
155
- /** The four frames an emote steps through: open, blink, action, action. */
156
- export function emoteFrames(emote: Emote): readonly string[] {
157
- const finish = emote.action ?? emote.open;
158
- return [emote.open, emote.blink, finish, finish];
159
- }
160
-
161
- /** What a slot is going through, as far as its face is concerned. */
162
- export interface Situation {
163
- status: SlotState;
164
- /** Live one-word activity while working (reading, editing, running, thinking, ...). */
165
- activity?: string;
166
- flag?: "waiting" | "quiet" | "retry";
167
- /** The run was asked to wrap up early. */
168
- wrappedUp?: boolean;
169
- /** A file was just handed to this agent. */
170
- handover?: boolean;
171
- }
172
-
173
- const ACTIVITY_EMOTIONS: Record<string, readonly Emotion[]> = {
174
- reading: ["curious", "focused"],
175
- searching: ["curious", "thinking"],
176
- editing: ["focused", "excited", "proud"],
177
- running: ["nervous", "focused"],
178
- thinking: ["thinking", "curious"],
179
- writing: ["focused", "proud"],
180
- researching: ["curious", "thinking"],
181
- };
182
-
183
- /** The emotions that fit a situation, most fitting first. */
184
- export function emotionsFor(situation: Situation): readonly Emotion[] {
185
- switch (situation.status) {
186
- case "idle":
187
- return ["sleepy", "waiting", "happy"];
188
- case "done":
189
- return situation.wrappedUp ? ["nervous", "proud"] : ["happy", "proud", "love", "excited"];
190
- case "failed":
191
- return ["sad", "angry", "confused", "surprised"];
192
- default:
193
- break;
194
- }
195
- if (situation.flag === "waiting") return ["waiting"];
196
- if (situation.flag === "quiet") return ["confused", "sleepy"];
197
- if (situation.flag === "retry") return ["nervous", "sad"];
198
- if (situation.handover) return ["grateful", "happy"];
199
- if (situation.wrappedUp) return ["nervous", "surprised"];
200
- return ACTIVITY_EMOTIONS[situation.activity ?? ""] ?? ["focused", "excited", "happy"];
201
- }
202
-
203
- /** Share of picks drawn from the agent's own personality set (out of 10). */
204
- const PERSONALITY_SHARE = 6;
205
-
206
- function positive(variant: number): number {
207
- return Number.isFinite(variant) ? Math.abs(Math.floor(variant)) : 0;
208
- }
209
-
210
- /**
211
- * The emote for one expression: the emotion from the situation, then a face
212
- * from the agent's personality (about 60%) or the shared pool, all chosen by
213
- * the expression's `variant`.
214
- */
215
- export function pickEmote(slot: SlotId, situation: Situation, variant: number): Emote {
216
- const seed = positive(variant);
217
- const emotions = emotionsFor(situation);
218
- const emotion = emotions[seed % emotions.length]!;
219
- const own = (Math.floor(seed / 7) % 10) < PERSONALITY_SHARE;
220
- const pool = own ? PERSONALITY_EMOTES[slot][emotion] : SHARED_EMOTES[emotion];
221
- return pool[Math.floor(seed / 71) % pool.length]!;
222
- }
223
-
224
- /** The frames of the emote a slot plays for this expression. */
225
- export function slotEmote(slot: SlotId, situation: Situation, variant: number): readonly string[] {
226
- return emoteFrames(pickEmote(slot, situation, variant));
227
- }