gentle-pi 4.2.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 +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -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 +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- 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/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -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/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- 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 +20 -5
- 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-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- 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/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- 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-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- 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 +485 -7
- 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 +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- 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/destructive-command-guard.test.ts +85 -1
- 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 +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- 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-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- 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 +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- 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 +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- 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 +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -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-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- 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/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- 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 +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
|
|
2
2
|
import { dataArgumentRanges, recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
|
|
3
|
+
import { defaultTempScratchFs, isTempScratchRemoval, type TempScratchFs } from "../lib/yolo-temp-scratch.ts";
|
|
3
4
|
import { SHELL_COMMAND_TOOLS } from "../lib/background-jobs.ts";
|
|
5
|
+
import { evaluateDeliveredTestTool } from "../lib/delivered-test-lint.ts";
|
|
6
|
+
import { commitSliceToolResult } from "../lib/commit-slice-signal.ts";
|
|
7
|
+
import { appendContextBackstopNotice, createContextBackstopSignal, createThrottledContextMeasure } from "../lib/context-backstop.ts";
|
|
4
8
|
import { blockChildDestructiveCommand } from "./child-safety.ts";
|
|
5
|
-
import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
|
|
9
|
+
import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUndocumentedWriterLaunch, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
|
|
10
|
+
import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
|
|
6
11
|
import { consumeReviewMutation, pendingReviewMutation, pendingReviewMutationProfiles, recordReviewMutation, type ReceiptSession } from "../lib/review-reminder-receipt.ts";
|
|
7
12
|
import { createReviewSidebarPublisher } from "../lib/review-sidebar-state.ts";
|
|
8
13
|
import { StatusTimingDiagnostics } from "../lib/status-timing-diagnostics.ts";
|
|
@@ -20,6 +25,7 @@ import {
|
|
|
20
25
|
readdirSync,
|
|
21
26
|
readFileSync,
|
|
22
27
|
realpathSync,
|
|
28
|
+
statSync,
|
|
23
29
|
writeFileSync,
|
|
24
30
|
} from "node:fs";
|
|
25
31
|
import {
|
|
@@ -30,6 +36,7 @@ import {
|
|
|
30
36
|
writeFile,
|
|
31
37
|
} from "node:fs/promises";
|
|
32
38
|
import { homedir } from "node:os";
|
|
39
|
+
import { discoverAgents, loadAgentsConfig, unmatchedProfileKeys } from "../lib/agents-config.ts";
|
|
33
40
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
34
41
|
import { fileURLToPath } from "node:url";
|
|
35
42
|
import type {
|
|
@@ -38,8 +45,10 @@ import type {
|
|
|
38
45
|
Theme,
|
|
39
46
|
ThemeColor,
|
|
40
47
|
ToolCallEventResult,
|
|
48
|
+
ToolResultEvent,
|
|
49
|
+
ToolResultEventResult,
|
|
41
50
|
} from "@earendil-works/pi-coding-agent";
|
|
42
|
-
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";
|
|
43
52
|
import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
44
53
|
import {
|
|
45
54
|
BACKGROUND_SUBAGENTS_FILE,
|
|
@@ -77,6 +86,7 @@ import {
|
|
|
77
86
|
PROFILE_ORCHESTRATOR_KEY,
|
|
78
87
|
profileExportPath,
|
|
79
88
|
profileRoutingRows,
|
|
89
|
+
unmatchedRoutingNotes,
|
|
80
90
|
profilesFilePath,
|
|
81
91
|
readProfileOrchestrator,
|
|
82
92
|
readProfilesFileResult,
|
|
@@ -222,7 +232,7 @@ import {
|
|
|
222
232
|
isSmallWriterProfile,
|
|
223
233
|
resolveWriterProfile,
|
|
224
234
|
decodeAgentRiskEscalation,
|
|
225
|
-
|
|
235
|
+
verificationRisk,
|
|
226
236
|
HIGH_RISK_ITEMS,
|
|
227
237
|
RDD_LINE,
|
|
228
238
|
WRITER_PROFILE,
|
|
@@ -264,6 +274,8 @@ import {
|
|
|
264
274
|
sameReviewSessionIdentity,
|
|
265
275
|
type ReviewSessionIdentity,
|
|
266
276
|
} from "../lib/review-session-standing-permission.ts";
|
|
277
|
+
import { herdrSetupHint } from "../lib/herdr-activity.ts";
|
|
278
|
+
import { isInteractiveMode } from "../lib/rpc-host.ts";
|
|
267
279
|
|
|
268
280
|
const GRAPH_V1_ORDINARY_READ_ONLY = "Graph-v1 ordinary review authority is read-only; use native compact-v2 review operations";
|
|
269
281
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -788,6 +800,7 @@ const rddStatusMemo = new Map<string, { readonly status: NativeReviewModeStatus
|
|
|
788
800
|
/** @internal test seam: clears the per-cwd RDD status memo. */
|
|
789
801
|
function clearRddStatusMemoForTesting(): void {
|
|
790
802
|
rddStatusMemo.clear();
|
|
803
|
+
rddStatusUnavailableWarned = false;
|
|
791
804
|
}
|
|
792
805
|
|
|
793
806
|
// gentle-pi#668 (corrected): last-known outcome for ONE candidate, keyed by
|
|
@@ -852,29 +865,62 @@ async function readRddModeStatusOnce(
|
|
|
852
865
|
cwd: string,
|
|
853
866
|
signal?: AbortSignal,
|
|
854
867
|
): Promise<NativeReviewModeStatus | undefined> {
|
|
855
|
-
|
|
868
|
+
const read = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
|
|
869
|
+
return "status" in read ? read.status : undefined;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// gentle-shell#2250: the same read, keeping why it failed so the one-shot
|
|
873
|
+
// unavailable notice can tell a timeout from a native refusal (for example a
|
|
874
|
+
// repository on a network share that Git or gentle-ai rejects by ownership).
|
|
875
|
+
async function readRddModeStatusWithCause(
|
|
876
|
+
nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
|
|
877
|
+
cwd: string,
|
|
878
|
+
signal?: AbortSignal,
|
|
879
|
+
): Promise<{ readonly status: NativeReviewModeStatus } | { readonly cause: string }> {
|
|
880
|
+
if (!nativeReviewCli?.reviewMode) return { cause: "the native gentle-ai review CLI is not available" };
|
|
856
881
|
try {
|
|
857
882
|
const call = nativeReviewCli.reviewMode({ cwd, operation: NATIVE_REVIEW_MODE_OPERATION.STATUS, signal });
|
|
858
883
|
const result = signal === undefined ? await call : await Promise.race([call, rddAbortRejection(signal)]);
|
|
859
|
-
return result.status;
|
|
860
|
-
} catch {
|
|
861
|
-
return
|
|
884
|
+
return { status: result.status };
|
|
885
|
+
} catch (error) {
|
|
886
|
+
return { cause: describeRddStatusFailure(error, signal) };
|
|
862
887
|
}
|
|
863
888
|
}
|
|
864
889
|
|
|
890
|
+
const RDD_STATUS_CAUSE_LIMIT = 300;
|
|
891
|
+
|
|
892
|
+
function describeRddStatusFailure(error: unknown, signal: AbortSignal | undefined): string {
|
|
893
|
+
const command = "native `gentle-ai review mode status`";
|
|
894
|
+
if (signal?.aborted || (error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.TIMEOUT)) {
|
|
895
|
+
return `${command} did not answer within ${RDD_STATUS_TIMEOUT_MS} ms`;
|
|
896
|
+
}
|
|
897
|
+
if (error instanceof NativeReviewCliError) {
|
|
898
|
+
const exit = error.diagnostics.exit_code === undefined ? "" : `, exit ${error.diagnostics.exit_code}`;
|
|
899
|
+
const detail = (error.diagnostics.stderr ?? "").replace(/\s+/g, " ").trim() || error.message;
|
|
900
|
+
return `${command} failed (${error.code}${exit}): ${boundRddStatusCause(detail)}`;
|
|
901
|
+
}
|
|
902
|
+
return boundRddStatusCause(error instanceof Error ? error.message : String(error));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function boundRddStatusCause(text: string): string {
|
|
906
|
+
const trimmed = text.replace(/[.\s]+$/, "");
|
|
907
|
+
return trimmed.length <= RDD_STATUS_CAUSE_LIMIT ? trimmed : `${trimmed.slice(0, RDD_STATUS_CAUSE_LIMIT - 1)}…`;
|
|
908
|
+
}
|
|
909
|
+
|
|
865
910
|
// gentle-pi#662: read-only combined native risk assessment plus the computed
|
|
866
911
|
// verification plan (`lib/review-risk-assessment.ts`), for the `gentle_review`
|
|
867
912
|
// tool's `assess` operation. Never throws: an unavailable/failed native
|
|
868
913
|
// assess call (older binary without the verb, timeout, malformed response)
|
|
869
|
-
// resolves to the `unassessable` tier, which
|
|
870
|
-
//
|
|
914
|
+
// resolves to the `unassessable` native tier, which informs only: like a
|
|
915
|
+
// native high, it adds no independent verifier without an agent escalation.
|
|
871
916
|
interface ReviewAssessmentPlanDetails {
|
|
872
917
|
schema: "gentle-pi.review-assessment-plan/v1";
|
|
873
918
|
risk: VerificationTier;
|
|
874
|
-
// gentle-shell#1494: the native tier before any agent escalation. `risk`
|
|
875
|
-
//
|
|
919
|
+
// gentle-shell#1494: the native tier before any agent escalation. `risk` is
|
|
920
|
+
// the plan tier: high only through an escalation, else passive or medium.
|
|
876
921
|
nativeRisk: VerificationTier;
|
|
877
|
-
// `applied` is
|
|
922
|
+
// `applied` is true whenever an escalation is present: without it the plan
|
|
923
|
+
// would be passive or medium.
|
|
878
924
|
agentEscalation?: { item: number; label: string; reason: string; applied: boolean };
|
|
879
925
|
reasons: readonly ReviewAssessmentReason[];
|
|
880
926
|
changedPaths: number;
|
|
@@ -1031,9 +1077,10 @@ async function resolveReviewAssessmentPlan(
|
|
|
1031
1077
|
}
|
|
1032
1078
|
|
|
1033
1079
|
const nativeRisk: VerificationTier = assessment?.risk ?? VERIFICATION_TIER.UNASSESSABLE;
|
|
1034
|
-
// gentle-shell#1494:
|
|
1035
|
-
//
|
|
1036
|
-
|
|
1080
|
+
// gentle-shell#1494: the plan is high only through an agent escalation
|
|
1081
|
+
// naming a Task Size high-risk item; a native high or unassessable tier
|
|
1082
|
+
// informs as medium. The native tier stays visible as `nativeRisk`.
|
|
1083
|
+
const risk = verificationRisk(nativeRisk, input.escalate);
|
|
1037
1084
|
// gentle-pi#668: the memo is read only for THIS candidate's own target
|
|
1038
1085
|
// identity, never repository-only. It is still needed when the caller
|
|
1039
1086
|
// claims `closed`, because a recorded decline beats closure (gentle-pi#1175).
|
|
@@ -1048,7 +1095,7 @@ async function resolveReviewAssessmentPlan(
|
|
|
1048
1095
|
nativeRisk,
|
|
1049
1096
|
...(input.escalate === undefined
|
|
1050
1097
|
? {}
|
|
1051
|
-
: { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== nativeRisk } }),
|
|
1098
|
+
: { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== verificationRisk(nativeRisk, undefined) } }),
|
|
1052
1099
|
// No path is known for an unassessable candidate: omit it rather than
|
|
1053
1100
|
// emitting an empty string the native reason shape forbids.
|
|
1054
1101
|
reasons: assessment?.reasons ?? (unassessableDetail === undefined ? [] : [{ code: unassessableCode, detail: unassessableDetail }]),
|
|
@@ -1119,14 +1166,15 @@ async function resolveRddModeStatus(
|
|
|
1119
1166
|
// read status instead of flipping the prompt to "unknown" and back, which
|
|
1120
1167
|
// would invalidate the provider's prefix cache for the whole conversation.
|
|
1121
1168
|
// Only a status the native reader actually reports replaces a known one.
|
|
1122
|
-
const
|
|
1169
|
+
const outcome = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
|
|
1170
|
+
const read = "status" in outcome ? outcome.status : undefined;
|
|
1123
1171
|
const status = read ?? cached?.status;
|
|
1124
1172
|
rddStatusMemo.set(cwd, { status, expiresAt: nowMs + RDD_STATUS_MEMO_TTL_MS });
|
|
1125
|
-
if (
|
|
1173
|
+
if ("cause" in outcome && !rddStatusUnavailableWarned) {
|
|
1126
1174
|
rddStatusUnavailableWarned = true;
|
|
1127
1175
|
if (ctx?.hasUI) {
|
|
1128
1176
|
ctx.ui.notify(
|
|
1129
|
-
|
|
1177
|
+
`Gentle AI: receipt-driven-development status is unavailable: ${outcome.cause}. The parent prompt keeps the last known status, or renders "unknown" when none was read; this notice will not repeat this session.`,
|
|
1130
1178
|
"warning",
|
|
1131
1179
|
);
|
|
1132
1180
|
}
|
|
@@ -1139,6 +1187,46 @@ function isRddStatusLineOff(line: string | undefined): boolean {
|
|
|
1139
1187
|
return line !== undefined && line.startsWith("Receipt-driven development: off ");
|
|
1140
1188
|
}
|
|
1141
1189
|
|
|
1190
|
+
/**
|
|
1191
|
+
* gentle-shell#2195: the rendered line sits in the cached system-prompt
|
|
1192
|
+
* prefix, so a session keeps the first line it rendered and re-renders only
|
|
1193
|
+
* when the off state changes, because that also adds or drops the review
|
|
1194
|
+
* execution contract (gentle-shell#1494). An unknown-to-on flip or a
|
|
1195
|
+
* deciding-source change keeps the prefix; review tools still read the live
|
|
1196
|
+
* mode whenever they run, so the pinned text never grants or blocks anything.
|
|
1197
|
+
*/
|
|
1198
|
+
function pinRddStatusLine(pinned: string | undefined, next: string): string {
|
|
1199
|
+
return pinned !== undefined && isRddStatusLineOff(pinned) === isRddStatusLineOff(next) ? pinned : next;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* gentle-pi S11 T1: while the pinned line reads off, no review can start and
|
|
1204
|
+
* the orchestrator only calls ASSESS, so the primary request swaps the review
|
|
1205
|
+
* family's long guidance for this lean loadout: one short gentle_review
|
|
1206
|
+
* snippet, guideline, and description, with the capture, capture-group, and
|
|
1207
|
+
* scope declarations hidden (they stay registered and active). It follows the
|
|
1208
|
+
* pinned line, so it changes only when that line's off state flips, together
|
|
1209
|
+
* with the review execution contract; on and unknown keep the registered text.
|
|
1210
|
+
*/
|
|
1211
|
+
const RDD_OFF_REVIEW_SNIPPET = "Assess an authorized change and return its verification plan (read-only)";
|
|
1212
|
+
const RDD_OFF_REVIEW_GUIDELINES: readonly string[] = [
|
|
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.',
|
|
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.",
|
|
1215
|
+
];
|
|
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.";
|
|
1217
|
+
const RDD_OFF_HIDDEN_REVIEW_TOOLS: readonly string[] = ["gentle_review_scope", "gentle_review_capture_group", "gentle_review_capture"];
|
|
1218
|
+
|
|
1219
|
+
interface ReviewToolPromptOptions {
|
|
1220
|
+
toolSnippets?: Record<string, string>;
|
|
1221
|
+
toolGuidelines?: Record<string, string[]>;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/** Replaces gentle_review's snippet and guidelines in this run's prompt options only. */
|
|
1225
|
+
function applyRddOffReviewPrompt(options: ReviewToolPromptOptions | null | undefined): void {
|
|
1226
|
+
if (options?.toolSnippets?.gentle_review !== undefined) options.toolSnippets.gentle_review = RDD_OFF_REVIEW_SNIPPET;
|
|
1227
|
+
if (options?.toolGuidelines?.gentle_review !== undefined) options.toolGuidelines.gentle_review = [...RDD_OFF_REVIEW_GUIDELINES];
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1142
1230
|
/** Resolves and renders the RDD status line for a production call site in one call. */
|
|
1143
1231
|
async function resolveRddStatusLine(
|
|
1144
1232
|
nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
|
|
@@ -1205,13 +1293,13 @@ function renderOrchestratorPrompt(
|
|
|
1205
1293
|
// pinned at an 8192-byte budget (tests/orchestrator-budget.test.ts).
|
|
1206
1294
|
const PROVIDER_CONTRACT_MIRROR_ROOT = join(PACKAGE_ROOT, "contracts", "review-provider-contract-mirror");
|
|
1207
1295
|
const PROVIDER_CONTRACT_LOCK_FILE = "provider-contract.lock.json";
|
|
1208
|
-
const
|
|
1296
|
+
const PI_MIRROR_RUNTIME = "pi";
|
|
1209
1297
|
|
|
1210
1298
|
let reviewContractPromptFragmentCache: string | null | undefined;
|
|
1211
1299
|
let reviewContractPromptMissingWarned = false;
|
|
1212
1300
|
|
|
1213
|
-
//
|
|
1214
|
-
function
|
|
1301
|
+
// Reads one mirrored bundle file after verifying its bytes against the lock's digest (gentle-ai R1/R3).
|
|
1302
|
+
function readVerifiedMirrorFile(mirrorRoot: string, bundlePath: string): { contractSemver: string; text: string } | null {
|
|
1215
1303
|
try {
|
|
1216
1304
|
const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
|
|
1217
1305
|
const lock = JSON.parse(readFileSync(lockPath, "utf8")) as {
|
|
@@ -1219,20 +1307,71 @@ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRA
|
|
|
1219
1307
|
entries?: Record<string, unknown>;
|
|
1220
1308
|
};
|
|
1221
1309
|
if (typeof lock.contract_semver !== "string" || lock.contract_semver === "") return null;
|
|
1222
|
-
const expectedSha256 = lock.entries?.[
|
|
1310
|
+
const expectedSha256 = lock.entries?.[bundlePath];
|
|
1223
1311
|
if (typeof expectedSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedSha256)) return null;
|
|
1224
|
-
const
|
|
1225
|
-
const rawBytes = readFileSync(
|
|
1312
|
+
const filePath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", ...bundlePath.split("/"));
|
|
1313
|
+
const rawBytes = readFileSync(filePath);
|
|
1226
1314
|
const actualSha256 = createHash("sha256").update(rawBytes).digest("hex");
|
|
1227
1315
|
if (!timingSafeEqual(Buffer.from(expectedSha256, "hex"), Buffer.from(actualSha256, "hex"))) return null;
|
|
1228
|
-
|
|
1229
|
-
if (text.length === 0) return null;
|
|
1230
|
-
return `## Gentle AI review execution contract (mirrored provider bundle ${lock.contract_semver})\n\n${text}`;
|
|
1316
|
+
return { contractSemver: lock.contract_semver, text: rawBytes.toString("utf8") };
|
|
1231
1317
|
} catch {
|
|
1232
1318
|
return null;
|
|
1233
1319
|
}
|
|
1234
1320
|
}
|
|
1235
1321
|
|
|
1322
|
+
// Verifies the mirrored orchestration/pi.md bytes against the lock's digest before injection (gentle-ai R1/R3).
|
|
1323
|
+
function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string | null {
|
|
1324
|
+
const file = readVerifiedMirrorFile(mirrorRoot, `orchestration/${PI_MIRROR_RUNTIME}.md`);
|
|
1325
|
+
if (file === null) return null;
|
|
1326
|
+
const text = file.text.trim();
|
|
1327
|
+
if (text.length === 0) return null;
|
|
1328
|
+
return `## Gentle AI review execution contract (mirrored provider bundle ${file.contractSemver})\n\n${text}`;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// gentle-ai R3: the harness ODD steps 1-7, the phase-reporting line, and the
|
|
1332
|
+
// resume line come from the mirrored bundle's routing/pi.md (contract >= 1.4.0),
|
|
1333
|
+
// the ODD routing block Gentle AI renders for Pi, instead of a hand-copied
|
|
1334
|
+
// literal. The bundle's blank lines between them are not carried into the
|
|
1335
|
+
// prompt. Like assets/orchestrator.md, this is package-shipped prompt source:
|
|
1336
|
+
// a missing, tampered, or reshaped file fails closed instead of rendering a
|
|
1337
|
+
// harness without its workflow.
|
|
1338
|
+
const ODD_PROTOCOL_HEADING = "### ODD protocol (MANDATORY, in this order, on every request)";
|
|
1339
|
+
const ODD_PROTOCOL_STEP_COUNT = 7;
|
|
1340
|
+
|
|
1341
|
+
let oddProtocolStepsCache: string | undefined;
|
|
1342
|
+
|
|
1343
|
+
function extractOddProtocolLines(routing: string): string {
|
|
1344
|
+
const lines = routing.split("\n");
|
|
1345
|
+
const start = lines.indexOf(ODD_PROTOCOL_HEADING);
|
|
1346
|
+
const section: string[] = [];
|
|
1347
|
+
for (const line of start === -1 ? [] : lines.slice(start + 1)) {
|
|
1348
|
+
if (line.startsWith("#")) break;
|
|
1349
|
+
if (line !== "") section.push(line);
|
|
1350
|
+
}
|
|
1351
|
+
const shapeHolds =
|
|
1352
|
+
section.length === ODD_PROTOCOL_STEP_COUNT + 2 &&
|
|
1353
|
+
section.slice(0, ODD_PROTOCOL_STEP_COUNT).every((line, index) => line.startsWith(`${index + 1}. **`)) &&
|
|
1354
|
+
section[ODD_PROTOCOL_STEP_COUNT]!.startsWith("Phase reporting: ") &&
|
|
1355
|
+
section[ODD_PROTOCOL_STEP_COUNT + 1]!.startsWith("Resume an interrupted feature ");
|
|
1356
|
+
if (!shapeHolds) {
|
|
1357
|
+
throw new Error(`The mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md ODD protocol section has an unexpected shape: expected ${JSON.stringify(ODD_PROTOCOL_HEADING)} followed by steps 1-${ODD_PROTOCOL_STEP_COUNT}, phase reporting, and resume.`);
|
|
1358
|
+
}
|
|
1359
|
+
return section.join("\n");
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function readMirroredOddProtocolSteps(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string {
|
|
1363
|
+
const file = readVerifiedMirrorFile(mirrorRoot, `routing/${PI_MIRROR_RUNTIME}.md`);
|
|
1364
|
+
if (file === null) {
|
|
1365
|
+
throw new Error(`Gentle Shell cannot render its ODD steps: the mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md is missing, unreadable, or fails digest verification. Reinstall gentle-pi.`);
|
|
1366
|
+
}
|
|
1367
|
+
return extractOddProtocolLines(file.text);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function oddProtocolSteps(): string {
|
|
1371
|
+
oddProtocolStepsCache ??= readMirroredOddProtocolSteps();
|
|
1372
|
+
return oddProtocolStepsCache;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1236
1375
|
function loadReviewContractPromptFragment(
|
|
1237
1376
|
ctx: Pick<ExtensionContext, "hasUI" | "ui">,
|
|
1238
1377
|
mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT,
|
|
@@ -1324,27 +1463,27 @@ For background work, launch the bounded action and end the turn when no independ
|
|
|
1324
1463
|
|
|
1325
1464
|
Default workflow: Organic Driven Development (MANDATORY)
|
|
1326
1465
|
Organic Driven Development (ODD) is the predefined workflow of this orchestrator. Every request enters it, without the user asking for a workflow, a plan, or task tracking. Never describe this workflow only when asked about it: run it. Run these steps in this order on every request:
|
|
1327
|
-
|
|
1328
|
-
2. **Explore.** Explore existing code and requirements first, proportionately to the request, before proposing or writing anything. Do not delegate exploration of files you will read anyway to work inline; explore only for a map you need to decide or route.
|
|
1329
|
-
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.
|
|
1330
|
-
4. **Classify.** Size the task by the orchestrator's Task Size section: small when understood, risk is contained, and the work could be resumed from the original request and \`git diff\` alone; large only when that resume test fails. Never classify by counting files, commands, tests, fixes, or a requested todo list. Small work stays inline and creates no durable task artifacts.
|
|
1331
|
-
5. **Track before the first write.** For large 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. The document is the specification subagents read, in this order: a two- or three-line header; \`## Specs\` with numbered \`S#\` that quote the user's exact strings, error messages, and examples verbatim, never summarized and never adding unrequested requirements; \`## Tasks\` with one line per task (ID, linked \`S#\`, route, commit); \`## Log\` last, where \`L1\` is the user's original request verbatim and later user corrections, evidence, and decisions are appended. A requirement change appends its verbatim Log entry, rewrites only the affected \`S#\`, and reopens only its task.
|
|
1332
|
-
6. **Implement task by task.** Hand off by reference, never by paraphrase: name the document, task, and specs (for example \`Spec: odd/tasks/<feature>.md, T2, S3-S4\`), tell workers to read until \`## Log\`, and ask which \`S#\` were covered. Without a feature document, include the user's request verbatim. Verify reads the whole document, runs the spec's examples the parent authorized, against isolated state when they mutate data, and returns a verdict per \`S#\`. When the user reports a failure, reproduce it before deciding it already works. Each test asserts every observable effect of the rule it covers (exit code, exact stdout and stderr, and that rejected input leaves stored data and counters unchanged), covers the cases the rule itself names (its examples, boundaries, and errors), and checks through the public interface, never internal storage. When you add or change a command, option, or message, update the help text and docs that describe it. Route each task through the orchestrator's Mechanisms, honoring its mandatory delegation triggers, with applicable test-first development and 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 tracked 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 large ODD implementation; push, pull request creation, and merge remain the user's decisions.
|
|
1333
|
-
7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. Before writing \`Risk: none\`, check whether your diff changes code that existing behavior the request did not mention also uses (shared options, parsers, helpers); if it does, that is item 3. Never end with a tracked task pending unless you quote the user's explicit stop. An applicable quick check runs once; an unavailable verifier or subagent is reported as unavailable, never retried or escalated into extra ceremony. Partial, blocked, unavailable, or exhausted proof becomes one **Needs your decision** result naming the open blockers or missing proof, never more verification; that result is a valid stop, hedged wording is not. 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.
|
|
1334
|
-
Phase reporting: the Gentle Shell prompt label is inferred automatically from the primary session's tool activity (reads show \`exploring\`, edits \`implementing\`, test runs \`checking\`, user questions \`deciding\`). When the \`gentle_odd_phase\` tool is available, use it to refine that label with phases tools cannot show (\`authorizing\`, \`researching\`, \`deciding\`, \`closing\`): call \`gentle_odd_phase\` only when the primary session's ODD phase actually changes, never per tool call or on a fixed cadence, and never from a subagent. It drives the Gentle Shell prompt label only.
|
|
1335
|
-
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\`.
|
|
1466
|
+
${oddProtocolSteps()}
|
|
1336
1467
|
|
|
1337
1468
|
Harness principles:
|
|
1338
1469
|
- el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
|
|
1339
|
-
- Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track large work before the first write, implement task by task with proportionate checks, close each tracked task with a work-unit commit, and close.
|
|
1340
1470
|
- Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
|
|
1341
1471
|
- Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
|
|
1342
|
-
-
|
|
1343
|
-
- 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). 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.
|
|
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.
|
|
1344
1473
|
- Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
|
|
1345
1474
|
- Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
|
|
1346
1475
|
|
|
1347
|
-
${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}`;
|
|
1476
|
+
${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}${headlessDeliveryNote(hostMode)}`;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// Bench gate-p53-r1-BS40 x6 run 3: in a headless RPC session the ask-on-risk
|
|
1480
|
+
// delivery question ran before any code was written, ask_user_question was
|
|
1481
|
+
// unavailable, and the orchestrator printed the menu and stopped. Hosts where
|
|
1482
|
+
// no human can answer (the same rule ask_user_question applies) resolve that
|
|
1483
|
+
// question to the recommended chain. Interactive sessions render nothing here.
|
|
1484
|
+
function headlessDeliveryNote(hostMode: string | undefined): string {
|
|
1485
|
+
if (hostMode === undefined || isInteractiveMode(hostMode)) return "";
|
|
1486
|
+
return "\n\nNo human can answer questions in this session. For the ask-on-risk delivery question, do not ask: use `feature-branch-chain`, record that choice in the feature document's `## Log`, and continue. Consent, maintenance, and opaque-token prompts keep their own rules.";
|
|
1348
1487
|
}
|
|
1349
1488
|
|
|
1350
1489
|
// Matches `git [global-flags] push` — tolerates flags like -C /repo or --work-tree=/tmp
|
|
@@ -1859,8 +1998,15 @@ const YOLO_SEGMENT_END_RE = /&&|\|\||;|\||\n|\r/;
|
|
|
1859
1998
|
// most a `-C <path>`. Other global options (`-c key=value`, `--exec-path`,
|
|
1860
1999
|
// `--git-dir`) can run or redirect code, so they keep the confirmation.
|
|
1861
2000
|
const YOLO_GIT_PREFIX_RE = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=[^\s'"`$]*\s+|env\s+(?:-i\s+)?|command\s+)*git(?:\s+-C\s+[^\s'"`$;&|]+)?\s+$/;
|
|
1862
|
-
|
|
1863
|
-
const
|
|
2001
|
+
// Output redirections that write nothing: to stdout/stderr or /dev/null.
|
|
2002
|
+
const YOLO_REDIRECT_SRC = String.raw`[12]?>&[12]|(?:[12]?|&)>>?\s*/dev/null(?![^\s;&|])`;
|
|
2003
|
+
const YOLO_PUSH_SEGMENT_RE = new RegExp(String.raw`^push(?:\s+(?:${YOLO_REDIRECT_SRC}|-u|--set-upstream|-q|--quiet|-v|--verbose|--tags|--follow-tags|--no-verify|--porcelain|[A-Za-z0-9_][A-Za-z0-9_./-]*(?::[A-Za-z0-9_][A-Za-z0-9_./-]*)?))*\s*$`);
|
|
2004
|
+
const YOLO_REBASE_SEGMENT_RE = new RegExp(String.raw`^rebase(?:\s+(?:${YOLO_REDIRECT_SRC}|--onto\s+[A-Za-z0-9_][A-Za-z0-9_./~^-]*|--continue|--abort|--skip|--autostash|--no-autostash|[A-Za-z0-9_][A-Za-z0-9_./~^-]*))*\s*$`);
|
|
2005
|
+
// A pipe out of a waived segment may only feed read-only filters with plain or
|
|
2006
|
+
// literally quoted arguments and end at a sequential separator. Git output
|
|
2007
|
+
// carries remote-controlled text, so `| sh`, `| tee`, `| xargs` and a trailing
|
|
2008
|
+
// background `&` keep the confirmation.
|
|
2009
|
+
const YOLO_FILTER_PIPELINE_RE = new RegExp(String.raw`^(?:\|(?!\|)\s*(?:tail|head|cat|grep|wc)(?:\s+(?:${YOLO_REDIRECT_SRC}|[A-Za-z0-9_.,:=+/%@-]+|'[^'\n]*'|"[^"$\x60\\\n]*"))*\s*)*(?=$|&&|\|\||;|\n|\r)`);
|
|
1864
2010
|
|
|
1865
2011
|
function yoloGuardedSegment(command: string, triggerIndex: number): string {
|
|
1866
2012
|
const rest = command.slice(triggerIndex);
|
|
@@ -1875,20 +2021,29 @@ function yoloSegmentPrefix(command: string, triggerIndex: number): string {
|
|
|
1875
2021
|
return last === undefined ? head : head.slice(last.index + last[0].length);
|
|
1876
2022
|
}
|
|
1877
2023
|
|
|
1878
|
-
/**
|
|
1879
|
-
|
|
1880
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* Every guarded match and every recognized Git side effect must be a reversible
|
|
2026
|
+
* push or non-interactive rebase; the only data loss allowed is recursive
|
|
2027
|
+
* removal of the session's own temp scratch (lib/yolo-temp-scratch.ts).
|
|
2028
|
+
*/
|
|
2029
|
+
function isReversibleYoloCommand(command: string, evaluation: GuardEvaluation, scratchFs?: TempScratchFs): boolean {
|
|
2030
|
+
if (evaluation.action !== "confirm") return false;
|
|
2031
|
+
const effects = recognizeDestructiveCommands(command);
|
|
2032
|
+
const scratch = effects.map((effect) => effect.removalTargets !== undefined && isTempScratchRemoval(effect, scratchFs ??= defaultTempScratchFs()));
|
|
2033
|
+
if (evaluation.matches.length === 0 && !scratch.includes(true)) return false;
|
|
1881
2034
|
for (const match of evaluation.matches) {
|
|
1882
2035
|
if (!YOLO_WAIVABLE_KEYS.has(match.key)) return false;
|
|
1883
2036
|
if (!YOLO_GIT_PREFIX_RE.test(yoloSegmentPrefix(command, match.triggerIndex))) return false;
|
|
1884
2037
|
const segment = yoloGuardedSegment(command, match.triggerIndex);
|
|
1885
2038
|
if (!(match.key === "gitPush" ? YOLO_PUSH_SEGMENT_RE : YOLO_REBASE_SEGMENT_RE).test(segment)) return false;
|
|
2039
|
+
if (!YOLO_FILTER_PIPELINE_RE.test(command.slice(match.triggerIndex + segment.length))) return false;
|
|
1886
2040
|
}
|
|
1887
2041
|
// The data-loss recognizer leaves non-hard Git effects (reset, stash drop,
|
|
1888
2042
|
// restore, checkout --) to the primary policy; under YOLO only a rebase that
|
|
1889
2043
|
// a guarded match already vetted may remain.
|
|
1890
2044
|
const rebaseTriggers = new Set(evaluation.matches.filter((match) => match.key === "gitRebase").map((match) => match.triggerIndex));
|
|
1891
|
-
for (const effect of
|
|
2045
|
+
for (const [index, effect] of effects.entries()) {
|
|
2046
|
+
if (scratch[index]) continue;
|
|
1892
2047
|
if (effect.hardDeny || effect.kind !== "git") return false;
|
|
1893
2048
|
const rebase = /^git(?:\s+(?:-C|-c|--git-dir|--work-tree|--namespace)\s+\S+|\s+-\S+)*\s+(rebase)\b/.exec(command.slice(effect.triggerIndex));
|
|
1894
2049
|
if (rebase === null || !rebaseTriggers.has(effect.triggerIndex + rebase.index + rebase[0].lastIndexOf("rebase"))) return false;
|
|
@@ -1917,6 +2072,21 @@ function yoloConfiguredRestriction(cwd: string, keys: readonly GuardedCommandKey
|
|
|
1917
2072
|
return restriction;
|
|
1918
2073
|
}
|
|
1919
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
|
+
|
|
1920
2090
|
async function confirmCommand(
|
|
1921
2091
|
command: string,
|
|
1922
2092
|
ctx: ExtensionContext,
|
|
@@ -1924,7 +2094,19 @@ async function confirmCommand(
|
|
|
1924
2094
|
herdrLifecycle: HerdrConfirmationLifecycle,
|
|
1925
2095
|
yoloActive = false,
|
|
1926
2096
|
autoGate?: () => Promise<ToolCallEventResult | undefined>,
|
|
2097
|
+
superActive = false,
|
|
1927
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
|
+
}
|
|
1928
2110
|
const guardrailsConfig = loadRuntimeGuardrailsConfig(ctx.cwd);
|
|
1929
2111
|
const evaluation = evaluateGuardedCommand(command, guardrailsConfig);
|
|
1930
2112
|
// Configuration provenance is distinct from legacy autonomy resolution, which
|
|
@@ -2940,7 +3122,7 @@ interface OverlayComponent {
|
|
|
2940
3122
|
|
|
2941
3123
|
type ModelPanelResult =
|
|
2942
3124
|
| { type: "save"; config: AgentModelConfig }
|
|
2943
|
-
// `save`, then snapshot saved agent routing and the
|
|
3125
|
+
// `save`, then snapshot saved agent routing and the chosen orchestrator into
|
|
2944
3126
|
// the current profile without changing the global orchestrator defaults.
|
|
2945
3127
|
| { type: "save-profile"; config: AgentModelConfig }
|
|
2946
3128
|
| { type: "custom"; agent: string | "all"; config: AgentModelConfig }
|
|
@@ -2949,6 +3131,7 @@ type ModelPanelResult =
|
|
|
2949
3131
|
| { type: "cancel" };
|
|
2950
3132
|
|
|
2951
3133
|
const SET_ALL_AGENTS = "Set all agents";
|
|
3134
|
+
const LEAVE_ORCHESTRATOR_UNTOUCHED = "leave untouched";
|
|
2952
3135
|
|
|
2953
3136
|
const PANEL_TONE = {
|
|
2954
3137
|
BORDER: "border",
|
|
@@ -2979,6 +3162,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
2979
3162
|
private query = "";
|
|
2980
3163
|
private readonly draft: AgentModelConfig;
|
|
2981
3164
|
private readonly rows: string[];
|
|
3165
|
+
private readonly agents: string[];
|
|
2982
3166
|
private readonly modelOptions: string[];
|
|
2983
3167
|
private readonly done: (result: ModelPanelResult) => void;
|
|
2984
3168
|
private readonly theme: Theme | undefined;
|
|
@@ -2997,7 +3181,8 @@ class SddModelPanel implements OverlayComponent {
|
|
|
2997
3181
|
terminalRows?: () => number,
|
|
2998
3182
|
) {
|
|
2999
3183
|
this.draft = cloneModelConfig(initialConfig);
|
|
3000
|
-
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
3184
|
+
this.rows = [SET_ALL_AGENTS, PROFILE_ORCHESTRATOR_KEY, ...agents];
|
|
3185
|
+
this.agents = [...agents];
|
|
3001
3186
|
this.modelOptions = modelOptions;
|
|
3002
3187
|
this.done = done;
|
|
3003
3188
|
this.theme = theme;
|
|
@@ -3120,6 +3305,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3120
3305
|
}
|
|
3121
3306
|
if (matchesKey(data, "e")) {
|
|
3122
3307
|
this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
|
|
3308
|
+
if (this.selectedRow === PROFILE_ORCHESTRATOR_KEY && !this.draft[this.selectedRow]?.model) return;
|
|
3123
3309
|
this.mode = "effort";
|
|
3124
3310
|
this.effortCursor = 0;
|
|
3125
3311
|
return;
|
|
@@ -3201,7 +3387,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3201
3387
|
return;
|
|
3202
3388
|
}
|
|
3203
3389
|
this.applyModelSelection(
|
|
3204
|
-
selected === INHERIT_MODEL ? undefined : selected,
|
|
3390
|
+
selected === INHERIT_MODEL || selected === LEAVE_ORCHESTRATOR_UNTOUCHED ? undefined : selected,
|
|
3205
3391
|
);
|
|
3206
3392
|
this.mode = "agents";
|
|
3207
3393
|
return;
|
|
@@ -3215,7 +3401,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3215
3401
|
private applyModelSelection(model: string | undefined): void {
|
|
3216
3402
|
const row = this.rows[this.cursor];
|
|
3217
3403
|
if (row === SET_ALL_AGENTS) {
|
|
3218
|
-
for (const name of this.
|
|
3404
|
+
for (const name of this.agents) this.setModel(name, model);
|
|
3219
3405
|
return;
|
|
3220
3406
|
}
|
|
3221
3407
|
if (!row) return;
|
|
@@ -3225,7 +3411,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3225
3411
|
private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
|
|
3226
3412
|
const row = this.selectedRow;
|
|
3227
3413
|
if (row === SET_ALL_AGENTS) {
|
|
3228
|
-
for (const name of this.
|
|
3414
|
+
for (const name of this.agents) this.setThinking(name, thinking);
|
|
3229
3415
|
return;
|
|
3230
3416
|
}
|
|
3231
3417
|
this.setThinking(row, thinking);
|
|
@@ -3234,13 +3420,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3234
3420
|
private applyInherit(): void {
|
|
3235
3421
|
const row = this.rows[this.cursor];
|
|
3236
3422
|
if (row === SET_ALL_AGENTS) {
|
|
3237
|
-
for (const name of this.
|
|
3423
|
+
for (const name of this.agents) this.clearEntry(name);
|
|
3238
3424
|
return;
|
|
3239
3425
|
}
|
|
3240
3426
|
if (row) this.clearEntry(row);
|
|
3241
3427
|
}
|
|
3242
3428
|
|
|
3243
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
|
+
}
|
|
3244
3434
|
const current = this.draft[name] ?? {};
|
|
3245
3435
|
if (model === undefined) delete current.model;
|
|
3246
3436
|
else current.model = model;
|
|
@@ -3249,6 +3439,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3249
3439
|
}
|
|
3250
3440
|
|
|
3251
3441
|
private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
|
|
3442
|
+
if (name === PROFILE_ORCHESTRATOR_KEY && !this.draft[name]?.model) return;
|
|
3252
3443
|
const current = this.draft[name] ?? {};
|
|
3253
3444
|
if (thinking === undefined) delete current.thinking;
|
|
3254
3445
|
else current.thinking = thinking;
|
|
@@ -3257,13 +3448,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3257
3448
|
}
|
|
3258
3449
|
|
|
3259
3450
|
private clearEntry(name: string): void {
|
|
3260
|
-
this.draft[name]
|
|
3451
|
+
if (name === PROFILE_ORCHESTRATOR_KEY) delete this.draft[name];
|
|
3452
|
+
else this.draft[name] = {};
|
|
3261
3453
|
}
|
|
3262
3454
|
|
|
3263
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;
|
|
3264
3459
|
const query = this.query.trim().toLowerCase();
|
|
3265
|
-
if (!query) return
|
|
3266
|
-
return
|
|
3460
|
+
if (!query) return options;
|
|
3461
|
+
return options.filter((option) =>
|
|
3267
3462
|
option.toLowerCase().includes(query),
|
|
3268
3463
|
);
|
|
3269
3464
|
}
|
|
@@ -3320,13 +3515,13 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3320
3515
|
// longer fits the minimum width once the profile key joins the save keys.
|
|
3321
3516
|
lines.push(
|
|
3322
3517
|
line(
|
|
3323
|
-
`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`,
|
|
3324
3519
|
"muted",
|
|
3325
3520
|
),
|
|
3326
3521
|
);
|
|
3327
3522
|
lines.push(
|
|
3328
3523
|
line(
|
|
3329
|
-
`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`,
|
|
3330
3525
|
"muted",
|
|
3331
3526
|
),
|
|
3332
3527
|
);
|
|
@@ -3426,11 +3621,9 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3426
3621
|
}
|
|
3427
3622
|
|
|
3428
3623
|
private renderSetAllLabel(row: string): string {
|
|
3429
|
-
const models = this.
|
|
3430
|
-
.slice(1)
|
|
3624
|
+
const models = this.agents
|
|
3431
3625
|
.map((name) => this.draft[name]?.model ?? routingDefaultLabel(name, "model"));
|
|
3432
|
-
const efforts = this.
|
|
3433
|
-
.slice(1)
|
|
3626
|
+
const efforts = this.agents
|
|
3434
3627
|
.map((name) => this.draft[name]?.thinking ?? routingDefaultLabel(name, "effort"));
|
|
3435
3628
|
const firstModel = models[0] ?? "inherit";
|
|
3436
3629
|
const firstEffort = efforts[0] ?? "inherit";
|
|
@@ -3447,6 +3640,9 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3447
3640
|
}
|
|
3448
3641
|
|
|
3449
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
|
+
}
|
|
3450
3646
|
const model = this.draft[row]?.model ?? routingDefaultLabel(row, "model");
|
|
3451
3647
|
const effort = this.draft[row]?.thinking ?? routingDefaultLabel(row, "effort");
|
|
3452
3648
|
return `${this.renderText(sanitizeTerminalText(row).padEnd(20), "text")} ${this.renderText("model=", "muted")}${this.renderText(model, "status")}${this.renderText(
|
|
@@ -3521,8 +3717,18 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
|
|
|
3521
3717
|
);
|
|
3522
3718
|
return;
|
|
3523
3719
|
}
|
|
3524
|
-
let config = savedConfig.status === "valid" ? savedConfig.config : {};
|
|
3525
|
-
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);
|
|
3526
3732
|
let result = await showSddModelPanel(ctx, config, profileLabel);
|
|
3527
3733
|
while (result.type === "custom" || result.type === "export" || result.type === "restore") {
|
|
3528
3734
|
config = cloneModelConfig(result.config);
|
|
@@ -3552,7 +3758,10 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
|
|
|
3552
3758
|
result = await showSddModelPanel(ctx, config, profileLabel);
|
|
3553
3759
|
continue;
|
|
3554
3760
|
}
|
|
3555
|
-
|
|
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;
|
|
3556
3765
|
try {
|
|
3557
3766
|
const applyResult = await applyModelConfigAsync(ctx.cwd, restored);
|
|
3558
3767
|
ctx.ui.notify([
|
|
@@ -3614,18 +3823,21 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
|
|
|
3614
3823
|
result = await showSddModelPanel(ctx, config, profileLabel);
|
|
3615
3824
|
}
|
|
3616
3825
|
if (result.type !== "save" && result.type !== "save-profile") return;
|
|
3617
|
-
|
|
3618
|
-
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);
|
|
3619
3831
|
ctx.ui.notify(
|
|
3620
3832
|
[
|
|
3621
3833
|
"el Gentleman global model config saved.",
|
|
3622
3834
|
`Global config: ${modelConfigPath(ctx.cwd)}`,
|
|
3623
3835
|
`Agents updated: ${applyResult.updated}`,
|
|
3624
|
-
...describeModelConfig(ctx.cwd,
|
|
3836
|
+
...describeModelConfig(ctx.cwd, routing),
|
|
3625
3837
|
].join("\n"),
|
|
3626
3838
|
"info",
|
|
3627
3839
|
);
|
|
3628
|
-
if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx,
|
|
3840
|
+
if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx, result.config[PROFILE_ORCHESTRATOR_KEY]);
|
|
3629
3841
|
}
|
|
3630
3842
|
|
|
3631
3843
|
/** The profile `u` in `/gentle:models` writes to, and why it is that one. */
|
|
@@ -3657,11 +3869,10 @@ function describeCurrentProfileTarget(target: CurrentProfileTarget | undefined):
|
|
|
3657
3869
|
|
|
3658
3870
|
/**
|
|
3659
3871
|
* The second half of `u` captures saved global agent routing (never the pin)
|
|
3660
|
-
* and the
|
|
3661
|
-
*
|
|
3662
|
-
* 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.
|
|
3663
3874
|
*/
|
|
3664
|
-
function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext,
|
|
3875
|
+
function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, orchestrator: AgentRoutingEntry | undefined): void {
|
|
3665
3876
|
const path = profilesFilePath(gentleAiConfigHome());
|
|
3666
3877
|
const read = readProfilesFileResult(path);
|
|
3667
3878
|
if (read.status === "invalid") {
|
|
@@ -3671,16 +3882,9 @@ function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, pi: Extensi
|
|
|
3671
3882
|
);
|
|
3672
3883
|
return;
|
|
3673
3884
|
}
|
|
3674
|
-
const snapshot =
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
);
|
|
3678
|
-
if (ctx.model) {
|
|
3679
|
-
snapshot[PROFILE_ORCHESTRATOR_KEY] = {
|
|
3680
|
-
model: `${ctx.model.provider}/${ctx.model.id}`,
|
|
3681
|
-
thinking: pi.getThinkingLevel(),
|
|
3682
|
-
};
|
|
3683
|
-
}
|
|
3885
|
+
const snapshot = cloneModelConfig(readGlobalEffectiveModelConfig(ctx.cwd));
|
|
3886
|
+
delete snapshot[PROFILE_ORCHESTRATOR_KEY];
|
|
3887
|
+
if (orchestrator?.model) snapshot[PROFILE_ORCHESTRATOR_KEY] = { ...orchestrator };
|
|
3684
3888
|
if (read.status === "missing") {
|
|
3685
3889
|
// The same seed `/gentle:profiles` performs on its first open, so pressing
|
|
3686
3890
|
// `u` before ever opening that panel lands on the same "current" profile.
|
|
@@ -3859,10 +4063,10 @@ function profilePinScopeNote(cwd: string): string | undefined {
|
|
|
3859
4063
|
* write that already succeeded must not become an error because a probe about sharing
|
|
3860
4064
|
* it failed.
|
|
3861
4065
|
*/
|
|
3862
|
-
function repoDeclarationSharing(root: string, path: string): string {
|
|
4066
|
+
export function repoDeclarationSharing(root: string, path: string): string {
|
|
3863
4067
|
const relativePath = relative(root, path).split(sep).join("/");
|
|
3864
4068
|
const run = (...arguments_: string[]): string =>
|
|
3865
|
-
execFileSync("git", arguments_, { cwd: root, encoding: "utf8" }).trim();
|
|
4069
|
+
execFileSync("git", arguments_, { cwd: root, encoding: "utf8", windowsHide: true }).trim();
|
|
3866
4070
|
try {
|
|
3867
4071
|
// Exit 0 means gitignore matches the path; the exact negation line is what makes
|
|
3868
4072
|
// the declaration committable again.
|
|
@@ -3914,6 +4118,8 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
3914
4118
|
// Actions reopen the panel, refreshing this snapshot without disk reads during rendering.
|
|
3915
4119
|
private readonly pinStatus: ProfilePinStatus | undefined;
|
|
3916
4120
|
private readonly sessionBoundName: string | undefined;
|
|
4121
|
+
private readonly discoveredAgentNames: string[];
|
|
4122
|
+
private readonly unmatchedKeys: string[];
|
|
3917
4123
|
private readonly liveOrchestrator: (() => AgentRoutingEntry | undefined) | undefined;
|
|
3918
4124
|
|
|
3919
4125
|
constructor(
|
|
@@ -3929,6 +4135,8 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
3929
4135
|
requestRender: () => void,
|
|
3930
4136
|
pinStatus: () => ProfilePinStatus | undefined,
|
|
3931
4137
|
sessionBound: () => string | undefined,
|
|
4138
|
+
discoveredAgentNames: string[],
|
|
4139
|
+
unmatchedKeys: string[],
|
|
3932
4140
|
liveOrchestrator?: () => AgentRoutingEntry | undefined,
|
|
3933
4141
|
feedback?: string,
|
|
3934
4142
|
) {
|
|
@@ -3943,6 +4151,8 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
3943
4151
|
this.orchestratorSettings = orchestratorSettings;
|
|
3944
4152
|
this.pinStatus = pinStatus();
|
|
3945
4153
|
this.sessionBoundName = sessionBound();
|
|
4154
|
+
this.discoveredAgentNames = discoveredAgentNames;
|
|
4155
|
+
this.unmatchedKeys = unmatchedKeys;
|
|
3946
4156
|
this.liveOrchestrator = liveOrchestrator;
|
|
3947
4157
|
const items = buildProfileListItems(file, evaluateProfilePin(this.pinStatus, file.profiles).winner?.profile, this.sessionBoundName);
|
|
3948
4158
|
this.listItems = items;
|
|
@@ -4178,10 +4388,30 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
4178
4388
|
: [this.renderLine(`session ${sanitizeTerminalText(this.sessionBoundName)} (session) — launches resolve it ahead of pins and the global default; nothing was written`, width, "muted")]),
|
|
4179
4389
|
"",
|
|
4180
4390
|
this.renderLine("Profile routing", width, "accent"),
|
|
4181
|
-
...this.indentLines(this.routingLines(profileRows, widths), width),
|
|
4391
|
+
...this.indentLines(this.routingLines(profileRows, widths, width), width),
|
|
4182
4392
|
"",
|
|
4183
4393
|
this.renderLine("Current routing (effective)", width, "accent"),
|
|
4184
|
-
...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),
|
|
4185
4415
|
];
|
|
4186
4416
|
}
|
|
4187
4417
|
|
|
@@ -4203,11 +4433,24 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
4203
4433
|
private routingLines(
|
|
4204
4434
|
rows: ProfileRoutingRow[],
|
|
4205
4435
|
widths: { agent: number; model: number },
|
|
4436
|
+
width: number,
|
|
4206
4437
|
): string[] {
|
|
4207
4438
|
if (rows.length === 0) {
|
|
4208
4439
|
return ["No routing entries — every agent inherits its default model."];
|
|
4209
4440
|
}
|
|
4210
|
-
|
|
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
|
+
});
|
|
4211
4454
|
}
|
|
4212
4455
|
|
|
4213
4456
|
private indentLines(lines: string[], width: number): string[] {
|
|
@@ -4253,6 +4496,28 @@ async function showProfilesPanel(
|
|
|
4253
4496
|
// Both orchestrator and pin state are snapshots for this panel visit.
|
|
4254
4497
|
// Actions (including p/P) reopen the panel and read fresh state.
|
|
4255
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);
|
|
4256
4521
|
return ctx.ui.custom<ProfilesPanelResult>(
|
|
4257
4522
|
(tui, theme, keybindings, done) => {
|
|
4258
4523
|
const panel = new ProfilesPanel(
|
|
@@ -4268,6 +4533,8 @@ async function showProfilesPanel(
|
|
|
4268
4533
|
() => tui.requestRender(),
|
|
4269
4534
|
() => readProfilePinStatus(ctx.cwd),
|
|
4270
4535
|
() => sessionBoundName,
|
|
4536
|
+
discoveredAgentNames,
|
|
4537
|
+
unmatchedKeys,
|
|
4271
4538
|
() => liveOrchestratorSnapshot(ctx, live)?.entry,
|
|
4272
4539
|
status,
|
|
4273
4540
|
);
|
|
@@ -5140,7 +5407,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
|
|
|
5140
5407
|
},
|
|
5141
5408
|
input: {
|
|
5142
5409
|
type: "string",
|
|
5143
|
-
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.",
|
|
5144
5411
|
},
|
|
5145
5412
|
outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
5146
5413
|
inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
|
|
@@ -5411,7 +5678,7 @@ function parseReviewControllerParameters(value: unknown): ReviewControllerParame
|
|
|
5411
5678
|
};
|
|
5412
5679
|
for (const key of ["changeName", "idempotencyKey", "transition", "input", "outputPath", "inputPath", "operationId", "lineageIds", "acknowledgeUntrustedBundleSource", "workspaceRoot"] as const) {
|
|
5413
5680
|
const optional = value[key];
|
|
5414
|
-
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)) {
|
|
5415
5682
|
parameters.input = serializeReviewJsonArgument(optional);
|
|
5416
5683
|
continue;
|
|
5417
5684
|
}
|
|
@@ -5639,21 +5906,18 @@ const REVIEW_MODE_DISABLED_OUTCOME = "review-mode-disabled";
|
|
|
5639
5906
|
// to make the global opt-in first when needed, then clear this clone override.
|
|
5640
5907
|
// Pi never mutates the operator's global gentle-ai state automatically.
|
|
5641
5908
|
//
|
|
5642
|
-
//
|
|
5643
|
-
//
|
|
5644
|
-
//
|
|
5645
|
-
// `default`
|
|
5646
|
-
//
|
|
5647
|
-
//
|
|
5648
|
-
//
|
|
5649
|
-
//
|
|
5650
|
-
// global is the only scope that can turn reviews on at all, and Pi answers the
|
|
5651
|
-
// same. Leaving this undefined would hand the single most common state a dead
|
|
5652
|
-
// 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.
|
|
5653
5917
|
function reviewModeContinuation(source: NativeReviewModeSource): string | undefined {
|
|
5654
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.";
|
|
5655
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.";
|
|
5656
|
-
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.";
|
|
5657
5921
|
}
|
|
5658
5922
|
|
|
5659
5923
|
// Names the situation before the mechanism, then the mechanism, mirroring
|
|
@@ -6611,6 +6875,8 @@ const processRetainedNativeStatusSelections = new Map<PendingReviewConsentSessio
|
|
|
6611
6875
|
// named-agent start increments the depth, a matching end decrements it,
|
|
6612
6876
|
// and a fresh primary-loop start resets it to 0.
|
|
6613
6877
|
const processAgentEndSubagentDepth = new Map<PendingReviewConsentSessionKey, number>();
|
|
6878
|
+
// gentle-shell#2195: the RDD status line each primary session last rendered into its system prompt.
|
|
6879
|
+
const pinnedRddStatusLines = new Map<PendingReviewConsentSessionKey, string>();
|
|
6614
6880
|
|
|
6615
6881
|
// gentle-shell#1064 slice 1: the parent-session profile binding store lives in
|
|
6616
6882
|
// lib/session-profile-binding.ts (in-process, keyed by parent session id). The
|
|
@@ -7304,11 +7570,24 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
|
|
|
7304
7570
|
// its provider through the live model registry the caller supplies, or it is
|
|
7305
7571
|
// refused before materialize ever runs (validateReviewerSelectionConfiguration
|
|
7306
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.
|
|
7307
7579
|
function reviewHostRelaySelection(lens: string | undefined, config: AgentModelConfig): { selection?: string; thinking?: string; routingKey: string } {
|
|
7308
7580
|
const routingKey = lens === undefined || lens.length === 0 ? "review capture" : lens.startsWith("review-") ? lens : `review-${lens}`;
|
|
7309
7581
|
const entry = config[routingKey];
|
|
7310
|
-
|
|
7311
|
-
|
|
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
|
+
}
|
|
7312
7591
|
return {
|
|
7313
7592
|
...(model === undefined ? {} : { selection: model }),
|
|
7314
7593
|
...(thinking === undefined ? {} : { thinking }),
|
|
@@ -7990,6 +8269,20 @@ function isSelectedReviewCapture(value: SelectedReviewCapture | Record<string, u
|
|
|
7990
8269
|
|
|
7991
8270
|
function captureGroupRejected(reason: string): Record<string, unknown> { return captureBindingRejected(reason, true); }
|
|
7992
8271
|
|
|
8272
|
+
function captureGroupStatusFailure(error: unknown): Record<string, unknown> {
|
|
8273
|
+
return {
|
|
8274
|
+
...captureGroupRejected(error instanceof Error ? error.message : String(error)),
|
|
8275
|
+
outcome: "native-status-failed",
|
|
8276
|
+
// Only project the decoded native envelope, never arbitrary error fields.
|
|
8277
|
+
// Group mutation reporting belongs to its submitted-reviewer progress.
|
|
8278
|
+
...(error instanceof NativeReviewIntegrationError ? {
|
|
8279
|
+
native_failure: error.failureEnvelope.raw,
|
|
8280
|
+
replayability: error.failureEnvelope.replayability,
|
|
8281
|
+
next_action: error.failureEnvelope.nextAction,
|
|
8282
|
+
} : {}),
|
|
8283
|
+
};
|
|
8284
|
+
}
|
|
8285
|
+
|
|
7993
8286
|
function hasExactReviewCaptureSuffix(status: ReviewStatusV3, expected: readonly string[]): boolean {
|
|
7994
8287
|
const current = status.nextTransition?.kind === "collect"
|
|
7995
8288
|
? (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation === "review.capture-result").map(canonicalReviewCaptureBinding)
|
|
@@ -8296,7 +8589,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
8296
8589
|
if (negotiated.transport !== undefined) return hostTransportUnavailable("gentle_review_capture_group", negotiated.transport);
|
|
8297
8590
|
status = negotiated.status!;
|
|
8298
8591
|
} catch (error) {
|
|
8299
|
-
return
|
|
8592
|
+
return captureGroupStatusFailure(error);
|
|
8300
8593
|
}
|
|
8301
8594
|
const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings);
|
|
8302
8595
|
if (!isSelectedReviewCaptureGroup(group)) return group;
|
|
@@ -8342,7 +8635,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
8342
8635
|
if (!hasExactReviewCaptureSuffix(negotiated.status!, canonicalBindings.slice(index))) return { ...captureGroupAuthorityDrift(negotiated.status!), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8343
8636
|
current = selectExactReviewCapture(negotiated.status!, parameters.lineageId, canonicalBindings[index]!);
|
|
8344
8637
|
} catch (error) {
|
|
8345
|
-
return { ...
|
|
8638
|
+
return { ...captureGroupStatusFailure(error), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8346
8639
|
}
|
|
8347
8640
|
if (!isSelectedReviewCapture(current)) return { ...captureGroupRejected(String(current.reason ?? "current STATUS rejected a reviewer binding")), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8348
8641
|
try {
|
|
@@ -9461,6 +9754,7 @@ export const __testing = {
|
|
|
9461
9754
|
guardedCommandTitle,
|
|
9462
9755
|
loadRuntimeGuardrailsConfig,
|
|
9463
9756
|
isReversibleYoloCommand,
|
|
9757
|
+
evaluateSuperYoloCommand,
|
|
9464
9758
|
yoloConfiguredRestriction,
|
|
9465
9759
|
buildGentlePrompt,
|
|
9466
9760
|
nativeStatusUnsupported,
|
|
@@ -9477,6 +9771,7 @@ export const __testing = {
|
|
|
9477
9771
|
renderOrchestratorPrompt,
|
|
9478
9772
|
loadReviewContractPromptFragment,
|
|
9479
9773
|
readMirroredReviewContractFragment,
|
|
9774
|
+
readMirroredOddProtocolSteps,
|
|
9480
9775
|
loadBackgroundSubagentsPolicy,
|
|
9481
9776
|
resolveBackgroundSubagentsPolicy,
|
|
9482
9777
|
renderBackgroundSubagentsReport,
|
|
@@ -9486,6 +9781,7 @@ export const __testing = {
|
|
|
9486
9781
|
readActiveToolNames,
|
|
9487
9782
|
renderBackgroundSubagentsStatusLine,
|
|
9488
9783
|
renderRddStatusLine,
|
|
9784
|
+
pinRddStatusLine,
|
|
9489
9785
|
isValidRddModeStatus,
|
|
9490
9786
|
resolveRddModeStatus,
|
|
9491
9787
|
resolveRddStatusLine,
|
|
@@ -9529,6 +9825,9 @@ export interface GentleAiRuntimeDependencies {
|
|
|
9529
9825
|
telemetryExecFileAdapter?: ExecFileAdapter;
|
|
9530
9826
|
/** Test-only inference seam; production always uses the active session model. */
|
|
9531
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 };
|
|
9532
9831
|
}
|
|
9533
9832
|
|
|
9534
9833
|
export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencies = {}): (pi: ExtensionAPI) => void {
|
|
@@ -9607,11 +9906,17 @@ function createGentleAiExtensionForTesting(
|
|
|
9607
9906
|
reviewSidebar.reset(ctx);
|
|
9608
9907
|
statusTiming.reset(ctx);
|
|
9609
9908
|
});
|
|
9909
|
+
// Lean RDD-off review loadout (applyRddOffReviewPrompt): every
|
|
9910
|
+
// before_agent_start sets it from the pinned line before Pi prepares that
|
|
9911
|
+
// run's tool loadout, so gentle_review's prepareLoadout reads this run's value.
|
|
9912
|
+
let rddOffReviewLoadout = false;
|
|
9610
9913
|
let reminderSessionActive = true;
|
|
9611
9914
|
let reminderEpoch = 0;
|
|
9612
9915
|
let reminderManager: ExtensionContext["sessionManager"] | undefined;
|
|
9613
9916
|
let unbindPreparation: (() => void) | undefined;
|
|
9614
9917
|
pi.on("session_shutdown", (event, context) => {
|
|
9918
|
+
pinnedRddStatusLines.delete(pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey));
|
|
9919
|
+
rddOffReviewLoadout = false;
|
|
9615
9920
|
auto.reset();
|
|
9616
9921
|
yolo.reset(context);
|
|
9617
9922
|
reviewSidebar.reset();
|
|
@@ -9859,10 +10164,20 @@ function createGentleAiExtensionForTesting(
|
|
|
9859
10164
|
"For blocked-legacy or blocked-mixed, do not call START repeatedly. Explain invalidation, request explicit user authorization, then call RESET or RECOVER only after authorization. RESET and RECOVER_LOCK route to audited native `gentle-ai review reclaim`; only RESET carries the legacy repositoryId, commonDirHash, inventoryHash, and confirmation challenge. RECOVER routes to native `gentle-ai review recover` with exactly six inputs: predecessorLineage, expectedPredecessorRevision, successorLineage, disposition, actor, and reason. Never send RECOVER the reset challenge and never send it a maintainerAuthorization: Pi reads fresh native target status, pins the predecessor lineage, revision, provider-selected disposition, and target identity, derives the exact six-line native authorization binding, displays it for fresh UI approval, and re-reads status before mutating. Negotiated target status supplies the sole accepted recovery disposition, and a caller-supplied substitute is rejected. Treat a native-input-required envelope as a request for exact values, never as permission to invent them. After a committed native recovery record, INSPECT before any fresh ordinary START.",
|
|
9860
10165
|
"A consent-required START may be resolved inside the eligible interactive Pi host. Its third UI action is host-owned: it runs this envelope's exact provider grant once and allows later fresh validated envelopes only for the same live SessionManager, nonempty session ID, and canonical Git common-directory identity, including sibling worktrees; an unrelated repository requires a new explicit human grant. Revoke removes the current repository grant, while nonreload replacement, quit, and process exit remove all session grants; reload preserves them. It grants no provider mode, verdict, acknowledgement, maintenance, delivery, or cross-repository authority. A package-owned child may ask its parent only with the canonical digest of its exact pending target; the parent binds that digest to the task repository and fails closed otherwise. If the tool returns an unresolved envelope, present the original two provider choices without changing machine tokens, commands, target IDs, or invocations; never add the host action to the decoded provider envelope. After one explicit relayed human answer, call answer-consent exactly once with only consentBinding and answer (`granted` or `declined`). Never create host permission from tool arguments, model prose, child/headless responses, or an uncertain native result. A reported lineage_created false or pre-authority validation error proves no lineage was created. After ambiguous START output, the controller calls target-scoped native status once and returns only its declared action. An ambiguous gentle_review_capture outcome independently reconciles once and never replays the capture.",
|
|
9861
10166
|
"Use gentle_review only for native review authority operations; delivery commands follow ordinary repository policy.",
|
|
9862
|
-
'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it
|
|
10167
|
+
'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it is the only way to get an independent verifier, keeps nativeRisk, returns agentEscalation, and can never lower a tier). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); caller writerModelId/writerEffort are only a fallback when no runtime evidence exists (caller), otherwise the profile is small (fallback). The writer profile is reported but never adds a verifier. The plan follows its risk tier whatever RDD reads and whatever the native review outcome: the native review is an additional outside view and never removes a verifier the tier requires (verify-always-rdd-high S1). ASSESS derives closed only from the native candidate.consumed fact for this exact candidate, written natively when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown; the outcome only explains the plan. A decline or unavailable review recorded by this process is bound to that exact candidate\'s own target identity, never to a different candidate or to bare repository state, and wins over closure. The result\'s outcome_source (explicit|derived|unknown) and writerProfileSource (runtime|caller|fallback) state which evidence produced each value. When native reports them, ASSESS projects reviewDue, reviewDueReason, candidate.consumed, and the native continuation verbatim; older binaries omit them and nothing is invented. Relay that continuation unchanged; never rebuild it. A native code review is not a substitute for applicable functional checks: tests, builds, and functional verification such as browser checks for UI changes still run when applicable. A failed or unavailable native assessment (nativeRisk "unassessable"), or a native high, informs only: the plan adds the independent verifier only when escalate names a Task Size high-risk item. This never mutates review authority state.',
|
|
9863
10168
|
],
|
|
9864
10169
|
parameters: REVIEW_CONTROLLER_PARAMETERS,
|
|
9865
10170
|
executionMode: "sequential",
|
|
10171
|
+
// Hidden declarations stay active and callable; Pi leaves their
|
|
10172
|
+
// declarations and guidelines out of the request.
|
|
10173
|
+
prepareLoadout(loadout) {
|
|
10174
|
+
if (!rddOffReviewLoadout) return undefined;
|
|
10175
|
+
const declared = new Set(loadout.declared.map((tool) => tool.name));
|
|
10176
|
+
return {
|
|
10177
|
+
descriptions: { gentle_review: RDD_OFF_REVIEW_DESCRIPTION },
|
|
10178
|
+
hiddenDeclarations: RDD_OFF_HIDDEN_REVIEW_TOOLS.filter((name) => declared.has(name)),
|
|
10179
|
+
};
|
|
10180
|
+
},
|
|
9866
10181
|
renderCall(args, theme, context) {
|
|
9867
10182
|
return renderGentleAiLifecycleCall(
|
|
9868
10183
|
reviewToolOperationPath(args),
|
|
@@ -10139,7 +10454,13 @@ function createGentleAiExtensionForTesting(
|
|
|
10139
10454
|
if (inFlightModeRead !== undefined) await Promise.race([inFlightModeRead, rddAbortRejection(rddStatusSignal)]).catch(() => undefined);
|
|
10140
10455
|
const rddStatusLine = !isPrimarySession
|
|
10141
10456
|
? undefined
|
|
10142
|
-
:
|
|
10457
|
+
: pinRddStatusLine(
|
|
10458
|
+
pinnedRddStatusLines.get(subagentDepthKey),
|
|
10459
|
+
await resolveRddStatusLine(nativeReviewCli, ctx.cwd, rddStatusSignal, undefined, ctx),
|
|
10460
|
+
);
|
|
10461
|
+
if (rddStatusLine !== undefined) pinnedRddStatusLines.set(subagentDepthKey, rddStatusLine);
|
|
10462
|
+
rddOffReviewLoadout = isPrimarySession && isRddStatusLineOff(rddStatusLine);
|
|
10463
|
+
if (rddOffReviewLoadout) applyRddOffReviewPrompt(event.systemPromptOptions as ReviewToolPromptOptions | undefined);
|
|
10143
10464
|
const gentlePrompt = rddStatusLine === undefined
|
|
10144
10465
|
? ""
|
|
10145
10466
|
: `\n\n${buildGentlePrompt(
|
|
@@ -10165,7 +10486,7 @@ function createGentleAiExtensionForTesting(
|
|
|
10165
10486
|
// and forwards only systemPromptOptions, so the harness is delivered
|
|
10166
10487
|
// through the mutable appendSystemPrompt section instead of a replacement.
|
|
10167
10488
|
appendSystemPromptOnce(event.systemPromptOptions, `${gentlePrompt}${reviewContractPrompt}`);
|
|
10168
|
-
updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx));
|
|
10489
|
+
updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx), isPrimarySession && await yolo.superActive(ctx));
|
|
10169
10490
|
updateAutoPrompt(event.systemPromptOptions, isPrimarySession && await yolo.mode(ctx) === "auto");
|
|
10170
10491
|
return undefined;
|
|
10171
10492
|
});
|
|
@@ -10222,7 +10543,7 @@ function createGentleAiExtensionForTesting(
|
|
|
10222
10543
|
consumeReviewMutation(pi, ctx.sessionManager, root, mutation, "nudged", targetIdentity);
|
|
10223
10544
|
});
|
|
10224
10545
|
|
|
10225
|
-
|
|
10546
|
+
const recordWriteResult = async (event: ToolResultEvent, ctx: ExtensionContext): Promise<void> => {
|
|
10226
10547
|
if (!reminderSessionActive || (reminderManager && reminderManager !== ctx.sessionManager) || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
10227
10548
|
if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
|
|
10228
10549
|
try {
|
|
@@ -10234,21 +10555,69 @@ function createGentleAiExtensionForTesting(
|
|
|
10234
10555
|
if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
|
|
10235
10556
|
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && prospectiveRoot && !resolveSessionWorktree(ctx.cwd, ctx.cwd)) await prepareBoundSessionRepository(ctx.sessionManager, ctx.sessionManager.getCwd?.() ?? ctx.cwd, ctx.signal);
|
|
10236
10557
|
} catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
|
|
10558
|
+
};
|
|
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
|
+
});
|
|
10568
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
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.
|
|
10573
|
+
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
10574
|
+
const primary = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(sessionKey) ?? 0) === 0;
|
|
10575
|
+
let notice: string | undefined;
|
|
10576
|
+
try { notice = primary ? contextBackstopSignal(sessionKey, contextBackstopMeasure(sessionKey, () => ctx.getContextUsage?.())) : undefined; } catch { notice = undefined; }
|
|
10577
|
+
const withNotice = (patch: ToolResultEventResult | undefined) => notice ? appendContextBackstopNotice(event, patch, notice) : patch;
|
|
10578
|
+
// Harness roadmap T4: a parent commit reports the branch's authored
|
|
10579
|
+
// changed lines so delivery slicing reads a number, not an estimate.
|
|
10580
|
+
if (SHELL_COMMAND_TOOLS.has(event.toolName)) {
|
|
10581
|
+
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD === "1") return undefined;
|
|
10582
|
+
try { return withNotice(await commitSliceToolResult(event, ctx.cwd)); } catch { return withNotice(undefined); }
|
|
10583
|
+
}
|
|
10584
|
+
await recordWriteResult(event, ctx);
|
|
10585
|
+
return withNotice(undefined);
|
|
10237
10586
|
});
|
|
10238
10587
|
|
|
10239
10588
|
pi.on("tool_call", async (event, ctx) => {
|
|
10589
|
+
// Native review tools stalled the session when a codemode script called them
|
|
10590
|
+
// (a nested call, which carries parentToolCallId); direct calls return at once.
|
|
10591
|
+
if (event.toolName.startsWith("gentle_review") && typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string") {
|
|
10592
|
+
return { block: true, reason: `${event.toolName} runs only as a direct tool call: call it directly, never from codemode or another tool.` };
|
|
10593
|
+
}
|
|
10240
10594
|
const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
|
|
10241
|
-
|
|
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";
|
|
10242
10600
|
const sensitivePathDenied = evaluateSensitivePathTool(
|
|
10243
10601
|
event.toolName,
|
|
10244
10602
|
event.input,
|
|
10245
10603
|
);
|
|
10246
10604
|
if (sensitivePathDenied) return sensitivePathDenied;
|
|
10605
|
+
const deliveredTestDenied = evaluateDeliveredTestTool(event.toolName, event.input, ctx.cwd);
|
|
10606
|
+
if (deliveredTestDenied) return deliveredTestDenied;
|
|
10247
10607
|
if (event.toolName === "subagent_run") {
|
|
10248
10608
|
const judgmentDayFixDenied = rejectInvalidJudgmentDayFixDispatch(event.input);
|
|
10249
10609
|
if (judgmentDayFixDenied) return judgmentDayFixDenied;
|
|
10250
10610
|
const writerScopeDenied = rejectUnscopedBoundedWriterDispatch(event.input);
|
|
10251
10611
|
if (writerScopeDenied) return writerScopeDenied;
|
|
10612
|
+
if (primaryToolCall) {
|
|
10613
|
+
const launch = event.input as Record<string, unknown>;
|
|
10614
|
+
const selected = [launch.workspace_root, launch.repository_root].find((value): value is string => typeof value === "string" && value.trim() !== "");
|
|
10615
|
+
const root = selected ? resolve(ctx.cwd, selected) : ctx.cwd;
|
|
10616
|
+
const undocumentedWriter = rejectUndocumentedWriterLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
|
|
10617
|
+
if (undocumentedWriter) return undocumentedWriter;
|
|
10618
|
+
const unriskedVerifier = rejectUnriskedVerifierLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
|
|
10619
|
+
if (unriskedVerifier) return unriskedVerifier;
|
|
10620
|
+
}
|
|
10252
10621
|
try {
|
|
10253
10622
|
injectReviewCandidateView(event.input, candidateViews);
|
|
10254
10623
|
return undefined;
|
|
@@ -10273,13 +10642,17 @@ function createGentleAiExtensionForTesting(
|
|
|
10273
10642
|
if (childDenied) return childDenied;
|
|
10274
10643
|
}
|
|
10275
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.
|
|
10276
10649
|
const permissionMode = primaryToolCall ? await yolo.mode(ctx) : "normal";
|
|
10277
10650
|
// A superseded lookup returns Normal for display, not permission to skip Auto.
|
|
10278
10651
|
if (primaryToolCall && modeEpoch !== yolo.epoch()) {
|
|
10279
10652
|
return { block: true, reason: "Permission mode changed during tool admission. This action was not run; retry under the current mode." };
|
|
10280
10653
|
}
|
|
10281
10654
|
const autoGate = permissionMode === "auto" ? gate : undefined;
|
|
10282
|
-
return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate);
|
|
10655
|
+
return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate, superYoloActive);
|
|
10283
10656
|
});
|
|
10284
10657
|
|
|
10285
10658
|
for (const owner of ["delegation", "review"] as const) {
|
|
@@ -10298,7 +10671,7 @@ function createGentleAiExtensionForTesting(
|
|
|
10298
10671
|
}
|
|
10299
10672
|
|
|
10300
10673
|
pi.registerCommand("gentle:models", {
|
|
10301
|
-
description: "Configure global
|
|
10674
|
+
description: "Configure global agent models and choose a profile orchestrator.",
|
|
10302
10675
|
handler: async (_args, ctx) => {
|
|
10303
10676
|
await handleModelsCommand(ctx, pi);
|
|
10304
10677
|
},
|
|
@@ -10402,6 +10775,10 @@ function createGentleAiExtensionForTesting(
|
|
|
10402
10775
|
if (modelConfig.status === "invalid") {
|
|
10403
10776
|
lines.push(`remedy: fix or remove ${modelConfig.path}`);
|
|
10404
10777
|
}
|
|
10778
|
+
lines.push(...herdrSetupHint(process.env, {
|
|
10779
|
+
interactive: ctx.mode === "tui" && ctx.hasUI,
|
|
10780
|
+
isSocket: (path) => { try { return statSync(path).isSocket(); } catch { return false; } },
|
|
10781
|
+
}));
|
|
10405
10782
|
ctx.ui.notify(
|
|
10406
10783
|
lines.join("\n"),
|
|
10407
10784
|
lines.some((line) => line.startsWith("fail:")) || assetLines.some((line) => line.startsWith("warn:")) ? "warning" : "info",
|