@gonrocca/zero-pi 0.1.27 → 0.1.28
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 +1 -3
- package/extensions/autotune-extension.ts +2 -2
- package/package.json +2 -4
- package/extensions/startup-banner.ts +0 -280
package/README.md
CHANGED
|
@@ -64,7 +64,6 @@ into `/forge` for you.
|
|
|
64
64
|
| **`/zero-sync`** | Folds each run's spec delta into a canonical, project-wide spec store. |
|
|
65
65
|
| **Run memory** | Every run recalls and saves traces to Cortex, so runs learn from each other. |
|
|
66
66
|
| **Provider guard** | Warns when the `anthropic` provider runs on a metered API key instead of your subscription. |
|
|
67
|
-
| **Startup banner** | The animated `ZERO` wordmark — Tetris-cell assembly + a sparkle pass. |
|
|
68
67
|
| **Working-phrase ticker** | Swaps pi's `Working...` for a context-aware Spanish phrase + spinner. |
|
|
69
68
|
| **Conversation resume** | Writes `.pi/zero-resume.md` on exit — the restore command + a conversation tail. |
|
|
70
69
|
| **Windows tree-kill** | Aborting a turn kills the whole process tree — no orphaned `claude`. |
|
|
@@ -84,8 +83,7 @@ into `/forge` for you.
|
|
|
84
83
|
|
|
85
84
|
zero-pi keeps its state in `~/.pi/zero.json` (per-phase models + autotune mode)
|
|
86
85
|
and `~/.pi/zero-runs.jsonl` (the run-metrics log); per-project artifacts live
|
|
87
|
-
under `.sdd/`.
|
|
88
|
-
`off`) and `ZERO_RESUME` (`off` disables the resume note).
|
|
86
|
+
under `.sdd/`. Set `ZERO_RESUME=off` to disable the conversation-resume note.
|
|
89
87
|
|
|
90
88
|
## 🔗 Relationship to `zero`
|
|
91
89
|
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
//
|
|
16
16
|
// All decisions live in `autotune.ts`; this file only reads files, calls those
|
|
17
17
|
// functions, and applies/notifies. The whole handler is wrapped in a swallowing
|
|
18
|
-
// `try/catch` —
|
|
19
|
-
//
|
|
18
|
+
// `try/catch` — a failure must never break a pi session. The package stays
|
|
19
|
+
// dependency-free: `node:fs`/`node:os`/`node:path`
|
|
20
20
|
// only, plus minimal local interfaces for the pi API.
|
|
21
21
|
|
|
22
22
|
import { readFileSync, writeFileSync } from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow,
|
|
3
|
+
"version": "0.1.28",
|
|
4
|
+
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, per-phase model autotune, and skill auto-learning. Adds capability to pi without modifying pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"./themes"
|
|
24
24
|
],
|
|
25
25
|
"extensions": [
|
|
26
|
-
"./extensions/startup-banner.ts",
|
|
27
26
|
"./extensions/working-phrases.ts",
|
|
28
27
|
"./extensions/win-tree-kill.ts",
|
|
29
28
|
"./extensions/sdd-agents.ts",
|
|
@@ -38,7 +37,6 @@
|
|
|
38
37
|
"prompts",
|
|
39
38
|
"skills",
|
|
40
39
|
"themes",
|
|
41
|
-
"extensions/startup-banner.ts",
|
|
42
40
|
"extensions/working-phrases.ts",
|
|
43
41
|
"extensions/win-tree-kill.ts",
|
|
44
42
|
"extensions/sdd-agents.ts",
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
// zero-pi - animated startup banner.
|
|
2
|
-
//
|
|
3
|
-
// A pi extension that renders the ZERO wordmark as ASCII-safe Tetris cells.
|
|
4
|
-
// The animated mode progressively assembles the wordmark from the bottom up,
|
|
5
|
-
// then settles into the complete banner. The extension stays dependency-free
|
|
6
|
-
// and keeps the historical ZERO_BANNER controls.
|
|
7
|
-
|
|
8
|
-
type RGB = [number, number, number];
|
|
9
|
-
|
|
10
|
-
const COLORS: Record<string, RGB> = {
|
|
11
|
-
cyan: [80, 210, 255],
|
|
12
|
-
blue: [116, 151, 255],
|
|
13
|
-
mint: [79, 221, 171],
|
|
14
|
-
amber: [238, 190, 92],
|
|
15
|
-
rose: [255, 106, 122],
|
|
16
|
-
violet: [175, 138, 255],
|
|
17
|
-
peak: [255, 245, 218],
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/** Number of rows in every glyph. */
|
|
21
|
-
const ROWS = 6;
|
|
22
|
-
|
|
23
|
-
// Five-cell-wide glyphs. Non-space characters become visible Tetris cells.
|
|
24
|
-
const FONT: Record<string, string[]> = {
|
|
25
|
-
Z: ["ZZZZZ", " ZZ", " ZZ ", " ZZ ", "ZZ ", "ZZZZZ"],
|
|
26
|
-
E: ["EEEEE", "EE ", "EEEE ", "EE ", "EE ", "EEEEE"],
|
|
27
|
-
R: ["RRRR ", "RR RR", "RRRR ", "RR RR", "RR R", "RR R"],
|
|
28
|
-
O: [" OOO ", "OO OO", "OO OO", "OO OO", "OO OO", " OOO "],
|
|
29
|
-
P: ["PPPP ", "PP PP", "PP PP", "PPPP ", "PP ", "PP "],
|
|
30
|
-
I: ["IIIII", " II ", " II ", " II ", " II ", "IIIII"],
|
|
31
|
-
"-": [" ", " ", "-----", " ", " ", " "],
|
|
32
|
-
" ": [" ", " ", " ", " ", " ", " "],
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const PIECE_COLORS: Record<string, RGB> = {
|
|
36
|
-
Z: COLORS.cyan,
|
|
37
|
-
E: COLORS.amber,
|
|
38
|
-
R: COLORS.mint,
|
|
39
|
-
O: COLORS.rose,
|
|
40
|
-
P: COLORS.violet,
|
|
41
|
-
I: COLORS.blue,
|
|
42
|
-
"-": COLORS.amber,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
interface Matrix {
|
|
46
|
-
rows: string[];
|
|
47
|
-
width: number;
|
|
48
|
-
totalCells: number;
|
|
49
|
-
ranks: Map<string, number>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function glyphFor(ch: string): string[] {
|
|
53
|
-
return FONT[ch.toUpperCase()] ?? FONT[" "];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function matrixForText(text: string): Matrix {
|
|
57
|
-
const rows = Array.from({ length: ROWS }, () => "");
|
|
58
|
-
for (const [index, raw] of Array.from(text).entries()) {
|
|
59
|
-
const glyph = glyphFor(raw);
|
|
60
|
-
for (let r = 0; r < ROWS; r++) {
|
|
61
|
-
rows[r] += (index === 0 ? "" : " ") + glyph[r];
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const cells: Array<{ row: number; col: number }> = [];
|
|
66
|
-
for (let row = 0; row < ROWS; row++) {
|
|
67
|
-
for (let col = 0; col < rows[row].length; col++) {
|
|
68
|
-
if (rows[row][col] !== " ") cells.push({ row, col });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Tetris reads as a pile building upward: bottom rows settle first.
|
|
73
|
-
cells.sort((a, b) => b.row - a.row || a.col - b.col);
|
|
74
|
-
|
|
75
|
-
const ranks = new Map<string, number>();
|
|
76
|
-
for (const [rank, cell] of cells.entries()) {
|
|
77
|
-
ranks.set(`${cell.row}:${cell.col}`, rank);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
rows,
|
|
82
|
-
width: rows[0]?.length ?? 0,
|
|
83
|
-
totalCells: cells.length,
|
|
84
|
-
ranks,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Lay a word out as six rows of ASCII-safe Tetris cells. */
|
|
89
|
-
export function bannerLines(text = "ZERO"): string[] {
|
|
90
|
-
return matrixForText(text).rows.map((row) =>
|
|
91
|
-
Array.from(row)
|
|
92
|
-
.map((cell) => (cell === " " ? " " : "[]"))
|
|
93
|
-
.join(""),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function lerp(a: number, b: number, t: number): number {
|
|
98
|
-
return a + (b - a) * t;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function lerpColor(c1: RGB, c2: RGB, t: number): RGB {
|
|
102
|
-
const k = Math.max(0, Math.min(1, t));
|
|
103
|
-
return [
|
|
104
|
-
Math.round(lerp(c1[0], c2[0], k)),
|
|
105
|
-
Math.round(lerp(c1[1], c2[1], k)),
|
|
106
|
-
Math.round(lerp(c1[2], c2[2], k)),
|
|
107
|
-
];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function ansiFg([r, g, b]: RGB, text: string): string {
|
|
111
|
-
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function colorForPiece(piece: string, row: number, rank: number, active: boolean): RGB {
|
|
115
|
-
if (active) return COLORS.peak;
|
|
116
|
-
const base = PIECE_COLORS[piece] ?? COLORS.blue;
|
|
117
|
-
const heightWarmth = (ROWS - 1 - row) / Math.max(1, ROWS - 1);
|
|
118
|
-
const rankPulse = (rank % 7) / 18;
|
|
119
|
-
return lerpColor(base, COLORS.peak, heightWarmth * 0.18 + rankPulse);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function paintMatrixLine(
|
|
123
|
-
matrix: Matrix,
|
|
124
|
-
row: number,
|
|
125
|
-
visibleCells: number,
|
|
126
|
-
sparkles?: ReadonlySet<string>,
|
|
127
|
-
): string {
|
|
128
|
-
let out = "";
|
|
129
|
-
const line = matrix.rows[row] ?? "";
|
|
130
|
-
for (let col = 0; col < matrix.width; col++) {
|
|
131
|
-
const piece = line[col] ?? " ";
|
|
132
|
-
if (piece === " ") {
|
|
133
|
-
out += " ";
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const key = `${row}:${col}`;
|
|
137
|
-
const rank = matrix.ranks.get(key) ?? Number.POSITIVE_INFINITY;
|
|
138
|
-
if (rank >= visibleCells) {
|
|
139
|
-
out += " ";
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
// A cell glints when it is the assembly's leading cell or when this
|
|
143
|
-
// sparkle frame picked it.
|
|
144
|
-
const glint = rank === visibleCells - 1 || (sparkles?.has(key) ?? false);
|
|
145
|
-
out += ansiFg(colorForPiece(piece, row, rank, glint), "[]");
|
|
146
|
-
}
|
|
147
|
-
return out;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Pick a handful of distinct settled cells to glint for one sparkle frame. */
|
|
151
|
-
function pickSparkles(matrix: Matrix, count: number, rand: () => number): Set<string> {
|
|
152
|
-
const keys = Array.from(matrix.ranks.keys());
|
|
153
|
-
const chosen = new Set<string>();
|
|
154
|
-
const target = Math.min(Math.max(0, count), keys.length);
|
|
155
|
-
for (let guard = 0; chosen.size < target && guard < target * 8; guard++) {
|
|
156
|
-
chosen.add(keys[Math.floor(rand() * keys.length)]);
|
|
157
|
-
}
|
|
158
|
-
return chosen;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
interface OutStream {
|
|
162
|
-
write(chunk: string): unknown;
|
|
163
|
-
isTTY?: boolean;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Whether colour output is appropriate for the given stream. */
|
|
167
|
-
function shouldColor(stream: OutStream): boolean {
|
|
168
|
-
if (process.env.NO_COLOR) return false;
|
|
169
|
-
if (process.env.FORCE_COLOR) return true;
|
|
170
|
-
return Boolean(stream && stream.isTTY);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** The three banner modes, resolved from the ZERO_BANNER environment variable. */
|
|
174
|
-
export type BannerMode = "off" | "static" | "shimmer";
|
|
175
|
-
|
|
176
|
-
/** Resolve the banner mode from an environment-variable value. */
|
|
177
|
-
export function resolveBannerMode(raw: string | undefined): BannerMode {
|
|
178
|
-
const value = (raw ?? "").trim().toLowerCase();
|
|
179
|
-
if (value === "off" || value === "0" || value === "false" || value === "none") return "off";
|
|
180
|
-
if (value === "static" || value === "plain" || value === "no-animate") return "static";
|
|
181
|
-
return "shimmer";
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Atomics.wait gives us a synchronous sleep without adding a dependency. The
|
|
185
|
-
// banner renders before Pi's TUI takes over, so blocking briefly is intentional.
|
|
186
|
-
const SLEEP_SLOT = new Int32Array(new SharedArrayBuffer(4));
|
|
187
|
-
function sleepSync(ms: number): void {
|
|
188
|
-
if (ms > 0) Atomics.wait(SLEEP_SLOT, 0, 0, ms);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function easeOutCubic(t: number): number {
|
|
192
|
-
return 1 - Math.pow(1 - t, 3);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Options for {@link renderBanner}. */
|
|
196
|
-
export interface RenderOptions {
|
|
197
|
-
mode?: BannerMode;
|
|
198
|
-
text?: string;
|
|
199
|
-
frames?: number;
|
|
200
|
-
frameMs?: number;
|
|
201
|
-
/** Number of post-settle sparkle frames (shimmer mode). `0` disables it. */
|
|
202
|
-
sparkleFrames?: number;
|
|
203
|
-
/** Delay between sparkle frames, in milliseconds. */
|
|
204
|
-
sparkleMs?: number;
|
|
205
|
-
stream?: OutStream;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Render the ZERO banner synchronously.
|
|
210
|
-
*
|
|
211
|
-
* On a non-colour stream the banner is printed plain. Otherwise the animated
|
|
212
|
-
* mode assembles cells frame by frame, then settles on the completed wordmark.
|
|
213
|
-
*/
|
|
214
|
-
export function renderBanner(options: RenderOptions = {}): void {
|
|
215
|
-
const mode = options.mode ?? resolveBannerMode(process.env.ZERO_BANNER);
|
|
216
|
-
if (mode === "off") return;
|
|
217
|
-
|
|
218
|
-
const stream: OutStream = options.stream ?? process.stdout;
|
|
219
|
-
const matrix = matrixForText(options.text ?? "ZERO");
|
|
220
|
-
|
|
221
|
-
if (!shouldColor(stream)) {
|
|
222
|
-
stream.write("\n" + bannerLines(options.text ?? "ZERO").join("\n") + "\n\n");
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
stream.write("\n" + Array.from({ length: ROWS }, () => "").join("\n") + "\n");
|
|
227
|
-
|
|
228
|
-
// Repaint the six rows in place; optionally with this frame's sparkle set.
|
|
229
|
-
const paintRows = (sparkles?: ReadonlySet<string>): void => {
|
|
230
|
-
stream.write(`\x1b[${ROWS}A\r`);
|
|
231
|
-
for (let r = 0; r < ROWS; r++) {
|
|
232
|
-
stream.write("\x1b[2K" + paintMatrixLine(matrix, r, matrix.totalCells, sparkles) + "\n");
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
if (mode === "shimmer") {
|
|
237
|
-
const frames = Math.max(2, options.frames ?? 42);
|
|
238
|
-
const frameMs = Math.max(1, options.frameMs ?? 18);
|
|
239
|
-
for (let f = 0; f < frames; f++) {
|
|
240
|
-
const t = easeOutCubic(f / (frames - 1));
|
|
241
|
-
const visibleCells = Math.max(1, Math.ceil(matrix.totalCells * t));
|
|
242
|
-
stream.write(`\x1b[${ROWS}A\r`);
|
|
243
|
-
for (let r = 0; r < ROWS; r++) {
|
|
244
|
-
stream.write("\x1b[2K" + paintMatrixLine(matrix, r, visibleCells) + "\n");
|
|
245
|
-
}
|
|
246
|
-
sleepSync(frameMs);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Settle on the completed wordmark.
|
|
251
|
-
paintRows();
|
|
252
|
-
|
|
253
|
-
// Sparkle pass — a few settled cells glint bright each frame, then a clean
|
|
254
|
-
// final settle. Shimmer only; `sparkleFrames: 0` disables it.
|
|
255
|
-
if (mode === "shimmer") {
|
|
256
|
-
const sparkleFrames = Math.max(0, options.sparkleFrames ?? 22);
|
|
257
|
-
const sparkleMs = Math.max(1, options.sparkleMs ?? 36);
|
|
258
|
-
for (let f = 0; f < sparkleFrames; f++) {
|
|
259
|
-
paintRows(pickSparkles(matrix, 3 + (f % 4), Math.random));
|
|
260
|
-
sleepSync(sparkleMs);
|
|
261
|
-
}
|
|
262
|
-
if (sparkleFrames > 0) paintRows();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
stream.write("\n");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* The pi extension entry point.
|
|
270
|
-
*
|
|
271
|
-
* The pi argument is accepted for API compatibility. The banner is rendered
|
|
272
|
-
* defensively: visual sugar should never break an interactive session.
|
|
273
|
-
*/
|
|
274
|
-
export default function register(_pi?: unknown): void {
|
|
275
|
-
try {
|
|
276
|
-
renderBanner();
|
|
277
|
-
} catch {
|
|
278
|
-
// A banner failure must never break a pi session.
|
|
279
|
-
}
|
|
280
|
-
}
|