pi-extended-teams 2.1.16

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 (130) hide show
  1. package/README.md +105 -0
  2. package/assets/pi-extended-teams-agent-navigation.png +0 -0
  3. package/assets/pi-extended-teams-in-action.png +0 -0
  4. package/extensions/agents/read-agent-report.ts +181 -0
  5. package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
  6. package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
  7. package/extensions/agents/read-agent.test.ts +3077 -0
  8. package/extensions/agents/read-agent.ts +1119 -0
  9. package/extensions/agents/write-agent.test.ts +513 -0
  10. package/extensions/agents/write-agent.ts +392 -0
  11. package/extensions/events/register-events.test.ts +465 -0
  12. package/extensions/events/register-events.ts +409 -0
  13. package/extensions/index.test.ts +1659 -0
  14. package/extensions/index.ts +1228 -0
  15. package/extensions/internal/agent-session-files.test.ts +164 -0
  16. package/extensions/internal/agent-session-files.ts +320 -0
  17. package/extensions/internal/debug.ts +44 -0
  18. package/extensions/internal/model-selection.ts +82 -0
  19. package/extensions/internal/pi-command.test.ts +191 -0
  20. package/extensions/internal/pi-command.ts +224 -0
  21. package/extensions/internal/pi-runtime-api.test.ts +43 -0
  22. package/extensions/internal/pi-runtime-api.ts +78 -0
  23. package/extensions/internal/schema.ts +19 -0
  24. package/extensions/internal/session-context-reference.test.ts +290 -0
  25. package/extensions/internal/session-context-reference.ts +430 -0
  26. package/extensions/internal/session-files.test.ts +225 -0
  27. package/extensions/internal/session-files.ts +273 -0
  28. package/extensions/internal/session-usage.ts +59 -0
  29. package/extensions/resources/spawn-resource-plan.test.ts +233 -0
  30. package/extensions/resources/spawn-resource-plan.ts +247 -0
  31. package/extensions/runtime/active-agent-sleep.test.ts +157 -0
  32. package/extensions/runtime/active-agent-sleep.ts +117 -0
  33. package/extensions/runtime/nested-read-agents.ts +22 -0
  34. package/extensions/runtime/pending-child-controller.test.ts +169 -0
  35. package/extensions/runtime/pending-child-controller.ts +280 -0
  36. package/extensions/runtime/types.ts +68 -0
  37. package/extensions/team/contracts.test.ts +111 -0
  38. package/extensions/team/lifecycle.test.ts +1095 -0
  39. package/extensions/team/lifecycle.ts +502 -0
  40. package/extensions/team/recipient-closure.test.ts +97 -0
  41. package/extensions/team/recipient-closure.ts +120 -0
  42. package/extensions/team/roster.test.ts +129 -0
  43. package/extensions/team/roster.ts +165 -0
  44. package/extensions/team/team-contracts.json +43 -0
  45. package/extensions/team/writer-screens.test.ts +50 -0
  46. package/extensions/team/writer-screens.ts +156 -0
  47. package/extensions/tools/agent-communication-tools.test.ts +306 -0
  48. package/extensions/tools/agent-communication-tools.ts +183 -0
  49. package/extensions/tools/coordination-tools.test.ts +670 -0
  50. package/extensions/tools/coordination-tools.ts +304 -0
  51. package/extensions/tools/delegation-guard.test.ts +95 -0
  52. package/extensions/tools/delegation-guard.ts +65 -0
  53. package/extensions/tools/file-claim-tools.test.ts +104 -0
  54. package/extensions/tools/file-claim-tools.ts +68 -0
  55. package/extensions/tools/model-tools.ts +53 -0
  56. package/extensions/tools/predefined-tools.test.ts +647 -0
  57. package/extensions/tools/predefined-tools.ts +331 -0
  58. package/extensions/tools/read-helper.test.ts +35 -0
  59. package/extensions/tools/task-runtime-tools.test.ts +334 -0
  60. package/extensions/tools/task-runtime-tools.ts +227 -0
  61. package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
  62. package/extensions/tools/team-tools.ts +1270 -0
  63. package/extensions/ui/agent-follow-view.test.ts +520 -0
  64. package/extensions/ui/agent-follow-view.ts +779 -0
  65. package/extensions/ui/agent-navigation.test.ts +52 -0
  66. package/extensions/ui/agent-navigation.ts +55 -0
  67. package/extensions/ui/ansi.ts +16 -0
  68. package/extensions/ui/extensions-command.test.ts +336 -0
  69. package/extensions/ui/extensions-command.ts +282 -0
  70. package/extensions/ui/favorite-models-command.test.ts +280 -0
  71. package/extensions/ui/favorite-models-command.ts +423 -0
  72. package/extensions/ui/frame.ts +77 -0
  73. package/extensions/ui/input.ts +17 -0
  74. package/extensions/ui/read-agent-status.test.ts +117 -0
  75. package/extensions/ui/read-agent-status.ts +125 -0
  76. package/extensions/ui/renderers.ts +243 -0
  77. package/extensions/ui/status-widget.test.ts +143 -0
  78. package/extensions/ui/status-widget.ts +440 -0
  79. package/extensions/ui-frame.test.ts +69 -0
  80. package/package.json +75 -0
  81. package/skills/teams.md +256 -0
  82. package/src/adapters/terminal-registry.ts +78 -0
  83. package/src/adapters/tmux-adapter.test.ts +276 -0
  84. package/src/adapters/tmux-adapter.ts +200 -0
  85. package/src/orchestration/index.ts +437 -0
  86. package/src/orchestration/orchestrator.test.ts +396 -0
  87. package/src/orchestration/types.ts +125 -0
  88. package/src/utils/atomic-json.ts +21 -0
  89. package/src/utils/claims.test.ts +137 -0
  90. package/src/utils/claims.ts +169 -0
  91. package/src/utils/hooks.test.ts +75 -0
  92. package/src/utils/hooks.ts +35 -0
  93. package/src/utils/lifecycle-tombstone.test.ts +105 -0
  94. package/src/utils/lifecycle-tombstone.ts +268 -0
  95. package/src/utils/lock.race.child.ts +44 -0
  96. package/src/utils/lock.race.test.ts +198 -0
  97. package/src/utils/lock.test.ts +90 -0
  98. package/src/utils/lock.ts +186 -0
  99. package/src/utils/messaging.test.ts +337 -0
  100. package/src/utils/messaging.ts +441 -0
  101. package/src/utils/model-resolution.test.ts +231 -0
  102. package/src/utils/model-resolution.ts +322 -0
  103. package/src/utils/models.test.ts +8 -0
  104. package/src/utils/models.ts +115 -0
  105. package/src/utils/paths.ts +81 -0
  106. package/src/utils/predefined-teams/types.ts +48 -0
  107. package/src/utils/predefined-teams.save-template.test.ts +74 -0
  108. package/src/utils/predefined-teams.test.ts +441 -0
  109. package/src/utils/predefined-teams.ts +471 -0
  110. package/src/utils/read-helper-queue.ts +99 -0
  111. package/src/utils/report-events.test.ts +154 -0
  112. package/src/utils/report-events.ts +222 -0
  113. package/src/utils/runtime.test.ts +314 -0
  114. package/src/utils/runtime.ts +261 -0
  115. package/src/utils/security.test.ts +43 -0
  116. package/src/utils/settings.test.ts +480 -0
  117. package/src/utils/settings.ts +645 -0
  118. package/src/utils/shared-memory.test.ts +80 -0
  119. package/src/utils/shared-memory.ts +81 -0
  120. package/src/utils/tasks.race.test.ts +44 -0
  121. package/src/utils/tasks.test.ts +229 -0
  122. package/src/utils/tasks.ts +396 -0
  123. package/src/utils/teams.ts +231 -0
  124. package/src/utils/terminal-adapter.ts +103 -0
  125. package/src/utils/thinking-levels.test.ts +56 -0
  126. package/src/utils/thinking-levels.ts +47 -0
  127. package/src/utils/workflow-metadata.test.ts +11 -0
  128. package/src/utils/workflow-metadata.ts +88 -0
  129. package/src/utils/write-queue.test.ts +251 -0
  130. package/src/utils/write-queue.ts +202 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Terminal Adapter Interface
