gentle-pi 3.3.0 → 3.4.0
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 +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +26 -2
- package/docs/readme-reference.md +32 -6
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +8 -1
- package/extensions/gentle-ai.ts +59 -18
- package/extensions/gentle-shell.ts +99 -10
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/inprocess-reviewer.ts +38 -1
- package/lib/native-review-cli.ts +36 -10
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +10 -0
- package/lib/review-integration-v2.ts +4 -1
- package/lib/shell-bar.ts +13 -0
- package/lib/shell-sidebar-layout.ts +10 -4
- package/lib/shell-usage-view.ts +5 -2
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +35 -9
- package/runtime/review-integration-v2.mjs +4 -1
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -3
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +9 -0
- package/tests/gentle-agents.test.ts +71 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +56 -4
- package/tests/gentle-shell.test.ts +355 -2
- package/tests/inprocess-reviewer.test.ts +92 -0
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay.test.ts +21 -0
- package/tests/review-integration-v2.test.ts +30 -0
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +107 -2
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/shell-bar.test.ts +8 -0
- package/tests/shell-sidebar-layout.test.ts +60 -5
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/skills/release/SKILL.md +0 -137
|
@@ -2,7 +2,9 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { syncBuiltinESMExports } from "node:module";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
import startup, { readGitBranch } from "../extensions/startup-banner.ts";
|
|
7
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
10
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -74,6 +76,15 @@ test("startup branch lookup uses direct git argv and hides its Windows child", a
|
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
test("startup banner keeps animating after invalidate and cleans up on dispose", async (t) => {
|
|
79
|
+
const home = mkdtempSync(join(tmpdir(), "gp-banner-quality-"));
|
|
80
|
+
writeFileSync(join(home, "animations.json"), '{"schema":"gentle-pi.animations/v1","policy":"quality"}');
|
|
81
|
+
const previousHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
82
|
+
process.env.GENTLE_PI_CONFIG_HOME = home;
|
|
83
|
+
t.after(() => {
|
|
84
|
+
if (previousHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
85
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previousHome;
|
|
86
|
+
rmSync(home, { recursive: true, force: true });
|
|
87
|
+
});
|
|
77
88
|
t.mock.timers.enable({ apis: ["setTimeout", "setInterval", "Date"] });
|
|
78
89
|
t.mock.method(fs, "readFile", async () => JSON.stringify({ showRose: true, showTextLogo: true, color: "pink" }));
|
|
79
90
|
syncBuiltinESMExports();
|
|
@@ -115,6 +126,85 @@ test("startup banner keeps animating after invalidate and cleans up on dispose",
|
|
|
115
126
|
assert.equal(renders, afterDispose, "session_shutdown cleanup stays idle");
|
|
116
127
|
});
|
|
117
128
|
|
|
129
|
+
test("animation modes retain banner lifetime policy, final artwork and approximate duration", async (t) => {
|
|
130
|
+
const home = mkdtempSync(join(tmpdir(), "gp-banner-animations-"));
|
|
131
|
+
const previousHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
132
|
+
process.env.GENTLE_PI_CONFIG_HOME = home;
|
|
133
|
+
t.after(() => {
|
|
134
|
+
if (previousHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
135
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previousHome;
|
|
136
|
+
rmSync(home, { recursive: true, force: true });
|
|
137
|
+
});
|
|
138
|
+
t.mock.method(fs, "readFile", async () => JSON.stringify({ showRose: true, showTextLogo: true, color: "pink" }));
|
|
139
|
+
syncBuiltinESMExports();
|
|
140
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
141
|
+
const argv = process.argv;
|
|
142
|
+
process.argv = ["node"];
|
|
143
|
+
t.after(() => { process.argv = argv; });
|
|
144
|
+
for (const [key, value] of [["rows", 40], ["columns", 200]] as const) {
|
|
145
|
+
const descriptor = Object.getOwnPropertyDescriptor(process.stdout, key);
|
|
146
|
+
Object.defineProperty(process.stdout, key, { configurable: true, writable: true, value });
|
|
147
|
+
t.after(() => descriptor ? Object.defineProperty(process.stdout, key, descriptor) : Reflect.deleteProperty(process.stdout, key));
|
|
148
|
+
}
|
|
149
|
+
let boot: () => void;
|
|
150
|
+
let pulse: () => void;
|
|
151
|
+
let active = false;
|
|
152
|
+
let delay = 0;
|
|
153
|
+
let clock = 0;
|
|
154
|
+
let paints = 0;
|
|
155
|
+
t.mock.method(Date, "now", () => clock);
|
|
156
|
+
t.mock.method(globalThis, "setTimeout", (callback: () => void, ms: number) => {
|
|
157
|
+
if (ms === 50) boot = callback; // Never run operational stats/home reads.
|
|
158
|
+
return {};
|
|
159
|
+
});
|
|
160
|
+
t.mock.method(globalThis, "setInterval", (callback: () => void, ms: number) => {
|
|
161
|
+
pulse = callback; delay = ms; active = true; return {};
|
|
162
|
+
});
|
|
163
|
+
t.mock.method(globalThis, "clearInterval", () => { active = false; });
|
|
164
|
+
let start: (event: unknown, ctx: unknown) => Promise<void>;
|
|
165
|
+
let shutdown: () => void;
|
|
166
|
+
let header: { render(width: number): string[]; dispose(): void };
|
|
167
|
+
// Stroke warmup is module-global; keep duration driving isolated from cold-start tests.
|
|
168
|
+
const { default: isolatedStartup } = await import(new URL("../extensions/startup-banner.ts?animations", import.meta.url).href) as typeof import("../extensions/startup-banner.ts");
|
|
169
|
+
isolatedStartup({ on: (name: string, fn: typeof start) => {
|
|
170
|
+
if (name === "session_start") start = fn;
|
|
171
|
+
if (name === "session_shutdown") shutdown = fn as unknown as () => void;
|
|
172
|
+
}, registerCommand() {}, getCommands: () => [], getAllTools: () => [] } as unknown as ExtensionAPI);
|
|
173
|
+
const ctx = { hasUI: true, cwd: "/fixture", ui: { setHeader(factory: (tui: unknown, theme: unknown) => typeof header) {
|
|
174
|
+
header = factory({ requestRender() { paints++; } }, { fg: (_role: string, text: string) => text });
|
|
175
|
+
} } };
|
|
176
|
+
const save = (policy: string) => writeFileSync(join(home, "animations.json"), JSON.stringify({ schema: "gentle-pi.animations/v1", policy }));
|
|
177
|
+
try {
|
|
178
|
+
save("potato");
|
|
179
|
+
await start!({}, ctx); boot!();
|
|
180
|
+
assert.equal(active, false, "potato starts no animation interval");
|
|
181
|
+
const staticArt = stripAnsi(header!.render(200).join("\n"));
|
|
182
|
+
assert.match(staticArt, /[▄▀█]/);
|
|
183
|
+
assert.match(staticArt, /[\u2800-\u28ff]/);
|
|
184
|
+
header!.dispose();
|
|
185
|
+
let qualityDuration = 0;
|
|
186
|
+
let qualityPaints = 0;
|
|
187
|
+
for (const policy of ["quality", "performance"]) {
|
|
188
|
+
save(policy);
|
|
189
|
+
await start!({}, ctx); boot!();
|
|
190
|
+
for (let i = 0; i < 15; i++) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
191
|
+
assert.equal(delay, policy === "quality" ? 25 : 250);
|
|
192
|
+
save("potato"); // Already-created animation keeps its policy.
|
|
193
|
+
const began = clock;
|
|
194
|
+
const before = paints;
|
|
195
|
+
while (active && clock - began <= 5500) { clock += delay; pulse!(); }
|
|
196
|
+
assert.equal(active, false, "finishes and clears the interval");
|
|
197
|
+
assert.equal(stripAnsi(header!.render(200).join("\n")), staticArt);
|
|
198
|
+
if (policy === "quality") { qualityDuration = clock - began; qualityPaints = paints - before; }
|
|
199
|
+
else {
|
|
200
|
+
assert.ok(Math.abs(clock - began - qualityDuration) <= 250);
|
|
201
|
+
assert.ok(paints - before <= Math.ceil(qualityPaints / 10));
|
|
202
|
+
}
|
|
203
|
+
header!.dispose();
|
|
204
|
+
}
|
|
205
|
+
} finally { shutdown!(); }
|
|
206
|
+
});
|
|
207
|
+
|
|
118
208
|
// Drive the real header factory; background git/home reads never run.
|
|
119
209
|
for (const showRose of [false, true]) for (const showTextLogo of [false, true]) {
|
|
120
210
|
test(`startup art respects rose=${showRose}, logo=${showTextLogo} and cyan palette`, async (t) => {
|
|
@@ -134,7 +224,8 @@ for (const showRose of [false, true]) for (const showTextLogo of [false, true])
|
|
|
134
224
|
let shutdown: Function;
|
|
135
225
|
let header: { render(width: number): string[]; dispose(): void };
|
|
136
226
|
const writes: string[] = [];
|
|
137
|
-
|
|
227
|
+
const { default: coldStartup } = await import(new URL(`../extensions/startup-banner.ts?art-${showRose}-${showTextLogo}`, import.meta.url).href) as typeof import("../extensions/startup-banner.ts");
|
|
228
|
+
coldStartup({ on: (name: string, fn: Function) => {
|
|
138
229
|
if (name === "session_start") start = fn;
|
|
139
230
|
if (name === "session_shutdown") shutdown = fn;
|
|
140
231
|
}, registerCommand() {}, getCommands: () => [], getAllTools: () => [] } as unknown as ExtensionAPI);
|
|
Binary file
|
package/skills/release/SKILL.md
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: release
|
|
3
|
-
description: "Release gentle-pi through GitHub and npm. Trigger: release, publish, npm publish, GitHub release, version bump."
|
|
4
|
-
license: Apache-2.0
|
|
5
|
-
metadata:
|
|
6
|
-
author: gentleman-programming
|
|
7
|
-
version: "1.2"
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## When to Use
|
|
11
|
-
|
|
12
|
-
Use this skill when preparing, publishing, or verifying a `gentle-pi` release.
|
|
13
|
-
|
|
14
|
-
## Hard Rules
|
|
15
|
-
|
|
16
|
-
- Do not publish `gentle-pi` to npm from a local machine.
|
|
17
|
-
- npm publishing MUST go through the GitHub Actions workflow `.github/workflows/publish.yml` so provenance, environment protection, and registry credentials are controlled by GitHub.
|
|
18
|
-
- Dispatch the trusted workflow definition from protected default `main`, never from a release tag. Its only caller input is the exact annotated version tag.
|
|
19
|
-
- Use a clean worktree for release commits. Do not package unrelated local files or scratch artifacts.
|
|
20
|
-
- Review outcomes are informational. Release delivery follows ordinary repository policy and must not be blocked, authorized, or rewritten by RDD.
|
|
21
|
-
- Never infer the release tag target from local `HEAD`; use the freshly fetched `origin/main` commit and the repository's normal release safeguards.
|
|
22
|
-
- Never skip package verification. The publish workflow runs verification again, but local validation should still pass before tagging.
|
|
23
|
-
|
|
24
|
-
## Release Procedure
|
|
25
|
-
|
|
26
|
-
1. **Inspect state**
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
git status --short
|
|
30
|
-
git fetch origin main --tags
|
|
31
|
-
git log --oneline --decorate --max-count=5 origin/main
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
2. **Prepare the release commit**
|
|
35
|
-
|
|
36
|
-
- Apply only intended changes.
|
|
37
|
-
- Bump `package.json` to the next semver version.
|
|
38
|
-
- Keep lockfile changes out unless dependency resolution actually changed.
|
|
39
|
-
|
|
40
|
-
3. **Verify locally**
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
pnpm test
|
|
44
|
-
node scripts/verify-package-files.mjs
|
|
45
|
-
npm pack --dry-run
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`npm pack --dry-run` verifies package contents and lifecycle scripts without entering a publish path.
|
|
49
|
-
|
|
50
|
-
4. **Commit and push**
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
git add <intended-files>
|
|
54
|
-
git commit -m "<type(scope): release-ready change>"
|
|
55
|
-
git push origin HEAD:main
|
|
56
|
-
git fetch origin main --tags
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
5. **Create and verify the exact version tag**
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
version="$(node -p "require('./package.json').version")"
|
|
63
|
-
tag="v${version}"
|
|
64
|
-
release_sha="$(git rev-parse 'origin/main^{commit}')"
|
|
65
|
-
|
|
66
|
-
test "$(git rev-parse 'HEAD^{commit}')" = "${release_sha}"
|
|
67
|
-
test -z "$(git ls-remote --tags origin "refs/tags/${tag}")"
|
|
68
|
-
|
|
69
|
-
git tag -a "${tag}" "${release_sha}" -m "gentle-pi ${tag}"
|
|
70
|
-
test "$(git rev-parse "${tag}^{commit}")" = "${release_sha}"
|
|
71
|
-
|
|
72
|
-
git fetch origin main
|
|
73
|
-
test "$(git rev-parse 'origin/main^{commit}')" = "${release_sha}"
|
|
74
|
-
git push origin "refs/tags/${tag}"
|
|
75
|
-
|
|
76
|
-
git fetch --no-tags origin "refs/tags/${tag}"
|
|
77
|
-
test "$(git rev-parse 'FETCH_HEAD^{commit}')" = "${release_sha}"
|
|
78
|
-
|
|
79
|
-
gh release create "${tag}" \
|
|
80
|
-
--repo Gentleman-Programming/gentle-pi \
|
|
81
|
-
--verify-tag \
|
|
82
|
-
--title "gentle-pi ${tag}" \
|
|
83
|
-
--notes "<release notes>"
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Do not retag or overwrite an existing version. The tag target comes from the freshly fetched immutable `origin/main` commit, not an ambient local branch.
|
|
87
|
-
|
|
88
|
-
6. **Publish npm through GitHub Actions**
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
version="$(node -p "require('./package.json').version")"
|
|
92
|
-
tag="v${version}"
|
|
93
|
-
gh workflow run publish.yml \
|
|
94
|
-
--repo Gentleman-Programming/gentle-pi \
|
|
95
|
-
--ref main \
|
|
96
|
-
-f tag="${tag}"
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
The workflow definition always comes from protected default `main`. It accepts only one exact `vSemVer` tag, fetches the remote annotated tag and current remote `main`, and requires the peeled tag commit, dispatch/main workflow commit, checkout, and `package.json` version to match. It re-queries remote tag and `main` immediately before npm publication, derives the dist-tag internally, and uses trusted OIDC with provenance.
|
|
100
|
-
|
|
101
|
-
Watch the run and fail the release if it fails:
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
gh run list --repo Gentleman-Programming/gentle-pi --workflow publish.yml --limit 3
|
|
105
|
-
gh run watch <run-id> --repo Gentleman-Programming/gentle-pi --exit-status
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
7. **Verify npm**
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npm view gentle-pi@<version> version --registry=https://registry.npmjs.org/
|
|
112
|
-
npm dist-tag ls gentle-pi --registry=https://registry.npmjs.org/
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Failure Handling
|
|
116
|
-
|
|
117
|
-
- A publication failure is handled through ordinary repository policy. It does not reopen or alter a review lineage.
|
|
118
|
-
- Never attempt or retry `npm publish` locally. Re-dispatch from trusted `main` only when the same tag still targets the current remote `main` and the failure was publication-only.
|
|
119
|
-
- If remote `main` advances, do not move or recreate the existing tag. Prepare a new release commit/version and create a new annotated version tag.
|
|
120
|
-
- If the workflow fails, inspect logs with:
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
gh run view <run-id> --repo Gentleman-Programming/gentle-pi --log
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
- If npm verification is briefly stale after a successful workflow, check the exact version first (`npm view gentle-pi@<version> version`) before assuming publish failed.
|
|
127
|
-
|
|
128
|
-
## Output Contract
|
|
129
|
-
|
|
130
|
-
Report:
|
|
131
|
-
|
|
132
|
-
- Commit SHA pushed to `main`.
|
|
133
|
-
- Exact version tag and its peeled commit SHA.
|
|
134
|
-
- GitHub release URL.
|
|
135
|
-
- Publish workflow run URL and conclusion.
|
|
136
|
-
- npm exact version and the workflow-derived dist-tag (`latest`, `beta`, or `next`).
|
|
137
|
-
- Any remaining follow-up or warnings.
|