@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.
- package/README.md +43 -1
- package/extensions/gh-runner.ts +77 -0
- package/extensions/pr-body.ts +129 -0
- package/extensions/scan-guard-extension.ts +104 -0
- package/extensions/scan-guard.ts +190 -0
- package/extensions/sdd-links.ts +38 -0
- package/extensions/spec-merge-extension.ts +4 -0
- package/extensions/spec-merge.ts +105 -215
- package/extensions/zero-diff-extension.ts +59 -0
- package/extensions/zero-issue-extension.ts +51 -0
- package/extensions/zero-pr-extension.ts +78 -0
- package/extensions/zero-status-extension.ts +59 -0
- package/extensions/zero-status.ts +65 -0
- package/extensions/zero-validate-extension.ts +65 -0
- package/extensions/zero-validate.ts +133 -0
- package/package.json +20 -2
- package/prompts/phases/explore.md +6 -0
- package/prompts/phases/plan.md +5 -0
- package/prompts/phases/veredicto.md +24 -11
|
@@ -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.
|
|
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/
|
|
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
|
|
package/prompts/phases/plan.md
CHANGED
|
@@ -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.**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|