pi-crew 0.1.24 → 0.1.26

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 (73) hide show
  1. package/docs/refactor-tasks-phase3.md +394 -0
  2. package/docs/refactor-tasks-phase4.md +564 -0
  3. package/docs/refactor-tasks-phase5.md +402 -0
  4. package/docs/refactor-tasks.md +1484 -0
  5. package/package.json +98 -95
  6. package/src/agents/agent-config.ts +30 -30
  7. package/src/config/config.ts +153 -89
  8. package/src/config/defaults.ts +60 -0
  9. package/src/extension/autonomous-policy.ts +1 -1
  10. package/src/extension/help.ts +1 -0
  11. package/src/extension/management.ts +15 -2
  12. package/src/extension/register.ts +124 -170
  13. package/src/extension/registration/command-utils.ts +54 -0
  14. package/src/extension/registration/subagent-helpers.ts +70 -0
  15. package/src/extension/registration/viewers.ts +32 -0
  16. package/src/extension/result-watcher.ts +98 -89
  17. package/src/extension/team-tool/api.ts +276 -0
  18. package/src/extension/team-tool/config-patch.ts +36 -0
  19. package/src/extension/team-tool/context.ts +48 -0
  20. package/src/extension/team-tool/doctor.ts +178 -0
  21. package/src/extension/team-tool/run.ts +133 -0
  22. package/src/extension/team-tool-types.ts +6 -0
  23. package/src/extension/team-tool.ts +31 -623
  24. package/src/extension/tool-result.ts +16 -16
  25. package/src/runtime/async-runner.ts +42 -60
  26. package/src/runtime/child-pi.ts +434 -332
  27. package/src/runtime/concurrency.ts +50 -42
  28. package/src/runtime/crew-agent-records.ts +166 -156
  29. package/src/runtime/manifest-cache.ts +214 -0
  30. package/src/runtime/parallel-utils.ts +99 -0
  31. package/src/runtime/post-exit-stdio-guard.ts +86 -0
  32. package/src/runtime/runtime-resolver.ts +77 -74
  33. package/src/runtime/subagent-manager.ts +291 -236
  34. package/src/runtime/task-graph-scheduler.ts +122 -107
  35. package/src/runtime/team-runner.ts +46 -51
  36. package/src/schema/config-schema.ts +92 -0
  37. package/src/state/artifact-store.ts +108 -36
  38. package/src/state/atomic-write.ts +114 -49
  39. package/src/state/event-log.ts +189 -138
  40. package/src/state/jsonl-writer.ts +77 -0
  41. package/src/state/locks.ts +149 -40
  42. package/src/state/mailbox.ts +200 -188
  43. package/src/state/state-store.ts +104 -15
  44. package/src/teams/discover-teams.ts +94 -84
  45. package/src/teams/team-config.ts +26 -22
  46. package/src/ui/crew-footer.ts +101 -0
  47. package/src/ui/crew-select-list.ts +111 -0
  48. package/src/ui/crew-widget.ts +285 -219
  49. package/src/ui/dynamic-border.ts +25 -0
  50. package/src/ui/layout-primitives.ts +106 -0
  51. package/src/ui/live-run-sidebar.ts +163 -95
  52. package/src/ui/loaders.ts +158 -0
  53. package/src/ui/mascot.ts +441 -0
  54. package/src/ui/powerbar-publisher.ts +94 -71
  55. package/src/ui/render-diff.ts +119 -0
  56. package/src/ui/run-dashboard.ts +155 -120
  57. package/src/ui/status-colors.ts +54 -0
  58. package/src/ui/syntax-highlight.ts +116 -0
  59. package/src/ui/theme-adapter.ts +190 -0
  60. package/src/ui/transcript-viewer.ts +194 -111
  61. package/src/utils/completion-dedupe.ts +63 -0
  62. package/src/utils/file-coalescer.ts +84 -33
  63. package/src/utils/fs-watch.ts +31 -0
  64. package/src/utils/git.ts +262 -0
  65. package/src/utils/internal-error.ts +6 -0
  66. package/src/utils/paths.ts +33 -15
  67. package/src/utils/sleep.ts +32 -0
  68. package/src/utils/timings.ts +31 -0
  69. package/src/utils/visual.ts +159 -0
  70. package/src/workflows/discover-workflows.ts +109 -101
  71. package/src/workflows/workflow-config.ts +25 -24
  72. package/src/workflows/workflow-serializer.ts +32 -31
  73. package/tsconfig.json +19 -19