3
+ *
4
+ * Abstracts tmux window/pane operations to provide a unified API for spawning,
5
+ * managing, and terminating writer screens. pi-extended-teams is tmux-only.
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process";
9
+
10
+ /**
11
+ * Options for spawning a new terminal pane
12
+ */
13
+ export interface SpawnOptions {
14
+ /** Name/identifier for the background tmux window/pane */
15
+ name: string;
16
+ /** Working directory for the new pane */
17
+ cwd: string;
18
+ /** Command to execute in the pane */
19
+ command: string;
20
+ /** Environment variables to set (key-value pairs) */
21
+ env: Record<string, string>;
22
+ /** Optional pane ID used to create the background window in the same tmux session */
23
+ anchorPaneId?: string;
24
+ }
25
+
26
+ /**
27
+ * Terminal Adapter Interface
28
+ *
29
+ * Implementations provide tmux-specific logic for writer screen management.
30
+ */
31
+ export interface TerminalAdapter {
32
+ /** Unique name identifier for this terminal type */
33
+ readonly name: string;
34
+
35
+ /**
36
+ * Detect if this terminal is currently available/active.
37
+ *
38
+ * @returns true if this terminal should be used
39
+ */
40
+ detect(): boolean;
41
+
42
+ /**
43
+ * Spawn a new background terminal screen with the given options.
44
+ *
45
+ * @param options - Spawn configuration
46
+ * @returns Pane ID that can be used for subsequent operations
47
+ * @throws Error if spawn fails
48
+ */
49
+ spawn(options: SpawnOptions): string;
50
+
51
+ /**
52
+ * Return the current tmux pane ID when available.
53
+ */
54
+ getCurrentPaneId?(): string | null;
55
+
56
+ /**
57
+ * Return the tmux window ID for a pane when available.
58
+ */
59
+ getWindowIdForPane?(paneId: string | null | undefined): string | null;
60
+
61
+ /**
62
+ * Focus/select a tmux pane so the user can watch it live.
63
+ *
64
+ * @returns true when the pane was selected
65
+ */
66
+ focusPane?(paneId: string): boolean;
67
+
68
+ /**
69
+ * Kill/terminate a terminal pane.
70
+ * Should be idempotent - no error if pane doesn't exist.
71
+ *
72
+ * @param paneId - The pane ID returned from spawn()
73
+ */
74
+ kill(paneId: string): void;
75
+
76
+ /**
77
+ * Check if a terminal pane is still alive/active.
78
+ *
79
+ * @param paneId - The pane ID returned from spawn()
80
+ * @returns true if pane exists and is active
81
+ */
82
+ isAlive(paneId: string): boolean;
83
+
84
+ /**
85
+ * Set the title of the current terminal pane.
86
+ * Used for identifying panes/windows in the terminal UI.
87
+ *
88
+ * @param title - The title to set
89
+ */
90
+ setTitle(title: string): void;
91
+ }
92
+
93
+ /**
94
+ * Base helper for adapters to execute commands synchronously.
95
+ */
96
+ export function execCommand(command: string, args: string[]): { stdout: string; stderr: string; status: number | null } {
97
+ const result = spawnSync(command, args, { encoding: "utf-8" });
98
+ return {
99
+ stdout: result.stdout?.toString() ?? "",
100
+ stderr: result.stderr?.toString() ?? "",
101
+ status: result.status,
102
+ };
103
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ clampThinkingLevel,
4
+ getSupportedThinkingLevels,
5
+ THINKING_LEVEL_NAMES,
6
+ } from "./thinking-levels";
7
+
8
+ describe("Pi model thinking capabilities", () => {
9
+ it("includes max in the canonical thinking-level list", () => {
10
+ expect(THINKING_LEVEL_NAMES).toEqual([
11
+ "off",
12
+ "minimal",
13
+ "low",
14
+ "medium",
15
+ "high",
16
+ "xhigh",
17
+ "max",
18
+ ]);
19
+ });
20
+
21
+ it("exposes only off for non-reasoning models", () => {
22
+ expect(getSupportedThinkingLevels({ reasoning: false })).toEqual(["off"]);
23
+ });
24
+
25
+ it("provides standard levels by default and keeps xhigh/max opt-in", () => {
26
+ expect(getSupportedThinkingLevels({ reasoning: true })).toEqual([
27
+ "off",
28
+ "minimal",
29
+ "low",
30
+ "medium",
31
+ "high",
32
+ ]);
33
+ });
34
+
35
+ it("uses non-null model mappings to expose xhigh and max and null to hide levels", () => {
36
+ expect(getSupportedThinkingLevels({
37
+ reasoning: true,
38
+ thinkingLevelMap: {
39
+ minimal: null,
40
+ xhigh: "xhigh",
41
+ max: "max",
42
+ },
43
+ })).toEqual(["off", "low", "medium", "high", "xhigh", "max"]);
44
+ });
45
+
46
+ it("matches Pi's nearest-supported fallback order", () => {
47
+ const standardModel = { reasoning: true };
48
+ const maxWithoutXhigh = {
49
+ reasoning: true,
50
+ thinkingLevelMap: { high: null, xhigh: null, max: "max" },
51
+ };
52
+
53
+ expect(clampThinkingLevel(standardModel, "max")).toBe("high");
54
+ expect(clampThinkingLevel(maxWithoutXhigh, "high")).toBe("max");
55
+ });
56
+ });
@@ -0,0 +1,47 @@
1
+ export const THINKING_LEVEL_NAMES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
2
+
3
+ export type ThinkingLevelName = (typeof THINKING_LEVEL_NAMES)[number];
4
+
5
+ export interface ThinkingCapableModel {
6
+ reasoning?: boolean;
7
+ thinkingLevelMap?: Partial<Record<ThinkingLevelName, string | null>>;
8
+ }
9
+
10
+ /**
11
+ * Mirror Pi's model-capability rules for thinking levels.
12
+ *
13
+ * Standard levels through `high` are available by default for reasoning models.
14
+ * `xhigh` and `max` are opt-in and require a non-null mapping on the model.
15
+ * Any level explicitly mapped to `null` is unsupported.
16
+ */
17
+ export function getSupportedThinkingLevels(model: ThinkingCapableModel): ThinkingLevelName[] {
18
+ if (!model.reasoning) return ["off"];
19
+
20
+ return THINKING_LEVEL_NAMES.filter((level) => {
21
+ const mapped = model.thinkingLevelMap?.[level];
22
+ if (mapped === null) return false;
23
+ if (level === "xhigh" || level === "max") return mapped !== undefined;
24
+ return true;
25
+ });
26
+ }
27
+
28
+ /** Match Pi's nearest-supported fallback: requested, then higher, then lower. */
29
+ export function clampThinkingLevel(
30
+ model: ThinkingCapableModel,
31
+ requested: ThinkingLevelName,
32
+ ): ThinkingLevelName {
33
+ const supported = getSupportedThinkingLevels(model);
34
+ if (supported.includes(requested)) return requested;
35
+
36
+ const requestedIndex = THINKING_LEVEL_NAMES.indexOf(requested);
37
+ for (let index = requestedIndex + 1; index < THINKING_LEVEL_NAMES.length; index += 1) {
38
+ const candidate = THINKING_LEVEL_NAMES[index];
39
+ if (supported.includes(candidate)) return candidate;
40
+ }
41
+ for (let index = requestedIndex - 1; index >= 0; index -= 1) {
42
+ const candidate = THINKING_LEVEL_NAMES[index];
43
+ if (supported.includes(candidate)) return candidate;
44
+ }
45
+
46
+ return supported[0] ?? "off";
47
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "./workflow-metadata";
3
+
4
+ describe("lead report injection policy", () => {
5
+ it("suppresses pi-prompt writer reports consumed through the private event channel", () => {
6
+ expect(isPiPromptPlanningMember({ metadata: { piPromptPlanning: { version: 1, correlation: "private" } } } as never)).toBe(true);
7
+ expect(shouldSuppressLeadReportInjection({ metadata: { piPromptPlanning: { version: 1, correlation: "private" } } } as never)).toBe(true);
8
+ expect(shouldSuppressLeadReportInjection({ metadata: { piPromptPlanning: { version: 2 } } } as never)).toBe(false);
9
+ expect(shouldSuppressLeadReportInjection({ metadata: {} } as never)).toBe(false);
10
+ });
11
+ });
@@ -0,0 +1,88 @@
1
+ import type { Member } from "./models";
2
+
3
+ export type MemberMetadataLike = Pick<Member, "metadata"> | null | undefined;
4
+
5
+ function asRecord(value: unknown): Record<string, any> | undefined {
6
+ return value && typeof value === "object" && !Array.isArray(value)
7
+ ? value as Record<string, any>
8
+ : undefined;
9
+ }
10
+
11
+ function stringValue(value: unknown): string | undefined {
12
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
13
+ }
14
+
15
+ function booleanTrue(value: unknown): boolean {
16
+ return value === true;
17
+ }
18
+
19
+ function stringList(value: unknown): string[] | undefined {
20
+ if (!Array.isArray(value)) return undefined;
21
+ const values = value.map(item => stringValue(item)).filter((item): item is string => !!item);
22
+ return values.length > 0 ? values : [];
23
+ }
24
+
25
+ export function workflowRunIdFromMetadata(metadata: Record<string, any> | undefined): string | undefined {
26
+ const orchestration = asRecord(metadata?.orchestration);
27
+ return stringValue(metadata?.workflowRunId) || stringValue(orchestration?.workflowRunId);
28
+ }
29
+
30
+ export function operationIdFromMetadata(metadata: Record<string, any> | undefined): string | undefined {
31
+ const orchestration = asRecord(metadata?.orchestration);
32
+ return stringValue(metadata?.operationId) || stringValue(orchestration?.operationId);
33
+ }
34
+
35
+ export function memberWorkflowRunId(member: MemberMetadataLike): string | undefined {
36
+ return workflowRunIdFromMetadata(member?.metadata);
37
+ }
38
+
39
+ export function memberOperationId(member: MemberMetadataLike): string | undefined {
40
+ return operationIdFromMetadata(member?.metadata);
41
+ }
42
+
43
+ export function isWorkflowSpawnedMember(member: MemberMetadataLike): boolean {
44
+ return !!memberWorkflowRunId(member);
45
+ }
46
+
47
+ export function workflowPolicyForMember(member: MemberMetadataLike): Record<string, any> {
48
+ const metadata = member?.metadata;
49
+ const orchestration = asRecord(metadata?.orchestration);
50
+ return asRecord(metadata?.workflowPolicy)
51
+ || asRecord(metadata?.workflow)
52
+ || asRecord(orchestration?.workflowPolicy)
53
+ || asRecord(orchestration?.workflow)
54
+ || {};
55
+ }
56
+
57
+ export function workflowAllowsReadHelper(member: MemberMetadataLike): boolean {
58
+ if (!isWorkflowSpawnedMember(member)) return true;
59
+ const policy = workflowPolicyForMember(member);
60
+ return booleanTrue(policy.allowReadHelper) || booleanTrue(policy.allow_read_helper);
61
+ }
62
+
63
+ export function workflowAllowedSkills(member: MemberMetadataLike): string[] | true | null {
64
+ if (!isWorkflowSpawnedMember(member)) return true;
65
+ const policy = workflowPolicyForMember(member);
66
+ if (booleanTrue(policy.allowSkills) || booleanTrue(policy.allow_skills)) return true;
67
+ return stringList(policy.allowedSkills)
68
+ ?? stringList(policy.allowed_skills)
69
+ ?? stringList(policy.declaredSkills)
70
+ ?? stringList(policy.declared_skills)
71
+ ?? stringList(policy.skills)
72
+ ?? null;
73
+ }
74
+
75
+ export function workflowAllowsSkill(member: MemberMetadataLike, skillName: string): boolean {
76
+ const allowed = workflowAllowedSkills(member);
77
+ if (allowed === true) return true;
78
+ if (!allowed) return false;
79
+ return allowed.includes(skillName);
80
+ }
81
+
82
+ export function isPiPromptPlanningMember(member: MemberMetadataLike): boolean {
83
+ return asRecord(member?.metadata?.piPromptPlanning)?.version === 1;
84
+ }
85
+
86
+ export function shouldSuppressLeadReportInjection(member: MemberMetadataLike): boolean {
87
+ return isWorkflowSpawnedMember(member) || isPiPromptPlanningMember(member);
88
+ }
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import * as paths from "./paths";
6
+ import {
7
+ cancelQueuedWriteSpawn,
8
+ dequeueWriteSpawn,
9
+ dequeueWriteSpawns,
10
+ enqueueWriteSpawn,
11
+ findQueuedWriteSpawn,
12
+ listWriteQueue,
13
+ removeQueuedWriteSpawnsByName,
14
+ queuedWriteSpawnToMember,
15
+ } from "./write-queue";
16
+
17
+ type EnqueueWriteSpawnRequest = Parameters<typeof enqueueWriteSpawn>[1];
18
+
19
+ const testDir = path.join(os.tmpdir(), "pi-extended-teams-write-queue-" + Date.now());
20
+ const queuePath = path.join(testDir, "write-queue.json");
21
+ const HIGH_VOLUME_WRITER_COUNT = 350;
22
+ const HIGH_VOLUME_STAGE_BUDGET_MS = 5_000;
23
+ const HIGH_VOLUME_TEST_TIMEOUT_MS = 15_000;
24
+
25
+ function highVolumeWriterId(index: number): string {
26
+ return `writer-${String(index).padStart(3, "0")}`;
27
+ }
28
+
29
+ function highVolumeWriteSpawn(index: number): EnqueueWriteSpawnRequest {
30
+ const id = highVolumeWriterId(index);
31
+ return {
32
+ id,
33
+ name: id,
34
+ prompt: `Implement high-volume shard ${index}`,
35
+ cwd: testDir,
36
+ category: index % 2 === 0 ? "feature" : undefined,
37
+ modelSlot: "writing-hard",
38
+ planModeRequired: index % 2 === 0,
39
+ color: index % 2 === 0 ? "green" : "blue",
40
+ operationId: `scale-op-${String(index).padStart(3, "0")}`,
41
+ workflowRunId: "scale-run",
42
+ metadata: { slot: index, fanout: HIGH_VOLUME_WRITER_COUNT },
43
+ requestedAt: index + 1,
44
+ };
45
+ }
46
+
47
+ function writeFavoriteLevels() {
48
+ const settingsPath = path.join(testDir, ".pi", "agent", "pi-extended-teams", "settings.json");
49
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
50
+ fs.writeFileSync(settingsPath, JSON.stringify({
51
+ favoriteModels: {
52
+ "read-review": { model: "provider/model", thinking: "xhigh" },
53
+ "writing-basic": { model: "provider/model", thinking: "high" },
54
+ "writing-hard": { model: "provider/model", thinking: "xhigh" },
55
+ },
56
+ }));
57
+ }
58
+
59
+ describe("write queue utilities", () => {
60
+ beforeEach(() => {
61
+ if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
62
+ fs.mkdirSync(testDir, { recursive: true });
63
+ vi.spyOn(paths, "writeQueuePath").mockReturnValue(queuePath);
64
+ vi.spyOn(os, "homedir").mockReturnValue(testDir);
65
+ writeFavoriteLevels();
66
+ });
67
+
68
+ afterEach(() => {
69
+ vi.restoreAllMocks();
70
+ if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
71
+ });
72
+
73
+ it("queues and dequeues write spawns in FIFO order", async () => {
74
+ const first = await enqueueWriteSpawn("team", {
75
+ id: "first",
76
+ name: "writer-a",
77
+ prompt: "A",
78
+ cwd: testDir,
79
+ modelSlot: "writing-basic",
80
+ requestedAt: 1,
81
+ });
82
+ const second = await enqueueWriteSpawn("team", {
83
+ id: "second",
84
+ name: "writer-b",
85
+ prompt: "B",
86
+ cwd: testDir,
87
+ modelSlot: "writing-hard",
88
+ requestedAt: 2,
89
+ });
90
+
91
+ expect((await listWriteQueue("team")).map(item => item.id)).toEqual([first.id, second.id]);
92
+ expect((await dequeueWriteSpawn("team"))?.id).toBe("first");
93
+ expect((await dequeueWriteSpawn("team"))?.id).toBe("second");
94
+ expect(await dequeueWriteSpawn("team")).toBeNull();
95
+ });
96
+
97
+ it("handles a high configured writer capacity without dropping order or metadata", async () => {
98
+ const enqueueStartedAt = Date.now();
99
+ await Promise.all(Array.from({ length: HIGH_VOLUME_WRITER_COUNT }, (_value, index) => (
100
+ enqueueWriteSpawn("team", highVolumeWriteSpawn(index))
101
+ )));
102
+ const enqueueMs = Date.now() - enqueueStartedAt;
103
+
104
+ const queue = await listWriteQueue("team");
105
+ expect(queue).toHaveLength(HIGH_VOLUME_WRITER_COUNT);
106
+ expect(queue.slice(0, 3).map(item => item.id)).toEqual([
107
+ highVolumeWriterId(0),
108
+ highVolumeWriterId(1),
109
+ highVolumeWriterId(2),
110
+ ]);
111
+ expect(queue.at(-1)).toMatchObject({
112
+ id: highVolumeWriterId(HIGH_VOLUME_WRITER_COUNT - 1),
113
+ name: highVolumeWriterId(HIGH_VOLUME_WRITER_COUNT - 1),
114
+ requestedAt: HIGH_VOLUME_WRITER_COUNT,
115
+ metadata: { fanout: HIGH_VOLUME_WRITER_COUNT },
116
+ });
117
+
118
+ const middleIndex = Math.floor(HIGH_VOLUME_WRITER_COUNT / 2);
119
+ const middle = await findQueuedWriteSpawn("team", {
120
+ operationId: `scale-op-${String(middleIndex).padStart(3, "0")}`,
121
+ workflowRunId: "scale-run",
122
+ });
123
+ expect(middle).toMatchObject({
124
+ id: highVolumeWriterId(middleIndex),
125
+ metadata: { slot: middleIndex, fanout: HIGH_VOLUME_WRITER_COUNT },
126
+ });
127
+ expect(enqueueMs).toBeLessThan(HIGH_VOLUME_STAGE_BUDGET_MS);
128
+
129
+ const dequeueStartedAt = Date.now();
130
+ const drainedIds: string[] = [];
131
+ for (let index = 0; index < HIGH_VOLUME_WRITER_COUNT; index++) {
132
+ const queued = await dequeueWriteSpawn("team");
133
+ expect(queued).not.toBeNull();
134
+ drainedIds.push(queued!.id);
135
+ }
136
+ const dequeueMs = Date.now() - dequeueStartedAt;
137
+
138
+ expect(drainedIds[0]).toBe(highVolumeWriterId(0));
139
+ expect(drainedIds.at(-1)).toBe(highVolumeWriterId(HIGH_VOLUME_WRITER_COUNT - 1));
140
+ expect(await dequeueWriteSpawn("team")).toBeNull();
141
+ expect(dequeueMs).toBeLessThan(HIGH_VOLUME_STAGE_BUDGET_MS);
142
+ }, HIGH_VOLUME_TEST_TIMEOUT_MS);
143
+
144
+ it("deduplicates concurrent queued writers by name under the queue lock", async () => {
145
+ const results = await Promise.all(Array.from({ length: 50 }, (_value, index) => (
146
+ enqueueWriteSpawn("team", {
147
+ id: `duplicate-${index}`,
148
+ name: "writer-a",
149
+ prompt: `work ${index}`,
150
+ cwd: testDir,
151
+ modelSlot: "writing-basic",
152
+ })
153
+ )));
154
+
155
+ const queue = await listWriteQueue("team");
156
+ expect(queue).toHaveLength(1);
157
+ expect(new Set(results.map(item => item.id))).toEqual(new Set([queue[0].id]));
158
+ });
159
+
160
+ it("dequeues batches atomically without duplicating or dropping concurrent drains", async () => {
161
+ for (let index = 0; index < 10; index++) {
162
+ await enqueueWriteSpawn("team", highVolumeWriteSpawn(index));
163
+ }
164
+
165
+ const [first, second] = await Promise.all([
166
+ dequeueWriteSpawns("team", 7),
167
+ dequeueWriteSpawns("team", 7),
168
+ ]);
169
+ const drainedIds = [...first, ...second].map(item => item.id);
170
+
171
+ expect(drainedIds).toHaveLength(10);
172
+ expect(new Set(drainedIds).size).toBe(10);
173
+ expect(drainedIds.sort()).toEqual(Array.from({ length: 10 }, (_value, index) => highVolumeWriterId(index)).sort());
174
+ expect(await listWriteQueue("team")).toEqual([]);
175
+ });
176
+
177
+ it("cancels a queued writer by id", async () => {
178
+ await enqueueWriteSpawn("team", {
179
+ id: "keep",
180
+ name: "writer-a",
181
+ prompt: "A",
182
+ cwd: testDir,
183
+ modelSlot: "writing-basic",
184
+ });
185
+ const canceled = await enqueueWriteSpawn("team", {
186
+ id: "cancel-me",
187
+ name: "writer-b",
188
+ prompt: "B",
189
+ cwd: testDir,
190
+ modelSlot: "writing-hard",
191
+ });
192
+
193
+ expect(await cancelQueuedWriteSpawn("team", canceled.id)).toEqual(canceled);
194
+ expect((await listWriteQueue("team")).map(item => item.id)).toEqual(["keep"]);
195
+ expect(await cancelQueuedWriteSpawn("team", "missing")).toBeNull();
196
+ });
197
+
198
+ it("removes all queued writers by teammate name, including legacy duplicates", async () => {
199
+ fs.writeFileSync(queuePath, JSON.stringify([
200
+ { id: "one", name: "writer-a", prompt: "A", cwd: testDir, modelSlot: "writing-basic", requestedAt: 1 },
201
+ { id: "two", name: "writer-a", prompt: "B", cwd: testDir, modelSlot: "writing-hard", requestedAt: 2 },
202
+ { id: "three", name: "writer-b", prompt: "C", cwd: testDir, modelSlot: "writing-basic", requestedAt: 3 },
203
+ ]));
204
+
205
+ expect((await removeQueuedWriteSpawnsByName("team", "writer-a")).map(item => item.id)).toEqual(["one", "two"]);
206
+ expect((await listWriteQueue("team")).map(item => item.id)).toEqual(["three"]);
207
+ });
208
+
209
+ it("uses canonical write-* intent-tier wording for read-tier validation failures", () => {
210
+ expect(() => queuedWriteSpawnToMember("team", {
211
+ id: "queued-read",
212
+ name: "reader-a",
213
+ prompt: "Inspect it",
214
+ cwd: testDir,
215
+ modelSlot: "read-review",
216
+ requestedAt: 1,
217
+ })).toThrow(
218
+ "Queued writer reader-a requires a write-* intent tier configured via /agents-favorite-models, got read-review."
219
+ );
220
+ });
221
+
222
+ it("converts a queued spawn into a write member", () => {
223
+ const member = queuedWriteSpawnToMember("team", {
224
+ id: "queued",
225
+ name: "writer-a",
226
+ prompt: "Implement it",
227
+ cwd: testDir,
228
+ category: "feature",
229
+ modelSlot: "writing-hard",
230
+ planModeRequired: true,
231
+ requestedAt: 1,
232
+ });
233
+
234
+ expect(member).toMatchObject({
235
+ agentId: "writer-a@team",
236
+ name: "writer-a",
237
+ agentType: "teammate",
238
+ role: "write",
239
+ category: "feature",
240
+ model: "provider/model",
241
+ modelSlot: "write-system",
242
+ cwd: testDir,
243
+ prompt: "Implement it",
244
+ color: "blue",
245
+ thinking: "xhigh",
246
+ planModeRequired: true,
247
+ tmuxPaneId: "",
248
+ });
249
+ expect(member.joinedAt).toEqual(expect.any(Number));
250
+ });
251
+ });