@gonrocca/zero-pi 0.1.48 → 0.1.50

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,162 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { checkGuardrails, parseDelta, type MergeError, type RenameBlock, type RequirementBlock } from "./spec-merge.ts";
5
+
6
+ export interface ValidationDefect {
7
+ kind: string;
8
+ message: string;
9
+ task?: string;
10
+ path?: string;
11
+ name?: string;
12
+ declaredTotal?: number;
13
+ computedTotal?: number;
14
+ }
15
+
16
+ export interface TaskRecord {
17
+ id: string;
18
+ title: string;
19
+ files: { path: string; isNew: boolean }[];
20
+ evidence: string | null;
21
+ review: number | null;
22
+ reviewRaw: string | null;
23
+ }
24
+
25
+ export interface WorkloadSection {
26
+ estimates: Map<string, number>;
27
+ declaredTotal: number | null;
28
+ }
29
+
30
+ export function parseTasks(text: string): { tasks: TaskRecord[]; workload: WorkloadSection | null; defects: ValidationDefect[] } {
31
+ const tasks: TaskRecord[] = [];
32
+ const defects: ValidationDefect[] = [];
33
+ const lines = typeof text === "string" ? text.split(/\r?\n/) : [];
34
+ let current: TaskRecord | null = null;
35
+ let collectingFiles = false;
36
+
37
+ const pushFile = (line: string): void => {
38
+ if (!current) return;
39
+ const matches = [...line.matchAll(/`([^`]+)`\s*(\(new\))?/g)];
40
+ for (const m of matches) current.files.push({ path: m[1], isNew: Boolean(m[2]) });
41
+ };
42
+
43
+ for (const line of lines) {
44
+ const task = line.match(/^\s*- \[[ xX]\]\s+\*\*(T\d+)\.\s+([^*]+)\*\*/) ?? line.match(/^###\s+(T\d+)\s+[—-]\s+(.+)$/) ?? line.match(/^##\s+\[[ xX]\]\s+(T\d+)\s*(?:[—-]\s*(.+))?$/);
45
+ if (task) {
46
+ current = { id: task[1], title: (task[2] ?? "").trim(), files: [], evidence: null, review: null, reviewRaw: null };
47
+ tasks.push(current);
48
+ collectingFiles = false;
49
+ continue;
50
+ }
51
+ if (!current) continue;
52
+ if (/^\s*-\s+files:/.test(line)) {
53
+ collectingFiles = true;
54
+ pushFile(line);
55
+ continue;
56
+ }
57
+ if (/^\s*-\s+evidence:/.test(line)) {
58
+ collectingFiles = false;
59
+ current.evidence = line.replace(/^\s*-\s+evidence:\s*/, "").trim();
60
+ continue;
61
+ }
62
+ if (/^\s*-\s+review:/.test(line)) {
63
+ collectingFiles = false;
64
+ const raw = line.replace(/^\s*-\s+review:\s*/, "").trim();
65
+ current.reviewRaw = raw;
66
+ const m = raw.match(/~?(\d+)\s+changed lines/i);
67
+ current.review = m ? Number(m[1]) : null;
68
+ continue;
69
+ }
70
+ if (collectingFiles) {
71
+ if (/^\s*`[^`]+`/.test(line) || /^\s+`[^`]+`/.test(line) || /^\s*-\s+`[^`]+`/.test(line)) pushFile(line);
72
+ else if (/^\s*-\s+/.test(line)) collectingFiles = false;
73
+ }
74
+ }
75
+
76
+ for (const task of tasks) {
77
+ if (task.files.length === 0) defects.push({ kind: "missing-files", task: task.id, message: `${task.id} is missing files list` });
78
+ if (task.evidence === null || task.evidence === "") defects.push({ kind: "missing-evidence", task: task.id, message: `${task.id} is missing evidence` });
79
+ if (task.reviewRaw === null) defects.push({ kind: "missing-review", task: task.id, message: `${task.id} is missing review estimate` });
80
+ else if (task.review === null) defects.push({ kind: "non-integer-review", task: task.id, message: `${task.id} review estimate is not an integer` });
81
+ }
82
+
83
+ const workload = parseWorkload(lines);
84
+ return { tasks, workload, defects };
85
+ }
86
+
87
+ function parseWorkload(lines: string[]): WorkloadSection | null {
88
+ const start = lines.findIndex((l) => /^##\s+Review Workload\s*$/.test(l));
89
+ if (start < 0) return null;
90
+ const estimates = new Map<string, number>();
91
+ let declaredTotal: number | null = null;
92
+ for (let i = start + 1; i < lines.length; i++) {
93
+ const line = lines[i];
94
+ if (/^##\s+/.test(line)) break;
95
+ let m = line.match(/^\|\s*(T\d+)\s*\|\s*~?(\d+)\s*\|/);
96
+ if (m) estimates.set(m[1], Number(m[2]));
97
+ m = line.match(/^\s*-\s*(T\d+)\s*[:|-]\s*~?(\d+)/);
98
+ if (m) estimates.set(m[1], Number(m[2]));
99
+ m = line.match(/\*\*Total:\s*~?(\d+)\s+changed lines\*\*/i);
100
+ if (m) declaredTotal = Number(m[1]);
101
+ }
102
+ return { estimates, declaredTotal };
103
+ }
104
+
105
+ export function validateTasksFile(text: string): ValidationDefect[] {
106
+ const parsed = parseTasks(text);
107
+ const defects = [...parsed.defects];
108
+ if (parsed.workload === null) {
109
+ defects.push({ kind: "missing-review-workload", message: "tasks.md is missing ## Review Workload" });
110
+ } else {
111
+ const computedTotal = parsed.tasks.reduce((sum, task) => sum + (task.review ?? 0), 0);
112
+ if (parsed.workload.declaredTotal === null) defects.push({ kind: "missing-workload-total", message: "Review Workload is missing total" });
113
+ 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}` });
114
+ for (const task of parsed.tasks) {
115
+ const est = parsed.workload.estimates.get(task.id);
116
+ 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` });
117
+ }
118
+ }
119
+ return defects;
120
+ }
121
+
122
+ function allBlocks(delta: ReturnType<typeof parseDelta>): Array<RequirementBlock | RenameBlock> {
123
+ return [...delta.added, ...delta.modified, ...delta.removed, ...delta.renamed];
124
+ }
125
+
126
+ export function validateSpecDelta(text: string): ValidationDefect[] {
127
+ const delta = parseDelta(text);
128
+ const defects: ValidationDefect[] = checkGuardrails([], delta).map((e: MergeError) => ({ kind: e.kind, name: e.name, message: e.message }));
129
+ for (const block of allBlocks(delta)) {
130
+ const name = "name" in block ? block.name : block.to;
131
+ if (block.body.trim() === "") defects.push({ kind: "empty-body", name, message: `Requirement ${name} has an empty body` });
132
+ if (!block.body.includes("Acceptance criteria")) defects.push({ kind: "missing-acceptance-criteria", name, message: `Requirement ${name} is missing Acceptance criteria` });
133
+ }
134
+ return defects;
135
+ }
136
+
137
+ export function discoverSpecFiles(runDir: string): string[] {
138
+ const files: string[] = [];
139
+ if (existsSync(join(runDir, "spec.md"))) files.push(join(runDir, "spec.md"));
140
+ const specsDir = join(runDir, "specs");
141
+ try {
142
+ for (const e of readdirSync(specsDir, { withFileTypes: true })) if (e.isDirectory()) {
143
+ const file = join(specsDir, e.name, "spec.md");
144
+ if (existsSync(file)) files.push(file);
145
+ }
146
+ } catch {}
147
+ return files;
148
+ }
149
+
150
+ export function validateSpecInputs(runDir: string): ValidationDefect[] {
151
+ const files = discoverSpecFiles(runDir);
152
+ if (files.length === 0) return [{ kind: "missing-spec", path: "spec.md", message: "spec.md or specs/<domain>/spec.md is missing" }];
153
+ return files.flatMap((file) => validateSpecDelta(readFileSync(file, "utf8")).map((d) => ({ ...d, path: file })));
154
+ }
155
+
156
+ export function validateArtifactSet(present: { proposal: boolean; spec: boolean; design: boolean; tasks: boolean }): ValidationDefect[] {
157
+ const defects: ValidationDefect[] = [];
158
+ for (const key of ["proposal", "spec", "design", "tasks"] as const) {
159
+ if (!present[key]) defects.push({ kind: `missing-${key}`, path: `${key}.md`, message: `${key}.md is missing` });
160
+ }
161
+ return defects;
162
+ }
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.50",
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,16 @@
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-branch-extension.ts",
40
+ "./extensions/zero-git-validate-extension.ts",
41
+ "./extensions/zero-archive-extension.ts",
42
+ "./extensions/zero-issue-extension.ts",
43
+ "./extensions/zero-diff-extension.ts",
44
+ "./extensions/provider-guard-extension.ts",
45
+ "./extensions/scan-guard-extension.ts"
37
46
  ]