package/package.json CHANGED
@@ -1,95 +1,98 @@
1
- {
2
- "name": "pi-crew",
3
- "version": "0.1.24",
4
- "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
- "author": "baphuongna",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/baphuongna/pi-crew.git"
10
- },
11
- "homepage": "https://github.com/baphuongna/pi-crew#readme",
12
- "bugs": {
13
- "url": "https://github.com/baphuongna/pi-crew/issues"
14
- },
15
- "type": "module",
16
- "bin": {
17
- "pi-crew": "install.mjs"
18
- },
19
- "keywords": [
20
- "pi-package",
21
- "pi",
22
- "pi-coding-agent",
23
- "teams",
24
- "agents",
25
- "multi-agent",
26
- "orchestration"
27
- ],
28
- "files": [
29
- "*.ts",
30
- "*.mjs",
31
- "src/**/*.ts",
32
- "agents/",
33
- "teams/",
34
- "workflows/",
35
- "skills/**/*",
36
- "README.md",
37
- "AGENTS.md",
38
- "docs/",
39
- "tsconfig.json",
40
- "schema.json",
41
- "CHANGELOG.md",
42
- "LICENSE",
43
- "NOTICE.md"
44
- ],
45
- "scripts": {
46
- "check": "npm run ci",
47
- "ci": "npm run typecheck && npm test && npm pack --dry-run",
48
- "typecheck": "tsc --noEmit && node --experimental-strip-types -e \"await import('./index.ts'); console.log('strip-types import ok')\"",
49
- "test": "npm run test:unit",
50
- "test:unit": "node --experimental-strip-types --test test/unit/*.test.ts",
51
- "smoke:pi": "pi install ."
52
- },
53
- "exports": {
54
- "./schema.json": "./schema.json"
55
- },
56
- "pi": {
57
- "extensions": [
58
- "./index.ts"
59
- ],
60
- "skills": [
61
- "./skills"
62
- ]
63
- },
64
- "peerDependencies": {
65
- "@mariozechner/pi-agent-core": "*",
66
- "@mariozechner/pi-ai": "*",
67
- "@mariozechner/pi-coding-agent": "*",
68
- "@mariozechner/pi-tui": "*"
69
- },
70
- "dependencies": {
71
- "jiti": "^2.6.1",
72
- "typebox": "^1.1.24"
73
- },
74
- "devDependencies": {
75
- "@mariozechner/pi-agent-core": "^0.65.0",
76
- "@mariozechner/pi-ai": "^0.65.0",
77
- "@mariozechner/pi-coding-agent": "^0.65.0",
78
- "typescript": "^5.9.3"
79
- },
80
- "peerDependenciesMeta": {
81
- "@mariozechner/pi-agent-core": {
82
- "optional": true
83
- },
84
- "@mariozechner/pi-ai": {
85
- "optional": true
86
- },
87
- "@mariozechner/pi-coding-agent": {
88
- "optional": true
89
- },
90
- "@mariozechner/pi-tui": {
91
- "optional": true
92
- }
93
- },
94
- "readmeFilename": "README.md"
95
- }
1
+ {
2
+ "name": "pi-crew",
3
+ "version": "0.1.26",
4
+ "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
+ "author": "baphuongna",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/baphuongna/pi-crew.git"
10
+ },
11
+ "homepage": "https://github.com/baphuongna/pi-crew#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/baphuongna/pi-crew/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "pi-crew": "install.mjs"
18
+ },
19
+ "keywords": [
20
+ "pi-package",
21
+ "pi",
22
+ "pi-coding-agent",
23
+ "teams",
24
+ "agents",
25
+ "multi-agent",
26
+ "orchestration"
27
+ ],
28
+ "files": [
29
+ "*.ts",
30
+ "*.mjs",
31
+ "src/**/*.ts",
32
+ "agents/",
33
+ "teams/",
34
+ "workflows/",
35
+ "skills/**/*",
36
+ "README.md",
37
+ "AGENTS.md",
38
+ "docs/",
39
+ "tsconfig.json",
40
+ "schema.json",
41
+ "CHANGELOG.md",
42
+ "LICENSE",
43
+ "NOTICE.md"
44
+ ],
45
+ "scripts": {
46
+ "check": "npm run ci",
47
+ "ci": "npm run typecheck && npm test && npm pack --dry-run",
48
+ "typecheck": "tsc --noEmit && node --experimental-strip-types -e \"await import('./index.ts'); console.log('strip-types import ok')\"",
49
+ "test": "npm run test:unit && npm run test:integration",
50
+ "test:unit": "node --experimental-strip-types --test --test-timeout=30000 test/unit/*.test.ts",
51
+ "test:integration": "node --experimental-strip-types --test --test-timeout=120000 test/integration/*.test.ts",
52
+ "smoke:pi": "pi install ."
53
+ },
54
+ "exports": {
55
+ "./schema.json": "./schema.json"
56
+ },
57
+ "pi": {
58
+ "extensions": [
59
+ "./index.ts"
60
+ ],
61
+ "skills": [
62
+ "./skills"
63
+ ]
64
+ },
65
+ "peerDependencies": {
66
+ "@mariozechner/pi-agent-core": "*",
67
+ "@mariozechner/pi-ai": "*",
68
+ "@mariozechner/pi-coding-agent": "*",
69
+ "@mariozechner/pi-tui": "*"
70
+ },
71
+ "dependencies": {
72
+ "cli-highlight": "^2.1.11",
73
+ "diff": "^5.2.0",
74
+ "jiti": "^2.6.1",
75
+ "typebox": "^1.1.24"
76
+ },
77
+ "devDependencies": {
78
+ "@mariozechner/pi-agent-core": "^0.65.0",
79
+ "@mariozechner/pi-ai": "^0.65.0",
80
+ "@mariozechner/pi-coding-agent": "^0.65.0",
81
+ "typescript": "^5.9.3"
82
+ },
83
+ "peerDependenciesMeta": {
84
+ "@mariozechner/pi-agent-core": {
85
+ "optional": true
86
+ },
87
+ "@mariozechner/pi-ai": {
88
+ "optional": true
89
+ },
90
+ "@mariozechner/pi-coding-agent": {
91
+ "optional": true
92
+ },
93
+ "@mariozechner/pi-tui": {
94
+ "optional": true
95
+ }
96
+ },
97
+ "readmeFilename": "README.md"
98
+ }
@@ -1,30 +1,30 @@
1
- export type ResourceSource = "builtin" | "user" | "project";
2
-
3
- export interface RoutingMetadata {
4
- triggers?: string[];
5
- useWhen?: string[];
6
- avoidWhen?: string[];
7
- cost?: "free" | "cheap" | "expensive";
8
- category?: string;
9
- }
10
-
11
- export interface AgentConfig {
12
- name: string;
13
- description: string;
14
- source: ResourceSource;
15
- filePath: string;
16
- systemPrompt: string;
17
- model?: string;
18
- fallbackModels?: string[];
19
- thinking?: string;
20
- tools?: string[];
21
- extensions?: string[];
22
- skills?: string[];
23
- systemPromptMode?: "replace" | "append";
24
- inheritProjectContext?: boolean;
25
- inheritSkills?: boolean;
26
- routing?: RoutingMetadata;
27
- memory?: "user" | "project" | "local";
28
- disabled?: boolean;
29
- override?: { source: "config"; path: string };
30
- }
1
+ export type ResourceSource = "builtin" | "user" | "project" | "git";
2
+
3
+ export interface RoutingMetadata {
4
+ triggers?: string[];
5
+ useWhen?: string[];
6
+ avoidWhen?: string[];
7
+ cost?: "free" | "cheap" | "expensive";
8
+ category?: string;
9
+ }
10
+
11
+ export interface AgentConfig {
12
+ name: string;
13
+ description: string;
14
+ source: ResourceSource;
15
+ filePath: string;
16
+ systemPrompt: string;
17
+ model?: string;
18
+ fallbackModels?: string[];
19
+ thinking?: string;
20
+ tools?: string[];
21
+ extensions?: string[];
22
+ skills?: string[];
23
+ systemPromptMode?: "replace" | "append";
24
+ inheritProjectContext?: boolean;
25
+ inheritSkills?: boolean;
26
+ routing?: RoutingMetadata;
27
+ memory?: "user" | "project" | "local";
28
+ disabled?: boolean;
29
+ override?: { source: "config"; path: string };
30
+ }
@@ -1,6 +1,9 @@
1
+ import { Type, type Static, type TSchema } from "typebox";
2
+ import { Value } from "typebox/value";
1
3
  import * as fs from "node:fs";
