pi-orchestrator-config 4.1.5

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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Project-level settings — loads .pi/pi-config-settings.json with env var fallback.
3
+ *
4
+ * Resolution order:
5
+ * 1. Project .pi/pi-config-settings.json (wins if set)
6
+ * 2. Global ~/.pi/pi-config-settings.json (fallback for all projects)
7
+ * 3. Env var (PI_COMMIT_TRAILER, PI_USE_WORKTREES, PI_DREAM_INTERVAL_HOURS, PI_DCO,
8
+ * ACPX_AGENTS, CLI_AGENTS, PI_PIDASH_ENABLE, PI_PIDIFF_ENABLE, PI_PIDASH_PORT, PI_IMAGE_MODEL,
9
+ * PI_ASYNC_LLM_PROVIDER, PI_ASYNC_LLM_MODEL, PI_REVIEW_LOOP_MAX_CYCLES)
10
+ * 4. Default (dream_interval_hours defaults to 3; acpx_agents/cli_agents to []; pidash_enable/pidiff_enable to true; pidash_port to 19190;
11
+ * review_loop_max_cycles to 3)
12
+ *
13
+ * review_loop_max_cycles accepts JSON integers 1-10, or digit strings "1"-"10" only (after trim).
14
+ * Rejects out-of-range values and non-digit forms ("01", "10.0", "1e1", "0b1010", "inf", Infinity, …) —
15
+ * those fall through to the next resolution layer / default (3). Disable the review loop via
16
+ * review_loop_enforcement: false — not via max_cycles.
17
+ */
18
+
19
+ import { existsSync, statSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { homedir } from "node:os";
22
+ import { resolveRepoRoot } from "./utils.js";
23
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
24
+
25
+ interface ProjectSettings {
26
+ commit_trailer?: boolean | string;
27
+ allow_push_to_protected_branches?: boolean;
28
+ use_worktrees?: boolean;
29
+ dream_interval_hours?: number;
30
+ dco?: boolean;
31
+ comment_signature?: boolean;
32
+ review_loop_enforcement?: boolean;
33
+ /** Block orchestrator from using edit and write tools directly (must delegate to subagents). Default: false. */
34
+ orchestrator_edit_write_block?: boolean;
35
+ acpx_agents?: string | string[];
36
+ /** CLI-backed providers to register as cli-${agent} (claude, gemini, cursor). */
37
+ cli_agents?: string | string[];
38
+ pidash_enable?: boolean;
39
+ pidiff_enable?: boolean;
40
+ pidash_port?: number;
41
+ image_model?: string;
42
+ /** Provider for detached LLM async children when parent is acpx (must-async / dream). */
43
+ async_llm_provider?: string;
44
+ /** Model id for detached LLM async children when parent is acpx. */
45
+ async_llm_model?: string;
46
+ /** Max review-loop cycles injected into the rules prompt. Integer 1-10 only. */
47
+ review_loop_max_cycles?: number;
48
+ /** Default provider for all subagents. */
49
+ agent_provider?: string;
50
+ /** Default model for all subagents. */
51
+ agent_model?: string;
52
+ /** Per-agent provider/model overrides. null = use parent model (skip global agent_provider/agent_model). */
53
+ agent_overrides?: Record<string, { provider?: string | null; model?: string | null }>;
54
+ }
55
+
56
+ const SETTINGS_FILENAME = "pi-config-settings.json";
57
+
58
+ function getSettingsPath(cwd: string): string {
59
+ return join(resolveRepoRoot(cwd), ".pi", SETTINGS_FILENAME);
60
+ }
61
+
62
+ function parseSettingsFile(filePath: string): ProjectSettings {
63
+ if (!existsSync(filePath)) return {};
64
+ try {
65
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
66
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
67
+ const result: ProjectSettings = {};
68
+ if (typeof raw.commit_trailer === "boolean" || typeof raw.commit_trailer === "string") result.commit_trailer = raw.commit_trailer;
69
+ if (typeof raw.allow_push_to_protected_branches === "boolean") result.allow_push_to_protected_branches = raw.allow_push_to_protected_branches;
70
+ if (typeof raw.use_worktrees === "boolean") result.use_worktrees = raw.use_worktrees;
71
+ if (typeof raw.dream_interval_hours === "number" && Number.isFinite(raw.dream_interval_hours)) {
72
+ result.dream_interval_hours = raw.dream_interval_hours;
73
+ }
74
+ if (typeof raw.dco === "boolean") result.dco = raw.dco;
75
+ if (typeof raw.comment_signature === "boolean") result.comment_signature = raw.comment_signature;
76
+ if (typeof raw.review_loop_enforcement === "boolean") result.review_loop_enforcement = raw.review_loop_enforcement;
77
+ if (typeof raw.orchestrator_edit_write_block === "boolean") result.orchestrator_edit_write_block = raw.orchestrator_edit_write_block;
78
+ if (typeof raw.pidash_enable === "boolean") result.pidash_enable = raw.pidash_enable;
79
+ if (typeof raw.pidiff_enable === "boolean") result.pidiff_enable = raw.pidiff_enable;
80
+ if (typeof raw.pidash_port === "number" && Number.isInteger(raw.pidash_port) && raw.pidash_port > 0 && raw.pidash_port <= 65535) {
81
+ result.pidash_port = raw.pidash_port;
82
+ }
83
+ if (typeof raw.image_model === "string" && raw.image_model.trim()) {
84
+ result.image_model = raw.image_model.trim();
85
+ }
86
+ if (typeof raw.async_llm_provider === "string" && raw.async_llm_provider.trim()) {
87
+ result.async_llm_provider = raw.async_llm_provider.trim();
88
+ }
89
+ if (typeof raw.async_llm_model === "string" && raw.async_llm_model.trim()) {
90
+ result.async_llm_model = raw.async_llm_model.trim();
91
+ }
92
+ const parsedMaxCycles = parseReviewLoopMaxCycles(raw.review_loop_max_cycles);
93
+ if (parsedMaxCycles !== undefined) {
94
+ result.review_loop_max_cycles = parsedMaxCycles;
95
+ }
96
+ if (typeof raw.acpx_agents === "string") {
97
+ result.acpx_agents = raw.acpx_agents;
98
+ } else if (Array.isArray(raw.acpx_agents)) {
99
+ result.acpx_agents = raw.acpx_agents;
100
+ }
101
+ if (typeof raw.cli_agents === "string") {
102
+ result.cli_agents = raw.cli_agents;
103
+ } else if (Array.isArray(raw.cli_agents)) {
104
+ result.cli_agents = raw.cli_agents;
105
+ }
106
+ if (typeof raw.agent_provider === "string") {
107
+ result.agent_provider = raw.agent_provider.trim();
108
+ }
109
+ if (typeof raw.agent_model === "string") {
110
+ result.agent_model = raw.agent_model.trim();
111
+ }
112
+ if (typeof raw.agent_overrides === "object" && raw.agent_overrides !== null && !Array.isArray(raw.agent_overrides)) {
113
+ const overrides: Record<string, { provider?: string | null; model?: string | null }> = {};
114
+ for (const [name, val] of Object.entries(raw.agent_overrides)) {
115
+ if (typeof val === "object" && val !== null && !Array.isArray(val)) {
116
+ const v = val as { provider?: unknown; model?: unknown };
117
+ const entry: { provider?: string | null; model?: string | null } = {};
118
+ if (v.provider === null || (typeof v.provider === "string")) entry.provider = v.provider === null ? null : v.provider.trim() || undefined;
119
+ if (v.model === null || (typeof v.model === "string")) entry.model = v.model === null ? null : v.model.trim() || undefined;
120
+ if (entry.provider !== undefined || entry.model !== undefined) overrides[name] = entry;
121
+ }
122
+ }
123
+ if (Object.keys(overrides).length > 0) result.agent_overrides = overrides;
124
+ }
125
+ return result;
126
+ } catch (e: any) {
127
+ console.debug(`[project-settings] failed to parse ${filePath}:`, e?.message?.slice(0, 100));
128
+ return {};
129
+ }
130
+ }
131
+
132
+ function loadProjectSettings(cwd: string): ProjectSettings {
133
+ return parseSettingsFile(getSettingsPath(cwd));
134
+ }
135
+
136
+ /** Override global settings path for testing. When set, loadGlobalSettings reads from this instead of ~/.pi/. */
137
+ let globalSettingsPathOverride: string | null = null;
138
+
139
+ /** Set a custom global settings path (for tests). Pass null to restore default. Clears cache immediately. */
140
+ export function setGlobalSettingsPath(path: string | null): void {
141
+ globalSettingsPathOverride = path;
142
+ clearSettingsCache();
143
+ }
144
+
145
+ function loadGlobalSettings(): ProjectSettings {
146
+ const globalPath = globalSettingsPathOverride ?? join(homedir(), ".pi", SETTINGS_FILENAME);
147
+ return parseSettingsFile(globalPath);
148
+ }
149
+
150
+ function projectSettingsFileHasKey(cwd: string, key: string): boolean {
151
+ const filePath = getSettingsPath(cwd);
152
+ if (!existsSync(filePath)) return false;
153
+ try {
154
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
155
+ return typeof raw === "object" && raw !== null && !Array.isArray(raw) && key in raw;
156
+ } catch (e: any) {
157
+ console.debug(`[project-settings] failed to check key in ${filePath}:`, e?.message?.slice(0, 100));
158
+ return false;
159
+ }
160
+ }
161
+
162
+ function parseDisabledEnv(name: string): boolean | undefined {
163
+ const val = process.env[name];
164
+ if (val === undefined || val === "") return undefined;
165
+ return ["false", "0", "no", "off"].includes(val.toLowerCase());
166
+ }
167
+
168
+ function parseBoolEnv(name: string): boolean | undefined {
169
+ const val = process.env[name];
170
+ if (val === undefined || val === "") return undefined;
171
+ return ["true", "1", "yes", "on"].includes(val.toLowerCase());
172
+ }
173
+
174
+ function parsePortEnv(name: string): number | undefined {
175
+ const val = process.env[name];
176
+ if (val === undefined || val === "") return undefined;
177
+ const port = parseInt(val, 10);
178
+ return Number.isFinite(port) && port > 0 && port <= 65535 ? port : undefined;
179
+ }
180
+
181
+ /** Parse agent name lists (acpx_agents / cli_agents) — comma-separated string or JSON array. */
182
+ export function parseAgentNameList(value: string | string[] | undefined): string[] {
183
+ if (value === undefined) return [];
184
+ const parts = Array.isArray(value)
185
+ ? value.map((a) => (typeof a === "string" ? a.trim().toLowerCase() : ""))
186
+ : value.split(",").map((a) => a.trim().toLowerCase());
187
+ const filtered = parts.filter((a) => /^[a-z0-9_-]+$/.test(a));
188
+ return [...new Set(filtered)];
189
+ }
190
+
191
+ /**
192
+ * Coerce a getSetting result to string[].
193
+ * Mixed/stale ~/.pi installs may pair a new provider with an older project-settings
194
+ * that returns `false` for unknown keys — never assume .filter/.map is safe.
195
+ */
196
+ export function asStringArray(value: unknown): string[] {
197
+ if (!Array.isArray(value)) return [];
198
+ return value.filter((x): x is string => typeof x === "string");
199
+ }
200
+
201
+ /** @deprecated Use parseAgentNameList — kept for existing imports. */
202
+ export function parseAcpxAgentList(value: string | string[] | undefined): string[] {
203
+ return parseAgentNameList(value);
204
+ }
205
+
206
+ function parseNumEnv(name: string): number | undefined {
207
+ const val = process.env[name];
208
+ if (val === undefined || val === "") return undefined;
209
+ const n = parseFloat(val);
210
+ return Number.isFinite(n) ? n : undefined;
211
+ }
212
+
213
+ /**
214
+ * Parse review_loop_max_cycles from a raw settings-file or env value.
215
+ * Accepts an integer 1-10, or a digit string "1"-"10" only (no exponents/binary/hex/decimals).
216
+ * Returns undefined for anything else (0, negative, >10, NaN, "inf", Infinity, "1e1", "10.0", …) —
217
+ * callers should fall through to the next resolution layer (global settings → env var → default 3).
218
+ */
219
+ export function parseReviewLoopMaxCycles(raw: unknown): number | undefined {
220
+ if (typeof raw === "number") {
221
+ return Number.isInteger(raw) && raw >= 1 && raw <= 10 ? raw : undefined;
222
+ }
223
+ if (typeof raw === "string") {
224
+ const trimmed = raw.trim();
225
+ // Strict digit strings only — reject "1e1", "0b1010", "10.0", "01", etc.
226
+ if (!/^(?:[1-9]|10)$/.test(trimmed)) return undefined;
227
+ return parseInt(trimmed, 10);
228
+ }
229
+ return undefined;
230
+ }
231
+
232
+ /** Parse PI_REVIEW_LOOP_MAX_CYCLES env var — digit string "1"-"10" only (same rules as parseReviewLoopMaxCycles). */
233
+ function parseReviewLoopMaxCyclesEnv(name: string): number | undefined {
234
+ const val = process.env[name];
235
+ if (val === undefined || val === "") return undefined;
236
+ return parseReviewLoopMaxCycles(val);
237
+ }
238
+
239
+ /** Cached settings per cwd */
240
+ let cachedCwd = "";
241
+ let cachedSettings: ProjectSettings = {};
242
+ let cachedMtime = 0;
243
+ let cachedGlobalMtime = 0;
244
+ let lastMtimeCheck = 0;
245
+ const MTIME_CHECK_INTERVAL_MS = 30_000; // Check file mtime at most every 30s
246
+
247
+ function getSettings(cwd: string): ProjectSettings {
248
+ const now = Date.now();
249
+ // Different cwd — always reload
250
+ if (cwd !== cachedCwd) {
251
+ const projectSettings = loadProjectSettings(cwd);
252
+ const globalSettings = loadGlobalSettings();
253
+ cachedSettings = { ...globalSettings, ...projectSettings };
254
+ cachedCwd = cwd;
255
+ try {
256
+ const settingsPath = getSettingsPath(cwd);
257
+ cachedMtime = existsSync(settingsPath) ? statSync(settingsPath).mtimeMs : 0;
258
+ } catch { cachedMtime = 0; }
259
+ try {
260
+ const globalPath = globalSettingsPathOverride ?? join(homedir(), ".pi", SETTINGS_FILENAME);
261
+ cachedGlobalMtime = existsSync(globalPath) ? statSync(globalPath).mtimeMs : 0;
262
+ } catch { cachedGlobalMtime = 0; }
263
+ lastMtimeCheck = now;
264
+ return cachedSettings;
265
+ }
266
+ // Same cwd — throttle mtime checks
267
+ if (now - lastMtimeCheck < MTIME_CHECK_INTERVAL_MS) return cachedSettings;
268
+ lastMtimeCheck = now;
269
+ const settingsPath = getSettingsPath(cwd);
270
+ const globalPath = globalSettingsPathOverride ?? join(homedir(), ".pi", SETTINGS_FILENAME);
271
+ let mtime = 0;
272
+ let globalMtime = 0;
273
+ try { if (existsSync(settingsPath)) mtime = statSync(settingsPath).mtimeMs; } catch {}
274
+ try { if (existsSync(globalPath)) globalMtime = statSync(globalPath).mtimeMs; } catch {}
275
+ if (mtime === cachedMtime && globalMtime === cachedGlobalMtime) return cachedSettings;
276
+ cachedSettings = { ...loadGlobalSettings(), ...loadProjectSettings(cwd) };
277
+ cachedMtime = mtime;
278
+ cachedGlobalMtime = globalMtime;
279
+ return cachedSettings;
280
+ }
281
+
282
+ /** Clear cache — call after migration or manual edits */
283
+ export function clearSettingsCache(): void {
284
+ cachedCwd = "";
285
+ cachedSettings = {};
286
+ }
287
+
288
+ /**
289
+ * Get a setting value. Resolution: project file → global ~/.pi/pi-config-settings.json → env var → default.
290
+ */
291
+ export function getSetting(cwd: string, key: "commit_trailer"): boolean | string;
292
+ export function getSetting(cwd: string, key: "allow_push_to_protected_branches"): boolean;
293
+ export function getSetting(cwd: string, key: "use_worktrees"): boolean;
294
+ export function getSetting(cwd: string, key: "dream_interval_hours"): number;
295
+ export function getSetting(cwd: string, key: "dco"): boolean;
296
+ export function getSetting(cwd: string, key: "comment_signature"): boolean;
297
+ export function getSetting(cwd: string, key: "review_loop_enforcement"): boolean;
298
+ export function getSetting(cwd: string, key: "orchestrator_edit_write_block"): boolean;
299
+ export function getSetting(cwd: string, key: "acpx_agents"): string[];
300
+ export function getSetting(cwd: string, key: "cli_agents"): string[];
301
+ export function getSetting(cwd: string, key: "pidash_enable"): boolean;
302
+ export function getSetting(cwd: string, key: "pidiff_enable"): boolean;
303
+ export function getSetting(cwd: string, key: "pidash_port"): number;
304
+ export function getSetting(cwd: string, key: "image_model"): string;
305
+ export function getSetting(cwd: string, key: "async_llm_provider"): string;
306
+ export function getSetting(cwd: string, key: "async_llm_model"): string;
307
+ export function getSetting(cwd: string, key: "review_loop_max_cycles"): number;
308
+ export function getSetting(cwd: string, key: "agent_provider"): string;
309
+ export function getSetting(cwd: string, key: "agent_model"): string;
310
+ export function getSetting(cwd: string, key: "agent_overrides"): Record<string, { provider?: string | null; model?: string | null }>;
311
+ export function getSetting(cwd: string, key: string): boolean | string | number | string[] | Record<string, { provider?: string | null; model?: string | null }> {
312
+ const settings = getSettings(cwd);
313
+
314
+ switch (key) {
315
+ case "commit_trailer": {
316
+ if (settings.commit_trailer !== undefined) return settings.commit_trailer;
317
+ const envStr = process.env.PI_COMMIT_TRAILER;
318
+ if (envStr !== undefined && envStr !== "") {
319
+ if (["true", "1", "yes", "on"].includes(envStr.toLowerCase())) return true;
320
+ if (["false", "0", "no", "off"].includes(envStr.toLowerCase())) return false;
321
+ return envStr; // treat as custom trailer string
322
+ }
323
+ return false; // default: disabled
324
+ }
325
+ case "allow_push_to_protected_branches": {
326
+ if (settings.allow_push_to_protected_branches !== undefined) return settings.allow_push_to_protected_branches;
327
+ const env = parseBoolEnv("PI_ALLOW_PUSH_TO_PROTECTED_BRANCHES");
328
+ if (env !== undefined) return env;
329
+ return false; // default: block pushes to protected branches
330
+ }
331
+ case "use_worktrees": {
332
+ if (settings.use_worktrees !== undefined) return settings.use_worktrees;
333
+ const env = parseBoolEnv("PI_USE_WORKTREES");
334
+ if (env !== undefined) return env;
335
+ return false; // default: disabled
336
+ }
337
+ case "dream_interval_hours": {
338
+ if (settings.dream_interval_hours !== undefined) return settings.dream_interval_hours;
339
+ const env = parseNumEnv("PI_DREAM_INTERVAL_HOURS");
340
+ if (env !== undefined) return env;
341
+ return 3; // default: 3 hours
342
+ }
343
+ case "dco": {
344
+ if (settings.dco !== undefined) return settings.dco;
345
+ const env = parseBoolEnv("PI_DCO");
346
+ if (env !== undefined) return env;
347
+ return false; // default: disabled
348
+ }
349
+ case "comment_signature": {
350
+ if (settings.comment_signature !== undefined) return settings.comment_signature;
351
+ return false; // default: disabled
352
+ }
353
+ case "review_loop_enforcement": {
354
+ if (settings.review_loop_enforcement !== undefined) return settings.review_loop_enforcement;
355
+ const env = parseBoolEnv("PI_REVIEW_LOOP_ENFORCEMENT");
356
+ if (env !== undefined) return env;
357
+ return false; // default: disabled (opt-in)
358
+ }
359
+ case "orchestrator_edit_write_block": {
360
+ if (settings.orchestrator_edit_write_block !== undefined) return settings.orchestrator_edit_write_block;
361
+ return false; // default: disabled
362
+ }
363
+ case "acpx_agents": {
364
+ if (projectSettingsFileHasKey(cwd, "acpx_agents")) {
365
+ const projectValue = loadProjectSettings(cwd).acpx_agents;
366
+ // Only override when the raw value parsed as a valid type (string or array).
367
+ // Invalid types (number, object, etc.) are skipped by parseSettingsFile,
368
+ // leaving projectValue undefined — fall through to global/env.
369
+ if (projectValue !== undefined) {
370
+ return parseAgentNameList(projectValue);
371
+ }
372
+ }
373
+ const globalAgents = loadGlobalSettings().acpx_agents;
374
+ if (globalAgents !== undefined) {
375
+ return parseAgentNameList(globalAgents);
376
+ }
377
+ const env = process.env.ACPX_AGENTS;
378
+ if (env !== undefined && env !== "") {
379
+ return parseAgentNameList(env);
380
+ }
381
+ return [];
382
+ }
383
+ case "cli_agents": {
384
+ if (projectSettingsFileHasKey(cwd, "cli_agents")) {
385
+ const projectValue = loadProjectSettings(cwd).cli_agents;
386
+ if (projectValue !== undefined) {
387
+ return parseAgentNameList(projectValue);
388
+ }
389
+ }
390
+ const globalAgents = loadGlobalSettings().cli_agents;
391
+ if (globalAgents !== undefined) {
392
+ return parseAgentNameList(globalAgents);
393
+ }
394
+ const env = process.env.CLI_AGENTS;
395
+ if (env !== undefined && env !== "") {
396
+ return parseAgentNameList(env);
397
+ }
398
+ return [];
399
+ }
400
+ case "pidash_enable": {
401
+ if (settings.pidash_enable !== undefined) return settings.pidash_enable;
402
+ const disabled = parseDisabledEnv("PI_PIDASH_ENABLE");
403
+ if (disabled !== undefined) return !disabled;
404
+ return true;
405
+ }
406
+ case "pidiff_enable": {
407
+ if (settings.pidiff_enable !== undefined) return settings.pidiff_enable;
408
+ const disabled = parseDisabledEnv("PI_PIDIFF_ENABLE");
409
+ if (disabled !== undefined) return !disabled;
410
+ return true;
411
+ }
412
+ case "pidash_port": {
413
+ if (settings.pidash_port !== undefined) {
414
+ // Defensive: validate even though parseSettingsFile already checks
415
+ if (Number.isInteger(settings.pidash_port) && settings.pidash_port > 0 && settings.pidash_port <= 65535) {
416
+ return settings.pidash_port;
417
+ }
418
+ // Invalid value in merged settings — fall through to env/default
419
+ }
420
+ const envPort = parsePortEnv("PI_PIDASH_PORT");
421
+ if (envPort !== undefined) return envPort;
422
+ return 19190;
423
+ }
424
+ case "image_model": {
425
+ if (settings.image_model !== undefined) return settings.image_model;
426
+ const env = process.env.PI_IMAGE_MODEL;
427
+ return env !== undefined && env !== "" ? env : "";
428
+ }
429
+ case "async_llm_provider": {
430
+ if (settings.async_llm_provider !== undefined) return settings.async_llm_provider;
431
+ const env = process.env.PI_ASYNC_LLM_PROVIDER;
432
+ return env !== undefined && env !== "" ? env.trim() : "";
433
+ }
434
+ case "async_llm_model": {
435
+ if (settings.async_llm_model !== undefined) return settings.async_llm_model;
436
+ const env = process.env.PI_ASYNC_LLM_MODEL;
437
+ return env !== undefined && env !== "" ? env.trim() : "";
438
+ }
439
+ case "review_loop_max_cycles": {
440
+ if (settings.review_loop_max_cycles !== undefined) return settings.review_loop_max_cycles;
441
+ const env = parseReviewLoopMaxCyclesEnv("PI_REVIEW_LOOP_MAX_CYCLES");
442
+ if (env !== undefined) return env;
443
+ return 3; // default: 3 cycles
444
+ }
445
+ case "agent_provider": {
446
+ return settings.agent_provider || "";
447
+ }
448
+ case "agent_model": {
449
+ return settings.agent_model || "";
450
+ }
451
+ case "agent_overrides": {
452
+ return settings.agent_overrides || {};
453
+ }
454
+ default:
455
+ return false;
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Register project settings — runs migration on session_start.
461
+ */
462
+ export function registerProjectSettings(pi: ExtensionAPI): void {
463
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
464
+
465
+ pi.on("session_start", (_event, ctx) => {
466
+ clearSettingsCache();
467
+ });
468
+ }