@gonrocca/zero-pi 0.1.54 → 0.1.56
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 +28 -5
- package/extensions/sdd-agents.ts +55 -1
- package/extensions/zero-banner.ts +38 -20
- package/extensions/zero-models-picker.ts +70 -21
- package/extensions/zero-models.ts +164 -18
- package/extensions/zero-statusline.ts +16 -15
- package/package.json +1 -1
- package/prompts/orchestrator.md +13 -5
- package/themes/zero-sunset.json +79 -0
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ into `/forge` for you.
|
|
|
74
74
|
| Feature | What it does |
|
|
75
75
|
| ------- | ------------ |
|
|
76
76
|
| **Strict TDD** | The build phase drives RED → GREEN → TRIANGULATE → REFACTOR with a TDD Cycle Evidence table; veredicto audits it. On by default, runtime-gated on a test runner; `tdd.mode: "off"` disables it. |
|
|
77
|
-
| **`/zero-models`** | Pick the model + provider for each SDD phase — a boxed-window picker, or set one directly. |
|
|
77
|
+
| **`/zero-models`** | Pick the model + provider + thinking level for each SDD phase — a boxed-window picker, or set one directly. |
|
|
78
78
|
| **Autotune** | Learns which model fits each phase from your run history and re-tunes itself. |
|
|
79
79
|
| **`/zero-sync` / `/zero-archive`** | Folds each run's spec delta into a canonical, project-wide spec store and archives approved runs. |
|
|
80
80
|
| **Git / PR / Issues** | `/zero-branch`, `/zero-git-validate`, `/zero-pr`, and `/zero-issue` keep branches and GitHub links audit-ready. |
|
|
@@ -86,13 +86,14 @@ into `/forge` for you.
|
|
|
86
86
|
| **Windows tree-kill** | Aborting a turn kills the whole process tree — no orphaned `claude`. |
|
|
87
87
|
| **Skill auto-learning** | Distills reusable skills from substantial tasks and surfaces them later. |
|
|
88
88
|
| **`zero-sdd` theme** | A dark, high-contrast pi theme tuned for SDD work. |
|
|
89
|
+
| **`zero-sunset` theme** | A warm sunset variant — gold/coral/magenta accents over warm-dark panels, with one cool tone kept for syntax legibility. Activate with `/theme zero-sunset`. |
|
|
89
90
|
|
|
90
91
|
## ⌨️ Commands
|
|
91
92
|
|
|
92
93
|
| Command | Does |
|
|
93
94
|
| ------- | ---- |
|
|
94
95
|
| `/forge <feature>` | Run the SDD pipeline — `--continue [slug]` resumes. |
|
|
95
|
-
| `/zero-models [<phase>=[<provider>/]<model>]` | Show or set per-phase models — `autotune=auto\|ask\|off`. |
|
|
96
|
+
| `/zero-models [<phase>=[<provider>/]<model> [thinking=<level>]]` | Show or set per-phase models, providers, and thinking — `thinking=<level>` (`off\|minimal\|low\|medium\|high\|xhigh`); `autotune=auto\|ask\|off`. |
|
|
96
97
|
| `/zero-sync <slug>` | Fold a run's spec delta into the canonical spec store; a first all-`## ADDED` delta creates the store lazily. |
|
|
97
98
|
| `/zero-archive <slug>` | Merge an approved run into `.sdd/specs/`, move it to `.sdd/archive/YYYY-MM-DD-<slug>/`, and persist `archivePath`. |
|
|
98
99
|
| `/zero-validate <slug>` | Validate proposal/spec/design/tasks artifacts, including task schema and per-domain specs. |
|
|
@@ -165,9 +166,31 @@ Acceptance criteria:
|
|
|
165
166
|
|
|
166
167
|
## 🔧 Configuration
|
|
167
168
|
|
|
168
|
-
zero-pi keeps its state in `~/.pi/zero.json` (per-phase models
|
|
169
|
-
and `~/.pi/zero-runs.jsonl` (the run-metrics log);
|
|
170
|
-
under `.sdd/`. Set `ZERO_RESUME=off` to disable the
|
|
169
|
+
zero-pi keeps its state in `~/.pi/zero.json` (per-phase `models`, `providers`,
|
|
170
|
+
`thinking`, and autotune mode) and `~/.pi/zero-runs.jsonl` (the run-metrics log);
|
|
171
|
+
per-project artifacts live under `.sdd/`. Set `ZERO_RESUME=off` to disable the
|
|
172
|
+
conversation-resume note.
|
|
173
|
+
|
|
174
|
+
`~/.pi/zero.json` stores three parallel per-phase maps plus the autotune mode:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"models": { "explore": "claude-haiku-4-5", "build": "claude-sonnet-4-6" },
|
|
179
|
+
"providers": { "explore": "anthropic", "build": "anthropic" },
|
|
180
|
+
"thinking": { "explore": "low", "build": "high" },
|
|
181
|
+
"autotune": "ask"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The `thinking` map sets each phase's pi effort level — one of `off`, `minimal`,
|
|
186
|
+
`low`, `medium`, `high`, `xhigh`. A phase with no entry gets no `thinking:` line
|
|
187
|
+
in its generated sub-agent (no aggressive default). Set it from the picker's
|
|
188
|
+
after-model thinking screen, or directly:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
/zero-models build=anthropic/claude-sonnet-4-6 thinking=high
|
|
192
|
+
/zero-models build=anthropic/claude-sonnet-4-6 high # trailing shorthand
|
|
193
|
+
```
|
|
171
194
|
|
|
172
195
|
`.sdd/config.json` carries the per-project git and TDD settings:
|
|
173
196
|
|
package/extensions/sdd-agents.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
|
+
import { isThinkingLevel } from "./zero-models.ts";
|
|
20
|
+
import type { ThinkingLevel } from "./zero-models.ts";
|
|
21
|
+
|
|
19
22
|
/** The four SDD phases, each backed by a `prompts/phases/<phase>.md`. */
|
|
20
23
|
export const PHASES = ["explore", "plan", "build", "veredicto"] as const;
|
|
21
24
|
export type Phase = (typeof PHASES)[number];
|
|
@@ -59,6 +62,7 @@ export function buildAgentFile(
|
|
|
59
62
|
body: string,
|
|
60
63
|
description: string,
|
|
61
64
|
model: string | undefined,
|
|
65
|
+
thinking?: ThinkingLevel,
|
|
62
66
|
): string {
|
|
63
67
|
const front = [
|
|
64
68
|
"---",
|
|
@@ -66,6 +70,11 @@ export function buildAgentFile(
|
|
|
66
70
|
`description: ${description || `zero SDD ${phase} phase`}`,
|
|
67
71
|
];
|
|
68
72
|
if (model) front.push(`model: ${model}`);
|
|
73
|
+
// The `thinking:` line sits after the optional `model:` and before
|
|
74
|
+
// `systemPromptMode:`. It is emitted only when a level is present AND valid —
|
|
75
|
+
// defensive: callers already validate, but the file builder must never write
|
|
76
|
+
// a bad level into agent frontmatter.
|
|
77
|
+
if (thinking && isThinkingLevel(thinking)) front.push(`thinking: ${thinking}`);
|
|
69
78
|
front.push(
|
|
70
79
|
"systemPromptMode: replace",
|
|
71
80
|
"inheritProjectContext: true",
|
|
@@ -101,6 +110,32 @@ export function phaseModel(data: unknown, phase: Phase): string | undefined {
|
|
|
101
110
|
return model;
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the agent `thinking:` level for a phase from a parsed `zero.json`.
|
|
115
|
+
*
|
|
116
|
+
* The explicit `thinking[phase]` value wins when it is one of the six real pi
|
|
117
|
+
* effort levels. Otherwise a legacy model string of the form `"<model> <level>"`
|
|
118
|
+
* is mined: its trailing whitespace-separated token supplies the level, but
|
|
119
|
+
* only when that token is a valid level. `max`/`ultracode` and any other token
|
|
120
|
+
* resolve to `undefined`, so no `thinking:` line is emitted. Exported for tests.
|
|
121
|
+
*/
|
|
122
|
+
export function phaseThinking(data: unknown, phase: Phase): ThinkingLevel | undefined {
|
|
123
|
+
if (!data || typeof data !== "object") return undefined;
|
|
124
|
+
const d = data as {
|
|
125
|
+
thinking?: Record<string, unknown>;
|
|
126
|
+
models?: Record<string, unknown>;
|
|
127
|
+
};
|
|
128
|
+
const direct = d.thinking?.[phase];
|
|
129
|
+
if (isThinkingLevel(direct)) return direct;
|
|
130
|
+
// Legacy recovery: a valid trailing level on "<model> <level>".
|
|
131
|
+
const model = d.models?.[phase];
|
|
132
|
+
if (typeof model === "string" && model.trim().includes(" ")) {
|
|
133
|
+
const tail = model.trim().split(/\s+/).slice(-1)[0];
|
|
134
|
+
if (isThinkingLevel(tail)) return tail;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
104
139
|
/** Read the per-phase model from `~/.pi/zero.json`; `undefined` when absent. */
|
|
105
140
|
function readPhaseModel(phase: Phase): string | undefined {
|
|
106
141
|
try {
|
|
@@ -113,6 +148,19 @@ function readPhaseModel(phase: Phase): string | undefined {
|
|
|
113
148
|
}
|
|
114
149
|
}
|
|
115
150
|
|
|
151
|
+
/** Read the per-phase thinking level from `~/.pi/zero.json`; `undefined` when
|
|
152
|
+
* absent, invalid, or unrecoverable. Mirrors {@link readPhaseModel}. */
|
|
153
|
+
function readPhaseThinking(phase: Phase): ThinkingLevel | undefined {
|
|
154
|
+
try {
|
|
155
|
+
return phaseThinking(
|
|
156
|
+
JSON.parse(readFileSync(join(homedir(), ".pi", "zero.json"), "utf8")),
|
|
157
|
+
phase,
|
|
158
|
+
);
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
116
164
|
/**
|
|
117
165
|
* The pi extension entry point. Generates the four `zero-<phase>` agent files
|
|
118
166
|
* so `/forge` has real sub-agents to delegate to. Every failure is swallowed —
|
|
@@ -131,7 +179,13 @@ export default function register(_pi?: unknown): void {
|
|
|
131
179
|
try {
|
|
132
180
|
const raw = readFileSync(join(phasesDir, `${phase}.md`), "utf8");
|
|
133
181
|
const { description, body } = splitPhasePrompt(raw);
|
|
134
|
-
const file = buildAgentFile(
|
|
182
|
+
const file = buildAgentFile(
|
|
183
|
+
phase,
|
|
184
|
+
body,
|
|
185
|
+
description,
|
|
186
|
+
readPhaseModel(phase),
|
|
187
|
+
readPhaseThinking(phase),
|
|
188
|
+
);
|
|
135
189
|
writeFileSync(join(agentsDir, `zero-${phase}.md`), file, "utf8");
|
|
136
190
|
} catch {
|
|
137
191
|
// A single phase failing must not block the other three.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// zero-pi — static ZERO SDD startup banner.
|
|
2
2
|
//
|
|
3
3
|
// Renders the ZERO wordmark ONCE, at extension load, as an "ANSI Shadow" 3D
|
|
4
|
-
// block in
|
|
5
|
-
//
|
|
4
|
+
// block in a sunset gradient — gold at the lit top edge melting down through
|
|
5
|
+
// coral and magenta to a ceroclawd-violet base, with dark shadow strokes and
|
|
6
|
+
// a cast shadow for depth.
|
|
6
7
|
//
|
|
7
8
|
// It writes a single block to stdout before pi's UI takes over. There is
|
|
8
9
|
// deliberately NO setHeader and NO animation timer: an animated header that
|
|
@@ -29,14 +30,23 @@ const FONT: Record<string, string[]> = {
|
|
|
29
30
|
" ": [" ", " ", " ", " ", " ", " "],
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
// Sunset palette — a warm sky gradient (gold → peach → coral → rose → magenta
|
|
34
|
+
// → violet) for the letter faces, plus glow, shadow and darkness. The violet
|
|
35
|
+
// base keeps the ZERO wordmark ending on the ceroclawd brand colour.
|
|
36
|
+
const SKY: RGB[] = [
|
|
37
|
+
[255, 214, 130], // gold
|
|
38
|
+
[255, 168, 99], // peach
|
|
39
|
+
[255, 124, 92], // coral
|
|
40
|
+
[255, 92, 122], // rose
|
|
41
|
+
[214, 74, 140], // magenta
|
|
42
|
+
[142, 59, 158], // violet
|
|
43
|
+
];
|
|
44
|
+
const PEAK: RGB = [255, 244, 224];
|
|
45
|
+
const SHADOW: RGB = [46, 22, 54];
|
|
46
|
+
const INK: RGB = [22, 12, 30];
|
|
47
|
+
const CORAL: RGB = [255, 124, 92];
|
|
48
|
+
const PEACH: RGB = [255, 168, 99];
|
|
49
|
+
const MUTED: RGB = [150, 120, 130];
|
|
40
50
|
|
|
41
51
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
42
52
|
|
|
@@ -57,6 +67,14 @@ function mix(a: RGB, b: RGB, t: number): RGB {
|
|
|
57
67
|
];
|
|
58
68
|
}
|
|
59
69
|
|
|
70
|
+
/** Sample a multi-stop colour ramp at t∈[0,1] — the sunset sky, top to base. */
|
|
71
|
+
function ramp(stops: RGB[], t: number): RGB {
|
|
72
|
+
const k = Math.max(0, Math.min(1, t));
|
|
73
|
+
const seg = k * (stops.length - 1);
|
|
74
|
+
const i = Math.min(stops.length - 2, Math.floor(seg));
|
|
75
|
+
return mix(stops[i], stops[i + 1], seg - i);
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
/** Printable width of a string, ignoring ANSI colour escapes. */
|
|
61
79
|
export function visibleWidth(text: string): number {
|
|
62
80
|
return text.replace(ANSI_RE, "").length;
|
|
@@ -76,17 +94,17 @@ function matrixFor(text: string): { rows: string[]; width: number } {
|
|
|
76
94
|
return { rows, width: rows[0]?.length ?? 0 };
|
|
77
95
|
}
|
|
78
96
|
|
|
79
|
-
/** Front-face colour:
|
|
97
|
+
/** Front-face colour: sunset vertical gradient with a lit top edge. */
|
|
80
98
|
function faceColor(row: number, topEdge: boolean): RGB {
|
|
81
|
-
const vt = Math.pow(row / (ROWS - 1), 0.
|
|
82
|
-
const base =
|
|
83
|
-
return topEdge ? mix(base, PEAK, 0.
|
|
99
|
+
const vt = Math.pow(row / (ROWS - 1), 0.82);
|
|
100
|
+
const base = ramp(SKY, vt);
|
|
101
|
+
return topEdge ? mix(base, PEAK, 0.55) : base;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
/** Extrusion side: a darker, shaded version of the face it belongs to. */
|
|
87
105
|
function sideColor(row: number): RGB {
|
|
88
|
-
const vt = Math.pow(row / (ROWS - 1), 0.
|
|
89
|
-
return mix(
|
|
106
|
+
const vt = Math.pow(row / (ROWS - 1), 0.82);
|
|
107
|
+
return mix(ramp(SKY, vt), SHADOW, 0.62);
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
function renderLogo(width: number): string[] {
|
|
@@ -114,13 +132,13 @@ function renderLogo(width: number): string[] {
|
|
|
114
132
|
return lines;
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
/** A thin
|
|
135
|
+
/** A thin sunset rule, brightest in the middle. */
|
|
118
136
|
function ornament(width: number): string {
|
|
119
137
|
const length = Math.min(46, Math.max(22, Math.floor(width * 0.5)));
|
|
120
138
|
let line = "";
|
|
121
139
|
for (let i = 0; i < length; i++) {
|
|
122
140
|
const t = i / Math.max(1, length - 1);
|
|
123
|
-
line += fg(mix(
|
|
141
|
+
line += fg(mix(CORAL, PEACH, Math.sin(t * Math.PI)), "─");
|
|
124
142
|
}
|
|
125
143
|
return center(line, width);
|
|
126
144
|
}
|
|
@@ -136,9 +154,9 @@ function ornament(width: number): string {
|
|
|
136
154
|
*/
|
|
137
155
|
export function bannerBlock(width: number): string[] {
|
|
138
156
|
if (width < 64) {
|
|
139
|
-
return [center(fg(
|
|
157
|
+
return [center(fg(PEACH, "ZERO SDD"), width), center(fg(MUTED, "pi.dev · spec-driven work"), width)];
|
|
140
158
|
}
|
|
141
|
-
const tag = fg(
|
|
159
|
+
const tag = fg(PEACH, "ZERO SDD") + fg(MUTED, " explore → plan → build → veredicto");
|
|
142
160
|
return [ornament(width), ...renderLogo(width), center(tag, width), ornament(width)];
|
|
143
161
|
}
|
|
144
162
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// new state. Type-only imports are `import type` so `--experimental-strip-types`
|
|
13
13
|
// erases them with no runtime resolution. Mirrors the `autotune.ts` precedent.
|
|
14
14
|
|
|
15
|
-
import type { Phase, PhaseModels, PhaseProviders } from "./zero-models.ts";
|
|
15
|
+
import type { Phase, PhaseModels, PhaseProviders, PhaseThinking, ThinkingLevel } from "./zero-models.ts";
|
|
16
16
|
import type { AutotuneMode } from "./autotune.ts";
|
|
17
17
|
import type { AutotunePending } from "./autotune-extension.ts";
|
|
18
18
|
|
|
@@ -24,12 +24,17 @@ const PHASES = ["explore", "plan", "build", "veredicto"] as const;
|
|
|
24
24
|
/** The three autotune modes offered on the autotune screen. */
|
|
25
25
|
const AUTOTUNE_MODES = ["auto", "ask", "off"] as const;
|
|
26
26
|
|
|
27
|
+
/** The six real pi effort levels, in ascending order — re-stated locally so
|
|
28
|
+
* the pure module carries no value import. Must stay in lockstep with
|
|
29
|
+
* `THINKING_LEVELS` in `zero-models.ts`. No `max`/`ultracode` aliases. */
|
|
30
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
31
|
+
|
|
27
32
|
// ---------------------------------------------------------------------------
|
|
28
33
|
// Screen model
|
|
29
34
|
// ---------------------------------------------------------------------------
|
|
30
35
|
|
|
31
36
|
/** Which sub-screen the picker is currently showing. */
|
|
32
|
-
export type Screen = "main" | "provider" | "model" | "autotune";
|
|
37
|
+
export type Screen = "main" | "provider" | "model" | "thinking" | "autotune";
|
|
33
38
|
|
|
34
39
|
/** One selectable row in the current screen. */
|
|
35
40
|
export interface MenuEntry {
|
|
@@ -43,6 +48,7 @@ export interface MenuEntry {
|
|
|
43
48
|
| "custom-provider" // — otro provider (escribir) —
|
|
44
49
|
| "model" // a concrete model id (model screen)
|
|
45
50
|
| "custom-model" // — otro modelo (escribir) —
|
|
51
|
+
| "thinking-level" // off | minimal | … | xhigh (thinking screen)
|
|
46
52
|
| "autotune-mode"; // auto | ask | off (autotune screen)
|
|
47
53
|
/** The text shown for the row (Spanish, voseo). */
|
|
48
54
|
label: string;
|
|
@@ -60,6 +66,9 @@ export interface StagedEdits {
|
|
|
60
66
|
models: PhaseModels;
|
|
61
67
|
/** From `readProviders` — a mutated copy, parallel to {@link models}. */
|
|
62
68
|
providers: PhaseProviders;
|
|
69
|
+
/** From `readThinking` — a mutated copy, parallel to {@link models}. A
|
|
70
|
+
* partial map: an absent phase means no thinking level configured. */
|
|
71
|
+
thinking: PhaseThinking;
|
|
63
72
|
/** The autotune mode, possibly changed from disk. */
|
|
64
73
|
autotuneMode: AutotuneMode;
|
|
65
74
|
/** Any phase model/provider changed. */
|
|
@@ -95,6 +104,9 @@ export interface PickerState {
|
|
|
95
104
|
drillPhase: Phase | null;
|
|
96
105
|
/** Drill-down context: provider chosen so far (model screen). */
|
|
97
106
|
drillProvider: string | null;
|
|
107
|
+
/** Drill-down context: the model chosen, awaiting a thinking level
|
|
108
|
+
* (thinking screen). Held out of `edits` until a level commits it. */
|
|
109
|
+
drillModel: string | null;
|
|
98
110
|
/** When non-null, the component shows an inline text input for this. */
|
|
99
111
|
textPrompt: { for: "provider" | "model"; label: string } | null;
|
|
100
112
|
}
|
|
@@ -120,15 +132,19 @@ const CUSTOM_MODEL_LABEL = "— otro modelo (escribir) —";
|
|
|
120
132
|
/** The save-and-exit row label. */
|
|
121
133
|
const SAVE_LABEL = "— guardar y salir —";
|
|
122
134
|
|
|
123
|
-
/** Render a phase's current `provider/model` (provider omitted when empty)
|
|
135
|
+
/** Render a phase's current `provider/model` (provider omitted when empty),
|
|
136
|
+
* with ` · thinking <level>` appended when a level is staged — mirroring
|
|
137
|
+
* `formatPhases` in `zero-models.ts`. No artifact when no level is set. */
|
|
124
138
|
function phaseLabel(
|
|
125
139
|
phase: Phase,
|
|
126
140
|
models: PhaseModels,
|
|
127
141
|
providers: PhaseProviders,
|
|
142
|
+
thinking: PhaseThinking,
|
|
128
143
|
): string {
|
|
129
144
|
const provider = providers[phase];
|
|
130
145
|
const model = provider ? `${provider}/${models[phase]}` : models[phase];
|
|
131
|
-
|
|
146
|
+
const level = thinking[phase];
|
|
147
|
+
return `${phase} → ${model}${level ? ` · thinking ${level}` : ""}`;
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
/** Render the `★ aplicar sugerencia` label from the pending adjustments. */
|
|
@@ -149,6 +165,7 @@ function applyLabel(pending: readonly AutotunePending[]): string {
|
|
|
149
165
|
export function createPickerState(input: {
|
|
150
166
|
models: PhaseModels;
|
|
151
167
|
providers: PhaseProviders;
|
|
168
|
+
thinking: PhaseThinking;
|
|
152
169
|
autotuneMode: AutotuneMode;
|
|
153
170
|
pending: AutotunePending[];
|
|
154
171
|
groups: Map<string, string[]>;
|
|
@@ -161,6 +178,7 @@ export function createPickerState(input: {
|
|
|
161
178
|
edits: {
|
|
162
179
|
models: { ...input.models },
|
|
163
180
|
providers: { ...input.providers },
|
|
181
|
+
thinking: { ...input.thinking },
|
|
164
182
|
autotuneMode: input.autotuneMode,
|
|
165
183
|
changed: false,
|
|
166
184
|
autotuneChanged: false,
|
|
@@ -171,6 +189,7 @@ export function createPickerState(input: {
|
|
|
171
189
|
fallbackModels: input.fallbackModels,
|
|
172
190
|
drillPhase: null,
|
|
173
191
|
drillProvider: null,
|
|
192
|
+
drillModel: null,
|
|
174
193
|
textPrompt: null,
|
|
175
194
|
};
|
|
176
195
|
return rebuildEntries(state);
|
|
@@ -197,7 +216,12 @@ function mainEntries(state: PickerState): MenuEntry[] {
|
|
|
197
216
|
for (const phase of PHASES) {
|
|
198
217
|
entries.push({
|
|
199
218
|
kind: "phase",
|
|
200
|
-
label: phaseLabel(
|
|
219
|
+
label: phaseLabel(
|
|
220
|
+
phase,
|
|
221
|
+
state.edits.models,
|
|
222
|
+
state.edits.providers,
|
|
223
|
+
state.edits.thinking,
|
|
224
|
+
),
|
|
201
225
|
value: phase,
|
|
202
226
|
});
|
|
203
227
|
}
|
|
@@ -242,6 +266,15 @@ function modelEntries(state: PickerState): MenuEntry[] {
|
|
|
242
266
|
return entries;
|
|
243
267
|
}
|
|
244
268
|
|
|
269
|
+
/** Build the rows for the `thinking` screen — the six real pi effort levels. */
|
|
270
|
+
function thinkingEntries(): MenuEntry[] {
|
|
271
|
+
return THINKING_LEVELS.map((level) => ({
|
|
272
|
+
kind: "thinking-level" as const,
|
|
273
|
+
label: level,
|
|
274
|
+
value: level,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
|
|
245
278
|
/** Build the rows for the `autotune` screen — the three modes. */
|
|
246
279
|
function autotuneEntries(): MenuEntry[] {
|
|
247
280
|
return AUTOTUNE_MODES.map((mode) => ({
|
|
@@ -270,6 +303,9 @@ export function rebuildEntries(state: PickerState): PickerState {
|
|
|
270
303
|
case "model":
|
|
271
304
|
state.entries = modelEntries(state);
|
|
272
305
|
break;
|
|
306
|
+
case "thinking":
|
|
307
|
+
state.entries = thinkingEntries();
|
|
308
|
+
break;
|
|
273
309
|
case "autotune":
|
|
274
310
|
state.entries = autotuneEntries();
|
|
275
311
|
break;
|
|
@@ -365,20 +401,34 @@ export function enter(state: PickerState): EnterResult {
|
|
|
365
401
|
}
|
|
366
402
|
|
|
367
403
|
case "model": {
|
|
368
|
-
|
|
404
|
+
// Selecting a model no longer commits — it stages the model in
|
|
405
|
+
// `drillModel` and advances to the thinking screen, so model + provider
|
|
406
|
+
// + thinking are written together (atomically) only when a level is
|
|
407
|
+
// chosen. An Esc before that leaves `edits` untouched.
|
|
408
|
+
state.drillModel = entry.value;
|
|
409
|
+
state.screen = "thinking";
|
|
410
|
+
state.cursor = 0;
|
|
411
|
+
return { type: "state", state: rebuildEntries(state) };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
case "thinking-level": {
|
|
415
|
+
// The single commit point: write model + provider + thinking together
|
|
416
|
+
// for the drilled phase, then clear all drill context and return to main.
|
|
417
|
+
const level = entry.value as ThinkingLevel;
|
|
418
|
+
if (state.drillPhase !== null && state.drillModel !== null) {
|
|
369
419
|
const phase = state.drillPhase;
|
|
370
|
-
state.edits.models[phase] =
|
|
420
|
+
state.edits.models[phase] = state.drillModel;
|
|
371
421
|
// `drillProvider` is the provider picked/typed on the provider screen,
|
|
372
|
-
// or `null` when the empty-registry skip jumped straight
|
|
373
|
-
// which case there is no provider, so `""`.
|
|
374
|
-
// `resolveProvider` term is unreachable in the pure module: a null
|
|
375
|
-
// `drillProvider` only ever co-occurs with an empty `groups`.)
|
|
422
|
+
// or `null` when the empty-registry skip jumped straight to the model
|
|
423
|
+
// screen — in which case there is no provider, so `""`.
|
|
376
424
|
state.edits.providers[phase] = state.drillProvider ?? "";
|
|
425
|
+
state.edits.thinking[phase] = level;
|
|
377
426
|
state.edits.changed = true;
|
|
378
427
|
}
|
|
379
428
|
state.screen = "main";
|
|
380
429
|
state.drillPhase = null;
|
|
381
430
|
state.drillProvider = null;
|
|
431
|
+
state.drillModel = null;
|
|
382
432
|
state.cursor = 0;
|
|
383
433
|
return { type: "state", state: rebuildEntries(state) };
|
|
384
434
|
}
|
|
@@ -428,6 +478,10 @@ export function back(state: PickerState): EnterResult {
|
|
|
428
478
|
state.screen = "main";
|
|
429
479
|
state.drillPhase = null;
|
|
430
480
|
state.drillProvider = null;
|
|
481
|
+
// Clearing `drillModel` here is what makes the model+provider+thinking write
|
|
482
|
+
// atomic: Esc from the thinking screen drops the staged model so no partial
|
|
483
|
+
// edit is ever committed.
|
|
484
|
+
state.drillModel = null;
|
|
431
485
|
state.textPrompt = null;
|
|
432
486
|
state.cursor = 0;
|
|
433
487
|
return { type: "state", state: rebuildEntries(state) };
|
|
@@ -470,16 +524,11 @@ export function submitText(state: PickerState, typed: string): PickerState {
|
|
|
470
524
|
state.screen = "model";
|
|
471
525
|
state.cursor = 0;
|
|
472
526
|
} else {
|
|
473
|
-
// prompt.for === "model":
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
state.edits.changed = true;
|
|
479
|
-
}
|
|
480
|
-
state.screen = "main";
|
|
481
|
-
state.drillPhase = null;
|
|
482
|
-
state.drillProvider = null;
|
|
527
|
+
// prompt.for === "model": stage the typed model in `drillModel` and advance
|
|
528
|
+
// to the thinking screen — the commit happens atomically when a level is
|
|
529
|
+
// chosen, never here. Mirrors the `model`-row `enter` path.
|
|
530
|
+
state.drillModel = value;
|
|
531
|
+
state.screen = "thinking";
|
|
483
532
|
state.cursor = 0;
|
|
484
533
|
}
|
|
485
534
|
|
|
@@ -60,6 +60,20 @@ export type PhaseModels = Record<Phase, string>;
|
|
|
60
60
|
/** The per-phase provider map — parallel to {@link PhaseModels}. */
|
|
61
61
|
export type PhaseProviders = Record<Phase, string>;
|
|
62
62
|
|
|
63
|
+
/** The six real pi effort levels, in ascending order. The single source of
|
|
64
|
+
* truth for thinking-level validity — no `max`/`ultracode` aliases exist. */
|
|
65
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
66
|
+
/** One of the six real pi effort levels. */
|
|
67
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
68
|
+
/** The per-phase thinking map — partial on purpose: an absent phase means
|
|
69
|
+
* "no thinking configured", which renders to no `thinking:` frontmatter. */
|
|
70
|
+
export type PhaseThinking = Partial<Record<Phase, ThinkingLevel>>;
|
|
71
|
+
|
|
72
|
+
/** Whether a value is one of the six real pi effort levels. */
|
|
73
|
+
export function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
74
|
+
return typeof value === "string" && (THINKING_LEVELS as readonly string[]).includes(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
/** Fallback models when `~/.pi/zero.json` has none — cheap to explore, strong
|
|
64
78
|
* to plan and review. */
|
|
65
79
|
const DEFAULT_MODELS: PhaseModels = {
|
|
@@ -105,11 +119,25 @@ export function readModels(data: Record<string, unknown>): PhaseModels {
|
|
|
105
119
|
const raw = (data.models ?? {}) as Record<string, unknown>;
|
|
106
120
|
const models: PhaseModels = { ...DEFAULT_MODELS };
|
|
107
121
|
for (const phase of PHASES) {
|
|
108
|
-
if (typeof raw[phase] === "string") models[phase] = raw[phase] as string;
|
|
122
|
+
if (typeof raw[phase] === "string") models[phase] = stripThinkingSuffix(raw[phase] as string);
|
|
109
123
|
}
|
|
110
124
|
return models;
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Strip a single whitespace-separated trailing token from a stored model string
|
|
129
|
+
* only when that token is a valid thinking level (legacy `"<model> <level>"`
|
|
130
|
+
* form). A non-level trailing token is left intact — dropping it would silently
|
|
131
|
+
* lose data — and a plain model id is returned unchanged.
|
|
132
|
+
*/
|
|
133
|
+
function stripThinkingSuffix(model: string): string {
|
|
134
|
+
const trimmed = model.trim();
|
|
135
|
+
const idx = trimmed.lastIndexOf(" ");
|
|
136
|
+
if (idx < 0) return model;
|
|
137
|
+
const tail = trimmed.slice(idx + 1);
|
|
138
|
+
return isThinkingLevel(tail) ? trimmed.slice(0, idx).trim() : model;
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
/**
|
|
114
142
|
* Extract the per-phase providers from a zero.json object. A missing provider
|
|
115
143
|
* is an empty string — the consumer resolves or ignores it.
|
|
@@ -123,16 +151,83 @@ export function readProviders(data: Record<string, unknown>): PhaseProviders {
|
|
|
123
151
|
return providers;
|
|
124
152
|
}
|
|
125
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Extract the per-phase thinking levels from a zero.json object.
|
|
156
|
+
*
|
|
157
|
+
* For each phase the explicit `thinking[phase]` value wins when it is a valid
|
|
158
|
+
* level. Otherwise a legacy model string of the form `"<model> <level>"` is
|
|
159
|
+
* mined: its trailing whitespace-separated token supplies the level, but only
|
|
160
|
+
* when that token is one of the six real levels. The result is a partial map —
|
|
161
|
+
* a phase with no valid or recoverable level is simply absent, never defaulted.
|
|
162
|
+
*/
|
|
163
|
+
export function readThinking(data: Record<string, unknown>): PhaseThinking {
|
|
164
|
+
const out: PhaseThinking = {};
|
|
165
|
+
const raw = (data.thinking ?? {}) as Record<string, unknown>;
|
|
166
|
+
const models = data.models as Record<string, unknown> | undefined;
|
|
167
|
+
for (const phase of PHASES) {
|
|
168
|
+
if (isThinkingLevel(raw[phase])) {
|
|
169
|
+
out[phase] = raw[phase];
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// Legacy recovery: "<model> <level>" with a valid trailing level.
|
|
173
|
+
const model = models?.[phase];
|
|
174
|
+
if (typeof model === "string") {
|
|
175
|
+
const trimmed = model.trim();
|
|
176
|
+
const tail = trimmed.split(/\s+/).slice(-1)[0];
|
|
177
|
+
if (trimmed.includes(" ") && isThinkingLevel(tail)) out[phase] = tail;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
126
183
|
/** A provider-qualified model assignment from the direct command form. */
|
|
127
184
|
export interface Assignment {
|
|
128
185
|
phase: Phase;
|
|
129
186
|
model: string;
|
|
130
187
|
provider?: string;
|
|
188
|
+
thinking?: ThinkingLevel;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The result of mining a thinking token out of an assignment value:
|
|
192
|
+
* the cleaned value plus the level, or the `"invalid"` sentinel when an
|
|
193
|
+
* explicit `thinking=` token names an unknown level. */
|
|
194
|
+
export type ThinkingTokenResult = { value: string; thinking?: ThinkingLevel } | "invalid";
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Mine a thinking level out of an assignment value.
|
|
198
|
+
*
|
|
199
|
+
* Precedence:
|
|
200
|
+
* 1. An explicit `thinking=<level>` token anywhere in the value. When the
|
|
201
|
+
* level is valid it is removed from the value and returned; when it is
|
|
202
|
+
* unknown the whole parse is `"invalid"` so the handler shows usage help.
|
|
203
|
+
* 2. Otherwise a trailing bare `<level>` token — recognized only when it is
|
|
204
|
+
* one of the six real levels; a non-level trailing token stays in the value.
|
|
205
|
+
* 3. Otherwise the value is returned unchanged with no thinking.
|
|
206
|
+
*/
|
|
207
|
+
export function parseThinkingToken(value: string): ThinkingTokenResult {
|
|
208
|
+
const tokens = value.trim().split(/\s+/);
|
|
209
|
+
// 1. explicit thinking=<level> anywhere in the value.
|
|
210
|
+
const idx = tokens.findIndex((t) => /^thinking=/i.test(t));
|
|
211
|
+
if (idx >= 0) {
|
|
212
|
+
const level = tokens[idx].slice("thinking=".length);
|
|
213
|
+
if (!isThinkingLevel(level)) return "invalid";
|
|
214
|
+
const rest = tokens.slice(0, idx).concat(tokens.slice(idx + 1));
|
|
215
|
+
return { value: rest.join(" "), thinking: level };
|
|
216
|
+
}
|
|
217
|
+
// 2. trailing bare <level> shorthand.
|
|
218
|
+
if (tokens.length > 1 && isThinkingLevel(tokens[tokens.length - 1])) {
|
|
219
|
+
return { value: tokens.slice(0, -1).join(" "), thinking: tokens[tokens.length - 1] };
|
|
220
|
+
}
|
|
221
|
+
// 3. no thinking token.
|
|
222
|
+
return { value: value.trim() };
|
|
131
223
|
}
|
|
132
224
|
|
|
133
225
|
/**
|
|
134
226
|
* Parse a direct `<phase>=<model>` assignment. The value may carry an explicit
|
|
135
|
-
* provider as `<provider>/<model>` — the first `/` splits them
|
|
227
|
+
* provider as `<provider>/<model>` — the first `/` splits them — and an optional
|
|
228
|
+
* thinking level via `thinking=<level>` or a trailing bare `<level>` shorthand.
|
|
229
|
+
* An invalid explicit thinking level makes the whole parse `null` so the handler
|
|
230
|
+
* writes nothing and shows usage help.
|
|
136
231
|
*/
|
|
137
232
|
export function parseAssignment(arg: string): Assignment | null {
|
|
138
233
|
const match = arg.trim().match(/^(\w+)\s*[=\s]\s*(.+)$/);
|
|
@@ -142,18 +237,40 @@ export function parseAssignment(arg: string): Assignment | null {
|
|
|
142
237
|
let value = match[2].trim();
|
|
143
238
|
if (value === "") return null;
|
|
144
239
|
|
|
240
|
+
const parsed = parseThinkingToken(value);
|
|
241
|
+
if (parsed === "invalid") return null;
|
|
242
|
+
value = parsed.value.trim();
|
|
243
|
+
if (value === "") return null;
|
|
244
|
+
const thinking = parsed.thinking;
|
|
245
|
+
|
|
145
246
|
const slash = value.indexOf("/");
|
|
146
247
|
if (slash > 0 && slash < value.length - 1) {
|
|
147
|
-
|
|
248
|
+
const out: Assignment = {
|
|
249
|
+
phase,
|
|
250
|
+
provider: value.slice(0, slash).trim(),
|
|
251
|
+
model: value.slice(slash + 1).trim(),
|
|
252
|
+
};
|
|
253
|
+
if (thinking) out.thinking = thinking;
|
|
254
|
+
return out;
|
|
148
255
|
}
|
|
149
|
-
|
|
256
|
+
const out: Assignment = { phase, model: value };
|
|
257
|
+
if (thinking) out.thinking = thinking;
|
|
258
|
+
return out;
|
|
150
259
|
}
|
|
151
260
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Render the per-phase model map as an aligned `provider/model` block, with the
|
|
263
|
+
* thinking level appended as ` · thinking <level>` for any phase that has one.
|
|
264
|
+
*/
|
|
265
|
+
export function formatPhases(
|
|
266
|
+
models: PhaseModels,
|
|
267
|
+
providers: PhaseProviders,
|
|
268
|
+
thinking: PhaseThinking,
|
|
269
|
+
): string {
|
|
154
270
|
return PHASES.map((phase) => {
|
|
155
271
|
const provider = providers[phase];
|
|
156
|
-
|
|
272
|
+
let label = provider ? `${provider}/${models[phase]}` : models[phase];
|
|
273
|
+
if (thinking[phase]) label += ` · thinking ${thinking[phase]}`;
|
|
157
274
|
return ` ${phase.padEnd(10)} ${label}`;
|
|
158
275
|
}).join("\n");
|
|
159
276
|
}
|
|
@@ -541,6 +658,7 @@ export default function register(pi?: PiExtensionAPI): void {
|
|
|
541
658
|
const data = readZeroJson();
|
|
542
659
|
const models = readModels(data);
|
|
543
660
|
const providers = readProviders(data);
|
|
661
|
+
const thinking = readThinking(data);
|
|
544
662
|
const groups = providerGroups(ctx.modelRegistry);
|
|
545
663
|
|
|
546
664
|
// Direct form: /zero-models build=claude-opus-4-7
|
|
@@ -566,11 +684,23 @@ export default function register(pi?: PiExtensionAPI): void {
|
|
|
566
684
|
return;
|
|
567
685
|
}
|
|
568
686
|
|
|
687
|
+
// A malformed explicit `thinking=<level>` gets a targeted usage help
|
|
688
|
+
// and writes nothing — distinct from the generic assignment help.
|
|
689
|
+
const thinkingToken = arg.match(/(?:^|\s)thinking=(\S+)/i);
|
|
690
|
+
if (thinkingToken && !isThinkingLevel(thinkingToken[1])) {
|
|
691
|
+
ctx.ui.notify(
|
|
692
|
+
"uso: thinking=<nivel> (nivel: off | minimal | low | medium | high | xhigh)",
|
|
693
|
+
"warning",
|
|
694
|
+
);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
569
698
|
const assignment = parseAssignment(arg);
|
|
570
699
|
if (!assignment) {
|
|
571
700
|
ctx.ui.notify(
|
|
572
|
-
"uso: /zero-models —o— /zero-models <fase>=[<provider>/]<modelo> " +
|
|
573
|
-
"(fase: explore | plan | build | veredicto
|
|
701
|
+
"uso: /zero-models —o— /zero-models <fase>=[<provider>/]<modelo> [thinking=<nivel>] " +
|
|
702
|
+
"(fase: explore | plan | build | veredicto · " +
|
|
703
|
+
"nivel: off | minimal | low | medium | high | xhigh) —o— " +
|
|
574
704
|
"/zero-models autotune=<modo>",
|
|
575
705
|
"warning",
|
|
576
706
|
);
|
|
@@ -581,15 +711,25 @@ export default function register(pi?: PiExtensionAPI): void {
|
|
|
581
711
|
assignment.provider ??
|
|
582
712
|
resolveProvider(ctx.modelRegistry, assignment.model) ??
|
|
583
713
|
providers[assignment.phase];
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
)
|
|
714
|
+
// Resolve the thinking map: start from the recovered/persisted levels
|
|
715
|
+
// (legacy suffixes already mined by `readThinking`), apply the new
|
|
716
|
+
// level when given, preserve the prior level when absent. `models` was
|
|
717
|
+
// read via `readModels`, which already stripped any valid legacy
|
|
718
|
+
// suffix — so the written store is normalized (model token only).
|
|
719
|
+
const nextThinking = { ...thinking };
|
|
720
|
+
if (assignment.thinking) nextThinking[assignment.phase] = assignment.thinking;
|
|
721
|
+
const merged: Record<string, unknown> = { ...data, models, providers };
|
|
722
|
+
if (Object.keys(nextThinking).length > 0) merged.thinking = nextThinking;
|
|
723
|
+
else delete merged.thinking;
|
|
724
|
+
writeFileSync(zeroJsonPath(), `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
589
725
|
const shown = providers[assignment.phase]
|
|
590
726
|
? `${providers[assignment.phase]}/${assignment.model}`
|
|
591
727
|
: assignment.model;
|
|
592
|
-
|
|
728
|
+
const level = nextThinking[assignment.phase];
|
|
729
|
+
ctx.ui.notify(
|
|
730
|
+
`zero models: ${assignment.phase} → ${shown}${level ? ` · thinking ${level}` : ""}`,
|
|
731
|
+
"info",
|
|
732
|
+
);
|
|
593
733
|
return;
|
|
594
734
|
}
|
|
595
735
|
|
|
@@ -600,6 +740,7 @@ export default function register(pi?: PiExtensionAPI): void {
|
|
|
600
740
|
const initialState = createPickerState({
|
|
601
741
|
models,
|
|
602
742
|
providers,
|
|
743
|
+
thinking,
|
|
603
744
|
autotuneMode,
|
|
604
745
|
pending,
|
|
605
746
|
groups,
|
|
@@ -618,7 +759,7 @@ export default function register(pi?: PiExtensionAPI): void {
|
|
|
618
759
|
// `zero.json` byte-for-byte unchanged, and report the leave-as-is
|
|
619
760
|
// state — the existing "sin cambios" notification text.
|
|
620
761
|
ctx.ui.notify(
|
|
621
|
-
`zero · modelos SDD (sin cambios):\n${formatPhases(models, providers)}\n` +
|
|
762
|
+
`zero · modelos SDD (sin cambios):\n${formatPhases(models, providers, thinking)}\n` +
|
|
622
763
|
` autotune ${autotuneMode}`,
|
|
623
764
|
"info",
|
|
624
765
|
);
|
|
@@ -634,21 +775,26 @@ export default function register(pi?: PiExtensionAPI): void {
|
|
|
634
775
|
models: edits.models,
|
|
635
776
|
providers: edits.providers,
|
|
636
777
|
};
|
|
778
|
+
// Persist the staged per-phase thinking map alongside models/
|
|
779
|
+
// providers; omit the key entirely when no phase has a level so the
|
|
780
|
+
// store stays byte-minimal and backward-compatible.
|
|
781
|
+
if (Object.keys(edits.thinking).length > 0) patch.thinking = edits.thinking;
|
|
637
782
|
if (edits.autotuneChanged) patch.autotune = edits.autotuneMode;
|
|
638
783
|
|
|
639
784
|
const merged = { ...data, ...patch };
|
|
785
|
+
if (Object.keys(edits.thinking).length === 0) delete merged.thinking;
|
|
640
786
|
if (edits.pendingApplied) delete merged.autotunePending;
|
|
641
787
|
writeFileSync(zeroJsonPath(), `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
642
788
|
|
|
643
789
|
const summary = [
|
|
644
|
-
`zero · modelos SDD guardados:\n${formatPhases(edits.models, edits.providers)}`,
|
|
790
|
+
`zero · modelos SDD guardados:\n${formatPhases(edits.models, edits.providers, edits.thinking)}`,
|
|
645
791
|
];
|
|
646
792
|
summary.push(` autotune ${edits.autotuneMode}`);
|
|
647
793
|
if (edits.pendingApplied) summary.push("sugerencia aplicada");
|
|
648
794
|
ctx.ui.notify(summary.join("\n"), "info");
|
|
649
795
|
} else {
|
|
650
796
|
ctx.ui.notify(
|
|
651
|
-
`zero · modelos SDD (sin cambios):\n${formatPhases(edits.models, edits.providers)}\n` +
|
|
797
|
+
`zero · modelos SDD (sin cambios):\n${formatPhases(edits.models, edits.providers, edits.thinking)}\n` +
|
|
652
798
|
` autotune ${edits.autotuneMode}`,
|
|
653
799
|
"info",
|
|
654
800
|
);
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// claude-opus-4-7 · tok ↑12.3K ↓4.1K · diff +50/-12 · ctx 45% · master · www.ceroclawd.com
|
|
6
6
|
//
|
|
7
|
-
// Themed: model
|
|
8
|
-
// by load, branch steel, brand
|
|
7
|
+
// Themed (sunset): model coral, tokens gold/peach, diff mint/rose, ctx
|
|
8
|
+
// mint→amber→rose by load, branch steel, brand orchid. Pure 24-bit ANSI.
|
|
9
9
|
//
|
|
10
10
|
// Refreshes on `session_start`, `model_select`, `message_update`
|
|
11
11
|
// (accumulates tokens), and `tool_execution_end` (re-reads git, since tools
|
|
@@ -17,17 +17,18 @@ import { promisify } from "node:util";
|
|
|
17
17
|
|
|
18
18
|
const execAsync = promisify(exec);
|
|
19
19
|
|
|
20
|
-
// ─── Color palette (matches the zero-
|
|
20
|
+
// ─── Color palette (matches the zero-sunset theme `vars`) ─────────────────────
|
|
21
21
|
|
|
22
22
|
type RGB = [number, number, number];
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
23
|
+
const CORAL: RGB = [255, 124, 92]; // model
|
|
24
|
+
const GOLD: RGB = [255, 214, 130]; // tokens in
|
|
25
|
+
const PEACH: RGB = [255, 168, 99]; // tokens out
|
|
26
|
+
const ORCHID: RGB = [176, 106, 179]; // brand
|
|
27
|
+
const MINT: RGB = [79, 221, 171]; // diff added / ctx ok (semantic)
|
|
28
|
+
const AMBER: RGB = [238, 190, 92]; // ctx mid (semantic)
|
|
29
|
+
const ROSE: RGB = [255, 106, 122]; // diff removed / ctx hot (semantic)
|
|
30
|
+
const STEEL: RGB = [176, 152, 142]; // branch (warm gray)
|
|
31
|
+
const DIM: RGB = [120, 104, 98]; // labels / separators (warm gray)
|
|
31
32
|
|
|
32
33
|
function fg([r, g, b]: RGB, text: string): string {
|
|
33
34
|
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
@@ -78,11 +79,11 @@ const SEP = fg(DIM, " · ");
|
|
|
78
79
|
export function composeStatusline(p: StatuslineParts): string {
|
|
79
80
|
const parts: string[] = [];
|
|
80
81
|
|
|
81
|
-
if (p.model) parts.push(fg(
|
|
82
|
+
if (p.model) parts.push(fg(CORAL, p.model));
|
|
82
83
|
|
|
83
84
|
if (p.tokensIn != null || p.tokensOut != null) {
|
|
84
|
-
const inS = fg(
|
|
85
|
-
const outS = fg(
|
|
85
|
+
const inS = fg(GOLD, `↑${formatTokenCount(p.tokensIn ?? 0)}`);
|
|
86
|
+
const outS = fg(PEACH, `↓${formatTokenCount(p.tokensOut ?? 0)}`);
|
|
86
87
|
parts.push(`${fg(DIM, "tok")} ${inS} ${outS}`);
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -98,7 +99,7 @@ export function composeStatusline(p: StatuslineParts): string {
|
|
|
98
99
|
|
|
99
100
|
if (p.branch) parts.push(fg(STEEL, p.branch));
|
|
100
101
|
|
|
101
|
-
if (p.brand) parts.push(fg(
|
|
102
|
+
if (p.brand) parts.push(fg(ORCHID, p.brand));
|
|
102
103
|
|
|
103
104
|
return parts.join(SEP);
|
|
104
105
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
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": [
|
package/prompts/orchestrator.md
CHANGED
|
@@ -178,11 +178,19 @@ on the sub-agent to discover it alone.
|
|
|
178
178
|
## Model configuration
|
|
179
179
|
|
|
180
180
|
The per-phase model assignments live in `~/.pi/zero.json`: `models` maps each
|
|
181
|
-
phase (`explore`, `plan`, `build`, `veredicto`) to a model id,
|
|
182
|
-
`providers` map gives the provider that model belongs to
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
phase (`explore`, `plan`, `build`, `veredicto`) to a model id, the parallel
|
|
182
|
+
`providers` map gives the provider that model belongs to, and a parallel
|
|
183
|
+
`thinking` map gives the pi effort level (`off`, `minimal`, `low`, `medium`,
|
|
184
|
+
`high`, `xhigh`) for each phase. Read that file at the start of a run and
|
|
185
|
+
delegate each phase's sub-agent to its configured provider + model. When the
|
|
186
|
+
file is absent, a phase is missing, or its provider entry is empty, fall back to
|
|
187
|
+
the session's default model.
|
|
188
|
+
|
|
189
|
+
The `thinking` map is optional and partial: a phase with no entry simply gets no
|
|
190
|
+
`thinking:` line in its generated `zero-<phase>.md` frontmatter — no aggressive
|
|
191
|
+
default is written. When a phase has a valid level, the generated agent file
|
|
192
|
+
carries a `thinking: <level>` line in its frontmatter (placed after `model:` and
|
|
193
|
+
before `systemPromptMode:`), so the sub-agent runs at that effort level.
|
|
186
194
|
|
|
187
195
|
## Language Boundary
|
|
188
196
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
+
"name": "zero-sunset",
|
|
4
|
+
"vars": {
|
|
5
|
+
"gold": "#ffd682",
|
|
6
|
+
"peach": "#ffa863",
|
|
7
|
+
"coral": "#ff7c5c",
|
|
8
|
+
"rose": "#ff5c7a",
|
|
9
|
+
"magenta": "#d64a8c",
|
|
10
|
+
"orchid": "#b06ab3",
|
|
11
|
+
"sky": "#6db8e6",
|
|
12
|
+
"mint": "#5fd6a0",
|
|
13
|
+
"amber": "#eebe5c",
|
|
14
|
+
"steel": "#b0978f",
|
|
15
|
+
"dimSteel": "#7d6a64",
|
|
16
|
+
"panel": "#1c1117",
|
|
17
|
+
"selected": "#3a2230",
|
|
18
|
+
"okPanel": "#152318",
|
|
19
|
+
"errPanel": "#2e1518"
|
|
20
|
+
},
|
|
21
|
+
"colors": {
|
|
22
|
+
"accent": "coral",
|
|
23
|
+
"border": "orchid",
|
|
24
|
+
"borderAccent": "gold",
|
|
25
|
+
"borderMuted": "dimSteel",
|
|
26
|
+
"success": "mint",
|
|
27
|
+
"error": "rose",
|
|
28
|
+
"warning": "amber",
|
|
29
|
+
"muted": "steel",
|
|
30
|
+
"dim": "dimSteel",
|
|
31
|
+
"text": "",
|
|
32
|
+
"thinkingText": "steel",
|
|
33
|
+
"selectedBg": "selected",
|
|
34
|
+
"userMessageBg": "panel",
|
|
35
|
+
"userMessageText": "coral",
|
|
36
|
+
"customMessageBg": "panel",
|
|
37
|
+
"customMessageText": "",
|
|
38
|
+
"customMessageLabel": "gold",
|
|
39
|
+
"toolPendingBg": "panel",
|
|
40
|
+
"toolSuccessBg": "okPanel",
|
|
41
|
+
"toolErrorBg": "errPanel",
|
|
42
|
+
"toolTitle": "peach",
|
|
43
|
+
"toolOutput": "steel",
|
|
44
|
+
"mdHeading": "gold",
|
|
45
|
+
"mdLink": "peach",
|
|
46
|
+
"mdLinkUrl": "dimSteel",
|
|
47
|
+
"mdCode": "mint",
|
|
48
|
+
"mdCodeBlock": "mint",
|
|
49
|
+
"mdCodeBlockBorder": "orchid",
|
|
50
|
+
"mdQuote": "steel",
|
|
51
|
+
"mdQuoteBorder": "dimSteel",
|
|
52
|
+
"mdHr": "dimSteel",
|
|
53
|
+
"mdListBullet": "gold",
|
|
54
|
+
"toolDiffAdded": "mint",
|
|
55
|
+
"toolDiffRemoved": "rose",
|
|
56
|
+
"toolDiffContext": "steel",
|
|
57
|
+
"syntaxComment": "dimSteel",
|
|
58
|
+
"syntaxKeyword": "magenta",
|
|
59
|
+
"syntaxFunction": "peach",
|
|
60
|
+
"syntaxVariable": "gold",
|
|
61
|
+
"syntaxString": "mint",
|
|
62
|
+
"syntaxNumber": "rose",
|
|
63
|
+
"syntaxType": "sky",
|
|
64
|
+
"syntaxOperator": "coral",
|
|
65
|
+
"syntaxPunctuation": "steel",
|
|
66
|
+
"thinkingOff": "dimSteel",
|
|
67
|
+
"thinkingMinimal": "steel",
|
|
68
|
+
"thinkingLow": "sky",
|
|
69
|
+
"thinkingMedium": "peach",
|
|
70
|
+
"thinkingHigh": "coral",
|
|
71
|
+
"thinkingXhigh": "rose",
|
|
72
|
+
"bashMode": "gold"
|
|
73
|
+
},
|
|
74
|
+
"export": {
|
|
75
|
+
"pageBg": "#160e13",
|
|
76
|
+
"cardBg": "#1c1117",
|
|
77
|
+
"infoBg": "#241620"
|
|
78
|
+
}
|
|
79
|
+
}
|