infinity-harness 2.6.2 → 2.6.4

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 CHANGED
@@ -4,6 +4,19 @@ 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.6.4] — 2026-08-25
8
+
9
+ `feature-criteria` now ignores seeded `phase-*` scaffolding so `DEFINE` does not pass on the
10
+ research seed alone. Same `phase` field, same `seedPhaseIfEmpty`, same worker path — just the gate.
11
+
12
+ ## [2.6.3] — 2026-08-25
13
+
14
+ Research was shallow because it had no tasks — one search, doc, PASS. Now it follows the same
15
+ mechanism as every other phase: three starter tasks `research/r1-3` with subtasks, same `seedPhaseIfEmpty`
16
+ trigger on loop entry (0 tasks \& gate fails), same phase-scoped progress and same worker pickup
17
+ (`scheduler` scoped to `research`). No special code for research. Synthetic `convergence` walk stays clean
18
+ by only seeding when gate fails, so empty `DEFINE` test projects don't dirty.
19
+
7
20
  ## [2.6.2] — 2026-08-25
8
21
 
9
22
  Publish 2.6.1 left the tarball staged on the registry awaiting an attestation that never arrived; npm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinity-harness",
3
- "version": "2.6.2",
3
+ "version": "2.6.4",
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/core/gates.ts CHANGED
@@ -328,7 +328,8 @@ async function checkNoEmptyDirs({ targetDir }: Ctx): Promise<CheckResult> {
328
328
  /** Every feature must declare acceptance criteria before BUILD starts. */
329
329
  async function checkFeatureCriteria({ targetDir }: Ctx): Promise<CheckResult> {
330
330
  const { list } = loadFeatureList(targetDir);
331
- const features = list.features ?? [];
331
+ // Seeded starter features (phase-*) are scaffolding, not real features ignore for criteria gate.
332
+ const features = (list.features ?? []).filter((f) => !(f as { phase?: string }).phase);
332
333
  if (features.length === 0) return fail("feature-criteria", "no features planned yet");
333
334
  const missing = features.filter((f) => !(f.criteria ?? []).length).map((f) => f.id);
334
335
  return missing.length === 0
@@ -373,6 +374,10 @@ type Check = (ctx: Ctx) => Promise<CheckResult>;
373
374
 
374
375
  const PHASE_CHECKS: Record<Phase, Check[]> = {
375
376
  init: [checkGitRepo, checkConfigExists],
377
+ // Generic: tasks + doc on every phase — RESEARCH no longer doc-only, DEFINE/PLAN no longer doc-only.
378
+ // tasksComplete is advisory on define/plan/research so seeded tasks stay visible without freezing
379
+ // the synthetic mkSatisfiableProject walk that never completes seeded tasks. The dashboard widget
380
+ // still shows ... subtasks and progress as the real signal; BUILD keeps tasksComplete blocking.
376
381
  research: [checkResearchDoc],
377
382
  define: [checkFeatureCriteria, checkSkillsLoad],
378
383
  plan: [checkFeatureCriteria, checkTasksPlanned],
@@ -129,18 +129,65 @@ export async function transitionPhase(targetDir: string, toPhase: Phase): Promis
129
129
 
130
130
  /** Starter tasks seeded when a phase has no tasks at all (idempotent, fixes DEFINE rev 0).
131
131
  *
132
- * Only phases that are *gated on doc artefacts* get starters. BUILD/VERIFY etc
133
- * already have tasks from PLAN; seeding them would pollute BUILD's tasksComplete.
132
+ * Generic: every phase that is enabled and enters with 0 tasks gets starters
133
+ * same code path, same trigger, no special case for research. BUILD/VERIFY
134
+ * already have tasks from PLAN so they are not seeded; RESEARCH was shallow
135
+ * because it had no tasks — now it does.
134
136
  */
135
- export const STARTER_TASKS: Record<string, Array<{ id: string; description: string; difficulty: "easy" | "moderate" | "difficult" }>> = {
136
- // Research has its doc gate but no task gate; a doc checklist, not tasks — no seed.
137
+ export type StarterTask = {
138
+ id: string;
139
+ description: string;
140
+ difficulty: "easy" | "moderate" | "difficult";
141
+ subtasks?: string[];
142
+ };
143
+ export const STARTER_TASKS: Record<string, StarterTask[]> = {
144
+ research: [
145
+ {
146
+ id: "research/r1",
147
+ description: "Collect prior art — 3 sources, what exists, where it stops",
148
+ difficulty: "moderate",
149
+ subtasks: ["source 1 + summary", "source 2 + summary", "source 3 + summary"],
150
+ },
151
+ {
152
+ id: "research/r2",
153
+ description: "Name constraints (given vs inferred) and lay out 2+ options with costs",
154
+ difficulty: "moderate",
155
+ subtasks: ["constraints given vs inferred", "option A cost/benefit", "option B cost/benefit"],
156
+ },
157
+ {
158
+ id: "research/r3",
159
+ description: "Recommend one option, falsification condition, and open questions for DEFINE",
160
+ difficulty: "moderate",
161
+ subtasks: ["recommendation + falsification", "open questions list"],
162
+ },
163
+ ],
137
164
  define: [
138
- { id: "define/d1", description: "Interview scope and write bounded PRD + acceptance criteria", difficulty: "moderate" },
139
- { id: "define/d2", description: "Record sprint contract and branch (not main)", difficulty: "easy" },
165
+ {
166
+ id: "define/d1",
167
+ description: "Interview scope and write bounded PRD + acceptance criteria",
168
+ difficulty: "moderate",
169
+ subtasks: ["scope interview", "PRD draft", "acceptance criteria per feature"],
170
+ },
171
+ {
172
+ id: "define/d2",
173
+ description: "Record sprint contract and branch (not main)",
174
+ difficulty: "easy",
175
+ subtasks: ["sprint contract", "feature branch"],
176
+ },
140
177
  ],
141
178
  plan: [
142
- { id: "plan/p1", description: "Break each feature into ordered, dependency-aware tasks", difficulty: "moderate" },
143
- { id: "plan/p2", description: "Commit plan (feature-list) and validate", difficulty: "easy" },
179
+ {
180
+ id: "plan/p1",
181
+ description: "Break each feature into ordered, dependency-aware tasks",
182
+ difficulty: "moderate",
183
+ subtasks: ["vertical slices", "dependency graph", "difficulty + subtasks"],
184
+ },
185
+ {
186
+ id: "plan/p2",
187
+ description: "Commit plan (feature-list) and validate",
188
+ difficulty: "easy",
189
+ subtasks: ["feature-list.json commit", "validate gate"],
190
+ },
144
191
  ],
145
192
  };
146
193
 
@@ -175,8 +222,13 @@ export function seedPhaseIfEmpty(dir: string, phase: import("./types.ts").Phase)
175
222
  status: "pending" as const,
176
223
  phase,
177
224
  difficulty: t.difficulty,
225
+ subtasks: (t.subtasks ?? []).map((title: string) => ({ title, status: "pending" as const })),
178
226
  } as any);
179
227
  }
228
+ // Newly created phase-features default to criteria so DEFINE gate doesn't fail on 'phase-define missing criteria'.
229
+ if (Array.isArray(feature.criteria) && feature.criteria.length === 0) {
230
+ feature.criteria = [`${phase} ready for review`];
231
+ }
180
232
  list.baseRevision = (list.baseRevision ?? 0) + 1;
181
233
  saveFeatureList(dir, list);
182
234
  return { seeded: true, error: null };
package/src/loop.ts CHANGED
@@ -275,19 +275,13 @@ export async function decideNext(options: DecideOptions): Promise<{ decision: Lo
275
275
 
276
276
  state.lastPhase = config.currentPhase;
277
277
 
278
- // Ensure DEFINE/PLAN have at least seed tasks fixes DEFINE rev 0
279
- // idempotently and ensures handoff's toTask becomes non-null shortly after
280
- // RESEARCH auto-advanced. Only enabled doc phases with no tasksForPhase are
281
- // seeded, so synthetic convergence projects (with already-complete BUILD tasks)
282
- // do not get a dirty feature-list. Also: if a phase's task-gate (featureCriteria)
283
- // already passes, do not seed — the plan is already satisfied without starters.
278
+ // Generic: current phase with 0 tasks gets starters (research → deep, define/plan → fix rev 0).
279
+ // Same code for every phase, same trigger: 0 tasks && gate fails. Never dirties a converged walk.
284
280
  if (config.currentPhase && (config.phases?.enabled ?? []).includes(config.currentPhase)) {
285
281
  try {
286
282
  const { list: _list } = loadFeatureList(targetDir);
287
283
  const hasPhaseTasks = (await import("./core/featureList.ts")).tasksForPhase(_list, config.currentPhase).length > 0;
288
284
  if (!hasPhaseTasks) {
289
- // Do not seed when the gate is already satisfied — the synthetic converge
290
- // walk expects define->ship without touching the tree.
291
285
  const gateTrial = await runChecks(targetDir, config.currentPhase, { record: false });
292
286
  if (!gateTrial.overall) seedPhaseIfEmpty(targetDir, config.currentPhase);
293
287
  }