@polderlabs/bizar 10.5.0 → 10.7.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/bizar-dash/src/server/agent-bus.mjs +485 -0
- package/bizar-dash/src/server/api.mjs +6 -0
- package/bizar-dash/src/server/loop-runtime.mjs +429 -0
- package/bizar-dash/src/server/routes/agent-bus.mjs +185 -0
- package/bizar-dash/src/server/routes/loops.mjs +138 -0
- package/bizar-dash/src/server/task-splitter.mjs +241 -0
- package/bizar-dash/tests/agent-bus.test.mjs +289 -0
- package/bizar-dash/tests/loop-runtime.test.mjs +312 -0
- package/bizar-dash/tests/loops-agent-bus-routes.test.mjs +293 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +18 -11
- package/bizar-dash/tests/task-splitter.test.mjs +268 -0
- package/cli/__tests__/feature-list-bridge.test.mjs +42 -42
- package/cli/agent-browser-up.sh +35 -35
- package/cli/agent-browser-update.mjs +42 -42
- package/cli/agent-browser-update.test.mjs +7 -7
- package/cli/audit.mjs +3 -3
- package/cli/bin.mjs +10 -10
- package/cli/bootstrap.mjs +1 -1
- package/cli/cli-commands-validation.test.mjs +2 -2
- package/cli/commands/claim.mjs +3 -3
- package/cli/commands/claude-cmd.mjs +3 -3
- package/cli/commands/eval.mjs +6 -6
- package/cli/commands/util.mjs +18 -18
- package/cli/commands/validate.mjs +10 -10
- package/cli/init.mjs +1 -1
- package/cli/install/postinstall.mjs +4 -4
- package/cli/plow-through.test.mjs +7 -6
- package/cli/prompts.mjs +11 -11
- package/cli/provision.mjs +4 -16
- package/config/skills/bizar/SKILL.md +1 -1
- package/config/skills/skillopt/SKILL.md +129 -0
- package/config/skills/thinking-archetypes/SKILL.md +90 -0
- package/config/skills/thinking-bayesian/SKILL.md +267 -0
- package/config/skills/thinking-bounded-rationality/SKILL.md +406 -0
- package/config/skills/thinking-circle-of-competence/SKILL.md +216 -0
- package/config/skills/thinking-cynefin/SKILL.md +70 -0
- package/config/skills/thinking-debiasing/SKILL.md +192 -0
- package/config/skills/thinking-dual-process/SKILL.md +282 -0
- package/config/skills/thinking-effectuation/SKILL.md +366 -0
- package/config/skills/thinking-feedback-loops/SKILL.md +464 -0
- package/config/skills/thinking-fermi-estimation/SKILL.md +263 -0
- package/config/skills/thinking-first-principles/SKILL.md +167 -0
- package/config/skills/thinking-five-whys-plus/SKILL.md +139 -0
- package/config/skills/thinking-inversion/SKILL.md +195 -0
- package/config/skills/thinking-jobs-to-be-done/SKILL.md +363 -0
- package/config/skills/thinking-kepner-tregoe/SKILL.md +154 -0
- package/config/skills/thinking-leverage-points/SKILL.md +390 -0
- package/config/skills/thinking-lindy-effect/SKILL.md +331 -0
- package/config/skills/thinking-map-territory/SKILL.md +111 -0
- package/config/skills/thinking-margin-of-safety/SKILL.md +330 -0
- package/config/skills/thinking-model-combination/SKILL.md +406 -0
- package/config/skills/thinking-model-router/SKILL.md +360 -0
- package/config/skills/thinking-model-selection/SKILL.md +341 -0
- package/config/skills/thinking-occams-razor/SKILL.md +129 -0
- package/config/skills/thinking-ooda/SKILL.md +127 -0
- package/config/skills/thinking-opportunity-cost/SKILL.md +360 -0
- package/config/skills/thinking-pre-mortem/SKILL.md +170 -0
- package/config/skills/thinking-probabilistic/SKILL.md +324 -0
- package/config/skills/thinking-red-team/SKILL.md +142 -0
- package/config/skills/thinking-regret-minimization/SKILL.md +335 -0
- package/config/skills/thinking-reversibility/SKILL.md +326 -0
- package/config/skills/thinking-scientific-method/SKILL.md +162 -0
- package/config/skills/thinking-second-order/SKILL.md +184 -0
- package/config/skills/thinking-socratic/SKILL.md +198 -0
- package/config/skills/thinking-steel-manning/SKILL.md +332 -0
- package/config/skills/thinking-systems/SKILL.md +238 -0
- package/config/skills/thinking-theory-of-constraints/SKILL.md +338 -0
- package/config/skills/thinking-thought-experiment/SKILL.md +354 -0
- package/config/skills/thinking-triz/SKILL.md +171 -0
- package/config/skills/thinking-via-negativa/SKILL.md +358 -0
- package/package.json +2 -3
- package/packages/sdk/dist/consensus/index.js +4 -4
- package/packages/sdk/dist/router/agent-model-registry.js +2 -2
- package/packages/sdk/dist/router/index.d.ts +3 -3
- package/packages/sdk/dist/router/index.js +4 -4
- package/packages/sdk/dist/router/q-learning-router.d.ts +2 -2
- package/packages/sdk/dist/router/q-learning-router.js +18 -18
- package/packages/sdk/package.json +1 -1
- package/scripts/bh-full-e2e.mjs +10 -8
- package/scripts/check-agents.mjs +27 -24
- package/scripts/mirror-claude-md.sh +0 -0
- package/scripts/sync-skills-mirror.mjs +113 -0
- package/scripts/verify-thinking-skills.mjs +135 -0
- package/config/agents/_shared/AGENT_BASELINE.md +0 -155
- package/config/agents/_shared/CLINE_TOOLS.md +0 -398
- package/config/agents/_shared/SKILLS.md +0 -109
- package/config/agents/agent-browser.md +0 -90
- package/config/agents/baldr.md +0 -60
- package/config/agents/forseti.md +0 -60
- package/config/agents/frigg.md +0 -52
- package/config/agents/heimdall.md +0 -44
- package/config/agents/hermod.md +0 -64
- package/config/agents/mimir.md +0 -60
- package/config/agents/odin.md +0 -310
- package/config/agents/quick.md +0 -47
- package/config/agents/semble-search.md +0 -59
- package/config/agents/thor.md +0 -64
- package/config/agents/tyr.md +0 -67
- package/config/agents/vidarr.md +0 -65
- package/config/agents/vor.md +0 -62
- package/config/commands/audit.md +0 -5
- package/config/commands/bizar.md +0 -22
- package/config/commands/explain.md +0 -5
- package/config/commands/init.md +0 -26
- package/config/commands/learn.md +0 -5
- package/config/commands/plan.md +0 -30
- package/config/commands/plow-through.md +0 -47
- package/config/commands/pr-review.md +0 -5
- package/config/commands/setup-provider.md +0 -95
- package/config/commands/tailscale-serve.md +0 -97
- package/config/commands/team.md +0 -124
- package/config/commands/test.md +0 -60
- package/config/commands/validate.md +0 -66
- package/config/commands/visual-plan.md +0 -19
- package/config/hooks/PostToolUse +0 -66
- package/config/hooks/PreToolUse +0 -82
- package/config/hooks/README.md +0 -68
- package/config/hooks/TaskResume +0 -48
- package/config/hooks/TaskStart +0 -47
- package/config/hooks/UserPromptSubmit +0 -55
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/task-splitter.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Self-check for task-splitter.mjs (G-autoloop Phase 3).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
estimateSize,
|
|
12
|
+
avgHistoricalMinutes,
|
|
13
|
+
shouldSplit,
|
|
14
|
+
planSplit,
|
|
15
|
+
acceptSplit,
|
|
16
|
+
rollupProgress,
|
|
17
|
+
goalAwarePriority,
|
|
18
|
+
} = await import('../src/server/task-splitter.mjs');
|
|
19
|
+
|
|
20
|
+
// ---- estimateSize ---------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
test('estimateSize: tiny tasks → S', () => {
|
|
23
|
+
const size = estimateSize({ title: 'fix typo', description: '' });
|
|
24
|
+
assert.equal(size, 'S');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('estimateSize: medium tasks with description → M or L', () => {
|
|
28
|
+
const size = estimateSize({
|
|
29
|
+
title: 'Add a /metrics Prometheus endpoint to the dash server',
|
|
30
|
+
description: 'Wire app.get("/metrics") using prom-client; expose httpRequestsTotal as gauge and counter, label by route+status; gate on auth.',
|
|
31
|
+
});
|
|
32
|
+
assert.ok(['M', 'L'].includes(size), `expected M or L, got ${size}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('estimateSize: linked goal+kr pushes to L or XL', () => {
|
|
36
|
+
const size = estimateSize({
|
|
37
|
+
title: 'Refactor agent routing',
|
|
38
|
+
metadata: { goalId: 'G-1', krId: 'KR-1' },
|
|
39
|
+
description: 'medium',
|
|
40
|
+
});
|
|
41
|
+
assert.ok(['L', 'XL'].includes(size), `got ${size}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('estimateSize: long description pushes to XL', () => {
|
|
45
|
+
const longDesc = 'word '.repeat(800).trim();
|
|
46
|
+
const size = estimateSize({
|
|
47
|
+
title: 'huge task',
|
|
48
|
+
description: longDesc,
|
|
49
|
+
});
|
|
50
|
+
assert.equal(size, 'XL');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('estimateSize: history of > 6h avg pushes up', () => {
|
|
54
|
+
const history = [
|
|
55
|
+
{ title: 'Huge task redesign dashboard', timeSpent: 400 },
|
|
56
|
+
{ title: 'Huge task refactor navigation', timeSpent: 500 },
|
|
57
|
+
{ title: 'Huge task wire up', timeSpent: 450 },
|
|
58
|
+
];
|
|
59
|
+
const size = estimateSize({ title: 'Huge task integrate', description: '' }, history);
|
|
60
|
+
assert.ok(['L', 'XL'].includes(size), `got ${size}`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('estimateSize: history < 2 items is ignored', () => {
|
|
64
|
+
const history = [{ title: 'Huge task first', timeSpent: 600 }];
|
|
65
|
+
const size = estimateSize({ title: 'tiny', description: '' }, history);
|
|
66
|
+
assert.equal(size, 'S');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('estimateSize: bad input → M', () => {
|
|
70
|
+
assert.equal(estimateSize(null), 'M');
|
|
71
|
+
assert.equal(estimateSize(undefined), 'M');
|
|
72
|
+
assert.equal(estimateSize('not a task'), 'M');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ---- avgHistoricalMinutes -------------------------------------------------
|
|
76
|
+
|
|
77
|
+
test('avgHistoricalMinutes: matches on overlapping title words', () => {
|
|
78
|
+
const history = [
|
|
79
|
+
{ title: 'Refactor the sidebar navigation', timeSpent: 30 },
|
|
80
|
+
{ title: 'Refactor the dashboard navigation', timeSpent: 90 },
|
|
81
|
+
{ title: 'Refresh everything else', timeSpent: 60 },
|
|
82
|
+
];
|
|
83
|
+
const mins = avgHistoricalMinutes({ title: 'Refactor the sidebar navigation' }, history);
|
|
84
|
+
// overlap≥2 with the candidate; first two share "Refactor" + "navigation".
|
|
85
|
+
assert.equal(mins, (30 + 90) / 2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('avgHistoricalMinutes: ignores zero/negative timeSpent rows', () => {
|
|
89
|
+
const history = [
|
|
90
|
+
{ title: 'wire up routes', timeSpent: 30 },
|
|
91
|
+
{ title: 'wire up nav', timeSpent: 0 },
|
|
92
|
+
{ title: 'wire up tasks', timeSpent: -5 },
|
|
93
|
+
];
|
|
94
|
+
// Title words: "wire", "up", "routes" / "nav" / "tasks"; "up" is short
|
|
95
|
+
// and dropped (>3 filter). Only "wire" overlap → match by ≥1? No, we
|
|
96
|
+
// require ≥2. So only "wire up routes" should match (shares "wire",
|
|
97
|
+
// "routes").
|
|
98
|
+
const mins = avgHistoricalMinutes({ title: 'wire up routes' }, history);
|
|
99
|
+
assert.equal(mins, 30);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('avgHistoricalMinutes: short history is unreliable', () => {
|
|
103
|
+
const history = [{ title: 'Refactor the dashboard', timeSpent: 600 }];
|
|
104
|
+
assert.equal(avgHistoricalMinutes({ title: 'Refactor the dashboard' }, history), 0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('avgHistoricalMinutes: no word overlap → 0', () => {
|
|
108
|
+
const history = [
|
|
109
|
+
{ title: 'completely unrelated task alpha beta', timeSpent: 600 },
|
|
110
|
+
{ title: 'completely unrelated task gamma delta', timeSpent: 600 },
|
|
111
|
+
];
|
|
112
|
+
assert.equal(avgHistoricalMinutes({ title: 'tiny change' }, history), 0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---- shouldSplit ----------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
test('shouldSplit: implicit L+ → true', () => {
|
|
118
|
+
const task = {
|
|
119
|
+
title: 'Refactor the dashboard and rewrite every component',
|
|
120
|
+
description: 'huge'.repeat(200),
|
|
121
|
+
metadata: { goalId: 'G-1', krId: 'KR-1' },
|
|
122
|
+
};
|
|
123
|
+
assert.equal(shouldSplit(task), true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('shouldSplit: small tasks → false', () => {
|
|
127
|
+
assert.equal(shouldSplit({ title: 'fix typo' }), false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('shouldSplit: explicit metadata wins over heuristic', () => {
|
|
131
|
+
// Small task, but force split via metadata.
|
|
132
|
+
assert.equal(shouldSplit({ title: 'tiny', metadata: { autoSplit: true } }), true);
|
|
133
|
+
// Big task, but explicitly forbid splitting.
|
|
134
|
+
assert.equal(
|
|
135
|
+
shouldSplit({
|
|
136
|
+
title: 'huge '.repeat(50).trim(),
|
|
137
|
+
description: 'big',
|
|
138
|
+
metadata: { autoSplit: false },
|
|
139
|
+
}),
|
|
140
|
+
false,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ---- planSplit ------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
test('planSplit: requires id, returns prompt + constraints', () => {
|
|
147
|
+
assert.throws(() => planSplit(null), TypeError);
|
|
148
|
+
assert.throws(() => planSplit({}), TypeError);
|
|
149
|
+
const plan = planSplit({
|
|
150
|
+
id: 'tsk_abc',
|
|
151
|
+
title: 'wire up autonomous loop',
|
|
152
|
+
description: 'big change',
|
|
153
|
+
assignee: 'tyr',
|
|
154
|
+
metadata: { goalId: 'G-1' },
|
|
155
|
+
});
|
|
156
|
+
assert.equal(plan.kind, 'task-decompose');
|
|
157
|
+
assert.equal(plan.parentTaskId, 'tsk_abc');
|
|
158
|
+
assert.ok(plan.prompt.includes('wire up autonomous loop'));
|
|
159
|
+
assert.ok(plan.prompt.includes('Linked goal: G-1'));
|
|
160
|
+
assert.equal(plan.constraints.min, 2);
|
|
161
|
+
assert.equal(plan.constraints.max, 5);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---- acceptSplit ----------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
test('acceptSplit: filters out malformed candidates', () => {
|
|
167
|
+
const candidates = [
|
|
168
|
+
{ title: 'subtask 1', description: 'ok' },
|
|
169
|
+
null,
|
|
170
|
+
{ title: '' },
|
|
171
|
+
{ title: 'subtask 2' },
|
|
172
|
+
{ title: 42 }, // wrong type
|
|
173
|
+
{ title: 'subtask 3' },
|
|
174
|
+
{ title: 'subtask 4' },
|
|
175
|
+
{ title: 'subtask 5' },
|
|
176
|
+
{ title: 'subtask 6' },
|
|
177
|
+
'string not object',
|
|
178
|
+
];
|
|
179
|
+
const out = acceptSplit(candidates);
|
|
180
|
+
assert.equal(out.length, 5); // capped at max=5; we drop empty/numbered/string
|
|
181
|
+
assert.deepEqual(out.map((o) => o.title), ['subtask 1', 'subtask 2', 'subtask 3', 'subtask 4', 'subtask 5']);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('acceptSplit: below minimum → empty', () => {
|
|
185
|
+
const out = acceptSplit([{ title: 'only' }]);
|
|
186
|
+
assert.equal(out.length, 0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('acceptSplit: trims oversize title and description', () => {
|
|
190
|
+
const longTitle = 'x'.repeat(500);
|
|
191
|
+
const longDesc = 'y'.repeat(10000);
|
|
192
|
+
const out = acceptSplit([
|
|
193
|
+
{ title: longTitle, description: longDesc },
|
|
194
|
+
{ title: 'second' },
|
|
195
|
+
]);
|
|
196
|
+
assert.equal(out[0].title.length, 200);
|
|
197
|
+
assert.equal(out[0].description.length, 4000);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('acceptSplit: tags default to autosplit, priority defaults to 5', () => {
|
|
201
|
+
const out = acceptSplit([{ title: 'a' }, { title: 'b', priority: 2, tags: ['foo'] }]);
|
|
202
|
+
assert.equal(out[0].priority, 5);
|
|
203
|
+
assert.deepEqual(out[0].tags, ['autosplit']);
|
|
204
|
+
assert.equal(out[1].priority, 2);
|
|
205
|
+
assert.deepEqual(out[1].tags, ['foo']);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ---- rollupProgress -------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
test('rollupProgress: counts done children', () => {
|
|
211
|
+
const parent = {
|
|
212
|
+
subtasks: [
|
|
213
|
+
{ status: 'done' },
|
|
214
|
+
{ status: 'doing' },
|
|
215
|
+
{ status: 'done' },
|
|
216
|
+
{ status: 'queued' },
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
const r = rollupProgress(parent);
|
|
220
|
+
assert.equal(r.ratio, 0.5);
|
|
221
|
+
assert.equal(r.allDone, false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('rollupProgress: allDone when every child is done', () => {
|
|
225
|
+
const parent = {
|
|
226
|
+
subtasks: [{ status: 'done' }, { status: 'done' }],
|
|
227
|
+
};
|
|
228
|
+
const r = rollupProgress(parent);
|
|
229
|
+
assert.equal(r.ratio, 1);
|
|
230
|
+
assert.equal(r.allDone, true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('rollupProgress: no children → zeros', () => {
|
|
234
|
+
assert.deepEqual(rollupProgress({}), { ratio: 0, allDone: false });
|
|
235
|
+
assert.deepEqual(rollupProgress(null), { ratio: 0, allDone: false });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ---- goalAwarePriority ----------------------------------------------------
|
|
239
|
+
|
|
240
|
+
test('goalAwarePriority: at-risk goal bumps priority', () => {
|
|
241
|
+
const out = goalAwarePriority({ priority: 4 }, { status: 'at-risk' });
|
|
242
|
+
assert.equal(out.priority, 3);
|
|
243
|
+
assert.equal(out.action, 'bump');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('goalAwarePriority: at-risk clamps at 1', () => {
|
|
247
|
+
const out = goalAwarePriority({ priority: 1 }, { status: 'at-risk' });
|
|
248
|
+
assert.equal(out.priority, 1);
|
|
249
|
+
assert.equal(out.action, 'bump');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('goalAwarePriority: blocked goal holds priority', () => {
|
|
253
|
+
const out = goalAwarePriority({ priority: 3 }, { status: 'blocked' });
|
|
254
|
+
assert.equal(out.priority, 3);
|
|
255
|
+
assert.equal(out.action, 'none');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('goalAwarePriority: done goal de-prioritises', () => {
|
|
259
|
+
const out = goalAwarePriority({ priority: 2 }, { status: 'done' });
|
|
260
|
+
assert.equal(out.priority, 9);
|
|
261
|
+
assert.equal(out.action, 'drop');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('goalAwarePriority: missing goal or task → no change', () => {
|
|
265
|
+
assert.equal(goalAwarePriority(null, { status: 'at-risk' }).action, 'none');
|
|
266
|
+
assert.equal(goalAwarePriority({ priority: 3 }, null).action, 'none');
|
|
267
|
+
assert.equal(goalAwarePriority({ priority: 3 }, { status: 'on-track' }).action, 'none');
|
|
268
|
+
});
|
|
@@ -82,10 +82,10 @@ describe('claim()', () => {
|
|
|
82
82
|
afterEach(() => { /* tmp cleaned in after() */ });
|
|
83
83
|
|
|
84
84
|
test('first claim succeeds', () => {
|
|
85
|
-
const result = br.claim('F-A', { type: 'human', id: '
|
|
85
|
+
const result = br.claim('F-A', { type: 'human', id: 'mike', name: 'Odin' }, 'first-run');
|
|
86
86
|
assert.equal(result.ok, true);
|
|
87
87
|
const f = result.feature;
|
|
88
|
-
assert.equal(f.claimant.id, '
|
|
88
|
+
assert.equal(f.claimant.id, 'mike');
|
|
89
89
|
assert.equal(f.claimStatus, 'claimed');
|
|
90
90
|
assert.ok(f.claimedAt);
|
|
91
91
|
assert.equal(f.claimReason, 'first-run');
|
|
@@ -94,9 +94,9 @@ describe('claim()', () => {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test('second claim rejected with ALREADY_CLAIMED', () => {
|
|
97
|
-
br.claim('F-A', { type: 'human', id: '
|
|
97
|
+
br.claim('F-A', { type: 'human', id: 'mike', name: 'Odin' }, 'first-run');
|
|
98
98
|
assert.throws(
|
|
99
|
-
() => br.claim('F-A', { type: 'agent', id: '
|
|
99
|
+
() => br.claim('F-A', { type: 'agent', id: 'karen' }, 'second-run'),
|
|
100
100
|
(err) => err instanceof ClaimError && err.code === 'ALREADY_CLAIMED',
|
|
101
101
|
);
|
|
102
102
|
});
|
|
@@ -116,10 +116,10 @@ describe('claim()', () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
test('can re-claim a completed feature', () => {
|
|
119
|
-
br.claim('F-A', { type: 'human', id: '
|
|
120
|
-
br.transition('F-A', 'completed', { id: '
|
|
119
|
+
br.claim('F-A', { type: 'human', id: 'mike' }, '');
|
|
120
|
+
br.transition('F-A', 'completed', { id: 'mike' }, '');
|
|
121
121
|
// released -> re-claimable
|
|
122
|
-
const r2 = br.claim('F-A', { type: 'agent', id: '
|
|
122
|
+
const r2 = br.claim('F-A', { type: 'agent', id: 'karen' }, 'second life');
|
|
123
123
|
assert.equal(r2.ok, true);
|
|
124
124
|
});
|
|
125
125
|
});
|
|
@@ -135,8 +135,8 @@ describe('release()', () => {
|
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
test('release after claim sets status to unclaimed', () => {
|
|
138
|
-
br.claim('F-X', { type: 'human', id: '
|
|
139
|
-
const result = br.release('F-X', { id: '
|
|
138
|
+
br.claim('F-X', { type: 'human', id: 'mike' }, '');
|
|
139
|
+
const result = br.release('F-X', { id: 'mike' }, 'done');
|
|
140
140
|
assert.equal(result.ok, true);
|
|
141
141
|
assert.equal(result.feature.claimStatus, 'unclaimed');
|
|
142
142
|
assert.equal(result.feature.claimant, null);
|
|
@@ -145,15 +145,15 @@ describe('release()', () => {
|
|
|
145
145
|
|
|
146
146
|
test('release without claim errors with NOT_CLAIMED', () => {
|
|
147
147
|
assert.throws(
|
|
148
|
-
() => br.release('F-X', { id: '
|
|
148
|
+
() => br.release('F-X', { id: 'mike' }, ''),
|
|
149
149
|
(err) => err instanceof ClaimError && err.code === 'NOT_CLAIMED',
|
|
150
150
|
);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
test('release with wrong claimant errors with WRONG_CLAIMANT', () => {
|
|
154
|
-
br.claim('F-X', { type: 'human', id: '
|
|
154
|
+
br.claim('F-X', { type: 'human', id: 'mike' }, '');
|
|
155
155
|
assert.throws(
|
|
156
|
-
() => br.release('F-X', { id: '
|
|
156
|
+
() => br.release('F-X', { id: 'karen' }, ''),
|
|
157
157
|
(err) => err instanceof ClaimError && err.code === 'WRONG_CLAIMANT',
|
|
158
158
|
);
|
|
159
159
|
});
|
|
@@ -170,15 +170,15 @@ describe('handoff()', () => {
|
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
test('handoff moves claimant', () => {
|
|
173
|
-
br.claim('F-H', { type: 'human', id: '
|
|
173
|
+
br.claim('F-H', { type: 'human', id: 'mike' }, '');
|
|
174
174
|
const result = br.handoff(
|
|
175
175
|
'F-H',
|
|
176
|
-
{ type: 'human', id: '
|
|
177
|
-
{ type: 'agent', id: '
|
|
176
|
+
{ type: 'human', id: 'mike' },
|
|
177
|
+
{ type: 'agent', id: 'karen', name: 'Tyr' },
|
|
178
178
|
'context finished',
|
|
179
179
|
);
|
|
180
180
|
assert.equal(result.ok, true);
|
|
181
|
-
assert.equal(result.feature.claimant.id, '
|
|
181
|
+
assert.equal(result.feature.claimant.id, 'karen');
|
|
182
182
|
assert.equal(result.feature.claimStatus, 'claimed');
|
|
183
183
|
// history: claim + handoff-initiated + handoff-accepted
|
|
184
184
|
const events = result.feature.claimHistory.map((e) => e.event);
|
|
@@ -189,8 +189,8 @@ describe('handoff()', () => {
|
|
|
189
189
|
assert.throws(
|
|
190
190
|
() => br.handoff(
|
|
191
191
|
'F-H',
|
|
192
|
-
{ type: 'human', id: '
|
|
193
|
-
{ type: 'agent', id: '
|
|
192
|
+
{ type: 'human', id: 'mike' },
|
|
193
|
+
{ type: 'agent', id: 'karen' },
|
|
194
194
|
'',
|
|
195
195
|
),
|
|
196
196
|
(err) => err instanceof ClaimError && err.code === 'NOT_CLAIMED',
|
|
@@ -209,17 +209,17 @@ describe('steal()', () => {
|
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
test('steal moves the claim', () => {
|
|
212
|
-
br.claim('F-S', { type: 'human', id: '
|
|
212
|
+
br.claim('F-S', { type: 'human', id: 'mike' }, '');
|
|
213
213
|
for (const reason of STEAL_REASONS) {
|
|
214
214
|
// Steal a fresh feature for each reason to avoid ALREADY_CLAIMED.
|
|
215
215
|
const f = minimalFeature(`F-S-${reason}`);
|
|
216
216
|
const fFile = writeFixture(tmp, `s-${reason}.json`, [f]);
|
|
217
217
|
const subBridge = bridgeFactory(fFile);
|
|
218
218
|
subBridge.claim(`F-S-${reason}`, { type: 'agent', id: 'previous' }, '');
|
|
219
|
-
const result = subBridge.steal(`F-S-${reason}`, { type: 'agent', id: '
|
|
219
|
+
const result = subBridge.steal(`F-S-${reason}`, { type: 'agent', id: 'karen' }, reason);
|
|
220
220
|
assert.equal(result.ok, true);
|
|
221
221
|
assert.equal(result.previousClaimant.id, 'previous');
|
|
222
|
-
assert.equal(result.feature.claimant.id, '
|
|
222
|
+
assert.equal(result.feature.claimant.id, 'karen');
|
|
223
223
|
assert.equal(result.stealReason, reason);
|
|
224
224
|
assert.ok(
|
|
225
225
|
result.feature.claimHistory.some((e) => e.event === 'steal' && e.reason === reason),
|
|
@@ -228,9 +228,9 @@ describe('steal()', () => {
|
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
test('steal rejects unknown reason', () => {
|
|
231
|
-
br.claim('F-S', { type: 'human', id: '
|
|
231
|
+
br.claim('F-S', { type: 'human', id: 'mike' }, '');
|
|
232
232
|
assert.throws(
|
|
233
|
-
() => br.steal('F-S', { type: 'agent', id: '
|
|
233
|
+
() => br.steal('F-S', { type: 'agent', id: 'karen' }, 'bogus-reason'),
|
|
234
234
|
(err) => err instanceof ClaimError && err.code === 'UNKNOWN_STEAL_REASON',
|
|
235
235
|
);
|
|
236
236
|
});
|
|
@@ -247,30 +247,30 @@ describe('transition()', () => {
|
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
test('claim → active → paused → active → completed', () => {
|
|
250
|
-
br.claim('F-T', { type: 'human', id: '
|
|
251
|
-
br.transition('F-T', 'active', { id: '
|
|
250
|
+
br.claim('F-T', { type: 'human', id: 'mike' }, '');
|
|
251
|
+
br.transition('F-T', 'active', { id: 'mike' }, '');
|
|
252
252
|
assert.equal(br.get('F-T').claimStatus, 'active');
|
|
253
|
-
br.transition('F-T', 'paused', { id: '
|
|
253
|
+
br.transition('F-T', 'paused', { id: 'mike' }, 'lunch');
|
|
254
254
|
assert.equal(br.get('F-T').claimStatus, 'paused');
|
|
255
|
-
br.transition('F-T', 'active', { id: '
|
|
255
|
+
br.transition('F-T', 'active', { id: 'mike' }, '');
|
|
256
256
|
assert.equal(br.get('F-T').claimStatus, 'active');
|
|
257
|
-
br.transition('F-T', 'completed', { id: '
|
|
257
|
+
br.transition('F-T', 'completed', { id: 'mike' }, '');
|
|
258
258
|
assert.equal(br.get('F-T').claimStatus, 'completed');
|
|
259
259
|
});
|
|
260
260
|
|
|
261
261
|
test('illegal transition rejected', () => {
|
|
262
|
-
br.claim('F-T', { type: 'human', id: '
|
|
262
|
+
br.claim('F-T', { type: 'human', id: 'mike' }, '');
|
|
263
263
|
// claimed → blocked is NOT in ALLOWED_TRANSITIONS['claimed']
|
|
264
264
|
assert.throws(
|
|
265
|
-
() => br.transition('F-T', 'blocked', { id: '
|
|
265
|
+
() => br.transition('F-T', 'blocked', { id: 'mike' }, ''),
|
|
266
266
|
(err) => err instanceof ClaimError && err.code === 'BAD_TRANSITION',
|
|
267
267
|
);
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
test('unknown status rejected', () => {
|
|
271
|
-
br.claim('F-T', { type: 'human', id: '
|
|
271
|
+
br.claim('F-T', { type: 'human', id: 'mike' }, '');
|
|
272
272
|
assert.throws(
|
|
273
|
-
() => br.transition('F-T', 'bogus', { id: '
|
|
273
|
+
() => br.transition('F-T', 'bogus', { id: 'mike' }, ''),
|
|
274
274
|
(err) => err instanceof ClaimError && err.code === 'UNKNOWN_STATUS',
|
|
275
275
|
);
|
|
276
276
|
});
|
|
@@ -292,12 +292,12 @@ describe('list()', () => {
|
|
|
292
292
|
});
|
|
293
293
|
|
|
294
294
|
test('claims 2, handoffs 1, steals 1 — list filters', () => {
|
|
295
|
-
br.claim('F-1', { type: 'human', id: '
|
|
296
|
-
br.claim('F-2', { type: 'agent', id: '
|
|
297
|
-
br.claim('F-3', { type: 'human', id: '
|
|
298
|
-
br.handoff('F-3', { type: 'human', id: '
|
|
299
|
-
br.claim('F-4', { type: 'human', id: '
|
|
300
|
-
br.steal('F-4', { type: 'agent', id: '
|
|
295
|
+
br.claim('F-1', { type: 'human', id: 'mike' }, '');
|
|
296
|
+
br.claim('F-2', { type: 'agent', id: 'karen' }, '');
|
|
297
|
+
br.claim('F-3', { type: 'human', id: 'mike' }, '');
|
|
298
|
+
br.handoff('F-3', { type: 'human', id: 'mike' }, { type: 'agent', id: 'karen' }, '');
|
|
299
|
+
br.claim('F-4', { type: 'human', id: 'greg' }, '');
|
|
300
|
+
br.steal('F-4', { type: 'agent', id: 'karen' }, 'voluntary');
|
|
301
301
|
|
|
302
302
|
const all = br.list();
|
|
303
303
|
assert.equal(all.length, 4);
|
|
@@ -310,8 +310,8 @@ describe('list()', () => {
|
|
|
310
310
|
assert.ok(humanIds.includes('F-1'), 'F-1 stayed human');
|
|
311
311
|
assert.ok(!humanIds.includes('F-4'), 'F-4 is now an agent claim');
|
|
312
312
|
|
|
313
|
-
const byOdin = br.list({ claimantId: '
|
|
314
|
-
assert.equal(byOdin.length, 1, 'only F-1 still belongs to
|
|
313
|
+
const byOdin = br.list({ claimantId: 'mike' });
|
|
314
|
+
assert.equal(byOdin.length, 1, 'only F-1 still belongs to mike');
|
|
315
315
|
assert.equal(byOdin[0].id, 'F-1');
|
|
316
316
|
});
|
|
317
317
|
|
|
@@ -327,9 +327,9 @@ describe('persistence', () => {
|
|
|
327
327
|
test('bridges are independent — each gets its own file', () => {
|
|
328
328
|
const file = writeFixture(tmp, `p-${Math.random().toString(36).slice(2, 6)}.json`, [minimalFeature('F-P')]);
|
|
329
329
|
const a = bridgeFactory(file);
|
|
330
|
-
a.claim('F-P', { type: 'human', id: '
|
|
330
|
+
a.claim('F-P', { type: 'human', id: 'mike' }, '');
|
|
331
331
|
const b = bridgeFactory(file);
|
|
332
|
-
assert.equal(b.get('F-P').claimant.id, '
|
|
332
|
+
assert.equal(b.get('F-P').claimant.id, 'mike');
|
|
333
333
|
assert.equal(b.get('F-P').claimStatus, 'claimed');
|
|
334
334
|
});
|
|
335
335
|
|
package/cli/agent-browser-up.sh
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# cli/
|
|
3
|
+
# cli/kevin-up.sh — install/launch kevin for Cline.
|
|
4
4
|
# v6.0.0 — Replaces browser-harness (Python, v3.20.7-v5.6.0) with
|
|
5
|
-
#
|
|
5
|
+
# kevin (native Rust CLI from vercel-labs, ~38K★).
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# kevin is a thin CLI wrapper around Chrome for Testing that
|
|
8
8
|
# gives agents access to:
|
|
9
9
|
# - 100+ typed CLI commands (open, snapshot, click, fill, screenshot, …)
|
|
10
|
-
# - Native MCP stdio server (`
|
|
10
|
+
# - Native MCP stdio server (`kevin mcp`)
|
|
11
11
|
# - Self-healing snapshot-based element detection
|
|
12
12
|
# - Plugins (vault, recorder, …)
|
|
13
13
|
# - Vercel AI SDK + AI Gateway integration (natural-language `chat`)
|
|
14
14
|
#
|
|
15
|
-
# Install: npm install -g
|
|
16
|
-
#
|
|
15
|
+
# Install: npm install -g kevin
|
|
16
|
+
# kevin install # downloads Chrome for Testing
|
|
17
17
|
#
|
|
18
18
|
# This script ensures the daemon is up before Bizar agents try to
|
|
19
19
|
# drive the browser. Idempotent: re-running is a no-op if already up.
|
|
20
20
|
#
|
|
21
21
|
# Usage:
|
|
22
|
-
# cli/
|
|
23
|
-
# cli/
|
|
24
|
-
# cli/
|
|
25
|
-
# cli/
|
|
26
|
-
# cli/
|
|
27
|
-
# cli/
|
|
22
|
+
# cli/kevin-up.sh # start if not running
|
|
23
|
+
# cli/kevin-up.sh status # print status
|
|
24
|
+
# cli/kevin-up.sh stop # kill daemon
|
|
25
|
+
# cli/kevin-up.sh restart # stop + start
|
|
26
|
+
# cli/kevin-up.sh install # install + bootstrap
|
|
27
|
+
# cli/kevin-up.sh doctor # run kevin doctor
|
|
28
28
|
#
|
|
29
29
|
# Environment overrides:
|
|
30
|
-
# AB_PROFILE — user-data-dir (default: ~/.
|
|
30
|
+
# AB_PROFILE — user-data-dir (default: ~/.kevin/profile)
|
|
31
31
|
# AGENT_BROWSER_API_KEY — for natural-language chat
|
|
32
32
|
#
|
|
33
33
|
# Exit codes:
|
|
@@ -36,23 +36,23 @@
|
|
|
36
36
|
#
|
|
37
37
|
set -euo pipefail
|
|
38
38
|
|
|
39
|
-
AB_BIN="${AB_BIN:-$(command -v
|
|
39
|
+
AB_BIN="${AB_BIN:-$(command -v kevin || true)}"
|
|
40
40
|
AB_DAEMON_HOST="127.0.0.1"
|
|
41
41
|
AB_DAEMON_PORT="${AB_DAEMON_PORT:-9223}"
|
|
42
|
-
AB_PROFILE="${AB_PROFILE:-$HOME/.
|
|
42
|
+
AB_PROFILE="${AB_PROFILE:-$HOME/.kevin/profile}"
|
|
43
43
|
|
|
44
|
-
log() { echo "[
|
|
44
|
+
log() { echo "[kevin-up] $*" >&2; }
|
|
45
45
|
fail() { log "FAIL: $*"; exit 1; }
|
|
46
46
|
|
|
47
47
|
ensure_binary() {
|
|
48
48
|
if [ -z "$AB_BIN" ] || ! [ -x "$AB_BIN" ]; then
|
|
49
|
-
log "
|
|
49
|
+
log "kevin not on PATH. Installing via npm..."
|
|
50
50
|
command -v npm >/dev/null || fail "npm not found — install Node.js 24+ first"
|
|
51
|
-
npm install -g
|
|
52
|
-
AB_BIN="$(command -v
|
|
53
|
-
[ -x "$AB_BIN" ] || fail "
|
|
51
|
+
npm install -g kevin
|
|
52
|
+
AB_BIN="$(command -v kevin)"
|
|
53
|
+
[ -x "$AB_BIN" ] || fail "kevin install failed"
|
|
54
54
|
fi
|
|
55
|
-
log "
|
|
55
|
+
log "kevin binary: $AB_BIN"
|
|
56
56
|
log "version: $($AB_BIN --version 2>/dev/null || echo 'unknown')"
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -61,7 +61,7 @@ ensure_chrome() {
|
|
|
61
61
|
log "Chrome profile already exists: $AB_PROFILE"
|
|
62
62
|
return 0
|
|
63
63
|
fi
|
|
64
|
-
log "Bootstrapping Chrome for Testing via
|
|
64
|
+
log "Bootstrapping Chrome for Testing via kevin install"
|
|
65
65
|
$AB_BIN install --silent || true
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -72,7 +72,7 @@ is_daemon_running() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
start_daemon() {
|
|
75
|
-
log "Starting
|
|
75
|
+
log "Starting kevin daemon (profile: $AB_PROFILE, port: $AB_DAEMON_PORT)..."
|
|
76
76
|
mkdir -p "$AB_PROFILE"
|
|
77
77
|
|
|
78
78
|
# Spawn the daemon detached so it survives parent shell exit
|
|
@@ -89,13 +89,13 @@ start_daemon() {
|
|
|
89
89
|
local retries=30
|
|
90
90
|
while [ $retries -gt 0 ]; do
|
|
91
91
|
if curl -fsS --max-time 2 "http://${AB_DAEMON_HOST}:${AB_DAEMON_PORT}/json/version" >/dev/null 2>&1; then
|
|
92
|
-
log "
|
|
92
|
+
log "kevin daemon is up (pid $pid)"
|
|
93
93
|
return 0
|
|
94
94
|
fi
|
|
95
95
|
sleep 1
|
|
96
96
|
retries=$((retries - 1))
|
|
97
97
|
done
|
|
98
|
-
fail "
|
|
98
|
+
fail "kevin daemon did not bind to ${AB_DAEMON_HOST}:${AB_DAEMON_PORT}"
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
stop_daemon() {
|
|
@@ -104,24 +104,24 @@ stop_daemon() {
|
|
|
104
104
|
local pid
|
|
105
105
|
pid="$(cat "$pidfile" 2>/dev/null || true)"
|
|
106
106
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
107
|
-
log "Stopping
|
|
107
|
+
log "Stopping kevin daemon (pid $pid)"
|
|
108
108
|
kill "$pid" 2>/dev/null || true
|
|
109
109
|
sleep 1
|
|
110
110
|
kill -9 "$pid" 2>/dev/null || true
|
|
111
111
|
fi
|
|
112
112
|
rm -f "$pidfile"
|
|
113
113
|
fi
|
|
114
|
-
# Belt-and-braces: kill any orphan
|
|
115
|
-
pkill -f '
|
|
114
|
+
# Belt-and-braces: kill any orphan kevin processes
|
|
115
|
+
pkill -f 'kevin serve' 2>/dev/null || true
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
print_status() {
|
|
119
119
|
if is_daemon_running; then
|
|
120
|
-
echo "
|
|
120
|
+
echo "kevin: RUNNING (http://${AB_DAEMON_HOST}:${AB_DAEMON_PORT})"
|
|
121
121
|
$AB_BIN doctor 2>&1 | tail -20 || true
|
|
122
122
|
else
|
|
123
|
-
echo "
|
|
124
|
-
echo "Start with: cli/
|
|
123
|
+
echo "kevin: STOPPED"
|
|
124
|
+
echo "Start with: cli/kevin-up.sh start"
|
|
125
125
|
fi
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -134,17 +134,17 @@ case "${1:-start}" in
|
|
|
134
134
|
install) ensure_binary; ensure_chrome; $AB_BIN install ;;
|
|
135
135
|
help|--help|-h)
|
|
136
136
|
cat <<'AGENT_BROWSER_UP_HELP'
|
|
137
|
-
|
|
137
|
+
kevin-up -- start/stop the kevin daemon
|
|
138
138
|
|
|
139
|
-
Usage: cli/
|
|
139
|
+
Usage: cli/kevin-up.sh <command>
|
|
140
140
|
|
|
141
141
|
Commands:
|
|
142
142
|
start Start the daemon (idempotent)
|
|
143
143
|
stop Stop the daemon
|
|
144
144
|
restart Stop + start
|
|
145
145
|
status Print daemon status
|
|
146
|
-
doctor Run
|
|
147
|
-
install Install
|
|
146
|
+
doctor Run kevin doctor
|
|
147
|
+
install Install kevin + download Chrome
|
|
148
148
|
AGENT_BROWSER_UP_HELP
|
|
149
149
|
exit 0
|
|
150
150
|
;;
|