gentle-pi 4.1.0 → 4.3.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 +352 -128
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +9 -9
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +35 -7
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +443 -8
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-shell-bin.test.ts +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +131 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +69 -9
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/vim-editor-adapter.test.ts +41 -15
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +24 -1
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -133,13 +133,13 @@ test("public docs and metadata advertise ODD and review without retired phase wo
|
|
|
133
133
|
|
|
134
134
|
test("technical reference declares the tested Pi minimum required for agent_settled", () => {
|
|
135
135
|
const manifest = readPackageJson();
|
|
136
|
-
assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=
|
|
137
|
-
assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], ">=1.
|
|
136
|
+
assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=1.2.0");
|
|
137
|
+
assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], ">=1.2.0");
|
|
138
138
|
assert.equal(manifest.peerDependenciesMeta?.["@earendil-works/pi-coding-agent"]?.optional, true);
|
|
139
139
|
assert.equal(manifest.engines?.node, ">=22.19.0");
|
|
140
140
|
for (const path of ["docs/readme-reference.md", "docs/gentle-shell.md"]) {
|
|
141
141
|
const source = readFileSync(join(PACKAGE_ROOT, path), "utf8");
|
|
142
|
-
assert.match(source, /Pi
|
|
142
|
+
assert.match(source, /Pi 1\.2\.0 or newer/, path);
|
|
143
143
|
assert.match(source, /open `>=1\.0\.0` development range/, path);
|
|
144
144
|
assert.doesNotMatch(source, /development tests pin Pi/, path);
|
|
145
145
|
// Docs name exactly the audited Vim editor releases, never a future one.
|
|
@@ -162,7 +162,7 @@ test("packed runtime uses optional Pi host peers with one open development range
|
|
|
162
162
|
// one exact release shared by every Pi host package.
|
|
163
163
|
const installed = new Set<string>();
|
|
164
164
|
for (const name of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
|
|
165
|
-
assert.equal(manifest.devDependencies?.[name], ">=1.
|
|
165
|
+
assert.equal(manifest.devDependencies?.[name], ">=1.2.0", name);
|
|
166
166
|
const metadata = JSON.parse(readFileSync(join(PACKAGE_ROOT, "node_modules", name, "package.json"), "utf8")) as { name: string; version: string };
|
|
167
167
|
assert.equal(metadata.name, name);
|
|
168
168
|
installed.add(metadata.version);
|
|
@@ -232,7 +232,7 @@ test("package verification excludes the retired init extension while retaining O
|
|
|
232
232
|
assert.equal(existsSync(join(PACKAGE_ROOT, "extensions", "sdd-init.ts")), false);
|
|
233
233
|
assert.doesNotMatch(verifier, /^\s*"extensions\/sdd-init\.ts",?$/m);
|
|
234
234
|
assert.match(verifier, /existsSync\(join\(root, "extensions\/sdd-init\.ts"\)\)/);
|
|
235
|
-
for (const resource of ["extensions/gentle-ai.ts", "extensions/skill-registry.ts", "assets/orchestrator.md", "assets/orchestrator-delegation.md", "assets/agents/gentle-ai-worker.md", "assets/agents/review-risk.md", "assets/chains/4r-review.chain.md"]) {
|
|
235
|
+
for (const resource of ["extensions/gentle-ai.ts", "extensions/skill-registry.ts", "assets/orchestrator.md", "assets/orchestrator-delegation.md", "assets/orchestrator-delegation-reference.md", "assets/agents/gentle-ai-worker.md", "assets/agents/review-risk.md", "assets/chains/4r-review.chain.md"]) {
|
|
236
236
|
assert.ok(verifier.includes(`"${resource}"`), `${resource} must remain required`);
|
|
237
237
|
}
|
|
238
238
|
});
|
|
@@ -431,20 +431,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
|
|
|
431
431
|
);
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
-
test("package verification binds the published Gentle AI v4.
|
|
434
|
+
test("package verification binds the published Gentle AI v4.2.0 runtime pin", () => {
|
|
435
435
|
const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
|
|
436
436
|
const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
|
|
437
437
|
const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
438
438
|
|
|
439
|
-
assert.match(installer, /INSTALLER_VERSION = "4\.
|
|
439
|
+
assert.match(installer, /INSTALLER_VERSION = "4\.2\.0"/);
|
|
440
440
|
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
|
|
441
|
-
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1
|
|
441
|
+
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:J8EluS7QrfM\+m3vmQlF26B\/shZRjD6rBfsKtrABL5mc="/);
|
|
442
442
|
assert.match(installer, /GOTOOLCHAIN: "local"/);
|
|
443
443
|
assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
|
|
444
444
|
assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
|
|
445
445
|
assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
|
|
446
446
|
assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
|
|
447
|
-
assert.match(verifier, /v4\.
|
|
447
|
+
assert.match(verifier, /v4\.2\.0/);
|
|
448
448
|
});
|
|
449
449
|
|
|
450
450
|
|
|
@@ -1722,27 +1722,29 @@ test("normal and forced installation copy generic agents with complete role cont
|
|
|
1722
1722
|
});
|
|
1723
1723
|
|
|
1724
1724
|
test("bounded implementation routing resolves the explicit canonical fallback reference", () => {
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1725
|
+
// S11 T2: the Delegation Triggers live in the delegation card and the Work Routing Ladder (with its
|
|
1726
|
+
// Simple Delegation pointer back to them) in the reference, so the pointer resolves across both files.
|
|
1727
|
+
const routing = ["orchestrator-delegation.md", "orchestrator-delegation-reference.md"]
|
|
1728
|
+
.map((file) => readFileSync(join(PACKAGE_ROOT, "assets", file), "utf8"))
|
|
1729
|
+
.join("\n\n");
|
|
1730
|
+
// S9 canon change (gentle-ai 87a3b2eb): the canonical heading is "Delegation Triggers" (no "Mandatory").
|
|
1731
|
+
const reference = "For bounded writes, follow the canonical Writer rule under Delegation Triggers.";
|
|
1730
1732
|
const resolveSimpleDelegation = (source: string): string => {
|
|
1731
1733
|
const simpleDelegation = readMarkdownSection(source, "2. Simple Delegation");
|
|
1732
1734
|
assert.ok(simpleDelegation.split("\n").includes(reference), "Simple Delegation must name the exact canonical Writer rule");
|
|
1733
|
-
const canonical = readMarkdownSection(source, "
|
|
1735
|
+
const canonical = readMarkdownSection(source, "Delegation Triggers");
|
|
1734
1736
|
assertWorkerFallbackRouting(canonical, "resolved Simple Delegation");
|
|
1735
1737
|
return canonical;
|
|
1736
1738
|
};
|
|
1737
|
-
const
|
|
1739
|
+
const delegationTriggers = readMarkdownSection(routing, "Delegation Triggers");
|
|
1738
1740
|
|
|
1739
1741
|
assertWorkerFallbackRouting(resolveSimpleDelegation(routing), "Simple Delegation");
|
|
1740
|
-
assertWorkerFallbackRouting(
|
|
1742
|
+
assertWorkerFallbackRouting(delegationTriggers, "Delegation Triggers");
|
|
1741
1743
|
assert.throws(() => resolveSimpleDelegation(routing.replace(reference, "")), /must name the exact canonical Writer rule/);
|
|
1742
|
-
assert.throws(() => resolveSimpleDelegation(routing.replace(reference, reference.replace("
|
|
1744
|
+
assert.throws(() => resolveSimpleDelegation(routing.replace(reference, reference.replace("Delegation Triggers", "Other Rule"))),
|
|
1743
1745
|
/must name the exact canonical Writer rule/);
|
|
1744
|
-
assert.throws(() => resolveSimpleDelegation(routing.replace("####
|
|
1745
|
-
/exactly one
|
|
1746
|
+
assert.throws(() => resolveSimpleDelegation(routing.replace("#### Delegation Triggers", "#### Missing Canonical Rule")),
|
|
1747
|
+
/exactly one Delegation Triggers section/);
|
|
1746
1748
|
assert.throws(() => resolveSimpleDelegation(routing.replace("user-configured `worker`", "unspecified worker")),
|
|
1747
1749
|
/must prefer the package-owned worker before a user-configured worker/);
|
|
1748
1750
|
assert.doesNotMatch(
|
|
@@ -1758,8 +1760,10 @@ test("bounded implementation routing resolves the explicit canonical fallback re
|
|
|
1758
1760
|
});
|
|
1759
1761
|
|
|
1760
1762
|
test("orchestrator routes generic roles without static RDD lens routing", () => {
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
+
// S11 T2: the explorer fallback binding moved from the delegation card to its reference; the card and
|
|
1764
|
+
// the reference together are the lazy delegation route.
|
|
1765
|
+
for (const files of [["orchestrator.md"], ["orchestrator-delegation.md", "orchestrator-delegation-reference.md"]]) {
|
|
1766
|
+
const routing = files.map((file) => readFileSync(join(PACKAGE_ROOT, "assets", file), "utf8")).join("\n\n");
|
|
1763
1767
|
assert.match(routing, /`gentle-ai-explore`/);
|
|
1764
1768
|
assert.match(routing, /`gentle-ai-worker`/);
|
|
1765
1769
|
assert.match(routing, /`gentle-ai-verify`/);
|
|
@@ -1789,9 +1793,9 @@ test("pi-pretty wrapper uses cached ESM loading for compiled and pnpm symlink in
|
|
|
1789
1793
|
assert.match(wrapper, /quietToolsEnabled/);
|
|
1790
1794
|
});
|
|
1791
1795
|
|
|
1792
|
-
test("Gentle Shell v4.
|
|
1796
|
+
test("Gentle Shell v4.3.0 package manifest declares the release version", () => {
|
|
1793
1797
|
const packageJson = readPackageJson();
|
|
1794
|
-
assert.equal(packageJson.version, "4.
|
|
1798
|
+
assert.equal(packageJson.version, "4.3.0", "the release manifest must be explicitly pinned to v4.3.0");
|
|
1795
1799
|
assert.equal(packageJson.scripts?.test, "node scripts/run-test-suite.mjs");
|
|
1796
1800
|
assert.ok(packageJson.files?.includes("assets/"));
|
|
1797
1801
|
assert.ok(packageJson.files?.includes("contracts/"));
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
|
|
5
|
+
const PI = '@earendil-works/pi-coding-agent';
|
|
6
|
+
// A virtual symlink filesystem: listed global links point into a pnpm store.
|
|
7
|
+
// The store's adjacent peer is a sibling, not Shell/node_modules/Pi.
|
|
8
|
+
function launch(args: string[], scenario = 'owned', piVersion = '1.2.0') {
|
|
9
|
+
const adjacent = scenario.startsWith('adjacent');
|
|
10
|
+
const npm = scenario.startsWith('npm-');
|
|
11
|
+
const nodeModules = '/global/v11/selected/node_modules';
|
|
12
|
+
const shellRoot = '/store/shell-peer/node_modules/gentle-pi';
|
|
13
|
+
const piRoot = adjacent ? '/store/shell-peer/node_modules/' + PI : '/store/pi/node_modules/' + PI;
|
|
14
|
+
const target = adjacent ? 'gentle-pi' : PI;
|
|
15
|
+
const dependencies: Record<string, object> = {[target]:{path: `${nodeModules}/${target}`,
|
|
16
|
+
...(scenario === 'npm-link' ? {link:true} : {}),
|
|
17
|
+
...(scenario === 'npm-file' ? {resolved:'file:/source/pi'} : {})}};
|
|
18
|
+
if (scenario.endsWith('shared')) dependencies['unrelated'] = {path:`${nodeModules}/unrelated`};
|
|
19
|
+
if (scenario === 'separate') dependencies['unrelated'] = {path:'/global/v11/other/node_modules/unrelated'};
|
|
20
|
+
const groups = [{path:'/global/v11',dependencies}, ...(scenario === 'ambiguous' ? [{path:'/global/other',dependencies}] : [])];
|
|
21
|
+
const listing = scenario === 'malformed' ? '{' : JSON.stringify(npm ? groups[0] : groups);
|
|
22
|
+
// pnpm links only the top-level package into the global group; the peer
|
|
23
|
+
// is reachable solely as the Shell package's store sibling.
|
|
24
|
+
const links = {[`${nodeModules}/${target}`]:adjacent ? shellRoot : piRoot};
|
|
25
|
+
const roots = [nodeModules, '/global/v11/other/node_modules', shellRoot, piRoot, `${piRoot}/dist/bundle/cli.js`];
|
|
26
|
+
const bootstrap = `import { registerHooks } from 'node:module';
|
|
27
|
+
const original = ${JSON.stringify(new URL('../bin/gentle-shell.mjs', import.meta.url).href)};
|
|
28
|
+
registerHooks({load(url, context, next) {
|
|
29
|
+
if (url === original) {
|
|
30
|
+
let source = next(url, context).source.toString();
|
|
31
|
+
source = source.replace('const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));', ${JSON.stringify(`const packageRoot = '${shellRoot}';`)});
|
|
32
|
+
source = source.replace('const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });', 'const bundled = undefined;');
|
|
33
|
+
source = source.replace('runtime = resolvePiRuntime({', ${JSON.stringify(`runtime = ({ kind: '${adjacent ? 'bundled' : 'env'}', command: '${adjacent ? 'node' : piRoot + '/dist/bundle/cli.js'}', args: ${JSON.stringify(adjacent ? [piRoot + '/dist/bundle/cli.js'] : [])} }); /*`)}).replace('nodeExecPath: process.execPath,\\n\\t\\t});', 'nodeExecPath: process.execPath, */');
|
|
34
|
+
source = source.replace('const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {', 'const versionProbe = { stdout: "${piVersion}" }; /*').replace('env: bundledEnv ?? process.env,\\n\\t});', 'env: bundledEnv ?? process.env, */');
|
|
35
|
+
source = source.replace('async function packageManagerAdapters() {', 'async function packageManagerAdapters() { return globalThis.fakeAdapters; } async function unusedAdapters() {');
|
|
36
|
+
source = source.replace('let homeHadContentBeforeBootstrap = false;', 'if (passthrough.length === 1) { console.error("error: pi cannot self-update this installation. This installation is not managed by a global pnpm install."); process.exitCode = 1; return; } console.log("FORWARD " + JSON.stringify(passthrough)); return; let homeHadContentBeforeBootstrap = false;');
|
|
37
|
+
return {format:'module', source, shortCircuit:true};
|
|
38
|
+
}
|
|
39
|
+
return next(url,context);
|
|
40
|
+
}});
|
|
41
|
+
const links = ${JSON.stringify(links)}, roots = ${JSON.stringify(roots)};
|
|
42
|
+
globalThis.fakeAdapters = {
|
|
43
|
+
fs: { realpath: async p => { if (links[p]) return links[p]; if (roots.includes(p)) return p; throw new Error('ENOENT ' + p); },
|
|
44
|
+
readFile: async p => { if (p === '${shellRoot}/package.json') return JSON.stringify({name:'gentle-pi',version:'4.1.0'}); if(p === '${piRoot}/package.json') return JSON.stringify({name:'${PI}',version:'1.2.0',bin:{pi:'dist/bundle/cli.js'}}); throw new Error('ENOENT ' + p); } },
|
|
45
|
+
which: async n => n === '${npm ? 'npm' : 'pnpm'}' ? '/fake/' + n : null,
|
|
46
|
+
run: async (cmd, args) => {
|
|
47
|
+
if(args.includes('list') || args.includes('ls')) return {code:0,stdout:${JSON.stringify(listing)}};
|
|
48
|
+
console.log('MUTATION ' + JSON.stringify(args)); return {code:0};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
await import(original);`;
|
|
52
|
+
return spawnSync(process.execPath, ['--import', `data:text/javascript,${encodeURIComponent(bootstrap)}`, 'bin/gentle-shell.mjs', ...args], {encoding:'utf8'});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const scenario of ['owned','adjacent','separate']) test(`public linked-store update succeeds for ${scenario}`, () => {
|
|
56
|
+
const result = launch(['update'], scenario);
|
|
57
|
+
assert.equal(result.status, 0, result.stderr);
|
|
58
|
+
assert.match(result.stdout, scenario === 'adjacent' ? /MUTATION \["add","-g","gentle-pi@4\.1\.0","--allow-build=gentle-pi"\]/ : /MUTATION \["add","-g","@earendil-works\/pi-coding-agent@latest"\]/);
|
|
59
|
+
assert.doesNotMatch(result.stdout, /FORWARD/);
|
|
60
|
+
// The fake manager changes nothing, so the readback reports the same version and pnpm's release-age rule.
|
|
61
|
+
assert.match(result.stdout, /the selected Pi is still 1\.2\.0\./);
|
|
62
|
+
assert.match(result.stdout, /minimumReleaseAge/);
|
|
63
|
+
assert.equal(result.stderr, '');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('public all updates only its owner and then forwards extension updates', () => {
|
|
67
|
+
const result = launch(['update', '--all']);
|
|
68
|
+
assert.equal(result.status, 0, result.stderr);
|
|
69
|
+
assert.match(result.stdout, /MUTATION .*add.*@earendil-works\/pi-coding-agent@latest/);
|
|
70
|
+
assert.match(result.stdout, /FORWARD \["update","--extensions"\]/);
|
|
71
|
+
assert.equal(result.stderr, '');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const scenario of ['ambiguous', 'malformed', 'npm-link', 'npm-file', 'shared', 'adjacent-shared']) test(`public update rejects ${scenario} ownership without mutation`, () => {
|
|
75
|
+
const result = launch(['update'], scenario);
|
|
76
|
+
assert.equal(result.status, 1, result.stdout);
|
|
77
|
+
assert.equal(result.stdout, '');
|
|
78
|
+
assert.match(result.stderr, /ownership/);
|
|
79
|
+
assert.doesNotMatch(result.stdout + result.stderr, /MUTATION/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// `update` is how an old Pi gets replaced, so the minimum-version gate must not block it.
|
|
83
|
+
test('public update replaces a Pi older than the required minimum', () => {
|
|
84
|
+
const result = launch(['update'], 'owned', '1.1.0');
|
|
85
|
+
assert.equal(result.status, 0, result.stderr);
|
|
86
|
+
assert.match(result.stdout, /MUTATION \["add","-g","@earendil-works\/pi-coding-agent@latest"\]/);
|
|
87
|
+
assert.doesNotMatch(result.stdout + result.stderr, /older than the required minimum/);
|
|
88
|
+
assert.equal(result.stderr, '');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('an old Pi still fails the minimum-version gate outside a self update', () => {
|
|
92
|
+
for (const args of [['list'], ['update', '--extensions-only']]) {
|
|
93
|
+
const result = launch(args, 'owned', '1.1.0');
|
|
94
|
+
assert.equal(result.status, 1, result.stdout);
|
|
95
|
+
assert.equal(result.stdout, '');
|
|
96
|
+
assert.match(result.stderr, /pi version 1\.1\.0 is older than the required minimum 1\.2\.0\./);
|
|
97
|
+
assert.doesNotMatch(result.stdout, /MUTATION/);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('public extensions-only update preserves the forwarded command', () => {
|
|
102
|
+
const result = launch(['update', '--extensions-only']);
|
|
103
|
+
assert.equal(result.status, 0, result.stderr);
|
|
104
|
+
assert.equal(result.stdout, 'FORWARD ["update","--extensions-only"]\n');
|
|
105
|
+
assert.equal(result.stderr, '');
|
|
106
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { managedUpdatePlan, updateSelectedPi } from '../scripts/pi-update.mjs';
|
|
4
|
+
import { bundledUpdatePlan, bundledUpdateNext } from '../runtime/gentle-shell-bundled.mjs';
|
|
5
|
+
|
|
6
|
+
const PI = '@earendil-works/pi-coding-agent';
|
|
7
|
+
const CLI = 'dist/bundle/cli.js';
|
|
8
|
+
|
|
9
|
+
// The layouts pnpm v11 and npm really produce. pnpm links each top-level
|
|
10
|
+
// package from its own global group into the linked store, where Shell's
|
|
11
|
+
// peer is a sibling of the Shell package; a separate global Pi links to the
|
|
12
|
+
// same store entry. npm nests the peer under the Shell package.
|
|
13
|
+
function fixture({windows = false, npm = false, runtime = 'adjacent', groups = 'both', peerAfter = '1.2.0', shellVersion = '4.1.0',
|
|
14
|
+
badManifest = false, failed = false, mismatch = false, linked = false, readback = true, unrelatedPeer = false, shim = ''} = {}) {
|
|
15
|
+
const base = windows ? 'C:/pnpm' : '/pnpm';
|
|
16
|
+
const storePi = (version: string) => `${base}/store/links/@earendil-works/pi-coding-agent/${version}/h/node_modules/${PI}`;
|
|
17
|
+
const shellModules = npm ? `${base}/npm/lib/node_modules` : `${base}/store/links/@/gentle-pi/4.1.0/h1/node_modules`;
|
|
18
|
+
const shell = `${shellModules}/gentle-pi`;
|
|
19
|
+
const nestedPeer = `${shell}/node_modules/${PI}`;
|
|
20
|
+
const g1 = npm ? shellModules : `${base}/global/v11/g1/node_modules`;
|
|
21
|
+
const g2 = npm ? shellModules : `${base}/global/v11/g2/node_modules`;
|
|
22
|
+
const g3 = `${base}/global/v11/g3/node_modules`;
|
|
23
|
+
const state = {peer: '1.1.0', group: '1.1.0'};
|
|
24
|
+
const peerRoot = () => npm ? nestedPeer : storePi(state.peer);
|
|
25
|
+
const selectedCli = `${npm ? (runtime === 'adjacent' ? nestedPeer : `${shellModules}/${PI}`) : storePi('1.1.0')}/${CLI}`;
|
|
26
|
+
// A package-manager shim on PATH: its text names the global Pi group's cli.js.
|
|
27
|
+
const shimFile = `${base}/bin/pi${windows ? '.CMD' : ''}`;
|
|
28
|
+
const shimText = shim === 'posix' ? `#!/bin/sh\nbasedir=$(dirname "$0")\nexec node "$basedir/../global/v11/g2/node_modules/${PI}/${CLI}" "$@"\n`
|
|
29
|
+
: shim === 'windows' ? `@"%~dp0\\..\\global\\v11\\g2\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\bundle\\cli.js" %*\r\n`
|
|
30
|
+
: shim === 'two' ? `"$basedir/../a/dist/bundle/cli.js" "$basedir/../b/dist/bundle/cli.js"` : shim === 'none' ? '#!/bin/sh\nexec something\n' : '';
|
|
31
|
+
const mutations: string[][] = [];
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
const cwds: unknown[] = [];
|
|
34
|
+
const entry = (pkg: string, modules: string, extra = {}) => ({[pkg]: {path: `${modules}/${pkg}`, ...extra}});
|
|
35
|
+
const shellEntry = entry('gentle-pi', g1, linked ? {link: true} : {});
|
|
36
|
+
const piEntry = entry(PI, g2, linked ? {resolved: 'file:/src/pi'} : {});
|
|
37
|
+
const listing = () => {
|
|
38
|
+
const deps = {...(groups !== 'pi' ? shellEntry : {}), ...(groups !== 'shell' ? piEntry : {}),
|
|
39
|
+
...(groups === 'shared' ? entry('unrelated-tool', g1) : {}), ...(groups === 'separate' ? entry('unrelated-tool', g3) : {})};
|
|
40
|
+
const group = {path: npm ? shellModules : `${base}/global/v11`, dependencies: deps};
|
|
41
|
+
return npm ? group : [group];
|
|
42
|
+
};
|
|
43
|
+
const exists = (n: string) => [g1, g2, g3, shellModules, shell, storePi(state.peer), storePi(state.group), storePi('1.1.0'), nestedPeer]
|
|
44
|
+
.some(root => n === root || n.startsWith(root + '/'));
|
|
45
|
+
const realpath = async (value: string) => {
|
|
46
|
+
const n = value.replaceAll('\\', '/');
|
|
47
|
+
if (n === shimFile) return n;
|
|
48
|
+
// Symlinked components inside a longer path resolve like the real filesystem does.
|
|
49
|
+
if (n.startsWith(`${g2}/${PI}/`)) return (npm ? `${g2}/${PI}` : storePi(state.group)) + n.slice(`${g2}/${PI}`.length);
|
|
50
|
+
if (n === `${g1}/gentle-pi` || n === `${g1}/unrelated-tool` || n === `${g3}/unrelated-tool`) return n === `${g1}/gentle-pi` ? shell : n;
|
|
51
|
+
if (n === `${g2}/${PI}`) return npm ? n : storePi(state.group);
|
|
52
|
+
if (!npm && n === `${shellModules}/${PI}`) return storePi(state.peer);
|
|
53
|
+
if (!npm && n === nestedPeer) throw new Error('ENOENT nested peer');
|
|
54
|
+
// Only joined (backslash) paths mismatch: the selected CLI itself stays canonical.
|
|
55
|
+
if (mismatch && n.endsWith(CLI) && value.includes('\\')) return `${base}/other/cli.js`;
|
|
56
|
+
if (exists(n)) return n;
|
|
57
|
+
throw new Error(`ENOENT ${n}`);
|
|
58
|
+
};
|
|
59
|
+
const readFile = async (value: string) => {
|
|
60
|
+
const n = value.replaceAll('\\', '/');
|
|
61
|
+
if (n === shimFile) return shimText;
|
|
62
|
+
if (n === `${shell}/package.json`) return JSON.stringify({name: 'gentle-pi', ...(shellVersion ? {version: shellVersion} : {})});
|
|
63
|
+
if (n === `${g1}/unrelated-tool/package.json`) return JSON.stringify({name: 'unrelated-tool'});
|
|
64
|
+
const version = n.startsWith(nestedPeer) ? state.peer : /pi-coding-agent\/([\d.]+)\/h\//.exec(n)?.[1];
|
|
65
|
+
if (badManifest || unrelatedPeer) return JSON.stringify({name: unrelatedPeer ? 'other' : PI, version});
|
|
66
|
+
return JSON.stringify({name: PI, version, bin: {pi: CLI}});
|
|
67
|
+
};
|
|
68
|
+
const run = async (_command: string, args: string[], options?: {cwd?: unknown}) => {
|
|
69
|
+
cwds.push(options?.cwd);
|
|
70
|
+
if (args.includes('list') || args.includes('ls')) return {code: failed ? 1 : 0, stdout: readback || mutations.length === 0 ? JSON.stringify(listing()) : '{'};
|
|
71
|
+
mutations.push(args);
|
|
72
|
+
if (args.some(arg => arg.startsWith('gentle-pi@'))) state.peer = peerAfter;
|
|
73
|
+
if (args.some(arg => arg.startsWith(PI + '@'))) state.group = peerAfter;
|
|
74
|
+
return {code: 0};
|
|
75
|
+
};
|
|
76
|
+
return {mutations, out, cwds, state, shell, selectedCli, input: {platform: windows ? 'win32' : 'linux', packageRoot: `${g1}/gentle-pi`, currentVersion: '1.1.0', cwd: '/neutral',
|
|
77
|
+
runtime: runtime === 'adjacent' ? {kind: 'bundled', command: 'node', args: [selectedCli]} : {kind: runtime, command: shim ? shimFile : selectedCli, args: []},
|
|
78
|
+
out: (line: string) => out.push(line),
|
|
79
|
+
adapters: {which: async (name: string) => name === (npm ? 'npm' : 'pnpm') ? `/fake/${name}` : null, fs: {realpath, readFile}, run}}};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test('prefix distribution still redirects self update and forwards only extensions for all', () => {
|
|
83
|
+
assert.deepEqual(bundledUpdatePlan(['update']), {self: true});
|
|
84
|
+
assert.deepEqual(bundledUpdateNext(bundledUpdatePlan(['update']), 0), {exitCode: 0});
|
|
85
|
+
assert.deepEqual(bundledUpdatePlan(['update', '--all']), {self: true, piArgs: ['update', '--extensions']});
|
|
86
|
+
assert.deepEqual(bundledUpdatePlan(['update', '--extensions-only']), {self: false, piArgs: ['update', '--extensions-only']});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const args of [['update', '--extensions-only'], ['update', '--extensions'], ['update', 'npm:custom'], ['update', '--models'], ['update', '--help'], ['update', '--unknown']]) test(`no ownership interception for ${args.join(' ')}`, () => {
|
|
90
|
+
assert.deepEqual(managedUpdatePlan(args), {self: false, piArgs: args});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('all strips only self update before forwarding extension flags', () => {
|
|
94
|
+
assert.deepEqual(managedUpdatePlan(['update', '--all', '--approve']), {self: true, piArgs: ['update', '--extensions', '--approve']});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for (const windows of [false, true]) test(`an adjacent peer is refreshed by reinstalling its Shell package at the same version, never the separate global Pi (windows: ${windows})`, async () => {
|
|
98
|
+
const f = fixture({windows});
|
|
99
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
100
|
+
assert.deepEqual(f.mutations, [['add', '-g', 'gentle-pi@4.1.0', '--allow-build=gentle-pi']]);
|
|
101
|
+
assert.deepEqual(f.out, ['pnpm installed gentle-pi@4.1.0; the selected Pi is now 1.2.0 (was 1.1.0).']);
|
|
102
|
+
assert.equal(f.state.group, '1.1.0');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
for (const runtime of ['path', 'env']) test(`a ${runtime} runtime sharing the store entry is updated through its own global Pi package`, async () => {
|
|
106
|
+
const f = fixture({runtime});
|
|
107
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
108
|
+
assert.deepEqual(f.mutations, [['add', '-g', `${PI}@latest`]]);
|
|
109
|
+
assert.deepEqual(f.out, [`pnpm installed ${PI}@latest; the selected Pi is now 1.2.0 (was 1.1.0).`]);
|
|
110
|
+
assert.equal(f.state.peer, '1.1.0');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('npm nests the peer under Shell and reinstalls Shell for it', async () => {
|
|
114
|
+
const f = fixture({npm: true});
|
|
115
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
116
|
+
assert.deepEqual(f.mutations, [['install', '-g', 'gentle-pi@4.1.0']]);
|
|
117
|
+
assert.deepEqual(f.out, ['npm installed gentle-pi@4.1.0; the selected Pi is now 1.2.0 (was 1.1.0).']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('an npm PATH runtime updates the global Pi without its scripts', async () => {
|
|
121
|
+
const f = fixture({npm: true, runtime: 'path'});
|
|
122
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
123
|
+
assert.deepEqual(f.mutations, [['install', '-g', `${PI}@latest`, '--ignore-scripts']]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('an unchanged pnpm result names the minimum-release-age rule instead of claiming latest', async () => {
|
|
127
|
+
const f = fixture({peerAfter: '1.1.0'});
|
|
128
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
129
|
+
assert.equal(f.out[0], 'pnpm installed gentle-pi@4.1.0, and the selected Pi is still 1.1.0.');
|
|
130
|
+
assert.match(f.out[1], /minimumReleaseAge/);
|
|
131
|
+
assert.equal(f.out.length, 2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('every package manager call runs from the neutral directory, never the project cwd', async () => {
|
|
135
|
+
const f = fixture();
|
|
136
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
137
|
+
assert.deepEqual(f.cwds, ['/neutral', '/neutral', '/neutral']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('an unreadable readback is reported, not invented', async () => {
|
|
141
|
+
const f = fixture({readback: false});
|
|
142
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
143
|
+
assert.deepEqual(f.out, ['pnpm installed gentle-pi@4.1.0. The Pi version it now supplies could not be read back; run \'gentle-shell --version\' to check it.']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('separate aggregated groups do not block the Shell reinstall', async () => {
|
|
147
|
+
const f = fixture({groups: 'separate'});
|
|
148
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
149
|
+
assert.deepEqual(f.mutations, [['add', '-g', 'gentle-pi@4.1.0', '--allow-build=gentle-pi']]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
for (const [name, options] of Object.entries({
|
|
153
|
+
'listing failed': {failed: true},
|
|
154
|
+
'peer identity differs': {badManifest: true},
|
|
155
|
+
'peer package is not Pi': {unrelatedPeer: true},
|
|
156
|
+
'selected CLI mismatches on Windows': {windows: true, mismatch: true},
|
|
157
|
+
'linked or file suppliers': {linked: true},
|
|
158
|
+
'Shell shares its global group': {groups: 'shared'},
|
|
159
|
+
'Shell version is missing': {shellVersion: ''},
|
|
160
|
+
'only a separate global Pi is listed for an adjacent runtime': {groups: 'pi'},
|
|
161
|
+
'only Shell is listed for a PATH runtime': {runtime: 'path', groups: 'shell'},
|
|
162
|
+
})) test(`ownership rejects without mutation when ${name}`, async () => {
|
|
163
|
+
const f = fixture(options);
|
|
164
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
165
|
+
assert.deepEqual(f.mutations, []);
|
|
166
|
+
assert.deepEqual(f.out, []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
for (const [name, options] of Object.entries({'pnpm POSIX shim': {shim: 'posix', runtime: 'path'}, 'Windows .CMD shim': {shim: 'windows', runtime: 'path', windows: true}, 'GENTLE_SHELL_PI shim override': {shim: 'posix', runtime: 'env'}})) test(`a ${name} resolves to the global Pi it runs`, async () => {
|
|
170
|
+
const f = fixture(options);
|
|
171
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
172
|
+
assert.deepEqual(f.mutations, [['add', '-g', `${PI}@latest`]]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
for (const [name, shim] of Object.entries({'names several targets': 'two', 'is not a Pi entry': 'none'})) test(`a shim that ${name} is refused without mutation`, async () => {
|
|
176
|
+
const f = fixture({shim, runtime: 'path'});
|
|
177
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
178
|
+
assert.deepEqual(f.mutations, []);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('an unrelated selected override cannot authorize the adjacent Shell group', async () => {
|
|
182
|
+
const f = fixture();
|
|
183
|
+
f.input.runtime = {kind: 'env', command: '/unrelated/pi', args: []};
|
|
184
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
185
|
+
assert.deepEqual(f.mutations, []);
|
|
186
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test, { after, before } from "node:test";
|
|
7
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
+
|
|
9
|
+
// gentle-ai R3 (odd/tasks/pi-routing-from-bundle.md): the harness ODD step
|
|
10
|
+
// lines 1-7, the phase-reporting line, and the resume line come from the
|
|
11
|
+
// mirrored provider contract bundle's routing/pi.md instead of a hand-copied
|
|
12
|
+
// literal, and the rendered primary-session prompt stays byte-identical.
|
|
13
|
+
//
|
|
14
|
+
// The golden was captured from the base commit (main 4372ff2d, after R1) before the steps moved into the
|
|
15
|
+
// bundle, with the inputs below and the package assets root normalized. When a
|
|
16
|
+
// prompt change is intended, regenerate it with
|
|
17
|
+
// UPDATE_PRIMARY_SESSION_PROMPT_GOLDEN=1 and review the golden diff.
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = join(import.meta.dirname, "..");
|
|
20
|
+
const GOLDEN_PATH = join(REPO_ROOT, "tests", "fixtures", "primary-session-prompt", "gentleman.golden.md");
|
|
21
|
+
const ACTIVE_TOOLS = ["read", "bash", "edit", "write", "subagent_run"] as const;
|
|
22
|
+
const MIRROR_ROOT = join(REPO_ROOT, "contracts", "review-provider-contract-mirror");
|
|
23
|
+
|
|
24
|
+
let scratch: string;
|
|
25
|
+
const previousEnvironment = new Map<string, string | undefined>();
|
|
26
|
+
before(() => {
|
|
27
|
+
scratch = mkdtempSync(join(tmpdir(), "gentle-pi-primary-prompt-"));
|
|
28
|
+
for (const key of ["GENTLE_PI_CONFIG_HOME", "GENTLE_PI_BACKGROUND_SUBAGENTS"]) previousEnvironment.set(key, process.env[key]);
|
|
29
|
+
process.env.GENTLE_PI_CONFIG_HOME = join(scratch, "config");
|
|
30
|
+
delete process.env.GENTLE_PI_BACKGROUND_SUBAGENTS;
|
|
31
|
+
});
|
|
32
|
+
after(() => {
|
|
33
|
+
for (const [key, value] of previousEnvironment) {
|
|
34
|
+
if (value === undefined) delete process.env[key];
|
|
35
|
+
else process.env[key] = value;
|
|
36
|
+
}
|
|
37
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function renderPrimaryPrompt(): string {
|
|
41
|
+
const prompt = __testing.buildGentlePrompt("gentleman", join(scratch, "project"), ACTIVE_TOOLS);
|
|
42
|
+
return prompt.replaceAll(join(REPO_ROOT, "assets"), "{{GENTLE_PI_ASSETS_ROOT}}");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mirroredRoutingText(): string {
|
|
46
|
+
const lock = JSON.parse(readFileSync(join(MIRROR_ROOT, "provider-contract.lock.json"), "utf8")) as { contract_semver: string };
|
|
47
|
+
return readFileSync(join(MIRROR_ROOT, `v${lock.contract_semver}`, "bundle", "routing", "pi.md"), "utf8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Writes a one-file mirror whose lock records `entrySha256` for routing/pi.md.
|
|
51
|
+
function tempRoutingMirror(text: string, entrySha256 = createHash("sha256").update(text, "utf8").digest("hex")): string {
|
|
52
|
+
const root = mkdtempSync(join(scratch, "mirror-"));
|
|
53
|
+
const routingDir = join(root, "v9.9.9", "bundle", "routing");
|
|
54
|
+
mkdirSync(routingDir, { recursive: true });
|
|
55
|
+
writeFileSync(join(routingDir, "pi.md"), text, "utf8");
|
|
56
|
+
writeFileSync(join(root, "provider-contract.lock.json"), JSON.stringify({ contract_semver: "9.9.9", entries: { "routing/pi.md": entrySha256 } }), "utf8");
|
|
57
|
+
return root;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("the rendered primary-session prompt is byte-identical to the golden captured from the base", () => {
|
|
61
|
+
const rendered = renderPrimaryPrompt();
|
|
62
|
+
if (process.env.UPDATE_PRIMARY_SESSION_PROMPT_GOLDEN === "1") writeFileSync(GOLDEN_PATH, rendered, "utf8");
|
|
63
|
+
assert.equal(rendered, readFileSync(GOLDEN_PATH, "utf8"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the ODD steps, phase reporting, and resume lines come from the mirrored routing/pi.md without its blank lines", () => {
|
|
67
|
+
const routing = mirroredRoutingText();
|
|
68
|
+
const section = routing.slice(routing.indexOf("### ODD protocol"), routing.indexOf("\n### Task Size"));
|
|
69
|
+
const expected = section.split("\n").slice(1).filter((line) => line !== "");
|
|
70
|
+
assert.equal(expected.length, 9);
|
|
71
|
+
assert.equal(__testing.readMirroredOddProtocolSteps(), expected.join("\n"));
|
|
72
|
+
assert.ok(renderPrimaryPrompt().includes(`Run these steps in this order on every request:\n${expected.join("\n")}\n\nHarness principles:`));
|
|
73
|
+
assert.ok(routing.includes(`${expected[6]}\n\n${expected[7]}\n\n${expected[8]}`), "the bundle separates step 7, phase reporting, and resume with blank lines");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("extensions/gentle-ai.ts no longer carries the ODD step lines as string literals", () => {
|
|
77
|
+
const source = readFileSync(join(REPO_ROOT, "extensions", "gentle-ai.ts"), "utf8");
|
|
78
|
+
for (const literal of ["1. **Authorize.**", "5. **Track before the first write.**", "7. **Close.**", "Phase reporting: the Gentle Shell prompt label", "Resume an interrupted feature with"]) {
|
|
79
|
+
assert.ok(!source.includes(literal), `extensions/gentle-ai.ts still hard-codes ${JSON.stringify(literal)}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a changed mirrored routing block changes the rendered steps", () => {
|
|
84
|
+
const routing = mirroredRoutingText().replace("1. **Authorize.** ", "1. **Authorize.** Changed upstream. ");
|
|
85
|
+
const mirror = tempRoutingMirror(routing);
|
|
86
|
+
assert.match(__testing.readMirroredOddProtocolSteps(mirror), /^1\. \*\*Authorize\.\*\* Changed upstream\. /);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a tampered or missing mirrored routing/pi.md fails closed with an explicit error", () => {
|
|
90
|
+
const unavailable = /Gentle Shell cannot render its ODD steps: the mirrored provider bundle's routing\/pi\.md is missing, unreadable, or fails digest verification/;
|
|
91
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(tempRoutingMirror(mirroredRoutingText(), "0".repeat(64))), unavailable);
|
|
92
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(join(scratch, "absent-mirror")), unavailable);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a routing block whose ODD protocol section changed shape fails closed", () => {
|
|
96
|
+
const unexpectedShape = /routing\/pi\.md ODD protocol section has an unexpected shape/;
|
|
97
|
+
const withoutResume = mirroredRoutingText().replace(/\nResume an interrupted feature[^\n]*\n/, "\n");
|
|
98
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(tempRoutingMirror(withoutResume)), unexpectedShape);
|
|
99
|
+
const withoutSection = mirroredRoutingText().replace("### ODD protocol (MANDATORY, in this order, on every request)", "### ODD protocol");
|
|
100
|
+
assert.throws(() => __testing.readMirroredOddProtocolSteps(tempRoutingMirror(withoutSection)), unexpectedShape);
|
|
101
|
+
});
|