2
4
  import * as os from "node:os";
3
5
  import * as path from "node:path";
6
+ import { PiTeamsAutonomyProfileSchema, PiTeamsConfigSchema } from "../schema/config-schema.ts";
4
7
 
5
8
  export type PiTeamsAutonomyProfile = "manual" | "suggested" | "assisted" | "aggressive";
6
9
 
@@ -59,6 +62,8 @@ export interface CrewUiConfig {
59
62
  showModel?: boolean;
60
63
  showTokens?: boolean;
61
64
  showTools?: boolean;
65
+ mascotStyle?: "cat" | "armin";
66
+ mascotEffect?: "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve";
62
67
  }
63
68
 
64
69
  export interface AgentOverrideConfig {
@@ -93,6 +98,12 @@ export interface LoadedPiTeamsConfig {
93
98
  path: string;
94
99
  paths: string[];
95
100
  error?: string;
101
+ warnings?: string[];
102
+ }
103
+
104
+ export interface ConfigValidationResult {
105
+ config: PiTeamsConfig;
106
+ warnings: string[];
96
107
  }
97
108
 
98
109
  export interface SavedPiTeamsConfig {
@@ -119,6 +130,24 @@ function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<
119
130
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as Partial<T>;
120
131
  }
121
132
 
133
+ function errorPathFromValidation(error: unknown): string {
134
+ if (error && typeof error === "object") {
135
+ if (typeof (error as { path?: unknown }).path === "string") return (error as { path: string }).path;
136
+ if (typeof (error as { instancePath?: unknown }).instancePath === "string") return (error as { instancePath: string }).instancePath;
137
+ if (typeof (error as { keyword?: unknown }).keyword === "string" && typeof (error as { schemaPath?: unknown }).schemaPath === "string") return (error as { schemaPath: string }).schemaPath;
138
+ }
139
+ return "config";
140
+ }
141
+
142
+ function validateConfigWithWarnings(raw: unknown): string[] {
143
+ if (!Value.Check(PiTeamsConfigSchema, raw)) {
144
+ return [...Value.Errors(PiTeamsConfigSchema, raw)].map((error) => {
145
+ return `${errorPathFromValidation(error)}: ${(error as { message?: unknown }).message ?? "invalid value"}`;
146
+ });
147
+ }
148
+ return [];
149
+ }
150
+
122
151
  function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfig {
123
152
  const merged: PiTeamsConfig = { ...base, ...withoutUndefined(override as Record<string, unknown>) };
124
153
  if (base.autonomous || override.autonomous) {
@@ -171,8 +200,47 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
171
200
  return merged;
172
201
  }
173
202
 
174
- function parseAutonomyProfile(value: unknown): PiTeamsAutonomyProfile | undefined {
175
- return value === "manual" || value === "suggested" || value === "assisted" || value === "aggressive" ? value : undefined;
203
+ const LIMIT_CEILINGS = {
204
+ maxConcurrentWorkers: 1024,
205
+ maxTaskDepth: 100,
206
+ maxChildrenPerTask: 1000,
207
+ maxRunMinutes: 1440,
208
+ maxRetriesPerTask: 100,
209
+ maxTasksPerRun: 10_000,
210
+ heartbeatStaleMs: 24 * 60 * 60 * 1000,
211
+ runtimeMaxTurns: 10_000,
212
+ runtimeGraceTurns: 1_000,
213
+ } as const;
214
+
215
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
216
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
217
+ return value as Record<string, unknown>;
218
+ }
219
+
220
+ function parseWithSchema<T extends TSchema>(schema: T, value: unknown): Static<T> | undefined {
221
+ if (!Value.Check(schema, value)) return undefined;
222
+ return Value.Decode(schema, value);
223
+ }
224
+
225
+ function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
226
+ return parseWithSchema(Type.Integer({ minimum: 1, maximum: max }), value);
227
+ }
228
+
229
+ function parseProfile(value: unknown): PiTeamsAutonomyProfile | undefined {
230
+ return parseWithSchema(PiTeamsAutonomyProfileSchema, value);
231
+ }
232
+
233
+ function parseStringList(value: unknown): string[] | undefined {
234
+ const items = parseWithSchema(Type.Array(Type.String()), value);
235
+ if (!items || items.length === 0) return undefined;
236
+ const normalized = items.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
237
+ return normalized.length > 0 ? normalized : undefined;
238
+ }
239
+
240
+ function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
241
+ if (value === false) return false;
242
+ if (typeof value === "string") return parseStringList(value.split(","));
243
+ return parseStringList(value);
176
244
  }
