@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.
- package/README.md +106 -0
- package/lib/codex.js +211 -0
- package/lib/control.js +168 -0
- package/lib/live.js +493 -0
- package/lib/opencode.js +447 -0
- package/lib/paths.js +12 -0
- package/lib/roster.js +204 -0
- package/lib/transcript.js +361 -0
- package/lib/usage.js +346 -0
- package/package.json +27 -0
- package/public/app.js +1021 -0
- package/public/audio-controls.js +165 -0
- package/public/avatar.js +467 -0
- package/public/characters/dev-auburn.json +32 -0
- package/public/characters/dev-auburn.png +0 -0
- package/public/characters/dev-beanie.json +32 -0
- package/public/characters/dev-beanie.png +0 -0
- package/public/characters/dev-glasses.json +32 -0
- package/public/characters/dev-glasses.png +0 -0
- package/public/chat-panel.css +514 -0
- package/public/chat-panel.js +815 -0
- package/public/index.html +190 -0
- package/public/lab.html +129 -0
- package/public/leaderboard.js +222 -0
- package/public/metric.js +34 -0
- package/public/mock-agents.js +70 -0
- package/public/mock.js +277 -0
- package/public/music/Console_Morning.mp3 +0 -0
- package/public/music/Midnight_Desk.mp3 +0 -0
- package/public/music/The_Plant_Beside_the_Door.mp3 +0 -0
- package/public/music/Three_AM_Window.mp3 +0 -0
- package/public/office.js +1484 -0
- package/public/sound.js +382 -0
- package/public/sprites.js +983 -0
- package/public/style.css +506 -0
- package/public/ui.js +50 -0
- package/scripts/_pixpng.mjs +104 -0
- package/scripts/animsheet.mjs +60 -0
- package/scripts/charsheet.mjs +61 -0
- package/scripts/install-hook.mjs +120 -0
- 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
|
|
Binary file
|