gentle-pi 4.2.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 +336 -137
- 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/gentle-shell.mjs +170 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +33 -3
- 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 +5 -4
- package/docs/install-wizard.md +8 -1
- package/docs/permission-modes.md +1 -1
- package/docs/readme-reference.md +107 -19
- package/docs/yolo-mode.md +21 -8
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +99 -12
- package/extensions/gentle-ai.ts +284 -52
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +27 -12
- package/extensions/gentle-todo.ts +43 -9
- 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/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 +120 -9
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +39 -3
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +145 -11
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- 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/session-profile-freeze.ts +30 -1
- 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-gauge.ts +49 -6
- package/lib/shell-todo.ts +23 -4
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +38 -2
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +143 -9
- 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/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- 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/autonomous-guard.test.ts +122 -0
- package/tests/bounded-writer-admission.test.ts +11 -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 +1 -1
- 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 +282 -3
- 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 +163 -0
- package/tests/gentle-shell-bundled.test.ts +169 -6
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +79 -0
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +42 -9
- package/tests/gentle-todo.test.ts +179 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- 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/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 +24 -20
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +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/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +23 -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-gauge.test.ts +42 -0
- 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/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 +18 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { chmodSync, closeSync, linkSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { detectJsonFormatting } from "./gentle-shell-launcher.ts";
|
|
4
|
+
|
|
5
|
+
// `gentle-shell home import-pi`: a one-time, opt-in copy of the user's pi
|
|
6
|
+
// sign-ins and MCP servers into a separate Gentle Shell home, as an
|
|
7
|
+
// alternative to --link. Only the files and settings keys below are read;
|
|
8
|
+
// packages, extensions, caches, AGENTS.md and sessions are never copied, so
|
|
9
|
+
// the target home keeps its own provisioning. Nothing is ever overwritten and
|
|
10
|
+
// the source is only read.
|
|
11
|
+
|
|
12
|
+
// auth.json holds OAuth tokens and API keys: always private in the copy.
|
|
13
|
+
const IMPORTED_FILES: readonly { name: string; mode?: number }[] = [
|
|
14
|
+
{ name: "auth.json", mode: 0o600 },
|
|
15
|
+
{ name: "mcp.json" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Pi 1.2.0's own settings for the default model (SettingsManager
|
|
19
|
+
// get/setDefaultProvider, DefaultModel, DefaultThinkingLevel).
|
|
20
|
+
export const IMPORTED_SETTINGS_KEYS: readonly string[] = ["defaultProvider", "defaultModel", "defaultThinkingLevel"];
|
|
21
|
+
|
|
22
|
+
export interface ImportPiFs {
|
|
23
|
+
// lstat: a symbolic link is "other", never followed.
|
|
24
|
+
kind: (path: string) => "missing" | "file" | "dir" | "other";
|
|
25
|
+
mode: (path: string) => number;
|
|
26
|
+
readText: (path: string) => string;
|
|
27
|
+
// Creates `to` with exactly `mode`; throws when it already exists.
|
|
28
|
+
copyNew: (from: string, to: string, mode: number) => void;
|
|
29
|
+
// Temp file next to `path`, then a rename onto it.
|
|
30
|
+
writeAtomic: (path: string, text: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ImportPiStatus = "copied" | "would copy" | "present" | "not found" | "skipped" | "failed";
|
|
34
|
+
|
|
35
|
+
export interface ImportPiItem {
|
|
36
|
+
item: string;
|
|
37
|
+
status: ImportPiStatus;
|
|
38
|
+
// The copy's mode, or why the item was skipped or failed. Never file content.
|
|
39
|
+
detail?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ImportPiResult {
|
|
43
|
+
items: ImportPiItem[];
|
|
44
|
+
failed: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tempPathFor(path: string): string {
|
|
48
|
+
return join(dirname(path), `.${basename(path)}.gentle-shell-${process.pid}.tmp`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function removeQuietly(path: string): void {
|
|
52
|
+
try {
|
|
53
|
+
unlinkSync(path);
|
|
54
|
+
} catch {
|
|
55
|
+
// Nothing left to clean up.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeTemp(path: string, data: string | Buffer, mode: number): string {
|
|
60
|
+
const temp = tempPathFor(path);
|
|
61
|
+
const fd = openSync(temp, "wx", mode);
|
|
62
|
+
try {
|
|
63
|
+
try {
|
|
64
|
+
writeFileSync(fd, data);
|
|
65
|
+
} finally {
|
|
66
|
+
closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
chmodSync(temp, mode); // the umask must not change the mode
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// A partial copy may hold credentials: never leave it behind.
|
|
71
|
+
removeQuietly(temp);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return temp;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const nodeImportPiFs: ImportPiFs = {
|
|
78
|
+
kind: (path) => {
|
|
79
|
+
try {
|
|
80
|
+
const stat = lstatSync(path);
|
|
81
|
+
if (stat.isSymbolicLink()) return "other";
|
|
82
|
+
return stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "missing";
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
mode: (path) => lstatSync(path).mode & 0o777,
|
|
89
|
+
readText: (path) => readFileSync(path, "utf8"),
|
|
90
|
+
copyNew: (from, to, mode) => {
|
|
91
|
+
const temp = writeTemp(to, readFileSync(from), mode);
|
|
92
|
+
try {
|
|
93
|
+
linkSync(temp, to); // EEXIST instead of replacing a file that appeared meanwhile
|
|
94
|
+
} finally {
|
|
95
|
+
removeQuietly(temp);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
writeAtomic: (path, text) => {
|
|
99
|
+
const mode = lstatSync(path, { throwIfNoEntry: false })?.mode;
|
|
100
|
+
const temp = writeTemp(path, text, mode === undefined ? 0o600 : mode & 0o777);
|
|
101
|
+
try {
|
|
102
|
+
renameSync(temp, path);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
removeQuietly(temp);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function reason(error: unknown): string {
|
|
111
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
112
|
+
return typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseObject(text: string): Record<string, unknown> | undefined {
|
|
116
|
+
try {
|
|
117
|
+
const value: unknown = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
118
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function importFile(name: string, mode: number | undefined, input: { source: string; target: string; dryRun: boolean; fs: ImportPiFs }): ImportPiItem {
|
|
125
|
+
const { fs } = input;
|
|
126
|
+
const from = join(input.source, name);
|
|
127
|
+
const to = join(input.target, name);
|
|
128
|
+
// Classification can throw too (ENOTDIR under a file): one failed item, never an aborted report.
|
|
129
|
+
try {
|
|
130
|
+
const kind = fs.kind(from);
|
|
131
|
+
if (kind === "missing") return { item: name, status: "not found" };
|
|
132
|
+
if (kind !== "file") return { item: name, status: "skipped", detail: "not a regular file" };
|
|
133
|
+
if (fs.kind(to) !== "missing") return { item: name, status: "present" };
|
|
134
|
+
const copyMode = mode ?? fs.mode(from);
|
|
135
|
+
const detail = `${copyMode.toString(8).padStart(4, "0")}`;
|
|
136
|
+
if (input.dryRun) return { item: name, status: "would copy", detail };
|
|
137
|
+
fs.copyNew(from, to, copyMode);
|
|
138
|
+
return { item: name, status: "copied", detail };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return { item: name, status: "failed", detail: reason(error) };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function importSettings(input: { source: string; target: string; dryRun: boolean; fs: ImportPiFs }): ImportPiItem[] {
|
|
145
|
+
try {
|
|
146
|
+
return planAndWriteSettings(input);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// An unreadable settings.json stays one failed item, never an aborted report.
|
|
149
|
+
return [{ item: "settings.json", status: "failed", detail: reason(error) }];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function planAndWriteSettings(input: { source: string; target: string; dryRun: boolean; fs: ImportPiFs }): ImportPiItem[] {
|
|
154
|
+
const { fs } = input;
|
|
155
|
+
const from = join(input.source, "settings.json");
|
|
156
|
+
const to = join(input.target, "settings.json");
|
|
157
|
+
const sourceKind = fs.kind(from);
|
|
158
|
+
if (sourceKind === "missing") return [{ item: "settings.json", status: "not found" }];
|
|
159
|
+
if (sourceKind !== "file") return [{ item: "settings.json", status: "skipped", detail: "not a regular file" }];
|
|
160
|
+
const source = parseObject(fs.readText(from));
|
|
161
|
+
if (source === undefined) return [{ item: "settings.json", status: "skipped", detail: `${from} is not a JSON object` }];
|
|
162
|
+
const targetKind = fs.kind(to);
|
|
163
|
+
if (targetKind !== "missing" && targetKind !== "file") return [{ item: "settings.json", status: "skipped", detail: `${to} is not a regular file` }];
|
|
164
|
+
const targetText = targetKind === "missing" ? undefined : fs.readText(to);
|
|
165
|
+
const target = targetText === undefined ? {} : parseObject(targetText);
|
|
166
|
+
if (target === undefined) return [{ item: "settings.json", status: "skipped", detail: `${to} is not a JSON object` }];
|
|
167
|
+
|
|
168
|
+
const items: ImportPiItem[] = [];
|
|
169
|
+
const added: Record<string, unknown> = {};
|
|
170
|
+
for (const key of IMPORTED_SETTINGS_KEYS) {
|
|
171
|
+
const item = `settings.json ${key}`;
|
|
172
|
+
if (!Object.hasOwn(source, key)) items.push({ item, status: "not found" });
|
|
173
|
+
else if (Object.hasOwn(target, key)) items.push({ item, status: "present" });
|
|
174
|
+
else if (typeof source[key] !== "string") items.push({ item, status: "skipped", detail: "not a string" });
|
|
175
|
+
else {
|
|
176
|
+
added[key] = source[key];
|
|
177
|
+
items.push({ item, status: input.dryRun ? "would copy" : "copied" });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (input.dryRun || Object.keys(added).length === 0) return items;
|
|
181
|
+
try {
|
|
182
|
+
// Keep the target's own indentation and trailing-newline style.
|
|
183
|
+
const style = targetText === undefined ? { indent: undefined, trailingNewline: true } : detectJsonFormatting(targetText);
|
|
184
|
+
const body = JSON.stringify({ ...target, ...added }, null, style.indent ?? 2);
|
|
185
|
+
fs.writeAtomic(to, style.trailingNewline ? `${body}\n` : body);
|
|
186
|
+
return items;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const detail = reason(error);
|
|
189
|
+
return items.map((entry) => (entry.status === "copied" ? { ...entry, status: "failed", detail } : entry));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Copies what the target lacks. A dry run reads the same files and writes
|
|
194
|
+
// nothing. The caller has already made sure `target` is a home this launcher
|
|
195
|
+
// owns (and exists, unless this is a dry run).
|
|
196
|
+
export function importPi(input: { source: string; target: string; dryRun: boolean; fs?: ImportPiFs }): ImportPiResult {
|
|
197
|
+
const options = { ...input, fs: input.fs ?? nodeImportPiFs };
|
|
198
|
+
const items = IMPORTED_FILES.map((file) => importFile(file.name, file.mode, options));
|
|
199
|
+
items.push(...importSettings(options));
|
|
200
|
+
return { items, failed: items.some((item) => item.status === "failed") };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function describe(item: ImportPiItem): string {
|
|
204
|
+
switch (item.status) {
|
|
205
|
+
case "copied":
|
|
206
|
+
case "would copy":
|
|
207
|
+
return item.detail !== undefined && item.item === "auth.json" ? `${item.status} (${item.detail})` : item.status;
|
|
208
|
+
case "present":
|
|
209
|
+
return "already present, kept";
|
|
210
|
+
case "not found":
|
|
211
|
+
return "not found";
|
|
212
|
+
case "skipped":
|
|
213
|
+
return `skipped: ${item.detail}`;
|
|
214
|
+
case "failed":
|
|
215
|
+
return `failed: ${item.detail}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function formatImportPiReport(result: ImportPiResult, context: { source: string; target: string; dryRun: boolean }): string[] {
|
|
220
|
+
const lines = [
|
|
221
|
+
context.dryRun
|
|
222
|
+
? `Dry run: would import from ${context.source} into ${context.target} (nothing is written)`
|
|
223
|
+
: `Importing from ${context.source} into ${context.target}`,
|
|
224
|
+
];
|
|
225
|
+
for (const item of result.items) lines.push(` ${item.item}: ${describe(item)}`);
|
|
226
|
+
const count = (...statuses: ImportPiStatus[]) => result.items.filter((item) => statuses.includes(item.status)).length;
|
|
227
|
+
const imported = count("copied", "would copy");
|
|
228
|
+
const rest = `${count("present")} already present, ${count("not found")} not found, ${count("skipped")} skipped`;
|
|
229
|
+
if (result.failed) lines.push(`Import failed for ${count("failed")} item(s); ${count("copied")} copied and kept. ${rest}.`);
|
|
230
|
+
else if (imported === 0) lines.push(`Nothing to import: ${rest}.`);
|
|
231
|
+
else lines.push(`${context.dryRun ? "Would import" : "Imported"} ${imported} item(s); ${rest}.`);
|
|
232
|
+
return lines;
|
|
233
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
1
|
+
import { basename, dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
2
2
|
import { CHILD_PACKAGE_INJECTION_ENV, encodeChildPackageInjection, type ChildPackageInjection } from "./child-package-injection.ts";
|
|
3
|
+
import { GENTLE_SHELL_CONFIG_HOME_ENV } from "./gentle-shell-bundled.ts";
|
|
3
4
|
|
|
4
5
|
// The gentle-shell launcher: pure, side-effect-free functions over injected
|
|
5
6
|
// env/fs/exec. `bin/gentle-shell.mjs` (T2) wires these into the real process,
|
|
6
7
|
// filesystem and child process so this module stays fully unit-testable.
|
|
7
8
|
|
|
8
|
-
export type LauncherCommand = "home" | "setup" | "upgrade" | "self-uninstall";
|
|
9
|
+
export type LauncherCommand = "home" | "setup" | "upgrade" | "self-uninstall" | "memory";
|
|
9
10
|
|
|
10
11
|
// pi's own package-management subcommands (see pi's cli/args.ts printHelp
|
|
11
12
|
// "Commands" list): each is dispatched by pi itself, before pi's own flag
|
|
12
13
|
// parsing, purely on argv[0]. `uninstall` is pi's alias for `remove`.
|
|
13
|
-
export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth"] as const;
|
|
14
|
+
export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth", "mcp"] as const;
|
|
14
15
|
|
|
15
16
|
export type PiSubcommand = (typeof PI_SUBCOMMANDS)[number];
|
|
16
17
|
|
|
@@ -41,7 +42,7 @@ export interface ParsedLauncherArgs {
|
|
|
41
42
|
// untouched as commandArgs — T2 owns interpreting `home link|isolated|<path>`.
|
|
42
43
|
// `self-uninstall` is not `uninstall`, which stays pi's own alias for `remove`.
|
|
43
44
|
export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
|
|
44
|
-
if (argv[0] === "home" || argv[0] === "upgrade" || argv[0] === "self-uninstall") {
|
|
45
|
+
if (argv[0] === "home" || argv[0] === "upgrade" || argv[0] === "self-uninstall" || argv[0] === "memory") {
|
|
45
46
|
return {
|
|
46
47
|
link: false,
|
|
47
48
|
isolated: false,
|
|
@@ -76,6 +77,9 @@ export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
|
|
|
76
77
|
if (passthrough.length === 0 && rest.length > 0 && isPiSubcommand(rest[0])) {
|
|
77
78
|
piSubcommand = rest[0];
|
|
78
79
|
}
|
|
80
|
+
// After a pi subcommand the separator belongs to it, e.g.
|
|
81
|
+
// `mcp add <server> -- <command>`; only a leading one is the launcher's.
|
|
82
|
+
if (piSubcommand !== undefined && passthrough.length > 0) passthrough.push(arg);
|
|
79
83
|
passthrough.push(...rest);
|
|
80
84
|
break;
|
|
81
85
|
}
|
|
@@ -147,6 +151,14 @@ export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
|
|
|
147
151
|
commandArgs = argv.slice(i + 1);
|
|
148
152
|
break;
|
|
149
153
|
}
|
|
154
|
+
// `home import-pi` likewise takes the home selectors ahead of it, so it
|
|
155
|
+
// imports into the home they resolve to; any other `home` after a
|
|
156
|
+
// selector stays pi's.
|
|
157
|
+
if (arg === "home" && argv[i + 1] === "import-pi" && command === undefined && passthrough.length === 0) {
|
|
158
|
+
command = "home";
|
|
159
|
+
commandArgs = argv.slice(i + 1);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
150
162
|
if (passthrough.length === 0 && isPiSubcommand(arg)) {
|
|
151
163
|
piSubcommand = arg;
|
|
152
164
|
}
|
|
@@ -211,6 +223,77 @@ export function isolatedDir(env: Record<string, string | undefined>, homedir: st
|
|
|
211
223
|
return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
|
|
212
224
|
}
|
|
213
225
|
|
|
226
|
+
// #2201: the isolated home keeps Gentle Shell's config in a gentle-ai folder
|
|
227
|
+
// next to its agent dir (~/.gentle-shell/gentle-ai by default), given to Pi as
|
|
228
|
+
// GENTLE_SHELL_CONFIG_HOME_ENV. `--link` and a custom `--home` keep the shared
|
|
229
|
+
// ~/.pi/gentle-ai: undefined for them.
|
|
230
|
+
export function standaloneConfigHome(home: ResolvedHome): string | undefined {
|
|
231
|
+
if (home.mode !== "isolated") return undefined;
|
|
232
|
+
const dir = resolvePath(home.dir);
|
|
233
|
+
const parent = dirname(dir);
|
|
234
|
+
// Only inside a .gentle-shell folder is the sibling Gentle Shell's own: next
|
|
235
|
+
// to a custom GENTLE_SHELL_HOME (~/myagent) it would adopt ~/gentle-ai, which
|
|
236
|
+
// may be anyone's, so such homes keep the shared config home.
|
|
237
|
+
return basename(dir) === "agent" && basename(parent) === ".gentle-shell" ? join(parent, "gentle-ai") : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The filesystem behind migrateStandaloneConfig; bin/gentle-shell.mjs wires the real one.
|
|
241
|
+
export interface ConfigMigrationFs {
|
|
242
|
+
// lstat: a symbolic link is "other", never followed.
|
|
243
|
+
kind: (path: string) => "missing" | "dir" | "file" | "other";
|
|
244
|
+
// The entry names of a directory (following a linked directory); throws when unreadable.
|
|
245
|
+
list: (path: string) => string[];
|
|
246
|
+
// mkdir -p with mode 0700; throws on failure.
|
|
247
|
+
mkdirPrivate: (path: string) => void;
|
|
248
|
+
// Copies a regular file, never overwriting `to`; throws on failure.
|
|
249
|
+
copyNew: (from: string, to: string) => void;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Install-level main-channel state stays in ~/.pi/gentle-ai: the installer and
|
|
253
|
+
// `gentle-shell upgrade` write it outside any session (gentleAiDevBinaryRegistrationPath).
|
|
254
|
+
const UNMIGRATED_CONFIG_FILES = new Set(["dev-binary.json", "channel.json"]);
|
|
255
|
+
|
|
256
|
+
// #2201: the first isolated launch after the move keeps the user's settings.
|
|
257
|
+
// Only when `target` does not exist yet: it is created (0700), and the regular
|
|
258
|
+
// files of `source` are copied into it, never overwriting and never changing
|
|
259
|
+
// `source`, which stays for Pi; symbolic links, subdirectories and the
|
|
260
|
+
// main-channel files are skipped. Once the folder exists this does nothing, so
|
|
261
|
+
// it is a one-time snapshot. A failure is reported in `lines`, never thrown:
|
|
262
|
+
// it never blocks the launch.
|
|
263
|
+
export function migrateStandaloneConfig(input: { source: string; target: string; fs: ConfigMigrationFs }): { lines: string[] } {
|
|
264
|
+
const { source, target, fs } = input;
|
|
265
|
+
if (fs.kind(target) !== "missing") return { lines: [] };
|
|
266
|
+
const reason = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
|
267
|
+
try {
|
|
268
|
+
fs.mkdirPrivate(target);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return { lines: [`gentle-shell: could not create ${target} for your Gentle Shell settings (${reason(error)}); continuing`] };
|
|
271
|
+
}
|
|
272
|
+
if (fs.kind(source) === "missing") return { lines: [] };
|
|
273
|
+
let names: string[];
|
|
274
|
+
try {
|
|
275
|
+
names = fs.list(source).sort();
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return { lines: [`gentle-shell: could not copy your settings from ${source} to ${target} (${reason(error)}); continuing`] };
|
|
278
|
+
}
|
|
279
|
+
const copied: string[] = [];
|
|
280
|
+
const failed: string[] = [];
|
|
281
|
+
for (const name of names) {
|
|
282
|
+
if (UNMIGRATED_CONFIG_FILES.has(name) || fs.kind(join(source, name)) !== "file") continue;
|
|
283
|
+
try {
|
|
284
|
+
fs.copyNew(join(source, name), join(target, name));
|
|
285
|
+
copied.push(name);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
failed.push(`${name}: ${reason(error)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (failed.length > 0) {
|
|
291
|
+
return { lines: [`gentle-shell: could not copy ${failed.join("; ")} from ${source} to ${target}; continuing with ${copied.length} copied file(s)`] };
|
|
292
|
+
}
|
|
293
|
+
if (copied.length === 0) return { lines: [] };
|
|
294
|
+
return { lines: [`gentle-shell: copied ${copied.length} settings file(s) from ${source} to ${target}, Gentle Shell's own config folder from now on (one-time copy; ${source} stays for Pi)`] };
|
|
295
|
+
}
|
|
296
|
+
|
|
214
297
|
export function resolveHome(input: ResolveHomeInput): ResolvedHome {
|
|
215
298
|
const { args, env, homedir, config } = input;
|
|
216
299
|
|
|
@@ -241,6 +324,15 @@ export function homeSelectorFlags(home: ResolvedHome): string[] {
|
|
|
241
324
|
return [];
|
|
242
325
|
}
|
|
243
326
|
|
|
327
|
+
// The first-run line of a separate home. `home import-pi` is offered only
|
|
328
|
+
// when the user's pi home has sign-ins (auth.json) to copy.
|
|
329
|
+
export function separateHomeHint(home: ResolvedHome, input: { piSignIns: boolean }): string {
|
|
330
|
+
const base = `gentle-shell: using a separate home at ${home.dir}. Run 'gentle-shell --link' to reuse your pi sign-ins and chats`;
|
|
331
|
+
if (!input.piSignIns) return `${base}.`;
|
|
332
|
+
const command = ["gentle-shell", ...homeSelectorFlags(home).map(shellQuote), "home", "import-pi"].join(" ");
|
|
333
|
+
return `${base}, or '${command}' to copy your sign-ins and MCP servers into this home.`;
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
export function launcherConfigPath(homedir: string): string {
|
|
245
337
|
return join(homedir, ".gentle-shell", "config.json");
|
|
246
338
|
}
|
|
@@ -575,6 +667,20 @@ const POST_INSTALL_REMOVAL_PACKAGES: readonly { readonly name: string; readonly
|
|
|
575
667
|
// bin/gentle-shell.mjs.
|
|
576
668
|
export const POST_INSTALL_REMOVAL_SOURCES: readonly string[] = POST_INSTALL_REMOVAL_PACKAGES.map((entry) => entry.source);
|
|
577
669
|
|
|
670
|
+
// #2143: a Gentle AI with package exclusion (gentle-ai#5503) skips these
|
|
671
|
+
// managed Pi packages instead of installing them only for the post-install
|
|
672
|
+
// removal above to delete them. An older Gentle AI ignores the variable (a flag
|
|
673
|
+
// would be rejected), so the removal stays as the fallback. Only sources Gentle
|
|
674
|
+
// AI manages belong here: it rejects any other value.
|
|
675
|
+
export const GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV = "GENTLE_AI_PI_EXCLUDE_PACKAGES";
|
|
676
|
+
const LAUNCHER_EXCLUDED_PI_PACKAGES: readonly string[] = ["npm:gentle-pi"];
|
|
677
|
+
|
|
678
|
+
export function withLauncherPackagesExcluded(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
679
|
+
const own = (env[GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV] ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
680
|
+
const merged = [...own, ...LAUNCHER_EXCLUDED_PI_PACKAGES.filter((source) => !own.includes(source))];
|
|
681
|
+
return { ...env, [GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV]: merged.join(",") };
|
|
682
|
+
}
|
|
683
|
+
|
|
578
684
|
// Scans a settings.json `packages` list (same string/object-source parsing
|
|
579
685
|
// as settingsDeclareGentlePi/findGentlePiDeclaration above) for any entry
|
|
580
686
|
// whose npm package name matches POST_INSTALL_REMOVAL_PACKAGES, at any
|
|
@@ -875,10 +981,13 @@ export interface BuildPiInvocationInput {
|
|
|
875
981
|
passthrough: string[];
|
|
876
982
|
// Set when parseLauncherArgs recognised passthrough[0] as one of
|
|
877
983
|
// PI_SUBCOMMANDS. pi dispatches install/remove/uninstall/update/list/
|
|
878
|
-
// config/auth on argv[0] before its own flag parsing, so none of the
|
|
984
|
+
// config/auth/mcp on argv[0] before its own flag parsing, so none of the
|
|
879
985
|
// gentle-pi extension injection below may precede it.
|
|
880
986
|
piSubcommand?: PiSubcommand;
|
|
881
987
|
baseEnv: Record<string, string | undefined>;
|
|
988
|
+
// A bundled launch: its gentle-pi keeps its config in the prefix, so Pi gets
|
|
989
|
+
// no standalone config marker (standaloneConfigHome).
|
|
990
|
+
bundled?: boolean;
|
|
882
991
|
// The OS home behind userPiHome's conventional ~/.pi/agent fallback.
|
|
883
992
|
homedir: string;
|
|
884
993
|
// The directory pi is spawned in, which pi resolves a relative -e path
|
|
@@ -896,7 +1005,7 @@ export interface PiInvocation {
|
|
|
896
1005
|
|
|
897
1006
|
// Four cases, checked in this order — `piSubcommand` first, then `takeOver`:
|
|
898
1007
|
// - piSubcommand: pi dispatches install/remove/uninstall/update/list/
|
|
899
|
-
// config/auth on argv[0] before it even parses flags, so any injected
|
|
1008
|
+
// config/auth/mcp on argv[0] before it even parses flags, so any injected
|
|
900
1009
|
// -e flag ahead of it stops pi from
|
|
901
1010
|
// recognising its subcommand at all — this is exactly the observed
|
|
902
1011
|
// 2026-09-22 bug where `gentle-shell install npm:x` opened an
|
|
@@ -977,6 +1086,12 @@ export function buildPiInvocation(input: BuildPiInvocationInput): PiInvocation {
|
|
|
977
1086
|
GENTLE_PI_AGENT_HOME: input.home.dir,
|
|
978
1087
|
[USER_PI_HOME_ENV]: userPiHome(input.baseEnv, input.homedir),
|
|
979
1088
|
};
|
|
1089
|
+
// #2201: only the standalone isolated home gets its own config folder; any
|
|
1090
|
+
// other launch removes an inherited marker, so a nested `--link` launch from
|
|
1091
|
+
// an isolated session reads ~/.pi/gentle-ai again.
|
|
1092
|
+
const configHome = input.bundled === true ? undefined : standaloneConfigHome(input.home);
|
|
1093
|
+
if (configHome === undefined) delete env[GENTLE_SHELL_CONFIG_HOME_ENV];
|
|
1094
|
+
else env[GENTLE_SHELL_CONFIG_HOME_ENV] = configHome;
|
|
980
1095
|
if (childInjection === undefined) delete env[CHILD_PACKAGE_INJECTION_ENV];
|
|
981
1096
|
else env[CHILD_PACKAGE_INJECTION_ENV] = encodeChildPackageInjection(childInjection);
|
|
982
1097
|
|
|
@@ -1052,7 +1167,7 @@ export function planSpawn(input: PlanSpawnInput): SpawnPlan {
|
|
|
1052
1167
|
// formatting as practical instead of imposing its own. `indent` is
|
|
1053
1168
|
// `undefined` for compact (no-whitespace) JSON, matching what
|
|
1054
1169
|
// `JSON.stringify(value)` (no third argument) produces.
|
|
1055
|
-
function detectJsonFormatting(text: string): { indent: string | undefined; trailingNewline: boolean } {
|
|
1170
|
+
export function detectJsonFormatting(text: string): { indent: string | undefined; trailingNewline: boolean } {
|
|
1056
1171
|
const match = text.match(/\{\r?\n([ \t]+)/);
|
|
1057
1172
|
return { indent: match ? match[1] : undefined, trailingNewline: text.endsWith("\n") };
|
|
1058
1173
|
}
|
|
@@ -1174,9 +1289,11 @@ export function helpText(): string {
|
|
|
1174
1289
|
return [
|
|
1175
1290
|
"Usage: gentle-shell [options] [-- pi-args...]",
|
|
1176
1291
|
" gentle-shell home [link|isolated|<path>]",
|
|
1292
|
+
" gentle-shell [home selectors] home import-pi [--dry-run]",
|
|
1177
1293
|
" gentle-shell [home selectors] setup [--dry-run]",
|
|
1178
1294
|
" gentle-shell upgrade [--channel release|main]",
|
|
1179
1295
|
" gentle-shell upgrade --rollback",
|
|
1296
|
+
" gentle-shell memory migrate --confirm-quiescent",
|
|
1180
1297
|
" gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared]",
|
|
1181
1298
|
"",
|
|
1182
1299
|
"Opens pi with the Gentle Shell package loaded, without touching your",
|
|
@@ -1184,7 +1301,8 @@ export function helpText(): string {
|
|
|
1184
1301
|
"",
|
|
1185
1302
|
"Options:",
|
|
1186
1303
|
" --link Use your existing pi agent home (never edits its settings.json).",
|
|
1187
|
-
" --isolated Use the dedicated ~/.gentle-shell/agent home (default)
|
|
1304
|
+
" --isolated Use the dedicated ~/.gentle-shell/agent home (default), with its own",
|
|
1305
|
+
" settings in ~/.gentle-shell/gentle-ai (copied once from ~/.pi/gentle-ai).",
|
|
1188
1306
|
" --home <path> Use a custom agent home directory.",
|
|
1189
1307
|
" --package-root <dir> Force this directory as the gentle-pi package to load, taking over",
|
|
1190
1308
|
" from any conflicting package the target settings.json already declares.",
|
|
@@ -1193,6 +1311,11 @@ export function helpText(): string {
|
|
|
1193
1311
|
"",
|
|
1194
1312
|
"Commands:",
|
|
1195
1313
|
" home Print or persist the effective home mode (link, isolated, or a path).",
|
|
1314
|
+
" home import-pi Copy your pi sign-ins (auth.json) and MCP servers (mcp.json) into the",
|
|
1315
|
+
" resolved separate home, plus your default provider, model and thinking",
|
|
1316
|
+
" level when it has none. Never overwrites a file or setting, never copies",
|
|
1317
|
+
" packages, extensions or caches, and never changes your pi home.",
|
|
1318
|
+
" --dry-run only shows the plan. Refuses --link (nothing to import).",
|
|
1196
1319
|
" setup Provision the resolved home with the gentle-ai companion packages",
|
|
1197
1320
|
" (runs the package-local gentle-ai 'install --agent pi --scope global').",
|
|
1198
1321
|
" Accepts --dry-run, forwarded to gentle-ai. Accepts a home selector",
|
|
@@ -1204,9 +1327,15 @@ export function helpText(): string {
|
|
|
1204
1327
|
" installer's own Node, pnpm and Pi) follows the release channel only:",
|
|
1205
1328
|
" it installs the new version beside the current one, verifies it, then",
|
|
1206
1329
|
" switches; --rollback switches back to the previous kept version.",
|
|
1330
|
+
" memory migrate Explicit bundled-store recovery using the verified core's migrate-copy.",
|
|
1331
|
+
" Requires --confirm-quiescent: stop ALL Engram clients first. Keeps the",
|
|
1332
|
+
" original directory as a backup; never runs automatically on upgrade.",
|
|
1333
|
+
" Requires a bundled core release supporting migrate-copy; older cores",
|
|
1334
|
+
" fail closed without changing the source or its provenance.",
|
|
1207
1335
|
" self-uninstall Remove Gentle Shell: its own data, the isolated home and the gentle-pi",
|
|
1208
|
-
" package
|
|
1209
|
-
" the question). The shared
|
|
1336
|
+
" package, with its settings in ~/.gentle-shell/gentle-ai. Shows the plan",
|
|
1337
|
+
" first and asks before removing (--yes skips the question). The shared",
|
|
1338
|
+
" ~/.pi/gentle-ai configuration is kept unless",
|
|
1210
1339
|
" you accept or pass --include-shared; --dry-run only shows the plan.",
|
|
1211
1340
|
" Never touches your pi home or custom --home homes.",
|
|
1212
1341
|
"",
|
|
@@ -1219,6 +1348,8 @@ export function helpText(): string {
|
|
|
1219
1348
|
" runs 'gentle-shell upgrade' instead of updating Pi.",
|
|
1220
1349
|
" gentle-shell config Run pi's own 'config' against the resolved home.",
|
|
1221
1350
|
" gentle-shell auth <command> Run pi's own 'auth' against the resolved home.",
|
|
1351
|
+
" gentle-shell mcp <command> Run pi's own 'mcp' (add, list, login, logout) against the",
|
|
1352
|
+
" resolved home, e.g. gentle-shell mcp add <name> -- <command>.",
|
|
1222
1353
|
" 'gentle-shell uninstall <source>' is pi's own alias for 'remove'; to remove Gentle",
|
|
1223
1354
|
" Shell itself, use 'gentle-shell self-uninstall'.",
|
|
1224
1355
|
" These run pi's own commands, forwarded verbatim, against the --isolated home",
|
|
@@ -1233,7 +1364,10 @@ export function helpText(): string {
|
|
|
1233
1364
|
"",
|
|
1234
1365
|
"Environment variables:",
|
|
1235
1366
|
" GENTLE_SHELL_PI Path to the pi executable to run.",
|
|
1236
|
-
" GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent)
|
|
1367
|
+
" GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent);",
|
|
1368
|
+
" inside a .gentle-shell folder, its settings live in the gentle-ai",
|
|
1369
|
+
" folder next to it; elsewhere they stay in ~/.pi/gentle-ai.",
|
|
1370
|
+
" GENTLE_PI_CONFIG_HOME Gentle Shell settings folder for every home, overriding the above.",
|
|
1237
1371
|
" PI_CODING_AGENT_DIR Directory for the --link home, shared with pi itself.",
|
|
1238
1372
|
"",
|
|
1239
1373
|
"Every other argument is forwarded to pi unchanged.",
|
|
@@ -263,6 +263,14 @@ async function planSelfUninstall(input: SelfUninstallInput, configHome: string,
|
|
|
263
263
|
{ path: gentleAiState, label: "Gentle AI's state", inside: true },
|
|
264
264
|
];
|
|
265
265
|
const defaultConfigHome = canonical(join(homedir, ".pi", "gentle-ai"));
|
|
266
|
+
// #2201: the standalone launcher keeps the isolated home's config in a
|
|
267
|
+
// gentle-ai folder next to it (standaloneConfigHome). Next to the default
|
|
268
|
+
// home it lies inside ~/.gentle-shell and is Gentle Shell's by location;
|
|
269
|
+
// next to a GENTLE_SHELL_HOME it may be anyone's, so it is only listed.
|
|
270
|
+
// When GENTLE_PI_CONFIG_HOME names it, the config home's rules apply instead.
|
|
271
|
+
const defaultHome = resolve(isolated) === resolve(join(homedir, ".gentle-shell", "agent"));
|
|
272
|
+
const ownConfig = join(dirname(isolated), "gentle-ai");
|
|
273
|
+
const ownConfigSeparate = comparable(canonical(ownConfig), platform) !== comparable(canonical(configHome), platform);
|
|
266
274
|
const candidates = [
|
|
267
275
|
{
|
|
268
276
|
who: env.GENTLE_SHELL_HOME ? `GENTLE_SHELL_HOME points at ${isolated}` : `The isolated home ${isolated}`,
|
|
@@ -278,6 +286,12 @@ async function planSelfUninstall(input: SelfUninstallInput, configHome: string,
|
|
|
278
286
|
allowedInsidePi: defaultConfigHome,
|
|
279
287
|
},
|
|
280
288
|
{ who: `The launcher folder ${shellDirectory}`, path: shellDirectory, guards, allowedInsidePi: undefined },
|
|
289
|
+
...(defaultHome && ownConfigSeparate ? [{
|
|
290
|
+
who: `Gentle Shell's config folder ${ownConfig}`,
|
|
291
|
+
path: ownConfig,
|
|
292
|
+
guards: [...guards, { path: configHome, label: "the Gentle AI config home", inside: true }],
|
|
293
|
+
allowedInsidePi: undefined,
|
|
294
|
+
}] : []),
|
|
281
295
|
];
|
|
282
296
|
for (const candidate of candidates) {
|
|
283
297
|
const real = canonical(candidate.path);
|
|
@@ -312,11 +326,14 @@ async function planSelfUninstall(input: SelfUninstallInput, configHome: string,
|
|
|
312
326
|
|
|
313
327
|
// The default isolated home is Gentle Shell's by location; a GENTLE_SHELL_HOME
|
|
314
328
|
// directory only with the launcher's ownership marker or setup record.
|
|
315
|
-
const defaultHome = resolve(isolated) === resolve(join(homedir, ".gentle-shell", "agent"));
|
|
316
329
|
const owned = defaultHome || inspect(join(isolated, HOME_OWNERSHIP_MARKER), "file") === "ok"
|
|
317
330
|
|| provisionedEntry(launcherConfig, isolatedReal) !== undefined;
|
|
318
331
|
if (owned || inspect(isolated, "dir") !== "ok") consider({ path: isolated, label: "isolated home: its sign-ins, chats and settings", kind: "dir" });
|
|
319
332
|
else plan.kept.push(`${isolated}: not created by Gentle Shell (no ${HOME_OWNERSHIP_MARKER} marker or setup record)`);
|
|
333
|
+
if (defaultHome && ownConfigSeparate) consider({ path: ownConfig, label: "Gentle Shell's own configuration: profiles, banner and other preferences", kind: "dir" });
|
|
334
|
+
else if (ownConfigSeparate && inspect(ownConfig, "dir") !== "missing") {
|
|
335
|
+
plan.kept.push(`${ownConfig}: next to GENTLE_SHELL_HOME, so it may not be Gentle Shell's; remove it yourself if it holds only Gentle Shell settings`);
|
|
336
|
+
}
|
|
320
337
|
consider({ path: configPath, label: "launcher settings", kind: "file" });
|
|
321
338
|
|
|
322
339
|
for (const name of SHARED_CONFIG_FILES) {
|
package/lib/herdr-activity.ts
CHANGED
|
@@ -111,6 +111,30 @@ export function activitySummary(tasks: unknown, columns = FALLBACK_COLUMNS): str
|
|
|
111
111
|
return `${first}\n ${second}${overflow ? "…" : ""}`;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Herdr pane environment: HERDR_ENV=1 plus nonblank pane and socket variables. */
|
|
115
|
+
export function herdrDetected(env: NodeJS.ProcessEnv): boolean {
|
|
116
|
+
return env.HERDR_ENV === "1" && !!env.HERDR_PANE_ID?.trim() && !!env.HERDR_SOCKET_PATH?.trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Read-only /gentle:doctor guidance; empty outside Herdr. Never touches Herdr config. */
|
|
120
|
+
export function herdrSetupHint(
|
|
121
|
+
env: NodeJS.ProcessEnv,
|
|
122
|
+
session: { interactive: boolean; isSocket: (path: string) => boolean },
|
|
123
|
+
): string[] {
|
|
124
|
+
if (!herdrDetected(env)) return [];
|
|
125
|
+
const inactive = env.GENTLE_PI_AGENTS_CHILD === "1" ? "Gentle Agents child session"
|
|
126
|
+
: !session.interactive ? "requires the interactive Pi TUI"
|
|
127
|
+
: !session.isSocket(env.HERDR_SOCKET_PATH!) ? "HERDR_SOCKET_PATH is not a Unix socket" : undefined;
|
|
128
|
+
return [
|
|
129
|
+
"Herdr",
|
|
130
|
+
inactive ? `warn: Herdr task-summary publishing inactive: ${inactive}` : "pass: Herdr task-summary publishing active",
|
|
131
|
+
"info: Herdr's default sidebar does not show task titles; add these two rows to the Agents sidebar rows in your Herdr config: ['$summary'], ['$summary2']",
|
|
132
|
+
"info: Sidebar rows belong to the config of the Herdr client that displays the session, including when Pi runs over SSH",
|
|
133
|
+
"info: After editing, reload or restart Herdr; Gentle Shell never changes Herdr config",
|
|
134
|
+
"info: Details: https://github.com/Gentleman-Programming/gentle-shell/blob/main/docs/readme-reference.md#herdr-active-work-summary",
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
export function metadataArgs(pane: string, summary: string | null, seq: number): string[] {
|
|
115
139
|
const rows = summary?.split("\n") ?? [];
|
|
116
140
|
const tokens = ["summary", "summary2"].flatMap((token, index) => rows[index]
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -119,7 +119,8 @@ export interface NativeReviewCli {
|
|
|
119
119
|
// Read-only risk assessment (gentle-ai#4295, landing in parallel with
|
|
120
120
|
// gentle-pi#662). Same plain-versioned shape as reviewMode/reviewStatus:
|
|
121
121
|
// an older binary without the verb, or any other process/decode failure,
|
|
122
|
-
// rejects the returned promise -- callers
|
|
122
|
+
// rejects the returned promise -- callers report the tier as `unassessable`, which informs
|
|
123
|
+
// the verification plan but never adds an independent verifier by itself.
|
|
123
124
|
assess?(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1>;
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -1082,6 +1083,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1082
1083
|
// b04b6b75d1a446c942a816fbf92932d16f8c3d3001f93159dee2a92bbf2ebe44.
|
|
1083
1084
|
// No new negotiated capability identity is asserted.
|
|
1084
1085
|
"4.1.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1086
|
+
// v4.2.0 repeats 4.1.1: the published binary still advertises
|
|
1087
|
+
// capabilities/v2.6 under review-integration/v2. The provider contract moved
|
|
1088
|
+
// from 1.3.0 to 1.4.0 (routing/pi.md, gentle-ai#5536), with published
|
|
1089
|
+
// tarball SHA-256
|
|
1090
|
+
// 9d2927a84ba9a596f69029e3f854b16f86ca4c8b32cc5d63683764ccf2fb394b.
|
|
1091
|
+
// No new negotiated capability identity is asserted.
|
|
1092
|
+
"4.2.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1085
1093
|
});
|
|
1086
1094
|
|
|
1087
1095
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { readCatalog, type CatalogPage } from "./orchestrator-catalog.ts";
|
|
2
2
|
import type { PublishedWork } from "./orchestrator-work.ts";
|
|
3
3
|
import type { PresenceRecord } from "./agents-session-transport.ts";
|
|
4
|
-
import { activationHash, listPresence, readDiscovery, sessionHash, type DiscoveryMetadata } from "./orchestrator-presence.ts";
|
|
4
|
+
import { activationHash, discoveryAbsent, listPresence, readDiscovery, sessionHash, type DiscoveryMetadata } from "./orchestrator-presence.ts";
|
|
5
5
|
|
|
6
6
|
export interface OrchestratorCandidate {
|
|
7
7
|
sessionId: string;
|
|
8
8
|
reachability: "unknown";
|
|
9
9
|
freshness: "unknown" | "recent" | "stale";
|
|
10
10
|
label?: string;
|
|
11
|
+
/** Display-only label of a live peer whose code predates discovery publication:
|
|
12
|
+
* its schema-1 header heartbeats, but it never writes a discovery sidecar. */
|
|
13
|
+
legacyLabel?: string;
|
|
11
14
|
aliases?: DiscoveryMetadata["aliases"];
|
|
12
15
|
workspace?: string;
|
|
13
16
|
tasks?: DiscoveryMetadata["tasks"];
|
|
@@ -36,6 +39,7 @@ export function discoverOrchestrators(profile: string, peers: readonly PresenceR
|
|
|
36
39
|
if (matches.length !== 1) return unknown;
|
|
37
40
|
const header = matches[0];
|
|
38
41
|
const metadata = readDiscovery(profile, header);
|
|
42
|
+
if (!metadata && header.recent && discoveryAbsent(profile, header)) return { ...unknown, legacyLabel: header.label };
|
|
39
43
|
if (metadata?.activation !== activationHash(activations[0])) return unknown;
|
|
40
44
|
if (!header.recent) return { ...unknown, freshness: "stale" };
|
|
41
45
|
const catalog = readCatalog(profile, header, metadata.activation, selection?.recipientSessionId === sessionId ? selection.cursor : undefined);
|