gentle-pi 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -128
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +9 -9
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +35 -7
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +443 -8
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-shell-bin.test.ts +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +131 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +69 -9
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/vim-editor-adapter.test.ts +41 -15
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +24 -1
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -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
|
|
|
@@ -176,6 +176,29 @@ export function bundledGentlePiConfigHome(env ) {
|
|
|
176
176
|
return prefix === undefined ? undefined : bundledPiConfigDir(prefix);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// #2201: the standalone launcher gives Pi this variable in its isolated home
|
|
180
|
+
// only, naming the config folder next to the agent dir (standaloneConfigHome:
|
|
181
|
+
// ~/.gentle-shell/gentle-ai by default). It is launcher-owned, never
|
|
182
|
+
// GENTLE_PI_CONFIG_HOME: Gentle AI 4.0.0 reads that one as the parent of its
|
|
183
|
+
// own gentle-ai folder, and the agent's shell would pass it to any gentle-ai.
|
|
184
|
+
export const GENTLE_SHELL_CONFIG_HOME_ENV = "GENTLE_SHELL_CONFIG_HOME";
|
|
185
|
+
|
|
186
|
+
// gentle-pi's config home, the one rule every reader shares: a bundled
|
|
187
|
+
// prefix's folder, then an explicit GENTLE_PI_CONFIG_HOME, then the launcher's
|
|
188
|
+
// marker (absolute only), then ~/.pi/gentle-ai. Empty values count as unset.
|
|
189
|
+
// `launcherMarker: false` is for install-level state written outside a session
|
|
190
|
+
// (the main channel's channel.json, builds and dev-binary.json): the installer
|
|
191
|
+
// and `gentle-shell upgrade` never see the marker, so their readers ignore it.
|
|
192
|
+
export function resolveGentlePiConfigHome(env , home , options = {}) {
|
|
193
|
+
const path = pathFor(options.platform ?? process.platform);
|
|
194
|
+
const bundled = bundledGentlePiConfigHome(env);
|
|
195
|
+
if (bundled !== undefined) return bundled;
|
|
196
|
+
if (env.GENTLE_PI_CONFIG_HOME) return env.GENTLE_PI_CONFIG_HOME;
|
|
197
|
+
const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
|
|
198
|
+
if (options.launcherMarker !== false && marker && path.isAbsolute(marker)) return marker;
|
|
199
|
+
return path.join(home, ".pi", "gentle-ai");
|
|
200
|
+
}
|
|
201
|
+
|
|
179
202
|
// The exact Pi of the version (S8): the Pi the version's package.json pins,
|
|
180
203
|
// installed inside the version folder with Pi's own bin entry. Never another Pi.
|
|
181
204
|
export function bundledPi(install , input ) {
|
|
@@ -214,14 +237,32 @@ export function bundledPi(install , input
|
|
|
214
237
|
// memory is allowed locally; otherwise the Shell-owned guard, so no provider,
|
|
215
238
|
// whatever loaded it, runs an Engram binary against an unverified store.
|
|
216
239
|
export const ENGRAM_COMPANION_SOURCE = "npm:gentle-engram@0.4.2";
|
|
217
|
-
export const
|
|
240
|
+
export const ENGRAM_PRIVATE_REQUIRED_NOTICE = "Engram memory disabled: session-owned servers require a supporting published bundled core and companion. This bundle does not establish both capabilities; it will not fall back to a detached shared server. Install a supporting published bundle and restart Shell. Existing shared services are not stopped or adopted as session-owned.";
|
|
241
|
+
export const ENGRAM_CORE_CHANGED_NOTICE = "Engram memory disabled: the bundled core changed since Shell recorded this store. Install a published bundle supporting migrate-copy, stop every Engram client, retain a complete backup, then run gentle-shell memory migrate --confirm-quiescent. The command verifies and migrates a copy before updating provenance; older bundles refuse safely. Do not edit coreSha256 or delete the original store. Shell remains usable without memory.";
|
|
242
|
+
export const ENGRAM_RECOVERY_NOTICE = "Engram memory disabled: Shell could not verify safe store or server reuse. Matching versions or updating alone do not prove compatibility. Reuse requires a server with verified /compatibility evidence or matching Shell-provisioned store identity. 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.";
|
|
218
243
|
// `gentle-shell setup` publishes the verified core (ensureEngram) on every run.
|
|
219
244
|
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
245
|
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.";
|
|
246
|
+
// The first Engram release whose server answers GET /compatibility (PR #1792).
|
|
247
|
+
export const ENGRAM_COMPATIBILITY_MIN_VERSION = "3.4.0";
|
|
248
|
+
export function engramOutdatedServerNotice(version ) {
|
|
249
|
+
return `Engram memory disabled: the running Engram server reports version ${version}, which cannot prove store compatibility. Back up your Engram data, update that server to ${ENGRAM_COMPATIBILITY_MIN_VERSION} or later with the method you installed it with, and restart it; updating Shell alone does not replace a running server. Shell rechecks on every launch and remains usable without memory.`;
|
|
250
|
+
}
|
|
251
|
+
// A canonical numeric x.y.z (optionally v-prefixed, no leading zeros) older than
|
|
252
|
+
// the minimum. The version comes from the server, so anything else is never
|
|
253
|
+
// echoed to the user.
|
|
254
|
+
function outdatedServerVersion(version ) {
|
|
255
|
+
const match = /^v?(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})$/.exec(version ?? "");
|
|
256
|
+
if (match === null) return undefined;
|
|
257
|
+
const parts = match.slice(1).map(Number);
|
|
258
|
+
const minimum = ENGRAM_COMPATIBILITY_MIN_VERSION.split(".").map(Number);
|
|
259
|
+
const index = parts.findIndex((part, i) => part !== minimum[i]);
|
|
260
|
+
return index >= 0 && parts[index] < minimum[index] ? parts.join(".") : undefined;
|
|
261
|
+
}
|
|
221
262
|
// The default endpoint gentle-engram uses without ENGRAM_URL or ENGRAM_PORT.
|
|
222
263
|
export const ENGRAM_DEFAULT_PORT = 7437;
|
|
223
264
|
export const ENGRAM_DEFAULT_URL = `http://127.0.0.1:${ENGRAM_DEFAULT_PORT}`;
|
|
224
|
-
const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_DATABASE_URL"];
|
|
265
|
+
const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_SOCKET", "ENGRAM_DATABASE_URL"];
|
|
225
266
|
const HEX32 = /^[a-f0-9]{32}$/;
|
|
226
267
|
|
|
227
268
|
|
|
@@ -239,11 +280,13 @@ export function engramProbeAllowed(env ) {
|
|
|
239
280
|
// Evidence only: never run an executable, open SQLite or infer compatibility
|
|
240
281
|
// from versions. `provenance` means Shell's own record matched the store
|
|
241
282
|
// (directory, .instance-id, core digest) and its header showed no newer schema.
|
|
283
|
+
// A server version only selects the wording of a disabled decision.
|
|
242
284
|
export function classifyEngramStartup(input
|
|
243
285
|
) {
|
|
244
|
-
|
|
245
|
-
if (!engramProbeAllowed(input.env)) return unknown;
|
|
286
|
+
if (!engramProbeAllowed(input.env)) return { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE };
|
|
246
287
|
const endpoint = input.endpoint;
|
|
288
|
+
const outdated = endpoint.kind === "listening" && !endpoint.compatible ? outdatedServerVersion(endpoint.healthVersion) : undefined;
|
|
289
|
+
const unknown = { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
|
|
247
290
|
if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
|
|
248
291
|
if (endpoint.kind === "refused" && input.store === "absent") {
|
|
249
292
|
return input.core ? { policy: "allow", fresh: true } : { policy: "disabled", notice: ENGRAM_CORE_MISSING_NOTICE };
|
|
@@ -253,7 +296,7 @@ export function classifyEngramStartup(input
|
|
|
253
296
|
// Shell's own store, created by this exact core: offline, the provider starts
|
|
254
297
|
// that core; a running server must be that store and that exact core.
|
|
255
298
|
if (endpoint.kind === "refused") return { policy: "allow" };
|
|
256
|
-
if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow" };
|
|
299
|
+
if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
|
|
257
300
|
return unknown;
|
|
258
301
|
}
|
|
259
302
|
|
|
@@ -334,9 +377,9 @@ export function engramProvenanceMatches(record , ob
|
|
|
334
377
|
}
|
|
335
378
|
|
|
336
379
|
// The bundled core's own schema (SQLite user_version), verified from the
|
|
337
|
-
// tagged
|
|
380
|
+
// tagged sources v3.3.2 and v3.4.0: internal/store/store.go `const schemaVersion = 1`.
|
|
338
381
|
// A pin without a verified entry leaves the header check unavailable.
|
|
339
|
-
const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1 };
|
|
382
|
+
const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1 };
|
|
340
383
|
|
|
341
384
|
export function bundledEngramSchema(coreVersion ) {
|
|
342
385
|
return Object.hasOwn(BUNDLED_ENGRAM_SCHEMA, coreVersion) ? BUNDLED_ENGRAM_SCHEMA[coreVersion] : undefined;
|
|
@@ -384,6 +427,11 @@ export function supportsEngramStartupPolicy(metadata )
|
|
|
384
427
|
return pkg?.name === "gentle-engram" && pkg.engram?.startupPolicy === 1;
|
|
385
428
|
}
|
|
386
429
|
|
|
430
|
+
export function supportsEngramSessionOwned(metadata ) {
|
|
431
|
+
const pkg = parseJson(metadata);
|
|
432
|
+
return supportsEngramStartupPolicy(metadata) && pkg?.engram?.sessionOwned === 1;
|
|
433
|
+
}
|
|
434
|
+
|
|
387
435
|
export function bundledChildEnv(base , input ) {
|
|
388
436
|
const env = { ...base, PI_SKIP_VERSION_CHECK: "1", [GENTLE_SHELL_PREFIX_ENV]: input.prefix };
|
|
389
437
|
delete env.GENTLE_SHELL_PI;
|
|
@@ -408,6 +456,12 @@ export function bundledChildEnv(base , input
|
|
|
408
456
|
if (memory.url !== undefined) env.ENGRAM_URL = memory.url;
|
|
409
457
|
const local = memory.policy === "allow" && memory.url === undefined ? input.engram : undefined;
|
|
410
458
|
env.ENGRAM_BIN = local ?? input.guard;
|
|
459
|
+
if (local && memory.owned) {
|
|
460
|
+
env.ENGRAM_SERVER_MODE = "session-owned";
|
|
461
|
+
delete env.ENGRAM_URL;
|
|
462
|
+
delete env.ENGRAM_PORT;
|
|
463
|
+
delete env.ENGRAM_SOCKET;
|
|
464
|
+
}
|
|
411
465
|
return env;
|
|
412
466
|
}
|
|
413
467
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Generated by scripts/build-runtime-modules.mjs. Do not edit.
|
|
2
|
+
import { chmodSync, closeSync, linkSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { detectJsonFormatting } from "./gentle-shell-launcher.mjs";
|
|
5
|
+
|
|
6
|
+
// `gentle-shell home import-pi`: a one-time, opt-in copy of the user's pi
|
|
7
|
+
// sign-ins and MCP servers into a separate Gentle Shell home, as an
|
|
8
|
+
// alternative to --link. Only the files and settings keys below are read;
|
|
9
|
+
// packages, extensions, caches, AGENTS.md and sessions are never copied, so
|
|
10
|
+
// the target home keeps its own provisioning. Nothing is ever overwritten and
|
|
11
|
+
// the source is only read.
|
|
12
|
+
|
|
13
|
+
// auth.json holds OAuth tokens and API keys: always private in the copy.
|
|
14
|
+
const IMPORTED_FILES = [
|
|
15
|
+
{ name: "auth.json", mode: 0o600 },
|
|
16
|
+
{ name: "mcp.json" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Pi 1.2.0's own settings for the default model (SettingsManager
|
|
20
|
+
// get/setDefaultProvider, DefaultModel, DefaultThinkingLevel).
|
|
21
|
+
export const IMPORTED_SETTINGS_KEYS = ["defaultProvider", "defaultModel", "defaultThinkingLevel"];
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
function tempPathFor(path ) {
|
|
49
|
+
return join(dirname(path), `.${basename(path)}.gentle-shell-${process.pid}.tmp`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeQuietly(path ) {
|
|
53
|
+
try {
|
|
54
|
+
unlinkSync(path);
|
|
55
|
+
} catch {
|
|
56
|
+
// Nothing left to clean up.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeTemp(path , data , mode ) {
|
|
61
|
+
const temp = tempPathFor(path);
|
|
62
|
+
const fd = openSync(temp, "wx", mode);
|
|
63
|
+
try {
|
|
64
|
+
try {
|
|
65
|
+
writeFileSync(fd, data);
|
|
66
|
+
} finally {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
chmodSync(temp, mode); // the umask must not change the mode
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// A partial copy may hold credentials: never leave it behind.
|
|
72
|
+
removeQuietly(temp);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return temp;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const nodeImportPiFs = {
|
|
79
|
+
kind: (path) => {
|
|
80
|
+
try {
|
|
81
|
+
const stat = lstatSync(path);
|
|
82
|
+
if (stat.isSymbolicLink()) return "other";
|
|
83
|
+
return stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if ((error ).code === "ENOENT") return "missing";
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
mode: (path) => lstatSync(path).mode & 0o777,
|
|
90
|
+
readText: (path) => readFileSync(path, "utf8"),
|
|
91
|
+
copyNew: (from, to, mode) => {
|
|
92
|
+
const temp = writeTemp(to, readFileSync(from), mode);
|
|
93
|
+
try {
|
|
94
|
+
linkSync(temp, to); // EEXIST instead of replacing a file that appeared meanwhile
|
|
95
|
+
} finally {
|
|
96
|
+
removeQuietly(temp);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
writeAtomic: (path, text) => {
|
|
100
|
+
const mode = lstatSync(path, { throwIfNoEntry: false })?.mode;
|
|
101
|
+
const temp = writeTemp(path, text, mode === undefined ? 0o600 : mode & 0o777);
|
|
102
|
+
try {
|
|
103
|
+
renameSync(temp, path);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
removeQuietly(temp);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function reason(error ) {
|
|
112
|
+
const code = (error )?.code;
|
|
113
|
+
return typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseObject(text ) {
|
|
117
|
+
try {
|
|
118
|
+
const value = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
119
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value ) : undefined;
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function importFile(name , mode , input ) {
|
|
126
|
+
const { fs } = input;
|
|
127
|
+
const from = join(input.source, name);
|
|
128
|
+
const to = join(input.target, name);
|
|
129
|
+
// Classification can throw too (ENOTDIR under a file): one failed item, never an aborted report.
|
|
130
|
+
try {
|
|
131
|
+
const kind = fs.kind(from);
|
|
132
|
+
if (kind === "missing") return { item: name, status: "not found" };
|
|
133
|
+
if (kind !== "file") return { item: name, status: "skipped", detail: "not a regular file" };
|
|
134
|
+
if (fs.kind(to) !== "missing") return { item: name, status: "present" };
|
|
135
|
+
const copyMode = mode ?? fs.mode(from);
|
|
136
|
+
const detail = `${copyMode.toString(8).padStart(4, "0")}`;
|
|
137
|
+
if (input.dryRun) return { item: name, status: "would copy", detail };
|
|
138
|
+
fs.copyNew(from, to, copyMode);
|
|
139
|
+
return { item: name, status: "copied", detail };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { item: name, status: "failed", detail: reason(error) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function importSettings(input ) {
|
|
146
|
+
try {
|
|
147
|
+
return planAndWriteSettings(input);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// An unreadable settings.json stays one failed item, never an aborted report.
|
|
150
|
+
return [{ item: "settings.json", status: "failed", detail: reason(error) }];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function planAndWriteSettings(input ) {
|
|
155
|
+
const { fs } = input;
|
|
156
|
+
const from = join(input.source, "settings.json");
|
|
157
|
+
const to = join(input.target, "settings.json");
|
|
158
|
+
const sourceKind = fs.kind(from);
|
|
159
|
+
if (sourceKind === "missing") return [{ item: "settings.json", status: "not found" }];
|
|
160
|
+
if (sourceKind !== "file") return [{ item: "settings.json", status: "skipped", detail: "not a regular file" }];
|
|
161
|
+
const source = parseObject(fs.readText(from));
|
|
162
|
+
if (source === undefined) return [{ item: "settings.json", status: "skipped", detail: `${from} is not a JSON object` }];
|
|
163
|
+
const targetKind = fs.kind(to);
|
|
164
|
+
if (targetKind !== "missing" && targetKind !== "file") return [{ item: "settings.json", status: "skipped", detail: `${to} is not a regular file` }];
|
|
165
|
+
const targetText = targetKind === "missing" ? undefined : fs.readText(to);
|
|
166
|
+
const target = targetText === undefined ? {} : parseObject(targetText);
|
|
167
|
+
if (target === undefined) return [{ item: "settings.json", status: "skipped", detail: `${to} is not a JSON object` }];
|
|
168
|
+
|
|
169
|
+
const items = [];
|
|
170
|
+
const added = {};
|
|
171
|
+
for (const key of IMPORTED_SETTINGS_KEYS) {
|
|
172
|
+
const item = `settings.json ${key}`;
|
|
173
|
+
if (!Object.hasOwn(source, key)) items.push({ item, status: "not found" });
|
|
174
|
+
else if (Object.hasOwn(target, key)) items.push({ item, status: "present" });
|
|
175
|
+
else if (typeof source[key] !== "string") items.push({ item, status: "skipped", detail: "not a string" });
|
|
176
|
+
else {
|
|
177
|
+
added[key] = source[key];
|
|
178
|
+
items.push({ item, status: input.dryRun ? "would copy" : "copied" });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (input.dryRun || Object.keys(added).length === 0) return items;
|
|
182
|
+
try {
|
|
183
|
+
// Keep the target's own indentation and trailing-newline style.
|
|
184
|
+
const style = targetText === undefined ? { indent: undefined, trailingNewline: true } : detectJsonFormatting(targetText);
|
|
185
|
+
const body = JSON.stringify({ ...target, ...added }, null, style.indent ?? 2);
|
|
186
|
+
fs.writeAtomic(to, style.trailingNewline ? `${body}\n` : body);
|
|
187
|
+
return items;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const detail = reason(error);
|
|
190
|
+
return items.map((entry) => (entry.status === "copied" ? { ...entry, status: "failed", detail } : entry));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Copies what the target lacks. A dry run reads the same files and writes
|
|
195
|
+
// nothing. The caller has already made sure `target` is a home this launcher
|
|
196
|
+
// owns (and exists, unless this is a dry run).
|
|
197
|
+
export function importPi(input ) {
|
|
198
|
+
const options = { ...input, fs: input.fs ?? nodeImportPiFs };
|
|
199
|
+
const items = IMPORTED_FILES.map((file) => importFile(file.name, file.mode, options));
|
|
200
|
+
items.push(...importSettings(options));
|
|
201
|
+
return { items, failed: items.some((item) => item.status === "failed") };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function describe(item ) {
|
|
205
|
+
switch (item.status) {
|
|
206
|
+
case "copied":
|
|
207
|
+
case "would copy":
|
|
208
|
+
return item.detail !== undefined && item.item === "auth.json" ? `${item.status} (${item.detail})` : item.status;
|
|
209
|
+
case "present":
|
|
210
|
+
return "already present, kept";
|
|
211
|
+
case "not found":
|
|
212
|
+
return "not found";
|
|
213
|
+
case "skipped":
|
|
214
|
+
return `skipped: ${item.detail}`;
|
|
215
|
+
case "failed":
|
|
216
|
+
return `failed: ${item.detail}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function formatImportPiReport(result , context ) {
|
|
221
|
+
const lines = [
|
|
222
|
+
context.dryRun
|
|
223
|
+
? `Dry run: would import from ${context.source} into ${context.target} (nothing is written)`
|
|
224
|
+
: `Importing from ${context.source} into ${context.target}`,
|
|
225
|
+
];
|
|
226
|
+
for (const item of result.items) lines.push(` ${item.item}: ${describe(item)}`);
|
|
227
|
+
const count = (...statuses ) => result.items.filter((item) => statuses.includes(item.status)).length;
|
|
228
|
+
const imported = count("copied", "would copy");
|
|
229
|
+
const rest = `${count("present")} already present, ${count("not found")} not found, ${count("skipped")} skipped`;
|
|
230
|
+
if (result.failed) lines.push(`Import failed for ${count("failed")} item(s); ${count("copied")} copied and kept. ${rest}.`);
|
|
231
|
+
else if (imported === 0) lines.push(`Nothing to import: ${rest}.`);
|
|
232
|
+
else lines.push(`${context.dryRun ? "Would import" : "Imported"} ${imported} item(s); ${rest}.`);
|
|
233
|
+
return lines;
|
|
234
|
+
}
|