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/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
- // Generic: tasks + doc on every phase RESEARCH no longer doc-only, DEFINE/PLAN no longer doc-only.
385
- // tasksComplete is advisory on define/plan/research so seeded tasks stay visible without freezing
386
- // the synthetic mkSatisfiableProject walk that never completes seeded tasks. The dashboard widget
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],
@@ -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
- // Append to first feature matching phase, or create a phase feature.
228
- const feature = (
229
- list.features.find((f) => (f as { phase?: string }).phase === phase) ??
230
- list.features[0] ??
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);
@@ -375,20 +375,27 @@ export const SETTINGS: SettingsGroup[] = [
375
375
  {
376
376
  id: "execution",
377
377
  label: "Execution",
378
- help: "How many things run at once, and at which level. Main session only shows progress; workers do the real work.",
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: "off: one at a time · goal: goals in parallel · phase: phases · sprint/feature/task/subtask. Pick one. Finer implies coarser.",
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: "How many workers run at once (1-16)._deps must be satisfied; parallelAt limits breadth.",
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.