177
245
 
178
246
  export function effectiveAutonomousConfig(config: PiTeamsAutonomousConfig | undefined): Required<Pick<PiTeamsAutonomousConfig, "profile" | "enabled" | "injectPolicy" | "preferAsyncForLongTasks" | "allowWorktreeSuggestion">> & Pick<PiTeamsAutonomousConfig, "magicKeywords"> {
@@ -195,48 +263,33 @@ export function effectiveAutonomousConfig(config: PiTeamsAutonomousConfig | unde
195
263
  }
196
264
 
197
265
  function parseStringArrayRecord(value: unknown): Record<string, string[]> | undefined {
198
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
266
+ const record = parseWithSchema(Type.Record(Type.String({ minLength: 1 }), Type.Array(Type.String())), value);
267
+ if (!record) return undefined;
199
268
  const result: Record<string, string[]> = {};
200
- for (const [key, rawValues] of Object.entries(value)) {
201
- if (!Array.isArray(rawValues)) continue;
202
- const values = rawValues.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
203
- if (values.length > 0) result[key] = values;
269
+ for (const [key, rawValues] of Object.entries(record)) {
270
+ const parsed = parseStringList(rawValues);
271
+ if (parsed && parsed.length > 0) result[key] = parsed;
204
272
  }
205
273
  return Object.keys(result).length > 0 ? result : undefined;
206
274
  }
207
275
 
208
276
  function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefined {
209
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
210
- const obj = value as Record<string, unknown>;
211
- return {
212
- profile: parseAutonomyProfile(obj.profile),
213
- enabled: typeof obj.enabled === "boolean" ? obj.enabled : undefined,
214
- injectPolicy: typeof obj.injectPolicy === "boolean" ? obj.injectPolicy : undefined,
215
- preferAsyncForLongTasks: typeof obj.preferAsyncForLongTasks === "boolean" ? obj.preferAsyncForLongTasks : undefined,
216
- allowWorktreeSuggestion: typeof obj.allowWorktreeSuggestion === "boolean" ? obj.allowWorktreeSuggestion : undefined,
277
+ const obj = asRecord(value);
278
+ if (!obj) return undefined;
279
+ const config: PiTeamsAutonomousConfig = {
280
+ profile: parseProfile(obj.profile),
281
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
282
+ injectPolicy: parseWithSchema(Type.Boolean(), obj.injectPolicy),
283
+ preferAsyncForLongTasks: parseWithSchema(Type.Boolean(), obj.preferAsyncForLongTasks),
284
+ allowWorktreeSuggestion: parseWithSchema(Type.Boolean(), obj.allowWorktreeSuggestion),
217
285
  magicKeywords: parseStringArrayRecord(obj.magicKeywords),
218
286
  };
219
- }
220
-
221
- const LIMIT_CEILINGS = {
222
- maxConcurrentWorkers: 1024,
223
- maxTaskDepth: 100,
224
- maxChildrenPerTask: 1000,
225
- maxRunMinutes: 1440,
226
- maxRetriesPerTask: 100,
227
- maxTasksPerRun: 10_000,
228
- heartbeatStaleMs: 24 * 60 * 60 * 1000,
229
- runtimeMaxTurns: 10_000,
230
- runtimeGraceTurns: 1_000,
231
- } as const;
232
-
233
- function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
234
- return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= max ? value : undefined;
287
+ return Object.values(config).some((entry) => entry !== undefined) ? config : undefined;
235
288
  }
236
289
 
237
290
  function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
238
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
239
- const obj = value as Record<string, unknown>;
291
+ const obj = asRecord(value);
292
+ if (!obj) return undefined;
240
293
  const limits: CrewLimitsConfig = {
241
294
  maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers, LIMIT_CEILINGS.maxConcurrentWorkers),
242
295
  maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth, LIMIT_CEILINGS.maxTaskDepth),
@@ -249,111 +302,106 @@ function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
249
302
  return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
250
303
  }
