gentle-pi 3.1.1 → 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 +18 -11
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +15 -5
- package/docs/readme-reference.md +43 -18
- package/docs/review-integration.md +15 -6
- package/extensions/gentle-agents.ts +22 -1
- package/extensions/gentle-ai.ts +103 -143
- package/extensions/gentle-shell.ts +29 -7
- package/lib/background-subagents-policy.ts +148 -0
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +18 -0
- package/lib/opaque-pi-reviewer-adapter.ts +130 -10
- package/lib/review-candidate-view.ts +27 -6
- 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 +18 -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/skills/chained-pr/SKILL.md +2 -1
- package/skills/work-unit-commits/SKILL.md +9 -0
- package/tests/background-subagents-default-mode.test.ts +105 -0
- package/tests/gentle-agents.test.ts +14 -1
- 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 +27 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +101 -2
- package/tests/opaque-pi-reviewer-adapter.test.ts +153 -9
- package/tests/package-manifest.test.ts +6 -6
- package/tests/review-base-ref-hint.test.ts +39 -0
- package/tests/review-candidate-view.test.ts +55 -0
- 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/extensions/gentle-ai.ts
CHANGED
|
@@ -34,6 +34,15 @@ import type {
|
|
|
34
34
|
} from "@earendil-works/pi-coding-agent";
|
|
35
35
|
import { Key, isKeyRelease, matchesKey, truncateToWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
36
36
|
import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
37
|
+
import {
|
|
38
|
+
BACKGROUND_SUBAGENTS_FILE,
|
|
39
|
+
BACKGROUND_SUBAGENTS_SCHEMA,
|
|
40
|
+
loadBackgroundSubagentsPolicy,
|
|
41
|
+
parseBackgroundSubagentsPolicyFile,
|
|
42
|
+
resolveBackgroundSubagentsPolicy,
|
|
43
|
+
type BackgroundSubagentsPolicy,
|
|
44
|
+
type BackgroundSubagentsResolution,
|
|
45
|
+
} from "../lib/background-subagents-policy.ts";
|
|
37
46
|
import {
|
|
38
47
|
ensureSddPreflight,
|
|
39
48
|
getSddPreflightPreferences,
|
|
@@ -127,6 +136,7 @@ import {
|
|
|
127
136
|
reviewHostRelayUnachievableDetail,
|
|
128
137
|
reviewHostRelayUnachievableReason,
|
|
129
138
|
reviewProviderRoleVectorSlots,
|
|
139
|
+
resolveReviewHostRelayExtensionPaths,
|
|
130
140
|
resolveReviewHostRelaySubmission,
|
|
131
141
|
runReviewHostRelayReviewerGroup,
|
|
132
142
|
runReviewHostRelaySlot,
|
|
@@ -158,7 +168,7 @@ import {
|
|
|
158
168
|
} from "../lib/review-snapshot.ts";
|
|
159
169
|
import { renderGentleAiLifecycleCall, renderGentleAiResult, type GentleAiRenderContext } from "../lib/gentle-ai-renderer.ts";
|
|
160
170
|
import { sanitizeTerminalText, stripAnsi } from "../lib/terminal-theme.ts";
|
|
161
|
-
import { CandidateViewError, CandidateViewRegistry, injectReviewCandidateView, readCandidateContextManifestPage, resolveCanonicalCandidateBase, type CandidateView } from "../lib/review-candidate-view.ts";
|
|
171
|
+
import { BASE_REF_ACCEPTED_FORMS, CandidateViewError, CandidateViewRegistry, injectReviewCandidateView, readCandidateContextManifestPage, resolveCanonicalCandidateBase, type CandidateView } from "../lib/review-candidate-view.ts";
|
|
162
172
|
import {
|
|
163
173
|
GentleAiDevBinaryOverrideError,
|
|
164
174
|
GENTLE_AI_INSTALL_RECOVERY_COMMAND,
|
|
@@ -349,9 +359,16 @@ function localAgentOverrideCount(cwd: string, owner: PackageAssetOwner): number
|
|
|
349
359
|
|
|
350
360
|
// ---------------------------------------------------------------------------
|
|
351
361
|
// Background subagents policy — project > global > env > default off
|
|
362
|
+
//
|
|
363
|
+
// The pure resolver (parseBackgroundSubagentsPolicyFile,
|
|
364
|
+
// resolveBackgroundSubagentsPolicy, loadBackgroundSubagentsPolicy, and their
|
|
365
|
+
// types/constants) lives in lib/background-subagents-policy.ts so the
|
|
366
|
+
// runtime side (extensions/gentle-agents.ts) can read the effective policy
|
|
367
|
+
// without importing the pi extension surface. Everything below this point
|
|
368
|
+
// (capability probing, report rendering, the global-file writer) stays here
|
|
369
|
+
// because it is specific to this extension's UI-facing surface.
|
|
352
370
|
// ---------------------------------------------------------------------------
|
|
353
371
|
|
|
354
|
-
type BackgroundSubagentsPolicy = "on" | "off";
|
|
355
372
|
type BackgroundSubagentsCapability = "ready" | "absent";
|
|
356
373
|
|
|
357
374
|
interface BackgroundSubagentsRendering {
|
|
@@ -359,142 +376,11 @@ interface BackgroundSubagentsRendering {
|
|
|
359
376
|
capability: BackgroundSubagentsCapability;
|
|
360
377
|
}
|
|
361
378
|
|
|
362
|
-
/** Which of the four sources decided the effective policy. */
|
|
363
|
-
type BackgroundSubagentsSource =
|
|
364
|
-
| "project_file"
|
|
365
|
-
| "global_file"
|
|
366
|
-
| "environment"
|
|
367
|
-
| "default";
|
|
368
|
-
|
|
369
|
-
interface BackgroundSubagentsResolution {
|
|
370
|
-
policy: BackgroundSubagentsPolicy;
|
|
371
|
-
source: BackgroundSubagentsSource;
|
|
372
|
-
/** The deciding file was present but failed the strict decode. */
|
|
373
|
-
malformed: boolean;
|
|
374
|
-
projectFile: string;
|
|
375
|
-
globalFile: string;
|
|
376
|
-
projectFileExists: boolean;
|
|
377
|
-
globalFileExists: boolean;
|
|
378
|
-
/** The raw env value, reported even when it is unrecognized and inert. */
|
|
379
|
-
envValue: string | undefined;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
interface LoadBackgroundSubagentsOptions {
|
|
383
|
-
/** Override the config home directory (used in tests to avoid touching ~/.pi). */
|
|
384
|
-
gentlePiConfigHome?: string;
|
|
385
|
-
/** Override the environment lookup (used in tests). */
|
|
386
|
-
env?: Record<string, string | undefined>;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const BACKGROUND_SUBAGENTS_SCHEMA = "gentle-pi.background-subagents/v1";
|
|
390
|
-
const BACKGROUND_SUBAGENTS_FILE = "background-subagents.json";
|
|
391
|
-
|
|
392
379
|
const DEFAULT_BACKGROUND_SUBAGENTS_RENDERING: BackgroundSubagentsRendering = {
|
|
393
380
|
policy: "off",
|
|
394
381
|
capability: "absent",
|
|
395
382
|
};
|
|
396
383
|
|
|
397
|
-
/**
|
|
398
|
-
* Strict decode of {"schema":"gentle-pi.background-subagents/v1","policy":"on"|"off"}.
|
|
399
|
-
* Any malformed shape (bad JSON, wrong schema, unknown keys, invalid policy)
|
|
400
|
-
* returns undefined so the caller fails closed to "off".
|
|
401
|
-
*/
|
|
402
|
-
function parseBackgroundSubagentsPolicyFile(
|
|
403
|
-
raw: string,
|
|
404
|
-
): BackgroundSubagentsPolicy | undefined {
|
|
405
|
-
let parsed: unknown;
|
|
406
|
-
try {
|
|
407
|
-
parsed = JSON.parse(raw);
|
|
408
|
-
} catch {
|
|
409
|
-
return undefined;
|
|
410
|
-
}
|
|
411
|
-
if (!isRecord(parsed)) return undefined;
|
|
412
|
-
if (parsed.schema !== BACKGROUND_SUBAGENTS_SCHEMA) return undefined;
|
|
413
|
-
if (parsed.policy !== "on" && parsed.policy !== "off") return undefined;
|
|
414
|
-
if (Object.keys(parsed).length !== 2) return undefined;
|
|
415
|
-
return parsed.policy;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Resolve the background-subagents policy AND the source that decided it.
|
|
420
|
-
*
|
|
421
|
-
* Resolution order (first hit wins, mirroring loadRuntimeGuardrailsConfig):
|
|
422
|
-
* 1. Project file `${cwd}/.pi/gentle-ai/background-subagents.json`
|
|
423
|
-
* 2. Global file `${configHome}/background-subagents.json`
|
|
424
|
-
* (configHome honors GENTLE_PI_CONFIG_HOME, default ~/.pi/gentle-ai)
|
|
425
|
-
* 3. Env var GENTLE_PI_BACKGROUND_SUBAGENTS ("on" | "off")
|
|
426
|
-
* 4. Default "off"
|
|
427
|
-
*
|
|
428
|
-
* A present-but-malformed file fails closed to "off" instead of falling
|
|
429
|
-
* through to a lower-priority source, and it stays attributed to that file:
|
|
430
|
-
* "off decided by a broken project file" and "off by default" are different
|
|
431
|
-
* situations, and only the first one is a mistake to fix.
|
|
432
|
-
*
|
|
433
|
-
* Four sources with first-hit-wins is exactly the shape that makes an edit
|
|
434
|
-
* look like it did nothing, so the deciding source is part of the result
|
|
435
|
-
* rather than something a caller has to re-derive.
|
|
436
|
-
*/
|
|
437
|
-
function resolveBackgroundSubagentsPolicy(
|
|
438
|
-
cwd: string,
|
|
439
|
-
options: LoadBackgroundSubagentsOptions = {},
|
|
440
|
-
): BackgroundSubagentsResolution {
|
|
441
|
-
const env = options.env ?? process.env;
|
|
442
|
-
const envValue = env.GENTLE_PI_BACKGROUND_SUBAGENTS;
|
|
443
|
-
let projectFile = "";
|
|
444
|
-
let globalFile = "";
|
|
445
|
-
try {
|
|
446
|
-
const configHome = options.gentlePiConfigHome ?? gentleAiConfigHome();
|
|
447
|
-
projectFile = join(cwd, ".pi", "gentle-ai", BACKGROUND_SUBAGENTS_FILE);
|
|
448
|
-
globalFile = join(configHome, BACKGROUND_SUBAGENTS_FILE);
|
|
449
|
-
const projectFileExists = existsSync(projectFile);
|
|
450
|
-
const globalFileExists = existsSync(globalFile);
|
|
451
|
-
const locations = { projectFile, globalFile, projectFileExists, globalFileExists, envValue };
|
|
452
|
-
for (const [source, path, present] of [
|
|
453
|
-
["project_file", projectFile, projectFileExists],
|
|
454
|
-
["global_file", globalFile, globalFileExists],
|
|
455
|
-
] as const) {
|
|
456
|
-
if (!present) continue;
|
|
457
|
-
let decoded: BackgroundSubagentsPolicy | undefined;
|
|
458
|
-
try {
|
|
459
|
-
decoded = parseBackgroundSubagentsPolicyFile(readFileSync(path, "utf8"));
|
|
460
|
-
} catch {
|
|
461
|
-
// Unreadable is indistinguishable from unusable at this layer, and
|
|
462
|
-
// both must fail closed on the file that claimed the decision.
|
|
463
|
-
decoded = undefined;
|
|
464
|
-
}
|
|
465
|
-
return decoded === undefined
|
|
466
|
-
? { policy: "off", source, malformed: true, ...locations }
|
|
467
|
-
: { policy: decoded, source, malformed: false, ...locations };
|
|
468
|
-
}
|
|
469
|
-
if (envValue === "on" || envValue === "off") {
|
|
470
|
-
return { policy: envValue, source: "environment", malformed: false, ...locations };
|
|
471
|
-
}
|
|
472
|
-
return { policy: "off", source: "default", malformed: false, ...locations };
|
|
473
|
-
} catch {
|
|
474
|
-
return {
|
|
475
|
-
policy: "off",
|
|
476
|
-
source: "default",
|
|
477
|
-
malformed: false,
|
|
478
|
-
projectFile,
|
|
479
|
-
globalFile,
|
|
480
|
-
projectFileExists: false,
|
|
481
|
-
globalFileExists: false,
|
|
482
|
-
envValue,
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* The effective policy alone, for callers that do not report a source.
|
|
489
|
-
* It delegates so the loader and the resolver can never disagree.
|
|
490
|
-
*/
|
|
491
|
-
function loadBackgroundSubagentsPolicy(
|
|
492
|
-
cwd: string,
|
|
493
|
-
options: LoadBackgroundSubagentsOptions = {},
|
|
494
|
-
): BackgroundSubagentsPolicy {
|
|
495
|
-
return resolveBackgroundSubagentsPolicy(cwd, options).policy;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
384
|
/** Write the global policy file, creating the config home when needed. */
|
|
499
385
|
function writeGlobalBackgroundSubagentsPolicy(
|
|
500
386
|
policy: BackgroundSubagentsPolicy,
|
|
@@ -1389,13 +1275,13 @@ Organic Driven Development (ODD) is the predefined workflow of this orchestrator
|
|
|
1389
1275
|
3. **Resolve uncertainty.** Recommend optional research only for a named uncertainty; ask one focused user question only for a real unresolved product decision, then stop and wait; use at most one scoped read-only assumption challenge for a high-consequence unproven premise.
|
|
1390
1276
|
4. **Classify.** The work is substantial when exploration yields two or more meaningful implementation steps, or progress worth recovering after an interruption. Small, understood work stays small and creates no durable task artifacts.
|
|
1391
1277
|
5. **Track before the first write.** For substantial authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, then create or rebuild the visible \`todo\` list from the reconciled feature tasks, all before the first source write and without asking permission for tasks or storage. Tell the user in one line which feature document was created and how many tasks it holds.
|
|
1392
|
-
6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, with the configured TDD mode and applicable checks. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change.
|
|
1393
|
-
7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step.
|
|
1278
|
+
6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, honoring its mandatory delegation triggers, with the configured TDD mode and applicable checks. These triggers are mandatory, not advisory: executing past a fired trigger inline is a routing defect even if the work succeeds. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change. Every task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence. Work-unit commits on the feature branch are part of authorized substantial ODD implementation; push, pull request creation, and merge remain the user's decisions.
|
|
1279
|
+
7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch; native review runs only under the user-owned RDD switch.
|
|
1394
1280
|
Resume an interrupted feature with \`mem_context\`, then project- and feature-scoped \`mem_search\`, then \`mem_get_observation\` for the full document, then the task file itself; reconcile before continuing the next unfinished task. Detail for steps 3–7: \`orchestrator-delegation.md\` and \`orchestrator-memory.md\`.
|
|
1395
1281
|
|
|
1396
1282
|
Harness principles:
|
|
1397
1283
|
- el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
|
|
1398
|
-
- Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track substantial work before the first write, implement task by task with proportionate checks, close. SDD is explicitly selected.
|
|
1284
|
+
- Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track substantial work before the first write, implement task by task with proportionate checks, close each task with a work-unit commit, and close. SDD is explicitly selected.
|
|
1399
1285
|
- Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
|
|
1400
1286
|
- Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
|
|
1401
1287
|
- Keep writes single-threaded unless the user explicitly approves parallel write isolation.
|
|
@@ -4805,7 +4691,7 @@ const REVIEW_CONTROLLER_PARAMETERS = {
|
|
|
4805
4691
|
},
|
|
4806
4692
|
input: {
|
|
4807
4693
|
type: "string",
|
|
4808
|
-
description: "A JSON-serialized object string, not a nested object. New native ordinary START uses {\"mode\":\"ordinary\"}; answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668); omitting writerModelId and writerEffort assesses the ambient working tree and fails closed to a small writer profile (never large) because the writer's actual profile is unknown to this call. nativeReviewOutcome (one of closed, declined, unavailable, unknown) tells ASSESS whether the native review actually closed for this candidate: when Receipt-driven development reads on but the review was declined for this candidate, is unavailable, or its outcome is unknown, ASSESS falls back to the exact risk-gated plan it returns when RDD is off, re-enabling the separate verifier -- a decline is candidate-scoped and never lowers the bar below the RDD-off path. Omitting it lets ASSESS try to derive declined/unavailable from what this process itself recorded for this exact candidate (never a different one, and never from repository state alone), failing closed to unknown when it cannot; `closed` is never derived -- pass it explicitly, and only right after acknowledging the approved review for this same candidate. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
|
|
4694
|
+
description: "A JSON-serialized object string, not a nested object. New native ordinary START uses {\"mode\":\"ordinary\"}; answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668); omitting writerModelId and writerEffort assesses the ambient working tree and fails closed to a small writer profile (never large) because the writer's actual profile is unknown to this call. nativeReviewOutcome (one of closed, declined, unavailable, unknown) tells ASSESS whether the native review actually closed for this candidate: when Receipt-driven development reads on but the review was declined for this candidate, is unavailable, or its outcome is unknown, ASSESS falls back to the exact risk-gated plan it returns when RDD is off, re-enabling the separate verifier -- a decline is candidate-scoped and never lowers the bar below the RDD-off path. Omitting it lets ASSESS try to derive declined/unavailable from what this process itself recorded for this exact candidate (never a different one, and never from repository state alone), failing closed to unknown when it cannot; `closed` is never derived -- pass it explicitly, and only right after acknowledging the approved review for this same candidate. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
|
|
4809
4695
|
},
|
|
4810
4696
|
outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
4811
4697
|
inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
|
|
@@ -5840,6 +5726,12 @@ function validateNativeStartUntrackedSelection(value: Record<string, unknown>):
|
|
|
5840
5726
|
};
|
|
5841
5727
|
}
|
|
5842
5728
|
|
|
5729
|
+
const BASE_REF_REJECTION_REASONS = new Set(["base-ref-unresolvable", "base-ref-ambiguous", "base-ref-moved", "base-ref-invalid"]);
|
|
5730
|
+
|
|
5731
|
+
function baseRefRejectionHint(reason: string): { hint?: string } {
|
|
5732
|
+
return BASE_REF_REJECTION_REASONS.has(reason) ? { hint: BASE_REF_ACCEPTED_FORMS } : {};
|
|
5733
|
+
}
|
|
5734
|
+
|
|
5843
5735
|
function nativeStartRejection(reason: string, field?: string): Record<string, unknown> {
|
|
5844
5736
|
return {
|
|
5845
5737
|
operation: REVIEW_CONTROLLER_OPERATION.START,
|
|
@@ -5861,6 +5753,7 @@ function nativeStartRejection(reason: string, field?: string): Record<string, un
|
|
|
5861
5753
|
: "native-start-policy-path-invalid",
|
|
5862
5754
|
reason,
|
|
5863
5755
|
...(field === undefined ? {} : { field }),
|
|
5756
|
+
...baseRefRejectionHint(reason),
|
|
5864
5757
|
...nativeStartPreAuthorityRejection(),
|
|
5865
5758
|
};
|
|
5866
5759
|
}
|
|
@@ -5872,6 +5765,20 @@ function nativeStatusInputRejection(reason: string, field?: string): Record<stri
|
|
|
5872
5765
|
outcome: "native-status-input-invalid",
|
|
5873
5766
|
reason,
|
|
5874
5767
|
...(field === undefined ? {} : { field }),
|
|
5768
|
+
...baseRefRejectionHint(reason),
|
|
5769
|
+
mutation_performed: false,
|
|
5770
|
+
mutation_outcome: "none",
|
|
5771
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5774
|
+
function nativeInspectInputRejection(reason: string, field?: string): Record<string, unknown> {
|
|
5775
|
+
return {
|
|
5776
|
+
operation: REVIEW_CONTROLLER_OPERATION.INSPECT,
|
|
5777
|
+
status: "blocked",
|
|
5778
|
+
outcome: "native-inspect-input-invalid",
|
|
5779
|
+
reason,
|
|
5780
|
+
...(field === undefined ? {} : { field }),
|
|
5781
|
+
...baseRefRejectionHint(reason),
|
|
5875
5782
|
mutation_performed: false,
|
|
5876
5783
|
mutation_outcome: "none",
|
|
5877
5784
|
};
|
|
@@ -6682,6 +6589,23 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
|
|
|
6682
6589
|
// refusal reason lives (gentle-pi#524); dropping it hid every admission
|
|
6683
6590
|
// refusal behind "submission-refused".
|
|
6684
6591
|
...(error.stderr.length === 0 ? {} : { stderr: error.stderr }),
|
|
6592
|
+
// gentle-shell#1156: what the reviewer child's own event stream revealed.
|
|
6593
|
+
...(error.reviewerEvidence === undefined ? {} : { reviewer: error.reviewerEvidence }),
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
|
|
6597
|
+
// gentle-shell#1136 / #1158: the only two user-owned launch selections the
|
|
6598
|
+
// relay accepts. The lens's model comes from the agent model routing config
|
|
6599
|
+
// under the lens's agent name; the extension allowlist comes from the
|
|
6600
|
+
// environment. Both are optional, and neither is ever invented here.
|
|
6601
|
+
function reviewHostRelayLaunchSelection(lens: string | undefined, config: AgentModelConfig, environment: NodeJS.ProcessEnv): { reviewerModel?: string; reviewerExtensionPaths?: readonly string[] } {
|
|
6602
|
+
const agentName = lens === undefined || lens.length === 0 ? undefined : lens.startsWith("review-") ? lens : `review-${lens}`;
|
|
6603
|
+
const entry = agentName === undefined ? undefined : config[agentName];
|
|
6604
|
+
const model = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).model === "string" && (entry as AgentRoutingEntry).model!.length > 0 ? (entry as AgentRoutingEntry).model : undefined;
|
|
6605
|
+
const extensionPaths = resolveReviewHostRelayExtensionPaths(environment);
|
|
6606
|
+
return {
|
|
6607
|
+
...(model === undefined ? {} : { reviewerModel: model }),
|
|
6608
|
+
...(extensionPaths.length === 0 ? {} : { reviewerExtensionPaths: extensionPaths }),
|
|
6685
6609
|
};
|
|
6686
6610
|
}
|
|
6687
6611
|
|
|
@@ -6793,12 +6717,19 @@ async function executeReviewHostRelayCapture(
|
|
|
6793
6717
|
REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE,
|
|
6794
6718
|
);
|
|
6795
6719
|
}
|
|
6796
|
-
const result = await activeReviewHostRelayRunner({
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6720
|
+
const result = await activeReviewHostRelayRunner((() => {
|
|
6721
|
+
// gentle-shell#1136 / #1158: the lens's user-owned reviewer selection and
|
|
6722
|
+
// the extension allowlist ride the request; the relay validates and
|
|
6723
|
+
// refuses broken configurations typed before anything launches.
|
|
6724
|
+
const launch = reviewHostRelayLaunchSelection(slot.lens, readModelConfig(cwd), process.env);
|
|
6725
|
+
return {
|
|
6726
|
+
captureArgumentTokens: slot.captureArgumentTokens,
|
|
6727
|
+
targetCwd: cwd,
|
|
6728
|
+
submission: slot.submission,
|
|
6729
|
+
...launch,
|
|
6730
|
+
...(signal === undefined ? {} : { signal }),
|
|
6731
|
+
};
|
|
6732
|
+
})());
|
|
6802
6733
|
const closure = decodeRelayLastEventClosure(result.submission);
|
|
6803
6734
|
if (closure !== undefined) return mapAndClearLastEventClosure(closure, binding, selections, cwd);
|
|
6804
6735
|
return {
|
|
@@ -7570,6 +7501,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7570
7501
|
captureArgumentTokens: slot.captureArgumentTokens,
|
|
7571
7502
|
targetCwd: cwd,
|
|
7572
7503
|
submission: slot.submission!,
|
|
7504
|
+
...reviewHostRelayLaunchSelection(slot.lens, readModelConfig(cwd), process.env),
|
|
7573
7505
|
...(signal === undefined ? {} : { signal }),
|
|
7574
7506
|
}));
|
|
7575
7507
|
let prepared: readonly ReviewHostRelayPreparedResult[];
|
|
@@ -7692,6 +7624,30 @@ async function executeReviewControllerOperation(
|
|
|
7692
7624
|
parameters.operation === REVIEW_CONTROLLER_OPERATION.INSPECT &&
|
|
7693
7625
|
nativeReviewCli !== null
|
|
7694
7626
|
) {
|
|
7627
|
+
const rawInspect = parameters.input === undefined
|
|
7628
|
+
? undefined
|
|
7629
|
+
: parseControllerJson(parameters.input, REVIEW_CONTROLLER_OPERATION.INSPECT);
|
|
7630
|
+
const unknownField = rawInspect === undefined
|
|
7631
|
+
? undefined
|
|
7632
|
+
: Object.keys(rawInspect).find((field) => !["baseRef", "committedOnly"].includes(field));
|
|
7633
|
+
if (unknownField !== undefined) return nativeInspectInputRejection("unknown-field", unknownField);
|
|
7634
|
+
const baseRef = rawInspect?.baseRef;
|
|
7635
|
+
if (baseRef !== undefined && !isCanonicalProcessString(baseRef)) return nativeInspectInputRejection("base-ref-invalid");
|
|
7636
|
+
if (baseRef !== undefined && rawInspect?.committedOnly !== true) return nativeInspectInputRejection("committed-only-required");
|
|
7637
|
+
if (rawInspect !== undefined && baseRef === undefined) return nativeInspectInputRejection("committed-only-invalid");
|
|
7638
|
+
let canonicalBaseRef: string | undefined;
|
|
7639
|
+
if (typeof baseRef === "string") {
|
|
7640
|
+
try {
|
|
7641
|
+
canonicalBaseRef = resolveCanonicalCandidateBase(defaultCwd, baseRef).commit;
|
|
7642
|
+
} catch (error) {
|
|
7643
|
+
if (error instanceof CandidateViewError && error.diagnostics !== undefined) return nativeOperationFailure(parameters.operation, Object.assign(error, { candidateViewPreNative: true }));
|
|
7644
|
+
if (error instanceof CandidateViewError && (error.reason === "base-ref-ambiguous" || error.reason === "base-ref-unresolvable" || error.reason === "base-ref-moved")) return nativeInspectInputRejection(error.reason);
|
|
7645
|
+
return nativeInspectInputRejection("base-ref-unresolvable");
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
const inspectSelector = canonicalBaseRef === undefined
|
|
7649
|
+
? {}
|
|
7650
|
+
: { baseRef: canonicalBaseRef, committedOnly: true as const };
|
|
7695
7651
|
// A new inspect supersedes every pre-lineage selection before its first
|
|
7696
7652
|
// STATUS attempt. A failed or changed-candidate inspect cannot leave an
|
|
7697
7653
|
// older selection available for a later START.
|
|
@@ -7702,6 +7658,7 @@ async function executeReviewControllerOperation(
|
|
|
7702
7658
|
nativeReviewCli,
|
|
7703
7659
|
{
|
|
7704
7660
|
cwd: defaultCwd,
|
|
7661
|
+
...inspectSelector,
|
|
7705
7662
|
...(signal === undefined ? {} : { signal }),
|
|
7706
7663
|
},
|
|
7707
7664
|
retainedUntrackedSelections,
|
|
@@ -7777,6 +7734,7 @@ async function executeReviewControllerOperation(
|
|
|
7777
7734
|
nativeReviewCli,
|
|
7778
7735
|
{
|
|
7779
7736
|
cwd: defaultCwd,
|
|
7737
|
+
...inspectSelector,
|
|
7780
7738
|
untrackedScope: parameters.untrackedScope,
|
|
7781
7739
|
expectedUntrackedInventory: inventory,
|
|
7782
7740
|
intendedUntracked: selected.intendedUntracked,
|
|
@@ -8595,6 +8553,8 @@ export const __testing = {
|
|
|
8595
8553
|
loadRuntimeGuardrailsConfig,
|
|
8596
8554
|
buildGentlePrompt,
|
|
8597
8555
|
nativeStatusUnsupported,
|
|
8556
|
+
nativeStartRejection,
|
|
8557
|
+
nativeStatusInputRejection,
|
|
8598
8558
|
executeReviewControllerOperation,
|
|
8599
8559
|
executeReviewCaptureOperation,
|
|
8600
8560
|
executeReviewCaptureGroupOperation,
|
|
@@ -15,7 +15,7 @@ import { buildCommandPaletteGroups } from "../lib/command-palette-catalog.ts";
|
|
|
15
15
|
import { agentsViewKey } from "../lib/agents-keys.ts";
|
|
16
16
|
import { GentleAiDevBinaryOverrideError, resolveGentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
|
|
17
17
|
import { framePromptLines, PROMPT_HINT, PROMPT_STATE, SHELL_PULSE_MS, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
|
|
18
|
-
import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, parseCodexUsage, parseUsageHeaders, UsageStore, type ProviderUsage } from "../lib/shell-usage.ts";
|
|
18
|
+
import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, NAN_PROVIDER, NAN_QUOTA_URL, parseCodexUsage, parseNanQuota, parseUsageHeaders, UsageStore, type ProviderUsage } from "../lib/shell-usage.ts";
|
|
19
19
|
import { UsageView } from "../lib/shell-usage-view.ts";
|
|
20
20
|
import { sidebarPart } from "../lib/shell-sidebar.ts";
|
|
21
21
|
import { installSidebar, invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
@@ -482,21 +482,43 @@ export async function fetchCodexUsage(token: string | undefined, fetchFn: typeof
|
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
// The NaN Cloud quota endpoint is the one the official dashboard reads with the
|
|
486
|
+
// same API key pi already holds. The key travels in the header only: the request
|
|
487
|
+
// refuses redirects so it cannot be replayed to another origin, asks for no
|
|
488
|
+
// stored copy, and nothing here logs, renders, or persists it.
|
|
489
|
+
export async function fetchNanUsage(apiKey: string | undefined, fetchFn: typeof fetch, now: number): Promise<ProviderUsage | undefined> {
|
|
490
|
+
if (!apiKey) return undefined;
|
|
491
|
+
try {
|
|
492
|
+
const response = await fetchFn(NAN_QUOTA_URL, {
|
|
493
|
+
redirect: "error",
|
|
494
|
+
cache: "no-store",
|
|
495
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", "User-Agent": "gentle-pi" },
|
|
496
|
+
});
|
|
497
|
+
if (!response.ok) return undefined;
|
|
498
|
+
const parsed = parseNanQuota(await response.json(), now);
|
|
499
|
+
return parsed.limits.length > 0 ? parsed : undefined;
|
|
500
|
+
} catch {
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
485
505
|
export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, overrides: Partial<ShellDeps> = {}): void {
|
|
486
506
|
installSessionChangeCapture(pi, env, overrides.resolveWorktree ?? resolveSessionWorktree);
|
|
487
507
|
if (!shellEnabled(env)) return;
|
|
488
508
|
const deps: ShellDeps = { ...defaultShellDeps, activeProfile: createActiveProfileReader(env), ...overrides };
|
|
489
509
|
const usage = new UsageStore();
|
|
490
510
|
let renderHost: ShellRenderHost | undefined;
|
|
491
|
-
|
|
511
|
+
// The 5-minute rule is per provider: one provider's fetch cannot leave the
|
|
512
|
+
// next one waiting for an interval it never used.
|
|
513
|
+
const usageFetchedAt = new Map<string, number>();
|
|
492
514
|
const refreshUsage = async (ctx: ExtensionContext, force: boolean) => {
|
|
493
515
|
const provider = ctx.model?.provider;
|
|
494
|
-
if (provider !== CODEX_PROVIDER) return;
|
|
516
|
+
if (provider !== CODEX_PROVIDER && provider !== NAN_PROVIDER) return;
|
|
495
517
|
const now = deps.now();
|
|
496
|
-
if (!force && now - usageFetchedAt < USAGE_REFRESH_MS) return;
|
|
497
|
-
usageFetchedAt
|
|
498
|
-
const
|
|
499
|
-
const fetched = await fetchCodexUsage(
|
|
518
|
+
if (!force && now - (usageFetchedAt.get(provider) ?? 0) < USAGE_REFRESH_MS) return;
|
|
519
|
+
usageFetchedAt.set(provider, now);
|
|
520
|
+
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider).catch(() => undefined);
|
|
521
|
+
const fetched = provider === NAN_PROVIDER ? await fetchNanUsage(apiKey, deps.fetch, deps.now()) : await fetchCodexUsage(apiKey, deps.fetch, deps.now());
|
|
500
522
|
if (!fetched) return;
|
|
501
523
|
usage.record(fetched);
|
|
502
524
|
renderHost?.invalidateSidebar?.();
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { gentlePiConfigHome } from "./agent-home.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Background subagents policy — project > global > env > default off
|
|
7
|
+
//
|
|
8
|
+
// Pure resolver, extracted from extensions/gentle-ai.ts so the runtime side
|
|
9
|
+
// (extensions/gentle-agents.ts) can read the effective policy without
|
|
10
|
+
// importing the pi extension surface. No pi imports belong in this file.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export type BackgroundSubagentsPolicy = "on" | "off";
|
|
14
|
+
|
|
15
|
+
/** Which of the four sources decided the effective policy. */
|
|
16
|
+
export type BackgroundSubagentsSource =
|
|
17
|
+
| "project_file"
|
|
18
|
+
| "global_file"
|
|
19
|
+
| "environment"
|
|
20
|
+
| "default";
|
|
21
|
+
|
|
22
|
+
export interface BackgroundSubagentsResolution {
|
|
23
|
+
policy: BackgroundSubagentsPolicy;
|
|
24
|
+
source: BackgroundSubagentsSource;
|
|
25
|
+
/** The deciding file was present but failed the strict decode. */
|
|
26
|
+
malformed: boolean;
|
|
27
|
+
projectFile: string;
|
|
28
|
+
globalFile: string;
|
|
29
|
+
projectFileExists: boolean;
|
|
30
|
+
globalFileExists: boolean;
|
|
31
|
+
/** The raw env value, reported even when it is unrecognized and inert. */
|
|
32
|
+
envValue: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LoadBackgroundSubagentsOptions {
|
|
36
|
+
/** Override the config home directory (used in tests to avoid touching ~/.pi). */
|
|
37
|
+
gentlePiConfigHome?: string;
|
|
38
|
+
/** Override the environment lookup (used in tests). */
|
|
39
|
+
env?: Record<string, string | undefined>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const BACKGROUND_SUBAGENTS_SCHEMA = "gentle-pi.background-subagents/v1";
|
|
43
|
+
export const BACKGROUND_SUBAGENTS_FILE = "background-subagents.json";
|
|
44
|
+
|
|
45
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Strict decode of {"schema":"gentle-pi.background-subagents/v1","policy":"on"|"off"}.
|
|
51
|
+
* Any malformed shape (bad JSON, wrong schema, unknown keys, invalid policy)
|
|
52
|
+
* returns undefined so the caller fails closed to "off".
|
|
53
|
+
*/
|
|
54
|
+
export function parseBackgroundSubagentsPolicyFile(
|
|
55
|
+
raw: string,
|
|
56
|
+
): BackgroundSubagentsPolicy | undefined {
|
|
57
|
+
let parsed: unknown;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (!isRecord(parsed)) return undefined;
|
|
64
|
+
if (parsed.schema !== BACKGROUND_SUBAGENTS_SCHEMA) return undefined;
|
|
65
|
+
if (parsed.policy !== "on" && parsed.policy !== "off") return undefined;
|
|
66
|
+
if (Object.keys(parsed).length !== 2) return undefined;
|
|
67
|
+
return parsed.policy;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the background-subagents policy AND the source that decided it.
|
|
72
|
+
*
|
|
73
|
+
* Resolution order (first hit wins, mirroring loadRuntimeGuardrailsConfig):
|
|
74
|
+
* 1. Project file `${cwd}/.pi/gentle-ai/background-subagents.json`
|
|
75
|
+
* 2. Global file `${configHome}/background-subagents.json`
|
|
76
|
+
* (configHome honors GENTLE_PI_CONFIG_HOME, default ~/.pi/gentle-ai)
|
|
77
|
+
* 3. Env var GENTLE_PI_BACKGROUND_SUBAGENTS ("on" | "off")
|
|
78
|
+
* 4. Default "off"
|
|
79
|
+
*
|
|
80
|
+
* A present-but-malformed file fails closed to "off" instead of falling
|
|
81
|
+
* through to a lower-priority source, and it stays attributed to that file:
|
|
82
|
+
* "off decided by a broken project file" and "off by default" are different
|
|
83
|
+
* situations, and only the first one is a mistake to fix.
|
|
84
|
+
*
|
|
85
|
+
* Four sources with first-hit-wins is exactly the shape that makes an edit
|
|
86
|
+
* look like it did nothing, so the deciding source is part of the result
|
|
87
|
+
* rather than something a caller has to re-derive.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveBackgroundSubagentsPolicy(
|
|
90
|
+
cwd: string,
|
|
91
|
+
options: LoadBackgroundSubagentsOptions = {},
|
|
92
|
+
): BackgroundSubagentsResolution {
|
|
93
|
+
const env = options.env ?? process.env;
|
|
94
|
+
const envValue = env.GENTLE_PI_BACKGROUND_SUBAGENTS;
|
|
95
|
+
let projectFile = "";
|
|
96
|
+
let globalFile = "";
|
|
97
|
+
try {
|
|
98
|
+
const configHome = options.gentlePiConfigHome ?? gentlePiConfigHome();
|
|
99
|
+
projectFile = join(cwd, ".pi", "gentle-ai", BACKGROUND_SUBAGENTS_FILE);
|
|
100
|
+
globalFile = join(configHome, BACKGROUND_SUBAGENTS_FILE);
|
|
101
|
+
const projectFileExists = existsSync(projectFile);
|
|
102
|
+
const globalFileExists = existsSync(globalFile);
|
|
103
|
+
const locations = { projectFile, globalFile, projectFileExists, globalFileExists, envValue };
|
|
104
|
+
for (const [source, path, present] of [
|
|
105
|
+
["project_file", projectFile, projectFileExists],
|
|
106
|
+
["global_file", globalFile, globalFileExists],
|
|
107
|
+
] as const) {
|
|
108
|
+
if (!present) continue;
|
|
109
|
+
let decoded: BackgroundSubagentsPolicy | undefined;
|
|
110
|
+
try {
|
|
111
|
+
decoded = parseBackgroundSubagentsPolicyFile(readFileSync(path, "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
// Unreadable is indistinguishable from unusable at this layer, and
|
|
114
|
+
// both must fail closed on the file that claimed the decision.
|
|
115
|
+
decoded = undefined;
|
|
116
|
+
}
|
|
117
|
+
return decoded === undefined
|
|
118
|
+
? { policy: "off", source, malformed: true, ...locations }
|
|
119
|
+
: { policy: decoded, source, malformed: false, ...locations };
|
|
120
|
+
}
|
|
121
|
+
if (envValue === "on" || envValue === "off") {
|
|
122
|
+
return { policy: envValue, source: "environment", malformed: false, ...locations };
|
|
123
|
+
}
|
|
124
|
+
return { policy: "off", source: "default", malformed: false, ...locations };
|
|
125
|
+
} catch {
|
|
126
|
+
return {
|
|
127
|
+
policy: "off",
|
|
128
|
+
source: "default",
|
|
129
|
+
malformed: false,
|
|
130
|
+
projectFile,
|
|
131
|
+
globalFile,
|
|
132
|
+
projectFileExists: false,
|
|
133
|
+
globalFileExists: false,
|
|
134
|
+
envValue,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The effective policy alone, for callers that do not report a source.
|
|
141
|
+
* It delegates so the loader and the resolver can never disagree.
|
|
142
|
+
*/
|
|
143
|
+
export function loadBackgroundSubagentsPolicy(
|
|
144
|
+
cwd: string,
|
|
145
|
+
options: LoadBackgroundSubagentsOptions = {},
|
|
146
|
+
): BackgroundSubagentsPolicy {
|
|
147
|
+
return resolveBackgroundSubagentsPolicy(cwd, options).policy;
|
|
148
|
+
}
|
|
@@ -25,7 +25,7 @@ export type ModelConfigFileResult =
|
|
|
25
25
|
| { status: "invalid"; path: string }
|
|
26
26
|
| { status: "valid"; config: AgentModelConfig };
|
|
27
27
|
|
|
28
|
-
const SAFE_MODEL_ID_PATTERN = /^[A-Za-z0-9._~:@/+%-]+$/;
|
|
28
|
+
export const SAFE_MODEL_ID_PATTERN = /^[A-Za-z0-9._~:@/+%-]+$/;
|
|
29
29
|
const SAFE_AGENT_NAME_PATTERN = /^[A-Za-z0-9._:@/+%-]+$/;
|
|
30
30
|
|
|
31
31
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -986,6 +986,24 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
986
986
|
// and hint remain dark because neither is proven to reach the negotiated
|
|
987
987
|
// START path Pi consumes.
|
|
988
988
|
"3.0.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 }),
|
|
989
|
+
// v3.1.0 changed the ODD orchestrator contract only (gentle-ai #4714).
|
|
990
|
+
// Ground-truthed by diffing contracts/review-integration/v2 and
|
|
991
|
+
// contracts/review-provider-contract between the v3.0.2 and v3.1.0 tags
|
|
992
|
+
// in the gentle-ai source tree: zero bytes changed (provider contract
|
|
993
|
+
// stays 1.2.0). Neither change touches the closed START/STATUS fields
|
|
994
|
+
// this row negotiates, so it repeats 3.0.1 exactly. riskEvidence and hint
|
|
995
|
+
// remain dark because neither is proven to reach the negotiated START
|
|
996
|
+
// path Pi consumes.
|
|
997
|
+
"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 }),
|
|
998
|
+
// v3.2.1 changed the ODD orchestrator contract only (gentle-ai #4714
|
|
999
|
+
// follow-up). Ground-truthed by diffing contracts/review-integration/v2 and
|
|
1000
|
+
// contracts/review-provider-contract between the v3.1.0 and v3.2.1 tags
|
|
1001
|
+
// in the gentle-ai source tree: zero bytes changed (provider contract
|
|
1002
|
+
// stays 1.2.0). Neither change touches the closed START/STATUS fields
|
|
1003
|
+
// this row negotiates, so it repeats 3.1.0 exactly. riskEvidence and hint
|
|
1004
|
+
// remain dark because neither is proven to reach the negotiated START
|
|
1005
|
+
// path Pi consumes.
|
|
1006
|
+
"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 }),
|
|
989
1007
|
});
|
|
990
1008
|
|
|
991
1009
|
export interface NativeReviewProcessDiagnostics {
|