gentle-pi 2.0.0 → 2.1.1
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 +362 -123
- package/lib/gentle-ai-binary.ts +64 -17
- package/lib/native-review-cli.ts +207 -27
- 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 +206 -26
- 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 +205 -0
- package/tests/native-review-parity-runtime.test.ts +15 -6
- package/tests/native-review-parity.test.ts +228 -53
- package/tests/package-manifest.test.ts +10 -3
- package/tests/review-candidate-view.test.ts +43 -0
- package/tests/review-controller-lock-status.test.ts +25 -7
- package/tests/review-controller-native-routing.test.ts +419 -20
- package/tests/review-controller-workspace-root.test.ts +40 -10
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-integration-v2.test.ts +10 -1
- package/tests/runtime-harness.mjs +24 -12
|
@@ -118,6 +118,7 @@ export const NATIVE_SDD_ARTIFACT_STATE = {
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
|
|
121
122
|
|
|
122
123
|
|
|
123
124
|
|
|
@@ -372,6 +373,21 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
|
|
|
372
373
|
|
|
373
374
|
|
|
374
375
|
|
|
376
|
+
export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } ;
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
375
391
|
|
|
376
392
|
|
|
377
393
|
|
|
@@ -560,7 +576,13 @@ const REVIEW_RISK_SUBJECT_BY_SIGNAL = Object.f
|
|
|
560
576
|
// an unmapped signal degrades to this rather than dropping the path entirely.
|
|
561
577
|
const REVIEW_RISK_UNKNOWN_SIGNAL_SUBJECT = "a sensitive area";
|
|
562
578
|
|
|
563
|
-
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
function nativeRiskEvidenceSubject(reason ) {
|
|
564
586
|
const code = typeof reason.code === "string" ? reason.code : "";
|
|
565
587
|
if (code === "hot_path") {
|
|
566
588
|
const signal = typeof reason.signal === "string" ? reason.signal : "";
|
|
@@ -569,7 +591,7 @@ function nativeRiskEvidenceSubject(reason ) {
|
|
|
569
591
|
return REVIEW_RISK_SUBJECT_BY_CODE[code] ?? "";
|
|
570
592
|
}
|
|
571
593
|
|
|
572
|
-
function nativeRiskEvidencePhrase(reason
|
|
594
|
+
function nativeRiskEvidencePhrase(reason ) {
|
|
573
595
|
const path = typeof reason.path === "string" ? reason.path.trim() : "";
|
|
574
596
|
// An empty file is named first and described second. Every other subject
|
|
575
597
|
// reads "<what changed> in <path>", which for a file with no bytes would
|
|
@@ -582,7 +604,7 @@ function nativeRiskEvidencePhrase(reason ) {
|
|
|
582
604
|
return `${subject} in ${path}`;
|
|
583
605
|
}
|
|
584
606
|
|
|
585
|
-
export function nativeRiskEvidencePhrases(riskLevel , reasons
|
|
607
|
+
export function nativeRiskEvidencePhrases(riskLevel , reasons ) {
|
|
586
608
|
if (riskLevel !== "high" && riskLevel !== "medium") return [];
|
|
587
609
|
const phrases = reasons.map((reason) => nativeRiskEvidencePhrase(reason)).filter((phrase) => phrase !== "");
|
|
588
610
|
return riskLevel === "medium" ? [REVIEW_MEDIUM_RISK_REASON, ...phrases] : phrases;
|
|
@@ -1016,6 +1038,21 @@ function decodeNativeReviewStatus(value ) {
|
|
|
1016
1038
|
};
|
|
1017
1039
|
}
|
|
1018
1040
|
function isWindowsRepositoryPath(value ) { return /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value); }
|
|
1041
|
+
export function normalizeNativeReviewCwd(value , platform = process.platform) {
|
|
1042
|
+
if (platform !== "win32") return value;
|
|
1043
|
+
const gitBashDrive = /^\/([A-Za-z])(?:\/(.*))?$/.exec(value);
|
|
1044
|
+
const windowsPath = gitBashDrive === null
|
|
1045
|
+
? value
|
|
1046
|
+
: `${gitBashDrive[1] .toUpperCase()}:/${gitBashDrive[2] ?? ""}`;
|
|
1047
|
+
if (!isWindowsRepositoryPath(windowsPath)) return windowsPath;
|
|
1048
|
+
const normalized = win32.normalize(windowsPath);
|
|
1049
|
+
return normalized.replace(/^([a-z]):/, (_match, drive ) => `${drive.toUpperCase()}:`);
|
|
1050
|
+
}
|
|
1051
|
+
async function canonicalNativeReviewCwd(value ) {
|
|
1052
|
+
const normalized = normalizeNativeReviewCwd(value);
|
|
1053
|
+
try { return await realpath(normalized); }
|
|
1054
|
+
catch { return normalized; }
|
|
1055
|
+
}
|
|
1019
1056
|
async function repositoryPathIdentity(value ) {
|
|
1020
1057
|
const windowsPath = isWindowsRepositoryPath(value);
|
|
1021
1058
|
try { return `filesystem:${windowsPath ? (await realpath(value)).toLowerCase() : await realpath(value)}`; }
|
|
@@ -1124,6 +1161,7 @@ function nativeError(code , operation
|
|
|
1124
1161
|
|
|
1125
1162
|
|
|
1126
1163
|
|
|
1164
|
+
|
|
1127
1165
|
export class NativeReviewCliV214 {
|
|
1128
1166
|
adapter ;
|
|
1129
1167
|
executable ;
|
|
@@ -1338,12 +1376,13 @@ export class NativeReviewCliV214 {
|
|
|
1338
1376
|
// and always pass `--scope clone` so Pi's own kill-switch command surface
|
|
1339
1377
|
// never mutates the operator's global gentle-ai state across other clones.
|
|
1340
1378
|
async reviewMode(request ) {
|
|
1341
|
-
await
|
|
1379
|
+
const cwd = await canonicalNativeReviewCwd(request.cwd);
|
|
1380
|
+
await this.verifyVersion(cwd, request.signal, ["mode"]);
|
|
1342
1381
|
const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
|
|
1343
1382
|
const { body } = await this.execute(
|
|
1344
1383
|
NATIVE_REVIEW_OPERATION.MODE,
|
|
1345
|
-
|
|
1346
|
-
["review", "mode", request.operation, "--cwd",
|
|
1384
|
+
cwd,
|
|
1385
|
+
["review", "mode", request.operation, "--cwd", cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
|
|
1347
1386
|
mutating,
|
|
1348
1387
|
request.signal,
|
|
1349
1388
|
);
|
|
@@ -1618,12 +1657,13 @@ export class NativeReviewIntegrationError extends Error {
|
|
|
1618
1657
|
}
|
|
1619
1658
|
|
|
1620
1659
|
// Raised when negotiated START answers `consent/v2` (action:
|
|
1621
|
-
// "consent_required") instead of `start/v3`.
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1660
|
+
// "consent_required") instead of `start/v3`. The provider has frozen no
|
|
1661
|
+
// authority yet: Pi must relay this complete candidate-scoped question and may
|
|
1662
|
+
// answer only through one of the exact invocations carried by the envelope.
|
|
1624
1663
|
export class NativeReviewConsentRequiredError extends Error {
|
|
1625
1664
|
consent ;
|
|
1626
1665
|
launchAttempted = true;
|
|
1666
|
+
mutationOutcome = "none";
|
|
1627
1667
|
constructor(consent ) {
|
|
1628
1668
|
super(consent.headline);
|
|
1629
1669
|
this.name = "NativeReviewConsentRequiredError";
|
|
@@ -1631,6 +1671,117 @@ export class NativeReviewConsentRequiredError extends Error {
|
|
|
1631
1671
|
}
|
|
1632
1672
|
}
|
|
1633
1673
|
|
|
1674
|
+
// Raised when the provider-issued consent invocation no longer matches the
|
|
1675
|
+
// binding Pi is answering for. Every one of these guards runs before the
|
|
1676
|
+
// provider is launched, so the failure is local and nothing was mutated. It
|
|
1677
|
+
// carries its own identity precisely so callers never report it as a provider
|
|
1678
|
+
// outage: an opaque `native-operation-failed` here sent issue #247 chasing a
|
|
1679
|
+
// missing `--cwd` that Pi does forward.
|
|
1680
|
+
export class NativeReviewConsentBindingError extends Error {
|
|
1681
|
+
reason ;
|
|
1682
|
+
launchAttempted = false;
|
|
1683
|
+
mutationOutcome = "none";
|
|
1684
|
+
constructor(reason , message ) {
|
|
1685
|
+
super(message);
|
|
1686
|
+
this.name = "NativeReviewConsentBindingError";
|
|
1687
|
+
this.reason = reason;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function splitNativeConsentInvocation(invocation ) {
|
|
1692
|
+
const words = [];
|
|
1693
|
+
let current = "";
|
|
1694
|
+
let quote ;
|
|
1695
|
+
let escaping = false;
|
|
1696
|
+
let started = false;
|
|
1697
|
+
for (const character of invocation.trim()) {
|
|
1698
|
+
if (escaping) {
|
|
1699
|
+
current += character;
|
|
1700
|
+
escaping = false;
|
|
1701
|
+
started = true;
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (character === "\\" && quote !== "'") {
|
|
1705
|
+
escaping = true;
|
|
1706
|
+
started = true;
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
if (quote !== undefined) {
|
|
1710
|
+
if (character === quote) quote = undefined;
|
|
1711
|
+
else current += character;
|
|
1712
|
+
started = true;
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (character === "'" || character === '"') {
|
|
1716
|
+
quote = character;
|
|
1717
|
+
started = true;
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (/\s/.test(character)) {
|
|
1721
|
+
if (started) {
|
|
1722
|
+
words.push(current);
|
|
1723
|
+
current = "";
|
|
1724
|
+
started = false;
|
|
1725
|
+
}
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
current += character;
|
|
1729
|
+
started = true;
|
|
1730
|
+
}
|
|
1731
|
+
if (quote !== undefined || escaping) throw new TypeError("Native consent invocation has invalid quoting");
|
|
1732
|
+
if (started) words.push(current);
|
|
1733
|
+
return words;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function exactConsentOption(arguments_ , name ) {
|
|
1737
|
+
const values = [];
|
|
1738
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
1739
|
+
const token = arguments_[index] ;
|
|
1740
|
+
if (token === name) {
|
|
1741
|
+
const value = arguments_[index + 1];
|
|
1742
|
+
if (value === undefined) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", `Native consent invocation ${name} is missing its value`);
|
|
1743
|
+
values.push(value);
|
|
1744
|
+
index += 1;
|
|
1745
|
+
} else if (token.startsWith(`${name}=`)) values.push(token.slice(name.length + 1));
|
|
1746
|
+
}
|
|
1747
|
+
if (values.length !== 1) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", `Native consent invocation requires exactly one ${name}`);
|
|
1748
|
+
return values[0] ;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function consentInvocationArguments(request ) {
|
|
1752
|
+
const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
|
|
1753
|
+
if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
|
|
1754
|
+
const words = splitNativeConsentInvocation(choice.invocation);
|
|
1755
|
+
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");
|
|
1756
|
+
const arguments_ = words.slice(1);
|
|
1757
|
+
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new NativeReviewConsentBindingError("consent-invocation-contract-changed", "Native consent invocation contract changed");
|
|
1758
|
+
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
|
|
1759
|
+
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
|
|
1760
|
+
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
|
|
1761
|
+
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new NativeReviewConsentBindingError("consent-invocation-answer-changed", "Native consent invocation answer binding changed");
|
|
1762
|
+
return arguments_;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function decodeDeclinedConsentStart(value , expected ) {
|
|
1766
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("Native declined consent result must be an object");
|
|
1767
|
+
const body = value ;
|
|
1768
|
+
if (body.operation !== "review/start" || body.action !== "declined" || body.consent !== "declined_this_candidate") throw new TypeError("Native declined consent result has an invalid identity");
|
|
1769
|
+
if (body.target_identity !== expected.consent.targetIdentity || body.projection !== expected.consent.projection || body.risk_level !== expected.consent.riskLevel) throw new TypeError("Native declined consent result target binding changed");
|
|
1770
|
+
if (body.lenses_required !== false || !Array.isArray(body.selected_lenses) || body.selected_lenses.length !== 0 || !Array.isArray(body.lens_bindings) || body.lens_bindings.length !== 0) throw new TypeError("Native declined consent result must create no review authority");
|
|
1771
|
+
if (typeof body.changed_files !== "number" || !Number.isSafeInteger(body.changed_files) || body.changed_files < 0 || typeof body.changed_lines !== "number" || !Number.isSafeInteger(body.changed_lines) || body.changed_lines < 0) throw new TypeError("Native declined consent result has invalid change counts");
|
|
1772
|
+
if (body.lineage_id !== "" || body.state !== "" || body.correction_budget !== 0) throw new TypeError("Native declined consent result cannot carry review authority");
|
|
1773
|
+
return {
|
|
1774
|
+
kind: "declined",
|
|
1775
|
+
targetIdentity: expected.consent.targetIdentity,
|
|
1776
|
+
projection: expected.consent.projection,
|
|
1777
|
+
riskLevel: expected.consent.riskLevel,
|
|
1778
|
+
changedFiles: body.changed_files,
|
|
1779
|
+
changedLines: body.changed_lines,
|
|
1780
|
+
consent: "declined_this_candidate",
|
|
1781
|
+
raw: body,
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1634
1785
|
|
|
1635
1786
|
const nativeCapabilitiesByDigest = new Map ();
|
|
1636
1787
|
|
|
@@ -1820,41 +1971,40 @@ export class NativeReviewCliV216 {
|
|
|
1820
1971
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
|
|
1821
1972
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
1822
1973
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
1823
|
-
|
|
1824
|
-
//
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
// a target frozen from the wrong root would name a snapshot this START
|
|
1828
|
-
// never inspects. targetStatus is read-only, so this costs one extra
|
|
1829
|
-
// read and cannot create authority.
|
|
1974
|
+
if (request.targetIdentity !== undefined && !/^sha256:[0-9a-f]{64}$/.test(request.targetIdentity)) throw new TypeError("Native START targetIdentity must be a canonical sha256 identity");
|
|
1975
|
+
// The controller supplies the target it already projected from the
|
|
1976
|
+
// authority workspace after proving its immutable actor view is identical.
|
|
1977
|
+
// Direct adapter callers may omit it and retain the same-root projection.
|
|
1830
1978
|
const projection = request.projection ?? "workspace";
|
|
1831
|
-
const
|
|
1979
|
+
const targetIdentity = request.targetIdentity ?? (await this.targetStatus({
|
|
1832
1980
|
cwd: request.cwd,
|
|
1833
1981
|
projection,
|
|
1834
1982
|
...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
|
|
1835
1983
|
...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
|
|
1836
1984
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
1837
|
-
});
|
|
1985
|
+
})).targetIdentity;
|
|
1838
1986
|
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
|
|
1839
1987
|
"review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
1840
|
-
"--target",
|
|
1988
|
+
"--target", targetIdentity, "--projection", projection,
|
|
1841
1989
|
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
|
|
1842
1990
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1843
1991
|
...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
|
|
1844
1992
|
...(request.focus === undefined ? [] : ["--focus", request.focus]),
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
], true, request.signal, REVIEW_CONSENT_NOTICES);
|
|
1993
|
+
"--consent", "relay",
|
|
1994
|
+
], true, request.signal);
|
|
1848
1995
|
// A negotiated v2 START may answer `consent/v2` (action:
|
|
1849
1996
|
// "consent_required") instead of `start/v3` when the provider needs an
|
|
1850
|
-
// explicit answer it cannot infer.
|
|
1851
|
-
//
|
|
1852
|
-
// relay`, so this is always surfaced to the caller, never answered here.
|
|
1997
|
+
// explicit answer it cannot infer. Discriminate before decode and surface
|
|
1998
|
+
// the complete envelope; only the caller can map a human answer.
|
|
1853
1999
|
if (execution.body.action === "consent_required") {
|
|
1854
|
-
|
|
2000
|
+
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body));
|
|
2001
|
+
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");
|
|
2002
|
+
throw new NativeReviewConsentRequiredError(consent);
|
|
1855
2003
|
}
|
|
1856
2004
|
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
1857
2005
|
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");
|
|
2006
|
+
const resultTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2007
|
+
if (resultTarget !== undefined && resultTarget !== targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start target mismatch");
|
|
1858
2008
|
return {
|
|
1859
2009
|
lineageId: result.lineageId,
|
|
1860
2010
|
state: result.state ,
|
|
@@ -1883,6 +2033,36 @@ export class NativeReviewCliV216 {
|
|
|
1883
2033
|
};
|
|
1884
2034
|
}
|
|
1885
2035
|
|
|
2036
|
+
async answerConsent(request ) {
|
|
2037
|
+
const arguments_ = consentInvocationArguments(request);
|
|
2038
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, arguments_, true, request.signal);
|
|
2039
|
+
if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
|
|
2040
|
+
return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
|
|
2041
|
+
}
|
|
2042
|
+
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
2043
|
+
const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2044
|
+
if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
|
|
2045
|
+
const lineageId = exactConsentOption(arguments_, "--lineage");
|
|
2046
|
+
if (result.lineageId !== lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
|
|
2047
|
+
return { kind: "started", start: {
|
|
2048
|
+
lineageId: result.lineageId,
|
|
2049
|
+
state: result.state ,
|
|
2050
|
+
riskLevel: result.riskLevel,
|
|
2051
|
+
selectedLenses: result.selectedLenses,
|
|
2052
|
+
changedFiles: result.changedFiles,
|
|
2053
|
+
changedLines: result.changedLines,
|
|
2054
|
+
correctionBudget: result.correctionBudget,
|
|
2055
|
+
action: result.action ,
|
|
2056
|
+
lensesRequired: result.lensesRequired,
|
|
2057
|
+
riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
|
|
2058
|
+
...(() => {
|
|
2059
|
+
const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
|
|
2060
|
+
return evidence.length === 0 ? {} : { riskEvidence: evidence };
|
|
2061
|
+
})(),
|
|
2062
|
+
raw: result.raw,
|
|
2063
|
+
} };
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1886
2066
|
async stageDocument(directory , name , document ) {
|
|
1887
2067
|
const path = join(directory, `${name}.json`);
|
|
1888
2068
|
await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
|
|
@@ -1304,10 +1304,14 @@ export function decodeReviewConsentV2(value ) {
|
|
|
1304
1304
|
if (body.action !== "consent_required") throw new TypeError("consent.action must be consent_required");
|
|
1305
1305
|
if (body.blocking !== true) throw new TypeError("consent.blocking must be true");
|
|
1306
1306
|
|
|
1307
|
+
const targetIdentity = sha256(body.target_identity, "consent.target_identity");
|
|
1307
1308
|
const choicesArray = body.choices;
|
|
1308
1309
|
if (!Array.isArray(choicesArray) || choicesArray.length !== 2) throw new TypeError("consent.choices must have exactly 2 items");
|
|
1309
1310
|
const granted = decodeConsentChoice(choicesArray[0], "consent.choices[0]", "granted");
|
|
1310
1311
|
const declined = decodeConsentChoice(choicesArray[1], "consent.choices[1]", "declined");
|
|
1312
|
+
for (const choice of [granted, declined]) {
|
|
1313
|
+
if (!choice.invocation.includes(` --target ${targetIdentity} `)) throw new TypeError(`consent choice ${choice.answer} invocation must bind consent.target_identity`);
|
|
1314
|
+
}
|
|
1311
1315
|
|
|
1312
1316
|
const offPathSource = exactRecord(body.off_path, "consent.off_path", ["note", "command"]);
|
|
1313
1317
|
if (offPathSource.command !== "gentle-ai review mode disable") throw new TypeError("consent.off_path.command is unsupported");
|
|
@@ -1318,7 +1322,7 @@ export function decodeReviewConsentV2(value ) {
|
|
|
1318
1322
|
operation: "review.start",
|
|
1319
1323
|
action: "consent_required",
|
|
1320
1324
|
blocking: true,
|
|
1321
|
-
targetIdentity
|
|
1325
|
+
targetIdentity,
|
|
1322
1326
|
projection: enumeration(body.projection, REQUIRED_PROJECTIONS, "consent.projection"),
|
|
1323
1327
|
riskLevel: enumeration(body.risk_level, ["medium", "high"] , "consent.risk_level"),
|
|
1324
1328
|
changedFiles: integer(body.changed_files, "consent.changed_files"),
|