infinity-harness 2.3.0 → 2.4.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.
- package/CHANGELOG.md +85 -1
- package/README.md +87 -22
- package/extensions/infinity-harness/index.ts +223 -39
- package/package.json +2 -2
- package/src/approval.ts +15 -5
- package/src/core/config.ts +44 -1
- package/src/core/init.ts +22 -1
- package/src/core/paths.ts +25 -0
- package/src/core/settings.ts +141 -12
- package/src/core/types.ts +49 -1
- package/src/intake.ts +124 -115
- package/src/remote.ts +7 -1
- package/src/ui/config.ts +6 -0
- package/src/ui/dashboard.ts +90 -27
- package/src/ui/display.ts +267 -0
- package/src/ui/planTree.ts +38 -16
- package/src/ui/widget.ts +50 -18
- package/src/ui/wizard.ts +257 -90
- package/src/workflow.ts +309 -0
package/src/core/settings.ts
CHANGED
|
@@ -49,6 +49,9 @@ export type SettingsGroup = {
|
|
|
49
49
|
|
|
50
50
|
// ── The schema ──────────────────────────────────────────────────────────────
|
|
51
51
|
|
|
52
|
+
/** The two things a phase can do when its gate passes. */
|
|
53
|
+
const PHASE_MODE_CHOICES = ["autopilot", "copilot"] as const;
|
|
54
|
+
|
|
52
55
|
const DIFFICULTY_HELP =
|
|
53
56
|
"Tasks the planner marked at this difficulty run on this model. Empty means: use whatever model pi is already on.";
|
|
54
57
|
|
|
@@ -152,31 +155,157 @@ export const SETTINGS: SettingsGroup[] = [
|
|
|
152
155
|
],
|
|
153
156
|
},
|
|
154
157
|
{
|
|
155
|
-
id: "
|
|
156
|
-
label: "
|
|
157
|
-
help: "Which phases stop and wait for your signature
|
|
158
|
+
id: "workflow",
|
|
159
|
+
label: "Workflow",
|
|
160
|
+
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.",
|
|
158
161
|
settings: [
|
|
159
162
|
{
|
|
160
|
-
path: "
|
|
163
|
+
path: "phaseModes.research",
|
|
161
164
|
file: "config",
|
|
162
|
-
label: "
|
|
163
|
-
help: "
|
|
164
|
-
type: { kind: "
|
|
165
|
+
label: "RESEARCH",
|
|
166
|
+
help: "copilot stops so you can read harness/docs/RESEARCH.md before anything is specified.",
|
|
167
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
165
168
|
},
|
|
166
169
|
{
|
|
167
|
-
path: "
|
|
170
|
+
path: "phaseModes.define",
|
|
168
171
|
file: "config",
|
|
169
|
-
label: "
|
|
172
|
+
label: "DEFINE",
|
|
170
173
|
help: "The highest-leverage signature: a wrong definition is a weekend building the wrong thing perfectly.",
|
|
174
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: "phaseModes.plan",
|
|
178
|
+
file: "config",
|
|
179
|
+
label: "PLAN",
|
|
180
|
+
help: "copilot shows you the whole task list before a line of it is built.",
|
|
181
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
path: "phaseModes.build",
|
|
185
|
+
file: "config",
|
|
186
|
+
label: "BUILD",
|
|
187
|
+
help: "copilot stops once the code passes its gate, so you can read the diff.",
|
|
188
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
path: "phaseModes.verify",
|
|
192
|
+
file: "config",
|
|
193
|
+
label: "VERIFY",
|
|
194
|
+
help: "copilot asks whether the tests prove the thing works or only that it runs.",
|
|
195
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
path: "phaseModes.simplify",
|
|
199
|
+
file: "config",
|
|
200
|
+
label: "SIMPLIFY",
|
|
201
|
+
help: "copilot shows you what was deleted before it moves on.",
|
|
202
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
path: "phaseModes.review",
|
|
206
|
+
file: "config",
|
|
207
|
+
label: "REVIEW",
|
|
208
|
+
help: "copilot asks whether you would approve this if someone else had written it.",
|
|
209
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
path: "phaseModes.ship",
|
|
213
|
+
file: "config",
|
|
214
|
+
label: "SHIP",
|
|
215
|
+
help: "copilot stops before the tag goes on. The last chance to say no.",
|
|
216
|
+
type: { kind: "choice", choices: PHASE_MODE_CHOICES },
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "display",
|
|
222
|
+
label: "Display",
|
|
223
|
+
help: "What the terminal widget and the web dashboard draw. `/infinity:display` picks a template or edits this level by level.",
|
|
224
|
+
settings: [
|
|
225
|
+
{
|
|
226
|
+
path: "display.levels.goal",
|
|
227
|
+
file: "config",
|
|
228
|
+
label: "Goals",
|
|
229
|
+
help: "The outermost level. Off on a plan with one goal costs you nothing.",
|
|
230
|
+
type: { kind: "boolean" },
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
path: "display.levels.sprint",
|
|
234
|
+
file: "config",
|
|
235
|
+
label: "Sprints",
|
|
236
|
+
help: "Off hides the sprint rows; the features under them still show, one level shallower.",
|
|
171
237
|
type: { kind: "boolean" },
|
|
172
238
|
},
|
|
173
239
|
{
|
|
174
|
-
path: "
|
|
240
|
+
path: "display.levels.feature",
|
|
175
241
|
file: "config",
|
|
176
|
-
label: "
|
|
177
|
-
help: "
|
|
242
|
+
label: "Features",
|
|
243
|
+
help: "Off hides the feature rows; their tasks still show.",
|
|
178
244
|
type: { kind: "boolean" },
|
|
179
245
|
},
|
|
246
|
+
{
|
|
247
|
+
path: "display.levels.task",
|
|
248
|
+
file: "config",
|
|
249
|
+
label: "Tasks",
|
|
250
|
+
help: "Off leaves the shape of the run without the work — see the `overview` template.",
|
|
251
|
+
type: { kind: "boolean" },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
path: "display.levels.subtask",
|
|
255
|
+
file: "config",
|
|
256
|
+
label: "Subtasks",
|
|
257
|
+
help: "active shows them only on the task being worked, which is what fits in a widget.",
|
|
258
|
+
type: { kind: "choice", choices: ["none", "active", "all"] },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
path: "display.counts",
|
|
262
|
+
file: "config",
|
|
263
|
+
label: "Counts",
|
|
264
|
+
help: "The done/total figure on goals, sprints and features.",
|
|
265
|
+
type: { kind: "boolean" },
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
path: "display.dependencies",
|
|
269
|
+
file: "config",
|
|
270
|
+
label: "Dependency labels",
|
|
271
|
+
help: "The `← #3` markers that say which task a task is waiting on.",
|
|
272
|
+
type: { kind: "boolean" },
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
path: "display.criteria",
|
|
276
|
+
file: "config",
|
|
277
|
+
label: "Acceptance criteria",
|
|
278
|
+
help: "Shown under each feature on the dashboard. No room for them in a terminal widget.",
|
|
279
|
+
type: { kind: "boolean" },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
path: "display.rail",
|
|
283
|
+
file: "config",
|
|
284
|
+
label: "Phase rail",
|
|
285
|
+
help: "The `define ─ plan ─ BUILD ─ …` strip.",
|
|
286
|
+
type: { kind: "boolean" },
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
path: "display.progress",
|
|
290
|
+
file: "config",
|
|
291
|
+
label: "Progress meter",
|
|
292
|
+
help: "The bar and the task/feature counts beside it.",
|
|
293
|
+
type: { kind: "boolean" },
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
path: "display.alerts",
|
|
297
|
+
file: "config",
|
|
298
|
+
label: "Alert strip",
|
|
299
|
+
help: "Blocked and rework counts, retries, sessions, and any phase waiting for you.",
|
|
300
|
+
type: { kind: "boolean" },
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
path: "display.taskWindow",
|
|
304
|
+
file: "config",
|
|
305
|
+
label: "Widget rows",
|
|
306
|
+
help: "How many rows of plan the terminal shows before it starts scrolling.",
|
|
307
|
+
type: { kind: "number", min: 3, max: 60 },
|
|
308
|
+
},
|
|
180
309
|
],
|
|
181
310
|
},
|
|
182
311
|
{
|
package/src/core/types.ts
CHANGED
|
@@ -176,13 +176,55 @@ export type SessionPolicy = {
|
|
|
176
176
|
carryNotes: boolean;
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Which phases stop and wait for a human signature before the run continues.
|
|
181
|
+
*
|
|
182
|
+
* Superseded by `HarnessConfig.phaseModes`, which says the same thing for
|
|
183
|
+
* *every* phase rather than only these three. Kept because configs written by
|
|
184
|
+
* 2.3 have it, and `loadConfig` migrates them on read.
|
|
185
|
+
*/
|
|
180
186
|
export type ApprovalPolicy = {
|
|
181
187
|
research: boolean;
|
|
182
188
|
define: boolean;
|
|
183
189
|
plan: boolean;
|
|
184
190
|
};
|
|
185
191
|
|
|
192
|
+
/** What happens when a phase's gate passes: stop for the human, or advance. */
|
|
193
|
+
export type PhaseMode = "copilot" | "autopilot";
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Which parts of the plan a surface draws.
|
|
197
|
+
*
|
|
198
|
+
* Two people watching the same run want different things on screen: one works
|
|
199
|
+
* in sprints and never opens a subtask, the next has no sprints at all and
|
|
200
|
+
* lives in the subtask list. Rather than pick a winner, the levels are a
|
|
201
|
+
* setting, and the widget and the dashboard read the same one.
|
|
202
|
+
*/
|
|
203
|
+
export type DisplayPolicy = {
|
|
204
|
+
/** Name of the template this came from, or "custom" once it is edited. */
|
|
205
|
+
preset: string;
|
|
206
|
+
levels: {
|
|
207
|
+
goal: boolean;
|
|
208
|
+
sprint: boolean;
|
|
209
|
+
feature: boolean;
|
|
210
|
+
task: boolean;
|
|
211
|
+
/** "active" shows them only on the task being worked. */
|
|
212
|
+
subtask: "none" | "active" | "all";
|
|
213
|
+
};
|
|
214
|
+
/** `2/5` counts on the grouping rows. */
|
|
215
|
+
counts: boolean;
|
|
216
|
+
/** `← #3` dependency labels on tasks. */
|
|
217
|
+
dependencies: boolean;
|
|
218
|
+
/** The phase rail, the progress meter and the alert strip. */
|
|
219
|
+
rail: boolean;
|
|
220
|
+
progress: boolean;
|
|
221
|
+
alerts: boolean;
|
|
222
|
+
/** Acceptance criteria under each feature. Dashboard only — no room in a widget. */
|
|
223
|
+
criteria: boolean;
|
|
224
|
+
/** Rows of plan in the terminal widget before it starts scrolling. */
|
|
225
|
+
taskWindow: number;
|
|
226
|
+
};
|
|
227
|
+
|
|
186
228
|
/** What the start-up wizard settled, so it is never asked twice. */
|
|
187
229
|
export type IntakeState = {
|
|
188
230
|
/** True once the wizard has run to completion for this project. */
|
|
@@ -227,7 +269,13 @@ export type HarnessConfig = {
|
|
|
227
269
|
phases: { enabled: Phase[] };
|
|
228
270
|
roles: { strict: boolean };
|
|
229
271
|
session: SessionPolicy;
|
|
272
|
+
/** Legacy: the three-phase approval switch 2.3 shipped. Migrated to `phaseModes`. */
|
|
230
273
|
approvals: ApprovalPolicy;
|
|
274
|
+
/** Mode per phase — the setting `approvals` became. */
|
|
275
|
+
phaseModes: Partial<Record<Phase, PhaseMode>>;
|
|
276
|
+
/** Which named workflow the modes above came from, before any hand-editing. */
|
|
277
|
+
workflow: { id: string; name: string } | null;
|
|
278
|
+
display: DisplayPolicy;
|
|
231
279
|
intake: IntakeState;
|
|
232
280
|
/** Set when a gate passed but the phase needs a human signature first. */
|
|
233
281
|
awaitingApproval: Phase | null;
|
package/src/intake.ts
CHANGED
|
@@ -7,100 +7,98 @@
|
|
|
7
7
|
* "you decide everything, including what I want", which is not a mode anybody
|
|
8
8
|
* asked for.
|
|
9
9
|
*
|
|
10
|
-
* The fix
|
|
10
|
+
* The fix was to separate two questions that were tangled together:
|
|
11
11
|
*
|
|
12
|
-
* 1. What are we building? — always asked
|
|
13
|
-
* 2. Who signs off on what? — the
|
|
12
|
+
* 1. What are we building? — always asked
|
|
13
|
+
* 2. Who signs off on what? — the workflow
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* walk away" mode; keeping DEFINE is the common middle.
|
|
22
|
-
*
|
|
23
|
-
* RESEARCH is a separate, optional phase that runs before DEFINE: the human
|
|
24
|
-
* gives an idea, the model goes and finds out what it actually has to be.
|
|
15
|
+
* And the second of those turned out to be tangled too. "copilot" and
|
|
16
|
+
* "autopilot" are one switch, and one switch cannot say "let it define and
|
|
17
|
+
* plan on its own but show me the review". So the answer is a **workflow**: a
|
|
18
|
+
* mode per phase, with the two familiar words as two named points in that
|
|
19
|
+
* space rather than the only two points in it. `src/workflow.ts` owns what a
|
|
20
|
+
* workflow is and where saved ones live.
|
|
25
21
|
*
|
|
26
22
|
* Nothing here talks to pi. It takes answers and returns a plan of record, so
|
|
27
23
|
* the flow can be unit-tested without a terminal and driven from the adapter,
|
|
28
24
|
* from a test, or from a config file.
|
|
29
25
|
*/
|
|
30
26
|
|
|
31
|
-
import type { ApprovalPolicy, Phase, SessionPolicy } from "./core/types.ts";
|
|
27
|
+
import type { ApprovalPolicy, DisplayPolicy, Phase, SessionPolicy } from "./core/types.ts";
|
|
32
28
|
import { DEFAULT_ENABLED_PHASES, PHASE_ORDER } from "./core/types.ts";
|
|
29
|
+
import {
|
|
30
|
+
BUILTIN_WORKFLOWS,
|
|
31
|
+
describeModes,
|
|
32
|
+
normalizeModes,
|
|
33
|
+
normalizePhases,
|
|
34
|
+
signedPhases as signedIn,
|
|
35
|
+
type PhaseMode,
|
|
36
|
+
type PhaseModes,
|
|
37
|
+
type Workflow,
|
|
38
|
+
} from "./workflow.ts";
|
|
39
|
+
import { defaultDisplay, normalizeDisplay } from "./ui/display.ts";
|
|
33
40
|
|
|
34
41
|
export type Mode = "copilot" | "autopilot";
|
|
35
42
|
|
|
36
43
|
/** The wizard's questions, in the order they are asked. */
|
|
37
|
-
export const INTAKE_STEPS = ["
|
|
44
|
+
export const INTAKE_STEPS = ["workflow", "brief", "handoff", "display"] as const;
|
|
38
45
|
export type IntakeStep = (typeof INTAKE_STEPS)[number];
|
|
39
46
|
|
|
40
47
|
export type IntakeAnswers = {
|
|
41
|
-
|
|
48
|
+
/** The chosen workflow: a built-in, one they saved, or one they just built. */
|
|
49
|
+
workflow: Workflow;
|
|
42
50
|
/** What the human wants built, in their words. Empty is allowed but warned about. */
|
|
43
51
|
brief: string;
|
|
44
|
-
/** Run the optional RESEARCH phase before DEFINE. */
|
|
45
|
-
research: boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Which phases the human wants to sign, for autopilot only.
|
|
48
|
-
* Ignored in copilot, where all three are always signed.
|
|
49
|
-
*/
|
|
50
|
-
approvals?: Partial<ApprovalPolicy>;
|
|
51
52
|
/** Session handoff policy. Defaults to a fresh session per phase. */
|
|
52
53
|
handoff?: SessionPolicy["handoff"];
|
|
53
|
-
/**
|
|
54
|
-
|
|
54
|
+
/** What the surfaces should draw. Defaults to the `focus` template. */
|
|
55
|
+
display?: DisplayPolicy;
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
export type IntakePlan = {
|
|
59
|
+
/** Derived: "copilot" when the run stops for the human anywhere, else "autopilot". */
|
|
58
60
|
mode: Mode;
|
|
61
|
+
workflow: { id: string; name: string };
|
|
59
62
|
brief: string | null;
|
|
60
63
|
phases: Phase[];
|
|
64
|
+
phaseModes: PhaseModes;
|
|
65
|
+
/** Kept in step with `phaseModes` so a 2.3 config read by a 2.3 tool still works. */
|
|
61
66
|
approvals: ApprovalPolicy;
|
|
62
67
|
session: SessionPolicy;
|
|
68
|
+
display: DisplayPolicy;
|
|
63
69
|
/** What the human should be told about what they just chose. */
|
|
64
70
|
summary: string;
|
|
65
71
|
/** Things that will bite later if left as they are. */
|
|
66
72
|
warnings: string[];
|
|
67
73
|
};
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* phase that is enabled is theirs. Making this configurable would make
|
|
72
|
-
* "copilot" mean nothing.
|
|
73
|
-
*/
|
|
74
|
-
export function copilotApprovals(phases: Phase[]): ApprovalPolicy {
|
|
75
|
-
return {
|
|
76
|
-
research: phases.includes("research"),
|
|
77
|
-
define: true,
|
|
78
|
-
plan: true,
|
|
79
|
-
};
|
|
75
|
+
export function builtInByName(name: string): Workflow | null {
|
|
76
|
+
return BUILTIN_WORKFLOWS.find((w) => w.id === name || w.name === name) ?? null;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
/** Build an ad-hoc workflow from a phase list and a mode per phase. */
|
|
80
|
+
export function customWorkflow(
|
|
81
|
+
phases: Phase[],
|
|
82
|
+
modes: PhaseModes,
|
|
83
|
+
name = "custom",
|
|
84
|
+
): Workflow {
|
|
85
|
+
const ordered = normalizePhases(phases);
|
|
86
|
+
const normalized = normalizeModes(modes, ordered);
|
|
87
|
+
return {
|
|
88
|
+
id: "custom",
|
|
89
|
+
name,
|
|
90
|
+
description: describeModes(ordered, normalized),
|
|
91
|
+
builtIn: false,
|
|
92
|
+
phases: ordered,
|
|
93
|
+
modes: normalized,
|
|
94
|
+
};
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
/** Turn the wizard's answers into everything `initHarness` needs. */
|
|
92
98
|
export function planIntake(answers: IntakeAnswers): IntakePlan {
|
|
93
|
-
const
|
|
94
|
-
const phases = normalizePhases(
|
|
95
|
-
|
|
96
|
-
const approvals: ApprovalPolicy =
|
|
97
|
-
answers.mode === "copilot"
|
|
98
|
-
? copilotApprovals(phases)
|
|
99
|
-
: {
|
|
100
|
-
research: phases.includes("research") && answers.approvals?.research === true,
|
|
101
|
-
define: answers.approvals?.define === true,
|
|
102
|
-
plan: answers.approvals?.plan === true,
|
|
103
|
-
};
|
|
99
|
+
const workflow = answers.workflow;
|
|
100
|
+
const phases = normalizePhases(workflow.phases);
|
|
101
|
+
const phaseModes = normalizeModes(workflow.modes, phases);
|
|
104
102
|
|
|
105
103
|
const handoff = answers.handoff ?? "phase";
|
|
106
104
|
const session: SessionPolicy = {
|
|
@@ -109,7 +107,10 @@ export function planIntake(answers: IntakeAnswers): IntakePlan {
|
|
|
109
107
|
carryNotes: true,
|
|
110
108
|
};
|
|
111
109
|
|
|
110
|
+
const display = normalizeDisplay(answers.display ?? defaultDisplay());
|
|
112
111
|
const brief = answers.brief?.trim() ? answers.brief.trim() : null;
|
|
112
|
+
const signed = PHASE_ORDER.filter((p) => phaseModes[p] === "copilot");
|
|
113
|
+
const mode: Mode = signed.length > 0 ? "copilot" : "autopilot";
|
|
113
114
|
|
|
114
115
|
const warnings: string[] = [];
|
|
115
116
|
if (!brief) {
|
|
@@ -118,13 +119,19 @@ export function planIntake(answers: IntakeAnswers): IntakePlan {
|
|
|
118
119
|
"It will not guess a project.",
|
|
119
120
|
);
|
|
120
121
|
}
|
|
121
|
-
if (
|
|
122
|
+
if (signed.length === 0) {
|
|
122
123
|
warnings.push(
|
|
123
124
|
"Nothing is being approved by you. The model decides what to build and how, " +
|
|
124
125
|
"and you see it when it is done. This is the right setting for a run you " +
|
|
125
126
|
"want to walk away from — and the wrong one if the goal is vague.",
|
|
126
127
|
);
|
|
127
128
|
}
|
|
129
|
+
if (signed.length === phases.length) {
|
|
130
|
+
warnings.push(
|
|
131
|
+
"Every phase stops for you. Nothing moves while you are away, which is the " +
|
|
132
|
+
"point — just do not expect to start this and go to bed.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
128
135
|
if (handoff === "off") {
|
|
129
136
|
warnings.push(
|
|
130
137
|
"Session handoff is off, so the whole run shares one context window. " +
|
|
@@ -132,20 +139,36 @@ export function planIntake(answers: IntakeAnswers): IntakePlan {
|
|
|
132
139
|
);
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
mode,
|
|
144
|
+
workflow: { id: workflow.id, name: workflow.name },
|
|
145
|
+
brief,
|
|
146
|
+
phases,
|
|
147
|
+
phaseModes,
|
|
148
|
+
approvals: {
|
|
149
|
+
research: phaseModes.research === "copilot",
|
|
150
|
+
define: phaseModes.define === "copilot",
|
|
151
|
+
plan: phaseModes.plan === "copilot",
|
|
152
|
+
},
|
|
153
|
+
session,
|
|
154
|
+
display,
|
|
155
|
+
summary: summarize(workflow, phases, phaseModes, session, display, brief),
|
|
156
|
+
warnings,
|
|
157
|
+
};
|
|
136
158
|
}
|
|
137
159
|
|
|
138
160
|
function summarize(
|
|
139
|
-
|
|
161
|
+
workflow: Workflow,
|
|
140
162
|
phases: Phase[],
|
|
141
|
-
|
|
163
|
+
modes: PhaseModes,
|
|
142
164
|
session: SessionPolicy,
|
|
165
|
+
display: DisplayPolicy,
|
|
143
166
|
brief: string | null,
|
|
144
167
|
): string {
|
|
145
|
-
const signed =
|
|
168
|
+
const signed = phases.filter((p) => modes[p] === "copilot");
|
|
146
169
|
const L: string[] = [];
|
|
147
|
-
L.push(`
|
|
148
|
-
L.push(`Pipeline ${phases.join(" → ")}`);
|
|
170
|
+
L.push(`Workflow ${workflow.name}`);
|
|
171
|
+
L.push(`Pipeline ${phases.map((p) => (modes[p] === "copilot" ? `[${p}]` : p)).join(" → ")}`);
|
|
149
172
|
L.push(
|
|
150
173
|
`You sign ${signed.length ? signed.map((s) => s.toUpperCase()).join(", ") : "nothing — the model decides and runs"}`,
|
|
151
174
|
);
|
|
@@ -158,6 +181,7 @@ function summarize(
|
|
|
158
181
|
: "fresh session per phase"
|
|
159
182
|
}`,
|
|
160
183
|
);
|
|
184
|
+
L.push(`Display ${display.preset}`);
|
|
161
185
|
L.push(`Goal ${brief ?? "(none yet — you will be asked first thing)"}`);
|
|
162
186
|
return L.join("\n");
|
|
163
187
|
}
|
|
@@ -177,21 +201,9 @@ export type Question = {
|
|
|
177
201
|
placeholder?: string;
|
|
178
202
|
};
|
|
179
203
|
|
|
180
|
-
export const
|
|
181
|
-
id: "
|
|
182
|
-
title: "How
|
|
183
|
-
options: [
|
|
184
|
-
{
|
|
185
|
-
value: "copilot",
|
|
186
|
-
label: "copilot — I approve the definition and the plan",
|
|
187
|
-
help: "The run stops and shows you its work before it starts building. You can send any phase back with a note.",
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
value: "autopilot",
|
|
191
|
-
label: "autopilot — I choose what to approve, if anything",
|
|
192
|
-
help: "You pick which of research, definition and plan you sign. Approve none of them and the run is yours to walk away from.",
|
|
193
|
-
},
|
|
194
|
-
],
|
|
204
|
+
export const WORKFLOW_QUESTION: Question = {
|
|
205
|
+
id: "workflow",
|
|
206
|
+
title: "How should this run — which phases, and which of them stop for you?",
|
|
195
207
|
};
|
|
196
208
|
|
|
197
209
|
export const BRIEF_QUESTION: Question = {
|
|
@@ -200,23 +212,6 @@ export const BRIEF_QUESTION: Question = {
|
|
|
200
212
|
placeholder: "e.g. a CLI that reconciles Stripe payouts against our ledger",
|
|
201
213
|
};
|
|
202
214
|
|
|
203
|
-
export const RESEARCH_QUESTION: Question = {
|
|
204
|
-
id: "research",
|
|
205
|
-
title: "Research the idea first?",
|
|
206
|
-
options: [
|
|
207
|
-
{
|
|
208
|
-
value: "no",
|
|
209
|
-
label: "no — go straight to defining it",
|
|
210
|
-
help: "Right when you already know what has to be built.",
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
value: "yes",
|
|
214
|
-
label: "yes — find out what it has to be first",
|
|
215
|
-
help: "Adds a RESEARCH phase before DEFINE: prior art, constraints, options with costs, a recommendation, and the questions only you can answer.",
|
|
216
|
-
},
|
|
217
|
-
],
|
|
218
|
-
};
|
|
219
|
-
|
|
220
215
|
export const HANDOFF_QUESTION: Question = {
|
|
221
216
|
id: "handoff",
|
|
222
217
|
title: "When should the run start a fresh session?",
|
|
@@ -239,25 +234,39 @@ export const HANDOFF_QUESTION: Question = {
|
|
|
239
234
|
],
|
|
240
235
|
};
|
|
241
236
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
237
|
+
export const DISPLAY_QUESTION: Question = {
|
|
238
|
+
id: "display",
|
|
239
|
+
title: "How much of the plan do you want on screen?",
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** The two modes, as a question about one phase. */
|
|
243
|
+
export const PHASE_MODE_OPTIONS: { value: PhaseMode; label: string; help: string }[] = [
|
|
244
|
+
{
|
|
245
|
+
value: "autopilot",
|
|
246
|
+
label: "autopilot — it passes the gate and moves on",
|
|
247
|
+
help: "The deterministic gate is the only referee for this phase.",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
value: "copilot",
|
|
251
|
+
label: "copilot — it stops and waits for you",
|
|
252
|
+
help: "When the gate passes, the run parks and asks you to sign it off before advancing.",
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
/** What each phase is for, one line, shown while picking modes. */
|
|
257
|
+
export const PHASE_PURPOSE: Record<Phase, string> = {
|
|
258
|
+
init: "set the project up",
|
|
259
|
+
research: "find out what it actually has to be",
|
|
260
|
+
define: "write down what is being built, and the criteria",
|
|
261
|
+
plan: "break it into ordered, dependency-aware tasks",
|
|
262
|
+
build: "implement it, one task at a time",
|
|
263
|
+
verify: "prove it behaves; hunt what the tests miss",
|
|
264
|
+
simplify: "delete more than you add",
|
|
265
|
+
review: "judge it as if someone else wrote it",
|
|
266
|
+
ship: "tag, changelog, leave the tree clean",
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** Phases someone can put in a pipeline. INIT is not one of them. */
|
|
270
|
+
export const SELECTABLE_PHASES: Phase[] = PHASE_ORDER.filter((p) => p !== "init");
|
|
271
|
+
|
|
272
|
+
export { DEFAULT_ENABLED_PHASES, signedIn as signedPhases };
|
package/src/remote.ts
CHANGED
|
@@ -18,12 +18,13 @@ import { createServer, type Server } from "node:http";
|
|
|
18
18
|
import type { AddressInfo } from "node:net";
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
20
|
|
|
21
|
-
import type { FeatureList, Feature, Goal, GateResult, Phase, Sprint } from "./core/types.ts";
|
|
21
|
+
import type { DisplayPolicy, FeatureList, Feature, Goal, GateResult, Phase, Sprint } from "./core/types.ts";
|
|
22
22
|
import { loadFeatureList, computeProgress } from "./core/featureList.ts";
|
|
23
23
|
import { loadConfig } from "./core/config.ts";
|
|
24
24
|
import { modelRouterPath, reworkPath } from "./core/paths.ts";
|
|
25
25
|
import { readJsonSafe } from "./core/fsx.ts";
|
|
26
26
|
import { loadRunState } from "./runState.ts";
|
|
27
|
+
import { normalizeDisplay } from "./ui/display.ts";
|
|
27
28
|
import { renderDashboard, escapeHtml, type DashboardState } from "./ui/dashboard.ts";
|
|
28
29
|
|
|
29
30
|
export { escapeHtml };
|
|
@@ -54,6 +55,8 @@ export interface RemoteState {
|
|
|
54
55
|
/** pi sessions this run has spent — proof the handoff is doing its job. */
|
|
55
56
|
sessions: number | null;
|
|
56
57
|
goalPass: { current: number; max: number } | null;
|
|
58
|
+
/** What the reader has asked the dashboard to draw. */
|
|
59
|
+
display: DisplayPolicy;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export interface RemoteServer {
|
|
@@ -118,6 +121,7 @@ export function buildRemoteState(projectDir?: string): RemoteState {
|
|
|
118
121
|
? { current: config.goalPass, max: config.goalMaxPasses }
|
|
119
122
|
: null,
|
|
120
123
|
sprints: list.sprints ?? [],
|
|
124
|
+
display: normalizeDisplay(config.display),
|
|
121
125
|
};
|
|
122
126
|
}
|
|
123
127
|
|
|
@@ -136,6 +140,7 @@ function toDashboardState(s: RemoteState): DashboardState {
|
|
|
136
140
|
awaitingApproval: s.awaitingApproval,
|
|
137
141
|
sessions: s.sessions,
|
|
138
142
|
goalPass: s.goalPass,
|
|
143
|
+
display: s.display,
|
|
139
144
|
};
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -159,6 +164,7 @@ export function buildApiPayload(state: RemoteState): Record<string, unknown> {
|
|
|
159
164
|
sessions: state.sessions,
|
|
160
165
|
goalPass: state.goalPass,
|
|
161
166
|
sprints: state.sprints,
|
|
167
|
+
display: state.display,
|
|
162
168
|
features: state.features.map((f) => ({
|
|
163
169
|
id: f.id,
|
|
164
170
|
name: f.name,
|
package/src/ui/config.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Setting, SettingsGroup } from "../core/settings.ts";
|
|
18
|
+
import { summarizeWorkflow } from "../workflow.ts";
|
|
19
|
+
import { normalizeDisplay, summarizeDisplay } from "./display.ts";
|
|
18
20
|
import {
|
|
19
21
|
SETTINGS,
|
|
20
22
|
coerce,
|
|
@@ -92,6 +94,10 @@ function summarize(group: SettingsGroup, io: ReturnType<typeof readAll>): string
|
|
|
92
94
|
}
|
|
93
95
|
case "pipeline":
|
|
94
96
|
return (io.config.phases?.enabled ?? []).join(" → ") || "(none)";
|
|
97
|
+
case "workflow":
|
|
98
|
+
return summarizeWorkflow(io.config);
|
|
99
|
+
case "display":
|
|
100
|
+
return summarizeDisplay(normalizeDisplay(io.config.display));
|
|
95
101
|
case "commands": {
|
|
96
102
|
const set = Object.entries(io.config.commands ?? {}).filter(([, v]) => Boolean(v));
|
|
97
103
|
return set.length ? set.map(([k]) => k).join(", ") : "none set";
|