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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,70 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.4.0] — 2026-08-24
|
|
8
|
+
|
|
9
|
+
Two settings that were one switch each, and one switch turned out to be the wrong shape for both
|
|
10
|
+
questions.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A mode per phase, not a mode per run.** "copilot" and "autopilot" could not say "let it define
|
|
15
|
+
and plan on its own but show me the review", and that is a thing people want. Every phase now
|
|
16
|
+
carries its own mode: `autopilot` advances when the gate passes, `copilot` stops and waits for
|
|
17
|
+
your signature. The two familiar words survive as two named points in that space rather than the
|
|
18
|
+
only two points in it, and every phase except INIT can be a checkpoint — the three that decide
|
|
19
|
+
*what gets built* are the ones that usually pay for themselves, but that is a default rather than
|
|
20
|
+
a limit.
|
|
21
|
+
|
|
22
|
+
- **Five workflows ship, and you can build your own.** `copilot`, `autopilot`, `spec and ship`
|
|
23
|
+
(you sign the scope going in and the release coming out), `research first`, and `every gate`.
|
|
24
|
+
Building one is its own short flow: pick the phases, then say for each whether it stops for you.
|
|
25
|
+
Name it and it is kept — in `~/.pi/agent/infinity-harness/`, with *you* rather than with the
|
|
26
|
+
project, because a workflow you designed is worth as much on the next one. It is then the first
|
|
27
|
+
thing offered there.
|
|
28
|
+
|
|
29
|
+
Built-ins are read-only, and their names cannot be taken. "copilot" has to mean the same thing in
|
|
30
|
+
every conversation about this tool; someone who wants a different copilot makes their own and
|
|
31
|
+
gives it their own name.
|
|
32
|
+
|
|
33
|
+
- **`/infinity:workflow`** — choose one, build one, switch by name (`/infinity:workflow
|
|
34
|
+
spec-and-ship`), or `list` what is available and what you are on. `/infinity:config` →
|
|
35
|
+
**Workflow** edits one phase at a time. All of it changes at any time and takes effect at the next
|
|
36
|
+
gate: three phases into a run is exactly when someone realises they do want to see the review.
|
|
37
|
+
|
|
38
|
+
- **Display templates.** Shipping all five plan levels to everyone was the wrong answer for the same
|
|
39
|
+
reason shipping two was: one person works in sprints and never opens a subtask, the next has no
|
|
40
|
+
sprints and lives in the subtask list. Four ship — `focus`, `everything`, `overview` (the shape,
|
|
41
|
+
no tasks), `worklist` (tasks only, no rail) — and anything else is chosen level by level: the
|
|
42
|
+
five plan levels, the `done/total` counts, the `← #3` dependency labels, the acceptance criteria,
|
|
43
|
+
the phase rail, the progress meter, the alert strip, and how many rows the terminal shows before
|
|
44
|
+
it scrolls. Name what you end up with and it is saved with you like a workflow.
|
|
45
|
+
|
|
46
|
+
- **`/infinity:display`** — pick a template, choose level by level, switch by name, or `list`.
|
|
47
|
+
`/infinity:config` → **Display** edits the same things one at a time.
|
|
48
|
+
|
|
49
|
+
- **The widget and the dashboard read the same setting.** Configure how you like to read a plan
|
|
50
|
+
once, not twice.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- `harness/config.json` gains `phaseModes`, `workflow` and `display`. A 2.3 config is migrated on
|
|
55
|
+
read: its three-phase `approvals` becomes the equivalent modes and is labelled with the workflow
|
|
56
|
+
it amounts to, so a project mid-run keeps exactly the approvals it was configured with and nobody
|
|
57
|
+
has to edit JSON to upgrade. The legacy field is kept in step on write.
|
|
58
|
+
- Hiding a plan level hides the row, never the work beneath it: turn off sprints on a plan organised
|
|
59
|
+
into sprints and the features move up one indent rather than vanishing, and task numbers do not
|
|
60
|
+
shift, so `← #3` still points at the same task. A task nobody can see is a task that gets stuck
|
|
61
|
+
forever.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **`initHarness` ignored a caller that passed the old `approvals` shape** once `phaseModes`
|
|
66
|
+
existed, silently producing an autopilot run. It falls back to `approvals` when no modes are
|
|
67
|
+
given — the same rule `loadConfig` applies to an older file.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
7
71
|
## [2.3.1] — 2026-08-24
|
|
8
72
|
|
|
9
73
|
*(2.3.0 was staged at the registry and never completed; 2.3.1 is that release plus the last two
|
package/README.md
CHANGED
|
@@ -100,11 +100,10 @@ It detects your stack and its lint/test/build commands, then asks you five quest
|
|
|
100
100
|
|
|
101
101
|
| | |
|
|
102
102
|
|---|---|
|
|
103
|
-
| **
|
|
104
|
-
| **What are you building?** | One or two sentences. Asked
|
|
105
|
-
| **Research it first?** | Adds an optional RESEARCH phase before DEFINE: prior art, constraints, options with costs, a recommendation, and the questions only you can answer. |
|
|
106
|
-
| **Which phases do you sign?** *(autopilot only)* | RESEARCH, DEFINE, PLAN — tick any, all or none. None is the walk-away setting. |
|
|
103
|
+
| **Which workflow?** | A built-in, one you saved, or *build one* — pick the phases, then say for each whether it stops for you |
|
|
104
|
+
| **What are you building?** | One or two sentences. Asked whatever the workflow, because a run with no goal has no business inventing one. |
|
|
107
105
|
| **When should it start a fresh session?** | Every phase (default) · every task · never |
|
|
106
|
+
| **How much of the plan on screen?** | A display template — `focus`, `everything`, `overview`, `worklist`, or level by level |
|
|
108
107
|
|
|
109
108
|
Then it writes `harness/` — the config, an empty plan, the phase and role docs, and starters
|
|
110
109
|
for the documents the review gate will demand — and hands the model its first brief. It never
|
|
@@ -134,14 +133,45 @@ the craft skills that match the work, and what to do next. Do the work, then:
|
|
|
134
133
|
|
|
135
134
|
## Who decides what
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
"copilot" and "autopilot" used to be one switch, and one switch is the wrong shape for the
|
|
137
|
+
question. What people actually want is per-phase: let the model define and plan on its own but
|
|
138
|
+
show me the review; or grill me on the definition and then leave me alone until it ships. Two
|
|
139
|
+
words cannot say that.
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
|
144
|
-
|
|
141
|
+
So the setting is a **mode per phase**, and the two familiar words are two named points in that
|
|
142
|
+
space rather than the only two points in it.
|
|
143
|
+
|
|
144
|
+
| Mode | When a phase's gate passes |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `autopilot` | it advances |
|
|
147
|
+
| `copilot` | it stops and waits for your signature |
|
|
148
|
+
|
|
149
|
+
Five workflows ship with the package, and none of them can be edited — `copilot` has to mean the
|
|
150
|
+
same thing in every conversation about this tool:
|
|
151
|
+
|
|
152
|
+
| Workflow | Stops at |
|
|
153
|
+
|---|---|
|
|
154
|
+
| **copilot** | DEFINE, PLAN |
|
|
155
|
+
| **autopilot** | nothing |
|
|
156
|
+
| **spec and ship** | DEFINE, SHIP — you sign the scope going in and the release coming out |
|
|
157
|
+
| **research first** | adds a RESEARCH phase, and stops on all three thinking phases |
|
|
158
|
+
| **every gate** | every phase. Slowest, and the one you want on something that matters |
|
|
159
|
+
|
|
160
|
+
Anything else you build yourself: pick the phases, then say for each whether it stops for you. Give
|
|
161
|
+
it a name and it is saved with *you*, not with the project, so it is the first thing offered on your
|
|
162
|
+
next one.
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
/infinity:workflow choose one, or build one
|
|
166
|
+
/infinity:workflow spec-and-ship switch to a named one, no menu
|
|
167
|
+
/infinity:workflow list what is available, and what you are on now
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Any of it changes at any time and takes effect at the next gate — three phases into a run is
|
|
171
|
+
exactly when someone realises they do want to see the review after all. `/infinity:config` →
|
|
172
|
+
**Workflow** edits one phase at a time.
|
|
173
|
+
|
|
174
|
+
### Signing a phase
|
|
145
175
|
|
|
146
176
|
When a phase you signed up for passes its gate, the run **stops and asks you** rather than
|
|
147
177
|
advancing. Approving continues it; answering with a sentence sends the phase back carrying your
|
|
@@ -152,9 +182,9 @@ words, so it is redone against your objection rather than redone identically:
|
|
|
152
182
|
/infinity:approve the criteria say nothing about refunds
|
|
153
183
|
```
|
|
154
184
|
|
|
155
|
-
A rejection is pinned to the state of the project when you made it, so the run will not ask you
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
A rejection is pinned to the state of the project when you made it, so the run will not ask you the
|
|
186
|
+
same question again until the agent has actually changed something in response. If it never does,
|
|
187
|
+
the run stops and says so instead of nagging forever.
|
|
158
188
|
|
|
159
189
|
## One run, many sessions
|
|
160
190
|
|
|
@@ -224,7 +254,8 @@ valid; the menu is the same data with prompts and bounds checking attached.
|
|
|
224
254
|
|---|---|
|
|
225
255
|
| **Models** | Which model runs each difficulty tier, the master model, consultation budget |
|
|
226
256
|
| **Pipeline** | Which phases run, copilot vs autopilot, role strictness, pause |
|
|
227
|
-
| **
|
|
257
|
+
| **Workflow** | The mode for each phase — which of them stop and wait for your signature |
|
|
258
|
+
| **Display** | Which plan levels and which chrome the widget and the dashboard draw |
|
|
228
259
|
| **Sessions** | Fresh session per phase or per task, the context threshold, the carry note |
|
|
229
260
|
| **Project commands** | lint / test / coverage / build — what the gate actually runs |
|
|
230
261
|
| **Gates** | Enable, coverage threshold, placeholder rejection |
|
|
@@ -349,7 +380,7 @@ The agent edits it by submitting the **complete** task list through the `infinit
|
|
|
349
380
|
|
|
350
381
|
## Watching it work
|
|
351
382
|
|
|
352
|
-
**In the terminal** — the widget
|
|
383
|
+
**In the terminal** — the widget updates on every turn, showing the plan as deep as you asked for:
|
|
353
384
|
goal, sprint, feature, task, subtask. It is a *window*, not a truncation — the rows above and
|
|
354
385
|
below are counted, and one keypress away:
|
|
355
386
|
|
|
@@ -362,14 +393,43 @@ below are counted, and one keypress away:
|
|
|
362
393
|
It is responsive down to ~58 columns, degrades to ASCII when the locale isn't UTF-8, and drops
|
|
363
394
|
colour under `NO_COLOR`.
|
|
364
395
|
|
|
365
|
-
**In a browser** — `/infinity:dashboard` serves a live page on loopback: phase rail, stacked
|
|
366
|
-
meters that show stuck work as colour rather than absence, the
|
|
367
|
-
|
|
368
|
-
|
|
396
|
+
**In a browser** — `/infinity:dashboard` serves a live page on loopback: phase rail, stacked
|
|
397
|
+
progress meters that show stuck work as colour rather than absence, the plan as a collapsible tree
|
|
398
|
+
with counts at every level, and the last gate verdict. It refreshes itself every 5 seconds and
|
|
399
|
+
reconnects with backoff if the run ends.
|
|
369
400
|
|
|
370
401
|
The dashboard is strictly read-only and binds to `127.0.0.1`. It never writes, and never bumps
|
|
371
402
|
`baseRevision` — opening it can't perturb the run you're watching.
|
|
372
403
|
|
|
404
|
+
### What they show is yours to choose
|
|
405
|
+
|
|
406
|
+
Two people watching the same run want different things on screen. One works in sprints and never
|
|
407
|
+
opens a subtask; the next has no sprints and lives in the subtask list. So it is a setting, and
|
|
408
|
+
**the widget and the dashboard read the same one** — configure how you like to read a plan once,
|
|
409
|
+
not twice.
|
|
410
|
+
|
|
411
|
+
| Template | |
|
|
412
|
+
|---|---|
|
|
413
|
+
| **focus** *(default)* | every level, with subtasks on the task being worked |
|
|
414
|
+
| **everything** | all five levels, every subtask on every task, a taller window |
|
|
415
|
+
| **overview** | goals, sprints and features with their counts. No tasks — the shape, not the work |
|
|
416
|
+
| **worklist** | tasks only. No grouping rows, no rail — for when you already know the plan |
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
/infinity:display pick a template, or choose level by level
|
|
420
|
+
/infinity:display overview switch straight to one
|
|
421
|
+
/infinity:display list what is available, and what you are on now
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Choosing level by level also lets you turn off the phase rail, the progress meter, the alert strip,
|
|
425
|
+
the `done/total` counts, the `← #3` dependency labels and the acceptance criteria, and set how many
|
|
426
|
+
rows the terminal shows before it scrolls. Name what you end up with and it is saved with you,
|
|
427
|
+
offered on your next project. `/infinity:config` → **Display** edits the same things one at a time.
|
|
428
|
+
|
|
429
|
+
Hiding a level never hides what is under it: turn off sprints on a plan organised into sprints and
|
|
430
|
+
the features move up one indent, they do not vanish. A task nobody can see is a task that gets
|
|
431
|
+
stuck forever.
|
|
432
|
+
|
|
373
433
|
## Craft skills
|
|
374
434
|
|
|
375
435
|
28 short documents on how to do the work well — how to write a test worth
|
|
@@ -447,10 +507,12 @@ infinity-harness/
|
|
|
447
507
|
│ │ · featureList (the SSOT) · lock · exec
|
|
448
508
|
│ │ · skills (match) · skillsAudit (guard)
|
|
449
509
|
│ ├── ui/ theme · planTree (the five levels, once)
|
|
510
|
+
│ │ · display (what to draw, and the templates)
|
|
450
511
|
│ │ · widget (terminal) · dashboard (web) · wizard · config
|
|
451
512
|
│ ├── loop.ts the continuous-run driver and its stop conditions
|
|
452
513
|
│ ├── runState.ts is a run armed, and which run is it — on disk, across sessions
|
|
453
514
|
│ ├── handoff.ts when to continue in a fresh session, and what to tell it
|
|
515
|
+
│ ├── workflow.ts a mode per phase, the built-ins, and the ones you save
|
|
454
516
|
│ ├── approval.ts · intake.ts human sign-off · what the start-up wizard's answers mean
|
|
455
517
|
│ ├── escalate.ts the ladder's actuator: chooses a rung and takes it
|
|
456
518
|
│ ├── goal.ts the outer loop: is the thing asked for actually done?
|
|
@@ -467,7 +529,10 @@ infinity-harness/
|
|
|
467
529
|
│ ├── model-router.json optional routing
|
|
468
530
|
│ ├── docs/ architecture · decisions · phase and role docs
|
|
469
531
|
│ └── skills/ 28 craft skills the brief points at
|
|
470
|
-
|
|
532
|
+
│
|
|
533
|
+
│ ~/.pi/agent/infinity-harness/ the workflows and display templates you saved —
|
|
534
|
+
│ they belong to you, not to a project
|
|
535
|
+
├── tests/ 33 files, plain node:assert
|
|
471
536
|
└── scripts/
|
|
472
537
|
├── run-tests.mjs
|
|
473
538
|
├── e2e.mjs 16 scenarios, including one against a real pi process
|
|
@@ -482,7 +547,7 @@ there is one implementation, and the adapter calls it.
|
|
|
482
547
|
```bash
|
|
483
548
|
npm install
|
|
484
549
|
npm run check # tsc --noEmit, strict
|
|
485
|
-
npm test #
|
|
550
|
+
npm test # 33 test files
|
|
486
551
|
npm run e2e # 16 end-to-end scenarios
|
|
487
552
|
npm run e2e -- --only realpi # just the ones that drive a real pi process
|
|
488
553
|
npm run e2e -- --list # what the scenarios are
|
|
@@ -78,7 +78,29 @@ import {
|
|
|
78
78
|
type HandoffReason,
|
|
79
79
|
} from "../../src/handoff.ts";
|
|
80
80
|
import { needsApproval, resolveApproval, approvedPhases } from "../../src/approval.ts";
|
|
81
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
buildDisplay,
|
|
83
|
+
pickDisplay,
|
|
84
|
+
pickWorkflow,
|
|
85
|
+
runIntakeWizard,
|
|
86
|
+
unattendedIntake,
|
|
87
|
+
} from "../../src/ui/wizard.ts";
|
|
88
|
+
import {
|
|
89
|
+
applyWorkflow,
|
|
90
|
+
findWorkflow,
|
|
91
|
+
listWorkflows,
|
|
92
|
+
matchWorkflow,
|
|
93
|
+
renderWorkflow,
|
|
94
|
+
signedPhases,
|
|
95
|
+
summarizeWorkflow,
|
|
96
|
+
} from "../../src/workflow.ts";
|
|
97
|
+
import {
|
|
98
|
+
findDisplay,
|
|
99
|
+
listDisplays,
|
|
100
|
+
normalizeDisplay,
|
|
101
|
+
summarizeDisplay,
|
|
102
|
+
} from "../../src/ui/display.ts";
|
|
103
|
+
import type { DisplayPolicy } from "../../src/core/types.ts";
|
|
82
104
|
import { defaultView, scrollView, SCROLL_STEP, TASK_WINDOW, EXPANDED_WINDOW, type WidgetView } from "../../src/ui/widget.ts";
|
|
83
105
|
import { buildPlanRows } from "../../src/ui/planTree.ts";
|
|
84
106
|
|
|
@@ -165,6 +187,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
165
187
|
sessions: run?.sessions ?? null,
|
|
166
188
|
intake: typeof config.intake?.brief === "string" ? config.intake.brief : null,
|
|
167
189
|
awaitingApproval: config.awaitingApproval ?? null,
|
|
190
|
+
display: normalizeDisplay(config.display),
|
|
168
191
|
phase: config.currentPhase,
|
|
169
192
|
enabledPhases: config.phases?.enabled,
|
|
170
193
|
paused: Boolean(config.paused),
|
|
@@ -1246,7 +1269,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1246
1269
|
});
|
|
1247
1270
|
|
|
1248
1271
|
pi.registerCommand("infinity:init", {
|
|
1249
|
-
description: "Set up a harness here —
|
|
1272
|
+
description: "Set up a harness here — workflow, goal, sessions, display",
|
|
1250
1273
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
1251
1274
|
const dir = projectDir(ctx);
|
|
1252
1275
|
const force = /\bforce\b/.test(args);
|
|
@@ -1262,57 +1285,36 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1262
1285
|
}
|
|
1263
1286
|
|
|
1264
1287
|
const detected = detectStack(dir);
|
|
1265
|
-
let phases: Phase[] | undefined;
|
|
1266
1288
|
|
|
1267
|
-
//
|
|
1289
|
+
// Three things used to be wrong here, and they compounded.
|
|
1268
1290
|
//
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
// "you decide everything, including what I want".
|
|
1291
|
+
// The wizard never asked what was being built — so picking "autopilot"
|
|
1292
|
+
// started a run with no idea and no scope, and the harness invented a
|
|
1293
|
+
// project and began building it.
|
|
1273
1294
|
//
|
|
1274
|
-
//
|
|
1275
|
-
// yourself, but show me the plan before you build it"
|
|
1276
|
-
// most people actually want from an unattended run.
|
|
1295
|
+
// "mode" was the only question, and one switch cannot say "drive
|
|
1296
|
+
// yourself, but show me the plan before you build it".
|
|
1277
1297
|
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
// answers mean
|
|
1298
|
+
// And even that switch only reached three phases. It is a mode per
|
|
1299
|
+
// phase now, chosen from a workflow the human can build, name and reuse.
|
|
1300
|
+
// `src/workflow.ts` owns what a workflow is, `src/intake.ts` what the
|
|
1301
|
+
// answers mean, `src/ui/wizard.ts` how they are asked.
|
|
1282
1302
|
if (ctx.hasUI) {
|
|
1283
1303
|
const cmds = Object.entries(detected.commands).filter(([, v]) => Boolean(v));
|
|
1284
1304
|
const summary = cmds.length ? cmds.map(([k, v]) => `${k}: ${v}`).join(", ") : "no commands detected";
|
|
1285
|
-
const go = await ctx.ui.select(
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
);
|
|
1305
|
+
const go = await ctx.ui.select(`Create a harness here? ${detected.label} · ${summary}`, [
|
|
1306
|
+
"yes",
|
|
1307
|
+
"cancel",
|
|
1308
|
+
]);
|
|
1289
1309
|
if (go === undefined || go === "cancel") {
|
|
1290
1310
|
notify(ctx, "init cancelled — nothing was written.", "info");
|
|
1291
1311
|
return;
|
|
1292
1312
|
}
|
|
1293
|
-
|
|
1294
|
-
if (go.includes("phases")) {
|
|
1295
|
-
const chosen = new Set<Phase>(DEFAULT_ENABLED_PHASES);
|
|
1296
|
-
for (;;) {
|
|
1297
|
-
const rows = SELECTABLE_PHASES.map((p) => `${chosen.has(p) ? "[x]" : "[ ]"} ${p}`);
|
|
1298
|
-
const hit = await ctx.ui.select("Phases to run", [...rows, "✓ done"]);
|
|
1299
|
-
if (hit === undefined || hit === "✓ done") break;
|
|
1300
|
-
const key = SELECTABLE_PHASES[rows.indexOf(hit)];
|
|
1301
|
-
if (!key) break;
|
|
1302
|
-
if (chosen.has(key)) chosen.delete(key);
|
|
1303
|
-
else chosen.add(key);
|
|
1304
|
-
}
|
|
1305
|
-
phases = [...chosen];
|
|
1306
|
-
}
|
|
1307
1313
|
}
|
|
1308
1314
|
|
|
1309
1315
|
const wizard = ctx.hasUI
|
|
1310
|
-
? await runIntakeWizard({
|
|
1311
|
-
|
|
1312
|
-
phases,
|
|
1313
|
-
brief: goalFromArgs || null,
|
|
1314
|
-
})
|
|
1315
|
-
: ({ cancelled: false, plan: unattendedIntake(goalFromArgs || null, phases) } as const);
|
|
1316
|
+
? await runIntakeWizard({ prompt: prompterFor(ctx), brief: goalFromArgs || null })
|
|
1317
|
+
: ({ cancelled: false, plan: unattendedIntake(goalFromArgs || null) } as const);
|
|
1316
1318
|
|
|
1317
1319
|
if (wizard.cancelled) {
|
|
1318
1320
|
notify(ctx, "init cancelled — nothing was written.", "info");
|
|
@@ -1324,6 +1326,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1324
1326
|
mode: plan.mode,
|
|
1325
1327
|
phases: plan.phases,
|
|
1326
1328
|
approvals: plan.approvals,
|
|
1329
|
+
phaseModes: plan.phaseModes,
|
|
1330
|
+
workflow: plan.workflow,
|
|
1331
|
+
display: plan.display,
|
|
1327
1332
|
session: plan.session,
|
|
1328
1333
|
brief: plan.brief,
|
|
1329
1334
|
force,
|
|
@@ -1353,6 +1358,143 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1353
1358
|
},
|
|
1354
1359
|
});
|
|
1355
1360
|
|
|
1361
|
+
/**
|
|
1362
|
+
* Change the workflow mid-run.
|
|
1363
|
+
*
|
|
1364
|
+
* Any of this is editable at any time and takes effect on the next gate —
|
|
1365
|
+
* a run three phases deep is exactly when someone realises they do want to
|
|
1366
|
+
* see the review after all.
|
|
1367
|
+
*/
|
|
1368
|
+
pi.registerCommand("infinity:workflow", {
|
|
1369
|
+
description: "Choose or build the workflow — which phases run, and which stop for you",
|
|
1370
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
1371
|
+
const dir = projectDir(ctx);
|
|
1372
|
+
if (!isHarnessProject(dir)) {
|
|
1373
|
+
notify(ctx, NO_HARNESS, "warning");
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const { config } = loadConfig(dir);
|
|
1378
|
+
const arg = args.trim();
|
|
1379
|
+
|
|
1380
|
+
if (arg === "" && !ctx.hasUI) {
|
|
1381
|
+
notify(ctx, describeCurrentWorkflow(dir), "info");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (arg === "show" || arg === "list") {
|
|
1385
|
+
const rows = listWorkflows().map((w) => ` ${w.builtIn ? " " : "*"} ${w.name} — ${w.description}`);
|
|
1386
|
+
notify(
|
|
1387
|
+
ctx,
|
|
1388
|
+
`${describeCurrentWorkflow(dir)}\n\nAvailable (* = yours):\n${rows.join("\n")}`,
|
|
1389
|
+
"info",
|
|
1390
|
+
);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// `/infinity:workflow <name>` switches without a menu, which is what a
|
|
1395
|
+
// second run in the same terminal wants.
|
|
1396
|
+
let chosen = arg ? findWorkflow(arg) : undefined;
|
|
1397
|
+
if (arg && !chosen) {
|
|
1398
|
+
notify(ctx, `No workflow called "${arg}". \`/infinity:workflow list\` shows them.`, "warning");
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (!chosen) {
|
|
1402
|
+
if (!ctx.hasUI) {
|
|
1403
|
+
notify(ctx, "This mode has no dialogs — `/infinity:workflow <name>` switches directly.", "warning");
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
chosen = (await pickWorkflow(prompterFor(ctx))) ?? undefined;
|
|
1407
|
+
if (!chosen) {
|
|
1408
|
+
notify(ctx, "Unchanged.", "info");
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const { value } = await withLock(configPath(dir), () => {
|
|
1414
|
+
const fresh = loadConfig(dir);
|
|
1415
|
+
if (!fresh.ok) return false;
|
|
1416
|
+
applyWorkflow(fresh.config, chosen!);
|
|
1417
|
+
// Keep the legacy field in step so a 2.3 tool reading this config
|
|
1418
|
+
// still sees the same three answers it understands.
|
|
1419
|
+
fresh.config.approvals = {
|
|
1420
|
+
research: fresh.config.phaseModes?.research === "copilot",
|
|
1421
|
+
define: fresh.config.phaseModes?.define === "copilot",
|
|
1422
|
+
plan: fresh.config.phaseModes?.plan === "copilot",
|
|
1423
|
+
};
|
|
1424
|
+
fresh.config.mode = signedPhases(fresh.config).length > 0 ? "copilot" : "autopilot";
|
|
1425
|
+
return saveConfig(dir, fresh.config).ok;
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
if (!value) {
|
|
1429
|
+
notify(ctx, "Could not save the workflow — config unreadable.", "error");
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
notify(ctx, `${renderWorkflow(chosen)}\n\nIt takes effect at the next gate.`, "info");
|
|
1433
|
+
refreshWidget(ctx);
|
|
1434
|
+
void config;
|
|
1435
|
+
},
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
pi.registerCommand("infinity:display", {
|
|
1439
|
+
description: "Choose what the widget and the dashboard show, level by level",
|
|
1440
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
1441
|
+
const dir = projectDir(ctx);
|
|
1442
|
+
if (!isHarnessProject(dir)) {
|
|
1443
|
+
notify(ctx, NO_HARNESS, "warning");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const arg = args.trim();
|
|
1447
|
+
const current = normalizeDisplay(loadConfig(dir).config.display);
|
|
1448
|
+
|
|
1449
|
+
if (arg === "show" || arg === "list") {
|
|
1450
|
+
const rows = listDisplays().map((d) => ` ${d.builtIn ? " " : "*"} ${d.name} — ${d.description}`);
|
|
1451
|
+
notify(
|
|
1452
|
+
ctx,
|
|
1453
|
+
`Now: ${summarizeDisplay(current)}\n\nTemplates (* = yours):\n${rows.join("\n")}`,
|
|
1454
|
+
"info",
|
|
1455
|
+
);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
let next: DisplayPolicy | undefined;
|
|
1460
|
+
if (arg) {
|
|
1461
|
+
const template = findDisplay(arg);
|
|
1462
|
+
if (!template) {
|
|
1463
|
+
notify(ctx, `No template called "${arg}". \`/infinity:display list\` shows them.`, "warning");
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
next = template.policy;
|
|
1467
|
+
} else {
|
|
1468
|
+
if (!ctx.hasUI) {
|
|
1469
|
+
notify(ctx, `Now: ${summarizeDisplay(current)}. \`/infinity:display <template>\` switches.`, "info");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
next = await pickDisplay(prompterFor(ctx));
|
|
1473
|
+
if (!next) {
|
|
1474
|
+
notify(ctx, "Unchanged.", "info");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const { value } = await withLock(configPath(dir), () => {
|
|
1480
|
+
const fresh = loadConfig(dir);
|
|
1481
|
+
if (!fresh.ok) return false;
|
|
1482
|
+
fresh.config.display = normalizeDisplay(next);
|
|
1483
|
+
return saveConfig(dir, fresh.config).ok;
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
if (!value) {
|
|
1487
|
+
notify(ctx, "Could not save the display settings — config unreadable.", "error");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
// The widget is the answer to "did that do what I wanted", so redraw it
|
|
1491
|
+
// before saying anything about it.
|
|
1492
|
+
view = defaultView();
|
|
1493
|
+
refreshWidget(ctx);
|
|
1494
|
+
notify(ctx, `Showing: ${summarizeDisplay(normalizeDisplay(next))}`, "info");
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1356
1498
|
/**
|
|
1357
1499
|
* Continue the run in a replacement session.
|
|
1358
1500
|
*
|
|
@@ -2295,6 +2437,20 @@ function errMsg(e: unknown): string {
|
|
|
2295
2437
|
return e instanceof Error ? e.message : String(e);
|
|
2296
2438
|
}
|
|
2297
2439
|
|
|
2440
|
+
/** The workflow this project is on, and whether it still matches a named one. */
|
|
2441
|
+
function describeCurrentWorkflow(dir: string): string {
|
|
2442
|
+
const { config } = loadConfig(dir);
|
|
2443
|
+
const named = matchWorkflow(config);
|
|
2444
|
+
const head = `Workflow: ${summarizeWorkflow(config)}`;
|
|
2445
|
+
const rail = (config.phases?.enabled ?? [])
|
|
2446
|
+
.map((p) => (config.phaseModes?.[p] === "copilot" ? `[${p}]` : p))
|
|
2447
|
+
.join(" → ");
|
|
2448
|
+
const drift = named
|
|
2449
|
+
? ""
|
|
2450
|
+
: "\n\nThese settings do not match any saved workflow. `/infinity:workflow` can save them as one.";
|
|
2451
|
+
return `${head}\n ${rail}\n (a phase in [brackets] stops for you)${drift}`;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2298
2454
|
/**
|
|
2299
2455
|
* The few sentences the run cannot afford to have summarised away.
|
|
2300
2456
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinity-harness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "A pi agent extension that runs a gated build pipeline unattended \u2014 enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
package/src/approval.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { HarnessConfig, Phase } from "./core/types.ts";
|
|
22
|
-
import { APPROVABLE_PHASES } from "./core/types.ts";
|
|
23
22
|
import { loadConfig, saveConfig } from "./core/config.ts";
|
|
23
|
+
import { modeFor, signedPhases, SIGNABLE_PHASES } from "./workflow.ts";
|
|
24
24
|
|
|
25
25
|
export type ApprovalRequest = {
|
|
26
26
|
phase: Phase;
|
|
@@ -29,26 +29,31 @@ export type ApprovalRequest = {
|
|
|
29
29
|
prompt: string;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
/** Every phase except INIT, which is plumbing rather than work. */
|
|
32
33
|
export function isApprovable(phase: Phase | null): boolean {
|
|
33
|
-
return phase !== null &&
|
|
34
|
+
return phase !== null && SIGNABLE_PHASES.includes(phase);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** Does `phase` need a signature before the pipeline may leave it? */
|
|
37
38
|
export function needsApproval(config: HarnessConfig, phase: Phase | null): boolean {
|
|
38
39
|
if (!isApprovable(phase)) return false;
|
|
39
|
-
|
|
40
|
-
return approvals[phase as string] === true;
|
|
40
|
+
return modeFor(config, phase) === "copilot";
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Which phases the human has asked to sign, in pipeline order. */
|
|
44
44
|
export function approvedPhases(config: HarnessConfig): Phase[] {
|
|
45
|
-
return
|
|
45
|
+
return signedPhases(config);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const ARTIFACTS: Record<string, string[]> = {
|
|
49
49
|
research: ["harness/docs/RESEARCH.md"],
|
|
50
50
|
define: ["specs/prd.md", "harness/sprint-contract.md", "the acceptance criteria in the plan"],
|
|
51
51
|
plan: ["the task list in the widget, or `/infinity:dashboard`"],
|
|
52
|
+
build: ["the diff since the last phase — `git diff`", "the tasks marked complete in the plan"],
|
|
53
|
+
verify: ["the test output", "what the tests still do not cover"],
|
|
54
|
+
simplify: ["what was deleted — `git diff --stat`"],
|
|
55
|
+
review: ["harness/evaluator-rubric.md and the score against it", "README.md and harness/docs/"],
|
|
56
|
+
ship: ["CHANGELOG.md", "the tag, and `git log` since the last one"],
|
|
52
57
|
};
|
|
53
58
|
|
|
54
59
|
const ASKS: Record<string, string> = {
|
|
@@ -57,6 +62,11 @@ const ASKS: Record<string, string> = {
|
|
|
57
62
|
define:
|
|
58
63
|
"Is this the thing you want built, and would meeting these criteria convince you it works?",
|
|
59
64
|
plan: "Does this plan build that thing, in an order that makes sense, with nothing important missing?",
|
|
65
|
+
build: "Is this the code you wanted written, and would you be happy to own it?",
|
|
66
|
+
verify: "Do these tests actually prove the thing works, or only that it runs?",
|
|
67
|
+
simplify: "Is what is left simpler, and is anything missing that should not be?",
|
|
68
|
+
review: "Would you approve this if someone else had written it?",
|
|
69
|
+
ship: "Is this ready to go out, under this version, with this changelog?",
|
|
60
70
|
};
|
|
61
71
|
|
|
62
72
|
export function describeApproval(phase: Phase): ApprovalRequest {
|