gentle-pi 2.6.0 → 2.6.1

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 CHANGED
@@ -85,7 +85,7 @@ Most coding-agent sessions fail for operational reasons, not model reasons:
85
85
  | **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. |
86
86
  | **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
87
87
  | **Bounded native review** | Freezes one candidate, dispatches only controller-selected lenses, and records native authority. Review outcomes are informational; delivery follows ordinary repository policy. |
88
- | **Verified native runtime** | Provisions the exact package-local Gentle AI v2.8.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. |
88
+ | **Verified native runtime** | Provisions the exact package-local Gentle AI v2.8.1 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. |
89
89
  | **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
90
90
 
91
91
  ## Native pointer regions
@@ -138,7 +138,7 @@ pi install npm:gentle-pi@0.14.0
138
138
  pi install npm:gentle-pi@latest
139
139
  ```
140
140
 
141
- The latest RDD package 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 stable pins such as the current v2.8.0; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v2.8.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.
141
+ The latest RDD package 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 stable pins such as the current v2.8.1; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v2.8.1` 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.
142
142
 
143
143
  Recommended companion packages:
144
144
 
@@ -294,13 +294,13 @@ flowchart TD
294
294
 
295
295
  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.
296
296
 
297
- Native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v2.8.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.
297
+ Native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v2.8.1 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.
298
298
 
299
299
  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.
300
300
 
301
301
  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.
302
302
 
303
- Once the pinned gentle-ai runtime (currently v2.8.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.
303
+ Once the pinned gentle-ai runtime (currently v2.8.1) 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.
304
304
 
305
305
  ### FINALIZE wrapper input
306
306
 
@@ -34,13 +34,15 @@
34
34
  "effort": {"enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max", "not_selected", "unknown", "custom", "unavailable", "unsupported"]},
35
35
  "model": {
36
36
  "type": "object", "additionalProperties": false, "required": ["provider", "id"],
37
- "properties": {"provider": {"type": "string"}, "id": {"type": "string"}},
38
- "oneOf": [
39
- {"properties": {"provider": {"const": "anthropic"}, "id": {"enum": ["claude-opus-5", "claude-haiku-4-5", "claude-haiku-4-5-20251001", "claude-sonnet-5"]}}},
40
- {"properties": {"provider": {"enum": ["openai", "openai-codex"]}, "id": {"enum": ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.3-codex-spark", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.2", "gpt-5.3-codex", "gpt-5.6"]}}},
41
- {"properties": {"provider": {"const": "opencode"}, "id": {"const": "custom"}}},
37
+ "properties": {
38
+ "provider": {"type": "string", "maxLength": 32},
39
+ "id": {"type": "string", "maxLength": 64}
40
+ },
41
+ "anyOf": [
42
+ {"properties": {"provider": {"pattern": "^[a-z0-9][a-z0-9-]{0,31}$"}, "id": {"pattern": "^(claude|gpt|o[1-9]|codex|gemini|gemma|deepseek|glm|qwen|qwq|kimi|moonshot|llama|codellama|mistral|mixtral|codestral|devstral|magistral|ministral|minimax|grok|phi|nemotron|jamba|hunyuan|doubao|ernie|mimo|granite|olmo|smollm|starcoder|titan)[a-z0-9]*([-_.:][a-z0-9]+){0,8}$"}}},
42
43
  {"properties": {"provider": {"const": "unknown"}, "id": {"const": "unknown"}}},
43
- {"properties": {"provider": {"const": "custom"}, "id": {"const": "custom"}}}
44
+ {"properties": {"provider": {"const": "custom"}, "id": {"const": "custom"}}},
45
+ {"properties": {"provider": {"const": "opencode"}, "id": {"const": "custom"}}}
44
46
  ]
45
47
  },
46
48
  "row": {
@@ -30,7 +30,6 @@ import { openInExternalEditor } from "./gentle-shell.ts";
30
30
  import { resolveGentlePiAgentHome } from "../lib/agent-home.ts";
31
31
  import { assertResearchCheckpoint, parseResearchPersistence, RESEARCH_PERSISTENCE_ENTRY, canonicalArtifactPath, researchAgent, renderResearchCapabilities, RESEARCH_CHILD_TOOLS_ENV, RESEARCH_SELECTION_ENV, RESEARCH_ARTIFACT_ENV, parseResearchArtifactIntent, researchArtifactCall, researchArtifactReadback, type ResearchArtifactIntent, type ResearchWriteIdentity } from "../lib/sdd-research-capabilities.ts";
32
32
  import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, childEvent, launchSelection, type LaunchSelection } from "../lib/runtime-metrics-children.ts";
