gentle-pi 3.2.0 → 3.3.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/assets/orchestrator-delegation.md +13 -8
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +40 -17
- package/docs/readme-reference.md +41 -7
- package/docs/review-integration.md +25 -11
- package/extensions/gentle-agents.ts +85 -17
- package/extensions/gentle-ai.ts +179 -12
- package/extensions/gentle-shell.ts +408 -38
- package/extensions/gentle-todo.ts +19 -1
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +260 -0
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +23 -0
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/review-host-relay.ts +262 -94
- package/lib/review-integration-v2.ts +110 -26
- package/lib/shell-bar.ts +158 -29
- 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 +111 -15
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +98 -10
- package/lib/shell-usage.ts +226 -10
- package/package.json +2 -1
- package/runtime/native-review-cli.mjs +23 -0
- package/runtime/review-integration-v2.mjs +110 -26
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/mirror-odd-routing.mjs +242 -0
- package/scripts/verify-package-files.mjs +3 -3
- 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/command-palette.test.ts +1 -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 +528 -5
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +69 -5
- package/tests/gentle-shell.test.ts +903 -25
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +368 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +32 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +57 -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 -6
- package/tests/review-controller-native-routing.test.ts +60 -1
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +285 -239
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +116 -1
- package/tests/review-relay-transport-agent.test.ts +83 -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 +224 -6
- 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 +243 -5
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +123 -3
- package/tests/shell-usage.test.ts +254 -6
- package/lib/opaque-pi-reviewer-adapter.ts +0 -284
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -266
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,6 +134,7 @@ 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,
|
|
@@ -146,6 +149,7 @@ import {
|
|
|
146
149
|
type ReviewHostRelaySlot,
|
|
147
150
|
type ReviewProviderRoleVectorSlot,
|
|
148
151
|
} from "../lib/review-host-relay.ts";
|
|
152
|
+
import type { InProcessReviewerRegistry } from "../lib/inprocess-reviewer.ts";
|
|
149
153
|
import {
|
|
150
154
|
JOURNAL_STATUS,
|
|
151
155
|
REVIEW_OPERATION,
|
|
@@ -1274,7 +1278,7 @@ Organic Driven Development (ODD) is the predefined workflow of this orchestrator
|
|
|
1274
1278
|
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.
|
|
1275
1279
|
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.
|
|
1276
1280
|
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.
|
|
1277
|
-
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. 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.
|
|
1281
|
+
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.
|
|
1278
1282
|
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.
|
|
1279
1283
|
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\`.
|
|
1280
1284
|
|
|
@@ -4136,6 +4140,39 @@ function reportProfilesDrops(ctx: ExtensionContext, path: string, drops: Profile
|
|
|
4136
4140
|
}
|
|
4137
4141
|
}
|
|
4138
4142
|
|
|
4143
|
+
/** Pi's own live-session controls: the ExtensionAPI's setModel/setThinkingLevel. */
|
|
4144
|
+
type LiveSession = Pick<ExtensionAPI, "setModel" | "setThinkingLevel">;
|
|
4145
|
+
|
|
4146
|
+
/**
|
|
4147
|
+
* Switch the running session to the profile's orchestrator. `settings.json`
|
|
4148
|
+
* is the default for new sessions only; Pi's `setModel`/`setThinkingLevel`
|
|
4149
|
+
* are what move the live one. Failures never undo the persisted default: the
|
|
4150
|
+
* profile is applied for the next session either way, and the note says what
|
|
4151
|
+
* this session did. Nothing here may throw — settings.json is already written
|
|
4152
|
+
* and the apply must finish reporting.
|
|
4153
|
+
*/
|
|
4154
|
+
async function switchLiveOrchestrator(ctx: ExtensionContext, live: LiveSession, entry: AgentRoutingEntry): Promise<string> {
|
|
4155
|
+
const reference = parseOrchestratorModelRef(entry.model);
|
|
4156
|
+
if (reference === undefined) return "";
|
|
4157
|
+
const label = `${reference.provider}/${reference.model}`;
|
|
4158
|
+
const model = ctx.modelRegistry.find(reference.provider, reference.model);
|
|
4159
|
+
if (model === undefined) return `\n${label} is not in the model catalog; this session keeps its current model.`;
|
|
4160
|
+
let switched = false;
|
|
4161
|
+
try {
|
|
4162
|
+
switched = await live.setModel(model);
|
|
4163
|
+
} catch (error) {
|
|
4164
|
+
return `\nThis session could not switch to ${label}: ${sanitizeTerminalText(error instanceof Error ? error.message : String(error))}.`;
|
|
4165
|
+
}
|
|
4166
|
+
if (!switched) return `\nno authentication is configured for ${reference.provider}; this session keeps its current model.`;
|
|
4167
|
+
if (entry.thinking === undefined) return `\nThis session now runs on ${label}.`;
|
|
4168
|
+
try {
|
|
4169
|
+
live.setThinkingLevel(entry.thinking);
|
|
4170
|
+
} catch (error) {
|
|
4171
|
+
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))}.`;
|
|
4172
|
+
}
|
|
4173
|
+
return `\nThis session now runs on ${label} · ${entry.thinking}.`;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4139
4176
|
function profileSnapshotFrom(
|
|
4140
4177
|
current: AgentModelConfig,
|
|
4141
4178
|
settings: OrchestratorSettingsReadResult,
|
|
@@ -4149,6 +4186,7 @@ function profileSnapshotFrom(
|
|
|
4149
4186
|
|
|
4150
4187
|
async function runProfilesPanelAction(
|
|
4151
4188
|
ctx: ExtensionContext,
|
|
4189
|
+
live: LiveSession,
|
|
4152
4190
|
path: string,
|
|
4153
4191
|
file: AgentProfilesFile,
|
|
4154
4192
|
result: Exclude<ProfilesPanelResult, { type: "close" }>,
|
|
@@ -4290,6 +4328,10 @@ async function runProfilesPanelAction(
|
|
|
4290
4328
|
orchestratorRollback = () => restoreOrchestratorSettings(settingsPath, previous);
|
|
4291
4329
|
orchestratorNote = `\nOrchestrator set to ${formatOrchestratorSelection(orchestratorEntry)} in ${sanitizeTerminalText(settingsPath)}.`;
|
|
4292
4330
|
}
|
|
4331
|
+
// settings.json only governs future sessions. The session the user is
|
|
4332
|
+
// sitting in keeps its model until told otherwise, which made a profile
|
|
4333
|
+
// look applied while the orchestrator kept answering with the old one.
|
|
4334
|
+
orchestratorNote += await switchLiveOrchestrator(ctx, live, orchestratorEntry);
|
|
4293
4335
|
}
|
|
4294
4336
|
// A pin that does not resolve changes nothing at launch, so the global apply
|
|
4295
4337
|
// above is what governs this repository. Saying so keeps a broken pin from
|
|
@@ -4536,7 +4578,7 @@ async function runProfilesPanelAction(
|
|
|
4536
4578
|
}
|
|
4537
4579
|
}
|
|
4538
4580
|
|
|
4539
|
-
async function handleProfilesCommand(ctx: ExtensionContext): Promise<void> {
|
|
4581
|
+
async function handleProfilesCommand(ctx: ExtensionContext, live: LiveSession): Promise<void> {
|
|
4540
4582
|
const path = profilesFilePath(gentleAiConfigHome());
|
|
4541
4583
|
const read = readProfilesFileResult(path);
|
|
4542
4584
|
if (read.status === "invalid") {
|
|
@@ -4586,7 +4628,7 @@ async function handleProfilesCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
4586
4628
|
);
|
|
4587
4629
|
while (result.type !== "close") {
|
|
4588
4630
|
selectedName = "name" in result ? result.name : undefined;
|
|
4589
|
-
file = await runProfilesPanelAction(ctx, path, file, result);
|
|
4631
|
+
file = await runProfilesPanelAction(ctx, live, path, file, result);
|
|
4590
4632
|
result = await showProfilesPanel(
|
|
4591
4633
|
ctx,
|
|
4592
4634
|
file,
|
|
@@ -5770,6 +5812,19 @@ function nativeStatusInputRejection(reason: string, field?: string): Record<stri
|
|
|
5770
5812
|
};
|
|
5771
5813
|
}
|
|
5772
5814
|
|
|
5815
|
+
function nativeInspectInputRejection(reason: string, field?: string): Record<string, unknown> {
|
|
5816
|
+
return {
|
|
5817
|
+
operation: REVIEW_CONTROLLER_OPERATION.INSPECT,
|
|
5818
|
+
status: "blocked",
|
|
5819
|
+
outcome: "native-inspect-input-invalid",
|
|
5820
|
+
reason,
|
|
5821
|
+
...(field === undefined ? {} : { field }),
|
|
5822
|
+
...baseRefRejectionHint(reason),
|
|
5823
|
+
mutation_performed: false,
|
|
5824
|
+
mutation_outcome: "none",
|
|
5825
|
+
};
|
|
5826
|
+
}
|
|
5827
|
+
|
|
5773
5828
|
const PENDING_REVIEW_CONSENT_TTL_MS = 10 * 60 * 1000;
|
|
5774
5829
|
const REVIEW_SESSION_PERMISSION_STATUS_KEY = "gentle-review-session-permission";
|
|
5775
5830
|
const REVIEW_SESSION_PERMISSION_STATUS_TEXT = "reviews allowed for this session";
|
|
@@ -6575,6 +6630,27 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
|
|
|
6575
6630
|
// refusal reason lives (gentle-pi#524); dropping it hid every admission
|
|
6576
6631
|
// refusal behind "submission-refused".
|
|
6577
6632
|
...(error.stderr.length === 0 ? {} : { stderr: error.stderr }),
|
|
6633
|
+
// gentle-shell#1156: what the reviewer child's own event stream revealed.
|
|
6634
|
+
...(error.reviewerEvidence === undefined ? {} : { reviewer: error.reviewerEvidence }),
|
|
6635
|
+
};
|
|
6636
|
+
}
|
|
6637
|
+
|
|
6638
|
+
// gentle-pi#311 P2 (superseding gentle-shell#1136 / #1158): the lens's
|
|
6639
|
+
// completion selection comes from its entry in the agent model routing
|
|
6640
|
+
// config, keyed by its routing key (`review-<lens>`). There is no extension
|
|
6641
|
+
// allowlist and no ambient default model: the in-process completion resolves
|
|
6642
|
+
// its provider through the live model registry the caller supplies, or it is
|
|
6643
|
+
// refused before materialize ever runs (validateReviewerSelectionConfiguration
|
|
6644
|
+
// in lib/review-host-relay.ts).
|
|
6645
|
+
function reviewHostRelaySelection(lens: string | undefined, config: AgentModelConfig): { selection?: string; thinking?: string; routingKey: string } {
|
|
6646
|
+
const routingKey = lens === undefined || lens.length === 0 ? "review capture" : lens.startsWith("review-") ? lens : `review-${lens}`;
|
|
6647
|
+
const entry = config[routingKey];
|
|
6648
|
+
const model = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).model === "string" && (entry as AgentRoutingEntry).model!.length > 0 ? (entry as AgentRoutingEntry).model : undefined;
|
|
6649
|
+
const thinking = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).thinking === "string" ? (entry as AgentRoutingEntry).thinking : undefined;
|
|
6650
|
+
return {
|
|
6651
|
+
...(model === undefined ? {} : { selection: model }),
|
|
6652
|
+
...(thinking === undefined ? {} : { thinking }),
|
|
6653
|
+
routingKey,
|
|
6578
6654
|
};
|
|
6579
6655
|
}
|
|
6580
6656
|
|
|
@@ -6677,6 +6753,7 @@ async function executeReviewHostRelayCapture(
|
|
|
6677
6753
|
selections: Map<string, RetainedNativeStatusSelection>,
|
|
6678
6754
|
route: RetainedNativeCaptureRoute | undefined,
|
|
6679
6755
|
signal?: AbortSignal,
|
|
6756
|
+
modelRegistry?: InProcessReviewerRegistry,
|
|
6680
6757
|
): Promise<Record<string, unknown>> {
|
|
6681
6758
|
try {
|
|
6682
6759
|
if (slot.submission === undefined) {
|
|
@@ -6686,12 +6763,23 @@ async function executeReviewHostRelayCapture(
|
|
|
6686
6763
|
REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE,
|
|
6687
6764
|
);
|
|
6688
6765
|
}
|
|
6689
|
-
const result = await activeReviewHostRelayRunner({
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6766
|
+
const result = await activeReviewHostRelayRunner((() => {
|
|
6767
|
+
// gentle-pi#311 P2 / P3: the lens's (or, for a v9 host-mediated role
|
|
6768
|
+
// slot, the fixed review-refuter/review-validator routing key's)
|
|
6769
|
+
// user-owned completion selection rides the request alongside the
|
|
6770
|
+
// live model registry; the relay validates and refuses a missing
|
|
6771
|
+
// registry or a routing entry with no configured model typed before
|
|
6772
|
+
// anything launches, never a fallback to a child.
|
|
6773
|
+
const launch = reviewHostRelaySelection(slot.routingKey ?? slot.lens, readModelConfig(cwd));
|
|
6774
|
+
return {
|
|
6775
|
+
captureArgumentTokens: slot.captureArgumentTokens,
|
|
6776
|
+
targetCwd: cwd,
|
|
6777
|
+
submission: slot.submission,
|
|
6778
|
+
...launch,
|
|
6779
|
+
...(modelRegistry === undefined ? {} : { reviewerRegistry: modelRegistry }),
|
|
6780
|
+
...(signal === undefined ? {} : { signal }),
|
|
6781
|
+
};
|
|
6782
|
+
})());
|
|
6695
6783
|
const closure = decodeRelayLastEventClosure(result.submission);
|
|
6696
6784
|
if (closure !== undefined) return mapAndClearLastEventClosure(closure, binding, selections, cwd);
|
|
6697
6785
|
return {
|
|
@@ -6704,6 +6792,7 @@ async function executeReviewHostRelayCapture(
|
|
|
6704
6792
|
...(slot.lens === undefined ? {} : { lens: slot.lens }),
|
|
6705
6793
|
...(slot.order === undefined ? {} : { order: slot.order }),
|
|
6706
6794
|
...(slot.subjectHash === undefined ? {} : { subject_hash: slot.subjectHash }),
|
|
6795
|
+
...(slot.routingKey === undefined ? {} : { role: slot.name }),
|
|
6707
6796
|
prompt_bytes: result.promptByteLength,
|
|
6708
6797
|
result_bytes: result.resultByteLength,
|
|
6709
6798
|
submission: result.submission,
|
|
@@ -7285,6 +7374,11 @@ async function executeReviewCaptureOperation(
|
|
|
7285
7374
|
candidateViews: CandidateViewRegistry | null = new CandidateViewRegistry(),
|
|
7286
7375
|
retainedUntrackedSelections: Map<string, RetainedNativeStatusSelection> = new Map(),
|
|
7287
7376
|
requireRegisteredRoute = false,
|
|
7377
|
+
// gentle-pi#311 P2: the live model registry a lens materialize capture
|
|
7378
|
+
// resolves its in-process completion through. Appended last (rather than
|
|
7379
|
+
// inserted) so every existing positional call site — none of which pass an
|
|
7380
|
+
// eighth argument — keeps compiling unchanged.
|
|
7381
|
+
modelRegistry?: InProcessReviewerRegistry,
|
|
7288
7382
|
): Promise<Record<string, unknown>> {
|
|
7289
7383
|
const parameters = parseReviewCaptureParameters(parametersValue);
|
|
7290
7384
|
if (nativeReviewCli === null || nativeReviewCli.targetStatus === undefined) {
|
|
@@ -7344,7 +7438,33 @@ async function executeReviewCaptureOperation(
|
|
|
7344
7438
|
mutation_outcome: "none",
|
|
7345
7439
|
};
|
|
7346
7440
|
}
|
|
7347
|
-
return withCorrectionTarget(await executeReviewHostRelayCapture(hostRelaySlots[0]!, nativeReviewCli, cwd, selected.binding, retainedUntrackedSelections, route, signal));
|
|
7441
|
+
return withCorrectionTarget(await executeReviewHostRelayCapture(hostRelaySlots[0]!, nativeReviewCli, cwd, selected.binding, retainedUntrackedSelections, route, signal, modelRegistry));
|
|
7442
|
+
}
|
|
7443
|
+
|
|
7444
|
+
// gentle-pi#311 P3: gentle-ai's v9 contract renders the refuter and
|
|
7445
|
+
// targeted-validator role captures host-mediated, exactly like the lens
|
|
7446
|
+
// slot above — same relay machinery, only its fixed review-refuter /
|
|
7447
|
+
// review-validator routing key (carried on the slot) and its own schema
|
|
7448
|
+
// differ. An older gentle-ai's self-contained --execute=true vector still
|
|
7449
|
+
// falls through to reviewProviderRoleVectorSlots below unchanged.
|
|
7450
|
+
const hostMediatedRoleSlots = reviewHostMediatedRoleSlots([selected.input]);
|
|
7451
|
+
if (hostMediatedRoleSlots.length === 1) {
|
|
7452
|
+
if (parameters.correctionLines !== undefined) return captureBindingRejected("correctionLines is valid only for a correction-plan capture");
|
|
7453
|
+
if (parameters.reviewerRunAcknowledged !== true) {
|
|
7454
|
+
return {
|
|
7455
|
+
tool: "gentle_review_capture",
|
|
7456
|
+
status: "blocked",
|
|
7457
|
+
outcome: "reviewer-model-run-forecast",
|
|
7458
|
+
cost_forecast: {
|
|
7459
|
+
transport: "pi_host_relay",
|
|
7460
|
+
model_runs: 1,
|
|
7461
|
+
roles: [hostMediatedRoleSlots[0]!.routingKey],
|
|
7462
|
+
},
|
|
7463
|
+
mutation_performed: false,
|
|
7464
|
+
mutation_outcome: "none",
|
|
7465
|
+
};
|
|
7466
|
+
}
|
|
7467
|
+
return withCorrectionTarget(await executeReviewHostRelayCapture(hostMediatedRoleSlots[0]!, nativeReviewCli, cwd, selected.binding, retainedUntrackedSelections, route, signal, modelRegistry));
|
|
7348
7468
|
}
|
|
7349
7469
|
|
|
7350
7470
|
if (selected.input.captureOperation === "review.capture-correction-plan") {
|
|
@@ -7423,6 +7543,8 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7423
7543
|
candidateViews: CandidateViewRegistry | null = new CandidateViewRegistry(),
|
|
7424
7544
|
retainedUntrackedSelections: Map<string, RetainedNativeStatusSelection> = new Map(),
|
|
7425
7545
|
requireRegisteredRoute = false,
|
|
7546
|
+
// gentle-pi#311 P2: see executeReviewCaptureOperation's matching parameter.
|
|
7547
|
+
modelRegistry?: InProcessReviewerRegistry,
|
|
7426
7548
|
): Promise<Record<string, unknown>> {
|
|
7427
7549
|
const parameters = parseReviewCaptureGroupParameters(parametersValue);
|
|
7428
7550
|
if (nativeReviewCli === null || nativeReviewCli.targetStatus === undefined) return { ...captureGroupRejected("native target STATUS is unavailable"), outcome: "native-status-unsupported" };
|
|
@@ -7463,6 +7585,8 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7463
7585
|
captureArgumentTokens: slot.captureArgumentTokens,
|
|
7464
7586
|
targetCwd: cwd,
|
|
7465
7587
|
submission: slot.submission!,
|
|
7588
|
+
...reviewHostRelaySelection(slot.lens, readModelConfig(cwd)),
|
|
7589
|
+
...(modelRegistry === undefined ? {} : { reviewerRegistry: modelRegistry }),
|
|
7466
7590
|
...(signal === undefined ? {} : { signal }),
|
|
7467
7591
|
}));
|
|
7468
7592
|
let prepared: readonly ReviewHostRelayPreparedResult[];
|
|
@@ -7585,6 +7709,30 @@ async function executeReviewControllerOperation(
|
|
|
7585
7709
|
parameters.operation === REVIEW_CONTROLLER_OPERATION.INSPECT &&
|
|
7586
7710
|
nativeReviewCli !== null
|
|
7587
7711
|
) {
|
|
7712
|
+
const rawInspect = parameters.input === undefined
|
|
7713
|
+
? undefined
|
|
7714
|
+
: parseControllerJson(parameters.input, REVIEW_CONTROLLER_OPERATION.INSPECT);
|
|
7715
|
+
const unknownField = rawInspect === undefined
|
|
7716
|
+
? undefined
|
|
7717
|
+
: Object.keys(rawInspect).find((field) => !["baseRef", "committedOnly"].includes(field));
|
|
7718
|
+
if (unknownField !== undefined) return nativeInspectInputRejection("unknown-field", unknownField);
|
|
7719
|
+
const baseRef = rawInspect?.baseRef;
|
|
7720
|
+
if (baseRef !== undefined && !isCanonicalProcessString(baseRef)) return nativeInspectInputRejection("base-ref-invalid");
|
|
7721
|
+
if (baseRef !== undefined && rawInspect?.committedOnly !== true) return nativeInspectInputRejection("committed-only-required");
|
|
7722
|
+
if (rawInspect !== undefined && baseRef === undefined) return nativeInspectInputRejection("committed-only-invalid");
|
|
7723
|
+
let canonicalBaseRef: string | undefined;
|
|
7724
|
+
if (typeof baseRef === "string") {
|
|
7725
|
+
try {
|
|
7726
|
+
canonicalBaseRef = resolveCanonicalCandidateBase(defaultCwd, baseRef).commit;
|
|
7727
|
+
} catch (error) {
|
|
7728
|
+
if (error instanceof CandidateViewError && error.diagnostics !== undefined) return nativeOperationFailure(parameters.operation, Object.assign(error, { candidateViewPreNative: true }));
|
|
7729
|
+
if (error instanceof CandidateViewError && (error.reason === "base-ref-ambiguous" || error.reason === "base-ref-unresolvable" || error.reason === "base-ref-moved")) return nativeInspectInputRejection(error.reason);
|
|
7730
|
+
return nativeInspectInputRejection("base-ref-unresolvable");
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
const inspectSelector = canonicalBaseRef === undefined
|
|
7734
|
+
? {}
|
|
7735
|
+
: { baseRef: canonicalBaseRef, committedOnly: true as const };
|
|
7588
7736
|
// A new inspect supersedes every pre-lineage selection before its first
|
|
7589
7737
|
// STATUS attempt. A failed or changed-candidate inspect cannot leave an
|
|
7590
7738
|
// older selection available for a later START.
|
|
@@ -7595,6 +7743,7 @@ async function executeReviewControllerOperation(
|
|
|
7595
7743
|
nativeReviewCli,
|
|
7596
7744
|
{
|
|
7597
7745
|
cwd: defaultCwd,
|
|
7746
|
+
...inspectSelector,
|
|
7598
7747
|
...(signal === undefined ? {} : { signal }),
|
|
7599
7748
|
},
|
|
7600
7749
|
retainedUntrackedSelections,
|
|
@@ -7670,6 +7819,7 @@ async function executeReviewControllerOperation(
|
|
|
7670
7819
|
nativeReviewCli,
|
|
7671
7820
|
{
|
|
7672
7821
|
cwd: defaultCwd,
|
|
7822
|
+
...inspectSelector,
|
|
7673
7823
|
untrackedScope: parameters.untrackedScope,
|
|
7674
7824
|
expectedUntrackedInventory: inventory,
|
|
7675
7825
|
intendedUntracked: selected.intendedUntracked,
|
|
@@ -8583,6 +8733,11 @@ function createGentleAiExtensionForTesting(
|
|
|
8583
8733
|
const candidateViews = dependencies.candidateViews === undefined ? new CandidateViewRegistry() : dependencies.candidateViews;
|
|
8584
8734
|
const herdrLifecycle = createHerdrConfirmationLifecycle(pi.events);
|
|
8585
8735
|
const permissionEnvironment = dependencies.processEnv ?? process.env;
|
|
8736
|
+
const oddDelegationGate = new OddRuntimeDelegationGate();
|
|
8737
|
+
const oddSessionId = (ctx: ExtensionContext): string => {
|
|
8738
|
+
try { return ctx.sessionManager.getSessionId(); }
|
|
8739
|
+
catch { return ""; }
|
|
8740
|
+
};
|
|
8586
8741
|
|
|
8587
8742
|
const setReviewSessionPermissionStatus = (context: ExtensionContext, active: boolean): void => {
|
|
8588
8743
|
try {
|
|
@@ -8704,6 +8859,7 @@ function createGentleAiExtensionForTesting(
|
|
|
8704
8859
|
candidateViews,
|
|
8705
8860
|
((sessionKey: PendingReviewConsentSessionKey) => processRetainedNativeStatusSelections.get(sessionKey) ?? processRetainedNativeStatusSelections.set(sessionKey, new Map()).get(sessionKey)!)(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)),
|
|
8706
8861
|
true,
|
|
8862
|
+
ctx.modelRegistry,
|
|
8707
8863
|
);
|
|
8708
8864
|
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
8709
8865
|
},
|
|
@@ -8717,7 +8873,7 @@ function createGentleAiExtensionForTesting(
|
|
|
8717
8873
|
promptSnippet: "Use one exact current STATUS collectBinding for one ordinary native capture; call fresh STATUS before every additional capture.",
|
|
8718
8874
|
promptGuidelines: [
|
|
8719
8875
|
"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.",
|
|
8720
|
-
"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.
|
|
8876
|
+
"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.",
|
|
8721
8877
|
"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.",
|
|
8722
8878
|
],
|
|
8723
8879
|
parameters: REVIEW_CAPTURE_PARAMETERS,
|
|
@@ -8742,6 +8898,7 @@ function createGentleAiExtensionForTesting(
|
|
|
8742
8898
|
candidateViews,
|
|
8743
8899
|
((sessionKey: PendingReviewConsentSessionKey) => processRetainedNativeStatusSelections.get(sessionKey) ?? processRetainedNativeStatusSelections.set(sessionKey, new Map()).get(sessionKey)!)(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)),
|
|
8744
8900
|
true,
|
|
8901
|
+
ctx.modelRegistry,
|
|
8745
8902
|
);
|
|
8746
8903
|
return {
|
|
8747
8904
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
@@ -8978,6 +9135,10 @@ function createGentleAiExtensionForTesting(
|
|
|
8978
9135
|
const retiredSync = readAgentStartNames(event).includes("sdd-sync") || /\bSDD sync executor\b/i.test(event.systemPrompt ?? "");
|
|
8979
9136
|
const isSddAgent = retiredSync || isSddAgentStartEvent(event);
|
|
8980
9137
|
const isNamedAgent = isNamedAgentStartEvent(event);
|
|
9138
|
+
oddDelegationGate.start(
|
|
9139
|
+
oddSessionId(ctx),
|
|
9140
|
+
!isNamedAgent && !isSddAgent && permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1",
|
|
9141
|
+
);
|
|
8981
9142
|
const subagentDepthKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
8982
9143
|
if (isSddAgent || isNamedAgent) {
|
|
8983
9144
|
processAgentEndSubagentDepth.set(subagentDepthKey, (processAgentEndSubagentDepth.get(subagentDepthKey) ?? 0) + 1);
|
|
@@ -9087,6 +9248,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9087
9248
|
// consent, or chooses a partial candidate. Durable own-mutation receipts
|
|
9088
9249
|
// gate STATUS and consume only the generation captured before that await.
|
|
9089
9250
|
pi.on("agent_end", async (_event, ctx) => {
|
|
9251
|
+
oddDelegationGate.endChild(oddSessionId(ctx));
|
|
9090
9252
|
if (nativeReviewCli?.reviewMode === undefined || nativeReviewCli.targetStatus === undefined) return;
|
|
9091
9253
|
if (ctx.hasUI !== true || !reminderSessionActive) return;
|
|
9092
9254
|
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
@@ -9122,6 +9284,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9122
9284
|
pi.on("tool_result", (event, ctx) => {
|
|
9123
9285
|
if (!reminderSessionActive || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
9124
9286
|
if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
|
|
9287
|
+
oddDelegationGate.recordSuccess(oddSessionId(ctx), event.toolName, event.input, ctx.cwd);
|
|
9125
9288
|
try {
|
|
9126
9289
|
const root = resolveSessionWorktree(event.input.path, ctx.cwd)?.root;
|
|
9127
9290
|
if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId });
|
|
@@ -9135,6 +9298,10 @@ function createGentleAiExtensionForTesting(
|
|
|
9135
9298
|
event.input,
|
|
9136
9299
|
);
|
|
9137
9300
|
if (sensitivePathDenied) return sensitivePathDenied;
|
|
9301
|
+
const oddDelegationDenied = oddDelegationGate.beforeTool(
|
|
9302
|
+
oddSessionId(ctx), event.toolName, event.input, ctx.cwd, readActiveToolNames(pi),
|
|
9303
|
+
);
|
|
9304
|
+
if (oddDelegationDenied) return oddDelegationDenied;
|
|
9138
9305
|
if (event.toolName === "subagent_run") {
|
|
9139
9306
|
const sddAgent = sddDispatchAgentName(event.input);
|
|
9140
9307
|
if (sddAgent === "invalid") {
|
|
@@ -9300,7 +9467,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9300
9467
|
pi.registerCommand("gentle:profiles", {
|
|
9301
9468
|
description: "Create, switch, and manage global agent-model profiles for el Gentleman.",
|
|
9302
9469
|
handler: async (_args, ctx) => {
|
|
9303
|
-
await handleProfilesCommand(ctx);
|
|
9470
|
+
await handleProfilesCommand(ctx, pi);
|
|
9304
9471
|
},
|
|
9305
9472
|
});
|
|
9306
9473
|
|