ai-fob 1.7.0 → 1.7.1

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.
@@ -27,14 +27,47 @@ export type ReconcileAction = {
27
27
  reason: string;
28
28
  };
29
29
 
30
+ /**
31
+ * Per-step resume verdict — contract with
32
+ * `references/resume-reconciliation-table.md` §1/§2/§4/§5. The recipe
33
+ * (`prompts/build-phase-pi.md`) gates STATE.md mutation on the
34
+ * `requires_confirmation` / `reset` cases.
35
+ */
36
+ export type StepVerdict =
37
+ | "valid"
38
+ | "valid_lenient"
39
+ | "stale"
40
+ | "missing"
41
+ | "reset"
42
+ | "requires_confirmation";
43
+
44
+ export type StepReconcile = {
45
+ n: number; // 0..6
46
+ marker: "[ ]" | "[~]" | "[x]";
47
+ artifact: string; // canonical artifact path the matrix names
48
+ verdict: StepVerdict;
49
+ reason?: string; // human-readable; surfaced in renderResult + recipe confirmation prompt
50
+ };
51
+
30
52
  export type ReconcileResult = {
31
53
  scan: ArtifactScan;
32
- entryStep: number; // 0..6
54
+ entryStep: number; // 0..6 — preserved for legacy consumers (mirrors details.resumeAt)
33
55
  actions: ReconcileAction[];
34
56
  specialCases: string[];
35
57
  table: string; // expanded cross-reference table (markdown)
58
+ // Additive: per-step verdict array and deterministic resume-step integer.
59
+ steps: StepReconcile[];
60
+ resumeAt: number; // 0..6 — first step whose verdict is NOT in {valid, valid_lenient}
36
61
  };
37
62
 
63
+ // Internal classification — separates on-disk absence from content-check
64
+ // failure so decideVerdict() can map (marker, classification) ⇒ StepVerdict.
65
+ type ArtifactClassification =
66
+ | { kind: "valid" }
67
+ | { kind: "valid_lenient"; reason: string }
68
+ | { kind: "stale"; reason: string }
69
+ | { kind: "missing"; reason: string };
70
+
38
71
  // Canonical 4-value Step 5 result vocabulary — see
39
72
  // .pi/skills/phase-build-workflow/references/result-vocabulary.md. Mirrors
40
73
  // state-md.ts's STEP5_COMPLETE_RESULTS exactly (Coordination Note 6 of the
