gentle-pi 2.0.0 → 2.1.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 +3 -12
- package/extensions/gentle-ai.ts +313 -119
- package/lib/gentle-ai-binary.ts +64 -17
- package/lib/native-review-cli.ts +158 -12
- package/lib/review-candidate-view.ts +9 -5
- package/lib/review-integration-v2.ts +5 -1
- package/package.json +1 -1
- package/runtime/gentle-ai-binary.mjs +64 -17
- package/runtime/native-review-cli.mjs +158 -12
- package/runtime/review-integration-v2.mjs +5 -1
- package/scripts/gentle-ai-installer.mjs +454 -71
- package/scripts/test-packed-runner.mjs +1 -1
- package/skills/_shared/review-ledger-contract.md +3 -1
- package/tests/gentle-ai-binary.test.ts +70 -3
- package/tests/gentle-ai-installer.test.ts +543 -7
- package/tests/gentle-ai.test.ts +2 -2
- package/tests/native-review-consent.test.ts +128 -0
- package/tests/native-review-parity-runtime.test.ts +15 -6
- package/tests/native-review-parity.test.ts +155 -46
- package/tests/package-manifest.test.ts +10 -3
- package/tests/review-candidate-view.test.ts +43 -0
- package/tests/review-controller-native-routing.test.ts +247 -6
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-integration-v2.test.ts +10 -1
- package/tests/runtime-harness.mjs +24 -12
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
existsSync,
|
|
5
5
|
lstatSync,
|
|
@@ -96,7 +96,7 @@ import {
|
|
|
96
96
|
type ReviewProjectionV1,
|
|
97
97
|
} from "../lib/review-snapshot.ts";
|
|
98
98
|
import { sanitizeTerminalText, stripAnsi } from "../lib/terminal-theme.ts";
|
|
99
|
-
import { CandidateViewError, CandidateViewRegistry, injectReviewCandidateView, resolveCanonicalCandidateBase, type NativeCandidateProjectionDescriptor } from "../lib/review-candidate-view.ts";
|
|
99
|
+
import { CandidateViewError, CandidateViewRegistry, injectReviewCandidateView, resolveCanonicalCandidateBase, type CandidateView, type NativeCandidateProjectionDescriptor } from "../lib/review-candidate-view.ts";
|
|
100
100
|
import {
|
|
101
101
|
createNativeReviewCli,
|
|
102
102
|
isCanonicalProcessString,
|
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
nativeReviewLegacyQuarantineAuthorization,
|
|
106
106
|
nativeReviewReconcileAuthorization,
|
|
107
107
|
NativeReviewCliError,
|
|
108
|
+
NativeReviewConsentRequiredError,
|
|
108
109
|
NATIVE_REVIEW_ERROR_CODE,
|
|
109
110
|
NATIVE_REVIEW_LEGACY_QUARANTINE,
|
|
110
111
|
NATIVE_REVIEW_LEGACY_ALIAS_REPAIR,
|
|
@@ -120,8 +121,7 @@ import {
|
|
|
120
121
|
type NativeStartResult,
|
|
121
122
|
type NativeValidateResult,
|
|
122
123
|
} from "../lib/native-review-cli.ts";
|
|
123
|
-
import {
|
|
124
|
-
import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
124
|
+
import type { ReviewConsentV2, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
125
125
|
import { assertDistinctCorrectionEvidence, resolveCorrectionStep, type CorrectionEvidence, type CorrectionOutcome, type CorrectionStep } from "../lib/review-correction-lifecycle.ts";
|
|
126
126
|
|
|
127
127
|
const GRAPH_V1_ORDINARY_READ_ONLY = "Graph-v1 ordinary review authority is read-only; use native compact-v2 review operations";
|
|
@@ -2106,6 +2106,7 @@ async function handlePersonaCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
2106
2106
|
|
|
2107
2107
|
const REVIEW_CONTROLLER_OPERATION = {
|
|
2108
2108
|
START: "start",
|
|
2109
|
+
ANSWER_CONSENT: "answer-consent",
|
|
2109
2110
|
FINALIZE: "finalize",
|
|
2110
2111
|
ADVANCE: "advance",
|
|
2111
2112
|
STATUS: "status",
|
|
@@ -2161,7 +2162,7 @@ const REVIEW_CONTROLLER_PARAMETERS = {
|
|
|
2161
2162
|
},
|
|
2162
2163
|
input: {
|
|
2163
2164
|
type: "string",
|
|
2164
|
-
description: "A JSON-serialized object string, not a nested object. New native ordinary START uses {\"mode\":\"ordinary\"};
|
|
2165
|
+
description: "A JSON-serialized object string, not a nested object. New native ordinary START uses {\"mode\":\"ordinary\"}; answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. Legacy compact START retains policyHash. FINALIZE supplies reviewer results, correction forecast, targeted validation, final evidence, and an explicit final_verification_passed boolean. Judgment Day retains graph-v1 input.",
|
|
2165
2166
|
},
|
|
2166
2167
|
outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
|
|
2167
2168
|
inputPath: { type: "string", description: "Repository-local JSON input file for finalize/advance (alternative to input). Legacy bundle import is retired." },
|
|
@@ -2461,7 +2462,7 @@ function parseReviewControllerParameters(value: unknown): ReviewControllerParame
|
|
|
2461
2462
|
// VALIDATE defers its lineage requirement to execution time: the proven
|
|
2462
2463
|
// release-from-protected-main fast path needs no receipt lineage, while
|
|
2463
2464
|
// every other validation still requires one before receipt validation.
|
|
2464
|
-
const needsLineage = ![REVIEW_CONTROLLER_OPERATION.START, REVIEW_CONTROLLER_OPERATION.FINALIZE, REVIEW_CONTROLLER_OPERATION.STATUS, REVIEW_CONTROLLER_OPERATION.EXPORT, REVIEW_CONTROLLER_OPERATION.IMPORT, REVIEW_CONTROLLER_OPERATION.INSPECT, REVIEW_CONTROLLER_OPERATION.RESET, REVIEW_CONTROLLER_OPERATION.RECOVER, REVIEW_CONTROLLER_OPERATION.RECOVER_LOCK, REVIEW_CONTROLLER_OPERATION.ABANDON, REVIEW_CONTROLLER_OPERATION.QUARANTINE_LEGACY, REVIEW_CONTROLLER_OPERATION.RECONCILE_AUTHORITY, REVIEW_CONTROLLER_OPERATION.REPAIR_LEGACY_ALIAS, REVIEW_CONTROLLER_OPERATION.REPAIR, REVIEW_CONTROLLER_OPERATION.VALIDATE, REVIEW_CONTROLLER_OPERATION.BIND_SDD].includes(value.operation as ReviewControllerOperation);
|
|
2465
|
+
const needsLineage = ![REVIEW_CONTROLLER_OPERATION.START, REVIEW_CONTROLLER_OPERATION.ANSWER_CONSENT, REVIEW_CONTROLLER_OPERATION.FINALIZE, REVIEW_CONTROLLER_OPERATION.STATUS, REVIEW_CONTROLLER_OPERATION.EXPORT, REVIEW_CONTROLLER_OPERATION.IMPORT, REVIEW_CONTROLLER_OPERATION.INSPECT, REVIEW_CONTROLLER_OPERATION.RESET, REVIEW_CONTROLLER_OPERATION.RECOVER, REVIEW_CONTROLLER_OPERATION.RECOVER_LOCK, REVIEW_CONTROLLER_OPERATION.ABANDON, REVIEW_CONTROLLER_OPERATION.QUARANTINE_LEGACY, REVIEW_CONTROLLER_OPERATION.RECONCILE_AUTHORITY, REVIEW_CONTROLLER_OPERATION.REPAIR_LEGACY_ALIAS, REVIEW_CONTROLLER_OPERATION.REPAIR, REVIEW_CONTROLLER_OPERATION.VALIDATE, REVIEW_CONTROLLER_OPERATION.BIND_SDD].includes(value.operation as ReviewControllerOperation);
|
|
2465
2466
|
if (needsLineage && (typeof value.lineageId !== "string" || value.lineageId.trim().length === 0)) {
|
|
2466
2467
|
throw new Error("Review controller requires a lineageId");
|
|
2467
2468
|
}
|
|
@@ -3866,6 +3867,7 @@ function nativeReviewModeSkipped(operation: ReviewControllerOperation, source: N
|
|
|
3866
3867
|
operation,
|
|
3867
3868
|
status: "skipped",
|
|
3868
3869
|
outcome: REVIEW_MODE_DISABLED_OUTCOME,
|
|
3870
|
+
delivery: "disabled/unmanaged",
|
|
3869
3871
|
mode_source: source,
|
|
3870
3872
|
reason: `receipt-driven development is disabled: ${operation} is skipped because the ${source} mode source keeps it off`,
|
|
3871
3873
|
...(continuation === undefined ? {} : { next_action: continuation }),
|
|
@@ -4326,12 +4328,33 @@ function assertFrozenPreCommitProjection(
|
|
|
4326
4328
|
return projection.candidateTree;
|
|
4327
4329
|
}
|
|
4328
4330
|
|
|
4331
|
+
function reproveNativePreCommitTree(
|
|
4332
|
+
derived: DerivedReviewGateTarget,
|
|
4333
|
+
lineageId: string,
|
|
4334
|
+
candidateViews: CandidateViewRegistry | null,
|
|
4335
|
+
): string | undefined {
|
|
4336
|
+
return assertFrozenPreCommitProjection(derived, lineageId, candidateViews) ??
|
|
4337
|
+
(derived.command.event === "pre-commit" ? derived.actualIntendedCommitTree : undefined);
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4329
4340
|
function authorizationTargetHash(derived: DerivedReviewGateTarget): string {
|
|
4330
4341
|
return derived.nativePublication === undefined
|
|
4331
4342
|
? canonicalHash(derived.target)
|
|
4332
4343
|
: canonicalHash({ target: derived.target, native_publication: derived.nativePublication });
|
|
4333
4344
|
}
|
|
4334
4345
|
|
|
4346
|
+
function nativeAuthorizationConsumptionIdentity(authorization: PendingReviewAuthorization | undefined): string | undefined {
|
|
4347
|
+
if (authorization?.native_gate === undefined) return undefined;
|
|
4348
|
+
return canonicalHash({
|
|
4349
|
+
command_hash: authorization.command_hash,
|
|
4350
|
+
target_hash: authorization.target_hash,
|
|
4351
|
+
lineage_id: authorization.native_gate.lineage_id,
|
|
4352
|
+
store_revision: authorization.native_gate.store_revision,
|
|
4353
|
+
fingerprint: authorization.native_gate.fingerprint,
|
|
4354
|
+
intended_tree: authorization.native_gate.intended_tree ?? null,
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4335
4358
|
function assertNativePublicationBinding(result: NativeValidateResult, derived: DerivedReviewGateTarget): void {
|
|
4336
4359
|
const returnedGate = result.gateContext.raw.gate;
|
|
4337
4360
|
if (returnedGate !== requestedNativeGate(derived) && (result.allowed || returnedGate !== "")) {
|
|
@@ -4383,6 +4406,64 @@ async function rederiveNativePublicationTarget(
|
|
|
4383
4406
|
return fresh;
|
|
4384
4407
|
}
|
|
4385
4408
|
|
|
4409
|
+
interface NativePreCommitReceiptConsumption {
|
|
4410
|
+
authorization?: PendingReviewAuthorization;
|
|
4411
|
+
delivery?: "disabled/unmanaged";
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
async function consumeNativePreCommitReceipt(
|
|
4415
|
+
command: string,
|
|
4416
|
+
defaultCwd: string,
|
|
4417
|
+
derivedTarget: DerivedReviewGateTarget,
|
|
4418
|
+
nativeReviewCli: NativeReviewCli,
|
|
4419
|
+
publicationProbe: PublicationProbe,
|
|
4420
|
+
publicationProbeTimeoutMs: number,
|
|
4421
|
+
signal?: AbortSignal,
|
|
4422
|
+
): Promise<NativePreCommitReceiptConsumption> {
|
|
4423
|
+
const derived = await deriveNativePublicationTarget(
|
|
4424
|
+
derivedTarget,
|
|
4425
|
+
publicationProbe,
|
|
4426
|
+
publicationProbeTimeoutMs,
|
|
4427
|
+
signal,
|
|
4428
|
+
);
|
|
4429
|
+
if (derived.command.event !== "pre-commit" || derived.actualIntendedCommitTree === undefined) return {};
|
|
4430
|
+
const result = await nativeReviewCli.validate({
|
|
4431
|
+
cwd: derived.command.cwd,
|
|
4432
|
+
gate: "pre-commit",
|
|
4433
|
+
...(signal === undefined ? {} : { signal }),
|
|
4434
|
+
});
|
|
4435
|
+
assertNativePublicationBinding(result, derived);
|
|
4436
|
+
const fresh = await rederiveNativePublicationTarget(
|
|
4437
|
+
derived,
|
|
4438
|
+
command,
|
|
4439
|
+
defaultCwd,
|
|
4440
|
+
publicationProbe,
|
|
4441
|
+
publicationProbeTimeoutMs,
|
|
4442
|
+
signal,
|
|
4443
|
+
);
|
|
4444
|
+
if (result.delivery === "disabled/unmanaged") return { delivery: result.delivery };
|
|
4445
|
+
if (!result.allowed || result.result !== "allow") return {};
|
|
4446
|
+
if (
|
|
4447
|
+
result.gateContext.lineageId.length === 0 ||
|
|
4448
|
+
result.gateContext.raw.candidate_tree !== derived.actualIntendedCommitTree ||
|
|
4449
|
+
fresh.actualIntendedCommitTree !== derived.actualIntendedCommitTree
|
|
4450
|
+
) throw new Error("Native approved receipt does not bind the exact current pre-commit tree");
|
|
4451
|
+
const commandHash = reviewAuthorizationKey(command, fresh.command.cwd);
|
|
4452
|
+
return {
|
|
4453
|
+
authorization: {
|
|
4454
|
+
command_hash: commandHash,
|
|
4455
|
+
target_hash: authorizationTargetHash(fresh),
|
|
4456
|
+
receipt_hash: null,
|
|
4457
|
+
native_gate: {
|
|
4458
|
+
lineage_id: result.gateContext.lineageId,
|
|
4459
|
+
store_revision: result.gateContext.storeRevision,
|
|
4460
|
+
fingerprint: nativeGateFingerprint(result, fresh),
|
|
4461
|
+
intended_tree: derived.actualIntendedCommitTree,
|
|
4462
|
+
},
|
|
4463
|
+
},
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4386
4467
|
interface NativeStartPolicyValidation {
|
|
4387
4468
|
policyPath?: string;
|
|
4388
4469
|
reason?: string;
|
|
@@ -4464,71 +4545,65 @@ function nativeStartRejection(reason: string, field?: string): Record<string, un
|
|
|
4464
4545
|
};
|
|
4465
4546
|
}
|
|
4466
4547
|
|
|
4467
|
-
|
|
4468
|
-
// consent question, distinct from and in addition to gentle-ai's own
|
|
4469
|
-
// internal headless notice tolerated in lib/native-review-cli.ts. Pi always
|
|
4470
|
-
// spawns gentle-ai without a TTY, so gentle-ai's own interactive prompt never
|
|
4471
|
-
// fires from Pi — this is Pi's own UI-layer question, asked once per clone.
|
|
4472
|
-
const REVIEW_CONSENT_TITLE = "Run the review now?";
|
|
4473
|
-
const REVIEW_CONSENT_VALUE_LINE = "Reviewing takes a bit longer, and it makes the result substantially safer.";
|
|
4474
|
-
const REVIEW_CONSENT_ANSWERS_LINE = "Yes = run the review now / No = not now, just this once";
|
|
4475
|
-
const REVIEW_CONSENT_OFF_PATH_LINE = "To turn reviews off for good, run /gentle:review-mode disable.";
|
|
4476
|
-
const REVIEW_CONSENT_HEADLESS_NOTICE = "Gentle AI reviewed this change without asking, because this session has no interface to answer on. Run /gentle:review-mode disable to turn reviews off, or /gentle:review-mode status to see the current setting.";
|
|
4477
|
-
const REVIEW_CONSENT_UNREADABLE_NOTICE = "Gentle AI could not read an answer, so it reviewed this change and will ask again next time.";
|
|
4548
|
+
const PENDING_REVIEW_CONSENT_TTL_MS = 10 * 60 * 1000;
|
|
4478
4549
|
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
)
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
if (
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4550
|
+
interface PendingReviewConsent {
|
|
4551
|
+
id: string;
|
|
4552
|
+
repositoryCwd: string;
|
|
4553
|
+
candidateView: CandidateView;
|
|
4554
|
+
consent: ReviewConsentV2;
|
|
4555
|
+
consentDigest: string;
|
|
4556
|
+
expiresAt: number;
|
|
4557
|
+
expiry?: ReturnType<typeof setTimeout>;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
function consumePendingReviewConsent(pending: PendingReviewConsent, pendingReviewConsents: Map<string, PendingReviewConsent>): void {
|
|
4561
|
+
if (pending.expiry !== undefined) clearTimeout(pending.expiry);
|
|
4562
|
+
pending.expiry = undefined;
|
|
4563
|
+
if (pendingReviewConsents.get(pending.id) === pending) pendingReviewConsents.delete(pending.id);
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
function cleanupPendingReviewConsent(pending: PendingReviewConsent, pendingReviewConsents: Map<string, PendingReviewConsent>, candidateViews: CandidateViewRegistry | null): void {
|
|
4567
|
+
consumePendingReviewConsent(pending, pendingReviewConsents);
|
|
4568
|
+
if (![...pendingReviewConsents.values()].some((current) => current.candidateView.token === pending.candidateView.token)) candidateViews?.cleanup(pending.candidateView.token);
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
function cleanupAllPendingReviewConsents(pendingReviewConsents: Map<string, PendingReviewConsent>, candidateViews: CandidateViewRegistry | null): void {
|
|
4572
|
+
for (const pending of [...pendingReviewConsents.values()]) cleanupPendingReviewConsent(pending, pendingReviewConsents, candidateViews);
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
function reviewConsentDigest(consent: ReviewConsentV2): string {
|
|
4576
|
+
return createHash("sha256").update(JSON.stringify(consent)).digest("hex");
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
function completeNativeStart(
|
|
4580
|
+
operation: ReviewControllerOperation,
|
|
4581
|
+
result: NativeStartResult,
|
|
4582
|
+
workspaceRoot: string,
|
|
4583
|
+
candidateView: CandidateView | undefined,
|
|
4584
|
+
candidateViews: CandidateViewRegistry | null,
|
|
4585
|
+
): Record<string, unknown> {
|
|
4586
|
+
if (candidateView === undefined) return { operation, result: mapNativeStartResult(result), workspace_root: workspaceRoot };
|
|
4587
|
+
if (candidateViews && result.lensesRequired) {
|
|
4588
|
+
const binding = { token: candidateView.token, lineageId: result.lineageId, selectedLenses: result.selectedLenses };
|
|
4589
|
+
if (result.action === "resumed" && !candidateViews.hasCurrentBinding()) candidateViews.restoreCurrentFromNativeStart(binding);
|
|
4590
|
+
else candidateViews.bindCurrent(binding);
|
|
4591
|
+
} else if (candidateViews && ((result.action === "created" && result.state === "reviewing") || result.action === "resumed" || result.action === "reuse-receipt")) candidateViews.retain(candidateView.token, result.lineageId);
|
|
4592
|
+
else candidateViews?.cleanup(candidateView.token);
|
|
4593
|
+
const actorBinding = result.lensesRequired
|
|
4594
|
+
? {
|
|
4595
|
+
workspace_root: workspaceRoot,
|
|
4596
|
+
candidate_root: candidateView.root,
|
|
4597
|
+
candidate_tree: candidateView.candidateTree,
|
|
4598
|
+
candidate_paths: candidateView.paths,
|
|
4599
|
+
}
|
|
4600
|
+
: undefined;
|
|
4601
|
+
return {
|
|
4602
|
+
operation,
|
|
4603
|
+
result: mapNativeStartResult(result),
|
|
4604
|
+
workspace_root: workspaceRoot,
|
|
4605
|
+
...(actorBinding === undefined ? {} : { actor_binding: actorBinding }),
|
|
4606
|
+
};
|
|
4532
4607
|
}
|
|
4533
4608
|
|
|
4534
4609
|
function nativeOperationFailure(operation: ReviewControllerOperation, error: unknown): Record<string, unknown> {
|
|
@@ -4826,6 +4901,7 @@ async function executeReviewControllerOperation(
|
|
|
4826
4901
|
candidateViews: CandidateViewRegistry | null = new CandidateViewRegistry(),
|
|
4827
4902
|
context?: ExtensionContext,
|
|
4828
4903
|
correctionEvidenceByLineage: Map<string, CorrectionEvidence> = new Map(),
|
|
4904
|
+
pendingReviewConsents: Map<string, PendingReviewConsent> = new Map(),
|
|
4829
4905
|
): Promise<Record<string, unknown>> {
|
|
4830
4906
|
const parameters = parseReviewControllerParameters(parametersValue);
|
|
4831
4907
|
const defaultCwd = resolveReviewControllerWorkspaceRoot(parameters.workspaceRoot, sessionCwd);
|
|
@@ -4974,6 +5050,60 @@ async function executeReviewControllerOperation(
|
|
|
4974
5050
|
});
|
|
4975
5051
|
}
|
|
4976
5052
|
}
|
|
5053
|
+
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.ANSWER_CONSENT) {
|
|
5054
|
+
const input = parseControllerJson(requiredControllerString(parameters, "input"), parameters.operation);
|
|
5055
|
+
if (Object.keys(input).some((key) => key !== "consentBinding" && key !== "answer") || Object.keys(input).length !== 2) throw new Error("Review controller answer-consent input must contain exactly consentBinding and answer");
|
|
5056
|
+
if (typeof input.consentBinding !== "string" || input.consentBinding.length === 0) throw new Error("Review controller answer-consent requires an opaque consentBinding");
|
|
5057
|
+
if (input.answer !== "granted" && input.answer !== "declined") throw new Error("Review controller answer-consent answer must be granted or declined");
|
|
5058
|
+
const pending = pendingReviewConsents.get(input.consentBinding);
|
|
5059
|
+
if (pending === undefined || pending.expiresAt <= Date.now()) {
|
|
5060
|
+
if (pending !== undefined) cleanupPendingReviewConsent(pending, pendingReviewConsents, candidateViews);
|
|
5061
|
+
throw new Error("Review controller consent binding is unknown, expired, or already consumed");
|
|
5062
|
+
}
|
|
5063
|
+
if (realpathSync(defaultCwd) !== pending.repositoryCwd) throw new Error("Review controller consent repository binding changed");
|
|
5064
|
+
if (reviewConsentDigest(pending.consent) !== pending.consentDigest) throw new Error("Review controller consent envelope binding changed");
|
|
5065
|
+
pending.candidateView.verify();
|
|
5066
|
+
if (nativeReviewCli?.answerConsent === undefined) throw new Error("Native review consent follow-up is unavailable");
|
|
5067
|
+
try {
|
|
5068
|
+
const gated = await resolveReviewModeGate(nativeReviewCli, parameters.operation, defaultCwd, signal);
|
|
5069
|
+
if (gated !== undefined) {
|
|
5070
|
+
cleanupPendingReviewConsent(pending, pendingReviewConsents, candidateViews);
|
|
5071
|
+
return gated;
|
|
5072
|
+
}
|
|
5073
|
+
} catch (error) {
|
|
5074
|
+
return nativeOperationFailure(parameters.operation, error);
|
|
5075
|
+
}
|
|
5076
|
+
// The one-shot binding is consumed before the provider mutation. Any
|
|
5077
|
+
// ambiguous result reconciles through STATUS and can never be replayed.
|
|
5078
|
+
consumePendingReviewConsent(pending, pendingReviewConsents);
|
|
5079
|
+
try {
|
|
5080
|
+
const answered = await nativeReviewCli.answerConsent({
|
|
5081
|
+
cwd: pending.candidateView.root,
|
|
5082
|
+
consent: pending.consent,
|
|
5083
|
+
answer: input.answer,
|
|
5084
|
+
...(signal === undefined ? {} : { signal }),
|
|
5085
|
+
});
|
|
5086
|
+
if (answered.kind === "declined") {
|
|
5087
|
+
candidateViews?.cleanup(pending.candidateView.token);
|
|
5088
|
+
return {
|
|
5089
|
+
operation: parameters.operation,
|
|
5090
|
+
status: "skipped",
|
|
5091
|
+
outcome: "consent-declined-this-candidate",
|
|
5092
|
+
consent: answered.raw,
|
|
5093
|
+
...nativeStartPreAuthorityRejection(),
|
|
5094
|
+
};
|
|
5095
|
+
}
|
|
5096
|
+
return completeNativeStart(parameters.operation, answered.start, pending.repositoryCwd, pending.candidateView, candidateViews);
|
|
5097
|
+
} catch (error) {
|
|
5098
|
+
const value = error as { mutationOutcome?: unknown };
|
|
5099
|
+
if (value.mutationOutcome === "none") candidateViews?.cleanup(pending.candidateView.token);
|
|
5100
|
+
return await reconcileNativeMutationFailure(parameters.operation, error, nativeReviewCli, {
|
|
5101
|
+
cwd: pending.candidateView.root,
|
|
5102
|
+
...(pending.candidateView.committedOnly ? { baseRef: pending.candidateView.baseCommit } : {}),
|
|
5103
|
+
projection: "workspace",
|
|
5104
|
+
});
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
4977
5107
|
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.START) {
|
|
4978
5108
|
const rawStart = parseControllerJson(
|
|
4979
5109
|
requiredControllerString(parameters, "input"),
|
|
@@ -5024,50 +5154,42 @@ async function executeReviewControllerOperation(
|
|
|
5024
5154
|
try {
|
|
5025
5155
|
candidateView = candidateViews?.createOrReuse({ contributorRoot: defaultCwd, replayKey, ...(canonicalBaseRef === undefined ? {} : { baseRef: canonicalBaseRef, committedOnly: true }) });
|
|
5026
5156
|
nativeStartAttempted = true;
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
const
|
|
5045
|
-
if (
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
// actors against a stale previously-active worktree (#166, #169).
|
|
5053
|
-
// A declined organic-parity consent withholds actor_binding for this
|
|
5054
|
-
// work unit only (Design Data Flow, organic-rdd-parity): the native
|
|
5055
|
-
// START already ran and its result is still reported.
|
|
5056
|
-
const actorBinding = candidateView !== undefined && result.lensesRequired && !consentDeclined
|
|
5057
|
-
? {
|
|
5058
|
-
workspace_root: defaultCwd,
|
|
5059
|
-
candidate_root: candidateView.root,
|
|
5060
|
-
candidate_tree: candidateView.candidateTree,
|
|
5061
|
-
candidate_paths: candidateView.paths,
|
|
5157
|
+
let result: NativeStartResult;
|
|
5158
|
+
try {
|
|
5159
|
+
result = await nativeReviewCli.start({
|
|
5160
|
+
cwd: candidateView?.root ?? defaultCwd,
|
|
5161
|
+
...(canonicalBaseRef === undefined
|
|
5162
|
+
? {}
|
|
5163
|
+
: { baseRef: candidateView?.baseCommit ?? canonicalBaseRef, committedOnly: true }),
|
|
5164
|
+
...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
|
|
5165
|
+
...(policy.policyPath === undefined ? {} : { policyPath: policy.policyPath }),
|
|
5166
|
+
...(signal === undefined ? {} : { signal }),
|
|
5167
|
+
});
|
|
5168
|
+
} catch (error) {
|
|
5169
|
+
if (!(error instanceof NativeReviewConsentRequiredError)) throw error;
|
|
5170
|
+
if (candidateView === undefined) throw new CandidateViewError("native consent requires a frozen candidate view");
|
|
5171
|
+
const consentCandidateView = candidateView;
|
|
5172
|
+
const repositoryCwd = realpathSync(defaultCwd);
|
|
5173
|
+
const consentDigest = reviewConsentDigest(error.consent);
|
|
5174
|
+
const existing = [...pendingReviewConsents.values()].find((pending) => pending.repositoryCwd === repositoryCwd && pending.candidateView.token === consentCandidateView.token && pending.consentDigest === consentDigest && pending.expiresAt > Date.now());
|
|
5175
|
+
if (existing === undefined) for (const pending of [...pendingReviewConsents.values()]) if (pending.candidateView.token === consentCandidateView.token) consumePendingReviewConsent(pending, pendingReviewConsents);
|
|
5176
|
+
const id = existing?.id ?? randomUUID();
|
|
5177
|
+
if (existing === undefined) {
|
|
5178
|
+
const pending: PendingReviewConsent = { id, repositoryCwd, candidateView: consentCandidateView, consent: error.consent, consentDigest, expiresAt: Date.now() + PENDING_REVIEW_CONSENT_TTL_MS };
|
|
5179
|
+
pendingReviewConsents.set(id, pending);
|
|
5180
|
+
pending.expiry = setTimeout(() => cleanupPendingReviewConsent(pending, pendingReviewConsents, candidateViews), PENDING_REVIEW_CONSENT_TTL_MS);
|
|
5181
|
+
pending.expiry.unref();
|
|
5062
5182
|
}
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5183
|
+
return {
|
|
5184
|
+
operation: parameters.operation,
|
|
5185
|
+
status: "blocked",
|
|
5186
|
+
outcome: "native-review-consent-required",
|
|
5187
|
+
consent: error.consent.raw,
|
|
5188
|
+
consent_binding: id,
|
|
5189
|
+
...nativeStartPreAuthorityRejection(),
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
return completeNativeStart(parameters.operation, result, defaultCwd, candidateView, candidateViews);
|
|
5071
5193
|
} catch (error) {
|
|
5072
5194
|
if (error instanceof CandidateViewError && (error.reason === "base-ref-ambiguous" || error.reason === "base-ref-unresolvable" || error.reason === "base-ref-moved")) return nativeStartRejection(error.reason);
|
|
5073
5195
|
const value = error as { mutationOutcome?: unknown; nextAction?: unknown };
|
|
@@ -5577,6 +5699,19 @@ async function gateLifecycleCommand(
|
|
|
5577
5699
|
if (!inspection.command) {
|
|
5578
5700
|
return { block: true, reason: inspection.failClosedReason ?? "Lifecycle command failed closed." };
|
|
5579
5701
|
}
|
|
5702
|
+
if (nativeReviewCli?.reviewMode !== undefined) {
|
|
5703
|
+
try {
|
|
5704
|
+
const mode = await nativeReviewCli.reviewMode({ cwd: inspection.command.cwd, operation: NATIVE_REVIEW_MODE_OPERATION.STATUS, ...(signal === undefined ? {} : { signal }) });
|
|
5705
|
+
if (mode.status.effective === "off") {
|
|
5706
|
+
pendingAuthorizations.clear();
|
|
5707
|
+
return undefined;
|
|
5708
|
+
}
|
|
5709
|
+
} catch (error) {
|
|
5710
|
+
if (asNativeReviewCliError(error)?.code !== NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE) {
|
|
5711
|
+
return { block: true, reason: `Gentle AI ${inspection.event} gate could not reconsult review mode and failed closed.` };
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5580
5715
|
let derived: DerivedReviewGateTarget;
|
|
5581
5716
|
try {
|
|
5582
5717
|
derived = deriveReviewGateTarget(command, defaultCwd);
|
|
@@ -5616,7 +5751,7 @@ async function gateLifecycleCommand(
|
|
|
5616
5751
|
try {
|
|
5617
5752
|
const intendedTree = authorization.native_gate === undefined
|
|
5618
5753
|
? undefined
|
|
5619
|
-
:
|
|
5754
|
+
: reproveNativePreCommitTree(derived, authorization.native_gate.lineage_id, candidateViews);
|
|
5620
5755
|
if (authorization.native_gate?.intended_tree !== undefined && intendedTree !== authorization.native_gate.intended_tree) {
|
|
5621
5756
|
return {
|
|
5622
5757
|
block: true,
|
|
@@ -5672,7 +5807,7 @@ async function gateLifecycleCommand(
|
|
|
5672
5807
|
publicationProbeTimeoutMs,
|
|
5673
5808
|
signal,
|
|
5674
5809
|
);
|
|
5675
|
-
const postNativeIntendedTree =
|
|
5810
|
+
const postNativeIntendedTree = reproveNativePreCommitTree(postNativeDerived, authorization.native_gate.lineage_id, candidateViews);
|
|
5676
5811
|
if (authorization.native_gate.intended_tree !== undefined && postNativeIntendedTree !== authorization.native_gate.intended_tree) throw new CandidateViewError("staged projection changed during native validation");
|
|
5677
5812
|
assertNativePublicationBinding(fresh, postNativeDerived);
|
|
5678
5813
|
if (nativeGateFingerprint(fresh, postNativeDerived) !== authorization.native_gate.fingerprint) {
|
|
@@ -5746,7 +5881,6 @@ export async function enforceReviewGateAndCommandSafety(
|
|
|
5746
5881
|
/** @internal */
|
|
5747
5882
|
export const __testing = {
|
|
5748
5883
|
resolveReviewModeGate,
|
|
5749
|
-
requestReviewConsent,
|
|
5750
5884
|
listAgentsFromDir,
|
|
5751
5885
|
listAgentsFromDirAsync,
|
|
5752
5886
|
listDiscoverableAgents,
|
|
@@ -5833,10 +5967,16 @@ export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencie
|
|
|
5833
5967
|
if (!Number.isSafeInteger(bashTimeRevalidationTimeoutMs) || bashTimeRevalidationTimeoutMs <= 0) throw new TypeError("Bash-time revalidation timeout must be a positive safe integer");
|
|
5834
5968
|
return function gentleAi(pi: ExtensionAPI): void {
|
|
5835
5969
|
const pendingReviewAuthorizations = new Map<string, PendingReviewAuthorization>();
|
|
5970
|
+
const pendingReviewConsents = new Map<string, PendingReviewConsent>();
|
|
5971
|
+
const consumedNativeAuthorizations = new Set<string>();
|
|
5836
5972
|
const pendingCommitTransactions = new Map<string, { cwd: string; transactionId: string }>();
|
|
5837
5973
|
const correctionEvidenceByLineage = new Map<string, CorrectionEvidence>();
|
|
5838
5974
|
const candidateViews = dependencies.candidateViews === undefined ? new CandidateViewRegistry() : dependencies.candidateViews;
|
|
5839
5975
|
|
|
5976
|
+
pi.on("session_shutdown", () => {
|
|
5977
|
+
cleanupAllPendingReviewConsents(pendingReviewConsents, candidateViews);
|
|
5978
|
+
});
|
|
5979
|
+
|
|
5840
5980
|
pi.registerTool({
|
|
5841
5981
|
name: "gentle_review",
|
|
5842
5982
|
label: "Gentle Review Controller",
|
|
@@ -5849,7 +5989,7 @@ export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencie
|
|
|
5849
5989
|
"Use ABANDON or QUARANTINE_LEGACY only after an explicit user decision and with exact native inputs. ABANDON needs lineage, expectedRevision, snapshotIdentity, actor, and reason; QUARANTINE_LEGACY accepts only the published malformed freeze-findings diagnostic/disposition. A dual reconciliation may supply only anomalies `unchanged_target,malformed_recovery_authorization` in that exact order. Use REPAIR_LEGACY_ALIAS only with lineage, actor, and reason: Pi freshly reads native inventory and derives repository, revision, diagnostic, disposition, and the exact eight-line binding before interactive approval. `review dispose-result` is unsupported pending design.",
|
|
5850
5990
|
"Run selected lenses once, then call FINALIZE with a JSON string containing review_result.lens_results entries for every START-selected lens. Each entry has lens, findings, and non-empty evidence; clean lenses use findings: []. Pair final_evidence with exactly one of final_verification_passed or final_verification_outcome (passed, verification_failed, procedural_tooling_failed). Correction evidence is captured natively before STATUS can expose targeted validation. This Pi wrapper shape differs from native CLI --result, --refuter, --validation, and --evidence files. Use ADVANCE only for explicit graph-v1 Judgment Day.",
|
|
5851
5991
|
"For blocked-legacy or blocked-mixed, do not call START repeatedly. Explain invalidation, request explicit user authorization for the exact reset_request challenge, then call RESET or RECOVER only after authorization. RESET and RECOVER_LOCK route to audited native `gentle-ai review reclaim` and RECOVER routes to native `gentle-ai review recover`; 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.",
|
|
5852
|
-
"A reported lineage_created false or pre-authority validation error proves no lineage was created. After ambiguous START or FINALIZE output, the controller calls target-scoped native status first and returns only its declared action. Never infer or prescribe replay unless native explicitly reports exact_replay_safe for the same canonical request and required lineage.",
|
|
5992
|
+
"A consent-required START returns the complete provider envelope and an opaque consent_binding, then stops. The parent presents and localizes that envelope without changing machine tokens, commands, target IDs, or invocations. After one explicit human answer, call answer-consent exactly once with a JSON string containing only consentBinding and answer (`granted` or `declined`). A reported lineage_created false or pre-authority validation error proves no lineage was created. After ambiguous START, answer-consent, or FINALIZE output, the controller calls target-scoped native status first and returns only its declared action. Never infer or prescribe replay unless native explicitly reports exact_replay_safe for the same canonical request and required lineage.",
|
|
5853
5993
|
"Use gentle_review for bounded review transaction operations and exact lifecycle validation; never fabricate bash tool metadata or a separate gate target.",
|
|
5854
5994
|
],
|
|
5855
5995
|
parameters: REVIEW_CONTROLLER_PARAMETERS,
|
|
@@ -5868,6 +6008,7 @@ export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencie
|
|
|
5868
6008
|
candidateViews,
|
|
5869
6009
|
ctx,
|
|
5870
6010
|
correctionEvidenceByLineage,
|
|
6011
|
+
pendingReviewConsents,
|
|
5871
6012
|
);
|
|
5872
6013
|
return {
|
|
5873
6014
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
@@ -5991,12 +6132,60 @@ export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencie
|
|
|
5991
6132
|
return undefined;
|
|
5992
6133
|
const originalCommand = event.input.command;
|
|
5993
6134
|
const inspection = inspectReviewLifecycleCommand(originalCommand, ctx.cwd);
|
|
5994
|
-
|
|
5995
|
-
|
|
6135
|
+
let reviewModeDisabled = false;
|
|
6136
|
+
if (inspection.command?.event === "pre-commit" && nativeReviewCli?.reviewMode !== undefined) {
|
|
6137
|
+
try {
|
|
6138
|
+
const mode = await nativeReviewCli.reviewMode({ cwd: inspection.command.cwd, operation: NATIVE_REVIEW_MODE_OPERATION.STATUS, ...(ctx.signal === undefined ? {} : { signal: ctx.signal }) });
|
|
6139
|
+
reviewModeDisabled = mode.status.effective === "off";
|
|
6140
|
+
if (reviewModeDisabled) pendingReviewAuthorizations.clear();
|
|
6141
|
+
} catch (error) {
|
|
6142
|
+
if (asNativeReviewCliError(error)?.code !== NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE) return { block: true, reason: "Gentle AI lifecycle gate could not reconsult review mode and failed closed." };
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
const commandAuthorizationKey = inspection.command?.event === "pre-commit"
|
|
6146
|
+
? reviewAuthorizationKey(originalCommand, inspection.command.cwd)
|
|
5996
6147
|
: undefined;
|
|
6148
|
+
let nativeCommitAuthorization = commandAuthorizationKey === undefined
|
|
6149
|
+
? undefined
|
|
6150
|
+
: pendingReviewAuthorizations.get(commandAuthorizationKey);
|
|
6151
|
+
let authorizationConsumptionIdentity = nativeAuthorizationConsumptionIdentity(nativeCommitAuthorization);
|
|
6152
|
+
if (authorizationConsumptionIdentity !== undefined && consumedNativeAuthorizations.has(authorizationConsumptionIdentity)) {
|
|
6153
|
+
if (commandAuthorizationKey !== undefined) pendingReviewAuthorizations.delete(commandAuthorizationKey);
|
|
6154
|
+
nativeCommitAuthorization = undefined;
|
|
6155
|
+
authorizationConsumptionIdentity = undefined;
|
|
6156
|
+
}
|
|
6157
|
+
let unmanagedPreCommit = reviewModeDisabled && inspection.command?.event === "pre-commit";
|
|
6158
|
+
if (!unmanagedPreCommit && inspection.command?.event === "pre-commit" && commandAuthorizationKey !== undefined && nativeCommitAuthorization === undefined && nativeReviewCli !== null) {
|
|
6159
|
+
let derived: DerivedReviewGateTarget;
|
|
6160
|
+
try {
|
|
6161
|
+
derived = deriveReviewGateTarget(originalCommand, ctx.cwd);
|
|
6162
|
+
} catch (error) {
|
|
6163
|
+
return {
|
|
6164
|
+
block: true,
|
|
6165
|
+
reason: `Gentle AI pre-commit gate could not exactly derive the command target and failed closed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6166
|
+
};
|
|
6167
|
+
}
|
|
6168
|
+
const deadline = AbortSignal.timeout(bashTimeRevalidationTimeoutMs);
|
|
6169
|
+
const signal = ctx.signal === undefined ? deadline : AbortSignal.any([ctx.signal, deadline]);
|
|
6170
|
+
try {
|
|
6171
|
+
const consumed = await consumeNativePreCommitReceipt(originalCommand, ctx.cwd, derived, nativeReviewCli, publicationProbe, publicationProbeTimeoutMs, signal);
|
|
6172
|
+
nativeCommitAuthorization = consumed.authorization;
|
|
6173
|
+
unmanagedPreCommit = consumed.delivery === "disabled/unmanaged";
|
|
6174
|
+
authorizationConsumptionIdentity = nativeAuthorizationConsumptionIdentity(nativeCommitAuthorization);
|
|
6175
|
+
if (authorizationConsumptionIdentity !== undefined && consumedNativeAuthorizations.has(authorizationConsumptionIdentity)) {
|
|
6176
|
+
nativeCommitAuthorization = undefined;
|
|
6177
|
+
authorizationConsumptionIdentity = undefined;
|
|
6178
|
+
} else if (nativeCommitAuthorization !== undefined) {
|
|
6179
|
+
pendingReviewAuthorizations.set(nativeCommitAuthorization.command_hash, nativeCommitAuthorization);
|
|
6180
|
+
}
|
|
6181
|
+
} catch (error) {
|
|
6182
|
+
return { block: true, reason: `Gentle AI pre-commit receipt consumption failed closed: ${error instanceof Error ? error.message : String(error)}` };
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
5997
6185
|
const gateResult = await enforceReviewGateAndCommandSafety(
|
|
5998
6186
|
originalCommand,
|
|
5999
6187
|
(command) => {
|
|
6188
|
+
if (unmanagedPreCommit) return Promise.resolve(undefined);
|
|
6000
6189
|
const deadline = AbortSignal.timeout(bashTimeRevalidationTimeoutMs);
|
|
6001
6190
|
const signal = ctx.signal === undefined ? deadline : AbortSignal.any([ctx.signal, deadline]);
|
|
6002
6191
|
return gateLifecycleCommand(command, ctx.cwd, pendingReviewAuthorizations, nativeReviewCli, publicationProbe, publicationProbeTimeoutMs, signal, candidateViews);
|
|
@@ -6004,6 +6193,7 @@ export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencie
|
|
|
6004
6193
|
(command) => confirmCommand(command, ctx),
|
|
6005
6194
|
);
|
|
6006
6195
|
if (gateResult) return gateResult;
|
|
6196
|
+
if (authorizationConsumptionIdentity !== undefined) consumedNativeAuthorizations.add(authorizationConsumptionIdentity);
|
|
6007
6197
|
if (inspection.command?.event !== "pre-commit" || nativeCommitAuthorization?.native_gate === undefined) return undefined;
|
|
6008
6198
|
if (nativeReviewCli?.targetStatus === undefined) return undefined;
|
|
6009
6199
|
if (nativeCommitAuthorization.native_gate.intended_tree === undefined) {
|
|
@@ -6209,6 +6399,10 @@ export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencie
|
|
|
6209
6399
|
}
|
|
6210
6400
|
try {
|
|
6211
6401
|
const result = await nativeReviewCli.reviewMode({ cwd: ctx.cwd, operation: subAction as NativeReviewModeOperation });
|
|
6402
|
+
if (subAction === NATIVE_REVIEW_MODE_OPERATION.DISABLE && result.status.effective === "off") {
|
|
6403
|
+
pendingReviewAuthorizations.clear();
|
|
6404
|
+
cleanupAllPendingReviewConsents(pendingReviewConsents, candidateViews);
|
|
6405
|
+
}
|
|
6212
6406
|
const report = `receipt-driven development: ${result.status.effective} (decided by ${result.status.source})`;
|
|
6213
6407
|
// A mutating sub-action that left the effective mode unchanged did
|
|
6214
6408
|
// not do what the user asked, and reporting only the resulting
|