gentle-pi 4.3.0 → 4.5.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 +10 -7
- package/assets/agents/gentle-ai-worker.md +1 -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 +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +138 -155
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +82 -0
- package/docs/gentle-shell.md +80 -10
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +72 -55
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +187 -40
- package/extensions/gentle-ai.ts +1765 -1185
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +354 -0
- package/extensions/gentle-shell.ts +418 -79
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- 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/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -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/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +141 -57
- package/lib/gentle-shell-launcher.ts +11 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/jobs-sidebar-state.ts +22 -3
- package/lib/jobs-widget.ts +112 -0
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- 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-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -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 +2 -2
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +6 -7
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +27 -7
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +14 -10
- 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/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +140 -56
- package/runtime/gentle-shell-launcher.mjs +11 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/bundled-install.mjs +209 -13
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +31 -7
- package/scripts/installer-preflight.mjs +6 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +37 -7
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- 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 +226 -5
- 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 +20 -0
- package/tests/bundled-install.test.ts +276 -5
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +4 -4
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- 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/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +369 -24
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +928 -448
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +452 -0
- package/tests/gentle-shell-bundled.test.ts +321 -248
- package/tests/gentle-shell-launcher.test.ts +29 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +227 -27
- package/tests/gentle-todo.test.ts +11 -0
- 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-downloads.test.ts +32 -0
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/jobs-widget.test.ts +138 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/model-profiles-preservation.test.ts +2 -2
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +104 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- 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 +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/runtime-harness.mjs +188 -211
- 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 +6 -4
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +18 -6
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +67 -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/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/vim-editor-adapter.test.ts +2 -2
- 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 +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
package/bin/gentle-shell.mjs
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
import { constants as osConstants, homedir, tmpdir } from "node:os";
|
|
29
29
|
import { delimiter, dirname, join, resolve as resolvePath } from "node:path";
|
|
30
30
|
import { spawn, spawnSync } from "node:child_process";
|
|
31
|
-
import { createHash } from "node:crypto";
|
|
32
31
|
import { fileURLToPath } from "node:url";
|
|
33
32
|
import {
|
|
34
33
|
buildPiInvocation,
|
|
@@ -67,24 +66,18 @@ import {
|
|
|
67
66
|
import { formatImportPiReport, importPi } from "../runtime/gentle-shell-import-pi.mjs";
|
|
68
67
|
import {
|
|
69
68
|
BUNDLED_VERIFY_ENV,
|
|
70
|
-
bundledEngramSchema,
|
|
71
69
|
classifyEngramStartup,
|
|
72
70
|
ENGRAM_COMPANION_SOURCE,
|
|
73
|
-
|
|
74
|
-
ENGRAM_PRIVATE_REQUIRED_NOTICE,
|
|
75
|
-
ENGRAM_PROVISION_FAILED_NOTICE,
|
|
71
|
+
ENGRAM_DEFAULT_URL,
|
|
76
72
|
ENGRAM_RECOVERY_NOTICE,
|
|
77
|
-
|
|
73
|
+
ENGRAM_START_FAILED_NOTICE,
|
|
78
74
|
GENTLE_SHELL_CONFIG_HOME_ENV,
|
|
79
75
|
engramProbeAllowed,
|
|
80
|
-
|
|
81
|
-
engramProvenancePath,
|
|
82
|
-
parseEngramProvenance,
|
|
76
|
+
sharedEngramEnv,
|
|
83
77
|
piVersionOrHelpRequested,
|
|
84
78
|
probeEngramEndpoint,
|
|
85
79
|
sqliteHeaderUserVersion,
|
|
86
80
|
supportsEngramStartupPolicy,
|
|
87
|
-
supportsEngramSessionOwned,
|
|
88
81
|
bundledChildEnv,
|
|
89
82
|
bundledPi,
|
|
90
83
|
bundledSetupEnv,
|
|
@@ -95,6 +88,7 @@ import {
|
|
|
95
88
|
parseBundledUpgradeArgs,
|
|
96
89
|
resolveBundledInstall,
|
|
97
90
|
withBundledNpmCommand,
|
|
91
|
+
withoutMissingBundledNpmCommand,
|
|
98
92
|
} from "../runtime/gentle-shell-bundled.mjs";
|
|
99
93
|
import {
|
|
100
94
|
parseResumeHandoff,
|
|
@@ -106,8 +100,8 @@ import {
|
|
|
106
100
|
import { bundledGentleAiPaths, GENTLE_AI_VERSION, gentleAiBinaryPath, gentleAiIsolationEnv, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
|
|
107
101
|
import { DEFAULT_THEME_NAME, installIsolatedTuiModeSetting } from "../scripts/install-tui-mode-setting.mjs";
|
|
108
102
|
import { artifactFor, engramPinVersion } from "../scripts/installer-downloads.mjs";
|
|
109
|
-
import { migrateBundledEngram, migrationPending,
|
|
110
|
-
import { shouldCheckStartupUpdate,
|
|
103
|
+
import { migrateBundledEngram, migrationPending, MIGRATION_PENDING_NOTICE, MIGRATION_USAGE } from "../scripts/bundled-engram-migration.mjs";
|
|
104
|
+
import { findStartupUpdate, parseStartupReleaseCache, shouldCheckStartupUpdate, writeStartupReleaseCache } from "../runtime/gentle-shell-startup-update.mjs";
|
|
111
105
|
import { promptStartupUpdate } from "../runtime/gentle-shell-update-panel.mjs";
|
|
112
106
|
import { applyStartupChoice, omitStartupVersion, resolveStartupRelaunch } from "../runtime/gentle-shell-startup-flow.mjs";
|
|
113
107
|
|
|
@@ -204,19 +198,6 @@ function verifiedBundledCore() {
|
|
|
204
198
|
return undefined;
|
|
205
199
|
}
|
|
206
200
|
|
|
207
|
-
function sha256File(path) {
|
|
208
|
-
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function readSmallRegularFile(path, limit) {
|
|
212
|
-
try {
|
|
213
|
-
const info = lstatSync(path);
|
|
214
|
-
return info.isFile() && info.size <= limit ? readFileSync(path, "utf8") : undefined;
|
|
215
|
-
} catch {
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
201
|
// The database's raw SQLite header, read as a plain file (never through
|
|
221
202
|
// SQLite): a regular, unlinked engram.db whose opened identity is the one seen.
|
|
222
203
|
function readSqliteUserVersion(path) {
|
|
@@ -236,112 +217,49 @@ function readSqliteUserVersion(path) {
|
|
|
236
217
|
}
|
|
237
218
|
}
|
|
238
219
|
|
|
239
|
-
//
|
|
240
|
-
// the .
|
|
241
|
-
|
|
242
|
-
const dir = join(homedir(), ".engram");
|
|
243
|
-
let dataDir;
|
|
244
|
-
try {
|
|
245
|
-
if (!lstatSync(dir).isDirectory()) return { store: "unknown" };
|
|
246
|
-
dataDir = realpathSync(dir);
|
|
247
|
-
} catch (error) {
|
|
248
|
-
return { store: error.code === "ENOENT" ? "absent" : "unknown" };
|
|
249
|
-
}
|
|
250
|
-
return { store: "present", dir, dataDir, instanceId: readSmallRegularFile(join(dir, ".instance-id"), 128)?.trim() };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Only the identity files Engram's `instance-id` writes: a store Shell
|
|
254
|
-
// provisioned whose session has not created its database yet.
|
|
255
|
-
function identityOnlyStore(dir) {
|
|
256
|
-
try {
|
|
257
|
-
return readdirSync(dir).every((name) => name === ".instance-id" || name === ".instance-id.lock" || name.startsWith(".instance-id-"));
|
|
258
|
-
} catch {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Shell's own store: the record Shell wrote when it provisioned this exact
|
|
264
|
-
// directory's identity, the same core digest, and a database header no newer
|
|
265
|
-
// than the bundled schema (or no database yet).
|
|
266
|
-
function ownEngramStore(observed, core) {
|
|
267
|
-
let record;
|
|
268
|
-
try {
|
|
269
|
-
record = parseEngramProvenance(readJsonIfExists(engramProvenancePath(bundledInstall.root, process.platform)));
|
|
270
|
-
} catch {
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
if (record === undefined || observed.store !== "present" || core === undefined) return undefined;
|
|
274
|
-
if (!engramProvenanceMatches(record, { dataDir: observed.dataDir, instanceId: observed.instanceId, coreSha256: sha256File(core) })) return undefined;
|
|
275
|
-
const headerAllows = engramHeaderAllows(readSqliteUserVersion(join(observed.dir, "engram.db")), bundledEngramSchema(engramPinVersion));
|
|
276
|
-
return headerAllows || identityOnlyStore(observed.dir) ? { instanceId: record.instanceId } : undefined;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Evidence only: file metadata, the raw header and a bounded read-only probe
|
|
280
|
-
// of the default endpoint. No Engram executable, SQLite, identity repair, or
|
|
281
|
-
// contact with an explicitly selected endpoint.
|
|
220
|
+
// Evidence only: the default store's raw header and a bounded read-only probe
|
|
221
|
+
// of the default endpoint. No Engram executable, SQLite, or contact with an
|
|
222
|
+
// explicitly selected endpoint. A `spawn` decision starts nothing here.
|
|
282
223
|
async function classifyBundledMemory() {
|
|
283
224
|
if (migrationPending(bundledInstall.root)) return { policy: "disabled", notice: MIGRATION_PENDING_NOTICE };
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
if (!engramProbeAllowed(process.env)) return classifyEngramStartup({ ...decision, store: "unknown", endpoint: { kind: "unknown" } });
|
|
287
|
-
const observed = observeEngramStore();
|
|
225
|
+
const decision = { env: process.env, core: verifiedBundledCore() !== undefined, coreVersion: engramPinVersion };
|
|
226
|
+
if (!engramProbeAllowed(process.env)) return classifyEngramStartup({ ...decision, endpoint: { kind: "unknown" } });
|
|
288
227
|
const endpoint = await probeEngramEndpoint();
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// without a database. Keep update checks suppressed and GitHub tokens removed
|
|
306
|
-
// for this provisioning invocation. The timeout stays below the identity lock
|
|
307
|
-
// wait: a held lock means a concurrent creator, so memory stays off that launch.
|
|
308
|
-
const ENGRAM_PROVISION_TIMEOUT_MS = 15000;
|
|
309
|
-
|
|
310
|
-
// A fresh launch's store identity, provisioned by Shell itself with the verified
|
|
311
|
-
// core before Pi starts, then recorded. False when any step fails.
|
|
312
|
-
function provisionEngramStore() {
|
|
228
|
+
const userVersion = endpoint.kind === "refused" ? readSqliteUserVersion(join(homedir(), ".engram", "engram.db")) : undefined;
|
|
229
|
+
return classifyEngramStartup({ ...decision, endpoint, userVersion });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Like other agents' hooks, Shell starts the shared server when nothing
|
|
233
|
+
// listens: the verified core's plain `serve` on Engram's own default port and
|
|
234
|
+
// data directory, detached so it outlives Pi. Its environment drops the user's
|
|
235
|
+
// Engram settings and GitHub tokens and skips the update check. Ready means the
|
|
236
|
+
// same /compatibility contract any reused server must answer, so a concurrent
|
|
237
|
+
// launch that wins the bind converges too: after the child exits, polling
|
|
238
|
+
// continues for a short grace within the overall bound.
|
|
239
|
+
const ENGRAM_START_TIMEOUT_MS = 10000;
|
|
240
|
+
const ENGRAM_START_EXIT_GRACE_MS = 2000;
|
|
241
|
+
|
|
242
|
+
async function startSharedEngram() {
|
|
243
|
+
const failed = { policy: "disabled", notice: ENGRAM_START_FAILED_NOTICE };
|
|
313
244
|
const core = verifiedBundledCore();
|
|
314
|
-
if (core === undefined
|
|
315
|
-
let
|
|
245
|
+
if (core === undefined) return failed;
|
|
246
|
+
let deadline = Date.now() + ENGRAM_START_TIMEOUT_MS;
|
|
247
|
+
const exited = () => { deadline = Math.min(deadline, Date.now() + ENGRAM_START_EXIT_GRACE_MS); };
|
|
316
248
|
try {
|
|
317
|
-
|
|
249
|
+
const env = sharedEngramEnv(process.env);
|
|
250
|
+
const child = spawn(core, ["serve"], { cwd: homedir(), env, detached: true, stdio: "ignore", windowsHide: true });
|
|
251
|
+
child.once("error", exited);
|
|
252
|
+
child.once("exit", exited);
|
|
253
|
+
child.unref();
|
|
318
254
|
} catch {
|
|
319
|
-
return
|
|
255
|
+
return failed;
|
|
320
256
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
delete env.GITHUB_TOKEN;
|
|
326
|
-
const result = spawnSync(core, ["instance-id"], { env, encoding: "utf8", timeout: ENGRAM_PROVISION_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
327
|
-
const observed = observeEngramStore();
|
|
328
|
-
if (result.status !== 0 || observed.store !== "present" || observed.dataDir !== target || !/^[a-f0-9]{32}$/.test(observed.instanceId ?? "") ||
|
|
329
|
-
String(result.stdout).trim() !== observed.instanceId) return false;
|
|
330
|
-
return writeEngramProvenance({ schema: 1, dataDir: target, instanceId: observed.instanceId, coreSha256: sha256File(core) });
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function writeEngramProvenance(record) {
|
|
334
|
-
try {
|
|
335
|
-
const path = engramProvenancePath(bundledInstall.root, process.platform);
|
|
336
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
337
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
338
|
-
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
339
|
-
renameSync(temporary, path);
|
|
340
|
-
return true;
|
|
341
|
-
} catch (error) {
|
|
342
|
-
process.stderr.write(`gentle-shell: could not record Engram provenance (${error.message})\n`);
|
|
343
|
-
return false;
|
|
257
|
+
while (Date.now() < deadline) {
|
|
258
|
+
const endpoint = await probeEngramEndpoint({ timeoutMs: 500 });
|
|
259
|
+
if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
344
261
|
}
|
|
262
|
+
return failed;
|
|
345
263
|
}
|
|
346
264
|
|
|
347
265
|
// Use Pi's supported configuration-only install command, never an alternate
|
|
@@ -357,7 +275,6 @@ async function ensureEngramCompanion(home, runtime, stdio, timeoutMs) {
|
|
|
357
275
|
if (!Array.isArray(packages)) return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from non-array packages" };
|
|
358
276
|
let pinned = false;
|
|
359
277
|
let local = false;
|
|
360
|
-
let localOwned = true;
|
|
361
278
|
let npmSelected = false;
|
|
362
279
|
for (const entry of packages) {
|
|
363
280
|
const source = typeof entry === "string" ? entry : entry?.source;
|
|
@@ -371,10 +288,9 @@ async function ensureEngramCompanion(home, runtime, stdio, timeoutMs) {
|
|
|
371
288
|
return { ok: false, exitCode: 1, message: `gentle-shell: selected companion ${source} lacks engram.startupPolicy=1; update it and recheck (selection preserved)` };
|
|
372
289
|
}
|
|
373
290
|
local = true;
|
|
374
|
-
localOwned &&= supportsEngramSessionOwned(readJsonIfExists(join(selected, "package.json")));
|
|
375
291
|
}
|
|
376
|
-
if (local && !npmSelected) return { ok: true
|
|
377
|
-
if (pinned && supportsEngramStartupPolicy(readJsonIfExists(metadata))) return { ok: true
|
|
292
|
+
if (local && !npmSelected) return { ok: true };
|
|
293
|
+
if (pinned && supportsEngramStartupPolicy(readJsonIfExists(metadata))) return { ok: true };
|
|
378
294
|
process.stderr.write(`gentle-shell: installing policy-capable companion ${ENGRAM_COMPANION_SOURCE} through Pi\n`);
|
|
379
295
|
const result = await spawnAndWait(runtime.command, [...runtime.args, "install", ENGRAM_COMPANION_SOURCE], buildSetupEnv(home, runtime), stdio, timeoutMs);
|
|
380
296
|
let declared = false;
|
|
@@ -385,7 +301,7 @@ async function ensureEngramCompanion(home, runtime, stdio, timeoutMs) {
|
|
|
385
301
|
if (!result.ok || result.timedOut || !declared || !supportsEngramStartupPolicy(readJsonIfExists(metadata))) {
|
|
386
302
|
return { ok: false, exitCode: 1, message: "gentle-shell: companion installation did not establish engram.startupPolicy=1; the Engram guard still keeps unverified stores closed. Repair gentle-engram, then restart Shell." };
|
|
387
303
|
}
|
|
388
|
-
return { ok: true
|
|
304
|
+
return { ok: true };
|
|
389
305
|
}
|
|
390
306
|
|
|
391
307
|
// The bundled children's environment (bundledChildEnv), with the pinned Engram
|
|
@@ -1210,7 +1126,7 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
|
|
|
1210
1126
|
if (!engram.ok) return engram;
|
|
1211
1127
|
}
|
|
1212
1128
|
// The memory decision never skips setup: it only selects the Engram
|
|
1213
|
-
// environment (
|
|
1129
|
+
// environment (guard, policy, ENGRAM_URL) setup's children and the
|
|
1214
1130
|
// launched Pi get. Gentle AI's `install --agent pi` and `pi-engram init` open
|
|
1215
1131
|
// no store. Reclassified now that the verified core may just be published.
|
|
1216
1132
|
if (bundledInstall) {
|
|
@@ -1688,7 +1604,9 @@ async function packageManagerAdapters() {
|
|
|
1688
1604
|
fs: await import("node:fs/promises"),
|
|
1689
1605
|
which: async (name) => findOnPath(name) ?? null,
|
|
1690
1606
|
...(invocation ? { invocation } : {}),
|
|
1691
|
-
|
|
1607
|
+
// stderrTail: a caller that asks for it, such as a failed `update` install, gets the bounded stderr.
|
|
1608
|
+
run: (command, argv, options = {}) => run(command, argv, { env: options.env ?? process.env, cwd: options.cwd, deadlineMs: options.deadlineMs ?? 20 * 60_000,
|
|
1609
|
+
...(options.stderrTail === undefined ? {} : { stderrTail: options.stderrTail }) }),
|
|
1692
1610
|
};
|
|
1693
1611
|
}
|
|
1694
1612
|
|
|
@@ -1766,6 +1684,32 @@ async function handleBundledUpgrade(bundled, commandArgs) {
|
|
|
1766
1684
|
}
|
|
1767
1685
|
}
|
|
1768
1686
|
|
|
1687
|
+
// `gentle-shell update --latest` in a bundled install: the newest published
|
|
1688
|
+
// Pi installed with this Gentle Shell beside the active version, without the
|
|
1689
|
+
// release's verified lockfile (after a warning), verified, then `current`
|
|
1690
|
+
// switched. Returns the exit code like handleBundledUpgrade; a failure leaves
|
|
1691
|
+
// `current` as it was.
|
|
1692
|
+
async function handleBundledPiLatest(bundled) {
|
|
1693
|
+
const { prefixLayout, publishedPi, upgradePiLatest } = await import("../scripts/bundled-install.mjs");
|
|
1694
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
|
|
1695
|
+
try {
|
|
1696
|
+
await upgradePiLatest({
|
|
1697
|
+
layout,
|
|
1698
|
+
home: homedir(),
|
|
1699
|
+
shell: ownPackageVersion(),
|
|
1700
|
+
platform: process.platform,
|
|
1701
|
+
arch: process.arch,
|
|
1702
|
+
env: process.env,
|
|
1703
|
+
adapters: { latestPi: () => publishedPi(layout, { platform: process.platform, env: process.env }) },
|
|
1704
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
1705
|
+
});
|
|
1706
|
+
return 0;
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
process.stderr.write(`gentle-shell update: ${error.message}${error.stderr ? `\n${error.stderr}` : ""}\n`);
|
|
1709
|
+
return 1;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1769
1713
|
// The bundled npm as the `npmCommand` of a home Gentle Shell owns (the isolated
|
|
1770
1714
|
// home, or a --home it created or provisioned; never --link or a foreign home),
|
|
1771
1715
|
// so Pi installs packages with our npm while its tools keep the user's PATH.
|
|
@@ -1787,6 +1731,26 @@ async function ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootst
|
|
|
1787
1731
|
}
|
|
1788
1732
|
}
|
|
1789
1733
|
|
|
1734
|
+
// The npmCommand a bundled install wrote into a home it owns must not outlive
|
|
1735
|
+
// it (#2469): Pi runs it for every package install, so a key naming a deleted
|
|
1736
|
+
// node.exe fails them all with ENOENT. A main-channel bootstrap drops the key
|
|
1737
|
+
// when it has the shape a bundled install writes and its node is gone; a
|
|
1738
|
+
// command the user set is never touched, and neither are an existing
|
|
1739
|
+
// executable, a link home and a foreign home.
|
|
1740
|
+
async function removeMissingNpmCommand(home, homeHadContentBeforeBootstrap) {
|
|
1741
|
+
if (home.mode === "link") return;
|
|
1742
|
+
const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
|
|
1743
|
+
if (homeIsForeign(home, previous, homeHadContentBeforeBootstrap) || !isDirectory(home.dir)) return;
|
|
1744
|
+
const settingsPath = join(home.dir, "settings.json");
|
|
1745
|
+
const edit = withoutMissingBundledNpmCommand(readJsonIfExists(settingsPath), (path) => existsSync(path));
|
|
1746
|
+
if (edit.status === "malformed") {
|
|
1747
|
+
process.stderr.write(`gentle-shell: ${settingsPath} is not a JSON object, so a stale npmCommand in it is not repaired\n`);
|
|
1748
|
+
} else if (edit.status === "removed") {
|
|
1749
|
+
writeFileAtomically(settingsPath, edit.text);
|
|
1750
|
+
process.stderr.write(`gentle-shell: removed the bundled npmCommand naming a missing executable from ${settingsPath}; Pi installs packages with the npm on your PATH\n`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1790
1754
|
// `gentle-shell self-uninstall` in a bundled install (S6, S7): the prefix's own
|
|
1791
1755
|
// folders and exactly the PATH entry its receipt records; on Windows the running
|
|
1792
1756
|
// node.exe and launcher are removed by a detached process after this one exits.
|
|
@@ -1844,11 +1808,27 @@ async function startupUpdate(bundled, args, startupEnv) {
|
|
|
1844
1808
|
if (!shouldCheckStartupUpdate({ args, env: startupEnv, bundled: Boolean(bundled),
|
|
1845
1809
|
inputTTY: process.stdin.isTTY === true, outputTTY: process.stdout.isTTY === true })) return undefined;
|
|
1846
1810
|
const configPath = resolveConfigPath();
|
|
1847
|
-
let skipped;
|
|
1848
|
-
try {
|
|
1811
|
+
let skipped, cache;
|
|
1812
|
+
try {
|
|
1813
|
+
const raw = readRawConfig(configPath);
|
|
1814
|
+
skipped = raw.skippedStartupRelease;
|
|
1815
|
+
// The last recorded lookup answers today's check without the network; a
|
|
1816
|
+
// stale or malformed record just means this launch looks again.
|
|
1817
|
+
cache = parseStartupReleaseCache(raw.startupReleaseCheck);
|
|
1818
|
+
} catch { /* A notice must not prevent opening. */ }
|
|
1849
1819
|
const { latestRelease } = await import("../scripts/main-channel.mjs");
|
|
1850
|
-
const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped,
|
|
1851
|
-
lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal }))
|
|
1820
|
+
const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped, cache,
|
|
1821
|
+
lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })),
|
|
1822
|
+
// Only a live lookup reaches this: persisting it keeps the next launches
|
|
1823
|
+
// off the network. A failed write is retried by tomorrow's stale cache.
|
|
1824
|
+
cacheWrite: (latest) => {
|
|
1825
|
+
try {
|
|
1826
|
+
writeStartupReleaseCache(() => readJsonIfExists(configPath), (text) => {
|
|
1827
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
1828
|
+
writeFileAtomically(configPath, text, { mode: 0o600 });
|
|
1829
|
+
}, latest);
|
|
1830
|
+
} catch { /* A notice must not prevent opening. */ }
|
|
1831
|
+
}, });
|
|
1852
1832
|
if (!offer) return undefined;
|
|
1853
1833
|
let choice;
|
|
1854
1834
|
try { choice = await promptStartupUpdate(offer, { color: process.stdout.hasColors?.() === true }); }
|
|
@@ -1886,7 +1866,7 @@ async function main() {
|
|
|
1886
1866
|
if (args.error !== undefined) fail(`${args.error}\nRun 'gentle-shell --help' for usage.`, 2);
|
|
1887
1867
|
if (args.help) {
|
|
1888
1868
|
process.stdout.write("Pi updates: update reinstalls the verified supplier of the Pi this launcher runs (a global Pi, or the\nShell package whose adjacent peer it is, at the same Shell version) and reads the resulting Pi version back.\nUnknown ownership refuses without installing; update --extensions-only stays Pi-only.\nPrefix-bundled Pi updates with gentle-shell upgrade.\n");
|
|
1889
|
-
process.stdout.write(`${helpText()}\nBundled Engram
|
|
1869
|
+
process.stdout.write(`${helpText()}\nBundled Engram memory: Shell shares the global Engram server on 127.0.0.1:7437, like other agents' hooks.\nA running server is reused only with verified /compatibility evidence; when nothing listens, Shell starts the\nbundled core's engram serve (default ~/.engram) and waits for that evidence. Otherwise ENGRAM_BIN is Shell's\nguard and memory stays off for that launch. Matching versions or updating alone do not prove compatibility.\nBack up existing data before manual changes; do not delete it or bypass the safety check.\nShell rechecks on every launch and remains usable without memory.\nAn explicit ENGRAM_BIN or ENGRAM_URL is kept and bypasses the guard.\n`);
|
|
1890
1870
|
process.exit(0);
|
|
1891
1871
|
}
|
|
1892
1872
|
if (args.command === "home") {
|
|
@@ -1933,7 +1913,7 @@ async function main() {
|
|
|
1933
1913
|
if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
|
|
1934
1914
|
|
|
1935
1915
|
// Version and help runs (Shell's own or forwarded to Pi) never probe Engram,
|
|
1936
|
-
// ensure the companion, print memory notices or
|
|
1916
|
+
// ensure the companion, print memory notices or start a server.
|
|
1937
1917
|
const engramQuiet = args.version || piVersionOrHelpRequested(args.passthrough);
|
|
1938
1918
|
let runtime;
|
|
1939
1919
|
if (bundled) {
|
|
@@ -1979,14 +1959,21 @@ async function main() {
|
|
|
1979
1959
|
|
|
1980
1960
|
// S5: in a bundled install Pi updates only with Gentle Shell, so Pi's own
|
|
1981
1961
|
// update becomes `gentle-shell upgrade` (run when interactive, named
|
|
1982
|
-
// otherwise)
|
|
1962
|
+
// otherwise), or with `--latest` the newest published Pi without the
|
|
1963
|
+
// release's lockfile (run even when not interactive: it was asked for);
|
|
1964
|
+
// package updates still reach Pi.
|
|
1983
1965
|
let passthrough = args.passthrough;
|
|
1984
1966
|
if (bundled && args.piSubcommand === "update") {
|
|
1985
1967
|
const plan = bundledUpdatePlan(args.passthrough);
|
|
1986
1968
|
if (plan.self) {
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1969
|
+
let code;
|
|
1970
|
+
if (plan.latest) {
|
|
1971
|
+
code = await handleBundledPiLatest(bundled);
|
|
1972
|
+
} else {
|
|
1973
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1974
|
+
for (const line of bundledUpdateNotice({ piVersion: versionCheck.version, interactive })) process.stdout.write(`${line}\n`);
|
|
1975
|
+
code = interactive ? await handleBundledUpgrade(bundled, []) : undefined;
|
|
1976
|
+
}
|
|
1990
1977
|
const next = bundledUpdateNext(plan, code);
|
|
1991
1978
|
if ("exitCode" in next) process.exit(next.exitCode);
|
|
1992
1979
|
if (next.note) process.stderr.write(`${next.note}\n`);
|
|
@@ -1996,7 +1983,7 @@ async function main() {
|
|
|
1996
1983
|
const { managedUpdatePlan, updateSelectedPi } = await import("../scripts/pi-update.mjs");
|
|
1997
1984
|
const plan = managedUpdatePlan(args.passthrough);
|
|
1998
1985
|
if (plan.self) {
|
|
1999
|
-
process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(),
|
|
1986
|
+
process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(), env: process.env, home: homedir(),
|
|
2000
1987
|
adapters: await packageManagerAdapters(), out: (line) => process.stdout.write(`${line}\n`) });
|
|
2001
1988
|
if (plan.piArgs === undefined) return;
|
|
2002
1989
|
}
|
|
@@ -2044,6 +2031,12 @@ async function main() {
|
|
|
2044
2031
|
} catch (error) {
|
|
2045
2032
|
process.stderr.write(`gentle-shell: could not give Pi the bundled npm in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
|
|
2046
2033
|
}
|
|
2034
|
+
} else {
|
|
2035
|
+
try {
|
|
2036
|
+
await removeMissingNpmCommand(home, homeHadContentBeforeBootstrap);
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
process.stderr.write(`gentle-shell: could not repair the npmCommand in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
|
|
2039
|
+
}
|
|
2047
2040
|
}
|
|
2048
2041
|
|
|
2049
2042
|
if (args.command === "setup") {
|
|
@@ -2177,7 +2170,7 @@ async function main() {
|
|
|
2177
2170
|
|
|
2178
2171
|
if (bundled) {
|
|
2179
2172
|
// Recheck after setup and before provider loading: never carry a stale
|
|
2180
|
-
//
|
|
2173
|
+
// decision across an installer that could create data.
|
|
2181
2174
|
if (args.piSubcommand === undefined && !engramQuiet) {
|
|
2182
2175
|
bundledMemory = await classifyBundledMemory();
|
|
2183
2176
|
bundledEnv = bundledProcessEnv();
|
|
@@ -2185,19 +2178,9 @@ async function main() {
|
|
|
2185
2178
|
// could not be verified is reported, never a reason to stop Shell.
|
|
2186
2179
|
const companion = await ensureEngramCompanion(home, runtime, ["ignore", 2, 2], 10000);
|
|
2187
2180
|
if (!companion.ok) process.stderr.write(`${companion.message}\n`);
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
bundledMemory = { policy: "disabled", notice: "Engram memory disabled: explicit ENGRAM_SERVER_MODE is invalid for a local bundled server; unset it or select exactly session-owned. Your selection has not been overridden." };
|
|
2192
|
-
} else {
|
|
2193
|
-
bundledMemory = companion.ok && companion.sessionOwned && supportsBundledSessionOwned(verifiedBundledCore(), process.env)
|
|
2194
|
-
? { ...bundledMemory, owned: true }
|
|
2195
|
-
: { policy: "disabled", notice: ENGRAM_PRIVATE_REQUIRED_NOTICE };
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
// A fresh store's identity and provenance exist before Pi starts, or
|
|
2199
|
-
// memory stays off: Pi never runs with allow without a written record.
|
|
2200
|
-
if (bundledMemory.fresh && !provisionEngramStore()) bundledMemory = { policy: "disabled", notice: ENGRAM_PROVISION_FAILED_NOTICE };
|
|
2181
|
+
// Only this final decision starts the shared server, and Pi gets its
|
|
2182
|
+
// URL only once the server verified.
|
|
2183
|
+
if (bundledMemory.spawn) bundledMemory = await startSharedEngram();
|
|
2201
2184
|
if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
|
|
2202
2185
|
}
|
|
2203
2186
|
bundledEnv = bundledProcessEnv();
|