gentle-pi 4.3.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 +8 -5
- 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 +101 -151
- 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 +61 -0
- package/docs/gentle-shell.md +77 -7
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +19 -15
- 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 +173 -38
- package/extensions/gentle-ai.ts +214 -69
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +332 -73
- 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 +2 -0
- 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 +123 -49
- package/lib/gentle-shell-launcher.ts +9 -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/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-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +4 -3
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- 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/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +122 -48
- package/runtime/gentle-shell-launcher.mjs +9 -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/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +34 -6
- 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 +3 -3
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +1 -1
- 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 +365 -20
- package/tests/gentle-ai-binary.test.ts +1 -1
- 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-bundled.test.ts +301 -248
- package/tests/gentle-shell-launcher.test.ts +28 -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 +183 -8
- 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-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/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 +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 +99 -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/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-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +14 -2
- 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 +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/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/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
|
|
|
@@ -1787,6 +1705,26 @@ async function ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootst
|
|
|
1787
1705
|
}
|
|
1788
1706
|
}
|
|
1789
1707
|
|
|
1708
|
+
// The npmCommand a bundled install wrote into a home it owns must not outlive
|
|
1709
|
+
// it (#2469): Pi runs it for every package install, so a key naming a deleted
|
|
1710
|
+
// node.exe fails them all with ENOENT. A main-channel bootstrap drops the key
|
|
1711
|
+
// when it has the shape a bundled install writes and its node is gone; a
|
|
1712
|
+
// command the user set is never touched, and neither are an existing
|
|
1713
|
+
// executable, a link home and a foreign home.
|
|
1714
|
+
async function removeMissingNpmCommand(home, homeHadContentBeforeBootstrap) {
|
|
1715
|
+
if (home.mode === "link") return;
|
|
1716
|
+
const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
|
|
1717
|
+
if (homeIsForeign(home, previous, homeHadContentBeforeBootstrap) || !isDirectory(home.dir)) return;
|
|
1718
|
+
const settingsPath = join(home.dir, "settings.json");
|
|
1719
|
+
const edit = withoutMissingBundledNpmCommand(readJsonIfExists(settingsPath), (path) => existsSync(path));
|
|
1720
|
+
if (edit.status === "malformed") {
|
|
1721
|
+
process.stderr.write(`gentle-shell: ${settingsPath} is not a JSON object, so a stale npmCommand in it is not repaired\n`);
|
|
1722
|
+
} else if (edit.status === "removed") {
|
|
1723
|
+
writeFileAtomically(settingsPath, edit.text);
|
|
1724
|
+
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`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1790
1728
|
// `gentle-shell self-uninstall` in a bundled install (S6, S7): the prefix's own
|
|
1791
1729
|
// folders and exactly the PATH entry its receipt records; on Windows the running
|
|
1792
1730
|
// node.exe and launcher are removed by a detached process after this one exits.
|
|
@@ -1844,11 +1782,27 @@ async function startupUpdate(bundled, args, startupEnv) {
|
|
|
1844
1782
|
if (!shouldCheckStartupUpdate({ args, env: startupEnv, bundled: Boolean(bundled),
|
|
1845
1783
|
inputTTY: process.stdin.isTTY === true, outputTTY: process.stdout.isTTY === true })) return undefined;
|
|
1846
1784
|
const configPath = resolveConfigPath();
|
|
1847
|
-
let skipped;
|
|
1848
|
-
try {
|
|
1785
|
+
let skipped, cache;
|
|
1786
|
+
try {
|
|
1787
|
+
const raw = readRawConfig(configPath);
|
|
1788
|
+
skipped = raw.skippedStartupRelease;
|
|
1789
|
+
// The last recorded lookup answers today's check without the network; a
|
|
1790
|
+
// stale or malformed record just means this launch looks again.
|
|
1791
|
+
cache = parseStartupReleaseCache(raw.startupReleaseCheck);
|
|
1792
|
+
} catch { /* A notice must not prevent opening. */ }
|
|
1849
1793
|
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 }))
|
|
1794
|
+
const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped, cache,
|
|
1795
|
+
lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })),
|
|
1796
|
+
// Only a live lookup reaches this: persisting it keeps the next launches
|
|
1797
|
+
// off the network. A failed write is retried by tomorrow's stale cache.
|
|
1798
|
+
cacheWrite: (latest) => {
|
|
1799
|
+
try {
|
|
1800
|
+
writeStartupReleaseCache(() => readJsonIfExists(configPath), (text) => {
|
|
1801
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
1802
|
+
writeFileAtomically(configPath, text, { mode: 0o600 });
|
|
1803
|
+
}, latest);
|
|
1804
|
+
} catch { /* A notice must not prevent opening. */ }
|
|
1805
|
+
}, });
|
|
1852
1806
|
if (!offer) return undefined;
|
|
1853
1807
|
let choice;
|
|
1854
1808
|
try { choice = await promptStartupUpdate(offer, { color: process.stdout.hasColors?.() === true }); }
|
|
@@ -1886,7 +1840,7 @@ async function main() {
|
|
|
1886
1840
|
if (args.error !== undefined) fail(`${args.error}\nRun 'gentle-shell --help' for usage.`, 2);
|
|
1887
1841
|
if (args.help) {
|
|
1888
1842
|
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
|
|
1843
|
+
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
1844
|
process.exit(0);
|
|
1891
1845
|
}
|
|
1892
1846
|
if (args.command === "home") {
|
|
@@ -1933,7 +1887,7 @@ async function main() {
|
|
|
1933
1887
|
if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
|
|
1934
1888
|
|
|
1935
1889
|
// Version and help runs (Shell's own or forwarded to Pi) never probe Engram,
|
|
1936
|
-
// ensure the companion, print memory notices or
|
|
1890
|
+
// ensure the companion, print memory notices or start a server.
|
|
1937
1891
|
const engramQuiet = args.version || piVersionOrHelpRequested(args.passthrough);
|
|
1938
1892
|
let runtime;
|
|
1939
1893
|
if (bundled) {
|
|
@@ -1996,7 +1950,7 @@ async function main() {
|
|
|
1996
1950
|
const { managedUpdatePlan, updateSelectedPi } = await import("../scripts/pi-update.mjs");
|
|
1997
1951
|
const plan = managedUpdatePlan(args.passthrough);
|
|
1998
1952
|
if (plan.self) {
|
|
1999
|
-
process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(),
|
|
1953
|
+
process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(), env: process.env, home: homedir(),
|
|
2000
1954
|
adapters: await packageManagerAdapters(), out: (line) => process.stdout.write(`${line}\n`) });
|
|
2001
1955
|
if (plan.piArgs === undefined) return;
|
|
2002
1956
|
}
|
|
@@ -2044,6 +1998,12 @@ async function main() {
|
|
|
2044
1998
|
} catch (error) {
|
|
2045
1999
|
process.stderr.write(`gentle-shell: could not give Pi the bundled npm in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
|
|
2046
2000
|
}
|
|
2001
|
+
} else {
|
|
2002
|
+
try {
|
|
2003
|
+
await removeMissingNpmCommand(home, homeHadContentBeforeBootstrap);
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
process.stderr.write(`gentle-shell: could not repair the npmCommand in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
|
|
2006
|
+
}
|
|
2047
2007
|
}
|
|
2048
2008
|
|
|
2049
2009
|
if (args.command === "setup") {
|
|
@@ -2177,7 +2137,7 @@ async function main() {
|
|
|
2177
2137
|
|
|
2178
2138
|
if (bundled) {
|
|
2179
2139
|
// Recheck after setup and before provider loading: never carry a stale
|
|
2180
|
-
//
|
|
2140
|
+
// decision across an installer that could create data.
|
|
2181
2141
|
if (args.piSubcommand === undefined && !engramQuiet) {
|
|
2182
2142
|
bundledMemory = await classifyBundledMemory();
|
|
2183
2143
|
bundledEnv = bundledProcessEnv();
|
|
@@ -2185,19 +2145,9 @@ async function main() {
|
|
|
2185
2145
|
// could not be verified is reported, never a reason to stop Shell.
|
|
2186
2146
|
const companion = await ensureEngramCompanion(home, runtime, ["ignore", 2, 2], 10000);
|
|
2187
2147
|
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 };
|
|
2148
|
+
// Only this final decision starts the shared server, and Pi gets its
|
|
2149
|
+
// URL only once the server verified.
|
|
2150
|
+
if (bundledMemory.spawn) bundledMemory = await startSharedEngram();
|
|
2201
2151
|
if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
|
|
2202
2152
|
}
|
|
2203
2153
|
bundledEnv = bundledProcessEnv();
|