38
47
  },
39
48
  "files": [
@@ -53,8 +62,25 @@
53
62
  "extensions/autotune-extension.ts",
54
63
  "extensions/spec-merge.ts",
55
64
  "extensions/spec-merge-extension.ts",
65
+ "extensions/zero-validate.ts",
66
+ "extensions/zero-validate-extension.ts",
67
+ "extensions/zero-status.ts",
68
+ "extensions/zero-status-extension.ts",
69
+ "extensions/pr-body.ts",
70
+ "extensions/sdd-links.ts",
71
+ "extensions/sdd-config.ts",
72
+ "extensions/git-runner.ts",
73
+ "extensions/gh-runner.ts",
74
+ "extensions/zero-pr-extension.ts",
75
+ "extensions/zero-branch-extension.ts",
76
+ "extensions/zero-git-validate-extension.ts",
77
+ "extensions/zero-archive-extension.ts",
78
+ "extensions/zero-issue-extension.ts",
79
+ "extensions/zero-diff-extension.ts",
56
80
  "extensions/provider-guard.ts",
57
81
  "extensions/provider-guard-extension.ts",
82
+ "extensions/scan-guard.ts",
83
+ "extensions/scan-guard-extension.ts",
58
84
  "README.md",
59
85
  "LICENSE"
60
86
  ],