@@ -229,13 +262,28 @@ export async function reconcile(
229
262
  const scan = await scanArtifacts(cwd, task, phase, onProgress);
230
263
  const stateMarkers = readStepMarkers(stateText, task, phase);
231
264
  const actions: ReconcileAction[] = [];
265
+ const steps: StepReconcile[] = [];
232
266
 
233
267
  // Walk steps 0..6 deciding per matrix in resume-reconciliation-table.md §2.
234
268
  for (let step = 0; step <= 6; step++) {
235
269
  const marker = stateMarkers[step] ?? " ";
236
270
  const artifact = primaryArtifactStatus(scan, step);
237
- const verdict = decide(marker, artifact);
238
- actions.push({ step, action: verdict.action, reason: verdict.reason });
271
+ const decision = decide(marker, artifact);
272
+ actions.push({ step, action: decision.action, reason: decision.reason });
273
+
274
+ // Per-step verdict (additive, recipe-facing). Classification keeps the
275
+ // file-on-disk check (missing) separate from the content-check-miss case
276
+ // (valid_lenient / stale), so the recipe can gate STATE.md mutation on
277
+ // requires_confirmation/reset without blindly cascading a reset.
278
+ const classification = classifyStep(scan, step);
279
+ const verdict = decideVerdict(marker, classification);
280
+ steps.push({
281
+ n: step,
282
+ marker: `[${marker}]` as "[ ]" | "[~]" | "[x]",
283
+ artifact: primaryArtifactName(step),
284
+ verdict: verdict.verdict,
285
+ reason: verdict.reason,
286
+ });
239
287
  }
240
288
 
241
289
  // Special cases §3.1–3.4 — fold into actions/specialCases list.
@@ -264,14 +312,27 @@ export async function reconcile(
264
312
  specialCases.push("Interrupted Step 4 (§3.3): post-build-sha pending.");
265
313
  }
266
314
 
267
- // Entry step — first step whose final action is "run" or "reset".
268
- let entryStep = 6;
269
- for (const a of actions) {
270
- if (a.action === "run" || a.action === "reset") {
271
- entryStep = a.step;
315
+ // resumeAt — first step whose verdict is NOT in {valid, valid_lenient}.
316
+ // If every step is valid/valid_lenient, resumeAt = lastCompletedStep + 1
317
+ // (preserves the "everything is fine, advance" semantic). entryStep mirrors
318
+ // resumeAt so legacy consumers (index.ts renderer, recipe fallback) keep
319
+ // reading the same integer.
320
+ let resumeAt = -1;
321
+ for (const s of steps) {
322
+ if (s.verdict !== "valid" && s.verdict !== "valid_lenient") {
323
+ resumeAt = s.n;
272
324
  break;
273
325
  }
274
326
  }
327
+ if (resumeAt < 0) {
328
+ // All steps valid/valid_lenient — find the last [x] and advance.
329
+ let lastCompleted = -1;
330
+ for (const s of steps) {
331
+ if (s.marker === "[x]") lastCompleted = s.n;
332
+ }
333
+ resumeAt = Math.min(6, lastCompleted + 1);
334
+ }
335
+ const entryStep = resumeAt;
275
336
 
276
337
  const tableHead =
277
338
  "| step | marker | artifact | action | reason |\n| ---- | ------ | -------- | ------ | ------ |";
@@ -289,6 +350,8 @@ export async function reconcile(
289
350
  actions,
290
351
  specialCases,
291
352
  table: `${tableHead}\n${tableRows}`,
353
+ steps,
354
+ resumeAt,
292
355
  };
293
356
  }
294
357
 
@@ -309,8 +372,17 @@ function primaryArtifactStatus(scan: ArtifactScan, step: number): ArtifactStatus
309
372
  if (stepEntries.some((e) => e.status === "invalid")) return "invalid";
310
373
  return "missing";
311
374
  }
375
+ // Step 1 has two canonical entries (`explorer_findings.md` required +
376
+ // `docs_research.md` optional companion). Mirror Step 4's OR-pattern so a
377
+ // present-on-disk explorer artifact reports Step 1 as "present" even if the
378
+ // optional docs companion is absent (kanban-core regression facet).
379
+ if (step === 1) {
380
+ const stepEntries = scan.entries.filter((e) => e.step === 1);
381
+ if (stepEntries.some((e) => e.status === "present")) return "present";
382
+ if (stepEntries.some((e) => e.status === "invalid")) return "invalid";
383
+ return "missing";
384
+ }
312
385
  const PRIMARY: Record<number, string> = {
313
- 1: "explorer_findings.md",
314
386
  2: "plan_V1.md",
315
387
  3: "plan_validation_report.md",
316
388
  5: "build_validation_report.md",
@@ -320,6 +392,82 @@ function primaryArtifactStatus(scan: ArtifactScan, step: number): ArtifactStatus
320
392
  return e?.status ?? "missing";
321
393
  }
322
394
 
395
+ /** Canonical artifact name surfaced in details.steps[].artifact. */
396
+ function primaryArtifactName(step: number): string {
397
+ if (step === 0) return "(none — Step 0 has no artifact)";
398
+ const NAMES: Record<number, string> = {
399
+ 1: "explorer_findings.md",
400
+ 2: "plan_V1.md",
401
+ 3: "plan_validation_report.md",
402
+ 4: "build_report.md",
403
+ 5: "build_validation_report.md",
404
+ 6: "phase_completion_report.md",
405
+ };
406
+ return NAMES[step] ?? "";
407
+ }
408
+
409
+ // Map ArtifactStatus → ArtifactClassification. Bug-fix behaviour: a
410
+ // present-on-disk artifact that failed a content check is `valid_lenient`
411
+ // (non-fatal); escalated to `stale` only on STRUCTURAL frontmatter mismatch.
412
+ function classifyStep(scan: ArtifactScan, step: number): ArtifactClassification {
413
+ if (step === 0) return { kind: "valid" };
414
+ const status = primaryArtifactStatus(scan, step);
415
+ if (status === "present") return { kind: "valid" };
416
+ // Find a representative entry to source the reason string and decide
417
+ // lenient vs stale.
418
+ const stepEntries = scan.entries.filter((e) => e.step === step);
419
+ if (status === "missing") {
420
+ const reason =
421
+ stepEntries.find((e) => e.status === "missing")?.reason ??
422
+ `${primaryArtifactName(step)} not present on disk`;
423
+ return { kind: "missing", reason };
424
+ }
425
+ // status === "invalid" — file exists, content check missed.
426
+ const invalidEntry = stepEntries.find((e) => e.status === "invalid");
427
+ const reason = invalidEntry?.reason ?? "content check failed";
428
+ // Heuristic: structural frontmatter mismatches escalate to `stale`
429
+ // (requires_confirmation); coarse line-count or schema-version drift is
430
+ // `valid_lenient` (non-fatal, no reset).
431
+ const structural =
432
+ /unparseable frontmatter|missing frontmatter|missing checks-passed|not in pass/.test(reason);
433
+ if (structural) return { kind: "stale", reason };
434
+ return { kind: "valid_lenient", reason };
435
+ }
436
+
437
+ // Cross-reference matrix from references/resume-reconciliation-table.md §2.
438
+ // Validity is DATA — never throws.
439
+ function decideVerdict(
440
+ marker: " " | "~" | "x",
441
+ status: ArtifactClassification,
442
+ ): { verdict: StepVerdict; reason?: string } {
443
+ if (marker === "x") {
444
+ // [x] + valid ⇒ valid (skip)
445
+ // [x] + valid_lenient ⇒ valid_lenient (continue past; no reset)
446
+ // [x] + stale ⇒ requires_confirmation (gated reset)
447
+ // [x] + missing ⇒ requires_confirmation (gated reset)
448
+ if (status.kind === "valid") return { verdict: "valid" };
449
+ if (status.kind === "valid_lenient")
450
+ return { verdict: "valid_lenient", reason: status.reason };
451
+ return { verdict: "requires_confirmation", reason: status.reason };
452
+ }
453
+ if (marker === "~") {
454
+ // [~] markers are always re-runnable; classify as `reset` so the recipe
455
+ // can re-run without prompting for in-progress crashes.
456
+ if (status.kind === "valid")
457
+ return { verdict: "reset", reason: "crash before STATE.md updated; re-run" };
458
+ return { verdict: "reset", reason: status.reason ?? "mid-execution interrupt" };
459
+ }
460
+ // marker === " ": untouched semantic — this row is not the bug. If the
461
+ // artifact exists, mark valid (recipe promotes via existing actions[]);
462
+ // otherwise this is the resume entry point.
463
+ if (status.kind === "valid") return { verdict: "valid" };
464
+ if (status.kind === "valid_lenient")
465
+ return { verdict: "valid_lenient", reason: status.reason };
466
+ if (status.kind === "stale")
467
+ return { verdict: "stale", reason: status.reason };
468
+ return { verdict: "missing", reason: status.reason };
469
+ }
470
+
323
471
  function decide(
324
472
  marker: " " | "~" | "x",
325
473
  status: ArtifactStatus,
@@ -42,11 +42,25 @@ Call the `state` tool three times with arguments shaped like:
42
42
  { "operation": "reconcile", "task": "<slug>", "phase": $2 }
43
43
  ```
44
44
 
45
- After `reconcile` returns, read `details.reconcile.entryStep` (0..6) as the
46
- starting step. If `entryStep > 0`, present `details.reconcile.table` to the
47
- user as a banner (echo $@ for context) and resume at `entryStep`. Do NOT
48
- re-execute any step `< entryStep`. The reconciler is the single source of
49
- truth for resume.
45
+ After `reconcile` returns, branch on `details.reconcile` as follows:
46
+
47
+ - Read `details.reconcile.resumeAt` (preferred) or `details.reconcile.entryStep`
48
+ (legacy fallback) as the starting step. Both fields are integers 0..6 and
49
+ (when both present) MUST agree.
50
+ - Inspect `details.reconcile.steps[]`. If ANY entry has
51
+ `verdict ∈ {requires_confirmation, reset}`, STOP and present the per-step
52
+ verdict table (step number, marker, artifact, verdict, reason) verbatim to
53
+ the user, plus `details.reconcile.table` for context (echo $@ as a banner).
54
+ For each `requires_confirmation` entry, ask the user to confirm or skip the
55
+ proposed reset. ONLY AFTER explicit confirmation may you mutate STATE.md.
56
+ - If all verdicts ∈ `{valid, valid_lenient}`, present `details.reconcile.table`
57
+ as a banner (echo $@) and resume directly at `resumeAt` without prompting.
58
+ `valid_lenient` means the artifact exists on disk but a coarse content check
59
+ downgraded — its `reason` has already been surfaced via `renderResult`; no
60
+ recipe action is required.
61
+ - Do NOT re-execute any step `< resumeAt`. The reconciler is the single source
62
+ of truth for resume DETERMINATION; the recipe is the gatekeeper for STATE.md
63
+ MUTATION.
50
64
 
51
65
  When Step 0 is complete:
52
66
 
@@ -859,6 +873,10 @@ summary as your final assistant message. Include:
859
873
  Do NOT:
860
874
 
861
875
  - Skip Step 0 (reconcile is mandatory; resume detection drives the entry).
876
+ - Mutate STATE.md (mark any step as `[ ]` or `[~]`) based on a reconciler
877
+ verdict of `requires_confirmation` or `reset` without first obtaining
878
+ explicit user confirmation. The reconciler DETERMINES; the recipe gates
879
+ the MUTATION.
862
880
  - Bypass either validator — `phase-plan-validator` and `phase-build-validator`
863
881
  MUST be called.
864
882
  - Continue past 3 fix cycles in either loop — escalate to the user instead.
@@ -243,11 +243,14 @@ browser/UI verification check OR the phase has a Frontend domain
243
243
  ## Resume / Reconciliation (summary)
244
244
 
245
245
  On every invocation, the workflow reconciles STATE.md against the on-disk
246
- artifacts in `PHASE_DIR` using a six-row decision matrix
247
- (STATE marker × artifact validity action). Special cases handled:
248
- phase already complete, partial Step 1, interrupted Step 4 with
249
- `post-build-sha: (pending)`. Full table, the per-step artifact validity
250
- rules, and the resume decision tree live in
246
+ artifacts in `PHASE_DIR`. `state.reconcile` returns per-step verdicts
247
+ (`valid | valid_lenient | stale | missing | reset | requires_confirmation`)
248
+ plus a deterministic `details.resumeAt` integer. Verdicts `valid` and
249
+ `valid_lenient` advance past the step without re-execution;
250
+ `requires_confirmation` and `reset` cascades MUST be surfaced to the user
251
+ and gated on explicit confirmation BEFORE STATE.md is mutated. Full table,
252
+ the per-step artifact validity rules, the resume decision tree, and the
253
+ extension contract live in
251
254
  [references/resume-reconciliation-table.md](references/resume-reconciliation-table.md).
252
255
 
253
256
  ## Fix-Loop Budgets
@@ -12,16 +12,32 @@ A Step is "valid" only when its primary artifact exists on disk AND meets
12
12
  the line / frontmatter requirements below. Validity is the input to the
13
13
  decision matrix in Section 2.
14
14
 
15
- | Step | Primary Artifact | Validity Rule |
16
- |------|-------------------------------------------------------------|------------------------------------------------------------------------------------------------|
17
- | 1 | `{PHASE_DIR}/explorer_findings.md` | exists AND > 10 lines |
18
- | 1 | `{PHASE_DIR}/docs_research.md` (optional companion) | exists AND > 10 lines (absent is acceptable — only required when HL plan cites third-party APIs)|
19
- | 2 | `{PHASE_DIR}/plan_V1.md` | exists AND > 20 lines AND frontmatter `type: phase-implementation-plan` |
20
- | 3 | `{PHASE_DIR}/plan_validation_report.md` | frontmatter `result: pass` AND `checks-passed:` present |
21
- | 4 | `{PHASE_DIR}/build_report.md` (single) | exists AND > 5 lines |
22
- | 4 | `{PHASE_DIR}/build_report_*.md` (parallel per domain) | each file exists AND > 5 lines; at least one such file present |
23
- | 5 | `{PHASE_DIR}/build_validation_report.md` | frontmatter `result: pass` OR `result: fail-asset` AND `checks-passed:` present (see [result-vocabulary.md](result-vocabulary.md)) |
24
- | 6 | `{PHASE_DIR}/phase_completion_report.md` | exists AND > 10 lines AND frontmatter `type: phase-report` |
15
+ **Verdict vocabulary (per-step).** `reconcile.ts` classifies each step's
16
+ artifact into one of four classifications:
17
+
18
+ - `valid` file exists AND content checks pass.
19
+ - `valid_lenient` — file exists; a coarse content check missed but the
20
+ miss is NON-FATAL (e.g. line-count threshold). Resume continues past
21
+ this step; the reason is surfaced once via `renderResult`.
22
+ - `stale` — file exists; the mismatch is STRUCTURAL (missing
23
+ required frontmatter key, unparseable frontmatter, wrong `type:`,
24
+ wrong `result:` enum value). The matrix in §2 escalates `stale` to
25
+ `requires_confirmation` for completed (`[x]`) markers.
26
+ - `missing` — file ABSENT on disk.
27
+
28
+ §2 then maps `(marker, classification)` to one of the user-facing
29
+ verdicts `{valid, valid_lenient, stale, missing, reset, requires_confirmation}`.
30
+
31
+ | Step | Primary Artifact | Validity Rule | Classification |
32
+ |------|-------------------------------------------------------------|------------------------------------------------------------------------------------------------|----------------|
33
+ | 1 | `{PHASE_DIR}/explorer_findings.md` | exists AND > 10 lines | absent ⇒ `missing`; ≤10 lines ⇒ `valid_lenient`; otherwise `valid` |
34
+ | 1 | `{PHASE_DIR}/docs_research.md` (optional companion) | exists AND > 10 lines (absent is acceptable — only required when HL plan cites third-party APIs)| absent ⇒ `missing` (acceptable for Step 1 as long as `explorer_findings.md` is `valid` — Step 1 OR's over its canonical entries); ≤10 lines ⇒ `valid_lenient` |
35
+ | 2 | `{PHASE_DIR}/plan_V1.md` | exists AND > 20 lines AND frontmatter `type: phase-implementation-plan` | absent ⇒ `missing`; ≤20 lines ⇒ `valid_lenient`; missing/wrong frontmatter `type:` ⇒ `stale`; otherwise `valid` |
36
+ | 3 | `{PHASE_DIR}/plan_validation_report.md` | frontmatter `result: pass` AND `checks-passed:` present | absent ⇒ `missing`; missing `checks-passed:` or `result:` not in accepted enum ⇒ `stale`; otherwise `valid` |
37
+ | 4 | `{PHASE_DIR}/build_report.md` (single) | exists AND > 5 lines | absent ⇒ `missing`; ≤5 lines ⇒ `valid_lenient`; otherwise `valid` |
38
+ | 4 | `{PHASE_DIR}/build_report_*.md` (parallel — per domain) | each file exists AND > 5 lines; at least one such file present | Step 4 OR's over single + per-domain entries (any `valid` ⇒ Step 4 `valid`) |
39
+ | 5 | `{PHASE_DIR}/build_validation_report.md` | frontmatter `result: pass` OR `result: fail-asset` AND `checks-passed:` present (see [result-vocabulary.md](result-vocabulary.md)) | (Step 5 row is the result-vocabulary cross-reference — see [result-vocabulary.md](result-vocabulary.md). Do NOT alter this row.) |
40
+ | 6 | `{PHASE_DIR}/phase_completion_report.md` | exists AND > 10 lines AND frontmatter `type: phase-report` | absent ⇒ `missing`; ≤10 lines ⇒ `valid_lenient`; missing/wrong frontmatter `type:` ⇒ `stale`; otherwise `valid` |
25
41
 
26
42
  Notes:
27
43
 
@@ -43,17 +59,29 @@ Notes:
43
59
  ## 2. Cross-Reference Reconciliation Matrix
44
60
 
45
61
  For each Step, combine the STATE.md marker with the artifact-validity
46
- verdict and look up the action below. The recipe applies these row by row
47
- in step order (0 → 6) at startup.
48
-
49
- | STATE.md marker | Artifact | Action |
50
- |---------------------|---------------|---------------------------------------------------------------------------------------------------|
51
- | `[x]` (completed) | valid | Trust both. Skip the step. |
52
- | `[x]` (completed) | invalid / missing | STATE.md is wrong. Warn the user. Reset Step to `[ ]`. Re-run it. |
53
- | `[~]` (in_progress) | valid | Crash occurred after the artifact was written but before STATE.md was updated. Mark `[x]`. Skip. |
54
- | `[~]` (in_progress) | invalid / missing | Mid-execution interrupt. Reset Step to `[ ]`. Re-run it. |
55
- | `[ ]` (pending) | valid | Unexpected (artifact present without STATE.md tracking). Warn. Mark `[x]`. Skip. |
56
- | `[ ]` (pending) | missing | Not started. Run the step normally. |
62
+ classification (§1) and look up the verdict below. The recipe walks Steps
63
+ 0 → 6 in order at startup and consumes the per-step verdict from
64
+ `details.steps[].verdict`.
65
+
66
+ | STATE.md marker | Artifact classification | Verdict | Recipe action |
67
+ |---------------------|-------------------------|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
68
+ | `[x]` (completed) | `valid` | `valid` | OK. Continue past this step (no execution, no mutation). |
69
+ | `[x]` (completed) | `valid_lenient` | `valid_lenient` | OK with note. Continue past this step. The `reason` is recorded in `details.steps[i].reason` for the user-facing `renderResult` table; no recipe action required. |
70
+ | `[x]` (completed) | `stale` | `requires_confirmation` | Surface the per-step `reason` to the user verbatim and OBTAIN EXPLICIT CONFIRMATION before resetting Step to `[ ]`. No implicit reset. |
71
+ | `[x]` (completed) | `missing` | `requires_confirmation` | Surface the missing-artifact path to the user and OBTAIN EXPLICIT CONFIRMATION before resetting Step to `[ ]`. No implicit reset. |
72
+ | `[~]` (in_progress) | any | `reset` | Reset Step to `[ ]` unconditionally. Re-run it. In-progress markers are always re-runnable; no user confirmation needed. |
73
+ | `[ ]` (pending) | `valid` | `valid` | Unexpected (artifact present without STATE.md tracking). Warn. Mark `[x]`. Skip. |
74
+ | `[ ]` (pending) | `valid_lenient` | `valid_lenient` | Treat as `valid` for resume; surface the reason once. |
75
+ | `[ ]` (pending) | `stale` | `stale` | This is the resume entry point; the structural mismatch will be re-emitted by the step's writer. |
76
+ | `[ ]` (pending) | `missing` | `missing` | Not started. This step is the resume entry point if no later step is `[x]`. |
77
+
78
+ > Footnote: the pre-repair single-row "`[x]` + invalid/missing ⇒ reset"
79
+ > was the source of the resume-cascade bug (the kanban-core Phase-2
80
+ > regression). This matrix replaces it. See
81
+ > `extensions/task-state/reconcile.ts`'s `decideVerdict()` (formerly
82
+ > `decide()` lines 323–346) for the runtime implementation. Every
83
+ > verdict name in this matrix MUST appear verbatim in `reconcile.ts`'s
84
+ > exported `StepVerdict` type.
57
85
 
58
86
  Cascade behaviour: each row's "Mark `[x]`" or "Reset to `[ ]`" goes
59
87
  through `task-state.MARK_STEP_COMPLETE` / direct write so that Phase and
@@ -120,13 +148,31 @@ Once per invocation, after STATE.md is loaded:
120
148
  2. **Check Phase-already-complete.** If Section 3.1 applies, stop with
121
149
  the "phase complete" message.
122
150
  3. **Walk Steps 0 → 6 in order.** For each Step:
123
- - Compute artifact validity (Section 1).
124
- - Apply the matrix row (Section 2) Skip, Mark `[x]`, or Reset.
151
+ - Compute artifact classification (Section 1).
152
+ - Apply the matrix row (Section 2) to derive the per-step verdict.
125
153
  - For Step 1, additionally apply Section 3.2 (partial research).
126
154
  - For Step 4, additionally apply Section 3.3 (interrupted build).
127
- 4. **Determine entry point.** The first Step whose post-matrix state is
128
- `[ ]` is the entry point the recipe begins execution there.
129
- 5. **Plan / Build aborted-loop re-entry.** If the entry point is Step 3
155
+ 4. **Compute `details.resumeAt`.** The first Step whose verdict is NOT
156
+ in `{valid, valid_lenient}` is the entry point. If every step is
157
+ `valid`/`valid_lenient`, `resumeAt = lastCompletedStep + 1` (preserves
158
+ the "everything is fine, advance" semantic). `entryStep` mirrors
159
+ `resumeAt` for legacy consumers.
160
+ 5. **User-confirmation gate (mandatory before any STATE.md mutation).**
161
+ If ANY step's verdict ∈ `{requires_confirmation, reset}`, the recipe
162
+ MUST:
163
+ a. Present `details.steps[]` verbatim to the user (use the
164
+ `renderResult` table).
165
+ b. For each `requires_confirmation` entry, ask the user to confirm
166
+ or skip the proposed reset.
167
+ c. ONLY AFTER explicit confirmation may the recipe call
168
+ `state.mark_step_reset` (or equivalent) to mutate STATE.md.
169
+ `reset` verdicts (only emitted for `[~]` markers) do not require user
170
+ confirmation but must still be reported in the same banner.
171
+ 6. **Re-execution boundary.** The recipe begins re-execution at
172
+ `resumeAt`. Steps with verdict `valid_lenient` are NOT re-executed
173
+ (their reasons are surfaced once via `renderResult` and the run
174
+ continues).
175
+ 7. **Plan / Build aborted-loop re-entry.** If `resumeAt` is Step 3
130
176
  or Step 5 and the previous validation report exists with a non-passing
131
177
  result for its own vocabulary (Step 3: `result: fail`; Step 5:
132
178
  `result: fail-code`), apply Section 3.4: start a fresh fix loop from
@@ -134,20 +180,34 @@ Once per invocation, after STATE.md is loaded:
134
180
  `result: fail-asset` is NOT an aborted-loop case (Section 3.4 does not
135
181
  apply) — it is a closed Step 5; the resume table treats that artifact
136
182
  as valid (Section 1).
137
- 6. **Report the resume decision** to the user before any agent is
138
- spawned: "Resuming Phase N at Step K. Skipped: <list>. Reset:
139
- <list>." Wait for explicit confirmation only if any Reset occurred.
183
+ 8. **Report the resume decision** to the user before any agent is
184
+ spawned: "Resuming Phase N at Step `resumeAt`. Skipped: <list>.
185
+ Confirmed reset: <list>." Wait for explicit confirmation only if any
186
+ `requires_confirmation` verdict occurred (per step 5 above).
140
187
 
141
188
  ## 5. `task-state` Extension Contract
142
189
 
143
190
  The `task-state` extension (Phase 11) MUST expose, at minimum:
144
191
 
145
- - `state.reconcile(phase_dir, phase_number)` — returns the resume entry
146
- point (an integer 0-6) AND a structured summary of every Skip / Mark /
147
- Reset decision applied.
192
+ - `state.reconcile(phase_dir, phase_number)` — returns a `ReconcileResult`
193
+ whose `details` carries:
194
+ - `details.steps[]` — per-step verdict array; each entry has
195
+ `{ n, marker, artifact, verdict, reason? }`.
196
+ `verdict ∈ {valid, valid_lenient, stale, missing, reset, requires_confirmation}`
197
+ — every name MUST match `reconcile.ts`'s exported `StepVerdict` type
198
+ byte-for-byte.
199
+ - `details.resumeAt` — integer 0–6; the first step whose verdict is
200
+ NOT in `{valid, valid_lenient}` (else `lastCompletedStep + 1`).
201
+ - Preserved fields (legacy consumers): `entryStep` (mirrors
202
+ `resumeAt`), `actions[]`, `scan`, `specialCases`, `table`.
148
203
  - The matrix and the special-case rules above are the spec; the
149
204
  extension MUST NOT introduce additional resume heuristics without
150
- updating this file.
205
+ updating this file. Any change to the matrix MUST be applied to
206
+ `reconcile.ts`'s `decideVerdict()` in the SAME change set.
151
207
  - Reconciliation MUST be idempotent: invoking it twice in a row on the
152
- same state produces the same entry point and the same (empty on the
153
- second call) set of actions.
208
+ same state produces the same `resumeAt`, the same per-step verdict
209
+ set, and the same (empty on the second call) set of mutating actions.
210
+ - The extension PROPOSES verdicts via `details.steps[]`; the RECIPE
211
+ gates STATE.md mutation. The extension MUST NOT mutate STATE.md based
212
+ on a `requires_confirmation` verdict without recipe-mediated user
213
+ confirmation.
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.7.0",
2
+ "version": "1.7.1",
3
3
  "presets": {
4
4
  "coding": {
5
5
  "description": "Research-driven coding workflow",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-fob",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Deploy research-driven AI coding assistant assets (skills, agents, commands) into your projects",
5
5
  "bin": {
6
6
  "ai-fob": "bin/install.js"