gentle-pi 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { consumeReviewMutation, pendingReviewMutation, recordReviewMutation } from "../lib/review-reminder-receipt.ts";
|
|
2
2
|
import { resolveSessionWorktree } from "../lib/session-worktree-registry.ts";
|
|
3
|
+
import { OddRuntimeDelegationGate } from "../lib/odd-runtime-delegation-gate.ts";
|
|
3
4
|
import { resolveResearchCapabilities, renderResearchCapabilities } from "../lib/sdd-research-capabilities.ts";
|
|
4
5
|
import { declareReviewRelayHandshake } from "../lib/review-relay-contract.ts";
|
|
5
6
|
import { execFileSync } from "node:child_process";
|
|
@@ -116,6 +117,7 @@ import {
|
|
|
116
117
|
readOrchestratorSettings,
|
|
117
118
|
restoreOrchestratorSettings,
|
|
118
119
|
type OrchestratorSettingsReadResult,
|
|
120
|
+
parseOrchestratorModelRef,
|
|
119
121
|
} from "../lib/profiles-orchestrator.ts";
|
|
120
122
|
import { measureAgentsViewLayout, type AgentsViewLayout } from "../lib/agents-view-layout.ts";
|
|
121
123
|
import { NativeChoiceList } from "../lib/native-choice-list.ts";
|
|
@@ -132,11 +134,11 @@ import {
|
|
|
132
134
|
REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE,
|
|
133
135
|
REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE,
|
|
134
136
|
ReviewHostRelayError,
|
|
137
|
+
reviewHostMediatedRoleSlots,
|
|
135
138
|
reviewHostRelaySlots,
|
|
136
139
|
reviewHostRelayUnachievableDetail,
|
|
137
140
|
reviewHostRelayUnachievableReason,
|
|
138
141
|
reviewProviderRoleVectorSlots,
|
|
139
|
-
resolveReviewHostRelayExtensionPaths,
|
|
140
142
|
resolveReviewHostRelaySubmission,
|
|
141
143
|
runReviewHostRelayReviewerGroup,
|
|
142
144
|
runReviewHostRelaySlot,
|
|
@@ -147,6 +149,7 @@ import {
|
|
|
147
149
|
type ReviewHostRelaySlot,
|
|
148
150
|
type ReviewProviderRoleVectorSlot,
|
|
149
151
|
} from "../lib/review-host-relay.ts";
|
|
152
|
+
import type { InProcessReviewerRegistry } from "../lib/inprocess-reviewer.ts";
|
|
150
153
|
import {
|
|
151
154
|
JOURNAL_STATUS,
|
|
152
155
|
REVIEW_OPERATION,
|
|
@@ -196,6 +199,7 @@ import {
|
|
|
196
199
|
nativeReviewRecoverAuthorization,
|
|
197
200
|
normalizeNativeReviewCwd,
|
|
198
201
|
NativeReviewCliError,
|
|
202
|
+
nativeUntrackedSelection,
|
|
199
203
|
NativeReviewConsentBindingError,
|
|
200
204
|
NativeReviewConsentRequiredError,
|
|
201
205
|
NativeReviewIntegrationError,
|
|
@@ -1020,18 +1024,26 @@ async function resolveReviewAssessmentPlan(
|
|
|
1020
1024
|
|
|
1021
1025
|
let assessment: ReviewAssessmentV1 | undefined;
|
|
1022
1026
|
let unassessableDetail: string | undefined;
|
|
1027
|
+
let unassessableCode = "native-assess-unavailable";
|
|
1023
1028
|
if (nativeReviewCli?.assess === undefined) {
|
|
1024
1029
|
unassessableDetail = "native review assess is unavailable: the installed gentle-ai binary does not expose the assess command.";
|
|
1025
1030
|
} else {
|
|
1026
1031
|
try {
|
|
1027
1032
|
const request: NativeReviewAssessRequest = {
|
|
1028
1033
|
cwd,
|
|
1034
|
+
...nativeUntrackedSelection(input),
|
|
1029
1035
|
...(input.baseRef === undefined ? {} : { baseRef: input.baseRef, committedOnly: true as const }),
|
|
1030
1036
|
...(signal === undefined ? {} : { signal }),
|
|
1031
1037
|
};
|
|
1032
1038
|
assessment = await nativeReviewCli.assess(request);
|
|
1033
1039
|
} catch (error) {
|
|
1034
|
-
|
|
1040
|
+
const nativeError = asNativeReviewCliError(error);
|
|
1041
|
+
unassessableCode = nativeError?.code ?? unassessableCode;
|
|
1042
|
+
// Only the sanitized process surface may supply native evidence.
|
|
1043
|
+
// Arbitrary thrown messages can contain argv or environment values.
|
|
1044
|
+
unassessableDetail = nativeError?.diagnostics.stderr
|
|
1045
|
+
? `native review assess failed: ${nativeError.diagnostics.stderr}`
|
|
1046
|
+
: "native review assess failed; no sanitized stderr diagnostic is available.";
|
|
1035
1047
|
}
|
|
1036
1048
|
}
|
|
1037
1049
|
|
|
@@ -1047,7 +1059,7 @@ async function resolveReviewAssessmentPlan(
|
|
|
1047
1059
|
return {
|
|
1048
1060
|
schema: "gentle-pi.review-assessment-plan/v1",
|
|
1049
1061
|
risk,
|
|
1050
|
-
reasons: assessment?.reasons ?? (unassessableDetail === undefined ? [] : [{ code:
|
|
1062
|
+
reasons: assessment?.reasons ?? (unassessableDetail === undefined ? [] : [{ code: unassessableCode, path: "", detail: unassessableDetail }]),
|
|
1051
1063
|
changedPaths: assessment?.changedPaths ?? 0,
|
|
1052
1064
|
changedLines: assessment?.changedLines ?? 0,
|
|
1053
1065
|
candidate: assessment === undefined ? null : { kind: assessment.candidate.kind, baseRef: assessment.candidate.baseRef },
|
|
@@ -4137,6 +4149,39 @@ function reportProfilesDrops(ctx: ExtensionContext, path: string, drops: Profile
|
|
|
4137
4149
|
}
|
|
4138
4150
|
}
|
|
4139
4151
|
|
|
4152
|
+
/** Pi's own live-session controls: the ExtensionAPI's setModel/setThinkingLevel. */
|
|
4153
|
+
type LiveSession = Pick<ExtensionAPI, "setModel" | "setThinkingLevel">;
|
|
4154
|
+
|
|
4155
|
+
/**
|
|
4156
|
+
* Switch the running session to the profile's orchestrator. `settings.json`
|
|
4157
|
+
* is the default for new sessions only; Pi's `setModel`/`setThinkingLevel`
|
|
4158
|
+
* are what move the live one. Failures never undo the persisted default: the
|
|
4159
|
+
* profile is applied for the next session either way, and the note says what
|
|
4160
|
+
* this session did. Nothing here may throw — settings.json is already written
|
|
4161
|
+
* and the apply must finish reporting.
|
|
4162
|
+
*/
|
|
4163
|
+
async function switchLiveOrchestrator(ctx: ExtensionContext, live: LiveSession, entry: AgentRoutingEntry): Promise<string> {
|
|
4164
|
+
const reference = parseOrchestratorModelRef(entry.model);
|
|
4165
|
+
if (reference === undefined) return "";
|
|
4166
|
+
const label = `${reference.provider}/${reference.model}`;
|
|
4167
|
+
const model = ctx.modelRegistry.find(reference.provider, reference.model);
|
|
4168
|
+
if (model === undefined) return `\n${label} is not in the model catalog; this session keeps its current model.`;
|
|
4169
|
+
let switched = false;
|
|
4170
|
+
try {
|
|
4171
|
+
switched = await live.setModel(model);
|
|
4172
|
+
} catch (error) {
|
|
4173
|
+
return `\nThis session could not switch to ${label}: ${sanitizeTerminalText(error instanceof Error ? error.message : String(error))}.`;
|
|
4174
|
+
}
|
|
4175
|
+
if (!switched) return `\nno authentication is configured for ${reference.provider}; this session keeps its current model.`;
|
|
4176
|
+
if (entry.thinking === undefined) return `\nThis session now runs on ${label}.`;
|
|
4177
|
+
try {
|
|
4178
|
+
live.setThinkingLevel(entry.thinking);
|
|
4179
|
+
} catch (error) {
|
|
4180
|
+
return `\nThis session now runs on ${label}, but its thinking level could not be set to ${entry.thinking}: ${sanitizeTerminalText(error instanceof Error ? error.message : String(error))}.`;
|
|
4181
|
+
}
|
|
4182
|
+
return `\nThis session now runs on ${label} · ${entry.thinking}.`;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4140
4185
|
function profileSnapshotFrom(
|
|
4141
4186
|
current: AgentModelConfig,
|
|
4142
4187
|
settings: OrchestratorSettingsReadResult,
|
|
@@ -4150,6 +4195,7 @@ function profileSnapshotFrom(
|
|
|
4150
4195
|
|
|
4151
4196
|
async function runProfilesPanelAction(
|
|
4152
4197
|
ctx: ExtensionContext,
|
|
4198
|
+
live: LiveSession,
|
|
4153
4199
|
path: string,
|
|
4154
4200
|
file: AgentProfilesFile,
|
|
4155
4201
|
result: Exclude<ProfilesPanelResult, { type: "close" }>,
|
|
@@ -4291,6 +4337,10 @@ async function runProfilesPanelAction(
|
|
|
4291
4337
|
orchestratorRollback = () => restoreOrchestratorSettings(settingsPath, previous);
|
|
4292
4338
|
orchestratorNote = `\nOrchestrator set to ${formatOrchestratorSelection(orchestratorEntry)} in ${sanitizeTerminalText(settingsPath)}.`;
|
|
4293
4339
|
}
|
|
4340
|
+
// settings.json only governs future sessions. The session the user is
|
|
4341
|
+
// sitting in keeps its model until told otherwise, which made a profile
|
|
4342
|
+
// look applied while the orchestrator kept answering with the old one.
|
|
4343
|
+
orchestratorNote += await switchLiveOrchestrator(ctx, live, orchestratorEntry);
|
|
4294
4344
|
}
|
|
4295
4345
|
// A pin that does not resolve changes nothing at launch, so the global apply
|
|
4296
4346
|
// above is what governs this repository. Saying so keeps a broken pin from
|
|
@@ -4537,7 +4587,7 @@ async function runProfilesPanelAction(
|
|
|
4537
4587
|
}
|
|
4538
4588
|
}
|
|
4539
4589
|
|
|
4540
|
-
async function handleProfilesCommand(ctx: ExtensionContext): Promise<void> {
|
|
4590
|
+
async function handleProfilesCommand(ctx: ExtensionContext, live: LiveSession): Promise<void> {
|
|
4541
4591
|
const path = profilesFilePath(gentleAiConfigHome());
|
|
4542
4592
|
const read = readProfilesFileResult(path);
|
|
4543
4593
|
if (read.status === "invalid") {
|
|
@@ -4587,7 +4637,7 @@ async function handleProfilesCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
4587
4637
|
);
|
|
4588
4638
|
while (result.type !== "close") {
|
|
4589
4639
|
selectedName = "name" in result ? result.name : undefined;
|
|
4590
|
-
file = await runProfilesPanelAction(ctx, path, file, result);
|
|
4640
|
+
file = await runProfilesPanelAction(ctx, live, path, file, result);
|
|
4591
4641
|
result = await showProfilesPanel(
|
|
4592
4642
|
ctx,
|
|
4593
4643
|
file,
|
|
@@ -4785,7 +4835,7 @@ interface ReviewScopeParameters {
|
|
|
4785
4835
|
// as `gentle_review` operation `assess` (not a dedicated tool), taking its
|
|
4786
4836
|
// optional fields through the controller's existing generic `input` JSON
|
|
4787
4837
|
// string, exactly like START's `{"mode":...,"baseRef":...}`.
|
|
4788
|
-
interface ReviewAssessInput {
|
|
4838
|
+
interface ReviewAssessInput extends Pick<NativeReviewAssessRequest, "untrackedScope" | "expectedUntrackedInventory" | "intendedUntracked"> {
|
|
4789
4839
|
baseRef?: string;
|
|
4790
4840
|
committedOnly?: boolean;
|
|
4791
4841
|
writerModelId?: string;
|
|
@@ -4804,7 +4854,7 @@ function isNativeReviewOutcome(value: unknown): value is NativeReviewOutcome {
|
|
|
4804
4854
|
function parseReviewAssessInput(operation: ReviewControllerOperation, raw: string | undefined): ReviewAssessInput {
|
|
4805
4855
|
if (raw === undefined) return {};
|
|
4806
4856
|
const value = parseControllerJson(raw, operation);
|
|
4807
|
-
const allowed = new Set(["baseRef", "committedOnly", "writerModelId", "writerEffort", "nativeReviewOutcome"]);
|
|
4857
|
+
const allowed = new Set(["baseRef", "committedOnly", "writerModelId", "writerEffort", "nativeReviewOutcome", "untrackedScope", "expectedUntrackedInventory", "intendedUntracked"]);
|
|
4808
4858
|
const unexpected = Object.keys(value).find((key) => !allowed.has(key));
|
|
4809
4859
|
if (unexpected !== undefined) throw new Error(`Review controller ${operation} input does not accept ${unexpected}`);
|
|
4810
4860
|
const { baseRef, committedOnly, writerModelId, writerEffort, nativeReviewOutcome } = value;
|
|
@@ -4818,6 +4868,7 @@ function parseReviewAssessInput(operation: ReviewControllerOperation, raw: strin
|
|
|
4818
4868
|
return {
|
|
4819
4869
|
...(baseRef === undefined ? {} : { baseRef: baseRef as string }),
|
|
4820
4870
|
...(committedOnly === undefined ? {} : { committedOnly: committedOnly as boolean }),
|
|
4871
|
+
...nativeUntrackedSelection(value),
|
|
4821
4872
|
...(writerModelId === undefined ? {} : { writerModelId: writerModelId as string }),
|
|
4822
4873
|
...(writerEffort === undefined ? {} : { writerEffort: writerEffort as string }),
|
|
4823
4874
|
...(nativeReviewOutcome === undefined ? {} : { nativeReviewOutcome: nativeReviewOutcome as NativeReviewOutcome }),
|
|
@@ -6594,18 +6645,22 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
|
|
|
6594
6645
|
};
|
|
6595
6646
|
}
|
|
6596
6647
|
|
|
6597
|
-
// gentle-shell#1136 / #1158: the
|
|
6598
|
-
//
|
|
6599
|
-
//
|
|
6600
|
-
//
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6648
|
+
// gentle-pi#311 P2 (superseding gentle-shell#1136 / #1158): the lens's
|
|
6649
|
+
// completion selection comes from its entry in the agent model routing
|
|
6650
|
+
// config, keyed by its routing key (`review-<lens>`). There is no extension
|
|
6651
|
+
// allowlist and no ambient default model: the in-process completion resolves
|
|
6652
|
+
// its provider through the live model registry the caller supplies, or it is
|
|
6653
|
+
// refused before materialize ever runs (validateReviewerSelectionConfiguration
|
|
6654
|
+
// in lib/review-host-relay.ts).
|
|
6655
|
+
function reviewHostRelaySelection(lens: string | undefined, config: AgentModelConfig): { selection?: string; thinking?: string; routingKey: string } {
|
|
6656
|
+
const routingKey = lens === undefined || lens.length === 0 ? "review capture" : lens.startsWith("review-") ? lens : `review-${lens}`;
|
|
6657
|
+
const entry = config[routingKey];
|
|
6604
6658
|
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
|
|
6659
|
+
const thinking = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).thinking === "string" ? (entry as AgentRoutingEntry).thinking : undefined;
|
|
6606
6660
|
return {
|
|
6607
|
-
...(model === undefined ? {} : {
|
|
6608
|
-
...(
|
|
6661
|
+
...(model === undefined ? {} : { selection: model }),
|
|
6662
|
+
...(thinking === undefined ? {} : { thinking }),
|
|
6663
|
+
routingKey,
|
|
6609
6664
|
};
|
|
6610
6665
|
}
|
|
6611
6666
|
|
|
@@ -6708,6 +6763,12 @@ async function executeReviewHostRelayCapture(
|
|
|
6708
6763
|
selections: Map<string, RetainedNativeStatusSelection>,
|
|
6709
6764
|
route: RetainedNativeCaptureRoute | undefined,
|
|
6710
6765
|
signal?: AbortSignal,
|
|
6766
|
+
modelRegistry?: InProcessReviewerRegistry,
|
|
6767
|
+
// The caller's live session id: forwarded into the relay request so an
|
|
6768
|
+
// OpenCode-routed reviewer completion carries its x-opencode-session
|
|
6769
|
+
// attribution header. Appended last so every existing positional call site
|
|
6770
|
+
// keeps compiling unchanged.
|
|
6771
|
+
reviewerSessionId?: string,
|
|
6711
6772
|
): Promise<Record<string, unknown>> {
|
|
6712
6773
|
try {
|
|
6713
6774
|
if (slot.submission === undefined) {
|
|
@@ -6718,15 +6779,20 @@ async function executeReviewHostRelayCapture(
|
|
|
6718
6779
|
);
|
|
6719
6780
|
}
|
|
6720
6781
|
const result = await activeReviewHostRelayRunner((() => {
|
|
6721
|
-
// gentle-
|
|
6722
|
-
//
|
|
6723
|
-
//
|
|
6724
|
-
|
|
6782
|
+
// gentle-pi#311 P2 / P3: the lens's (or, for a v9 host-mediated role
|
|
6783
|
+
// slot, the fixed review-refuter/review-validator routing key's)
|
|
6784
|
+
// user-owned completion selection rides the request alongside the
|
|
6785
|
+
// live model registry; the relay validates and refuses a missing
|
|
6786
|
+
// registry or a routing entry with no configured model typed before
|
|
6787
|
+
// anything launches, never a fallback to a child.
|
|
6788
|
+
const launch = reviewHostRelaySelection(slot.routingKey ?? slot.lens, readModelConfig(cwd));
|
|
6725
6789
|
return {
|
|
6726
6790
|
captureArgumentTokens: slot.captureArgumentTokens,
|
|
6727
6791
|
targetCwd: cwd,
|
|
6728
6792
|
submission: slot.submission,
|
|
6729
6793
|
...launch,
|
|
6794
|
+
...(modelRegistry === undefined ? {} : { reviewerRegistry: modelRegistry }),
|
|
6795
|
+
...(reviewerSessionId === undefined ? {} : { reviewerSessionId }),
|
|
6730
6796
|
...(signal === undefined ? {} : { signal }),
|
|
6731
6797
|
};
|
|
6732
6798
|
})());
|
|
@@ -6742,6 +6808,7 @@ async function executeReviewHostRelayCapture(
|
|
|
6742
6808
|
...(slot.lens === undefined ? {} : { lens: slot.lens }),
|
|
6743
6809
|
...(slot.order === undefined ? {} : { order: slot.order }),
|
|
6744
6810
|
...(slot.subjectHash === undefined ? {} : { subject_hash: slot.subjectHash }),
|
|
6811
|
+
...(slot.routingKey === undefined ? {} : { role: slot.name }),
|
|
6745
6812
|
prompt_bytes: result.promptByteLength,
|
|
6746
6813
|
result_bytes: result.resultByteLength,
|
|
6747
6814
|
submission: result.submission,
|
|
@@ -7323,6 +7390,16 @@ async function executeReviewCaptureOperation(
|
|
|
7323
7390
|
candidateViews: CandidateViewRegistry | null = new CandidateViewRegistry(),
|
|
7324
7391
|
retainedUntrackedSelections: Map<string, RetainedNativeStatusSelection> = new Map(),
|
|
7325
7392
|
requireRegisteredRoute = false,
|
|
7393
|
+
// gentle-pi#311 P2: the live model registry a lens materialize capture
|
|
7394
|
+
// resolves its in-process completion through. Appended last (rather than
|
|
7395
|
+
// inserted) so every existing positional call site — none of which pass an
|
|
7396
|
+
// eighth argument — keeps compiling unchanged.
|
|
7397
|
+
modelRegistry?: InProcessReviewerRegistry,
|
|
7398
|
+
// The caller's live session id, threaded into every relay request this
|
|
7399
|
+
// capture launches so an OpenCode-routed reviewer model carries its
|
|
7400
|
+
// x-opencode-session attribution header. Appended last for the same
|
|
7401
|
+
// positional-call-site reason as modelRegistry above.
|
|
7402
|
+
reviewerSessionId?: string,
|
|
7326
7403
|
): Promise<Record<string, unknown>> {
|
|
7327
7404
|
const parameters = parseReviewCaptureParameters(parametersValue);
|
|
7328
7405
|
if (nativeReviewCli === null || nativeReviewCli.targetStatus === undefined) {
|
|
@@ -7382,7 +7459,33 @@ async function executeReviewCaptureOperation(
|
|
|
7382
7459
|
mutation_outcome: "none",
|
|
7383
7460
|
};
|
|
7384
7461
|
}
|
|
7385
|
-
return withCorrectionTarget(await executeReviewHostRelayCapture(hostRelaySlots[0]!, nativeReviewCli, cwd, selected.binding, retainedUntrackedSelections, route, signal));
|
|
7462
|
+
return withCorrectionTarget(await executeReviewHostRelayCapture(hostRelaySlots[0]!, nativeReviewCli, cwd, selected.binding, retainedUntrackedSelections, route, signal, modelRegistry, reviewerSessionId));
|
|
7463
|
+
}
|
|
7464
|
+
|
|
7465
|
+
// gentle-pi#311 P3: gentle-ai's v9 contract renders the refuter and
|
|
7466
|
+
// targeted-validator role captures host-mediated, exactly like the lens
|
|
7467
|
+
// slot above — same relay machinery, only its fixed review-refuter /
|
|
7468
|
+
// review-validator routing key (carried on the slot) and its own schema
|
|
7469
|
+
// differ. An older gentle-ai's self-contained --execute=true vector still
|
|
7470
|
+
// falls through to reviewProviderRoleVectorSlots below unchanged.
|
|
7471
|
+
const hostMediatedRoleSlots = reviewHostMediatedRoleSlots([selected.input]);
|
|
7472
|
+
if (hostMediatedRoleSlots.length === 1) {
|
|
7473
|
+
if (parameters.correctionLines !== undefined) return captureBindingRejected("correctionLines is valid only for a correction-plan capture");
|
|
7474
|
+
if (parameters.reviewerRunAcknowledged !== true) {
|
|
7475
|
+
return {
|
|
7476
|
+
tool: "gentle_review_capture",
|
|
7477
|
+
status: "blocked",
|
|
7478
|
+
outcome: "reviewer-model-run-forecast",
|
|
7479
|
+
cost_forecast: {
|
|
7480
|
+
transport: "pi_host_relay",
|
|
7481
|
+
model_runs: 1,
|
|
7482
|
+
roles: [hostMediatedRoleSlots[0]!.routingKey],
|
|
7483
|
+
},
|
|
7484
|
+
mutation_performed: false,
|
|
7485
|
+
mutation_outcome: "none",
|
|
7486
|
+
};
|
|
7487
|
+
}
|
|
7488
|
+
return withCorrectionTarget(await executeReviewHostRelayCapture(hostMediatedRoleSlots[0]!, nativeReviewCli, cwd, selected.binding, retainedUntrackedSelections, route, signal, modelRegistry, reviewerSessionId));
|
|
7386
7489
|
}
|
|
7387
7490
|
|
|
7388
7491
|
if (selected.input.captureOperation === "review.capture-correction-plan") {
|
|
@@ -7461,6 +7564,12 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7461
7564
|
candidateViews: CandidateViewRegistry | null = new CandidateViewRegistry(),
|
|
7462
7565
|
retainedUntrackedSelections: Map<string, RetainedNativeStatusSelection> = new Map(),
|
|
7463
7566
|
requireRegisteredRoute = false,
|
|
7567
|
+
// gentle-pi#311 P2: see executeReviewCaptureOperation's matching parameter.
|
|
7568
|
+
modelRegistry?: InProcessReviewerRegistry,
|
|
7569
|
+
// The caller's live session id, set on every grouped relay request so an
|
|
7570
|
+
// OpenCode-routed reviewer model carries its x-opencode-session attribution
|
|
7571
|
+
// header. Appended last for the same positional-call-site reason as above.
|
|
7572
|
+
reviewerSessionId?: string,
|
|
7464
7573
|
): Promise<Record<string, unknown>> {
|
|
7465
7574
|
const parameters = parseReviewCaptureGroupParameters(parametersValue);
|
|
7466
7575
|
if (nativeReviewCli === null || nativeReviewCli.targetStatus === undefined) return { ...captureGroupRejected("native target STATUS is unavailable"), outcome: "native-status-unsupported" };
|
|
@@ -7501,7 +7610,9 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7501
7610
|
captureArgumentTokens: slot.captureArgumentTokens,
|
|
7502
7611
|
targetCwd: cwd,
|
|
7503
7612
|
submission: slot.submission!,
|
|
7504
|
-
...
|
|
7613
|
+
...reviewHostRelaySelection(slot.lens, readModelConfig(cwd)),
|
|
7614
|
+
...(modelRegistry === undefined ? {} : { reviewerRegistry: modelRegistry }),
|
|
7615
|
+
...(reviewerSessionId === undefined ? {} : { reviewerSessionId }),
|
|
7505
7616
|
...(signal === undefined ? {} : { signal }),
|
|
7506
7617
|
}));
|
|
7507
7618
|
let prepared: readonly ReviewHostRelayPreparedResult[];
|
|
@@ -8232,11 +8343,13 @@ async function executeReviewControllerOperation(
|
|
|
8232
8343
|
// native START are resolved, and re-derive the target for that range,
|
|
8233
8344
|
// so all three agree on one base-diff identity. Adopting the offer
|
|
8234
8345
|
// later left the workspace target and the base-diff candidate view
|
|
8235
|
-
// disagreeing, and START failed with identity-mismatch.
|
|
8236
|
-
//
|
|
8237
|
-
// play
|
|
8238
|
-
//
|
|
8239
|
-
|
|
8346
|
+
// disagreeing, and START failed with identity-mismatch. An explicit
|
|
8347
|
+
// caller baseRef still wins (it already is the adopted range), but an
|
|
8348
|
+
// in-play untracked selection now also pays this second read-only
|
|
8349
|
+
// STATUS: the renegotiated target is a base-diff projection, so the
|
|
8350
|
+
// candidate view must be materialized WITH the offered base instead of
|
|
8351
|
+
// the base-less view that tripped candidate-target-projection-drift.
|
|
8352
|
+
if (canonicalBaseRef === undefined) {
|
|
8240
8353
|
const offeredBaseRef = offeredCommittedRangeBaseRef(target);
|
|
8241
8354
|
if (offeredBaseRef !== undefined) {
|
|
8242
8355
|
const renegotiated = await negotiatedStatusForHostTransport(nativeReviewCli, {
|
|
@@ -8244,6 +8357,8 @@ async function executeReviewControllerOperation(
|
|
|
8244
8357
|
...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
|
|
8245
8358
|
baseRef: offeredBaseRef,
|
|
8246
8359
|
committedOnly: true,
|
|
8360
|
+
...(untrackedSelection.untrackedScope === undefined ? {} : untrackedSelection),
|
|
8361
|
+
...(untrackedSubmission === undefined ? {} : { intendedUntrackedSelection: untrackedSubmission }),
|
|
8247
8362
|
...(signal === undefined ? {} : { signal }),
|
|
8248
8363
|
}, retainedUntrackedSelections, defaultCwd);
|
|
8249
8364
|
if (renegotiated.transport !== undefined) return hostTransportUnavailable(parameters.operation, renegotiated.transport);
|
|
@@ -8290,11 +8405,7 @@ async function executeReviewControllerOperation(
|
|
|
8290
8405
|
let nativeStartAttempted = false;
|
|
8291
8406
|
try {
|
|
8292
8407
|
const candidateRequest = { contributorRoot: defaultCwd, replayKey, ...(canonicalBaseRef === undefined ? {} : { baseRef: canonicalBaseRef, committedOnly: true }) };
|
|
8293
|
-
candidateView = candidateViews?.createOrReuse({ ...candidateRequest,
|
|
8294
|
-
if (candidateView !== undefined && candidateIntendedUntracked.length === 0 && candidateView.candidateTree !== target.projection.currentCandidateTree) {
|
|
8295
|
-
candidateView.cleanup();
|
|
8296
|
-
candidateView = candidateViews?.createOrReuse({ ...candidateRequest, intendedUntracked: [] });
|
|
8297
|
-
}
|
|
8408
|
+
candidateView = candidateViews?.createOrReuse({ ...candidateRequest, intendedUntracked: candidateIntendedUntracked });
|
|
8298
8409
|
if (candidateView !== undefined) assertNativeStartCandidateBinding(candidateView, target);
|
|
8299
8410
|
let result: NativeStartResult;
|
|
8300
8411
|
try {
|
|
@@ -8648,6 +8759,11 @@ function createGentleAiExtensionForTesting(
|
|
|
8648
8759
|
const candidateViews = dependencies.candidateViews === undefined ? new CandidateViewRegistry() : dependencies.candidateViews;
|
|
8649
8760
|
const herdrLifecycle = createHerdrConfirmationLifecycle(pi.events);
|
|
8650
8761
|
const permissionEnvironment = dependencies.processEnv ?? process.env;
|
|
8762
|
+
const oddDelegationGate = new OddRuntimeDelegationGate();
|
|
8763
|
+
const oddSessionId = (ctx: ExtensionContext): string => {
|
|
8764
|
+
try { return ctx.sessionManager.getSessionId(); }
|
|
8765
|
+
catch { return ""; }
|
|
8766
|
+
};
|
|
8651
8767
|
|
|
8652
8768
|
const setReviewSessionPermissionStatus = (context: ExtensionContext, active: boolean): void => {
|
|
8653
8769
|
try {
|
|
@@ -8769,6 +8885,11 @@ function createGentleAiExtensionForTesting(
|
|
|
8769
8885
|
candidateViews,
|
|
8770
8886
|
((sessionKey: PendingReviewConsentSessionKey) => processRetainedNativeStatusSelections.get(sessionKey) ?? processRetainedNativeStatusSelections.set(sessionKey, new Map()).get(sessionKey)!)(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)),
|
|
8771
8887
|
true,
|
|
8888
|
+
ctx.modelRegistry,
|
|
8889
|
+
// The live session id rides into the reviewer side-call so an
|
|
8890
|
+
// OpenCode-routed completion carries its attribution headers
|
|
8891
|
+
// (pi adds those inside the main agent loop; this is not that loop).
|
|
8892
|
+
reviewSessionManagerAndId(ctx)?.sessionId,
|
|
8772
8893
|
);
|
|
8773
8894
|
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
8774
8895
|
},
|
|
@@ -8782,7 +8903,7 @@ function createGentleAiExtensionForTesting(
|
|
|
8782
8903
|
promptSnippet: "Use one exact current STATUS collectBinding for one ordinary native capture; call fresh STATUS before every additional capture.",
|
|
8783
8904
|
promptGuidelines: [
|
|
8784
8905
|
"Pass only lineageId, the JSON-serialized exact collectBinding from current STATUS, and the route-specific optional acknowledgement or correctionLines value. Never compose provider argument tokens, prompts, results, verdicts, or lens arrays.",
|
|
8785
|
-
"A materialize reviewer slot first forecasts one model run; re-submit that same exact binding with reviewerRunAcknowledged: true to authorize one host relay. Correction-plan slots require correctionLines inside the provider-issued bounds, counted in diff lines (one replaced source line is one deletion plus one addition) — a different unit from the frozen logical correction budget.
|
|
8906
|
+
"A materialize reviewer slot first forecasts one model run; re-submit that same exact binding with reviewerRunAcknowledged: true to authorize one host relay. Correction-plan slots require correctionLines inside the provider-issued bounds, counted in diff lines (one replaced source line is one deletion plus one addition) — a different unit from the frozen logical correction budget. A refuter or targeted-validator slot forecasts and runs the same way when the provider renders it host-mediated; an older provider's self-contained refuter/validation vector still executes exactly once as provider-rendered.",
|
|
8786
8907
|
"A native terminal closure or nonterminal capture returns directly. Do not expect automatic STATUS, FINALIZE, receipt, delivery, or another capture; call fresh STATUS before any next capture.",
|
|
8787
8908
|
],
|
|
8788
8909
|
parameters: REVIEW_CAPTURE_PARAMETERS,
|
|
@@ -8807,6 +8928,11 @@ function createGentleAiExtensionForTesting(
|
|
|
8807
8928
|
candidateViews,
|
|
8808
8929
|
((sessionKey: PendingReviewConsentSessionKey) => processRetainedNativeStatusSelections.get(sessionKey) ?? processRetainedNativeStatusSelections.set(sessionKey, new Map()).get(sessionKey)!)(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)),
|
|
8809
8930
|
true,
|
|
8931
|
+
ctx.modelRegistry,
|
|
8932
|
+
// The live session id rides into the reviewer side-call so an
|
|
8933
|
+
// OpenCode-routed completion carries its attribution headers
|
|
8934
|
+
// (pi adds those inside the main agent loop; this is not that loop).
|
|
8935
|
+
reviewSessionManagerAndId(ctx)?.sessionId,
|
|
8810
8936
|
);
|
|
8811
8937
|
return {
|
|
8812
8938
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
@@ -9043,6 +9169,10 @@ function createGentleAiExtensionForTesting(
|
|
|
9043
9169
|
const retiredSync = readAgentStartNames(event).includes("sdd-sync") || /\bSDD sync executor\b/i.test(event.systemPrompt ?? "");
|
|
9044
9170
|
const isSddAgent = retiredSync || isSddAgentStartEvent(event);
|
|
9045
9171
|
const isNamedAgent = isNamedAgentStartEvent(event);
|
|
9172
|
+
oddDelegationGate.start(
|
|
9173
|
+
oddSessionId(ctx),
|
|
9174
|
+
!isNamedAgent && !isSddAgent && permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1",
|
|
9175
|
+
);
|
|
9046
9176
|
const subagentDepthKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
9047
9177
|
if (isSddAgent || isNamedAgent) {
|
|
9048
9178
|
processAgentEndSubagentDepth.set(subagentDepthKey, (processAgentEndSubagentDepth.get(subagentDepthKey) ?? 0) + 1);
|
|
@@ -9152,6 +9282,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9152
9282
|
// consent, or chooses a partial candidate. Durable own-mutation receipts
|
|
9153
9283
|
// gate STATUS and consume only the generation captured before that await.
|
|
9154
9284
|
pi.on("agent_end", async (_event, ctx) => {
|
|
9285
|
+
oddDelegationGate.endChild(oddSessionId(ctx));
|
|
9155
9286
|
if (nativeReviewCli?.reviewMode === undefined || nativeReviewCli.targetStatus === undefined) return;
|
|
9156
9287
|
if (ctx.hasUI !== true || !reminderSessionActive) return;
|
|
9157
9288
|
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
@@ -9187,6 +9318,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9187
9318
|
pi.on("tool_result", (event, ctx) => {
|
|
9188
9319
|
if (!reminderSessionActive || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
9189
9320
|
if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
|
|
9321
|
+
oddDelegationGate.recordSuccess(oddSessionId(ctx), event.toolName, event.input, ctx.cwd);
|
|
9190
9322
|
try {
|
|
9191
9323
|
const root = resolveSessionWorktree(event.input.path, ctx.cwd)?.root;
|
|
9192
9324
|
if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId });
|
|
@@ -9200,6 +9332,10 @@ function createGentleAiExtensionForTesting(
|
|
|
9200
9332
|
event.input,
|
|
9201
9333
|
);
|
|
9202
9334
|
if (sensitivePathDenied) return sensitivePathDenied;
|
|
9335
|
+
const oddDelegationDenied = oddDelegationGate.beforeTool(
|
|
9336
|
+
oddSessionId(ctx), event.toolName, event.input, ctx.cwd, readActiveToolNames(pi),
|
|
9337
|
+
);
|
|
9338
|
+
if (oddDelegationDenied) return oddDelegationDenied;
|
|
9203
9339
|
if (event.toolName === "subagent_run") {
|
|
9204
9340
|
const sddAgent = sddDispatchAgentName(event.input);
|
|
9205
9341
|
if (sddAgent === "invalid") {
|
|
@@ -9365,7 +9501,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9365
9501
|
pi.registerCommand("gentle:profiles", {
|
|
9366
9502
|
description: "Create, switch, and manage global agent-model profiles for el Gentleman.",
|
|
9367
9503
|
handler: async (_args, ctx) => {
|
|
9368
|
-
await handleProfilesCommand(ctx);
|
|
9504
|
+
await handleProfilesCommand(ctx, pi);
|
|
9369
9505
|
},
|
|
9370
9506
|
});
|
|
9371
9507
|
|
|
@@ -9594,9 +9730,16 @@ function createGentleAiExtensionForTesting(
|
|
|
9594
9730
|
// background subagents may be launched at all, so nothing in Pi may write
|
|
9595
9731
|
// it. The only writer is this handler, reached only by explicit invocation.
|
|
9596
9732
|
pi.registerCommand("gentle:background-subagents", {
|
|
9597
|
-
description: "Show or set the managed background-subagents policy (status|enable|disable). Every sub-action is user-initiated only; Pi automation never toggles it.",
|
|
9733
|
+
description: "Show or set the managed background-subagents policy; no argument opens a selectable menu (status|enable|disable). Every sub-action is user-initiated only; Pi automation never toggles it.",
|
|
9734
|
+
// No argument opens a selectable menu when an interactive UI is present;
|
|
9735
|
+
// headless callers and fakes without ui.select keep the status fallback.
|
|
9598
9736
|
handler: async (args, ctx) => {
|
|
9599
|
-
|
|
9737
|
+
let subAction = args.trim().length === 0 ? "status" : args.trim();
|
|
9738
|
+
if (args.trim().length === 0 && ctx.hasUI && typeof ctx.ui.select === "function") {
|
|
9739
|
+
const selected = await ctx.ui.select("Background subagents policy", ["status", "enable", "disable"]);
|
|
9740
|
+
if (selected === undefined) return;
|
|
9741
|
+
subAction = selected;
|
|
9742
|
+
}
|
|
9600
9743
|
if (subAction !== "status" && subAction !== "enable" && subAction !== "disable") {
|
|
9601
9744
|
ctx.ui.notify(`Unknown /gentle:background-subagents sub-action "${subAction}". Use status, enable, or disable.`, "warning");
|
|
9602
9745
|
return;
|