@gonrocca/zero-pi 0.1.3 → 0.1.4

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 CHANGED
@@ -46,9 +46,19 @@ Run it with the `/forge <feature>` prompt. The orchestrator drives phase order
46
46
  and enforces a hard build/veredicto iteration cap. Each phase has its own prompt
47
47
  under `prompts/phases/` so it can be delegated to a dedicated sub-agent.
48
48
 
49
- `/forge models` (no feature request) opens an in-session editor for the
50
- per-phase models — it reads `~/.pi/zero.json`, lets you reassign any phase, and
51
- writes the choice back.
49
+ ### Per-phase models (`/zero-models`)
50
+
51
+ `/zero-models` is a real pi command — a code handler, not an LLM prompt — for
52
+ the SDD models. Run it with no argument to pick a phase and a model
53
+ interactively, or set one directly:
54
+
55
+ ```
56
+ /zero-models # interactive picker
57
+ /zero-models build=claude-opus-4-7 # set one phase
58
+ ```
59
+
60
+ It reads and writes `~/.pi/zero.json`; the orchestrator picks the change up on
61
+ the next `/forge` run.
52
62
 
53
63
  Per-phase model assignments are read from `~/.pi/zero.json`, which the `zero`
54
64
  CLI writes when it installs this layer.
@@ -0,0 +1,189 @@
1
+ // zero-pi — the /zero-models command.
2
+ //
3
+ // A real pi command — a code handler, not an LLM prompt — for reading and
4
+ // changing the per-phase SDD models in `~/.pi/zero.json`. It is deterministic:
5
+ // no model is involved, so it does exactly what you pick, every time.
6
+ //
7
+ // /zero-models interactive — pick a phase, pick a model
8
+ // /zero-models build=claude-opus-4-7 set one phase directly
9
+ //
10
+ // The SDD orchestrator reads `~/.pi/zero.json` at the start of every `/forge`
11
+ // run, so a change takes effect on the next run.
12
+
13
+ import { readFileSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ /** The SDD phases, in pipeline order. */
18
+ export const PHASES = ["explore", "plan", "build", "veredicto"] as const;
19
+ export type Phase = (typeof PHASES)[number];
20
+
21
+ /** The per-phase model map. */
22
+ export type PhaseModels = Record<Phase, string>;
23
+
24
+ /** Fallback models when `~/.pi/zero.json` has none — cheap to explore, strong
25
+ * to plan and review. */
26
+ const DEFAULT_MODELS: PhaseModels = {
27
+ explore: "claude-haiku-4-5",
28
+ plan: "claude-opus-4-7",
29
+ build: "claude-sonnet-4-6",
30
+ veredicto: "claude-opus-4-7",
31
+ };
32
+
33
+ /** Models offered in the interactive picker — the Claude lineup pi-claude-cli
34
+ * exposes. Any other model can still be typed via the custom option. */
35
+ const MODEL_CHOICES = [
36
+ "claude-opus-4-7",
37
+ "claude-opus-4-6",
38
+ "claude-sonnet-4-6",
39
+ "claude-haiku-4-5",
40
+ ];
41
+
42
+ /** Absolute path of pi's `zero.json` marker. */
43
+ function zeroJsonPath(): string {
44
+ return join(homedir(), ".pi", "zero.json");
45
+ }
46
+
47
+ /** Whether a string names an SDD phase. */
48
+ export function isPhase(value: string): value is Phase {
49
+ return (PHASES as readonly string[]).includes(value);
50
+ }
51
+
52
+ /** Read `~/.pi/zero.json`, returning an empty object when absent or invalid. */
53
+ function readZeroJson(): Record<string, unknown> {
54
+ try {
55
+ return JSON.parse(readFileSync(zeroJsonPath(), "utf8")) as Record<string, unknown>;
56
+ } catch {
57
+ return {};
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Extract the per-phase models from a zero.json object, filling any gap with
63
+ * the default so the picker always has a value to show.
64
+ */
65
+ export function readModels(data: Record<string, unknown>): PhaseModels {
66
+ const raw = (data.models ?? {}) as Record<string, unknown>;
67
+ const models: PhaseModels = { ...DEFAULT_MODELS };
68
+ for (const phase of PHASES) {
69
+ if (typeof raw[phase] === "string") models[phase] = raw[phase] as string;
70
+ }
71
+ return models;
72
+ }
73
+
74
+ /** Parse a `<phase>=<model>` (or `<phase> <model>`) assignment. */
75
+ export function parseAssignment(arg: string): { phase: Phase; model: string } | null {
76
+ const match = arg.trim().match(/^(\w+)\s*[=\s]\s*(.+)$/);
77
+ if (!match) return null;
78
+ const phase = match[1].toLowerCase();
79
+ const model = match[2].trim();
80
+ if (!isPhase(phase) || model === "") return null;
81
+ return { phase, model };
82
+ }
83
+
84
+ /** Render the per-phase model map as an aligned block. */
85
+ export function formatModels(models: PhaseModels): string {
86
+ return PHASES.map((phase) => ` ${phase.padEnd(10)} ${models[phase]}`).join("\n");
87
+ }
88
+
89
+ /** Write the models back into `~/.pi/zero.json`, preserving every other key. */
90
+ function writeModels(data: Record<string, unknown>, models: PhaseModels): void {
91
+ writeFileSync(zeroJsonPath(), `${JSON.stringify({ ...data, models }, null, 2)}\n`, "utf8");
92
+ }
93
+
94
+ /** The slice of pi's extension API this command uses. */
95
+ interface PiUI {
96
+ select(prompt: string, options: string[]): Promise<string | undefined>;
97
+ input(prompt: string, placeholder?: string): Promise<string | undefined>;
98
+ notify(message: string, type?: "info" | "warning" | "error"): void;
99
+ }
100
+ interface PiCommandContext {
101
+ ui: PiUI;
102
+ }
103
+ interface PiExtensionAPI {
104
+ registerCommand(
105
+ name: string,
106
+ options: {
107
+ description?: string;
108
+ handler: (args: string, ctx: PiCommandContext) => Promise<void> | void;
109
+ },
110
+ ): void;
111
+ }
112
+
113
+ const SAVE_AND_EXIT = "— guardar y salir —";
114
+ const CUSTOM_MODEL = "— otro modelo (escribir) —";
115
+
116
+ /**
117
+ * The pi extension entry point — registers the `/zero-models` command.
118
+ */
119
+ export default function register(pi?: PiExtensionAPI): void {
120
+ if (!pi || typeof pi.registerCommand !== "function") return;
121
+
122
+ pi.registerCommand("zero-models", {
123
+ description: "Show or set the per-phase SDD models — /zero-models [<phase>=<model>]",
124
+ handler: async (args: string, ctx: PiCommandContext): Promise<void> => {
125
+ try {
126
+ const data = readZeroJson();
127
+ const models = readModels(data);
128
+
129
+ // Direct form: /zero-models build=claude-opus-4-7
130
+ const arg = args.trim();
131
+ if (arg) {
132
+ const assignment = parseAssignment(arg);
133
+ if (!assignment) {
134
+ ctx.ui.notify(
135
+ "uso: /zero-models —o— /zero-models <fase>=<modelo> " +
136
+ "(fase: explore | plan | build | veredicto)",
137
+ "warning",
138
+ );
139
+ return;
140
+ }
141
+ models[assignment.phase] = assignment.model;
142
+ writeModels(data, models);
143
+ ctx.ui.notify(`zero models: ${assignment.phase} → ${assignment.model}`, "info");
144
+ return;
145
+ }
146
+
147
+ // Interactive form: pick a phase, pick a model, repeat until saved.
148
+ let changed = false;
149
+ for (;;) {
150
+ const phasePick = await ctx.ui.select(
151
+ "zero · modelos SDD — elegí una fase",
152
+ [...PHASES.map((p) => `${p} → ${models[p]}`), SAVE_AND_EXIT],
153
+ );
154
+ if (!phasePick || phasePick === SAVE_AND_EXIT) break;
155
+
156
+ const phase = phasePick.split(/\s/)[0];
157
+ if (!isPhase(phase)) break;
158
+
159
+ const modelPick = await ctx.ui.select(`Modelo para «${phase}»`, [
160
+ ...MODEL_CHOICES,
161
+ CUSTOM_MODEL,
162
+ ]);
163
+ if (!modelPick) continue;
164
+
165
+ let model = modelPick;
166
+ if (modelPick === CUSTOM_MODEL) {
167
+ const typed = await ctx.ui.input(`Modelo para «${phase}»`, models[phase]);
168
+ if (!typed || typed.trim() === "") continue;
169
+ model = typed.trim();
170
+ }
171
+ models[phase] = model;
172
+ changed = true;
173
+ }
174
+
175
+ if (changed) {
176
+ writeModels(data, models);
177
+ ctx.ui.notify(`zero · modelos SDD guardados:\n${formatModels(models)}`, "info");
178
+ } else {
179
+ ctx.ui.notify(`zero · modelos SDD (sin cambios):\n${formatModels(models)}`, "info");
180
+ }
181
+ } catch (err) {
182
+ ctx.ui.notify(
183
+ `zero-models: ${err instanceof Error ? err.message : String(err)}`,
184
+ "error",
185
+ );
186
+ }
187
+ },
188
+ });
189
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonrocca/zero-pi",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, skill auto-learning, and an animated ZERO startup banner. Adds capability to pi without modifying pi.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -20,13 +20,15 @@
20
20
  "./skills"
21
21
  ],
22
22
  "extensions": [
23
- "./extensions/startup-banner.ts"
23
+ "./extensions/startup-banner.ts",
24
+ "./extensions/zero-models.ts"
24
25
  ]
25
26
  },
26
27
  "files": [
27
28
  "prompts",
28
29
  "skills",
29
30
  "extensions/startup-banner.ts",
31
+ "extensions/zero-models.ts",
30
32
  "README.md",
31
33
  "LICENSE"
32
34
  ],
package/prompts/forge.md CHANGED
@@ -1,31 +1,8 @@
1
1
  ---
2
- description: Run the SDD pipeline for a feature — or `/forge models` to set the per-phase models
2
+ description: Run an automatic spec-driven development pipeline for a feature request
3
3
  ---
4
4
 
5
- The argument decides the mode.
6
-
7
- ## `/forge models` — configure the SDD models
8
-
9
- When the argument is exactly `models`, do **not** run the pipeline. Configure
10
- the per-phase models instead:
11
-
12
- 1. Read `~/.pi/zero.json`. Show the user the current `models` mapping — the
13
- model assigned to each phase: `explore`, `plan`, `build`, `veredicto`.
14
- 2. Ask which phases they want to change and to what. Offer the models the
15
- active provider exposes — run `pi --list-models` if you need the list.
16
- Guidance: a cheap, fast model suits `explore`; a strong model suits `plan`
17
- and the adversarial `veredicto`; `build` sits in between.
18
- 3. Write the chosen models back into `~/.pi/zero.json`, replacing only the
19
- `models` block and preserving every other key (`version`, `installedAt`).
20
- Validate each model name against `pi --list-models` before writing.
21
- 4. Confirm the new mapping back to the user.
22
-
23
- This changes the models for every later `/forge` run — the orchestrator reads
24
- `~/.pi/zero.json` at the start of each run.
25
-
26
- ## `/forge <feature request>` — run the SDD pipeline
27
-
28
- For any other argument, run the zero SDD pipeline for that feature request.
5
+ Run the zero SDD pipeline for the feature request in the arguments.
29
6
 
30
7
  Follow the zero SDD orchestrator instructions: drive the run through the
31
8
  explore → plan → build → veredicto phases, honour the build/veredicto iteration
@@ -40,4 +17,7 @@ In interactive mode, pause after each phase with a summary and ask before
40
17
  continuing. Never report success unless the veredicto phase returned a `pasa`
41
18
  verdict; if the cap is reached first, report that the result is not verified.
42
19
 
43
- Argument: $ARGUMENTS
20
+ To change the per-phase models, the user runs the `/zero-models` command — do
21
+ not handle model configuration here.
22
+
23
+ Feature request: $ARGUMENTS