gentle-pi 3.5.1 → 3.7.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 -1
- package/assets/orchestrator-delegation.md +2 -0
- package/bin/gentle-shell.mjs +900 -23
- package/docs/gentle-shell.md +5 -4
- package/docs/readme-reference.md +53 -39
- package/extensions/gentle-agents.ts +89 -22
- package/extensions/gentle-ai.ts +27 -48
- package/lib/agents-runner.ts +9 -0
- package/lib/foreign-target-grants.ts +32 -0
- package/lib/gentle-shell-launcher.ts +314 -2
- package/lib/inprocess-reviewer.ts +54 -6
- package/lib/native-review-cli.ts +16 -0
- package/lib/session-change-capture.ts +10 -1
- package/package.json +1 -1
- package/runtime/gentle-shell-launcher.mjs +313 -1
- package/runtime/native-review-cli.mjs +16 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-tui-mode-setting.mjs +21 -2
- package/scripts/verify-package-files.mjs +2 -2
- package/tests/agents-runner.test.ts +22 -0
- package/tests/foreign-target-grants.test.ts +58 -0
- package/tests/gentle-agents.test.ts +362 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +54 -49
- package/tests/gentle-ai.test.ts +147 -2
- package/tests/gentle-shell-bin.test.ts +1739 -4
- package/tests/gentle-shell-launcher.test.ts +394 -0
- package/tests/inprocess-reviewer.test.ts +179 -0
- package/tests/install-tui-mode-setting.test.ts +22 -4
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/odd-runtime-delegation-gate.test.ts +18 -197
- package/tests/package-manifest.test.ts +6 -6
- package/tests/runtime-harness.mjs +1 -2
- package/tests/session-change-capture.test.ts +12 -1
- package/lib/odd-runtime-delegation-gate.ts +0 -88
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
// the caller's "provider/id" selection through pi's live model registry,
|
|
10
10
|
// authenticates through the registry's own resolver, and completes exactly
|
|
11
11
|
// one frozen prompt as a single user message — no systemPrompt, no tools, no
|
|
12
|
-
// session,
|
|
12
|
+
// session, and none of pi's tool/skill/prompt extension hooks. Extension
|
|
13
|
+
// *providers* are the opposite: they are explicitly in scope. Reading the
|
|
14
|
+
// earlier "no extension hooks" wording as "no extension providers" is what
|
|
15
|
+
// produced gentle-shell#1304, where every extension-registered model was
|
|
16
|
+
// unreachable, so the distinction is stated here rather than implied.
|
|
13
17
|
//
|
|
14
18
|
// Every I/O seam is injected (`registry`, `complete`, `now`), so this module
|
|
15
19
|
// runs under tests with no network and no pi process. gentle-pi#311 P2 wires
|
|
@@ -22,18 +26,32 @@ import { SAFE_MODEL_ID_PATTERN } from "./model-routing-authority.ts";
|
|
|
22
26
|
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
24
28
|
// Registry seam — a structural subset of pi's live ModelRegistry
|
|
25
|
-
// (@earendil-works/pi-coding-agent core/model-registry.ts).
|
|
26
|
-
// `getApiKeyAndHeaders` are
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
+
// (@earendil-works/pi-coding-agent core/model-registry.ts). `find` and
|
|
30
|
+
// `getApiKeyAndHeaders` are required. `getProvider` is optional and selects
|
|
31
|
+
// the dispatch path: when present, the completion goes through the composed
|
|
32
|
+
// provider it returns (which is what reaches extension-registered providers);
|
|
33
|
+
// when absent, it falls back to `deps.complete`, so a test double with no
|
|
34
|
+
// composition layer still satisfies the seam. The real registry's resolved
|
|
35
|
+
// auth carries extra optional fields (`baseUrl`, `env`) that this narrower
|
|
36
|
+
// shape simply ignores.
|
|
29
37
|
// ---------------------------------------------------------------------------
|
|
30
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The single method this module needs from a resolved provider. pi's
|
|
41
|
+
* `Provider.streamSimple` returns an `AssistantMessageEventStream`; only its
|
|
42
|
+
* `result()` is consumed here, so the seam asks for nothing more.
|
|
43
|
+
*/
|
|
44
|
+
export interface InProcessReviewerProvider {
|
|
45
|
+
streamSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): { result(): Promise<AssistantMessage> };
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
export interface InProcessReviewerRegistry {
|
|
32
49
|
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
33
50
|
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
34
51
|
| { readonly ok: true; readonly apiKey?: string; readonly headers?: ProviderHeaders }
|
|
35
52
|
| { readonly ok: false; readonly error: string }
|
|
36
53
|
>;
|
|
54
|
+
getProvider?(provider: string): InProcessReviewerProvider | undefined;
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
export const INPROCESS_REVIEWER_FAILURE = {
|
|
@@ -180,6 +198,33 @@ export async function runInProcessReviewer(request: InProcessReviewerRequest, de
|
|
|
180
198
|
);
|
|
181
199
|
}
|
|
182
200
|
|
|
201
|
+
// pi's composed provider is the only layer that honors extension-registered
|
|
202
|
+
// providers (core/provider-composer.ts: `extension.streamSimple` when
|
|
203
|
+
// `model.api === extension.api`). pi-ai's `completeSimple` resolves against
|
|
204
|
+
// its own builtin-only registry and throws for any extension api, so a seam
|
|
205
|
+
// that carries `getProvider` is always preferred; a seam without it at all
|
|
206
|
+
// (a test double with no composition layer) still uses `deps.complete`.
|
|
207
|
+
// A registry that has `getProvider` yet owns no provider for a model its
|
|
208
|
+
// own `find` just resolved is incoherent: refuse, never route past it.
|
|
209
|
+
//
|
|
210
|
+
// The lookup deliberately uses `parsed.provider` — the key `find` was
|
|
211
|
+
// called with — and not `model.provider`, which is a field of the object
|
|
212
|
+
// `find` returned. In pi's registry both reads hit one provider map
|
|
213
|
+
// (core/model-runtime.ts `getProvider`/`getModel` -> pi-ai models.ts, where
|
|
214
|
+
// `getModels(provider)` returns [] for an unknown id), so a resolved model
|
|
215
|
+
// always has a provider under its own key and this branch is unreachable by
|
|
216
|
+
// construction. Keying on the returned field instead would make that
|
|
217
|
+
// guarantee depend on every provider's `getModels()` echoing its own id,
|
|
218
|
+
// which a native or OAuth-modified extension provider is free not to do.
|
|
219
|
+
const provider = deps.registry.getProvider?.(parsed.provider);
|
|
220
|
+
if (deps.registry.getProvider !== undefined && provider === undefined) {
|
|
221
|
+
return refuse(
|
|
222
|
+
INPROCESS_REVIEWER_FAILURE.MODEL_NOT_FOUND,
|
|
223
|
+
`The model registry resolved ${JSON.stringify(request.selection)} for ${request.routingKey} but owns no provider for ${JSON.stringify(parsed.provider)}; reassign ${request.routingKey} to a model whose provider the interactive pi can actually dispatch.`,
|
|
224
|
+
{ provider: parsed.provider, api: model.api },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
183
228
|
const auth = await deps.registry.getApiKeyAndHeaders(model);
|
|
184
229
|
// Negation narrowing (`!auth.ok`) does not eliminate the `ok: true` arm of
|
|
185
230
|
// this discriminated union under this project's `strict: false` tsconfig;
|
|
@@ -245,9 +290,12 @@ export async function runInProcessReviewer(request: InProcessReviewerRequest, de
|
|
|
245
290
|
return undefined;
|
|
246
291
|
};
|
|
247
292
|
|
|
293
|
+
// `SimpleStreamOptions` is identical on both paths; only the return shape
|
|
294
|
+
// differs (an event stream versus a promise), hence `.result()` — which is
|
|
295
|
+
// exactly what pi-ai's own compat layer does with the same stream.
|
|
248
296
|
let assistant: AssistantMessage;
|
|
249
297
|
try {
|
|
250
|
-
assistant = await deps.complete(model, context, options);
|
|
298
|
+
assistant = provider === undefined ? await deps.complete(model, context, options) : await provider.streamSimple(model, context, options).result();
|
|
251
299
|
} catch (error) {
|
|
252
300
|
return abortRefusal() ?? refuse(INPROCESS_REVIEWER_FAILURE.PROVIDER_FAILED, `Reviewer completion failed for ${request.routingKey}: ${sanitizeErrorExcerpt(error)}`);
|
|
253
301
|
}
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -1032,6 +1032,22 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1032
1032
|
// closed START/STATUS fields this row negotiates. riskEvidence and hint
|
|
1033
1033
|
// remain dark; neither is proven to reach Pi's negotiated START path.
|
|
1034
1034
|
"3.5.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 }),
|
|
1035
|
+
// v3.6.0 repeats 3.5.0: the published provider contract bundle is
|
|
1036
|
+
// byte-identical at 1.2.0, both binaries advertise capabilities/v2.6
|
|
1037
|
+
// with only build-identity differences, and no review-integration schema
|
|
1038
|
+
// changed between the v3.5.0 and v3.6.0 tags. riskEvidence and hint
|
|
1039
|
+
// remain dark; neither is proven to reach Pi's negotiated START path.
|
|
1040
|
+
"3.6.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 }),
|
|
1041
|
+
// v3.6.1 repeats 3.6.0: published provider-contract archives are byte-identical
|
|
1042
|
+
// (SHA-256 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c).
|
|
1043
|
+
// The published v3.6.1 binary advertises capabilities/v2.6, and the tag diff
|
|
1044
|
+
// changes no review-integration schema or capability source. riskEvidence and
|
|
1045
|
+
// hint remain dark because neither is proven in Pi's negotiated START path.
|
|
1046
|
+
"3.6.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 }),
|
|
1047
|
+
// v3.7.0 repeats 3.6.1: the published provider-contract tar remains SHA-256
|
|
1048
|
+
// 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c
|
|
1049
|
+
// at contract 1.2.0. No new negotiated capability is asserted.
|
|
1050
|
+
"3.7.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 }),
|
|
1035
1051
|
});
|
|
1036
1052
|
|
|
1037
1053
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -7,6 +7,12 @@ import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktre
|
|
|
7
7
|
import { SessionChanges, readChangeSnapshot, sameSnapshot, textSnapshot, isSessionChangeEvidence,
|
|
8
8
|
SESSION_CHANGE_ENTRY, SESSION_CHANGE_EVENT, SESSION_CHANGE_RELAY, type SessionChangeEvidence, type ChangeSnapshot } from "./session-changes.ts";
|
|
9
9
|
|
|
10
|
+
const foreignPublishers = new WeakMap<ExtensionAPI, (sessionId: string, evidence: SessionChangeEvidence) => void>();
|
|
11
|
+
/** Internal authenticated handoff from a successful child tool callback, never a model message or public event. */
|
|
12
|
+
export function publishForeignSessionChange(pi: ExtensionAPI, sessionId: string, evidence: SessionChangeEvidence): void {
|
|
13
|
+
foreignPublishers.get(pi)?.(sessionId, evidence);
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
interface Pending { sessionId: string; inputPath: string; path: string; root: string; relativePath: string; before: ChangeSnapshot; toolName: string; evidence?: SessionChangeEvidence }
|
|
11
17
|
const normalized = (text: string) => text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
12
18
|
const unknown = (): ChangeSnapshot => ({ kind: "unavailable", reason: "Tool snapshots could not be verified; diff unavailable." });
|
|
@@ -23,6 +29,9 @@ export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.Proces
|
|
|
23
29
|
const notice = store.takeNotice();
|
|
24
30
|
pi.events.emit(SESSION_CHANGE_EVENT, { sessionId: store.sessionId, ...(notice ? { notice } : {}) });
|
|
25
31
|
};
|
|
32
|
+
foreignPublishers.set(pi, (sessionId, evidence) => {
|
|
33
|
+
if (!child && current?.sessionManager.getSessionId() === sessionId && isSessionChangeEvidence(evidence)) publish(evidence);
|
|
34
|
+
});
|
|
26
35
|
pi.on("session_start", (_event, ctx) => {
|
|
27
36
|
pending.clear(); current = ctx;
|
|
28
37
|
store = new SessionChanges(ctx.sessionManager.getSessionId(), ctx.sessionManager.getEntries());
|
|
@@ -84,5 +93,5 @@ export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.Proces
|
|
|
84
93
|
try { publish(item.evidence); } catch { /* Preserve the tool's outcome. */ }
|
|
85
94
|
}
|
|
86
95
|
});
|
|
87
|
-
pi.on("session_shutdown", () => { pending.clear(); current = undefined; store = undefined; off(); });
|
|
96
|
+
pi.on("session_shutdown", () => { pending.clear(); current = undefined; store = undefined; foreignPublishers.delete(pi); off(); });
|
|
88
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -63,6 +63,8 @@ export function parseLauncherArgs(argv ) {
|
|
|
63
63
|
let help = false;
|
|
64
64
|
let version = false;
|
|
65
65
|
let error ;
|
|
66
|
+
let command ;
|
|
67
|
+
let commandArgs = [];
|
|
66
68
|
let piSubcommand ;
|
|
67
69
|
const passthrough = [];
|
|
68
70
|
|
|
@@ -132,6 +134,18 @@ export function parseLauncherArgs(argv ) {
|
|
|
132
134
|
i += 1;
|
|
133
135
|
continue;
|
|
134
136
|
}
|
|
137
|
+
// Unlike `home`, `setup` is not restricted to argv[0]: it accepts the
|
|
138
|
+
// home selectors (--link, --isolated, --home <dir>) ahead of it, same
|
|
139
|
+
// as a pi subcommand would, so it provisions whichever home those
|
|
140
|
+
// selectors resolve to. It is only recognised as the FIRST non-flag
|
|
141
|
+
// token — once a pi subcommand (or any other passthrough token) has
|
|
142
|
+
// already started, a later "setup" is just an ordinary passthrough
|
|
143
|
+
// argument, same as "home" is.
|
|
144
|
+
if (arg === "setup" && command === undefined && passthrough.length === 0) {
|
|
145
|
+
command = "setup";
|
|
146
|
+
commandArgs = argv.slice(i + 1);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
135
149
|
if (passthrough.length === 0 && isPiSubcommand(arg)) {
|
|
136
150
|
piSubcommand = arg;
|
|
137
151
|
}
|
|
@@ -148,7 +162,7 @@ export function parseLauncherArgs(argv ) {
|
|
|
148
162
|
}
|
|
149
163
|
}
|
|
150
164
|
|
|
151
|
-
return { link, isolated, home, packageRoot, help, version, command
|
|
165
|
+
return { link, isolated, home, packageRoot, help, version, command, commandArgs, passthrough, piSubcommand, error };
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
// --- home resolution -------------------------------------------------------
|
|
@@ -201,6 +215,20 @@ export function resolveHome(input ) {
|
|
|
201
215
|
return { mode: "isolated", dir: isolatedDir(env, homedir), source: "default" };
|
|
202
216
|
}
|
|
203
217
|
|
|
218
|
+
// The flags that reproduce `home`'s resolved mode on a later `gentle-shell
|
|
219
|
+
// <flags> ...` invocation — used by remediation messages (e.g. "run
|
|
220
|
+
// `gentle-shell <flags> remove <source>`") so they point at the exact home
|
|
221
|
+
// setup provisioned instead of silently defaulting to the isolated home.
|
|
222
|
+
// Mirrors the three ResolvedHome modes one-to-one: "link" needs --link
|
|
223
|
+
// (PI_CODING_AGENT_DIR-derived dirs aren't reproducible as a literal path),
|
|
224
|
+
// "path" needs its --home <dir>, and "isolated" needs nothing since it's
|
|
225
|
+
// gentle-shell's own default when no selector is given.
|
|
226
|
+
export function homeSelectorFlags(home ) {
|
|
227
|
+
if (home.mode === "link") return ["--link"];
|
|
228
|
+
if (home.mode === "path") return ["--home", home.dir];
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
204
232
|
export function launcherConfigPath(homedir ) {
|
|
205
233
|
return join(homedir, ".gentle-shell", "config.json");
|
|
206
234
|
}
|
|
@@ -229,6 +257,88 @@ export function parseLauncherConfig(text ) {
|
|
|
229
257
|
return { mode: "path", dir: home };
|
|
230
258
|
}
|
|
231
259
|
|
|
260
|
+
// --- provisioning marker (S7 auto-provision) --------------------------------
|
|
261
|
+
|
|
262
|
+
// Raw config.json shape as actually stored on disk: a plain object that may
|
|
263
|
+
// carry `home` (see LauncherConfig above), `provisioned`, and any other key
|
|
264
|
+
// a future feature adds. Unlike parseLauncherConfig's discriminated
|
|
265
|
+
// LauncherConfig, these helpers operate on (and return) the whole object so
|
|
266
|
+
// a write never drops a field it does not itself understand — notably
|
|
267
|
+
// another home's provisioned marker when `gentle-shell home ...` persists a
|
|
268
|
+
// mode change.
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
// Tolerant like parseLauncherConfig: a missing, malformed, or foreign
|
|
272
|
+
// config.json resolves to an empty object rather than throwing, so a caller
|
|
273
|
+
// can always merge into (and write back) whatever it finds.
|
|
274
|
+
export function parseRawLauncherConfig(text ) {
|
|
275
|
+
if (text === undefined) return {};
|
|
276
|
+
let parsed ;
|
|
277
|
+
try {
|
|
278
|
+
parsed = JSON.parse(text);
|
|
279
|
+
} catch {
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
283
|
+
return parsed ;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
function isProvisionedEntry(value ) {
|
|
296
|
+
if (typeof value !== "object" || value === null) return false;
|
|
297
|
+
const record = value ;
|
|
298
|
+
if (typeof record.gentleAi !== "string" || typeof record.at !== "string") return false;
|
|
299
|
+
return record.gentlePi === undefined || typeof record.gentlePi === "string";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Tolerant read of config.provisioned: a missing, non-object, or malformed
|
|
303
|
+
// map (or a malformed individual entry) is dropped rather than thrown, same
|
|
304
|
+
// tolerance policy as parseLauncherConfig above.
|
|
305
|
+
function provisionedMap(config ) {
|
|
306
|
+
const value = config.provisioned;
|
|
307
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
308
|
+
const map = {};
|
|
309
|
+
for (const [key, entry] of Object.entries(value )) {
|
|
310
|
+
if (isProvisionedEntry(entry)) map[key] = entry;
|
|
311
|
+
}
|
|
312
|
+
return map;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// The provisioning record for `homeDir` (the caller passes a realpath, so
|
|
316
|
+
// two different-looking paths to the same home never diverge), or undefined
|
|
317
|
+
// when that home has never been auto- or manually provisioned.
|
|
318
|
+
export function provisionedEntry(config , homeDir ) {
|
|
319
|
+
return provisionedMap(config)[homeDir];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// True when `homeDir` has never been provisioned, was provisioned with a
|
|
323
|
+
// gentle-ai pin other than `pin`, or was provisioned against a gentle-pi
|
|
324
|
+
// other than `gentlePiVersion` (the running launcher's own version, from its
|
|
325
|
+
// package.json) — the signal bin/gentle-shell.mjs uses to decide whether a
|
|
326
|
+
// plain launch should run the setup flow automatically before starting pi.
|
|
327
|
+
// A marker written before gentle-pi version tracking existed has no
|
|
328
|
+
// `gentlePi` field, which never strictly-equals a real version string, so it
|
|
329
|
+
// always counts as needing provisioning too — see ProvisionedEntry above.
|
|
330
|
+
export function needsProvisioning(config , homeDir , pin , gentlePiVersion ) {
|
|
331
|
+
const entry = provisionedEntry(config, homeDir);
|
|
332
|
+
return entry === undefined || entry.gentleAi !== pin || entry.gentlePi !== gentlePiVersion;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Returns a new config object recording `homeDir` as provisioned at `pin`
|
|
336
|
+
// and `gentlePiVersion`, preserving every other key — including every other
|
|
337
|
+
// home's provisioned entry — unchanged. Never mutates `config`.
|
|
338
|
+
export function recordProvisioned(config , homeDir , pin , gentlePiVersion , now ) {
|
|
339
|
+
return { ...config, provisioned: { ...provisionedMap(config), [homeDir]: { gentleAi: pin, gentlePi: gentlePiVersion, at: now } } };
|
|
340
|
+
}
|
|
341
|
+
|
|
232
342
|
// --- pi runtime resolution ---------------------------------------------------
|
|
233
343
|
|
|
234
344
|
|
|
@@ -299,6 +409,25 @@ export function checkPiVersion(output , minimum = MIN_PI_VERSION)
|
|
|
299
409
|
return { ok: true, version };
|
|
300
410
|
}
|
|
301
411
|
|
|
412
|
+
// --- setup subcommand's gentle-ai pin gate -----------------------------------
|
|
413
|
+
|
|
414
|
+
// The first gentle-ai release that honors PI_CODING_AGENT_DIR in its own
|
|
415
|
+
// `install --agent pi` provisioning. `gentle-shell setup` spawns the
|
|
416
|
+
// package-local pinned gentle-ai with PI_CODING_AGENT_DIR set to the
|
|
417
|
+
// resolved home; an older pin ignores that variable and silently provisions
|
|
418
|
+
// the caller's real ~/.pi/agent instead, so setup must refuse to run it.
|
|
419
|
+
export const MIN_SETUP_GENTLE_AI_VERSION = "3.6.0";
|
|
420
|
+
|
|
421
|
+
export function isSetupCapablePin(version , minimum = MIN_SETUP_GENTLE_AI_VERSION) {
|
|
422
|
+
const match = VERSION_PATTERN.exec(version);
|
|
423
|
+
if (!match) return false;
|
|
424
|
+
const minimumMatch = VERSION_PATTERN.exec(minimum);
|
|
425
|
+
if (!minimumMatch) throw new Error(`invalid minimum version "${minimum}"`);
|
|
426
|
+
const found = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
427
|
+
const wanted = [Number(minimumMatch[1]), Number(minimumMatch[2]), Number(minimumMatch[3])];
|
|
428
|
+
return compareVersions(found, wanted) >= 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
302
431
|
// --- packaging drift guard -----------------------------------------------------
|
|
303
432
|
|
|
304
433
|
|
|
@@ -401,6 +530,61 @@ export function settingsDeclareGentlePi(settingsText )
|
|
|
401
530
|
return packages.some(packageEntryDeclaresGentlePi);
|
|
402
531
|
}
|
|
403
532
|
|
|
533
|
+
// gentle-ai's own managed Pi stack still installs
|
|
534
|
+
// npm:@juicesharp/rpiv-ask-user-question, which conflicts with gentle-pi's
|
|
535
|
+
// first-party ask_user_question tool: Pi tool names are exclusive, so a
|
|
536
|
+
// second provider for the same name fails the whole load (see
|
|
537
|
+
// extensions/ask-user-question.ts). Tracked upstream as gentle-ai #4820 and
|
|
538
|
+
// gentle-shell #1277; the gentle-ai fix lands separately, so `gentle-shell
|
|
539
|
+
// setup` (bin/gentle-shell.mjs) must remove it from the provisioned home
|
|
540
|
+
// itself.
|
|
541
|
+
//
|
|
542
|
+
// gentle-ai's managed Pi stack also always declares npm:gentle-pi itself.
|
|
543
|
+
// That declaration must never survive setup either, for an unrelated reason:
|
|
544
|
+
// this launcher always loads its own gentle-pi (its own package root, or a
|
|
545
|
+
// take-over), never the one gentle-ai's stack installs, so leaving the
|
|
546
|
+
// declaration in place would silently let the home drift onto whatever
|
|
547
|
+
// gentle-pi npm last installed — or, for a developer running from a source
|
|
548
|
+
// checkout, onto the published npm package — instead of the running
|
|
549
|
+
// launcher's own copy. See docs/readme-reference.md's "setup" section.
|
|
550
|
+
//
|
|
551
|
+
// Table of every package `setup` removes after gentle-ai finishes, so a
|
|
552
|
+
// future addition only needs a new row here.
|
|
553
|
+
const POST_INSTALL_REMOVAL_PACKAGES = [
|
|
554
|
+
{ name: "@juicesharp/rpiv-ask-user-question", source: "npm:@juicesharp/rpiv-ask-user-question" },
|
|
555
|
+
{ name: "gentle-pi", source: "npm:gentle-pi" },
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
// The known removal sources, exposed so a `--dry-run` caller can report what
|
|
559
|
+
// setup would remove *if* gentle-ai's install declares it, without reading
|
|
560
|
+
// settings.json itself: a dry run writes nothing, so settings.json
|
|
561
|
+
// afterwards would only reflect whatever pre-existed the run, not what the
|
|
562
|
+
// (skipped) install would have declared. See runPostInstallCleanup in
|
|
563
|
+
// bin/gentle-shell.mjs.
|
|
564
|
+
export const POST_INSTALL_REMOVAL_SOURCES = POST_INSTALL_REMOVAL_PACKAGES.map((entry) => entry.source);
|
|
565
|
+
|
|
566
|
+
// Scans a settings.json `packages` list (same string/object-source parsing
|
|
567
|
+
// as settingsDeclareGentlePi/findGentlePiDeclaration above) for any entry
|
|
568
|
+
// whose npm package name matches POST_INSTALL_REMOVAL_PACKAGES, at any
|
|
569
|
+
// version spec. Returns each match's canonical unversioned source, deduped,
|
|
570
|
+
// in the order those packages first appear in `packages` — never the
|
|
571
|
+
// declared (possibly versioned) source text, since the caller always removes
|
|
572
|
+
// the bare package.
|
|
573
|
+
export function postInstallRemovals(settingsText ) {
|
|
574
|
+
const packages = parseSettingsPackages(settingsText);
|
|
575
|
+
if (packages === undefined) return [];
|
|
576
|
+
|
|
577
|
+
const found = [];
|
|
578
|
+
for (const entry of packages) {
|
|
579
|
+
const source = entrySource(entry);
|
|
580
|
+
if (source === undefined || packageSourceKind(source) !== "npm") continue;
|
|
581
|
+
const name = npmPackageName(source);
|
|
582
|
+
const match = POST_INSTALL_REMOVAL_PACKAGES.find((candidate) => candidate.name === name);
|
|
583
|
+
if (match !== undefined && !found.includes(match.source)) found.push(match.source);
|
|
584
|
+
}
|
|
585
|
+
return found;
|
|
586
|
+
}
|
|
587
|
+
|
|
404
588
|
|
|
405
589
|
|
|
406
590
|
|
|
@@ -790,6 +974,20 @@ export function quoteForCmdExe(token ) {
|
|
|
790
974
|
return `"${token.replace(/"/g, '\\"')}"`;
|
|
791
975
|
}
|
|
792
976
|
|
|
977
|
+
const POSIX_SHELL_SPECIAL_CHARS = /[\s"'`\\$&|;<>(){}*?[\]!#~]/;
|
|
978
|
+
|
|
979
|
+
// POSIX/bash single-quote shell quoting for a copy-pasteable command
|
|
980
|
+
// bin/gentle-shell.mjs prints to stderr (e.g. the setup remediation
|
|
981
|
+
// command): wraps a token in single quotes when it is empty or contains
|
|
982
|
+
// whitespace or a shell metacharacter, escaping an embedded single quote as
|
|
983
|
+
// `'\''` (close quote, escaped literal quote, reopen quote) — inside single
|
|
984
|
+
// quotes nothing else needs escaping, unlike cmd.exe's `"`-based quoting
|
|
985
|
+
// (quoteForCmdExe above).
|
|
986
|
+
export function shellQuote(value ) {
|
|
987
|
+
if (value.length > 0 && !POSIX_SHELL_SPECIAL_CHARS.test(value)) return value;
|
|
988
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
989
|
+
}
|
|
990
|
+
|
|
793
991
|
|
|
794
992
|
|
|
795
993
|
|
|
@@ -810,6 +1008,115 @@ export function planSpawn(input ) {
|
|
|
810
1008
|
return { command, args, shell: false };
|
|
811
1009
|
}
|
|
812
1010
|
|
|
1011
|
+
// --- JSON field restore --------------------------------------------------
|
|
1012
|
+
|
|
1013
|
+
// Detects the indentation unit and trailing-newline presence of a JSON text,
|
|
1014
|
+
// so restoreJsonField below can re-serialize as close to the original
|
|
1015
|
+
// formatting as practical instead of imposing its own. `indent` is
|
|
1016
|
+
// `undefined` for compact (no-whitespace) JSON, matching what
|
|
1017
|
+
// `JSON.stringify(value)` (no third argument) produces.
|
|
1018
|
+
function detectJsonFormatting(text ) {
|
|
1019
|
+
const match = text.match(/\{\r?\n([ \t]+)/);
|
|
1020
|
+
return { indent: match ? match[1] : undefined, trailingNewline: text.endsWith("\n") };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function jsonValuesEqual(a , b ) {
|
|
1024
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Pure JSON merge: restores `field` in `currentText` back to whatever it was
|
|
1028
|
+
// in `originalText`, keeping every other field exactly as `currentText` left
|
|
1029
|
+
// it, and formatting the result to match `originalText`'s indentation and
|
|
1030
|
+
// trailing newline. Used by bin/gentle-shell.mjs's setup flow to restore
|
|
1031
|
+
// `managed_asset_digest` in the user's shared `~/.gentle-ai/state.json` after
|
|
1032
|
+
// the pinned gentle-ai spawn rewrites it (the same shared-file problem
|
|
1033
|
+
// persona.json has — see sharedPersonaPath/snapshotFile/restoreFile in
|
|
1034
|
+
// bin/gentle-shell.mjs — but state.json also carries fields the pinned
|
|
1035
|
+
// gentle-ai is supposed to update, like installed_agents, so this restores
|
|
1036
|
+
// only the one field instead of the whole file).
|
|
1037
|
+
//
|
|
1038
|
+
// Returns the new text, or `undefined` when either text fails to parse as a
|
|
1039
|
+
// JSON object, or the field's presence and value are already identical on
|
|
1040
|
+
// both sides (nothing to restore). Never called by the caller when
|
|
1041
|
+
// `originalText` comes from a file that did not exist before the spawn —
|
|
1042
|
+
// there is nothing to restore a nonexistent file back to.
|
|
1043
|
+
export function restoreJsonField(originalText , currentText , field ) {
|
|
1044
|
+
let originalValue ;
|
|
1045
|
+
let currentValue ;
|
|
1046
|
+
try {
|
|
1047
|
+
originalValue = JSON.parse(originalText);
|
|
1048
|
+
currentValue = JSON.parse(currentText);
|
|
1049
|
+
} catch {
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|
|
1052
|
+
if (
|
|
1053
|
+
typeof originalValue !== "object" ||
|
|
1054
|
+
originalValue === null ||
|
|
1055
|
+
Array.isArray(originalValue) ||
|
|
1056
|
+
typeof currentValue !== "object" ||
|
|
1057
|
+
currentValue === null ||
|
|
1058
|
+
Array.isArray(currentValue)
|
|
1059
|
+
) {
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
const originalObj = originalValue ;
|
|
1063
|
+
const currentObj = currentValue ;
|
|
1064
|
+
const hadField = Object.prototype.hasOwnProperty.call(originalObj, field);
|
|
1065
|
+
const hasFieldNow = Object.prototype.hasOwnProperty.call(currentObj, field);
|
|
1066
|
+
const unchanged = hadField === hasFieldNow && (!hadField || jsonValuesEqual(originalObj[field], currentObj[field]));
|
|
1067
|
+
if (unchanged) return undefined;
|
|
1068
|
+
|
|
1069
|
+
let restored ;
|
|
1070
|
+
if (hadField) {
|
|
1071
|
+
restored = { ...currentObj, [field]: originalObj[field] };
|
|
1072
|
+
} else {
|
|
1073
|
+
restored = { ...currentObj };
|
|
1074
|
+
delete restored[field];
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const { indent, trailingNewline } = detectJsonFormatting(originalText);
|
|
1078
|
+
const serialized = JSON.stringify(restored, null, indent);
|
|
1079
|
+
return trailingNewline ? `${serialized}\n` : serialized;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Pure JSON merge: forces `field` in `currentText` to `value`, but only when
|
|
1083
|
+
// `originalText` (the state from before whatever wrote `currentText`) did not
|
|
1084
|
+
// declare that field at all — never overriding a value the original already
|
|
1085
|
+
// had, in either direction. Keeps every other field exactly as `currentText`
|
|
1086
|
+
// left it, and formats the result to match `currentText`'s own indentation
|
|
1087
|
+
// and trailing newline (unlike restoreJsonField above, which matches the
|
|
1088
|
+
// *original*'s formatting — here `currentText` is what the other writer just
|
|
1089
|
+
// produced, so its own convention is respected instead of imposed on).
|
|
1090
|
+
// Used by bin/gentle-shell.mjs's setup flow so a home gentle-shell provisions
|
|
1091
|
+
// ends up with the maintainer's default theme unless the home (or the user)
|
|
1092
|
+
// already had an opinion about it, even when gentle-ai's own managed install
|
|
1093
|
+
// writes a *different* default theme into settings.json.
|
|
1094
|
+
//
|
|
1095
|
+
// Returns the new text, or `undefined` when either text fails to parse as a
|
|
1096
|
+
// JSON object, the original text already declared `field` (nothing to
|
|
1097
|
+
// force), or the current value already equals `value` (nothing to change).
|
|
1098
|
+
export function forceJsonFieldIfAbsentInOriginal(originalText , currentText , field , value ) {
|
|
1099
|
+
let originalValue ;
|
|
1100
|
+
let currentValue ;
|
|
1101
|
+
try {
|
|
1102
|
+
originalValue = JSON.parse(originalText);
|
|
1103
|
+
currentValue = JSON.parse(currentText);
|
|
1104
|
+
} catch {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
if (typeof originalValue !== "object" || originalValue === null || Array.isArray(originalValue)) return undefined;
|
|
1108
|
+
if (typeof currentValue !== "object" || currentValue === null || Array.isArray(currentValue)) return undefined;
|
|
1109
|
+
const originalObj = originalValue ;
|
|
1110
|
+
const currentObj = currentValue ;
|
|
1111
|
+
if (Object.prototype.hasOwnProperty.call(originalObj, field)) return undefined;
|
|
1112
|
+
if (jsonValuesEqual(currentObj[field], value)) return undefined;
|
|
1113
|
+
|
|
1114
|
+
const forced = { ...currentObj, [field]: value };
|
|
1115
|
+
const { indent, trailingNewline } = detectJsonFormatting(currentText);
|
|
1116
|
+
const serialized = JSON.stringify(forced, null, indent);
|
|
1117
|
+
return trailingNewline ? `${serialized}\n` : serialized;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
813
1120
|
// --- reporting ---------------------------------------------------------------
|
|
814
1121
|
|
|
815
1122
|
|
|
@@ -830,6 +1137,7 @@ export function helpText() {
|
|
|
830
1137
|
return [
|
|
831
1138
|
"Usage: gentle-shell [options] [-- pi-args...]",
|
|
832
1139
|
" gentle-shell home [link|isolated|<path>]",
|
|
1140
|
+
" gentle-shell [home selectors] setup [--dry-run]",
|
|
833
1141
|
"",
|
|
834
1142
|
"Opens pi with the Gentle Shell package loaded, without touching your",
|
|
835
1143
|
"vanilla pi installation.",
|
|
@@ -845,6 +1153,10 @@ export function helpText() {
|
|
|
845
1153
|
"",
|
|
846
1154
|
"Commands:",
|
|
847
1155
|
" home Print or persist the effective home mode (link, isolated, or a path).",
|
|
1156
|
+
" setup Provision the resolved home with the gentle-ai companion packages",
|
|
1157
|
+
" (runs the package-local gentle-ai 'install --agent pi --scope global').",
|
|
1158
|
+
" Accepts --dry-run, forwarded to gentle-ai. Accepts a home selector",
|
|
1159
|
+
" (--link, --isolated, --home <dir>) before it.",
|
|
848
1160
|
"",
|
|
849
1161
|
"Managing packages:",
|
|
850
1162
|
" gentle-shell install npm:<pkg> Run pi's own 'install' against the resolved home.",
|
|
@@ -1033,6 +1033,22 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1033
1033
|
// closed START/STATUS fields this row negotiates. riskEvidence and hint
|
|
1034
1034
|
// remain dark; neither is proven to reach Pi's negotiated START path.
|
|
1035
1035
|
"3.5.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 }),
|
|
1036
|
+
// v3.6.0 repeats 3.5.0: the published provider contract bundle is
|
|
1037
|
+
// byte-identical at 1.2.0, both binaries advertise capabilities/v2.6
|
|
1038
|
+
// with only build-identity differences, and no review-integration schema
|
|
1039
|
+
// changed between the v3.5.0 and v3.6.0 tags. riskEvidence and hint
|
|
1040
|
+
// remain dark; neither is proven to reach Pi's negotiated START path.
|
|
1041
|
+
"3.6.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 }),
|
|
1042
|
+
// v3.6.1 repeats 3.6.0: published provider-contract archives are byte-identical
|
|
1043
|
+
// (SHA-256 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c).
|
|
1044
|
+
// The published v3.6.1 binary advertises capabilities/v2.6, and the tag diff
|
|
1045
|
+
// changes no review-integration schema or capability source. riskEvidence and
|
|
1046
|
+
// hint remain dark because neither is proven in Pi's negotiated START path.
|
|
1047
|
+
"3.6.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 }),
|
|
1048
|
+
// v3.7.0 repeats 3.6.1: the published provider-contract tar remains SHA-256
|
|
1049
|
+
// 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c
|
|
1050
|
+
// at contract 1.2.0. No new negotiated capability is asserted.
|
|
1051
|
+
"3.7.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 }),
|
|
1036
1052
|
});
|
|
1037
1053
|
|
|
1038
1054
|
|
|
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
36
36
|
// version check below) derives from this constant instead of repeating the
|
|
37
37
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
38
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
-
export const INSTALLER_VERSION = "3.
|
|
39
|
+
export const INSTALLER_VERSION = "3.7.0";
|
|
40
40
|
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
41
41
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
42
42
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
|
45
45
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
|
|
46
46
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
|
|
47
47
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.7.0`
|
|
49
49
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
-
// tag resolves to commit
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
50
|
+
// tag resolves to commit 6dee8f833aec9e46015759c5065a9035795d9af1, the published v3.7.0 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:MQbzHlLdPklUQn0rVE9Mz94UygHsN2OPe7xMfPn9aGw=";
|
|
52
52
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
53
53
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
54
54
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
67
67
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
68
68
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
69
69
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
70
|
-
// still holds it. The v3.
|
|
70
|
+
// still holds it. The v3.7.0 digests are pinned from the published release:
|
|
71
71
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
72
72
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
73
73
|
// executables.
|
|
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
112
|
-
// distributing unsigned Windows builds in c4b764d0, so v3.
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v3.7.0 publishes signed
|
|
113
113
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
114
114
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
115
115
|
// upstream ships signed Windows assets.
|
|
116
116
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
117
|
-
"darwin/amd64": asset("gentle-ai_3.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_3.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_3.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_3.
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_3.7.0_darwin_amd64.tar.gz", "e55ff3ee06258a90e9195c0e3a593b85f6d79cc439e9e6c04b2596725120a610", "aa30a940e3b75ac218249b3f92d17afd2b972f833258e8212175a73d8e76d5bf", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_3.7.0_darwin_arm64.tar.gz", "66eb5740c4506cb2cfd1cc5207439c61cfe07678854f4ac7c83027a95a0e666a", "ca726cf3f9a523dc0112aa113beb634cda389e8b47edc5851254c32979bee89e", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_3.7.0_linux_amd64.tar.gz", "a730a61a43758f04cc9a4ac644945cc0e8652a1e33d6997a0a3d3f0044d2fff5", "002d09fd2b9628a29986a660c1f51f8a5042ff7fd54c8ab15b7b27de96c6cccc", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_3.7.0_linux_arm64.tar.gz", "a3a3d3a974f3d9b67d935fe9e306ae83c305da4ec1baed4a5319c10b044cd0eb", "e29546c51d8d65528bf565239ed3fc174da73c5fa3e861e64f20f84b16f28f26", "gentle-ai"),
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|