@yycholla/pi-dynamic-workflows 3.3.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 (179) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +325 -0
  3. package/assets/readme/hero.png +0 -0
  4. package/assets/readme/hero.svg +70 -0
  5. package/assets/readme/package-cover.png +0 -0
  6. package/assets/readme/package-cover.svg +66 -0
  7. package/assets/readme/workflow.png +0 -0
  8. package/assets/readme/workflow.svg +77 -0
  9. package/dist/accept-workflow-guidance.d.ts +14 -0
  10. package/dist/accept-workflow-guidance.js +53 -0
  11. package/dist/adversarial-review.d.ts +30 -0
  12. package/dist/adversarial-review.js +107 -0
  13. package/dist/agent-history.d.ts +20 -0
  14. package/dist/agent-history.js +122 -0
  15. package/dist/agent-registry.d.ts +83 -0
  16. package/dist/agent-registry.js +190 -0
  17. package/dist/agent.d.ts +364 -0
  18. package/dist/agent.js +714 -0
  19. package/dist/builtin-commands.d.ts +19 -0
  20. package/dist/builtin-commands.js +251 -0
  21. package/dist/builtin-workflows.d.ts +45 -0
  22. package/dist/builtin-workflows.js +121 -0
  23. package/dist/code-review.d.ts +26 -0
  24. package/dist/code-review.js +181 -0
  25. package/dist/config.d.ts +37 -0
  26. package/dist/config.js +44 -0
  27. package/dist/deep-research.d.ts +30 -0
  28. package/dist/deep-research.js +124 -0
  29. package/dist/display.d.ts +134 -0
  30. package/dist/display.js +248 -0
  31. package/dist/effort-command.d.ts +28 -0
  32. package/dist/effort-command.js +68 -0
  33. package/dist/enums.d.ts +69 -0
  34. package/dist/enums.js +78 -0
  35. package/dist/errors.d.ts +113 -0
  36. package/dist/errors.js +140 -0
  37. package/dist/extension-reload.d.ts +37 -0
  38. package/dist/extension-reload.js +78 -0
  39. package/dist/fs-persistence.d.ts +63 -0
  40. package/dist/fs-persistence.js +102 -0
  41. package/dist/index.d.ts +57 -0
  42. package/dist/index.js +35 -0
  43. package/dist/logger.d.ts +21 -0
  44. package/dist/logger.js +66 -0
  45. package/dist/model-routing.d.ts +30 -0
  46. package/dist/model-routing.js +50 -0
  47. package/dist/model-spec.d.ts +29 -0
  48. package/dist/model-spec.js +252 -0
  49. package/dist/model-tier-config.d.ts +133 -0
  50. package/dist/model-tier-config.js +249 -0
  51. package/dist/run-persistence.d.ts +180 -0
  52. package/dist/run-persistence.js +294 -0
  53. package/dist/saved-commands.d.ts +28 -0
  54. package/dist/saved-commands.js +100 -0
  55. package/dist/shared-store.d.ts +98 -0
  56. package/dist/shared-store.js +212 -0
  57. package/dist/structured-output.d.ts +19 -0
  58. package/dist/structured-output.js +30 -0
  59. package/dist/task-panel.d.ts +61 -0
  60. package/dist/task-panel.js +422 -0
  61. package/dist/usage-limit-scheduler.d.ts +145 -0
  62. package/dist/usage-limit-scheduler.js +368 -0
  63. package/dist/web-tools.d.ts +20 -0
  64. package/dist/web-tools.js +120 -0
  65. package/dist/workflow-authoring-coverage.d.ts +70 -0
  66. package/dist/workflow-authoring-coverage.js +421 -0
  67. package/dist/workflow-authoring-reference.d.ts +20 -0
  68. package/dist/workflow-authoring-reference.js +156 -0
  69. package/dist/workflow-capability-contract.d.ts +131 -0
  70. package/dist/workflow-capability-contract.js +604 -0
  71. package/dist/workflow-commands.d.ts +18 -0
  72. package/dist/workflow-commands.js +260 -0
  73. package/dist/workflow-comprehension.d.ts +133 -0
  74. package/dist/workflow-comprehension.js +1321 -0
  75. package/dist/workflow-context-measurement.d.ts +72 -0
  76. package/dist/workflow-context-measurement.js +213 -0
  77. package/dist/workflow-control-tool.d.ts +30 -0
  78. package/dist/workflow-control-tool.js +176 -0
  79. package/dist/workflow-delivery-choice.d.ts +20 -0
  80. package/dist/workflow-delivery-choice.js +48 -0
  81. package/dist/workflow-editor.d.ts +93 -0
  82. package/dist/workflow-editor.js +363 -0
  83. package/dist/workflow-manager.d.ts +492 -0
  84. package/dist/workflow-manager.js +1124 -0
  85. package/dist/workflow-paths.d.ts +22 -0
  86. package/dist/workflow-paths.js +46 -0
  87. package/dist/workflow-release-gate.d.ts +39 -0
  88. package/dist/workflow-release-gate.js +309 -0
  89. package/dist/workflow-saved.d.ts +38 -0
  90. package/dist/workflow-saved.js +126 -0
  91. package/dist/workflow-settings.d.ts +70 -0
  92. package/dist/workflow-settings.js +131 -0
  93. package/dist/workflow-tool.d.ts +71 -0
  94. package/dist/workflow-tool.js +367 -0
  95. package/dist/workflow-ui.d.ts +182 -0
  96. package/dist/workflow-ui.js +1587 -0
  97. package/dist/workflow.d.ts +333 -0
  98. package/dist/workflow.js +1151 -0
  99. package/dist/workflows-models-command.d.ts +31 -0
  100. package/dist/workflows-models-command.js +156 -0
  101. package/dist/worktree.d.ts +25 -0
  102. package/dist/worktree.js +61 -0
  103. package/extensions/workflow.ts +151 -0
  104. package/package.json +104 -0
  105. package/skills/workflow-authoring/SKILL.md +30 -0
  106. package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
  107. package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
  108. package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
  109. package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
  110. package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
  111. package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
  112. package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
  113. package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
  114. package/skills/workflow-authoring/examples/structured-output.js +37 -0
  115. package/skills/workflow-authoring/examples/tournament.js +81 -0
  116. package/skills/workflow-authoring/examples/validated-gate.js +63 -0
  117. package/skills/workflow-authoring/references/capabilities.md +41 -0
  118. package/skills/workflow-authoring/references/capability-details.md +357 -0
  119. package/skills/workflow-authoring/references/common-helpers.md +4 -0
  120. package/skills/workflow-authoring/references/debugging.md +27 -0
  121. package/skills/workflow-authoring/references/focused-recipes.md +11 -0
  122. package/skills/workflow-authoring/references/helpers.md +9 -0
  123. package/skills/workflow-authoring/references/lifecycle.md +33 -0
  124. package/skills/workflow-authoring/references/pattern-selection.md +15 -0
  125. package/skills/workflow-authoring/references/quality-helpers.md +8 -0
  126. package/skills/workflow-authoring/references/registry-ownership.md +15 -0
  127. package/skills/workflow-authoring/references/retry-helper.md +5 -0
  128. package/skills/workflow-authoring/references/review.md +43 -0
  129. package/skills/workflow-authoring/references/runtime.md +27 -0
  130. package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
  131. package/skills/workflow-authoring/references/versions.md +13 -0
  132. package/skills/workflow-patterns/SKILL.md +51 -0
  133. package/src/accept-workflow-guidance.ts +71 -0
  134. package/src/adversarial-review.ts +120 -0
  135. package/src/agent-history.ts +157 -0
  136. package/src/agent-registry.ts +221 -0
  137. package/src/agent.ts +929 -0
  138. package/src/builtin-commands.ts +286 -0
  139. package/src/builtin-workflows.ts +155 -0
  140. package/src/code-review.ts +183 -0
  141. package/src/config.ts +55 -0
  142. package/src/deep-research.ts +135 -0
  143. package/src/display.ts +367 -0
  144. package/src/effort-command.ts +87 -0
  145. package/src/enums.ts +77 -0
  146. package/src/errors.ts +199 -0
  147. package/src/extension-reload.ts +100 -0
  148. package/src/fs-persistence.ts +124 -0
  149. package/src/index.ts +176 -0
  150. package/src/logger.ts +88 -0
  151. package/src/model-routing.ts +73 -0
  152. package/src/model-spec.ts +309 -0
  153. package/src/model-tier-config.ts +296 -0
  154. package/src/run-persistence.ts +484 -0
  155. package/src/saved-commands.ts +115 -0
  156. package/src/shared-store.ts +228 -0
  157. package/src/structured-output.ts +47 -0
  158. package/src/task-panel.ts +490 -0
  159. package/src/usage-limit-scheduler.ts +432 -0
  160. package/src/web-tools.ts +124 -0
  161. package/src/workflow-authoring-coverage.ts +486 -0
  162. package/src/workflow-authoring-reference.ts +186 -0
  163. package/src/workflow-capability-contract.ts +806 -0
  164. package/src/workflow-commands.ts +287 -0
  165. package/src/workflow-comprehension.ts +1673 -0
  166. package/src/workflow-context-measurement.ts +262 -0
  167. package/src/workflow-control-tool.ts +238 -0
  168. package/src/workflow-delivery-choice.ts +69 -0
  169. package/src/workflow-editor.ts +444 -0
  170. package/src/workflow-manager.ts +1405 -0
  171. package/src/workflow-paths.ts +63 -0
  172. package/src/workflow-release-gate.ts +529 -0
  173. package/src/workflow-saved.ts +180 -0
  174. package/src/workflow-settings.ts +194 -0
  175. package/src/workflow-tool.ts +464 -0
  176. package/src/workflow-ui.ts +1789 -0
  177. package/src/workflow.ts +1615 -0
  178. package/src/workflows-models-command.ts +211 -0
  179. package/src/worktree.ts +76 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Save and load reusable workflow commands.
