gentle-pi 2.1.0 → 2.1.2
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/extensions/gentle-ai.ts +54 -9
- package/lib/native-review-cli.ts +62 -25
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +61 -24
- package/tests/native-review-cli.test.ts +27 -0
- package/tests/native-review-consent.test.ts +77 -0
- package/tests/native-review-parity.test.ts +80 -14
- package/tests/package-manifest.test.ts +2 -2
- package/tests/review-controller-lock-status.test.ts +25 -7
- package/tests/review-controller-native-routing.test.ts +172 -14
- package/tests/review-controller-workspace-root.test.ts +40 -10
package/extensions/gentle-ai.ts
CHANGED
|
@@ -104,7 +104,9 @@ import {
|
|
|
104
104
|
nativeReviewLegacyAliasRepairAuthorization,
|
|
105
105
|
nativeReviewLegacyQuarantineAuthorization,
|
|
106
106
|
nativeReviewReconcileAuthorization,
|
|
107
|
+
normalizeNativeReviewCwd,
|
|
107
108
|
NativeReviewCliError,
|
|
109
|
+
NativeReviewConsentBindingError,
|
|
108
110
|
NativeReviewConsentRequiredError,
|
|
109
111
|
NATIVE_REVIEW_ERROR_CODE,
|
|
110
112
|
NATIVE_REVIEW_LEGACY_QUARANTINE,
|
|
@@ -2872,7 +2874,7 @@ export function resolveReviewLifecycleCommand(
|
|
|
2872
2874
|
}
|
|
2873
2875
|
if (words[0] !== "git") return null;
|
|
2874
2876
|
const gitGlobalArgs: string[] = [];
|
|
2875
|
-
let resolvedCwd = resolve(defaultCwd);
|
|
2877
|
+
let resolvedCwd = resolve(normalizeNativeReviewCwd(defaultCwd));
|
|
2876
2878
|
let index = 1;
|
|
2877
2879
|
while (index < words.length) {
|
|
2878
2880
|
const option = words[index];
|
|
@@ -2880,7 +2882,7 @@ export function resolveReviewLifecycleCommand(
|
|
|
2880
2882
|
const value = words[index + 1];
|
|
2881
2883
|
if (value === undefined) return null;
|
|
2882
2884
|
gitGlobalArgs.push(option, value);
|
|
2883
|
-
if (option === "-C") resolvedCwd = resolve(resolvedCwd, value);
|
|
2885
|
+
if (option === "-C") resolvedCwd = resolve(resolvedCwd, normalizeNativeReviewCwd(value));
|
|
2884
2886
|
else return null;
|
|
2885
2887
|
index += 2;
|
|
2886
2888
|
continue;
|
|
@@ -3917,6 +3919,14 @@ function asNativeReviewCliError(error: unknown): { code: string; diagnostics: Na
|
|
|
3917
3919
|
return diagnostics === undefined || value.code !== diagnostics.error_code ? undefined : { code: value.code, diagnostics };
|
|
3918
3920
|
}
|
|
3919
3921
|
|
|
3922
|
+
// Same coexisting-module-instance caveat as asNativeReviewCliError above.
|
|
3923
|
+
function asNativeReviewConsentBindingError(error: unknown): { reason: string; message: string } | undefined {
|
|
3924
|
+
if (error instanceof NativeReviewConsentBindingError) return { reason: error.reason, message: error.message };
|
|
3925
|
+
if (!(error instanceof Error) || error.name !== "NativeReviewConsentBindingError") return undefined;
|
|
3926
|
+
const reason = (error as unknown as { reason?: unknown }).reason;
|
|
3927
|
+
return typeof reason !== "string" || reason.length === 0 ? undefined : { reason, message: error.message };
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3920
3930
|
function nativeStatusFailed(operation: ReviewControllerOperation, error: unknown): Record<string, unknown> {
|
|
3921
3931
|
const cliError = asNativeReviewCliError(error);
|
|
3922
3932
|
if (cliError?.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE) return nativeStatusUnsupported(operation);
|
|
@@ -4550,6 +4560,7 @@ const PENDING_REVIEW_CONSENT_TTL_MS = 10 * 60 * 1000;
|
|
|
4550
4560
|
interface PendingReviewConsent {
|
|
4551
4561
|
id: string;
|
|
4552
4562
|
repositoryCwd: string;
|
|
4563
|
+
authorityCwd: string;
|
|
4553
4564
|
candidateView: CandidateView;
|
|
4554
4565
|
consent: ReviewConsentV2;
|
|
4555
4566
|
consentDigest: string;
|
|
@@ -4576,6 +4587,19 @@ function reviewConsentDigest(consent: ReviewConsentV2): string {
|
|
|
4576
4587
|
return createHash("sha256").update(JSON.stringify(consent)).digest("hex");
|
|
4577
4588
|
}
|
|
4578
4589
|
|
|
4590
|
+
function assertNativeStartCandidateBinding(candidateView: CandidateView, target: ReviewStatusV3): void {
|
|
4591
|
+
candidateView.verify();
|
|
4592
|
+
if (
|
|
4593
|
+
target.projection.projection !== "workspace" ||
|
|
4594
|
+
target.projection.baseTree !== candidateView.baseTree ||
|
|
4595
|
+
target.projection.initialReviewTree !== candidateView.candidateTree ||
|
|
4596
|
+
target.projection.currentCandidateTree !== candidateView.candidateTree ||
|
|
4597
|
+
JSON.stringify([...target.projection.paths].sort()) !== JSON.stringify([...candidateView.paths].sort())
|
|
4598
|
+
) {
|
|
4599
|
+
throw new CandidateViewError("native START workspace target does not match the immutable reviewer candidate view", "candidate-target-projection-drift");
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4579
4603
|
function completeNativeStart(
|
|
4580
4604
|
operation: ReviewControllerOperation,
|
|
4581
4605
|
result: NativeStartResult,
|
|
@@ -4623,6 +4647,23 @@ function nativeOperationFailure(operation: ReviewControllerOperation, error: unk
|
|
|
4623
4647
|
...(typeof value.failureEnvelope.nextAction === "string" ? { next_action: value.failureEnvelope.nextAction } : {}),
|
|
4624
4648
|
};
|
|
4625
4649
|
}
|
|
4650
|
+
// Every consent binding guard runs before the provider is launched, so this
|
|
4651
|
+
// is a local mismatch with nothing to reconcile. Reporting it as a native
|
|
4652
|
+
// operation failure hides the one fact that makes it fixable.
|
|
4653
|
+
const consentBinding = asNativeReviewConsentBindingError(error);
|
|
4654
|
+
if (consentBinding !== undefined) {
|
|
4655
|
+
return {
|
|
4656
|
+
operation,
|
|
4657
|
+
status: "blocked",
|
|
4658
|
+
outcome: "consent-binding-invalid",
|
|
4659
|
+
native_invocation_attempted: false,
|
|
4660
|
+
lineage_created: false,
|
|
4661
|
+
mutation_performed: false,
|
|
4662
|
+
mutation_outcome: "none" as const,
|
|
4663
|
+
diagnostics: { code: consentBinding.reason, message: consentBinding.message },
|
|
4664
|
+
next_action: "resolve-consent-binding",
|
|
4665
|
+
};
|
|
4666
|
+
}
|
|
4626
4667
|
const mutationOutcome = value.mutationOutcome === "unknown" ? "unknown" : "none";
|
|
4627
4668
|
const nativeDiagnostics = asNativeReviewCliError(error)?.diagnostics;
|
|
4628
4669
|
const diagnostics = operation === REVIEW_CONTROLLER_OPERATION.START && error instanceof CandidateViewError && value.candidateViewPreNative === true
|
|
@@ -5078,7 +5119,7 @@ async function executeReviewControllerOperation(
|
|
|
5078
5119
|
consumePendingReviewConsent(pending, pendingReviewConsents);
|
|
5079
5120
|
try {
|
|
5080
5121
|
const answered = await nativeReviewCli.answerConsent({
|
|
5081
|
-
cwd: pending.
|
|
5122
|
+
cwd: pending.authorityCwd,
|
|
5082
5123
|
consent: pending.consent,
|
|
5083
5124
|
answer: input.answer,
|
|
5084
5125
|
...(signal === undefined ? {} : { signal }),
|
|
@@ -5098,7 +5139,7 @@ async function executeReviewControllerOperation(
|
|
|
5098
5139
|
const value = error as { mutationOutcome?: unknown };
|
|
5099
5140
|
if (value.mutationOutcome === "none") candidateViews?.cleanup(pending.candidateView.token);
|
|
5100
5141
|
return await reconcileNativeMutationFailure(parameters.operation, error, nativeReviewCli, {
|
|
5101
|
-
cwd: pending.
|
|
5142
|
+
cwd: pending.authorityCwd,
|
|
5102
5143
|
...(pending.candidateView.committedOnly ? { baseRef: pending.candidateView.baseCommit } : {}),
|
|
5103
5144
|
projection: "workspace",
|
|
5104
5145
|
});
|
|
@@ -5137,8 +5178,9 @@ async function executeReviewControllerOperation(
|
|
|
5137
5178
|
return nativeStartRejection("base-ref-unresolvable");
|
|
5138
5179
|
}
|
|
5139
5180
|
}
|
|
5181
|
+
let target: ReviewStatusV3;
|
|
5140
5182
|
try {
|
|
5141
|
-
|
|
5183
|
+
target = await nativeReviewCli.targetStatus({
|
|
5142
5184
|
cwd: defaultCwd,
|
|
5143
5185
|
...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
|
|
5144
5186
|
...(canonicalBaseRef === undefined ? {} : { baseRef: canonicalBaseRef }),
|
|
@@ -5153,14 +5195,17 @@ async function executeReviewControllerOperation(
|
|
|
5153
5195
|
let nativeStartAttempted = false;
|
|
5154
5196
|
try {
|
|
5155
5197
|
candidateView = candidateViews?.createOrReuse({ contributorRoot: defaultCwd, replayKey, ...(canonicalBaseRef === undefined ? {} : { baseRef: canonicalBaseRef, committedOnly: true }) });
|
|
5156
|
-
|
|
5198
|
+
if (candidateView !== undefined) assertNativeStartCandidateBinding(candidateView, target);
|
|
5157
5199
|
let result: NativeStartResult;
|
|
5158
5200
|
try {
|
|
5201
|
+
nativeStartAttempted = true;
|
|
5159
5202
|
result = await nativeReviewCli.start({
|
|
5160
|
-
cwd:
|
|
5203
|
+
cwd: defaultCwd,
|
|
5161
5204
|
...(canonicalBaseRef === undefined
|
|
5162
5205
|
? {}
|
|
5163
5206
|
: { baseRef: candidateView?.baseCommit ?? canonicalBaseRef, committedOnly: true }),
|
|
5207
|
+
targetIdentity: target.targetIdentity,
|
|
5208
|
+
projection: target.projection.projection,
|
|
5164
5209
|
...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
|
|
5165
5210
|
...(policy.policyPath === undefined ? {} : { policyPath: policy.policyPath }),
|
|
5166
5211
|
...(signal === undefined ? {} : { signal }),
|
|
@@ -5175,7 +5220,7 @@ async function executeReviewControllerOperation(
|
|
|
5175
5220
|
if (existing === undefined) for (const pending of [...pendingReviewConsents.values()]) if (pending.candidateView.token === consentCandidateView.token) consumePendingReviewConsent(pending, pendingReviewConsents);
|
|
5176
5221
|
const id = existing?.id ?? randomUUID();
|
|
5177
5222
|
if (existing === undefined) {
|
|
5178
|
-
const pending: PendingReviewConsent = { id, repositoryCwd, candidateView: consentCandidateView, consent: error.consent, consentDigest, expiresAt: Date.now() + PENDING_REVIEW_CONSENT_TTL_MS };
|
|
5223
|
+
const pending: PendingReviewConsent = { id, repositoryCwd, authorityCwd: defaultCwd, candidateView: consentCandidateView, consent: error.consent, consentDigest, expiresAt: Date.now() + PENDING_REVIEW_CONSENT_TTL_MS };
|
|
5179
5224
|
pendingReviewConsents.set(id, pending);
|
|
5180
5225
|
pending.expiry = setTimeout(() => cleanupPendingReviewConsent(pending, pendingReviewConsents, candidateViews), PENDING_REVIEW_CONSENT_TTL_MS);
|
|
5181
5226
|
pending.expiry.unref();
|
|
@@ -5205,7 +5250,7 @@ async function executeReviewControllerOperation(
|
|
|
5205
5250
|
nextAction: "review.status",
|
|
5206
5251
|
});
|
|
5207
5252
|
return reconcileNativeMutationFailure(parameters.operation, failure, nativeReviewCli, {
|
|
5208
|
-
cwd:
|
|
5253
|
+
cwd: defaultCwd,
|
|
5209
5254
|
...(parameters.lineageId === undefined ? {} : { lineageId: parameters.lineageId }),
|
|
5210
5255
|
...(canonicalBaseRef === undefined ? {} : { baseRef: candidateView?.baseCommit ?? canonicalBaseRef }),
|
|
5211
5256
|
projection: "workspace",
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -371,7 +371,7 @@ export interface NativeReviewVerificationEvidenceV2 {
|
|
|
371
371
|
recordDigest: string;
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
export interface NativeStartRequest { cwd: string; baseRef?: string; committedOnly?: boolean; lineageId?: string; policyPath?: string; focus?: string; projection?: "workspace" | "staged"; signal?: AbortSignal; }
|
|
374
|
+
export interface NativeStartRequest { cwd: string; baseRef?: string; committedOnly?: boolean; lineageId?: string; policyPath?: string; focus?: string; targetIdentity?: string; projection?: "workspace" | "staged"; signal?: AbortSignal; }
|
|
375
375
|
export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } as const;
|
|
376
376
|
export type NativeReviewConsentAnswer = (typeof NATIVE_REVIEW_CONSENT_ANSWER)[keyof typeof NATIVE_REVIEW_CONSENT_ANSWER];
|
|
377
377
|
export interface NativeReviewConsentAnswerRequest { cwd: string; consent: ReviewConsentV2; answer: NativeReviewConsentAnswer; signal?: AbortSignal; }
|
|
@@ -480,6 +480,7 @@ export interface NativeReviewAuthorityEntry {
|
|
|
480
480
|
status: NativeReviewAuthorityEntryStatus;
|
|
481
481
|
state?: string;
|
|
482
482
|
revision?: string;
|
|
483
|
+
snapshotIdentity?: string;
|
|
483
484
|
chainIdentity?: string;
|
|
484
485
|
recovery?: NativeReviewRecovery;
|
|
485
486
|
problems: readonly string[];
|
|
@@ -748,6 +749,7 @@ function exactObject(value: unknown, required: readonly string[], optional: read
|
|
|
748
749
|
}
|
|
749
750
|
function requiredString(value: unknown): string { if (typeof value !== "string" || value.length === 0) throw new Error("expected string"); return value; }
|
|
750
751
|
function stringValue(value: unknown): string { if (typeof value !== "string") throw new Error("expected string"); return value; }
|
|
752
|
+
function sha256Identity(value: unknown): string { const parsed = requiredString(value); if (!/^sha256:[0-9a-f]{64}$/.test(parsed)) throw new Error("expected canonical SHA-256 identity"); return parsed; }
|
|
751
753
|
function booleanValue(value: unknown): boolean { if (typeof value !== "boolean") throw new Error("expected boolean"); return value; }
|
|
752
754
|
function nonNegativeInteger(value: unknown): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("expected safe non-negative integer"); return value; }
|
|
753
755
|
function positiveInteger(value: unknown): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new Error("expected safe positive integer"); return value; }
|
|
@@ -957,7 +959,7 @@ function decodeNativeReviewRecovery(value: unknown): NativeReviewRecovery {
|
|
|
957
959
|
};
|
|
958
960
|
}
|
|
959
961
|
function decodeNativeReviewStatusEntry(value: unknown): NativeReviewAuthorityEntry {
|
|
960
|
-
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "chain_identity", "recovery"]);
|
|
962
|
+
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery"]);
|
|
961
963
|
return {
|
|
962
964
|
version: enumString(entry.version, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)) as NativeReviewAuthorityEntryVersion,
|
|
963
965
|
...(entry.lineage_id === undefined ? {} : { lineageId: requiredString(entry.lineage_id) }),
|
|
@@ -965,6 +967,7 @@ function decodeNativeReviewStatusEntry(value: unknown): NativeReviewAuthorityEnt
|
|
|
965
967
|
status: enumString(entry.status, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS)) as NativeReviewAuthorityEntryStatus,
|
|
966
968
|
...(entry.state === undefined ? {} : { state: requiredString(entry.state) }),
|
|
967
969
|
...(entry.revision === undefined ? {} : { revision: requiredString(entry.revision) }),
|
|
970
|
+
...(entry.snapshot_identity === undefined ? {} : { snapshotIdentity: sha256Identity(entry.snapshot_identity) }),
|
|
968
971
|
...(entry.chain_identity === undefined ? {} : { chainIdentity: requiredString(entry.chain_identity) }),
|
|
969
972
|
...(entry.recovery === undefined ? {} : { recovery: decodeNativeReviewRecovery(entry.recovery) }),
|
|
970
973
|
problems: stringArray(entry.problems),
|
|
@@ -1037,6 +1040,21 @@ function decodeNativeReviewStatus(value: unknown): NativeReviewStatusResult {
|
|
|
1037
1040
|
};
|
|
1038
1041
|
}
|
|
1039
1042
|
function isWindowsRepositoryPath(value: string): boolean { return /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value); }
|
|
1043
|
+
export function normalizeNativeReviewCwd(value: string, platform: NodeJS.Platform = process.platform): string {
|
|
1044
|
+
if (platform !== "win32") return value;
|
|
1045
|
+
const gitBashDrive = /^\/([A-Za-z])(?:\/(.*))?$/.exec(value);
|
|
1046
|
+
const windowsPath = gitBashDrive === null
|
|
1047
|
+
? value
|
|
1048
|
+
: `${gitBashDrive[1]!.toUpperCase()}:/${gitBashDrive[2] ?? ""}`;
|
|
1049
|
+
if (!isWindowsRepositoryPath(windowsPath)) return windowsPath;
|
|
1050
|
+
const normalized = win32.normalize(windowsPath);
|
|
1051
|
+
return normalized.replace(/^([a-z]):/, (_match, drive: string) => `${drive.toUpperCase()}:`);
|
|
1052
|
+
}
|
|
1053
|
+
async function canonicalNativeReviewCwd(value: string): Promise<string> {
|
|
1054
|
+
const normalized = normalizeNativeReviewCwd(value);
|
|
1055
|
+
try { return await realpath(normalized); }
|
|
1056
|
+
catch { return normalized; }
|
|
1057
|
+
}
|
|
1040
1058
|
async function repositoryPathIdentity(value: string): Promise<string> {
|
|
1041
1059
|
const windowsPath = isWindowsRepositoryPath(value);
|
|
1042
1060
|
try { return `filesystem:${windowsPath ? (await realpath(value)).toLowerCase() : await realpath(value)}`; }
|
|
@@ -1360,12 +1378,13 @@ export class NativeReviewCliV214 {
|
|
|
1360
1378
|
// and always pass `--scope clone` so Pi's own kill-switch command surface
|
|
1361
1379
|
// never mutates the operator's global gentle-ai state across other clones.
|
|
1362
1380
|
async reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
|
|
1363
|
-
await
|
|
1381
|
+
const cwd = await canonicalNativeReviewCwd(request.cwd);
|
|
1382
|
+
await this.verifyVersion(cwd, request.signal, ["mode"]);
|
|
1364
1383
|
const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
|
|
1365
1384
|
const { body } = await this.execute(
|
|
1366
1385
|
NATIVE_REVIEW_OPERATION.MODE,
|
|
1367
|
-
|
|
1368
|
-
["review", "mode", request.operation, "--cwd",
|
|
1386
|
+
cwd,
|
|
1387
|
+
["review", "mode", request.operation, "--cwd", cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
|
|
1369
1388
|
mutating,
|
|
1370
1389
|
request.signal,
|
|
1371
1390
|
);
|
|
@@ -1654,6 +1673,23 @@ export class NativeReviewConsentRequiredError extends Error {
|
|
|
1654
1673
|
}
|
|
1655
1674
|
}
|
|
1656
1675
|
|
|
1676
|
+
// Raised when the provider-issued consent invocation no longer matches the
|
|
1677
|
+
// binding Pi is answering for. Every one of these guards runs before the
|
|
1678
|
+
// provider is launched, so the failure is local and nothing was mutated. It
|
|
1679
|
+
// carries its own identity precisely so callers never report it as a provider
|
|
1680
|
+
// outage: an opaque `native-operation-failed` here sent issue #247 chasing a
|
|
1681
|
+
// missing `--cwd` that Pi does forward.
|
|
1682
|
+
export class NativeReviewConsentBindingError extends Error {
|
|
1683
|
+
readonly reason: string;
|
|
1684
|
+
readonly launchAttempted = false;
|
|
1685
|
+
readonly mutationOutcome = "none";
|
|
1686
|
+
constructor(reason: string, message: string) {
|
|
1687
|
+
super(message);
|
|
1688
|
+
this.name = "NativeReviewConsentBindingError";
|
|
1689
|
+
this.reason = reason;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1657
1693
|
function splitNativeConsentInvocation(invocation: string): readonly string[] {
|
|
1658
1694
|
const words: string[] = [];
|
|
1659
1695
|
let current = "";
|
|
@@ -1705,26 +1741,26 @@ function exactConsentOption(arguments_: readonly string[], name: string): string
|
|
|
1705
1741
|
const token = arguments_[index]!;
|
|
1706
1742
|
if (token === name) {
|
|
1707
1743
|
const value = arguments_[index + 1];
|
|
1708
|
-
if (value === undefined) throw new
|
|
1744
|
+
if (value === undefined) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", `Native consent invocation ${name} is missing its value`);
|
|
1709
1745
|
values.push(value);
|
|
1710
1746
|
index += 1;
|
|
1711
1747
|
} else if (token.startsWith(`${name}=`)) values.push(token.slice(name.length + 1));
|
|
1712
1748
|
}
|
|
1713
|
-
if (values.length !== 1) throw new
|
|
1749
|
+
if (values.length !== 1) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", `Native consent invocation requires exactly one ${name}`);
|
|
1714
1750
|
return values[0]!;
|
|
1715
1751
|
}
|
|
1716
1752
|
|
|
1717
1753
|
function consentInvocationArguments(request: NativeReviewConsentAnswerRequest): readonly string[] {
|
|
1718
1754
|
const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
|
|
1719
|
-
if (choice === undefined) throw new
|
|
1755
|
+
if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
|
|
1720
1756
|
const words = splitNativeConsentInvocation(choice.invocation);
|
|
1721
|
-
if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new
|
|
1757
|
+
if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new NativeReviewConsentBindingError("consent-invocation-not-start", "Native consent invocation is not a provider review START");
|
|
1722
1758
|
const arguments_ = words.slice(1);
|
|
1723
|
-
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new
|
|
1724
|
-
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new
|
|
1725
|
-
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new
|
|
1726
|
-
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new
|
|
1727
|
-
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new
|
|
1759
|
+
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new NativeReviewConsentBindingError("consent-invocation-contract-changed", "Native consent invocation contract changed");
|
|
1760
|
+
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
|
|
1761
|
+
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
|
|
1762
|
+
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
|
|
1763
|
+
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new NativeReviewConsentBindingError("consent-invocation-answer-changed", "Native consent invocation answer binding changed");
|
|
1728
1764
|
return arguments_;
|
|
1729
1765
|
}
|
|
1730
1766
|
|
|
@@ -1937,24 +1973,21 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1937
1973
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
|
|
1938
1974
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
1939
1975
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
1940
|
-
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
//
|
|
1944
|
-
// a target frozen from the wrong root would name a snapshot this START
|
|
1945
|
-
// never inspects. targetStatus is read-only, so this costs one extra
|
|
1946
|
-
// read and cannot create authority.
|
|
1976
|
+
if (request.targetIdentity !== undefined && !/^sha256:[0-9a-f]{64}$/.test(request.targetIdentity)) throw new TypeError("Native START targetIdentity must be a canonical sha256 identity");
|
|
1977
|
+
// The controller supplies the target it already projected from the
|
|
1978
|
+
// authority workspace after proving its immutable actor view is identical.
|
|
1979
|
+
// Direct adapter callers may omit it and retain the same-root projection.
|
|
1947
1980
|
const projection = request.projection ?? "workspace";
|
|
1948
|
-
const
|
|
1981
|
+
const targetIdentity = request.targetIdentity ?? (await this.targetStatus({
|
|
1949
1982
|
cwd: request.cwd,
|
|
1950
1983
|
projection,
|
|
1951
1984
|
...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
|
|
1952
1985
|
...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
|
|
1953
1986
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
1954
|
-
});
|
|
1987
|
+
})).targetIdentity;
|
|
1955
1988
|
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
|
|
1956
1989
|
"review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
1957
|
-
"--target",
|
|
1990
|
+
"--target", targetIdentity, "--projection", projection,
|
|
1958
1991
|
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
|
|
1959
1992
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1960
1993
|
...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
|
|
@@ -1966,10 +1999,14 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1966
1999
|
// explicit answer it cannot infer. Discriminate before decode and surface
|
|
1967
2000
|
// the complete envelope; only the caller can map a human answer.
|
|
1968
2001
|
if (execution.body.action === "consent_required") {
|
|
1969
|
-
|
|
2002
|
+
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body));
|
|
2003
|
+
if (consent.targetIdentity !== targetIdentity || consent.projection !== projection) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent target binding mismatch");
|
|
2004
|
+
throw new NativeReviewConsentRequiredError(consent);
|
|
1970
2005
|
}
|
|
1971
2006
|
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
1972
2007
|
if (request.lineageId !== undefined && result.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
|
|
2008
|
+
const resultTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2009
|
+
if (resultTarget !== undefined && resultTarget !== targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start target mismatch");
|
|
1973
2010
|
return {
|
|
1974
2011
|
lineageId: result.lineageId,
|
|
1975
2012
|
state: result.state as NativeStartResult["state"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -505,6 +505,7 @@ export const NATIVE_REVIEW_RECOVERY_DISPOSITION = {
|
|
|
505
505
|
|
|
506
506
|
|
|
507
507
|
|
|
508
|
+
|
|
508
509
|
|
|
509
510
|
|
|
510
511
|
export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REUSE_RECEIPT: "reuse-receipt", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } ;
|
|
@@ -749,6 +750,7 @@ function exactObject(value , required , optional
|
|
|
749
750
|
}
|
|
750
751
|
function requiredString(value ) { if (typeof value !== "string" || value.length === 0) throw new Error("expected string"); return value; }
|
|
751
752
|
function stringValue(value ) { if (typeof value !== "string") throw new Error("expected string"); return value; }
|
|
753
|
+
function sha256Identity(value ) { const parsed = requiredString(value); if (!/^sha256:[0-9a-f]{64}$/.test(parsed)) throw new Error("expected canonical SHA-256 identity"); return parsed; }
|
|
752
754
|
function booleanValue(value ) { if (typeof value !== "boolean") throw new Error("expected boolean"); return value; }
|
|
753
755
|
function nonNegativeInteger(value ) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("expected safe non-negative integer"); return value; }
|
|
754
756
|
function positiveInteger(value ) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new Error("expected safe positive integer"); return value; }
|
|
@@ -958,7 +960,7 @@ function decodeNativeReviewRecovery(value ) {
|
|
|
958
960
|
};
|
|
959
961
|
}
|
|
960
962
|
function decodeNativeReviewStatusEntry(value ) {
|
|
961
|
-
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "chain_identity", "recovery"]);
|
|
963
|
+
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery"]);
|
|
962
964
|
return {
|
|
963
965
|
version: enumString(entry.version, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)) ,
|
|
964
966
|
...(entry.lineage_id === undefined ? {} : { lineageId: requiredString(entry.lineage_id) }),
|
|
@@ -966,6 +968,7 @@ function decodeNativeReviewStatusEntry(value )
|
|
|
966
968
|
status: enumString(entry.status, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS)) ,
|
|
967
969
|
...(entry.state === undefined ? {} : { state: requiredString(entry.state) }),
|
|
968
970
|
...(entry.revision === undefined ? {} : { revision: requiredString(entry.revision) }),
|
|
971
|
+
...(entry.snapshot_identity === undefined ? {} : { snapshotIdentity: sha256Identity(entry.snapshot_identity) }),
|
|
969
972
|
...(entry.chain_identity === undefined ? {} : { chainIdentity: requiredString(entry.chain_identity) }),
|
|
970
973
|
...(entry.recovery === undefined ? {} : { recovery: decodeNativeReviewRecovery(entry.recovery) }),
|
|
971
974
|
problems: stringArray(entry.problems),
|
|
@@ -1038,6 +1041,21 @@ function decodeNativeReviewStatus(value ) {
|
|
|
1038
1041
|
};
|
|
1039
1042
|
}
|
|
1040
1043
|
function isWindowsRepositoryPath(value ) { return /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value); }
|
|
1044
|
+
export function normalizeNativeReviewCwd(value , platform = process.platform) {
|
|
1045
|
+
if (platform !== "win32") return value;
|
|
1046
|
+
const gitBashDrive = /^\/([A-Za-z])(?:\/(.*))?$/.exec(value);
|
|
1047
|
+
const windowsPath = gitBashDrive === null
|
|
1048
|
+
? value
|
|
1049
|
+
: `${gitBashDrive[1] .toUpperCase()}:/${gitBashDrive[2] ?? ""}`;
|
|
1050
|
+
if (!isWindowsRepositoryPath(windowsPath)) return windowsPath;
|
|
1051
|
+
const normalized = win32.normalize(windowsPath);
|
|
1052
|
+
return normalized.replace(/^([a-z]):/, (_match, drive ) => `${drive.toUpperCase()}:`);
|
|
1053
|
+
}
|
|
1054
|
+
async function canonicalNativeReviewCwd(value ) {
|
|
1055
|
+
const normalized = normalizeNativeReviewCwd(value);
|
|
1056
|
+
try { return await realpath(normalized); }
|
|
1057
|
+
catch { return normalized; }
|
|
1058
|
+
}
|
|
1041
1059
|
async function repositoryPathIdentity(value ) {
|
|
1042
1060
|
const windowsPath = isWindowsRepositoryPath(value);
|
|
1043
1061
|
try { return `filesystem:${windowsPath ? (await realpath(value)).toLowerCase() : await realpath(value)}`; }
|
|
@@ -1361,12 +1379,13 @@ export class NativeReviewCliV214 {
|
|
|
1361
1379
|
// and always pass `--scope clone` so Pi's own kill-switch command surface
|
|
1362
1380
|
// never mutates the operator's global gentle-ai state across other clones.
|
|
1363
1381
|
async reviewMode(request ) {
|
|
1364
|
-
await
|
|
1382
|
+
const cwd = await canonicalNativeReviewCwd(request.cwd);
|
|
1383
|
+
await this.verifyVersion(cwd, request.signal, ["mode"]);
|
|
1365
1384
|
const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
|
|
1366
1385
|
const { body } = await this.execute(
|
|
1367
1386
|
NATIVE_REVIEW_OPERATION.MODE,
|
|
1368
|
-
|
|
1369
|
-
["review", "mode", request.operation, "--cwd",
|
|
1387
|
+
cwd,
|
|
1388
|
+
["review", "mode", request.operation, "--cwd", cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
|
|
1370
1389
|
mutating,
|
|
1371
1390
|
request.signal,
|
|
1372
1391
|
);
|
|
@@ -1655,6 +1674,23 @@ export class NativeReviewConsentRequiredError extends Error {
|
|
|
1655
1674
|
}
|
|
1656
1675
|
}
|
|
1657
1676
|
|
|
1677
|
+
// Raised when the provider-issued consent invocation no longer matches the
|
|
1678
|
+
// binding Pi is answering for. Every one of these guards runs before the
|
|
1679
|
+
// provider is launched, so the failure is local and nothing was mutated. It
|
|
1680
|
+
// carries its own identity precisely so callers never report it as a provider
|
|
1681
|
+
// outage: an opaque `native-operation-failed` here sent issue #247 chasing a
|
|
1682
|
+
// missing `--cwd` that Pi does forward.
|
|
1683
|
+
export class NativeReviewConsentBindingError extends Error {
|
|
1684
|
+
reason ;
|
|
1685
|
+
launchAttempted = false;
|
|
1686
|
+
mutationOutcome = "none";
|
|
1687
|
+
constructor(reason , message ) {
|
|
1688
|
+
super(message);
|
|
1689
|
+
this.name = "NativeReviewConsentBindingError";
|
|
1690
|
+
this.reason = reason;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1658
1694
|
function splitNativeConsentInvocation(invocation ) {
|
|
1659
1695
|
const words = [];
|
|
1660
1696
|
let current = "";
|
|
@@ -1706,26 +1742,26 @@ function exactConsentOption(arguments_ , name )
|
|
|
1706
1742
|
const token = arguments_[index] ;
|
|
1707
1743
|
if (token === name) {
|
|
1708
1744
|
const value = arguments_[index + 1];
|
|
1709
|
-
if (value === undefined) throw new
|
|
1745
|
+
if (value === undefined) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", `Native consent invocation ${name} is missing its value`);
|
|
1710
1746
|
values.push(value);
|
|
1711
1747
|
index += 1;
|
|
1712
1748
|
} else if (token.startsWith(`${name}=`)) values.push(token.slice(name.length + 1));
|
|
1713
1749
|
}
|
|
1714
|
-
if (values.length !== 1) throw new
|
|
1750
|
+
if (values.length !== 1) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", `Native consent invocation requires exactly one ${name}`);
|
|
1715
1751
|
return values[0] ;
|
|
1716
1752
|
}
|
|
1717
1753
|
|
|
1718
1754
|
function consentInvocationArguments(request ) {
|
|
1719
1755
|
const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
|
|
1720
|
-
if (choice === undefined) throw new
|
|
1756
|
+
if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
|
|
1721
1757
|
const words = splitNativeConsentInvocation(choice.invocation);
|
|
1722
|
-
if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new
|
|
1758
|
+
if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new NativeReviewConsentBindingError("consent-invocation-not-start", "Native consent invocation is not a provider review START");
|
|
1723
1759
|
const arguments_ = words.slice(1);
|
|
1724
|
-
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new
|
|
1725
|
-
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new
|
|
1726
|
-
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new
|
|
1727
|
-
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new
|
|
1728
|
-
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new
|
|
1760
|
+
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new NativeReviewConsentBindingError("consent-invocation-contract-changed", "Native consent invocation contract changed");
|
|
1761
|
+
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
|
|
1762
|
+
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
|
|
1763
|
+
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
|
|
1764
|
+
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new NativeReviewConsentBindingError("consent-invocation-answer-changed", "Native consent invocation answer binding changed");
|
|
1729
1765
|
return arguments_;
|
|
1730
1766
|
}
|
|
1731
1767
|
|
|
@@ -1938,24 +1974,21 @@ export class NativeReviewCliV216 {
|
|
|
1938
1974
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
|
|
1939
1975
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
1940
1976
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
1941
|
-
|
|
1942
|
-
//
|
|
1943
|
-
//
|
|
1944
|
-
//
|
|
1945
|
-
// a target frozen from the wrong root would name a snapshot this START
|
|
1946
|
-
// never inspects. targetStatus is read-only, so this costs one extra
|
|
1947
|
-
// read and cannot create authority.
|
|
1977
|
+
if (request.targetIdentity !== undefined && !/^sha256:[0-9a-f]{64}$/.test(request.targetIdentity)) throw new TypeError("Native START targetIdentity must be a canonical sha256 identity");
|
|
1978
|
+
// The controller supplies the target it already projected from the
|
|
1979
|
+
// authority workspace after proving its immutable actor view is identical.
|
|
1980
|
+
// Direct adapter callers may omit it and retain the same-root projection.
|
|
1948
1981
|
const projection = request.projection ?? "workspace";
|
|
1949
|
-
const
|
|
1982
|
+
const targetIdentity = request.targetIdentity ?? (await this.targetStatus({
|
|
1950
1983
|
cwd: request.cwd,
|
|
1951
1984
|
projection,
|
|
1952
1985
|
...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
|
|
1953
1986
|
...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
|
|
1954
1987
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
1955
|
-
});
|
|
1988
|
+
})).targetIdentity;
|
|
1956
1989
|
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
|
|
1957
1990
|
"review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
1958
|
-
"--target",
|
|
1991
|
+
"--target", targetIdentity, "--projection", projection,
|
|
1959
1992
|
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
|
|
1960
1993
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1961
1994
|
...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
|
|
@@ -1967,10 +2000,14 @@ export class NativeReviewCliV216 {
|
|
|
1967
2000
|
// explicit answer it cannot infer. Discriminate before decode and surface
|
|
1968
2001
|
// the complete envelope; only the caller can map a human answer.
|
|
1969
2002
|
if (execution.body.action === "consent_required") {
|
|
1970
|
-
|
|
2003
|
+
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body));
|
|
2004
|
+
if (consent.targetIdentity !== targetIdentity || consent.projection !== projection) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent target binding mismatch");
|
|
2005
|
+
throw new NativeReviewConsentRequiredError(consent);
|
|
1971
2006
|
}
|
|
1972
2007
|
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
1973
2008
|
if (request.lineageId !== undefined && result.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
|
|
2009
|
+
const resultTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2010
|
+
if (resultTarget !== undefined && resultTarget !== targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start target mismatch");
|
|
1974
2011
|
return {
|
|
1975
2012
|
lineageId: result.lineageId,
|
|
1976
2013
|
state: result.state ,
|
|
@@ -668,6 +668,33 @@ test("native review status uses the anticipated v2.1.5 contract, preserves Windo
|
|
|
668
668
|
}
|
|
669
669
|
});
|
|
670
670
|
|
|
671
|
+
test("native review status decodes and retains the v2.2.2 compact snapshot identity", async () => {
|
|
672
|
+
const snapshotIdentity = "sha256:0586c3d40c3acf3db92214ddaf07afe44ad862518b48d608ae4157377ab2f3cc";
|
|
673
|
+
const entry = {
|
|
674
|
+
version: "compact-v2",
|
|
675
|
+
lineage_id: "issue-136-v2-1-2-final",
|
|
676
|
+
path: "C:\\repo with spaces\\.git\\gentle-ai\\review-transactions\\v2\\issue-136-v2-1-2-final",
|
|
677
|
+
status: "active",
|
|
678
|
+
state: "reviewing",
|
|
679
|
+
revision: "sha256:8103efb360d10d1717a4d22e38c8b75074f99680ed6a5eb3ab40e0ed9fb69931",
|
|
680
|
+
snapshot_identity: snapshotIdentity,
|
|
681
|
+
problems: [],
|
|
682
|
+
};
|
|
683
|
+
const status = { ...JSON.parse(REVIEW_STATUS.stdout), status: "active", entries: [entry] };
|
|
684
|
+
const queue = queuedAdapter([{ stdout: "gentle-ai 2.2.2\n" }, { stdout: JSON.stringify(status) }]);
|
|
685
|
+
const decoded = await new NativeReviewCliV213(queue.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
686
|
+
assert.equal(decoded.entries[0]?.snapshotIdentity, snapshotIdentity);
|
|
687
|
+
|
|
688
|
+
const malformed = queuedAdapter([{ stdout: "gentle-ai 2.2.2\n" }, { stdout: JSON.stringify({ ...status, entries: [{ ...entry, snapshot_identity: snapshotIdentity.slice("sha256:".length) }] }) }]);
|
|
689
|
+
await assert.rejects(
|
|
690
|
+
() => new NativeReviewCliV213(malformed.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
691
|
+
(error: unknown) => error instanceof NativeReviewCliError
|
|
692
|
+
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
693
|
+
&& error.operation === "review/status"
|
|
694
|
+
&& error.mutationOutcome === "none",
|
|
695
|
+
);
|
|
696
|
+
});
|
|
697
|
+
|
|
671
698
|
test("native review status decodes 2.1.8 released lock residue and keeps unknown lock statuses fail-closed", async () => {
|
|
672
699
|
// gentle-ai 2.1.8 leaves review-transactions/v2/LOCK behind after ORDINARY
|
|
673
700
|
// successful operations and reports it as {"status":"released"} without
|