@gonrocca/zero-pi 0.1.48 → 0.1.49

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.
@@ -0,0 +1,59 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { readRunRecords } from "./autotune.ts";
6
+ import { readLinks, type LinksRecord } from "./sdd-links.ts";
7
+ import { buildStatus, type StatusRow } from "./zero-status.ts";
8
+
9
+ type NotifyType = "info" | "warning" | "error";
10
+ interface PiCommandContext { ui: { notify(message: string, type?: NotifyType): void } }
11
+ interface PiExtensionAPI { registerCommand(name: string, options: { description?: string; handler: (args: string, ctx: PiCommandContext) => void | Promise<void> }): void }
12
+
13
+ function listProjectSdd(): Record<string, string[]> {
14
+ const out: Record<string, string[]> = {};
15
+ for (const e of readdirSync(".sdd", { withFileTypes: true })) {
16
+ if (!e.isDirectory() || e.name === "specs" || e.name === "archive") continue;
17
+ out[e.name] = readdirSync(join(".sdd", e.name));
18
+ }
19
+ return out;
20
+ }
21
+
22
+ function render(rows: StatusRow[]): string {
23
+ if (rows.length === 0) return "zero-status: no encontré runs en .sdd/.";
24
+ const width = Math.max(4, ...rows.map((r) => r.slug.length));
25
+ const lines = [`zero-status: runs detectados`, `slug${" ".repeat(width - 4)} artf sync verdict github`];
26
+ for (const row of rows) {
27
+ const art = [row.artifacts.proposal, row.artifacts.spec, row.artifacts.design, row.artifacts.tasks].map((v) => (v ? "✓" : "·")).join("");
28
+ lines.push(`${row.slug.padEnd(width)} ${art} ${row.synced ? "synced " : "pending"} ${row.lastVerdict ?? "—"} ${row.github ?? "—"}`);
29
+ }
30
+ return lines.join("\n");
31
+ }
32
+
33
+ function readLinksBySlug(slugs: readonly string[]): Record<string, LinksRecord> {
34
+ const out: Record<string, LinksRecord> = {};
35
+ for (const slug of slugs) out[slug] = readLinks(".sdd", slug);
36
+ return out;
37
+ }
38
+
39
+ function runStatus(ctx: PiCommandContext): void {
40
+ const notify = (m: string, t?: NotifyType) => { try { ctx.ui.notify(m, t); } catch {} };
41
+ let sddDir: Record<string, string[]> = {};
42
+ let archiveEntries: string[] = [];
43
+ if (existsSync(".sdd")) sddDir = listProjectSdd();
44
+ if (existsSync(join(".sdd", "archive"))) archiveEntries = readdirSync(join(".sdd", "archive"));
45
+ const runRecords = readRunRecords(join(homedir(), ".pi", "zero-runs.jsonl"));
46
+ const linksBySlug = readLinksBySlug(Object.keys(sddDir));
47
+ notify(render(buildStatus({ sddDir, archiveEntries, runRecords, linksBySlug })), "info");
48
+ }
49
+
50
+ export default function register(pi?: PiExtensionAPI): void {
51
+ if (!pi || typeof pi.registerCommand !== "function") return;
52
+ pi.registerCommand("zero-status", {
53
+ description: "Muestra estado de artefactos, sync y último veredicto de runs .sdd/",
54
+ handler: (_args: string, ctx: PiCommandContext): void => {
55
+ try { if (ctx?.ui?.notify) runStatus(ctx); }
56
+ catch (err) { try { ctx.ui.notify(`zero-status: ${err instanceof Error ? err.message : String(err)}`, "error"); } catch {} }
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,65 @@
1
+ import type { RunRecord } from "./autotune.ts";
2
+
3
+ export interface StatusRow {
4
+ slug: string;
5
+ artifacts: { proposal: boolean; spec: boolean; design: boolean; tasks: boolean };
6
+ synced: boolean;
7
+ lastVerdict: string | null;
8
+ github?: string;
9
+ }
10
+
11
+ export interface GithubLinks { prNumber?: number; issueNumber?: number }
12
+
13
+ export function formatGithubCell(links: GithubLinks | undefined): string {
14
+ if (!links) return "—";
15
+ const parts: string[] = [];
16
+ if (typeof links.prNumber === "number") parts.push(`#${links.prNumber}`);
17
+ if (typeof links.issueNumber === "number") parts.push(`issue #${links.issueNumber}`);
18
+ return parts.length ? parts.join(" / ") : "—";
19
+ }
20
+
21
+ export function matchesArchiveEntry(slug: string, name: string): boolean {
22
+ const esc = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ return new RegExp(`^\\d{4}-\\d{2}-\\d{2}-${esc}(?:-\\d+)?$`).test(name);
24
+ }
25
+
26
+ export function latestVerdictBySlug(records: readonly Pick<RunRecord, "feature" | "ts" | "verdict">[]): Map<string, string> {
27
+ const latest = new Map<string, { ts: string; verdict: string }>();
28
+ for (const record of records) {
29
+ const prev = latest.get(record.feature);
30
+ if (!prev || record.ts > prev.ts) latest.set(record.feature, { ts: record.ts, verdict: record.verdict });
31
+ }
32
+ return new Map([...latest].map(([slug, v]) => [slug, v.verdict]));
33
+ }
34
+
35
+ export function buildStatus({
36
+ sddDir,
37
+ archiveEntries,
38
+ runRecords,
39
+ linksBySlug = {},
40
+ }: {
41
+ sddDir: Record<string, readonly string[]>;
42
+ archiveEntries: readonly string[];
43
+ runRecords: readonly Pick<RunRecord, "feature" | "ts" | "verdict">[];
44
+ linksBySlug?: Record<string, GithubLinks | undefined>;
45
+ }): StatusRow[] {
46
+ const verdicts = latestVerdictBySlug(runRecords);
47
+ return Object.keys(sddDir)
48
+ .filter((slug) => slug !== "specs" && slug !== "archive")
49
+ .sort((a, b) => a.localeCompare(b))
50
+ .map((slug) => {
51
+ const files = new Set(sddDir[slug]);
52
+ return {
53
+ slug,
54
+ artifacts: {
55
+ proposal: files.has("proposal.md"),
56
+ spec: files.has("spec.md"),
57
+ design: files.has("design.md"),
58
+ tasks: files.has("tasks.md"),
59
+ },
60
+ synced: archiveEntries.some((name) => matchesArchiveEntry(slug, name)),
61
+ lastVerdict: verdicts.get(slug) ?? null,
62
+ github: formatGithubCell(linksBySlug[slug]),
63
+ };
64
+ });
65
+ }
@@ -0,0 +1,65 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { validateArtifactSet, validateSpecDelta, validateTasksFile, type ValidationDefect } from "./zero-validate.ts";
5
+
6
+ const SDD_DIR = ".sdd";
7
+ const ARTIFACTS = ["proposal", "spec", "design", "tasks"] as const;
8
+
9
+ type NotifyType = "info" | "warning" | "error";
10
+ interface PiCommandContext { ui: { notify(message: string, type?: NotifyType): void } }
11
+ interface PiExtensionAPI { registerCommand(name: string, options: { description?: string; handler: (args: string, ctx: PiCommandContext) => void | Promise<void> }): void }
12
+
13
+ function readFileOrNull(path: string): string | null {
14
+ try { return readFileSync(path, "utf8"); } catch { return null; }
15
+ }
16
+
17
+ function resolveSlug(arg: string): string | null {
18
+ const explicit = arg.trim();
19
+ if (explicit !== "") return explicit;
20
+ try {
21
+ const candidates = readdirSync(SDD_DIR, { withFileTypes: true })
22
+ .filter((e) => e.isDirectory() && e.name !== "specs" && e.name !== "archive" && existsSync(join(SDD_DIR, e.name, "spec.md")))
23
+ .map((e) => e.name);
24
+ return candidates.length === 1 ? candidates[0] : null;
25
+ } catch { return null; }
26
+ }
27
+
28
+ function formatDefects(defects: ValidationDefect[]): string {
29
+ return defects.map((d) => ` - [${d.kind}] ${d.task ? `${d.task}: ` : ""}${d.message}`).join("\n");
30
+ }
31
+
32
+ function runValidate(args: string, ctx: PiCommandContext): void {
33
+ const notify = (m: string, t?: NotifyType) => { try { ctx.ui.notify(m, t); } catch {} };
34
+ const slug = resolveSlug(args);
35
+ if (slug === null) {
36
+ notify("zero-validate: no hay un único run para validar — corré /zero-validate <slug>", "warning");
37
+ return;
38
+ }
39
+ const dir = join(SDD_DIR, slug);
40
+ const texts = Object.fromEntries(ARTIFACTS.map((a) => [a, readFileOrNull(join(dir, `${a}.md`))])) as Record<(typeof ARTIFACTS)[number], string | null>;
41
+ const presence = Object.fromEntries(ARTIFACTS.map((a) => [a, texts[a] !== null])) as Record<(typeof ARTIFACTS)[number], boolean>;
42
+ const defects = validateArtifactSet(presence);
43
+ if (texts.spec !== null) defects.push(...validateSpecDelta(texts.spec));
44
+ if (texts.tasks !== null) defects.push(...validateTasksFile(texts.tasks));
45
+
46
+ const structural = defects.filter((d) => !d.kind.startsWith("missing-proposal"));
47
+ if (structural.length > 0) {
48
+ notify(`zero-validate: encontré defectos estructurales en ${slug}; revisalos antes de sync:\n${formatDefects(defects)}`, "error");
49
+ } else if (defects.length > 0) {
50
+ notify(`zero-validate: ${slug} está usable, pero te falta un artefacto opcional:\n${formatDefects(defects)}`, "warning");
51
+ } else {
52
+ notify(`zero-validate: ${slug} está limpio — tenés propuesta, spec, diseño y tareas coherentes.`, "info");
53
+ }
54
+ }
55
+
56
+ export default function register(pi?: PiExtensionAPI): void {
57
+ if (!pi || typeof pi.registerCommand !== "function") return;
58
+ pi.registerCommand("zero-validate", {
59
+ description: "Valida artefactos .sdd/<slug>/ antes de sincronizar el spec store",
60
+ handler: (args: string, ctx: PiCommandContext): void => {
61
+ try { if (ctx?.ui?.notify) runValidate(args, ctx); }
62
+ catch (err) { try { ctx.ui.notify(`zero-validate: ${err instanceof Error ? err.message : String(err)}`, "error"); } catch {} }
63
+ },
64
+ });
65
+ }
@@ -0,0 +1,133 @@
1
+ import { checkGuardrails, parseDelta, type MergeError, type RenameBlock, type RequirementBlock } from "./spec-merge.ts";
2
+
3
+ export interface ValidationDefect {
4
+ kind: string;
5
+ message: string;
6
+ task?: string;
7
+ path?: string;
8
+ name?: string;
9
+ declaredTotal?: number;
10
+ computedTotal?: number;
11
+ }
12
+
13
+ export interface TaskRecord {
14
+ id: string;
15
+ title: string;
16
+ files: { path: string; isNew: boolean }[];
17
+ review: number | null;
18
+ reviewRaw: string | null;
19
+ }
20
+
21
+ export interface WorkloadSection {
22
+ estimates: Map<string, number>;
23
+ declaredTotal: number | null;
24
+ }
25
+
26
+ export function parseTasks(text: string): { tasks: TaskRecord[]; workload: WorkloadSection | null; defects: ValidationDefect[] } {
27
+ const tasks: TaskRecord[] = [];
28
+ const defects: ValidationDefect[] = [];
29
+ const lines = typeof text === "string" ? text.split(/\r?\n/) : [];
30
+ let current: TaskRecord | null = null;
31
+ let collectingFiles = false;
32
+
33
+ const pushFile = (line: string): void => {
34
+ if (!current) return;
35
+ const matches = [...line.matchAll(/`([^`]+)`\s*(\(new\))?/g)];
36
+ for (const m of matches) current.files.push({ path: m[1], isNew: Boolean(m[2]) });
37
+ };
38
+
39
+ for (const line of lines) {
40
+ const task = line.match(/^\s*- \[[ xX]\]\s+\*\*(T\d+)\.\s+([^*]+)\*\*/);
41
+ if (task) {
42
+ current = { id: task[1], title: task[2].trim(), files: [], review: null, reviewRaw: null };
43
+ tasks.push(current);
44
+ collectingFiles = false;
45
+ continue;
46
+ }
47
+ if (!current) continue;
48
+ if (/^\s*-\s+files:/.test(line)) {
49
+ collectingFiles = true;
50
+ pushFile(line);
51
+ continue;
52
+ }
53
+ if (/^\s*-\s+review:/.test(line)) {
54
+ collectingFiles = false;
55
+ const raw = line.replace(/^\s*-\s+review:\s*/, "").trim();
56
+ current.reviewRaw = raw;
57
+ const m = raw.match(/~?(\d+)\s+changed lines/i);
58
+ current.review = m ? Number(m[1]) : null;
59
+ continue;
60
+ }
61
+ if (collectingFiles) {
62
+ if (/^\s*`[^`]+`/.test(line) || /^\s+`[^`]+`/.test(line)) pushFile(line);
63
+ else if (/^\s*-\s+/.test(line)) collectingFiles = false;
64
+ }
65
+ }
66
+
67
+ for (const task of tasks) {
68
+ if (task.files.length === 0) defects.push({ kind: "missing-files", task: task.id, message: `${task.id} is missing files list` });
69
+ if (task.reviewRaw === null) defects.push({ kind: "missing-review", task: task.id, message: `${task.id} is missing review estimate` });
70
+ else if (task.review === null) defects.push({ kind: "non-integer-review", task: task.id, message: `${task.id} review estimate is not an integer` });
71
+ }
72
+
73
+ const workload = parseWorkload(lines);
74
+ return { tasks, workload, defects };
75
+ }
76
+
77
+ function parseWorkload(lines: string[]): WorkloadSection | null {
78
+ const start = lines.findIndex((l) => /^##\s+Review Workload\s*$/.test(l));
79
+ if (start < 0) return null;
80
+ const estimates = new Map<string, number>();
81
+ let declaredTotal: number | null = null;
82
+ for (let i = start + 1; i < lines.length; i++) {
83
+ const line = lines[i];
84
+ if (/^##\s+/.test(line)) break;
85
+ let m = line.match(/^\|\s*(T\d+)\s*\|\s*~?(\d+)\s*\|/);
86
+ if (m) estimates.set(m[1], Number(m[2]));
87
+ m = line.match(/^\s*-\s*(T\d+)\s*[:|-]\s*~?(\d+)/);
88
+ if (m) estimates.set(m[1], Number(m[2]));
89
+ m = line.match(/\*\*Total:\s*~?(\d+)\s+changed lines\*\*/i);
90
+ if (m) declaredTotal = Number(m[1]);
91
+ }
92
+ return { estimates, declaredTotal };
93
+ }
94
+
95
+ export function validateTasksFile(text: string): ValidationDefect[] {
96
+ const parsed = parseTasks(text);
97
+ const defects = [...parsed.defects];
98
+ if (parsed.workload === null) {
99
+ defects.push({ kind: "missing-review-workload", message: "tasks.md is missing ## Review Workload" });
100
+ } else {
101
+ const computedTotal = parsed.tasks.reduce((sum, task) => sum + (task.review ?? 0), 0);
102
+ if (parsed.workload.declaredTotal === null) defects.push({ kind: "missing-workload-total", message: "Review Workload is missing total" });
103
+ else if (parsed.workload.declaredTotal !== computedTotal) defects.push({ kind: "total-mismatch", declaredTotal: parsed.workload.declaredTotal, computedTotal, message: `Review Workload total ${parsed.workload.declaredTotal} does not match computed ${computedTotal}` });
104
+ for (const task of parsed.tasks) {
105
+ const est = parsed.workload.estimates.get(task.id);
106
+ if (est !== undefined && task.review !== null && est !== task.review) defects.push({ kind: "workload-task-mismatch", task: task.id, declaredTotal: est, computedTotal: task.review, message: `${task.id} workload estimate does not match task review` });
107
+ }
108
+ }
109
+ return defects;
110
+ }
111
+
112
+ function allBlocks(delta: ReturnType<typeof parseDelta>): Array<RequirementBlock | RenameBlock> {
113
+ return [...delta.added, ...delta.modified, ...delta.removed, ...delta.renamed];
114
+ }
115
+
116
+ export function validateSpecDelta(text: string): ValidationDefect[] {
117
+ const delta = parseDelta(text);
118
+ const defects: ValidationDefect[] = checkGuardrails([], delta).map((e: MergeError) => ({ kind: e.kind, name: e.name, message: e.message }));
119
+ for (const block of allBlocks(delta)) {
120
+ const name = "name" in block ? block.name : block.to;
121
+ if (block.body.trim() === "") defects.push({ kind: "empty-body", name, message: `Requirement ${name} has an empty body` });
122
+ if (!block.body.includes("Acceptance criteria")) defects.push({ kind: "missing-acceptance-criteria", name, message: `Requirement ${name} is missing Acceptance criteria` });
123
+ }
124
+ return defects;
125
+ }
126
+
127
+ export function validateArtifactSet(present: { proposal: boolean; spec: boolean; design: boolean; tasks: boolean }): ValidationDefect[] {
128
+ const defects: ValidationDefect[] = [];
129
+ for (const key of ["proposal", "spec", "design", "tasks"] as const) {
130
+ if (!present[key]) defects.push({ kind: `missing-${key}`, path: `${key}.md`, message: `${key}.md is missing` });
131
+ }
132
+ return defects;
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonrocca/zero-pi",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow (explore → plan → build → veredicto) with per-phase model autotune and token-efficient batched builds. Adds capability to pi without modifying pi.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -33,7 +33,13 @@
33
33
  "./extensions/zero-models.ts",
34
34
  "./extensions/autotune-extension.ts",
35
35
  "./extensions/spec-merge-extension.ts",
36
- "./extensions/provider-guard-extension.ts"
36
+ "./extensions/zero-validate-extension.ts",
37
+ "./extensions/zero-status-extension.ts",
38
+ "./extensions/zero-pr-extension.ts",
39
+ "./extensions/zero-issue-extension.ts",
40
+ "./extensions/zero-diff-extension.ts",
41
+ "./extensions/provider-guard-extension.ts",
42
+ "./extensions/scan-guard-extension.ts"
37
43
  ]
38
44
  },
39
45
  "files": [
@@ -53,8 +59,20 @@
53
59
  "extensions/autotune-extension.ts",
54
60
  "extensions/spec-merge.ts",
55
61
  "extensions/spec-merge-extension.ts",
62
+ "extensions/zero-validate.ts",
63
+ "extensions/zero-validate-extension.ts",
64
+ "extensions/zero-status.ts",
65
+ "extensions/zero-status-extension.ts",
66
+ "extensions/pr-body.ts",
67
+ "extensions/sdd-links.ts",
68
+ "extensions/gh-runner.ts",
69
+ "extensions/zero-pr-extension.ts",
70
+ "extensions/zero-issue-extension.ts",
71
+ "extensions/zero-diff-extension.ts",
56
72
  "extensions/provider-guard.ts",
57
73
  "extensions/provider-guard-extension.ts",
74
+ "extensions/scan-guard.ts",
75
+ "extensions/scan-guard-extension.ts",
58
76
  "README.md",
59
77
  "LICENSE"
60
78
  ],
@@ -24,6 +24,12 @@ constraints — reading past that point burns tokens without improving the plan.
24
24
  As a rough gauge, a localized change rarely needs more than ~20 tool calls; if
25
25
  you blow past that on a small request, you are over-exploring.
26
26
 
27
+ **Scope every search to the project, never the filesystem root.** Search inside
28
+ the repo/code directory you are working in. A `find`, `grep -r`, or `rg` rooted
29
+ at `/`, a bare drive (`/c`, `C:\`), or `~`/`$HOME` is forbidden — on Windows it
30
+ hangs forever forcing OneDrive to hydrate every cloud placeholder it walks, and
31
+ zero blocks such commands at the tool boundary anyway.
32
+
27
33
  Produce a concise findings report the **plan** phase can build on: what exists,
28
34
  what is relevant to the request, and what to watch out for.
29
35
 
@@ -85,6 +85,11 @@ means the same number on every run.
85
85
  per-task estimates, and the over-budget exceptions list — state "none" when
86
86
  there are no exceptions.
87
87
 
88
+ When a run is likely to land as a chained PR series, make that explicit in the
89
+ forecast: group tasks into reviewable PR-sized batches, name any ordering
90
+ constraints between batches, and keep each batch small enough that `/zero-pr`
91
+ can produce a focused pull request after `veredicto` returns `pasa`.
92
+
88
93
  **Return contract.** Return a concise result envelope to the orchestrator: your
89
94
  phase's outcome (findings, plan, build result, or verdict with its concrete
90
95
  reasoning) and the `.sdd/<slug>/` artifact path(s) you touched. No step-by-step
@@ -13,16 +13,27 @@ orchestrator's existing run-trace machinery — the Cortex `zero-run/<slug>` sav
13
13
  and the `~/.pi/zero-runs.jsonl` append. Do not write a separate verdict file;
14
14
  `.sdd/` artifacts stay plan state only.
15
15
 
16
- **Locating the code — read, do not search.** To check the build against the
17
- plan, read the `## Code roots` section in `design.md` (or the explore findings)
18
- for the absolute paths this feature touches, and read the build result for the
19
- files it changed go straight to those paths to inspect the changes and run the
20
- tests. Do **not** run a filesystem-wide `find`/`grep` to rediscover where the
21
- code lives, and do **not** re-read a file you have already read this review
22
- unless it changed since; repeated re-reading is the main avoidable token cost on
23
- the review's model, which is often the most expensive in the pipeline. A single
24
- targeted search to fill a genuine gap is fine; a full-tree scan is not. This
25
- bounds cost only it never lowers the bar for the verdict.
16
+ **Locating the code — read, do not search.** Get the code root from the plan:
17
+ the `## Code roots` section in `design.md`, or for delta/forge runs that have
18
+ no `design.md` — the `Code root:` line in `tasks.md` or the absolute path in
19
+ your task input. Read the build result for the files it changed and go straight
20
+ to those paths to inspect the changes and run the tests.
21
+
22
+ **NEVER run a filesystem-wide scan to rediscover where the code lives.** A
23
+ `find`, `grep -r`, or `rg` rooted at `/`, a bare drive (`/c`, `C:\`), or `~`/
24
+ `$HOME` is forbidden on Windows it does not merely run slow, it **hangs
25
+ forever** forcing OneDrive to hydrate every cloud placeholder it walks (a real
26
+ run wedged on `find / -maxdepth 12 …` for 6+ hours). zero blocks such commands
27
+ at the tool boundary, so the call will fail anyway. Always scope a search to the
28
+ code-root absolute path (`find <code-root> -name …`). If you cannot find the
29
+ code root in the plan, say so in the verdict — do not go hunting for it.
30
+
31
+ Also do **not** re-read a file you have already read this review unless it
32
+ changed since; repeated re-reading is the main avoidable token cost on the
33
+ review's model, which is often the most expensive in the pipeline. A single
34
+ targeted search *inside the code root* to fill a genuine gap is fine; a
35
+ full-tree scan is not. This bounds cost only — it never lowers the bar for the
36
+ verdict.
26
37
 
27
38
  Review the build adversarially, with a fresh perspective. Check it against the
28
39
  plan's requirements, run the tests yourself, and look for gaps, regressions,
@@ -34,7 +45,9 @@ Record exactly one verdict:
34
45
  - `corregir` — fixable defects remain; the build phase must re-run.
35
46
  - `replantear` — the plan itself is wrong; the plan phase must re-run.
36
47
 
37
- Never return `pasa` unless the evidence supports it.
48
+ Never return `pasa` unless the evidence supports it. `/zero-pr` is only allowed
49
+ for runs whose latest recorded `veredicto` is `pasa`; if the verdict is
50
+ `corregir` or `replantear`, the PR must wait for the next successful review.
38
51
 
39
52
  State the verdict's reasoning concretely — the specific defects for `corregir`,
40
53
  the specific plan flaw for `replantear`. The orchestrator persists that