gentle-pi 3.7.0 → 4.0.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 +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
|
|
2
|
+
import { recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
|
|
3
|
+
import { blockChildDestructiveCommand } from "./child-safety.ts";
|
|
4
|
+
import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
|
|
5
|
+
import { consumeReviewMutation, pendingReviewMutation, pendingReviewMutationProfiles, recordReviewMutation, type ReceiptSession } from "../lib/review-reminder-receipt.ts";
|
|
6
|
+
import { createReviewSidebarPublisher } from "../lib/review-sidebar-state.ts";
|
|
7
|
+
import { isOddPhase, oddPhaseRegistry, ODD_PHASES } from "../lib/odd-phase.ts";
|
|
2
8
|
import { resolveSessionWorktree } from "../lib/session-worktree-registry.ts";
|
|
3
|
-
import { resolveResearchCapabilities, renderResearchCapabilities } from "../lib/sdd-research-capabilities.ts";
|
|
4
9
|
import { declareReviewRelayHandshake } from "../lib/review-relay-contract.ts";
|
|
5
10
|
import { execFileSync } from "node:child_process";
|
|
6
11
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -30,7 +35,7 @@ import type {
|
|
|
30
35
|
ThemeColor,
|
|
31
36
|
ToolCallEventResult,
|
|
32
37
|
} from "@earendil-works/pi-coding-agent";
|
|
33
|
-
import { Key, isKeyRelease, matchesKey, truncateToWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
38
|
+
import { Key, Text, isKeyRelease, matchesKey, truncateToWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
34
39
|
import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
35
40
|
import {
|
|
36
41
|
BACKGROUND_SUBAGENTS_FILE,
|
|
@@ -41,23 +46,7 @@ import {
|
|
|
41
46
|
type BackgroundSubagentsPolicy,
|
|
42
47
|
type BackgroundSubagentsResolution,
|
|
43
48
|
} from "../lib/background-subagents-policy.ts";
|
|
44
|
-
import {
|
|
45
|
-
ensureSddPreflight,
|
|
46
|
-
getSddPreflightPreferences,
|
|
47
|
-
installPackageAssets,
|
|
48
|
-
getPackageAssetOwner,
|
|
49
|
-
hasPackageAssetOwnerInstallation,
|
|
50
|
-
type PackageAssetOwner,
|
|
51
|
-
isPackageManagedSddAsset,
|
|
52
|
-
isSddPreflightTrigger,
|
|
53
|
-
renderSddPreflightPrompt,
|
|
54
|
-
isParentConfirmedSddPreflightContext,
|
|
55
|
-
SHIPPED_SDD_AGENT_NAMES,
|
|
56
|
-
SDD_PREFLIGHT_FIELDS,
|
|
57
|
-
type SddPreflightField,
|
|
58
|
-
type SddPreflightPreferences,
|
|
59
|
-
updatePackageManagedSddAgentOwnership,
|
|
60
|
-
} from "../lib/sdd-preflight.ts";
|
|
49
|
+
import { installPackageAssets, getPackageAssetOwner, hasPackageAssetOwnerInstallation, type PackageAssetOwner, isPackageManagedSddAsset, updatePackageManagedSddAgentOwnership } from "../lib/agent-assets.ts";
|
|
61
50
|
import {
|
|
62
51
|
THINKING_LEVELS,
|
|
63
52
|
normalizeModelConfig,
|
|
@@ -119,11 +108,6 @@ import {
|
|
|
119
108
|
import { measureAgentsViewLayout, type AgentsViewLayout } from "../lib/agents-view-layout.ts";
|
|
120
109
|
import { NativeChoiceList } from "../lib/native-choice-list.ts";
|
|
121
110
|
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
122
|
-
import {
|
|
123
|
-
parseSddStatusCommandArgs,
|
|
124
|
-
renderNativeSddPhasePrompt,
|
|
125
|
-
type SddPhase,
|
|
126
|
-
} from "../lib/sdd-status.ts";
|
|
127
111
|
import {
|
|
128
112
|
REVIEW_HOST_RELAY_FAILURE,
|
|
129
113
|
REVIEW_HOST_RELAY_PI_TIMEOUT_ENV,
|
|
@@ -166,9 +150,10 @@ import {
|
|
|
166
150
|
type ReviewMode,
|
|
167
151
|
type ReviewProjectionV1,
|
|
168
152
|
} from "../lib/review-snapshot.ts";
|
|
153
|
+
import { GentleAiElapsedTimingLedger } from "../lib/gentle-ai-elapsed-store.ts";
|
|
169
154
|
import { renderGentleAiLifecycleCall, renderGentleAiResult, type GentleAiRenderContext } from "../lib/gentle-ai-renderer.ts";
|
|
170
155
|
import { sanitizeTerminalText, stripAnsi } from "../lib/terminal-theme.ts";
|
|
171
|
-
import { BASE_REF_ACCEPTED_FORMS, CandidateViewError, CandidateViewRegistry, injectReviewCandidateView, readCandidateContextManifestPage, resolveCanonicalCandidateBase, type CandidateView } from "../lib/review-candidate-view.ts";
|
|
156
|
+
import { BASE_REF_ACCEPTED_FORMS, CandidateViewError, CandidateViewRegistry, injectReviewCandidateView, readCandidateContextManifestPage, resolveCanonicalCandidateBase, isProviderCandidateBaseTree, type CandidateView } from "../lib/review-candidate-view.ts";
|
|
172
157
|
import {
|
|
173
158
|
GentleAiDevBinaryOverrideError,
|
|
174
159
|
GENTLE_AI_INSTALL_RECOVERY_COMMAND,
|
|
@@ -186,7 +171,6 @@ import {
|
|
|
186
171
|
import {
|
|
187
172
|
createNativeReviewCli,
|
|
188
173
|
createNodeExecFileAdapter,
|
|
189
|
-
decodeNativeSddStatusV2,
|
|
190
174
|
isCanonicalProcessString,
|
|
191
175
|
isNativeReviewUnachievableVerbRefused,
|
|
192
176
|
nativeReviewAbandonAuthorization,
|
|
@@ -209,7 +193,6 @@ import {
|
|
|
209
193
|
NATIVE_REVIEW_RECONCILE_ANOMALIES,
|
|
210
194
|
sanitizeForeignNativeReviewDiagnostics,
|
|
211
195
|
type NativeReviewCli,
|
|
212
|
-
type NativeSddStatusV2,
|
|
213
196
|
type NativeIntendedUntrackedSelectionSubmission,
|
|
214
197
|
type NativeReviewAcknowledgeApprovedOutcome,
|
|
215
198
|
type NativeReviewAcknowledgeApprovedRequest,
|
|
@@ -226,14 +209,20 @@ import {
|
|
|
226
209
|
} from "../lib/native-review-cli.ts";
|
|
227
210
|
import {
|
|
228
211
|
verificationPlan,
|
|
212
|
+
isSmallWriterProfile,
|
|
229
213
|
resolveWriterProfile,
|
|
230
214
|
RDD_LINE,
|
|
215
|
+
WRITER_PROFILE,
|
|
231
216
|
VERIFICATION_TIER,
|
|
232
217
|
NATIVE_REVIEW_OUTCOME,
|
|
233
218
|
type RddLine,
|
|
234
219
|
type VerificationTier,
|
|
235
220
|
type ReviewAssessmentV1,
|
|
221
|
+
type ReviewAssessmentNextTransition,
|
|
222
|
+
type ReviewAssessmentReason,
|
|
223
|
+
type ReviewDueReason,
|
|
236
224
|
type NativeReviewOutcome,
|
|
225
|
+
type WriterProfile,
|
|
237
226
|
} from "../lib/review-risk-assessment.ts";
|
|
238
227
|
import {
|
|
239
228
|
assertReviewApprovedAcknowledgementExecuteV1,
|
|
@@ -245,6 +234,7 @@ import {
|
|
|
245
234
|
type ReviewStatusV3,
|
|
246
235
|
} from "../lib/review-integration-v2.ts";
|
|
247
236
|
import { reconcileUnknownReviewLastEventCapture } from "../lib/review-last-event-controller.ts";
|
|
237
|
+
import { registerYoloSessionPolicy, updateYoloPrompt } from "../lib/yolo-session-policy.ts";
|
|
248
238
|
import { acquireChildStandingReviewPermissionClient, type ChildStandingReviewPermissionClient } from "../lib/review-session-standing-permission-ipc.ts";
|
|
249
239
|
import { isPiConsentV3, presentReviewConsentUi } from "../lib/review-consent-ui.ts";
|
|
250
240
|
import {
|
|
@@ -316,15 +306,12 @@ function packageAssetAudit(owner: PackageAssetOwner): { stale: number; overrides
|
|
|
316
306
|
}
|
|
317
307
|
|
|
318
308
|
function packageAssetDiagnosticLines(cwd: string): string[] {
|
|
319
|
-
return (["delegation", "review"
|
|
320
|
-
const label = owner
|
|
321
|
-
const onDemand = owner === "sdd" && !hasPackageAssetOwnerInstallation(owner);
|
|
309
|
+
return (["delegation", "review"] as const).flatMap((owner) => {
|
|
310
|
+
const label = owner;
|
|
322
311
|
const { stale, overrides } = packageAssetAudit(owner);
|
|
323
312
|
const local = localAgentOverrideCount(cwd, owner);
|
|
324
|
-
const lines = [
|
|
325
|
-
|
|
326
|
-
: `${stale > 0 ? "warn" : "pass"}: Global ${label} assets stale: ${stale} file(s)`];
|
|
327
|
-
if (!onDemand && stale > 0) {
|
|
313
|
+
const lines = [`${stale > 0 ? "warn" : "pass"}: Global ${label} assets stale: ${stale} file(s)`];
|
|
314
|
+
if (stale > 0) {
|
|
328
315
|
lines[0] += ` — run /gentle:install-${owner} --force to refresh managed assets`;
|
|
329
316
|
}
|
|
330
317
|
if (overrides > 0) {
|
|
@@ -469,138 +456,11 @@ function renderBackgroundSubagentsReport(
|
|
|
469
456
|
const SUBAGENTS_PACKAGE_NAMES = ["pi-subagents-j0k3r", "pi-subagents"] as const;
|
|
470
457
|
const SUBAGENT_RUN_TOOL = "subagent_run";
|
|
471
458
|
const JUDGMENT_DAY_FIX_AGENT_NAME = "jd-fix-agent";
|
|
472
|
-
const BOUNDED_WRITER_AGENT_NAMES = ["gentle-ai-worker", "worker", JUDGMENT_DAY_FIX_AGENT_NAME] as const;
|
|
473
459
|
const JUDGMENT_DAY_ACTIVATION_HEADING = "## Judgment Day activation";
|
|
474
460
|
const JUDGMENT_DAY_ACTIVATION_SENTENCE = "User explicitly requested Judgment Day.";
|
|
475
461
|
const JUDGMENT_DAY_AUTHORIZED_SEVERE_IDS_HEADING = "## Exact authorized severe IDs";
|
|
476
462
|
const JUDGMENT_DAY_CORRECTION_BATCH_HEADING = "## Judgment Day correction batch";
|
|
477
463
|
const JUDGMENT_DAY_FROZEN_FINDING_ROWS_HEADING = "## Exact frozen finding rows";
|
|
478
|
-
const ALLOWED_EDIT_SURFACES_HEADING = /^## Allowed edit surfaces[ \t]*$/gim;
|
|
479
|
-
const MARKDOWN_HEADING_LINE = /^ {0,3}#{1,6} /;
|
|
480
|
-
const MARKDOWN_LIST_MARKER = /^(?:[-*+]|\d+[.)]) +/;
|
|
481
|
-
const WRITER_EDIT_SURFACE_REJECTION =
|
|
482
|
-
"Writer tasks must include the exact Markdown heading `## Allowed edit surfaces` with narrow repository-relative paths or narrow globs, one per line. Every non-empty line belongs to the section until the next canonical Markdown heading and must be a valid surface entry. Paths containing whitespace require whole-entry backticks; begin explanatory prose under the next Markdown heading. The parent must derive or map that canonical block from the delegated task and relaunch the writer; do not accept aliases, and do not ask the human to author paths or globs.";
|
|
483
|
-
|
|
484
|
-
function isTaskScopedRepositoryRelativePath(value: string, isWholeEntryBackticked: boolean): boolean {
|
|
485
|
-
const normalized = value.replace(/\\/g, "/");
|
|
486
|
-
if (
|
|
487
|
-
normalized.length === 0 ||
|
|
488
|
-
isAbsolute(value) ||
|
|
489
|
-
/^(?:[A-Za-z]:|\/|~)/.test(normalized) ||
|
|
490
|
-
/\p{Cc}|\p{Zl}|\p{Zp}/u.test(normalized) ||
|
|
491
|
-
(/\p{White_Space}/u.test(normalized) && !isWholeEntryBackticked)
|
|
492
|
-
) {
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const withoutCurrentDirectory = normalized.replace(/^(?:\.\/)+/, "");
|
|
497
|
-
if (
|
|
498
|
-
withoutCurrentDirectory.length === 0 ||
|
|
499
|
-
withoutCurrentDirectory === "." ||
|
|
500
|
-
withoutCurrentDirectory.startsWith("/") ||
|
|
501
|
-
withoutCurrentDirectory.split("/").some((segment) => segment === "..")
|
|
502
|
-
) {
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return !/[?*\[\]{}]/.test(withoutCurrentDirectory.split("/")[0]);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
type AllowedEditSurfaceEntry = {
|
|
510
|
-
source: string;
|
|
511
|
-
value: string;
|
|
512
|
-
isWholeEntryBackticked: boolean;
|
|
513
|
-
isValidMarkdownSyntax: boolean;
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
/** Reads one entry and records whether backticks delimit the whole path. */
|
|
517
|
-
function readSurfaceEntry(line: string): AllowedEditSurfaceEntry {
|
|
518
|
-
const withoutListMarker = line.replace(MARKDOWN_LIST_MARKER, "");
|
|
519
|
-
const backticked = withoutListMarker.match(/^`([^`]+)`$/);
|
|
520
|
-
return {
|
|
521
|
-
source: line,
|
|
522
|
-
value: backticked?.[1] ?? withoutListMarker,
|
|
523
|
-
isWholeEntryBackticked: backticked !== null,
|
|
524
|
-
isValidMarkdownSyntax:
|
|
525
|
-
!/^(?:[-*+]|\d+[.)])$/.test(line) && (!withoutListMarker.includes("`") || backticked !== null),
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Reads every non-empty line until the next Markdown heading as an edit surface.
|
|
531
|
-
* A prose line cannot terminate this section: it must fail validation instead.
|
|
532
|
-
*/
|
|
533
|
-
function readAllowedEditSurfaceEntries(following: string): AllowedEditSurfaceEntry[] {
|
|
534
|
-
const lines = following.split(/\r?\n/);
|
|
535
|
-
const headingIndex = lines.findIndex((line) => MARKDOWN_HEADING_LINE.test(line));
|
|
536
|
-
return (headingIndex === -1 ? lines : lines.slice(0, headingIndex))
|
|
537
|
-
.map((line) => line.replace(/ +$/g, ""))
|
|
538
|
-
.filter((line) => line.length > 0)
|
|
539
|
-
.map((line) => readSurfaceEntry(line.replace(/^ {0,3}/, "")));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function hasTaskScopedAllowedEditSurfaces(...values: unknown[]): boolean {
|
|
543
|
-
let expectedEntries: string[] | undefined;
|
|
544
|
-
let hasSection = false;
|
|
545
|
-
|
|
546
|
-
for (const value of values) {
|
|
547
|
-
if (typeof value !== "string") continue;
|
|
548
|
-
|
|
549
|
-
const headings = value.matchAll(ALLOWED_EDIT_SURFACES_HEADING);
|
|
550
|
-
for (const heading of headings) {
|
|
551
|
-
const bodyStart = (heading.index ?? 0) + heading[0].length;
|
|
552
|
-
const entries = readAllowedEditSurfaceEntries(value.slice(bodyStart));
|
|
553
|
-
if (
|
|
554
|
-
entries.length === 0 ||
|
|
555
|
-
!entries.every(
|
|
556
|
-
(entry) =>
|
|
557
|
-
entry.isValidMarkdownSyntax &&
|
|
558
|
-
!/\p{Cc}|\p{Zl}|\p{Zp}/u.test(entry.source) &&
|
|
559
|
-
isTaskScopedRepositoryRelativePath(entry.value, entry.isWholeEntryBackticked),
|
|
560
|
-
)
|
|
561
|
-
) {
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const uniqueEntries = [...new Set(entries.map((entry) => entry.value))].sort();
|
|
566
|
-
if (
|
|
567
|
-
expectedEntries &&
|
|
568
|
-
(expectedEntries.length !== uniqueEntries.length ||
|
|
569
|
-
expectedEntries.some((entry, index) => entry !== uniqueEntries[index]))
|
|
570
|
-
) {
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
expectedEntries = uniqueEntries;
|
|
574
|
-
hasSection = true;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return hasSection;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function sddDispatchAgentName(input: unknown): string | undefined {
|
|
582
|
-
if (!isRecord(input)) return undefined;
|
|
583
|
-
if (typeof input.agent === "string" && SDD_AGENT_NAME_SET.has(input.agent)) return input.agent;
|
|
584
|
-
if (Array.isArray(input.agent) && input.agent.some((agent) => typeof agent === "string" && SDD_AGENT_NAME_SET.has(agent))) {
|
|
585
|
-
return "invalid";
|
|
586
|
-
}
|
|
587
|
-
return undefined;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function rejectUnscopedBoundedWriterDispatch(input: unknown): { block: true; reason: string } | undefined {
|
|
591
|
-
if (
|
|
592
|
-
!isRecord(input) ||
|
|
593
|
-
typeof input.agent !== "string" ||
|
|
594
|
-
!(BOUNDED_WRITER_AGENT_NAMES as readonly string[]).includes(input.agent)
|
|
595
|
-
) {
|
|
596
|
-
return undefined;
|
|
597
|
-
}
|
|
598
|
-
if (hasTaskScopedAllowedEditSurfaces(input.task, input.context)) {
|
|
599
|
-
return undefined;
|
|
600
|
-
}
|
|
601
|
-
return { block: true, reason: WRITER_EDIT_SURFACE_REJECTION };
|
|
602
|
-
}
|
|
603
|
-
|
|
604
464
|
function hasJudgmentDayFixAgentReference(input: Record<string, unknown>): boolean {
|
|
605
465
|
return input.agent === JUDGMENT_DAY_FIX_AGENT_NAME ||
|
|
606
466
|
(Array.isArray(input.agent) && input.agent.includes(JUDGMENT_DAY_FIX_AGENT_NAME)) ||
|
|
@@ -883,7 +743,7 @@ function renderRddStatusLine(
|
|
|
883
743
|
: "Receipt-driven development: unknown (native status unavailable)";
|
|
884
744
|
}
|
|
885
745
|
|
|
886
|
-
// The primary-session prompt awaits this on every non-
|
|
746
|
+
// The primary-session prompt awaits this on every non-named agent
|
|
887
747
|
// start, so an unbounded native read would stall session start behind a
|
|
888
748
|
// hung `gentle-ai` child (gentle-pi#661 native-review escalation). The
|
|
889
749
|
// production call site (before_agent_start) passes
|
|
@@ -909,8 +769,9 @@ function clearRddStatusMemoForTesting(): void {
|
|
|
909
769
|
// repository realpath AND targetIdentity -- never repository alone, or one
|
|
910
770
|
// candidate's outcome would leak into every other candidate's `assess` call.
|
|
911
771
|
// Only declined/unavailable are ever written (from ANSWER_CONSENT); `closed`
|
|
912
|
-
// is never written
|
|
913
|
-
//
|
|
772
|
+
// is never written here -- it is derived per assess call only from that
|
|
773
|
+
// call's own native `candidate.consumed` evidence (gentle-pi#1175). A missing
|
|
774
|
+
// entry reads back `undefined`, treated as `unknown` (fail closed, like `off`).
|
|
914
775
|
const nativeReviewOutcomeByCandidate = new Map<string, "declined" | "unavailable">();
|
|
915
776
|
|
|
916
777
|
function nativeReviewOutcomeMemoKey(cwd: string, targetIdentity: string): string {
|
|
@@ -985,16 +846,28 @@ async function readRddModeStatusOnce(
|
|
|
985
846
|
interface ReviewAssessmentPlanDetails {
|
|
986
847
|
schema: "gentle-pi.review-assessment-plan/v1";
|
|
987
848
|
risk: VerificationTier;
|
|
988
|
-
reasons: readonly
|
|
849
|
+
reasons: readonly ReviewAssessmentReason[];
|
|
989
850
|
changedPaths: number;
|
|
990
851
|
changedLines: number;
|
|
991
|
-
|
|
852
|
+
// gentle-pi#1175: `consumed`, `reviewDue`, `reviewDueReason`, and
|
|
853
|
+
// `nextTransition` are projected only when native reported them; an older
|
|
854
|
+
// binary omits them and they are never fabricated here.
|
|
855
|
+
candidate: { kind: string; baseRef: string | undefined; consumed?: boolean } | null;
|
|
856
|
+
reviewDue?: boolean;
|
|
857
|
+
reviewDueReason?: ReviewDueReason;
|
|
858
|
+
// Opaque native continuation, carried verbatim.
|
|
859
|
+
nextTransition?: ReviewAssessmentNextTransition;
|
|
992
860
|
rddLine: RddLine;
|
|
993
861
|
nativeReviewOutcome: NativeReviewOutcome;
|
|
994
|
-
// gentle-pi#668: where nativeReviewOutcome came from -- explicit
|
|
995
|
-
//
|
|
996
|
-
|
|
997
|
-
|
|
862
|
+
// gentle-pi#668/#1175: where nativeReviewOutcome came from -- explicit
|
|
863
|
+
// (a caller-supplied declined/unavailable/unknown), derived (native
|
|
864
|
+
// consumed evidence or the per-candidate memo for this exact candidate),
|
|
865
|
+
// or unknown.
|
|
866
|
+
outcome_source: NativeReviewOutcomeSource;
|
|
867
|
+
writerProfile: WriterProfile;
|
|
868
|
+
// gentle-pi#1175: runtime (pending mutation receipts), caller (input,
|
|
869
|
+
// only when no runtime receipt is pending), or fallback (small).
|
|
870
|
+
writerProfileSource: WriterProfileSource;
|
|
998
871
|
plan: {
|
|
999
872
|
writerSelfVerification: boolean;
|
|
1000
873
|
structuralReadbackOnly: boolean;
|
|
@@ -1003,21 +876,104 @@ interface ReviewAssessmentPlanDetails {
|
|
|
1003
876
|
};
|
|
1004
877
|
}
|
|
1005
878
|
|
|
879
|
+
type NativeReviewOutcomeSource = "explicit" | "derived" | "unknown";
|
|
880
|
+
type WriterProfileSource = "runtime" | "caller" | "fallback";
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* gentle-pi#1175: the writer profile for ASSESS. Pending runtime mutation
|
|
884
|
+
* receipts for this root are the evidence: any writer without a recorded
|
|
885
|
+
* model id, or any small writer (`isSmallWriterProfile`), makes the profile
|
|
886
|
+
* small. Caller-declared writerModelId/writerEffort are consulted only when no
|
|
887
|
+
* runtime receipt is pending, and can never override runtime evidence. An
|
|
888
|
+
* unreadable receipt ledger fails closed to small.
|
|
889
|
+
*/
|
|
890
|
+
function resolveAssessWriterProfile(
|
|
891
|
+
session: ReceiptSession | undefined,
|
|
892
|
+
root: string,
|
|
893
|
+
input: ReviewAssessInput,
|
|
894
|
+
): { writerProfile: WriterProfile; writerProfileSource: WriterProfileSource } {
|
|
895
|
+
let pending: ReturnType<typeof pendingReviewMutationProfiles> = [];
|
|
896
|
+
try {
|
|
897
|
+
if (session !== undefined) pending = pendingReviewMutationProfiles(session, root);
|
|
898
|
+
} catch {
|
|
899
|
+
return { writerProfile: WRITER_PROFILE.SMALL, writerProfileSource: "fallback" };
|
|
900
|
+
}
|
|
901
|
+
if (pending.length > 0) {
|
|
902
|
+
const small = pending.some((profile) => profile.writerModelId === undefined ||
|
|
903
|
+
isSmallWriterProfile({ model: { id: profile.writerModelId }, thinking: profile.writerEffort }));
|
|
904
|
+
return { writerProfile: small ? WRITER_PROFILE.SMALL : WRITER_PROFILE.LARGE, writerProfileSource: "runtime" };
|
|
905
|
+
}
|
|
906
|
+
if (input.writerModelId === undefined && input.writerEffort === undefined) {
|
|
907
|
+
return { writerProfile: WRITER_PROFILE.SMALL, writerProfileSource: "fallback" };
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
writerProfile: resolveWriterProfile({
|
|
911
|
+
...(input.writerModelId === undefined ? {} : { model: { id: input.writerModelId } }),
|
|
912
|
+
thinking: input.writerEffort,
|
|
913
|
+
}),
|
|
914
|
+
writerProfileSource: "caller",
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* gentle-pi#1175: the parent session's own current model and thinking level,
|
|
920
|
+
* recorded on its direct mutation receipt. Omits whatever the runtime cannot
|
|
921
|
+
* report, so an unknown writer stays small for ASSESS.
|
|
922
|
+
*/
|
|
923
|
+
function directWriterProfile(
|
|
924
|
+
pi: Pick<ExtensionAPI, "getThinkingLevel">,
|
|
925
|
+
ctx: Pick<ExtensionContext, "model">,
|
|
926
|
+
): { writerModelId?: string; writerEffort?: string } {
|
|
927
|
+
const model = ctx.model;
|
|
928
|
+
const modelId = typeof model?.provider === "string" && model.provider && typeof model.id === "string" && model.id
|
|
929
|
+
? `${model.provider}/${model.id}`
|
|
930
|
+
: undefined;
|
|
931
|
+
let effort: string | undefined;
|
|
932
|
+
try {
|
|
933
|
+
const level: unknown = pi.getThinkingLevel();
|
|
934
|
+
if (typeof level === "string" && level) effort = level;
|
|
935
|
+
} catch { /* No runtime effort evidence. */ }
|
|
936
|
+
return {
|
|
937
|
+
...(modelId === undefined ? {} : { writerModelId: modelId }),
|
|
938
|
+
...(effort === undefined ? {} : { writerEffort: effort }),
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* gentle-pi#1175: closure comes only from exact native evidence. `closed` is
|
|
944
|
+
* derived solely from THIS assessment's `candidate.consumed === true`, which
|
|
945
|
+
* native writes only inside the approved-acknowledgement burn for that exact
|
|
946
|
+
* target identity; a caller-declared `closed` is only a claim and, without
|
|
947
|
+
* that corroboration, fails closed to `unknown`. Explicit declined,
|
|
948
|
+
* unavailable, or unknown are honored (they only raise the bar), and a
|
|
949
|
+
* declined/unavailable recorded for this exact candidate beats closure.
|
|
950
|
+
*/
|
|
951
|
+
function resolveAssessNativeReviewOutcome(
|
|
952
|
+
input: ReviewAssessInput,
|
|
953
|
+
assessment: ReviewAssessmentV1 | undefined,
|
|
954
|
+
memoOutcome: "declined" | "unavailable" | undefined,
|
|
955
|
+
): { nativeReviewOutcome: NativeReviewOutcome; outcomeSource: NativeReviewOutcomeSource } {
|
|
956
|
+
if (input.nativeReviewOutcome !== undefined && input.nativeReviewOutcome !== NATIVE_REVIEW_OUTCOME.CLOSED) {
|
|
957
|
+
return { nativeReviewOutcome: input.nativeReviewOutcome, outcomeSource: "explicit" };
|
|
958
|
+
}
|
|
959
|
+
if (memoOutcome !== undefined) return { nativeReviewOutcome: memoOutcome, outcomeSource: "derived" };
|
|
960
|
+
if (assessment?.candidate.consumed === true) return { nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED, outcomeSource: "derived" };
|
|
961
|
+
return { nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.UNKNOWN, outcomeSource: "unknown" };
|
|
962
|
+
}
|
|
963
|
+
|
|
1006
964
|
async function resolveReviewAssessmentPlan(
|
|
1007
965
|
nativeReviewCli: Pick<NativeReviewCli, "reviewMode" | "assess" | "targetStatus"> | null | undefined,
|
|
1008
966
|
cwd: string,
|
|
1009
967
|
input: ReviewAssessInput,
|
|
1010
968
|
signal?: AbortSignal,
|
|
969
|
+
receiptSession?: ReceiptSession,
|
|
1011
970
|
): Promise<ReviewAssessmentPlanDetails> {
|
|
1012
971
|
if (input.baseRef !== undefined && input.committedOnly !== true) throw new Error("Review assess baseRef requires committedOnly: true");
|
|
1013
972
|
if (input.baseRef === undefined && input.committedOnly !== undefined) throw new Error("Review assess committedOnly requires an explicit baseRef");
|
|
1014
973
|
|
|
1015
974
|
const status = await readRddModeStatusOnce(nativeReviewCli, cwd, signal);
|
|
1016
975
|
const rddLine: RddLine = isValidRddModeStatus(status) ? status.effective : RDD_LINE.UNKNOWN;
|
|
1017
|
-
const writerProfile =
|
|
1018
|
-
...(input.writerModelId === undefined ? {} : { model: { id: input.writerModelId } }),
|
|
1019
|
-
thinking: input.writerEffort,
|
|
1020
|
-
});
|
|
976
|
+
const { writerProfile, writerProfileSource } = resolveAssessWriterProfile(receiptSession, cwd, input);
|
|
1021
977
|
|
|
1022
978
|
let assessment: ReviewAssessmentV1 | undefined;
|
|
1023
979
|
let unassessableDetail: string | undefined;
|
|
@@ -1045,25 +1001,37 @@ async function resolveReviewAssessmentPlan(
|
|
|
1045
1001
|
}
|
|
1046
1002
|
|
|
1047
1003
|
const risk: VerificationTier = assessment?.risk ?? VERIFICATION_TIER.UNASSESSABLE;
|
|
1048
|
-
// gentle-pi#668:
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
const
|
|
1004
|
+
// gentle-pi#668: the memo is read only for THIS candidate's own target
|
|
1005
|
+
// identity, never repository-only. It is still needed when the caller
|
|
1006
|
+
// claims `closed`, because a recorded decline beats closure (gentle-pi#1175).
|
|
1007
|
+
const needsMemo = input.nativeReviewOutcome === undefined || input.nativeReviewOutcome === NATIVE_REVIEW_OUTCOME.CLOSED;
|
|
1008
|
+
const targetIdentity = needsMemo ? await readCurrentTargetIdentityBestEffort(nativeReviewCli, cwd, signal) : undefined;
|
|
1009
|
+
const memoOutcome = targetIdentity === undefined ? undefined : readNativeReviewOutcome(cwd, targetIdentity);
|
|
1010
|
+
const { nativeReviewOutcome, outcomeSource } = resolveAssessNativeReviewOutcome(input, assessment, memoOutcome);
|
|
1055
1011
|
const plan = verificationPlan({ rddLine, risk, writerProfile, nativeReviewOutcome });
|
|
1056
1012
|
return {
|
|
1057
1013
|
schema: "gentle-pi.review-assessment-plan/v1",
|
|
1058
1014
|
risk,
|
|
1059
|
-
|
|
1015
|
+
// No path is known for an unassessable candidate: omit it rather than
|
|
1016
|
+
// emitting an empty string the native reason shape forbids.
|
|
1017
|
+
reasons: assessment?.reasons ?? (unassessableDetail === undefined ? [] : [{ code: unassessableCode, detail: unassessableDetail }]),
|
|
1060
1018
|
changedPaths: assessment?.changedPaths ?? 0,
|
|
1061
1019
|
changedLines: assessment?.changedLines ?? 0,
|
|
1062
|
-
candidate: assessment === undefined
|
|
1020
|
+
candidate: assessment === undefined
|
|
1021
|
+
? null
|
|
1022
|
+
: {
|
|
1023
|
+
kind: assessment.candidate.kind,
|
|
1024
|
+
baseRef: assessment.candidate.baseRef,
|
|
1025
|
+
...(assessment.candidate.consumed === undefined ? {} : { consumed: assessment.candidate.consumed }),
|
|
1026
|
+
},
|
|
1027
|
+
...(assessment?.reviewDue === undefined ? {} : { reviewDue: assessment.reviewDue }),
|
|
1028
|
+
...(assessment?.reviewDueReason === undefined ? {} : { reviewDueReason: assessment.reviewDueReason }),
|
|
1029
|
+
...(assessment?.nextTransition === undefined ? {} : { nextTransition: assessment.nextTransition }),
|
|
1063
1030
|
rddLine,
|
|
1064
1031
|
nativeReviewOutcome,
|
|
1065
1032
|
outcome_source: outcomeSource,
|
|
1066
1033
|
writerProfile,
|
|
1034
|
+
writerProfileSource,
|
|
1067
1035
|
plan,
|
|
1068
1036
|
};
|
|
1069
1037
|
}
|
|
@@ -1267,9 +1235,9 @@ Current persona mode: ${persona}
|
|
|
1267
1235
|
You are el Gentleman: a Pi-specific coding-agent harness for controlled development work.
|
|
1268
1236
|
|
|
1269
1237
|
Identity contract:
|
|
1270
|
-
- When the user asks who or what you are, answer as el Gentleman, not as a generic assistant, and never introduce yourself as only "your assistant" or "the default assistant". Convey this meaning, translated into the user's language: "I am el Gentleman: a Pi-specific coding-agent harness for controlled development, with a senior architect persona. I run Organic Driven Development
|
|
1238
|
+
- When the user asks who or what you are, answer as el Gentleman, not as a generic assistant, and never introduce yourself as only "your assistant" or "the default assistant". Convey this meaning, translated into the user's language: "I am el Gentleman: a Pi-specific coding-agent harness for controlled development, with a senior architect persona. I run Organic Driven Development, coordinate subagents, track substantial work, run commands, and edit files. I am not a generic chatbot."
|
|
1271
1239
|
- Follow the currently selected persona mode.
|
|
1272
|
-
- Mention ODD as the
|
|
1240
|
+
- Mention ODD as the development workflow and subagents as a core capability.
|
|
1273
1241
|
- Mention memory only when memory packages or callable memory tools are actually active; never invent persistent memory.
|
|
1274
1242
|
- Do not claim portability outside the Pi runtime.
|
|
1275
1243
|
|
|
@@ -1278,23 +1246,24 @@ ${personaPrompt}
|
|
|
1278
1246
|
${languageBoundary}
|
|
1279
1247
|
|
|
1280
1248
|
Default workflow: Organic Driven Development (MANDATORY)
|
|
1281
|
-
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.
|
|
1249
|
+
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:
|
|
1282
1250
|
1. **Authorize.** Investigation, explanation, review, comparison, and proposal-only requests stay read-only: no writer, apply, or implementation artifacts. Ambiguous or conditional change intent gets one clarification; stop and wait.
|
|
1283
1251
|
2. **Explore.** Explore existing code and requirements first, proportionately to the request, before proposing or writing anything.
|
|
1284
1252
|
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.
|
|
1285
1253
|
4. **Classify.** The work is substantial when exploration yields two or more meaningful implementation steps, or progress worth recovering after an interruption. Small, understood work stays small and creates no durable task artifacts.
|
|
1286
1254
|
5. **Track before the first write.** For substantial authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, then create or rebuild the visible \`todo\` list from the reconciled feature tasks, all before the first source write and without asking permission for tasks or storage. Tell the user in one line which feature document was created and how many tasks it holds.
|
|
1287
|
-
6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, honoring its mandatory delegation triggers, with
|
|
1255
|
+
6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, 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 task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence. Work-unit commits on the feature branch are part of authorized substantial ODD implementation; push, pull request creation, and merge remain the user's decisions.
|
|
1288
1256
|
7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch; native review runs only under the user-owned RDD switch.
|
|
1257
|
+
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.
|
|
1289
1258
|
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\`.
|
|
1290
1259
|
|
|
1291
1260
|
Harness principles:
|
|
1292
1261
|
- el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
|
|
1293
|
-
- Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track substantial work before the first write, implement task by task with proportionate checks, close each task with a work-unit commit, and close.
|
|
1262
|
+
- Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track substantial work before the first write, implement task by task with proportionate checks, close each task with a work-unit commit, and close.
|
|
1294
1263
|
- Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
|
|
1295
1264
|
- Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
|
|
1296
1265
|
- Keep writes single-threaded unless the user explicitly approves parallel write isolation.
|
|
1297
|
-
-
|
|
1266
|
+
- 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. 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.
|
|
1298
1267
|
- Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
|
|
1299
1268
|
- Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
|
|
1300
1269
|
|
|
@@ -1312,8 +1281,8 @@ const DENIED_BASH_PATTERNS: RegExp[] = [
|
|
|
1312
1281
|
/\bgit\s+reset\s+--hard\b/,
|
|
1313
1282
|
/\bgit\s+clean\b(?=[^\n]*(?:-[^\n]*f|--force))(?=[^\n]*(?:-[^\n]*d|--directories))/,
|
|
1314
1283
|
// Force-push deny: tolerates git global flags (e.g. -C /repo) before the subcommand
|
|
1315
|
-
new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}push\b(?=[^\n]*\s--force(?:-with-lease)?\b)`),
|
|
1316
|
-
new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}push\b(?=[^\n]*\s-[^\s-]*f)`),
|
|
1284
|
+
new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}push\b(?=[^\r\n;&|]*\s--force(?:-with-lease)?\b)`),
|
|
1285
|
+
new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}push\b(?=[^\r\n;&|]*\s-[^\s-]*f)`),
|
|
1317
1286
|
/\bchmod\s+-R\s+777\b/,
|
|
1318
1287
|
/\bchown\s+-R\b/,
|
|
1319
1288
|
];
|
|
@@ -1339,6 +1308,7 @@ interface GuardMatch {
|
|
|
1339
1308
|
|
|
1340
1309
|
interface GuardEvaluation {
|
|
1341
1310
|
action: GuardClassification;
|
|
1311
|
+
dataLoss?: boolean;
|
|
1342
1312
|
key?: GuardedCommandKey;
|
|
1343
1313
|
triggerIndex: number;
|
|
1344
1314
|
matches: GuardMatch[];
|
|
@@ -1434,6 +1404,9 @@ function evaluateGuardedCommand(
|
|
|
1434
1404
|
config: RuntimeGuardrailsConfig,
|
|
1435
1405
|
): GuardEvaluation {
|
|
1436
1406
|
const matches = collectGuardedMatches(command, config);
|
|
1407
|
+
const destructive = recognizeDestructiveCommands(command);
|
|
1408
|
+
const hardDeny = destructive.find((match) => match.hardDeny);
|
|
1409
|
+
if (hardDeny) return { action: "block", triggerIndex: hardDeny.triggerIndex, matches };
|
|
1437
1410
|
|
|
1438
1411
|
// Hard denies override every configured action across the complete command.
|
|
1439
1412
|
for (const pattern of DENIED_BASH_PATTERNS) {
|
|
@@ -1450,7 +1423,13 @@ function evaluateGuardedCommand(
|
|
|
1450
1423
|
};
|
|
1451
1424
|
}
|
|
1452
1425
|
|
|
1453
|
-
//
|
|
1426
|
+
// Explicit configured blocks outrank recognized data-loss confirmation.
|
|
1427
|
+
const configuredBlock = matches.find((match) => match.action === "block");
|
|
1428
|
+
if (configuredBlock) return { ...configuredBlock, matches };
|
|
1429
|
+
const dataLoss = destructive.find((match) => match.kind !== "git");
|
|
1430
|
+
if (dataLoss) return { action: "confirm", dataLoss: true, triggerIndex: dataLoss.triggerIndex, matches };
|
|
1431
|
+
|
|
1432
|
+
// Confirmation, then allow win across remaining matches.
|
|
1454
1433
|
const selected = matches.find((match) => match.action === "block")
|
|
1455
1434
|
?? matches.find((match) => match.action === "confirm")
|
|
1456
1435
|
?? matches.find((match) => match.action === "allow");
|
|
@@ -1589,20 +1568,13 @@ const SENSITIVE_PATH_PATTERNS: RegExp[] = [
|
|
|
1589
1568
|
/\.(?:pem|key|p12|pfx)$/,
|
|
1590
1569
|
];
|
|
1591
1570
|
|
|
1592
|
-
const SDD_AGENT_NAME_SET = new Set<string>(SHIPPED_SDD_AGENT_NAMES);
|
|
1593
|
-
const SDD_CHANGE_FLAG = "gentle-sdd-change";
|
|
1594
|
-
const SDD_CHANGE_KEYS = ["changeName", "phase", "workspaceRoot"] as const;
|
|
1595
|
-
|
|
1596
1571
|
const JUDGMENT_DAY_AGENT_NAMES = [
|
|
1597
1572
|
"jd-judge-a",
|
|
1598
1573
|
"jd-judge-b",
|
|
1599
1574
|
"jd-fix-agent",
|
|
1600
1575
|
] as const;
|
|
1601
1576
|
|
|
1602
|
-
const CORE_MODEL_AGENT_NAMES =
|
|
1603
|
-
...SHIPPED_SDD_AGENT_NAMES,
|
|
1604
|
-
...JUDGMENT_DAY_AGENT_NAMES,
|
|
1605
|
-
] as const;
|
|
1577
|
+
const CORE_MODEL_AGENT_NAMES = JUDGMENT_DAY_AGENT_NAMES;
|
|
1606
1578
|
const CORE_MODEL_AGENT_NAME_SET = new Set<string>(CORE_MODEL_AGENT_NAMES);
|
|
1607
1579
|
|
|
1608
1580
|
type AgentSource = "project" | "user" | "builtin";
|
|
@@ -1631,7 +1603,11 @@ const MODEL_PANEL_MAX_RENDER_ROWS = 20;
|
|
|
1631
1603
|
// Rows the agent list does not own: two borders, title, current-profile line,
|
|
1632
1604
|
// blank, "Current assignments:", blank, both scroll indicators, blank, Continue,
|
|
1633
1605
|
// Back, blank, and the two footer rows.
|
|
1634
|
-
const
|
|
1606
|
+
const AGENT_LIST_CHROME_ROWS = 15;
|
|
1607
|
+
const AGENT_LIST_MAX_VISIBLE_ROWS = MODEL_PANEL_MAX_RENDER_ROWS - AGENT_LIST_CHROME_ROWS;
|
|
1608
|
+
// Rows the model list does not own: two borders, title, blank, search, blank,
|
|
1609
|
+
// blank, and the footer row.
|
|
1610
|
+
const MODEL_LIST_CHROME_ROWS = 8;
|
|
1635
1611
|
const MODEL_LIST_MAX_VISIBLE_ROWS = 12;
|
|
1636
1612
|
|
|
1637
1613
|
function readStringPath(value: unknown, path: string[]): string | undefined {
|
|
@@ -1643,17 +1619,6 @@ function readStringPath(value: unknown, path: string[]): string | undefined {
|
|
|
1643
1619
|
return typeof current === "string" ? current : undefined;
|
|
1644
1620
|
}
|
|
1645
1621
|
|
|
1646
|
-
function isSddAgentStartEvent(event: unknown): boolean {
|
|
1647
|
-
const candidates = readAgentStartNames(event);
|
|
1648
|
-
if (candidates.some((value) => SDD_AGENT_NAME_SET.has(value)) || sddPhaseFromAgentStartEvent(event) !== undefined) return true;
|
|
1649
|
-
|
|
1650
|
-
const systemPrompt = readStringPath(event, ["systemPrompt"]) ?? "";
|
|
1651
|
-
return SHIPPED_SDD_AGENT_NAMES.some((name) => {
|
|
1652
|
-
const phase = name.replace(/^sdd-/, "");
|
|
1653
|
-
return new RegExp(`\\bSDD ${phase} executor\\b`, "i").test(systemPrompt);
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
1622
|
function readAgentStartNames(event: unknown): string[] {
|
|
1658
1623
|
return [
|
|
1659
1624
|
readStringPath(event, ["agentName"]),
|
|
@@ -1671,102 +1636,6 @@ function isNamedAgentStartEvent(event: unknown): boolean {
|
|
|
1671
1636
|
return readAgentStartNames(event).length > 0;
|
|
1672
1637
|
}
|
|
1673
1638
|
|
|
1674
|
-
function sddPhaseFromAgentStartEvent(event: unknown): SddPhase | "remediate" | undefined {
|
|
1675
|
-
const phases = ["apply", "verify", "archive", "remediate"] as const;
|
|
1676
|
-
const names = readAgentStartNames(event);
|
|
1677
|
-
const systemPrompt = readStringPath(event, ["systemPrompt"]) ?? "";
|
|
1678
|
-
const promptPhases = phases.filter((phase) => new RegExp(`\\bSDD ${phase} executor\\b`, "i").test(systemPrompt));
|
|
1679
|
-
if (promptPhases.length > 1) return undefined;
|
|
1680
|
-
if (names.length === 0) return promptPhases[0];
|
|
1681
|
-
return phases.find((phase) => names.every((name) => name === `sdd-${phase}`) &&
|
|
1682
|
-
(promptPhases.length === 0 || promptPhases[0] === phase));
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function resolveSddChangeSelection(serialized: unknown, cwd: string, agentName: string): { changeName: string; workspaceRoot: string; phase: SddPhase | "remediate"; failedEvidenceRevision?: string } {
|
|
1686
|
-
if (typeof serialized !== "string") throw new Error("SDD selection must be a JSON string.");
|
|
1687
|
-
let value: unknown;
|
|
1688
|
-
try {
|
|
1689
|
-
value = JSON.parse(serialized);
|
|
1690
|
-
} catch {
|
|
1691
|
-
throw new Error("SDD selection is malformed.");
|
|
1692
|
-
}
|
|
1693
|
-
if (!isRecord(value) || Object.keys(value).sort().join(",") !== (value.phase === "remediate" ? "changeName,failedEvidenceRevision,phase,workspaceRoot" : SDD_CHANGE_KEYS.join(","))) {
|
|
1694
|
-
throw new Error("SDD selection must contain only changeName, workspaceRoot, and phase.");
|
|
1695
|
-
}
|
|
1696
|
-
const { changeName, workspaceRoot, phase } = value;
|
|
1697
|
-
if (typeof changeName !== "string" || changeName.length === 0 ||
|
|
1698
|
-
typeof workspaceRoot !== "string" || workspaceRoot.length === 0 ||
|
|
1699
|
-
(phase !== "apply" && phase !== "verify" && phase !== "archive" && phase !== "remediate")) {
|
|
1700
|
-
throw new Error("SDD selection has an invalid identity.");
|
|
1701
|
-
}
|
|
1702
|
-
if (agentName !== `sdd-${phase}`) throw new Error("SDD selection phase does not match the child agent.");
|
|
1703
|
-
let canonicalCwd: string;
|
|
1704
|
-
let canonicalSelectionRoot: string;
|
|
1705
|
-
try {
|
|
1706
|
-
canonicalCwd = realpathSync(cwd);
|
|
1707
|
-
canonicalSelectionRoot = realpathSync(workspaceRoot);
|
|
1708
|
-
} catch {
|
|
1709
|
-
throw new Error("SDD selection workspaceRoot cannot be resolved.");
|
|
1710
|
-
}
|
|
1711
|
-
if (canonicalCwd !== canonicalSelectionRoot || workspaceRoot !== canonicalSelectionRoot) {
|
|
1712
|
-
throw new Error("SDD selection workspaceRoot does not match the canonical child root.");
|
|
1713
|
-
}
|
|
1714
|
-
if (phase === "remediate" && (typeof value.failedEvidenceRevision !== "string" || !/^sha256:[0-9a-f]{64}$/.test(value.failedEvidenceRevision))) throw new Error("Invalid remediation revision");
|
|
1715
|
-
return { changeName, workspaceRoot: canonicalCwd, phase, ...(phase === "remediate" ? { failedEvidenceRevision: value.failedEvidenceRevision as string } : {}) };
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
function assertNativeSddPhaseReady(status: NativeSddStatusV2, phase: SddPhase | "remediate"): void {
|
|
1719
|
-
if (!status.phaseInstructions || !(phase in status.phaseInstructions)) {
|
|
1720
|
-
throw new Error(`Native SDD status cannot represent phase ${phase}.`);
|
|
1721
|
-
}
|
|
1722
|
-
if (phase === "remediate") {
|
|
1723
|
-
if (status.nextRecommended !== phase) throw new Error("Native SDD status does not select remediation.");
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
// Optional verification may be explicitly selected when native recommends
|
|
1727
|
-
// apply or archive. Preserve that recommendation; never advance an older provider.
|
|
1728
|
-
const optionalVerify = phase === "verify" && (status.nextRecommended === "archive" || status.nextRecommended === "apply") && status.blockedReasons.length === 0;
|
|
1729
|
-
if ((!optionalVerify && status.nextRecommended !== phase) || status.dependencies[phase] !== "ready" ||
|
|
1730
|
-
(status.blockedReasons.length > 0 && phase !== "verify")) {
|
|
1731
|
-
// A native-selected verify can refresh the evidence named by a blocker.
|
|
1732
|
-
// Apply/archive never inherit that exception (gentle-pi#972).
|
|
1733
|
-
throw new Error(`SDD selection native status blocks phase ${phase}; it cannot execute.`);
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
async function resolveSelectedNativeSddChangeStartup(
|
|
1738
|
-
serialized: unknown,
|
|
1739
|
-
cwd: string,
|
|
1740
|
-
agentName: string,
|
|
1741
|
-
native: Pick<NativeReviewCli, "sddStatus"> | null | undefined,
|
|
1742
|
-
): Promise<{ selection: { changeName: string; workspaceRoot: string; phase: SddPhase | "remediate"; failedEvidenceRevision?: string }; status: NativeSddStatusV2 }> {
|
|
1743
|
-
const selection = resolveSddChangeSelection(serialized, cwd, agentName);
|
|
1744
|
-
if (native?.sddStatus === undefined) throw new Error("SDD selection native status is unavailable.");
|
|
1745
|
-
let status: NativeSddStatusV2;
|
|
1746
|
-
try {
|
|
1747
|
-
status = decodeNativeSddStatusV2(
|
|
1748
|
-
await native.sddStatus({ changeName: selection.changeName, workspaceRoot: selection.workspaceRoot }),
|
|
1749
|
-
{ changeName: selection.changeName, workspaceRoot: selection.workspaceRoot },
|
|
1750
|
-
);
|
|
1751
|
-
} catch (error) {
|
|
1752
|
-
throw new Error(`SDD selection native status is blocked: ${error instanceof Error ? error.message : String(error)}`);
|
|
1753
|
-
}
|
|
1754
|
-
assertNativeSddPhaseReady(status, selection.phase);
|
|
1755
|
-
if (selection.phase === "remediate" && status.remediationState?.failedEvidenceRevision !== selection.failedEvidenceRevision) {
|
|
1756
|
-
throw new Error("Stale remediation selection");
|
|
1757
|
-
}
|
|
1758
|
-
return { selection, status };
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
function readSddChangeFlag(pi: ExtensionAPI): unknown {
|
|
1762
|
-
try {
|
|
1763
|
-
const value = (pi as unknown as { getFlag?: (name: string) => unknown }).getFlag?.(SDD_CHANGE_FLAG);
|
|
1764
|
-
return value === false ? undefined : value;
|
|
1765
|
-
} catch {
|
|
1766
|
-
return null;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
1639
|
function normalizePolicyPath(value: string): string {
|
|
1771
1640
|
return value.trim().replace(/^~(?=\/|$)/, homedir()).replace(/\\/g, "/").toLowerCase();
|
|
1772
1641
|
}
|
|
@@ -1819,6 +1688,7 @@ function evaluateSensitivePathTool(
|
|
|
1819
1688
|
}
|
|
1820
1689
|
|
|
1821
1690
|
const ASK_USER_CHOICE_BLOCKED_EVENT = "gentle-pi:ask-user-choice:blocked";
|
|
1691
|
+
const ASK_USER_QUESTION_BLOCKED_EVENT = "gentle-pi:ask-user-question:blocked";
|
|
1822
1692
|
|
|
1823
1693
|
const HERDR_BLOCKER_LABEL = {
|
|
1824
1694
|
CHOICE: "Choice awaiting input",
|
|
@@ -1836,17 +1706,20 @@ type HerdrConfirmationLifecycle = {
|
|
|
1836
1706
|
function createHerdrConfirmationLifecycle(events: ExtensionAPI["events"]): HerdrConfirmationLifecycle {
|
|
1837
1707
|
let pending = 0;
|
|
1838
1708
|
let choiceActive = false;
|
|
1839
|
-
let
|
|
1709
|
+
let nativeQuestionnaireActive = false;
|
|
1710
|
+
let legacyQuestionnaireActive = false;
|
|
1840
1711
|
let emittedLabel: HerdrBlockerLabel | undefined;
|
|
1841
1712
|
const emitEffectiveBlocker = (): void => {
|
|
1842
1713
|
const nextLabel = choiceActive
|
|
1843
1714
|
? HERDR_BLOCKER_LABEL.CHOICE
|
|
1844
|
-
:
|
|
1715
|
+
: nativeQuestionnaireActive || legacyQuestionnaireActive
|
|
1845
1716
|
? HERDR_BLOCKER_LABEL.QUESTIONNAIRE
|
|
1846
1717
|
: pending > 0
|
|
1847
1718
|
? HERDR_BLOCKER_LABEL.GUARDED_CONFIRMATION
|
|
1848
1719
|
: undefined;
|
|
1849
|
-
|
|
1720
|
+
// Herdr bridges count active/inactive edges. Keep the first label until
|
|
1721
|
+
// every source releases; relabel activations would leak a blocked count.
|
|
1722
|
+
if ((nextLabel === undefined) === (emittedLabel === undefined)) return;
|
|
1850
1723
|
emittedLabel = nextLabel;
|
|
1851
1724
|
if (nextLabel === undefined) events.emit("herdr:blocked", { active: false });
|
|
1852
1725
|
else events.emit("herdr:blocked", { active: true, label: nextLabel });
|
|
@@ -1858,9 +1731,15 @@ function createHerdrConfirmationLifecycle(events: ExtensionAPI["events"]): Herdr
|
|
|
1858
1731
|
emitEffectiveBlocker();
|
|
1859
1732
|
});
|
|
1860
1733
|
|
|
1734
|
+
events?.on?.(ASK_USER_QUESTION_BLOCKED_EVENT, (event) => {
|
|
1735
|
+
if (!isRecord(event) || typeof event.active !== "boolean" || event.active === nativeQuestionnaireActive) return;
|
|
1736
|
+
nativeQuestionnaireActive = event.active;
|
|
1737
|
+
emitEffectiveBlocker();
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1861
1740
|
events?.on?.("rpiv:ask-user:blocked", (event) => {
|
|
1862
|
-
if (!isRecord(event) || typeof event.active !== "boolean" || event.active ===
|
|
1863
|
-
|
|
1741
|
+
if (!isRecord(event) || typeof event.active !== "boolean" || event.active === legacyQuestionnaireActive) return;
|
|
1742
|
+
legacyQuestionnaireActive = event.active;
|
|
1864
1743
|
emitEffectiveBlocker();
|
|
1865
1744
|
});
|
|
1866
1745
|
|
|
@@ -1877,15 +1756,47 @@ function createHerdrConfirmationLifecycle(events: ExtensionAPI["events"]): Herdr
|
|
|
1877
1756
|
};
|
|
1878
1757
|
}
|
|
1879
1758
|
|
|
1759
|
+
/** Conservative waiver surface: one plain push in the current repository, no shell/wrapper or destination-changing options. */
|
|
1760
|
+
function isOrdinaryYoloPush(command: string, evaluation: GuardEvaluation): boolean {
|
|
1761
|
+
if (evaluation.action !== "confirm" || evaluation.dataLoss || evaluation.matches.length !== 1 || evaluation.key !== "gitPush") return false;
|
|
1762
|
+
return /^git\s+push(?:\s+(?:-u|--set-upstream|[A-Za-z0-9_][A-Za-z0-9_./-]*))*\s*$/.test(command);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/** Preserve explicitly configured confirmations even when legacy env/autonomy resolution ignores them. */
|
|
1766
|
+
function yoloPushConfiguredRestriction(cwd: string, options: LoadGuardrailsOptions = {}): GuardAction | undefined {
|
|
1767
|
+
let restriction: GuardAction | undefined;
|
|
1768
|
+
for (const path of [join(options.gentlePiConfigHome ?? gentleAiConfigHome(), "runtime-guardrails.json"), join(cwd, ".pi", "gentle-ai", "runtime-guardrails.json")]) {
|
|
1769
|
+
try {
|
|
1770
|
+
if (!existsSync(path)) continue;
|
|
1771
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
1772
|
+
if (!isRecord(parsed) || (parsed.guardedCommands !== undefined && !isRecord(parsed.guardedCommands))) {
|
|
1773
|
+
restriction ??= "confirm";
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
const action = isRecord(parsed.guardedCommands) ? parsed.guardedCommands.gitPush : undefined;
|
|
1777
|
+
if (action === "block") return "block";
|
|
1778
|
+
if (action !== undefined && action !== "allow") restriction = "confirm";
|
|
1779
|
+
} catch { restriction ??= "confirm"; }
|
|
1780
|
+
}
|
|
1781
|
+
return restriction;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1880
1784
|
async function confirmCommand(
|
|
1881
1785
|
command: string,
|
|
1882
1786
|
ctx: ExtensionContext,
|
|
1883
1787
|
events: ExtensionAPI["events"],
|
|
1884
1788
|
herdrLifecycle: HerdrConfirmationLifecycle,
|
|
1789
|
+
yoloActive = false,
|
|
1885
1790
|
): Promise<ToolCallEventResult | undefined> {
|
|
1886
1791
|
const guardrailsConfig = loadRuntimeGuardrailsConfig(ctx.cwd);
|
|
1887
1792
|
const evaluation = evaluateGuardedCommand(command, guardrailsConfig);
|
|
1888
|
-
|
|
1793
|
+
// Configuration provenance is distinct from legacy autonomy resolution, which
|
|
1794
|
+
// can ignore confirmations when an environment override is present. Activation
|
|
1795
|
+
// cannot waive a restriction from either user or project configuration layer.
|
|
1796
|
+
const configuredRestriction = yoloActive && evaluation.matches.some((match) => match.key === "gitPush")
|
|
1797
|
+
? yoloPushConfiguredRestriction(ctx.cwd) : undefined;
|
|
1798
|
+
const classification = evaluation.action === "block" || configuredRestriction === "block"
|
|
1799
|
+
? "block" : configuredRestriction ?? evaluation.action;
|
|
1889
1800
|
|
|
1890
1801
|
if (classification === "block") {
|
|
1891
1802
|
return {
|
|
@@ -1900,6 +1811,9 @@ async function confirmCommand(
|
|
|
1900
1811
|
// classification is "allow" or "confirm" from this point on
|
|
1901
1812
|
if (classification === "allow") return undefined;
|
|
1902
1813
|
|
|
1814
|
+
// Full T1 evaluation has already run. YOLO never changes persistent autonomy.
|
|
1815
|
+
if (yoloActive && configuredRestriction === undefined && isOrdinaryYoloPush(command, evaluation)) return undefined;
|
|
1816
|
+
|
|
1903
1817
|
// classification === "confirm"
|
|
1904
1818
|
if (!ctx.hasUI) {
|
|
1905
1819
|
return {
|
|
@@ -1908,7 +1822,9 @@ async function confirmCommand(
|
|
|
1908
1822
|
"Gentle AI safety policy requires interactive confirmation before this command.",
|
|
1909
1823
|
};
|
|
1910
1824
|
}
|
|
1911
|
-
const title =
|
|
1825
|
+
const title = evaluation.dataLoss
|
|
1826
|
+
? "Allow recognized data-loss command?"
|
|
1827
|
+
: guardedCommandTitle(evaluation.key, evaluation.matches);
|
|
1912
1828
|
const preview = guardedCommandPreview(command, evaluation.triggerIndex);
|
|
1913
1829
|
const requestId = randomUUID();
|
|
1914
1830
|
const emitPermissionRequest = (
|
|
@@ -1928,7 +1844,7 @@ async function confirmCommand(
|
|
|
1928
1844
|
emitPermissionRequest("waiting");
|
|
1929
1845
|
herdrLifecycle.begin();
|
|
1930
1846
|
try {
|
|
1931
|
-
approved = await ctx.ui.confirm(title, preview);
|
|
1847
|
+
approved = (await ctx.ui.confirm(title, preview)) === true;
|
|
1932
1848
|
} catch (error) {
|
|
1933
1849
|
confirmationFailed = true;
|
|
1934
1850
|
confirmationError = error;
|
|
@@ -2845,8 +2761,8 @@ interface OverlayComponent {
|
|
|
2845
2761
|
|
|
2846
2762
|
type ModelPanelResult =
|
|
2847
2763
|
| { type: "save"; config: AgentModelConfig }
|
|
2848
|
-
// `save`, then snapshot
|
|
2849
|
-
//
|
|
2764
|
+
// `save`, then snapshot saved agent routing and the live orchestrator into
|
|
2765
|
+
// the current profile without changing the global orchestrator defaults.
|
|
2850
2766
|
| { type: "save-profile"; config: AgentModelConfig }
|
|
2851
2767
|
| { type: "custom"; agent: string | "all"; config: AgentModelConfig }
|
|
2852
2768
|
| { type: "export"; config: AgentModelConfig }
|
|
@@ -2889,6 +2805,8 @@ class SddModelPanel implements OverlayComponent {
|
|
|
2889
2805
|
private readonly theme: Theme | undefined;
|
|
2890
2806
|
// The profile `u` writes to, named up front so the key never targets a surprise.
|
|
2891
2807
|
private readonly profileLabel: string;
|
|
2808
|
+
// Terminal rows, so the card fills the fullscreen overlay like `/gentle:profiles`.
|
|
2809
|
+
private readonly terminalRows: (() => number) | undefined;
|
|
2892
2810
|
|
|
2893
2811
|
constructor(
|
|
2894
2812
|
initialConfig: AgentModelConfig,
|
|
@@ -2897,6 +2815,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
2897
2815
|
done: (result: ModelPanelResult) => void,
|
|
2898
2816
|
theme?: Theme,
|
|
2899
2817
|
profileLabel = "none",
|
|
2818
|
+
terminalRows?: () => number,
|
|
2900
2819
|
) {
|
|
2901
2820
|
this.draft = cloneModelConfig(initialConfig);
|
|
2902
2821
|
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
@@ -2904,6 +2823,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
2904
2823
|
this.done = done;
|
|
2905
2824
|
this.theme = theme;
|
|
2906
2825
|
this.profileLabel = profileLabel;
|
|
2826
|
+
this.terminalRows = terminalRows;
|
|
2907
2827
|
}
|
|
2908
2828
|
|
|
2909
2829
|
invalidate(): void {}
|
|
@@ -2931,10 +2851,22 @@ class SddModelPanel implements OverlayComponent {
|
|
|
2931
2851
|
return this.renderCard(lines, width);
|
|
2932
2852
|
}
|
|
2933
2853
|
|
|
2934
|
-
|
|
2854
|
+
// In the fullscreen overlay a list owns every row its chrome leaves free;
|
|
2855
|
+
// without a terminal height it keeps the fixed window.
|
|
2856
|
+
private visibleListRows(fixedRows: number, chromeRows: number): number {
|
|
2857
|
+
if (!this.terminalRows) return fixedRows;
|
|
2858
|
+
return Math.max(1, Math.floor(this.terminalRows()) - chromeRows);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
private renderCard(body: string[], width: number): string[] {
|
|
2862
|
+
let lines = body;
|
|
2935
2863
|
const innerWidth = Math.max(1, width - 4);
|
|
2936
2864
|
const horizontal = "─".repeat(innerWidth + 2);
|
|
2937
2865
|
const border = (text: string) => this.renderText(text, "border");
|
|
2866
|
+
const bodyRows = this.terminalRows ? Math.floor(this.terminalRows()) - 2 : 0;
|
|
2867
|
+
if (lines.length < bodyRows) {
|
|
2868
|
+
lines = [...lines, ...Array<string>(bodyRows - lines.length).fill("")];
|
|
2869
|
+
}
|
|
2938
2870
|
return [
|
|
2939
2871
|
border(`╭${horizontal}╮`),
|
|
2940
2872
|
...lines.map(
|
|
@@ -3166,7 +3098,10 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3166
3098
|
lines.push("");
|
|
3167
3099
|
lines.push(line("Current assignments:", "muted"));
|
|
3168
3100
|
lines.push("");
|
|
3169
|
-
const visibleRows = Math.min(
|
|
3101
|
+
const visibleRows = Math.min(
|
|
3102
|
+
this.visibleListRows(AGENT_LIST_MAX_VISIBLE_ROWS, AGENT_LIST_CHROME_ROWS),
|
|
3103
|
+
this.rows.length,
|
|
3104
|
+
);
|
|
3170
3105
|
const listCursor = Math.min(this.cursor, this.rows.length - 1);
|
|
3171
3106
|
const start = Math.max(
|
|
3172
3107
|
0,
|
|
@@ -3212,7 +3147,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3212
3147
|
);
|
|
3213
3148
|
lines.push(
|
|
3214
3149
|
line(
|
|
3215
|
-
`x export • r restore • ctrl+s save • u
|
|
3150
|
+
`x export • r restore • ctrl+s save • u capture session in "${this.profileLabel}" • esc back`,
|
|
3216
3151
|
"muted",
|
|
3217
3152
|
),
|
|
3218
3153
|
);
|
|
@@ -3232,14 +3167,15 @@ class SddModelPanel implements OverlayComponent {
|
|
|
3232
3167
|
`${this.renderText("◎", "accent")} ${this.renderText(this.query || "search...", "muted")}`,
|
|
3233
3168
|
);
|
|
3234
3169
|
lines.push("");
|
|
3170
|
+
const visibleRows = this.visibleListRows(MODEL_LIST_MAX_VISIBLE_ROWS, MODEL_LIST_CHROME_ROWS);
|
|
3235
3171
|
const start = Math.max(
|
|
3236
3172
|
0,
|
|
3237
3173
|
Math.min(
|
|
3238
|
-
this.modelCursor - Math.floor(
|
|
3239
|
-
Math.max(0, options.length -
|
|
3174
|
+
this.modelCursor - Math.floor(visibleRows / 2),
|
|
3175
|
+
Math.max(0, options.length - visibleRows),
|
|
3240
3176
|
),
|
|
3241
3177
|
);
|
|
3242
|
-
const end = Math.min(options.length, start +
|
|
3178
|
+
const end = Math.min(options.length, start + visibleRows);
|
|
3243
3179
|
for (let i = start; i < end; i++) {
|
|
3244
3180
|
const focused = i === this.modelCursor;
|
|
3245
3181
|
lines.push(
|
|
@@ -3347,10 +3283,20 @@ function renderSddModelPanelForTesting(
|
|
|
3347
3283
|
agents: string[],
|
|
3348
3284
|
width: number,
|
|
3349
3285
|
theme?: Theme,
|
|
3286
|
+
terminalRows?: number,
|
|
3287
|
+
inputs: string[] = [],
|
|
3350
3288
|
): string[] {
|
|
3351
|
-
|
|
3352
|
-
|
|
3289
|
+
const panel = new SddModelPanel(
|
|
3290
|
+
initialConfig,
|
|
3291
|
+
modelOptions,
|
|
3292
|
+
agents,
|
|
3293
|
+
() => {},
|
|
3294
|
+
theme,
|
|
3295
|
+
undefined,
|
|
3296
|
+
terminalRows === undefined ? undefined : () => terminalRows,
|
|
3353
3297
|
);
|
|
3298
|
+
for (const data of inputs) panel.handleInput(data);
|
|
3299
|
+
return panel.render(width);
|
|
3354
3300
|
}
|
|
3355
3301
|
|
|
3356
3302
|
async function showSddModelPanel(
|
|
@@ -3361,21 +3307,24 @@ async function showSddModelPanel(
|
|
|
3361
3307
|
const modelOptions = await getPiModelOptions(ctx);
|
|
3362
3308
|
const agents = modelAssignmentNames(ctx.cwd);
|
|
3363
3309
|
return ctx.ui.custom<ModelPanelResult>(
|
|
3364
|
-
(
|
|
3365
|
-
new SddModelPanel(config, modelOptions, agents, done, theme, profileLabel)
|
|
3310
|
+
(tui, theme, _keybindings, done) =>
|
|
3311
|
+
new SddModelPanel(config, modelOptions, agents, done, theme, profileLabel, () =>
|
|
3312
|
+
Math.max(0, tui.terminal.rows),
|
|
3313
|
+
),
|
|
3366
3314
|
{
|
|
3367
3315
|
overlay: true,
|
|
3316
|
+
// Same fullscreen dimensions as the `/gentle:profiles` panel.
|
|
3368
3317
|
overlayOptions: {
|
|
3369
3318
|
anchor: "center",
|
|
3370
|
-
width: "
|
|
3371
|
-
|
|
3372
|
-
|
|
3319
|
+
width: "100%",
|
|
3320
|
+
maxHeight: "100%",
|
|
3321
|
+
margin: 0,
|
|
3373
3322
|
},
|
|
3374
3323
|
},
|
|
3375
3324
|
);
|
|
3376
3325
|
}
|
|
3377
3326
|
|
|
3378
|
-
async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
3327
|
+
async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
3379
3328
|
migrateLegacyProjectModelOverrides(ctx.cwd);
|
|
3380
3329
|
// A pinned repository resolves its subagent routing from the profile at launch,
|
|
3381
3330
|
// so global routing written here will not reach its subagents. Saying it before
|
|
@@ -3497,7 +3446,7 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
3497
3446
|
].join("\n"),
|
|
3498
3447
|
"info",
|
|
3499
3448
|
);
|
|
3500
|
-
if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx);
|
|
3449
|
+
if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx, pi);
|
|
3501
3450
|
}
|
|
3502
3451
|
|
|
3503
3452
|
/** The profile `u` in `/gentle:models` writes to, and why it is that one. */
|
|
@@ -3528,13 +3477,12 @@ function describeCurrentProfileTarget(target: CurrentProfileTarget | undefined):
|
|
|
3528
3477
|
}
|
|
3529
3478
|
|
|
3530
3479
|
/**
|
|
3531
|
-
* The second half of `u
|
|
3532
|
-
*
|
|
3533
|
-
*
|
|
3534
|
-
* pinned profile onto itself. The global save has already been reported, so every
|
|
3480
|
+
* The second half of `u` captures saved global agent routing (never the pin)
|
|
3481
|
+
* and the live session orchestrator. Without a live model it falls back to
|
|
3482
|
+
* persisted settings. The global save has already been reported, so every
|
|
3535
3483
|
* outcome here speaks only about the profile.
|
|
3536
3484
|
*/
|
|
3537
|
-
function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext): void {
|
|
3485
|
+
function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, pi: ExtensionAPI): void {
|
|
3538
3486
|
const path = profilesFilePath(gentleAiConfigHome());
|
|
3539
3487
|
const read = readProfilesFileResult(path);
|
|
3540
3488
|
if (read.status === "invalid") {
|
|
@@ -3548,6 +3496,12 @@ function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext): void {
|
|
|
3548
3496
|
readGlobalEffectiveModelConfig(ctx.cwd),
|
|
3549
3497
|
readOrchestratorSettings(orchestratorSettingsPath()),
|
|
3550
3498
|
);
|
|
3499
|
+
if (ctx.model) {
|
|
3500
|
+
snapshot[PROFILE_ORCHESTRATOR_KEY] = {
|
|
3501
|
+
model: `${ctx.model.provider}/${ctx.model.id}`,
|
|
3502
|
+
thinking: pi.getThinkingLevel(),
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3551
3505
|
if (read.status === "missing") {
|
|
3552
3506
|
// The same seed `/gentle:profiles` performs on its first open, so pressing
|
|
3553
3507
|
// `u` before ever opening that panel lands on the same "current" profile.
|
|
@@ -3604,6 +3558,16 @@ type ProfilesPanelResult =
|
|
|
3604
3558
|
|
|
3605
3559
|
type ProfilesSnapshotHandler = (name: string) => AgentProfilesFile;
|
|
3606
3560
|
|
|
3561
|
+
/**
|
|
3562
|
+
* What a panel action hands the reopened panel: a one-line status for the footer,
|
|
3563
|
+
* because notifications stay hidden behind the fullscreen overlay until it closes,
|
|
3564
|
+
* and the profile to select when the action created or renamed one.
|
|
3565
|
+
*/
|
|
3566
|
+
interface ProfilesPanelReport {
|
|
3567
|
+
status?: string;
|
|
3568
|
+
selectedName?: string;
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3607
3571
|
const PROFILES_PANEL_MIN_BODY_ROWS = 6;
|
|
3608
3572
|
|
|
3609
3573
|
type ProfilesPanelPointerLayout = AgentsViewLayout & { listTop: number };
|
|
@@ -3782,8 +3746,10 @@ class ProfilesPanel implements OverlayComponent {
|
|
|
3782
3746
|
saveSnapshot: ProfilesSnapshotHandler,
|
|
3783
3747
|
requestRender: () => void,
|
|
3784
3748
|
pinStatus: () => ProfilePinStatus | undefined,
|
|
3749
|
+
feedback?: string,
|
|
3785
3750
|
) {
|
|
3786
3751
|
this.file = file;
|
|
3752
|
+
this.feedback = feedback;
|
|
3787
3753
|
this.currentConfig = currentConfig;
|
|
3788
3754
|
this.done = done;
|
|
3789
3755
|
this.saveSnapshot = saveSnapshot;
|
|
@@ -4078,6 +4044,7 @@ async function showProfilesPanel(
|
|
|
4078
4044
|
currentConfig: AgentModelConfig,
|
|
4079
4045
|
selectedName: string | undefined,
|
|
4080
4046
|
saveSnapshot: ProfilesSnapshotHandler,
|
|
4047
|
+
status?: string,
|
|
4081
4048
|
): Promise<ProfilesPanelResult> {
|
|
4082
4049
|
// Both orchestrator and pin state are snapshots for this panel visit.
|
|
4083
4050
|
// Actions (including p/P) reopen the panel and read fresh state.
|
|
@@ -4096,6 +4063,7 @@ async function showProfilesPanel(
|
|
|
4096
4063
|
saveSnapshot,
|
|
4097
4064
|
() => tui.requestRender(),
|
|
4098
4065
|
() => readProfilePinStatus(ctx.cwd),
|
|
4066
|
+
status,
|
|
4099
4067
|
);
|
|
4100
4068
|
const container = createNativeFullscreenInteraction({
|
|
4101
4069
|
keyboardTarget: panel,
|
|
@@ -4198,6 +4166,7 @@ async function runProfilesPanelAction(
|
|
|
4198
4166
|
path: string,
|
|
4199
4167
|
file: AgentProfilesFile,
|
|
4200
4168
|
result: Exclude<ProfilesPanelResult, { type: "close" }>,
|
|
4169
|
+
report: ProfilesPanelReport = {},
|
|
4201
4170
|
): Promise<AgentProfilesFile> {
|
|
4202
4171
|
switch (result.type) {
|
|
4203
4172
|
case "apply": {
|
|
@@ -4359,14 +4328,25 @@ async function runProfilesPanelAction(
|
|
|
4359
4328
|
return claimed;
|
|
4360
4329
|
}
|
|
4361
4330
|
case "create": {
|
|
4362
|
-
const
|
|
4363
|
-
if (
|
|
4331
|
+
const answer = await ctx.ui.input("New profile name", "e.g. deep-work");
|
|
4332
|
+
if (answer === undefined) {
|
|
4333
|
+
report.status = "Create cancelled.";
|
|
4334
|
+
return file;
|
|
4335
|
+
}
|
|
4336
|
+
const name = answer.trim();
|
|
4337
|
+
if (name === "") {
|
|
4338
|
+
report.status = "No profile created: no name entered.";
|
|
4339
|
+
return file;
|
|
4340
|
+
}
|
|
4364
4341
|
try {
|
|
4365
|
-
const next = createProfile(file, name
|
|
4342
|
+
const next = createProfile(file, name, {});
|
|
4366
4343
|
writeProfilesFileSync(path, next);
|
|
4344
|
+
report.status = `Profile "${name}" created.`;
|
|
4345
|
+
report.selectedName = name;
|
|
4367
4346
|
return next;
|
|
4368
4347
|
} catch (error) {
|
|
4369
|
-
|
|
4348
|
+
report.status = `Profile not created: ${profilesErrorMessage(error)}`;
|
|
4349
|
+
ctx.ui.notify(report.status, "warning");
|
|
4370
4350
|
return file;
|
|
4371
4351
|
}
|
|
4372
4352
|
}
|
|
@@ -4393,36 +4373,57 @@ async function runProfilesPanelAction(
|
|
|
4393
4373
|
}
|
|
4394
4374
|
}
|
|
4395
4375
|
case "duplicate": {
|
|
4396
|
-
|
|
4397
|
-
|
|
4376
|
+
// The Pi host starts the field empty and ignores the placeholder, so the title
|
|
4377
|
+
// names the suggestion and Enter on an untouched field accepts it.
|
|
4378
|
+
const suggested = `${result.name}-copy`;
|
|
4379
|
+
const answer = await ctx.ui.input(`Duplicate profile "${result.name}" as (empty = ${suggested})`, suggested);
|
|
4380
|
+
if (answer === undefined) {
|
|
4381
|
+
report.status = "Duplicate cancelled.";
|
|
4382
|
+
return file;
|
|
4383
|
+
}
|
|
4384
|
+
const name = answer.trim() || suggested;
|
|
4398
4385
|
try {
|
|
4399
|
-
const next = duplicateProfile(file, result.name, name
|
|
4386
|
+
const next = duplicateProfile(file, result.name, name);
|
|
4400
4387
|
writeProfilesFileSync(path, next);
|
|
4388
|
+
report.status = `Profile "${result.name}" duplicated as "${name}".`;
|
|
4389
|
+
report.selectedName = name;
|
|
4401
4390
|
return next;
|
|
4402
4391
|
} catch (error) {
|
|
4403
|
-
|
|
4392
|
+
report.status = `Profile not duplicated: ${profilesErrorMessage(error)}`;
|
|
4393
|
+
ctx.ui.notify(report.status, "warning");
|
|
4404
4394
|
return file;
|
|
4405
4395
|
}
|
|
4406
4396
|
}
|
|
4407
4397
|
case "rename": {
|
|
4408
|
-
const
|
|
4409
|
-
if (
|
|
4398
|
+
const answer = await ctx.ui.input(`Rename profile "${result.name}" to (empty = keep ${result.name})`, result.name);
|
|
4399
|
+
if (answer === undefined) {
|
|
4400
|
+
report.status = "Rename cancelled.";
|
|
4401
|
+
return file;
|
|
4402
|
+
}
|
|
4403
|
+
const name = answer.trim();
|
|
4404
|
+
if (name === "") {
|
|
4405
|
+
report.status = `Profile "${result.name}" unchanged: no new name entered.`;
|
|
4406
|
+
return file;
|
|
4407
|
+
}
|
|
4410
4408
|
try {
|
|
4411
|
-
const next = renameProfile(file, result.name, name
|
|
4409
|
+
const next = renameProfile(file, result.name, name);
|
|
4412
4410
|
writeProfilesFileSync(path, next);
|
|
4413
4411
|
// A pin stores a name, so a rename that did not follow it would leave every
|
|
4414
4412
|
// pinned repository with a name the store no longer defines, which silently
|
|
4415
4413
|
// returns those repositories to the global routing.
|
|
4416
|
-
const follow = followRenamedPin(ctx.cwd, result.name, name
|
|
4414
|
+
const follow = followRenamedPin(ctx.cwd, result.name, name);
|
|
4417
4415
|
ctx.ui.notify(
|
|
4418
|
-
`el Gentleman renamed profile "${result.name}" to "${name
|
|
4416
|
+
`el Gentleman renamed profile "${result.name}" to "${name}".` +
|
|
4419
4417
|
(follow.followed.length > 0 ? `\nUpdated the clone pin: ${follow.followed.map((entry) => sanitizeTerminalText(entry)).join(", ")}.` : "") +
|
|
4420
|
-
(follow.stillDeclared.length > 0 ? `\nThe committed repository declaration ${follow.stillDeclared.map((entry) => sanitizeTerminalText(entry)).join(", ")} still names "${result.name}"; press P on "${name
|
|
4418
|
+
(follow.stillDeclared.length > 0 ? `\nThe committed repository declaration ${follow.stillDeclared.map((entry) => sanitizeTerminalText(entry)).join(", ")} still names "${result.name}"; press P on "${name}" to republish it.` : ""),
|
|
4421
4419
|
"info",
|
|
4422
4420
|
);
|
|
4421
|
+
report.status = `Profile "${result.name}" renamed to "${name}".`;
|
|
4422
|
+
report.selectedName = name;
|
|
4423
4423
|
return next;
|
|
4424
4424
|
} catch (error) {
|
|
4425
|
-
|
|
4425
|
+
report.status = `Profile not renamed: ${profilesErrorMessage(error)}`;
|
|
4426
|
+
ctx.ui.notify(report.status, "warning");
|
|
4426
4427
|
return file;
|
|
4427
4428
|
}
|
|
4428
4429
|
}
|
|
@@ -4635,14 +4636,16 @@ async function handleProfilesCommand(ctx: ExtensionContext, live: LiveSession):
|
|
|
4635
4636
|
saveSnapshot,
|
|
4636
4637
|
);
|
|
4637
4638
|
while (result.type !== "close") {
|
|
4638
|
-
|
|
4639
|
-
file = await runProfilesPanelAction(ctx, live, path, file, result);
|
|
4639
|
+
const report: ProfilesPanelReport = {};
|
|
4640
|
+
file = await runProfilesPanelAction(ctx, live, path, file, result, report);
|
|
4641
|
+
selectedName = report.selectedName ?? ("name" in result ? result.name : undefined);
|
|
4640
4642
|
result = await showProfilesPanel(
|
|
4641
4643
|
ctx,
|
|
4642
4644
|
file,
|
|
4643
4645
|
await readEffectiveModelConfigAsync(ctx.cwd),
|
|
4644
4646
|
selectedName,
|
|
4645
4647
|
saveSnapshot,
|
|
4648
|
+
report.status,
|
|
4646
4649
|
);
|
|
4647
4650
|
}
|
|
4648
4651
|
}
|
|
@@ -4740,7 +4743,7 @@ const REVIEW_CONTROLLER_PARAMETERS = {
|
|
|
4740
4743
|
},
|
|
4741
4744
|
input: {
|
|
4742
4745
|
type: "string",
|
|
4743
|
-
description: "A JSON-serialized object string, not a nested object. New native ordinary START uses {\"mode\":\"ordinary\"}; answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668)
|
|
4746
|
+
description: "A JSON-serialized object string, not a nested object. New native ordinary START uses {\"mode\":\"ordinary\"}; answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668/#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). A missing model, a mini model token (gemini is not mini), or low effort keeps the conservative small-model bias. 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. A declined, unavailable, or unknown outcome falls back to the exact risk-gated plan ASSESS returns when RDD is off, re-enabling the separate verifier; 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.",
|
|
4744
4747
|
},
|
|
4745
4748
|
outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
4746
4749
|
inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
|
|
@@ -4748,7 +4751,7 @@ const REVIEW_CONTROLLER_PARAMETERS = {
|
|
|
4748
4751
|
lineageIds: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
4749
4752
|
workspaceRoot: {
|
|
4750
4753
|
type: "string",
|
|
4751
|
-
description: "Optional explicit user-authorized absolute path
|
|
4754
|
+
description: "Optional explicit user-authorized absolute path to an existing directory that owns this review. Nested Git paths are canonicalized to their worktree root. Only inspect or new ordinary START with validated RDD on may ask native Gentle AI to prepare a non-Git directory; Pi never invents this selector. Absent, the session cwd is used unless one unambiguous lineage binding already identifies its target root.",
|
|
4752
4755
|
},
|
|
4753
4756
|
},
|
|
4754
4757
|
} as const;
|
|
@@ -5056,6 +5059,11 @@ function parseControllerJson(input: string, operation: ReviewControllerOperation
|
|
|
5056
5059
|
return value;
|
|
5057
5060
|
}
|
|
5058
5061
|
|
|
5062
|
+
// Explicit cancellation uses the AbortError convention so observers can tell it from failure.
|
|
5063
|
+
function reviewCancellation(message: string): Error {
|
|
5064
|
+
return Object.assign(new Error(message), { name: "AbortError" });
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5059
5067
|
async function authorizeDestructiveReviewOperation(
|
|
5060
5068
|
parametersValue: unknown,
|
|
5061
5069
|
ctx: ExtensionContext,
|
|
@@ -5216,6 +5224,26 @@ function nativeReviewModeSkipped(operation: ReviewControllerOperation, source: N
|
|
|
5216
5224
|
};
|
|
5217
5225
|
}
|
|
5218
5226
|
|
|
5227
|
+
// The legacy START compatibility gate is deliberately permissive when mode is
|
|
5228
|
+
// unavailable. It is not authorization to enter bootstrap-capable native code.
|
|
5229
|
+
async function nativeRepositoryPreparationEnabled(nativeReviewCli: NativeReviewCli | null, cwd: string, signal?: AbortSignal): Promise<boolean> {
|
|
5230
|
+
if (nativeReviewCli?.reviewMode === undefined) return false;
|
|
5231
|
+
try {
|
|
5232
|
+
const mode = await nativeReviewCli.reviewMode({ cwd, operation: NATIVE_REVIEW_MODE_OPERATION.STATUS, ...(signal === undefined ? {} : { signal }) });
|
|
5233
|
+
return mode.operation === NATIVE_REVIEW_MODE_OPERATION.STATUS && isValidRddModeStatus(mode.status) && mode.status.effective === "on";
|
|
5234
|
+
} catch {
|
|
5235
|
+
return false;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
function nativeRepositoryPreparationBlocked(operation: ReviewControllerOperation): Record<string, unknown> {
|
|
5240
|
+
return {
|
|
5241
|
+
operation, status: "blocked", outcome: "native-repository-bootstrap-not-authorized",
|
|
5242
|
+
reason: "A non-Git target requires successfully decoded effective RDD on before native repository preparation. The user owns the RDD switch; Pi does not enable it.",
|
|
5243
|
+
mutation_performed: false, mutation_outcome: "none", lineage_created: false,
|
|
5244
|
+
};
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5219
5247
|
async function resolveReviewModeGate(
|
|
5220
5248
|
nativeReviewCli: NativeReviewCli | null,
|
|
5221
5249
|
operation: ReviewControllerOperation,
|
|
@@ -5728,14 +5756,31 @@ interface RetainedNativeUntrackedSelection {
|
|
|
5728
5756
|
readonly submission?: NativeIntendedUntrackedSelectionSubmission;
|
|
5729
5757
|
}
|
|
5730
5758
|
|
|
5731
|
-
|
|
5759
|
+
// The untracked-selection fields are optional here, unlike the lineage-scoped
|
|
5760
|
+
// RetainedNativeUntrackedSelection above: a plain inspect that resolves ready
|
|
5761
|
+
// without ever needing an untrackedScope decision still retains its own
|
|
5762
|
+
// committed-range selector alone, bound only to targetIdentity/candidateTree.
|
|
5763
|
+
interface RetainedPreLineageNativeUntrackedSelection {
|
|
5764
|
+
readonly untrackedScope?: NativeStartUntrackedScope;
|
|
5765
|
+
readonly expectedUntrackedInventory?: string;
|
|
5766
|
+
readonly intendedUntracked?: readonly string[];
|
|
5767
|
+
readonly submission?: NativeIntendedUntrackedSelectionSubmission;
|
|
5732
5768
|
readonly targetIdentity: string;
|
|
5733
5769
|
readonly candidateTree: string;
|
|
5770
|
+
// gentle-pi#1192: only set when the inspect that produced this entry was a
|
|
5771
|
+
// committed-range inspect, so a later plain START can replay the same
|
|
5772
|
+
// selector instead of silently falling back to the workspace-diff range.
|
|
5773
|
+
readonly baseRef?: string;
|
|
5774
|
+
readonly committedOnly?: true;
|
|
5734
5775
|
}
|
|
5735
5776
|
|
|
5736
5777
|
interface RetainedNativeCaptureRoute { readonly workspaceRoot: string; readonly lineageId: string; readonly baseRef?: string; readonly committedOnly?: true; }
|
|
5737
5778
|
|
|
5738
|
-
|
|
5779
|
+
// An inspect that stops before submission has no resolved untracked selection
|
|
5780
|
+
// yet. Keep only its selector, bound to the exact provider collect input.
|
|
5781
|
+
interface RetainedNativeUntrackedStopSelector { readonly selectionBinding: string; readonly targetIdentity: string; readonly baseRef: string; readonly committedOnly: true; }
|
|
5782
|
+
|
|
5783
|
+
type RetainedNativeStatusSelection = RetainedNativeUntrackedSelection | RetainedPreLineageNativeUntrackedSelection | RetainedNativeCaptureRoute | RetainedNativeUntrackedStopSelector;
|
|
5739
5784
|
|
|
5740
5785
|
const MAX_RETAINED_NATIVE_STATUS_SELECTIONS = 64;
|
|
5741
5786
|
class NativeCaptureRouteRegistrationError extends Error {}
|
|
@@ -5790,7 +5835,7 @@ function nativeStartRejection(reason: string, field?: string): Record<string, un
|
|
|
5790
5835
|
? "native-start-committed-only-required"
|
|
5791
5836
|
: reason === "committed-only-invalid"
|
|
5792
5837
|
? "native-start-committed-only-invalid"
|
|
5793
|
-
: reason === "unknown-field" || reason === "focus-invalid" || reason === "untracked-selection-invalid"
|
|
5838
|
+
: reason === "unknown-field" || reason === "focus-invalid" || reason === "untracked-selection-invalid" || reason === "provider-base-offer-invalid" || reason === "provider-base-tree-mismatch"
|
|
5794
5839
|
? "native-start-input-invalid"
|
|
5795
5840
|
: "native-start-policy-path-invalid",
|
|
5796
5841
|
reason,
|
|
@@ -5946,7 +5991,7 @@ export class PendingReviewConsentRegistry {
|
|
|
5946
5991
|
const processPendingReviewConsentRegistry = new PendingReviewConsentRegistry();
|
|
5947
5992
|
const processRetainedNativeStatusSelections = new Map<PendingReviewConsentSessionKey, Map<string, RetainedNativeStatusSelection>>();
|
|
5948
5993
|
|
|
5949
|
-
// gentle-pi#556 / gentle-ai#4051: nesting depth of named-agent (
|
|
5994
|
+
// gentle-pi#556 / gentle-ai#4051: nesting depth of named-agent (
|
|
5950
5995
|
// executor or other subagent) starts vs. ends for a session. Starts and
|
|
5951
5996
|
// ends are paired so a subagent's own loop end never leaves the primary
|
|
5952
5997
|
// loop's `agent_end` preflight suppressed for the rest of the session: a
|
|
@@ -6152,18 +6197,26 @@ function consentBindingRepositoryMismatchOutcome(operation: ReviewControllerOper
|
|
|
6152
6197
|
// wants, so a plain START can adopt the route the provider just rendered
|
|
6153
6198
|
// instead of making the caller hand-copy `base-ref` into its input. Only the
|
|
6154
6199
|
// provider's own offered argument is read -- never a guess, a persisted value,
|
|
6155
|
-
// or a stale transition
|
|
6156
|
-
//
|
|
6157
|
-
//
|
|
6200
|
+
// or a stale transition. Full object ids paired with committed-only may name
|
|
6201
|
+
// commits or trees; only provider trees enter the separate frozen tree-base
|
|
6202
|
+
// path. Caller-supplied tree selectors remain invalid.
|
|
6203
|
+
// The worktree anchor is always a commit, but a provider-offered range can
|
|
6204
|
+
// have a tree selector. Transport and lineage replay must use the latter.
|
|
6205
|
+
function nativeCommittedRangeSelector(target: { baseCommit: string; providerBaseTree?: string }): string {
|
|
6206
|
+
return target.providerBaseTree ?? target.baseCommit;
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6158
6209
|
const OFFERED_COMMITTED_RANGE_BASE_REF = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
6159
6210
|
|
|
6160
6211
|
function offeredCommittedRangeBaseRef(target: ReviewStatusV3): string | undefined {
|
|
6161
6212
|
const execute = target.nextTransition?.kind === "execute" ? target.nextTransition.execute : undefined;
|
|
6162
6213
|
if (execute?.operation !== "review.start") return undefined;
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
if (
|
|
6166
|
-
|
|
6214
|
+
const selectors = execute.arguments.filter((argument) => argument.name === "base-ref" || argument.name === "committed-only");
|
|
6215
|
+
if (selectors.length !== 2 || new Set(selectors.map((argument) => argument.name)).size !== 2) return undefined;
|
|
6216
|
+
if (selectors.some((argument) => argument.token !== undefined && argument.token !== `--${argument.name}=${argument.value}`)) return undefined;
|
|
6217
|
+
const baseRef = selectors.find((argument) => argument.name === "base-ref")!.value;
|
|
6218
|
+
if (!OFFERED_COMMITTED_RANGE_BASE_REF.test(baseRef)) return undefined;
|
|
6219
|
+
if (selectors.find((argument) => argument.name === "committed-only")!.value !== "true") return undefined;
|
|
6167
6220
|
return baseRef;
|
|
6168
6221
|
}
|
|
6169
6222
|
|
|
@@ -6408,8 +6461,11 @@ async function reconcileNativeMutationFailure(
|
|
|
6408
6461
|
}
|
|
6409
6462
|
|
|
6410
6463
|
function reviewWorkspaceGitIdentity(cwd: string): { toplevel: string; commonDir: string } {
|
|
6464
|
+
// Discovery must describe the selected directory, never an ambient Git
|
|
6465
|
+
// override pointing at a different repository or worktree.
|
|
6466
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_")));
|
|
6411
6467
|
const git = (...arguments_: string[]): string =>
|
|
6412
|
-
execFileSync("git", arguments_, { cwd, encoding: "utf8", windowsHide: true }).trim();
|
|
6468
|
+
execFileSync("git", arguments_, { cwd, env, encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6413
6469
|
const toplevel = realpathSync(git("rev-parse", "--show-toplevel"));
|
|
6414
6470
|
const commonDir = realpathSync(resolve(cwd, git("rev-parse", "--git-common-dir")));
|
|
6415
6471
|
return { toplevel, commonDir };
|
|
@@ -6418,14 +6474,18 @@ function reviewWorkspaceGitIdentity(cwd: string): { toplevel: string; commonDir:
|
|
|
6418
6474
|
/**
|
|
6419
6475
|
* Resolves the explicit user-authorized workspace target. An explicit path may
|
|
6420
6476
|
* be nested and may belong to a repository unrelated to the Pi session cwd;
|
|
6421
|
-
* Git resolves it to its canonical worktree top-level.
|
|
6422
|
-
*
|
|
6477
|
+
* Git resolves it to its canonical worktree top-level. Only unbound inspect or
|
|
6478
|
+
* ordinary START may retain an existing unresolved directory for a later strict
|
|
6479
|
+
* effective-on gate; native, not this resolver, owns preparation and safeguards.
|
|
6480
|
+
* The session cwd is the default when no target was selected or remembered.
|
|
6423
6481
|
*/
|
|
6424
6482
|
function resolveReviewControllerWorkspaceRoot(
|
|
6425
6483
|
requested: string | undefined,
|
|
6426
6484
|
sessionCwd: string,
|
|
6427
6485
|
candidateViews: CandidateViewRegistry | null,
|
|
6428
6486
|
lineageId: string | undefined,
|
|
6487
|
+
allowNativePreparation = false,
|
|
6488
|
+
resolution?: { identity?: { toplevel: string; commonDir: string } },
|
|
6429
6489
|
): string {
|
|
6430
6490
|
const remembered = requested === undefined && lineageId !== undefined
|
|
6431
6491
|
? candidateViews?.resolveWorkspaceRoot(lineageId)
|
|
@@ -6445,10 +6505,11 @@ function resolveReviewControllerWorkspaceRoot(
|
|
|
6445
6505
|
try {
|
|
6446
6506
|
target = reviewWorkspaceGitIdentity(resolved);
|
|
6447
6507
|
} catch {
|
|
6448
|
-
if (
|
|
6508
|
+
if (allowNativePreparation && lineageId === undefined && safeBootstrapDirectory(resolved) === resolved) return resolved;
|
|
6449
6509
|
throw new Error(`Review controller workspaceRoot ${resolved} is not inside a Git worktree; review operations bind only to real worktrees of the session repository`);
|
|
6450
6510
|
}
|
|
6451
6511
|
if (lineageId !== undefined) candidateViews?.assertWorkspaceRoot(lineageId, target.toplevel);
|
|
6512
|
+
if (resolution) resolution.identity = target;
|
|
6452
6513
|
return target.toplevel;
|
|
6453
6514
|
}
|
|
6454
6515
|
|
|
@@ -6486,7 +6547,7 @@ function retainNativeUntrackedSelection(selections: Map<string, RetainedNativeSt
|
|
|
6486
6547
|
|
|
6487
6548
|
function readRetainedNativeUntrackedSelection(selections: Map<string, RetainedNativeStatusSelection>, workspaceRoot: string, lineageId: string): NativeStartUntrackedSelection {
|
|
6488
6549
|
const selection = selections.get(reviewLifecycleStorageKey(workspaceRoot, lineageId));
|
|
6489
|
-
return selection === undefined || "baseRef" in selection
|
|
6550
|
+
return selection === undefined || "baseRef" in selection || "selectionBinding" in selection
|
|
6490
6551
|
? {}
|
|
6491
6552
|
: {
|
|
6492
6553
|
untrackedScope: selection.untrackedScope,
|
|
@@ -6512,8 +6573,12 @@ function readRetainedPreLineageNativeUntrackedSelection(
|
|
|
6512
6573
|
workspaceRoot: string,
|
|
6513
6574
|
): RetainedPreLineageNativeUntrackedSelection | undefined {
|
|
6514
6575
|
const selection = selections.get(reviewLifecycleStorageKey(workspaceRoot, ""));
|
|
6576
|
+
// gentle-pi#1192: this entry may now carry its own optional baseRef, so the
|
|
6577
|
+
// discriminant against RetainedNativeCaptureRoute (a distinct key namespace
|
|
6578
|
+
// that always carries workspaceRoot/lineageId) can no longer be "no baseRef".
|
|
6515
6579
|
return selection !== undefined &&
|
|
6516
|
-
!(
|
|
6580
|
+
!isRetainedNativeCaptureRoute(selection) &&
|
|
6581
|
+
!("selectionBinding" in selection) &&
|
|
6517
6582
|
typeof (selection as Partial<RetainedPreLineageNativeUntrackedSelection>).targetIdentity === "string" &&
|
|
6518
6583
|
typeof (selection as Partial<RetainedPreLineageNativeUntrackedSelection>).candidateTree === "string"
|
|
6519
6584
|
? selection as RetainedPreLineageNativeUntrackedSelection
|
|
@@ -6536,7 +6601,7 @@ function isRetainedNativeCaptureRoute(selection: RetainedNativeStatusSelection |
|
|
|
6536
6601
|
|
|
6537
6602
|
function retainNativeCaptureRoutes(selections: Map<string, RetainedNativeStatusSelection>, workspaceRoot: string, status: ReviewStatusV3, baseRef: string | undefined): void {
|
|
6538
6603
|
const lineageId = status.authority?.lineageId;
|
|
6539
|
-
if (status
|
|
6604
|
+
if (!canOfferNativeCaptureBindings(status) || !isCanonicalProcessString(lineageId)) return;
|
|
6540
6605
|
const routes = (status.nextTransition?.kind === "collect" ? status.nextTransition.collect?.inputs ?? [] : []).map((input) => ({ key: reviewCaptureSelectionStorageKey(canonicalReviewCaptureBinding(input)), route: Object.freeze({ workspaceRoot, lineageId, ...(baseRef === undefined ? {} : { baseRef, committedOnly: true as const }) }) }));
|
|
6541
6606
|
for (const { key, route } of routes) {
|
|
6542
6607
|
const existing = selections.get(key);
|
|
@@ -6552,6 +6617,12 @@ function readRetainedNativeCaptureRoute(selections: Map<string, RetainedNativeSt
|
|
|
6552
6617
|
return isRetainedNativeCaptureRoute(selection) ? selection : undefined;
|
|
6553
6618
|
}
|
|
6554
6619
|
|
|
6620
|
+
function trustedNativeCaptureBaseRef(route: RetainedNativeCaptureRoute | undefined, candidateViews: CandidateViewRegistry | null, cwd: string, lineageId: string): string | undefined {
|
|
6621
|
+
if (route !== undefined) return route.baseRef;
|
|
6622
|
+
const target = candidateViews?.hasProjection(lineageId, cwd) ? candidateViews.resolveProjection(lineageId, cwd) : undefined;
|
|
6623
|
+
return target?.committedOnly === true ? nativeCommittedRangeSelector(target) : undefined;
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6555
6626
|
function isTerminalReviewAuthorityState(state: string | undefined): boolean { return state === "invalidated" || state === "approved" || state === "escalated"; }
|
|
6556
6627
|
|
|
6557
6628
|
function clearRetainedNativeUntrackedSelection(selections: Map<string, RetainedNativeStatusSelection>, workspaceRoot: string, lineageId: string): void {
|
|
@@ -6776,7 +6847,12 @@ async function executeReviewHostRelayCapture(
|
|
|
6776
6847
|
// live model registry; the relay validates and refuses a missing
|
|
6777
6848
|
// registry or a routing entry with no configured model typed before
|
|
6778
6849
|
// anything launches, never a fallback to a child.
|
|
6779
|
-
|
|
6850
|
+
// gentle-shell#1544: a repository profile pin re-anchors the whole reviewer
|
|
6851
|
+
// routing, so the relay resolves the pinned profile wholesale before the
|
|
6852
|
+
// global routing. A pin that omits a required role stays omitted: the
|
|
6853
|
+
// typed reviewer-config-invalid refusal below is fail-closed, never a
|
|
6854
|
+
// silent per-role fallback to another account's routing.
|
|
6855
|
+
const launch = reviewHostRelaySelection(slot.routingKey ?? slot.lens, pinnedEffectiveModelConfig(cwd) ?? readModelConfig(cwd));
|
|
6780
6856
|
return {
|
|
6781
6857
|
captureArgumentTokens: slot.captureArgumentTokens,
|
|
6782
6858
|
targetCwd: cwd,
|
|
@@ -7143,15 +7219,11 @@ async function resolveNegotiatedReviewStatusForSession(
|
|
|
7143
7219
|
sessionKey: PendingReviewConsentSessionKey,
|
|
7144
7220
|
): Promise<ReviewStatusV3 | undefined> {
|
|
7145
7221
|
if (nativeReviewCli?.reviewMode === undefined || nativeReviewCli.targetStatus === undefined) return undefined;
|
|
7146
|
-
if (ctx.hasUI !== true) return undefined;
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
} catch {
|
|
7152
|
-
return undefined;
|
|
7153
|
-
}
|
|
7154
|
-
if (modeEffective === "off") return undefined;
|
|
7222
|
+
if (ctx.hasUI !== true || !resolveSessionWorktree(ctx.cwd, ctx.cwd)) return undefined;
|
|
7223
|
+
// STATUS can prepare an unversioned directory natively. Only a successfully
|
|
7224
|
+
// decoded effective-on mode authorizes that entry; bootstrap never counts as
|
|
7225
|
+
// an own mutation and native owns all metadata/ancestor safety decisions.
|
|
7226
|
+
if (!await nativeRepositoryPreparationEnabled(nativeReviewCli, ctx.cwd)) return undefined;
|
|
7155
7227
|
try {
|
|
7156
7228
|
const retainedSelections = ((key: PendingReviewConsentSessionKey) => processRetainedNativeStatusSelections.get(key) ?? processRetainedNativeStatusSelections.set(key, new Map()).get(key)!)(sessionKey);
|
|
7157
7229
|
const negotiated = await negotiatedStatusForHostTransport(nativeReviewCli, { cwd: ctx.cwd }, retainedSelections, ctx.cwd);
|
|
@@ -7219,8 +7291,13 @@ const INSPECT_UNTRACKED_SELECTION_NEXT_STEP =
|
|
|
7219
7291
|
'The intended-untracked selection is required before START. The expected_untracked_inventory digest covers untracked path names only (git ls-files --others --exclude-standard); nothing is read or hashed at inventory time, and file content is hashed only for selected paths at candidate freeze. Either call gentle_review with operation "select-intended-untracked" passing this selectionBinding and intendedUntracked ([] excludes every eligible path, a subset includes only those paths), or call inspect again with untrackedScope ("exclude", or "select" with intendedUntracked) to resolve the round trip in one call. To keep a path out of the inventory permanently, ignore it through .gitignore or .git/info/exclude.';
|
|
7220
7292
|
|
|
7221
7293
|
interface PublicReviewCaptureBinding { collectBinding: string; }
|
|
7294
|
+
function canOfferNativeCaptureBindings(status: ReviewStatusV3): boolean {
|
|
7295
|
+
return status.applicability === "current_target" && isCanonicalProcessString(status.authority?.lineageId)
|
|
7296
|
+
&& isCanonicalProcessString(status.targetIdentity) && !isTerminalReviewAuthorityState(status.authority?.state);
|
|
7297
|
+
}
|
|
7298
|
+
|
|
7222
7299
|
function publicReviewCaptureBindings(status: ReviewStatusV3): readonly PublicReviewCaptureBinding[] {
|
|
7223
|
-
if (status.nextTransition?.kind !== "collect") return [];
|
|
7300
|
+
if (!canOfferNativeCaptureBindings(status) || status.nextTransition?.kind !== "collect") return [];
|
|
7224
7301
|
return (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation !== "external.select_intended_untracked").map((input) => ({ collectBinding: canonicalReviewCaptureBinding(input) }));
|
|
7225
7302
|
}
|
|
7226
7303
|
|
|
@@ -7251,7 +7328,7 @@ function selectExactReviewCapture(
|
|
|
7251
7328
|
!isCanonicalProcessString(lineageId) ||
|
|
7252
7329
|
!isCanonicalProcessString(statusLineageId) ||
|
|
7253
7330
|
!isCanonicalProcessString(statusTargetIdentity) ||
|
|
7254
|
-
status
|
|
7331
|
+
!canOfferNativeCaptureBindings(status) ||
|
|
7255
7332
|
statusLineageId !== lineageId
|
|
7256
7333
|
) {
|
|
7257
7334
|
return captureBindingRejected("current STATUS does not offer one non-empty matching lineage and target identity");
|
|
@@ -7404,16 +7481,17 @@ async function executeReviewCaptureOperation(
|
|
|
7404
7481
|
}
|
|
7405
7482
|
const canonicalBinding = parseCanonicalReviewCaptureBinding(parameters.collectBinding);
|
|
7406
7483
|
const cwd = resolveReviewControllerWorkspaceRoot(parameters.workspaceRoot, sessionCwd, candidateViews, parameters.lineageId);
|
|
7407
|
-
|
|
7408
|
-
if (requireRegisteredRoute &&
|
|
7409
|
-
return captureBindingRejected("collectBinding
|
|
7484
|
+
let route = readRetainedNativeCaptureRoute(retainedUntrackedSelections, canonicalBinding);
|
|
7485
|
+
if (requireRegisteredRoute && route !== undefined && (route.workspaceRoot !== cwd || route.lineageId !== parameters.lineageId)) {
|
|
7486
|
+
return captureBindingRejected("collectBinding belongs to a different registered route");
|
|
7410
7487
|
}
|
|
7488
|
+
const baseRef = trustedNativeCaptureBaseRef(route, candidateViews, cwd, parameters.lineageId);
|
|
7411
7489
|
let status: ReviewStatusV3;
|
|
7412
7490
|
try {
|
|
7413
7491
|
const negotiated = await negotiatedStatusForHostTransport(nativeReviewCli, {
|
|
7414
7492
|
cwd,
|
|
7415
7493
|
lineageId: parameters.lineageId,
|
|
7416
|
-
...(
|
|
7494
|
+
...(baseRef === undefined ? {} : { baseRef, committedOnly: true }),
|
|
7417
7495
|
...readRetainedNativeUntrackedSelection(retainedUntrackedSelections, cwd, parameters.lineageId),
|
|
7418
7496
|
...(signal === undefined ? {} : { signal }),
|
|
7419
7497
|
}, retainedUntrackedSelections, cwd);
|
|
@@ -7424,6 +7502,9 @@ async function executeReviewCaptureOperation(
|
|
|
7424
7502
|
}
|
|
7425
7503
|
const selected = selectExactReviewCapture(status, parameters.lineageId, canonicalBinding);
|
|
7426
7504
|
if (!isSelectedReviewCapture(selected)) return selected;
|
|
7505
|
+
// Exact fresh native admission validates this routing snapshot independently
|
|
7506
|
+
// of cache capacity. Carry its trusted selector into every downstream path.
|
|
7507
|
+
route = { workspaceRoot: cwd, lineageId: parameters.lineageId, ...(baseRef === undefined ? {} : { baseRef, committedOnly: true }) };
|
|
7427
7508
|
|
|
7428
7509
|
// During correction the flow carries both the original authority target
|
|
7429
7510
|
// identity and a distinct provider-issued correction target identity
|
|
@@ -7567,13 +7648,14 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7567
7648
|
const canonicalBindings = parameters.collectBindings.map((binding) => parseCanonicalReviewCaptureBinding(binding));
|
|
7568
7649
|
const cwd = resolveReviewControllerWorkspaceRoot(parameters.workspaceRoot, sessionCwd, candidateViews, parameters.lineageId);
|
|
7569
7650
|
const routes = canonicalBindings.map((binding) => readRetainedNativeCaptureRoute(retainedUntrackedSelections, binding));
|
|
7570
|
-
|
|
7571
|
-
if (requireRegisteredRoute &&
|
|
7572
|
-
return captureGroupRejected("collectBindings
|
|
7651
|
+
let route = routes.find((candidate) => candidate !== undefined);
|
|
7652
|
+
if (requireRegisteredRoute && routes.some((candidate) => candidate !== undefined && (candidate.workspaceRoot !== cwd || candidate.lineageId !== parameters.lineageId || candidate.baseRef !== route?.baseRef))) {
|
|
7653
|
+
return captureGroupRejected("collectBindings belong to different registered routes");
|
|
7573
7654
|
}
|
|
7655
|
+
const baseRef = trustedNativeCaptureBaseRef(route, candidateViews, cwd, parameters.lineageId);
|
|
7574
7656
|
const freshStatus = () => negotiatedStatusForHostTransport(nativeReviewCli, {
|
|
7575
7657
|
cwd, lineageId: parameters.lineageId,
|
|
7576
|
-
...(
|
|
7658
|
+
...(baseRef === undefined ? {} : { baseRef, committedOnly: true }),
|
|
7577
7659
|
...readRetainedNativeUntrackedSelection(retainedUntrackedSelections, cwd, parameters.lineageId),
|
|
7578
7660
|
...(signal === undefined ? {} : { signal }),
|
|
7579
7661
|
}, retainedUntrackedSelections, cwd);
|
|
@@ -7587,6 +7669,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7587
7669
|
}
|
|
7588
7670
|
const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings);
|
|
7589
7671
|
if (!("slots" in group && "binding" in group)) return group;
|
|
7672
|
+
route = { workspaceRoot: cwd, lineageId: parameters.lineageId, ...(baseRef === undefined ? {} : { baseRef, committedOnly: true }) };
|
|
7590
7673
|
if (parameters.reviewerRunAcknowledged !== true) {
|
|
7591
7674
|
return {
|
|
7592
7675
|
tool: "gentle_review_capture_group",
|
|
@@ -7597,11 +7680,15 @@ async function executeReviewCaptureGroupOperation(
|
|
|
7597
7680
|
mutation_outcome: "none",
|
|
7598
7681
|
};
|
|
7599
7682
|
}
|
|
7683
|
+
// One routing snapshot for the whole group: the pin-over-global precedence
|
|
7684
|
+
// is identical for every slot, so resolving it once before the map avoids
|
|
7685
|
+
// re-running the pin resolver and config reads per slot.
|
|
7686
|
+
const reviewerRouting = pinnedEffectiveModelConfig(cwd) ?? readModelConfig(cwd);
|
|
7600
7687
|
const requests: readonly ReviewHostRelayRequest[] = group.slots.map((slot) => ({
|
|
7601
7688
|
captureArgumentTokens: slot.captureArgumentTokens,
|
|
7602
7689
|
targetCwd: cwd,
|
|
7603
7690
|
submission: slot.submission!,
|
|
7604
|
-
...reviewHostRelaySelection(slot.lens,
|
|
7691
|
+
...reviewHostRelaySelection(slot.lens, reviewerRouting),
|
|
7605
7692
|
...(modelRegistry === undefined ? {} : { reviewerRegistry: modelRegistry }),
|
|
7606
7693
|
...(reviewerSessionId === undefined ? {} : { reviewerSessionId }),
|
|
7607
7694
|
...(signal === undefined ? {} : { signal }),
|
|
@@ -7655,6 +7742,9 @@ function hydrateDispatchBindingFromStatus(candidateViews: CandidateViewRegistry
|
|
|
7655
7742
|
const lenses = pendingReviewerLenses(status);
|
|
7656
7743
|
if (lenses.length === 0) return undefined;
|
|
7657
7744
|
try {
|
|
7745
|
+
// STATUS proves the current target, not the historical START offer.
|
|
7746
|
+
// Resolve an existing commit-backed range by its reachable frozen tree;
|
|
7747
|
+
// never pass descriptor.baseTree as a provider-owned tree selector.
|
|
7658
7748
|
candidateViews.restoreCurrentForDispatchFromNative(lineageId, contributorRoot, status.projection, lenses);
|
|
7659
7749
|
return { hydrated: true, lineage_id: lineageId, lenses };
|
|
7660
7750
|
} catch (error) {
|
|
@@ -7685,15 +7775,8 @@ async function executeReviewControllerOperation(
|
|
|
7685
7775
|
intendedUntrackedSelection?: NativeIntendedUntrackedSelectionSubmission,
|
|
7686
7776
|
): Promise<Record<string, unknown>> {
|
|
7687
7777
|
const parameters = parseReviewControllerParameters(parametersValue);
|
|
7688
|
-
const defaultCwd = resolveReviewControllerWorkspaceRoot(parameters.workspaceRoot, sessionCwd, candidateViews, parameters.lineageId);
|
|
7689
|
-
const pendingReviewConsentSession = pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey);
|
|
7690
|
-
const _useTargetLifecycleRoot = requiresExplicitTargetLifecycleRoot(parameters.workspaceRoot, sessionCwd, defaultCwd);
|
|
7691
|
-
const includeWorkspaceRoot = parameters.workspaceRoot !== undefined || defaultCwd !== sessionCwd;
|
|
7692
7778
|
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.EXPORT || parameters.operation === REVIEW_CONTROLLER_OPERATION.IMPORT) {
|
|
7693
|
-
//
|
|
7694
|
-
// stores. The native v2.1.11 CLI exposes no bundle equivalent, so both
|
|
7695
|
-
// operations return a structured retirement envelope; the enum members are
|
|
7696
|
-
// kept so the tool schema stays stable for existing callers.
|
|
7779
|
+
// Retired transport needs no repository or lifecycle authority.
|
|
7697
7780
|
return {
|
|
7698
7781
|
operation: parameters.operation,
|
|
7699
7782
|
status: "blocked",
|
|
@@ -7704,13 +7787,34 @@ async function executeReviewControllerOperation(
|
|
|
7704
7787
|
next_action: "Use the native `gentle-ai review` CLI (start/finalize/validate/status/recover) against the repository review authority; receipts and canonical artifacts live in the Git common-directory store at .git/gentle-ai/reviews and travel with the repository through normal Git replication.",
|
|
7705
7788
|
};
|
|
7706
7789
|
}
|
|
7790
|
+
const allowsPreparation = parameters.lineageId === undefined && (
|
|
7791
|
+
parameters.operation === REVIEW_CONTROLLER_OPERATION.INSPECT ||
|
|
7792
|
+
(parameters.operation === REVIEW_CONTROLLER_OPERATION.START &&
|
|
7793
|
+
parseControllerJson(requiredControllerString(parameters, "input"), parameters.operation).mode === REVIEW_MODE.ORDINARY)
|
|
7794
|
+
);
|
|
7795
|
+
const workspaceResolution: { identity?: { toplevel: string; commonDir: string } } = {};
|
|
7796
|
+
const defaultCwd = resolveReviewControllerWorkspaceRoot(parameters.workspaceRoot, sessionCwd, candidateViews, parameters.lineageId, allowsPreparation, workspaceResolution);
|
|
7797
|
+
const needsPreparation = allowsPreparation && workspaceResolution.identity === undefined;
|
|
7798
|
+
const preparationCurrent = allowsPreparation && context?.sessionManager
|
|
7799
|
+
? captureBoundSessionRepositoryAuthority(context.sessionManager, context.sessionManager.getCwd?.() ?? sessionCwd)
|
|
7800
|
+
: undefined;
|
|
7801
|
+
const assertPreparationAuthority = () => {
|
|
7802
|
+
if (allowsPreparation && signal?.aborted) throw reviewCancellation("Review controller operation was cancelled");
|
|
7803
|
+
if (preparationCurrent && !preparationCurrent()) {
|
|
7804
|
+
throw new Error("Session Git authority or lifecycle changed; cannot prepare its review repository.");
|
|
7805
|
+
}
|
|
7806
|
+
};
|
|
7807
|
+
assertPreparationAuthority();
|
|
7808
|
+
const pendingReviewConsentSession = pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey);
|
|
7809
|
+
const _useTargetLifecycleRoot = requiresExplicitTargetLifecycleRoot(parameters.workspaceRoot, sessionCwd, defaultCwd);
|
|
7810
|
+
const includeWorkspaceRoot = parameters.workspaceRoot !== undefined || defaultCwd !== sessionCwd;
|
|
7707
7811
|
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.ASSESS) {
|
|
7708
7812
|
// Read-only native risk assessment (gentle-ai#4295, gentle-pi#662). Never
|
|
7709
7813
|
// mutates, never requires a lineageId, and never routes through
|
|
7710
7814
|
// authorizeDestructiveReviewOperation (it returns early for any
|
|
7711
7815
|
// operation that is neither RESET nor a maintenance operation).
|
|
7712
7816
|
const input = parseReviewAssessInput(parameters.operation, parameters.input);
|
|
7713
|
-
const details = await resolveReviewAssessmentPlan(nativeReviewCli, defaultCwd, input, signal);
|
|
7817
|
+
const details = await resolveReviewAssessmentPlan(nativeReviewCli, defaultCwd, input, signal, context?.sessionManager as ReceiptSession | undefined);
|
|
7714
7818
|
return { operation: parameters.operation, ...details, ...(includeWorkspaceRoot ? { workspace_root: defaultCwd } : {}) };
|
|
7715
7819
|
}
|
|
7716
7820
|
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.REPAIR_LEGACY_ALIAS) {
|
|
@@ -7747,6 +7851,10 @@ async function executeReviewControllerOperation(
|
|
|
7747
7851
|
return nativeInspectInputRejection("base-ref-unresolvable");
|
|
7748
7852
|
}
|
|
7749
7853
|
}
|
|
7854
|
+
if (needsPreparation && !await nativeRepositoryPreparationEnabled(nativeReviewCli, defaultCwd, signal)) {
|
|
7855
|
+
return nativeRepositoryPreparationBlocked(parameters.operation);
|
|
7856
|
+
}
|
|
7857
|
+
assertPreparationAuthority();
|
|
7750
7858
|
const inspectSelector = canonicalBaseRef === undefined
|
|
7751
7859
|
? {}
|
|
7752
7860
|
: { baseRef: canonicalBaseRef, committedOnly: true as const };
|
|
@@ -7779,6 +7887,18 @@ async function executeReviewControllerOperation(
|
|
|
7779
7887
|
undefined,
|
|
7780
7888
|
);
|
|
7781
7889
|
if (parameters.untrackedScope === undefined) {
|
|
7890
|
+
if (canonicalBaseRef !== undefined && typeof plainMapped.selectionBinding === "string") {
|
|
7891
|
+
retainNativeStatusSelection(retainedUntrackedSelections, reviewLifecycleStorageKey(defaultCwd, ""), Object.freeze({ selectionBinding: plainMapped.selectionBinding, targetIdentity: status.targetIdentity, baseRef: canonicalBaseRef, committedOnly: true as const }));
|
|
7892
|
+
} else if (canonicalBaseRef !== undefined && plainMapped.status === "ready") {
|
|
7893
|
+
// A plain inspect that resolves ready with no untracked decision still
|
|
7894
|
+
// retains its own committed-range selector, so the following plain
|
|
7895
|
+
// START replays this exact inspected range instead of adopting the
|
|
7896
|
+
// native default base-ref (gentle-pi#874).
|
|
7897
|
+
const readyCandidateIdentity = nativePreLineageCandidateIdentity(status);
|
|
7898
|
+
if (readyCandidateIdentity !== undefined) {
|
|
7899
|
+
retainNativeStatusSelection(retainedUntrackedSelections, reviewLifecycleStorageKey(defaultCwd, ""), Object.freeze({ ...readyCandidateIdentity, baseRef: canonicalBaseRef, committedOnly: true as const }));
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7782
7902
|
// gentle-pi#706: the stop alone never tells the caller what to do next.
|
|
7783
7903
|
return {
|
|
7784
7904
|
...plainMapped,
|
|
@@ -7865,6 +7985,10 @@ async function executeReviewControllerOperation(
|
|
|
7865
7985
|
intendedUntracked: Object.freeze([...selected.intendedUntracked!]),
|
|
7866
7986
|
submission,
|
|
7867
7987
|
...candidateIdentity,
|
|
7988
|
+
// gentle-pi#1192: remember the committed-range selector this
|
|
7989
|
+
// inspect used, so a later plain START replays it instead of
|
|
7990
|
+
// recomputing an unrelated workspace-diff candidate.
|
|
7991
|
+
...(canonicalBaseRef === undefined ? {} : { baseRef: canonicalBaseRef, committedOnly: true as const }),
|
|
7868
7992
|
}),
|
|
7869
7993
|
);
|
|
7870
7994
|
}
|
|
@@ -7927,7 +8051,7 @@ async function executeReviewControllerOperation(
|
|
|
7927
8051
|
const statusRequest = {
|
|
7928
8052
|
cwd: defaultCwd,
|
|
7929
8053
|
lineageId: String(input.predecessorLineage),
|
|
7930
|
-
...(frozenTarget?.committedOnly === true ? { baseRef: frozenTarget
|
|
8054
|
+
...(frozenTarget?.committedOnly === true ? { baseRef: nativeCommittedRangeSelector(frozenTarget), committedOnly: true } : {}),
|
|
7931
8055
|
...(signal === undefined ? {} : { signal }),
|
|
7932
8056
|
};
|
|
7933
8057
|
let status: ReviewStatusV3;
|
|
@@ -8001,11 +8125,11 @@ async function executeReviewControllerOperation(
|
|
|
8001
8125
|
const frozenTarget = parameters.lineageId === undefined || !candidateViews?.hasProjection(parameters.lineageId, defaultCwd) ? undefined : candidateViews.resolveProjection(parameters.lineageId, defaultCwd);
|
|
8002
8126
|
let status: ReviewStatusV3;
|
|
8003
8127
|
try {
|
|
8004
|
-
status = await nativeReviewCli.targetStatus({ cwd: defaultCwd, ...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }), ...(frozenTarget?.committedOnly === true ? { baseRef: frozenTarget
|
|
8128
|
+
status = await nativeReviewCli.targetStatus({ cwd: defaultCwd, ...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }), ...(frozenTarget?.committedOnly === true ? { baseRef: nativeCommittedRangeSelector(frozenTarget), committedOnly: true } : {}), ...(signal === undefined ? {} : { signal }) });
|
|
8005
8129
|
} catch (error) {
|
|
8006
8130
|
return nativeStatusFailed(parameters.operation, error);
|
|
8007
8131
|
}
|
|
8008
|
-
clearRetainedNativeStatusSelectionsOnTerminal(retainedUntrackedSelections, defaultCwd, status.authority?.lineageId, status.authority?.state); retainNativeCaptureRoutes(retainedUntrackedSelections, defaultCwd, status, frozenTarget?.committedOnly === true ? frozenTarget
|
|
8132
|
+
clearRetainedNativeStatusSelectionsOnTerminal(retainedUntrackedSelections, defaultCwd, status.authority?.lineageId, status.authority?.state); retainNativeCaptureRoutes(retainedUntrackedSelections, defaultCwd, status, frozenTarget?.committedOnly === true ? nativeCommittedRangeSelector(frozenTarget) : undefined);
|
|
8009
8133
|
if (status.authority?.version === "compact-v2") return { operation: parameters.operation, repaired: false, compact_authority: "immutable-untouched", status: mapNativeTargetStatus(parameters.operation, status, parameters.lineageId) };
|
|
8010
8134
|
if (status.authority?.version !== "legacy-v1") return mapNativeTargetStatus(parameters.operation, status, parameters.lineageId);
|
|
8011
8135
|
const store = ReviewTransactionStore.forRepository(defaultCwd);
|
|
@@ -8045,7 +8169,7 @@ async function executeReviewControllerOperation(
|
|
|
8045
8169
|
const target = {
|
|
8046
8170
|
cwd: defaultCwd,
|
|
8047
8171
|
lineageId: parameters.lineageId,
|
|
8048
|
-
...(frozenTarget?.committedOnly === true ? { baseRef: frozenTarget
|
|
8172
|
+
...(frozenTarget?.committedOnly === true ? { baseRef: nativeCommittedRangeSelector(frozenTarget), committedOnly: true } : {}),
|
|
8049
8173
|
...readRetainedNativeUntrackedSelection(retainedUntrackedSelections, defaultCwd, parameters.lineageId),
|
|
8050
8174
|
...(signal === undefined ? {} : { signal }),
|
|
8051
8175
|
};
|
|
@@ -8109,9 +8233,10 @@ async function executeReviewControllerOperation(
|
|
|
8109
8233
|
// The registry owns restoring writability of its 0555 views before
|
|
8110
8234
|
// removal; a terminal approved cleanup keeps the lineage projection.
|
|
8111
8235
|
const candidateViewCleanup = deferredPostBurnCleanup(POST_BURN_CLEANUP.candidateView, () => candidateViews?.cleanupTerminal(parameters.lineageId, "approved", defaultCwd));
|
|
8112
|
-
// gentle-pi#668: `closed` is never
|
|
8113
|
-
//
|
|
8114
|
-
//
|
|
8236
|
+
// gentle-pi#668/#1175: `closed` is never recorded here -- the burn this
|
|
8237
|
+
// acknowledgement completed makes native report candidate.consumed for
|
|
8238
|
+
// this exact candidate, and the next assess call derives closure from
|
|
8239
|
+
// that evidence alone.
|
|
8115
8240
|
return {
|
|
8116
8241
|
operation: parameters.operation,
|
|
8117
8242
|
status: "closed",
|
|
@@ -8225,7 +8350,7 @@ async function executeReviewControllerOperation(
|
|
|
8225
8350
|
if (value.mutationOutcome === "none") pending.cleanupCandidate();
|
|
8226
8351
|
return await reconcileNativeMutationFailure(parameters.operation, error, nativeReviewCli, {
|
|
8227
8352
|
cwd: pending.authorityCwd,
|
|
8228
|
-
...(pending.candidateView.committedOnly ? { baseRef: pending.candidateView
|
|
8353
|
+
...(pending.candidateView.committedOnly ? { baseRef: nativeCommittedRangeSelector(pending.candidateView), committedOnly: true } : {}),
|
|
8229
8354
|
projection: "workspace",
|
|
8230
8355
|
}, retainedUntrackedSelections);
|
|
8231
8356
|
}
|
|
@@ -8234,9 +8359,13 @@ async function executeReviewControllerOperation(
|
|
|
8234
8359
|
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.SELECT_INTENDED_UNTRACKED) {
|
|
8235
8360
|
if (nativeReviewCli?.targetStatus === undefined || nativeReviewCli.start === undefined) return nativeStatusUnsupported(parameters.operation);
|
|
8236
8361
|
const canonicalBinding = parseCanonicalReviewCaptureBinding(parameters.selectionBinding!);
|
|
8362
|
+
const retainedStop = retainedUntrackedSelections.get(reviewLifecycleStorageKey(defaultCwd, ""));
|
|
8363
|
+
const stopSelector = retainedStop !== undefined && "selectionBinding" in retainedStop ? retainedStop : undefined;
|
|
8364
|
+
if (stopSelector !== undefined && stopSelector.selectionBinding !== canonicalBinding) return { operation: parameters.operation, status: "blocked", outcome: "intended-untracked-selection-binding-rejected", mutation_performed: false, mutation_outcome: "none" };
|
|
8365
|
+
const committedSelector = stopSelector === undefined ? {} : { baseRef: stopSelector.baseRef, committedOnly: true as const };
|
|
8237
8366
|
let status: ReviewStatusV3;
|
|
8238
8367
|
try {
|
|
8239
|
-
const negotiated = await negotiatedStatusForHostTransport(nativeReviewCli, { cwd: defaultCwd, ...(signal === undefined ? {} : { signal }) }, retainedUntrackedSelections, defaultCwd);
|
|
8368
|
+
const negotiated = await negotiatedStatusForHostTransport(nativeReviewCli, { cwd: defaultCwd, ...committedSelector, ...(signal === undefined ? {} : { signal }) }, retainedUntrackedSelections, defaultCwd);
|
|
8240
8369
|
if (negotiated.transport !== undefined) return hostTransportUnavailable(parameters.operation, negotiated.transport);
|
|
8241
8370
|
status = negotiated.status!;
|
|
8242
8371
|
} catch (error) { return nativeStatusFailed(parameters.operation, error); }
|
|
@@ -8245,10 +8374,10 @@ async function executeReviewControllerOperation(
|
|
|
8245
8374
|
try { eligible = JSON.parse(eligibleJson ?? ""); } catch { eligible = undefined; }
|
|
8246
8375
|
const scope = parameters.intendedUntracked!.length === 0 ? NATIVE_START_UNTRACKED_SCOPE.EXCLUDE : NATIVE_START_UNTRACKED_SCOPE.SELECT;
|
|
8247
8376
|
const selected = validateNativeStartUntrackedSelection({ untrackedScope: scope, expectedUntrackedInventory: inventory, intendedUntracked: parameters.intendedUntracked });
|
|
8248
|
-
const rejected = input === undefined || canonicalReviewCaptureBinding(input) !== canonicalBinding || exactCollectArgument(input, "target_identity") !== status.targetIdentity || exactCollectArgument(input, "projection") !== status.projection.projection || exactCollectArgument(input, "base_tree") !== status.projection.baseTree || exactCollectArgument(input, "candidate_tree") !== status.projection.currentCandidateTree || !Array.isArray(eligible) || selected.reason !== undefined || selected.intendedUntracked!.some((path) => !eligible.includes(path));
|
|
8377
|
+
const rejected = (stopSelector !== undefined && stopSelector.targetIdentity !== status.targetIdentity) || input === undefined || canonicalReviewCaptureBinding(input) !== canonicalBinding || exactCollectArgument(input, "target_identity") !== status.targetIdentity || exactCollectArgument(input, "projection") !== status.projection.projection || exactCollectArgument(input, "base_tree") !== status.projection.baseTree || exactCollectArgument(input, "candidate_tree") !== status.projection.currentCandidateTree || !Array.isArray(eligible) || selected.reason !== undefined || selected.intendedUntracked!.some((path) => !eligible.includes(path));
|
|
8249
8378
|
if (rejected) return { operation: parameters.operation, status: "blocked", outcome: "intended-untracked-selection-binding-rejected", mutation_performed: false, mutation_outcome: "none" };
|
|
8250
8379
|
const submission = { argumentTokens: input.submission!.argumentTokens, value: JSON.stringify({ schema: "gentle-ai.review-intended-untracked-selection/v1", untracked_scope: scope, expected_untracked_inventory: inventory, intended_untracked: selected.intendedUntracked }) };
|
|
8251
|
-
const result = await executeReviewControllerOperation({ operation: REVIEW_CONTROLLER_OPERATION.START, ...(parameters.workspaceRoot === undefined ? {} : { workspaceRoot: parameters.workspaceRoot }), input: JSON.stringify({ mode: REVIEW_MODE.ORDINARY, untrackedScope: scope, expectedUntrackedInventory: inventory, intendedUntracked: selected.intendedUntracked }) }, sessionCwd, nativeReviewCli, signal, candidateViews, context, retainedUntrackedSelections, pendingReviewConsentRegistry, pendingReviewConsentFallbackKey, reviewConsentNow, reviewConsentScheduleTimer, submission);
|
|
8380
|
+
const result = await executeReviewControllerOperation({ operation: REVIEW_CONTROLLER_OPERATION.START, ...(parameters.workspaceRoot === undefined ? {} : { workspaceRoot: parameters.workspaceRoot }), input: JSON.stringify({ mode: REVIEW_MODE.ORDINARY, ...committedSelector, untrackedScope: scope, expectedUntrackedInventory: inventory, intendedUntracked: selected.intendedUntracked }) }, sessionCwd, nativeReviewCli, signal, candidateViews, context, retainedUntrackedSelections, pendingReviewConsentRegistry, pendingReviewConsentFallbackKey, reviewConsentNow, reviewConsentScheduleTimer, submission);
|
|
8252
8381
|
return { ...result, operation: parameters.operation };
|
|
8253
8382
|
}
|
|
8254
8383
|
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.START) {
|
|
@@ -8277,7 +8406,7 @@ async function executeReviewControllerOperation(
|
|
|
8277
8406
|
// gentle-pi#706: a plain START adopts the selection an inspect
|
|
8278
8407
|
// untrackedScope round trip retained pre-lineage; explicit input or a
|
|
8279
8408
|
// carried submission always wins over the retained entry.
|
|
8280
|
-
const
|
|
8409
|
+
const rawRetainedPreLineageSelection =
|
|
8281
8410
|
explicitUntrackedSelection.untrackedScope === undefined &&
|
|
8282
8411
|
intendedUntrackedSelection === undefined
|
|
8283
8412
|
? readRetainedPreLineageNativeUntrackedSelection(
|
|
@@ -8285,21 +8414,8 @@ async function executeReviewControllerOperation(
|
|
|
8285
8414
|
defaultCwd,
|
|
8286
8415
|
)
|
|
8287
8416
|
: undefined;
|
|
8288
|
-
const untrackedSelection: NativeStartUntrackedSelection =
|
|
8289
|
-
retainedPreLineageSelection === undefined
|
|
8290
|
-
? explicitUntrackedSelection
|
|
8291
|
-
: {
|
|
8292
|
-
untrackedScope: retainedPreLineageSelection.untrackedScope,
|
|
8293
|
-
expectedUntrackedInventory:
|
|
8294
|
-
retainedPreLineageSelection.expectedUntrackedInventory,
|
|
8295
|
-
intendedUntracked: [...retainedPreLineageSelection.intendedUntracked],
|
|
8296
|
-
};
|
|
8297
|
-
const untrackedSubmission =
|
|
8298
|
-
intendedUntrackedSelection ?? retainedPreLineageSelection?.submission;
|
|
8299
|
-
const retainedUntrackedSelection =
|
|
8300
|
-
retainedPreLineageSelection ??
|
|
8301
|
-
cloneRetainedNativeUntrackedSelection(explicitUntrackedSelection);
|
|
8302
8417
|
let canonicalBaseRef: string | undefined;
|
|
8418
|
+
let providerBaseTree: string | undefined;
|
|
8303
8419
|
if (baseRef !== undefined) {
|
|
8304
8420
|
try {
|
|
8305
8421
|
canonicalBaseRef = resolveCanonicalCandidateBase(defaultCwd, baseRef).commit;
|
|
@@ -8309,12 +8425,62 @@ async function executeReviewControllerOperation(
|
|
|
8309
8425
|
return nativeStartRejection("base-ref-unresolvable");
|
|
8310
8426
|
}
|
|
8311
8427
|
}
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8428
|
+
// gentle-pi#1192: an explicit baseRef always wins over a conflicting
|
|
8429
|
+
// retained one (same precedent as explicit untrackedScope/submission
|
|
8430
|
+
// above); drop the retained entry from *this* START rather than reject
|
|
8431
|
+
// outright, so a differently-scoped explicit START still proceeds. Any
|
|
8432
|
+
// STATUS/START this call still runs against the "" key clears it below
|
|
8433
|
+
// on success regardless (gentle-pi#706), same as any other START.
|
|
8434
|
+
const retainedBaseRefConflicts =
|
|
8435
|
+
canonicalBaseRef !== undefined &&
|
|
8436
|
+
rawRetainedPreLineageSelection?.baseRef !== undefined &&
|
|
8437
|
+
rawRetainedPreLineageSelection.baseRef !== canonicalBaseRef;
|
|
8438
|
+
const retainedPreLineageSelection = retainedBaseRefConflicts ? undefined : rawRetainedPreLineageSelection;
|
|
8439
|
+
// A plain START (no explicit baseRef) adopts the retained committed-range
|
|
8440
|
+
// selector verbatim; it was already canonicalized when the inspect stored
|
|
8441
|
+
// it, so no second resolveCanonicalCandidateBase round trip is needed.
|
|
8442
|
+
if (canonicalBaseRef === undefined && retainedPreLineageSelection?.baseRef !== undefined) canonicalBaseRef = retainedPreLineageSelection.baseRef;
|
|
8443
|
+
// A retained pre-lineage entry may carry only a committed-range selector
|
|
8444
|
+
// with no untracked decision at all (a plain inspect that resolved ready
|
|
8445
|
+
// without ever needing untrackedScope); only adopt its untracked fields
|
|
8446
|
+
// when it actually recorded one.
|
|
8447
|
+
const untrackedSelection: NativeStartUntrackedSelection =
|
|
8448
|
+
retainedPreLineageSelection === undefined || retainedPreLineageSelection.untrackedScope === undefined
|
|
8449
|
+
? explicitUntrackedSelection
|
|
8450
|
+
: {
|
|
8451
|
+
untrackedScope: retainedPreLineageSelection.untrackedScope,
|
|
8452
|
+
expectedUntrackedInventory:
|
|
8453
|
+
retainedPreLineageSelection.expectedUntrackedInventory!,
|
|
8454
|
+
intendedUntracked: [...retainedPreLineageSelection.intendedUntracked!],
|
|
8455
|
+
};
|
|
8456
|
+
const untrackedSubmission =
|
|
8457
|
+
intendedUntrackedSelection ??
|
|
8458
|
+
(retainedPreLineageSelection?.untrackedScope === undefined ? undefined : retainedPreLineageSelection.submission);
|
|
8459
|
+
// The stored value must stay a plain RetainedNativeUntrackedSelection
|
|
8460
|
+
// (no baseRef/targetIdentity/candidateTree): it is re-keyed under the
|
|
8461
|
+
// lineage-scoped entry below, and readRetainedNativeUntrackedSelection
|
|
8462
|
+
// discriminates that entry from a RetainedNativeCaptureRoute by the
|
|
8463
|
+
// absence of a baseRef field.
|
|
8464
|
+
const retainedUntrackedSelection: RetainedNativeUntrackedSelection | undefined =
|
|
8465
|
+
retainedPreLineageSelection === undefined || retainedPreLineageSelection.untrackedScope === undefined
|
|
8466
|
+
? cloneRetainedNativeUntrackedSelection(explicitUntrackedSelection)
|
|
8467
|
+
: Object.freeze({
|
|
8468
|
+
untrackedScope: retainedPreLineageSelection.untrackedScope,
|
|
8469
|
+
expectedUntrackedInventory: retainedPreLineageSelection.expectedUntrackedInventory,
|
|
8470
|
+
intendedUntracked: retainedPreLineageSelection.intendedUntracked,
|
|
8471
|
+
...(retainedPreLineageSelection.submission === undefined ? {} : { submission: retainedPreLineageSelection.submission }),
|
|
8472
|
+
});
|
|
8473
|
+
if (needsPreparation) {
|
|
8474
|
+
if (!await nativeRepositoryPreparationEnabled(nativeReviewCli, defaultCwd, signal)) return nativeRepositoryPreparationBlocked(parameters.operation);
|
|
8475
|
+
} else {
|
|
8476
|
+
try {
|
|
8477
|
+
const gated = await resolveReviewModeGate(nativeReviewCli, parameters.operation, defaultCwd, signal);
|
|
8478
|
+
if (gated !== undefined) return gated;
|
|
8479
|
+
} catch (error) {
|
|
8480
|
+
return nativeOperationFailure(parameters.operation, error);
|
|
8481
|
+
}
|
|
8317
8482
|
}
|
|
8483
|
+
assertPreparationAuthority();
|
|
8318
8484
|
if (nativeReviewCli?.targetStatus === undefined) return nativeStatusUnsupported(parameters.operation);
|
|
8319
8485
|
let target: ReviewStatusV3;
|
|
8320
8486
|
try {
|
|
@@ -8330,7 +8496,7 @@ async function executeReviewControllerOperation(
|
|
|
8330
8496
|
target = negotiated.status!;
|
|
8331
8497
|
// gentle-pi#874: the STATUS this START already fetched can itself
|
|
8332
8498
|
// offer the committed-range START for an empty workspace candidate.
|
|
8333
|
-
// Adopt its own base
|
|
8499
|
+
// Adopt its own base selector *here*, before the candidate view and the
|
|
8334
8500
|
// native START are resolved, and re-derive the target for that range,
|
|
8335
8501
|
// so all three agree on one base-diff identity. Adopting the offer
|
|
8336
8502
|
// later left the workspace target and the base-diff candidate view
|
|
@@ -8342,6 +8508,7 @@ async function executeReviewControllerOperation(
|
|
|
8342
8508
|
// the base-less view that tripped candidate-target-projection-drift.
|
|
8343
8509
|
if (canonicalBaseRef === undefined) {
|
|
8344
8510
|
const offeredBaseRef = offeredCommittedRangeBaseRef(target);
|
|
8511
|
+
if (offeredBaseRef === undefined && target.nextTransition?.kind === "execute" && target.nextTransition.execute.operation === "review.start" && target.nextTransition.execute.arguments.some((argument) => argument.name === "base-ref")) return nativeStartRejection("provider-base-offer-invalid");
|
|
8345
8512
|
if (offeredBaseRef !== undefined) {
|
|
8346
8513
|
const renegotiated = await negotiatedStatusForHostTransport(nativeReviewCli, {
|
|
8347
8514
|
cwd: defaultCwd,
|
|
@@ -8353,7 +8520,12 @@ async function executeReviewControllerOperation(
|
|
|
8353
8520
|
...(signal === undefined ? {} : { signal }),
|
|
8354
8521
|
}, retainedUntrackedSelections, defaultCwd);
|
|
8355
8522
|
if (renegotiated.transport !== undefined) return hostTransportUnavailable(parameters.operation, renegotiated.transport);
|
|
8356
|
-
|
|
8523
|
+
if (isProviderCandidateBaseTree(defaultCwd, offeredBaseRef)) {
|
|
8524
|
+
if (renegotiated.status!.projection.baseTree !== offeredBaseRef) return nativeStartRejection("provider-base-tree-mismatch");
|
|
8525
|
+
providerBaseTree = offeredBaseRef;
|
|
8526
|
+
} else {
|
|
8527
|
+
canonicalBaseRef = offeredBaseRef;
|
|
8528
|
+
}
|
|
8357
8529
|
target = renegotiated.status!;
|
|
8358
8530
|
}
|
|
8359
8531
|
}
|
|
@@ -8384,7 +8556,7 @@ async function executeReviewControllerOperation(
|
|
|
8384
8556
|
// recovery. Folding in currentCandidateTree makes a content change
|
|
8385
8557
|
// mint a fresh replay key -- and therefore a fresh candidate view --
|
|
8386
8558
|
// instead of reusing the stale one.
|
|
8387
|
-
const replayKey = JSON.stringify({ cwd: defaultCwd, lineageId: parameters.lineageId ?? null, input: parameters.input ?? null, inputPath: parameters.inputPath ?? null, candidateTree: target.projection.currentCandidateTree });
|
|
8559
|
+
const replayKey = JSON.stringify({ cwd: defaultCwd, lineageId: parameters.lineageId ?? null, input: parameters.input ?? null, inputPath: parameters.inputPath ?? null, candidateTree: target.projection.currentCandidateTree, providerBaseTree: providerBaseTree ?? null });
|
|
8388
8560
|
// Synchronously drop any binding whose TTL has already elapsed
|
|
8389
8561
|
// before reusing its retained candidate view, so a fresh-candidate
|
|
8390
8562
|
// retry cannot reuse a view tied to an expired binding and trip
|
|
@@ -8395,7 +8567,7 @@ async function executeReviewControllerOperation(
|
|
|
8395
8567
|
let candidateView: ReturnType<CandidateViewRegistry["create"]> | undefined;
|
|
8396
8568
|
let nativeStartAttempted = false;
|
|
8397
8569
|
try {
|
|
8398
|
-
const candidateRequest = { contributorRoot: defaultCwd, replayKey, ...(canonicalBaseRef === undefined ? {} : { baseRef: canonicalBaseRef, committedOnly: true }) };
|
|
8570
|
+
const candidateRequest = { contributorRoot: defaultCwd, replayKey, ...(canonicalBaseRef === undefined ? {} : { baseRef: canonicalBaseRef, committedOnly: true }), ...(providerBaseTree === undefined ? {} : { providerBaseTree, committedOnly: true }) };
|
|
8399
8571
|
candidateView = candidateViews?.createOrReuse({ ...candidateRequest, intendedUntracked: candidateIntendedUntracked });
|
|
8400
8572
|
if (candidateView !== undefined) assertNativeStartCandidateBinding(candidateView, target);
|
|
8401
8573
|
let result: NativeStartResult;
|
|
@@ -8406,6 +8578,7 @@ async function executeReviewControllerOperation(
|
|
|
8406
8578
|
...(canonicalBaseRef === undefined
|
|
8407
8579
|
? {}
|
|
8408
8580
|
: { baseRef: candidateView?.baseCommit ?? canonicalBaseRef, committedOnly: true }),
|
|
8581
|
+
...(providerBaseTree === undefined ? {} : { baseRef: providerBaseTree, committedOnly: true }),
|
|
8409
8582
|
targetIdentity: target.targetIdentity,
|
|
8410
8583
|
projection: target.projection.projection,
|
|
8411
8584
|
...(untrackedSelection.untrackedScope === undefined ? {} : untrackedSelection),
|
|
@@ -8495,6 +8668,7 @@ async function executeReviewControllerOperation(
|
|
|
8495
8668
|
cwd: defaultCwd,
|
|
8496
8669
|
...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
|
|
8497
8670
|
...(canonicalBaseRef === undefined ? {} : { baseRef: candidateView?.baseCommit ?? canonicalBaseRef, committedOnly: true }),
|
|
8671
|
+
...(providerBaseTree === undefined ? {} : { baseRef: providerBaseTree, committedOnly: true }),
|
|
8498
8672
|
...(untrackedSelection.untrackedScope === undefined ? {} : untrackedSelection),
|
|
8499
8673
|
projection: "workspace",
|
|
8500
8674
|
}, retainedUntrackedSelections);
|
|
@@ -8602,7 +8776,7 @@ async function executeReviewControllerOperation(
|
|
|
8602
8776
|
const retainedCommittedTarget = rawStatus === undefined && parameters.lineageId !== undefined && candidateViews?.hasProjection(parameters.lineageId, defaultCwd)
|
|
8603
8777
|
? candidateViews.resolveProjection(parameters.lineageId, defaultCwd)
|
|
8604
8778
|
: undefined;
|
|
8605
|
-
const effectiveBaseRef = baseRef ?? (retainedCommittedTarget?.committedOnly === true ? retainedCommittedTarget
|
|
8779
|
+
const effectiveBaseRef = baseRef ?? (retainedCommittedTarget?.committedOnly === true ? nativeCommittedRangeSelector(retainedCommittedTarget) : undefined);
|
|
8606
8780
|
if (nativeReviewCli?.targetStatus !== undefined) {
|
|
8607
8781
|
try {
|
|
8608
8782
|
const negotiated = await negotiatedStatusForHostTransport(nativeReviewCli, {
|
|
@@ -8653,6 +8827,8 @@ export const __testing = {
|
|
|
8653
8827
|
guardedCommandPreview,
|
|
8654
8828
|
guardedCommandTitle,
|
|
8655
8829
|
loadRuntimeGuardrailsConfig,
|
|
8830
|
+
isOrdinaryYoloPush,
|
|
8831
|
+
yoloPushConfiguredRestriction,
|
|
8656
8832
|
buildGentlePrompt,
|
|
8657
8833
|
nativeStatusUnsupported,
|
|
8658
8834
|
nativeStartRejection,
|
|
@@ -8686,8 +8862,6 @@ export const __testing = {
|
|
|
8686
8862
|
readNativeReviewOutcome,
|
|
8687
8863
|
recordNativeReviewOutcome,
|
|
8688
8864
|
clearNativeReviewOutcomeMemoForTesting,
|
|
8689
|
-
resolveSelectedNativeSddChangeStartup,
|
|
8690
|
-
readSddChangeFlag,
|
|
8691
8865
|
resetTelemetryTriggerGuardForTesting,
|
|
8692
8866
|
createGentleAiExtension: createGentleAiExtensionForTesting,
|
|
8693
8867
|
getPiModelOptions,
|
|
@@ -8743,16 +8917,12 @@ function createGentleAiExtensionForTesting(
|
|
|
8743
8917
|
const resolveTelemetryTriggerBinary = dependencies.resolveTelemetryTriggerBinary ?? resolveGentleAiBinary;
|
|
8744
8918
|
const telemetryExecFileAdapter = dependencies.telemetryExecFileAdapter ?? createNodeExecFileAdapter();
|
|
8745
8919
|
return function gentleAi(pi: ExtensionAPI): void {
|
|
8746
|
-
const flags = pi as unknown as { registerFlag?: (name: string, definition: { description: string; type: "string"; default?: string }) => void };
|
|
8747
|
-
flags.registerFlag?.(SDD_CHANGE_FLAG, {
|
|
8748
|
-
description: "Internal launch-local selected SDD change identity for package-owned child agents.",
|
|
8749
|
-
type: "string",
|
|
8750
|
-
});
|
|
8751
8920
|
declareReviewRelayHandshake(dependencies.processEnv ?? process.env);
|
|
8752
8921
|
const pendingReviewConsentFallbackKey = Symbol("pending-review-consent-fallback");
|
|
8753
8922
|
const candidateViews = dependencies.candidateViews === undefined ? new CandidateViewRegistry() : dependencies.candidateViews;
|
|
8754
8923
|
const herdrLifecycle = createHerdrConfirmationLifecycle(pi.events);
|
|
8755
8924
|
const permissionEnvironment = dependencies.processEnv ?? process.env;
|
|
8925
|
+
const yolo = registerYoloSessionPolicy(pi, permissionEnvironment);
|
|
8756
8926
|
|
|
8757
8927
|
const setReviewSessionPermissionStatus = (context: ExtensionContext, active: boolean): void => {
|
|
8758
8928
|
try {
|
|
@@ -8782,11 +8952,20 @@ function createGentleAiExtensionForTesting(
|
|
|
8782
8952
|
return revoked;
|
|
8783
8953
|
};
|
|
8784
8954
|
|
|
8955
|
+
const reviewSidebar = createReviewSidebarPublisher(pi);
|
|
8956
|
+
pi.on("session_tree", (_event, ctx) => reviewSidebar.reset(ctx));
|
|
8785
8957
|
let reminderSessionActive = true;
|
|
8786
8958
|
let reminderEpoch = 0;
|
|
8959
|
+
let reminderManager: ExtensionContext["sessionManager"] | undefined;
|
|
8960
|
+
let unbindPreparation: (() => void) | undefined;
|
|
8787
8961
|
pi.on("session_shutdown", (event, context) => {
|
|
8962
|
+
yolo.reset(context);
|
|
8963
|
+
reviewSidebar.reset();
|
|
8788
8964
|
reminderSessionActive = false;
|
|
8789
8965
|
reminderEpoch += 1;
|
|
8966
|
+
unbindPreparation?.();
|
|
8967
|
+
unbindPreparation = undefined;
|
|
8968
|
+
reminderManager = undefined;
|
|
8790
8969
|
// Pi tears down this registry on reload as well as session replacement/quit.
|
|
8791
8970
|
try { candidateViews?.cleanupAll(); } catch { /* Preserve failed owned views for later recovery. */ }
|
|
8792
8971
|
const reason = (event as { reason?: unknown }).reason;
|
|
@@ -8801,6 +8980,83 @@ function createGentleAiExtensionForTesting(
|
|
|
8801
8980
|
processAgentEndSubagentDepth.delete(sessionKey);
|
|
8802
8981
|
});
|
|
8803
8982
|
|
|
8983
|
+
// gentle-pi ODD input phase labels: the explicit half of the bounded ODD
|
|
8984
|
+
// phase signal for the Gentle prompt's working label. Gentle Shell infers
|
|
8985
|
+
// the phase from the primary session's tool activity
|
|
8986
|
+
// (lib/odd-phase-inference.ts); this tool lets the orchestrator refine it
|
|
8987
|
+
// at ODD protocol transitions tools cannot show, and its report is
|
|
8988
|
+
// "explicit" so a following read-only tool call never downgrades it. Never
|
|
8989
|
+
// inferred from prose. It is session-scoped in lib/odd-phase.ts: a
|
|
8990
|
+
// background/child agent runs as its own OS process with its own module
|
|
8991
|
+
// state, so it can never see or override the primary session's phase.
|
|
8992
|
+
const hiddenOddPhaseToolComponent = { render: (_width: number): string[] => [], invalidate() {} };
|
|
8993
|
+
pi.registerTool({
|
|
8994
|
+
name: "gentle_odd_phase",
|
|
8995
|
+
renderShell: "self",
|
|
8996
|
+
label: "Gentle ODD Phase",
|
|
8997
|
+
description: "Refine the primary session's current ODD phase for the Gentle prompt's working label, which is otherwise inferred automatically from tool activity. Best-effort UI only; never a source of truth for orchestration logic.",
|
|
8998
|
+
promptSnippet: "Refine the automatically inferred working label with authorizing/exploring/researching/deciding/planning/implementing/checking/closing only at real ODD phase transitions of the primary turn; never poll or report per tool call.",
|
|
8999
|
+
promptGuidelines: [
|
|
9000
|
+
`phase must be exactly one of ${ODD_PHASES.join(", ")}, or "clear" to leave the current phase before its turn ends. Call this only when the ODD phase actually changes for the primary session's active turn, not on every tool call or thought.`,
|
|
9001
|
+
"Never call this from a subagent or background/child task; it reports only the primary orchestrator's own phase, and a child's session id can never override the parent's label.",
|
|
9002
|
+
],
|
|
9003
|
+
parameters: {
|
|
9004
|
+
type: "object",
|
|
9005
|
+
additionalProperties: false,
|
|
9006
|
+
required: ["phase"],
|
|
9007
|
+
properties: {
|
|
9008
|
+
phase: { type: "string", enum: [...ODD_PHASES, "clear"], description: "The reported ODD phase, or 'clear' to leave the current phase." },
|
|
9009
|
+
},
|
|
9010
|
+
} as const,
|
|
9011
|
+
executionMode: "parallel",
|
|
9012
|
+
// The prompt editor owns the success indicator. An empty self-rendered
|
|
9013
|
+
// call avoids Pi's default transcript card while preserving error output.
|
|
9014
|
+
renderCall() {
|
|
9015
|
+
return hiddenOddPhaseToolComponent;
|
|
9016
|
+
},
|
|
9017
|
+
renderResult(result, _options, theme, context) {
|
|
9018
|
+
if (!context.isError) return hiddenOddPhaseToolComponent;
|
|
9019
|
+
const message = result.content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
9020
|
+
return new Text(theme.fg("error", sanitizeTerminalText(message || "ODD phase report failed.")), 0, 0);
|
|
9021
|
+
},
|
|
9022
|
+
async execute(_toolCallId, parameters, _signal, _onUpdate, ctx) {
|
|
9023
|
+
const phase = (parameters as { phase?: unknown }).phase;
|
|
9024
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
9025
|
+
// Pi's own contract (docs/extensions.md, Signaling errors): throw to
|
|
9026
|
+
// mark a tool execution as failed (sets isError: true on the result);
|
|
9027
|
+
// returning a value -- including an isError property on it -- never
|
|
9028
|
+
// sets that flag.
|
|
9029
|
+
if (!sessionId) throw new Error("No active session to report an ODD phase for; nothing changed.");
|
|
9030
|
+
// An invalid token never resets state: only the explicit "clear" token
|
|
9031
|
+
// leaves the current phase. This keeps a malformed report from wiping
|
|
9032
|
+
// out an otherwise-accurate label for the rest of the turn.
|
|
9033
|
+
if (phase === "clear") {
|
|
9034
|
+
oddPhaseRegistry.clear(sessionId);
|
|
9035
|
+
return { content: [{ type: "text", text: "ODD phase cleared." }], details: { phase: undefined } };
|
|
9036
|
+
}
|
|
9037
|
+
if (!isOddPhase(phase)) {
|
|
9038
|
+
throw new Error(`Invalid ODD phase; use one of ${ODD_PHASES.join(", ")}, or "clear". The previously reported phase, if any, is unchanged.`);
|
|
9039
|
+
}
|
|
9040
|
+
const reported = oddPhaseRegistry.report(sessionId, phase, "explicit");
|
|
9041
|
+
return { content: [{ type: "text", text: `ODD phase reported: ${reported}` }], details: { phase: reported } };
|
|
9042
|
+
},
|
|
9043
|
+
});
|
|
9044
|
+
|
|
9045
|
+
const GENTLE_TIMED_TOOLS = new Set(["gentle_review_scope", "gentle_review_capture_group", "gentle_review_capture", "gentle_review"]);
|
|
9046
|
+
let elapsedTiming: GentleAiElapsedTimingLedger | undefined;
|
|
9047
|
+
const recordReviewTiming = (event: { toolCallId: string; toolName: string }, endedAt?: number): void => {
|
|
9048
|
+
const ledger = elapsedTiming;
|
|
9049
|
+
if (!ledger || !GENTLE_TIMED_TOOLS.has(event.toolName)) return;
|
|
9050
|
+
try {
|
|
9051
|
+
if (endedAt === undefined) ledger.recordStart(event.toolCallId, Date.now());
|
|
9052
|
+
else ledger.recordEnd(event.toolCallId, endedAt);
|
|
9053
|
+
} catch { /* Timing persistence is best-effort and never breaks the tool event. */ }
|
|
9054
|
+
};
|
|
9055
|
+
pi.on("tool_execution_start", (event) => recordReviewTiming(event));
|
|
9056
|
+
pi.on("tool_execution_end", (event) => recordReviewTiming(event, Date.now()));
|
|
9057
|
+
const timingContext = (context: GentleAiRenderContext | undefined): GentleAiRenderContext | undefined =>
|
|
9058
|
+
elapsedTiming && context ? { ...context, elapsedTiming } : context;
|
|
9059
|
+
|
|
8804
9060
|
pi.registerTool({
|
|
8805
9061
|
name: "gentle_review_scope",
|
|
8806
9062
|
renderShell: "self",
|
|
@@ -8812,11 +9068,11 @@ function createGentleAiExtensionForTesting(
|
|
|
8812
9068
|
return renderGentleAiLifecycleCall(
|
|
8813
9069
|
"review scope",
|
|
8814
9070
|
theme,
|
|
8815
|
-
context as GentleAiRenderContext | undefined,
|
|
9071
|
+
timingContext(context as GentleAiRenderContext | undefined),
|
|
8816
9072
|
);
|
|
8817
9073
|
},
|
|
8818
9074
|
renderResult(result, options, theme, context) {
|
|
8819
|
-
return renderGentleAiResult(result, options, theme, context as GentleAiRenderContext | undefined);
|
|
9075
|
+
return renderGentleAiResult(result, options, theme, timingContext(context as GentleAiRenderContext | undefined));
|
|
8820
9076
|
},
|
|
8821
9077
|
async execute(_toolCallId, parameters) {
|
|
8822
9078
|
const input = parameters as ReviewScopeParameters;
|
|
@@ -8844,7 +9100,7 @@ function createGentleAiExtensionForTesting(
|
|
|
8844
9100
|
return named.length === 0 ? operation : `${operation} · ${named.join(" · ")}`;
|
|
8845
9101
|
};
|
|
8846
9102
|
|
|
8847
|
-
pi.registerTool({
|
|
9103
|
+
pi.registerTool(reviewSidebar.tool({
|
|
8848
9104
|
name: "gentle_review_capture_group",
|
|
8849
9105
|
renderShell: "self",
|
|
8850
9106
|
label: "Gentle Review Capture Group",
|
|
@@ -8859,13 +9115,13 @@ function createGentleAiExtensionForTesting(
|
|
|
8859
9115
|
renderCall(args, theme, context) {
|
|
8860
9116
|
const bindings = (args as { collectBindings?: unknown }).collectBindings;
|
|
8861
9117
|
const lenses = Array.isArray(bindings) ? bindings.map(collectBindingLens) : [];
|
|
8862
|
-
return renderGentleAiLifecycleCall(withLenses("review capture group", lenses), theme, context as GentleAiRenderContext | undefined);
|
|
9118
|
+
return renderGentleAiLifecycleCall(withLenses("review capture group", lenses), theme, timingContext(context as GentleAiRenderContext | undefined));
|
|
8863
9119
|
},
|
|
8864
9120
|
renderResult(result, options, theme, context) {
|
|
8865
|
-
return renderGentleAiResult(result, options, theme, context as GentleAiRenderContext | undefined);
|
|
9121
|
+
return renderGentleAiResult(result, options, theme, timingContext(context as GentleAiRenderContext | undefined));
|
|
8866
9122
|
},
|
|
8867
9123
|
async execute(_toolCallId, parameters, signal, _onUpdate, ctx) {
|
|
8868
|
-
if (signal?.aborted) throw
|
|
9124
|
+
if (signal?.aborted) throw reviewCancellation("Review capture group was cancelled");
|
|
8869
9125
|
const details = await executeReviewCaptureGroupOperation(
|
|
8870
9126
|
parameters,
|
|
8871
9127
|
ctx.cwd,
|
|
@@ -8882,9 +9138,9 @@ function createGentleAiExtensionForTesting(
|
|
|
8882
9138
|
);
|
|
8883
9139
|
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
8884
9140
|
},
|
|
8885
|
-
});
|
|
9141
|
+
}));
|
|
8886
9142
|
|
|
8887
|
-
pi.registerTool({
|
|
9143
|
+
pi.registerTool(reviewSidebar.tool({
|
|
8888
9144
|
name: "gentle_review_capture",
|
|
8889
9145
|
renderShell: "self",
|
|
8890
9146
|
label: "Gentle Review Capture",
|
|
@@ -8901,14 +9157,14 @@ function createGentleAiExtensionForTesting(
|
|
|
8901
9157
|
return renderGentleAiLifecycleCall(
|
|
8902
9158
|
withLenses("review capture", [collectBindingLens((args as { collectBinding?: unknown }).collectBinding)]),
|
|
8903
9159
|
theme,
|
|
8904
|
-
context as GentleAiRenderContext | undefined,
|
|
9160
|
+
timingContext(context as GentleAiRenderContext | undefined),
|
|
8905
9161
|
);
|
|
8906
9162
|
},
|
|
8907
9163
|
renderResult(result, options, theme, context) {
|
|
8908
|
-
return renderGentleAiResult(result, options, theme, context as GentleAiRenderContext | undefined);
|
|
9164
|
+
return renderGentleAiResult(result, options, theme, timingContext(context as GentleAiRenderContext | undefined));
|
|
8909
9165
|
},
|
|
8910
9166
|
async execute(_toolCallId, parameters, signal, _onUpdate, ctx) {
|
|
8911
|
-
if (signal?.aborted) throw
|
|
9167
|
+
if (signal?.aborted) throw reviewCancellation("Review capture was cancelled");
|
|
8912
9168
|
const details = await executeReviewCaptureOperation(
|
|
8913
9169
|
parameters,
|
|
8914
9170
|
ctx.cwd,
|
|
@@ -8928,9 +9184,9 @@ function createGentleAiExtensionForTesting(
|
|
|
8928
9184
|
details,
|
|
8929
9185
|
};
|
|
8930
9186
|
},
|
|
8931
|
-
});
|
|
9187
|
+
}));
|
|
8932
9188
|
|
|
8933
|
-
pi.registerTool({
|
|
9189
|
+
pi.registerTool(reviewSidebar.tool({
|
|
8934
9190
|
name: "gentle_review",
|
|
8935
9191
|
renderShell: "self",
|
|
8936
9192
|
label: "Gentle Review Controller",
|
|
@@ -8946,7 +9202,7 @@ function createGentleAiExtensionForTesting(
|
|
|
8946
9202
|
"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.",
|
|
8947
9203
|
"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.",
|
|
8948
9204
|
"Use gentle_review only for native review authority operations; delivery commands follow ordinary repository policy.",
|
|
8949
|
-
'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
|
|
9205
|
+
'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"}). 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). A missing model, a mini model token (gemini is not mini), or low effort keeps the conservative small-model bias. The on-path (writer self-verification is the record, no separate verifier) holds only when RDD reads on and nativeReviewOutcome resolves to "closed" for this candidate. 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. A declined, unavailable, or unknown outcome falls back to the exact risk-gated plan RDD off would return, re-enabling the separate verifier; unknown is never treated as closed, and a decline is candidate-scoped and never lowers the bar below RDD off. 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 reports risk "unassessable", verified exactly like "high". This never mutates review authority state.',
|
|
8950
9206
|
],
|
|
8951
9207
|
parameters: REVIEW_CONTROLLER_PARAMETERS,
|
|
8952
9208
|
executionMode: "sequential",
|
|
@@ -8954,14 +9210,14 @@ function createGentleAiExtensionForTesting(
|
|
|
8954
9210
|
return renderGentleAiLifecycleCall(
|
|
8955
9211
|
reviewToolOperationPath(args),
|
|
8956
9212
|
theme,
|
|
8957
|
-
context as GentleAiRenderContext | undefined,
|
|
9213
|
+
timingContext(context as GentleAiRenderContext | undefined),
|
|
8958
9214
|
);
|
|
8959
9215
|
},
|
|
8960
9216
|
renderResult(result, options, theme, context) {
|
|
8961
|
-
return renderGentleAiResult(result, options, theme, context as GentleAiRenderContext | undefined);
|
|
9217
|
+
return renderGentleAiResult(result, options, theme, timingContext(context as GentleAiRenderContext | undefined));
|
|
8962
9218
|
},
|
|
8963
9219
|
async execute(_toolCallId, parameters, signal, _onUpdate, ctx) {
|
|
8964
|
-
if (signal?.aborted) throw
|
|
9220
|
+
if (signal?.aborted) throw reviewCancellation("Review controller operation was cancelled");
|
|
8965
9221
|
await authorizeDestructiveReviewOperation(parameters, ctx);
|
|
8966
9222
|
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
8967
9223
|
const retainedSelections = processRetainedNativeStatusSelections.get(sessionKey)
|
|
@@ -9076,15 +9332,32 @@ function createGentleAiExtensionForTesting(
|
|
|
9076
9332
|
details,
|
|
9077
9333
|
};
|
|
9078
9334
|
},
|
|
9079
|
-
});
|
|
9080
|
-
|
|
9081
|
-
function runSddPreflight(ctx: ExtensionContext, promptFields: readonly SddPreflightField[] = []): Promise<SddPreflightPreferences> {
|
|
9082
|
-
return ensureSddPreflight(ctx, { pi, installAssets: (cwd) => installPackageAssets(cwd, true, ["sdd"]), applyModelConfig: async () => applySavedModelConfig(ctx) }, { promptFields });
|
|
9083
|
-
}
|
|
9335
|
+
}));
|
|
9084
9336
|
|
|
9085
9337
|
pi.on("session_start", async (event, ctx) => {
|
|
9338
|
+
yolo.reset(ctx);
|
|
9339
|
+
reviewSidebar.reset(ctx);
|
|
9340
|
+
elapsedTiming = new GentleAiElapsedTimingLedger(ctx.sessionManager, pi);
|
|
9086
9341
|
reminderSessionActive = true;
|
|
9087
9342
|
reminderEpoch += 1;
|
|
9343
|
+
unbindPreparation?.();
|
|
9344
|
+
reminderManager = ctx.sessionManager;
|
|
9345
|
+
const epoch = reminderEpoch;
|
|
9346
|
+
const manager = ctx.sessionManager;
|
|
9347
|
+
const originalCwd = manager?.getCwd?.() ?? ctx.cwd;
|
|
9348
|
+
unbindPreparation = manager ? bindSessionRepositoryPreparation(manager, originalCwd, async (root, current, signal) => {
|
|
9349
|
+
if (!current() || nativeReviewCli?.targetStatus === undefined || safeBootstrapDirectory(originalCwd) !== root) return false;
|
|
9350
|
+
if (!await nativeRepositoryPreparationEnabled(nativeReviewCli, root, signal) || !current() || safeBootstrapDirectory(originalCwd) !== root) return false;
|
|
9351
|
+
try {
|
|
9352
|
+
const existing = resolveSessionWorktree(originalCwd, originalCwd);
|
|
9353
|
+
if (existing) return existing.root === root;
|
|
9354
|
+
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
9355
|
+
const retained = processRetainedNativeStatusSelections.get(sessionKey) ?? new Map();
|
|
9356
|
+
const negotiated = await negotiatedStatusForHostTransport(nativeReviewCli, { cwd: root, signal }, retained, root);
|
|
9357
|
+
const identity = resolveSessionWorktree(originalCwd, originalCwd);
|
|
9358
|
+
return negotiated.status !== undefined && current() && safeBootstrapDirectory(originalCwd) === root && identity?.root === root;
|
|
9359
|
+
} catch { return false; }
|
|
9360
|
+
}, () => reminderSessionActive && reminderEpoch === epoch && reminderManager === manager) : undefined;
|
|
9088
9361
|
try { candidateViews?.sweepOrphans(ctx.cwd); } catch { /* Ownership sweeping must not block startup. */ }
|
|
9089
9362
|
const reason = (event as { reason?: unknown }).reason;
|
|
9090
9363
|
if (reason !== "reload") revokeCurrentReviewSessionPermission(ctx);
|
|
@@ -9135,42 +9408,23 @@ function createGentleAiExtensionForTesting(
|
|
|
9135
9408
|
}
|
|
9136
9409
|
});
|
|
9137
9410
|
|
|
9138
|
-
pi.on("input", async (event, ctx) => {
|
|
9139
|
-
if (typeof event.text !== "string" || !isSddPreflightTrigger(event.text)) {
|
|
9140
|
-
return { action: "continue" };
|
|
9141
|
-
}
|
|
9142
|
-
// An RPC child consumes the parent-rendered preflight block transported in
|
|
9143
|
-
// its task context; it never originates preflight. Re-entering the
|
|
9144
|
-
// parent-only resolver here would reject, and consuming the rejection
|
|
9145
|
-
// would swallow the delegated prompt before the agent ever starts.
|
|
9146
|
-
if (ctx.mode === "rpc") return { action: "continue" };
|
|
9147
|
-
try { await runSddPreflight(ctx); }
|
|
9148
|
-
catch (error) {
|
|
9149
|
-
if (ctx.hasUI) ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
9150
|
-
return { action: "handled" };
|
|
9151
|
-
}
|
|
9152
|
-
return { action: "continue" };
|
|
9153
|
-
});
|
|
9154
|
-
|
|
9155
|
-
let nativeSddStartupBlock: string | undefined;
|
|
9156
9411
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
9157
|
-
nativeSddStartupBlock = undefined;
|
|
9158
|
-
const retiredSync = readAgentStartNames(event).includes("sdd-sync") || /\bSDD sync executor\b/i.test(event.systemPrompt ?? "");
|
|
9159
|
-
const isSddAgent = retiredSync || isSddAgentStartEvent(event);
|
|
9160
9412
|
const isNamedAgent = isNamedAgentStartEvent(event);
|
|
9413
|
+
const isChildSession = permissionEnvironment.GENTLE_PI_AGENTS_CHILD === "1";
|
|
9414
|
+
const isPrimarySession = !isNamedAgent && !isChildSession;
|
|
9161
9415
|
const subagentDepthKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
9162
|
-
if (
|
|
9416
|
+
if (isNamedAgent || isChildSession) {
|
|
9163
9417
|
processAgentEndSubagentDepth.set(subagentDepthKey, (processAgentEndSubagentDepth.get(subagentDepthKey) ?? 0) + 1);
|
|
9164
9418
|
} else {
|
|
9165
9419
|
processAgentEndSubagentDepth.set(subagentDepthKey, 0);
|
|
9166
9420
|
}
|
|
9167
9421
|
// gentle-pi#677: nudge gentle-ai's own telemetry trigger for a primary
|
|
9168
|
-
// session only, reusing the exact isNamedAgent
|
|
9422
|
+
// session only, reusing the exact isNamedAgent predicate that
|
|
9169
9423
|
// decides the orchestrator prompt below. At most one attempt per
|
|
9170
9424
|
// process regardless of how many primary-session before_agent_start
|
|
9171
9425
|
// events this process observes; a missing/old binary or a spawn error
|
|
9172
9426
|
// must never affect activation, so every failure is swallowed silently.
|
|
9173
|
-
if (
|
|
9427
|
+
if (isPrimarySession && !processTelemetryTriggerAttempted) {
|
|
9174
9428
|
processTelemetryTriggerAttempted = true;
|
|
9175
9429
|
try {
|
|
9176
9430
|
const executable = resolveTelemetryTriggerBinary();
|
|
@@ -9184,60 +9438,14 @@ function createGentleAiExtensionForTesting(
|
|
|
9184
9438
|
// Best-effort only; never surfaced and never affects activation.
|
|
9185
9439
|
}
|
|
9186
9440
|
}
|
|
9187
|
-
try {
|
|
9188
|
-
if (isSddAgent && !getSddPreflightPreferences(ctx) && ctx.mode !== "rpc") {
|
|
9189
|
-
await runSddPreflight(ctx);
|
|
9190
|
-
}
|
|
9191
|
-
} catch (error) {
|
|
9192
|
-
// Pi logs thrown before_agent_start errors and continues. Return an
|
|
9193
|
-
// unresolved gate instead of silently losing the preflight instructions.
|
|
9194
|
-
return { systemPrompt: `${event.systemPrompt}\n\nSDD preflight unresolved: ${error instanceof Error ? error.message : String(error)}\nSTOP: Do not initialize the project, launch phases, write artifacts, or infer consent. Request session preflight confirmation before continuing.` };
|
|
9195
|
-
}
|
|
9196
|
-
const prefs = getSddPreflightPreferences(ctx);
|
|
9197
|
-
// RPC children never resolve or persist defaults. The parent dispatch gate
|
|
9198
|
-
// transports the rendered block in the existing task context, and Gentle
|
|
9199
|
-
// Agents refuses a missing/malformed payload before spawning the child.
|
|
9200
|
-
const sddPrompt =
|
|
9201
|
-
prefs && (!isNamedAgent || isSddAgent)
|
|
9202
|
-
? `\n\n${renderSddPreflightPrompt(prefs)}`
|
|
9203
|
-
: "";
|
|
9204
|
-
const phase = isSddAgent ? sddPhaseFromAgentStartEvent(event) : undefined;
|
|
9205
|
-
const launchSddChange = readSddChangeFlag(pi);
|
|
9206
|
-
if (retiredSync) nativeSddStartupBlock = "Standalone sync is retired. Return to the parent for native archive selection; archive composes applicable specs.";
|
|
9207
|
-
else if (launchSddChange !== undefined && !phase) nativeSddStartupBlock = "Receiving agent has no recognized SDD phase";
|
|
9208
|
-
const nativeStatusPrompt = phase
|
|
9209
|
-
? await (async () => {
|
|
9210
|
-
try {
|
|
9211
|
-
if (launchSddChange === undefined) {
|
|
9212
|
-
const { status } = await readCommandSddStatus("", ctx);
|
|
9213
|
-
if (status.changeName === null) throw new Error(`Native SDD discovery cannot run ${phase}.`);
|
|
9214
|
-
assertNativeSddPhaseReady(status, phase);
|
|
9215
|
-
return `\n\n${renderNativeSddPhasePrompt(status, phase)}`;
|
|
9216
|
-
}
|
|
9217
|
-
const agentName = `sdd-${phase}`;
|
|
9218
|
-
const startup = await resolveSelectedNativeSddChangeStartup(
|
|
9219
|
-
launchSddChange,
|
|
9220
|
-
ctx.cwd,
|
|
9221
|
-
agentName,
|
|
9222
|
-
nativeReviewCli,
|
|
9223
|
-
);
|
|
9224
|
-
return `\n\n${renderNativeSddPhasePrompt(startup.status, phase)}`;
|
|
9225
|
-
} catch (error) {
|
|
9226
|
-
nativeSddStartupBlock = error instanceof Error ? error.message : String(error);
|
|
9227
|
-
return `\n\n## Native SDD Status Engine\nSDD selection blocked: ${nativeSddStartupBlock}\nDo not run phase work; return this blocker to the parent.`;
|
|
9228
|
-
}
|
|
9229
|
-
})()
|
|
9230
|
-
: nativeSddStartupBlock === undefined
|
|
9231
|
-
? ""
|
|
9232
|
-
: `\n\n## Native SDD Status Engine\nSDD selection blocked: ${nativeSddStartupBlock}\nDo not run phase work; return this blocker to the parent.`;
|
|
9233
9441
|
// gentle-pi#661: the RDD status line (and the rest of the gentle prompt)
|
|
9234
9442
|
// is built only for the primary session, mirrored on the
|
|
9235
|
-
// reviewContractPrompt condition below -- named
|
|
9443
|
+
// reviewContractPrompt condition below -- named agents never reach
|
|
9236
9444
|
// this branch, so no line is resolved or computed for them.
|
|
9237
9445
|
// resolveRddStatusLine never throws and never hangs past
|
|
9238
9446
|
// RDD_STATUS_TIMEOUT_MS: an absent/timed-out/aborted/failing native
|
|
9239
9447
|
// binary renders the fail-closed "unknown" line instead.
|
|
9240
|
-
const gentlePrompt =
|
|
9448
|
+
const gentlePrompt = !isPrimarySession
|
|
9241
9449
|
? ""
|
|
9242
9450
|
: `\n\n${buildGentlePrompt(
|
|
9243
9451
|
readPersonaMode(ctx.cwd),
|
|
@@ -9249,15 +9457,18 @@ function createGentleAiExtensionForTesting(
|
|
|
9249
9457
|
// contract bundle's review execution contract for the primary session
|
|
9250
9458
|
// only, and only when a native review CLI is actually present.
|
|
9251
9459
|
const reviewContractPrompt =
|
|
9252
|
-
|
|
9460
|
+
isPrimarySession && nativeReviewCli !== null
|
|
9253
9461
|
? (() => {
|
|
9254
9462
|
const fragment = loadReviewContractPromptFragment(ctx);
|
|
9255
9463
|
return fragment === null ? "" : `\n\n${fragment}`;
|
|
9256
9464
|
})()
|
|
9257
9465
|
: "";
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9466
|
+
// gentle-shell#1485: pi-claude-bridge drops a handler-returned systemPrompt
|
|
9467
|
+
// and forwards only systemPromptOptions, so the harness is delivered
|
|
9468
|
+
// through the mutable appendSystemPrompt section instead of a replacement.
|
|
9469
|
+
appendSystemPromptOnce(event.systemPromptOptions, `${gentlePrompt}${reviewContractPrompt}`);
|
|
9470
|
+
updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx));
|
|
9471
|
+
return undefined;
|
|
9261
9472
|
});
|
|
9262
9473
|
|
|
9263
9474
|
// gentle-pi#556 / gentle-ai#4051: with RDD enabled, the agent could finish
|
|
@@ -9267,6 +9478,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9267
9478
|
// consent, or chooses a partial candidate. Durable own-mutation receipts
|
|
9268
9479
|
// gate STATUS and consume only the generation captured before that await.
|
|
9269
9480
|
pi.on("agent_end", async (_event, ctx) => {
|
|
9481
|
+
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD === "1") return;
|
|
9270
9482
|
if (nativeReviewCli?.reviewMode === undefined || nativeReviewCli.targetStatus === undefined) return;
|
|
9271
9483
|
if (ctx.hasUI !== true || !reminderSessionActive) return;
|
|
9272
9484
|
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
@@ -9299,65 +9511,29 @@ function createGentleAiExtensionForTesting(
|
|
|
9299
9511
|
consumeReviewMutation(pi, ctx.sessionManager, root, mutation, "nudged", targetIdentity);
|
|
9300
9512
|
});
|
|
9301
9513
|
|
|
9302
|
-
pi.on("tool_result", (event, ctx) => {
|
|
9303
|
-
if (!reminderSessionActive || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
9514
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
9515
|
+
if (!reminderSessionActive || (reminderManager && reminderManager !== ctx.sessionManager) || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
9304
9516
|
if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
|
|
9305
9517
|
try {
|
|
9306
|
-
const
|
|
9307
|
-
|
|
9308
|
-
|
|
9518
|
+
const identity = resolveSessionWorktree(event.input.path, ctx.cwd);
|
|
9519
|
+
const prospectiveRoot = identity ? undefined : sourcePathWithinProject(event.input.path, ctx.cwd);
|
|
9520
|
+
const root = identity?.root ?? prospectiveRoot;
|
|
9521
|
+
// Persist the observed own write before any await. Preparation is not
|
|
9522
|
+
// mutation evidence, and cannot invent a pre-write Changes baseline.
|
|
9523
|
+
if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
|
|
9524
|
+
if (prospectiveRoot && !resolveSessionWorktree(ctx.cwd, ctx.cwd)) await prepareBoundSessionRepository(ctx.sessionManager, ctx.sessionManager.getCwd?.() ?? ctx.cwd, ctx.signal);
|
|
9525
|
+
} catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
|
|
9309
9526
|
});
|
|
9310
9527
|
|
|
9311
9528
|
pi.on("tool_call", async (event, ctx) => {
|
|
9312
|
-
|
|
9529
|
+
const primaryToolCall = (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
|
|
9530
|
+
const yoloActive = primaryToolCall && await yolo.active(ctx);
|
|
9313
9531
|
const sensitivePathDenied = evaluateSensitivePathTool(
|
|
9314
9532
|
event.toolName,
|
|
9315
9533
|
event.input,
|
|
9316
9534
|
);
|
|
9317
9535
|
if (sensitivePathDenied) return sensitivePathDenied;
|
|
9318
9536
|
if (event.toolName === "subagent_run") {
|
|
9319
|
-
const sddAgent = sddDispatchAgentName(event.input);
|
|
9320
|
-
if (sddAgent === "invalid") {
|
|
9321
|
-
return { block: true, reason: "SDD dispatch requires exactly one shipped SDD agent name." };
|
|
9322
|
-
}
|
|
9323
|
-
if (sddAgent !== undefined) {
|
|
9324
|
-
// An RPC child is a delegated actor, never an authority originator. It
|
|
9325
|
-
// must receive the already-confirmed block from its interactive parent.
|
|
9326
|
-
if (ctx.mode === "rpc") {
|
|
9327
|
-
return { block: true, reason: "SDD dispatch refused: an RPC child cannot originate or persist SDD preflight defaults." };
|
|
9328
|
-
}
|
|
9329
|
-
try {
|
|
9330
|
-
const prefs = getSddPreflightPreferences(ctx) ?? await runSddPreflight(ctx);
|
|
9331
|
-
const rendered = renderSddPreflightPrompt(prefs);
|
|
9332
|
-
if (!prefs.prompted && ctx.hasUI) {
|
|
9333
|
-
return { block: true, reason: "SDD dispatch refused: interactive parent preflight lacks current-session confirmation." };
|
|
9334
|
-
}
|
|
9335
|
-
if (!isRecord(event.input)) {
|
|
9336
|
-
return { block: true, reason: "SDD dispatch refused: child input is malformed." };
|
|
9337
|
-
}
|
|
9338
|
-
if (event.input.context !== undefined && typeof event.input.context !== "string") {
|
|
9339
|
-
return { block: true, reason: "SDD dispatch refused: child context must be text." };
|
|
9340
|
-
}
|
|
9341
|
-
// The existing context payload is the sole parent-to-child transport.
|
|
9342
|
-
// Refuse a caller-authored lookalike so the child receives one exact,
|
|
9343
|
-
// parent-rendered authority block rather than an ambiguous mixture.
|
|
9344
|
-
const context = typeof event.input.context === "string" ? event.input.context.trim() : "";
|
|
9345
|
-
if (/^## SDD Session Preflight[ \t]*$/m.test(context)) {
|
|
9346
|
-
return { block: true, reason: "SDD dispatch refused: child context already contains an untrusted preflight block." };
|
|
9347
|
-
}
|
|
9348
|
-
event.input.context = context.length === 0
|
|
9349
|
-
? rendered
|
|
9350
|
-
: `${rendered}\n\n${context}`;
|
|
9351
|
-
if (!isParentConfirmedSddPreflightContext(event.input.context)) {
|
|
9352
|
-
return { block: true, reason: "SDD dispatch refused: rendered preflight transport is malformed." };
|
|
9353
|
-
}
|
|
9354
|
-
} catch (error) {
|
|
9355
|
-
return {
|
|
9356
|
-
block: true,
|
|
9357
|
-
reason: `SDD dispatch refused before child launch: ${error instanceof Error ? error.message : String(error)}`,
|
|
9358
|
-
};
|
|
9359
|
-
}
|
|
9360
|
-
}
|
|
9361
9537
|
const judgmentDayFixDenied = rejectInvalidJudgmentDayFixDispatch(event.input);
|
|
9362
9538
|
if (judgmentDayFixDenied) return judgmentDayFixDenied;
|
|
9363
9539
|
const writerScopeDenied = rejectUnscopedBoundedWriterDispatch(event.input);
|
|
@@ -9376,11 +9552,15 @@ function createGentleAiExtensionForTesting(
|
|
|
9376
9552
|
if (!isRecord(event.input) || typeof event.input.command !== "string") {
|
|
9377
9553
|
return undefined;
|
|
9378
9554
|
}
|
|
9379
|
-
|
|
9555
|
+
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD === "1") {
|
|
9556
|
+
const childDenied = blockChildDestructiveCommand(event.input.command);
|
|
9557
|
+
if (childDenied) return childDenied;
|
|
9558
|
+
}
|
|
9559
|
+
return await confirmCommand(event.input.command, ctx, pi.events, herdrLifecycle, yoloActive);
|
|
9380
9560
|
});
|
|
9381
9561
|
|
|
9382
|
-
for (const owner of ["delegation", "review"
|
|
9383
|
-
const label = owner
|
|
9562
|
+
for (const owner of ["delegation", "review"] as const) {
|
|
9563
|
+
const label = owner;
|
|
9384
9564
|
pi.registerCommand(`gentle:install-${owner}`, {
|
|
9385
9565
|
description: `Repair or refresh only global Gentle AI ${label} assets.`,
|
|
9386
9566
|
handler: async (args, ctx) => {
|
|
@@ -9394,86 +9574,10 @@ function createGentleAiExtensionForTesting(
|
|
|
9394
9574
|
});
|
|
9395
9575
|
}
|
|
9396
9576
|
|
|
9397
|
-
pi.registerCommand("gentle:sdd-preflight", {
|
|
9398
|
-
description:
|
|
9399
|
-
"Run or reuse session SDD preflight; use --edit to change preferences.",
|
|
9400
|
-
handler: async (args, ctx) => {
|
|
9401
|
-
if (args.trim() !== "" && args.trim() !== "--edit") {
|
|
9402
|
-
ctx.ui.notify("Usage: /gentle:sdd-preflight [--edit]", "warning");
|
|
9403
|
-
return;
|
|
9404
|
-
}
|
|
9405
|
-
try {
|
|
9406
|
-
await runSddPreflight(ctx, args.trim() === "--edit" ? SDD_PREFLIGHT_FIELDS : []);
|
|
9407
|
-
} catch (error) {
|
|
9408
|
-
ctx.ui?.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
9409
|
-
}
|
|
9410
|
-
},
|
|
9411
|
-
});
|
|
9412
|
-
|
|
9413
|
-
const readCommandSddStatus = async (args: string, ctx: ExtensionContext) => {
|
|
9414
|
-
const parsed = parseSddStatusCommandArgs(args);
|
|
9415
|
-
const request = { changeName: parsed.changeName, workspaceRoot: realpathSync(ctx.cwd) };
|
|
9416
|
-
if (!nativeReviewCli?.sddStatus) throw new Error("Native SDD status capability unavailable; no local fallback.");
|
|
9417
|
-
const status = decodeNativeSddStatusV2(await nativeReviewCli.sddStatus(request), request);
|
|
9418
|
-
return { parsed, request, status };
|
|
9419
|
-
};
|
|
9420
|
-
const showCommandSddStatus = (status: NativeSddStatusV2, json: boolean, ctx: ExtensionContext) => {
|
|
9421
|
-
ctx.ui?.notify(json ? JSON.stringify(status, null, 2) : renderNativeSddPhasePrompt(status), "info");
|
|
9422
|
-
};
|
|
9423
|
-
const handleSddStatusCommand = async (args: string, ctx: ExtensionContext) => {
|
|
9424
|
-
const { parsed, status } = await readCommandSddStatus(args, ctx);
|
|
9425
|
-
showCommandSddStatus(status, parsed.json, ctx);
|
|
9426
|
-
};
|
|
9427
|
-
|
|
9428
|
-
pi.registerCommand("gentle-sdd-status", {
|
|
9429
|
-
description: "Show deterministic SDD change status and instructions.",
|
|
9430
|
-
handler: async (args, ctx) => {
|
|
9431
|
-
await handleSddStatusCommand(args, ctx);
|
|
9432
|
-
},
|
|
9433
|
-
});
|
|
9434
|
-
|
|
9435
|
-
const handleSddContinueCommand = async (args: string, ctx: ExtensionContext) => {
|
|
9436
|
-
const { parsed, request, status } = await readCommandSddStatus(args, ctx);
|
|
9437
|
-
const planning = status.planningHome;
|
|
9438
|
-
const changeRoot = status.changeRoot;
|
|
9439
|
-
// Native context is an upper bound, never the human's per-call grant.
|
|
9440
|
-
if (status.changeName === null || !ctx.hasUI || typeof ctx.ui?.confirm !== "function" || !nativeReviewCli?.sddContinue) {
|
|
9441
|
-
showCommandSddStatus(status, parsed.json, ctx);
|
|
9442
|
-
return;
|
|
9443
|
-
}
|
|
9444
|
-
if (typeof planning !== "object" || planning === null || !("path" in planning) || typeof planning.path !== "string" || typeof changeRoot !== "string") throw new Error("Native SDD continuation lacks an exact planning path.");
|
|
9445
|
-
if (!["openspec", "both"].includes(String(status.artifactStore)) || !["repo-local", "workspace-planning"].includes(String(status.actionContext.mode))) throw new Error("Native SDD continuation has unsupported planning context.");
|
|
9446
|
-
const marker = join(changeRoot, ".gentle-ai-instance");
|
|
9447
|
-
const checkMarker = () => {
|
|
9448
|
-
try {
|
|
9449
|
-
if (!lstatSync(marker).isFile() || realpathSync(marker) !== marker) throw new Error("Native SDD marker is not a canonical regular file.");
|
|
9450
|
-
} catch (error) {
|
|
9451
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
9452
|
-
}
|
|
9453
|
-
};
|
|
9454
|
-
checkMarker();
|
|
9455
|
-
if (realpathSync(changeRoot) !== changeRoot || realpathSync(planning.path) !== planning.path || changeRoot !== join(planning.path, "changes", status.changeName) || !isStrictDescendantPath(request.workspaceRoot, marker)) throw new Error("Native SDD continuation workspace or planning path mismatch.");
|
|
9456
|
-
if (await ctx.ui.confirm("Prepare SDD marker?", `Authorize only continuation-time preparation of ${marker}? This grants no source roots and no persistent authority.`) !== true) {
|
|
9457
|
-
showCommandSddStatus(status, parsed.json, ctx);
|
|
9458
|
-
return;
|
|
9459
|
-
}
|
|
9460
|
-
if (realpathSync(ctx.cwd) !== request.workspaceRoot || realpathSync(changeRoot) !== changeRoot) throw new Error("Native SDD continuation workspace changed during confirmation.");
|
|
9461
|
-
checkMarker();
|
|
9462
|
-
const selected = { ...request, changeName: status.changeName };
|
|
9463
|
-
showCommandSddStatus(decodeNativeSddStatusV2(await nativeReviewCli.sddContinue(selected), selected), parsed.json, ctx);
|
|
9464
|
-
};
|
|
9465
|
-
|
|
9466
|
-
pi.registerCommand("gentle-sdd-continue", {
|
|
9467
|
-
description: "Resolve SDD status and route the next phase deterministically.",
|
|
9468
|
-
handler: async (args, ctx) => {
|
|
9469
|
-
await handleSddContinueCommand(args, ctx);
|
|
9470
|
-
},
|
|
9471
|
-
});
|
|
9472
|
-
|
|
9473
9577
|
pi.registerCommand("gentle:models", {
|
|
9474
9578
|
description: "Configure global per-agent models for el Gentleman.",
|
|
9475
9579
|
handler: async (_args, ctx) => {
|
|
9476
|
-
await handleModelsCommand(ctx);
|
|
9580
|
+
await handleModelsCommand(ctx, pi);
|
|
9477
9581
|
},
|
|
9478
9582
|
});
|
|
9479
9583
|
|
|
@@ -9555,9 +9659,6 @@ function createGentleAiExtensionForTesting(
|
|
|
9555
9659
|
description: "Run read-only Gentle AI diagnostics for this Pi workspace.",
|
|
9556
9660
|
handler: async (_args, ctx) => {
|
|
9557
9661
|
const assetLines = packageAssetDiagnosticLines(ctx.cwd);
|
|
9558
|
-
const openspecConfigured = existsSync(
|
|
9559
|
-
join(ctx.cwd, "openspec", "config.yaml"),
|
|
9560
|
-
);
|
|
9561
9662
|
const skillRegistryPresent = existsSync(
|
|
9562
9663
|
join(ctx.cwd, ".atl", "skill-registry.md"),
|
|
9563
9664
|
);
|
|
@@ -9567,7 +9668,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9567
9668
|
const lines = [
|
|
9568
9669
|
"el Gentleman doctor",
|
|
9569
9670
|
...assetLines,
|
|
9570
|
-
|
|
9671
|
+
"pass: Organic Driven Development (ODD): active",
|
|
9571
9672
|
`${skillRegistryPresent ? "pass" : "warn"}: Skill registry ${skillRegistryPresent ? "present" : "missing"}`,
|
|
9572
9673
|
`${modelConfig.status === "invalid" ? "fail" : "pass"}: Global model config ${modelConfig.status}`,
|
|
9573
9674
|
"pass: Sensitive-path guard active for read/write/edit tools",
|
|
@@ -9740,9 +9841,6 @@ function createGentleAiExtensionForTesting(
|
|
|
9740
9841
|
description: "Show Gentle AI package status for this project.",
|
|
9741
9842
|
handler: async (_args, ctx) => {
|
|
9742
9843
|
const assetLines = packageAssetDiagnosticLines(ctx.cwd);
|
|
9743
|
-
const openspecConfigured = existsSync(
|
|
9744
|
-
join(ctx.cwd, "openspec", "config.yaml"),
|
|
9745
|
-
);
|
|
9746
9844
|
const savedConfig = await readModelRoutingAuthorityAsync(
|
|
9747
9845
|
modelConfigPath(ctx.cwd),
|
|
9748
9846
|
legacyProjectModelConfigPath(ctx.cwd),
|
|
@@ -9754,7 +9852,7 @@ function createGentleAiExtensionForTesting(
|
|
|
9754
9852
|
...(devBinary.state === "inactive" ? [] : [devBinary.line]),
|
|
9755
9853
|
`Persona: ${readPersonaMode(ctx.cwd)}`,
|
|
9756
9854
|
...assetLines,
|
|
9757
|
-
|
|
9855
|
+
"Organic Driven Development (ODD): active",
|
|
9758
9856
|
`Global model config: ${existsSync(modelConfigPath(ctx.cwd)) ? "present" : "missing"}`,
|
|
9759
9857
|
`Saved model routing: ${savedConfig.status}${savedConfig.status === "invalid" ? ` (${savedConfig.path})` : ""}`,
|
|
9760
9858
|
...(savedConfig.status === "invalid" ? [] : describeModelConfig(ctx.cwd, savedConfig.status === "valid" ? savedConfig.config : {})),
|