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.
- package/assets/pi/extensions/task-state/reconcile.ts +157 -9
- package/assets/pi/prompts/build-phase-pi.md +23 -5
- package/assets/pi/skills/phase-build-workflow/SKILL.md +8 -5
- package/assets/pi/skills/phase-build-workflow/references/resume-reconciliation-table.md +95 -35
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
238
|
-
actions.push({ step, action:
|
|
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
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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`
|
|
247
|
-
(
|
|
248
|
-
|
|
249
|
-
`
|
|
250
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
| `[x]` (completed) |
|
|
53
|
-
| `[
|
|
54
|
-
| `[
|
|
55
|
-
| `[
|
|
56
|
-
| `[
|
|
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
|
|
124
|
-
- Apply the matrix row (Section 2)
|
|
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. **
|
|
128
|
-
`
|
|
129
|
-
|
|
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
|
-
|
|
138
|
-
spawned: "Resuming Phase N at Step
|
|
139
|
-
<list>." Wait for explicit confirmation only if any
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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