251
304
 
252
- function parseRuntimeMode(value: unknown): CrewRuntimeMode | undefined {
253
- return value === "auto" || value === "scaffold" || value === "child-process" || value === "live-session" ? value : undefined;
254
- }
255
-
256
305
  function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
257
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
258
- const obj = value as Record<string, unknown>;
306
+ const obj = asRecord(value);
307
+ if (!obj) return undefined;
259
308
  const runtime: CrewRuntimeConfig = {
260
- mode: parseRuntimeMode(obj.mode),
261
- preferLiveSession: typeof obj.preferLiveSession === "boolean" ? obj.preferLiveSession : undefined,
262
- allowChildProcessFallback: typeof obj.allowChildProcessFallback === "boolean" ? obj.allowChildProcessFallback : undefined,
309
+ mode: parseWithSchema(Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")]), obj.mode),
310
+ preferLiveSession: parseWithSchema(Type.Boolean(), obj.preferLiveSession),
311
+ allowChildProcessFallback: parseWithSchema(Type.Boolean(), obj.allowChildProcessFallback),
263
312
  maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
264
313
  graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
265
- inheritContext: typeof obj.inheritContext === "boolean" ? obj.inheritContext : undefined,
266
- promptMode: obj.promptMode === "replace" || obj.promptMode === "append" ? obj.promptMode : undefined,
267
- groupJoin: obj.groupJoin === "off" || obj.groupJoin === "group" || obj.groupJoin === "smart" ? obj.groupJoin : undefined,
314
+ inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext),
315
+ promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
316
+ groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
268
317
  };