@@ -177,7 +177,7 @@ A zero SDD run has two language surfaces — keep them apart.
177
177
  - **Fixed identifiers — verbatim.** Never translate identifiers. Keep verdict
178
178
  values (`pasa`, `corregir`, `replantear`, `cap-reached`), feature slugs,
179
179
  file and directory paths, model ids, and command names (`/forge`,
180
- `/zero-sync`) exactly as they are, even inside Spanish text.
180
+ `/zero-sync`, `/zero-branch`, `/zero-git-validate`, `/zero-pr`, `/zero-archive`) exactly as they are, even inside Spanish text.
181
181
 
182
182
  ## Output Contract
183
183
 
@@ -361,6 +361,14 @@ verdict" above). If the push fails for any reason, or zero runs with `--no-mcp`
361
361
  or Cortex is down — emit a non-blocking warning and continue, never block the
362
362
  run. The local line already stands.
363
363
 
364
+ ## Git/PR/archive commands
365
+
366
+ Recommended command order for an audit-ready SDD change is: `/zero-branch <slug>` → `/zero-issue <slug>` → `/forge <slug>` (plan/build/veredicto) → `/zero-git-validate <slug> --for=pr` → `/zero-pr <slug>` → `/zero-archive <slug>` after `pasa`.
367
+
368
+ - `/zero-branch <slug>` creates/reuses `sdd/<slug>` (configurable) and records `branch`/`baseBranch` in `links.json`.
369
+ - `/zero-git-validate <slug>` checks worktree, branch, remote, `gh auth`, and verdict gating without mutating.
370
+ - `/zero-archive <slug>` merges approved deltas into `.sdd/specs/` and moves the run to `.sdd/archive/YYYY-MM-DD-<slug>/`.
371
+
364
372
  ## Spec sync & archive
