infinity-harness 2.8.9 → 2.8.11
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.
- package/CHANGELOG.md +18 -0
- package/dist/extensions/infinity-harness/index.js +3614 -0
- package/dist/extensions/infinity-harness/index.js.map +1 -0
- package/dist/src/approval.js +154 -0
- package/dist/src/approval.js.map +1 -0
- package/dist/src/core/brief.js +309 -0
- package/dist/src/core/brief.js.map +1 -0
- package/dist/src/core/config.js +539 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/exec.js +106 -0
- package/dist/src/core/exec.js.map +1 -0
- package/dist/src/core/featureList.js +365 -0
- package/dist/src/core/featureList.js.map +1 -0
- package/dist/src/core/fsx.js +123 -0
- package/dist/src/core/fsx.js.map +1 -0
- package/dist/src/core/gates.js +490 -0
- package/dist/src/core/gates.js.map +1 -0
- package/dist/src/core/init.js +480 -0
- package/dist/src/core/init.js.map +1 -0
- package/dist/src/core/lock.js +183 -0
- package/dist/src/core/lock.js.map +1 -0
- package/dist/src/core/modelRouter.js +185 -0
- package/dist/src/core/modelRouter.js.map +1 -0
- package/dist/src/core/paths.js +129 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/phases.js +254 -0
- package/dist/src/core/phases.js.map +1 -0
- package/dist/src/core/plan.js +15 -0
- package/dist/src/core/plan.js.map +1 -0
- package/dist/src/core/runState.js +126 -0
- package/dist/src/core/runState.js.map +1 -0
- package/dist/src/core/settings.js +877 -0
- package/dist/src/core/settings.js.map +1 -0
- package/dist/src/core/skills.js +319 -0
- package/dist/src/core/skills.js.map +1 -0
- package/dist/src/core/skillsAudit.js +194 -0
- package/dist/src/core/skillsAudit.js.map +1 -0
- package/dist/src/core/types.js +77 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/daemon/budget.js +85 -0
- package/dist/src/daemon/budget.js.map +1 -0
- package/dist/src/daemon/guard.js +122 -0
- package/dist/src/daemon/guard.js.map +1 -0
- package/dist/src/daemon/index.js +461 -0
- package/dist/src/daemon/index.js.map +1 -0
- package/dist/src/daemon/isolation.js +84 -0
- package/dist/src/daemon/isolation.js.map +1 -0
- package/dist/src/daemon/preflight.js +131 -0
- package/dist/src/daemon/preflight.js.map +1 -0
- package/dist/src/daemon/server.js +174 -0
- package/dist/src/daemon/server.js.map +1 -0
- package/dist/src/daemon/supervisorState.js +42 -0
- package/dist/src/daemon/supervisorState.js.map +1 -0
- package/dist/src/daemon/worker.js +239 -0
- package/dist/src/daemon/worker.js.map +1 -0
- package/dist/src/daemon/worktree.js +102 -0
- package/dist/src/daemon/worktree.js.map +1 -0
- package/dist/src/escalate.js +312 -0
- package/dist/src/escalate.js.map +1 -0
- package/dist/src/exec/piWorker.js +630 -0
- package/dist/src/exec/piWorker.js.map +1 -0
- package/dist/src/goal.js +317 -0
- package/dist/src/goal.js.map +1 -0
- package/dist/src/goalLoop.js +374 -0
- package/dist/src/goalLoop.js.map +1 -0
- package/dist/src/goalSpec.js +397 -0
- package/dist/src/goalSpec.js.map +1 -0
- package/dist/src/goalState.js +255 -0
- package/dist/src/goalState.js.map +1 -0
- package/dist/src/handoff.js +205 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/intake.js +203 -0
- package/dist/src/intake.js.map +1 -0
- package/dist/src/loop.js +592 -0
- package/dist/src/loop.js.map +1 -0
- package/dist/src/modelRouter.js +216 -0
- package/dist/src/modelRouter.js.map +1 -0
- package/dist/src/remote.js +246 -0
- package/dist/src/remote.js.map +1 -0
- package/dist/src/replan.js +262 -0
- package/dist/src/replan.js.map +1 -0
- package/dist/src/review.js +54 -0
- package/dist/src/review.js.map +1 -0
- package/dist/src/rework.js +241 -0
- package/dist/src/rework.js.map +1 -0
- package/dist/src/runState.js +4 -0
- package/dist/src/runState.js.map +1 -0
- package/dist/src/scheduler.js +270 -0
- package/dist/src/scheduler.js.map +1 -0
- package/dist/src/supervisor.js +787 -0
- package/dist/src/supervisor.js.map +1 -0
- package/dist/src/taskList.js +430 -0
- package/dist/src/taskList.js.map +1 -0
- package/dist/src/ui/config.js +268 -0
- package/dist/src/ui/config.js.map +1 -0
- package/dist/src/ui/dashboard.js +1288 -0
- package/dist/src/ui/dashboard.js.map +1 -0
- package/dist/src/ui/display.js +229 -0
- package/dist/src/ui/display.js.map +1 -0
- package/dist/src/ui/planTree.js +219 -0
- package/dist/src/ui/planTree.js.map +1 -0
- package/dist/src/ui/theme.js +298 -0
- package/dist/src/ui/theme.js.map +1 -0
- package/dist/src/ui/viewState.js +65 -0
- package/dist/src/ui/viewState.js.map +1 -0
- package/dist/src/ui/widget.js +721 -0
- package/dist/src/ui/widget.js.map +1 -0
- package/dist/src/ui/wizard.js +540 -0
- package/dist/src/ui/wizard.js.map +1 -0
- package/dist/src/unstuck.js +210 -0
- package/dist/src/unstuck.js.map +1 -0
- package/dist/src/worker.js +267 -0
- package/dist/src/worker.js.map +1 -0
- package/dist/src/workflow.js +250 -0
- package/dist/src/workflow.js.map +1 -0
- package/dist/tests/approval.test.js +144 -0
- package/dist/tests/approval.test.js.map +1 -0
- package/dist/tests/brief.test.js +271 -0
- package/dist/tests/brief.test.js.map +1 -0
- package/dist/tests/config.test.js +320 -0
- package/dist/tests/config.test.js.map +1 -0
- package/dist/tests/dashboard.test.js +277 -0
- package/dist/tests/dashboard.test.js.map +1 -0
- package/dist/tests/display.test.js +229 -0
- package/dist/tests/display.test.js.map +1 -0
- package/dist/tests/escalate.test.js +272 -0
- package/dist/tests/escalate.test.js.map +1 -0
- package/dist/tests/featureList.test.js +429 -0
- package/dist/tests/featureList.test.js.map +1 -0
- package/dist/tests/gates.test.js +333 -0
- package/dist/tests/gates.test.js.map +1 -0
- package/dist/tests/goal.test.js +229 -0
- package/dist/tests/goal.test.js.map +1 -0
- package/dist/tests/goalLoop.test.js +170 -0
- package/dist/tests/goalLoop.test.js.map +1 -0
- package/dist/tests/goalSpec.test.js +146 -0
- package/dist/tests/goalSpec.test.js.map +1 -0
- package/dist/tests/goalState.test.js +202 -0
- package/dist/tests/goalState.test.js.map +1 -0
- package/dist/tests/handoff.test.js +127 -0
- package/dist/tests/handoff.test.js.map +1 -0
- package/dist/tests/init.test.js +264 -0
- package/dist/tests/init.test.js.map +1 -0
- package/dist/tests/intake.test.js +288 -0
- package/dist/tests/intake.test.js.map +1 -0
- package/dist/tests/loop.test.js +453 -0
- package/dist/tests/loop.test.js.map +1 -0
- package/dist/tests/modelRouter.test.js +131 -0
- package/dist/tests/modelRouter.test.js.map +1 -0
- package/dist/tests/phases.test.js +244 -0
- package/dist/tests/phases.test.js.map +1 -0
- package/dist/tests/planTree.test.js +190 -0
- package/dist/tests/planTree.test.js.map +1 -0
- package/dist/tests/remote.test.js +380 -0
- package/dist/tests/remote.test.js.map +1 -0
- package/dist/tests/replan.test.js +113 -0
- package/dist/tests/replan.test.js.map +1 -0
- package/dist/tests/review.test.js +77 -0
- package/dist/tests/review.test.js.map +1 -0
- package/dist/tests/rework.test.js +109 -0
- package/dist/tests/rework.test.js.map +1 -0
- package/dist/tests/routing-live.test.js +121 -0
- package/dist/tests/routing-live.test.js.map +1 -0
- package/dist/tests/runState.test.js +73 -0
- package/dist/tests/runState.test.js.map +1 -0
- package/dist/tests/settings.test.js +311 -0
- package/dist/tests/settings.test.js.map +1 -0
- package/dist/tests/skills.test.js +387 -0
- package/dist/tests/skills.test.js.map +1 -0
- package/dist/tests/supervisor.test.js +437 -0
- package/dist/tests/supervisor.test.js.map +1 -0
- package/dist/tests/surface.test.js +203 -0
- package/dist/tests/surface.test.js.map +1 -0
- package/dist/tests/taskList.test.js +376 -0
- package/dist/tests/taskList.test.js.map +1 -0
- package/dist/tests/theme.test.js +149 -0
- package/dist/tests/theme.test.js.map +1 -0
- package/dist/tests/unstuck.test.js +268 -0
- package/dist/tests/unstuck.test.js.map +1 -0
- package/dist/tests/widget.test.js +424 -0
- package/dist/tests/widget.test.js.map +1 -0
- package/dist/tests/worker.test.js +306 -0
- package/dist/tests/worker.test.js.map +1 -0
- package/dist/tests/workflow.test.js +187 -0
- package/dist/tests/workflow.test.js.map +1 -0
- package/extensions/infinity-harness/index.ts +39 -12
- package/package.json +5 -2
- package/src/core/modelRouter.ts +23 -1
- package/src/daemon/index.ts +3 -3
- package/src/daemon/worker.ts +28 -5
- package/src/ui/config.ts +25 -1
- package/src/ui/wizard.ts +10 -5
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infinity-harness — the settings schema.
|
|
3
|
+
*
|
|
4
|
+
* Every option the harness exposes is declared here once, as data: where it
|
|
5
|
+
* lives, what it accepts, what it defaults to, and how to explain it. The
|
|
6
|
+
* config TUI is *generated* from this list rather than hand-written, so a new
|
|
7
|
+
* option cannot be added to the file format and forgotten in the UI — the two
|
|
8
|
+
* cannot drift because there is only one source.
|
|
9
|
+
*
|
|
10
|
+
* Editing the JSON by hand stays entirely valid. This is the same data, with
|
|
11
|
+
* enough type information attached to prompt for it safely.
|
|
12
|
+
*/
|
|
13
|
+
import { PHASE_ORDER } from "./types.js";
|
|
14
|
+
import { loadConfig, saveConfig, getKey, setKey } from "./config.js";
|
|
15
|
+
import { loadRouterConfig, saveRouterConfig } from "../modelRouter.js";
|
|
16
|
+
import { configPath, modelRouterPath } from "./paths.js";
|
|
17
|
+
import { withLockSync } from "./lock.js";
|
|
18
|
+
// ── The schema ──────────────────────────────────────────────────────────────
|
|
19
|
+
/** The two things a phase can do when its gate passes. */
|
|
20
|
+
const PHASE_MODE_CHOICES = ["autopilot", "copilot"];
|
|
21
|
+
const DIFFICULTY_HELP = "Tasks the planner marked at this difficulty run on this model. Empty means: use whatever model pi is already on.";
|
|
22
|
+
export const SETTINGS = [
|
|
23
|
+
{
|
|
24
|
+
id: "models",
|
|
25
|
+
label: "Models",
|
|
26
|
+
help: "Route work to different models by difficulty. Choices come from the models pi has configured.",
|
|
27
|
+
settings: [
|
|
28
|
+
{
|
|
29
|
+
path: "enabled",
|
|
30
|
+
file: "router",
|
|
31
|
+
label: "Routing enabled",
|
|
32
|
+
help: "Off means every task uses pi's current model, whatever is set below.",
|
|
33
|
+
type: { kind: "boolean" },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: "byDifficulty.easy",
|
|
37
|
+
file: "router",
|
|
38
|
+
label: "Easy tier",
|
|
39
|
+
help: DIFFICULTY_HELP,
|
|
40
|
+
type: { kind: "model" },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: "thinkingByDifficulty.easy",
|
|
44
|
+
file: "router",
|
|
45
|
+
label: "Easy thinking",
|
|
46
|
+
help: "Thinking level for easy tasks. Empty inherits pi's current level.",
|
|
47
|
+
type: { kind: "thinking" },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: "byDifficulty.moderate",
|
|
51
|
+
file: "router",
|
|
52
|
+
label: "Moderate tier",
|
|
53
|
+
help: DIFFICULTY_HELP,
|
|
54
|
+
type: { kind: "model" },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
path: "thinkingByDifficulty.moderate",
|
|
58
|
+
file: "router",
|
|
59
|
+
label: "Moderate thinking",
|
|
60
|
+
help: "Thinking level for moderate tasks. Empty inherits pi's current level.",
|
|
61
|
+
type: { kind: "thinking" },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: "byDifficulty.difficult",
|
|
65
|
+
file: "router",
|
|
66
|
+
label: "Difficult tier",
|
|
67
|
+
help: DIFFICULTY_HELP,
|
|
68
|
+
type: { kind: "model" },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: "thinkingByDifficulty.difficult",
|
|
72
|
+
file: "router",
|
|
73
|
+
label: "Difficult thinking",
|
|
74
|
+
help: "Thinking level for difficult tasks. Empty inherits pi's current level.",
|
|
75
|
+
type: { kind: "thinking" },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
path: "master",
|
|
79
|
+
file: "router",
|
|
80
|
+
label: "Master (consultation only)",
|
|
81
|
+
help: "Never assigned to a task directly — reached only when the ladder is exhausted and the harness asks for one opinion.",
|
|
82
|
+
type: { kind: "model" },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
path: "thinkingMaster",
|
|
86
|
+
file: "router",
|
|
87
|
+
label: "Master thinking",
|
|
88
|
+
help: "Thinking level for the master consultation model.",
|
|
89
|
+
type: { kind: "thinking" },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
path: "default",
|
|
93
|
+
file: "router",
|
|
94
|
+
label: "Default",
|
|
95
|
+
help: "Used when nothing more specific matches. Empty means pi's current model.",
|
|
96
|
+
type: { kind: "model" },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
path: "thinkingDefault",
|
|
100
|
+
file: "router",
|
|
101
|
+
label: "Default thinking",
|
|
102
|
+
help: "Fallback thinking level when no tier-specific level is set.",
|
|
103
|
+
type: { kind: "thinking" },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
path: "consultation.enabled",
|
|
107
|
+
file: "router",
|
|
108
|
+
label: "Allow consultation",
|
|
109
|
+
help: "When a task is stuck at the top of the ladder, ask the master model once.",
|
|
110
|
+
type: { kind: "boolean" },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
path: "consultation.maxPerTask",
|
|
114
|
+
file: "router",
|
|
115
|
+
label: "Consultations per task",
|
|
116
|
+
help: "How many times a single task may escalate to the master model.",
|
|
117
|
+
type: { kind: "number", min: 0, max: 10 },
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "pipeline",
|
|
123
|
+
label: "Pipeline",
|
|
124
|
+
help: "Which phases run, and how strictly roles are enforced.",
|
|
125
|
+
settings: [
|
|
126
|
+
{
|
|
127
|
+
path: "phases.enabled",
|
|
128
|
+
file: "config",
|
|
129
|
+
label: "Enabled phases",
|
|
130
|
+
help: "The pipeline, in order. SIMPLIFY is off by default; INIT is only for a fresh project.",
|
|
131
|
+
type: { kind: "multi", choices: PHASE_ORDER },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
path: "mode",
|
|
135
|
+
file: "config",
|
|
136
|
+
label: "Mode",
|
|
137
|
+
help: "copilot expects a human in the loop; autopilot is for unattended runs.",
|
|
138
|
+
type: { kind: "choice", choices: ["copilot", "autopilot"] },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
path: "roles.strict",
|
|
142
|
+
file: "config",
|
|
143
|
+
label: "Strict roles",
|
|
144
|
+
help: "Make role boundaries blocking rather than advisory. Useful with multiple agents.",
|
|
145
|
+
type: { kind: "boolean" },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
path: "paused",
|
|
149
|
+
file: "config",
|
|
150
|
+
label: "Paused",
|
|
151
|
+
help: "A paused pipeline refuses to advance and stops the continuous run.",
|
|
152
|
+
type: { kind: "boolean" },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
path: "pilot",
|
|
156
|
+
file: "config",
|
|
157
|
+
label: "Pilot",
|
|
158
|
+
help: "Run-level preset over per-phase modes. copilot every phase stops, autopilot builds without you, full is hands-off intake->SHIP.",
|
|
159
|
+
type: { kind: "choice", choices: ["copilot", "autopilot", "full"] },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
path: "tiers.A.provider",
|
|
163
|
+
file: "config",
|
|
164
|
+
label: "Tier A provider",
|
|
165
|
+
help: "Provider for general harness work (orchestration, short-lived A workers).",
|
|
166
|
+
type: { kind: "text", placeholder: "anthropic", allowEmpty: true },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
path: "tiers.A.id",
|
|
170
|
+
file: "config",
|
|
171
|
+
label: "Tier A model",
|
|
172
|
+
help: "Model id for tier A general work.",
|
|
173
|
+
type: { kind: "text", placeholder: "claude-sonnet-4-5", allowEmpty: true },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
path: "tiers.B.provider",
|
|
177
|
+
file: "config",
|
|
178
|
+
label: "Tier B provider",
|
|
179
|
+
help: "Provider for easy tasks.",
|
|
180
|
+
type: { kind: "text", placeholder: "anthropic", allowEmpty: true },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
path: "tiers.B.id",
|
|
184
|
+
file: "config",
|
|
185
|
+
label: "Tier B model",
|
|
186
|
+
help: "Model id for B (easy). Empty means use baseModel X.",
|
|
187
|
+
type: { kind: "text", placeholder: "claude-sonnet-4-5", allowEmpty: true },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
path: "tiers.C.provider",
|
|
191
|
+
file: "config",
|
|
192
|
+
label: "Tier C provider",
|
|
193
|
+
help: "Provider for moderate tasks.",
|
|
194
|
+
type: { kind: "text", placeholder: "anthropic", allowEmpty: true },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
path: "tiers.C.id",
|
|
198
|
+
file: "config",
|
|
199
|
+
label: "Tier C model",
|
|
200
|
+
help: "Model id for C (moderate).",
|
|
201
|
+
type: { kind: "text", placeholder: "claude-opus-4-5", allowEmpty: true },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
path: "tiers.D.provider",
|
|
205
|
+
file: "config",
|
|
206
|
+
label: "Tier D provider",
|
|
207
|
+
help: "Provider for difficult tasks.",
|
|
208
|
+
type: { kind: "text", placeholder: "anthropic", allowEmpty: true },
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
path: "tiers.D.id",
|
|
212
|
+
file: "config",
|
|
213
|
+
label: "Tier D model",
|
|
214
|
+
help: "Model id for D (difficult).",
|
|
215
|
+
type: { kind: "text", placeholder: "claude-opus-4-5", allowEmpty: true },
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
path: "tiers.X.provider",
|
|
219
|
+
file: "config",
|
|
220
|
+
label: "Tier X provider",
|
|
221
|
+
help: "Provider for consultation / escalation (strongest).",
|
|
222
|
+
type: { kind: "text", placeholder: "anthropic", allowEmpty: true },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
path: "tiers.X.id",
|
|
226
|
+
file: "config",
|
|
227
|
+
label: "Tier X model",
|
|
228
|
+
help: "Model id for X (consultation).",
|
|
229
|
+
type: { kind: "text", placeholder: "claude-opus-4-5", allowEmpty: true },
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
path: "limits.unitWallClockMs",
|
|
233
|
+
file: "config",
|
|
234
|
+
label: "Unit wall-clock ms",
|
|
235
|
+
help: "How long one worker may run before abort + dispose. Default 30m.",
|
|
236
|
+
type: { kind: "number", min: 60000, max: 7200000, unit: "ms" },
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
path: "limits.maxRecycles",
|
|
240
|
+
file: "config",
|
|
241
|
+
label: "Max recycles",
|
|
242
|
+
help: "How many compaction recycles one unit tolerates before stopping. Default 2.",
|
|
243
|
+
type: { kind: "number", min: 0, max: 10 },
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
path: "limits.maxReworkPerUnit",
|
|
247
|
+
file: "config",
|
|
248
|
+
label: "Max rework per unit",
|
|
249
|
+
help: "How many times a unit may go back to rework. Default 2.",
|
|
250
|
+
type: { kind: "number", min: 0, max: 10 },
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
path: "limits.maxReplansPerPhase",
|
|
254
|
+
file: "config",
|
|
255
|
+
label: "Max replans per phase",
|
|
256
|
+
help: "How many plan mutations a phase may have. Default 3.",
|
|
257
|
+
type: { kind: "number", min: 0, max: 10 },
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
path: "limits.tokenCap",
|
|
261
|
+
file: "config",
|
|
262
|
+
label: "Token cap",
|
|
263
|
+
help: "Total tokens before the run stops. Null means unconstrained.",
|
|
264
|
+
type: { kind: "number", min: 1000, max: 100000000 },
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
path: "limits.costCap",
|
|
268
|
+
file: "config",
|
|
269
|
+
label: "Cost cap USD",
|
|
270
|
+
help: "Spend ceiling across A/B/C/D/X. Null means unconstrained.",
|
|
271
|
+
type: { kind: "number", min: 0, max: 100000 },
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "workflow",
|
|
277
|
+
label: "Workflow",
|
|
278
|
+
help: "Which phases stop and wait for your signature, one phase at a time. `/infinity:workflow` picks a named one or builds a new one.",
|
|
279
|
+
settings: [
|
|
280
|
+
{
|
|
281
|
+
path: "phaseModes.research",
|
|
282
|
+
file: "config",
|
|
283
|
+
label: "RESEARCH",
|
|
284
|
+
help: "copilot stops so you can read harness/docs/RESEARCH.md before anything is specified.",
|
|
285
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
path: "phaseModes.define",
|
|
289
|
+
file: "config",
|
|
290
|
+
label: "DEFINE",
|
|
291
|
+
help: "The highest-leverage signature: a wrong definition is a weekend building the wrong thing perfectly.",
|
|
292
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
path: "phaseModes.plan",
|
|
296
|
+
file: "config",
|
|
297
|
+
label: "PLAN",
|
|
298
|
+
help: "copilot shows you the whole task list before a line of it is built.",
|
|
299
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
path: "phaseModes.build",
|
|
303
|
+
file: "config",
|
|
304
|
+
label: "BUILD",
|
|
305
|
+
help: "copilot stops once the code passes its gate, so you can read the diff.",
|
|
306
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
path: "phaseModes.verify",
|
|
310
|
+
file: "config",
|
|
311
|
+
label: "VERIFY",
|
|
312
|
+
help: "copilot asks whether the tests prove the thing works or only that it runs.",
|
|
313
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
path: "phaseModes.simplify",
|
|
317
|
+
file: "config",
|
|
318
|
+
label: "SIMPLIFY",
|
|
319
|
+
help: "copilot shows you what was deleted before it moves on.",
|
|
320
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
path: "phaseModes.review",
|
|
324
|
+
file: "config",
|
|
325
|
+
label: "REVIEW",
|
|
326
|
+
help: "copilot asks whether you would approve this if someone else had written it.",
|
|
327
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
path: "phaseModes.ship",
|
|
331
|
+
file: "config",
|
|
332
|
+
label: "SHIP",
|
|
333
|
+
help: "copilot stops before the tag goes on. The last chance to say no.",
|
|
334
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: "display",
|
|
340
|
+
label: "Display",
|
|
341
|
+
help: "What the terminal widget and the web dashboard draw. `/infinity:display` picks a template or edits this level by level.",
|
|
342
|
+
settings: [
|
|
343
|
+
{
|
|
344
|
+
path: "display.levels.goal",
|
|
345
|
+
file: "config",
|
|
346
|
+
label: "Goals",
|
|
347
|
+
help: "The outermost level. Off on a plan with one goal costs you nothing.",
|
|
348
|
+
type: { kind: "boolean" },
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
path: "display.levels.sprint",
|
|
352
|
+
file: "config",
|
|
353
|
+
label: "Sprints",
|
|
354
|
+
help: "Off hides the sprint rows; the features under them still show, one level shallower.",
|
|
355
|
+
type: { kind: "boolean" },
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
path: "display.levels.feature",
|
|
359
|
+
file: "config",
|
|
360
|
+
label: "Features",
|
|
361
|
+
help: "Off hides the feature rows; their tasks still show.",
|
|
362
|
+
type: { kind: "boolean" },
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
path: "display.levels.task",
|
|
366
|
+
file: "config",
|
|
367
|
+
label: "Tasks",
|
|
368
|
+
help: "Off leaves the shape of the run without the work — see the `overview` template.",
|
|
369
|
+
type: { kind: "boolean" },
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
path: "display.levels.subtask",
|
|
373
|
+
file: "config",
|
|
374
|
+
label: "Subtasks",
|
|
375
|
+
help: "active shows them only on the task being worked, which is what fits in a widget.",
|
|
376
|
+
type: { kind: "choice", choices: ["none", "active", "all"] },
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
path: "display.counts",
|
|
380
|
+
file: "config",
|
|
381
|
+
label: "Counts",
|
|
382
|
+
help: "The done/total figure on goals, sprints and features.",
|
|
383
|
+
type: { kind: "boolean" },
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
path: "display.dependencies",
|
|
387
|
+
file: "config",
|
|
388
|
+
label: "Dependency labels",
|
|
389
|
+
help: "The `← #3` markers that say which task a task is waiting on.",
|
|
390
|
+
type: { kind: "boolean" },
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
path: "display.criteria",
|
|
394
|
+
file: "config",
|
|
395
|
+
label: "Acceptance criteria",
|
|
396
|
+
help: "Shown under each feature on the dashboard. No room for them in a terminal widget.",
|
|
397
|
+
type: { kind: "boolean" },
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
path: "display.rail",
|
|
401
|
+
file: "config",
|
|
402
|
+
label: "Phase rail",
|
|
403
|
+
help: "The `define ─ plan ─ BUILD ─ …` strip.",
|
|
404
|
+
type: { kind: "boolean" },
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
path: "display.progress",
|
|
408
|
+
file: "config",
|
|
409
|
+
label: "Progress meter",
|
|
410
|
+
help: "The bar and the task/feature counts beside it.",
|
|
411
|
+
type: { kind: "boolean" },
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
path: "display.alerts",
|
|
415
|
+
file: "config",
|
|
416
|
+
label: "Alert strip",
|
|
417
|
+
help: "Blocked and rework counts, retries, sessions, and any phase waiting for you.",
|
|
418
|
+
type: { kind: "boolean" },
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
path: "display.taskWindow",
|
|
422
|
+
file: "config",
|
|
423
|
+
label: "Widget rows",
|
|
424
|
+
help: "How many rows of plan the terminal shows before it starts scrolling.",
|
|
425
|
+
type: { kind: "number", min: 3, max: 60 },
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: "sessions",
|
|
431
|
+
label: "Sessions",
|
|
432
|
+
help: "How the run divides itself into pi sessions. A run that never starts a fresh one carries its whole history into every request.",
|
|
433
|
+
settings: [
|
|
434
|
+
{
|
|
435
|
+
path: "session.handoff",
|
|
436
|
+
file: "config",
|
|
437
|
+
label: "Fresh session",
|
|
438
|
+
help: "goal: one session · phase: per phase (old) · sprint/feature: when plan grouping changes · task: every task (default) · subtask: every subtask. Coarser levels still fire.",
|
|
439
|
+
type: { kind: "choice", choices: ["off", "goal", "phase", "sprint", "feature", "task", "subtask"] },
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
path: "session.contextThreshold",
|
|
443
|
+
file: "config",
|
|
444
|
+
label: "Context handoff threshold",
|
|
445
|
+
help: "Hand off early once the context is this full, as a fraction of the window. 0 disables it. 0.7 keeps a long BUILD phase out of compaction.",
|
|
446
|
+
type: { kind: "number", min: 0, max: 0.95 },
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
path: "session.carryNotes",
|
|
450
|
+
file: "config",
|
|
451
|
+
label: "Carry a note across",
|
|
452
|
+
help: "Tell the replacement session, in one line, what the previous one got done.",
|
|
453
|
+
type: { kind: "boolean" },
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: "execution",
|
|
459
|
+
label: "Execution",
|
|
460
|
+
help: "Who does the work, how many at once, and at which level. The session you are typing in is a control panel; background workers do the real work.",
|
|
461
|
+
settings: [
|
|
462
|
+
{
|
|
463
|
+
path: "execution.engine",
|
|
464
|
+
file: "config",
|
|
465
|
+
label: "Where work runs",
|
|
466
|
+
help: "background: every unit of work runs in its own pi session with its own model, and this session spends no tokens on it (recommended) · main-session: the old behaviour, everything runs here on your model.",
|
|
467
|
+
type: { kind: "choice", choices: ["background", "main-session"] },
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
path: "execution.parallelAt",
|
|
471
|
+
file: "config",
|
|
472
|
+
label: "Parallel at",
|
|
473
|
+
help: "The plan-phase for parallel work. The background engine runs one worker until worktrees land, so this is only the future parallelism knob.",
|
|
474
|
+
type: { kind: "choice", choices: ["off", "goal", "phase", "sprint", "feature", "task", "subtask"] },
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
path: "execution.maxWorkers",
|
|
478
|
+
file: "config",
|
|
479
|
+
label: "Max workers",
|
|
480
|
+
help: "Hard cap on concurrent workers (1..16). Guarded by lock and budget; 1 until worktrees exist. Effective from Daemon.",
|
|
481
|
+
type: { kind: "number", min: 1, max: 16 },
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
path: "execution.isolation",
|
|
485
|
+
file: "config",
|
|
486
|
+
label: "Isolation",
|
|
487
|
+
help: "How concurrent workers stay out of each others way. none forces maxWorkers=1 and no worktree.",
|
|
488
|
+
type: { kind: "choice", choices: ["worktree", "none"] },
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: "commands",
|
|
494
|
+
label: "Project commands",
|
|
495
|
+
help: "What the gate runs to judge the work. An empty command is reported as advisory, never as a failure.",
|
|
496
|
+
settings: [
|
|
497
|
+
{
|
|
498
|
+
path: "commands.lint",
|
|
499
|
+
file: "config",
|
|
500
|
+
label: "Lint",
|
|
501
|
+
help: "e.g. npm run lint · ruff check . · golangci-lint run",
|
|
502
|
+
type: { kind: "text", placeholder: "npm run lint", allowEmpty: true },
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
path: "commands.test",
|
|
506
|
+
file: "config",
|
|
507
|
+
label: "Test",
|
|
508
|
+
help: "e.g. npm test · pytest -q · go test ./...",
|
|
509
|
+
type: { kind: "text", placeholder: "npm test", allowEmpty: true },
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
path: "commands.coverage",
|
|
513
|
+
file: "config",
|
|
514
|
+
label: "Coverage",
|
|
515
|
+
help: "Must print a percentage. The lowest figure it prints is the one used.",
|
|
516
|
+
type: { kind: "text", placeholder: "npm run coverage", allowEmpty: true },
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
path: "commands.build",
|
|
520
|
+
file: "config",
|
|
521
|
+
label: "Build",
|
|
522
|
+
help: "Optional. Recorded for context; not currently gated on.",
|
|
523
|
+
type: { kind: "text", placeholder: "npm run build", allowEmpty: true },
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: "gates",
|
|
529
|
+
label: "Gates",
|
|
530
|
+
help: "The referee. Turning gates off removes every guarantee the harness makes.",
|
|
531
|
+
settings: [
|
|
532
|
+
{
|
|
533
|
+
path: "gates.enabled",
|
|
534
|
+
file: "config",
|
|
535
|
+
label: "Gates enabled",
|
|
536
|
+
help: "Off means nothing is enforced and phases advance freely. Rarely what you want.",
|
|
537
|
+
type: { kind: "boolean" },
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
path: "gates.coverage.enabled",
|
|
541
|
+
file: "config",
|
|
542
|
+
label: "Coverage gate",
|
|
543
|
+
help: "Require the coverage command to clear the threshold below.",
|
|
544
|
+
type: { kind: "boolean" },
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
path: "gates.coverage.threshold",
|
|
548
|
+
file: "config",
|
|
549
|
+
label: "Coverage threshold",
|
|
550
|
+
help: "Percent. The gate fails below this figure.",
|
|
551
|
+
type: { kind: "number", min: 0, max: 100, unit: "%" },
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
path: "gates.antiPlaceholder.enabled",
|
|
555
|
+
file: "config",
|
|
556
|
+
label: "Reject placeholders",
|
|
557
|
+
help: "Fail the gate when source still has unfinished markers.",
|
|
558
|
+
type: { kind: "boolean" },
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
id: "loop",
|
|
564
|
+
label: "Continuous run",
|
|
565
|
+
help: "The budgets that decide when an unattended run gives up. These are what make walking away safe.",
|
|
566
|
+
settings: [
|
|
567
|
+
{
|
|
568
|
+
path: "loop.maxIterations",
|
|
569
|
+
file: "config",
|
|
570
|
+
label: "Iteration ceiling",
|
|
571
|
+
help: "Hard stop after this many loop turns, regardless of the clock.",
|
|
572
|
+
type: { kind: "number", min: 1, max: 100000 },
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
path: "loop.maxWallClockMs",
|
|
576
|
+
file: "config",
|
|
577
|
+
label: "Wall-clock budget",
|
|
578
|
+
help: "Milliseconds. 86400000 is 24 hours.",
|
|
579
|
+
type: { kind: "number", min: 60000, unit: "ms" },
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
path: "loop.noProgressLimit",
|
|
583
|
+
file: "config",
|
|
584
|
+
label: "No-progress strikes",
|
|
585
|
+
help: "Stop after this many gate failures in a row with no change to the tree or the plan.",
|
|
586
|
+
type: { kind: "number", min: 1, max: 50 },
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
id: "retries",
|
|
592
|
+
label: "Retry budgets",
|
|
593
|
+
help: "How many attempts a task, feature or phase gets before the run escalates to you. Levels are the generic ladder: goal → phase → sprint → feature → task → subtask.",
|
|
594
|
+
settings: [
|
|
595
|
+
{
|
|
596
|
+
path: "maxRetries",
|
|
597
|
+
file: "config",
|
|
598
|
+
label: "Task retries (legacy)",
|
|
599
|
+
help: "Legacy top-level task budget. Prefer retry.levels.task.maxRetries below.",
|
|
600
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
path: "retry.levels.goal.enabled",
|
|
604
|
+
file: "config",
|
|
605
|
+
label: "Goal retries",
|
|
606
|
+
help: "Bound whole goal-pass retries (outer loop). Off by default.",
|
|
607
|
+
type: { kind: "boolean" },
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
path: "retry.levels.goal.maxRetries",
|
|
611
|
+
file: "config",
|
|
612
|
+
label: "Goal max",
|
|
613
|
+
help: "Attempts per goal when enabled.",
|
|
614
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
path: "retry.levels.phase.enabled",
|
|
618
|
+
file: "config",
|
|
619
|
+
label: "Phase retries",
|
|
620
|
+
help: "Bound how many times one phase may repeat. Off by default.",
|
|
621
|
+
type: { kind: "boolean" },
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
path: "retry.levels.phase.maxRetries",
|
|
625
|
+
file: "config",
|
|
626
|
+
label: "Phase max",
|
|
627
|
+
help: "Attempts per phase when enabled.",
|
|
628
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
path: "retry.levels.sprint.enabled",
|
|
632
|
+
file: "config",
|
|
633
|
+
label: "Sprint retries",
|
|
634
|
+
help: "Sprint-level retries. Off by default, as sprints are build-exclusive.",
|
|
635
|
+
type: { kind: "boolean" },
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
path: "retry.levels.sprint.maxRetries",
|
|
639
|
+
file: "config",
|
|
640
|
+
label: "Sprint max",
|
|
641
|
+
help: "Attempts per sprint when enabled.",
|
|
642
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
path: "retry.levels.feature.enabled",
|
|
646
|
+
file: "config",
|
|
647
|
+
label: "Feature retries",
|
|
648
|
+
help: "Bound retries per feature. Off by default.",
|
|
649
|
+
type: { kind: "boolean" },
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
path: "retry.levels.feature.maxRetries",
|
|
653
|
+
file: "config",
|
|
654
|
+
label: "Feature max",
|
|
655
|
+
help: "Attempts per feature when enabled.",
|
|
656
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
path: "retry.levels.task.enabled",
|
|
660
|
+
file: "config",
|
|
661
|
+
label: "Task retries",
|
|
662
|
+
help: "The main retry budget, on by default. Subtask goes here too.",
|
|
663
|
+
type: { kind: "boolean" },
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
path: "retry.levels.task.maxRetries",
|
|
667
|
+
file: "config",
|
|
668
|
+
label: "Task max",
|
|
669
|
+
help: "Attempts per task. Leave unset to use maxRetries (legacy).",
|
|
670
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
path: "retry.levels.subtask.enabled",
|
|
674
|
+
file: "config",
|
|
675
|
+
label: "Subtask retries",
|
|
676
|
+
help: "Whether subtasks get their own retry count before escalating to task.",
|
|
677
|
+
type: { kind: "boolean" },
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
path: "retry.levels.subtask.maxRetries",
|
|
681
|
+
file: "config",
|
|
682
|
+
label: "Subtask max",
|
|
683
|
+
help: "Attempts per subtask when enabled.",
|
|
684
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
path: "retry.features.enabled",
|
|
688
|
+
file: "config",
|
|
689
|
+
label: "(legacy) Feature retry",
|
|
690
|
+
help: "Legacy per-feature switch. Mirrors retry.levels.feature.enabled.",
|
|
691
|
+
type: { kind: "boolean" },
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
path: "retry.features.maxRetries",
|
|
695
|
+
file: "config",
|
|
696
|
+
label: "(legacy) Feature max",
|
|
697
|
+
help: "Legacy feature budget value.",
|
|
698
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
path: "retry.phases.enabled",
|
|
702
|
+
file: "config",
|
|
703
|
+
label: "(legacy) Phase retry",
|
|
704
|
+
help: "Legacy per-phase switch. Mirrors retry.levels.phase.enabled.",
|
|
705
|
+
type: { kind: "boolean" },
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
path: "retry.phases.maxRetries",
|
|
709
|
+
file: "config",
|
|
710
|
+
label: "(legacy) Phase max",
|
|
711
|
+
help: "Legacy phase budget value.",
|
|
712
|
+
type: { kind: "number", min: 1, max: 100 },
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
export function findGroup(id) {
|
|
718
|
+
return SETTINGS.find((g) => g.id === id);
|
|
719
|
+
}
|
|
720
|
+
export function allSettings() {
|
|
721
|
+
return SETTINGS.flatMap((g) => g.settings);
|
|
722
|
+
}
|
|
723
|
+
export function readAll(targetDir) {
|
|
724
|
+
return {
|
|
725
|
+
config: loadConfig(targetDir).config,
|
|
726
|
+
router: loadRouterConfig(targetDir),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
export function readSetting(io, setting) {
|
|
730
|
+
const source = setting.file === "config" ? io.config : io.router;
|
|
731
|
+
return getKey(source, setting.path);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Persist one setting.
|
|
735
|
+
*
|
|
736
|
+
* Read-modify-write under the file's lock, so a config edit made from the TUI
|
|
737
|
+
* cannot clobber a concurrent write from the loop (which records gate history
|
|
738
|
+
* on the same file).
|
|
739
|
+
*/
|
|
740
|
+
export function writeSetting(targetDir, setting, value) {
|
|
741
|
+
if (setting.file === "config") {
|
|
742
|
+
withLockSync(configPath(targetDir), () => {
|
|
743
|
+
const { config } = loadConfig(targetDir);
|
|
744
|
+
setKey(config, setting.path, value);
|
|
745
|
+
const res = saveConfig(targetDir, config);
|
|
746
|
+
if (!res.ok)
|
|
747
|
+
throw new Error(res.error ?? "could not save config");
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
withLockSync(modelRouterPath(targetDir), () => {
|
|
752
|
+
const router = loadRouterConfig(targetDir);
|
|
753
|
+
setKey(router, setting.path, value);
|
|
754
|
+
saveRouterConfig(targetDir, router);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
// ── Display ─────────────────────────────────────────────────────────────────
|
|
758
|
+
/** How a value is shown in the menu. Empty model slots read as inherited. */
|
|
759
|
+
export function formatValue(setting, value) {
|
|
760
|
+
if (setting.path === "execution.isolation")
|
|
761
|
+
return typeof value === "string" && value.trim() ? value : "worktree";
|
|
762
|
+
switch (setting.type.kind) {
|
|
763
|
+
case "boolean":
|
|
764
|
+
return value ? "on" : "off";
|
|
765
|
+
case "model":
|
|
766
|
+
return typeof value === "string" && value.trim() ? value : "(pi's current model)";
|
|
767
|
+
case "thinking":
|
|
768
|
+
return typeof value === "string" && value.trim() ? value : "(inherit)";
|
|
769
|
+
case "text":
|
|
770
|
+
return typeof value === "string" && value.trim() ? value : "(not set)";
|
|
771
|
+
case "multi":
|
|
772
|
+
return Array.isArray(value) && value.length ? value.join(" → ") : "(none)";
|
|
773
|
+
case "number": {
|
|
774
|
+
if (typeof value !== "number")
|
|
775
|
+
return "(not set)";
|
|
776
|
+
if (setting.type.unit === "ms")
|
|
777
|
+
return humanizeMs(value);
|
|
778
|
+
return setting.type.unit ? `${value}${setting.type.unit}` : String(value);
|
|
779
|
+
}
|
|
780
|
+
default:
|
|
781
|
+
return value === undefined || value === null ? "(not set)" : String(value);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
export function humanizeMs(ms) {
|
|
785
|
+
if (ms >= 3_600_000) {
|
|
786
|
+
const h = ms / 3_600_000;
|
|
787
|
+
return `${Number.isInteger(h) ? h : h.toFixed(1)}h`;
|
|
788
|
+
}
|
|
789
|
+
if (ms >= 60_000)
|
|
790
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
791
|
+
return `${ms}ms`;
|
|
792
|
+
}
|
|
793
|
+
/** Parse a duration the user typed: bare ms, or `12h` / `90m` / `30s`. */
|
|
794
|
+
export function parseDuration(input) {
|
|
795
|
+
const s = input.trim().toLowerCase();
|
|
796
|
+
if (!s)
|
|
797
|
+
return null;
|
|
798
|
+
const m = s.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/);
|
|
799
|
+
if (!m)
|
|
800
|
+
return null;
|
|
801
|
+
const n = Number.parseFloat(m[1]);
|
|
802
|
+
if (!Number.isFinite(n))
|
|
803
|
+
return null;
|
|
804
|
+
switch (m[2]) {
|
|
805
|
+
case "h":
|
|
806
|
+
return Math.round(n * 3_600_000);
|
|
807
|
+
case "m":
|
|
808
|
+
return Math.round(n * 60_000);
|
|
809
|
+
case "s":
|
|
810
|
+
return Math.round(n * 1000);
|
|
811
|
+
default:
|
|
812
|
+
return Math.round(n);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
export const THINKING_CHOICES = ["(inherit)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
816
|
+
/** Coerce and bounds-check a raw answer for `setting`. */
|
|
817
|
+
export function coerce(setting, raw) {
|
|
818
|
+
const t = setting.type;
|
|
819
|
+
switch (t.kind) {
|
|
820
|
+
case "number": {
|
|
821
|
+
const n = t.unit === "ms" ? parseDuration(raw) : Number(raw.trim());
|
|
822
|
+
if (n === null || !Number.isFinite(n)) {
|
|
823
|
+
return { ok: false, error: t.unit === "ms" ? `not a duration: "${raw}" (try 24h, 90m, or a number of ms)` : `not a number: "${raw}"` };
|
|
824
|
+
}
|
|
825
|
+
if (t.min !== undefined && n < t.min)
|
|
826
|
+
return { ok: false, error: `must be at least ${t.min}` };
|
|
827
|
+
if (t.max !== undefined && n > t.max)
|
|
828
|
+
return { ok: false, error: `must be at most ${t.max}` };
|
|
829
|
+
return { ok: true, value: n };
|
|
830
|
+
}
|
|
831
|
+
case "thinking": {
|
|
832
|
+
const v = raw.trim();
|
|
833
|
+
if (!v || v === "(inherit)")
|
|
834
|
+
return { ok: true, value: "" };
|
|
835
|
+
const allowed = new Set(THINKING_CHOICES.slice(1));
|
|
836
|
+
if (!allowed.has(v))
|
|
837
|
+
return { ok: false, error: `must be one of: ${THINKING_CHOICES.join(", ")}` };
|
|
838
|
+
return { ok: true, value: v };
|
|
839
|
+
}
|
|
840
|
+
case "text":
|
|
841
|
+
case "model": {
|
|
842
|
+
const v = raw.trim();
|
|
843
|
+
if (!v && t.kind === "text" && t.allowEmpty === false)
|
|
844
|
+
return { ok: false, error: "cannot be empty" };
|
|
845
|
+
// An empty model slot is meaningful: inherit pi's current model.
|
|
846
|
+
return { ok: true, value: v };
|
|
847
|
+
}
|
|
848
|
+
case "choice": {
|
|
849
|
+
const v = raw.trim();
|
|
850
|
+
if (!t.choices.includes(v))
|
|
851
|
+
return { ok: false, error: `must be one of: ${t.choices.join(", ")}` };
|
|
852
|
+
return { ok: true, value: v };
|
|
853
|
+
}
|
|
854
|
+
case "boolean": {
|
|
855
|
+
const v = raw.trim().toLowerCase();
|
|
856
|
+
if (["on", "true", "yes", "y", "1"].includes(v))
|
|
857
|
+
return { ok: true, value: true };
|
|
858
|
+
if (["off", "false", "no", "n", "0"].includes(v))
|
|
859
|
+
return { ok: true, value: false };
|
|
860
|
+
return { ok: false, error: `expected on or off, got "${raw}"` };
|
|
861
|
+
}
|
|
862
|
+
case "multi": {
|
|
863
|
+
const parts = raw
|
|
864
|
+
.split(/[,\s]+/)
|
|
865
|
+
.map((s) => s.trim())
|
|
866
|
+
.filter(Boolean);
|
|
867
|
+
const bad = parts.filter((p) => !t.choices.includes(p));
|
|
868
|
+
if (bad.length)
|
|
869
|
+
return { ok: false, error: `not valid: ${bad.join(", ")}` };
|
|
870
|
+
// Keep canonical order regardless of what order they were typed in —
|
|
871
|
+
// the pipeline is an ordered sequence, not a set.
|
|
872
|
+
const ordered = t.choices.filter((c) => parts.includes(c));
|
|
873
|
+
return { ok: true, value: ordered };
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
//# sourceMappingURL=settings.js.map
|