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.
Files changed (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Pi Team-Mode — Worktree Tests
3
+ *
4
+ * Skipped when git is unavailable. Creates a throwaway repo under tmpdir.
5
+ */
6
+
7
+ import assert from "node:assert/strict";
8
+ import { spawnSync } from "node:child_process";
9
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { describe, test } from "node:test";
13
+
14
+ import { cleanupWorktree, createWorktree } from "../runtime/worktree.ts";
15
+
16
+ function hasGit(): boolean {
17
+ const result = spawnSync("git", ["--version"], { stdio: "ignore" });
18
+ return result.status === 0;
19
+ }
20
+
21
+ function git(cwd: string, args: string[]): void {
22
+ const result = spawnSync("git", args, { cwd, stdio: "ignore" });
23
+ assert.equal(result.status, 0, `git ${args.join(" ")} failed`);
24
+ }
25
+
26
+ async function makeRepo(): Promise<string> {
27
+ const dir = await mkdtemp(join(tmpdir(), "team-mode-wt-"));
28
+ git(dir, ["init", "-q", "-b", "main"]);
29
+ git(dir, ["config", "user.email", "test@example.com"]);
30
+ git(dir, ["config", "user.name", "test"]);
31
+ await writeFile(join(dir, "README.md"), "hello", "utf8");
32
+ git(dir, ["add", "."]);
33
+ git(dir, ["commit", "-q", "-m", "init"]);
34
+ return dir;
35
+ }
36
+
37
+ describe("worktree lifecycle", () => {
38
+ test("clean worktree is removed", { skip: !hasGit() }, async () => {
39
+ const repo = await makeRepo();
40
+ try {
41
+ const handle = await createWorktree(repo);
42
+ const result = await cleanupWorktree(handle);
43
+ assert.equal(result.removed, true);
44
+ } finally {
45
+ await rm(repo, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test("dirty worktree is retained with path + branch", { skip: !hasGit() }, async () => {
50
+ const repo = await makeRepo();
51
+ try {
52
+ const handle = await createWorktree(repo);
53
+ await writeFile(join(handle.path, "new-file.txt"), "changes", "utf8");
54
+ const result = await cleanupWorktree(handle);
55
+ assert.equal(result.removed, false);
56
+ if (!result.removed) {
57
+ assert.equal(result.path, handle.path);
58
+ assert.equal(result.branch, handle.branch);
59
+ }
60
+ } finally {
61
+ await rm(repo, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test("worktree with only committed changes is retained", { skip: !hasGit() }, async () => {
66
+ const repo = await makeRepo();
67
+ try {
68
+ const handle = await createWorktree(repo);
69
+ await writeFile(join(handle.path, "new.txt"), "committed", "utf8");
70
+ git(handle.path, ["add", "."]);
71
+ git(handle.path, ["commit", "-q", "-m", "teammate work"]);
72
+ const result = await cleanupWorktree(handle);
73
+ assert.equal(result.removed, false);
74
+ } finally {
75
+ await rm(repo, { recursive: true, force: true });
76
+ }
77
+ });
78
+ });
@@ -0,0 +1,90 @@
1
+ // Pi Team-Mode — chain step template/file utilities
2
+
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+
7
+ import type { ExecutionRuntime, IsolationMode, SpawnOpts } from "./types.js";
8
+
9
+ export type DelegateTask = SpawnOpts & {
10
+ count?: number;
11
+ output?: string | false;
12
+ reads?: string[] | false;
13
+ };
14
+
15
+ export type DelegateChainParallelStep = {
16
+ parallel: DelegateTask[];
17
+ concurrency?: number;
18
+ failFast?: boolean;
19
+ isolation?: IsolationMode;
20
+ runtime?: ExecutionRuntime;
21
+ };
22
+
23
+ export type DelegateChainStep = DelegateTask | DelegateChainParallelStep;
24
+
25
+ export function isParallelChainStep(step: DelegateChainStep): step is DelegateChainParallelStep {
26
+ return "parallel" in step;
27
+ }
28
+
29
+ export async function createChainDir(runId: string): Promise<string> {
30
+ const dir = path.join(os.tmpdir(), "team-mode-chains", runId);
31
+ await mkdir(dir, { recursive: true });
32
+ return dir;
33
+ }
34
+
35
+ export function applyTemplate(
36
+ template: string,
37
+ vars: { task: string; previous: string; chainDir: string },
38
+ ): string {
39
+ return template
40
+ .replaceAll("{task}", vars.task)
41
+ .replaceAll("{previous}", vars.previous)
42
+ .replaceAll("{chain_dir}", vars.chainDir);
43
+ }
44
+
45
+ export function resolveChainPath(chainDir: string, file: string): string {
46
+ if (path.isAbsolute(file)) return file;
47
+ return path.resolve(chainDir, file);
48
+ }
49
+
50
+ export async function readStepInputs(
51
+ chainDir: string,
52
+ reads: string[] | false | undefined,
53
+ ): Promise<string> {
54
+ if (!reads || reads.length === 0) return "";
55
+ const parts: string[] = [];
56
+ for (const file of reads) {
57
+ const resolved = resolveChainPath(chainDir, file);
58
+ const content = await readFile(resolved, "utf8");
59
+ parts.push(`--- ${file} ---\n${content}`);
60
+ }
61
+ if (parts.length === 0) return "";
62
+ return `${parts.join("\n\n")}\n\n`;
63
+ }
64
+
65
+ export async function writeStepOutput(
66
+ chainDir: string,
67
+ output: string | false | undefined,
68
+ text: string,
69
+ ): Promise<string | undefined> {
70
+ if (!output) return undefined;
71
+ const resolved = resolveChainPath(chainDir, output);
72
+ await mkdir(path.dirname(resolved), { recursive: true });
73
+ await writeFile(resolved, text, "utf8");
74
+ return resolved;
75
+ }
76
+
77
+ export function expandCountedTasks(tasks: DelegateTask[]): DelegateTask[] {
78
+ const expanded: DelegateTask[] = [];
79
+ for (const task of tasks) {
80
+ const count = Math.max(1, Math.floor(task.count ?? 1));
81
+ if (count === 1) {
82
+ expanded.push({ ...task, count: undefined });
83
+ continue;
84
+ }
85
+ for (let i = 0; i < count; i += 1) {
86
+ expanded.push({ ...task, count: undefined, name: task.name ? `${task.name}-${i + 1}` : undefined });
87
+ }
88
+ }
89
+ return expanded;
90
+ }
@@ -0,0 +1,44 @@
1
+ // Pi Team-Mode — Shared FS helpers
2
+
3
+ import { readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ export async function atomicWriteJson(filePath: string, data: unknown): Promise<void> {
7
+ const tmp = `${filePath}.${randomUUID().slice(0, 8)}.tmp`;
8
+ try {
9
+ await writeFile(tmp, JSON.stringify(data, null, 2), "utf8");
10
+ await rename(tmp, filePath);
11
+ } catch (err) {
12
+ await unlink(tmp).catch(() => {});
13
+ throw err;
14
+ }
15
+ }
16
+
17
+ export async function readJson<T>(filePath: string): Promise<T | null> {
18
+ try {
19
+ const raw = await readFile(filePath, "utf8");
20
+ return JSON.parse(raw) as T;
21
+ } catch (err) {
22
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ export async function listSubdirs(dir: string): Promise<string[]> {
28
+ try {
29
+ const entries = await readdir(dir, { withFileTypes: true });
30
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
31
+ } catch (err) {
32
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
33
+ throw err;
34
+ }
35
+ }
36
+
37
+ export function slugify(value: string, fallback: string, maxLen = 32): string {
38
+ const slug = value
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9]+/g, "-")
41
+ .replace(/^-+|-+$/g, "")
42
+ .slice(0, maxLen);
43
+ return slug || fallback;
44
+ }
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Pi Team-Mode — Model Configuration
3
+ *
4
+ * Explicit role × tier × provider model catalog, matching the team-mode
5
+ * convention. Stored at `~/.pi/agent/extensions/team-mode/model-config.json`.
6
+ *
7
+ * This is the PRIMARY way team-mode picks models. The regex step-down logic
8
+ * in `model-picker.ts` is now only the fallback (for cases where no config
9
+ * file exists, or the resolved provider has no catalog entry).
10
+ *
11
+ * Schema (all fields optional; defaults are merged in):
12
+ *
13
+ * {
14
+ * "defaultTier": "md",
15
+ * "tiers": {
16
+ * "sm": { "name": "Small", "thinkingLevel": "low", "description": "Simple tasks" },
17
+ * "md": { "name": "Medium", "thinkingLevel": "medium", "description": "Moderate complexity" },
18
+ * "lg": { "name": "Large", "thinkingLevel": "high", "description": "Strong reasoning" },
19
+ * "xl": { "name": "Deep", "thinkingLevel": "xhigh", "description": "Complex domains" }
20
+ * },
21
+ * "roles": {
22
+ * "researcher": "sm",
23
+ * "docs": "xs",
24
+ * "backend": "md",
25
+ * "frontend": "md",
26
+ * "tester": "md",
27
+ * "planner": "lg",
28
+ * "reviewer": "md"
29
+ * },
30
+ * "provider": "openai-codex" | "anthropic" | "auto",
31
+ * "providers": {
32
+ * "openai-codex": { "sm": "openai-codex/gpt-5.4-mini", "md": "openai-codex/gpt-5.4" },
33
+ * "anthropic": { "sm": "anthropic/claude-haiku-4-5", "md": "anthropic/claude-sonnet-4-6" }
34
+ * }
35
+ * }
36
+ *
37
+ * Resolution order inside `resolveModel`:
38
+ * 1. tierOverride (from caller's `model` param if it matches a configured tier)
39
+ * 2. roles[role] / legacy roleTiers[role]
40
+ * 3. defaultTier
41
+ *
42
+ * Thinking-level resolution:
43
+ * 1. caller/spec explicit override (applied by AgentManager)
44
+ * 2. legacy roleThinkingLevels[role]
45
+ * 3. tiers[selectedTier].thinkingLevel
46
+ * 4. `:<thinking>` suffix in the selected catalog model (back compat)
47
+ * 5. legacy tierThinkingLevels[selectedTier]
48
+ * 6. defaultThinkingLevel
49
+ * 6. unset — let pi inherit its own defaultThinkingLevel
50
+ *
51
+ * Provider resolution:
52
+ * 1. config.provider when not "auto"
53
+ * 2. PI_TEAM_MATE_MODEL_PROVIDER env override
54
+ * 3. pi settings.json defaultProvider (if that provider exists in catalogs)
55
+ * 4. anthropic fallback
56
+ */
57
+
58
+ import { existsSync, readFileSync } from "node:fs";
59
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
60
+ import { homedir } from "node:os";
61
+ import { join } from "node:path";
62
+
63
+ import { getStorageRoot } from "./store.js";
64
+ import type { ThinkingLevel } from "./types.js";
65
+
66
+ export type ModelTier = string;
67
+
68
+ export type ProviderCatalog = Record<string, string>;
69
+
70
+ export type TierConfig = {
71
+ name?: string;
72
+ thinkingLevel?: ThinkingLevel;
73
+ description?: string;
74
+ };
75
+
76
+ export type ModelConfig = {
77
+ /** Provider to use for all teammates. Use "auto" to detect from settings. */
78
+ provider: string;
79
+ /** Per-provider tier → fully-qualified model id. */
80
+ providers: Record<string, ProviderCatalog>;
81
+ /** Tier metadata keyed by tier id (for example xs/sm/md/lg/xl). */
82
+ tiers: Record<string, TierConfig>;
83
+ /** Role (subagent_type) → default tier. Preferred spelling. */
84
+ roles: Record<string, ModelTier>;
85
+ /** Role (subagent_type) → default tier. Legacy spelling; merged into roles. */
86
+ roleTiers: Record<string, ModelTier>;
87
+ /** Fallback tier for roles not in roleTiers. */
88
+ defaultTier: ModelTier;
89
+ /** Fallback thinking level when no role/tier/model-specific thinking is configured. Undefined = inherit pi default. */
90
+ defaultThinkingLevel?: ThinkingLevel;
91
+ /** Per-tier thinking defaults. Legacy spelling; merged into tiers[tier].thinkingLevel. */
92
+ tierThinkingLevels?: Record<string, ThinkingLevel>;
93
+ /** Role (subagent_type) → default thinking level. */
94
+ roleThinkingLevels?: Record<string, ThinkingLevel>;
95
+ /** Shell command run after a task transitions to completed. Non-zero exit reverts the task to failed. */
96
+ taskCompletedHook?: string;
97
+ };
98
+
99
+ export type ResolvedModel = {
100
+ /** Bare model id (no `provider/` prefix). Pi `--model` value. */
101
+ model: string;
102
+ /** Pi `--provider` value. */
103
+ provider: string;
104
+ /** Tier that was selected. */
105
+ tier: ModelTier;
106
+ /** Thinking level selected from role/tier/default config or model suffix. */
107
+ thinkingLevel?: ThinkingLevel;
108
+ /** One-line explanation of the resolution path. */
109
+ rationale: string;
110
+ };
111
+
112
+ export const DEFAULT_MODEL_CONFIG: ModelConfig = {
113
+ provider: "auto",
114
+ providers: {
115
+ anthropic: {
116
+ xs: "anthropic/claude-haiku-4-5",
117
+ sm: "anthropic/claude-haiku-4-5",
118
+ md: "anthropic/claude-sonnet-4-6",
119
+ lg: "anthropic/claude-opus-4-7",
120
+ xl: "anthropic/claude-opus-4-7",
121
+ cheap: "anthropic/claude-haiku-4-5",
122
+ mid: "anthropic/claude-sonnet-4-6",
123
+ deep: "anthropic/claude-opus-4-7",
124
+ },
125
+ "openai-codex": {
126
+ xs: "openai-codex/gpt-5.4-mini",
127
+ sm: "openai-codex/gpt-5.4-mini",
128
+ md: "openai-codex/gpt-5.4",
129
+ lg: "openai-codex/gpt-5.4",
130
+ xl: "openai-codex/gpt-5.4",
131
+ cheap: "openai-codex/gpt-5.4-mini",
132
+ mid: "openai-codex/gpt-5.4",
133
+ deep: "openai-codex/gpt-5.4",
134
+ },
135
+ },
136
+ tiers: {
137
+ xs: {
138
+ name: "Extra Small",
139
+ thinkingLevel: "minimal",
140
+ description: "Very small tasks, simple rewrites, classification, and mechanical edits.",
141
+ },
142
+ sm: {
143
+ name: "Small",
144
+ thinkingLevel: "low",
145
+ description: "Simple tasks, deterministic outputs. Use for formatting, rewriting, classification.",
146
+ },
147
+ md: {
148
+ name: "Medium",
149
+ thinkingLevel: "medium",
150
+ description: "Handles moderate complexity. Use for workflows, APIs, structured tasks.",
151
+ },
152
+ lg: {
153
+ name: "Large",
154
+ thinkingLevel: "high",
155
+ description: "Strong reasoning, multi-step tasks. Use for reasoning, planning, debugging, decision support.",
156
+ },
157
+ xl: {
158
+ name: "Deep",
159
+ thinkingLevel: "xhigh",
160
+ description: "Near-frontier capability for complex domains, planning, abstraction, and ambiguous problems.",
161
+ },
162
+ cheap: { name: "Cheap", thinkingLevel: "minimal" },
163
+ mid: { name: "Mid", thinkingLevel: "medium" },
164
+ deep: { name: "Deep", thinkingLevel: "high" },
165
+ },
166
+ roles: {
167
+ researcher: "sm",
168
+ docs: "xs",
169
+ backend: "md",
170
+ frontend: "md",
171
+ tester: "md",
172
+ planner: "lg",
173
+ reviewer: "md",
174
+ },
175
+ roleTiers: {
176
+ researcher: "cheap",
177
+ docs: "cheap",
178
+ backend: "mid",
179
+ frontend: "mid",
180
+ tester: "mid",
181
+ planner: "deep",
182
+ reviewer: "deep",
183
+ },
184
+ defaultTier: "md",
185
+ tierThinkingLevels: {
186
+ cheap: "minimal",
187
+ mid: "medium",
188
+ deep: "high",
189
+ },
190
+ };
191
+
192
+ const CONFIG_FILENAME = "model-config.json";
193
+ const SETTINGS_FILENAME = "settings.json";
194
+ const AUTH_FILENAME = "auth.json";
195
+
196
+ export function isModelTier(value: string): value is ModelTier {
197
+ return value === "cheap" || value === "mid" || value === "deep";
198
+ }
199
+
200
+ export function isThinkingLevel(value: string): value is ThinkingLevel {
201
+ return (
202
+ value === "off" ||
203
+ value === "minimal" ||
204
+ value === "low" ||
205
+ value === "medium" ||
206
+ value === "high" ||
207
+ value === "xhigh"
208
+ );
209
+ }
210
+
211
+ /** Path to the model-config file for team-mode. */
212
+ export function modelConfigPath(storageRoot: string = getStorageRoot()): string {
213
+ return join(storageRoot, CONFIG_FILENAME);
214
+ }
215
+
216
+ let cachedConfig: { root: string; config: ModelConfig } | null = null;
217
+
218
+ /**
219
+ * Load model config from disk, merging with built-in defaults so missing keys
220
+ * are always populated. Returns DEFAULT_MODEL_CONFIG when no file exists.
221
+ *
222
+ * Cached for process lifetime (user-edited config rarely changes mid-session);
223
+ * call `invalidateModelConfigCache()` after `saveModelConfig` if you edit it.
224
+ */
225
+ export async function loadModelConfig(
226
+ storageRoot: string = getStorageRoot(),
227
+ ): Promise<ModelConfig> {
228
+ if (cachedConfig && cachedConfig.root === storageRoot) return cachedConfig.config;
229
+ try {
230
+ const raw = await readFile(modelConfigPath(storageRoot), "utf8");
231
+ const parsed = JSON.parse(raw) as Partial<ModelConfig>;
232
+ const merged = mergeWithDefaults(parsed);
233
+ cachedConfig = { root: storageRoot, config: merged };
234
+ return merged;
235
+ } catch {
236
+ cachedConfig = { root: storageRoot, config: DEFAULT_MODEL_CONFIG };
237
+ return DEFAULT_MODEL_CONFIG;
238
+ }
239
+ }
240
+
241
+ export function invalidateModelConfigCache(): void {
242
+ cachedConfig = null;
243
+ }
244
+
245
+ /** Persist a full model config to disk. Creates the storage dir if needed. */
246
+ export async function saveModelConfig(
247
+ config: ModelConfig,
248
+ storageRoot: string = getStorageRoot(),
249
+ ): Promise<void> {
250
+ await mkdir(storageRoot, { recursive: true });
251
+ await writeFile(modelConfigPath(storageRoot), JSON.stringify(config, null, 2), {
252
+ encoding: "utf8",
253
+ mode: 0o600,
254
+ });
255
+ invalidateModelConfigCache();
256
+ }
257
+
258
+ /**
259
+ * Resolve the concrete `{ provider, model }` for a role + optional tier.
260
+ * Returns `null` if the resolved provider has no catalog entry.
261
+ *
262
+ * @param config The loaded model config.
263
+ * @param role The teammate's role (subagent_type). Empty string = use defaultTier.
264
+ * @param tierOverride Optional tier to force, regardless of role mapping.
265
+ */
266
+ export function resolveModel(
267
+ config: ModelConfig,
268
+ role: string,
269
+ tierOverride?: ModelTier,
270
+ ): ResolvedModel | null {
271
+ const provider = detectProvider(config.provider);
272
+ const catalog = config.providers[provider];
273
+ if (!catalog) return null;
274
+
275
+ const tier = tierOverride ?? config.roles[role] ?? config.roleTiers[role] ?? config.defaultTier;
276
+ const fqn = catalog[tier];
277
+ if (!fqn) return null;
278
+
279
+ const { provider: splitProvider, id } = splitFqn(fqn);
280
+ const { model, thinkingLevel: suffixThinkingLevel } = splitThinkingSuffix(id);
281
+ const thinkingLevel =
282
+ config.roleThinkingLevels?.[role] ??
283
+ config.tiers[tier]?.thinkingLevel ??
284
+ suffixThinkingLevel ??
285
+ config.tierThinkingLevels?.[tier] ??
286
+ config.defaultThinkingLevel;
287
+ const rationale = buildRationale(role, tier, tierOverride, provider, config);
288
+
289
+ return {
290
+ provider: splitProvider ?? provider,
291
+ model,
292
+ tier,
293
+ thinkingLevel,
294
+ rationale,
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Infer which provider to use when `config.provider === "auto"`.
300
+ * Priority: env > pi settings.json defaultProvider > auth.json > api keys > anthropic.
301
+ */
302
+ export function detectProvider(explicit?: string): string {
303
+ if (explicit && explicit !== "auto") return explicit;
304
+
305
+ const envOverride = process.env.PI_TEAM_MATE_MODEL_PROVIDER;
306
+ if (envOverride) return envOverride;
307
+
308
+ const piAgentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
309
+
310
+ const settings = readJsonFileSync<{ defaultProvider?: string; defaultModel?: string }>(
311
+ join(piAgentDir, SETTINGS_FILENAME),
312
+ );
313
+ if (settings?.defaultProvider && settings.defaultProvider in DEFAULT_MODEL_CONFIG.providers) {
314
+ return settings.defaultProvider;
315
+ }
316
+ const hintedFromModel = providerFromModelHint(settings?.defaultModel ?? "");
317
+ if (hintedFromModel) return hintedFromModel;
318
+
319
+ const auth = readJsonFileSync<Record<string, unknown>>(join(piAgentDir, AUTH_FILENAME));
320
+ if (auth) {
321
+ if (settings?.defaultProvider && auth[settings.defaultProvider]) {
322
+ return settings.defaultProvider;
323
+ }
324
+ if (auth["openai-codex"]) return "openai-codex";
325
+ if (auth.anthropic) return "anthropic";
326
+ }
327
+
328
+ if (process.env.ANTHROPIC_API_KEY) return "anthropic";
329
+ if (process.env.OPENAI_API_KEY) return "openai-codex";
330
+
331
+ return "anthropic";
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Helpers
336
+ // ---------------------------------------------------------------------------
337
+
338
+ function mergeWithDefaults(partial: Partial<ModelConfig>): ModelConfig {
339
+ const mergedTiers = mergeTiers(partial.tiers, partial.tierThinkingLevels);
340
+ const mergedRoles = {
341
+ ...DEFAULT_MODEL_CONFIG.roles,
342
+ ...(partial.roles ?? {}),
343
+ ...(partial.roleTiers ?? {}),
344
+ };
345
+ const base: ModelConfig = {
346
+ provider: partial.provider ?? DEFAULT_MODEL_CONFIG.provider,
347
+ providers: {
348
+ ...DEFAULT_MODEL_CONFIG.providers,
349
+ ...(partial.providers ?? {}),
350
+ },
351
+ tiers: mergedTiers,
352
+ roles: mergedRoles,
353
+ roleTiers: {
354
+ ...DEFAULT_MODEL_CONFIG.roleTiers,
355
+ ...(partial.roleTiers ?? {}),
356
+ },
357
+ defaultTier: partial.defaultTier ?? DEFAULT_MODEL_CONFIG.defaultTier,
358
+ defaultThinkingLevel: partial.defaultThinkingLevel ?? DEFAULT_MODEL_CONFIG.defaultThinkingLevel,
359
+ tierThinkingLevels: {
360
+ ...(DEFAULT_MODEL_CONFIG.tierThinkingLevels ?? {}),
361
+ ...(partial.tierThinkingLevels ?? {}),
362
+ },
363
+ roleThinkingLevels: {
364
+ ...(DEFAULT_MODEL_CONFIG.roleThinkingLevels ?? {}),
365
+ ...(partial.roleThinkingLevels ?? {}),
366
+ },
367
+ };
368
+ if (partial.taskCompletedHook !== undefined) {
369
+ base.taskCompletedHook = partial.taskCompletedHook;
370
+ }
371
+
372
+ function mergeTiers(
373
+ tiers: Partial<ModelConfig>["tiers"],
374
+ tierThinkingLevels: Partial<ModelConfig>["tierThinkingLevels"],
375
+ ): Record<string, TierConfig> {
376
+ const merged: Record<string, TierConfig> = { ...DEFAULT_MODEL_CONFIG.tiers };
377
+ for (const [tier, config] of Object.entries(tiers ?? {})) {
378
+ merged[tier] = { ...(merged[tier] ?? {}), ...config };
379
+ }
380
+ for (const [tier, thinkingLevel] of Object.entries(tierThinkingLevels ?? {})) {
381
+ merged[tier] = { ...(merged[tier] ?? {}), thinkingLevel };
382
+ }
383
+ return merged;
384
+ }
385
+ return base;
386
+ }
387
+
388
+ function providerFromModelHint(modelHint: string): string | null {
389
+ if (!modelHint) return null;
390
+ if (/claude|anthropic|haiku|sonnet|opus/i.test(modelHint)) return "anthropic";
391
+ if (/gpt|codex|openai/i.test(modelHint)) return "openai-codex";
392
+ return null;
393
+ }
394
+
395
+ function readJsonFileSync<T>(filePath: string): T | null {
396
+ try {
397
+ if (!existsSync(filePath)) return null;
398
+ return JSON.parse(readFileSync(filePath, "utf8")) as T;
399
+ } catch {
400
+ return null;
401
+ }
402
+ }
403
+
404
+ function splitFqn(fqn: string): { provider?: string; id: string } {
405
+ const idx = fqn.indexOf("/");
406
+ if (idx < 0) return { id: fqn };
407
+ return { provider: fqn.slice(0, idx), id: fqn.slice(idx + 1) };
408
+ }
409
+
410
+ export function splitThinkingSuffix(model: string): { model: string; thinkingLevel?: ThinkingLevel } {
411
+ const idx = model.lastIndexOf(":");
412
+ if (idx < 0) return { model };
413
+ const suffix = model.slice(idx + 1);
414
+ if (!isThinkingLevel(suffix)) return { model };
415
+ return { model: model.slice(0, idx), thinkingLevel: suffix };
416
+ }
417
+
418
+ function buildRationale(
419
+ role: string,
420
+ tier: ModelTier,
421
+ tierOverride: ModelTier | undefined,
422
+ provider: string,
423
+ config: ModelConfig,
424
+ ): string {
425
+ if (tierOverride) {
426
+ return `tier override "${tier}" on provider "${provider}"`;
427
+ }
428
+ if (role && (config.roles[role] || config.roleTiers[role])) {
429
+ return `role "${role}" → tier "${tier}" on provider "${provider}"`;
430
+ }
431
+ return `default tier "${tier}" on provider "${provider}"`;
432
+ }
@@ -0,0 +1,48 @@
1
+ // Pi Team-Mode — tiny parallel execution helpers
2
+
3
+ export const DEFAULT_PARALLEL_CONCURRENCY = 4;
4
+
5
+ export type ParallelTaskResult = {
6
+ name: string;
7
+ output: string;
8
+ exitCode: number | null;
9
+ error?: string;
10
+ };
11
+
12
+ export async function mapConcurrent<T, R>(
13
+ items: T[],
14
+ limit: number,
15
+ fn: (item: T, index: number) => Promise<R>,
16
+ ): Promise<R[]> {
17
+ const bounded = Math.max(1, Math.floor(limit));
18
+ if (items.length === 0) return [];
19
+
20
+ const out = new Array<R>(items.length);
21
+ let next = 0;
22
+
23
+ await Promise.all(
24
+ Array.from({ length: Math.min(bounded, items.length) }, async () => {
25
+ for (;;) {
26
+ const current = next;
27
+ next += 1;
28
+ if (current >= items.length) return;
29
+ out[current] = await fn(items[current], current);
30
+ }
31
+ }),
32
+ );
33
+
34
+ return out;
35
+ }
36
+
37
+ export function aggregateParallelOutputs(results: ParallelTaskResult[]): string {
38
+ return results
39
+ .map((result, index) => {
40
+ const header = `=== Parallel Task ${index + 1} (${result.name}) ===`;
41
+ const status = `status: ${result.exitCode === 0 ? "completed" : "failed"} (exit=${result.exitCode ?? "n/a"})`;
42
+ const body = result.error
43
+ ? `${result.output}\n\nerror: ${result.error}`
44
+ : result.output;
45
+ return [header, status, "", body].join("\n");
46
+ })
47
+ .join("\n\n");
48
+ }