pi-mono-all 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/CHANGELOG.md +13 -0
- package/LICENCE.md +7 -0
- package/node_modules/pi-common/package.json +22 -0
- package/node_modules/pi-common/src/auth-config.ts +290 -0
- package/node_modules/pi-common/src/auth.ts +63 -0
- package/node_modules/pi-common/src/cache.ts +60 -0
- package/node_modules/pi-common/src/errors.ts +47 -0
- package/node_modules/pi-common/src/http-client.ts +118 -0
- package/node_modules/pi-common/src/index.ts +7 -0
- package/node_modules/pi-common/src/rate-limiter.ts +32 -0
- package/node_modules/pi-common/src/tool-result.ts +27 -0
- package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
- package/node_modules/pi-mono-ask-user-question/README.md +226 -0
- package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
- package/node_modules/pi-mono-ask-user-question/package.json +29 -0
- package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-auto-fix/README.md +77 -0
- package/node_modules/pi-mono-auto-fix/index.ts +488 -0
- package/node_modules/pi-mono-auto-fix/package.json +23 -0
- package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-btw/README.md +24 -0
- package/node_modules/pi-mono-btw/index.ts +499 -0
- package/node_modules/pi-mono-btw/package.json +29 -0
- package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-clear/README.md +40 -0
- package/node_modules/pi-mono-clear/index.ts +45 -0
- package/node_modules/pi-mono-clear/package.json +29 -0
- package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
- package/node_modules/pi-mono-context/README.md +74 -0
- package/node_modules/pi-mono-context/index.ts +641 -0
- package/node_modules/pi-mono-context/package.json +29 -0
- package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
- package/node_modules/pi-mono-context-guard/README.md +81 -0
- package/node_modules/pi-mono-context-guard/index.ts +212 -0
- package/node_modules/pi-mono-context-guard/package.json +23 -0
- package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-figma/README.md +236 -0
- package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
- package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
- package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
- package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
- package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
- package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
- package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
- package/node_modules/pi-mono-figma/index.ts +6 -0
- package/node_modules/pi-mono-figma/package.json +33 -0
- package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
- package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
- package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
- package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
- package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
- package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
- package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
- package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
- package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
- package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
- package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
- package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
- package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
- package/node_modules/pi-mono-linear/README.md +159 -0
- package/node_modules/pi-mono-linear/index.ts +6 -0
- package/node_modules/pi-mono-linear/package.json +30 -0
- package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
- package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
- package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
- package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
- package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
- package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-loop/README.md +54 -0
- package/node_modules/pi-mono-loop/index.ts +291 -0
- package/node_modules/pi-mono-loop/package.json +26 -0
- package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
- package/node_modules/pi-mono-multi-edit/README.md +244 -0
- package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
- package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
- package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
- package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
- package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
- package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
- package/node_modules/pi-mono-multi-edit/index.ts +266 -0
- package/node_modules/pi-mono-multi-edit/package.json +37 -0
- package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
- package/node_modules/pi-mono-multi-edit/types.ts +53 -0
- package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
- package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
- package/node_modules/pi-mono-review/README.md +30 -0
- package/node_modules/pi-mono-review/common.ts +930 -0
- package/node_modules/pi-mono-review/index.ts +8 -0
- package/node_modules/pi-mono-review/package.json +29 -0
- package/node_modules/pi-mono-review/review-tui.ts +194 -0
- package/node_modules/pi-mono-review/review.ts +119 -0
- package/node_modules/pi-mono-review/reviewer.ts +339 -0
- package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
- package/node_modules/pi-mono-sentinel/README.md +87 -0
- package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
- package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
- package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
- package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
- package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
- package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
- package/node_modules/pi-mono-sentinel/index.ts +43 -0
- package/node_modules/pi-mono-sentinel/package.json +26 -0
- package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
- package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
- package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
- package/node_modules/pi-mono-sentinel/session.ts +95 -0
- package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
- package/node_modules/pi-mono-sentinel/types.ts +39 -0
- package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
- package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-simplify/README.md +56 -0
- package/node_modules/pi-mono-simplify/index.ts +78 -0
- package/node_modules/pi-mono-simplify/package.json +29 -0
- package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-status-line/README.md +96 -0
- package/node_modules/pi-mono-status-line/basic.ts +89 -0
- package/node_modules/pi-mono-status-line/expert.ts +689 -0
- package/node_modules/pi-mono-status-line/index.ts +54 -0
- package/node_modules/pi-mono-status-line/package.json +29 -0
- package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
- package/node_modules/pi-mono-team-mode/README.md +246 -0
- package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
- package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
- package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
- package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
- package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
- package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
- package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
- package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
- package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
- package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
- package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
- package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
- package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
- package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
- package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
- package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
- package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
- package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
- package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
- package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
- package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
- package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
- package/node_modules/pi-mono-team-mode/index.ts +825 -0
- package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
- package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
- package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
- package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
- package/node_modules/pi-mono-team-mode/package.json +33 -0
- package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
- package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
- package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
- package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
- package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
- package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
- package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
- package/package.json +76 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Pi Team-Mode — Prompt addenda + notification XML
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { describe, test } from "node:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
TEAMMATE_SYSTEM_PROMPT_ADDENDUM,
|
|
8
|
+
formatTaskNotification,
|
|
9
|
+
getCoordinatorSystemPrompt,
|
|
10
|
+
isCoordinatorMode,
|
|
11
|
+
} from "../core/prompts.ts";
|
|
12
|
+
|
|
13
|
+
describe("TEAMMATE_SYSTEM_PROMPT_ADDENDUM", () => {
|
|
14
|
+
test("instructs teammates to use send_message", () => {
|
|
15
|
+
assert.match(TEAMMATE_SYSTEM_PROMPT_ADDENDUM, /send_message/);
|
|
16
|
+
assert.match(TEAMMATE_SYSTEM_PROMPT_ADDENDUM, /team-wide broadcasts/);
|
|
17
|
+
assert.match(TEAMMATE_SYSTEM_PROMPT_ADDENDUM, /not visible to others/);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("getCoordinatorSystemPrompt", () => {
|
|
22
|
+
const prompt = getCoordinatorSystemPrompt();
|
|
23
|
+
test("teaches coordinator role", () => {
|
|
24
|
+
assert.match(prompt, /coordinator/i);
|
|
25
|
+
assert.match(prompt, /Your Role/);
|
|
26
|
+
});
|
|
27
|
+
test("references coordinator tools", () => {
|
|
28
|
+
for (const name of [
|
|
29
|
+
"agent",
|
|
30
|
+
"delegate",
|
|
31
|
+
"send_message",
|
|
32
|
+
"task_stop",
|
|
33
|
+
"task_create",
|
|
34
|
+
"task_list",
|
|
35
|
+
]) {
|
|
36
|
+
assert.match(prompt, new RegExp(`\\b${name}\\b`), `mentions ${name}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
test("describes task-notification wake-up", () => {
|
|
40
|
+
assert.match(prompt, /<task-notification>/);
|
|
41
|
+
assert.match(prompt, /end your response/i);
|
|
42
|
+
assert.match(prompt, /Never fabricate or predict/);
|
|
43
|
+
});
|
|
44
|
+
test("avoids TODO overhead for a single task", () => {
|
|
45
|
+
assert.match(prompt, /single coherent task/i);
|
|
46
|
+
assert.match(prompt, /do not call task_create\/task_update/i);
|
|
47
|
+
assert.match(prompt, /Creating exactly one TODO item is overhead/i);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("formatTaskNotification", () => {
|
|
52
|
+
test("basic completion", () => {
|
|
53
|
+
const xml = formatTaskNotification({
|
|
54
|
+
taskId: "agent-researcher-abc",
|
|
55
|
+
status: "completed",
|
|
56
|
+
summary: 'Agent "research auth" completed',
|
|
57
|
+
result: "Found null pointer at validate.ts:42",
|
|
58
|
+
durationMs: 1234,
|
|
59
|
+
});
|
|
60
|
+
assert.match(xml, /<task-notification>/);
|
|
61
|
+
assert.match(xml, /<task-id>agent-researcher-abc<\/task-id>/);
|
|
62
|
+
assert.match(xml, /<status>completed<\/status>/);
|
|
63
|
+
assert.match(xml, /<duration_ms>1234<\/duration_ms>/);
|
|
64
|
+
assert.match(xml, /<result>Found null pointer at validate.ts:42<\/result>/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("escapes XML special chars", () => {
|
|
68
|
+
const xml = formatTaskNotification({
|
|
69
|
+
taskId: "agent-x",
|
|
70
|
+
status: "failed",
|
|
71
|
+
summary: "boom",
|
|
72
|
+
result: "<script>alert('xss')</script>",
|
|
73
|
+
});
|
|
74
|
+
assert.match(xml, /<script>/);
|
|
75
|
+
assert.doesNotMatch(xml, /<script>/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("omits optional sections", () => {
|
|
79
|
+
const xml = formatTaskNotification({
|
|
80
|
+
taskId: "agent-x",
|
|
81
|
+
status: "killed",
|
|
82
|
+
summary: "stopped by user",
|
|
83
|
+
});
|
|
84
|
+
assert.doesNotMatch(xml, /<result>/);
|
|
85
|
+
assert.doesNotMatch(xml, /<usage>/);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("isCoordinatorMode", () => {
|
|
90
|
+
test("reads PI_TEAM_MATE_COORDINATOR", () => {
|
|
91
|
+
const prev = process.env.PI_TEAM_MATE_COORDINATOR;
|
|
92
|
+
try {
|
|
93
|
+
process.env.PI_TEAM_MATE_COORDINATOR = "1";
|
|
94
|
+
assert.equal(isCoordinatorMode(), true);
|
|
95
|
+
process.env.PI_TEAM_MATE_COORDINATOR = "true";
|
|
96
|
+
assert.equal(isCoordinatorMode(), true);
|
|
97
|
+
process.env.PI_TEAM_MATE_COORDINATOR = "";
|
|
98
|
+
assert.equal(isCoordinatorMode(), false);
|
|
99
|
+
delete process.env.PI_TEAM_MATE_COORDINATOR;
|
|
100
|
+
assert.equal(isCoordinatorMode(), false);
|
|
101
|
+
} finally {
|
|
102
|
+
if (prev === undefined) delete process.env.PI_TEAM_MATE_COORDINATOR;
|
|
103
|
+
else process.env.PI_TEAM_MATE_COORDINATOR = prev;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Team-Mode — Store Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { describe, test } from "node:test";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
TeamMateStore,
|
|
13
|
+
generateTeamId,
|
|
14
|
+
generateTeammateId,
|
|
15
|
+
} from "../core/store.ts";
|
|
16
|
+
import type { TeamRecord, TeammateRecord } from "../core/types.ts";
|
|
17
|
+
|
|
18
|
+
async function setup(): Promise<{ store: TeamMateStore; dir: string }> {
|
|
19
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-team-mode-"));
|
|
20
|
+
return { store: new TeamMateStore(dir), dir };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeTeammate(overrides: Partial<TeammateRecord> = {}): TeammateRecord {
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
return {
|
|
26
|
+
id: "agent-researcher-abc12345",
|
|
27
|
+
name: "researcher",
|
|
28
|
+
isolation: "none",
|
|
29
|
+
cwd: "/tmp/fake",
|
|
30
|
+
status: "running",
|
|
31
|
+
background: false,
|
|
32
|
+
createdAt: now,
|
|
33
|
+
updatedAt: now,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeTeam(overrides: Partial<TeamRecord> = {}): TeamRecord {
|
|
39
|
+
return {
|
|
40
|
+
id: "billing-abc12345",
|
|
41
|
+
name: "billing",
|
|
42
|
+
createdAt: new Date().toISOString(),
|
|
43
|
+
defaultIsolation: "none",
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("generateTeammateId", () => {
|
|
49
|
+
test("slugifies name under agent- prefix (Claude Code parity)", () => {
|
|
50
|
+
const id = generateTeammateId("Some Teammate!");
|
|
51
|
+
assert.match(id, /^agent-some-teammate-[0-9a-f]{8}$/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("falls back to agent-teammate when name is empty", () => {
|
|
55
|
+
const id = generateTeammateId("");
|
|
56
|
+
assert.match(id, /^agent-teammate-[0-9a-f]{8}$/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("falls back to agent-teammate when name is undefined", () => {
|
|
60
|
+
const id = generateTeammateId(undefined);
|
|
61
|
+
assert.match(id, /^agent-teammate-[0-9a-f]{8}$/);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("generateTeamId", () => {
|
|
66
|
+
test("slugifies and appends random suffix", () => {
|
|
67
|
+
const id = generateTeamId("Billing Team");
|
|
68
|
+
assert.match(id, /^billing-team-[0-9a-f]{8}$/);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("TeamMateStore.saveTeammate / loadTeammate", () => {
|
|
73
|
+
test("round-trips a teammate record", async () => {
|
|
74
|
+
const { store, dir } = await setup();
|
|
75
|
+
try {
|
|
76
|
+
const record = makeTeammate();
|
|
77
|
+
await store.saveTeammate(record);
|
|
78
|
+
const loaded = await store.loadTeammate(record.id);
|
|
79
|
+
assert.deepEqual(loaded, record);
|
|
80
|
+
} finally {
|
|
81
|
+
await rm(dir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("loadTeammate returns null for missing id", async () => {
|
|
86
|
+
const { store, dir } = await setup();
|
|
87
|
+
try {
|
|
88
|
+
assert.equal(await store.loadTeammate("does-not-exist"), null);
|
|
89
|
+
} finally {
|
|
90
|
+
await rm(dir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("TeamMateStore.listTeammates", () => {
|
|
96
|
+
test("lists all saved teammates", async () => {
|
|
97
|
+
const { store, dir } = await setup();
|
|
98
|
+
try {
|
|
99
|
+
await store.saveTeammate(makeTeammate({ id: "agent-a-abc", name: "a" }));
|
|
100
|
+
await store.saveTeammate(makeTeammate({ id: "agent-b-abc", name: "b" }));
|
|
101
|
+
const list = await store.listTeammates();
|
|
102
|
+
const names = list.map((t) => t.name).sort();
|
|
103
|
+
assert.deepEqual(names, ["a", "b"]);
|
|
104
|
+
} finally {
|
|
105
|
+
await rm(dir, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("returns empty array when none exist", async () => {
|
|
110
|
+
const { store, dir } = await setup();
|
|
111
|
+
try {
|
|
112
|
+
assert.deepEqual(await store.listTeammates(), []);
|
|
113
|
+
} finally {
|
|
114
|
+
await rm(dir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("TeamMateStore team records", () => {
|
|
120
|
+
test("round-trips a team record", async () => {
|
|
121
|
+
const { store, dir } = await setup();
|
|
122
|
+
try {
|
|
123
|
+
const team = makeTeam();
|
|
124
|
+
await store.saveTeam(team);
|
|
125
|
+
const loaded = await store.loadTeam(team.id);
|
|
126
|
+
assert.deepEqual(loaded, team);
|
|
127
|
+
} finally {
|
|
128
|
+
await rm(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("TeamMateStore name index", () => {
|
|
134
|
+
test("round-trips the name index", async () => {
|
|
135
|
+
const { store, dir } = await setup();
|
|
136
|
+
try {
|
|
137
|
+
await store.setNameIndex("session-1", { researcher: "researcher-abc" });
|
|
138
|
+
const loaded = await store.getNameIndex("session-1");
|
|
139
|
+
assert.deepEqual(loaded, { researcher: "researcher-abc" });
|
|
140
|
+
} finally {
|
|
141
|
+
await rm(dir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("returns empty object when index is missing", async () => {
|
|
146
|
+
const { store, dir } = await setup();
|
|
147
|
+
try {
|
|
148
|
+
assert.deepEqual(await store.getNameIndex("never-created"), {});
|
|
149
|
+
} finally {
|
|
150
|
+
await rm(dir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("clearNameIndex removes the index", async () => {
|
|
155
|
+
const { store, dir } = await setup();
|
|
156
|
+
try {
|
|
157
|
+
await store.setNameIndex("s", { a: "x" });
|
|
158
|
+
await store.clearNameIndex("s");
|
|
159
|
+
assert.deepEqual(await store.getNameIndex("s"), {});
|
|
160
|
+
} finally {
|
|
161
|
+
await rm(dir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Pi Team-Mode — Task Board Tests
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { describe, test } from "node:test";
|
|
8
|
+
|
|
9
|
+
import { TaskStore, isUnblocked, type TaskRecord } from "../core/tasks.ts";
|
|
10
|
+
import { TaskManager, VersionConflictError } from "../managers/task-manager.ts";
|
|
11
|
+
|
|
12
|
+
function setup(): Promise<{ store: TaskStore; manager: TaskManager; dir: string; sessionId: string }> {
|
|
13
|
+
return mkdtemp(join(tmpdir(), "team-mode-tasks-")).then((dir) => {
|
|
14
|
+
const store = new TaskStore(dir);
|
|
15
|
+
const sessionId = "session-test";
|
|
16
|
+
const manager = new TaskManager({ store, getParentSessionId: () => sessionId });
|
|
17
|
+
return { store, manager, dir, sessionId };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function cleanup(dir: string): Promise<void> {
|
|
22
|
+
await rm(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("TaskStore round-trip", () => {
|
|
26
|
+
test("save + load + list + delete", async () => {
|
|
27
|
+
const { store, dir, sessionId } = await setup();
|
|
28
|
+
try {
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
const record: TaskRecord = {
|
|
31
|
+
id: "task-first-abcd1234",
|
|
32
|
+
subject: "first",
|
|
33
|
+
status: "pending",
|
|
34
|
+
owner: null,
|
|
35
|
+
blockedBy: [],
|
|
36
|
+
blocks: [],
|
|
37
|
+
parentSessionId: sessionId,
|
|
38
|
+
createdAt: now,
|
|
39
|
+
updatedAt: now,
|
|
40
|
+
version: 1,
|
|
41
|
+
};
|
|
42
|
+
await store.save(record);
|
|
43
|
+
assert.deepEqual(await store.load(sessionId, "task-first-abcd1234"), record);
|
|
44
|
+
assert.equal((await store.list(sessionId)).length, 1);
|
|
45
|
+
await store.delete(sessionId, "task-first-abcd1234");
|
|
46
|
+
assert.equal((await store.list(sessionId)).length, 0);
|
|
47
|
+
} finally {
|
|
48
|
+
await cleanup(dir);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("list returns [] on missing dir", async () => {
|
|
53
|
+
const { store, dir } = await setup();
|
|
54
|
+
try {
|
|
55
|
+
assert.deepEqual(await store.list("never-created"), []);
|
|
56
|
+
} finally {
|
|
57
|
+
await cleanup(dir);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("isUnblocked", () => {
|
|
63
|
+
const makeTask = (o: Partial<TaskRecord>): TaskRecord => ({
|
|
64
|
+
id: o.id ?? "t",
|
|
65
|
+
subject: o.subject ?? "t",
|
|
66
|
+
status: o.status ?? "pending",
|
|
67
|
+
owner: o.owner ?? null,
|
|
68
|
+
blockedBy: o.blockedBy ?? [],
|
|
69
|
+
blocks: o.blocks ?? [],
|
|
70
|
+
parentSessionId: "s",
|
|
71
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
72
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
73
|
+
version: 1,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("unblocked when all deps completed", () => {
|
|
77
|
+
const task = makeTask({ id: "t", blockedBy: ["a"] });
|
|
78
|
+
const byId = new Map([
|
|
79
|
+
[task.id, task],
|
|
80
|
+
["a", makeTask({ id: "a", status: "completed" })],
|
|
81
|
+
]);
|
|
82
|
+
assert.equal(isUnblocked(task, byId), true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("blocked when any dep pending", () => {
|
|
86
|
+
const task = makeTask({ id: "t", blockedBy: ["a"] });
|
|
87
|
+
const byId = new Map([
|
|
88
|
+
[task.id, task],
|
|
89
|
+
["a", makeTask({ id: "a", status: "pending" })],
|
|
90
|
+
]);
|
|
91
|
+
assert.equal(isUnblocked(task, byId), false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("deleted deps treated as resolved", () => {
|
|
95
|
+
const task = makeTask({ id: "t", blockedBy: ["a"] });
|
|
96
|
+
const byId = new Map([
|
|
97
|
+
[task.id, task],
|
|
98
|
+
["a", makeTask({ id: "a", status: "deleted" })],
|
|
99
|
+
]);
|
|
100
|
+
assert.equal(isUnblocked(task, byId), true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("TaskManager.create", () => {
|
|
105
|
+
test("creates pending + unassigned per Claude Code shape", async () => {
|
|
106
|
+
const { manager, dir } = await setup();
|
|
107
|
+
try {
|
|
108
|
+
const t = await manager.create({ subject: "a", description: "do a" });
|
|
109
|
+
assert.equal(t.status, "pending");
|
|
110
|
+
assert.equal(t.owner, null);
|
|
111
|
+
assert.deepEqual(t.blockedBy, []);
|
|
112
|
+
assert.deepEqual(t.blocks, []);
|
|
113
|
+
assert.equal(t.version, 1);
|
|
114
|
+
assert.match(t.id, /^task-a-[0-9a-f]{8}$/);
|
|
115
|
+
} finally {
|
|
116
|
+
await cleanup(dir);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("captures activeForm and metadata", async () => {
|
|
121
|
+
const { manager, dir } = await setup();
|
|
122
|
+
try {
|
|
123
|
+
const t = await manager.create({
|
|
124
|
+
subject: "run tests",
|
|
125
|
+
description: "run the suite",
|
|
126
|
+
activeForm: "Running tests",
|
|
127
|
+
metadata: { priority: "high" },
|
|
128
|
+
});
|
|
129
|
+
assert.equal(t.activeForm, "Running tests");
|
|
130
|
+
assert.deepEqual(t.metadata, { priority: "high" });
|
|
131
|
+
} finally {
|
|
132
|
+
await cleanup(dir);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("TaskManager.update — assignment + deps + CAS", () => {
|
|
138
|
+
test("coordinator assigns owner via task_update", async () => {
|
|
139
|
+
const { manager, dir } = await setup();
|
|
140
|
+
try {
|
|
141
|
+
const t = await manager.create({ subject: "a" });
|
|
142
|
+
const assigned = await manager.update(t.id, { owner: "alice", status: "in_progress" });
|
|
143
|
+
assert.equal(assigned.owner, "alice");
|
|
144
|
+
assert.equal(assigned.status, "in_progress");
|
|
145
|
+
} finally {
|
|
146
|
+
await cleanup(dir);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("addBlockedBy / addBlocks merges with existing arrays", async () => {
|
|
151
|
+
const { manager, dir } = await setup();
|
|
152
|
+
try {
|
|
153
|
+
const a = await manager.create({ subject: "a" });
|
|
154
|
+
const b = await manager.create({ subject: "b" });
|
|
155
|
+
const c = await manager.create({ subject: "c" });
|
|
156
|
+
const updated = await manager.update(c.id, { addBlockedBy: [a.id, b.id] });
|
|
157
|
+
assert.deepEqual(updated.blockedBy.sort(), [a.id, b.id].sort());
|
|
158
|
+
} finally {
|
|
159
|
+
await cleanup(dir);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("CAS guard rejects stale version", async () => {
|
|
164
|
+
const { manager, dir } = await setup();
|
|
165
|
+
try {
|
|
166
|
+
const t = await manager.create({ subject: "a" });
|
|
167
|
+
await manager.update(t.id, { status: "in_progress" });
|
|
168
|
+
await assert.rejects(
|
|
169
|
+
() => manager.update(t.id, { expectedVersion: t.version, owner: "alice" }),
|
|
170
|
+
VersionConflictError,
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
await cleanup(dir);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("CAS guard accepts fresh version", async () => {
|
|
178
|
+
const { manager, dir } = await setup();
|
|
179
|
+
try {
|
|
180
|
+
const t = await manager.create({ subject: "a" });
|
|
181
|
+
const updated = await manager.update(t.id, {
|
|
182
|
+
expectedVersion: t.version,
|
|
183
|
+
owner: "alice",
|
|
184
|
+
});
|
|
185
|
+
assert.equal(updated.version, t.version + 1);
|
|
186
|
+
} finally {
|
|
187
|
+
await cleanup(dir);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("TaskManager — TaskCompleted hook", () => {
|
|
193
|
+
test("hook success keeps task completed", async () => {
|
|
194
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-tasks-"));
|
|
195
|
+
try {
|
|
196
|
+
const store = new TaskStore(dir);
|
|
197
|
+
const manager = new TaskManager({
|
|
198
|
+
store,
|
|
199
|
+
getParentSessionId: () => "s",
|
|
200
|
+
getTaskCompletedHook: () => "exit 0",
|
|
201
|
+
});
|
|
202
|
+
const t = await manager.create({ subject: "a" });
|
|
203
|
+
const done = await manager.update(t.id, { status: "completed" });
|
|
204
|
+
assert.equal(done.status, "completed");
|
|
205
|
+
} finally {
|
|
206
|
+
await cleanup(dir);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("hook failure reverts to failed with output", async () => {
|
|
211
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-tasks-"));
|
|
212
|
+
try {
|
|
213
|
+
const store = new TaskStore(dir);
|
|
214
|
+
const manager = new TaskManager({
|
|
215
|
+
store,
|
|
216
|
+
getParentSessionId: () => "s",
|
|
217
|
+
getTaskCompletedHook: () => "echo 'test broke' >&2; exit 7",
|
|
218
|
+
});
|
|
219
|
+
const t = await manager.create({ subject: "a" });
|
|
220
|
+
const done = await manager.update(t.id, { status: "completed" });
|
|
221
|
+
assert.equal(done.status, "failed");
|
|
222
|
+
assert.match(done.result ?? "", /hook failed, exit 7/);
|
|
223
|
+
assert.match(done.hookOutput ?? "", /test broke/);
|
|
224
|
+
} finally {
|
|
225
|
+
await cleanup(dir);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("hook only runs on transition INTO completed", async () => {
|
|
230
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-tasks-"));
|
|
231
|
+
try {
|
|
232
|
+
const store = new TaskStore(dir);
|
|
233
|
+
let hookCalls = 0;
|
|
234
|
+
const manager = new TaskManager({
|
|
235
|
+
store,
|
|
236
|
+
getParentSessionId: () => "s",
|
|
237
|
+
getTaskCompletedHook: () => {
|
|
238
|
+
hookCalls++;
|
|
239
|
+
return "exit 0";
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
const t = await manager.create({ subject: "a" });
|
|
243
|
+
await manager.update(t.id, { status: "in_progress" });
|
|
244
|
+
await manager.update(t.id, { status: "failed" });
|
|
245
|
+
assert.equal(hookCalls, 0);
|
|
246
|
+
} finally {
|
|
247
|
+
await cleanup(dir);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("TaskManager.list filters", () => {
|
|
253
|
+
test("status + owner + teamId", async () => {
|
|
254
|
+
const { manager, dir } = await setup();
|
|
255
|
+
try {
|
|
256
|
+
const a = await manager.create({ subject: "a", teamId: "billing" });
|
|
257
|
+
await manager.create({ subject: "b", teamId: "billing" });
|
|
258
|
+
await manager.create({ subject: "c" });
|
|
259
|
+
await manager.update(a.id, { owner: "alice", status: "in_progress" });
|
|
260
|
+
assert.equal((await manager.list({ owner: "alice" })).length, 1);
|
|
261
|
+
assert.equal((await manager.list({ status: "in_progress" })).length, 1);
|
|
262
|
+
assert.equal((await manager.list({ teamId: "billing" })).length, 2);
|
|
263
|
+
} finally {
|
|
264
|
+
await cleanup(dir);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Team-Mode — Teammate Spec Loader Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { describe, test } from "node:test";
|
|
10
|
+
|
|
11
|
+
import { listTeammateSpecs, loadTeammateSpec, parseSpec } from "../core/teammate-specs.ts";
|
|
12
|
+
|
|
13
|
+
describe("parseSpec", () => {
|
|
14
|
+
test("parses frontmatter and body", () => {
|
|
15
|
+
const raw = `---
|
|
16
|
+
name: researcher
|
|
17
|
+
description: investigates the codebase
|
|
18
|
+
needsWorktree: false
|
|
19
|
+
hasMemory: true
|
|
20
|
+
modelTier: sonnet:high
|
|
21
|
+
thinkingLevel: high
|
|
22
|
+
tools: read, bash, grep
|
|
23
|
+
---
|
|
24
|
+
You are a researcher.`;
|
|
25
|
+
const spec = parseSpec(raw, "researcher", "/tmp/x.md");
|
|
26
|
+
assert.equal(spec.name, "researcher");
|
|
27
|
+
assert.equal(spec.description, "investigates the codebase");
|
|
28
|
+
assert.equal(spec.needsWorktree, false);
|
|
29
|
+
assert.equal(spec.hasMemory, true);
|
|
30
|
+
assert.equal(spec.modelTier, "sonnet:high");
|
|
31
|
+
assert.equal(spec.thinkingLevel, "high");
|
|
32
|
+
assert.deepEqual(spec.tools, ["read", "bash", "grep"]);
|
|
33
|
+
assert.equal(spec.systemPrompt, "You are a researcher.");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("falls back to role name when frontmatter is missing", () => {
|
|
37
|
+
const spec = parseSpec("You are a tester.", "tester", "/tmp/x.md");
|
|
38
|
+
assert.equal(spec.name, "tester");
|
|
39
|
+
assert.equal(spec.systemPrompt, "You are a tester.");
|
|
40
|
+
assert.equal(spec.description, undefined);
|
|
41
|
+
assert.equal(spec.tools, undefined);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("handles single-quoted values", () => {
|
|
45
|
+
const spec = parseSpec(
|
|
46
|
+
`---\nname: 'quoted'\n---\nbody`,
|
|
47
|
+
"fallback",
|
|
48
|
+
"/tmp/x.md",
|
|
49
|
+
);
|
|
50
|
+
assert.equal(spec.name, "quoted");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("loadTeammateSpec", () => {
|
|
55
|
+
test("reads .pi/teammates/<role>.md first", async () => {
|
|
56
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-specs-"));
|
|
57
|
+
try {
|
|
58
|
+
await mkdir(join(dir, ".pi", "teammates"), { recursive: true });
|
|
59
|
+
await writeFile(
|
|
60
|
+
join(dir, ".pi", "teammates", "researcher.md"),
|
|
61
|
+
"---\nname: researcher\n---\nsystem",
|
|
62
|
+
"utf8",
|
|
63
|
+
);
|
|
64
|
+
const spec = await loadTeammateSpec(dir, "researcher");
|
|
65
|
+
assert.ok(spec);
|
|
66
|
+
assert.equal(spec.name, "researcher");
|
|
67
|
+
assert.equal(spec.systemPrompt, "system");
|
|
68
|
+
} finally {
|
|
69
|
+
await rm(dir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("falls back to .claude/teammates/<role>.md", async () => {
|
|
74
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-specs-"));
|
|
75
|
+
try {
|
|
76
|
+
await mkdir(join(dir, ".claude", "teammates"), { recursive: true });
|
|
77
|
+
await writeFile(join(dir, ".claude", "teammates", "tester.md"), "body", "utf8");
|
|
78
|
+
const spec = await loadTeammateSpec(dir, "tester");
|
|
79
|
+
assert.ok(spec);
|
|
80
|
+
assert.equal(spec.systemPrompt, "body");
|
|
81
|
+
} finally {
|
|
82
|
+
await rm(dir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("returns null when no spec exists", async () => {
|
|
87
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-specs-"));
|
|
88
|
+
try {
|
|
89
|
+
assert.equal(await loadTeammateSpec(dir, "missing"), null);
|
|
90
|
+
} finally {
|
|
91
|
+
await rm(dir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("listTeammateSpecs", () => {
|
|
97
|
+
test("de-duplicates specs that exist in both locations", async () => {
|
|
98
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-specs-"));
|
|
99
|
+
try {
|
|
100
|
+
await mkdir(join(dir, ".pi", "teammates"), { recursive: true });
|
|
101
|
+
await mkdir(join(dir, ".claude", "teammates"), { recursive: true });
|
|
102
|
+
await writeFile(join(dir, ".pi", "teammates", "a.md"), "body a pi", "utf8");
|
|
103
|
+
await writeFile(join(dir, ".claude", "teammates", "a.md"), "body a claude", "utf8");
|
|
104
|
+
await writeFile(join(dir, ".claude", "teammates", "b.md"), "body b", "utf8");
|
|
105
|
+
const specs = await listTeammateSpecs(dir);
|
|
106
|
+
const names = specs.map((s) => s.name).sort();
|
|
107
|
+
assert.deepEqual(names, ["a", "b"]);
|
|
108
|
+
const a = specs.find((s) => s.name === "a")!;
|
|
109
|
+
assert.equal(a.systemPrompt, "body a pi", ".pi takes precedence over .claude");
|
|
110
|
+
} finally {
|
|
111
|
+
await rm(dir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { renderTeamMateWidget } from "../ui/widget.ts";
|
|
5
|
+
|
|
6
|
+
describe("renderTeamMateWidget", () => {
|
|
7
|
+
test("renders running rows + queued", () => {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const lines = renderTeamMateWidget(
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
record: {
|
|
13
|
+
id: "a",
|
|
14
|
+
name: "worker",
|
|
15
|
+
isolation: "none",
|
|
16
|
+
cwd: "/tmp",
|
|
17
|
+
status: "running",
|
|
18
|
+
background: false,
|
|
19
|
+
createdAt: new Date(now).toISOString(),
|
|
20
|
+
updatedAt: new Date(now).toISOString(),
|
|
21
|
+
},
|
|
22
|
+
metrics: {
|
|
23
|
+
turns: 1,
|
|
24
|
+
toolUses: 2,
|
|
25
|
+
tokens: 1234,
|
|
26
|
+
startedAt: now - 1200,
|
|
27
|
+
activityHint: "editing files…",
|
|
28
|
+
},
|
|
29
|
+
transcriptPath: "/tmp/s.jsonl",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
[],
|
|
33
|
+
2,
|
|
34
|
+
now,
|
|
35
|
+
0,
|
|
36
|
+
);
|
|
37
|
+
assert.equal(lines[0], "● Agents");
|
|
38
|
+
assert.match(lines.join("\n"), /worker/);
|
|
39
|
+
assert.match(lines.join("\n"), /2 queued/);
|
|
40
|
+
});
|
|
41
|
+
});
|