3
+ */
4
+
5
+ import { join } from "node:path";
6
+ import {
7
+ ensureDir as ensureDirFs,
8
+ listJsonFilesSafe,
9
+ type PersistenceFsLayer,
10
+ readJsonWithBackupRecovery,
11
+ resolvePersistenceFs,
12
+ unlinkIfExistsSafe,
13
+ writeJsonAtomicWithBackup,
14
+ } from "./fs-persistence.js";
15
+ import { workflowProjectPaths, workflowUserSavedDir } from "./workflow-paths.js";
16
+
17
+ export interface SavedWorkflow {
18
+ /** Command name (filename without extension). */
19
+ name: string;
20
+ /** Human-readable description. */
21
+ description: string;
22
+ /** The workflow script. */
23
+ script: string;
24
+ /** Optional parameter schema for parameterized workflows. */
25
+ parameters?: Record<string, { type: string; description?: string; required?: boolean; default?: unknown }>;
26
+ /** Where this workflow is saved. */
27
+ location: "project" | "user";
28
+ /** Full file path. */
29
+ path: string;
30
+ /** When it was saved. */
31
+ savedAt: string;
32
+ }
33
+
34
+ export interface WorkflowStorage {
35
+ /** Save a workflow. */
36
+ save(workflow: Omit<SavedWorkflow, "path" | "savedAt">, location?: "project" | "user"): SavedWorkflow;
37
+ /** Load a workflow by name. */
38
+ load(name: string): SavedWorkflow | null;
39
+ /** List all saved workflows. */
40
+ list(): SavedWorkflow[];
41
+ /** Delete a saved workflow. */
42
+ delete(name: string, location?: "project" | "user"): boolean;
43
+ }
44
+
45
+ export function isSafeSavedWorkflowName(name: string): boolean {
46
+ return (
47
+ name.length > 0 &&
48
+ name.length <= 128 &&
49
+ name.trim() === name &&
50
+ name !== "." &&
51
+ name !== ".." &&
52
+ !/[/\\\0]/.test(name)
53
+ );
54
+ }
55
+
56
+ export function assertSafeSavedWorkflowName(name: string): void {
57
+ if (!isSafeSavedWorkflowName(name)) {
58
+ throw new Error("Saved workflow name must be a non-empty path-safe name without slashes.");
59
+ }
60
+ }
61
+
62
+ export function createWorkflowStorage(cwd: string, fsOverride?: Partial<PersistenceFsLayer>): WorkflowStorage {
63
+ const fs = resolvePersistenceFs(fsOverride);
64
+ const paths = workflowProjectPaths(cwd);
65
+ const projectDir = paths.savedDir;
66
+ const legacyProjectDir = paths.legacySavedDir;
67
+ const userDir = workflowUserSavedDir();
68
+
69
+ const ensureDir = (dir: string) => ensureDirFs(fs, dir);
70
+
71
+ const workflowPath = (name: string, location: "project" | "user") => {
72
+ assertSafeSavedWorkflowName(name);
73
+ const dir = location === "project" ? projectDir : userDir;
74
+ return join(dir, `${name}.json`);
75
+ };
76
+ const legacyProjectWorkflowPath = (name: string) => {
77
+ assertSafeSavedWorkflowName(name);
78
+ return join(legacyProjectDir, `${name}.json`);
79
+ };
80
+
81
+ // Same atomic-write-with-backup + corrupt-file recovery contract as
82
+ // run-persistence.ts (see fs-persistence.ts) — a saved workflow is a
83
+ // user-authored artifact just as worth protecting from a crash mid-write
84
+ // or a truncated file as a run's resumable state is.
85
+ const loadFromFile = (path: string, location: "project" | "user"): SavedWorkflow | null => {
86
+ const data = readJsonWithBackupRecovery<Record<string, unknown>>(fs, path);
87
+ if (!data || typeof data !== "object" || !isSafeSavedWorkflowName((data as { name?: string }).name ?? "")) {
88
+ return null;
89
+ }
90
+ return {
91
+ ...(data as Omit<SavedWorkflow, "location" | "path">),
92
+ location,
93
+ path,
94
+ };
95
+ };
96
+
97
+ return {
98
+ save(workflow, location = "project") {
99
+ assertSafeSavedWorkflowName(workflow.name);
100
+ const dir = location === "project" ? projectDir : userDir;
101
+ ensureDir(dir);
102
+
103
+ const path = workflowPath(workflow.name, location);
104
+ const saved: SavedWorkflow = {
105
+ ...workflow,
106
+ location,
107
+ path,
108
+ savedAt: new Date().toISOString(),
109
+ };
110
+
111
+ writeJsonAtomicWithBackup(fs, path, saved);
112
+ return saved;
113
+ },
114
+
115
+ load(name: string): SavedWorkflow | null {
116
+ if (!isSafeSavedWorkflowName(name)) return null;
117
+ // Project takes precedence over user
118
+ const projectPath = workflowPath(name, "project");
119
+ const project = loadFromFile(projectPath, "project");
120
+ if (project) return project;
121
+
122
+ const legacyProject = loadFromFile(legacyProjectWorkflowPath(name), "project");
123
+ if (legacyProject) return legacyProject;
124
+
125
+ const userPath = workflowPath(name, "user");
126
+ return loadFromFile(userPath, "user");
127
+ },
128
+
129
+ list(): SavedWorkflow[] {
130
+ const workflows: SavedWorkflow[] = [];
131
+
132
+ const seen = new Set<string>();
133
+ const addDir = (dir: string, location: "project" | "user") => {
134
+ // A missing or unreadable directory (not yet created, deleted
135
+ // mid-race, permission-denied) degrades to "no files" here — same
136
+ // guard run-persistence.ts's list() uses — rather than throwing and
137
+ // taking down the whole listing over one bad storage location.
138
+ for (const file of listJsonFilesSafe(fs, dir)) {
139
+ const wf = loadFromFile(join(dir, file), location);
140
+ if (wf && !seen.has(wf.name)) {
141
+ seen.add(wf.name);
142
+ workflows.push(wf);
143
+ }
144
+ }
145
+ };
146
+
147
+ // Priority order mirrors load(): project > legacy project > user.
148
+ addDir(projectDir, "project");
149
+ addDir(legacyProjectDir, "project");
150
+ addDir(userDir, "user");
151
+
152
+ return workflows.sort((a, b) => a.name.localeCompare(b.name));
153
+ },
154
+
155
+ delete(name: string, location?: "project" | "user"): boolean {
156
+ if (!isSafeSavedWorkflowName(name)) return false;
157
+ const locations = location ? [location] : (["project", "user"] as const);
158
+ let deleted = false;
159
+
160
+ for (const loc of locations) {
161
+ const path = workflowPath(name, loc);
162
+ // Clean up the .bak sidecar too, mirroring run-persistence.ts's delete()
163
+ // (sidecar cleanup does not by itself count as "deleted the workflow").
164
+ unlinkIfExistsSafe(fs, `${path}.bak`);
165
+ if (unlinkIfExistsSafe(fs, path)) {
166
+ deleted = true;
167
+ }
168
+ if (loc === "project") {
169
+ const legacyPath = legacyProjectWorkflowPath(name);
170
+ unlinkIfExistsSafe(fs, `${legacyPath}.bak`);
171
+ if (unlinkIfExistsSafe(fs, legacyPath)) {
172
+ deleted = true;
173
+ }
174
+ }
175
+ }
176
+
177
+ return deleted;
178
+ },
179
+ };
180
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * User-level settings for pi-dynamic-workflows.
3
+ *
4
+ * Stored separately from Pi's own settings.json so extension preferences remain
5
+ * stable without depending on host-internal config shape.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { MAX_AGENT_RETRIES, MAX_CONCURRENCY, normalizeKeywordTriggerWord } from "./config.js";
11
+ import { workflowHomeDir, workflowProjectPaths } from "./workflow-paths.js";
12
+
13
+ export interface WorkflowSettings {
14
+ keywordTriggerEnabled?: boolean;
15
+ /** Literal keyword that arms workflows mode from interactive input. */
16
+ keywordTriggerWord?: string;
17
+ defaultAgentTimeoutMs?: number | null;
18
+ /**
19
+ * Default hard token budget applied to runs that don't pass their own
20
+ * `tokenBudget` (#68). null explicitly means "no budget" (useful in a
21
+ * project override to cancel a global budget); omitted also means no budget.
22
+ */
23
+ defaultTokenBudget?: number | null;
24
+ /** Default max concurrent agents per run. Clamped to the runtime maximum. */
25
+ defaultConcurrency?: number;
26
+ /** Default retry attempts after recoverable agent failures. */
27
+ defaultAgentRetries?: number;
28
+ /** Bottom task-panel display mode: "compact" (default, one line per run) | "detailed". */
29
+ progressPanelMode?: "compact" | "detailed";
30
+ /** Max agents shown per phase in detailed progress mode (default 8). */
31
+ progressPanelMaxAgents?: number;
32
+ /**
33
+ * Persist each workflow subagent transcript as a real pi session file under
34
+ * the standard sessions directory (~/.pi/agent/sessions/<encoded-cwd>/),
35
+ * keyed by the project cwd. Default false: subagent sessions stay in-memory
36
+ * and only the compacted history embedded in the run JSON survives.
37
+ */
38
+ persistAgentSessions?: boolean;
39
+ /**
40
+ * Character cap on a delivered background-run result's JSON-dump fallback
41
+ * before truncation (default 400). String results and `verdict`/`report`/
42
+ * `summary`/`synthesis` fields are never truncated.
43
+ */
44
+ deliveredResultMaxChars?: number;
45
+ /**
46
+ * Extra tool names to deny in workflow subagent sessions, on top of the
47
+ * always-on `workflow`/`workflow_control` defaults (#107). Use it to block
48
+ * other recursive-orchestration tools you have installed (e.g. a pi-subagents
49
+ * tool) so a subagent can't fan out through them.
50
+ */
51
+ excludeSubagentTools?: string[];
52
+ }
53
+
54
+ export interface WorkflowSettingsStore {
55
+ load(): WorkflowSettings;
56
+ save(settings: WorkflowSettings): void;
57
+ }
58
+
59
+ export interface WorkflowSettingsOptions {
60
+ /** Explicit settings path, primarily for tests and migrations. */
61
+ settingsPath?: string;
62
+ /** Project cwd whose project-level settings should override global settings. */
63
+ cwd?: string;
64
+ /** Explicit project settings path, primarily for tests. */
65
+ projectSettingsPath?: string;
66
+ /** Save destination when using saveWorkflowSettings with cwd. Default: global. */
67
+ scope?: "global" | "project";
68
+ }
69
+
70
+ /** Path to the user-level workflow settings JSON file (~/.pi/workflows/settings.json). */
71
+ export function getWorkflowSettingsPath(): string {
72
+ return join(workflowHomeDir(), "settings.json");
73
+ }
74
+
75
+ /** Path to this project's optional workflow settings override. */
76
+ export function getWorkflowProjectSettingsPath(cwd: string): string {
77
+ return workflowProjectPaths(cwd).settingsPath;
78
+ }
79
+
80
+ /** Load settings from disk. Missing, corrupt, or invalid files resolve to {}. */
81
+ export function loadWorkflowSettings(settingsPathOrOptions?: string | WorkflowSettingsOptions): WorkflowSettings {
82
+ const options = normalizeOptions(settingsPathOrOptions);
83
+ const globalSettings = readSettings(options.settingsPath ?? getWorkflowSettingsPath());
84
+ const projectPath =
85
+ options.projectSettingsPath ?? (options.cwd ? getWorkflowProjectSettingsPath(options.cwd) : undefined);
86
+ if (!projectPath) return globalSettings;
87
+ return { ...globalSettings, ...readSettings(projectPath) };
88
+ }
89
+
90
+ /** Merge known settings into the user-level settings file. */
91
+ export function saveWorkflowSettings(
92
+ settings: WorkflowSettings,
93
+ settingsPathOrOptions?: string | WorkflowSettingsOptions,
94
+ ): void {
95
+ const options = normalizeOptions(settingsPathOrOptions);
96
+ const projectPath =
97
+ options.projectSettingsPath ?? (options.cwd ? getWorkflowProjectSettingsPath(options.cwd) : undefined);
98
+ const path =
99
+ options.scope === "project" && projectPath ? projectPath : (options.settingsPath ?? getWorkflowSettingsPath());
100
+ const dir = dirname(path);
101
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
102
+
103
+ const existing = readObject(path);
104
+ writeFileSync(path, `${JSON.stringify({ ...existing, ...normalizeSettings(settings) }, null, 2)}\n`, "utf-8");
105
+ }
106
+
107
+ /** Save a global preference and update an existing project override if one is present. */
108
+ export function saveWorkflowSettingsForCwd(settings: WorkflowSettings, cwd: string): void {
109
+ saveWorkflowSettings(settings);
110
+ const projectPath = getWorkflowProjectSettingsPath(cwd);
111
+ if (existsSync(projectPath)) {
112
+ saveWorkflowSettings(settings, { projectSettingsPath: projectPath, scope: "project" });
113
+ }
114
+ }
115
+
116
+ function normalizeOptions(settingsPathOrOptions?: string | WorkflowSettingsOptions): WorkflowSettingsOptions {
117
+ return typeof settingsPathOrOptions === "string"
118
+ ? { settingsPath: settingsPathOrOptions }
119
+ : (settingsPathOrOptions ?? {});
120
+ }
121
+
122
+ function readSettings(path: string): WorkflowSettings {
123
+ if (!existsSync(path)) return {};
124
+ try {
125
+ return normalizeSettings(JSON.parse(readFileSync(path, "utf-8")));
126
+ } catch {
127
+ return {};
128
+ }
129
+ }
130
+
131
+ function normalizeSettings(value: unknown): WorkflowSettings {
132
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
133
+ const raw = value as Record<string, unknown>;
134
+ const settings: WorkflowSettings = {};
135
+ if (typeof raw.keywordTriggerEnabled === "boolean") {
136
+ settings.keywordTriggerEnabled = raw.keywordTriggerEnabled;
137
+ }
138
+ const keywordTriggerWord = normalizeKeywordTriggerWord(raw.keywordTriggerWord);
139
+ if (keywordTriggerWord !== undefined) settings.keywordTriggerWord = keywordTriggerWord;
140
+ if (raw.defaultAgentTimeoutMs === null) {
141
+ settings.defaultAgentTimeoutMs = null;
142
+ } else if (
143
+ typeof raw.defaultAgentTimeoutMs === "number" &&
144
+ Number.isFinite(raw.defaultAgentTimeoutMs) &&
145
+ raw.defaultAgentTimeoutMs > 0
146
+ ) {
147
+ settings.defaultAgentTimeoutMs = raw.defaultAgentTimeoutMs;
148
+ }
149
+ if (raw.defaultTokenBudget === null) {
150
+ settings.defaultTokenBudget = null;
151
+ } else {
152
+ const defaultTokenBudget = normalizeInteger(raw.defaultTokenBudget, 1, Number.MAX_SAFE_INTEGER);
153
+ if (defaultTokenBudget !== undefined) settings.defaultTokenBudget = defaultTokenBudget;
154
+ }
155
+ const defaultConcurrency = normalizeInteger(raw.defaultConcurrency, 1, MAX_CONCURRENCY);
156
+ if (defaultConcurrency !== undefined) settings.defaultConcurrency = defaultConcurrency;
157
+ const defaultAgentRetries = normalizeInteger(raw.defaultAgentRetries, 0, MAX_AGENT_RETRIES);
158
+ if (defaultAgentRetries !== undefined) settings.defaultAgentRetries = defaultAgentRetries;
159
+ if (raw.progressPanelMode === "compact" || raw.progressPanelMode === "detailed") {
160
+ settings.progressPanelMode = raw.progressPanelMode;
161
+ }
162
+ if (
163
+ typeof raw.progressPanelMaxAgents === "number" &&
164
+ Number.isFinite(raw.progressPanelMaxAgents) &&
165
+ raw.progressPanelMaxAgents >= 1
166
+ ) {
167
+ settings.progressPanelMaxAgents = Math.min(1000, Math.floor(raw.progressPanelMaxAgents));
168
+ }
169
+ if (typeof raw.persistAgentSessions === "boolean") {
170
+ settings.persistAgentSessions = raw.persistAgentSessions;
171
+ }
172
+ const deliveredResultMaxChars = normalizeInteger(raw.deliveredResultMaxChars, 1, 1_000_000);
173
+ if (deliveredResultMaxChars !== undefined) settings.deliveredResultMaxChars = deliveredResultMaxChars;
174
+ if (Array.isArray(raw.excludeSubagentTools)) {
175
+ const names = raw.excludeSubagentTools.filter((t): t is string => typeof t === "string" && t.trim().length > 0);
176
+ if (names.length) settings.excludeSubagentTools = names;
177
+ }
178
+ return settings;
179
+ }
180
+
181
+ function normalizeInteger(value: unknown, min: number, max: number): number | undefined {
182
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min) return undefined;
183
+ return Math.min(max, Math.floor(value));
184
+ }
185
+
186
+ function readObject(path: string): Record<string, unknown> {
187
+ if (!existsSync(path)) return {};
188
+ try {
189
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
190
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {};
191
+ } catch {
192
+ return {};
193
+ }
194
+ }