gentle-pi 4.2.0 → 4.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 +340 -138
- 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/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- 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 +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- 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/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- 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-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +485 -7
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +142 -0
- 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/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- 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 +645 -21
- 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-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- 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 +79 -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 +147 -3
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- 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/session-changes-shell.test.ts +1 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- 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 +70 -10
- 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 +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- 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
|
@@ -74,6 +74,35 @@ test("alias identity preserves status age, validates data, and rejects stale bra
|
|
|
74
74
|
assert.deepEqual(cache.get(manager)?.aliases, valid.aliases);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
test("explicit work topic refreshes the current alias when no subject is declared (#2553)", () => {
|
|
78
|
+
const cache = new OrchestratorStateCache();
|
|
79
|
+
const entries: { type: string; customType: string; data: unknown }[] = [];
|
|
80
|
+
const manager = { getSessionId: () => "owner", getCwd: () => "/repo", getBranch: () => entries };
|
|
81
|
+
const append = (customType: string, data: unknown) => entries.push({ type: "custom", customType, data });
|
|
82
|
+
cache.load(manager);
|
|
83
|
+
const work = { area: "gentle-shell", topic: "pr-2509-followup" };
|
|
84
|
+
cache.publish(manager, { work }, append, 10);
|
|
85
|
+
assert.deepEqual(cache.get(manager)?.aliases, { initialAlias: "pr-2509-followup", currentAlias: "pr-2509-followup" });
|
|
86
|
+
// Fresh topic on a later publication updates Current; Initial is retained.
|
|
87
|
+
cache.publish(manager, { work: { area: "gentle-shell", topic: "ci-watch" } }, append, 20);
|
|
88
|
+
assert.deepEqual(cache.get(manager)?.aliases, { initialAlias: "pr-2509-followup", currentAlias: "ci-watch" });
|
|
89
|
+
// A state-only refresh never re-derives the alias from stored work.
|
|
90
|
+
cache.publish(manager, undefined, append, 30);
|
|
91
|
+
assert.deepEqual(cache.get(manager)?.aliases, { initialAlias: "pr-2509-followup", currentAlias: "ci-watch" });
|
|
92
|
+
// An explicit subject in the same publication wins over the topic.
|
|
93
|
+
cache.publish(manager, { work }, append, 40, "declared-task");
|
|
94
|
+
assert.deepEqual(cache.get(manager)?.aliases, { initialAlias: "pr-2509-followup", currentAlias: "declared-task" });
|
|
95
|
+
// Legacy records without aliases: topic refreshes Current, Initial stays unknown.
|
|
96
|
+
const legacy = [{ type: "custom", customType: ORCHESTRATOR_STATE_ENTRY, data: {
|
|
97
|
+
schema: 1, sessionId: "owner", recordedAt: 1, cwd: "/repo", source: "owner-curated",
|
|
98
|
+
ownerReply: false, authority: "none", state: null } }];
|
|
99
|
+
const legacyManager = { getSessionId: () => "owner", getCwd: () => "/repo", getBranch: () => legacy };
|
|
100
|
+
const legacyCache = new OrchestratorStateCache();
|
|
101
|
+
legacyCache.load(legacyManager);
|
|
102
|
+
legacyCache.publish(legacyManager, { work }, append, 50);
|
|
103
|
+
assert.deepEqual(legacyCache.get(legacyManager)?.aliases, { initialAlias: null, currentAlias: "pr-2509-followup" });
|
|
104
|
+
});
|
|
105
|
+
|
|
77
106
|
test("recorded cwd preserves only unambiguous native absolute paths", () => {
|
|
78
107
|
const separators = [0x00a0, ...Array.from({ length: 11 }, (_, i) => 0x2000 + i), 0x202f, 0x205f, 0x3000];
|
|
79
108
|
const invalid = ["relative/repo", "/repo\nother", "/repo\ud800", "/" + "x".repeat(1024),
|
|
@@ -8,16 +8,20 @@ const source = readFileSync(join(root, "extensions/gentle-agents.ts"), "utf8");
|
|
|
8
8
|
const sessionTool = source.match(/name: "orchestrator_session_id",\s*label: "[^"]+",\s*description: ("(?:[^"\\]|\\.)*")/);
|
|
9
9
|
assert.ok(sessionTool, "Registered session tool exposes its publication contract");
|
|
10
10
|
const description: string = JSON.parse(sessionTool[1]);
|
|
11
|
-
|
|
11
|
+
// S11 T2: state publication is reference material; the delegation card only points at it.
|
|
12
|
+
const guidance = readFileSync(join(root, "assets/orchestrator-delegation-reference.md"), "utf8");
|
|
12
13
|
const docs = readFileSync(join(root, "docs/gentle-agents-activity.md"), "utf8");
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
// Harness roadmap T6: the runtime publishes todo progress, so guidance stops
|
|
16
|
+
// asking the model to refresh status at every task transition.
|
|
17
|
+
test("shipped guidance declares a subject and leaves progress to the todo list", () => {
|
|
18
|
+
assert.match(description, /Progress is published automatically from the todo list/);
|
|
19
|
+
assert.doesNotMatch(description, /refresh at task changes/);
|
|
16
20
|
assert.doesNotMatch(description, /Before meaningful work, publish subject/);
|
|
17
|
-
assert.match(guidance, /
|
|
21
|
+
assert.match(guidance, /Progress is published automatically from the todo list: never call `orchestrator_session_id` just to report status/);
|
|
22
|
+
assert.match(guidance, /Before work beyond small direct tasks, declare a `subject`/);
|
|
18
23
|
assert.doesNotMatch(guidance, /Before meaningful work or delegation, publish own `state`/);
|
|
19
|
-
assert.
|
|
20
|
-
assert.match(guidance, /Refresh at task changes\/completion/);
|
|
24
|
+
assert.doesNotMatch(guidance, /Refresh at task changes\/completion/);
|
|
21
25
|
assert.doesNotMatch(guidance, /when helpful, publish short explicit own `state`/);
|
|
22
26
|
assert.match(guidance, /Do not require a subject declaration for small direct tasks/);
|
|
23
27
|
assert.match(guidance, /no extra model turn, repeated reads or per-token\/tool updates/);
|
|
@@ -25,10 +29,10 @@ test("shipped guidance publishes status before work and refreshes task transitio
|
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
test("hub documentation separates busy-owner snapshots from replies and negative ownership claims", () => {
|
|
28
|
-
assert.match(docs, /before work beyond
|
|
32
|
+
assert.match(docs, /before work beyond\s+small direct tasks/);
|
|
29
33
|
assert.match(docs, /while the owner is busy/);
|
|
30
34
|
assert.match(docs, /current and next scope/);
|
|
31
|
-
assert.match(docs, /
|
|
35
|
+
assert.match(docs, /Progress is published automatically: after each todo write/);
|
|
32
36
|
assert.match(docs, /A search with no matches does not establish that nobody else is working on the feature/);
|
|
33
37
|
assert.match(docs, /state `recordedAt` and scope `resolvedAt` keep their historical meaning/);
|
|
34
38
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { missingRequiredPaths, requiredPaths } from "../scripts/verify-package-files.mjs";
|
|
5
|
+
|
|
6
|
+
// The voice worker is spawned by path and never imported, so only the package
|
|
7
|
+
// check keeps it in the published tarball next to the modules that launch it.
|
|
8
|
+
|
|
9
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
10
|
+
|
|
11
|
+
test("the package check requires the voice command, its modules and the worker script", () => {
|
|
12
|
+
const voice = ["extensions/gentle-voice.ts", "lib/voice-command.ts", "lib/voice-session.ts", "lib/voice-ui.ts", "scripts/voice-worker.cjs", "docs/voice.md"];
|
|
13
|
+
for (const path of voice) assert.ok(requiredPaths.includes(path), `${path} is required`);
|
|
14
|
+
assert.deepEqual(missingRequiredPaths(root, voice), []);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// The canvas server reads its page and scripts from assets/ by path, never by import.
|
|
18
|
+
const designCanvas = [
|
|
19
|
+
"lib/design-canvas-server.ts",
|
|
20
|
+
"lib/design-open-browser.ts",
|
|
21
|
+
"lib/design-comments.ts",
|
|
22
|
+
"lib/design-handoffs.ts",
|
|
23
|
+
"assets/design-canvas/index.html",
|
|
24
|
+
"assets/design-canvas/canvas.js",
|
|
25
|
+
"assets/design-canvas/canvas.css",
|
|
26
|
+
"assets/design-canvas/inspector.js",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// The design command names its skill file by path when Pi has not loaded it.
|
|
30
|
+
test("the package check requires the design command, its helpers and its skill", () => {
|
|
31
|
+
const design = ["extensions/gentle-design.ts", "lib/gentle-design.ts", "lib/design-sources.ts", "skills/design/SKILL.md", ...designCanvas];
|
|
32
|
+
for (const path of design) assert.ok(requiredPaths.includes(path), `${path} is required`);
|
|
33
|
+
assert.deepEqual(missingRequiredPaths(root, design), []);
|
|
34
|
+
});
|
|
@@ -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.3.1 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\.3\.1"/);
|
|
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:00p8dnjJrhYFq1Qikk2oY6K\/UXIty9A\/IBUUOUiX9XA="/);
|
|
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\.3\.1/);
|
|
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.4.0 package manifest declares the release version", () => {
|
|
1793
1797
|
const packageJson = readPackageJson();
|
|
1794
|
-
assert.equal(packageJson.version, "4.
|
|
1798
|
+
assert.equal(packageJson.version, "4.4.0", "the release manifest must be explicitly pinned to v4.4.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/"));
|
|
@@ -7,7 +7,7 @@ import { syncBuiltinESMExports } from "node:module";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { registerYoloSessionPolicy, discoverYoloUiAdapter } from "../lib/yolo-session-policy.ts";
|
|
10
|
-
import { parsePermissionModePolicyFile, resolvePermissionModePolicy, writePermissionModePolicy, permissionModePolicyFile, PERMISSION_MODE_SCHEMA } from "../lib/permission-mode-policy.ts";
|
|
10
|
+
import { isPermissionMode, parsePermissionModePolicyFile, resolvePermissionModePolicy, writePermissionModePolicy, permissionModePolicyFile, PERMISSION_MODE_SCHEMA } from "../lib/permission-mode-policy.ts";
|
|
11
11
|
|
|
12
12
|
function fixture(t: test.TestContext) {
|
|
13
13
|
const home = mkdtempSync(join(tmpdir(), "gentle-mode-"));
|
|
@@ -192,3 +192,12 @@ test("stale pending activation, scope loss and malformed preference remain close
|
|
|
192
192
|
assert.equal(await h.controller.saveDefault("yolo", h.context), false);
|
|
193
193
|
assert.equal(readFileSync(permissionModePolicyFile(options), "utf8"), "{");
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
test("super-yolo is a valid saved and parsed permission mode", t => {
|
|
197
|
+
const options = fixture(t);
|
|
198
|
+
assert.equal(isPermissionMode("super-yolo"), true);
|
|
199
|
+
assert.equal(isPermissionMode("superyolo"), false);
|
|
200
|
+
const path = writePermissionModePolicy("super-yolo", options);
|
|
201
|
+
assert.equal(parsePermissionModePolicyFile(readFileSync(path, "utf8")), "super-yolo");
|
|
202
|
+
assert.equal(resolvePermissionModePolicy(options).mode, "super-yolo");
|
|
203
|
+
});
|
|
@@ -5,7 +5,7 @@ import { test } from 'node:test';
|
|
|
5
5
|
const PI = '@earendil-works/pi-coding-agent';
|
|
6
6
|
// A virtual symlink filesystem: listed global links point into a pnpm store.
|
|
7
7
|
// The store's adjacent peer is a sibling, not Shell/node_modules/Pi.
|
|
8
|
-
function launch(args: string[], scenario = 'owned') {
|
|
8
|
+
function launch(args: string[], scenario = 'owned', piVersion = '1.2.0') {
|
|
9
9
|
const adjacent = scenario.startsWith('adjacent');
|
|
10
10
|
const npm = scenario.startsWith('npm-');
|
|
11
11
|
const nodeModules = '/global/v11/selected/node_modules';
|
|
@@ -31,7 +31,7 @@ function launch(args: string[], scenario = 'owned') {
|
|
|
31
31
|
source = source.replace('const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));', ${JSON.stringify(`const packageRoot = '${shellRoot}';`)});
|
|
32
32
|
source = source.replace('const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });', 'const bundled = undefined;');
|
|
33
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: "
|
|
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
35
|
source = source.replace('async function packageManagerAdapters() {', 'async function packageManagerAdapters() { return globalThis.fakeAdapters; } async function unusedAdapters() {');
|
|
36
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
37
|
return {format:'module', source, shortCircuit:true};
|
|
@@ -79,6 +79,25 @@ for (const scenario of ['ambiguous', 'malformed', 'npm-link', 'npm-file', 'share
|
|
|
79
79
|
assert.doesNotMatch(result.stdout + result.stderr, /MUTATION/);
|
|
80
80
|
});
|
|
81
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
|
+
|
|
82
101
|
test('public extensions-only update preserves the forwarded command', () => {
|
|
83
102
|
const result = launch(['update', '--extensions-only']);
|
|
84
103
|
assert.equal(result.status, 0, result.stderr);
|
package/tests/pi-update.test.ts
CHANGED
|
@@ -11,8 +11,12 @@ const CLI = 'dist/bundle/cli.js';
|
|
|
11
11
|
// peer is a sibling of the Shell package; a separate global Pi links to the
|
|
12
12
|
// same store entry. npm nests the peer under the Shell package.
|
|
13
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 = ''
|
|
14
|
+
badManifest = false, failed = false, mismatch = false, linked = false, readback = true, unrelatedPeer = false, shim = '',
|
|
15
|
+
artifact = true, configHome = '', installFailure = undefined as undefined | {stdout?: string, stderrTail?: string}} = {}) {
|
|
15
16
|
const base = windows ? 'C:/pnpm' : '/pnpm';
|
|
17
|
+
const home = windows ? 'C:/Users/u' : '/home/u';
|
|
18
|
+
// The tarball `gentle-shell upgrade --channel main` packs, named after its version.
|
|
19
|
+
const tarball = `${configHome || `${home}/.pi/gentle-ai`}/main/packages/gentle-pi-${shellVersion}.tgz`;
|
|
16
20
|
const storePi = (version: string) => `${base}/store/links/@earendil-works/pi-coding-agent/${version}/h/node_modules/${PI}`;
|
|
17
21
|
const shellModules = npm ? `${base}/npm/lib/node_modules` : `${base}/store/links/@/gentle-pi/4.1.0/h1/node_modules`;
|
|
18
22
|
const shell = `${shellModules}/gentle-pi`;
|
|
@@ -31,6 +35,7 @@ function fixture({windows = false, npm = false, runtime = 'adjacent', groups = '
|
|
|
31
35
|
const mutations: string[][] = [];
|
|
32
36
|
const out: string[] = [];
|
|
33
37
|
const cwds: unknown[] = [];
|
|
38
|
+
const runOptions: unknown[] = [];
|
|
34
39
|
const entry = (pkg: string, modules: string, extra = {}) => ({[pkg]: {path: `${modules}/${pkg}`, ...extra}});
|
|
35
40
|
const shellEntry = entry('gentle-pi', g1, linked ? {link: true} : {});
|
|
36
41
|
const piEntry = entry(PI, g2, linked ? {resolved: 'file:/src/pi'} : {});
|
|
@@ -41,7 +46,7 @@ function fixture({windows = false, npm = false, runtime = 'adjacent', groups = '
|
|
|
41
46
|
return npm ? group : [group];
|
|
42
47
|
};
|
|
43
48
|
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 + '/'));
|
|
49
|
+
.some(root => n === root || n.startsWith(root + '/')) || (artifact && n === tarball);
|
|
45
50
|
const realpath = async (value: string) => {
|
|
46
51
|
const n = value.replaceAll('\\', '/');
|
|
47
52
|
if (n === shimFile) return n;
|
|
@@ -69,11 +74,14 @@ function fixture({windows = false, npm = false, runtime = 'adjacent', groups = '
|
|
|
69
74
|
cwds.push(options?.cwd);
|
|
70
75
|
if (args.includes('list') || args.includes('ls')) return {code: failed ? 1 : 0, stdout: readback || mutations.length === 0 ? JSON.stringify(listing()) : '{'};
|
|
71
76
|
mutations.push(args);
|
|
72
|
-
|
|
77
|
+
runOptions.push(options);
|
|
78
|
+
if (installFailure) return {code: 1, stdout: installFailure.stdout ?? '', ...('stderrTail' in installFailure ? {stderrTail: installFailure.stderrTail} : {})};
|
|
79
|
+
if (args.some(arg => arg.startsWith('gentle-pi@') || arg.endsWith('.tgz'))) state.peer = peerAfter;
|
|
73
80
|
if (args.some(arg => arg.startsWith(PI + '@'))) state.group = peerAfter;
|
|
74
81
|
return {code: 0};
|
|
75
82
|
};
|
|
76
|
-
return {mutations, out, cwds, state, shell, selectedCli, input: {platform: windows ? 'win32' : 'linux', packageRoot: `${g1}/gentle-pi`, currentVersion: '1.1.0', cwd: '/neutral',
|
|
83
|
+
return {mutations, out, cwds, runOptions, state, shell, selectedCli, tarball, input: {platform: windows ? 'win32' : 'linux', packageRoot: `${g1}/gentle-pi`, currentVersion: '1.1.0', cwd: '/neutral',
|
|
84
|
+
home, env: configHome ? {GENTLE_PI_CONFIG_HOME: configHome} : {},
|
|
77
85
|
runtime: runtime === 'adjacent' ? {kind: 'bundled', command: 'node', args: [selectedCli]} : {kind: runtime, command: shim ? shimFile : selectedCli, args: []},
|
|
78
86
|
out: (line: string) => out.push(line),
|
|
79
87
|
adapters: {which: async (name: string) => name === (npm ? 'npm' : 'pnpm') ? `/fake/${name}` : null, fs: {realpath, readFile}, run}}};
|
|
@@ -117,6 +125,60 @@ test('npm nests the peer under Shell and reinstalls Shell for it', async () => {
|
|
|
117
125
|
assert.deepEqual(f.out, ['npm installed gentle-pi@4.1.0; the selected Pi is now 1.2.0 (was 1.1.0).']);
|
|
118
126
|
});
|
|
119
127
|
|
|
128
|
+
// #2324: main-channel Shell versions are never published; only the tarball
|
|
129
|
+
// `gentle-shell upgrade --channel main` packed can reinstall that exact build.
|
|
130
|
+
const MAIN = '4.2.0-main.fb5e731119a7';
|
|
131
|
+
const native = (path: string, windows: boolean) => windows ? path.replaceAll('/', '\\') : path;
|
|
132
|
+
|
|
133
|
+
for (const windows of [false, true]) test(`a main-channel Shell is reinstalled from its recorded tarball, never from the registry (windows: ${windows})`, async () => {
|
|
134
|
+
const f = fixture({windows, shellVersion: MAIN});
|
|
135
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
136
|
+
assert.deepEqual(f.mutations, [['add', '-g', native(f.tarball, windows), '--allow-build=gentle-pi']]);
|
|
137
|
+
assert.deepEqual(f.out, [`pnpm installed ${native(f.tarball, windows)}; the selected Pi is now 1.2.0 (was 1.1.0).`]);
|
|
138
|
+
assert.equal(f.state.group, '1.1.0');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('npm reinstalls a main-channel Shell from its recorded tarball', async () => {
|
|
142
|
+
const f = fixture({npm: true, shellVersion: MAIN});
|
|
143
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
144
|
+
assert.deepEqual(f.mutations, [['install', '-g', f.tarball]]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('a main-channel tarball is found under GENTLE_PI_CONFIG_HOME', async () => {
|
|
148
|
+
const f = fixture({shellVersion: MAIN, configHome: '/custom/config'});
|
|
149
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
150
|
+
assert.deepEqual(f.mutations, [['add', '-g', '/custom/config/main/packages/gentle-pi-4.2.0-main.fb5e731119a7.tgz', '--allow-build=gentle-pi']]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('a missing main-channel tarball names the rebuild command and installs nothing', async () => {
|
|
154
|
+
const f = fixture({shellVersion: MAIN, artifact: false});
|
|
155
|
+
await assert.rejects(updateSelectedPi(f.input), {message: `gentle-shell update: the main-channel build ${f.tarball} is missing, and this unpublished build cannot be reinstalled from the registry; run 'gentle-shell upgrade --channel release' and then 'gentle-shell upgrade --channel main' to rebuild it. No installation changed.`});
|
|
156
|
+
assert.deepEqual(f.mutations, []);
|
|
157
|
+
assert.deepEqual(f.out, []);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// A relative config home names different folders for `upgrade` and `update`
|
|
161
|
+
// run from different directories, and npm/pnpm resolve it from the neutral cwd.
|
|
162
|
+
test('a relative GENTLE_PI_CONFIG_HOME is refused for a main-channel Shell without mutation', async () => {
|
|
163
|
+
const f = fixture({shellVersion: MAIN, configHome: 'relative/config'});
|
|
164
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership cannot locate the main-channel build without an absolute config home/);
|
|
165
|
+
assert.deepEqual(f.mutations, []);
|
|
166
|
+
assert.deepEqual(f.out, []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('a main-channel Shell without a home directory is refused without mutation', async () => {
|
|
170
|
+
const f = fixture({shellVersion: MAIN});
|
|
171
|
+
delete (f.input as {home?: string}).home;
|
|
172
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership cannot locate the main-channel build without an absolute config home/);
|
|
173
|
+
assert.deepEqual(f.mutations, []);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
for (const runtime of ['path', 'env']) test(`a main-channel Shell never redirects the update of a selected ${runtime} Pi`, async () => {
|
|
177
|
+
const f = fixture({runtime, shellVersion: MAIN, artifact: false});
|
|
178
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
179
|
+
assert.deepEqual(f.mutations, [['add', '-g', `${PI}@latest`]]);
|
|
180
|
+
});
|
|
181
|
+
|
|
120
182
|
test('an npm PATH runtime updates the global Pi without its scripts', async () => {
|
|
121
183
|
const f = fixture({npm: true, runtime: 'path'});
|
|
122
184
|
assert.equal(await updateSelectedPi(f.input), 0);
|
|
@@ -184,3 +246,36 @@ test('an unrelated selected override cannot authorize the adjacent Shell group',
|
|
|
184
246
|
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
185
247
|
assert.deepEqual(f.mutations, []);
|
|
186
248
|
});
|
|
249
|
+
|
|
250
|
+
// #2371: a failed install shows the package manager it ran and one sanitized
|
|
251
|
+
// line of its own output; pnpm 11 prints ERR_PNPM_* on stdout, npm on stderr.
|
|
252
|
+
test('a failed pnpm install names the pnpm it ran and its ERR_PNPM line from stdout', async () => {
|
|
253
|
+
const f = fixture({shellVersion: MAIN, installFailure: {stderrTail: '',
|
|
254
|
+
stdout: `Progress: resolved 1\n\u001b[31m[ERR_PNPM_INVALID_VERSION_UNION]\u001b[39m Invalid versions union. Found: "gentle-pi@file:/home/u/x.tgz". Use exact versions only.\nDone\n`}});
|
|
255
|
+
await assert.rejects(updateSelectedPi(f.input), {message: `gentle-shell update: pnpm (/fake/pnpm) could not install ${f.tarball}\n[ERR_PNPM_INVALID_VERSION_UNION] Invalid versions union. Found: "gentle-pi@file:~/x.tgz". Use exact versions only.`});
|
|
256
|
+
assert.deepEqual(f.runOptions, [{deadlineMs: 20 * 60_000, cwd: '/neutral', stderrTail: 4096}]);
|
|
257
|
+
assert.deepEqual(f.out, []);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('a failed npm install shows its stderr error line before stdout', async () => {
|
|
261
|
+
const f = fixture({npm: true, installFailure: {stdout: 'unrelated', stderrTail: 'npm error code EACCES\nnpm error Error: EACCES: permission denied\nnpm error A complete log is in /home/u/.npm/x.log\n'}});
|
|
262
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: npm (/fake/npm) could not install gentle-pi@4.1.0\nnpm error Error: EACCES: permission denied'});
|
|
263
|
+
assert.deepEqual(f.out, []);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('a failed install without output keeps the single line, naming the command it ran', async () => {
|
|
267
|
+
const f = fixture({installFailure: {}});
|
|
268
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: pnpm (/fake/pnpm) could not install gentle-pi@4.1.0'});
|
|
269
|
+
assert.deepEqual(f.mutations, [['add', '-g', 'gentle-pi@4.1.0', '--allow-build=gentle-pi']]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('a stderr warning never hides the ERR_PNPM line pnpm printed on stdout', async () => {
|
|
273
|
+
const f = fixture({installFailure: {stderrTail: '(node:1) [DEP0040] DeprecationWarning: The `punycode` module is deprecated.\n',
|
|
274
|
+
stdout: '[ERR_PNPM_INVALID_VERSION_UNION] Invalid versions union.\n'}});
|
|
275
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: pnpm (/fake/pnpm) could not install gentle-pi@4.1.0\n[ERR_PNPM_INVALID_VERSION_UNION] Invalid versions union.'});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('without any error line the last stderr line is shown', async () => {
|
|
279
|
+
const f = fixture({installFailure: {stderrTail: 'killed\n', stdout: 'Progress: resolved 1\n'}});
|
|
280
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: pnpm (/fake/pnpm) could not install gentle-pi@4.1.0\nkilled'});
|
|
281
|
+
});
|
|
@@ -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
|
+
});
|