@henryz2004/agency 1.0.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.
Files changed (41) hide show
  1. package/README.md +106 -0
  2. package/lib/codex.js +211 -0
  3. package/lib/control.js +168 -0
  4. package/lib/live.js +493 -0
  5. package/lib/opencode.js +447 -0
  6. package/lib/paths.js +12 -0
  7. package/lib/roster.js +204 -0
  8. package/lib/transcript.js +361 -0
  9. package/lib/usage.js +346 -0
  10. package/package.json +27 -0
  11. package/public/app.js +1021 -0
  12. package/public/audio-controls.js +165 -0
  13. package/public/avatar.js +467 -0
  14. package/public/characters/dev-auburn.json +32 -0
  15. package/public/characters/dev-auburn.png +0 -0
  16. package/public/characters/dev-beanie.json +32 -0
  17. package/public/characters/dev-beanie.png +0 -0
  18. package/public/characters/dev-glasses.json +32 -0
  19. package/public/characters/dev-glasses.png +0 -0
  20. package/public/chat-panel.css +514 -0
  21. package/public/chat-panel.js +815 -0
  22. package/public/index.html +190 -0
  23. package/public/lab.html +129 -0
  24. package/public/leaderboard.js +222 -0
  25. package/public/metric.js +34 -0
  26. package/public/mock-agents.js +70 -0
  27. package/public/mock.js +277 -0
  28. package/public/music/Console_Morning.mp3 +0 -0
  29. package/public/music/Midnight_Desk.mp3 +0 -0
  30. package/public/music/The_Plant_Beside_the_Door.mp3 +0 -0
  31. package/public/music/Three_AM_Window.mp3 +0 -0
  32. package/public/office.js +1484 -0
  33. package/public/sound.js +382 -0
  34. package/public/sprites.js +983 -0
  35. package/public/style.css +506 -0
  36. package/public/ui.js +50 -0
  37. package/scripts/_pixpng.mjs +104 -0
  38. package/scripts/animsheet.mjs +60 -0
  39. package/scripts/charsheet.mjs +61 -0
  40. package/scripts/install-hook.mjs +120 -0
  41. package/server.js +370 -0
