gentle-pi 4.1.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 +352 -128
- 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/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- 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/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- 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 +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- 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/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- 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 +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- 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/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- 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-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- 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/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- 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/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/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 +35 -7
- 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 +443 -8
- 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 +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- 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/odd-runtime-delegation-gate.test.ts +4 -1
- 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 +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- 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 +131 -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/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -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-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- 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/vim-editor-adapter.test.ts +41 -15
- 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 +24 -1
- 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
|
-
import { recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
|
|
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 }]),
|
|
@@ -1098,9 +1143,11 @@ function homeRootedRepository(cwd: string): string | undefined {
|
|
|
1098
1143
|
* `NativeReviewModeStatus`) that the `/gentle:review-mode` command also
|
|
1099
1144
|
* calls. Never throws and never hangs past `signal`'s deadline when one is
|
|
1100
1145
|
* given: an absent binary, a timed-out/aborted process, or a native CLI
|
|
1101
|
-
* failure all resolve to `undefined
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1146
|
+
* failure all resolve to `undefined` unless an earlier read for the same cwd
|
|
1147
|
+
* succeeded, in which case that last known status is kept (gentle-shell#1029:
|
|
1148
|
+
* the rendered line sits in the cached prompt prefix). `ctx` is optional and
|
|
1149
|
+
* used only for a one-shot (per process) UI notice when the read is
|
|
1150
|
+
* swallowed, so a sustained native outage is observable beyond the prompt.
|
|
1104
1151
|
*/
|
|
1105
1152
|
async function resolveRddModeStatus(
|
|
1106
1153
|
nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
|
|
@@ -1112,13 +1159,20 @@ async function resolveRddModeStatus(
|
|
|
1112
1159
|
const nowMs = now();
|
|
1113
1160
|
const cached = rddStatusMemo.get(cwd);
|
|
1114
1161
|
if (cached !== undefined && cached.expiresAt > nowMs) return cached.status;
|
|
1115
|
-
|
|
1162
|
+
// gentle-shell#1029: the rendered line is part of the cached system-prompt
|
|
1163
|
+
// prefix. A re-read that fails or times out keeps the last successfully
|
|
1164
|
+
// read status instead of flipping the prompt to "unknown" and back, which
|
|
1165
|
+
// would invalidate the provider's prefix cache for the whole conversation.
|
|
1166
|
+
// Only a status the native reader actually reports replaces a known one.
|
|
1167
|
+
const outcome = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
|
|
1168
|
+
const read = "status" in outcome ? outcome.status : undefined;
|
|
1169
|
+
const status = read ?? cached?.status;
|
|
1116
1170
|
rddStatusMemo.set(cwd, { status, expiresAt: nowMs + RDD_STATUS_MEMO_TTL_MS });
|
|
1117
|
-
if (
|
|
1171
|
+
if ("cause" in outcome && !rddStatusUnavailableWarned) {
|
|
1118
1172
|
rddStatusUnavailableWarned = true;
|
|
1119
1173
|
if (ctx?.hasUI) {
|
|
1120
1174
|
ctx.ui.notify(
|
|
1121
|
-
|
|
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.`,
|
|
1122
1176
|
"warning",
|
|
1123
1177
|
);
|
|
1124
1178
|
}
|
|
@@ -1131,6 +1185,46 @@ function isRddStatusLineOff(line: string | undefined): boolean {
|
|
|
1131
1185
|
return line !== undefined && line.startsWith("Receipt-driven development: off ");
|
|
1132
1186
|
}
|
|
1133
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
|
+
|
|
1134
1228
|
/** Resolves and renders the RDD status line for a production call site in one call. */
|
|
1135
1229
|
async function resolveRddStatusLine(
|
|
1136
1230
|
nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
|
|
@@ -1197,13 +1291,13 @@ function renderOrchestratorPrompt(
|
|
|
1197
1291
|
// pinned at an 8192-byte budget (tests/orchestrator-budget.test.ts).
|
|
1198
1292
|
const PROVIDER_CONTRACT_MIRROR_ROOT = join(PACKAGE_ROOT, "contracts", "review-provider-contract-mirror");
|
|
1199
1293
|
const PROVIDER_CONTRACT_LOCK_FILE = "provider-contract.lock.json";
|
|
1200
|
-
const
|
|
1294
|
+
const PI_MIRROR_RUNTIME = "pi";
|
|
1201
1295
|
|
|
1202
1296
|
let reviewContractPromptFragmentCache: string | null | undefined;
|
|
1203
1297
|
let reviewContractPromptMissingWarned = false;
|
|
1204
1298
|
|
|
1205
|
-
//
|
|
1206
|
-
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 {
|
|
1207
1301
|
try {
|
|
1208
1302
|
const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
|
|
1209
1303
|
const lock = JSON.parse(readFileSync(lockPath, "utf8")) as {
|
|
@@ -1211,20 +1305,71 @@ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRA
|
|
|
1211
1305
|
entries?: Record<string, unknown>;
|
|
1212
1306
|
};
|
|
1213
1307
|
if (typeof lock.contract_semver !== "string" || lock.contract_semver === "") return null;
|
|
1214
|
-
const expectedSha256 = lock.entries?.[
|
|
1308
|
+
const expectedSha256 = lock.entries?.[bundlePath];
|
|
1215
1309
|
if (typeof expectedSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedSha256)) return null;
|
|
1216
|
-
const
|
|
1217
|
-
const rawBytes = readFileSync(
|
|
1310
|
+
const filePath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", ...bundlePath.split("/"));
|
|
1311
|
+
const rawBytes = readFileSync(filePath);
|
|
1218
1312
|
const actualSha256 = createHash("sha256").update(rawBytes).digest("hex");
|
|
1219
1313
|
if (!timingSafeEqual(Buffer.from(expectedSha256, "hex"), Buffer.from(actualSha256, "hex"))) return null;
|
|
1220
|
-
|
|
1221
|
-
if (text.length === 0) return null;
|
|
1222
|
-
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") };
|
|
1223
1315
|
} catch {
|
|
1224
1316
|
return null;
|
|
1225
1317
|
}
|
|
1226
1318
|
}
|
|
1227
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
|
+
|
|
1228
1373
|
function loadReviewContractPromptFragment(
|
|
1229
1374
|
ctx: Pick<ExtensionContext, "hasUI" | "ui">,
|
|
1230
1375
|
mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT,
|
|
@@ -1306,34 +1451,44 @@ ${personaPrompt}
|
|
|
1306
1451
|
|
|
1307
1452
|
${languageBoundary}
|
|
1308
1453
|
|
|
1454
|
+
## Authorized work continuity
|
|
1455
|
+
|
|
1456
|
+
An authorized outcome includes its necessary in-scope implementation, documentation and verification steps. Continue those steps without asking the human to approve each transition. Never end a turn merely announcing an immediately executable authorized next step; execute it instead. A progress update is not a request for permission.
|
|
1457
|
+
|
|
1458
|
+
Stop only for an unresolved consequential scope/product decision, required explicit consent, a denied action, unavailable required proof, or the human's explicit stop. Do not infer permission for publishing, destructive actions, unrelated scope or native consent from this continuity rule. Report a concrete blocker and the exact decision needed rather than requesting a generic "continue".
|
|
1459
|
+
|
|
1460
|
+
For background work, launch the bounded action and end the turn when no independent in-scope work remains. Retain its identity and resume the already-authorized continuation automatically on its completion event; do not ask the human to say "continue", poll, or claim completion while checks remain pending.
|
|
1461
|
+
|
|
1309
1462
|
Default workflow: Organic Driven Development (MANDATORY)
|
|
1310
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:
|
|
1311
|
-
|
|
1312
|
-
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.
|
|
1313
|
-
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.
|
|
1314
|
-
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.
|
|
1315
|
-
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.
|
|
1316
|
-
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.
|
|
1317
|
-
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.
|
|
1318
|
-
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.
|
|
1319
|
-
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()}
|
|
1320
1465
|
|
|
1321
1466
|
Harness principles:
|
|
1322
1467
|
- el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
|
|
1323
|
-
- 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.
|
|
1324
1468
|
- Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
|
|
1325
1469
|
- Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
|
|
1326
|
-
-
|
|
1327
|
-
- 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.
|
|
1328
1471
|
- Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
|
|
1329
1472
|
- Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
|
|
1330
1473
|
|
|
1331
|
-
${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.";
|
|
1332
1485
|
}
|
|
1333
1486
|
|
|
1334
1487
|
// Matches `git [global-flags] push` — tolerates flags like -C /repo or --work-tree=/tmp
|
|
1335
1488
|
// between `git` and the subcommand. Short flags may be followed by a separate value token.
|
|
1336
|
-
|
|
1489
|
+
// gentle-shell#2172: `\s+` rather than a literal space, so a tab-separated
|
|
1490
|
+
// `git\tpush` is guarded like any other spelling.
|
|
1491
|
+
const GIT_GLOBAL_FLAGS_SRC = String.raw`(?:\s+--?\S+(?:\s+[^-\s]\S*)?)*\s+`;
|
|
1337
1492
|
const GIT_PUSH_RE = new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}(push)\b`);
|
|
1338
1493
|
|
|
1339
1494
|
const DENIED_BASH_PATTERNS: RegExp[] = [
|
|
@@ -1399,7 +1554,7 @@ interface LoadGuardrailsOptions {
|
|
|
1399
1554
|
|
|
1400
1555
|
const GUARDED_KEY_PATTERNS: Record<GuardedCommandKey, RegExp> = {
|
|
1401
1556
|
gitPush: GIT_PUSH_RE,
|
|
1402
|
-
gitRebase:
|
|
1557
|
+
gitRebase: new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}(rebase)\b`),
|
|
1403
1558
|
gitBranchDeleteForce: /\bgit\s+(branch)\s+(?:-[a-zA-Z]*D[a-zA-Z]*|-[a-zA-Z]*d[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*d[a-zA-Z]*|--delete\b[^\r\n;&|]*--force\b|--force\b[^\r\n;&|]*--delete\b)/,
|
|
1404
1559
|
npmPublish: /\bnpm\s+(publish)\b/,
|
|
1405
1560
|
piRemove: /\bpi\s+(remove)\b/,
|
|
@@ -1441,12 +1596,15 @@ function collectGuardedMatches(
|
|
|
1441
1596
|
config: RuntimeGuardrailsConfig,
|
|
1442
1597
|
): GuardMatch[] {
|
|
1443
1598
|
const matches: GuardMatch[] = [];
|
|
1599
|
+
const dataRanges = dataArgumentRanges(command);
|
|
1444
1600
|
for (const [key, pattern] of Object.entries(GUARDED_KEY_PATTERNS) as [
|
|
1445
1601
|
GuardedCommandKey,
|
|
1446
1602
|
RegExp,
|
|
1447
1603
|
][]) {
|
|
1448
1604
|
const globalPattern = new RegExp(pattern.source, `${pattern.flags}g`);
|
|
1449
1605
|
for (const match of command.matchAll(globalPattern)) {
|
|
1606
|
+
// gentle-shell#2174: text inside a data command's argument is not an operation.
|
|
1607
|
+
if (inDataArgument(dataRanges, match.index)) continue;
|
|
1450
1608
|
const action = config.autonomousMode
|
|
1451
1609
|
? (config.guardedCommands[key] ?? AUTONOMOUS_DEFAULT_ACTIONS[key])
|
|
1452
1610
|
: "confirm";
|
|
@@ -1460,6 +1618,10 @@ function collectGuardedMatches(
|
|
|
1460
1618
|
return matches.sort((left, right) => left.triggerIndex - right.triggerIndex);
|
|
1461
1619
|
}
|
|
1462
1620
|
|
|
1621
|
+
function inDataArgument(ranges: ReadonlyArray<readonly [number, number]>, index: number): boolean {
|
|
1622
|
+
return ranges.some(([start, end]) => index >= start && index < end);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1463
1625
|
function evaluateGuardedCommand(
|
|
1464
1626
|
command: string,
|
|
1465
1627
|
config: RuntimeGuardrailsConfig,
|
|
@@ -1470,8 +1632,10 @@ function evaluateGuardedCommand(
|
|
|
1470
1632
|
if (hardDeny) return { action: "block", triggerIndex: hardDeny.triggerIndex, matches };
|
|
1471
1633
|
|
|
1472
1634
|
// Hard denies override every configured action across the complete command.
|
|
1635
|
+
const dataRanges = dataArgumentRanges(command);
|
|
1473
1636
|
for (const pattern of DENIED_BASH_PATTERNS) {
|
|
1474
|
-
const denied = pattern.
|
|
1637
|
+
const denied = [...command.matchAll(new RegExp(pattern.source, `${pattern.flags.replace("g", "")}g`))]
|
|
1638
|
+
.find((candidate) => !inDataArgument(dataRanges, candidate.index));
|
|
1475
1639
|
if (!denied) continue;
|
|
1476
1640
|
const matchedAction = matches.find((match) =>
|
|
1477
1641
|
match.triggerIndex >= denied.index && match.triggerIndex < denied.index + denied[0].length,
|
|
@@ -1817,14 +1981,76 @@ function createHerdrConfirmationLifecycle(events: ExtensionAPI["events"]): Herdr
|
|
|
1817
1981
|
};
|
|
1818
1982
|
}
|
|
1819
1983
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1984
|
+
// gentle-shell#2172: YOLO waives reversible delivery, not only one bare push.
|
|
1985
|
+
// A guarded segment is judged from its trigger word (`push`/`rebase`) to the
|
|
1986
|
+
// next command separator; a plain token or allow-listed option list is the
|
|
1987
|
+
// whole eligible grammar, so quotes, wrappers and unknown options fall back
|
|
1988
|
+
// to the ordinary confirmation. Everything that deletes or rewrites a remote
|
|
1989
|
+
// ref, publishes, or runs arbitrary commands is deliberately absent:
|
|
1990
|
+
// `--delete`/`-d`, `:dst` and `+src` refspecs, `--mirror`, `--prune`,
|
|
1991
|
+
// `--all`, every force spelling, `--receive-pack`/`--exec`, `--repo`, `-o`.
|
|
1992
|
+
const YOLO_WAIVABLE_KEYS: ReadonlySet<GuardedCommandKey> = new Set(["gitPush", "gitRebase"]);
|
|
1993
|
+
const YOLO_SEGMENT_END_RE = /&&|\|\||;|\||\n|\r/;
|
|
1994
|
+
// Text between the segment start and the `push`/`rebase` word: optional
|
|
1995
|
+
// variable assignments, `env`/`command` prefixes and the `git` word with at
|
|
1996
|
+
// most a `-C <path>`. Other global options (`-c key=value`, `--exec-path`,
|
|
1997
|
+
// `--git-dir`) can run or redirect code, so they keep the confirmation.
|
|
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+$/;
|
|
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)`);
|
|
2008
|
+
|
|
2009
|
+
function yoloGuardedSegment(command: string, triggerIndex: number): string {
|
|
2010
|
+
const rest = command.slice(triggerIndex);
|
|
2011
|
+
const end = rest.search(YOLO_SEGMENT_END_RE);
|
|
2012
|
+
return end < 0 ? rest : rest.slice(0, end);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function yoloSegmentPrefix(command: string, triggerIndex: number): string {
|
|
2016
|
+
const head = command.slice(0, triggerIndex);
|
|
2017
|
+
const separators = [...head.matchAll(new RegExp(YOLO_SEGMENT_END_RE.source, "g"))];
|
|
2018
|
+
const last = separators.at(-1);
|
|
2019
|
+
return last === undefined ? head : head.slice(last.index + last[0].length);
|
|
1824
2020
|
}
|
|
1825
2021
|
|
|
1826
|
-
/**
|
|
1827
|
-
|
|
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;
|
|
2032
|
+
for (const match of evaluation.matches) {
|
|
2033
|
+
if (!YOLO_WAIVABLE_KEYS.has(match.key)) return false;
|
|
2034
|
+
if (!YOLO_GIT_PREFIX_RE.test(yoloSegmentPrefix(command, match.triggerIndex))) return false;
|
|
2035
|
+
const segment = yoloGuardedSegment(command, match.triggerIndex);
|
|
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;
|
|
2038
|
+
}
|
|
2039
|
+
// The data-loss recognizer leaves non-hard Git effects (reset, stash drop,
|
|
2040
|
+
// restore, checkout --) to the primary policy; under YOLO only a rebase that
|
|
2041
|
+
// a guarded match already vetted may remain.
|
|
2042
|
+
const rebaseTriggers = new Set(evaluation.matches.filter((match) => match.key === "gitRebase").map((match) => match.triggerIndex));
|
|
2043
|
+
for (const [index, effect] of effects.entries()) {
|
|
2044
|
+
if (scratch[index]) continue;
|
|
2045
|
+
if (effect.hardDeny || effect.kind !== "git") return false;
|
|
2046
|
+
const rebase = /^git(?:\s+(?:-C|-c|--git-dir|--work-tree|--namespace)\s+\S+|\s+-\S+)*\s+(rebase)\b/.exec(command.slice(effect.triggerIndex));
|
|
2047
|
+
if (rebase === null || !rebaseTriggers.has(effect.triggerIndex + rebase.index + rebase[0].lastIndexOf("rebase"))) return false;
|
|
2048
|
+
}
|
|
2049
|
+
return true;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/** Preserve explicitly configured confirmations for the waivable keys present, even when legacy env/autonomy resolution ignores them. */
|
|
2053
|
+
function yoloConfiguredRestriction(cwd: string, keys: readonly GuardedCommandKey[], options: LoadGuardrailsOptions = {}): GuardAction | undefined {
|
|
1828
2054
|
let restriction: GuardAction | undefined;
|
|
1829
2055
|
for (const path of [join(options.gentlePiConfigHome ?? gentleAiConfigHome(), "runtime-guardrails.json"), join(cwd, ".pi", "gentle-ai", "runtime-guardrails.json")]) {
|
|
1830
2056
|
try {
|
|
@@ -1834,9 +2060,11 @@ function yoloPushConfiguredRestriction(cwd: string, options: LoadGuardrailsOptio
|
|
|
1834
2060
|
restriction ??= "confirm";
|
|
1835
2061
|
continue;
|
|
1836
2062
|
}
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
2063
|
+
for (const key of keys) {
|
|
2064
|
+
const action = isRecord(parsed.guardedCommands) ? parsed.guardedCommands[key] : undefined;
|
|
2065
|
+
if (action === "block") return "block";
|
|
2066
|
+
if (action !== undefined && action !== "allow") restriction = "confirm";
|
|
2067
|
+
}
|
|
1840
2068
|
} catch { restriction ??= "confirm"; }
|
|
1841
2069
|
}
|
|
1842
2070
|
return restriction;
|
|
@@ -1855,8 +2083,9 @@ async function confirmCommand(
|
|
|
1855
2083
|
// Configuration provenance is distinct from legacy autonomy resolution, which
|
|
1856
2084
|
// can ignore confirmations when an environment override is present. Activation
|
|
1857
2085
|
// cannot waive a restriction from either user or project configuration layer.
|
|
1858
|
-
const
|
|
1859
|
-
|
|
2086
|
+
const waivableKeys = evaluation.matches.map((match) => match.key).filter((key) => YOLO_WAIVABLE_KEYS.has(key));
|
|
2087
|
+
const configuredRestriction = yoloActive && waivableKeys.length > 0
|
|
2088
|
+
? yoloConfiguredRestriction(ctx.cwd, waivableKeys) : undefined;
|
|
1860
2089
|
const classification = evaluation.action === "block" || configuredRestriction === "block"
|
|
1861
2090
|
? "block" : configuredRestriction ?? evaluation.action;
|
|
1862
2091
|
|
|
@@ -1892,7 +2121,7 @@ async function confirmCommand(
|
|
|
1892
2121
|
}
|
|
1893
2122
|
|
|
1894
2123
|
// Full T1 evaluation has already run. YOLO never changes persistent autonomy.
|
|
1895
|
-
if (yoloActive && configuredRestriction === undefined &&
|
|
2124
|
+
if (yoloActive && configuredRestriction === undefined && isReversibleYoloCommand(command, evaluation)) return undefined;
|
|
1896
2125
|
|
|
1897
2126
|
// classification === "confirm"
|
|
1898
2127
|
if (!ctx.hasUI) {
|
|
@@ -5064,7 +5293,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
|
|
|
5064
5293
|
},
|
|
5065
5294
|
input: {
|
|
5066
5295
|
type: "string",
|
|
5067
|
-
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.",
|
|
5068
5297
|
},
|
|
5069
5298
|
outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
5070
5299
|
inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
|
|
@@ -6535,6 +6764,8 @@ const processRetainedNativeStatusSelections = new Map<PendingReviewConsentSessio
|
|
|
6535
6764
|
// named-agent start increments the depth, a matching end decrements it,
|
|
6536
6765
|
// and a fresh primary-loop start resets it to 0.
|
|
6537
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>();
|
|
6538
6769
|
|
|
6539
6770
|
// gentle-shell#1064 slice 1: the parent-session profile binding store lives in
|
|
6540
6771
|
// lib/session-profile-binding.ts (in-process, keyed by parent session id). The
|
|
@@ -7914,6 +8145,20 @@ function isSelectedReviewCapture(value: SelectedReviewCapture | Record<string, u
|
|
|
7914
8145
|
|
|
7915
8146
|
function captureGroupRejected(reason: string): Record<string, unknown> { return captureBindingRejected(reason, true); }
|
|
7916
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
|
+
|
|
7917
8162
|
function hasExactReviewCaptureSuffix(status: ReviewStatusV3, expected: readonly string[]): boolean {
|
|
7918
8163
|
const current = status.nextTransition?.kind === "collect"
|
|
7919
8164
|
? (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation === "review.capture-result").map(canonicalReviewCaptureBinding)
|
|
@@ -8220,7 +8465,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
8220
8465
|
if (negotiated.transport !== undefined) return hostTransportUnavailable("gentle_review_capture_group", negotiated.transport);
|
|
8221
8466
|
status = negotiated.status!;
|
|
8222
8467
|
} catch (error) {
|
|
8223
|
-
return
|
|
8468
|
+
return captureGroupStatusFailure(error);
|
|
8224
8469
|
}
|
|
8225
8470
|
const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings);
|
|
8226
8471
|
if (!isSelectedReviewCaptureGroup(group)) return group;
|
|
@@ -8266,7 +8511,7 @@ async function executeReviewCaptureGroupOperation(
|
|
|
8266
8511
|
if (!hasExactReviewCaptureSuffix(negotiated.status!, canonicalBindings.slice(index))) return { ...captureGroupAuthorityDrift(negotiated.status!), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8267
8512
|
current = selectExactReviewCapture(negotiated.status!, parameters.lineageId, canonicalBindings[index]!);
|
|
8268
8513
|
} catch (error) {
|
|
8269
|
-
return { ...
|
|
8514
|
+
return { ...captureGroupStatusFailure(error), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8270
8515
|
}
|
|
8271
8516
|
if (!isSelectedReviewCapture(current)) return { ...captureGroupRejected(String(current.reason ?? "current STATUS rejected a reviewer binding")), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
|
|
8272
8517
|
try {
|
|
@@ -9384,8 +9629,8 @@ export const __testing = {
|
|
|
9384
9629
|
guardedCommandPreview,
|
|
9385
9630
|
guardedCommandTitle,
|
|
9386
9631
|
loadRuntimeGuardrailsConfig,
|
|
9387
|
-
|
|
9388
|
-
|
|
9632
|
+
isReversibleYoloCommand,
|
|
9633
|
+
yoloConfiguredRestriction,
|
|
9389
9634
|
buildGentlePrompt,
|
|
9390
9635
|
nativeStatusUnsupported,
|
|
9391
9636
|
nativeStartRejection,
|
|
@@ -9401,6 +9646,7 @@ export const __testing = {
|
|
|
9401
9646
|
renderOrchestratorPrompt,
|
|
9402
9647
|
loadReviewContractPromptFragment,
|
|
9403
9648
|
readMirroredReviewContractFragment,
|
|
9649
|
+
readMirroredOddProtocolSteps,
|
|
9404
9650
|
loadBackgroundSubagentsPolicy,
|
|
9405
9651
|
resolveBackgroundSubagentsPolicy,
|
|
9406
9652
|
renderBackgroundSubagentsReport,
|
|
@@ -9410,6 +9656,7 @@ export const __testing = {
|
|
|
9410
9656
|
readActiveToolNames,
|
|
9411
9657
|
renderBackgroundSubagentsStatusLine,
|
|
9412
9658
|
renderRddStatusLine,
|
|
9659
|
+
pinRddStatusLine,
|
|
9413
9660
|
isValidRddModeStatus,
|
|
9414
9661
|
resolveRddModeStatus,
|
|
9415
9662
|
resolveRddStatusLine,
|
|
@@ -9487,6 +9734,9 @@ function createGentleAiExtensionForTesting(
|
|
|
9487
9734
|
cancel: () => classifier?.cancel(),
|
|
9488
9735
|
}, { configHome: gentlePiConfigHome(permissionEnvironment) });
|
|
9489
9736
|
const auto = autoPermissions;
|
|
9737
|
+
pi.on("tool_result", (event, ctx) => {
|
|
9738
|
+
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1") auto.toolResult(event, ctx);
|
|
9739
|
+
});
|
|
9490
9740
|
pi.on("input", (event, ctx) => {
|
|
9491
9741
|
if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1") auto.input(event, ctx);
|
|
9492
9742
|
return { action: "continue" };
|
|
@@ -9528,11 +9778,17 @@ function createGentleAiExtensionForTesting(
|
|
|
9528
9778
|
reviewSidebar.reset(ctx);
|
|
9529
9779
|
statusTiming.reset(ctx);
|
|
9530
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;
|
|
9531
9785
|
let reminderSessionActive = true;
|
|
9532
9786
|
let reminderEpoch = 0;
|
|
9533
9787
|
let reminderManager: ExtensionContext["sessionManager"] | undefined;
|
|
9534
9788
|
let unbindPreparation: (() => void) | undefined;
|
|
9535
9789
|
pi.on("session_shutdown", (event, context) => {
|
|
9790
|
+
pinnedRddStatusLines.delete(pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey));
|
|
9791
|
+
rddOffReviewLoadout = false;
|
|
9536
9792
|
auto.reset();
|
|
9537
9793
|
yolo.reset(context);
|
|
9538
9794
|
reviewSidebar.reset();
|
|
@@ -9780,10 +10036,20 @@ function createGentleAiExtensionForTesting(
|
|
|
9780
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.",
|
|
9781
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.",
|
|
9782
10038
|
"Use gentle_review only for native review authority operations; delivery commands follow ordinary repository policy.",
|
|
9783
|
-
'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.',
|
|
9784
10040
|
],
|
|
9785
10041
|
parameters: REVIEW_CONTROLLER_PARAMETERS,
|
|
9786
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
|
+
},
|
|
9787
10053
|
renderCall(args, theme, context) {
|
|
9788
10054
|
return renderGentleAiLifecycleCall(
|
|
9789
10055
|
reviewToolOperationPath(args),
|
|
@@ -10051,7 +10317,8 @@ function createGentleAiExtensionForTesting(
|
|
|
10051
10317
|
// this branch, so no line is resolved or computed for them.
|
|
10052
10318
|
// resolveRddStatusLine never throws and never hangs past
|
|
10053
10319
|
// RDD_STATUS_TIMEOUT_MS: an absent/timed-out/aborted/failing native
|
|
10054
|
-
// binary
|
|
10320
|
+
// binary keeps the last known line for this cwd, or renders the
|
|
10321
|
+
// fail-closed "unknown" line when none was ever read (gentle-shell#1029).
|
|
10055
10322
|
const rddStatusSignal = AbortSignal.timeout(RDD_STATUS_TIMEOUT_MS);
|
|
10056
10323
|
// A startup read still in flight fills the memo: wait for it within the
|
|
10057
10324
|
// same deadline instead of spawning the same STATUS a second time.
|
|
@@ -10059,7 +10326,13 @@ function createGentleAiExtensionForTesting(
|
|
|
10059
10326
|
if (inFlightModeRead !== undefined) await Promise.race([inFlightModeRead, rddAbortRejection(rddStatusSignal)]).catch(() => undefined);
|
|
10060
10327
|
const rddStatusLine = !isPrimarySession
|
|
10061
10328
|
? undefined
|
|
10062
|
-
:
|
|
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);
|
|
10063
10336
|
const gentlePrompt = rddStatusLine === undefined
|
|
10064
10337
|
? ""
|
|
10065
10338
|
: `\n\n${buildGentlePrompt(
|
|
@@ -10142,7 +10415,7 @@ function createGentleAiExtensionForTesting(
|
|
|
10142
10415
|
consumeReviewMutation(pi, ctx.sessionManager, root, mutation, "nudged", targetIdentity);
|
|
10143
10416
|
});
|
|
10144
10417
|
|
|
10145
|
-
|
|
10418
|
+
const recordWriteResult = async (event: ToolResultEvent, ctx: ExtensionContext): Promise<void> => {
|
|
10146
10419
|
if (!reminderSessionActive || (reminderManager && reminderManager !== ctx.sessionManager) || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
|
|
10147
10420
|
if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
|
|
10148
10421
|
try {
|
|
@@ -10154,9 +10427,33 @@ function createGentleAiExtensionForTesting(
|
|
|
10154
10427
|
if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
|
|
10155
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);
|
|
10156
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);
|
|
10157
10449
|
});
|
|
10158
10450
|
|
|
10159
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
|
+
}
|
|
10160
10457
|
const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
|
|
10161
10458
|
const yoloActive = primaryToolCall && await yolo.active(ctx);
|
|
10162
10459
|
const sensitivePathDenied = evaluateSensitivePathTool(
|
|
@@ -10164,11 +10461,22 @@ function createGentleAiExtensionForTesting(
|
|
|
10164
10461
|
event.input,
|
|
10165
10462
|
);
|
|
10166
10463
|
if (sensitivePathDenied) return sensitivePathDenied;
|
|
10464
|
+
const deliveredTestDenied = evaluateDeliveredTestTool(event.toolName, event.input, ctx.cwd);
|
|
10465
|
+
if (deliveredTestDenied) return deliveredTestDenied;
|
|
10167
10466
|
if (event.toolName === "subagent_run") {
|
|
10168
10467
|
const judgmentDayFixDenied = rejectInvalidJudgmentDayFixDispatch(event.input);
|
|
10169
10468
|
if (judgmentDayFixDenied) return judgmentDayFixDenied;
|
|
10170
10469
|
const writerScopeDenied = rejectUnscopedBoundedWriterDispatch(event.input);
|
|
10171
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
|
+
}
|
|
10172
10480
|
try {
|
|
10173
10481
|
injectReviewCandidateView(event.input, candidateViews);
|
|
10174
10482
|
return undefined;
|
|
@@ -10322,6 +10630,10 @@ function createGentleAiExtensionForTesting(
|
|
|
10322
10630
|
if (modelConfig.status === "invalid") {
|
|
10323
10631
|
lines.push(`remedy: fix or remove ${modelConfig.path}`);
|
|
10324
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
|
+
}));
|
|
10325
10637
|
ctx.ui.notify(
|
|
10326
10638
|
lines.join("\n"),
|
|
10327
10639
|
lines.some((line) => line.startsWith("fail:")) || assetLines.some((line) => line.startsWith("warn:")) ? "warning" : "info",
|