33
- import { lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
34
33
  import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/runtime-metrics-policy.ts";
35
34
 
36
35
  // Gentle Agents: subagents as isolated `pi --mode rpc` children, a task
@@ -244,7 +243,6 @@ export interface AgentsDeps extends RunnerDeps {
244
243
  runtimeMetricsPolicy?: RuntimeMetricsPolicyDeps;
245
244
  metricsNow?: () => number;
246
245
  metricsSchedule?: RunnerDeps["schedule"];
247
- lookupPiCatalogName?: typeof lookupPiCatalogName;
248
246
  }
249
247
 
250
248
  export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
@@ -633,7 +631,6 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
633
631
  const stoppingTaskIds = new Set<string>();
634
632
  const yieldedTaskIds = new Set<string>();
635
633
  const metricsNow = deps.metricsNow ?? (() => performance.now());
636
- const catalogLookup = deps.lookupPiCatalogName ?? lookupPiCatalogName;
637
634
  let metricsOwner = {};
638
635
  const metricTasks = new Map<string, { selection?: LaunchSelection; started: number; launched: boolean; finished: boolean; current(): boolean; valid(): boolean }>();
639
636
  const unsubscribeMetrics = pi.events.on(CHILD_METRICS_REVOKED, id => {
@@ -1121,7 +1118,6 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1121
1118
  onLaunch: () => { metrics.launched = true; request.onLaunch?.(); },
1122
1119
  ...(observe ? { canCollectResponseObservations: metrics.valid, prepareResponseObservations: async () => {
1123
1120
  if (metrics.finished || owner !== metricsOwner || request.parentSessionId !== activeSessionId() || !runtimeMetricsEnvAllows(deps.env)) return false;
1124
- void catalogLookup({ provider: "openai", modelId: "gpt-4o" }).catch(() => {});
1125
1121
  if (!metrics.valid()) return false;
1126
1122
  metrics.selection = launchSelection(request.agent, request.model, request.thinking);
1127
1123
  metrics.started = metricsNow();
@@ -1,6 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { EFFORTS, ORCHESTRATOR_AGENT_CLASS, RuntimeMetrics, UNKNOWN_AGENT_CLASS, type FinalResponse, type TokenMeasurement } from "../lib/runtime-metrics.ts";
3
- import { classifyPiCatalogName, lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
4
3
  import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, snapshotChildEvent, type ChildLaunchBucket } from "../lib/runtime-metrics-children.ts";
5
4
  import { RuntimeMetricsAttempt } from "../lib/runtime-metrics-delivery.ts";
6
5
  import { sendNativeRuntimeEvent, type NativeRuntimeTransportDeps } from "../lib/runtime-metrics-native.ts";
@@ -12,10 +11,8 @@ import { runtimeMetricsEnvAllows } from "../lib/runtime-metrics-policy.ts";
12
11
  * Pi hooks lack request correlation: latency and SDK-zero presence are unknown.
13
12
  */
14
13
  export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
15
- { lookup = lookupPiCatalogName, classify = classifyPiCatalogName, native, send = sendNativeRuntimeEvent,
16
- now = () => performance.now(), shutdownWaitMs = 1500 }:
17
- { lookup?: typeof lookupPiCatalogName; classify?: typeof classifyPiCatalogName; native?: NativeRuntimeTransportDeps;
18
- send?: typeof sendNativeRuntimeEvent; now?: () => number; shutdownWaitMs?: number } = {}): void {
14
+ { native, send = sendNativeRuntimeEvent, now = () => performance.now(), shutdownWaitMs = 1500 }:
15
+ { native?: NativeRuntimeTransportDeps; send?: typeof sendNativeRuntimeEvent; now?: () => number; shutdownWaitMs?: number } = {}): void {
19
16
  const allows = () => env.GENTLE_PI_AGENTS_CHILD !== "1" && runtimeMetricsEnvAllows(env);
20
17
  if (!allows()) return;
21
18
  type Selection = Pick<FinalResponse, "selectedModelId" | "selectedProvider" | "effort">;
@@ -24,7 +21,7 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
24
21
  let requestSeen = false;
25
22
  let ambiguous = false;
26
23
  let live: { id: string; started: number; ctx: ExtensionContext; attempt: RuntimeMetricsAttempt;
27
- seen: WeakSet<object>; children: Set<string>; catalog: boolean } | undefined;
24
+ seen: WeakSet<object>; children: Set<string> } | undefined;
28
25
  const missing = { state: "unavailable" } as const;
29
26
  function invalidate() { selection = undefined; ambiguous = true; }
30
27
  function dispose() { live?.attempt.dispose(); live = undefined; active = false; invalidate(); }
@@ -32,15 +29,10 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
32
29
  if (!allows() || live?.id !== ctx.sessionManager.getSessionId()) dispose();
33
30
  return live;
34
31
  }
35
- function refreshCatalog(owner: NonNullable<typeof live>, input: { provider: unknown; modelId: unknown }) {
36
- void lookup(input).then(result => {
37
- if (live === owner && result.classification === "catalog_public") owner.catalog = true;
38
- }, () => { /* A later classification callback may retry the bounded load. */ });
39
- }
40
32
  function submit(owner: NonNullable<typeof live>, responses: FinalResponse[], launches?: ChildLaunchBucket[]) {
41
33
  if (!responses.length || live !== owner || !current(owner.ctx)) return;
42
34
  // Ephemeral event-local accounting only; source IDs never enter these rows.
43
- const metrics = new RuntimeMetrics({ classifyModel: classify });
35
+ const metrics = new RuntimeMetrics();
44
36
  for (const [index, row] of responses.entries()) metrics.record({ ...row, responseId: String(index) });
45
37
  const rows = metrics.snapshot();
46
38
  if (!rows.length) return;
@@ -71,10 +63,8 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
71
63
  pi.on("session_start", (_event, ctx) => {
72
64
  dispose();
73
65
  if (!allows()) return;
74
- const owner = { id: ctx.sessionManager.getSessionId(), started: now(), ctx,
75
- attempt: new RuntimeMetricsAttempt(), seen: new WeakSet<object>(), children: new Set<string>(), catalog: false };
76
- live = owner;
77
- refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
66
+ live = { id: ctx.sessionManager.getSessionId(), started: now(), ctx,
67
+ attempt: new RuntimeMetricsAttempt(), seen: new WeakSet<object>(), children: new Set<string>() };
78
68
  });
79
69
  pi.on("session_shutdown", (_event, ctx) => {
80
70
  const teardown = () => { dispose(); offChild(); offRevoke(); };
@@ -95,8 +85,10 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
95
85
  if (!active || ambiguous) return;
96
86
  let effort: FinalResponse["effort"] = "unavailable";
97
87
  try { const value = pi.getThinkingLevel(); if (EFFORTS.includes(value)) effort = value; } catch { /* No evidence. */ }
98
- selection = { selectedProvider: owner.catalog ? ctx.model?.provider as FinalResponse["provider"] ?? "unknown" : "unknown",
99
- selectedModelId: owner.catalog && typeof ctx.model?.id === "string" && ctx.model.id.length <= 128 ? ctx.model.id : undefined, effort };
88
+ // No catalog gate: the selection is taken as-is (bounded to 128 chars) and
89
+ // the schema-driven normalizer decides what actually leaves the machine.
90
+ selection = { selectedProvider: typeof ctx.model?.provider === "string" && ctx.model.provider.length <= 128 ? ctx.model.provider : undefined,
91
+ selectedModelId: typeof ctx.model?.id === "string" && ctx.model.id.length <= 128 ? ctx.model.id : undefined, effort };
100
92
  });
101
93
  function token(value: unknown): TokenMeasurement {
102
94
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 1_000_000_000
@@ -109,7 +101,6 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
109
101
  if (!owner || message.role !== "assistant" || message.stopReason === "pending" || message.stopReason === "deferred"
110
102
  || owner.seen.has(message)) return;
111
103
  owner.seen.add(message);
112
- refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
113
104
  const selected = active && !ambiguous ? selection : undefined;
114
105
  submit(owner, [{ kind: "final_assistant_response", responseId: "0",
115
106
  selectedProvider: selected?.selectedProvider ?? "unknown", selectedModelId: selected?.selectedModelId,
@@ -966,6 +966,11 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
966
966
  // so this row repeats 2.7.0. riskEvidence and hint remain dark because
967
967
  // neither is proven to reach the negotiated START path Pi consumes.
968
968
  "2.8.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 }),
969
+ // v2.8.1 only changed runtime telemetry model attribution (gentle-ai#4536);
970
+ // the closed fields Pi consumes did not change between 2.8.0 and 2.8.1, so
971
+ // this row repeats 2.8.0 exactly. riskEvidence and hint remain dark
972
+ // because neither is proven to reach the negotiated START path Pi consumes.
973
+ "2.8.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 }),
969
974
  });
970
975
 
971
976
  export interface NativeReviewProcessDiagnostics {
@@ -2,14 +2,12 @@ import { readFileSync, readdirSync } from "node:fs";
2
2
  import { parseAgentDefinition, type AgentDefinition, type ModelRef } from "./agents-config.ts";
3
3
  import type { ChildObservationSnapshot } from "./agents-runner.ts";
4
4
  import { FINISHED_STATUSES, type TaskStatus } from "./agents-protocol.ts";
5
- import { classifyRuntimeModelId, EFFORTS, parseAgentClass, RuntimeMetrics, UNKNOWN_AGENT_CLASS, validRuntimeResponse, type AgentClass, type FinalResponse, type RuntimeMetricBucket } from "./runtime-metrics.ts";
6
- import { classifyPiCatalogName } from "./runtime-metrics-pi-identity.ts";
5
+ import { classifyRuntimeModelId, EFFORTS, normalizeRuntimeProvider, parseAgentClass, RuntimeMetrics, UNKNOWN_AGENT_CLASS, validRuntimeResponse, type AgentClass, type FinalResponse, type RuntimeMetricBucket } from "./runtime-metrics.ts";
7
6
  export const CHILD_METRICS_EVENT = "gentle:runtime-metrics:child/v1";
8
7
  // Local revocation notification invalidates active observations. Contains only
9
8
  // the local session join, never policy output.
10
9
  export const CHILD_METRICS_REVOKED = "gentle:runtime-metrics:revoked/v1";
11
10
  const missing = { state: "unavailable" } as const;
12
- const providers = ["anthropic", "openai", "openai-codex", "google", "google-vertex", "amazon-bedrock", "openrouter", "custom", "unknown"];
13
11
  const tokenFields = ["input", "output", "cacheRead", "cacheWrite", "reasoning", "totalTokens"] as const;
14
12
  /** Recognize only names from this package's fixed assets and the transport's
15
13
  * closed agent_class enum. Customized packaged agents retain their schema name;
@@ -44,11 +42,11 @@ export function classifyBuiltinAgent(agent: AgentDefinition): AgentClass {
44
42
  } catch { return UNKNOWN_AGENT_CLASS; }
45
43
  }
46
44
  function provider(value: unknown): FinalResponse["provider"] {
47
- return providers.includes(value as string) ? value as FinalResponse["provider"] : value ? "custom" : "unknown";
45
+ return normalizeRuntimeProvider(value);
48
46
  }
49
47
  function modelId(namespace: unknown, value: unknown): string {
50
48
  if (value === "unknown" || value === undefined) return "unknown";
51
- return classifyRuntimeModelId(namespace, value, classifyPiCatalogName);
49
+ return classifyRuntimeModelId(namespace, value);
52
50
  }
53
51
  function effort(value: unknown): FinalResponse["effort"] {
54
52
  return EFFORTS.includes(value as FinalResponse["effort"]) ? value as FinalResponse["effort"] : "unavailable";
@@ -1,7 +1,7 @@
1
1
  import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "node:child_process";
2
2
  import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
3
3
  import { runtimeMetricsEnvAllows } from "./runtime-metrics-policy.ts";
4
- import type { RuntimeMetricBucket } from "./runtime-metrics.ts";
4
+ import { isPublicRuntimeModelId, normalizeRuntimeModel, type RuntimeMetricBucket } from "./runtime-metrics.ts";
5
5
  import type { ChildLaunchBucket } from "./runtime-metrics-children.ts";
6
6
 
7
7
  import schema from "../contracts/telemetry/runtime-aggregate-v1.schema.json" with { type: "json" };
@@ -10,11 +10,11 @@ export type NativeRuntimeResult = "stored" | "duplicate" | "discarded" | "disabl
10
10
  export const NATIVE_SEND_ACK_SCHEMA = "gentle-ai.telemetry-runtime-send/v1";
11
11
  const MAX_METRIC = schema.$defs.count.maximum;
12
12
  const count = (value: number) => Number.isSafeInteger(value) && value >= 0 && value <= MAX_METRIC;
13
+ // Re-derives through the same schema-driven normalizer used at record time so
14
+ // an already-classified (provider, id) pair is re-validated against the
15
+ // transport's anyOf shape before it ever leaves the machine.
13
16
  function publicModel(provider: string, id: string) {
14
- const matches = (rule: { const?: string; enum?: string[] }, value: string) => rule.const === value || rule.enum?.includes(value);
15
- if (schema.$defs.model.oneOf.some(({ properties }) => matches(properties.provider, provider) && matches(properties.id, id))) return { provider, id };
16
- const kind = !id || id === "unknown" ? "unknown" : "custom";
17
- return { provider: kind, id: kind };
17
+ return normalizeRuntimeModel(provider, id);
18
18
  }
19
19
 
20
20
  /** Closed field projection of one event, not a session snapshot. Oversized or
@@ -34,14 +34,22 @@ export function encodeNativeRuntimeEvent(source: readonly RuntimeMetricBucket[],
34
34
  };
35
35
  const agentClass = schema.$defs.row.properties.agent_class.enum.includes(row.agentClass) ? row.agentClass : "unknown";
36
36
  const effort = (value: string) => schema.$defs.effort.enum.includes(value) ? value : "unavailable";
37
- const response = typeof row.responseModelId === "string" && row.responseModelId !== "unknown" && row.responseModelId !== "";
38
- const selected = typeof row.selectedModelId === "string" && row.selectedModelId !== "unknown" && row.selectedModelId !== "";
37
+ // Evidence order: an actually-dispatched response model wins; otherwise
38
+ // the caller's own selection; otherwise the SDK-observed model the
39
+ // selection never captured (still labeled "selected" evidence, since
40
+ // the transport has no separate "observed" category). Never response
41
+ // evidence unless the response model itself normalizes to a public id.
42
+ const response = isPublicRuntimeModelId(row.responseModelId);
43
+ const selected = !response && isPublicRuntimeModelId(row.selectedModelId);
44
+ const observed = !response && !selected && isPublicRuntimeModelId(row.observedModelId);
39
45
  const model = response ? publicModel(row.provider, row.responseModelId)
40
- : selected ? publicModel(row.selectedProvider, row.selectedModelId) : { provider: "unknown", id: "unknown" };
46
+ : selected ? publicModel(row.selectedProvider, row.selectedModelId)
47
+ : observed ? publicModel(row.provider, row.observedModelId)
48
+ : { provider: "unknown", id: "unknown" };
41
49
  const time = row.fullResponseMs;
42
50
  if (!count(time.measured) || !Number.isFinite(time.sum) || time.sum < 0 || time.sum > MAX_METRIC
43
51
  || (!time.measured && time.sum !== 0)) throw new Error("Invalid duration");
44
- return { model, model_evidence: response ? "response" : selected ? "selected" : "unknown",
52
+ return { model, model_evidence: response ? "response" : (selected || observed) ? "selected" : "unknown",
45
53
  agent_kind: agentClass === "orchestrator" || agentClass === "unknown" ? agentClass : "built_in", agent_class: agentClass,
46
54
  selected_effort: effort(row.effort), effective_effort: effort(row.providerThinkingLevel),
47
55
  launches: null, responses: row.responses,
@@ -1,5 +1,4 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { classifyPiCatalogName } from "./runtime-metrics-pi-identity.ts";
3
2
 
4
3
  const runtimeSchema = JSON.parse(readFileSync(new URL("../contracts/telemetry/runtime-aggregate-v1.schema.json", import.meta.url), "utf8"));
5
4
 
@@ -7,7 +6,6 @@ const runtimeSchema = JSON.parse(readFileSync(new URL("../contracts/telemetry/ru
7
6
  // Callers supply finalized assistant responses and authoritative classifications.
8
7
  // Never infer executor, usage availability, or measured timings from SDK defaults.
9
8
  const EXECUTORS = ["orchestrator", "worker", "reviewer", "unknown"] as const;
10
- const PROVIDERS = ["anthropic", "openai", "openai-codex", "google", "google-vertex", "amazon-bedrock", "openrouter", "custom", "unknown"] as const;
11
9
  // Stable families, not model IDs/versions: new models need no catalog update.
12
10
  // Callers map known native metadata to families; private aliases stay custom.
13
11
  const FAMILIES = ["claude", "gpt", "o-series", "gemini", "llama", "qwen", "deepseek", "kimi", "custom", "unknown"] as const;
@@ -42,38 +40,93 @@ function requiredAgentClass(value: string): AgentClass {
42
40
  export const UNKNOWN_AGENT_CLASS = requiredAgentClass("unknown");
43
41
  export const ORCHESTRATOR_AGENT_CLASS = requiredAgentClass("orchestrator");
44
42
 
45
- function registeredModels(): ReadonlySet<string> {
46
- const rules: unknown = runtimeSchema?.$defs?.model?.oneOf;
47
- if (!Array.isArray(rules) || !rules.length) throw new Error("Invalid runtime telemetry model schema");
48
- const values = (rule: unknown, field: "provider" | "id"): string[] => {
49
- if (!object(rule)) throw new Error("Invalid runtime telemetry model schema");
50
- const properties = rule.properties;
51
- if (!object(properties) || !object(properties[field])) throw new Error("Invalid runtime telemetry model schema");
52
- const property = properties[field];
53
- const candidates = typeof property.const === "string" ? [property.const] : property.enum;
54
- if (!Array.isArray(candidates) || !candidates.length || candidates.some(value => typeof value !== "string")) {
55
- throw new Error("Invalid runtime telemetry model schema");
56
- }
57
- return candidates as string[];
58
- };
59
- const models = new Set<string>();
60
- for (const rule of rules) for (const provider of values(rule, "provider")) {
61
- for (const id of values(rule, "id")) models.add(JSON.stringify([provider, id]));
43
+ interface ModelFieldRule { pattern: RegExp; maxLength: number }
44
+
45
+ function modelFieldRule(field: "provider" | "id"): ModelFieldRule {
46
+ // The length cap lives on $defs.model.properties.<field>; the shape pattern
47
+ // lives only on the first (public-pattern) branch of $defs.model.anyOf, next
48
+ // to the unknown/custom/opencode sentinel branches. Both are mirrored,
49
+ // byte-for-byte, from the Gentle AI transport schema.
50
+ const property: unknown = runtimeSchema?.$defs?.model?.properties?.[field];
51
+ if (!object(property) || typeof property.maxLength !== "number") {
52
+ throw new Error(`Invalid runtime telemetry model ${field} schema`);
62
53
  }
63
- return models;
54
+ const branches: unknown = runtimeSchema?.$defs?.model?.anyOf;
55
+ const patternProperty = Array.isArray(branches) && object(branches[0]) && object(branches[0].properties)
56
+ ? branches[0].properties[field] : undefined;
57
+ if (!object(patternProperty) || typeof patternProperty.pattern !== "string") {
58
+ throw new Error(`Invalid runtime telemetry model ${field} schema`);
59
+ }
60
+ return { pattern: new RegExp(patternProperty.pattern), maxLength: property.maxLength };
64
61
  }
65
62
 
66
- const REGISTERED_MODELS = registeredModels();
63
+ // Schema-driven, not a hardcoded TypeScript regex: the mirrored transport
64
+ // contract owns the open family-pattern rules for provider/id shape. A
65
+ // companion Gentle AI change keeps the Go side on the same patterns.
66
+ const MODEL_PROVIDER_RULE = modelFieldRule("provider");
67
+ const MODEL_ID_RULE = modelFieldRule("id");
67
68
 
68
- /** The mirrored transport registry is authoritative before the optional Pi
69
- * catalog. Unknown registry pairs still pass through the existing privacy
70
- * classifier, so private IDs can only become custom/unknown.
69
+ /** Provider-only normalization, independent of any specific model id: trims,
70
+ * lowercases, and keeps the slug only when it matches the schema provider
71
+ * pattern within its maxLength. Non-string/empty input is "unknown"; a
72
+ * non-conforming non-empty string is "custom". Shared by the standalone
73
+ * provider dimension and by normalizeRuntimeModel's own provider handling.
71
74
  */
72
- export function classifyRuntimeModelId(provider: unknown, modelId: unknown,
73
- classifyModel: typeof classifyPiCatalogName = classifyPiCatalogName): string {
74
- if (typeof provider === "string" && typeof modelId === "string"
75
- && REGISTERED_MODELS.has(JSON.stringify([provider, modelId]))) return modelId;
76
- return classifyModel({ provider, modelId }).modelId;
75
+ export function normalizeRuntimeProvider(value: unknown): string {
76
+ if (typeof value !== "string") return "unknown";
77
+ const trimmed = value.trim();
78
+ if (!trimmed) return "unknown";
79
+ const lower = trimmed.toLowerCase();
80
+ return lower.length <= MODEL_PROVIDER_RULE.maxLength && MODEL_PROVIDER_RULE.pattern.test(lower) ? lower : "custom";
81
+ }
82
+
83
+ /** Generic, schema-driven family-pattern normalizer for a (provider, id)
84
+ * selection or response pair (gentle-pi#968 / gentle-ai#4536). Open-weight
85
+ * models on arbitrary providers are reported by name; private aliases and
86
+ * fine-tunes stay custom. Rules, identical to the mirrored Go/schema side:
87
+ * - a non-string or empty provider or id fails closed to unknown/unknown;
88
+ * - id: trim, keep the LAST "/"-separated segment, lowercase; public only
89
+ * when it matches the schema id pattern within its maxLength, otherwise
90
+ * "custom";
91
+ * - provider: trim, lowercase; kept when it matches the schema provider
92
+ * pattern, otherwise "custom";
93
+ * - when the id is not public the provider becomes "custom" too, except
94
+ * "opencode", which stays opencode/custom;
95
+ * - the literal unknown/unknown and custom/custom pairs pass through.
96
+ */
97
+ export function normalizeRuntimeModel(provider: unknown, id: unknown): { provider: string; id: string } {
98
+ if (typeof provider !== "string" || typeof id !== "string") return { provider: "unknown", id: "unknown" };
99
+ const providerTrimmed = provider.trim();
100
+ const idTrimmed = id.trim();
101
+ if (!providerTrimmed || !idTrimmed) return { provider: "unknown", id: "unknown" };
102
+ const providerLower = providerTrimmed.toLowerCase();
103
+ const idSegment = idTrimmed.split("/").pop() ?? "";
104
+ const idLower = idSegment.toLowerCase();
105
+ if (providerLower === "unknown" && idLower === "unknown") return { provider: "unknown", id: "unknown" };
106
+ if (providerLower === "custom" && idLower === "custom") return { provider: "custom", id: "custom" };
107
+ const idPublic = idLower.length <= MODEL_ID_RULE.maxLength && MODEL_ID_RULE.pattern.test(idLower);
108
+ const id_ = idPublic ? idLower : "custom";
109
+ let providerResult = normalizeRuntimeProvider(providerTrimmed);
110
+ if (!idPublic) providerResult = providerResult === "opencode" ? "opencode" : "custom";
111
+ return { provider: providerResult, id: id_ };
112
+ }
113
+
114
+ /** Thin wrapper over normalizeRuntimeModel for callers that only track the
115
+ * id dimension (the provider is used only to decide the closed unknown/empty
116
+ * fast path; a valid non-empty provider string never changes the id result).
117
+ */
118
+ export function classifyRuntimeModelId(provider: unknown, modelId: unknown): string {
119
+ return normalizeRuntimeModel(provider, modelId).id;
120
+ }
121
+
122
+ /** True only for an id already in its normalized public form: it matches the
123
+ * schema id pattern within its maxLength (which, by construction, also
124
+ * excludes the "unknown"/"custom" sentinels, since neither matches any
125
+ * recognized family prefix). Used to pick the strongest available evidence
126
+ * tier among already-classified id dimensions (response/selected/observed).
127
+ */
128
+ export function isPublicRuntimeModelId(value: unknown): value is string {
129
+ return typeof value === "string" && value.length > 0 && value.length <= MODEL_ID_RULE.maxLength && MODEL_ID_RULE.pattern.test(value);
77
130
  }
78
131
 
79
132
  type Missing = { state: "unavailable" | "unsupported" };
@@ -84,20 +137,23 @@ export interface FinalResponse {
84
137
  /** Local dedupe only: 1..128 UTF-16 code units; never exported. */
85
138
  responseId: string;
86
139
  /** Caller-observed selected SDK model ID, never dispatched/response identity.
87
- * Catalog must be loaded before accounting; only public catalog names survive.
140
+ * Normalized through normalizeRuntimeModel at record time; only an id
141
+ * matching the schema family pattern survives as a public name.
88
142
  */
89
143
  selectedModelId?: string;
90
144
  /** Selection namespace, independent from observed response provider.
91
145
  * Omission preserves legacy same-provider callers; adapters must pass it explicitly.
92
146
  */
93
- selectedProvider?: typeof PROVIDERS[number];
147
+ selectedProvider?: string;
94
148
  agentClass?: AgentClass;
95
- /** Public catalog names from SDK response metadata, never endpoint proof. */
149
+ /** SDK-observed model id from response metadata, never endpoint proof.
150
+ * Normalized like selectedModelId; only a schema family match survives.
151
+ */
96
152
  observedModelId?: string;
97
153
  responseModelId?: string;
98
154
  providerThinkingLevel?: typeof EFFORTS[number];
99
155
  executor: typeof EXECUTORS[number];
100
- provider: typeof PROVIDERS[number];
156
+ provider: string;
101
157
  modelFamily: typeof FAMILIES[number];
102
158
  effort: typeof EFFORTS[number];
103
159
  error: typeof ERRORS[number];
@@ -195,17 +251,14 @@ export class RuntimeMetrics {
195
251
  #buckets = new Map<string, RuntimeMetricBucket>();
196
252
  #maxResponses: number;
197
253
  #maxBuckets: number;
198
- #classifyModel: typeof classifyPiCatalogName;
199
254
 
200
- constructor({ maxResponses = 1024, maxBuckets = 64, classifyModel = classifyPiCatalogName }:
201
- { maxResponses?: number; maxBuckets?: number; classifyModel?: typeof classifyPiCatalogName } = {}) {
255
+ constructor({ maxResponses = 1024, maxBuckets = 64 }: { maxResponses?: number; maxBuckets?: number } = {}) {
202
256
  if (!Number.isInteger(maxResponses) || maxResponses < 1 || maxResponses > 1024
203
257
  || !Number.isInteger(maxBuckets) || maxBuckets < 1 || maxBuckets > 64) {
204
258
  throw new RangeError("Invalid runtime metrics capacity");
205
259
  }
206
260
  this.#maxResponses = maxResponses;
207
261
  this.#maxBuckets = maxBuckets;
208
- this.#classifyModel = classifyModel;
209
262
  }
210
263
 
211
264
  record(response: FinalResponse): "recorded" | "duplicate" | "invalid" | "capacity" {
@@ -215,13 +268,13 @@ export class RuntimeMetrics {
215
268
  const dimensions = {
216
269
  hostAgent: "pi" as const,
217
270
  agentClass: category(AGENT_CLASSES, response.agentClass, UNKNOWN_AGENT_CLASS),
218
- observedModelId: classifyRuntimeModelId(response.provider, response.observedModelId, this.#classifyModel),
219
- responseModelId: classifyRuntimeModelId(response.provider, response.responseModelId, this.#classifyModel),
271
+ observedModelId: classifyRuntimeModelId(response.provider, response.observedModelId),
272
+ responseModelId: classifyRuntimeModelId(response.provider, response.responseModelId),
220
273
  providerThinkingLevel: category(EFFORTS, response.providerThinkingLevel, "unavailable"),
221
- selectedModelId: classifyRuntimeModelId(selectedProvider, response.selectedModelId, this.#classifyModel),
222
- selectedProvider: category(PROVIDERS, selectedProvider, typeof selectedProvider === "string" && selectedProvider ? "custom" : "unknown"),
274
+ selectedModelId: classifyRuntimeModelId(selectedProvider, response.selectedModelId),
275
+ selectedProvider: normalizeRuntimeProvider(selectedProvider),
223
276
  executor: category(EXECUTORS, response.executor, "unknown"),
224
- provider: category(PROVIDERS, response.provider, typeof response.provider === "string" && response.provider ? "custom" : "unknown"),
277
+ provider: normalizeRuntimeProvider(response.provider),
225
278
  modelFamily: category(FAMILIES, response.modelFamily, typeof response.modelFamily === "string" && response.modelFamily ? "custom" : "unknown"),
226
279
  effort: response.effort,
227
280
  error: response.error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
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",
@@ -967,6 +967,11 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
967
967
  // so this row repeats 2.7.0. riskEvidence and hint remain dark because
968
968
  // neither is proven to reach the negotiated START path Pi consumes.
969
969
  "2.8.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 }),
970
+ // v2.8.1 only changed runtime telemetry model attribution (gentle-ai#4536);
971
+ // the closed fields Pi consumes did not change between 2.8.0 and 2.8.1, so
972
+ // this row repeats 2.8.0 exactly. riskEvidence and hint remain dark
973
+ // because neither is proven to reach the negotiated START path Pi consumes.
974
+ "2.8.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 }),
970
975
  });
971
976
 
972
977
 
@@ -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.0";
39
+ export const INSTALLER_VERSION = "2.8.1";
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.0`
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.8.1`
49
49
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
- // tag resolves to commit 890ca948, the published v2.8.0 release head.
51
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:QNdrYDxq/Bi08o9PCk0jc4r5YObLdFmRCLXjKMqsze8=";
50
+ // tag resolves to commit 7aad6a51, the published v2.8.1 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:EixFRy7P3JLVi1ovr18pMxQIrtjDib9i7VEH9g4XRVE=";
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.0 digests are pinned from the published release:
70
+ // still holds it. The v2.8.1 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.0 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v2.8.1 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.0_darwin_amd64.tar.gz", "9c735e51916c31135926e62b21ac990216d810907a73894f68ec47bb0648d22d", "3880084f118533102a02eb0656a709db9817d330f82de8f533868667e0ac449d", "gentle-ai"),
118
- "darwin/arm64": asset("gentle-ai_2.8.0_darwin_arm64.tar.gz", "b62d01aadba484e79869e11878d47e81cd692ebce93a5de454d0889db65fac54", "fed81b2b3dbec02ab5f06ce73364b43d28c532231813a01e2015d585f08cbb2e", "gentle-ai"),
119
- "linux/amd64": asset("gentle-ai_2.8.0_linux_amd64.tar.gz", "56378fd47cfee127d5982398e79248194d3c7624d268efda9823a6b061a018b7", "df19c4ac20cfdfc0cc0c98c75cda42106340ca9966009f5dfb61575c1ada27c8", "gentle-ai"),
120
- "linux/arm64": asset("gentle-ai_2.8.0_linux_arm64.tar.gz", "41726bffee269206e5fe96d7bd900164d01f478d8916bdfba446a7991903dfc1", "a93fff44ea583546d37ce833cc2a0f14afec118ae4209c305cafcadb91f67bac", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_2.8.1_darwin_amd64.tar.gz", "cbfba54a72ec0a28ce5e42c3057a03cc3e809c28b1ff38d080ef42a242027897", "77784169c58b7f84d21c2df538031de2a9582cc1ea43242103d9487f3278c85f", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_2.8.1_darwin_arm64.tar.gz", "a5aca61d4e98fed0e0a8d1f2d6e1ae5d48746bd6baa541c91f629ade25f47839", "1c4b63db9aba22f02b6cf53b4b61cb750c2a7d8bf429c5aae82993227ca1e613", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_2.8.1_linux_amd64.tar.gz", "609190fa9e8ef1896a8b4fcbb5a44e1ad5ac64aae1d17eb0459735c5bbef199b", "c11a6d627a81741d00abc7bc9071522e9279de6f95219d810469a448615e618e", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_2.8.1_linux_arm64.tar.gz", "919232dcf7ba1b0cd228f808166146cce8f2ea5d691a59bda9cd42a294432ea4", "f4eb76ea6f466ba991e530b7fc6dce57b39da273d8b3013570edeb17dfe6908d", "gentle-ai"),
121
121
  });
122
122
 
123
123
  // A pinned asset is either a signed archive or, for a prerelease pin only,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "note": "Recorded TypeScript diagnostics. Regenerate with `node scripts/check-types.mjs --update` after fixing or deliberately accepting diagnostics. This file may only shrink without a review decision.",
3
- "total": 205,
3
+ "total": 200,
4
4
  "byFileAndCode": {
5
5
  "extensions/codegraph-tools.ts TS2339": 1,
6
6
  "extensions/codegraph-tools.ts TS2345": 1,
@@ -36,8 +36,6 @@
36
36
  "tests/agents-runner-process.test.ts TS2322": 1,
37
37
  "tests/devbinary/native-review-parity.devtest.ts TS2339": 1,
38
38
  "tests/devbinary/pi-host-relay.devtest.ts TS2339": 3,
39
- "tests/gentle-agents.test.ts TS2339": 1,
40
- "tests/gentle-agents.test.ts TS2352": 2,
41
39
  "tests/gentle-ai-binary.test.ts TS2339": 2,
42
40
  "tests/gentle-ai-installer.test.ts TS2339": 7,
43
41
  "tests/gentle-ai-installer.test.ts TS2556": 1,
@@ -87,8 +85,6 @@
87
85
  "tests/runtime-metrics-extension.test.ts TS2345": 1,
88
86
  "tests/sdd-preflight.test.ts TS2352": 1,
89
87
  "tests/sdd-preflight.test.ts TS2554": 2,
90
- "tests/sdd-research-capabilities.test.ts TS2339": 1,
91
- "tests/sdd-research-capabilities.test.ts TS2698": 1,
92
88
  "tests/sdd-research-live.test.ts TS2322": 1,
93
89
  "tests/shell-sidebar-layout.test.ts TS2339": 1,
94
90
  "tests/shell-sidebar-layout.test.ts TS2345": 1,