269
318
  return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
270
319
  }
271
320
 
272
321
  function parseControlConfig(value: unknown): CrewControlConfig | undefined {
273
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
274
- const obj = value as Record<string, unknown>;
322
+ const obj = asRecord(value);
323
+ if (!obj) return undefined;
275
324
  const control: CrewControlConfig = {
276
- enabled: typeof obj.enabled === "boolean" ? obj.enabled : undefined,
325
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
277
326
  needsAttentionAfterMs: parsePositiveInteger(obj.needsAttentionAfterMs),
278
327
  };
279
328
  return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
280
329
  }
281
330
 
282
331
  function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
283
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
284
- const obj = value as Record<string, unknown>;
332
+ const obj = asRecord(value);
333
+ if (!obj) return undefined;
334
+ const rawSetupHook = parseWithSchema(Type.String(), obj.setupHook);
335
+ const setupHook = rawSetupHook?.trim();
285
336
  const worktree: CrewWorktreeConfig = {
286
- setupHook: typeof obj.setupHook === "string" && obj.setupHook.trim() ? obj.setupHook.trim() : undefined,
337
+ setupHook: setupHook ? setupHook : undefined,
287
338
  setupHookTimeoutMs: parsePositiveInteger(obj.setupHookTimeoutMs, 300_000),
288
- linkNodeModules: typeof obj.linkNodeModules === "boolean" ? obj.linkNodeModules : undefined,
339
+ linkNodeModules: parseWithSchema(Type.Boolean(), obj.linkNodeModules),
289
340
  };
290
341
  return Object.values(worktree).some((entry) => entry !== undefined) ? worktree : undefined;
291
342
  }
292
343
 
293
- function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
294
- if (value === false) return false;
295
- if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
296
- if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
297
- return undefined;
298
- }
299
-
300
344
  function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
301
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
302
- const obj = value as Record<string, unknown>;
345
+ const obj = asRecord(value);
346
+ if (!obj) return undefined;
303
347
  const override: AgentOverrideConfig = {
304
- disabled: typeof obj.disabled === "boolean" ? obj.disabled : undefined,
305
- model: typeof obj.model === "string" || obj.model === false ? obj.model : undefined,
348
+ disabled: parseWithSchema(Type.Boolean(), obj.disabled),
349
+ model: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.model),
306
350
  fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
