infinity-harness 2.6.5 → 2.7.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 +90 -0
- package/README.md +68 -15
- package/extensions/infinity-harness/index.ts +416 -50
- package/package.json +1 -1
- package/src/core/config.ts +1 -1
- package/src/core/gates.ts +6 -5
- package/src/core/phases.ts +13 -6
- package/src/core/settings.ts +10 -3
- package/src/core/types.ts +16 -0
- package/src/exec/piWorker.ts +707 -0
- package/src/intake.ts +4 -1
- package/src/loop.ts +54 -38
- package/src/remote.ts +27 -7
- package/src/scheduler.ts +10 -3
- package/src/supervisor.ts +955 -0
- package/src/ui/dashboard.ts +211 -6
- package/src/ui/widget.ts +144 -7
- package/src/ui/wizard.ts +43 -7
package/src/core/gates.ts
CHANGED
|
@@ -336,8 +336,10 @@ async function checkNoEmptyDirs({ targetDir }: Ctx): Promise<CheckResult> {
|
|
|
336
336
|
async function checkFeatureCriteria({ targetDir }: Ctx): Promise<CheckResult> {
|
|
337
337
|
const { list } = loadFeatureList(targetDir);
|
|
338
338
|
// Seeded starter features (phase-*) are scaffolding, not real features — ignore for criteria gate.
|
|
339
|
+
// Also: if no non-scaffold features exist yet, DEFINE has not been planned and must not PASS on a
|
|
340
|
+
// single phase-* dummy. Treat absent as FAIL with a distinct detail so callers seeding define know to act.
|
|
339
341
|
const features = (list.features ?? []).filter((f) => !(f as { phase?: string }).phase);
|
|
340
|
-
if (features.length === 0) return fail("feature-criteria", "no features planned yet");
|
|
342
|
+
if (features.length === 0) return fail("feature-criteria", "no real features planned yet — run DEFINE");
|
|
341
343
|
const missing = features.filter((f) => !(f.criteria ?? []).length).map((f) => f.id);
|
|
342
344
|
return missing.length === 0
|
|
343
345
|
? pass("feature-criteria", `${features.length} feature(s) have criteria`)
|
|
@@ -381,10 +383,9 @@ type Check = (ctx: Ctx) => Promise<CheckResult>;
|
|
|
381
383
|
|
|
382
384
|
const PHASE_CHECKS: Record<Phase, Check[]> = {
|
|
383
385
|
init: [checkGitRepo, checkConfigExists],
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
// still shows ... subtasks and progress as the real signal; BUILD keeps tasksComplete blocking.
|
|
386
|
+
// tasksComplete is advisory on seeded-phase gates only when seeded work exists — otherwise the
|
|
387
|
+
// converge walk would freeze on the seeded define/plan tasks that test never completes. Once BUILD
|
|
388
|
+
// has tasksComplete remains blocking.
|
|
388
389
|
research: [checkResearchDoc],
|
|
389
390
|
define: [checkFeatureCriteria, checkSkillsLoad],
|
|
390
391
|
plan: [checkFeatureCriteria, checkTasksPlanned],
|
package/src/core/phases.ts
CHANGED
|
@@ -217,6 +217,15 @@ function startersForPhase(dir: string, phase: string): StarterTask[] {
|
|
|
217
217
|
return STARTER_TASKS_BY_DEPTH[DEFAULT_RESEARCH_DEPTH] ?? [];
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
export function ensurePhaseSeeded(dir: string, phase: import("./types.ts").Phase): boolean {
|
|
221
|
+
try {
|
|
222
|
+
const { list } = loadFeatureList(dir);
|
|
223
|
+
if (tasksForPhase(list, phase).length > 0) return false;
|
|
224
|
+
const r = seedPhaseIfEmpty(dir, phase);
|
|
225
|
+
return r.seeded;
|
|
226
|
+
} catch { return false; }
|
|
227
|
+
}
|
|
228
|
+
|
|
220
229
|
export function seedPhaseIfEmpty(dir: string, phase: import("./types.ts").Phase): { seeded: boolean; error: string | null } {
|
|
221
230
|
const seeded = startersForPhase(dir, phase);
|
|
222
231
|
if (seeded.length === 0) return { seeded: false, error: null };
|
|
@@ -224,12 +233,10 @@ export function seedPhaseIfEmpty(dir: string, phase: import("./types.ts").Phase)
|
|
|
224
233
|
const { list } = loadFeatureList(dir);
|
|
225
234
|
const existing = tasksForPhase(list, phase);
|
|
226
235
|
if (existing.length > 0) return { seeded: false, error: null };
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
({ id: `phase-${phase}`, name: phase.toUpperCase(), tasks: [] } as unknown as typeof list.features[number])
|
|
232
|
-
);
|
|
236
|
+
// Each phase gets its own feature (phase-<name>) so per-phase tabs/groups isolate correctly.
|
|
237
|
+
// Never append a "define" task to a "research" feature — the feature.phase is the grouping key.
|
|
238
|
+
const match = list.features.find((f) => (f as { phase?: string }).phase === phase);
|
|
239
|
+
const feature: typeof list.features[number] = match ?? ({ id: `phase-${phase}`, name: phase.toUpperCase(), tasks: [] } as unknown as typeof list.features[number]);
|
|
233
240
|
if (!list.features.includes(feature as any)) {
|
|
234
241
|
(feature as { phase?: string }).phase = phase;
|
|
235
242
|
list.features.push(feature as any);
|
package/src/core/settings.ts
CHANGED
|
@@ -375,20 +375,27 @@ export const SETTINGS: SettingsGroup[] = [
|
|
|
375
375
|
{
|
|
376
376
|
id: "execution",
|
|
377
377
|
label: "Execution",
|
|
378
|
-
help: "
|
|
378
|
+
help: "Who does the work, how many at once, and at which level. The session you are typing in is a control panel; background workers do the real work.",
|
|
379
379
|
settings: [
|
|
380
|
+
{
|
|
381
|
+
path: "execution.engine",
|
|
382
|
+
file: "config",
|
|
383
|
+
label: "Where work runs",
|
|
384
|
+
help: "background: every unit of work runs in its own pi session with its own model, and this session spends no tokens on it (recommended) · main-session: the old behaviour, everything runs here on your model.",
|
|
385
|
+
type: { kind: "choice", choices: ["background", "main-session"] },
|
|
386
|
+
},
|
|
380
387
|
{
|
|
381
388
|
path: "execution.parallelAt",
|
|
382
389
|
file: "config",
|
|
383
390
|
label: "Parallel at",
|
|
384
|
-
help: "
|
|
391
|
+
help: "Legacy engine only. The background engine runs one unit at a time, in its own session, at the level you chose for session handoff.",
|
|
385
392
|
type: { kind: "choice", choices: ["off", "goal", "phase", "sprint", "feature", "task", "subtask"] },
|
|
386
393
|
},
|
|
387
394
|
{
|
|
388
395
|
path: "execution.maxWorkers",
|
|
389
396
|
file: "config",
|
|
390
397
|
label: "Max workers",
|
|
391
|
-
help: "
|
|
398
|
+
help: "Legacy engine only. The background engine runs one worker; parallel background workers are not wired up yet.",
|
|
392
399
|
type: { kind: "number", min: 1, max: 16 },
|
|
393
400
|
},
|
|
394
401
|
],
|
package/src/core/types.ts
CHANGED
|
@@ -168,12 +168,28 @@ export type RetryLevel = (typeof RETRY_LEVELS)[number];
|
|
|
168
168
|
export type HandoffGranularity = "off" | "goal" | "phase" | "sprint" | "feature" | "task" | "subtask";
|
|
169
169
|
|
|
170
170
|
export type ExecutionPolicy = {
|
|
171
|
+
/**
|
|
172
|
+
* Who actually does the work.
|
|
173
|
+
*
|
|
174
|
+
* `background` — the default, and the reason this harness exists. Each unit
|
|
175
|
+
* of work runs in its own `pi` process with its own model and its own
|
|
176
|
+
* context window; the session the human is typing into stays a control
|
|
177
|
+
* panel and spends no tokens on the run.
|
|
178
|
+
*
|
|
179
|
+
* `main-session` — the pre-2.7 behaviour, kept because a run on a machine
|
|
180
|
+
* that cannot spawn a second pi still has to be able to work. Everything
|
|
181
|
+
* happens in the human's session, on the human's model.
|
|
182
|
+
*/
|
|
183
|
+
engine: ExecutionEngine;
|
|
171
184
|
/** Level at which parallel work is allowed. `off` = one task at a time. */
|
|
172
185
|
parallelAt: HandoffGranularity;
|
|
173
186
|
/** Max parallel workers (1..16). Guarded by lock and budget. */
|
|
174
187
|
maxWorkers: number;
|
|
175
188
|
};
|
|
176
189
|
|
|
190
|
+
export type ExecutionEngine = "background" | "main-session";
|
|
191
|
+
export const EXECUTION_ENGINES: readonly ExecutionEngine[] = ["background", "main-session"] as const;
|
|
192
|
+
|
|
177
193
|
export type SessionPolicy = {
|
|
178
194
|
/**
|
|
179
195
|
* When to hand off to a fresh session.
|