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
|
@@ -10,6 +10,12 @@ export const AUTO_MODE_NOTICE = "Auto uses additional model calls with separate
|
|
|
10
10
|
|
|
11
11
|
export const YOLO_STATUS_KEY = "gentle:yolo";
|
|
12
12
|
export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
|
|
13
|
+
export const SUPER_YOLO_STATUS_TEXT = "💥 SUPER YOLO ON — no permission prompts; private paths stay blocked";
|
|
14
|
+
export const SUPER_YOLO_DIRECTIVE = `<gentle-super-yolo-session>
|
|
15
|
+
SUPER YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace. No permission questions are asked in this mode: proceed through ordinary and destructive shell work (including rm, resets, force operations and configured-guard confirmations) without confirmation dialogs, and make implementation and delivery choices directly.
|
|
16
|
+
The ONLY hard line is private material: never read, print, copy, transmit or commit secrets or private credentials — .env files, .ssh, keychains, *.pem/*.key/*.p12/*.pfx, secrets/ directories, cloud credential files. If a task seems to need them, stop and ask the human.
|
|
17
|
+
This does NOT override explicit human restrictions in the live conversation, and never auto-answers ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Children remain bounded by their delegated task; they receive no independent delivery grant or Super Yolo inheritance.
|
|
18
|
+
</gentle-super-yolo-session>`;
|
|
13
19
|
export const YOLO_DIRECTIVE = `<gentle-yolo-session>
|
|
14
20
|
YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace (directly or through a trusted global default).
|
|
15
21
|
Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
|
|
@@ -40,12 +46,14 @@ export class YoloSessionPolicy {
|
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
/** Remove our exact
|
|
44
|
-
export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean): void {
|
|
49
|
+
/** Remove our exact blocks as well as adding them: bridge options can be reused. */
|
|
50
|
+
export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean, superActive = false): void {
|
|
45
51
|
if (!options) return;
|
|
46
52
|
options.appendSystemPrompt = (options.appendSystemPrompt ?? "")
|
|
47
|
-
.replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "")
|
|
48
|
-
|
|
53
|
+
.replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "")
|
|
54
|
+
.replace(`\n\n${SUPER_YOLO_DIRECTIVE}`, "").replace(SUPER_YOLO_DIRECTIVE, "");
|
|
55
|
+
if (superActive) appendSystemPromptOnce(options, SUPER_YOLO_DIRECTIVE);
|
|
56
|
+
else if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
|
|
49
57
|
}
|
|
50
58
|
const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
|
|
51
59
|
type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
|
|
@@ -86,7 +94,8 @@ export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContex
|
|
|
86
94
|
}
|
|
87
95
|
export interface YoloSessionController {
|
|
88
96
|
active(context: ExtensionContext): Promise<boolean>;
|
|
89
|
-
|
|
97
|
+
superActive(context: ExtensionContext): Promise<boolean>;
|
|
98
|
+
mode(context: ExtensionContext, admission?: { toolCallId: string }): Promise<PermissionMode>;
|
|
90
99
|
setMode(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
|
|
91
100
|
defaultMode(): PermissionMode;
|
|
92
101
|
saveDefault(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
|
|
@@ -101,7 +110,7 @@ interface ModeIdentity {
|
|
|
101
110
|
}
|
|
102
111
|
/** UI/slash authoring only. Defaults are read from the dedicated trusted user store. */
|
|
103
112
|
export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv,
|
|
104
|
-
options: { onChange?: () => void } = {}): YoloSessionController {
|
|
113
|
+
options: { onChange?: () => void; resolveIdentity?: typeof resolveCanonicalGitRepositoryIdentity } = {}): YoloSessionController {
|
|
105
114
|
const store = { gentlePiConfigHome: gentlePiConfigHome(env) };
|
|
106
115
|
let generation = 0;
|
|
107
116
|
let ownerGeneration = 0;
|
|
@@ -112,6 +121,15 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
112
121
|
const seen = new WeakMap<object, Set<string>>();
|
|
113
122
|
const observers = new Set<() => void>();
|
|
114
123
|
const pendingCaptures = new Set<{ manager: ModeIdentity["manager"]; id: string }>();
|
|
124
|
+
// Identity is re-captured for every tool admission: freshness is a security
|
|
125
|
+
// invariant, so cross-admission reuse is never allowed (a mid-lookup mode
|
|
126
|
+
// reselection must invalidate). Within one admission the redundant mode()
|
|
127
|
+
// lookups of the same tool call share at most ONE capture, keyed by that
|
|
128
|
+
// admission's tool call id; a different or absent id re-captures, and a
|
|
129
|
+
// generation bump or cwd change still invalidates a shared slot.
|
|
130
|
+
let capturedMode: (ModeIdentity & { cwd: string; generation: number; admission: string }) | undefined;
|
|
131
|
+
// Rendered strings last published for each context; identical repaints are skipped.
|
|
132
|
+
const published = new WeakMap<ExtensionContext, { yolo: string | undefined; modeText: string }>();
|
|
115
133
|
const primary = () => env.GENTLE_PI_AGENTS_CHILD !== "1";
|
|
116
134
|
const interactive = (ctx: ExtensionContext): boolean => {
|
|
117
135
|
if (!primary() || !ctx.hasUI) return false;
|
|
@@ -129,10 +147,15 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
129
147
|
const ids = seen.get(scope.manager) ?? new Set<string>(); ids.add(scope.id); seen.set(scope.manager, ids);
|
|
130
148
|
};
|
|
131
149
|
const publish = (ctx: ExtensionContext): void => {
|
|
132
|
-
const yolo = liveMode === "yolo";
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
const yolo = liveMode === "yolo" ? YOLO_STATUS_TEXT : liveMode === "super-yolo" ? SUPER_YOLO_STATUS_TEXT : undefined;
|
|
151
|
+
const modeText = `Permissions: ${liveMode.toUpperCase()}`;
|
|
152
|
+
const previous = published.get(ctx);
|
|
153
|
+
if (previous && previous.yolo === yolo && previous.modeText === modeText) return;
|
|
154
|
+
let painted = true;
|
|
155
|
+
try { ctx.ui.setStatus(YOLO_STATUS_KEY, yolo); } catch { painted = false; /* display only */ }
|
|
156
|
+
try { ctx.ui.setWidget(YOLO_STATUS_KEY, yolo ? [yolo] : undefined); } catch { painted = false; /* display only */ }
|
|
157
|
+
try { ctx.ui.setStatus(MODE_STATUS_KEY, modeText); } catch { painted = false; /* display only */ }
|
|
158
|
+
if (painted) published.set(ctx, { yolo, modeText });
|
|
136
159
|
};
|
|
137
160
|
const changed = (ctx: ExtensionContext): void => {
|
|
138
161
|
generation += 1; publish(ctx);
|
|
@@ -162,17 +185,25 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
162
185
|
try {
|
|
163
186
|
const canonical = await realpath(cwd);
|
|
164
187
|
if (!current()) return undefined;
|
|
165
|
-
const clone = await resolveCanonicalGitRepositoryIdentity(canonical);
|
|
188
|
+
const clone = await (options.resolveIdentity ?? resolveCanonicalGitRepositoryIdentity)(canonical);
|
|
166
189
|
if (!current()) return undefined;
|
|
167
190
|
return { ...scope, workspace: clone ?? `cwd:${canonical}` };
|
|
168
191
|
} catch { return undefined; }
|
|
169
192
|
finally { pendingCaptures.delete(scope); }
|
|
170
193
|
};
|
|
171
194
|
const defaultMode = (): PermissionMode => resolvePermissionModePolicy(store).mode;
|
|
172
|
-
const mode = async (ctx: ExtensionContext): Promise<PermissionMode> => {
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
195
|
+
const mode = async (ctx: ExtensionContext, admission?: { toolCallId: string }): Promise<PermissionMode> => {
|
|
196
|
+
const scope = session(ctx);
|
|
197
|
+
const admissionId = admission?.toolCallId;
|
|
198
|
+
let identity: ModeIdentity | undefined;
|
|
199
|
+
if (admissionId !== undefined && scope && capturedMode &&
|
|
200
|
+
capturedMode.admission === admissionId && capturedMode.generation === generation &&
|
|
201
|
+
capturedMode.manager === scope.manager && capturedMode.id === scope.id && capturedMode.cwd === ctx.cwd) identity = capturedMode;
|
|
202
|
+
if (!identity) {
|
|
203
|
+
const epoch = generation;
|
|
204
|
+
identity = await capture(ctx);
|
|
205
|
+
if (epoch !== generation) return "normal";
|
|
206
|
+
}
|
|
176
207
|
if (binding && (!identity || binding.manager !== identity.manager || binding.id !== identity.id ||
|
|
177
208
|
binding.workspace !== identity.workspace || (interactiveGrant && !interactive(ctx)))) {
|
|
178
209
|
binding = undefined; liveMode = "normal"; interactiveGrant = false; changed(ctx);
|
|
@@ -182,12 +213,17 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
182
213
|
remember(identity); binding = identity;
|
|
183
214
|
if (fresh) { liveMode = defaultMode(); interactiveGrant = false; changed(ctx); }
|
|
184
215
|
}
|
|
216
|
+
// Record after the binding pass: internal changed() bumps above must not strand
|
|
217
|
+
// a cache entry stamped with a superseded generation. Only admission-keyed
|
|
218
|
+
// lookups record; the slot is overwritten when a different admission id arrives.
|
|
219
|
+
if (identity && scope && admissionId !== undefined) capturedMode = { ...identity, cwd: ctx.cwd, generation, admission: admissionId };
|
|
185
220
|
publish(ctx);
|
|
186
221
|
return identity && binding ? liveMode : "normal";
|
|
187
222
|
};
|
|
188
223
|
const active = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "yolo";
|
|
224
|
+
const superActive = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "super-yolo";
|
|
189
225
|
const reset = (ctx: ExtensionContext): void => {
|
|
190
|
-
ownerGeneration += 1;
|
|
226
|
+
ownerGeneration += 1; capturedMode = undefined;
|
|
191
227
|
// Initial session_start remains fresh; shutdown/reload of an observed session revokes.
|
|
192
228
|
if (binding) remember(binding);
|
|
193
229
|
const scope = session(ctx);
|
|
@@ -228,7 +264,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
228
264
|
};
|
|
229
265
|
const humanAction = async (action: HumanAction, ctx: ExtensionContext, current = () => true): Promise<boolean> => {
|
|
230
266
|
if (!current()) return false;
|
|
231
|
-
const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo");
|
|
267
|
+
const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo" && liveMode !== "super-yolo");
|
|
232
268
|
const pending = setMode(enable ? "yolo" : "normal", ctx, current);
|
|
233
269
|
const epoch = generation; // setMode revokes synchronously, then publishes once on success.
|
|
234
270
|
const result = await pending;
|
|
@@ -253,7 +289,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
253
289
|
const epoch = generation, value = await mode(ctx);
|
|
254
290
|
if (!current()) return YOLO_DISPLAY.unavailable;
|
|
255
291
|
if (epoch !== generation) return read();
|
|
256
|
-
return binding ? value === "yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
|
|
292
|
+
return binding ? value === "yolo" || value === "super-yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
|
|
257
293
|
};
|
|
258
294
|
request.respond({
|
|
259
295
|
read,
|
|
@@ -276,7 +312,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
276
312
|
} catch { /* obsolete trusted context: discovery expires closed */ }
|
|
277
313
|
});
|
|
278
314
|
pi.registerCommand("gentle:mode", {
|
|
279
|
-
description: "Permissions: normal|auto|yolo|status; default normal|auto|yolo saves for new primary sessions.",
|
|
315
|
+
description: "Permissions: normal|auto|yolo|super-yolo|status; default normal|auto|yolo|super-yolo saves for new primary sessions.",
|
|
280
316
|
handler: async (args, ctx) => {
|
|
281
317
|
const words = args.trim().split(/\s+/);
|
|
282
318
|
if (!args.trim() || args.trim() === "status") {
|
|
@@ -290,7 +326,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
290
326
|
else notify(ctx, "Mode unchanged — interactive primary session required or the session changed.", true);
|
|
291
327
|
return;
|
|
292
328
|
}
|
|
293
|
-
|
|
329
|
+
notify(ctx, "Use /gentle:mode normal|auto|yolo|super-yolo|status or /gentle:mode default normal|auto|yolo|super-yolo. State unchanged.", true);
|
|
294
330
|
},
|
|
295
331
|
});
|
|
296
332
|
pi.registerCommand("gentle:yolo", {
|
|
@@ -301,7 +337,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
301
337
|
if (!args.trim() && ctx.hasUI && typeof ctx.ui.select === "function") {
|
|
302
338
|
current = bindCurrent(ctx);
|
|
303
339
|
const state = await active(ctx) ? YOLO_DISPLAY.on : YOLO_DISPLAY.off;
|
|
304
|
-
const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 —
|
|
340
|
+
const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 — standing permission (current: ${state})`, ["enable", "disable", "status"]);
|
|
305
341
|
if (selected === undefined) return;
|
|
306
342
|
action = selected;
|
|
307
343
|
}
|
|
@@ -313,5 +349,5 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
313
349
|
notify(ctx, "YOLO unchanged — the session changed while the menu was open.", true);
|
|
314
350
|
},
|
|
315
351
|
});
|
|
316
|
-
return { active, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
|
|
352
|
+
return { active, superActive, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
|
|
317
353
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// YOLO temp-scratch waiver: a recursive `rm` whose every literal target is the
|
|
2
|
+
// session's own scratch under a temp root needs no data-loss confirmation.
|
|
3
|
+
// Pure over an injectable filesystem view so tests never touch real paths.
|
|
4
|
+
// Callers own the policy gate: primary session with active YOLO only.
|
|
5
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { posix } from "node:path";
|
|
8
|
+
import type { DestructiveCommandMatch } from "./destructive-command-guard.ts";
|
|
9
|
+
|
|
10
|
+
export interface TempScratchStat {
|
|
11
|
+
uid: number;
|
|
12
|
+
isDirectory(): boolean;
|
|
13
|
+
isFile(): boolean;
|
|
14
|
+
isSymbolicLink(): boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TempScratchFs {
|
|
18
|
+
/** Throws an error with code `ENOENT` when the path is missing. */
|
|
19
|
+
lstat(path: string): TempScratchStat;
|
|
20
|
+
/** Current process uid; undefined where POSIX ownership is unavailable. */
|
|
21
|
+
uid: number | undefined;
|
|
22
|
+
/** Resolved `os.tmpdir()`, when resolvable. */
|
|
23
|
+
tmpdir: string | undefined;
|
|
24
|
+
home: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FIXED_TEMP_ROOTS = ["/tmp", "/var/tmp"];
|
|
28
|
+
const MKTEMP_NAME_RE = /^tmp\.[A-Za-z0-9]{6,}$/;
|
|
29
|
+
// A closed character set: expansion, quoting, glob, brace and redirection
|
|
30
|
+
// characters (`$`, backtick, `~`, `*?[{`, quotes, `<>`) make the shell
|
|
31
|
+
// target differ from the text or add a write elsewhere.
|
|
32
|
+
const LITERAL_PATH_RE = /^\/[A-Za-z0-9._@%+=:,/-]*$/;
|
|
33
|
+
|
|
34
|
+
export function defaultTempScratchFs(): TempScratchFs {
|
|
35
|
+
let resolvedTmpdir: string | undefined;
|
|
36
|
+
try { resolvedTmpdir = realpathSync(tmpdir()); } catch { resolvedTmpdir = undefined; }
|
|
37
|
+
return { lstat: (path) => lstatSync(path), uid: process.getuid?.(), tmpdir: resolvedTmpdir, home: homedir() };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A temp root equal to, above or below home would expose home entries as scratch. */
|
|
41
|
+
function tempRoots(fs: TempScratchFs): string[] {
|
|
42
|
+
const home = posix.normalize(fs.home);
|
|
43
|
+
return [...FIXED_TEMP_ROOTS, fs.tmpdir].filter((root): root is string =>
|
|
44
|
+
root !== undefined && posix.isAbsolute(root) && posix.normalize(root) === root && root !== "/" &&
|
|
45
|
+
root !== home && !home.startsWith(`${root}/`) && !root.startsWith(`${home}/`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One literal path component directly under a temp root, proven own scratch by an admission-time lstat. */
|
|
49
|
+
export function isTempScratchTarget(target: string, fs: TempScratchFs): boolean {
|
|
50
|
+
if (fs.uid === undefined || !LITERAL_PATH_RE.test(target)) return false;
|
|
51
|
+
if (target.endsWith("/") || posix.normalize(target) !== target) return false;
|
|
52
|
+
const name = posix.basename(target);
|
|
53
|
+
if (name === "" || name === "." || name === ".." || !tempRoots(fs).includes(posix.dirname(target))) return false;
|
|
54
|
+
let stat: TempScratchStat;
|
|
55
|
+
try {
|
|
56
|
+
stat = fs.lstat(target);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return (error as { code?: unknown }).code === "ENOENT";
|
|
59
|
+
}
|
|
60
|
+
if (stat.isSymbolicLink()) return true;
|
|
61
|
+
if (stat.uid !== fs.uid) return false;
|
|
62
|
+
return stat.isFile() || (stat.isDirectory() && MKTEMP_NAME_RE.test(name));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A recognized effect is waivable temp scratch only when it is a direct recursive `rm` of temp scratch alone. */
|
|
66
|
+
export function isTempScratchRemoval(match: DestructiveCommandMatch, fs: TempScratchFs): boolean {
|
|
67
|
+
if (match.hardDeny || match.kind !== "filesystem" || match.removalTargets === undefined) return false;
|
|
68
|
+
return match.removalTargets.every((target) => isTempScratchTarget(target, fs));
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: an ODD development harness with focused subagents, configured TDD evidence, native review, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
44
|
"check:provider-contract": "node scripts/check-provider-contract.mjs",
|
|
45
|
-
"mirror:odd-routing": "node scripts/mirror-odd-routing.mjs",
|
|
46
45
|
"postinstall": "node scripts/install-gentle-ai.mjs",
|
|
47
46
|
"test": "node scripts/run-test-suite.mjs",
|
|
48
47
|
"test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
resolveGentleAiReleaseAsset,
|
|
16
16
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
17
17
|
import { engramPinVersion } from "../scripts/installer-downloads.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { bundledPiConfigDir, bundledPiConfigParent, GENTLE_SHELL_CONFIG_HOME_ENV, resolveGentlePiConfigHome, runningBundledPrefix } from "./gentle-shell-bundled.mjs";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
21
|
export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
|
|
@@ -87,9 +87,11 @@ export function bundledGentleAiPaths(prefix , platform = process.
|
|
|
87
87
|
* (only when the pinned Engram is published), GENTLE_AI_NO_SELF_UPDATE=1 and
|
|
88
88
|
* GENTLE_PI_CONFIG_HOME (the parent of GENTLE_AI_PI_CONFIG_DIR)
|
|
89
89
|
* when this gentle-pi runs from the valid bundled install GENTLE_SHELL_PREFIX
|
|
90
|
-
* names (runningBundledPrefix).
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* names (runningBundledPrefix). In the standalone launcher's isolated home
|
|
91
|
+
* (#2201) only the config pair, for the folder its marker names. Otherwise
|
|
92
|
+
* `env` itself, unchanged: a gentle-pi installed elsewhere (the user's own,
|
|
93
|
+
* started from the agent's shell, which inherits the variable) keeps its own
|
|
94
|
+
* Gentle AI.
|
|
93
95
|
*/
|
|
94
96
|
export function gentleAiIsolationEnv(
|
|
95
97
|
env ,
|
|
@@ -97,7 +99,7 @@ export function gentleAiIsolationEnv(
|
|
|
97
99
|
) {
|
|
98
100
|
const platform = options.platform ?? process.platform;
|
|
99
101
|
const prefix = runningBundledPrefix(env, { packageRoot: options.packageRoot, platform });
|
|
100
|
-
if (prefix === undefined) return env;
|
|
102
|
+
if (prefix === undefined) return standaloneGentleAiEnv(env, platform);
|
|
101
103
|
const paths = bundledGentleAiPaths(prefix, platform);
|
|
102
104
|
let engram = false;
|
|
103
105
|
try {
|
|
@@ -126,6 +128,23 @@ export function gentleAiIsolationEnv(
|
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
// #2201: the same config pair as a bundled install, for the folder the
|
|
132
|
+
// standalone launcher's marker names, when it decides the config home
|
|
133
|
+
// (resolveGentlePiConfigHome; an explicit GENTLE_PI_CONFIG_HOME keeps its
|
|
134
|
+
// existing meaning for Gentle AI). Gentle AI's own state stays where it was.
|
|
135
|
+
// The parent is given only for a folder named gentle-ai, the one Gentle AI
|
|
136
|
+
// 4.0.0 appends to it.
|
|
137
|
+
function standaloneGentleAiEnv(env , platform ) {
|
|
138
|
+
const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
|
|
139
|
+
if (marker === undefined || resolveGentlePiConfigHome(env, homedir(), { platform }) !== marker) return env;
|
|
140
|
+
const path = platform === "win32" ? win32 : posix;
|
|
141
|
+
return {
|
|
142
|
+
...env,
|
|
143
|
+
GENTLE_AI_PI_CONFIG_DIR: marker,
|
|
144
|
+
...(path.basename(marker) === "gentle-ai" ? { GENTLE_PI_CONFIG_HOME: path.dirname(marker) } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
129
148
|
function sha256(value ) {
|
|
130
149
|
return createHash("sha256").update(value).digest("hex");
|
|
131
150
|
}
|
|
@@ -137,7 +156,9 @@ function sha256(value ) {
|
|
|
137
156
|
// 1. GENTLE_PI_GENTLE_AI_DEV_BINARY (session override, absolute path), then
|
|
138
157
|
// 2. the persistent registration file at
|
|
139
158
|
// <config home>/dev-binary.json (a bundled prefix's gentle-pi, else
|
|
140
|
-
// GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai
|
|
159
|
+
// GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai, never the standalone
|
|
160
|
+
// launcher's marker: the main channel writes it outside any session,
|
|
161
|
+
// #2201) with the strict
|
|
141
162
|
// shape {"schema":"gentle-pi.dev-binary/v1","path":"<absolute path>"}.
|
|
142
163
|
//
|
|
143
164
|
// The registration deliberately pins no digest: it is the unpinned field-test
|
|
@@ -195,7 +216,7 @@ function ambientDevBinaryEnvironment() {
|
|
|
195
216
|
}
|
|
196
217
|
|
|
197
218
|
export function gentleAiDevBinaryRegistrationPath(environment = ambientDevBinaryEnvironment()) {
|
|
198
|
-
const configHome =
|
|
219
|
+
const configHome = resolveGentlePiConfigHome(environment.env, environment.home, { launcherMarker: false });
|
|
199
220
|
return join(configHome, "dev-binary.json");
|
|
200
221
|
}
|
|
201
222
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
3
|
import { request as httpRequest } from "node:http";
|
|
4
4
|
import { createConnection } from "node:net";
|
|
5
|
-
import { dirname, posix, win32 } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, posix, win32 } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
// The launcher in a bundled install (S5, S8): pure decisions over an injected
|
|
@@ -28,6 +28,46 @@ import { fileURLToPath } from "node:url";
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Builds the detached shared server's environment without retaining session scratch.
|
|
33
|
+
* Windows Node can reintroduce an omitted TEMP from its parent, so use the
|
|
34
|
+
* original stable ancestor there; POSIX omits scratch temp settings instead.
|
|
35
|
+
*/
|
|
36
|
+
export function sharedEngramEnv(base , platform = process.platform) {
|
|
37
|
+
const path = pathFor(platform);
|
|
38
|
+
const env = {};
|
|
39
|
+
const scratch = (value ) => /^(gp|ga)[A-Za-z0-9]{6}$/.test(path.basename(path.normalize(value)));
|
|
40
|
+
let stableTemp ;
|
|
41
|
+
let tempKey ;
|
|
42
|
+
for (const [key, value] of Object.entries(base)) {
|
|
43
|
+
const name = platform === "win32" ? key.toUpperCase() : key;
|
|
44
|
+
if (name.startsWith("ENGRAM_") || name === "GH_TOKEN" || name === "GITHUB_TOKEN") continue;
|
|
45
|
+
if (["TMPDIR", "TMP", "TEMP"].includes(name) && value !== undefined && scratch(value)) {
|
|
46
|
+
if (platform !== "win32") continue;
|
|
47
|
+
// A drive-relative or root-relative path depends on ambient state.
|
|
48
|
+
const root = win32.parse(value).root;
|
|
49
|
+
if (!/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+[\\/]?)$/.test(root) || value.split(/[\\/]/).includes("..")) {
|
|
50
|
+
throw new Error("An absolute Windows scratch temp path is required for shared Engram.");
|
|
51
|
+
}
|
|
52
|
+
let stable = win32.normalize(value);
|
|
53
|
+
while (scratch(stable)) {
|
|
54
|
+
const parent = win32.dirname(stable);
|
|
55
|
+
if (parent === stable) throw new Error("An absolute Windows scratch temp path is required for shared Engram.");
|
|
56
|
+
stable = parent;
|
|
57
|
+
}
|
|
58
|
+
env[key] = stable;
|
|
59
|
+
stableTemp ??= stable;
|
|
60
|
+
} else {
|
|
61
|
+
env[key] = value;
|
|
62
|
+
}
|
|
63
|
+
if (name === "TEMP") tempKey = key;
|
|
64
|
+
}
|
|
65
|
+
// Keep an existing custom TEMP, even if TMP/TMPDIR chose another root.
|
|
66
|
+
if (stableTemp !== undefined && (tempKey === undefined || env[tempKey] === undefined)) env[tempKey ?? "TEMP"] = stableTemp;
|
|
67
|
+
env.ENGRAM_NO_UPDATE_CHECK = "1";
|
|
68
|
+
return env;
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
// The same rules scripts/bundled-install.mjs writes versions and markers with.
|
|
32
72
|
const VERSION_ID = /^[0-9A-Za-z][0-9A-Za-z.-]*$/;
|
|
33
73
|
const VERSION_MARKER = ".gentle-shell-version";
|
|
@@ -176,6 +216,29 @@ export function bundledGentlePiConfigHome(env ) {
|
|
|
176
216
|
return prefix === undefined ? undefined : bundledPiConfigDir(prefix);
|
|
177
217
|
}
|
|
178
218
|
|
|
219
|
+
// #2201: the standalone launcher gives Pi this variable in its isolated home
|
|
220
|
+
// only, naming the config folder next to the agent dir (standaloneConfigHome:
|
|
221
|
+
// ~/.gentle-shell/gentle-ai by default). It is launcher-owned, never
|
|
222
|
+
// GENTLE_PI_CONFIG_HOME: Gentle AI 4.0.0 reads that one as the parent of its
|
|
223
|
+
// own gentle-ai folder, and the agent's shell would pass it to any gentle-ai.
|
|
224
|
+
export const GENTLE_SHELL_CONFIG_HOME_ENV = "GENTLE_SHELL_CONFIG_HOME";
|
|
225
|
+
|
|
226
|
+
// gentle-pi's config home, the one rule every reader shares: a bundled
|
|
227
|
+
// prefix's folder, then an explicit GENTLE_PI_CONFIG_HOME, then the launcher's
|
|
228
|
+
// marker (absolute only), then ~/.pi/gentle-ai. Empty values count as unset.
|
|
229
|
+
// `launcherMarker: false` is for install-level state written outside a session
|
|
230
|
+
// (the main channel's channel.json, builds and dev-binary.json): the installer
|
|
231
|
+
// and `gentle-shell upgrade` never see the marker, so their readers ignore it.
|
|
232
|
+
export function resolveGentlePiConfigHome(env , home , options = {}) {
|
|
233
|
+
const path = pathFor(options.platform ?? process.platform);
|
|
234
|
+
const bundled = bundledGentlePiConfigHome(env);
|
|
235
|
+
if (bundled !== undefined) return bundled;
|
|
236
|
+
if (env.GENTLE_PI_CONFIG_HOME) return env.GENTLE_PI_CONFIG_HOME;
|
|
237
|
+
const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
|
|
238
|
+
if (options.launcherMarker !== false && marker && path.isAbsolute(marker)) return marker;
|
|
239
|
+
return path.join(home, ".pi", "gentle-ai");
|
|
240
|
+
}
|
|
241
|
+
|
|
179
242
|
// The exact Pi of the version (S8): the Pi the version's package.json pins,
|
|
180
243
|
// installed inside the version folder with Pi's own bin entry. Never another Pi.
|
|
181
244
|
export function bundledPi(install , input ) {
|
|
@@ -210,13 +273,14 @@ export function bundledPi(install , input
|
|
|
210
273
|
// Gentle AI's variables, never set here because the agent's shell would pass
|
|
211
274
|
// them to a gentle-ai the user runs), and the published Engram as
|
|
212
275
|
// gentle-engram's ENGRAM_BIN unless the user chose one. With a memory decision
|
|
213
|
-
// (S2, S6): the startup policy, and ENGRAM_BIN is the
|
|
214
|
-
//
|
|
215
|
-
//
|
|
276
|
+
// (S2, S6): the startup policy, and ENGRAM_BIN is the Shell-owned guard, so no
|
|
277
|
+
// provider, whatever loaded it, runs an Engram binary against a store; allowed
|
|
278
|
+
// memory reaches the shared server through ENGRAM_URL only.
|
|
216
279
|
export const ENGRAM_COMPANION_SOURCE = "npm:gentle-engram@0.4.2";
|
|
217
|
-
export const ENGRAM_RECOVERY_NOTICE = "Engram memory disabled: Shell could not verify safe
|
|
280
|
+
export const ENGRAM_RECOVERY_NOTICE = "Engram memory disabled: Shell could not verify safe server reuse. Matching versions or updating alone do not prove compatibility. Reuse requires a server on 127.0.0.1:7437 with verified /compatibility evidence; Shell never stops or replaces a server already listening there. Back up existing Engram data before manual changes; do not delete it or bypass the safety check. Shell rechecks on every launch and remains usable without memory.";
|
|
281
|
+
export const ENGRAM_START_FAILED_NOTICE = "Engram memory disabled for this launch: Shell could not start the shared Engram server, or it did not report ready in time. Restart Shell to retry; if another program holds 127.0.0.1:7437, stop it first. Shell remains usable without memory.";
|
|
282
|
+
export const ENGRAM_NEWER_SCHEMA_NOTICE = "Engram memory disabled: ~/.engram was migrated by a newer Engram than Shell's bundled core, so Shell will not start a server on it. Start that newer Engram's server yourself (engram serve); Shell reuses a running server with verified /compatibility evidence. Back up the store; do not delete or downgrade it. Shell remains usable without memory.";
|
|
218
283
|
// `gentle-shell setup` publishes the verified core (ensureEngram) on every run.
|
|
219
|
-
export const ENGRAM_PROVISION_FAILED_NOTICE = "Engram memory disabled for this launch: Shell could not provision a new Engram store identity, so it cannot vouch for that store. Restart Shell to retry. If Shell keeps reporting unknown compatibility and ~/.engram holds only .instance-id files, back it up and remove it, then restart Shell. Shell remains usable without memory.";
|
|
220
284
|
export const ENGRAM_CORE_MISSING_NOTICE = "Engram memory disabled: the bundled Engram core is not installed yet. Run 'gentle-shell setup' to install the verified core, then restart Shell. Shell remains usable without memory.";
|
|
221
285
|
// The first Engram release whose server answers GET /compatibility (PR #1792).
|
|
222
286
|
export const ENGRAM_COMPATIBILITY_MIN_VERSION = "3.4.0";
|
|
@@ -237,12 +301,13 @@ function outdatedServerVersion(version )
|
|
|
237
301
|
// The default endpoint gentle-engram uses without ENGRAM_URL or ENGRAM_PORT.
|
|
238
302
|
export const ENGRAM_DEFAULT_PORT = 7437;
|
|
239
303
|
export const ENGRAM_DEFAULT_URL = `http://127.0.0.1:${ENGRAM_DEFAULT_PORT}`;
|
|
240
|
-
const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_DATABASE_URL"];
|
|
304
|
+
const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_SOCKET", "ENGRAM_DATABASE_URL", "ENGRAM_SERVER_MODE"];
|
|
241
305
|
const HEX32 = /^[a-f0-9]{32}$/;
|
|
242
306
|
|
|
243
307
|
|
|
244
|
-
// `
|
|
245
|
-
// `
|
|
308
|
+
// `url`: the shared server, reached explicitly once /compatibility verified it.
|
|
309
|
+
// `spawn`: nothing listens yet; Shell starts the bundled core's server and must
|
|
310
|
+
// see it verified before Pi starts, or memory is disabled for that launch.
|
|
246
311
|
|
|
247
312
|
|
|
248
313
|
// Without an invalid policy or an explicit Engram selection, the default
|
|
@@ -253,26 +318,24 @@ export function engramProbeAllowed(env ) {
|
|
|
253
318
|
}
|
|
254
319
|
|
|
255
320
|
// Evidence only: never run an executable, open SQLite or infer compatibility
|
|
256
|
-
// from versions.
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
321
|
+
// from versions. Like other agents' hooks, Shell shares the global server: a
|
|
322
|
+
// verified listener is reused, and with nothing listening the verified bundled
|
|
323
|
+
// core starts one on Engram's defaults, unless the default store's header
|
|
324
|
+
// (`userVersion`) shows a newer schema than that core's. A listener that does
|
|
325
|
+
// not verify is never replaced. A server version only selects the wording.
|
|
326
|
+
export function classifyEngramStartup(input ) {
|
|
261
327
|
if (!engramProbeAllowed(input.env)) return { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE };
|
|
262
328
|
const endpoint = input.endpoint;
|
|
263
|
-
const outdated = endpoint.kind === "listening" && !endpoint.compatible ? outdatedServerVersion(endpoint.healthVersion) : undefined;
|
|
264
|
-
const unknown = { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
|
|
265
329
|
if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
|
|
266
|
-
if (endpoint.kind === "refused"
|
|
267
|
-
|
|
330
|
+
if (endpoint.kind === "refused") {
|
|
331
|
+
if (!input.core) return { policy: "disabled", notice: ENGRAM_CORE_MISSING_NOTICE };
|
|
332
|
+
if (input.userVersion !== undefined && !engramHeaderAllows(input.userVersion, bundledEngramSchema(input.coreVersion))) {
|
|
333
|
+
return { policy: "disabled", notice: ENGRAM_NEWER_SCHEMA_NOTICE };
|
|
334
|
+
}
|
|
335
|
+
return { policy: "allow", url: ENGRAM_DEFAULT_URL, spawn: true };
|
|
268
336
|
}
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
// Shell's own store, created by this exact core: offline, the provider starts
|
|
272
|
-
// that core; a running server must be that store and that exact core.
|
|
273
|
-
if (endpoint.kind === "refused") return { policy: "allow" };
|
|
274
|
-
if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow" };
|
|
275
|
-
return unknown;
|
|
337
|
+
const outdated = endpoint.kind === "listening" ? outdatedServerVersion(endpoint.healthVersion) : undefined;
|
|
338
|
+
return { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
|
|
276
339
|
}
|
|
277
340
|
|
|
278
341
|
// The exact contract a startup-verified store reports (Engram GET /compatibility).
|
|
@@ -332,8 +395,8 @@ export async function probeEngramEndpoint(input
|
|
|
332
395
|
return { kind: "listening", compatible: false, healthInstanceId: identified ? body.instance_id : undefined, healthVersion: identified ? body.version : undefined };
|
|
333
396
|
}
|
|
334
397
|
|
|
335
|
-
// Shell's
|
|
336
|
-
//
|
|
398
|
+
// Shell's record of a store an earlier Shell provisioned with its bundled core,
|
|
399
|
+
// read only by the explicit `gentle-shell memory migrate` recovery.
|
|
337
400
|
|
|
338
401
|
|
|
339
402
|
export function engramProvenancePath(prefix , platform = process.platform) {
|
|
@@ -347,14 +410,10 @@ export function parseEngramProvenance(text )
|
|
|
347
410
|
return { schema: 1, dataDir: record.dataDir, instanceId: record.instanceId, coreSha256: record.coreSha256 };
|
|
348
411
|
}
|
|
349
412
|
|
|
350
|
-
export function engramProvenanceMatches(record , observed ) {
|
|
351
|
-
return record !== undefined && record.dataDir === observed.dataDir && record.instanceId === observed.instanceId && record.coreSha256 === observed.coreSha256;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
413
|
// The bundled core's own schema (SQLite user_version), verified from the
|
|
355
|
-
// tagged sources v3.3.2 and v3.
|
|
414
|
+
// tagged sources v3.3.2, v3.4.0 and v3.5.2: internal/store/store.go `const schemaVersion = 1`.
|
|
356
415
|
// A pin without a verified entry leaves the header check unavailable.
|
|
357
|
-
const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1 };
|
|
416
|
+
const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1, "3.5.2": 1 };
|
|
358
417
|
|
|
359
418
|
export function bundledEngramSchema(coreVersion ) {
|
|
360
419
|
return Object.hasOwn(BUNDLED_ENGRAM_SCHEMA, coreVersion) ? BUNDLED_ENGRAM_SCHEMA[coreVersion] : undefined;
|
|
@@ -424,8 +483,7 @@ export function bundledChildEnv(base , input
|
|
|
424
483
|
return env;
|
|
425
484
|
}
|
|
426
485
|
if (memory.url !== undefined) env.ENGRAM_URL = memory.url;
|
|
427
|
-
|
|
428
|
-
env.ENGRAM_BIN = local ?? input.guard;
|
|
486
|
+
env.ENGRAM_BIN = input.guard;
|
|
429
487
|
return env;
|
|
430
488
|
}
|
|
431
489
|
|
|
@@ -458,6 +516,58 @@ export function withBundledNpmCommand(settingsText , command
|
|
|
458
516
|
|
|
459
517
|
|
|
460
518
|
|
|
519
|
+
// settings.json without `npmCommand`: every other key kept in order and the
|
|
520
|
+
// file's indentation and final newline kept.
|
|
521
|
+
function settingsWithoutNpmCommand(settingsText , settings ) {
|
|
522
|
+
const indent = settingsText.match(/\{\r?\n([ \t]+)/)?.[1] ?? " ";
|
|
523
|
+
const { npmCommand: _removed, ...rest } = settings;
|
|
524
|
+
const serialized = JSON.stringify(rest, null, indent);
|
|
525
|
+
return settingsText.endsWith("\n") ? `${serialized}\n` : serialized;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// The agent home's settings.json without a `npmCommand` naming the prefix a
|
|
529
|
+
// bundled self-uninstall removed (gentle-shell-uninstall.ts): one absolute
|
|
530
|
+
// entry inside `withinRemoved` drops the whole key, every other key kept in
|
|
531
|
+
// order and the file's indentation and final newline kept. A file that is not
|
|
532
|
+
// a JSON object is never rewritten; a command naming nothing inside it stays.
|
|
533
|
+
export function withoutBundledNpmCommand(settingsText , withinRemoved ) {
|
|
534
|
+
if (settingsText === undefined) return { status: "unchanged" };
|
|
535
|
+
const settings = parseJson(settingsText);
|
|
536
|
+
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return { status: "malformed" };
|
|
537
|
+
const current = settings.npmCommand;
|
|
538
|
+
if (!Array.isArray(current) || !current.some((part) => typeof part === "string" && isAbsolute(part) && withinRemoved(part))) return { status: "unchanged" };
|
|
539
|
+
return { status: "removed", text: settingsWithoutNpmCommand(settingsText, settings) };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// An `npmCommand` a bundled install wrote (scripts/bundled-install.mjs
|
|
543
|
+
// bundledNpmCommand, with npm-cli.js and --prefix in earlier releases): our
|
|
544
|
+
// node in runtime/node-<v>, then npm's settings and cache flags, and
|
|
545
|
+
// --update-notifier=false last. A command the user chose never matches, even
|
|
546
|
+
// when its program is missing.
|
|
547
|
+
export function isBundledNpmCommand(command ) {
|
|
548
|
+
const [node] = command;
|
|
549
|
+
return typeof node === "string" && /[\\/]runtime[\\/]node-[^\\/]+[\\/](node\.exe|bin[\\/]node)$/.test(node)
|
|
550
|
+
&& ["--userconfig=", "--globalconfig=", "--cache="].every((flag) => command.some((part) => typeof part === "string" && part.startsWith(flag)))
|
|
551
|
+
&& command[command.length - 1] === "--update-notifier=false";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// The agent home's settings.json without an `npmCommand` a bundled install
|
|
555
|
+
// wrote whose executable (its first entry) no longer exists, as after a
|
|
556
|
+
// bundled self-uninstall that kept the home (#2469): Pi runs that command for
|
|
557
|
+
// every package install, so a key naming a deleted node.exe fails them all
|
|
558
|
+
// with ENOENT. A command whose executable exists, that names no absolute
|
|
559
|
+
// path, or that the user set themselves is never touched.
|
|
560
|
+
export function withoutMissingBundledNpmCommand(settingsText , exists ) {
|
|
561
|
+
if (settingsText === undefined) return { status: "unchanged" };
|
|
562
|
+
const settings = parseJson(settingsText);
|
|
563
|
+
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return { status: "malformed" };
|
|
564
|
+
const executable = Array.isArray(settings.npmCommand) ? settings.npmCommand[0] : undefined;
|
|
565
|
+
if (typeof executable !== "string" || !isAbsolute(executable) || exists(executable) || !isBundledNpmCommand(settings.npmCommand )) return { status: "unchanged" };
|
|
566
|
+
return { status: "removed", text: settingsWithoutNpmCommand(settingsText, settings) };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
|
|
461
571
|
|
|
462
572
|
|
|
463
573
|
|