gentle-pi 2.6.2 → 2.6.3

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.
@@ -13,7 +13,7 @@ The [v2.6.0 release](https://github.com/Gentleman-Programming/gentle-pi/releases
13
13
  - The Agents List and Details views preserve the orchestrator/session hierarchy and completion, abort, and lost-exit history. Parent-child queries and notifications have an explicit handoff path, while model, effort, and usage stay observable per task.
14
14
  - Named `/gentle:profiles` atomically route the orchestrator separately from packaged and review roles; see the [technical reference](readme-reference.md#agent-model-profiles) for the profile model.
15
15
 
16
- The source checkout currently prepares `gentle-pi` `2.6.2` with a package-local Gentle AI `v2.8.2` pin; this is not a claim that `2.6.2` is published.
16
+ The source checkout currently prepares `gentle-pi` `2.6.3` with a package-local Gentle AI `v2.9.0` pin; this is not a claim that `2.6.3` is published.
17
17
 
18
18
  ## Shell interactions and runtime behavior
19
19
 
@@ -30,7 +30,7 @@ This reference preserves the detailed installation, configuration, SDD/OpenSpec,
30
30
  | **Skill creation workflow** | Provides the `gentle-ai-skill-creator`/`gentle-ai-skill-improver` skills, `/skill-creation` prompt, and packaged style guide for LLM-first skills. |
31
31
  | **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
32
32
  | **Bounded native review** | Freezes one candidate, dispatches only controller-selected lenses, and records native authority. Review outcomes are informational; delivery follows ordinary repository policy. |
33
- | **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v2.8.2 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
33
+ | **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v2.9.0 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
34
34
  | **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
35
35
 
36
36
  ## Native pointer regions
@@ -67,7 +67,7 @@ The stable release is [`v2.6.0`](https://github.com/Gentleman-Programming/gentle
67
67
 
68
68
  ### Source checkout
69
69
 
70
- This checkout prepares `gentle-pi` `2.6.2`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v2.8.2`, distinct from the published `v2.6.0` pairing.
70
+ This checkout prepares `gentle-pi` `2.6.3`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v2.9.0`, distinct from the published `v2.6.0` pairing.
71
71
 
72
72
  ### Pi compatibility
73
73
 
@@ -94,7 +94,7 @@ pi install npm:gentle-pi@2.6.0
94
94
 
95
95
  RDD remains opt-in. Enable it only through an explicit user decision with `/gentle:review-mode enable`; `status` lets you inspect the mode without changing it.
96
96
 
97
- The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v2.8.2`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v2.8.2` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
97
+ The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v2.9.0`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v2.9.0` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
98
98
 
99
99
  Recommended companion packages:
100
100
 
@@ -250,13 +250,13 @@ flowchart TD
250
250
 
251
251
  VALIDATE is informational. Commit, push, PR, and release commands follow ordinary repository policy; RDD never authorizes, rewrites, consumes review state for, or blocks them. Dangerous-command safety and destructive-review consent remain independent.
252
252
 
253
- For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v2.8.2 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
253
+ For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v2.9.0 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
254
254
 
255
255
  Contract `/v2` replaces the Base64 `candidate_diff` reviewer transport of `/v1` with immutable `base_tree`/`candidate_tree` plus an ordered `changed_path_manifest` and never an inline patch. `gentle-pi` negotiates `/v2` only, with no dual-lane fallback; the cutover landed as one atomic commit against gentle-ai v2.2.2 (tracked by the `migrate-review-integration-v2` change), and the `/v1` schemas stay packaged because the `/v2` schemas `$ref` into their fragments. This provider contract version is unrelated to Pi's own internal "compact-v2" review-authority naming used below — the shared digit is coincidental, not a version pairing.
256
256
 
257
257
  Target status owns `current_target`, `unrelated`, `ambiguous`, and `corrupted` applicability and returns one native action. Pi does not reconstruct ordinary authority from provider-private files or choose a lineage from repository-wide history. Restart recovery rebuilds only the derived candidate view from the native Git/content projection, including intended-untracked paths, symlinks, and immutable gitlink identities. Native failure envelopes retain their exact mutation outcome, replayability, required inputs, request digest, and next action. After an unknown or lost mutating result, Pi calls target status before any replay decision and returns only the provider-declared action.
258
258
 
259
- Once the source checkout's pinned gentle-ai runtime (currently v2.8.2) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
259
+ Once the source checkout's pinned gentle-ai runtime (currently v2.9.0) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
260
260
 
261
261
  ### FINALIZE wrapper input
262
262
 
@@ -1,6 +1,6 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { extractParentConfirmedSddPreflightContext, getPackageAssetOwner, isParentConfirmedSddPreflightContext, SHIPPED_SDD_AGENT_NAMES } from "../lib/sdd-preflight.ts";
3
- import { NativeReviewCliV216, NativeReviewCliError, createNodeExecFileAdapter, decodeNativeSddStatusV2, type NativeReviewCli, type NativeSddAcquireRequest, type NativeSddSettleRequest } from "../lib/native-review-cli.ts";
3
+ import { NativeReviewCliV216, NativeReviewCliError, createNodeExecFileAdapter, decodeNativeSddStatusV2, type NativeReviewCli, type NativeSddAcquireRequest, type NativeSddAttemptResult, type NativeSddSettleRequest } from "../lib/native-review-cli.ts";
4
4
  import { spawn } from "node:child_process";
5
5
  import { recordReviewMutation } from "../lib/review-reminder-receipt.ts";
6
6
  import { SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
@@ -19,7 +19,7 @@ import { isFinished, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord }
19
19
  import { AgentRunner, piCommand, abortReasonText, plannedCommands, type RemediationPlan, type RemediationScope, REMEDIATION_PLAN_ENV, parseRemediationPlan, remediationEvidence, type AskAnswer, type RunnerDeps, type SddChangeSelection, type TaskRequest, type RemediationTerminalFacts } from "../lib/agents-runner.ts";
20
20
  import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
21
21
  import { hasReviewSessionPermission, resolveCanonicalGitRepositoryIdentitySync, type ReviewSessionManager } from "../lib/review-session-standing-permission.ts";
22
- import { historyDir, remediationUnresolved, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
22
+ import { acquireTaskLock, historyDir, remediationUnresolved, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
23
23
  import { sessionToMarkdown } from "../lib/agents-transcript.ts";
24
24
  import { AgentsView } from "../lib/agents-view.ts";
25
25
  import { PresencePublisher } from "../lib/orchestrator-presence.ts";
@@ -129,6 +129,97 @@ export function remediationToolAllowed(scope: RemediationScope | undefined, cwd:
129
129
  } catch { return false; }
130
130
  }
131
131
 
132
+ export interface RemediationReconciliationResult {
133
+ acquireState?: NativeSddAttemptResult["state"];
134
+ settlementState?: NativeSddAttemptResult["state"];
135
+ }
136
+
137
+ async function replayUncertainNativeMutation<T>(label: "acquire" | "settlement", invoke: () => Promise<T>): Promise<T> {
138
+ try { return await invoke(); }
139
+ catch (error) {
140
+ if (error instanceof TypeError || error instanceof NativeReviewCliError && error.mutationOutcome === "none") throw error;
141
+ try { return await invoke(); }
142
+ catch { throw new Error(`Native remediation ${label} remains unresolved; retry only this exact retained task`); }
143
+ }
144
+ }
145
+
146
+ function finishReconciledTask(task: TaskRecord, message: string): void {
147
+ task.status = TASK_STATUS.FAILED;
148
+ task.error = message;
149
+ task.lastStep = "reconciled";
150
+ task.endedAt ??= Date.now();
151
+ task.lastActivityAt = Date.now();
152
+ }
153
+
154
+ export async function reconcileManagedRemediation(task: TaskRecord, native: NativeReviewCli, persist: (task: TaskRecord) => Promise<void>): Promise<RemediationReconciliationResult> {
155
+ const state = task.sddRemediation;
156
+ if (task.agent !== "sdd-remediate" || !state?.acquire || state.acquire.workspaceRoot !== task.cwd) throw new Error("Task is not an exact managed remediation record");
157
+ if (state.settlement) throw new Error("Managed remediation task already has a terminal settlement");
158
+
159
+ const settleExact = async (settle: NativeSddSettleRequest, acquireState?: NativeSddAttemptResult["state"]): Promise<RemediationReconciliationResult> => {
160
+ if (!native.sddAttemptSettle) throw new Error("Native remediation settlement reconciliation is unavailable");
161
+ let settlement: NativeSddAttemptResult;
162
+ try { settlement = await replayUncertainNativeMutation("settlement", () => native.sddAttemptSettle!(structuredClone(settle))); }
163
+ catch (error) {
164
+ state.settlementUncertain = true;
165
+ await persist(task);
166
+ throw error;
167
+ }
168
+ state.settlement = structuredClone(settlement);
169
+ delete state.settlementUncertain;
170
+ finishReconciledTask(task, `Managed remediation settlement reconciled as ${settlement.state}${settlement.reason ? `(${settlement.reason})` : ""}; no actor started`);
171
+ await persist(task);
172
+ return { ...(acquireState === undefined ? {} : { acquireState }), settlementState: settlement.state };
173
+ };
174
+
175
+ if (state.settle) {
176
+ const settle = state.settle, acquire = state.acquire;
177
+ const sameUntracked = JSON.stringify({ scope: settle.untrackedScope, inventory: settle.expectedUntrackedInventory, intended: settle.intendedUntracked }) === JSON.stringify({ scope: acquire.untrackedScope, inventory: acquire.expectedUntrackedInventory, intended: acquire.intendedUntracked });
178
+ if (state.acquireResult?.state !== "proceed" || !state.token || state.acquireResult.token !== state.token || settle.token !== state.token || settle.workspaceRoot !== acquire.workspaceRoot || settle.changeName !== acquire.changeName || settle.remediatesEvidenceRevision !== acquire.remediatesEvidenceRevision || !sameUntracked) throw new Error("Retained remediation settlement does not match its exact acquired authority");
179
+ return settleExact(structuredClone(settle), state.acquireResult.state);
180
+ }
181
+ if (!state.acquireUncertain) throw new Error("Managed remediation task has no uncertain acquire or settlement to reconcile");
182
+ if (state.actorClaimed) throw new Error("Managed remediation actor effects are uncertain; exact settlement or maintainer intervention is required");
183
+ if (state.acquireResult || state.token || state.settlementUncertain) throw new Error("Managed remediation acquire history is inconsistent; maintainer intervention is required");
184
+ if (!native.sddAttemptAcquire) throw new Error("Native remediation acquire reconciliation is unavailable");
185
+
186
+ let acquired: NativeSddAttemptResult;
187
+ try { acquired = await replayUncertainNativeMutation("acquire", () => native.sddAttemptAcquire!(structuredClone(state.acquire))); }
188
+ catch (error) {
189
+ state.acquireUncertain = true;
190
+ await persist(task);
191
+ throw error;
192
+ }
193
+ if (acquired.state === "proceed" && !acquired.token) throw new Error("Native remediation acquire reconciliation returned no token");
194
+ state.acquireResult = structuredClone(acquired);
195
+ delete state.acquireUncertain;
196
+ if (acquired.state !== "proceed") {
197
+ delete state.token;
198
+ finishReconciledTask(task, `Managed remediation acquire reconciled as ${acquired.state}${acquired.reason ? `(${acquired.reason})` : ""}; no actor started`);
199
+ await persist(task);
200
+ return { acquireState: acquired.state };
201
+ }
202
+
203
+ state.token = acquired.token;
204
+ const acquire = state.acquire;
205
+ const settle: NativeSddSettleRequest = {
206
+ workspaceRoot: acquire.workspaceRoot,
207
+ changeName: acquire.changeName,
208
+ token: acquired.token!,
209
+ requestId: `reconcile-${createHash("sha256").update(JSON.stringify(acquire)).digest("hex").slice(0, 32)}`,
210
+ outcome: "interrupted",
211
+ diagnosis: "Acquire outcome was recovered after the actor launch boundary was refused; no managed actor was launched",
212
+ harnessDisposition: "invalidated",
213
+ cleanupEvidence: "No managed actor was spawned; no child cleanup was required",
214
+ processEvidence: "spawned=false; actor_claimed=false; reconciliation=acquire",
215
+ remediatesEvidenceRevision: acquire.remediatesEvidenceRevision,
216
+ ...(acquire.untrackedScope === undefined ? {} : { untrackedScope: acquire.untrackedScope, expectedUntrackedInventory: acquire.expectedUntrackedInventory, intendedUntracked: acquire.intendedUntracked }),
217
+ };
218
+ state.settle = structuredClone(settle);
219
+ await persist(task); // Recovered token and exact settlement replay input become durable atomically before native mutation.
220
+ return settleExact(settle, acquired.state);
221
+ }
222
+
132
223
  export async function admitManagedRemediation(request: TaskRequest, input: unknown, native: NativeReviewCli, persist: (task: TaskRecord) => Promise<void>, context?: Pick<ExtensionContext, "hasUI" | "ui">, preparedTask?: TaskRecord): Promise<Partial<TaskRequest>> {
133
224
  const selected = request.sddChange;
134
225
  const asset = fileURLToPath(new URL("../assets/agents/sdd-remediate.md", import.meta.url));
@@ -871,7 +962,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
871
962
  const selected = store.get(task.id);
872
963
  if (!isOwnedActive(selected)) return;
873
964
  if (selected.status === TASK_STATUS.QUEUED) {
874
- if (runner.cancel(selected.id)) ctx.ui.notify(`Stopped ${selected.agent}.`);
965
+ if (runner.cancel(selected.id, "stopped from the agents panel")) ctx.ui.notify(`Stopped ${selected.agent}.`);
875
966
  else ctx.ui.notify(`Task ${selected.agent} already finished.`, "warning");
876
967
  return;
877
968
  }
@@ -885,7 +976,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
885
976
  ctx.ui.notify(`Task ${selected.agent} already finished.`, "warning");
886
977
  return;
887
978
  }
888
- if (runner.cancel(current.id)) ctx.ui.notify(`Stopped ${current.agent}.`);
979
+ if (runner.cancel(current.id, "stopped from the agents panel")) ctx.ui.notify(`Stopped ${current.agent}.`);
889
980
  else ctx.ui.notify(`Task ${current.agent} already finished.`, "warning");
890
981
  } finally {
891
982
  stoppingTaskIds.delete(selected.id);
@@ -904,7 +995,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
904
995
  const confirmation = (async () => {
905
996
  try {
906
997
  if (!await ctx.ui.confirm(`Stop ${count} active ${noun}?`, `Only these ${count} ${noun} will stop. Current work may be incomplete.`)) return;
907
- const cancelled = active.filter((task) => runner.cancel(task.id)).length;
998
+ const cancelled = active.filter((task) => runner.cancel(task.id, "stopped from the agents panel (stop all)")).length;
908
999
  ctx.ui.notify(`Stopped ${cancelled} ${cancelled === 1 ? "subagent" : "subagents"}.`);
909
1000
  } finally {
910
1001
  stopAllConfirmation = undefined;
@@ -1136,7 +1227,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1136
1227
  // no recorded reason. Cancel through the runner so the lifecycle runs and the
1137
1228
  // record is persisted, and tell the user why.
1138
1229
  const onAbort = (): void => {
1139
- if (runner.cancel(task.id)) {
1230
+ if (runner.cancel(task.id, `cancelled: the tool call was aborted${abortReasonText(signal?.reason)}`)) {
1140
1231
  ctx.ui.notify(
1141
1232
  `Subagent ${task.agent} cancelled: the tool call was aborted${abortReasonText(signal?.reason)}. The run is recorded as cancelled.`,
1142
1233
  "warning",
@@ -1220,6 +1311,25 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1220
1311
  return task ? text(describeTask(task), taskDetails(task)) : text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
1221
1312
  });
1222
1313
 
1314
+ tool("reconcile", "Reconcile one retained managed remediation mutation without launching an actor.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params, ctx) => {
1315
+ const id = String(params.task_id);
1316
+ const lock = acquireTaskLock(tasksDir, id);
1317
+ try {
1318
+ const stored = await loadStoredTask(tasksDir, id);
1319
+ if (!stored) return text(`Error: no task ${id}`, { error: "unknown task" });
1320
+ const task = stored.task, retainedThread = stored.thread;
1321
+ const target = registryFor(ctx).validate(task.cwd);
1322
+ if (target !== resolve(task.cwd) || task.sddRemediation?.acquire.workspaceRoot !== target) throw new Error("Retained remediation task must resolve to its exact worktree in the same Git clone as this session");
1323
+ const native = deps.nativeSdd ?? new NativeReviewCliV216(createNodeExecFileAdapter());
1324
+ const persist = async (current: TaskRecord) => {
1325
+ await saveTask(tasksDir, current, retainedThread);
1326
+ store.update(current.id, { status: current.status, error: current.error, lastStep: current.lastStep, endedAt: current.endedAt, lastActivityAt: current.lastActivityAt, sddRemediation: current.sddRemediation });
1327
+ };
1328
+ const result = await reconcileManagedRemediation(task, native, persist);
1329
+ return text(`Managed remediation task ${task.id} reconciled; no actor started. Use fresh native status and admission for later work.`, { gentleAgents: { taskId: task.id, agent: task.agent, status: task.status, mode: task.mode }, reconciliation: { ...result, actorStarted: false } });
1330
+ } finally { lock.release(); }
1331
+ });
1332
+
1223
1333
  tool("result", "Return the final answer of a finished subagent task, or its current state if it is still running.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
1224
1334
  const task = await resolveTask(String(params.task_id));
1225
1335
  if (!task) return text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
@@ -1241,7 +1351,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1241
1351
 
1242
1352
  tool("cancel", "Cancel a queued or running subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
1243
1353
  const id = String(params.task_id);
1244
- return runner.cancel(id) ? text(`Cancelled task ${id}.`) : text(`Error: task ${id} is not running.`, { error: "not running" });
1354
+ return runner.cancel(id, "cancelled by the cancel tool") ? text(`Cancelled task ${id}.`) : text(`Error: task ${id} is not running.`, { error: "not running" });
1245
1355
  });
1246
1356
 
1247
1357
  tool("send_message", "Steer a running subagent with a message delivered before its next model call.", { required: ["task_id", "message"], properties: { task_id: { type: "string" }, message: { type: "string" } } }, async (params) => {
@@ -1331,6 +1441,6 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1331
1441
  sidebarTui = undefined;
1332
1442
  worktrees?.close();
1333
1443
  worktrees = undefined;
1334
- runner.cancelAll();
1444
+ runner.cancelAll("cancelled: parent session shut down");
1335
1445
  });
1336
1446
  }
@@ -1,5 +1,9 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { closeSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
1
4
  import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
5
+ import os from "node:os";
6
+ import { dirname, join } from "node:path";
3
7
  import { emptyThread, type TaskRecord, type TaskThread } from "./agents-protocol.ts";
4
8
 
5
9
  // Gentle Agents history: one JSON file per finished task, written by the
@@ -38,6 +42,83 @@ function fileFor(dir: string, id: string): string {
38
42
  return join(dir, `${id}${FILE_SUFFIX}`);
39
43
  }
40
44
 
45
+ const TASK_LOCK_SCHEMA = "gentle-pi.task-reconciliation-lock/v1" as const;
46
+ const UUID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
47
+ type TaskLockOwner = { schema: typeof TASK_LOCK_SCHEMA; taskId: string; token: string; pid: number; host: string | null };
48
+ export interface TaskLock { readonly path: string; readonly taskId: string; readonly token: string; release(): void; }
49
+ function taskLockHost(): string | null {
50
+ try {
51
+ const hostname = os.hostname().trim();
52
+ if (process.platform === "linux") { const boot = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim(), namespace = readlinkSync("/proc/self/ns/pid"); return UUID.test(boot) && /^pid:\[\d+\]$/.test(namespace) ? `linux:${hostname}:${boot}:${namespace}` : null; }
53
+ if (process.platform === "darwin") { const boot = execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], { encoding: "utf8", timeout: 1000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"] }).trim(); return UUID.test(boot) ? `darwin:${hostname}:${boot.toLowerCase()}` : null; }
54
+ return hostname ? `${process.platform}:${hostname}` : null;
55
+ } catch { return null; }
56
+ }
57
+ function errorCode(error: unknown): string | undefined {
58
+ return typeof error === "object" && error !== null && "code" in error && typeof (error as { code?: unknown }).code === "string" ? (error as { code: string }).code : undefined;
59
+ }
60
+ function lockBusy(path: string, reason: string): Error { return new Error(`Task reconciliation lock is busy or ambiguous at ${path}: ${reason}`); }
61
+ function validTaskLockOwner(value: unknown, taskId: string): value is TaskLockOwner {
62
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
63
+ const owner = value as Partial<TaskLockOwner>;
64
+ return Object.keys(value).sort().join(",") === "host,pid,schema,taskId,token" && owner.schema === TASK_LOCK_SCHEMA && owner.taskId === taskId && typeof owner.token === "string" && UUID.test(owner.token) && Number.isSafeInteger(owner.pid) && owner.pid > 0 && (owner.host === null || typeof owner.host === "string" && owner.host.length > 0 && !owner.host.includes("\0"));
65
+ }
66
+ function ownerAt(path: string, taskId: string, token: string): TaskLockOwner {
67
+ if (!path.endsWith(`${taskId}.reconcile.${token}`)) throw lockBusy(path, "candidate filename is malformed");
68
+ const stat = lstatSync(path);
69
+ if (!stat.isFile() || stat.isSymbolicLink()) throw lockBusy(path, "candidate is not an expected regular file");
70
+ try {
71
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
72
+ if (!validTaskLockOwner(parsed, taskId) || parsed.token !== token) throw new Error("filename and owner metadata disagree");
73
+ return parsed;
74
+ } catch (error) { throw lockBusy(path, `candidate owner metadata is malformed${error instanceof Error ? `: ${error.message}` : ""}`); }
75
+ }
76
+ function ownerDead(owner: TaskLockOwner): boolean {
77
+ const host = taskLockHost();
78
+ if (!owner.host || !host || owner.host !== host || owner.pid === process.pid) return false;
79
+ try { process.kill(owner.pid, 0); return false; } catch (error) { return errorCode(error) === "ESRCH"; }
80
+ }
81
+ function syncTaskLock(path: string, directory = false): void {
82
+ if (directory && process.platform === "win32") return;
83
+ const descriptor = openSync(path, directory ? "r" : "r+");
84
+ try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
85
+ }
86
+ function publishExclusive(path: string, owner: TaskLockOwner): void {
87
+ const temporary = `${path}.tmp`;
88
+ try { writeFileSync(temporary, JSON.stringify(owner), { encoding: "utf8", flag: "wx", mode: 0o600 }); syncTaskLock(temporary); linkSync(temporary, path); syncTaskLock(dirname(path), true); }
89
+ finally { try { unlinkSync(temporary); } catch {} }
90
+ }
91
+ function scanTaskCandidates(dir: string, id: string, ownPath: string): void {
92
+ const prefix = `${id}.reconcile.`;
93
+ for (const name of readdirSync(dir)) {
94
+ if (!name.startsWith(prefix) || name.endsWith(".tmp")) continue;
95
+ const path = join(dir, name), token = name.slice(prefix.length);
96
+ if (!UUID.test(token)) throw lockBusy(path, "candidate filename is malformed");
97
+ let owner: TaskLockOwner;
98
+ try { owner = ownerAt(path, id, token); } catch (error) { if (errorCode(error) === "ENOENT") throw lockBusy(path, "candidate disappeared during election"); throw error; }
99
+ if (path === ownPath) continue;
100
+ if (ownerDead(owner)) { try { unlinkSync(path); } catch (error) { if (errorCode(error) !== "ENOENT") throw error; } continue; }
101
+ throw lockBusy(path, "owner is live, foreign, or its death is inconclusive");
102
+ }
103
+ }
104
+ function releaseTaskLock(path: string, owner: TaskLockOwner): void {
105
+ let current: TaskLockOwner;
106
+ try { current = ownerAt(path, owner.taskId, owner.token); } catch (error) { if (errorCode(error) === "ENOENT") return; throw error; }
107
+ if (current.pid !== owner.pid || current.host !== owner.host) throw new Error("Task reconciliation lock owner token does not match");
108
+ unlinkSync(path); syncTaskLock(dirname(path), true);
109
+ }
110
+ export function acquireTaskLock(dir: string, id: string): TaskLock {
111
+ if (!SAFE_ID.test(id)) throw new Error(`invalid task id: ${id}`);
112
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
113
+ const owner: TaskLockOwner = { schema: TASK_LOCK_SCHEMA, taskId: id, token: randomUUID(), pid: process.pid, host: taskLockHost() };
114
+ const path = join(dir, `${id}.reconcile.${owner.token}`);
115
+ publishExclusive(path, owner);
116
+ try { scanTaskCandidates(dir, id, path); }
117
+ catch (error) { try { releaseTaskLock(path, owner); } catch {} throw error; }
118
+ let released = false;
119
+ return { path, taskId: id, token: owner.token, release() { if (!released) { releaseTaskLock(path, owner); released = true; } } };
120
+ }
121
+
41
122
  function isRecord(value: unknown): value is TaskRecord {
42
123
  const task = value as Partial<TaskRecord> | undefined;
43
124
  return typeof task?.id === "string" && typeof task.agent === "string" && typeof task.status === "string" && typeof task.createdAt === "number";
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { Duplex, Readable, Writable } from "node:stream";
3
+ import { stripVTControlCharacters } from "node:util";
3
4
  import { RESEARCH_SELECTION_ENV, RESEARCH_ARTIFACT_ENV, type ResearchArtifactIntent } from "./sdd-research-capabilities.ts";
4
5
  import { AGENT_MODE, formatModelRef, type AgentDefinition, type AgentMode, type ModelRef } from "./agents-config.ts";
5
6
  import { CHILD_QUERY_MAX_INFLIGHT, CHILD_QUERY_TIMEOUT_MS, parseChildFrame, validChildMessage, validChildQueryId } from "./agents-messaging.ts";
@@ -275,8 +276,13 @@ interface LiveTask {
275
276
  acknowledgedIpcIds: Set<string>;
276
277
  acknowledgedIpcOrder: string[];
277
278
  mutationStarts: Map<string, { toolName: "write" | "edit"; toolCallId: string; path: string }>;
279
+ // Bounded ring buffer of the child's raw stderr output, capped to the last
280
+ // STDERR_TAIL_MAX characters. Only surfaced on the stall and pre-settle exit
281
+ // terminal paths, never on completed, cancelled, or other failure reasons.
282
+ stderrTail: string;
278
283
  }
279
284
 
285
+ const STDERR_TAIL_MAX = 512;
280
286
  const CHILD_MARKER = "GENTLE_PI_AGENTS_CHILD";
281
287
  const IPC_MARKER = "GENTLE_PI_AGENTS_OWNED_IPC";
282
288
  const PARENT_NOTIFICATION_TOOL = "subagent_parent_message";
@@ -487,15 +493,15 @@ export class AgentRunner {
487
493
  return accepted;
488
494
  }
489
495
 
490
- cancel(id: string): boolean {
496
+ cancel(id: string, reason = "cancelled"): boolean {
491
497
  const queued = this.queue.findIndex((entry) => entry.task.id === id);
492
498
  if (queued >= 0) {
493
499
  this.queue.splice(queued, 1);
494
- this.finish(id, TASK_STATUS.CANCELLED, "cancelled before start");
500
+ this.finish(id, TASK_STATUS.CANCELLED, `${reason} before start`);
495
501
  return true;
496
502
  }
497
503
  if (!this.live.has(id)) return false;
498
- this.requestStop(id, TASK_STATUS.CANCELLED, "cancelled", true);
504
+ this.requestStop(id, TASK_STATUS.CANCELLED, reason, true);
499
505
  return true;
500
506
  }
501
507
 
@@ -506,9 +512,9 @@ export class AgentRunner {
506
512
  if (live) { live.observations = undefined; live.observationGuard = undefined; }
507
513
  }
508
514
 
509
- cancelAll(): number {
515
+ cancelAll(reason = "cancelled"): number {
510
516
  const ids = [...this.queue.map((entry) => entry.task.id), ...this.live.keys()];
511
- return ids.filter((id) => this.cancel(id)).length;
517
+ return ids.filter((id) => this.cancel(id, reason)).length;
512
518
  }
513
519
 
514
520
  steer(id: string, message: string): boolean {
@@ -555,7 +561,7 @@ export class AgentRunner {
555
561
  return;
556
562
  }
557
563
  const processGroup = detached && typeof child.pid === "number" && child.pid > 0 ? child.pid : undefined;
558
- const live: LiveTask = { child, mutationStarts: new Map(), pending: new Map(), queries: new Map(), replies: new Map(), cancelStall: () => {}, cancelGrace: () => {}, processGroup, terminal: undefined, childExit: undefined, cleanupDeadlineAt: undefined, quarantined: false, nextId: 0, ipcClosed: false, acknowledgedIpcIds: new Set(), acknowledgedIpcOrder: [] };
564
+ const live: LiveTask = { child, mutationStarts: new Map(), pending: new Map(), queries: new Map(), replies: new Map(), cancelStall: () => {}, cancelGrace: () => {}, processGroup, terminal: undefined, childExit: undefined, cleanupDeadlineAt: undefined, quarantined: false, nextId: 0, ipcClosed: false, acknowledgedIpcIds: new Set(), acknowledgedIpcOrder: [], stderrTail: "" };
559
565
  if (request.prepareResponseObservations) {
560
566
  let ready = false;
561
567
  live.observationPreparation = () => ready;
@@ -592,7 +598,11 @@ export class AgentRunner {
592
598
  const lines = new JsonLines((value) => this.receive(id, request, value));
593
599
  child.stdout.setEncoding("utf8");
594
600
  child.stdout.on("data", (chunk: string) => lines.push(chunk));
595
- child.stderr?.on("data", () => {});
601
+ child.stderr?.setEncoding("utf8");
602
+ child.stderr?.on("data", (chunk: string) => {
603
+ const tail = live.stderrTail + chunk;
604
+ live.stderrTail = tail.length > STDERR_TAIL_MAX ? tail.slice(-STDERR_TAIL_MAX) : tail;
605
+ });
596
606
  child.on("exit", (code) => this.exited(id, code));
597
607
  if (request.sddRemediation && child.pid === undefined) {
598
608
  this.childError(id, new Error("remediation child has no process ID"));
@@ -602,6 +612,7 @@ export class AgentRunner {
602
612
  const data = response.data as { sessionFile?: unknown; model?: { provider?: unknown; id?: unknown } | null; thinkingLevel?: unknown } | undefined;
603
613
  if (response.success !== true || live.terminal || this.live.get(id) !== live || !data) return;
604
614
  const resolved: Partial<TaskRecord> = {};
615
+ if (this.canAdvanceLastStep(id, ["starting"])) resolved.lastStep = "pi ready";
605
616
  if (typeof data.sessionFile === "string" && data.sessionFile) resolved.sessionPath = data.sessionFile;
606
617
  if (data.model === null) resolved.model = "default";
607
618
  else if (typeof data.model?.provider === "string" && data.model.provider && typeof data.model.id === "string" && data.model.id) {
@@ -611,13 +622,35 @@ export class AgentRunner {
611
622
  this.store.update(id, resolved);
612
623
  });
613
624
  void this.send(id, { type: "prompt", message: promptText(request) }).then((response) => {
614
- if (response.success === false) this.requestStop(id, TASK_STATUS.FAILED, String(response.error ?? "prompt rejected"));
625
+ if (response.success === false) {
626
+ this.requestStop(id, TASK_STATUS.FAILED, String(response.error ?? "prompt rejected"));
627
+ return;
628
+ }
629
+ if (!live.terminal && this.live.get(id) === live && this.canAdvanceLastStep(id, ["starting", "pi ready"])) this.store.update(id, { lastStep: "prompt accepted" });
615
630
  });
616
631
  }
617
632
 
633
+ // A late get_state/prompt reply must never overwrite a stage that a child
634
+ // event (or the other reply) has already advanced lastStep past.
635
+ private canAdvanceLastStep(id: string, from: readonly string[]): boolean {
636
+ const current = this.store.get(id)?.lastStep;
637
+ return current !== undefined && from.includes(current);
638
+ }
639
+
640
+ // Cleaned for display only: raw bytes stay in live.stderrTail so later
641
+ // appends keep working from the unstripped ring buffer.
642
+ private stderrSuffix(live: LiveTask): string {
643
+ const cleaned = stripVTControlCharacters(live.stderrTail).replace(/\s+/g, " ").trim();
644
+ return cleaned ? `; stderr: ${cleaned}` : "";
645
+ }
646
+
618
647
  private armStall(id: string, live: LiveTask): void {
619
648
  live.cancelStall();
620
- live.cancelStall = this.deps.schedule(() => this.requestStop(id, TASK_STATUS.TIMED_OUT, `stalled for ${Math.round(this.limits.stallTimeoutMs / 60_000)} min`), this.limits.stallTimeoutMs);
649
+ live.cancelStall = this.deps.schedule(() => {
650
+ const lastStep = this.store.get(id)?.lastStep ?? "starting";
651
+ const minutes = Math.round(this.limits.stallTimeoutMs / 60_000);
652
+ this.requestStop(id, TASK_STATUS.TIMED_OUT, `stalled for ${minutes} min after: ${lastStep}${this.stderrSuffix(live)}`);
653
+ }, this.limits.stallTimeoutMs);
621
654
  }
622
655
 
623
656
  private send(id: string, command: Record<string, unknown>): Promise<Record<string, unknown>> {
@@ -933,7 +966,7 @@ export class AgentRunner {
933
966
  if (!live) return;
934
967
  live.childExit = code;
935
968
  if (this.groupExists(live)) {
936
- if (!live.terminal) this.requestStop(id, TASK_STATUS.FAILED, `pi exited with code ${code ?? "unknown"} before agent_settled`);
969
+ if (!live.terminal) this.requestStop(id, TASK_STATUS.FAILED, `pi exited with code ${code ?? "unknown"} before agent_settled${this.stderrSuffix(live)}`);
937
970
  return;
938
971
  }
939
972
  this.completeExit(id, live);
@@ -951,7 +984,7 @@ export class AgentRunner {
951
984
  return;
952
985
  }
953
986
  const terminal = live.terminal;
954
- this.finish(id, terminal ? terminal.status : TASK_STATUS.FAILED, terminal ? terminal.error : `pi exited with code ${live.childExit ?? "unknown"} before agent_settled`, live);
987
+ this.finish(id, terminal ? terminal.status : TASK_STATUS.FAILED, terminal ? terminal.error : `pi exited with code ${live.childExit ?? "unknown"} before agent_settled${this.stderrSuffix(live)}`, live);
955
988
  }
956
989
 
957
990
  private finish(id: string, status: TaskRecord["status"], error: string | null, live?: LiveTask): void {
@@ -980,6 +980,19 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
980
980
  // repeats 2.8.1 exactly. riskEvidence and hint remain dark because neither
981
981
  // is proven to reach the negotiated START path Pi consumes.
982
982
  "2.8.2": 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 }),
983
+ // v2.9.0 shipped RTK opt-in Community Tool integration (#4560, installer/
984
+ // sync/TUI only), SDD attempt-ledger fixes (#4564, #4567, #4569 — the
985
+ // remediation pointer is now decided by chain equality before shape, and
986
+ // the refusal wording changed), sync telemetry-runtime symlinked root
987
+ // (#4565), OpenCode reviewer Task wrapper decoding (#4545), and Engram
988
+ // protocol asset wording (#4179). Ground-truthed by diffing
989
+ // contracts/review-integration/v2 and contracts/review-provider-contract
990
+ // between the v2.8.2 and v2.9.0 tags in the gentle-ai source tree: zero
991
+ // bytes changed. None of the above touch the closed START/STATUS fields
992
+ // this row negotiates, so it repeats 2.8.2 exactly. riskEvidence and hint
993
+ // remain dark because neither is proven to reach the negotiated START
994
+ // path Pi consumes.
995
+ "2.9.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 }),
983
996
  });
984
997
 
985
998
  export interface NativeReviewProcessDiagnostics {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
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",
@@ -981,6 +981,19 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
981
981
  // repeats 2.8.1 exactly. riskEvidence and hint remain dark because neither
982
982
  // is proven to reach the negotiated START path Pi consumes.
983
983
  "2.8.2": 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 }),
984
+ // v2.9.0 shipped RTK opt-in Community Tool integration (#4560, installer/
985
+ // sync/TUI only), SDD attempt-ledger fixes (#4564, #4567, #4569 — the
986
+ // remediation pointer is now decided by chain equality before shape, and
987
+ // the refusal wording changed), sync telemetry-runtime symlinked root
988
+ // (#4565), OpenCode reviewer Task wrapper decoding (#4545), and Engram
989
+ // protocol asset wording (#4179). Ground-truthed by diffing
990
+ // contracts/review-integration/v2 and contracts/review-provider-contract
991
+ // between the v2.8.2 and v2.9.0 tags in the gentle-ai source tree: zero
992
+ // bytes changed. None of the above touch the closed START/STATUS fields
993
+ // this row negotiates, so it repeats 2.8.2 exactly. riskEvidence and hint
994
+ // remain dark because neither is proven to reach the negotiated START
995
+ // path Pi consumes.
996
+ "2.9.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 }),
984
997
  });
985
998
 
986
999
 
@@ -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 = "2.8.2";
39
+ export const INSTALLER_VERSION = "2.9.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/v2/cmd/gentle-ai";
46
46
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v2";
47
47
  export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
48
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.8.2`
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.9.0`
49
49
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
- // tag resolves to commit e3f53de6, the published v2.8.2 release head.
51
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:54VJ0ruRiPc6MbNw4iFmlcBkQFOyF5D8iHY0A54Ivug=";
50
+ // tag resolves to commit be495547, the published v2.9.0 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:WqmYw1UfVYCbtOxU1uTuiYq9+ROCUIDrmKIOjyYggQI=";
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 v2.8.2 digests are pinned from the published release:
70
+ // still holds it. The v2.9.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 v2.8.2 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v2.9.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_2.8.2_darwin_amd64.tar.gz", "0daa28897e6e54ce584f12ccefebf0d0df84e4ea8fbbd3a07e596ca82b079fa2", "17069156869ceda8e23eaa4fe5d7565cd57c1d78528f121dabba7301b82a0c14", "gentle-ai"),
118
- "darwin/arm64": asset("gentle-ai_2.8.2_darwin_arm64.tar.gz", "12265017e0fb6d5dd1ddb1751f188fa8c47d95a95d7c7515ae174394da5f6e97", "491542e4b60e432048d4074f21c1b04417b980cf77eb34cd0a2e7447ca57dd77", "gentle-ai"),
119
- "linux/amd64": asset("gentle-ai_2.8.2_linux_amd64.tar.gz", "5b95b184606168685a4ff576c103b051ceb23346a7f81ba557e047fa4d744146", "a55f4d2e114128866810c25195e5efecdded4502660e9a9c35c0b7a41c909dd8", "gentle-ai"),
120
- "linux/arm64": asset("gentle-ai_2.8.2_linux_arm64.tar.gz", "a33c91ca0f5c5c85a4fd69f5169138089eb53e0a92c6ab80de5094d0688debb7", "e2759aca09ffe97638e984491319e43c9477d1ae117726ecbdf20d29d44c8950", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_2.9.0_darwin_amd64.tar.gz", "0e1ce0b117e6f15b56e05defecb33a33825c2e25eb8306df09401d41e0a176fb", "0dc22de552e0403f318770067adccd82783b1f654ed4d0410d7d969e28e39602", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_2.9.0_darwin_arm64.tar.gz", "0a58d81cd7d76315e1d11ecf6b38abb27ee8e7e709a04c9a786e89636fb559bb", "5e72f1f34667a858fc179504e794847a1407142240361f99effd252d6ad49ee9", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_2.9.0_linux_amd64.tar.gz", "7d414cd8cba8ddc0ab9fa4bc309932638c533217b2f999b2a52e7a4fc0bf6f14", "5c47029fb6520f99b7df1b6841766b09d7184efed2e18694d83795bf4d4a5ad4", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_2.9.0_linux_arm64.tar.gz", "2bdab4684b5d415df9c9423c2022b017c7c1c1d9056f56acc08633fa0d821cfc", "56501a4d91f4e3255726b63c59b96a6d42c6952af2d265f54c8e99b361cda0c9", "gentle-ai"),
121
121
  });
122
122
 
123
123
  // A pinned asset is either a signed archive or, for a prerelease pin only,
@@ -340,7 +340,7 @@ async function main() {
340
340
  });
341
341
 
342
342
  if (driftedContracts.length > 0) {
343
- console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v2.8.2 runtime's vendored Gentle AI contract artifacts:");
343
+ console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v2.9.0 runtime's vendored Gentle AI contract artifacts:");
344
344
  for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
345
345
  process.exit(1);
346
346
  }
@@ -385,7 +385,7 @@ async function main() {
385
385
  process.exit(1);
386
386
  }
387
387
 
388
- console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v2.8.2 runtime).`);
388
+ console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v2.9.0 runtime).`);
389
389
  }
390
390
 
391
391
  const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;