infinity-harness 2.8.0 → 2.8.2
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 +22 -0
- package/extensions/infinity-harness/index.ts +67 -9
- package/harness/docs/DECISIONS.md +107 -107
- package/package.json +2 -2
- package/src/ui/wizard.ts +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.8.2] — 2026-08-30
|
|
8
|
+
|
|
9
|
+
Republishes 2.8.1. The 2.8.1 tarball on disk and on the registry was built from 2.8.0 content while `package.json` already said 2.8.1, so `/infinity:init` still auto-started RESEARCH in your session and the wizard never asked `Start the run now?`. No code changes since `v2.8.1` (`81d16590`).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Stale 2.8.1 artifact.** Rebuilt from `HEAD` so the `launchNow` wizard step, the parked-by-default `NOT running — /infinity:run` opener, and the `controlPanelContract` are in the published package.
|
|
14
|
+
|
|
15
|
+
## [2.8.1] — 2026-08-30
|
|
16
|
+
|
|
17
|
+
Init no longer steals your session. The harness only runs when you say so.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **"/infinity:init" auto-started research in your session.** After the wizard, the extension sent the RESEARCH brief as followUp, so FullAuto began doing RESEARCH in the same session that just created the harness — before you could decide to walk away. The harness is now parked after init. The opener says "harness is NOT running — /infinity:run starts it" and the agent is idle in control-panel mode, regardless of workflow or phase.
|
|
22
|
+
|
|
23
|
+
- **No way to go straight from init to running.** The wizard now asks a final question "Start the run now?" ("yes — start the run now" / "no — I will run /infinity:run when ready"). Saying yes arms harness/run.json, captures baseModel for the daemon, and spawns the detached daemon (or the in-process supervisor fallback) — the same path as "/infinity:run". Saying no (or an unattended init or old scripted harness with no answer) stays parked.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Opener after init always names the next command. With a goal it shows the RESEARCH or DEFINE brief plus "NOT running — /infinity:run"; without a goal it asks for the goal first, then the same tagline.
|
|
28
|
+
|
|
7
29
|
## [2.8.0] — 2026-08-30
|
|
8
30
|
|
|
9
31
|
The harness stops shipping its own run. The repo is the driver, not the project.
|
|
@@ -1826,15 +1826,73 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1826
1826
|
notify(ctx, lines.join("\n"), plan.warnings.length ? "warning" : "info");
|
|
1827
1827
|
refreshWidget(ctx);
|
|
1828
1828
|
|
|
1829
|
-
//
|
|
1830
|
-
// the harness is
|
|
1831
|
-
//
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1829
|
+
// Show the next step, but do NOT auto-start the run.
|
|
1830
|
+
// After /infinity:init the harness is initialized and parked.
|
|
1831
|
+
// The agent can answer questions in this session, but the pipeline
|
|
1832
|
+
// only runs after the human says so: /infinity:run (or "yes" to the
|
|
1833
|
+
// last wizard question). Auto-starting here is the bug that made
|
|
1834
|
+
// full-auto research steal the session that just created the harness.
|
|
1835
|
+
if ((wizard as { launchNow?: boolean }).launchNow) {
|
|
1836
|
+
// Opt-in: wizard asked "Start the run now?" and the human said yes.
|
|
1837
|
+
armRun(dir, sessionId);
|
|
1838
|
+
// Capture baseModel for daemon (same as /infinity:run does)
|
|
1839
|
+
try {
|
|
1840
|
+
const bm = baseModelOf(ctx);
|
|
1841
|
+
if (bm) {
|
|
1842
|
+
const { loadRunState: _ld, saveRunState: _sv, runIdFor: _rf } = await import("../../src/core/runState.ts");
|
|
1843
|
+
const rid = _rf(dir, sessionId);
|
|
1844
|
+
const rs = _ld(dir);
|
|
1845
|
+
const parts = bm.split("/");
|
|
1846
|
+
const baseModel = parts.length >= 2 ? { provider: parts[0]!, id: parts.slice(1).join("/") } : { provider: "anthropic", id: bm };
|
|
1847
|
+
if (rs) { (rs as unknown as { baseModel: unknown }).baseModel = baseModel; _sv(dir, rs); }
|
|
1848
|
+
else { const { newRunState } = await import("../../src/core/runState.ts"); const ns = newRunState(rid); (ns as unknown as { baseModel: unknown }).baseModel = baseModel; _sv(dir, ns); }
|
|
1849
|
+
}
|
|
1850
|
+
} catch {}
|
|
1851
|
+
// Try detached daemon, fall back to supervisor in this pi session.
|
|
1852
|
+
const tryDaemonSpawn = async (): Promise<{ spawned: boolean }> => {
|
|
1853
|
+
try {
|
|
1854
|
+
const { spawn: _sp } = await import("node:child_process");
|
|
1855
|
+
const { existsSync: _ex, openSync: _op, closeSync: _cl } = await import("node:fs");
|
|
1856
|
+
const { resolve: _re } = await import("node:path");
|
|
1857
|
+
const cands = [_re(dir, "dist/daemon/index.js"), _re(dir, "src/daemon/index.ts")];
|
|
1858
|
+
let entry: string | null = null;
|
|
1859
|
+
for (const c of cands) if (_ex(c)) { entry = c; break; }
|
|
1860
|
+
if (!entry) return { spawned: false };
|
|
1861
|
+
try { const { loadDaemon: _ld, isDaemonAlive: _al } = await import("../../src/daemon/guard.ts"); const live = _ld(dir); if (live && _al(live)) return { spawned: true }; } catch {}
|
|
1862
|
+
const fd = _op(_re(dir, "harness/daemon.log"), "a");
|
|
1863
|
+
const a: string[] = [];
|
|
1864
|
+
if (entry.endsWith(".ts")) a.push("--experimental-strip-types", "--no-warnings=ExperimentalWarning");
|
|
1865
|
+
a.push(entry, dir);
|
|
1866
|
+
const ch = _sp(process.execPath, a, { detached: true, stdio: ["ignore", fd as unknown as number, fd as unknown as number], windowsHide: true, env: { ...process.env, INFINITY_HARNESS_WORKER: "1" } } as unknown as Parameters<typeof _sp>[2]);
|
|
1867
|
+
try { ch.unref(); } catch {}
|
|
1868
|
+
try { _cl(fd); } catch {}
|
|
1869
|
+
for (let i = 0; i < 40; i++) { await new Promise(r=>setTimeout(r,250)); try { const { loadDaemon: l2, isDaemonAlive: a2 } = await import("../../src/daemon/guard.ts"); const v = l2(dir); if (v && a2(v)) return { spawned: true }; } catch {}
|
|
1870
|
+
}
|
|
1871
|
+
return { spawned: true };
|
|
1872
|
+
} catch { return { spawned: false }; }
|
|
1873
|
+
};
|
|
1874
|
+
const dr = await tryDaemonSpawn();
|
|
1875
|
+
if (!dr.spawned) await startEngine(ctx, dir);
|
|
1876
|
+
notify(ctx, "infinity-harness: run armed — background run started. /infinity:halt stops it.", "info");
|
|
1877
|
+
} else {
|
|
1878
|
+
const brief = await briefText(dir);
|
|
1879
|
+
if (!plan.brief) {
|
|
1880
|
+
pi.sendUserMessage(
|
|
1881
|
+
`The human has not said what they want built yet. Ask them, in one short question, and do not start any work or invent a scope until they answer. The harness is NOT running — "/infinity:run" starts it.\n\n${brief}`,
|
|
1882
|
+
{ deliverAs: "followUp" },
|
|
1883
|
+
);
|
|
1884
|
+
} else if (plan.phases[0] === "research") {
|
|
1885
|
+
pi.sendUserMessage(
|
|
1886
|
+
`${brief}\n\nThis is RESEARCH — survey constraints and options first, then validate (infinity_validate) to advance. The harness is NOT running yet. Run "/infinity:run" when you are ready, or answer "yes — start the run now" next time you init.`,
|
|
1887
|
+
{ deliverAs: "followUp" },
|
|
1888
|
+
);
|
|
1889
|
+
} else {
|
|
1890
|
+
pi.sendUserMessage(
|
|
1891
|
+
`${brief}\n\nThe harness is ready but NOT running. Run "/infinity:run" to start, or answer "yes — start the run now" in the wizard next time.`,
|
|
1892
|
+
{ deliverAs: "followUp" },
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1838
1896
|
},
|
|
1839
1897
|
});
|
|
1840
1898
|
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
# Decisions
|
|
2
|
-
|
|
3
|
-
Architectural decisions and the reasoning behind them. Outcomes without reasons are useless to the
|
|
4
|
-
next person — record why, and what it cost.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 1. The gate is the only referee
|
|
9
|
-
|
|
10
|
-
**Context.** An agent asked whether its work is finished will say yes. Repeatedly, and wrongly, over
|
|
11
|
-
a long run.
|
|
12
|
-
|
|
13
|
-
**Decision.** Completion is decided by deterministic checks that are a pure function of the project
|
|
14
|
-
on disk. The agent cannot mark its own work complete, and the extension blocks hand-edits to
|
|
15
|
-
`currentPhase` while the gate is failing.
|
|
16
|
-
|
|
17
|
-
**Cost.** Gates must be cheap enough to run every iteration, and must never fail for a reason the
|
|
18
|
-
agent cannot fix — a gate that deadlocks the loop is worse than no gate. Hence advisory checks:
|
|
19
|
-
an unconfigured lint command is reported, not enforced.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 2. Phases move forward, one step at a time
|
|
24
|
-
|
|
25
|
-
**Decision.** `isValidTransition` permits only the next enabled phase, or re-running the current one.
|
|
26
|
-
Backward movement exists only as an explicit, budgeted rework that records why it happened.
|
|
27
|
-
|
|
28
|
-
**Why.** It removes a decision the model is bad at. Without it, a struggling agent reorders the
|
|
29
|
-
pipeline to reach a phase whose gate it can pass.
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## 3. The plan is submitted whole; omission means deletion
|
|
34
|
-
|
|
35
|
-
**Decision.** `infinity_plan` takes the complete task list, not a patch.
|
|
36
|
-
|
|
37
|
-
**Why.** Incremental edits require the model to track what exists. Over hours it stops being able to:
|
|
38
|
-
it re-adds deleted tasks, forgets others, and the file diverges from reality. A full submission is
|
|
39
|
-
self-correcting, and one unambiguous rule beats a set of merge semantics nobody can predict.
|
|
40
|
-
|
|
41
|
-
**Cost.** Every write carries the whole plan. Capped at 200 tasks, which is far beyond a sensible
|
|
42
|
-
sprint.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 4. `baseRevision` is not a compare-and-swap
|
|
47
|
-
|
|
48
|
-
**Context.** The original write did read → check `baseRevision` → write, with no mutual exclusion,
|
|
49
|
-
and the docs claimed this protected parallel workers. It does not. Two processes that both read
|
|
50
|
-
revision N both pass the check and both write N+1. Measured: 2 lost updates in a 6-way fan-out.
|
|
51
|
-
|
|
52
|
-
**Decision.** `writeTaskList` holds an exclusive lock across the whole read-apply-write and fails
|
|
53
|
-
closed — a write that cannot take the lock is refused, not raced.
|
|
54
|
-
|
|
55
|
-
**Cost.** Plan writes serialise. The critical section is milliseconds, so this is not felt; a
|
|
56
|
-
`LockTimeoutError` is retryable and names the stuck lock.
|
|
57
|
-
|
|
58
|
-
**Also.** The sync lock uses `<path>.ilock`. `proper-lockfile` owns `<path>.lock` and it is a
|
|
59
|
-
directory there too, so sharing the name made a nested async+sync lock deadlock against itself.
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## 5. One implementation, in `src/`
|
|
64
|
-
|
|
65
|
-
**Context.** The extension carried inlined copies of the plan engine and the widget. The tests
|
|
66
|
-
exercised `src/`; the shipped code path never called it. The two drifted, and the drift was
|
|
67
|
-
invisible because the suite was green. The same pattern later reappeared in `rework.ts` and
|
|
68
|
-
`replan.ts`, which kept private plan loaders — and one of them mishandled status aliases, rejecting
|
|
69
|
-
every amendment to a plan that used `"done"`.
|
|
70
|
-
|
|
71
|
-
**Decision.** `src/` is the single implementation. The extension owns pi's lifecycle and nothing
|
|
72
|
-
else. A private `loadFeatureList` is a bug, not a shortcut.
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## 6. Knowing when to stop is the product
|
|
77
|
-
|
|
78
|
-
**Decision.** The loop halts on no-progress, wall clock, iteration count, retry budgets, or a human
|
|
79
|
-
brake — and every stop names its reason.
|
|
80
|
-
|
|
81
|
-
**Why.** Continuing is trivial. The default failure mode of an autonomous loop with a weak model is
|
|
82
|
-
re-running a failing gate against an unchanged tree until the budget is gone. The no-progress
|
|
83
|
-
detector compares a fingerprint of the working tree and the plan; the first failing iteration is a
|
|
84
|
-
baseline and never counts as a stall.
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## 7. Ship vendor-neutral defaults
|
|
89
|
-
|
|
90
|
-
**Context.** The router shipped enabled, with one third-party vendor's model ids hardcoded in every
|
|
91
|
-
slot.
|
|
92
|
-
|
|
93
|
-
**Decision.** Routing is disabled by default and every slot is empty, meaning "use whatever model pi
|
|
94
|
-
is already configured with". Installing an extension must never silently redirect someone's work to
|
|
95
|
-
a model they did not choose.
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## 8. The dashboard cannot perturb the run
|
|
100
|
-
|
|
101
|
-
**Decision.** Read-only, loopback-only, and it does not run the gate — it reports the last recorded
|
|
102
|
-
verdict instead.
|
|
103
|
-
|
|
104
|
-
**Why.** Running lint and the test suite because someone opened a web page is a surprising and
|
|
105
|
-
expensive side effect. And a page rendering model output on a public interface leaks the project;
|
|
106
|
-
binding elsewhere requires an explicit opt-in, and the CSP is tight enough that an escaping slip
|
|
107
|
-
cannot become script execution.
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
Architectural decisions and the reasoning behind them. Outcomes without reasons are useless to the
|
|
4
|
+
next person — record why, and what it cost.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. The gate is the only referee
|
|
9
|
+
|
|
10
|
+
**Context.** An agent asked whether its work is finished will say yes. Repeatedly, and wrongly, over
|
|
11
|
+
a long run.
|
|
12
|
+
|
|
13
|
+
**Decision.** Completion is decided by deterministic checks that are a pure function of the project
|
|
14
|
+
on disk. The agent cannot mark its own work complete, and the extension blocks hand-edits to
|
|
15
|
+
`currentPhase` while the gate is failing.
|
|
16
|
+
|
|
17
|
+
**Cost.** Gates must be cheap enough to run every iteration, and must never fail for a reason the
|
|
18
|
+
agent cannot fix — a gate that deadlocks the loop is worse than no gate. Hence advisory checks:
|
|
19
|
+
an unconfigured lint command is reported, not enforced.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Phases move forward, one step at a time
|
|
24
|
+
|
|
25
|
+
**Decision.** `isValidTransition` permits only the next enabled phase, or re-running the current one.
|
|
26
|
+
Backward movement exists only as an explicit, budgeted rework that records why it happened.
|
|
27
|
+
|
|
28
|
+
**Why.** It removes a decision the model is bad at. Without it, a struggling agent reorders the
|
|
29
|
+
pipeline to reach a phase whose gate it can pass.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 3. The plan is submitted whole; omission means deletion
|
|
34
|
+
|
|
35
|
+
**Decision.** `infinity_plan` takes the complete task list, not a patch.
|
|
36
|
+
|
|
37
|
+
**Why.** Incremental edits require the model to track what exists. Over hours it stops being able to:
|
|
38
|
+
it re-adds deleted tasks, forgets others, and the file diverges from reality. A full submission is
|
|
39
|
+
self-correcting, and one unambiguous rule beats a set of merge semantics nobody can predict.
|
|
40
|
+
|
|
41
|
+
**Cost.** Every write carries the whole plan. Capped at 200 tasks, which is far beyond a sensible
|
|
42
|
+
sprint.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 4. `baseRevision` is not a compare-and-swap
|
|
47
|
+
|
|
48
|
+
**Context.** The original write did read → check `baseRevision` → write, with no mutual exclusion,
|
|
49
|
+
and the docs claimed this protected parallel workers. It does not. Two processes that both read
|
|
50
|
+
revision N both pass the check and both write N+1. Measured: 2 lost updates in a 6-way fan-out.
|
|
51
|
+
|
|
52
|
+
**Decision.** `writeTaskList` holds an exclusive lock across the whole read-apply-write and fails
|
|
53
|
+
closed — a write that cannot take the lock is refused, not raced.
|
|
54
|
+
|
|
55
|
+
**Cost.** Plan writes serialise. The critical section is milliseconds, so this is not felt; a
|
|
56
|
+
`LockTimeoutError` is retryable and names the stuck lock.
|
|
57
|
+
|
|
58
|
+
**Also.** The sync lock uses `<path>.ilock`. `proper-lockfile` owns `<path>.lock` and it is a
|
|
59
|
+
directory there too, so sharing the name made a nested async+sync lock deadlock against itself.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 5. One implementation, in `src/`
|
|
64
|
+
|
|
65
|
+
**Context.** The extension carried inlined copies of the plan engine and the widget. The tests
|
|
66
|
+
exercised `src/`; the shipped code path never called it. The two drifted, and the drift was
|
|
67
|
+
invisible because the suite was green. The same pattern later reappeared in `rework.ts` and
|
|
68
|
+
`replan.ts`, which kept private plan loaders — and one of them mishandled status aliases, rejecting
|
|
69
|
+
every amendment to a plan that used `"done"`.
|
|
70
|
+
|
|
71
|
+
**Decision.** `src/` is the single implementation. The extension owns pi's lifecycle and nothing
|
|
72
|
+
else. A private `loadFeatureList` is a bug, not a shortcut.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 6. Knowing when to stop is the product
|
|
77
|
+
|
|
78
|
+
**Decision.** The loop halts on no-progress, wall clock, iteration count, retry budgets, or a human
|
|
79
|
+
brake — and every stop names its reason.
|
|
80
|
+
|
|
81
|
+
**Why.** Continuing is trivial. The default failure mode of an autonomous loop with a weak model is
|
|
82
|
+
re-running a failing gate against an unchanged tree until the budget is gone. The no-progress
|
|
83
|
+
detector compares a fingerprint of the working tree and the plan; the first failing iteration is a
|
|
84
|
+
baseline and never counts as a stall.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 7. Ship vendor-neutral defaults
|
|
89
|
+
|
|
90
|
+
**Context.** The router shipped enabled, with one third-party vendor's model ids hardcoded in every
|
|
91
|
+
slot.
|
|
92
|
+
|
|
93
|
+
**Decision.** Routing is disabled by default and every slot is empty, meaning "use whatever model pi
|
|
94
|
+
is already configured with". Installing an extension must never silently redirect someone's work to
|
|
95
|
+
a model they did not choose.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 8. The dashboard cannot perturb the run
|
|
100
|
+
|
|
101
|
+
**Decision.** Read-only, loopback-only, and it does not run the gate — it reports the last recorded
|
|
102
|
+
verdict instead.
|
|
103
|
+
|
|
104
|
+
**Why.** Running lint and the test suite because someone opened a web page is a surprising and
|
|
105
|
+
expensive side effect. And a page rendering model output on a public interface leaks the project;
|
|
106
|
+
binding elsewhere requires an explicit opt-in, and the CSP is tight enough that an escaping slip
|
|
107
|
+
cannot become script execution.
|
|
108
108
|
|
|
109
109
|
---
|
|
110
110
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinity-harness",
|
|
3
|
-
"version": "2.8.
|
|
4
|
-
"description": "A pi agent extension that runs a gated build pipeline unattended
|
|
3
|
+
"version": "2.8.2",
|
|
4
|
+
"description": "A pi agent extension that runs a gated build pipeline unattended — enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
package/src/ui/wizard.ts
CHANGED
|
@@ -66,7 +66,7 @@ export type WizardOptions = {
|
|
|
66
66
|
|
|
67
67
|
export type WizardResult =
|
|
68
68
|
| { cancelled: true }
|
|
69
|
-
| { cancelled: false; plan: IntakePlan; answers: IntakeAnswers };
|
|
69
|
+
| { cancelled: false; plan: IntakePlan; answers: IntakeAnswers; launchNow?: boolean };
|
|
70
70
|
|
|
71
71
|
const CONFIRM = "start with these settings";
|
|
72
72
|
const RESTART = "change something";
|
|
@@ -267,7 +267,7 @@ export async function runIntakeWizard(options: WizardOptions): Promise<WizardRes
|
|
|
267
267
|
const answers: IntakeAnswers = { workflow, researchDepth, brief, handoff, display, router: modelsAnswer.router, parallelAt, maxWorkers };
|
|
268
268
|
const plan = planIntake(answers);
|
|
269
269
|
|
|
270
|
-
if (options.skipConfirm) return { cancelled: false, plan, answers };
|
|
270
|
+
if (options.skipConfirm) return { cancelled: false, plan, answers, launchNow: false };
|
|
271
271
|
|
|
272
272
|
const body = [plan.summary, ...(plan.warnings.length ? ["", ...plan.warnings.map((w) => `! ${w}`)] : [])].join(
|
|
273
273
|
"\n",
|
|
@@ -277,7 +277,16 @@ export async function runIntakeWizard(options: WizardOptions): Promise<WizardRes
|
|
|
277
277
|
const confirm = await prompt.select("Ready?", [CONFIRM, RESTART, CANCEL]);
|
|
278
278
|
if (confirm === undefined || confirm === CANCEL) return { cancelled: true };
|
|
279
279
|
if (confirm === RESTART) continue;
|
|
280
|
-
|
|
280
|
+
// Last step: ask whether to arm and start immediately (opt-in).
|
|
281
|
+
// After init the harness is never auto-started — only /infinity:run (or
|
|
282
|
+
// "yes" here) arms it. Older tests/E2E scripts that do not answer this
|
|
283
|
+
// are treated as "later" so they keep passing.
|
|
284
|
+
const LAUNCH_NOW = "yes — start the run now";
|
|
285
|
+
const LAUNCH_LATER = "no — I'll run /infinity:run when ready";
|
|
286
|
+
const launchPick = await prompt.select("Start the run now?", [LAUNCH_NOW, LAUNCH_LATER]);
|
|
287
|
+
let launchNow = false;
|
|
288
|
+
if (launchPick !== undefined) launchNow = launchPick === LAUNCH_NOW;
|
|
289
|
+
return { cancelled: false, plan, answers, launchNow };
|
|
281
290
|
}
|
|
282
291
|
}
|
|
283
292
|
|