infinity-harness 2.3.1 → 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 +64 -0
- package/README.md +87 -22
- package/extensions/infinity-harness/index.ts +195 -39
- package/package.json +1 -1
- 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/ui/wizard.ts
CHANGED
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
* suite, by a script answering over pi's RPC extension-UI protocol, which is
|
|
8
8
|
* as close to watching a human use it as this gets.
|
|
9
9
|
*
|
|
10
|
-
* The flow is short on purpose.
|
|
10
|
+
* The flow is short on purpose. Four questions, three of them one keypress:
|
|
11
11
|
*
|
|
12
|
-
* 1.
|
|
12
|
+
* 1. which workflow? — a built-in, one you saved, or "build one"
|
|
13
13
|
* 2. what are you building?
|
|
14
|
-
* 3.
|
|
15
|
-
* 4.
|
|
16
|
-
*
|
|
14
|
+
* 3. when should it start a fresh session?
|
|
15
|
+
* 4. how much of the plan do you want on screen?
|
|
16
|
+
*
|
|
17
|
+
* Building a workflow is its own small flow: pick the phases, then say for
|
|
18
|
+
* each one whether it stops for you, then optionally give it a name and keep
|
|
19
|
+
* it. A kept workflow is offered first thing on the next project.
|
|
17
20
|
*
|
|
18
21
|
* Cancelling any question cancels the wizard. Nothing is written until the
|
|
19
22
|
* human has seen the summary and said yes, because a wizard that half-commits
|
|
@@ -21,27 +24,41 @@
|
|
|
21
24
|
*/
|
|
22
25
|
|
|
23
26
|
import type { Prompter } from "./config.ts";
|
|
24
|
-
import type {
|
|
27
|
+
import type { DisplayPolicy, Phase } from "../core/types.ts";
|
|
25
28
|
import {
|
|
26
29
|
BRIEF_QUESTION,
|
|
30
|
+
DISPLAY_QUESTION,
|
|
27
31
|
HANDOFF_QUESTION,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
PHASE_MODE_OPTIONS,
|
|
33
|
+
PHASE_PURPOSE,
|
|
34
|
+
SELECTABLE_PHASES,
|
|
35
|
+
WORKFLOW_QUESTION,
|
|
36
|
+
customWorkflow,
|
|
31
37
|
planIntake,
|
|
32
38
|
type IntakeAnswers,
|
|
33
39
|
type IntakePlan,
|
|
34
|
-
type Mode,
|
|
35
40
|
} from "../intake.ts";
|
|
41
|
+
import {
|
|
42
|
+
listWorkflows,
|
|
43
|
+
normalizeModes,
|
|
44
|
+
normalizePhases,
|
|
45
|
+
renderWorkflow,
|
|
46
|
+
saveWorkflow,
|
|
47
|
+
type PhaseMode,
|
|
48
|
+
type PhaseModes,
|
|
49
|
+
type Workflow,
|
|
50
|
+
} from "../workflow.ts";
|
|
51
|
+
import { DEFAULT_ENABLED_PHASES } from "../core/types.ts";
|
|
52
|
+
import { defaultDisplay, listDisplays, normalizeDisplay, saveDisplay } from "./display.ts";
|
|
36
53
|
|
|
37
54
|
export type WizardOptions = {
|
|
38
55
|
prompt: Prompter;
|
|
39
|
-
/**
|
|
40
|
-
phases?: Phase[];
|
|
41
|
-
/** Pre-fill the goal, e.g. from `/infinity:start <goal>`. */
|
|
56
|
+
/** Pre-fill the goal, e.g. from `/infinity:init <goal>`. */
|
|
42
57
|
brief?: string | null;
|
|
43
58
|
/** Skip the final confirmation. Used when the caller does its own. */
|
|
44
59
|
skipConfirm?: boolean;
|
|
60
|
+
/** Where saved workflows and templates live. Tests point this elsewhere. */
|
|
61
|
+
env?: NodeJS.ProcessEnv;
|
|
45
62
|
};
|
|
46
63
|
|
|
47
64
|
export type WizardResult =
|
|
@@ -51,6 +68,8 @@ export type WizardResult =
|
|
|
51
68
|
const CONFIRM = "start with these settings";
|
|
52
69
|
const RESTART = "change something";
|
|
53
70
|
const CANCEL = "cancel";
|
|
71
|
+
const BUILD_ONE = "build one — I choose the phases and which of them stop for me";
|
|
72
|
+
const DONE = "✓ done";
|
|
54
73
|
|
|
55
74
|
/** Render a choice as one selectable line: the label, then why you would pick it. */
|
|
56
75
|
function line(label: string, help: string): string {
|
|
@@ -58,44 +77,22 @@ function line(label: string, help: string): string {
|
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
export async function runIntakeWizard(options: WizardOptions): Promise<WizardResult> {
|
|
61
|
-
const { prompt } = options;
|
|
80
|
+
const { prompt, env } = options;
|
|
62
81
|
|
|
63
82
|
for (;;) {
|
|
64
|
-
// -- 1.
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const modePick = await prompt.select(MODE_QUESTION.title, modeLabels);
|
|
68
|
-
if (modePick === undefined) return { cancelled: true };
|
|
69
|
-
const mode = (modeOptions[modeLabels.indexOf(modePick)]?.value ?? "copilot") as Mode;
|
|
83
|
+
// -- 1. the workflow ----------------------------------------------------
|
|
84
|
+
const workflow = await pickWorkflow(prompt, env);
|
|
85
|
+
if (workflow === undefined) return { cancelled: true };
|
|
70
86
|
|
|
71
87
|
// -- 2. what are we building -------------------------------------------
|
|
72
88
|
//
|
|
73
|
-
// Asked
|
|
74
|
-
// skipping it is why autopilot used to invent a project and
|
|
75
|
-
// building it.
|
|
89
|
+
// Asked whatever the workflow is. This is the question the old flow
|
|
90
|
+
// skipped, and skipping it is why autopilot used to invent a project and
|
|
91
|
+
// start building it.
|
|
76
92
|
const brief =
|
|
77
93
|
(await prompt.input(BRIEF_QUESTION.title, BRIEF_QUESTION.placeholder)) ?? options.brief ?? "";
|
|
78
94
|
|
|
79
|
-
// -- 3.
|
|
80
|
-
const researchOptions = RESEARCH_QUESTION.options ?? [];
|
|
81
|
-
const researchLabels = researchOptions.map((o) => line(o.label, o.help));
|
|
82
|
-
const researchPick = await prompt.select(RESEARCH_QUESTION.title, researchLabels);
|
|
83
|
-
if (researchPick === undefined) return { cancelled: true };
|
|
84
|
-
const research = researchOptions[researchLabels.indexOf(researchPick)]?.value === "yes";
|
|
85
|
-
|
|
86
|
-
// -- 4. approvals -------------------------------------------------------
|
|
87
|
-
//
|
|
88
|
-
// Only autopilot gets a choice. In copilot the human is in the loop by
|
|
89
|
-
// definition, and offering them a way out of it would make the word mean
|
|
90
|
-
// nothing.
|
|
91
|
-
let approvals: Partial<ApprovalPolicy> | undefined;
|
|
92
|
-
if (mode === "autopilot") {
|
|
93
|
-
const picked = await pickApprovals(prompt, research);
|
|
94
|
-
if (picked === undefined) return { cancelled: true };
|
|
95
|
-
approvals = picked;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// -- 5. sessions --------------------------------------------------------
|
|
95
|
+
// -- 3. sessions --------------------------------------------------------
|
|
99
96
|
const handoffOptions = HANDOFF_QUESTION.options ?? [];
|
|
100
97
|
const handoffLabels = handoffOptions.map((o) => line(o.label, o.help));
|
|
101
98
|
const handoffPick = await prompt.select(HANDOFF_QUESTION.title, handoffLabels);
|
|
@@ -105,14 +102,11 @@ export async function runIntakeWizard(options: WizardOptions): Promise<WizardRes
|
|
|
105
102
|
| "phase"
|
|
106
103
|
| "task";
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
handoff,
|
|
114
|
-
phases: options.phases,
|
|
115
|
-
};
|
|
105
|
+
// -- 4. display ---------------------------------------------------------
|
|
106
|
+
const display = await pickDisplay(prompt, env);
|
|
107
|
+
if (display === undefined) return { cancelled: true };
|
|
108
|
+
|
|
109
|
+
const answers: IntakeAnswers = { workflow, brief, handoff, display };
|
|
116
110
|
const plan = planIntake(answers);
|
|
117
111
|
|
|
118
112
|
if (options.skipConfirm) return { cancelled: false, plan, answers };
|
|
@@ -129,62 +123,235 @@ export async function runIntakeWizard(options: WizardOptions): Promise<WizardRes
|
|
|
129
123
|
}
|
|
130
124
|
}
|
|
131
125
|
|
|
132
|
-
|
|
126
|
+
// ── choosing a workflow ─────────────────────────────────────────────────────
|
|
133
127
|
|
|
134
128
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
129
|
+
* Pick a workflow, or build one.
|
|
130
|
+
*
|
|
131
|
+
* Built-ins first, then whatever this person has saved, then "build one".
|
|
132
|
+
* Undefined means they cancelled.
|
|
137
133
|
*/
|
|
138
|
-
async function
|
|
134
|
+
export async function pickWorkflow(
|
|
139
135
|
prompt: Prompter,
|
|
140
|
-
|
|
141
|
-
): Promise<
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
136
|
+
env?: NodeJS.ProcessEnv,
|
|
137
|
+
): Promise<Workflow | undefined> {
|
|
138
|
+
const available = listWorkflows(env);
|
|
139
|
+
const rows = available.map((w) => line(w.builtIn ? w.name : `${w.name} (yours)`, w.description));
|
|
140
|
+
const picked = await prompt.select(WORKFLOW_QUESTION.title, [...rows, BUILD_ONE]);
|
|
141
|
+
if (picked === undefined) return undefined;
|
|
142
|
+
if (picked === BUILD_ONE) return buildWorkflow(prompt, env);
|
|
143
|
+
return available[rows.indexOf(picked)];
|
|
144
|
+
}
|
|
146
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The custom flow: phases, then a mode for each, then keep it or don't.
|
|
148
|
+
*
|
|
149
|
+
* Saving is offered rather than required. Someone trying a one-off shape for
|
|
150
|
+
* one project should not have to name it, and someone who has found the shape
|
|
151
|
+
* they always want should not have to rebuild it every time.
|
|
152
|
+
*/
|
|
153
|
+
export async function buildWorkflow(
|
|
154
|
+
prompt: Prompter,
|
|
155
|
+
env?: NodeJS.ProcessEnv,
|
|
156
|
+
): Promise<Workflow | undefined> {
|
|
157
|
+
// -- which phases run ---------------------------------------------------
|
|
158
|
+
const chosen = new Set<Phase>(DEFAULT_ENABLED_PHASES);
|
|
147
159
|
for (;;) {
|
|
148
|
-
const rows =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
const rows = SELECTABLE_PHASES.map(
|
|
161
|
+
(p) => `${chosen.has(p) ? "[x]" : "[ ]"} ${p} — ${PHASE_PURPOSE[p]}`,
|
|
162
|
+
);
|
|
163
|
+
const hit = await prompt.select(
|
|
164
|
+
`Which phases should run? (${chosen.size ? [...SELECTABLE_PHASES].filter((p) => chosen.has(p)).join(" → ") : "none yet"})`,
|
|
165
|
+
[...rows, DONE],
|
|
166
|
+
);
|
|
167
|
+
if (hit === undefined) return undefined;
|
|
168
|
+
if (hit === DONE) break;
|
|
169
|
+
const key = SELECTABLE_PHASES[rows.indexOf(hit)];
|
|
170
|
+
if (!key) break;
|
|
171
|
+
if (chosen.has(key)) chosen.delete(key);
|
|
172
|
+
else chosen.add(key);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const phases = normalizePhases([...chosen]);
|
|
176
|
+
|
|
177
|
+
// -- a mode for each ----------------------------------------------------
|
|
178
|
+
//
|
|
179
|
+
// Asked one phase at a time rather than as a checklist, because "does this
|
|
180
|
+
// one stop for me" is a different question for RESEARCH than for SHIP and
|
|
181
|
+
// the help text is the useful part.
|
|
182
|
+
const modes: PhaseModes = {};
|
|
183
|
+
const modeLabels = PHASE_MODE_OPTIONS.map((o) => line(o.label, o.help));
|
|
184
|
+
for (const phase of phases) {
|
|
185
|
+
const answer = await prompt.select(
|
|
186
|
+
`${phase.toUpperCase()} — ${PHASE_PURPOSE[phase]}`,
|
|
187
|
+
modeLabels,
|
|
188
|
+
);
|
|
189
|
+
if (answer === undefined) return undefined;
|
|
190
|
+
modes[phase] = (PHASE_MODE_OPTIONS[modeLabels.indexOf(answer)]?.value ?? "autopilot") as PhaseMode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const draft = customWorkflow(phases, modes);
|
|
194
|
+
prompt.notify(renderWorkflow(draft), "info");
|
|
195
|
+
|
|
196
|
+
// -- keep it? -----------------------------------------------------------
|
|
197
|
+
const KEEP = "save it under a name I can reuse";
|
|
198
|
+
const ONCE = "just use it here";
|
|
199
|
+
const keep = await prompt.select("Keep this workflow?", [KEEP, ONCE]);
|
|
200
|
+
if (keep === undefined) return undefined;
|
|
201
|
+
if (keep !== KEEP) return draft;
|
|
202
|
+
|
|
203
|
+
for (;;) {
|
|
204
|
+
const name = await prompt.input("Call it what?", "e.g. spec-heavy, weekend-run, client-work");
|
|
205
|
+
if (name === undefined || !name.trim()) return draft;
|
|
206
|
+
const saved = saveWorkflow(
|
|
207
|
+
{ name: name.trim(), phases, modes },
|
|
208
|
+
env,
|
|
209
|
+
);
|
|
210
|
+
if (saved.ok && saved.workflow) {
|
|
211
|
+
prompt.notify(`Saved "${saved.workflow.name}". It will be offered on every project.`, "info");
|
|
212
|
+
return saved.workflow;
|
|
213
|
+
}
|
|
214
|
+
prompt.notify(saved.error ?? "Could not save that.", "warning");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── choosing a display template ─────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
const CUSTOMISE = "choose level by level";
|
|
221
|
+
|
|
222
|
+
export async function pickDisplay(
|
|
223
|
+
prompt: Prompter,
|
|
224
|
+
env?: NodeJS.ProcessEnv,
|
|
225
|
+
): Promise<DisplayPolicy | undefined> {
|
|
226
|
+
const available = listDisplays(env);
|
|
227
|
+
const rows = available.map((d) => line(d.builtIn ? d.name : `${d.name} (yours)`, d.description));
|
|
228
|
+
const picked = await prompt.select(DISPLAY_QUESTION.title, [...rows, CUSTOMISE]);
|
|
229
|
+
if (picked === undefined) return undefined;
|
|
230
|
+
if (picked !== CUSTOMISE) return available[rows.indexOf(picked)]?.policy ?? defaultDisplay();
|
|
231
|
+
return buildDisplay(prompt, defaultDisplay(), env);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const SUBTASK_CYCLE: DisplayPolicy["levels"]["subtask"][] = ["none", "active", "all"];
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The level-by-level editor, shared by the wizard and `/infinity:display`.
|
|
238
|
+
*
|
|
239
|
+
* Every row toggles; subtasks cycle through none → active → all, because
|
|
240
|
+
* "only on the task being worked" is the answer most people want and a plain
|
|
241
|
+
* on/off cannot express it.
|
|
242
|
+
*/
|
|
243
|
+
export async function buildDisplay(
|
|
244
|
+
prompt: Prompter,
|
|
245
|
+
starting: DisplayPolicy,
|
|
246
|
+
env?: NodeJS.ProcessEnv,
|
|
247
|
+
): Promise<DisplayPolicy | undefined> {
|
|
248
|
+
let policy = normalizeDisplay(starting);
|
|
249
|
+
|
|
250
|
+
for (;;) {
|
|
251
|
+
const rows = [
|
|
252
|
+
`${policy.levels.goal ? "[x]" : "[ ]"} goals`,
|
|
253
|
+
`${policy.levels.sprint ? "[x]" : "[ ]"} sprints`,
|
|
254
|
+
`${policy.levels.feature ? "[x]" : "[ ]"} features`,
|
|
255
|
+
`${policy.levels.task ? "[x]" : "[ ]"} tasks`,
|
|
256
|
+
`[${policy.levels.subtask}] subtasks — none · active (only the task being worked) · all`,
|
|
257
|
+
`${policy.counts ? "[x]" : "[ ]"} done/total counts on goals, sprints and features`,
|
|
258
|
+
`${policy.dependencies ? "[x]" : "[ ]"} dependency labels (← #3)`,
|
|
259
|
+
`${policy.criteria ? "[x]" : "[ ]"} acceptance criteria (dashboard only)`,
|
|
260
|
+
`${policy.rail ? "[x]" : "[ ]"} the phase rail`,
|
|
261
|
+
`${policy.progress ? "[x]" : "[ ]"} the progress meter`,
|
|
262
|
+
`${policy.alerts ? "[x]" : "[ ]"} the alert strip (blocked, rework, retries, approvals)`,
|
|
263
|
+
`[${policy.taskWindow}] rows of plan in the terminal before it scrolls`,
|
|
264
|
+
];
|
|
265
|
+
const SAVE = "save this as a template I can reuse";
|
|
266
|
+
const hit = await prompt.select("What should the widget and the dashboard show?", [
|
|
153
267
|
...rows,
|
|
154
|
-
|
|
268
|
+
SAVE,
|
|
269
|
+
DONE,
|
|
155
270
|
]);
|
|
156
|
-
if (
|
|
157
|
-
if (
|
|
158
|
-
const hit = options[rows.indexOf(pick)];
|
|
159
|
-
if (!hit) break;
|
|
160
|
-
if (chosen.has(hit.value)) chosen.delete(hit.value);
|
|
161
|
-
else chosen.add(hit.value);
|
|
162
|
-
}
|
|
271
|
+
if (hit === undefined) return undefined;
|
|
272
|
+
if (hit === DONE) return { ...policy, preset: "custom" };
|
|
163
273
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
274
|
+
if (hit === SAVE) {
|
|
275
|
+
const name = await prompt.input("Call it what?", "e.g. sprint-view, my-focus");
|
|
276
|
+
if (name && name.trim()) {
|
|
277
|
+
const saved = saveDisplay({ name: name.trim(), policy }, env);
|
|
278
|
+
if (saved.ok && saved.template) {
|
|
279
|
+
prompt.notify(`Saved "${saved.template.name}".`, "info");
|
|
280
|
+
return saved.template.policy;
|
|
281
|
+
}
|
|
282
|
+
prompt.notify(saved.error ?? "Could not save that.", "warning");
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const idx = rows.indexOf(hit);
|
|
288
|
+
switch (idx) {
|
|
289
|
+
case 0:
|
|
290
|
+
policy = { ...policy, levels: { ...policy.levels, goal: !policy.levels.goal } };
|
|
291
|
+
break;
|
|
292
|
+
case 1:
|
|
293
|
+
policy = { ...policy, levels: { ...policy.levels, sprint: !policy.levels.sprint } };
|
|
294
|
+
break;
|
|
295
|
+
case 2:
|
|
296
|
+
policy = { ...policy, levels: { ...policy.levels, feature: !policy.levels.feature } };
|
|
297
|
+
break;
|
|
298
|
+
case 3:
|
|
299
|
+
policy = { ...policy, levels: { ...policy.levels, task: !policy.levels.task } };
|
|
300
|
+
break;
|
|
301
|
+
case 4: {
|
|
302
|
+
const next = SUBTASK_CYCLE[(SUBTASK_CYCLE.indexOf(policy.levels.subtask) + 1) % SUBTASK_CYCLE.length]!;
|
|
303
|
+
policy = { ...policy, levels: { ...policy.levels, subtask: next } };
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case 5:
|
|
307
|
+
policy = { ...policy, counts: !policy.counts };
|
|
308
|
+
break;
|
|
309
|
+
case 6:
|
|
310
|
+
policy = { ...policy, dependencies: !policy.dependencies };
|
|
311
|
+
break;
|
|
312
|
+
case 7:
|
|
313
|
+
policy = { ...policy, criteria: !policy.criteria };
|
|
314
|
+
break;
|
|
315
|
+
case 8:
|
|
316
|
+
policy = { ...policy, rail: !policy.rail };
|
|
317
|
+
break;
|
|
318
|
+
case 9:
|
|
319
|
+
policy = { ...policy, progress: !policy.progress };
|
|
320
|
+
break;
|
|
321
|
+
case 10:
|
|
322
|
+
policy = { ...policy, alerts: !policy.alerts };
|
|
323
|
+
break;
|
|
324
|
+
case 11: {
|
|
325
|
+
const answer = await prompt.input("How many rows?", String(policy.taskWindow));
|
|
326
|
+
const n = Number(String(answer ?? "").trim());
|
|
327
|
+
if (Number.isFinite(n)) policy = normalizeDisplay({ ...policy, taskWindow: n });
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
return { ...policy, preset: "custom" };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
169
334
|
}
|
|
170
335
|
|
|
171
336
|
/**
|
|
172
337
|
* The wizard's answers when nobody is there to give any.
|
|
173
338
|
*
|
|
174
339
|
* A headless run must not stall on a prompt, and it must not silently become
|
|
175
|
-
* a
|
|
176
|
-
* approved — because
|
|
177
|
-
*
|
|
340
|
+
* a workflow the human did not pick. The safe default is autopilot with
|
|
341
|
+
* nothing approved — because an approval gate with no human to answer it would
|
|
342
|
+
* park forever — plus a loud warning that says exactly that.
|
|
178
343
|
*/
|
|
179
344
|
export function unattendedIntake(brief: string | null, phases?: Phase[]): IntakePlan {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
phases,
|
|
187
|
-
|
|
345
|
+
const ordered = normalizePhases(phases ?? [...DEFAULT_ENABLED_PHASES]);
|
|
346
|
+
const workflow: Workflow = {
|
|
347
|
+
id: "autopilot",
|
|
348
|
+
name: "autopilot",
|
|
349
|
+
description: "You approve nothing.",
|
|
350
|
+
builtIn: true,
|
|
351
|
+
phases: ordered,
|
|
352
|
+
modes: normalizeModes({}, ordered),
|
|
353
|
+
};
|
|
354
|
+
const plan = planIntake({ workflow, brief: brief ?? "", handoff: "phase" });
|
|
188
355
|
return {
|
|
189
356
|
...plan,
|
|
190
357
|
warnings: [
|