365
373
 
366
374
  The project keeps a **canonical spec store** at `.sdd/specs/requirements.md` —
@@ -369,12 +377,13 @@ emits a delta `spec.md` against that store; once the run reaches a `pasa`
369
377
  verdict the delta is folded back into the store.
370
378
 
371
379
  **After a `pasa` verdict — and only then.** Alongside the Cortex save and the
372
- `zero-runs.jsonl` append, invoke the **`/zero-sync <slug>`** command, passing
373
- the run's feature slug explicitly. `/zero-sync` is a real pi command — a
380
+ `zero-runs.jsonl` append, invoke the **`/zero-archive <slug>`** command, passing
381
+ the run's feature slug explicitly. `/zero-archive` is a real pi command — a
374
382
  deterministic, unit-tested merge, not a prompt instruction — that reads
375
- `.sdd/specs/requirements.md` and `.sdd/<slug>/spec.md`, folds the delta into the
376
- store, writes the store atomically, and archives the change. You only call it;
377
- you never edit the store yourself.
383
+ `.sdd/specs/requirements.md` or per-domain `.sdd/specs/<domain>/requirements.md`
384
+ and `.sdd/<slug>/spec.md` or `.sdd/<slug>/specs/<domain>/spec.md`, folds the
385
+ delta into the store, writes atomically, moves the run to `.sdd/archive/`, and
386
+ records `archivePath` in `links.json`. You only call it; you never edit the store yourself.
378
387
 
379
388
  **Never sync on a non-`pasa` outcome.** Do **not** invoke `/zero-sync` for a
380
389
  `corregir` or `replantear` verdict, or when the iteration cap was reached
@@ -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
 
@@ -62,6 +62,38 @@ Write all four into `.sdd/<slug>/`:
62
62
  absolute (or code-root-relative) paths it creates or edits, marking new files
63
63
  `(new)`, so the build phase edits them directly instead of rediscovering them.
64
64
 
65
+ ## Task schema
66
+
67
+ Use this exact checklist shape so build/validate/review can parse it without rediscovery:
68
+
69
+ ```markdown
70
+ ### T001 — Implement focused capability
71
+
72
+ - files:
73
+ - `<root>/src/example.ts`
74
+ - `<root>/src/example.test.ts` (new)
75
+ - detail: concrete implementation notes and boundaries.
76
+ - evidence: `npm test -- example` passes, or the exact manual check expected.
77
+ - review: ~120 changed lines
78
+ ```
79
+
80
+ Rules:
81
+ - Task ids are monotonic `T###`; keep existing completed ids stable on resume.
82
+ - Add `[P]` only for tasks that are truly parallel-safe, and `[Story:S1]`/`[Story:S2]` when the plan has independently testable stories.
83
+ - `files:` is mandatory and uses exact paths; append `(new)` for created files.
84
+ - `evidence:` is mandatory and names a concrete verification command or artifact.
85
+ - Keep `## Review Workload` present and synchronized with the task list.
86
+
87
+ ## Constitution / Steering check
88
+
89
+ Before finalizing tasks, check project steering/constitution files when present (`.sdd/constitution.md`, `.sdd/steering.md`, `.kiro/steering/*`, or project equivalents). If absent, mark `n/a`; absence is not a blocker. Include this table in `design.md` or `tasks.md`:
90
+
91
+ | rule | status | waiver |
92
+ | --- | --- | --- |
93
+ | Steering/constitution present | n/a | No local steering file found |
94
+ | Scope matches product/tech constraints | pass | — |
95
+ | No forbidden dependency or workflow change | pass | — |
96
+
65
97
  Honour the prior-run lessons carried in the explore findings: when a past run