307
- thinking: typeof obj.thinking === "string" || obj.thinking === false ? obj.thinking : undefined,
351
+ thinking: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.thinking),
308
352
  tools: parseStringArrayOrFalse(obj.tools),
309
353
  };
310
354
  return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
311
355
  }
312
356
 
313
357
  function parseUiConfig(value: unknown): CrewUiConfig | undefined {
314
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
315
- const obj = value as Record<string, unknown>;
358
+ const obj = asRecord(value);
359
+ if (!obj) return undefined;
360
+ const rawWidgetPlacement = parseWithSchema(Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor")]), obj.widgetPlacement);
361
+ const rawDashboardPlacement = parseWithSchema(Type.Union([Type.Literal("center"), Type.Literal("right")]), obj.dashboardPlacement);
316
362
  const ui: CrewUiConfig = {
317
- widgetPlacement: obj.widgetPlacement === "aboveEditor" || obj.widgetPlacement === "belowEditor" ? obj.widgetPlacement : undefined,
363
+ widgetPlacement: rawWidgetPlacement,
318
364
  widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
319
- powerbar: typeof obj.powerbar === "boolean" ? obj.powerbar : undefined,
320
- dashboardPlacement: obj.dashboardPlacement === "center" || obj.dashboardPlacement === "right" ? obj.dashboardPlacement : undefined,
365
+ powerbar: parseWithSchema(Type.Boolean(), obj.powerbar),
366
+ dashboardPlacement: rawDashboardPlacement,
321
367
  dashboardWidth: parsePositiveInteger(obj.dashboardWidth, 120),
322
368
  dashboardLiveRefreshMs: parsePositiveInteger(obj.dashboardLiveRefreshMs, 60_000),
323
- autoOpenDashboard: typeof obj.autoOpenDashboard === "boolean" ? obj.autoOpenDashboard : undefined,
324
- autoOpenDashboardForForegroundRuns: typeof obj.autoOpenDashboardForForegroundRuns === "boolean" ? obj.autoOpenDashboardForForegroundRuns : undefined,
325
- showModel: typeof obj.showModel === "boolean" ? obj.showModel : undefined,
326
- showTokens: typeof obj.showTokens === "boolean" ? obj.showTokens : undefined,
327
- showTools: typeof obj.showTools === "boolean" ? obj.showTools : undefined,
369
+ autoOpenDashboard: parseWithSchema(Type.Boolean(), obj.autoOpenDashboard),
370
+ autoOpenDashboardForForegroundRuns: parseWithSchema(Type.Boolean(), obj.autoOpenDashboardForForegroundRuns),
371
+ showModel: parseWithSchema(Type.Boolean(), obj.showModel),
372
+ showTokens: parseWithSchema(Type.Boolean(), obj.showTokens),
373
+ showTools: parseWithSchema(Type.Boolean(), obj.showTools),
374
+ mascotStyle: parseWithSchema(Type.Union([Type.Literal("cat"), Type.Literal("armin")]), obj.mascotStyle),
375
+ mascotEffect: parseWithSchema(Type.Union([Type.Literal("random"), Type.Literal("none"), Type.Literal("typewriter"), Type.Literal("scanline"), Type.Literal("rain"), Type.Literal("fade"), Type.Literal("crt"), Type.Literal("glitch"), Type.Literal("dissolve")]), obj.mascotEffect),
328
376
  };
329
377
  return Object.values(ui).some((entry) => entry !== undefined) ? ui : undefined;
330
378
  }
331
379
 
332
380
  function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
333
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
334
- const obj = value as Record<string, unknown>;
381
+ const obj = asRecord(value);
382
+ if (!obj) return undefined;
335
383
  const overrides: Record<string, AgentOverrideConfig> = {};
336
384
  if (obj.overrides && typeof obj.overrides === "object" && !Array.isArray(obj.overrides)) {
337
- for (const [name, rawOverride] of Object.entries(obj.overrides)) {
385
+ for (const [name, rawOverride] of Object.entries(obj.overrides as Record<string, unknown>)) {
338
386
  const parsed = parseAgentOverride(rawOverride);
339
- if (parsed) overrides[name] = parsed;
387
+ if (parsed && name.trim()) overrides[name.trim()] = parsed;
340
388
  }
341
389
  }
342
390
  const agents: CrewAgentsConfig = {
343
- disableBuiltins: typeof obj.disableBuiltins === "boolean" ? obj.disableBuiltins : undefined,
391
+ disableBuiltins: parseWithSchema(Type.Boolean(), obj.disableBuiltins),
344
392
  overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
345
393
  };
346
394
  return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
347
395
  }
348
396
 
349
- function parseConfig(raw: unknown): PiTeamsConfig {
350
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
351
- const obj = raw as Record<string, unknown>;
397
+ export function parseConfig(raw: unknown): PiTeamsConfig {
398
+ const obj = asRecord(raw);
399
+ if (!obj) return {};
352
400
  return {
353
- asyncByDefault: typeof obj.asyncByDefault === "boolean" ? obj.asyncByDefault : undefined,
354
- executeWorkers: typeof obj.executeWorkers === "boolean" ? obj.executeWorkers : undefined,
355
- notifierIntervalMs: typeof obj.notifierIntervalMs === "number" && Number.isFinite(obj.notifierIntervalMs) && obj.notifierIntervalMs >= 1000 ? obj.notifierIntervalMs : undefined,
356
- requireCleanWorktreeLeader: typeof obj.requireCleanWorktreeLeader === "boolean" ? obj.requireCleanWorktreeLeader : undefined,
401
+ asyncByDefault: parseWithSchema(Type.Boolean(), obj.asyncByDefault),
402
+ executeWorkers: parseWithSchema(Type.Boolean(), obj.executeWorkers),
403
+ notifierIntervalMs: parseWithSchema(Type.Number({ minimum: 1_000 }), obj.notifierIntervalMs),
404
+ requireCleanWorktreeLeader: parseWithSchema(Type.Boolean(), obj.requireCleanWorktreeLeader),
357
405
  autonomous: parseAutonomousConfig(obj.autonomous),
358
406
  limits: parseLimitsConfig(obj.limits),
359
407
  runtime: parseRuntimeConfig(obj.runtime),
@@ -364,6 +412,14 @@ function parseConfig(raw: unknown): PiTeamsConfig {
364
412
  };
365
413
  }
366
414
 
415
+ export function parseConfigWithWarnings(raw: unknown): ConfigValidationResult {
416
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { config: {}, warnings: [] };
417
+ const parsed = parseConfig(raw);
418
+ const warnings = validateConfigWithWarnings(raw as Record<string, unknown>);
419
+ return { config: parsed, warnings };
420
+ }
421
+
422
+
367
423
  function unsetPath(record: Record<string, unknown>, dottedPath: string): void {
368
424
  const parts = dottedPath.split(".").filter(Boolean);
369
425
  if (parts.length === 0) return;
@@ -387,9 +443,17 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
387
443
  const filePath = configPath();
388
444
  const paths = cwd ? [filePath, projectConfigPath(cwd)] : [filePath];
389
445
  try {
390
- let config = parseConfig(readConfigRecord(filePath));
391
- if (cwd) config = mergeConfig(config, parseConfig(readConfigRecord(projectConfigPath(cwd))));
392
- return { path: filePath, paths, config };
446
+ const userRaw = readConfigRecord(filePath);
447
+ const userConfig = parseConfigWithWarnings(userRaw);
448
+ let config = userConfig.config;
449
+ const warnings: string[] = userConfig.warnings.map((warning) => `${filePath}: ${warning}`);
450
+ if (cwd) {
451
+ const projectPath = projectConfigPath(cwd);
452
+ const projectConfig = parseConfigWithWarnings(readConfigRecord(projectPath));
453
+ warnings.push(...projectConfig.warnings.map((warning) => `${projectPath}: ${warning}`));
454
+ config = mergeConfig(config, projectConfig.config);
455
+ }
456
+ return { path: filePath, paths, config, warnings: warnings.length > 0 ? warnings : undefined };
393
457
  } catch (error) {
394
458
  const message = error instanceof Error ? error.message : String(error);
395
459
  return { path: filePath, paths, config: {}, error: message };