gentle-pi 4.3.0 → 4.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 +8 -5
- package/assets/agents/gentle-ai-worker.md +1 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +101 -151
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell.md +77 -7
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +19 -15
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +173 -38
- package/extensions/gentle-ai.ts +214 -69
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +332 -73
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +123 -49
- package/lib/gentle-shell-launcher.ts +9 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +4 -3
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +122 -48
- package/runtime/gentle-shell-launcher.mjs +9 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +34 -6
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +226 -5
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +20 -0
- package/tests/bundled-install.test.ts +3 -3
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +1 -1
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +365 -20
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bundled.test.ts +301 -248
- package/tests/gentle-shell-launcher.test.ts +28 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +183 -8
- package/tests/gentle-todo.test.ts +11 -0
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +99 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +14 -2
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
package/extensions/gentle-ai.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { defaultTempScratchFs, isTempScratchRemoval, type TempScratchFs } from "
|
|
|
4
4
|
import { SHELL_COMMAND_TOOLS } from "../lib/background-jobs.ts";
|
|
5
5
|
import { evaluateDeliveredTestTool } from "../lib/delivered-test-lint.ts";
|
|
6
6
|
import { commitSliceToolResult } from "../lib/commit-slice-signal.ts";
|
|
7
|
-
import { appendContextBackstopNotice, createContextBackstopSignal } from "../lib/context-backstop.ts";
|
|
7
|
+
import { appendContextBackstopNotice, createContextBackstopSignal, createThrottledContextMeasure } from "../lib/context-backstop.ts";
|
|
8
8
|
import { blockChildDestructiveCommand } from "./child-safety.ts";
|
|
9
9
|
import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUndocumentedWriterLaunch, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
|
|
10
10
|
import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
writeFile,
|
|
37
37
|
} from "node:fs/promises";
|
|
38
38
|
import { homedir } from "node:os";
|
|
39
|
+
import { discoverAgents, loadAgentsConfig, unmatchedProfileKeys } from "../lib/agents-config.ts";
|
|
39
40
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
40
41
|
import { fileURLToPath } from "node:url";
|
|
41
42
|
import type {
|
|
@@ -47,7 +48,7 @@ import type {
|
|
|
47
48
|
ToolResultEvent,
|
|
48
49
|
ToolResultEventResult,
|
|
49
50
|
} from "@earendil-works/pi-coding-agent";
|
|
50
|
-
import { Key, Text, isKeyRelease, matchesKey, truncateToWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
51
|
+
import { Key, Text, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
51
52
|
import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
52
53
|
import {
|
|
53
54
|
BACKGROUND_SUBAGENTS_FILE,
|
|
@@ -85,6 +86,7 @@ import {
|
|
|
85
86
|
PROFILE_ORCHESTRATOR_KEY,
|
|
86
87
|
profileExportPath,
|
|
87
88
|
profileRoutingRows,
|
|
89
|
+
unmatchedRoutingNotes,
|
|
88
90
|
profilesFilePath,
|
|
89
91
|
readProfileOrchestrator,
|
|
90
92
|
readProfilesFileResult,
|
|
@@ -1209,7 +1211,7 @@ function pinRddStatusLine(pinned: string | undefined, next: string): string {
|
|
|
1209
1211
|
const RDD_OFF_REVIEW_SNIPPET = "Assess an authorized change and return its verification plan (read-only)";
|
|
1210
1212
|
const RDD_OFF_REVIEW_GUIDELINES: readonly string[] = [
|
|
1211
1213
|
'Receipt-driven development is off, so use gentle_review only for {"operation":"assess"} (read-only): after an authorized implementation, delegated or inline, call it over the diff and follow the returned plan; pass input {"escalate":{"item":1-6,"reason":"<one line>"}} only when the change touches a Task Size high-risk item, or {"baseRef":"<ref>","committedOnly":true} to assess a committed range.',
|
|
1212
|
-
"When ASSESS returns reviewDue or a native continuation, relay it unchanged
|
|
1214
|
+
"When ASSESS returns reviewDue or a native continuation, relay it unchanged but do not execute it while RDD is off; assessment metadata does not authorize STATUS, another review lifecycle operation, or enabling RDD. Continue the returned verification plan; tests, builds, and functional checks still run when applicable, and no assessment authorizes delivery.",
|
|
1213
1215
|
];
|
|
1214
1216
|
const RDD_OFF_REVIEW_DESCRIPTION = "Assess the current changes, or a committed range, and return the verification plan. Read-only; review outcomes never authorize delivery: commit, push, pull-request, and release commands follow ordinary repository policy.";
|
|
1215
1217
|
const RDD_OFF_HIDDEN_REVIEW_TOOLS: readonly string[] = ["gentle_review_scope", "gentle_review_capture_group", "gentle_review_capture"];
|
|
@@ -1467,7 +1469,7 @@ Harness principles:
|
|
|
1467
1469
|
- el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
|
|
1468
1470
|
- Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
|
|
1469
1471
|
- Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
|
|
1470
|
-
- For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation).
|
|
1472
|
+
- For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation). Only when your diff changes shared code (options, parsers, helpers) that an existing command the request did not name also uses, run one differential check before finishing, as a single script: for each such command, run it with the new or changed input (for example a new option passed to a sibling subcommand) against the previous code (a copy of the base commit in a \`mktemp -d\` scratch directory, never committed as a test; committed tests never read git history or a fixed commit) and against your change, and compare stdout, stderr, and exit code; any difference nobody asked for is a regression to fix and pin with a test. Test presence alone does not establish applicability; no chat or TUI toggle activates it. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, explain why and run proportionate ordinary functional or structural verification. Never invent lifecycle evidence or skip checks. Follow orchestrator-delegation.md for ODD forwarding and evidence.
|
|
1471
1473
|
- Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
|
|
1472
1474
|
- Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
|
|
1473
1475
|
|
|
@@ -2070,6 +2072,21 @@ function yoloConfiguredRestriction(cwd: string, keys: readonly GuardedCommandKey
|
|
|
2070
2072
|
return restriction;
|
|
2071
2073
|
}
|
|
2072
2074
|
|
|
2075
|
+
/** Super Yolo ignores every configurable guardrail: only code-level denies survive. */
|
|
2076
|
+
const SUPER_YOLO_GUARDRAILS: RuntimeGuardrailsConfig = {
|
|
2077
|
+
autonomousMode: true,
|
|
2078
|
+
guardedCommands: { gitPush: "allow", gitRebase: "allow", gitBranchDeleteForce: "allow", npmPublish: "allow", piRemove: "allow" },
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* Super Yolo command decision: run everything without confirmation except the
|
|
2083
|
+
* code-level catastrophic denies (root/home recursive removal, --no-preserve-root,
|
|
2084
|
+
* hard-denied patterns). Configured guardrails are deliberately not consulted.
|
|
2085
|
+
*/
|
|
2086
|
+
function evaluateSuperYoloCommand(command: string): GuardAction {
|
|
2087
|
+
return evaluateGuardedCommand(command, SUPER_YOLO_GUARDRAILS).action === "block" ? "block" : "allow";
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2073
2090
|
async function confirmCommand(
|
|
2074
2091
|
command: string,
|
|
2075
2092
|
ctx: ExtensionContext,
|
|
@@ -2077,7 +2094,19 @@ async function confirmCommand(
|
|
|
2077
2094
|
herdrLifecycle: HerdrConfirmationLifecycle,
|
|
2078
2095
|
yoloActive = false,
|
|
2079
2096
|
autoGate?: () => Promise<ToolCallEventResult | undefined>,
|
|
2097
|
+
superActive = false,
|
|
2080
2098
|
): Promise<ToolCallEventResult | undefined> {
|
|
2099
|
+
if (superActive) {
|
|
2100
|
+
// Sensitive-path and delivered-test guards already ran before this call;
|
|
2101
|
+
// here only the code-level catastrophic denies remain.
|
|
2102
|
+
if (evaluateSuperYoloCommand(command) === "block") {
|
|
2103
|
+
return {
|
|
2104
|
+
block: true,
|
|
2105
|
+
reason: "Gentle AI safety policy blocked a catastrophic destructive shell command. Ask the user for an explicit safer plan.",
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
return autoGate?.();
|
|
2109
|
+
}
|
|
2081
2110
|
const guardrailsConfig = loadRuntimeGuardrailsConfig(ctx.cwd);
|
|
2082
2111
|
const evaluation = evaluateGuardedCommand(command, guardrailsConfig);
|
|
2083
2112
|
// Configuration provenance is distinct from legacy autonomy resolution, which
|
|
@@ -3093,7 +3122,7 @@ interface OverlayComponent {
|
|
|
3093
3122
|
|
|
3094
3123
|
type ModelPanelResult =
|
|
3095
3124
|
| { type: "save"; config: AgentModelConfig }
|
|
3096
|
-
// `save`, then snapshot saved agent routing and the
|
|
3125
|
+
// `save`, then snapshot saved agent routing and the chosen orchestrator into
|
|
3097
3126
|
// the current profile without changing the global orchestrator defaults.
|
|
3098
3127
|
| { type: "save-profile"; config: AgentModelConfig }
|
|
3099
3128
|
| { type: "custom"; agent: string | "all"; config: AgentModelConfig }
|
|
@@ -3102,6 +3131,7 @@ type ModelPanelResult =
|
|
|
3102
3131
|
| { type: "cancel" };
|
|
3103
3132
|
|
|
3104
3133
|
const SET_ALL_AGENTS = "Set all agents";
|
|
3134
|
+
const LEAVE_ORCHESTRATOR_UNTOUCHED = "leave untouched";
|
|
3105
3135
|
|
|
3106
3136
|
const PANEL_TONE = {
|
|
3107
3137
|
BORDER: "border",
|
|
@@ -3132,6 +3162,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3132
3162
|
private query = "";
|
|
3133
3163
|
private readonly draft: AgentModelConfig;
|
|
3134
3164
|
private readonly rows: string[];
|
|
3165
|
+
private readonly agents: string[];
|
|
3135
3166
|
private readonly modelOptions: string[];
|
|
3136
3167
|
private readonly done: (result: ModelPanelResult) => void;
|
|
3137
3168
|
private readonly theme: Theme | undefined;
|
|
@@ -3150,7 +3181,8 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3150
3181
|
terminalRows?: () => number,
|
|
3151
3182
|
) {
|
|
3152
3183
|
this.draft = cloneModelConfig(initialConfig);
|
|
3153
|
-
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
3184
|
+
this.rows = [SET_ALL_AGENTS, PROFILE_ORCHESTRATOR_KEY, ...agents];
|
|
3185
|
+
this.agents = [...agents];
|
|
3154
3186
|
this.modelOptions = modelOptions;
|
|
3155
3187
|
this.done = done;
|
|
3156
3188
|
this.theme = theme;
|
|
@@ -3273,6 +3305,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3273
3305
|
}
|
|
3274
3306
|
if (matchesKey(data, "e")) {
|
|
3275
3307
|
this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
|
|
3308
|
+
if (this.selectedRow === PROFILE_ORCHESTRATOR_KEY && !this.draft[this.selectedRow]?.model) return;
|
|
3276
3309
|
this.mode = "effort";
|
|
3277
3310
|
this.effortCursor = 0;
|
|
3278
3311
|
return;
|
|
@@ -3354,7 +3387,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3354
3387
|
return;
|
|
3355
3388
|
}
|
|
3356
3389
|
this.applyModelSelection(
|
|
3357
|
-
selected === INHERIT_MODEL ? undefined : selected,
|
|
3390
|
+
selected === INHERIT_MODEL || selected === LEAVE_ORCHESTRATOR_UNTOUCHED ? undefined : selected,
|
|
3358
3391
|
);
|
|
3359
3392
|
this.mode = "agents";
|
|
3360
3393
|
return;
|
|
@@ -3368,7 +3401,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3368
3401
|
private applyModelSelection(model: string | undefined): void {
|
|
3369
3402
|
const row = this.rows[this.cursor];
|
|
3370
3403
|
if (row === SET_ALL_AGENTS) {
|
|
3371
|
-
for (const name of this.
|
|
3404
|
+
for (const name of this.agents) this.setModel(name, model);
|
|
3372
3405
|
return;
|
|
3373
3406
|
}
|
|
3374
3407
|
if (!row) return;
|
|
@@ -3378,7 +3411,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3378
3411
|
private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
|
|
3379
3412
|
const row = this.selectedRow;
|
|
3380
3413
|
if (row === SET_ALL_AGENTS) {
|
|
3381
|
-
for (const name of this.
|
|
3414
|
+
for (const name of this.agents) this.setThinking(name, thinking);
|
|
3382
3415
|
return;
|
|
3383
3416
|
}
|
|
3384
3417
|
this.setThinking(row, thinking);
|
|
@@ -3387,13 +3420,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3387
3420
|
private applyInherit(): void {
|
|
3388
3421
|
const row = this.rows[this.cursor];
|
|
3389
3422
|
if (row === SET_ALL_AGENTS) {
|
|
3390
|
-
for (const name of this.
|
|
3423
|
+
for (const name of this.agents) this.clearEntry(name);
|
|
3391
3424
|
return;
|
|
3392
3425
|
}
|
|
3393
3426
|
if (row) this.clearEntry(row);
|
|
3394
3427
|
}
|
|
3395
3428
|
|
|
3396
3429
|
private setModel(name: string, model: string | undefined): void {
|
|
3430
|
+
if (name === PROFILE_ORCHESTRATOR_KEY && model === undefined) {
|
|
3431
|
+
delete this.draft[name];
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3397
3434
|
const current = this.draft[name] ?? {};
|
|
3398
3435
|
if (model === undefined) delete current.model;
|
|
3399
3436
|
else current.model = model;
|
|
@@ -3402,6 +3439,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3402
3439
|
}
|
|
3403
3440
|
|
|
3404
3441
|
private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
|
|
3442
|
+
if (name === PROFILE_ORCHESTRATOR_KEY && !this.draft[name]?.model) return;
|
|
3405
3443
|
const current = this.draft[name] ?? {};
|
|
3406
3444
|
if (thinking === undefined) delete current.thinking;
|
|
3407
3445
|
else current.thinking = thinking;
|
|
@@ -3410,13 +3448,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3410
3448
|
}
|
|
3411
3449
|
|
|
3412
3450
|
private clearEntry(name: string): void {
|
|
3413
|
-
this.draft[name]
|
|
3451
|
+
if (name === PROFILE_ORCHESTRATOR_KEY) delete this.draft[name];
|
|
3452
|
+
else this.draft[name] = {};
|
|
3414
3453
|
}
|
|
3415
3454
|
|
|
3416
3455
|
private filteredModelOptions(): string[] {
|
|
3456
|
+
const options = this.selectedRow === PROFILE_ORCHESTRATOR_KEY
|
|
3457
|
+
? this.modelOptions.map((option) => option === INHERIT_MODEL ? LEAVE_ORCHESTRATOR_UNTOUCHED : option)
|
|
3458
|
+
: this.modelOptions;
|
|
3417
3459
|
const query = this.query.trim().toLowerCase();
|
|
3418
|
-
if (!query) return
|
|
3419
|
-
return
|
|
3460
|
+
if (!query) return options;
|
|
3461
|
+
return options.filter((option) =>
|
|
3420
3462
|
option.toLowerCase().includes(query),
|
|
3421
3463
|
);
|
|
3422
3464
|
}
|
|
@@ -3473,13 +3515,13 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3473
3515
|
// longer fits the minimum width once the profile key joins the save keys.
|
|
3474
3516
|
lines.push(
|
|
3475
3517
|
line(
|
|
3476
|
-
`j/k scroll • enter model/save • e effort • i ${isProviderReviewRole(this.rows[this.cursor] ?? "") ? "Pi persisted defaults" : "inherit"} • c custom`,
|
|
3518
|
+
`j/k scroll • enter model/save • e effort • i ${this.rows[this.cursor] === PROFILE_ORCHESTRATOR_KEY ? LEAVE_ORCHESTRATOR_UNTOUCHED : isProviderReviewRole(this.rows[this.cursor] ?? "") ? "Pi persisted defaults" : "inherit"} • c custom`,
|
|
3477
3519
|
"muted",
|
|
3478
3520
|
),
|
|
3479
3521
|
);
|
|
3480
3522
|
lines.push(
|
|
3481
3523
|
line(
|
|
3482
|
-
`x export • r restore • ctrl+s save • u
|
|
3524
|
+
`x export • r restore • ctrl+s save agents • u save choice in "${this.profileLabel}" • esc back`,
|
|
3483
3525
|
"muted",
|
|
3484
3526
|
),
|
|
3485
3527
|
);
|
|
@@ -3579,11 +3621,9 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3579
3621
|
}
|
|
3580
3622
|
|
|
3581
3623
|
private renderSetAllLabel(row: string): string {
|
|
3582
|
-
const models = this.
|
|
3583
|
-
.slice(1)
|
|
3624
|
+
const models = this.agents
|
|
3584
3625
|
.map((name) => this.draft[name]?.model ?? routingDefaultLabel(name, "model"));
|
|
3585
|
-
const efforts = this.
|
|
3586
|
-
.slice(1)
|
|
3626
|
+
const efforts = this.agents
|
|
3587
3627
|
.map((name) => this.draft[name]?.thinking ?? routingDefaultLabel(name, "effort"));
|
|
3588
3628
|
const firstModel = models[0] ?? "inherit";
|
|
3589
3629
|
const firstEffort = efforts[0] ?? "inherit";
|
|
@@ -3600,6 +3640,9 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3600
3640
|
}
|
|
3601
3641
|
|
|
3602
3642
|
private renderAgentLabel(row: string): string {
|
|
3643
|
+
if (row === PROFILE_ORCHESTRATOR_KEY && !this.draft[row]?.model) {
|
|
3644
|
+
return `${this.renderText(row.padEnd(20), "text")} ${this.renderText(LEAVE_ORCHESTRATOR_UNTOUCHED, "status")}`;
|
|
3645
|
+
}
|
|
3603
3646
|
const model = this.draft[row]?.model ?? routingDefaultLabel(row, "model");
|
|
3604
3647
|
const effort = this.draft[row]?.thinking ?? routingDefaultLabel(row, "effort");
|
|
3605
3648
|
return `${this.renderText(sanitizeTerminalText(row).padEnd(20), "text")} ${this.renderText("model=", "muted")}${this.renderText(model, "status")}${this.renderText(
|
|
@@ -3674,8 +3717,18 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
|
|
|
3674
3717
|
);
|
|
3675
3718
|
return;
|
|
3676
3719
|
}
|
|
3677
|
-
let config = savedConfig.status === "valid" ? savedConfig.config : {};
|
|
3678
|
-
const
|
|
3720
|
+
let config = savedConfig.status === "valid" ? cloneModelConfig(savedConfig.config) : {};
|
|
3721
|
+
const store = readProfilesFileResult(profilesFilePath(gentleAiConfigHome()));
|
|
3722
|
+
const target = resolveCurrentProfileTarget(ctx.cwd, store);
|
|
3723
|
+
// Existing profiles own the initial choice, including absence. Reopens keep the draft.
|
|
3724
|
+
delete config[PROFILE_ORCHESTRATOR_KEY];
|
|
3725
|
+
if (store.status === "valid" && target && hasOwnProfile(store.file.profiles, target.name)) {
|
|
3726
|
+
const entry = store.file.profiles[target.name][PROFILE_ORCHESTRATOR_KEY];
|
|
3727
|
+
if (entry) config[PROFILE_ORCHESTRATOR_KEY] = { ...entry };
|
|
3728
|
+
} else {
|
|
3729
|
+
config = profileSnapshotFrom(config, liveOrchestratorSnapshot(ctx, pi) ?? readOrchestratorSettings(orchestratorSettingsPath()));
|
|
3730
|
+
}
|
|
3731
|
+
const profileLabel = describeCurrentProfileTarget(target);
|
|
3679
3732
|
let result = await showSddModelPanel(ctx, config, profileLabel);
|
|
3680
3733
|
while (result.type === "custom" || result.type === "export" || result.type === "restore") {
|
|
3681
3734
|
config = cloneModelConfig(result.config);
|
|
@@ -3705,7 +3758,10 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
|
|
|
3705
3758
|
result = await showSddModelPanel(ctx, config, profileLabel);
|
|
3706
3759
|
continue;
|
|
3707
3760
|
}
|
|
3708
|
-
|
|
3761
|
+
const orchestratorChoice = config[PROFILE_ORCHESTRATOR_KEY];
|
|
3762
|
+
config = cloneModelConfig(restored);
|
|
3763
|
+
delete config[PROFILE_ORCHESTRATOR_KEY];
|
|
3764
|
+
if (orchestratorChoice) config[PROFILE_ORCHESTRATOR_KEY] = orchestratorChoice;
|
|
3709
3765
|
try {
|
|
3710
3766
|
const applyResult = await applyModelConfigAsync(ctx.cwd, restored);
|
|
3711
3767
|
ctx.ui.notify([
|
|
@@ -3767,18 +3823,21 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
|
|
|
3767
3823
|
result = await showSddModelPanel(ctx, config, profileLabel);
|
|
3768
3824
|
}
|
|
3769
3825
|
if (result.type !== "save" && result.type !== "save-profile") return;
|
|
3770
|
-
|
|
3771
|
-
const
|
|
3826
|
+
// The panel's orchestrator is snapshot-only, never a global agent route.
|
|
3827
|
+
const routing = cloneModelConfig(result.config);
|
|
3828
|
+
delete routing[PROFILE_ORCHESTRATOR_KEY];
|
|
3829
|
+
writeModelConfig(ctx.cwd, routing);
|
|
3830
|
+
const applyResult = await applyModelConfigAsync(ctx.cwd, routing);
|
|
3772
3831
|
ctx.ui.notify(
|
|
3773
3832
|
[
|
|
3774
3833
|
"el Gentleman global model config saved.",
|
|
3775
3834
|
`Global config: ${modelConfigPath(ctx.cwd)}`,
|
|
3776
3835
|
`Agents updated: ${applyResult.updated}`,
|
|
3777
|
-
...describeModelConfig(ctx.cwd,
|
|
3836
|
+
...describeModelConfig(ctx.cwd, routing),
|
|
3778
3837
|
].join("\n"),
|
|
3779
3838
|
"info",
|
|
3780
3839
|
);
|
|
3781
|
-
if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx,
|
|
3840
|
+
if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx, result.config[PROFILE_ORCHESTRATOR_KEY]);
|
|
3782
3841
|
}
|
|
3783
3842
|
|
|
3784
3843
|
/** The profile `u` in `/gentle:models` writes to, and why it is that one. */
|
|
@@ -3810,11 +3869,10 @@ function describeCurrentProfileTarget(target: CurrentProfileTarget | undefined):
|
|
|
3810
3869
|
|
|
3811
3870
|
/**
|
|
3812
3871
|
* The second half of `u` captures saved global agent routing (never the pin)
|
|
3813
|
-
* and the
|
|
3814
|
-
*
|
|
3815
|
-
* outcome here speaks only about the profile.
|
|
3872
|
+
* and the explicit panel choice, including key absence. The global save has
|
|
3873
|
+
* already been reported, so every outcome here speaks only about the profile.
|
|
3816
3874
|
*/
|
|
3817
|
-
function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext,
|
|
3875
|
+
function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, orchestrator: AgentRoutingEntry | undefined): void {
|
|
3818
3876
|
const path = profilesFilePath(gentleAiConfigHome());
|
|
3819
3877
|
const read = readProfilesFileResult(path);
|
|
3820
3878
|
if (read.status === "invalid") {
|
|
@@ -3824,16 +3882,9 @@ function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, pi: Extensi
|
|
|
3824
3882
|
);
|
|
3825
3883
|
return;
|
|
3826
3884
|
}
|
|
3827
|
-
const snapshot =
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
);
|
|
3831
|
-
if (ctx.model) {
|
|
3832
|
-
snapshot[PROFILE_ORCHESTRATOR_KEY] = {
|
|
3833
|
-
model: `${ctx.model.provider}/${ctx.model.id}`,
|
|
3834
|
-
thinking: pi.getThinkingLevel(),
|
|
3835
|
-
};
|
|
3836
|
-
}
|
|
3885
|
+
const snapshot = cloneModelConfig(readGlobalEffectiveModelConfig(ctx.cwd));
|
|
3886
|
+
delete snapshot[PROFILE_ORCHESTRATOR_KEY];
|
|
3887
|
+
if (orchestrator?.model) snapshot[PROFILE_ORCHESTRATOR_KEY] = { ...orchestrator };
|
|
3837
3888
|
if (read.status === "missing") {
|
|
3838
3889
|
// The same seed `/gentle:profiles` performs on its first open, so pressing
|
|
3839
3890
|
// `u` before ever opening that panel lands on the same "current" profile.
|
|
@@ -4012,10 +4063,10 @@ function profilePinScopeNote(cwd: string): string | undefined {
|
|
|
4012
4063
|
* write that already succeeded must not become an error because a probe about sharing
|
|
4013
4064
|
* it failed.
|
|
4014
4065
|
*/
|
|
4015
|
-
function repoDeclarationSharing(root: string, path: string): string {
|
|
4066
|
+
export function repoDeclarationSharing(root: string, path: string): string {
|
|
4016
4067
|
const relativePath = relative(root, path).split(sep).join("/");
|
|
4017
4068
|
const run = (...arguments_: string[]): string =>
|
|
4018
|
-
execFileSync("git", arguments_, { cwd: root, encoding: "utf8" }).trim();
|
|
4069
|
+
execFileSync("git", arguments_, { cwd: root, encoding: "utf8", windowsHide: true }).trim();
|
|
4019
4070
|
try {
|
|
4020
4071
|
// Exit 0 means gitignore matches the path; the exact negation line is what makes
|
|
4021
4072
|
// the declaration committable again.
|
|
@@ -4067,6 +4118,8 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
4067
4118
|
// Actions reopen the panel, refreshing this snapshot without disk reads during rendering.
|
|
4068
4119
|
private readonly pinStatus: ProfilePinStatus | undefined;
|
|
4069
4120
|
private readonly sessionBoundName: string | undefined;
|
|
4121
|
+
private readonly discoveredAgentNames: string[];
|
|
4122
|
+
private readonly unmatchedKeys: string[];
|
|
4070
4123
|
private readonly liveOrchestrator: (() => AgentRoutingEntry | undefined) | undefined;
|
|
4071
4124
|
|
|
4072
4125
|
constructor(
|
|
@@ -4082,6 +4135,8 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
4082
4135
|
requestRender: () => void,
|
|
4083
4136
|
pinStatus: () => ProfilePinStatus | undefined,
|
|
4084
4137
|
sessionBound: () => string | undefined,
|
|
4138
|
+
discoveredAgentNames: string[],
|
|
4139
|
+
unmatchedKeys: string[],
|
|
4085
4140
|
liveOrchestrator?: () => AgentRoutingEntry | undefined,
|
|
4086
4141
|
feedback?: string,
|
|
4087
4142
|
) {
|
|
@@ -4096,6 +4151,8 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
4096
4151
|
this.orchestratorSettings = orchestratorSettings;
|
|
4097
4152
|
this.pinStatus = pinStatus();
|
|
4098
4153
|
this.sessionBoundName = sessionBound();
|
|
4154
|
+
this.discoveredAgentNames = discoveredAgentNames;
|
|
4155
|
+
this.unmatchedKeys = unmatchedKeys;
|
|
4099
4156
|
this.liveOrchestrator = liveOrchestrator;
|
|
4100
4157
|
const items = buildProfileListItems(file, evaluateProfilePin(this.pinStatus, file.profiles).winner?.profile, this.sessionBoundName);
|
|
4101
4158
|
this.listItems = items;
|
|
@@ -4331,10 +4388,30 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
4331
4388
|
: [this.renderLine(`session ${sanitizeTerminalText(this.sessionBoundName)} (session) — launches resolve it ahead of pins and the global default; nothing was written`, width, "muted")]),
|
|
4332
4389
|
"",
|
|
4333
4390
|
this.renderLine("Profile routing", width, "accent"),
|
|
4334
|
-
...this.indentLines(this.routingLines(profileRows, widths), width),
|
|
4391
|
+
...this.indentLines(this.routingLines(profileRows, widths, width), width),
|
|
4335
4392
|
"",
|
|
4336
4393
|
this.renderLine("Current routing (effective)", width, "accent"),
|
|
4337
|
-
...this.indentLines(this.routingLines(currentRows, widths), width),
|
|
4394
|
+
...this.indentLines(this.routingLines(currentRows, widths, width), width),
|
|
4395
|
+
...this.unmatchedKeyLines(currentRows, width),
|
|
4396
|
+
];
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
private unmatchedKeyLines(currentRows: ProfileRoutingRow[], width: number): string[] {
|
|
4400
|
+
// Issue #2400: a model_profiles key that matches no discovered agent and
|
|
4401
|
+
// suggests none never becomes a routing row, so it is listed here, one key
|
|
4402
|
+
// per line, with the same inline-or-↳ note rule as the routing rows. A key
|
|
4403
|
+
// already shown as a current row carries its note there instead.
|
|
4404
|
+
const shown = new Set(currentRows.map((row) => row.agent));
|
|
4405
|
+
const keys = this.unmatchedKeys.filter((key) => !shown.has(key));
|
|
4406
|
+
if (keys.length === 0) return [];
|
|
4407
|
+
const note = "no matching agent";
|
|
4408
|
+
return [
|
|
4409
|
+
"",
|
|
4410
|
+
this.renderLine("Unmatched keys (ignored)", width, "accent"),
|
|
4411
|
+
...this.indentLines(keys.flatMap((key) => {
|
|
4412
|
+
const inline = `${key} ← ${note}`;
|
|
4413
|
+
return visibleWidth(inline) + 2 <= width ? [inline] : [key, ` ↳ ${note}`];
|
|
4414
|
+
}), width),
|
|
4338
4415
|
];
|
|
4339
4416
|
}
|
|
4340
4417
|
|
|
@@ -4356,11 +4433,24 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
4356
4433
|
private routingLines(
|
|
4357
4434
|
rows: ProfileRoutingRow[],
|
|
4358
4435
|
widths: { agent: number; model: number },
|
|
4436
|
+
width: number,
|
|
4359
4437
|
): string[] {
|
|
4360
4438
|
if (rows.length === 0) {
|
|
4361
4439
|
return ["No routing entries — every agent inherits its default model."];
|
|
4362
4440
|
}
|
|
4363
|
-
|
|
4441
|
+
// Issue #2196: a key that names no discovered agent was rendered as a
|
|
4442
|
+
// valid-looking row while its setting was silently ignored at launch, so
|
|
4443
|
+
// each unmatched row now carries its note inline. When the row would push
|
|
4444
|
+
// the note past the pane edge (indentLines adds two columns before
|
|
4445
|
+
// truncating), the note moves to its own line so it is never cut off.
|
|
4446
|
+
const notes = unmatchedRoutingNotes(rows, this.discoveredAgentNames);
|
|
4447
|
+
return rows.flatMap((row) => {
|
|
4448
|
+
const line = formatRoutingRow(row, widths);
|
|
4449
|
+
const note = notes.get(row.agent);
|
|
4450
|
+
if (note === undefined) return [line];
|
|
4451
|
+
const inline = `${line} ← ${note}`;
|
|
4452
|
+
return visibleWidth(inline) + 2 <= width ? [inline] : [line, ` ↳ ${note}`];
|
|
4453
|
+
});
|
|
4364
4454
|
}
|
|
4365
4455
|
|
|
4366
4456
|
private indentLines(lines: string[], width: number): string[] {
|
|
@@ -4406,6 +4496,28 @@ async function showProfilesPanel(
|
|
|
4406
4496
|
// Both orchestrator and pin state are snapshots for this panel visit.
|
|
4407
4497
|
// Actions (including p/P) reopen the panel and read fresh state.
|
|
4408
4498
|
const orchestratorSettings = readOrchestratorSettings(orchestratorSettingsPath());
|
|
4499
|
+
// Issue #2196: the panel flags routing keys that name no discovered agent, so
|
|
4500
|
+
// it needs one discovery snapshot per visit; a failed or partial discovery is
|
|
4501
|
+
// not an error here, it just renders without notes.
|
|
4502
|
+
const discoveryRoots = {
|
|
4503
|
+
cwd: ctx.sessionManager.getCwd?.() ?? ctx.cwd,
|
|
4504
|
+
home: homedir(),
|
|
4505
|
+
agentHome: resolveGentlePiAgentHome(),
|
|
4506
|
+
};
|
|
4507
|
+
const discoveredAgents = discoverAgents(discoveryRoots).agents;
|
|
4508
|
+
// gentle-pi#2271: review-refuter and review-validator are provider review
|
|
4509
|
+
// roles the relay routes by key; they never have an agent file, so they
|
|
4510
|
+
// count as matched instead of being flagged "no matching agent".
|
|
4511
|
+
const discoveredAgentNames = [...new Set([...PROVIDER_REVIEW_ROLES, ...discoveredAgents.map((agent) => agent.name)])];
|
|
4512
|
+
// Issue #2400: launches resolve from the subagents.json model_profiles, which
|
|
4513
|
+
// the effective table only reads per discovered agent, so a key with no match
|
|
4514
|
+
// and no suggestion never appeared. When a session binding or a pin wins, the
|
|
4515
|
+
// table already shows that snapshot's own keys with their notes.
|
|
4516
|
+
const unmatchedKeys = sessionBoundName !== undefined || pinnedEffectiveModelConfig(ctx.cwd) !== undefined
|
|
4517
|
+
? []
|
|
4518
|
+
: unmatchedProfileKeys(loadAgentsConfig(discoveryRoots).modelProfiles, discoveredAgents)
|
|
4519
|
+
.filter((entry) => entry.suggestion === undefined && !isProviderReviewRole(entry.key))
|
|
4520
|
+
.map((entry) => entry.key);
|
|
4409
4521
|
return ctx.ui.custom<ProfilesPanelResult>(
|
|
4410
4522
|
(tui, theme, keybindings, done) => {
|
|
4411
4523
|
const panel = new ProfilesPanel(
|
|
@@ -4421,6 +4533,8 @@ async function showProfilesPanel(
|
|
|
4421
4533
|
() => tui.requestRender(),
|
|
4422
4534
|
() => readProfilePinStatus(ctx.cwd),
|
|
4423
4535
|
() => sessionBoundName,
|
|
4536
|
+
discoveredAgentNames,
|
|
4537
|
+
unmatchedKeys,
|
|
4424
4538
|
() => liveOrchestratorSnapshot(ctx, live)?.entry,
|
|
4425
4539
|
status,
|
|
4426
4540
|
);
|
|
@@ -5293,7 +5407,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
|
|
|
5293
5407
|
},
|
|
5294
5408
|
input: {
|
|
5295
5409
|
type: "string",
|
|
5296
|
-
description: "A JSON object or serialized object string for START/ASSESS
|
|
5410
|
+
description: "A JSON object or serialized object string for START/ASSESS/answer-consent; every other operation requires a serialized object string. New native ordinary START uses {\"mode\":\"ordinary\"}, plus the agent lens selection {\"lenses\":[\"risk\",...],\"lensesReason\":\"<what you touched and how>\"} (both or neither, 1-4 of risk, resilience, readability, reliability; never with focus; providers that predate it keep the tier default); 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/#1175). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); writerModelId and writerEffort are only a fallback when no runtime evidence exists (caller), and with neither the profile fails closed to small (fallback). The writer profile is reported but never adds a verifier: the plan adds the independent verifier only when escalate names a Task Size high-risk item. ASSESS derives `closed` only from the native candidate.consumed fact for this exact candidate, which native records only when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown. Every outcome yields the exact risk-gated plan ASSESS returns when RDD is off: a closed review never removes the separate verifier the tier requires (verify-always-rdd-high S1), unknown is never treated as closed, and a decline is candidate-scoped and never lowers the bar below the RDD-off path. A declined or unavailable outcome, stated explicitly or recorded by this process for this exact candidate (never a different one, and never from repository state alone), wins over closure. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
|
|
5297
5411
|
},
|
|
5298
5412
|
outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
5299
5413
|
inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
|
|
@@ -5564,7 +5678,7 @@ function parseReviewControllerParameters(value: unknown): ReviewControllerParame
|
|
|
5564
5678
|
};
|
|
5565
5679
|
for (const key of ["changeName", "idempotencyKey", "transition", "input", "outputPath", "inputPath", "operationId", "lineageIds", "acknowledgeUntrustedBundleSource", "workspaceRoot"] as const) {
|
|
5566
5680
|
const optional = value[key];
|
|
5567
|
-
if (key === "input" && key in value && (value.operation === REVIEW_CONTROLLER_OPERATION.START || value.operation === REVIEW_CONTROLLER_OPERATION.ASSESS)) {
|
|
5681
|
+
if (key === "input" && key in value && (value.operation === REVIEW_CONTROLLER_OPERATION.START || value.operation === REVIEW_CONTROLLER_OPERATION.ASSESS || value.operation === REVIEW_CONTROLLER_OPERATION.ANSWER_CONSENT)) {
|
|
5568
5682
|
parameters.input = serializeReviewJsonArgument(optional);
|
|
5569
5683
|
continue;
|
|
5570
5684
|
}
|
|
@@ -5792,21 +5906,18 @@ const REVIEW_MODE_DISABLED_OUTCOME = "review-mode-disabled";
|
|
|
5792
5906
|
// to make the global opt-in first when needed, then clear this clone override.
|
|
5793
5907
|
// Pi never mutates the operator's global gentle-ai state automatically.
|
|
5794
5908
|
//
|
|
5795
|
-
//
|
|
5796
|
-
//
|
|
5797
|
-
//
|
|
5798
|
-
// `default`
|
|
5799
|
-
//
|
|
5800
|
-
//
|
|
5801
|
-
//
|
|
5802
|
-
//
|
|
5803
|
-
// global is the only scope that can turn reviews on at all, and Pi answers the
|
|
5804
|
-
// same. Leaving this undefined would hand the single most common state a dead
|
|
5805
|
-
// end.
|
|
5909
|
+
// Receipt-driven development is on by default (gentle-ai v4.0.0, #4803): an
|
|
5910
|
+
// all-sources-unset install resolves to ON with source `default`. The pinned
|
|
5911
|
+
// v2.4.0 runtime had briefly made it opt-in, which is why this branch exists.
|
|
5912
|
+
// Today `default` only reaches here as gentle-ai's fail-closed projection: the
|
|
5913
|
+
// mode could not be resolved, so it reports OFF with source `default`. The
|
|
5914
|
+
// continuation says so instead of claiming the install never opted in, and
|
|
5915
|
+
// still names `global`, the only scope that can turn reviews on explicitly.
|
|
5916
|
+
// Leaving this undefined would hand that state a dead end.
|
|
5806
5917
|
function reviewModeContinuation(source: NativeReviewModeSource): string | undefined {
|
|
5807
5918
|
if (source === NATIVE_REVIEW_MODE_SOURCE.CLONE_LOCAL) return "Run `gentle-ai review mode enable --scope=global` if global RDD is still off, then run /gentle:review-mode enable to clear this clone-local override.";
|
|
5808
5919
|
if (source === NATIVE_REVIEW_MODE_SOURCE.GLOBAL) return "Run `gentle-ai review mode enable --scope=global` to turn reviews back on; /gentle:review-mode enable only clears the clone-local setting, which cannot override a global off.";
|
|
5809
|
-
return "Run `gentle-ai review mode enable --scope=global` to
|
|
5920
|
+
return "RDD is on by default, but this install could not resolve the review mode and failed closed to off. Run `gentle-ai review mode status` to see why, or `gentle-ai review mode enable --scope=global` to turn reviews on explicitly. /gentle:review-mode enable only clears a clone-local override and cannot enable global RDD.";
|
|
5810
5921
|
}
|
|
5811
5922
|
|
|
5812
5923
|
// Names the situation before the mechanism, then the mechanism, mirroring
|
|
@@ -7459,11 +7570,24 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
|
|
|
7459
7570
|
// its provider through the live model registry the caller supplies, or it is
|
|
7460
7571
|
// refused before materialize ever runs (validateReviewerSelectionConfiguration
|
|
7461
7572
|
// in lib/review-host-relay.ts).
|
|
7573
|
+
//
|
|
7574
|
+
// gentle-pi#2271: review-refuter and review-validator are provider review
|
|
7575
|
+
// roles, not installable agents, so nothing seeds their entries and profiles
|
|
7576
|
+
// never carry them. For those two keys only, a snapshot with no model falls
|
|
7577
|
+
// back to Pi's persisted default model and effort, the default the routing
|
|
7578
|
+
// panel already labels them with. Lens keys keep the fail-closed rule.
|
|
7462
7579
|
function reviewHostRelaySelection(lens: string | undefined, config: AgentModelConfig): { selection?: string; thinking?: string; routingKey: string } {
|
|
7463
7580
|
const routingKey = lens === undefined || lens.length === 0 ? "review capture" : lens.startsWith("review-") ? lens : `review-${lens}`;
|
|
7464
7581
|
const entry = config[routingKey];
|
|
7465
|
-
|
|
7466
|
-
|
|
7582
|
+
let model = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).model === "string" && (entry as AgentRoutingEntry).model!.length > 0 ? (entry as AgentRoutingEntry).model : undefined;
|
|
7583
|
+
let thinking = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).thinking === "string" ? (entry as AgentRoutingEntry).thinking : undefined;
|
|
7584
|
+
if (model === undefined && isProviderReviewRole(routingKey)) {
|
|
7585
|
+
const persisted = readOrchestratorSettings(orchestratorSettingsPath());
|
|
7586
|
+
if (persisted.status === "valid" && persisted.entry?.model !== undefined) {
|
|
7587
|
+
model = persisted.entry.model;
|
|
7588
|
+
thinking ??= persisted.entry.thinking;
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
7467
7591
|
return {
|
|
7468
7592
|
...(model === undefined ? {} : { selection: model }),
|
|
7469
7593
|
...(thinking === undefined ? {} : { thinking }),
|
|
@@ -9630,6 +9754,7 @@ export const __testing = {
|
|
|
9630
9754
|
guardedCommandTitle,
|
|
9631
9755
|
loadRuntimeGuardrailsConfig,
|
|
9632
9756
|
isReversibleYoloCommand,
|
|
9757
|
+
evaluateSuperYoloCommand,
|
|
9633
9758
|
yoloConfiguredRestriction,
|
|
9634
9759
|
buildGentlePrompt,
|
|
9635
9760
|
nativeStatusUnsupported,
|
|
@@ -9700,6 +9825,9 @@ export interface GentleAiRuntimeDependencies {
|
|
|
9700
9825
|
telemetryExecFileAdapter?: ExecFileAdapter;
|
|
9701
9826
|
/** Test-only inference seam; production always uses the active session model. */
|
|
9702
9827
|
autoPermissionClassifier?: AutoRuntimeClassifier;
|
|
9828
|
+
// The backstop samples the measured context at most once per window (its measure rebuilds the whole
|
|
9829
|
+
// session projection). Tests inject a clock or a zero window; production resolves the env and default.
|
|
9830
|
+
contextBackstopMeasure?: { now?: () => number; minIntervalMs?: number };
|
|
9703
9831
|
}
|
|
9704
9832
|
|
|
9705
9833
|
export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencies = {}): (pi: ExtensionAPI) => void {
|
|
@@ -10358,7 +10486,7 @@ function createGentleAiExtensionForTesting(
|
|
|
10358
10486
|
// and forwards only systemPromptOptions, so the harness is delivered
|
|
10359
10487
|
// through the mutable appendSystemPrompt section instead of a replacement.
|
|
10360
10488
|
appendSystemPromptOnce(event.systemPromptOptions, `${gentlePrompt}${reviewContractPrompt}`);
|
|
10361
|
-
updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx));
|
|
10489
|
+
updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx), isPrimarySession && await yolo.superActive(ctx));
|
|
10362
10490
|
updateAutoPrompt(event.systemPromptOptions, isPrimarySession && await yolo.mode(ctx) === "auto");
|
|
10363
10491
|
return undefined;
|
|
10364
10492
|
});
|
|
@@ -10429,14 +10557,23 @@ function createGentleAiExtensionForTesting(
|
|
|
10429
10557
|
} catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
|
|
10430
10558
|
};
|
|
10431
10559
|
const contextBackstopSignal = createContextBackstopSignal(permissionEnvironment);
|
|
10560
|
+
// One projection rebuild per session per window, not one per tool result: the measured context is
|
|
10561
|
+
// sampled (see createThrottledContextMeasure), so long sessions stop paying an O(session) scan on
|
|
10562
|
+
// every tool result while the backstop keeps its once-per-crossing notice semantics.
|
|
10563
|
+
const contextBackstopMeasure = createThrottledContextMeasure({
|
|
10564
|
+
now: dependencies.contextBackstopMeasure?.now,
|
|
10565
|
+
minIntervalMs: dependencies.contextBackstopMeasure?.minIntervalMs,
|
|
10566
|
+
env: permissionEnvironment,
|
|
10567
|
+
});
|
|
10432
10568
|
pi.on("tool_result", async (event, ctx) => {
|
|
10433
|
-
// Harness roadmap R1: the primary session's measured context is
|
|
10434
|
-
//
|
|
10435
|
-
//
|
|
10569
|
+
// Harness roadmap R1: the primary session's measured context is sampled against the backstop; the
|
|
10570
|
+
// first sampled result at or past it carries one notice, after any other patch below. The sample
|
|
10571
|
+
// runs at most once per window per session (default 5 s, GENTLE_PI_CONTEXT_BACKSTOP_MEASURE_MS),
|
|
10572
|
+
// so a crossing is announced on the next sampled result — never later than one window past it.
|
|
10436
10573
|
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
10437
10574
|
const primary = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(sessionKey) ?? 0) === 0;
|
|
10438
10575
|
let notice: string | undefined;
|
|
10439
|
-
try { notice = primary ? contextBackstopSignal(sessionKey, ctx.getContextUsage?.()) : undefined; } catch { notice = undefined; }
|
|
10576
|
+
try { notice = primary ? contextBackstopSignal(sessionKey, contextBackstopMeasure(sessionKey, () => ctx.getContextUsage?.())) : undefined; } catch { notice = undefined; }
|
|
10440
10577
|
const withNotice = (patch: ToolResultEventResult | undefined) => notice ? appendContextBackstopNotice(event, patch, notice) : patch;
|
|
10441
10578
|
// Harness roadmap T4: a parent commit reports the branch's authored
|
|
10442
10579
|
// changed lines so delivery slicing reads a number, not an estimate.
|
|
@@ -10455,7 +10592,11 @@ function createGentleAiExtensionForTesting(
|
|
|
10455
10592
|
return { block: true, reason: `${event.toolName} runs only as a direct tool call: call it directly, never from codemode or another tool.` };
|
|
10456
10593
|
}
|
|
10457
10594
|
const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
|
|
10458
|
-
|
|
10595
|
+
// One early mode read settles the session binding (and its one-time epoch
|
|
10596
|
+
// bump) before the guarded region below captures its own epoch.
|
|
10597
|
+
const settledMode = primaryToolCall ? await yolo.mode(ctx, { toolCallId: event.toolCallId }) : "normal";
|
|
10598
|
+
const yoloActive = primaryToolCall && settledMode === "yolo";
|
|
10599
|
+
const superYoloActive = primaryToolCall && settledMode === "super-yolo";
|
|
10459
10600
|
const sensitivePathDenied = evaluateSensitivePathTool(
|
|
10460
10601
|
event.toolName,
|
|
10461
10602
|
event.input,
|
|
@@ -10501,13 +10642,17 @@ function createGentleAiExtensionForTesting(
|
|
|
10501
10642
|
if (childDenied) return childDenied;
|
|
10502
10643
|
}
|
|
10503
10644
|
const modeEpoch = yolo.epoch();
|
|
10645
|
+
// The admission's authoritative mode read is always a fresh capture, never
|
|
10646
|
+
// the admission slot: a reselection while this lookup is in flight must
|
|
10647
|
+
// invalidate it (mode() answers Normal, the epoch check below blocks), and
|
|
10648
|
+
// tests/auto-mode-invariants.test.ts holds exactly this lookup to lock it.
|
|
10504
10649
|
const permissionMode = primaryToolCall ? await yolo.mode(ctx) : "normal";
|
|
10505
10650
|
// A superseded lookup returns Normal for display, not permission to skip Auto.
|
|
10506
10651
|
if (primaryToolCall && modeEpoch !== yolo.epoch()) {
|
|
10507
10652
|
return { block: true, reason: "Permission mode changed during tool admission. This action was not run; retry under the current mode." };
|
|
10508
10653
|
}
|
|
10509
10654
|
const autoGate = permissionMode === "auto" ? gate : undefined;
|
|
10510
|
-
return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate);
|
|
10655
|
+
return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate, superYoloActive);
|
|
10511
10656
|
});
|
|
10512
10657
|
|
|
10513
10658
|
for (const owner of ["delegation", "review"] as const) {
|
|
@@ -10526,7 +10671,7 @@ function createGentleAiExtensionForTesting(
|
|
|
10526
10671
|
}
|
|
10527
10672
|
|
|
10528
10673
|
pi.registerCommand("gentle:models", {
|
|
10529
|
-
description: "Configure global
|
|
10674
|
+
description: "Configure global agent models and choose a profile orchestrator.",
|
|
10530
10675
|
handler: async (_args, ctx) => {
|
|
10531
10676
|
await handleModelsCommand(ctx, pi);
|
|
10532
10677
|
},
|