package/public/mock.js ADDED
@@ -0,0 +1,277 @@
1
+ // mock.js — synthesize /api/state-shaped data so the presentation layer can be
2
+ // developed with no real agents running. Enable via URL: ?mock (default cast),
3
+ // ?mock=12 (N agents), ?mock=empty (empty floor). A tiny "director" re-rolls
4
+ // activity each poll so working / shell / idle / done + subagents all animate.
5
+ //
6
+ // ponytail: frontend-only fixture, never touches lib/ — keeps the plumbing/UI
7
+ // split intact. The cast is built once (stable identities so selection sticks);
8
+ // only activity/subagents/uptime change between polls.
9
+
10
+ const params = new URLSearchParams(location.search);
11
+ export const mockEnabled = params.has('mock');
12
+
13
+ const arg = params.get('mock');
14
+ const EMPTY = arg === 'empty';
15
+ const N = Number(arg) > 0 ? Math.min(Number(arg), 60) : 7;
16
+
17
+ // ---- identity helpers (roster.js runs server-side; reproduce just enough) ---
18
+
19
+ const FIRST = ['Ada', 'Ravi', 'Mona', 'Kenji', 'Lena', 'Otis', 'Priya', 'Theo', 'Yara', 'Cole', 'Nina', 'Dax', 'Ines', 'Bram', 'Suki', 'Ezra'];
20
+ const LAST = ['Vance', 'Okoro', 'Sato', 'Reyes', 'Novak', 'Kapoor', 'Holt', 'Ferro', 'Lund', 'Cruz', 'Bose', 'Pike', 'Calder', 'Ono', 'Frost', 'Wells'];
21
+ const SKINS = ['#f0c8a0', '#e8b890', '#d8a070', '#c08858', '#a87048', '#8a5a3a'];
22
+ const HAIRS = ['#2b2233', '#5a3a26', '#3a2e22', '#1f2a33', '#4a2233', '#26333a'];
23
+ const SHIRTS = ['#e05d5d', '#5d9ce0', '#5dc98a', '#e0b05d', '#a87de0', '#e07db0', '#5dc9c9', '#9bd45d'];
24
+ const TITLES = { opus: 'Principal Engineer', sonnet: 'Senior Engineer', haiku: 'Junior Engineer', unknown: 'Contractor' };
25
+
26
+ const PROJECTS = ['startup-agency', 'browser-harness', 'auth-service', 'data-pipeline', 'mobile-app', 'ml-infra', 'billing'];
27
+ const TASKS = [
28
+ 'refactor the auth flow', 'wire up the live endpoint', 'fix the flaky test suite',
29
+ 'add the payroll meter', 'migrate to the new schema', 'chase down a memory leak',
30
+ 'draft the release notes', 'tighten the camera clamps', 'parse codex sqlite state',
31
+ ];
32
+
33
+ // model + source per cast slot — covers every tier and every source/badge.
34
+ const CAST = [
35
+ { model: 'claude-opus-4-8[1m]', source: 'claude' },
36
+ { model: 'claude-sonnet-4-6', source: 'claude' },
37
+ { model: 'claude-haiku-4-5-20251001', source: 'claude' },
38
+ { model: 'gpt-5-codex', source: 'codex' },
39
+ { model: 'claude-sonnet-4-6', source: 'opencode' },
40
+ { model: 'claude-opus-4-8', source: 'claude' },
41
+ { model: 'claude-haiku-4-5-20251001', source: 'opencode' },
42
+ ];
43
+
44
+ const pick = (arr, i) => arr[i % arr.length];
45
+ const rint = (n) => Math.floor(Math.random() * n);
46
+
47
+ function tierFor(model) {
48
+ const m = (model || '').toLowerCase();
49
+ if (m.includes('opus') || m.includes('fable')) return 'opus';
50
+ if (m.includes('sonnet')) return 'sonnet';
51
+ if (m.includes('haiku')) return 'haiku';
52
+ return 'unknown';
53
+ }
54
+
55
+ // Build the stable cast once.
56
+ const now0 = Date.now();
57
+ const LEAD_SESSION = `mock-lead-${(now0 % 100000).toString(36)}`;
58
+
59
+ const cast = EMPTY
60
+ ? []
61
+ : Array.from({ length: N }, (_, i) => {
62
+ const base = CAST[i % CAST.length];
63
+ const tier = tierFor(base.model);
64
+ // slot 0 is the team lead (PM); it also carries a couple of foreground
65
+ // subagents so the "minions clustered under one worker" path stays visible.
66
+ const isLead = i === 0;
67
+ return {
68
+ pid: 4000 + i,
69
+ sessionId: isLead ? LEAD_SESSION : `mock-${i}-${(now0 % 100000).toString(36)}`,
70
+ source: base.source,
71
+ cwd: `/Users/you/code/${pick(PROJECTS, i)}`,
72
+ project: pick(PROJECTS, i),
73
+ kind: 'interactive',
74
+ model: base.model,
75
+ modelSlug: base.source === 'opencode' ? `${base.source}/${base.model}` : base.model,
76
+ provider: base.source === 'codex' ? 'openai' : 'anthropic',
77
+ name: `${pick(FIRST, i)} ${pick(LAST, i + 3)}`,
78
+ title: isLead ? 'Engineering Manager' : TITLES[tier],
79
+ tier,
80
+ skin: pick(SKINS, i + 1),
81
+ hair: pick(HAIRS, i + 2),
82
+ shirt: pick(SHIRTS, i),
83
+ chatName: pick(TASKS, i),
84
+ lastPrompt: pick(TASKS, i + 4),
85
+ task: null,
86
+ role: isLead ? 'lead' : null, // marks the orchestrator → distinct render
87
+ teamColor: null,
88
+ hiredAt: now0 - rint(40) * 86400e3,
89
+ // varied ages so uptime labels differ (minutes → hours)
90
+ startedAt: now0 - (60e3 + rint(5 * 3600e3)),
91
+ // director-controlled, seeded so the first frame is already varied:
92
+ activity: ['working', 'shell', 'idle'][i % 3],
93
+ state: i === 5 ? 'done' : null,
94
+ // slot 1 is paused on a Stop hook, awaiting a reply — previews the
95
+ // Control Phase-1 reply box + "needs you" HUD with no real hook (?mock).
96
+ awaitingReply: i === 1,
97
+ pendingSince: i === 1 ? now0 - 45e3 : undefined,
98
+ pendingQuestion: i === 1
99
+ ? 'I\'ve finished the refactor. Should I also update the tests, or leave them for a follow-up?'
100
+ : undefined,
101
+ // lead always shows 1-2 foreground minions; others occasionally do.
102
+ subagents: isLead
103
+ ? [{ type: 'general-purpose' }, { type: 'general-purpose' }]
104
+ : i % 4 === 0 ? [{ type: 'agent' }, { type: 'agent' }] : [],
105
+ };
106
+ });
107
+
108
+ // Two in-process teammates the lead launched with run_in_background:true. They
109
+ // have no pid (mirrors real teammates) and render as INDIVIDUAL workers, shirted
110
+ // by their team color. Mirrors lib/live.js buildTeammate() + the server's
111
+ // teammate identity merge (name = config label, title = subagent_type).
112
+ const TEAMMATE_DEFS = EMPTY
113
+ ? []
114
+ : [
115
+ { name: 'cc-internals', color: 'blue', model: 'claude-opus-4-8', task: 'scout Claude Code internals' },
116
+ { name: 'sprite-audit', color: 'green', model: 'claude-sonnet-4-6', task: 'audit the sprite sheet' },
117
+ ];
118
+ const teammates = TEAMMATE_DEFS.map((t, k) => ({
119
+ pid: null,
120
+ sessionId: `${t.name}@${LEAD_SESSION}`,
121
+ source: 'claude',
122
+ cwd: '/Users/you/code/startup-agency',
123
+ project: 'startup-agency',
124
+ kind: 'teammate',
125
+ model: t.model,
126
+ modelSlug: t.model,
127
+ provider: 'anthropic',
128
+ name: t.name, // config label, preserved over any roster name
129
+ title: 'general-purpose', // = subagent_type
130
+ tier: tierFor(t.model),
131
+ skin: pick(SKINS, k + 2),
132
+ hair: pick(HAIRS, k + 1),
133
+ shirt: pick(SHIRTS, k + 4), // overridden by teamColor at draw time
134
+ chatName: null,
135
+ lastPrompt: t.task,
136
+ task: null,
137
+ role: 'teammate',
138
+ teamColor: t.color,
139
+ teammateName: t.name,
140
+ teammateType: 'general-purpose',
141
+ hiredAt: now0 - rint(10) * 86400e3,
142
+ startedAt: now0 - (120e3 + rint(2 * 3600e3)),
143
+ activity: 'working',
144
+ state: null,
145
+ subagents: [],
146
+ }));
147
+ if (cast.length) cast.push(...teammates);
148
+
149
+ // ---- the director: mutate volatile fields each poll ------------------------
150
+
151
+ function direct(a) {
152
+ if (a.state === 'done') { a.activity = 'idle'; a.subagents = []; return; }
153
+ const r = Math.random();
154
+ a.activity = r < 0.5 ? 'working' : r < 0.75 ? 'shell' : 'idle';
155
+ // The lead keeps a steady clutch of foreground minions (it's orchestrating);
156
+ // teammates are individual workers and never cluster minions of their own.
157
+ if (a.role === 'lead') {
158
+ a.subagents = Array.from({ length: 1 + rint(2) }, () => ({ type: 'general-purpose' }));
159
+ return;
160
+ }
161
+ if (a.role === 'teammate') { a.subagents = []; return; }
162
+ a.subagents = a.activity === 'working' && Math.random() < 0.5
163
+ ? Array.from({ length: 1 + rint(3) }, () => ({ type: 'agent' }))
164
+ : [];
165
+ }
166
+
167
+ // ---- synthetic usage (built once; today's bar nudges up so it animates) ----
168
+
169
+ let usage = null;
170
+ function buildUsage() {
171
+ const daily = [];
172
+ for (let i = 29; i >= 0; i--) {
173
+ const d = new Date();
174
+ d.setDate(d.getDate() - i);
175
+ const out = 180000 + Math.round(90000 * Math.sin(i / 3.3)) + rint(140000);
176
+ daily.push({
177
+ date: d.toLocaleDateString('en-CA'),
178
+ out, in: Math.round(out * 0.55), cr: out * 5, cc: Math.round(out * 0.35),
179
+ tools: 40 + rint(160), agents: rint(9), msgs: 60 + rint(220),
180
+ });
181
+ }
182
+ const byModel = {
183
+ 'claude-opus-4-8[1m]': { out: 14_200_000, in: 9_100_000, msgs: 5400 },
184
+ 'claude-sonnet-4-6': { out: 6_800_000, in: 4_300_000, msgs: 7200 },
185
+ 'gpt-5-codex': { out: 3_100_000, in: 2_000_000, msgs: 2600 },
186
+ 'claude-haiku-4-5-20251001': { out: 1_200_000, in: 900_000, msgs: 3100 },
187
+ };
188
+ const byProject = {};
189
+ PROJECTS.forEach((p, i) => {
190
+ const out = 9_000_000 - i * 1_100_000 + rint(800000);
191
+ // First ~4 projects are "currently active" (recent output); the rest are
192
+ // stale all-time workspaces, so the departments panel's recency scope is
193
+ // visible in ?mock. lastTs is an ISO string to match the real adapter.
194
+ const recent = i < 4;
195
+ const lastTs = new Date(now0 - (recent ? i * 6 * 3600e3 : (9 + i * 3) * 86400e3)).toISOString();
196
+ byProject[p] = {
197
+ out,
198
+ recentOut: recent ? Math.round(out * 0.4) + rint(300000) : 0,
199
+ recentDays: recent ? 3 + rint(4) : 0,
200
+ msgs: 1200 - i * 120, tools: 4200 - i * 480,
201
+ agents: 90 - i * 10, sessions: 40 - i * 4,
202
+ lastTs,
203
+ };
204
+ });
205
+ const lifetime = {
206
+ out: 25_300_000, in: 16_300_000, cr: 410_000_000, cc: 8_900_000,
207
+ tools: 38_400, msgs: 18_300, agents: 612, sessions: 184,
208
+ };
209
+ return {
210
+ lifetime,
211
+ today: { date: daily[daily.length - 1].date, ...daily[daily.length - 1] },
212
+ daily,
213
+ byModel,
214
+ byProject,
215
+ firstDay: daily[0].date,
216
+ activeDays: 184,
217
+ recentWindowDays: 7,
218
+ };
219
+ }
220
+
221
+ // ---- public: produce a full /api/state-shaped object -----------------------
222
+
223
+ export function getMockState() {
224
+ const now = Date.now();
225
+ if (!usage) usage = buildUsage();
226
+ // nudge today's output up a touch each poll so the "today" bar + eng-days move
227
+ const last = usage.daily[usage.daily.length - 1];
228
+ last.out += 200 + rint(1500);
229
+ usage.today = { date: last.date, ...last };
230
+
231
+ cast.forEach(direct);
232
+ const agents = cast
233
+ .map((a) => ({ ...a, uptimeMs: a.startedAt ? Math.max(0, now - a.startedAt) : null }))
234
+ .sort((x, y) => (y.uptimeMs || 0) - (x.uptimeMs || 0));
235
+
236
+ // Enriched team record matching lib/live.js readTeams(): a lead member plus
237
+ // the in-process teammates, each with the per-member fields the frontend now
238
+ // reads (color/model/agentType/isLead).
239
+ const teams = cast.length
240
+ ? [
241
+ {
242
+ name: 'launch-squad',
243
+ createdAt: now0 - 3600e3,
244
+ leadAgentId: `team-lead@${LEAD_SESSION}`,
245
+ leadSessionId: LEAD_SESSION,
246
+ members: [
247
+ {
248
+ agentId: `team-lead@${LEAD_SESSION}`,
249
+ name: 'team-lead',
250
+ color: null,
251
+ model: cast[0].model,
252
+ agentType: 'team-lead',
253
+ prompt: null,
254
+ backendType: 'in-process',
255
+ cwd: cast[0].cwd,
256
+ joinedAt: now0 - 3600e3,
257
+ isLead: true,
258
+ },
259
+ ...TEAMMATE_DEFS.map((t) => ({
260
+ agentId: `${t.name}@${LEAD_SESSION}`,
261
+ name: t.name,
262
+ color: t.color,
263
+ model: t.model,
264
+ agentType: 'general-purpose',
265
+ prompt: t.task,
266
+ backendType: 'in-process',
267
+ cwd: '/Users/you/code/startup-agency',
268
+ joinedAt: now0 - 1800e3,
269
+ isLead: false,
270
+ })),
271
+ ],
272
+ },
273
+ ]
274
+ : [];
275
+
276
+ return { generatedAt: now, live: { agents, teams, now }, usage };
277
+ }
Binary file
Binary file
Binary file