gentle-pi 3.2.0 → 3.2.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/assets/orchestrator-delegation.md +13 -8
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +15 -5
- package/docs/readme-reference.md +6 -6
- package/docs/review-integration.md +15 -6
- package/extensions/gentle-ai.ts +72 -7
- package/extensions/gentle-shell.ts +29 -7
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +9 -0
- package/lib/opaque-pi-reviewer-adapter.ts +130 -10
- package/lib/review-host-relay.ts +95 -2
- package/lib/shell-bar.ts +63 -9
- package/lib/shell-usage.ts +226 -10
- package/package.json +2 -1
- package/runtime/native-review-cli.mjs +9 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/mirror-odd-routing.mjs +242 -0
- package/scripts/verify-package-files.mjs +3 -3
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-shell.test.ts +109 -3
- package/tests/native-review-capability-contract.test.ts +14 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +57 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +153 -9
- package/tests/package-manifest.test.ts +6 -6
- package/tests/review-controller-native-routing.test.ts +60 -1
- package/tests/review-host-relay.test.ts +83 -14
- package/tests/review-relay-transport-agent.test.ts +86 -1
- package/tests/shell-bar.test.ts +153 -3
- package/tests/shell-usage-view.test.ts +3 -2
- package/tests/shell-usage.test.ts +254 -6
package/lib/shell-usage.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface UsageWindow {
|
|
|
11
11
|
usedPercent: number;
|
|
12
12
|
windowSeconds: number;
|
|
13
13
|
resetAt: number | null;
|
|
14
|
+
// Raw allowance numbers, kept only by providers that report them (NaN).
|
|
15
|
+
// Aggregates are weighted by budget, so averaging percentages is never
|
|
16
|
+
// needed; nothing renders these fields directly.
|
|
17
|
+
used?: number;
|
|
18
|
+
budget?: number;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export interface UsageLimit {
|
|
@@ -54,8 +59,27 @@ interface RawCodexUsage {
|
|
|
54
59
|
additional_rate_limits?: RawAdditionalLimit[] | null;
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
interface RawNanModel {
|
|
63
|
+
model?: unknown;
|
|
64
|
+
cap?: unknown;
|
|
65
|
+
fullCap?: unknown;
|
|
66
|
+
tokensUsed?: unknown;
|
|
67
|
+
periodEnd?: unknown;
|
|
68
|
+
windowHours?: unknown;
|
|
69
|
+
windowTokens?: unknown;
|
|
70
|
+
fullWindowTokens?: unknown;
|
|
71
|
+
windowTokensUsed?: unknown;
|
|
72
|
+
windowResetsAt?: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface RawNanQuota {
|
|
76
|
+
models?: unknown;
|
|
77
|
+
periodEnd?: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
export const CODEX_PROVIDER = "openai-codex";
|
|
58
81
|
export const ANTHROPIC_PROVIDER = "anthropic";
|
|
82
|
+
export const NAN_PROVIDER = "nan";
|
|
59
83
|
const ANTHROPIC_MAIN_LIMIT = "claude";
|
|
60
84
|
const ANTHROPIC_PREFIX = "anthropic-ratelimit-unified-";
|
|
61
85
|
const ANTHROPIC_WINDOWS: ReadonlyArray<[key: string, seconds: number]> = [
|
|
@@ -63,6 +87,17 @@ const ANTHROPIC_WINDOWS: ReadonlyArray<[key: string, seconds: number]> = [
|
|
|
63
87
|
["7d", 604_800],
|
|
64
88
|
];
|
|
65
89
|
export const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
90
|
+
// The NaN Cloud dashboard backend; not part of NaN's published OpenAPI, so the
|
|
91
|
+
// fetch that uses it is fixed-origin, redirect-refusing, and schema-validated.
|
|
92
|
+
export const NAN_QUOTA_URL = "https://cloud-api.nan.builders/api/usage/quota";
|
|
93
|
+
// The model's own allowance for the billing period carries no label: the model
|
|
94
|
+
// id names it in the bar, and the reset text says what the window is in the
|
|
95
|
+
// panel. Only a sub-window on top of it (a rolling `4h`) needs a name.
|
|
96
|
+
const NAN_PERIOD_LABEL = "";
|
|
97
|
+
// The dashboard's own published fallbacks for a model that reports rolling
|
|
98
|
+
// numbers without naming its budget.
|
|
99
|
+
const NAN_DEFAULT_WINDOW_TOKENS = 400_000_000;
|
|
100
|
+
const NAN_DEFAULT_WINDOW_HOURS = 4;
|
|
66
101
|
const CODEX_MAIN_LIMIT = "codex";
|
|
67
102
|
const CODEX_ACCOUNT_CLAIM = "https://api.openai.com/auth";
|
|
68
103
|
const HEADER_PREFIX = "x-codex-";
|
|
@@ -81,9 +116,10 @@ const ROLE = {
|
|
|
81
116
|
SEPARATOR: "muted",
|
|
82
117
|
} as const;
|
|
83
118
|
export const USAGE_EMPTY_MESSAGE = "No subscription usage yet. Usage arrives with the next response, or press r to fetch it.";
|
|
84
|
-
export const SUPPORTED_USAGE_PROVIDERS: readonly string[] = [CODEX_PROVIDER, ANTHROPIC_PROVIDER];
|
|
119
|
+
export const SUPPORTED_USAGE_PROVIDERS: readonly string[] = [CODEX_PROVIDER, ANTHROPIC_PROVIDER, NAN_PROVIDER];
|
|
85
120
|
const PENDING_NOTE: Record<string, string> = {
|
|
86
121
|
[CODEX_PROVIDER]: "no usage yet · r to fetch",
|
|
122
|
+
[NAN_PROVIDER]: "no usage yet · r to fetch",
|
|
87
123
|
[ANTHROPIC_PROVIDER]: "usage arrives with the first response",
|
|
88
124
|
};
|
|
89
125
|
const UNSUPPORTED_NOTE = "no subscription usage for this provider";
|
|
@@ -174,6 +210,178 @@ export function parseUsageHeaders(headers: Record<string, string>, now: number):
|
|
|
174
210
|
return parseCodexHeaders(headers, now) ?? parseAnthropicHeaders(headers, now);
|
|
175
211
|
}
|
|
176
212
|
|
|
213
|
+
// NaN Cloud reports one allowance per model for the billing period, plus the
|
|
214
|
+
// rolling window the model applies on top of it. Percentages follow the
|
|
215
|
+
// dashboard exactly: tokens used over the period allowance, and window tokens
|
|
216
|
+
// over the full window budget. Every field is optional, because this payload
|
|
217
|
+
// lives outside NaN's published contract.
|
|
218
|
+
function quotaTimestamp(value: unknown): number | null {
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value * 1000;
|
|
220
|
+
if (typeof value !== "string") return null;
|
|
221
|
+
const parsed = Date.parse(value);
|
|
222
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function quotaNumber(value: unknown, positive: boolean): number | undefined {
|
|
226
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
227
|
+
return positive ? (value > 0 ? value : undefined) : value >= 0 ? value : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function nanRollingWindow(raw: RawNanModel): UsageWindow | undefined {
|
|
231
|
+
const used = quotaNumber(raw.windowTokensUsed, false);
|
|
232
|
+
if (used === undefined) return undefined;
|
|
233
|
+
const budget = quotaNumber(raw.fullWindowTokens, true) ?? quotaNumber(raw.windowTokens, true) ?? NAN_DEFAULT_WINDOW_TOKENS;
|
|
234
|
+
const hours = quotaNumber(raw.windowHours, true) ?? NAN_DEFAULT_WINDOW_HOURS;
|
|
235
|
+
return { label: windowLabel(hours * HOUR), usedPercent: (used / budget) * 100, windowSeconds: hours * HOUR, resetAt: quotaTimestamp(raw.windowResetsAt) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The allowance the dashboard divides by is the full-period cap, because `cap`
|
|
239
|
+
// is the allowance of the period in progress and comes back prorated on a first
|
|
240
|
+
// period. A model that reports neither figure reports no allowance at all, which
|
|
241
|
+
// is a state the surfaces already know how to draw nothing for.
|
|
242
|
+
function nanEffectiveAllowance(raw: RawNanModel): number | undefined {
|
|
243
|
+
return quotaNumber(raw.fullCap, true) ?? quotaNumber(raw.cap, true);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// One allowance per metered model, weighted by that model's own cap. The raw
|
|
247
|
+
// numbers travel with the period window so the bar and the panel can aggregate
|
|
248
|
+
// without ever averaging percentages.
|
|
249
|
+
function nanPeriodWindow(tokensUsed: number, cap: number, resetAt: number | null, now: number): UsageWindow {
|
|
250
|
+
return {
|
|
251
|
+
label: NAN_PERIOD_LABEL,
|
|
252
|
+
usedPercent: (tokensUsed / cap) * 100,
|
|
253
|
+
windowSeconds: resetAt === null ? 0 : Math.max(0, Math.round((resetAt - now) / 1000)),
|
|
254
|
+
resetAt,
|
|
255
|
+
used: tokensUsed,
|
|
256
|
+
budget: cap,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function parseNanQuota(payload: unknown, now: number): ProviderUsage {
|
|
261
|
+
const raw = (payload ?? {}) as RawNanQuota;
|
|
262
|
+
const fallbackResetAt = quotaTimestamp(raw.periodEnd);
|
|
263
|
+
const limits: UsageLimit[] = [];
|
|
264
|
+
if (Array.isArray(raw.models)) {
|
|
265
|
+
for (const entry of raw.models) {
|
|
266
|
+
if (!entry || typeof entry !== "object") continue;
|
|
267
|
+
const model = entry as RawNanModel;
|
|
268
|
+
if (typeof model.model !== "string" || model.model.length === 0) continue;
|
|
269
|
+
const allowance = nanEffectiveAllowance(model);
|
|
270
|
+
// A model that reports no allowance is not drift — the dashboard draws
|
|
271
|
+
// nothing for it either, and the live payload carries such entries. A metered
|
|
272
|
+
// allowance whose usage cannot be read is drift: a partial snapshot would
|
|
273
|
+
// understate every aggregate it feeds, so the read fails whole and the last
|
|
274
|
+
// valid snapshot survives instead.
|
|
275
|
+
if (allowance === undefined) continue;
|
|
276
|
+
const tokensUsed = quotaNumber(model.tokensUsed, false);
|
|
277
|
+
if (tokensUsed === undefined) return { provider: NAN_PROVIDER, plan: undefined, limits: [], fetchedAt: now };
|
|
278
|
+
const resetAt = quotaTimestamp(model.periodEnd) ?? fallbackResetAt;
|
|
279
|
+
const windows: UsageWindow[] = [nanPeriodWindow(tokensUsed, allowance, resetAt, now)];
|
|
280
|
+
const rolling = nanRollingWindow(model);
|
|
281
|
+
if (rolling) windows.push(rolling);
|
|
282
|
+
limits.push({ name: model.model, windows, limitReached: tokensUsed >= allowance });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { provider: NAN_PROVIDER, plan: undefined, limits, fetchedAt: now };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Aggregation. NaN reports one allowance per metered model and the payload
|
|
289
|
+
// order is the server's business, so surfaces pick by meaning, not by position.
|
|
290
|
+
function rawAllowance(limit: UsageLimit): UsageWindow | undefined {
|
|
291
|
+
const [first] = limit.windows;
|
|
292
|
+
if (!first || first.used === undefined || first.budget === undefined) return undefined;
|
|
293
|
+
return first.budget > 0 ? first : undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// The leading alphabetic run of a model id: glm5.3-flash and glm5.2 are both
|
|
297
|
+
// "glm". Derived from the id the payload reports, never from a vendor list.
|
|
298
|
+
export function modelFamily(modelId: string): string {
|
|
299
|
+
return (/^[a-z]+/i.exec(modelId)?.[0] ?? modelId).toLowerCase();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Only NaN carries raw allowance numbers, so this one gate is what keeps Codex
|
|
303
|
+
// and Anthropic on exactly the rows and the meter they had before. One metered
|
|
304
|
+
// model is still a payload that carries them: the gate answers "does this
|
|
305
|
+
// provider report allowances", never "are there enough rows to sort", because
|
|
306
|
+
// a single allowance read as "no allowances" sent the bar back to whichever
|
|
307
|
+
// model the payload listed first.
|
|
308
|
+
export function allowanceGroupsSupported(limits: readonly UsageLimit[]): boolean {
|
|
309
|
+
return limits.length > 0 && limits.every((limit) => rawAllowance(limit) !== undefined);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function percentOf(limit: UsageLimit): number {
|
|
313
|
+
return limit.windows[0]?.usedPercent ?? 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const GROUP_SUFFIX = " total";
|
|
317
|
+
|
|
318
|
+
// A group is an allowance share, never an average of shares: Σused / Σbudget.
|
|
319
|
+
// It carries no reset, because its members close their own billing period on
|
|
320
|
+
// their own date, and a single reset would be a lie.
|
|
321
|
+
function allowanceTotal(name: string, limits: readonly UsageLimit[]): UsageLimit | undefined {
|
|
322
|
+
const windows = limits.map(rawAllowance).filter((window): window is UsageWindow => window !== undefined);
|
|
323
|
+
if (windows.length === 0 || windows.length !== limits.length) return undefined;
|
|
324
|
+
const used = windows.reduce((total, window) => total + (window.used ?? 0), 0);
|
|
325
|
+
const budget = windows.reduce((total, window) => total + (window.budget ?? 0), 0);
|
|
326
|
+
if (budget <= 0) return undefined;
|
|
327
|
+
return {
|
|
328
|
+
name,
|
|
329
|
+
windows: [{ label: NAN_PERIOD_LABEL, usedPercent: (used / budget) * 100, windowSeconds: 0, resetAt: null }],
|
|
330
|
+
limitReached: limits.some((limit) => limit.limitReached),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// What the grouping is for now that the totals are gone: the order. A family
|
|
335
|
+
// stays together, families sort by what they consume and the members inside one
|
|
336
|
+
// follow the same rule, most used first. The account and family totals are the
|
|
337
|
+
// bar's fallback ladder only — they are never rows, because a total nobody can
|
|
338
|
+
// act on only costs space. Every row is a limit block, so nothing here
|
|
339
|
+
// introduces a shape the other providers do not already use.
|
|
340
|
+
export function groupUsageLimits(limits: readonly UsageLimit[]): UsageLimit[] {
|
|
341
|
+
if (!allowanceGroupsSupported(limits)) return [...limits];
|
|
342
|
+
const order: string[] = [];
|
|
343
|
+
const members = new Map<string, UsageLimit[]>();
|
|
344
|
+
for (const limit of limits) {
|
|
345
|
+
const family = modelFamily(limit.name);
|
|
346
|
+
if (!members.has(family)) {
|
|
347
|
+
members.set(family, []);
|
|
348
|
+
order.push(family);
|
|
349
|
+
}
|
|
350
|
+
members.get(family)?.push(limit);
|
|
351
|
+
}
|
|
352
|
+
return order
|
|
353
|
+
.map((family) => {
|
|
354
|
+
const sorted = [...(members.get(family) ?? [])].sort((a, b) => percentOf(b) - percentOf(a));
|
|
355
|
+
const total = allowanceTotal(`${family}${GROUP_SUFFIX}`, sorted);
|
|
356
|
+
return { percent: total?.windows[0]?.usedPercent ?? percentOf(sorted[0]), sorted };
|
|
357
|
+
})
|
|
358
|
+
.sort((a, b) => b.percent - a.percent)
|
|
359
|
+
.flatMap((family) => family.sorted);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The bar follows the model the session is using: exact allowance, then its
|
|
363
|
+
// family, then the account total, then the first limit (which is what every
|
|
364
|
+
// provider without raw numbers keeps using, and what a missing model keeps).
|
|
365
|
+
export function selectUsageLimit(usage: ProviderUsage, activeModelId?: string): UsageLimit | undefined {
|
|
366
|
+
if (!activeModelId) return usage.limits[0];
|
|
367
|
+
const exact = usage.limits.find((limit) => limit.name === activeModelId);
|
|
368
|
+
if (exact) return exact;
|
|
369
|
+
if (allowanceGroupsSupported(usage.limits)) {
|
|
370
|
+
const family = modelFamily(activeModelId);
|
|
371
|
+
const members = usage.limits.filter((limit) => modelFamily(limit.name) === family);
|
|
372
|
+
// The family rung is about the name of the meter, not about printing a row,
|
|
373
|
+
// so a single member counts: its family is a closer statement of what the
|
|
374
|
+
// session is drawing from than the whole account.
|
|
375
|
+
if (members.length > 0) {
|
|
376
|
+
const total = allowanceTotal(`${family}${GROUP_SUFFIX}`, members);
|
|
377
|
+
if (total) return total;
|
|
378
|
+
}
|
|
379
|
+
const account = allowanceTotal(`${usage.provider}${GROUP_SUFFIX}`, usage.limits);
|
|
380
|
+
if (account) return account;
|
|
381
|
+
}
|
|
382
|
+
return usage.limits[0];
|
|
383
|
+
}
|
|
384
|
+
|
|
177
385
|
export function accountIdFromToken(token: string): string | undefined {
|
|
178
386
|
const parts = token.split(".");
|
|
179
387
|
if (parts.length !== 3) return undefined;
|
|
@@ -190,11 +398,12 @@ function paintMeter(percent: number, cells: number, theme: UsageTheme): string {
|
|
|
190
398
|
return paintGauge(percent, theme, cells);
|
|
191
399
|
}
|
|
192
400
|
|
|
193
|
-
export function renderUsageBar(usage: ProviderUsage, theme: UsageTheme): string | undefined {
|
|
194
|
-
const main = usage
|
|
401
|
+
export function renderUsageBar(usage: ProviderUsage, theme: UsageTheme, activeModelId?: string): string | undefined {
|
|
402
|
+
const main = selectUsageLimit(usage, activeModelId);
|
|
195
403
|
const [first, ...rest] = main?.windows ?? [];
|
|
196
404
|
if (!first) return undefined;
|
|
197
|
-
|
|
405
|
+
// An unlabeled window prints as the name, the meter and the percentage.
|
|
406
|
+
const head = [theme.fg(ROLE.LABEL, main.name), ...(first.label.length === 0 ? [] : [theme.fg(ROLE.LABEL, first.label)]), paintMeter(first.usedPercent, 8, theme), theme.fg(ROLE.PERCENT, `${Math.round(first.usedPercent)}%`)].join(" ");
|
|
198
407
|
const tail = rest.map((window) => `${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.LABEL, window.label)} ${theme.fg(ROLE.PERCENT, `${Math.round(window.usedPercent)}%`)}`);
|
|
199
408
|
return [head, ...tail].join(" ");
|
|
200
409
|
}
|
|
@@ -218,12 +427,19 @@ export function renderUsagePanel(usages: ProviderUsage[], theme: UsageTheme, wid
|
|
|
218
427
|
const mark = usage === activeUsage ? `${theme.fg(ROLE.LIMIT, ACTIVE_MARK)} ` : "";
|
|
219
428
|
const plan = usage.plan ? ` ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.PLAN, usage.plan)}` : "";
|
|
220
429
|
lines.push(`${mark}${theme.fg(ROLE.PROVIDER, usage.provider)}${plan} ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.RESET, updatedAgo(usage.fetchedAt, now))}`);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
430
|
+
// One row per window: the limit name, its meter, its percentage and the reset
|
|
431
|
+
// that window reports, all on one line. A window without its own label (the
|
|
432
|
+
// model's allowance) is named by its limit alone, and one without a reset ends
|
|
433
|
+
// at its percentage, never on a dangling separator.
|
|
434
|
+
const rows = groupUsageLimits(usage.limits).flatMap((limit) =>
|
|
435
|
+
limit.windows.map((window) => ({ name: [limit.name, window.label].filter((part) => part.length > 0).join(" "), window })),
|
|
436
|
+
);
|
|
437
|
+
const nameWidth = rows.reduce((widest, row) => Math.max(widest, row.name.length), 0);
|
|
438
|
+
for (const row of rows) {
|
|
439
|
+
const percent = `${Math.round(row.window.usedPercent)}%`.padStart(4);
|
|
440
|
+
const reset = formatReset(row.window.resetAt, now);
|
|
441
|
+
const tail = reset.length > 0 ? ` ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.RESET, reset)}` : "";
|
|
442
|
+
lines.push(` ${theme.fg(ROLE.LABEL, row.name.padEnd(nameWidth))} ${paintMeter(row.window.usedPercent, PANEL_METER_CELLS, theme)} ${theme.fg(ROLE.PERCENT, percent)}${tail}`);
|
|
227
443
|
}
|
|
228
444
|
}
|
|
229
445
|
return lines.map((line) => truncateToWidth(line, width, "…"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.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",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"scripts": {
|
|
40
40
|
"check:provider-contract": "node scripts/check-provider-contract.mjs",
|
|
41
|
+
"mirror:odd-routing": "node scripts/mirror-odd-routing.mjs",
|
|
41
42
|
"postinstall": "node scripts/install-gentle-ai.mjs",
|
|
42
43
|
"test": "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
|
|
43
44
|
"test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
|
|
@@ -996,6 +996,15 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
996
996
|
// remain dark because neither is proven to reach the negotiated START
|
|
997
997
|
// path Pi consumes.
|
|
998
998
|
"3.1.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 }),
|
|
999
|
+
// v3.2.1 changed the ODD orchestrator contract only (gentle-ai #4714
|
|
1000
|
+
// follow-up). Ground-truthed by diffing contracts/review-integration/v2 and
|
|
1001
|
+
// contracts/review-provider-contract between the v3.1.0 and v3.2.1 tags
|
|
1002
|
+
// in the gentle-ai source tree: zero bytes changed (provider contract
|
|
1003
|
+
// stays 1.2.0). Neither change touches the closed START/STATUS fields
|
|
1004
|
+
// this row negotiates, so it repeats 3.1.0 exactly. riskEvidence and hint
|
|
1005
|
+
// remain dark because neither is proven to reach the negotiated START
|
|
1006
|
+
// path Pi consumes.
|
|
1007
|
+
"3.2.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 }),
|
|
999
1008
|
});
|
|
1000
1009
|
|
|
1001
1010
|
|
|
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
36
36
|
// version check below) derives from this constant instead of repeating the
|
|
37
37
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
38
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
-
export const INSTALLER_VERSION = "3.1
|
|
39
|
+
export const INSTALLER_VERSION = "3.2.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/v3/cmd/gentle-ai";
|
|
46
46
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
|
|
47
47
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.1
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.2.1`
|
|
49
49
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
-
// tag resolves to commit
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
50
|
+
// tag resolves to commit e7729359, the published v3.2.1 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:0QFo0ERv8/3lgTepEG0E/P7yD9qXXCx2M7ppQviVHMo=";
|
|
52
52
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
53
53
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
54
54
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
67
67
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
68
68
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
69
69
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
70
|
-
// still holds it. The v3.1
|
|
70
|
+
// still holds it. The v3.2.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 v3.1
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v3.2.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_3.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_3.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_3.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_3.
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_3.2.1_darwin_amd64.tar.gz", "1696a4852435920ebae351a5173adead6da17db6762359dcb12dc3971d983c3f", "ed7d18b7446c63f265a827a736ed4465ec562cc3c07c470743d3740fb4d2f4a9", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_3.2.1_darwin_arm64.tar.gz", "e3a991a03c6fb6373f6a254b808c3fabf36330634d93704c9d9ba365467fc6f8", "2ae66cfa11f8ab93ea46c20e79bd191dedde40a4898f0b312330a0790f802ca3", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_3.2.1_linux_amd64.tar.gz", "e2e3171377c040c27a93ae66b4adf824626f23cacd398b3b652af0d87ff65807", "3464525775b60cad6358a589c298a2d4098466bc0a5ae5f0d1e578ab10134659", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_3.2.1_linux_arm64.tar.gz", "342c7fc60cf062730c8f593f09269f75485b3b1fa2907b2b188eafded1a0e222", "d2c4d8ebfdc681ceece254d0d12cd04dd7af3b435c945e42623e3475981c2f8f", "gentle-ai"),
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ODD routing drift ratchet — canonical fixture regeneration.
|
|
3
|
+
//
|
|
4
|
+
// gentle-pi hand-mirrors the always-on ODD routing block rendered by gentle-ai
|
|
5
|
+
// `internal/components/agentguidance/routing.go` (RenderRouting). There is no
|
|
6
|
+
// automated sync, so a canonical change leaves the pi mirror stale with nothing
|
|
7
|
+
// catching it. This script renders the canonical block from a LOCAL gentle-ai
|
|
8
|
+
// checkout and vendors it into `fixtures/odd-routing-canonical.md` with managed
|
|
9
|
+
// provenance, where `tests/odd-routing-canonical-ratchet.test.ts` turns drift
|
|
10
|
+
// into a failing test.
|
|
11
|
+
//
|
|
12
|
+
// This script NEVER fetches anything from the network (GOPROXY=off), NEVER
|
|
13
|
+
// modifies the gentle-ai checkout beyond a transient temporary Go entrypoint
|
|
14
|
+
// that it always deletes, and NEVER touches the installer release pin
|
|
15
|
+
// (scripts/gentle-ai-installer.mjs INSTALLER_VERSION). Regeneration writes ONLY
|
|
16
|
+
// the fixture; it never auto-rewrites the pi mirror assets.
|
|
17
|
+
//
|
|
18
|
+
// Usage:
|
|
19
|
+
// node scripts/mirror-odd-routing.mjs [--gentle-ai <path to local checkout>]
|
|
20
|
+
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
|
+
|
|
27
|
+
export const ODD_ROUTING_FIXTURE_RELATIVE = "fixtures/odd-routing-canonical.md";
|
|
28
|
+
export const ODD_ROUTING_SOURCE_REPO = "https://github.com/Gentleman-Programming/gentle-ai";
|
|
29
|
+
export const ODD_ROUTING_SOURCE_PATH = "internal/components/agentguidance/routing.go";
|
|
30
|
+
export const ODD_ROUTING_HEADER_OPEN = "<!-- gentle-pi:managed-odd-routing-canonical\n";
|
|
31
|
+
export const ODD_ROUTING_HEADER_CLOSE = "\n-->\n";
|
|
32
|
+
export const ODD_ROUTING_TEMP_DIR = "tmp-odd-routing-dump";
|
|
33
|
+
export const ODD_ROUTING_BLOCK_PREFIX = "## Implementation Routing";
|
|
34
|
+
|
|
35
|
+
const GO_ENTRYPOINT = `package main
|
|
36
|
+
|
|
37
|
+
import (
|
|
38
|
+
"fmt"
|
|
39
|
+
"os"
|
|
40
|
+
|
|
41
|
+
"github.com/gentleman-programming/gentle-ai/v3/internal/components/agentguidance"
|
|
42
|
+
"github.com/gentleman-programming/gentle-ai/v3/internal/model"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
func main() {
|
|
46
|
+
rendered, err := agentguidance.RenderRouting(model.AgentPi)
|
|
47
|
+
if err != nil {
|
|
48
|
+
fmt.Fprintln(os.Stderr, err)
|
|
49
|
+
os.Exit(1)
|
|
50
|
+
}
|
|
51
|
+
fmt.Print(rendered)
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export function sha256Hex(value) {
|
|
56
|
+
return createHash("sha256").update(value).digest("hex");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseOddRoutingFixture(raw) {
|
|
60
|
+
if (!raw.startsWith(ODD_ROUTING_HEADER_OPEN)) {
|
|
61
|
+
throw new Error(`fixture does not start with the managed header ${JSON.stringify(ODD_ROUTING_HEADER_OPEN)}`);
|
|
62
|
+
}
|
|
63
|
+
const headerEnd = raw.indexOf(ODD_ROUTING_HEADER_CLOSE, ODD_ROUTING_HEADER_OPEN.length);
|
|
64
|
+
if (headerEnd === -1) {
|
|
65
|
+
throw new Error(`fixture managed header is not terminated by ${JSON.stringify(ODD_ROUTING_HEADER_CLOSE)}`);
|
|
66
|
+
}
|
|
67
|
+
const headerText = raw.slice(ODD_ROUTING_HEADER_OPEN.length, headerEnd);
|
|
68
|
+
const body = raw.slice(headerEnd + ODD_ROUTING_HEADER_CLOSE.length);
|
|
69
|
+
const header = {};
|
|
70
|
+
for (const line of headerText.split("\n")) {
|
|
71
|
+
const separator = line.indexOf(": ");
|
|
72
|
+
if (separator === -1) continue;
|
|
73
|
+
header[line.slice(0, separator)] = line.slice(separator + 2);
|
|
74
|
+
}
|
|
75
|
+
return { header, body };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function renderOddRoutingFixture(block, { sourceCommit, generatedAt }) {
|
|
79
|
+
const body = block.endsWith("\n") ? block : `${block}\n`;
|
|
80
|
+
const header = [
|
|
81
|
+
ODD_ROUTING_HEADER_OPEN.trimEnd(),
|
|
82
|
+
`source_repo: ${ODD_ROUTING_SOURCE_REPO}`,
|
|
83
|
+
`source_path: ${ODD_ROUTING_SOURCE_PATH}`,
|
|
84
|
+
`source_commit: ${sourceCommit}`,
|
|
85
|
+
`generated_at: ${generatedAt}`,
|
|
86
|
+
`block_sha256: ${sha256Hex(body)}`,
|
|
87
|
+
"-->",
|
|
88
|
+
].join("\n");
|
|
89
|
+
return `${header}\n${body}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// `git status --porcelain --untracked-files=no` emits one `XY PATH` line per
|
|
93
|
+
// modified tracked file. Extract just the repository-relative path so the
|
|
94
|
+
// fail-closed error can name what would make provenance unverifiable.
|
|
95
|
+
export function parsePorcelainPaths(porcelainOutput) {
|
|
96
|
+
return porcelainOutput
|
|
97
|
+
.split("\n")
|
|
98
|
+
.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line))
|
|
99
|
+
.filter((line) => line.length > 0)
|
|
100
|
+
.map((line) => line.slice(3).trim())
|
|
101
|
+
.filter((path) => path.length > 0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fail closed when the checkout carries uncommitted changes to tracked files.
|
|
105
|
+
// The fixture header asserts the rendered block came from the committed tree;
|
|
106
|
+
// rendering a dirty tree under that claim would make the provenance a lie.
|
|
107
|
+
// Untracked files never reach this guard because the git call excludes them.
|
|
108
|
+
export function assertCleanGentleAiCheckout(porcelainOutput) {
|
|
109
|
+
const dirtyPaths = parsePorcelainPaths(porcelainOutput);
|
|
110
|
+
if (dirtyPaths.length > 0) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`gentle-ai checkout is dirty; provenance would be unverifiable: ${dirtyPaths.join(", ")}. Commit or stash first.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseArguments(argv) {
|
|
118
|
+
const values = { gentleAi: undefined };
|
|
119
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
120
|
+
const flag = argv[index];
|
|
121
|
+
const value = argv[index + 1];
|
|
122
|
+
if (flag !== "--gentle-ai" || value === undefined) {
|
|
123
|
+
throw new Error("usage: node scripts/mirror-odd-routing.mjs [--gentle-ai <path to local checkout>]");
|
|
124
|
+
}
|
|
125
|
+
if (values.gentleAi !== undefined) throw new Error("duplicate --gentle-ai argument");
|
|
126
|
+
values.gentleAi = value;
|
|
127
|
+
}
|
|
128
|
+
return values;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function requireGentleAiCheckout(gentleAiRoot) {
|
|
132
|
+
if (!existsSync(gentleAiRoot)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`no gentle-ai checkout at ${gentleAiRoot}; pass --gentle-ai <path> or clone ${ODD_ROUTING_SOURCE_REPO} as a sibling of gentle-pi`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const routingPath = join(gentleAiRoot, ...ODD_ROUTING_SOURCE_PATH.split("/"));
|
|
138
|
+
if (!existsSync(routingPath)) {
|
|
139
|
+
throw new Error(`not a gentle-ai checkout: missing ${routingPath}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function renderCanonicalRouting(gentleAiRoot) {
|
|
144
|
+
const tempRoot = join(gentleAiRoot, ODD_ROUTING_TEMP_DIR);
|
|
145
|
+
// Clear any stale temp entrypoint from a previous crash so the render is clean.
|
|
146
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
147
|
+
mkdirSync(tempRoot, { recursive: true });
|
|
148
|
+
try {
|
|
149
|
+
writeFileSync(join(tempRoot, "main.go"), GO_ENTRYPOINT, { encoding: "utf8" });
|
|
150
|
+
const rendered = execFileSync("go", ["run", `./${ODD_ROUTING_TEMP_DIR}`], {
|
|
151
|
+
cwd: gentleAiRoot,
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
// GOPROXY=off fails closed instead of downloading: this script must
|
|
154
|
+
// never touch the network.
|
|
155
|
+
env: { ...process.env, GOPROXY: "off" },
|
|
156
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
157
|
+
});
|
|
158
|
+
if (!rendered.startsWith(ODD_ROUTING_BLOCK_PREFIX)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`RenderRouting(model.AgentPi) did not return the expected routing block (starts with ${JSON.stringify(ODD_ROUTING_BLOCK_PREFIX)})`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return rendered;
|
|
164
|
+
} finally {
|
|
165
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Resolve fixture provenance from the committed gentle-ai tree. `generatedAt`
|
|
170
|
+
// is the committer date, not wall-clock time, so rendering the same commit
|
|
171
|
+
// twice yields byte-identical fixture files. `execGit` is injectable for tests.
|
|
172
|
+
export function resolveOddRoutingProvenance(gentleAiRoot, execGit) {
|
|
173
|
+
const git = execGit ?? ((args) => execFileSync("git", args, { cwd: gentleAiRoot, encoding: "utf8" }));
|
|
174
|
+
assertCleanGentleAiCheckout(git(["status", "--porcelain", "--untracked-files=no"]));
|
|
175
|
+
return {
|
|
176
|
+
sourceCommit: git(["rev-parse", "HEAD"]).trim(),
|
|
177
|
+
generatedAt: git(["show", "-s", "--format=%cI", "HEAD"]).trim(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function mirrorOddRouting(packageRoot, gentleAiRoot) {
|
|
182
|
+
requireGentleAiCheckout(gentleAiRoot);
|
|
183
|
+
// Resolve (and fail closed on dirty) provenance BEFORE rendering: the
|
|
184
|
+
// transient Go entrypoint must not run against a tree whose commit hash the
|
|
185
|
+
// fixture would misrepresent.
|
|
186
|
+
const provenance = resolveOddRoutingProvenance(gentleAiRoot);
|
|
187
|
+
const block = renderCanonicalRouting(gentleAiRoot);
|
|
188
|
+
const contents = renderOddRoutingFixture(block, provenance);
|
|
189
|
+
|
|
190
|
+
const fixturePath = join(resolve(packageRoot), ...ODD_ROUTING_FIXTURE_RELATIVE.split("/"));
|
|
191
|
+
const previous = readPreviousFixture(fixturePath);
|
|
192
|
+
mkdirSync(dirname(fixturePath), { recursive: true });
|
|
193
|
+
|
|
194
|
+
// Atomic overwrite: write a sibling temp file, then rename it into place.
|
|
195
|
+
const stagingPath = `${fixturePath}.tmp`;
|
|
196
|
+
try {
|
|
197
|
+
writeFileSync(stagingPath, contents, { encoding: "utf8" });
|
|
198
|
+
renameSync(stagingPath, fixturePath);
|
|
199
|
+
} finally {
|
|
200
|
+
rmSync(stagingPath, { force: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const next = parseOddRoutingFixture(contents);
|
|
204
|
+
return {
|
|
205
|
+
fixturePath,
|
|
206
|
+
sourceCommit: next.header.source_commit,
|
|
207
|
+
blockSha256: next.header.block_sha256,
|
|
208
|
+
changed: previous === undefined || previous.body !== next.body || previous.header.source_commit !== next.header.source_commit,
|
|
209
|
+
previous,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readPreviousFixture(fixturePath) {
|
|
214
|
+
if (!existsSync(fixturePath)) return undefined;
|
|
215
|
+
const raw = readFileSync(fixturePath, "utf8");
|
|
216
|
+
// Verify the existing fixture before clobbering it: an unparseable file is a
|
|
217
|
+
// signal to inspect, not to silently overwrite.
|
|
218
|
+
const parsed = parseOddRoutingFixture(raw);
|
|
219
|
+
const recorded = parsed.header.block_sha256;
|
|
220
|
+
const actual = sha256Hex(parsed.body);
|
|
221
|
+
if (recorded !== undefined && recorded !== actual) {
|
|
222
|
+
console.warn(`warning: existing fixture body does not match its recorded digest (${recorded}); regenerating from canonical`);
|
|
223
|
+
}
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function main() {
|
|
228
|
+
const packageRoot = join(fileURLToPath(new URL("..", import.meta.url)));
|
|
229
|
+
const { gentleAi } = parseArguments(process.argv.slice(2));
|
|
230
|
+
const gentleAiRoot = resolve(gentleAi ?? join(packageRoot, "..", "gentle-ai"));
|
|
231
|
+
const result = mirrorOddRouting(packageRoot, gentleAiRoot);
|
|
232
|
+
console.log(`${result.changed ? "Wrote" : "Refreshed"} ${result.fixturePath}`);
|
|
233
|
+
console.log(`source commit ${result.sourceCommit}; block sha256 ${result.blockSha256}`);
|
|
234
|
+
if (result.previous !== undefined) {
|
|
235
|
+
console.log(`previous commit ${result.previous.header.source_commit}; previous block sha256 ${result.previous.header.block_sha256}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
240
|
+
if (isMainModule) {
|
|
241
|
+
await main();
|
|
242
|
+
}
|
|
@@ -181,7 +181,7 @@ const contractHashes = {
|
|
|
181
181
|
"contracts/review-integration/v2/schemas/start.schema.json": "2991e3fcca672d9257d61b6a336fb34e58b15a8e03f8a09a7adf892cae6a8085",
|
|
182
182
|
"contracts/review-integration/v2/schemas/status.schema.json": "c4dcc736cfc6300560a3c4262d2d982368529d5c49d58d499552a3b0beef9212",
|
|
183
183
|
"contracts/telemetry/runtime-aggregate-v1.schema.json": "eb0f2993d9271f55cb42eca343e6fbb601a733fb90bd40daeebc92ee60ae1ba9",
|
|
184
|
-
"docs/review-integration.md": "
|
|
184
|
+
"docs/review-integration.md": "9868cd52ae8f15cc2e6a6757014d5b12ff0904e06725d1a4cf29e9bcc3a7bae2",
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
requiredPaths.push(...Object.keys(contractHashes));
|
|
@@ -339,7 +339,7 @@ async function main() {
|
|
|
339
339
|
});
|
|
340
340
|
|
|
341
341
|
if (driftedContracts.length > 0) {
|
|
342
|
-
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.1
|
|
342
|
+
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.2.1 runtime's vendored Gentle AI contract artifacts:");
|
|
343
343
|
for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
|
|
344
344
|
process.exit(1);
|
|
345
345
|
}
|
|
@@ -384,7 +384,7 @@ async function main() {
|
|
|
384
384
|
process.exit(1);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.1
|
|
387
|
+
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.2.1 runtime).`);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -97,7 +97,7 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
|
|
|
97
97
|
method: "go-sumdb-source-build",
|
|
98
98
|
package: "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
|
|
99
99
|
module: "github.com/gentleman-programming/gentle-ai/v3",
|
|
100
|
-
tag: "v3.1
|
|
100
|
+
tag: "v3.2.1",
|
|
101
101
|
architecture: process.arch === "x64" ? "x64" : "arm64",
|
|
102
102
|
binarySha256: createHash("sha256").update(binary).digest("hex"),
|
|
103
103
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|