gentle-pi 2.5.0 → 2.6.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 +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
|
|
9
|
+
const helperUrl = new URL("../scripts/install-tui-mode-setting.mjs", import.meta.url);
|
|
10
|
+
const { installTuiModeSetting } = await import(helperUrl.href);
|
|
11
|
+
|
|
12
|
+
function fixture(t: { after(fn: () => void): void }, packagePath: readonly string[] = ["npm", "node_modules", "gentle-pi"]) {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-tui-test-"));
|
|
14
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
15
|
+
const home = join(root, "agent");
|
|
16
|
+
const packageRoot = join(home, ...packagePath);
|
|
17
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
18
|
+
const settings = join(home, "settings.json");
|
|
19
|
+
const env = { GENTLE_PI_AGENT_HOME: home, PI_CODING_AGENT_DIR: join(root, "other-agent") };
|
|
20
|
+
return { root, home, packageRoot, settings, env, options: { packageRoot, env, home: root } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function link(target: string, path: string, directory = false) {
|
|
24
|
+
symlinkSync(target, path, directory ? "junction" : "file");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const [name, packagePath] of [
|
|
28
|
+
["npm", ["npm", "node_modules", "gentle-pi"]],
|
|
29
|
+
["Pi Git", ["git", "github.com", "Gentleman-Programming", "gentle-pi"]],
|
|
30
|
+
] as const) {
|
|
31
|
+
test(`recognized global ${name} installation persists fullscreen and preserves other settings`, async (t) => {
|
|
32
|
+
const f = fixture(t, packagePath);
|
|
33
|
+
writeFileSync(f.settings, JSON.stringify({ tuiMode: "regular", theme: "rose", nested: { enabled: false } }));
|
|
34
|
+
assert.deepEqual(await installTuiModeSetting(f.options), { changed: true, recognized: true });
|
|
35
|
+
assert.deepEqual(JSON.parse(readFileSync(f.settings, "utf8")), { tuiMode: "fullscreen", theme: "rose", nested: { enabled: false } });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const initial of [undefined, '{ "tuiMode": "regular", "packages": ["npm:example"] }']) {
|
|
40
|
+
test(`creates or resets fullscreen: ${initial ?? "missing"}`, async (t) => {
|
|
41
|
+
const f = fixture(t);
|
|
42
|
+
if (initial) writeFileSync(f.settings, initial);
|
|
43
|
+
assert.equal((await installTuiModeSetting(f.options)).changed, true);
|
|
44
|
+
assert.equal(JSON.parse(readFileSync(f.settings, "utf8")).tuiMode, "fullscreen");
|
|
45
|
+
writeFileSync(f.settings, '{ "tuiMode": "regular", "theme": "custom" }');
|
|
46
|
+
await installTuiModeSetting(f.options);
|
|
47
|
+
assert.equal(JSON.parse(readFileSync(f.settings, "utf8")).theme, "custom");
|
|
48
|
+
assert.equal(JSON.parse(readFileSync(f.settings, "utf8")).tuiMode, "fullscreen");
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("already fullscreen preserves bytes and inode", async (t) => {
|
|
53
|
+
const f = fixture(t);
|
|
54
|
+
const text = '\uFEFF{ "tuiMode" : "fullscreen", "other": [1, null] }\n';
|
|
55
|
+
writeFileSync(f.settings, text);
|
|
56
|
+
const before = lstatSync(f.settings);
|
|
57
|
+
assert.equal((await installTuiModeSetting(f.options)).changed, false);
|
|
58
|
+
assert.equal(readFileSync(f.settings, "utf8"), text);
|
|
59
|
+
assert.equal(lstatSync(f.settings).ino, before.ino);
|
|
60
|
+
assert.deepEqual(readdirSync(f.home).sort(), ["npm", "settings.json"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
for (const name of ["GENTLE_PI_AGENT_HOME", "PI_CODING_AGENT_DIR", "default"]) {
|
|
64
|
+
test(`agent-home precedence: ${name}`, async (t) => {
|
|
65
|
+
const f = fixture(t);
|
|
66
|
+
const home = name === "default" ? join(f.root, ".pi", "agent") : f.home;
|
|
67
|
+
const packageRoot = join(home, "npm", "node_modules", "gentle-pi");
|
|
68
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
69
|
+
const env = name === "default" ? {} : name === "PI_CODING_AGENT_DIR" ? { PI_CODING_AGENT_DIR: home } : f.env;
|
|
70
|
+
await installTuiModeSetting({ packageRoot, env, home: f.root });
|
|
71
|
+
assert.equal(JSON.parse(readFileSync(join(home, "settings.json"), "utf8")).tuiMode, "fullscreen");
|
|
72
|
+
assert.equal(existsSync(join(f.env.PI_CODING_AGENT_DIR, "settings.json")), false);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const relative of [
|
|
77
|
+
"project/.pi/npm/node_modules/gentle-pi",
|
|
78
|
+
"project/.pi/git/github.com/Gentleman-Programming/gentle-pi",
|
|
79
|
+
"consumer/node_modules/gentle-pi",
|
|
80
|
+
"checkout",
|
|
81
|
+
"agent/git/github.com/gentle-pi",
|
|
82
|
+
"temporary/npm/node_modules/gentle-pi",
|
|
83
|
+
"store/.pnpm/gentle-pi/node_modules/gentle-pi",
|
|
84
|
+
]) {
|
|
85
|
+
test(`unowned install is untouched: ${relative}`, async (t) => {
|
|
86
|
+
const f = fixture(t);
|
|
87
|
+
const packageRoot = join(f.root, relative);
|
|
88
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
89
|
+
writeFileSync(f.settings, '{"tuiMode":"regular"}');
|
|
90
|
+
assert.deepEqual(await installTuiModeSetting({ ...f.options, packageRoot }), { changed: false, recognized: false });
|
|
91
|
+
assert.equal(readFileSync(f.settings, "utf8"), '{"tuiMode":"regular"}');
|
|
92
|
+
assert.equal(existsSync(join(f.root, "project", ".pi", "settings.json")), false);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const packagePath of [
|
|
97
|
+
["git", "gitlab.com", "Gentleman-Programming", "gentle-pi"],
|
|
98
|
+
["git", "github.com", "Other-Organization", "gentle-pi"],
|
|
99
|
+
["git", "github.com", "Gentleman-Programming", "other-pi"],
|
|
100
|
+
]) {
|
|
101
|
+
test(`unowned global Pi Git install is untouched: ${packagePath.join("/")}`, async (t) => {
|
|
102
|
+
const f = fixture(t);
|
|
103
|
+
const packageRoot = join(f.home, ...packagePath);
|
|
104
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
105
|
+
writeFileSync(f.settings, '{"tuiMode":"regular"}');
|
|
106
|
+
assert.deepEqual(await installTuiModeSetting({ ...f.options, packageRoot }), { changed: false, recognized: false });
|
|
107
|
+
assert.equal(readFileSync(f.settings, "utf8"), '{"tuiMode":"regular"}');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test("missing configured agent home is a no-op", async (t) => {
|
|
112
|
+
const f = fixture(t);
|
|
113
|
+
assert.equal((await installTuiModeSetting({ ...f.options, env: { GENTLE_PI_AGENT_HOME: join(f.root, "absent") } })).recognized, false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
for (const [name, packagePath] of [
|
|
117
|
+
["npm", ["npm", "node_modules", "gentle-pi"]],
|
|
118
|
+
["Pi Git", ["git", "github.com", "Gentleman-Programming", "gentle-pi"]],
|
|
119
|
+
] as const) {
|
|
120
|
+
test(`global ${name} package symlink into a store is not ownership`, async (t) => {
|
|
121
|
+
const f = fixture(t);
|
|
122
|
+
const home = join(f.root, "linked-agent");
|
|
123
|
+
const packageRoot = join(home, ...packagePath);
|
|
124
|
+
mkdirSync(join(packageRoot, ".."), { recursive: true });
|
|
125
|
+
link(f.packageRoot, packageRoot, true);
|
|
126
|
+
assert.equal((await installTuiModeSetting({ packageRoot, env: { GENTLE_PI_AGENT_HOME: home }, home: f.root })).recognized, false);
|
|
127
|
+
assert.equal(existsSync(join(home, "settings.json")), false);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
test("canonical agent-home alias is supported", async (t) => {
|
|
132
|
+
const f = fixture(t);
|
|
133
|
+
const alias = join(f.root, "alias");
|
|
134
|
+
link(f.home, alias, true);
|
|
135
|
+
await installTuiModeSetting({ ...f.options, env: { GENTLE_PI_AGENT_HOME: alias } });
|
|
136
|
+
assert.equal(JSON.parse(readFileSync(f.settings, "utf8")).tuiMode, "fullscreen");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
for (const text of ["{broken", "[]", "null", "false", "42", '"string"', ""]) {
|
|
140
|
+
test(`invalid settings remain unchanged: ${text}`, async (t) => {
|
|
141
|
+
const f = fixture(t);
|
|
142
|
+
writeFileSync(f.settings, text);
|
|
143
|
+
await assert.rejects(installTuiModeSetting(f.options));
|
|
144
|
+
assert.equal(readFileSync(f.settings, "utf8"), text);
|
|
145
|
+
assert.deepEqual(readdirSync(f.home).sort(), ["npm", "settings.json"]);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const kind of ["directory", "symlink", "dangling"]) {
|
|
150
|
+
test(`rejects nonregular settings: ${kind}`, async (t) => {
|
|
151
|
+
const f = fixture(t);
|
|
152
|
+
const target = join(f.root, "target.json");
|
|
153
|
+
if (kind === "directory") mkdirSync(f.settings);
|
|
154
|
+
else {
|
|
155
|
+
if (kind === "symlink") writeFileSync(target, "{}");
|
|
156
|
+
link(target, f.settings);
|
|
157
|
+
}
|
|
158
|
+
await assert.rejects(installTuiModeSetting(f.options), /regular/);
|
|
159
|
+
assert.equal(kind === "directory" ? lstatSync(f.settings).isDirectory() : lstatSync(f.settings).isSymbolicLink(), true);
|
|
160
|
+
if (kind === "symlink") assert.equal(readFileSync(target, "utf8"), "{}");
|
|
161
|
+
assert.equal(existsSync(`${f.settings}.lock`), false);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
test("preserves existing permissions and creates private new settings", { skip: process.platform === "win32" }, async (t) => {
|
|
166
|
+
const f = fixture(t);
|
|
167
|
+
await installTuiModeSetting(f.options);
|
|
168
|
+
assert.equal(lstatSync(f.settings).mode & 0o777, 0o600);
|
|
169
|
+
writeFileSync(f.settings, "{}");
|
|
170
|
+
chmodSync(f.settings, 0o640);
|
|
171
|
+
await installTuiModeSetting(f.options);
|
|
172
|
+
assert.equal(lstatSync(f.settings).mode & 0o777, 0o640);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("cooperative concurrent installers serialize without artifacts", async (t) => {
|
|
176
|
+
const f = fixture(t);
|
|
177
|
+
const results = await Promise.all(Array.from({ length: 6 }, () => installTuiModeSetting(f.options)));
|
|
178
|
+
assert.equal(results.filter((result) => result.changed).length, 1);
|
|
179
|
+
assert.deepEqual(readdirSync(f.home).sort(), ["npm", "settings.json"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("Pi proper-lockfile contention is bounded and never steals the lock", async (t) => {
|
|
183
|
+
const f = fixture(t);
|
|
184
|
+
writeFileSync(f.settings, '{"theme":"kept"}');
|
|
185
|
+
const piRequire = createRequire(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
186
|
+
const lockfile = piRequire("proper-lockfile");
|
|
187
|
+
const release = lockfile.lockSync(f.settings, { realpath: false });
|
|
188
|
+
try {
|
|
189
|
+
await assert.rejects(installTuiModeSetting(f.options), /lock is busy/);
|
|
190
|
+
assert.equal(readFileSync(f.settings, "utf8"), '{"theme":"kept"}');
|
|
191
|
+
assert.equal(existsSync(`${f.settings}.lock`), true);
|
|
192
|
+
} finally { release(); }
|
|
193
|
+
await installTuiModeSetting(f.options);
|
|
194
|
+
assert.equal(JSON.parse(readFileSync(f.settings, "utf8")).theme, "kept");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("Pi's later packages-only save retains persisted fullscreen", async (t) => {
|
|
198
|
+
const f = fixture(t);
|
|
199
|
+
writeFileSync(f.settings, '{"tuiMode":"regular"}');
|
|
200
|
+
const { SettingsManager } = await import("@earendil-works/pi-coding-agent");
|
|
201
|
+
const manager = SettingsManager.create(f.root, f.home);
|
|
202
|
+
await installTuiModeSetting(f.options);
|
|
203
|
+
manager.setPackages(["npm:gentle-pi"]);
|
|
204
|
+
await manager.flush();
|
|
205
|
+
assert.deepEqual(manager.drainErrors(), []);
|
|
206
|
+
assert.deepEqual(JSON.parse(readFileSync(f.settings, "utf8")), { tuiMode: "fullscreen", packages: ["npm:gentle-pi"] });
|
|
207
|
+
manager.setTuiMode("regular");
|
|
208
|
+
await manager.flush();
|
|
209
|
+
assert.equal(JSON.parse(readFileSync(f.settings, "utf8")).tuiMode, "regular");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
for (const [name, packagePath] of [
|
|
213
|
+
["npm", ["npm", "node_modules", "gentle-pi"]],
|
|
214
|
+
["Pi Git", ["git", "github.com", "Gentleman-Programming", "gentle-pi"]],
|
|
215
|
+
] as const) {
|
|
216
|
+
test(`symlinked ${name} ancestor cannot grant settings ownership`, async (t) => {
|
|
217
|
+
const f = fixture(t);
|
|
218
|
+
const home = join(f.root, "unsafe-agent");
|
|
219
|
+
mkdirSync(home);
|
|
220
|
+
mkdirSync(join(f.home, ...packagePath), { recursive: true });
|
|
221
|
+
link(join(f.home, packagePath[0]), join(home, packagePath[0]), true);
|
|
222
|
+
const result = await installTuiModeSetting({ packageRoot: join(home, ...packagePath), env: { GENTLE_PI_AGENT_HOME: home }, home: f.root });
|
|
223
|
+
assert.equal(result.recognized, false);
|
|
224
|
+
assert.equal(existsSync(join(home, "settings.json")), false);
|
|
225
|
+
assert.equal(existsSync(f.settings), false);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
test("waiting installer rereads another cooperative writer's settings", async (t) => {
|
|
230
|
+
const f = fixture(t);
|
|
231
|
+
writeFileSync(f.settings, "{}");
|
|
232
|
+
const piRequire = createRequire(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
233
|
+
const release = piRequire("proper-lockfile").lockSync(f.settings, { realpath: false });
|
|
234
|
+
const installing = installTuiModeSetting(f.options);
|
|
235
|
+
writeFileSync(f.settings, '{"theme":"concurrent"}');
|
|
236
|
+
release();
|
|
237
|
+
await installing;
|
|
238
|
+
assert.deepEqual(JSON.parse(readFileSync(f.settings, "utf8")), { theme: "concurrent", tuiMode: "fullscreen" });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
for (const fault of ["write", "rename", "concurrent-change"]) {
|
|
242
|
+
test(`atomic update preserves original or concurrent settings on ${fault}`, (t) => {
|
|
243
|
+
const f = fixture(t);
|
|
244
|
+
const original = '{"theme":"original"}';
|
|
245
|
+
writeFileSync(f.settings, original);
|
|
246
|
+
// Isolate built-in fault injection in a child; no production test hooks.
|
|
247
|
+
const code = `
|
|
248
|
+
import fs from 'node:fs';
|
|
249
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
250
|
+
const originalWrite = fs.writeFileSync;
|
|
251
|
+
const originalSync = fs.fsyncSync;
|
|
252
|
+
const fault = ${JSON.stringify(fault)};
|
|
253
|
+
if (fault === 'write') fs.writeFileSync = () => { throw new Error('injected write failure'); };
|
|
254
|
+
if (fault === 'rename') fs.renameSync = () => { throw new Error('injected rename failure'); };
|
|
255
|
+
if (fault === 'concurrent-change') fs.fsyncSync = (fd) => {
|
|
256
|
+
originalSync(fd);
|
|
257
|
+
originalWrite(${JSON.stringify(f.settings)}, '{"theme":"concurrent"}');
|
|
258
|
+
};
|
|
259
|
+
syncBuiltinESMExports();
|
|
260
|
+
const { installTuiModeSetting } = await import(${JSON.stringify(helperUrl.href)});
|
|
261
|
+
try { await installTuiModeSetting(${JSON.stringify(f.options)}); process.exitCode = 2; }
|
|
262
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
263
|
+
`;
|
|
264
|
+
const result = spawnSync(process.execPath, ["--input-type=module", "--eval", code], { encoding: "utf8", env: { ...process.env, ...f.env } });
|
|
265
|
+
assert.equal(result.status, 1, result.stderr);
|
|
266
|
+
assert.match(result.stderr, fault === "concurrent-change" ? /changed concurrently/ : /injected/);
|
|
267
|
+
assert.equal(readFileSync(f.settings, "utf8"), fault === "concurrent-change" ? '{"theme":"concurrent"}' : original);
|
|
268
|
+
assert.deepEqual(readdirSync(f.home).sort(), ["npm", "settings.json"]);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
type PostinstallRoute = "skip" | "success" | "failure";
|
|
273
|
+
|
|
274
|
+
function writePostinstallFixture(f: ReturnType<typeof fixture>, route: PostinstallRoute) {
|
|
275
|
+
const scripts = join(f.packageRoot, "scripts");
|
|
276
|
+
mkdirSync(scripts);
|
|
277
|
+
copyFileSync(helperUrl, join(scripts, "install-tui-mode-setting.mjs"));
|
|
278
|
+
copyFileSync(new URL("../scripts/install-gentle-ai.mjs", import.meta.url), join(scripts, "install-gentle-ai.mjs"));
|
|
279
|
+
const installer = route === "failure"
|
|
280
|
+
? 'throw new Error("native failed")'
|
|
281
|
+
: route === "skip"
|
|
282
|
+
? 'throw new Error("skip fixture native installer was invoked")'
|
|
283
|
+
: 'return { installed: true, binaryPath: "fixture" }';
|
|
284
|
+
writeFileSync(join(scripts, "gentle-ai-installer.mjs"), `export const INSTALLER_VERSION = "test"; export async function installGentleAi() { ${installer}; }`);
|
|
285
|
+
return scripts;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function runPostinstall(f: ReturnType<typeof fixture>, route: PostinstallRoute) {
|
|
289
|
+
const scripts = writePostinstallFixture(f, route);
|
|
290
|
+
return spawnSync(process.execPath, [join(scripts, "install-gentle-ai.mjs")], {
|
|
291
|
+
encoding: "utf8", env: { ...process.env, ...f.env, GENTLE_PI_SKIP_GENTLE_AI_INSTALL: route === "skip" ? "1" : "0" },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function assertPostinstallLifecycle(f: ReturnType<typeof fixture>, route: PostinstallRoute, result: ReturnType<typeof runPostinstall>, settingsExists = route !== "failure") {
|
|
296
|
+
assert.equal(result.status, route === "failure" ? 1 : 0, result.stderr);
|
|
297
|
+
assert.equal(existsSync(f.settings), settingsExists);
|
|
298
|
+
if (route === "failure") assert.match(result.stderr, /gentle-pi could not install its package-local Gentle AI vtest binary: native failed/);
|
|
299
|
+
else {
|
|
300
|
+
assert.doesNotMatch(result.stderr, /skip fixture native installer was invoked/);
|
|
301
|
+
assert.equal(JSON.parse(readFileSync(f.settings, "utf8")).tuiMode, "fullscreen");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const [name, packagePath] of [
|
|
306
|
+
["npm", ["npm", "node_modules", "gentle-pi"]],
|
|
307
|
+
["Pi Git", ["git", "github.com", "Gentleman-Programming", "gentle-pi"]],
|
|
308
|
+
] as const) {
|
|
309
|
+
for (const route of ["skip", "success", "failure"] as const) {
|
|
310
|
+
test(`${name} postinstall lifecycle: ${route}`, (t) => {
|
|
311
|
+
const f = fixture(t, packagePath);
|
|
312
|
+
const result = runPostinstall(f, route);
|
|
313
|
+
assertPostinstallLifecycle(f, route, result);
|
|
314
|
+
if (route === "failure") {
|
|
315
|
+
const existing = fixture(t, packagePath);
|
|
316
|
+
const original = '{"tuiMode":"regular","theme":"kept"}';
|
|
317
|
+
writeFileSync(existing.settings, original);
|
|
318
|
+
const existingResult = runPostinstall(existing, route);
|
|
319
|
+
assertPostinstallLifecycle(existing, route, existingResult, true);
|
|
320
|
+
assert.equal(readFileSync(existing.settings, "utf8"), original);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -35,6 +35,28 @@ test("fails closed before publication for invalid required Issue Form data", ()
|
|
|
35
35
|
assert.match(skill, /Never invent answers, selections, confirmations, or labels/i);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
function assertCheckboxAttestationContract(candidate: string) {
|
|
39
|
+
assert.match(candidate, /agent must complete.*duplicate search.*retain evidence/i);
|
|
40
|
+
assert.match(candidate, /agent must complete.*privacy scan\/redaction.*exact body.*retain evidence/i);
|
|
41
|
+
assert.match(candidate, /may explicitly attest.*own evidence-backed work.*must not attribute.*user/i);
|
|
42
|
+
assert.match(candidate, /personal facts, consent, legal declarations.*explicit user affirmation/i);
|
|
43
|
+
assert.match(candidate, /Do not blanket.*check(?:box|boxes)/i);
|
|
44
|
+
assert.match(candidate, /request to publish.*does not.*affirm/i);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("attests only agent-verifiable checkbox work and preserves human declarations", () => {
|
|
48
|
+
assertCheckboxAttestationContract(skill);
|
|
49
|
+
|
|
50
|
+
const missingEvidence = skill.replace(/retain evidence/gi, "skip evidence");
|
|
51
|
+
assert.throws(() => assertCheckboxAttestationContract(missingEvidence));
|
|
52
|
+
|
|
53
|
+
const userOnlyDeclaration = skill.replace(
|
|
54
|
+
/personal facts, consent, legal declarations.*explicit user affirmation/i,
|
|
55
|
+
"personal facts, consent, legal declarations may be inferred from a request to publish",
|
|
56
|
+
);
|
|
57
|
+
assert.throws(() => assertCheckboxAttestationContract(userOnlyDeclaration));
|
|
58
|
+
});
|
|
59
|
+
|
|
38
60
|
test("publishes reviewed Issue Form bodies through a private body file", () => {
|
|
39
61
|
assert.match(skill, /private `BODY_FILE`/);
|
|
40
62
|
assert.match(skill, /gh issue create --repo "\$TARGET" --title "\$TITLE" --body-file "\$BODY_FILE"/);
|
|
@@ -94,6 +94,18 @@ test("model routing authority normalizes and preserves sync/async source status"
|
|
|
94
94
|
path: invalidGlobalPath,
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
const sourceCases = [
|
|
98
|
+
[missingPath, missingPath, { status: "missing" }],
|
|
99
|
+
[missingPath, projectPath, { status: "valid", config: { project: { model: "google/gemini" } } }],
|
|
100
|
+
[validGlobalPath, projectPath, validSync],
|
|
101
|
+
[missingPath, invalidGlobalPath, { status: "invalid", path: invalidGlobalPath }],
|
|
102
|
+
[invalidGlobalPath, projectPath, { status: "invalid", path: invalidGlobalPath }],
|
|
103
|
+
] as const;
|
|
104
|
+
for (const [globalSource, projectSource, expected] of sourceCases) {
|
|
105
|
+
assert.deepEqual(authority.readSavedModelConfig(globalSource, projectSource), expected);
|
|
106
|
+
assert.deepEqual(await authority.readSavedModelConfigAsync(globalSource, projectSource), expected);
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
98
110
|
process.env.GENTLE_PI_CONFIG_HOME = globalDir;
|
|
99
111
|
t.after(() => {
|
|
@@ -182,11 +182,22 @@ test("2.7.0 repeats 2.6.0 because the negotiated lane Pi consumes is unchanged",
|
|
|
182
182
|
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.6.0"] as Record<string, boolean>);
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
test("2.8.0 repeats 2.7.0 because the negotiated lane Pi consumes is unchanged", () => {
|
|
186
|
+
// Ground-truthed against the published v2.8.0 linux/amd64 binary from its
|
|
187
|
+
// signed release archive: capabilities/v2.5 still advertises protocol minor
|
|
188
|
+
// 5 with status/v7, consent/v3, and start/v4 schemas. riskEvidence and hint
|
|
189
|
+
// remain dark because neither is proven to reach Pi's negotiated START path.
|
|
190
|
+
const contract = NATIVE_CLI_CONTRACTS["2.8.0"] as Record<string, boolean>;
|
|
191
|
+
assert.equal(contract.riskEvidence, false);
|
|
192
|
+
assert.equal(contract.hint, false);
|
|
193
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.7.0"] as Record<string, boolean>);
|
|
194
|
+
});
|
|
195
|
+
|
|
185
196
|
test("no shipped version key was added beyond the pin bump", () => {
|
|
186
197
|
// Rows are promises to consumers, so a new key only ever appears in a
|
|
187
198
|
// dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
|
|
188
199
|
// v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
|
|
189
200
|
// so they get no row: a row asserts ground truth measured against a binary
|
|
190
201
|
// Pi actually ran, and the table only has to be ascending, not gapless.
|
|
191
|
-
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0"]);
|
|
202
|
+
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0"]);
|
|
192
203
|
});
|