gentle-pi 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +336 -137
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +9 -9
- package/bin/gentle-shell.mjs +170 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +33 -3
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +5 -4
- package/docs/install-wizard.md +8 -1
- package/docs/permission-modes.md +1 -1
- package/docs/readme-reference.md +107 -19
- package/docs/yolo-mode.md +21 -8
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +99 -12
- package/extensions/gentle-ai.ts +284 -52
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +27 -12
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +39 -3
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +145 -11
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-todo.ts +23 -4
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +38 -2
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +143 -9
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/autonomous-guard.test.ts +122 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +1 -1
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +282 -3
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +169 -6
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +79 -0
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +42 -9
- package/tests/gentle-todo.test.ts +179 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +24 -20
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +69 -9
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +18 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
|
|
2
2
|
import { dataArgumentRanges, recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
|
|
3
|
+
import { defaultTempScratchFs, isTempScratchRemoval, type TempScratchFs } from "../lib/yolo-temp-scratch.ts";
|
|
3
4
|
import { SHELL_COMMAND_TOOLS } from "../lib/background-jobs.ts";
|
|
5
|
+
import { evaluateDeliveredTestTool } from "../lib/delivered-test-lint.ts";
|
|
6
|
+
import { commitSliceToolResult } from "../lib/commit-slice-signal.ts";
|
|
7
|
+
import { appendContextBackstopNotice, createContextBackstopSignal } from "../lib/context-backstop.ts";
|
|
4
8
|
import { blockChildDestructiveCommand } from "./child-safety.ts";
|
|
5
|
-
import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
|
|
9
|
+
import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUndocumentedWriterLaunch, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
|
|
10
|
+
import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
|
|
6
11
|
import { consumeReviewMutation, pendingReviewMutation, pendingReviewMutationProfiles, recordReviewMutation, type ReceiptSession } from "../lib/review-reminder-receipt.ts";
|
|
7
12
|
import { createReviewSidebarPublisher } from "../lib/review-sidebar-state.ts";
|
|
8
13
|
import { StatusTimingDiagnostics } from "../lib/status-timing-diagnostics.ts";
|
|
@@ -20,6 +25,7 @@ import {
|
|
|
20
25
|
readdirSync,
|
|
21
26
|
readFileSync,
|
|
22
27
|
realpathSync,
|
|
28
|
+
statSync,
|
|
23
29
|
writeFileSync,
|
|
24
30
|
} from "node:fs";
|
|
25
31
|
import {
|
|
@@ -38,6 +44,8 @@ import type {
|
|
|
38
44
|
Theme,
|
|
39
45
|
ThemeColor,
|
|
40
46
|
ToolCallEventResult,
|
|
47
|
+
ToolResultEvent,
|
|
48
|
+
ToolResultEventResult,
|
|
41
49
|
} from "@earendil-works/pi-coding-agent";
|
|
42
50
|
import { Key, Text, isKeyRelease, matchesKey, truncateToWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
43
51
|
import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
@@ -222,7 +230,7 @@ import {
|
|
|
222
230
|
isSmallWriterProfile,
|
|
223
231
|
resolveWriterProfile,
|
|
224
232
|
decodeAgentRiskEscalation,
|
|
225
|
-
|
|
233
|
+
verificationRisk,
|
|
226
234
|
HIGH_RISK_ITEMS,
|
|
227
235
|
RDD_LINE,
|
|
228
236
|
WRITER_PROFILE,
|
|
@@ -264,6 +272,8 @@ import {
|
|
|
264
272
|
sameReviewSessionIdentity,
|
|
265
273
|
type ReviewSessionIdentity,
|
|
266
274
|
} from "../lib/review-session-standing-permission.ts";
|
|
275
|
+
import { herdrSetupHint } from "../lib/herdr-activity.ts";
|
|
276
|
+
import { isInteractiveMode } from "../lib/rpc-host.ts";
|
|
267
277
|
|
|
268
278
|
const GRAPH_V1_ORDINARY_READ_ONLY = "Graph-v1 ordinary review authority is read-only; use native compact-v2 review operations";
|
|
269
279
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -788,6 +798,7 @@ const rddStatusMemo = new Map<string, { readonly status: NativeReviewModeStatus
|
|
|
788
798
|
/** @internal test seam: clears the per-cwd RDD status memo. */
|
|
789
799
|
function clearRddStatusMemoForTesting(): void {
|
|
790
800
|
rddStatusMemo.clear();
|
|
801
|
+
rddStatusUnavailableWarned = false;
|
|
791
802
|
}
|
|
792
803
|
|
|
793
804
|
// gentle-pi#668 (corrected): last-known outcome for ONE candidate, keyed by
|
|
@@ -852,29 +863,62 @@ async function readRddModeStatusOnce(
|
|
|
852
863
|
cwd: string,
|
|
853
864
|
signal?: AbortSignal,
|
|
854
865
|
): Promise<NativeReviewModeStatus | undefined> {
|
|
855
|
-
|
|
866
|
+
const read = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
|
|
867
|
+
return "status" in read ? read.status : undefined;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// gentle-shell#2250: the same read, keeping why it failed so the one-shot
|
|
871
|
+
// unavailable notice can tell a timeout from a native refusal (for example a
|
|
872
|
+
// repository on a network share that Git or gentle-ai rejects by ownership).
|
|
873
|
+
async function readRddModeStatusWithCause(
|
|
874
|
+
nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
|
|
875
|
+
cwd: string,
|
|
876
|
+
signal?: AbortSignal,
|
|
877
|
+
): Promise<{ readonly status: NativeReviewModeStatus } | { readonly cause: string }> {
|
|
878
|
+
if (!nativeReviewCli?.reviewMode) return { cause: "the native gentle-ai review CLI is not available" };
|
|
856
879
|
try {
|
|
857
880
|
const call = nativeReviewCli.reviewMode({ cwd, operation: NATIVE_REVIEW_MODE_OPERATION.STATUS, signal });
|
|
858
881
|
const result = signal === undefined ? await call : await Promise.race([call, rddAbortRejection(signal)]);
|
|
859
|
-
return result.status;
|
|
860
|
-
} catch {
|
|
861
|
-
return
|
|
882
|
+
return { status: result.status };
|
|
883
|
+
} catch (error) {
|
|
884
|
+
return { cause: describeRddStatusFailure(error, signal) };
|
|
862
885
|
}
|
|
863
886
|
}
|
|
864
887
|
|
|
888
|
+
const RDD_STATUS_CAUSE_LIMIT = 300;
|
|
889
|
+
|
|
890
|
+
function describeRddStatusFailure(error: unknown, signal: AbortSignal | undefined): string {
|
|
891
|
+
const command = "native `gentle-ai review mode status`";
|
|
892
|
+
if (signal?.aborted || (error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.TIMEOUT)) {
|
|
893
|
+
return `${command} did not answer within ${RDD_STATUS_TIMEOUT_MS} ms`;
|
|
894
|
+
}
|
|
895
|
+
if (error instanceof NativeReviewCliError) {
|
|
896
|
+
const exit = error.diagnostics.exit_code === undefined ? "" : `, exit ${error.diagnostics.exit_code}`;
|
|
897
|
+
const detail = (error.diagnostics.stderr ?? "").replace(/\s+/g, " ").trim() || error.message;
|
|
898
|
+
return `${command} failed (${error.code}${exit}): ${boundRddStatusCause(detail)}`;
|
|
899
|
+
}
|
|
900
|
+
return boundRddStatusCause(error instanceof Error ? error.message : String(error));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function boundRddStatusCause(text: string): string {
|
|
904
|
+
const trimmed = text.replace(/[.\s]+$/, "");
|
|
905
|
+
return trimmed.length <= RDD_STATUS_CAUSE_LIMIT ? trimmed : `${trimmed.slice(0, RDD_STATUS_CAUSE_LIMIT - 1)}…`;
|
|
906
|
+
}
|
|
907
|
+
|
|
865
908
|
// gentle-pi#662: read-only combined native risk assessment plus the computed
|
|
866
909
|
// verification plan (`lib/review-risk-assessment.ts`), for the `gentle_review`
|
|
867
910
|
// tool's `assess` operation. Never throws: an unavailable/failed native
|
|
868
911
|
// assess call (older binary without the verb, timeout, malformed response)
|
|
869
|
-
// resolves to the `unassessable` tier, which
|
|
870
|
-
//
|
|
912
|
+
// resolves to the `unassessable` native tier, which informs only: like a
|
|
913
|
+
// native high, it adds no independent verifier without an agent escalation.
|
|
871
914
|
interface ReviewAssessmentPlanDetails {
|
|
872
915
|
schema: "gentle-pi.review-assessment-plan/v1";
|
|
873
916
|
risk: VerificationTier;
|
|
874
|
-
// gentle-shell#1494: the native tier before any agent escalation. `risk`
|
|
875
|
-
//
|
|
917
|
+
// gentle-shell#1494: the native tier before any agent escalation. `risk` is
|
|
918
|
+
// the plan tier: high only through an escalation, else passive or medium.
|
|
876
919
|
nativeRisk: VerificationTier;
|
|
877
|
-
// `applied` is
|
|
920
|
+
// `applied` is true whenever an escalation is present: without it the plan
|
|
921
|
+
// would be passive or medium.
|
|
878
922
|
agentEscalation?: { item: number; label: string; reason: string; applied: boolean };
|
|
879
923
|
reasons: readonly ReviewAssessmentReason[];
|
|
880
924
|
changedPaths: number;
|
|
@@ -1031,9 +1075,10 @@ async function resolveReviewAssessmentPlan(
|
|
|
1031
1075
|
}
|
|
1032
1076
|
|
|
1033
1077
|
const nativeRisk: VerificationTier = assessment?.risk ?? VERIFICATION_TIER.UNASSESSABLE;
|
|
1034
|
-
// gentle-shell#1494:
|
|
1035
|
-
//
|
|
1036
|
-
|
|
1078
|
+
// gentle-shell#1494: the plan is high only through an agent escalation
|
|
1079
|
+
// naming a Task Size high-risk item; a native high or unassessable tier
|
|
1080
|
+
// informs as medium. The native tier stays visible as `nativeRisk`.
|
|
1081
|
+
const risk = verificationRisk(nativeRisk, input.escalate);
|
|
1037
1082
|
// gentle-pi#668: the memo is read only for THIS candidate's own target
|
|
1038
1083
|
// identity, never repository-only. It is still needed when the caller
|
|
1039
1084
|
// claims `closed`, because a recorded decline beats closure (gentle-pi#1175).
|
|
@@ -1048,7 +1093,7 @@ async function resolveReviewAssessmentPlan(
|
|
|
1048
1093
|
nativeRisk,
|
|
1049
1094
|
...(input.escalate === undefined
|
|
1050
1095
|
? {}
|
|
1051
|
-
: { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== nativeRisk } }),
|
|
1096
|
+
: { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== verificationRisk(nativeRisk, undefined) } }),
|
|
1052
1097
|
// No path is known for an unassessable candidate: omit it rather than
|
|
1053
1098
|
// emitting an empty string the native reason shape forbids.
|
|
1054
1099
|
reasons: assessment?.reasons ?? (unassessableDetail === undefined ? [] : [{ code: unassessableCode, detail: unassessableDetail }]),
|
|
@@ -1119,14 +1164,15 @@ async function resolveRddModeStatus(
|
|
|
1119
1164
|
// read status instead of flipping the prompt to "unknown" and back, which
|
|
1120
1165
|
// would invalidate the provider's prefix cache for the whole conversation.
|
|
1121
1166
|
// Only a status the native reader actually reports replaces a known one.
|
|
1122
|
-
const
|
|
1167
|
+
const outcome = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
|
|
1168
|
+
const read = "status" in outcome ? outcome.status : undefined;
|
|
1123
1169
|
const status = read ?? cached?.status;
|
|
1124
1170
|
rddStatusMemo.set(cwd, { status, expiresAt: nowMs + RDD_STATUS_MEMO_TTL_MS });
|
|
1125
|
-
if (
|
|
1171
|
+
if ("cause" in outcome && !rddStatusUnavailableWarned) {
|
|
1126
1172
|
rddStatusUnavailableWarned = true;
|
|
1127
1173
|
if (ctx?.hasUI) {
|
|
1128
1174
|
ctx.ui.notify(
|
|
1129
|
-
|
|
1175
|
+
`Gentle AI: receipt-driven-development status is unavailable: ${outcome.cause}. The parent prompt keeps the last known status, or renders "unknown" when none was read; this notice will not repeat this session.`,
|
|
1130
1176
|
"warning",
|
|
1131
1177
|
);
|
|
1132
1178
|
}
|
|
@@ -1139,6 +1185,46 @@ function isRddStatusLineOff(line: string | undefined): boolean {
|
|
|
1139
1185
|
return line !== undefined && line.startsWith("Receipt-driven development: off ");
|
|
1140
1186
|
}
|
|
1141
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* gentle-shell#2195: the rendered line sits in the cached system-prompt
|
|
1190
|
+
* prefix, so a session keeps the first line it rendered and re-renders only
|
|
1191
|
+
* when the off state changes, because that also adds or drops the review
|
|
1192
|
+
* execution contract (gentle-shell#1494). An unknown-to-on flip or a
|
|
1193
|
+
* deciding-source change keeps the prefix; review tools still read the live
|
|
1194
|
+
* mode whenever they run, so the pinned text never grants or blocks anything.
|
|
1195
|
+
*/
|
|
1196
|
+
function pinRddStatusLine(pinned: string | undefined, next: string): string {
|
|
1197
|
+
return pinned !== undefined && isRddStatusLineOff(pinned) === isRddStatusLineOff(next) ? pinned : next;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* gentle-pi S11 T1: while the pinned line reads off, no review can start and
|
|
1202
|
+
* the orchestrator only calls ASSESS, so the primary request swaps the review
|
|
1203
|
+
* family's long guidance for this lean loadout: one short gentle_review
|
|
1204
|
+
* snippet, guideline, and description, with the capture, capture-group, and
|
|
1205
|
+
* scope declarations hidden (they stay registered and active). It follows the
|
|
1206
|
+
* pinned line, so it changes only when that line's off state flips, together
|
|
1207
|
+
* with the review execution contract; on and unknown keep the registered text.
|
|
1208
|
+
*/
|
|
1209
|
+
const RDD_OFF_REVIEW_SNIPPET = "Assess an authorized change and return its verification plan (read-only)";
|
|
1210
|
+
const RDD_OFF_REVIEW_GUIDELINES: readonly string[] = [
|
|
1211
|
+
'Receipt-driven development is off, so use gentle_review only for {"operation":"assess"} (read-only): after an authorized implementation, delegated or inline, call it over the diff and follow the returned plan; pass input {"escalate":{"item":1-6,"reason":"<one line>"}} only when the change touches a Task Size high-risk item, or {"baseRef":"<ref>","committedOnly":true} to assess a committed range.',
|
|
1212
|
+
"When ASSESS returns reviewDue or a native continuation, relay it unchanged and follow it; tests, builds, and functional checks still run when applicable, and no assessment authorizes delivery.",
|
|
1213
|
+
];
|
|
1214
|
+
const RDD_OFF_REVIEW_DESCRIPTION = "Assess the current changes, or a committed range, and return the verification plan. Read-only; review outcomes never authorize delivery: commit, push, pull-request, and release commands follow ordinary repository policy.";
|
|
1215
|
+
const RDD_OFF_HIDDEN_REVIEW_TOOLS: readonly string[] = ["gentle_review_scope", "gentle_review_capture_group", "gentle_review_capture"];
|
|
1216
|
+
|
|
1217
|
+
interface ReviewToolPromptOptions {
|
|
1218
|
+
toolSnippets?: Record<string, string>;
|
|
1219
|
+
toolGuidelines?: Record<string, string[]>;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** Replaces gentle_review's snippet and guidelines in this run's prompt options only. */
|
|
1223
|
+
function applyRddOffReviewPrompt(options: ReviewToolPromptOptions | null | undefined): void {
|
|
1224
|
+
if (options?.toolSnippets?.gentle_review !== undefined) options.toolSnippets.gentle_review = RDD_OFF_REVIEW_SNIPPET;
|
|
1225
|
+
if (options?.toolGuidelines?.gentle_review !== undefined) options.toolGuidelines.gentle_review = [...RDD_OFF_REVIEW_GUIDELINES];
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1142
1228
|
/** Resolves and renders the RDD status line for a production call site in one call. */
|
|
1143
1229
|
async function resolveRddStatusLine(
|
|
1144
1230
|
nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
|
|
@@ -1205,13 +1291,13 @@ function renderOrchestratorPrompt(
|
|
|
1205
1291
|
// pinned at an 8192-byte budget (tests/orchestrator-budget.test.ts).
|
|
1206
1292
|
const PROVIDER_CONTRACT_MIRROR_ROOT = join(PACKAGE_ROOT, "contracts", "review-provider-contract-mirror");
|
|
1207
1293
|
const PROVIDER_CONTRACT_LOCK_FILE = "provider-contract.lock.json";
|
|
1208
|
-
const
|
|
1294
|
+
const PI_MIRROR_RUNTIME = "pi";
|
|
1209
1295
|
|
|
1210
1296
|
let reviewContractPromptFragmentCache: string | null | undefined;
|
|
1211
1297
|
let reviewContractPromptMissingWarned = false;
|
|
1212
1298
|
|
|
1213
|
-
//
|
|
1214
|
-
function
|
|
1299
|
+
// Reads one mirrored bundle file after verifying its bytes against the lock's digest (gentle-ai R1/R3).
|
|
1300
|
+
function readVerifiedMirrorFile(mirrorRoot: string, bundlePath: string): { contractSemver: string; text: string } | null {
|
|
1215
1301
|
try {
|
|
1216
1302
|
const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
|
|
1217
1303
|
const lock = JSON.parse(readFileSync(lockPath, "utf8")) as {
|
|
@@ -1219,20 +1305,71 @@ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRA
|
|
|
1219
1305
|
entries?: Record<string, unknown>;
|
|
1220
1306
|
};
|
|
1221
1307
|
if (typeof lock.contract_semver !== "string" || lock.contract_semver === "") return null;
|
|
1222
|
-
const expectedSha256 = lock.entries?.[
|
|
1308
|
+
const expectedSha256 = lock.entries?.[bundlePath];
|
|
1223
1309
|
if (typeof expectedSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedSha256)) return null;
|
|
1224
|
-
const
|
|
1225
|
-
const rawBytes = readFileSync(
|
|
1310
|
+
const filePath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", ...bundlePath.split("/"));
|
|
1311
|
+
const rawBytes = readFileSync(filePath);
|
|
1226
1312
|
const actualSha256 = createHash("sha256").update(rawBytes).digest("hex");
|
|
1227
1313
|
if (!timingSafeEqual(Buffer.from(expectedSha256, "hex"), Buffer.from(actualSha256, "hex"))) return null;
|
|
1228
|
-
|
|
1229
|
-
if (text.length === 0) return null;
|
|
1230
|
-
return `## Gentle AI review execution contract (mirrored provider bundle ${lock.contract_semver})\n\n${text}`;
|
|
1314
|
+
return { contractSemver: lock.contract_semver, text: rawBytes.toString("utf8") };
|
|
1231
1315
|
} catch {
|
|
1232
1316
|
return null;
|
|
1233
1317
|
}
|
|
1234
1318
|
}
|
|
1235
1319
|
|
|
1320
|
+
// Verifies the mirrored orchestration/pi.md bytes against the lock's digest before injection (gentle-ai R1/R3).
|
|
1321
|
+
function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string | null {
|
|
1322
|
+
const file = readVerifiedMirrorFile(mirrorRoot, `orchestration/${PI_MIRROR_RUNTIME}.md`);
|
|
1323
|
+
if (file === null) return null;
|
|
1324
|
+
const text = file.text.trim();
|
|
1325
|
+
if (text.length === 0) return null;
|
|
1326
|
+
return `## Gentle AI review execution contract (mirrored provider bundle ${file.contractSemver})\n\n${text}`;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// gentle-ai R3: the harness ODD steps 1-7, the phase-reporting line, and the
|
|
1330
|
+
// resume line come from the mirrored bundle's routing/pi.md (contract >= 1.4.0),
|
|
1331
|
+
// the ODD routing block Gentle AI renders for Pi, instead of a hand-copied
|
|
1332
|
+
// literal. The bundle's blank lines between them are not carried into the
|
|
1333
|
+
// prompt. Like assets/orchestrator.md, this is package-shipped prompt source:
|
|
1334
|
+
// a missing, tampered, or reshaped file fails closed instead of rendering a
|
|
1335
|
+
// harness without its workflow.
|
|
1336
|
+
const ODD_PROTOCOL_HEADING = "### ODD protocol (MANDATORY, in this order, on every request)";
|
|
1337
|
+
const ODD_PROTOCOL_STEP_COUNT = 7;
|
|
1338
|
+
|
|
1339
|
+
let oddProtocolStepsCache: string | undefined;
|
|
1340
|
+
|
|
1341
|
+
function extractOddProtocolLines(routing: string): string {
|
|
1342
|
+
const lines = routing.split("\n");
|
|
1343
|
+
const start = lines.indexOf(ODD_PROTOCOL_HEADING);
|
|
1344
|
+
const section: string[] = [];
|
|
1345
|
+
for (const line of start === -1 ? [] : lines.slice(start + 1)) {
|
|
1346
|
+
if (line.startsWith("#")) break;
|
|
1347
|
+
if (line !== "") section.push(line);
|
|
1348
|
+
}
|
|
1349
|
+
const shapeHolds =
|
|
1350
|
+
section.length === ODD_PROTOCOL_STEP_COUNT + 2 &&
|
|
1351
|
+
section.slice(0, ODD_PROTOCOL_STEP_COUNT).every((line, index) => line.startsWith(`${index + 1}. **`)) &&
|
|
1352
|
+
section[ODD_PROTOCOL_STEP_COUNT]!.startsWith("Phase reporting: ") &&
|
|
1353
|
+
section[ODD_PROTOCOL_STEP_COUNT + 1]!.startsWith("Resume an interrupted feature ");
|
|
1354
|
+
if (!shapeHolds) {
|
|
1355
|
+
throw new Error(`The mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md ODD protocol section has an unexpected shape: expected ${JSON.stringify(ODD_PROTOCOL_HEADING)} followed by steps 1-${ODD_PROTOCOL_STEP_COUNT}, phase reporting, and resume.`);
|
|
1356
|
+
}
|
|
1357
|
+
return section.join("\n");
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function readMirroredOddProtocolSteps(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string {
|
|
1361
|
+
const file = readVerifiedMirrorFile(mirrorRoot, `routing/${PI_MIRROR_RUNTIME}.md`);
|
|
1362
|
+
if (file === null) {
|
|
1363
|
+
throw new Error(`Gentle Shell cannot render its ODD steps: the mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md is missing, unreadable, or fails digest verification. Reinstall gentle-pi.`);
|
|
1364
|
+
}
|
|
1365
|
+
return extractOddProtocolLines(file.text);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function oddProtocolSteps(): string {
|
|
1369
|
+
oddProtocolStepsCache ??= readMirroredOddProtocolSteps();
|
|
1370
|
+
return oddProtocolStepsCache;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1236
1373
|
function loadReviewContractPromptFragment(
|
|
1237
1374
|
ctx: Pick<ExtensionContext, "hasUI" | "ui">,
|
|
1238
1375
|
mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT,
|
|
@@ -1324,27 +1461,27 @@ For background work, launch the bounded action and end the turn when no independ
|
|
|
1324
1461
|
|
|
1325
1462
|
Default workflow: Organic Driven Development (MANDATORY)
|
|
1326
1463
|
Organic Driven Development (ODD) is the predefined workflow of this orchestrator. Every request enters it, without the user asking for a workflow, a plan, or task tracking. Never describe this workflow only when asked about it: run it. Run these steps in this order on every request:
|
|
1327
|
-
|
|
1328
|
-
2. **Explore.** Explore existing code and requirements first, proportionately to the request, before proposing or writing anything. Do not delegate exploration of files you will read anyway to work inline; explore only for a map you need to decide or route.
|
|
1329
|
-
3. **Resolve uncertainty.** Recommend optional research only for a named uncertainty; ask one focused user question only for a real unresolved product decision, then stop and wait; use at most one scoped read-only assumption challenge for a high-consequence unproven premise.
|
|
1330
|
-
4. **Classify.** Size the task by the orchestrator's Task Size section: small when understood, risk is contained, and the work could be resumed from the original request and \`git diff\` alone; large only when that resume test fails. Never classify by counting files, commands, tests, fixes, or a requested todo list. Small work stays inline and creates no durable task artifacts.
|
|
1331
|
-
5. **Track before the first write.** For large authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, then create or rebuild the visible \`todo\` list from the reconciled feature tasks, all before the first source write and without asking permission for tasks or storage. Tell the user in one line which feature document was created and how many tasks it holds. The document is the specification subagents read, in this order: a two- or three-line header; \`## Specs\` with numbered \`S#\` that quote the user's exact strings, error messages, and examples verbatim, never summarized and never adding unrequested requirements; \`## Tasks\` with one line per task (ID, linked \`S#\`, route, commit); \`## Log\` last, where \`L1\` is the user's original request verbatim and later user corrections, evidence, and decisions are appended. A requirement change appends its verbatim Log entry, rewrites only the affected \`S#\`, and reopens only its task.
|
|
1332
|
-
6. **Implement task by task.** Hand off by reference, never by paraphrase: name the document, task, and specs (for example \`Spec: odd/tasks/<feature>.md, T2, S3-S4\`), tell workers to read until \`## Log\`, and ask which \`S#\` were covered. Without a feature document, include the user's request verbatim. Verify reads the whole document, runs the spec's examples the parent authorized, against isolated state when they mutate data, and returns a verdict per \`S#\`. When the user reports a failure, reproduce it before deciding it already works. Each test asserts every observable effect of the rule it covers (exit code, exact stdout and stderr, and that rejected input leaves stored data and counters unchanged), covers the cases the rule itself names (its examples, boundaries, and errors), and checks through the public interface, never internal storage. When you add or change a command, option, or message, update the help text and docs that describe it. Route each task through the orchestrator's Mechanisms, honoring its mandatory delegation triggers, with applicable test-first development and checks. These triggers are mandatory, not advisory: executing past a fired trigger inline is a routing defect even if the work succeeds. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change. Every tracked task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence. Work-unit commits on the feature branch are part of authorized large ODD implementation; push, pull request creation, and merge remain the user's decisions.
|
|
1333
|
-
7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. Before writing \`Risk: none\`, check whether your diff changes code that existing behavior the request did not mention also uses (shared options, parsers, helpers); if it does, that is item 3. Never end with a tracked task pending unless you quote the user's explicit stop. An applicable quick check runs once; an unavailable verifier or subagent is reported as unavailable, never retried or escalated into extra ceremony. Partial, blocked, unavailable, or exhausted proof becomes one **Needs your decision** result naming the open blockers or missing proof, never more verification; that result is a valid stop, hedged wording is not. The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch; native review runs only under the user-owned RDD switch.
|
|
1334
|
-
Phase reporting: the Gentle Shell prompt label is inferred automatically from the primary session's tool activity (reads show \`exploring\`, edits \`implementing\`, test runs \`checking\`, user questions \`deciding\`). When the \`gentle_odd_phase\` tool is available, use it to refine that label with phases tools cannot show (\`authorizing\`, \`researching\`, \`deciding\`, \`closing\`): call \`gentle_odd_phase\` only when the primary session's ODD phase actually changes, never per tool call or on a fixed cadence, and never from a subagent. It drives the Gentle Shell prompt label only.
|
|
1335
|
-
Resume an interrupted feature with \`mem_context\`, then project- and feature-scoped \`mem_search\`, then \`mem_get_observation\` for the full document, then the task file itself; reconcile before continuing the next unfinished task. Detail for steps 3–7: \`orchestrator-delegation.md\` and \`orchestrator-memory.md\`.
|
|
1464
|
+
${oddProtocolSteps()}
|
|
1336
1465
|
|
|
1337
1466
|
Harness principles:
|
|
1338
1467
|
- el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
|
|
1339
|
-
- Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track large work before the first write, implement task by task with proportionate checks, close each tracked task with a work-unit commit, and close.
|
|
1340
1468
|
- Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
|
|
1341
1469
|
- Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
|
|
1342
|
-
-
|
|
1343
|
-
- For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation). Test presence alone does not establish applicability; no chat or TUI toggle activates it. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, explain why and run proportionate ordinary functional or structural verification. Never invent lifecycle evidence or skip checks. Follow orchestrator-delegation.md for ODD forwarding and evidence.
|
|
1470
|
+
- For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation). Before finishing, run a differential check: for each existing command that shares the code you changed, run it with the new or changed input (for example a new option passed to a sibling subcommand) against the previous code (a copy of the base commit in a \`mktemp -d\` scratch directory, never committed as a test; committed tests never read git history or a fixed commit) and against your change, and compare stdout, stderr, and exit code; any difference nobody asked for is a regression to fix and pin with a test. Test presence alone does not establish applicability; no chat or TUI toggle activates it. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, explain why and run proportionate ordinary functional or structural verification. Never invent lifecycle evidence or skip checks. Follow orchestrator-delegation.md for ODD forwarding and evidence.
|
|
1344
1471
|
- Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
|
|
1345
1472
|
- Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
|
|
1346
1473
|
|
|
1347
|
-
${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}`;
|
|
1474
|
+
${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}${headlessDeliveryNote(hostMode)}`;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Bench gate-p53-r1-BS40 x6 run 3: in a headless RPC session the ask-on-risk
|
|
1478
|
+
// delivery question ran before any code was written, ask_user_question was
|
|
1479
|
+
// unavailable, and the orchestrator printed the menu and stopped. Hosts where
|
|
1480
|
+
// no human can answer (the same rule ask_user_question applies) resolve that
|
|
1481
|
+
// question to the recommended chain. Interactive sessions render nothing here.
|
|
1482
|
+
function headlessDeliveryNote(hostMode: string | undefined): string {
|
|
1483
|
+
if (hostMode === undefined || isInteractiveMode(hostMode)) return "";
|
|
1484
|
+
return "\n\nNo human can answer questions in this session. For the ask-on-risk delivery question, do not ask: use `feature-branch-chain`, record that choice in the feature document's `## Log`, and continue. Consent, maintenance, and opaque-token prompts keep their own rules.";
|
|
1348
1485
|
}
|
|
1349
1486
|
|
|
1350
1487
|
// Matches `git [global-flags] push` — tolerates flags like -C /repo or --work-tree=/tmp
|
|
@@ -1859,8 +1996,15 @@ const YOLO_SEGMENT_END_RE = /&&|\|\||;|\||\n|\r/;
|
|
|
1859
1996
|
// most a `-C <path>`. Other global options (`-c key=value`, `--exec-path`,
|
|
1860
1997
|
// `--git-dir`) can run or redirect code, so they keep the confirmation.
|
|
1861
1998
|
const YOLO_GIT_PREFIX_RE = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=[^\s'"`$]*\s+|env\s+(?:-i\s+)?|command\s+)*git(?:\s+-C\s+[^\s'"`$;&|]+)?\s+$/;
|
|
1862
|
-
|
|
1863
|
-
const
|
|
1999
|
+
// Output redirections that write nothing: to stdout/stderr or /dev/null.
|
|
2000
|
+
const YOLO_REDIRECT_SRC = String.raw`[12]?>&[12]|(?:[12]?|&)>>?\s*/dev/null(?![^\s;&|])`;
|
|
2001
|
+
const YOLO_PUSH_SEGMENT_RE = new RegExp(String.raw`^push(?:\s+(?:${YOLO_REDIRECT_SRC}|-u|--set-upstream|-q|--quiet|-v|--verbose|--tags|--follow-tags|--no-verify|--porcelain|[A-Za-z0-9_][A-Za-z0-9_./-]*(?::[A-Za-z0-9_][A-Za-z0-9_./-]*)?))*\s*$`);
|
|
2002
|
+
const YOLO_REBASE_SEGMENT_RE = new RegExp(String.raw`^rebase(?:\s+(?:${YOLO_REDIRECT_SRC}|--onto\s+[A-Za-z0-9_][A-Za-z0-9_./~^-]*|--continue|--abort|--skip|--autostash|--no-autostash|[A-Za-z0-9_][A-Za-z0-9_./~^-]*))*\s*$`);
|
|
2003
|
+
// A pipe out of a waived segment may only feed read-only filters with plain or
|
|
2004
|
+
// literally quoted arguments and end at a sequential separator. Git output
|
|
2005
|
+
// carries remote-controlled text, so `| sh`, `| tee`, `| xargs` and a trailing
|
|
2006
|
+
// background `&` keep the confirmation.
|
|
2007
|
+
const YOLO_FILTER_PIPELINE_RE = new RegExp(String.raw`^(?:\|(?!\|)\s*(?:tail|head|cat|grep|wc)(?:\s+(?:${YOLO_REDIRECT_SRC}|[A-Za-z0-9_.,:=+/%@-]+|'[^'\n]*'|"[^"$\x60\\\n]*"))*\s*)*(?=$|&&|\|\||;|\n|\r)`);
|
|
1864
2008
|
|
|
1865
2009
|
function yoloGuardedSegment(command: string, triggerIndex: number): string {
|
|
1866
2010
|
const rest = command.slice(triggerIndex);
|
|
@@ -1875,20 +2019,29 @@ function yoloSegmentPrefix(command: string, triggerIndex: number): string {
|
|
|
1875
2019
|
return last === undefined ? head : head.slice(last.index + last[0].length);
|
|
1876
2020
|
}
|
|
1877
2021
|
|
|
1878
|
-
/**
|
|
1879
|
-
|
|
1880
|
-
|
|
2022
|
+
/**
|
|
2023
|
+
* Every guarded match and every recognized Git side effect must be a reversible
|
|
2024
|
+
* push or non-interactive rebase; the only data loss allowed is recursive
|
|
2025
|
+
* removal of the session's own temp scratch (lib/yolo-temp-scratch.ts).
|
|
2026
|
+
*/
|
|
2027
|
+
function isReversibleYoloCommand(command: string, evaluation: GuardEvaluation, scratchFs?: TempScratchFs): boolean {
|
|
2028
|
+
if (evaluation.action !== "confirm") return false;
|
|
2029
|
+
const effects = recognizeDestructiveCommands(command);
|
|
2030
|
+
const scratch = effects.map((effect) => effect.removalTargets !== undefined && isTempScratchRemoval(effect, scratchFs ??= defaultTempScratchFs()));
|
|
2031
|
+
if (evaluation.matches.length === 0 && !scratch.includes(true)) return false;
|
|
1881
2032
|
for (const match of evaluation.matches) {
|
|
1882
2033
|
if (!YOLO_WAIVABLE_KEYS.has(match.key)) return false;
|
|
1883
2034
|
if (!YOLO_GIT_PREFIX_RE.test(yoloSegmentPrefix(command, match.triggerIndex))) return false;
|
|
1884
2035
|
const segment = yoloGuardedSegment(command, match.triggerIndex);
|
|
1885
2036
|
if (!(match.key === "gitPush" ? YOLO_PUSH_SEGMENT_RE : YOLO_REBASE_SEGMENT_RE).test(segment)) return false;
|
|
2037
|
+
if (!YOLO_FILTER_PIPELINE_RE.test(command.slice(match.triggerIndex + segment.length))) return false;
|
|
1886
2038
|
}
|
|
1887
2039
|
// The data-loss recognizer leaves non-hard Git effects (reset, stash drop,
|
|
1888
2040
|
// restore, checkout --) to the primary policy; under YOLO only a rebase that
|
|
1889
2041
|
// a guarded match already vetted may remain.
|
|
1890
2042
|
const rebaseTriggers = new Set(evaluation.matches.filter((match) => match.key === "gitRebase").map((match) => match.triggerIndex));
|
|
1891
|
-
for (const effect of
|
|
2043
|
+
for (const [index, effect] of effects.entries()) {
|
|
2044
|
+
if (scratch[index]) continue;
|
|
1892
2045
|
if (effect.hardDeny || effect.kind !== "git") return false;
|
|
1893
2046
|
const rebase = /^git(?:\s+(?:-C|-c|--git-dir|--work-tree|--namespace)\s+\S+|\s+-\S+)*\s+(rebase)\b/.exec(command.slice(effect.triggerIndex));
|
|
1894
2047
|
if (rebase === null || !rebaseTriggers.has(effect.triggerIndex + rebase.index + rebase[0].lastIndexOf("rebase"))) return false;
|
|
@@ -5140,7 +5293,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
|
|
|
5140
5293
|
},
|
|
5141
5294
|
input: {
|
|
5142
5295
|
type: "string",
|
|
5143
|
-
description: "A JSON object or serialized object string for START/ASSESS only; every other operation requires a serialized object string. New native ordinary START uses {\"mode\":\"ordinary\"}, plus the agent lens selection {\"lenses\":[\"risk\",...],\"lensesReason\":\"<what you touched and how>\"} (both or neither, 1-4 of risk, resilience, readability, reliability; never with focus; providers that predate it keep the tier default); answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668/#1175). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); writerModelId and writerEffort are only a fallback when no runtime evidence exists (caller), and with neither the profile fails closed to small (fallback).
|
|
5296
|
+
description: "A JSON object or serialized object string for START/ASSESS only; every other operation requires a serialized object string. New native ordinary START uses {\"mode\":\"ordinary\"}, plus the agent lens selection {\"lenses\":[\"risk\",...],\"lensesReason\":\"<what you touched and how>\"} (both or neither, 1-4 of risk, resilience, readability, reliability; never with focus; providers that predate it keep the tier default); answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668/#1175). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); writerModelId and writerEffort are only a fallback when no runtime evidence exists (caller), and with neither the profile fails closed to small (fallback). The writer profile is reported but never adds a verifier: the plan adds the independent verifier only when escalate names a Task Size high-risk item. ASSESS derives `closed` only from the native candidate.consumed fact for this exact candidate, which native records only when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown. Every outcome yields the exact risk-gated plan ASSESS returns when RDD is off: a closed review never removes the separate verifier the tier requires (verify-always-rdd-high S1), unknown is never treated as closed, and a decline is candidate-scoped and never lowers the bar below the RDD-off path. A declined or unavailable outcome, stated explicitly or recorded by this process for this exact candidate (never a different one, and never from repository state alone), wins over closure. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
|
|
5144
5297
|
},
|
|
5145
5298
|
outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
5146
5299
|
inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
|
|
@@ -6611,6 +6764,8 @@ const processRetainedNativeStatusSelections = new Map<PendingReviewConsentSessio
|
|
|
6611
6764
|
// named-agent start increments the depth, a matching end decrements it,
|
|
6612
6765
|
// and a fresh primary-loop start resets it to 0.
|
|
6613
6766
|
const processAgentEndSubagentDepth = new Map<PendingReviewConsentSessionKey, number>();
|
|
6767
|
+
// gentle-shell#2195: the RDD status line each primary session last rendered into its system prompt.
|
|
6768
|
+
const pinnedRddStatusLines = new Map<PendingReviewConsentSessionKey, string>();
|
|
6614
6769
|
|
|
6615
6770
|
// gentle-shell#1064 slice 1: the parent-session profile binding store lives in
|
|
6616
6771
|
// lib/session-profile-binding.ts (in-process, keyed by parent session id). The
|
|
@@ -7990,6 +8145,20 @@ function isSelectedReviewCapture(value: SelectedReviewCapture | Record<string, u
|
|
|
7990
8145
|
|
|
7991
8146
|
function captureGroupRejected(reason: string): Record<string, unknown> { return captureBindingRejected(reason, true); }
|
|
7992
8147
|
|
|
8148
|
+
function captureGroupStatusFailure(error: unknown): Record<string, unknown> {
|
|
8149
|
+
return {
|
|
8150
|
+
...captureGroupRejected(error instanceof Error ? error.message : String(error)),
|
|
8151
|
+
outcome: "native-status-failed",
|
|
8152
|
+
// Only project the decoded native envelope, never arbitrary error fields.
|
|
8153
|
+
// Group mutation reporting belongs to its submitted-reviewer progress.
|
|
8154
|
+
...(error instanceof NativeReviewIntegrationError ? {
|
|
8155
|
+
native_failure: error.failureEnvelope.raw,
|
|
8156
|
+
replayability: error.failureEnvelope.replayability,
|
|
8157
|
+
next_action: error.failureEnvelope.nextAction,
|
|
8158
|
+
} : {}),
|
|
8159
|
+
};
|
|
8160
|
+
}
|
|
8161
|
+
|
|
7993
8162
|
function hasExactReviewCaptureSuffix(status: ReviewStatusV3, expected: readonly string[]): boolean {
|
|
7994
8163
|
const current = status.nextTransition?.kind === "collect"
|
|
7995
8164
|
? (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation === "review.capture-result").map(canonicalReviewCaptureBinding)
|
|
@@ -8296,7 +8465,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
8296
8465
|
if (negotiated.transport !== undefined) return hostTransportUnavailable("gentle_review_capture_group", negotiated.transport);
|
|
8297
8466
|
status = negotiated.status!;
|
|
8298
8467
|
} catch (error) {
|
|
8299
|
-
return
|
|
8468
|
+
return captureGroupStatusFailure(error);
|
|
8300
8469
|
}
|
|
8301
8470
|
const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings);
|
|
8302
8471
|
if (!isSelectedReviewCaptureGroup(group)) return group;
|
|
@@ -8342,7 +8511,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
8342
8511
|
if (!hasExactReviewCaptureSuffix(negotiated.status!, canonicalBindings.slice(index))) return { ...captureGroupAuthorityDrift(negotiated.status!), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8343
8512
|
current = selectExactReviewCapture(negotiated.status!, parameters.lineageId, canonicalBindings[index]!);
|
|
8344
8513
|
} catch (error) {
|
|
8345
|
-
return { ...
|
|
8514
|
+
return { ...captureGroupStatusFailure(error), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8346
8515
|
}
|
|
8347
8516
|
if (!isSelectedReviewCapture(current)) return { ...captureGroupRejected(String(current.reason ?? "current STATUS rejected a reviewer binding")), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8348
8517
|
try {
|
|
@@ -9477,6 +9646,7 @@ export const __testing = {
|
|
|
9477
9646
|
renderOrchestratorPrompt,
|
|
9478
9647
|
loadReviewContractPromptFragment,
|
|
9479
9648
|
readMirroredReviewContractFragment,
|
|
9649
|
+
readMirroredOddProtocolSteps,
|
|
9480
9650
|
loadBackgroundSubagentsPolicy,
|
|
9481
9651
|
resolveBackgroundSubagentsPolicy,
|
|
9482
9652
|
renderBackgroundSubagentsReport,
|
|
@@ -9486,6 +9656,7 @@ export const __testing = {
|
|
|
9486
9656
|
readActiveToolNames,
|
|
9487
9657
|
renderBackgroundSubagentsStatusLine,
|
|
9488
9658
|
renderRddStatusLine,
|
|
9659
|
+
pinRddStatusLine,
|
|
9489
9660
|
isValidRddModeStatus,
|
|
9490
9661
|
resolveRddModeStatus,
|
|
9491
9662
|
resolveRddStatusLine,
|
|
@@ -9607,11 +9778,17 @@ function createGentleAiExtensionForTesting(
|
|
|
9607
9778
|
reviewSidebar.reset(ctx);
|
|
9608
9779
|
statusTiming.reset(ctx);
|
|
9609
9780
|
});
|
|
9781
|
+
// Lean RDD-off review loadout (applyRddOffReviewPrompt): every
|
|
9782
|
+
// before_agent_start sets it from the pinned line before Pi prepares that
|
|
9783
|
+
// run's tool loadout, so gentle_review's prepareLoadout reads this run's value.
|
|
9784
|
+
let rddOffReviewLoadout = false;
|
|
9610
9785
|
let reminderSessionActive = true;
|
|
9611
9786
|
let reminderEpoch = 0;
|
|
9612
9787
|
let reminderManager: ExtensionContext["sessionManager"] | undefined;
|
|
9613
9788
|
let unbindPreparation: (() => void) | undefined;
|
|
9614
9789
|
pi.on("session_shutdown", (event, context) => {
|
|
9790
|
+
pinnedRddStatusLines.delete(pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey));
|
|
9791
|
+
rddOffReviewLoadout = false;
|
|
9615
9792
|
auto.reset();
|
|
9616
9793
|
yolo.reset(context);
|
|
9617
9794
|
reviewSidebar.reset();
|
|
@@ -9859,10 +10036,20 @@ function createGentleAiExtensionForTesting(
|
|
|
9859
10036
|
"For blocked-legacy or blocked-mixed, do not call START repeatedly. Explain invalidation, request explicit user authorization, then call RESET or RECOVER only after authorization. RESET and RECOVER_LOCK route to audited native `gentle-ai review reclaim`; only RESET carries the legacy repositoryId, commonDirHash, inventoryHash, and confirmation challenge. RECOVER routes to native `gentle-ai review recover` with exactly six inputs: predecessorLineage, expectedPredecessorRevision, successorLineage, disposition, actor, and reason. Never send RECOVER the reset challenge and never send it a maintainerAuthorization: Pi reads fresh native target status, pins the predecessor lineage, revision, provider-selected disposition, and target identity, derives the exact six-line native authorization binding, displays it for fresh UI approval, and re-reads status before mutating. Negotiated target status supplies the sole accepted recovery disposition, and a caller-supplied substitute is rejected. Treat a native-input-required envelope as a request for exact values, never as permission to invent them. After a committed native recovery record, INSPECT before any fresh ordinary START.",
|
|
9860
10037
|
"A consent-required START may be resolved inside the eligible interactive Pi host. Its third UI action is host-owned: it runs this envelope's exact provider grant once and allows later fresh validated envelopes only for the same live SessionManager, nonempty session ID, and canonical Git common-directory identity, including sibling worktrees; an unrelated repository requires a new explicit human grant. Revoke removes the current repository grant, while nonreload replacement, quit, and process exit remove all session grants; reload preserves them. It grants no provider mode, verdict, acknowledgement, maintenance, delivery, or cross-repository authority. A package-owned child may ask its parent only with the canonical digest of its exact pending target; the parent binds that digest to the task repository and fails closed otherwise. If the tool returns an unresolved envelope, present the original two provider choices without changing machine tokens, commands, target IDs, or invocations; never add the host action to the decoded provider envelope. After one explicit relayed human answer, call answer-consent exactly once with only consentBinding and answer (`granted` or `declined`). Never create host permission from tool arguments, model prose, child/headless responses, or an uncertain native result. A reported lineage_created false or pre-authority validation error proves no lineage was created. After ambiguous START output, the controller calls target-scoped native status once and returns only its declared action. An ambiguous gentle_review_capture outcome independently reconciles once and never replays the capture.",
|
|
9861
10038
|
"Use gentle_review only for native review authority operations; delivery commands follow ordinary repository policy.",
|
|
9862
|
-
'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it
|
|
10039
|
+
'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it is the only way to get an independent verifier, keeps nativeRisk, returns agentEscalation, and can never lower a tier). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); caller writerModelId/writerEffort are only a fallback when no runtime evidence exists (caller), otherwise the profile is small (fallback). The writer profile is reported but never adds a verifier. The plan follows its risk tier whatever RDD reads and whatever the native review outcome: the native review is an additional outside view and never removes a verifier the tier requires (verify-always-rdd-high S1). ASSESS derives closed only from the native candidate.consumed fact for this exact candidate, written natively when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown; the outcome only explains the plan. A decline or unavailable review recorded by this process is bound to that exact candidate\'s own target identity, never to a different candidate or to bare repository state, and wins over closure. The result\'s outcome_source (explicit|derived|unknown) and writerProfileSource (runtime|caller|fallback) state which evidence produced each value. When native reports them, ASSESS projects reviewDue, reviewDueReason, candidate.consumed, and the native continuation verbatim; older binaries omit them and nothing is invented. Relay that continuation unchanged; never rebuild it. A native code review is not a substitute for applicable functional checks: tests, builds, and functional verification such as browser checks for UI changes still run when applicable. A failed or unavailable native assessment (nativeRisk "unassessable"), or a native high, informs only: the plan adds the independent verifier only when escalate names a Task Size high-risk item. This never mutates review authority state.',
|
|
9863
10040
|
],
|
|
9864
10041
|
parameters: REVIEW_CONTROLLER_PARAMETERS,
|
|
9865
10042
|
executionMode: "sequential",
|
|
10043
|
+
// Hidden declarations stay active and callable; Pi leaves their
|
|
10044
|
+
// declarations and guidelines out of the request.
|
|
10045
|
+
prepareLoadout(loadout) {
|
|
10046
|
+
if (!rddOffReviewLoadout) return undefined;
|
|
10047
|
+
const declared = new Set(loadout.declared.map((tool) => tool.name));
|
|
10048
|
+
return {
|
|
10049
|
+
descriptions: { gentle_review: RDD_OFF_REVIEW_DESCRIPTION },
|
|
10050
|
+
hiddenDeclarations: RDD_OFF_HIDDEN_REVIEW_TOOLS.filter((name) => declared.has(name)),
|
|
10051
|
+
};
|
|
10052
|
+
},
|
|
9866
10053
|
renderCall(args, theme, context) {
|
|
9867
10054
|
return renderGentleAiLifecycleCall(
|
|
9868
10055
|
reviewToolOperationPath(args),
|
|
@@ -10139,7 +10326,13 @@ function createGentleAiExtensionForTesting(
|
|
|
10139
10326
|
if (inFlightModeRead !== undefined) await Promise.race([inFlightModeRead, rddAbortRejection(rddStatusSignal)]).catch(() => undefined);
|
|
10140
10327
|
const rddStatusLine = !isPrimarySession
|
|
10141
10328
|
? undefined
|
|
10142
|
-
:
|
|
10329
|
+
: pinRddStatusLine(
|
|
10330
|
+
pinnedRddStatusLines.get(subagentDepthKey),
|
|
10331
|
+
await resolveRddStatusLine(nativeReviewCli, ctx.cwd, rddStatusSignal, undefined, ctx),
|
|
10332
|
+
);
|
|
10333
|
+
if (rddStatusLine !== undefined) pinnedRddStatusLines.set(subagentDepthKey, rddStatusLine);
|
|
10334
|
+
rddOffReviewLoadout = isPrimarySession && isRddStatusLineOff(rddStatusLine);
|
|
10335
|
+
if (rddOffReviewLoadout) applyRddOffReviewPrompt(event.systemPromptOptions as ReviewToolPromptOptions | undefined);
|
|
10143
10336
|
const gentlePrompt = rddStatusLine === undefined
|
|
10144
10337
|
? ""
|
|
10145
10338
|
: `\n\n${buildGentlePrompt(
|
|
@@ -10222,7 +10415,7 @@ function createGentleAiExtensionForTesting(
|
|
|
10222
10415
|
consumeReviewMutation(pi, ctx.sessionManager, root, mutation, "nudged", targetIdentity);
|
|
10223
10416
|
});
|
|
10224
10417
|
|
|
10225
|
-
|
|
10418
|
+
const recordWriteResult = async (event: ToolResultEvent, ctx: ExtensionContext): Promise<void> => {
|
|
10226
10419
|
if (!reminderSessionActive || (reminderManager && reminderManager !== ctx.sessionManager) || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
10227
10420
|
if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
|
|
10228
10421
|
try {
|
|
@@ -10234,9 +10427,33 @@ function createGentleAiExtensionForTesting(
|
|
|
10234
10427
|
if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
|
|
10235
10428
|
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && prospectiveRoot && !resolveSessionWorktree(ctx.cwd, ctx.cwd)) await prepareBoundSessionRepository(ctx.sessionManager, ctx.sessionManager.getCwd?.() ?? ctx.cwd, ctx.signal);
|
|
10236
10429
|
} catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
|
|
10430
|
+
};
|
|
10431
|
+
const contextBackstopSignal = createContextBackstopSignal(permissionEnvironment);
|
|
10432
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
10433
|
+
// Harness roadmap R1: the primary session's measured context is checked
|
|
10434
|
+
// against the backstop on every tool result; the first result at or past
|
|
10435
|
+
// it carries one notice, after any other patch below.
|
|
10436
|
+
const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
|
|
10437
|
+
const primary = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(sessionKey) ?? 0) === 0;
|
|
10438
|
+
let notice: string | undefined;
|
|
10439
|
+
try { notice = primary ? contextBackstopSignal(sessionKey, ctx.getContextUsage?.()) : undefined; } catch { notice = undefined; }
|
|
10440
|
+
const withNotice = (patch: ToolResultEventResult | undefined) => notice ? appendContextBackstopNotice(event, patch, notice) : patch;
|
|
10441
|
+
// Harness roadmap T4: a parent commit reports the branch's authored
|
|
10442
|
+
// changed lines so delivery slicing reads a number, not an estimate.
|
|
10443
|
+
if (SHELL_COMMAND_TOOLS.has(event.toolName)) {
|
|
10444
|
+
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD === "1") return undefined;
|
|
10445
|
+
try { return withNotice(await commitSliceToolResult(event, ctx.cwd)); } catch { return withNotice(undefined); }
|
|
10446
|
+
}
|
|
10447
|
+
await recordWriteResult(event, ctx);
|
|
10448
|
+
return withNotice(undefined);
|
|
10237
10449
|
});
|
|
10238
10450
|
|
|
10239
10451
|
pi.on("tool_call", async (event, ctx) => {
|
|
10452
|
+
// Native review tools stalled the session when a codemode script called them
|
|
10453
|
+
// (a nested call, which carries parentToolCallId); direct calls return at once.
|
|
10454
|
+
if (event.toolName.startsWith("gentle_review") && typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string") {
|
|
10455
|
+
return { block: true, reason: `${event.toolName} runs only as a direct tool call: call it directly, never from codemode or another tool.` };
|
|
10456
|
+
}
|
|
10240
10457
|
const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
|
|
10241
10458
|
const yoloActive = primaryToolCall && await yolo.active(ctx);
|
|
10242
10459
|
const sensitivePathDenied = evaluateSensitivePathTool(
|
|
@@ -10244,11 +10461,22 @@ function createGentleAiExtensionForTesting(
|
|
|
10244
10461
|
event.input,
|
|
10245
10462
|
);
|
|
10246
10463
|
if (sensitivePathDenied) return sensitivePathDenied;
|
|
10464
|
+
const deliveredTestDenied = evaluateDeliveredTestTool(event.toolName, event.input, ctx.cwd);
|
|
10465
|
+
if (deliveredTestDenied) return deliveredTestDenied;
|
|
10247
10466
|
if (event.toolName === "subagent_run") {
|
|
10248
10467
|
const judgmentDayFixDenied = rejectInvalidJudgmentDayFixDispatch(event.input);
|
|
10249
10468
|
if (judgmentDayFixDenied) return judgmentDayFixDenied;
|
|
10250
10469
|
const writerScopeDenied = rejectUnscopedBoundedWriterDispatch(event.input);
|
|
10251
10470
|
if (writerScopeDenied) return writerScopeDenied;
|
|
10471
|
+
if (primaryToolCall) {
|
|
10472
|
+
const launch = event.input as Record<string, unknown>;
|
|
10473
|
+
const selected = [launch.workspace_root, launch.repository_root].find((value): value is string => typeof value === "string" && value.trim() !== "");
|
|
10474
|
+
const root = selected ? resolve(ctx.cwd, selected) : ctx.cwd;
|
|
10475
|
+
const undocumentedWriter = rejectUndocumentedWriterLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
|
|
10476
|
+
if (undocumentedWriter) return undocumentedWriter;
|
|
10477
|
+
const unriskedVerifier = rejectUnriskedVerifierLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
|
|
10478
|
+
if (unriskedVerifier) return unriskedVerifier;
|
|
10479
|
+
}
|
|
10252
10480
|
try {
|
|
10253
10481
|
injectReviewCandidateView(event.input, candidateViews);
|
|
10254
10482
|
return undefined;
|
|
@@ -10402,6 +10630,10 @@ function createGentleAiExtensionForTesting(
|
|
|
10402
10630
|
if (modelConfig.status === "invalid") {
|
|
10403
10631
|
lines.push(`remedy: fix or remove ${modelConfig.path}`);
|
|
10404
10632
|
}
|
|
10633
|
+
lines.push(...herdrSetupHint(process.env, {
|
|
10634
|
+
interactive: ctx.mode === "tui" && ctx.hasUI,
|
|
10635
|
+
isSocket: (path) => { try { return statSync(path).isSocket(); } catch { return false; } },
|
|
10636
|
+
}));
|
|
10405
10637
|
ctx.ui.notify(
|
|
10406
10638
|
lines.join("\n"),
|
|
10407
10639
|
lines.some((line) => line.startsWith("fail:")) || assetLines.some((line) => line.startsWith("warn:")) ? "warning" : "info",
|