66
98
  was sent back with `replantear`, do not repeat the plan mistake it recorded.
67
99
 
@@ -85,6 +117,11 @@ means the same number on every run.
85
117
  per-task estimates, and the over-budget exceptions list — state "none" when
86
118
  there are no exceptions.
87
119
 
120
+ When a run is likely to land as a chained PR series, make that explicit in the
121
+ forecast: group tasks into reviewable PR-sized batches, name any ordering
122
+ constraints between batches, and keep each batch small enough that `/zero-pr`
123
+ can produce a focused pull request after `veredicto` returns `pasa`.
124
+
88
125
  **Return contract.** Return a concise result envelope to the orchestrator: your
89
126
  phase's outcome (findings, plan, build result, or verdict with its concrete
90
127
  reasoning) and the `.sdd/<slug>/` artifact path(s) you touched. No step-by-step
@@ -13,28 +13,56 @@ 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,
29
40
  and unmet acceptance criteria.
30
41
 
42
+ ## Constitution / Steering gate
43
+
44
+ Re-check the plan's Constitution/Steering table before choosing a verdict. If a
45
+ present steering or constitution rule is violated and the plan did not record an
46
+ explicit waiver, return `corregir` even if the code compiles. If steering files
47
+ are absent, treat the row as `n/a`, not as a failure.
48
+
49
+ Example gate table to cite in reasoning when relevant:
50
+
51
+ | rule | status | waiver |
52
+ | --- | --- | --- |
53
+ | Steering/constitution present | n/a | No local steering file found |
54
+ | Scope matches product/tech constraints | pass | — |
55
+ | No forbidden dependency or workflow change | fail | corregir: added undeclared dependency |
56
+
31
57
  Record exactly one verdict:
32
58
 
33
59
  - `pasa` — the build meets the plan; the run finishes successfully.
34
60
  - `corregir` — fixable defects remain; the build phase must re-run.
35
61
  - `replantear` — the plan itself is wrong; the plan phase must re-run.
36
62
 
37
- Never return `pasa` unless the evidence supports it.
63
+ Never return `pasa` unless the evidence supports it. `/zero-pr` is only allowed
64
+ for runs whose latest recorded `veredicto` is `pasa`; if the verdict is
65
+ `corregir` or `replantear`, the PR must wait for the next successful review.
38
66
 
39
67
  State the verdict's reasoning concretely — the specific defects for `corregir`,
40
68
  the specific plan flaw for `replantear`. The orchestrator persists that
@@ -54,3 +54,12 @@ invoke `/forge` explicitly. A missed natural-language route is harmless — the
54
54
  user can still type `/forge`. A wrong route forces routine work through the
55
55
  heavy pipeline, which is not. Triggering only on a clear signal phrase is the
56
56
  rule; `/forge` remains the explicit primary entry point.
57
+
58
+ ## Related zero commands
59
+
60
+ - `/zero-branch <slug>` — create/reuse the configured Git branch for the SDD run and persist it in `.sdd/<slug>/links.json`.
61
+ - `/zero-git-validate <slug>` — validate worktree, branch, remote, GitHub CLI auth, and verdict gating before PR/archive.
62
+ - `/zero-pr <slug>` — create an audit-ready GitHub PR from SDD artifacts after `veredicto` returns `pasa`.
63
+ - `/zero-archive <slug>` — merge approved deltas into `.sdd/specs/` and move the run to `.sdd/archive/YYYY-MM-DD-<slug>/`.
64
+
65
+ Recommended flow: `/zero-branch` → `/zero-issue` → `/forge` → `/zero-git-validate --for=pr` → `/zero-pr` → `/zero-archive`.