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
|
@@ -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;
|
|
@@ -1124,6 +1146,7 @@ function nativeError(code , operation
|
|
|
1124
1146
|
|
|
1125
1147
|
|
|
1126
1148
|
|
|
1149
|
+
|
|
1127
1150
|
export class NativeReviewCliV214 {
|
|
1128
1151
|
adapter ;
|
|
1129
1152
|
executable ;
|
|
@@ -1618,12 +1641,13 @@ export class NativeReviewIntegrationError extends Error {
|
|
|
1618
1641
|
}
|
|
1619
1642
|
|
|
1620
1643
|
// Raised when negotiated START answers `consent/v2` (action:
|
|
1621
|
-
// "consent_required") instead of `start/v3`.
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1644
|
+
// "consent_required") instead of `start/v3`. The provider has frozen no
|
|
1645
|
+
// authority yet: Pi must relay this complete candidate-scoped question and may
|
|
1646
|
+
// answer only through one of the exact invocations carried by the envelope.
|
|
1624
1647
|
export class NativeReviewConsentRequiredError extends Error {
|
|
1625
1648
|
consent ;
|
|
1626
1649
|
launchAttempted = true;
|
|
1650
|
+
mutationOutcome = "none";
|
|
1627
1651
|
constructor(consent ) {
|
|
1628
1652
|
super(consent.headline);
|
|
1629
1653
|
this.name = "NativeReviewConsentRequiredError";
|
|
@@ -1631,6 +1655,100 @@ export class NativeReviewConsentRequiredError extends Error {
|
|
|
1631
1655
|
}
|
|
1632
1656
|
}
|
|
1633
1657
|
|
|
1658
|
+
function splitNativeConsentInvocation(invocation ) {
|
|
1659
|
+
const words = [];
|
|
1660
|
+
let current = "";
|
|
1661
|
+
let quote ;
|
|
1662
|
+
let escaping = false;
|
|
1663
|
+
let started = false;
|
|
1664
|
+
for (const character of invocation.trim()) {
|
|
1665
|
+
if (escaping) {
|
|
1666
|
+
current += character;
|
|
1667
|
+
escaping = false;
|
|
1668
|
+
started = true;
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (character === "\\" && quote !== "'") {
|
|
1672
|
+
escaping = true;
|
|
1673
|
+
started = true;
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
if (quote !== undefined) {
|
|
1677
|
+
if (character === quote) quote = undefined;
|
|
1678
|
+
else current += character;
|
|
1679
|
+
started = true;
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (character === "'" || character === '"') {
|
|
1683
|
+
quote = character;
|
|
1684
|
+
started = true;
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (/\s/.test(character)) {
|
|
1688
|
+
if (started) {
|
|
1689
|
+
words.push(current);
|
|
1690
|
+
current = "";
|
|
1691
|
+
started = false;
|
|
1692
|
+
}
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
current += character;
|
|
1696
|
+
started = true;
|
|
1697
|
+
}
|
|
1698
|
+
if (quote !== undefined || escaping) throw new TypeError("Native consent invocation has invalid quoting");
|
|
1699
|
+
if (started) words.push(current);
|
|
1700
|
+
return words;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function exactConsentOption(arguments_ , name ) {
|
|
1704
|
+
const values = [];
|
|
1705
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
1706
|
+
const token = arguments_[index] ;
|
|
1707
|
+
if (token === name) {
|
|
1708
|
+
const value = arguments_[index + 1];
|
|
1709
|
+
if (value === undefined) throw new TypeError(`Native consent invocation ${name} is missing its value`);
|
|
1710
|
+
values.push(value);
|
|
1711
|
+
index += 1;
|
|
1712
|
+
} else if (token.startsWith(`${name}=`)) values.push(token.slice(name.length + 1));
|
|
1713
|
+
}
|
|
1714
|
+
if (values.length !== 1) throw new TypeError(`Native consent invocation requires exactly one ${name}`);
|
|
1715
|
+
return values[0] ;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function consentInvocationArguments(request ) {
|
|
1719
|
+
const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
|
|
1720
|
+
if (choice === undefined) throw new TypeError("Native consent answer must be granted or declined");
|
|
1721
|
+
const words = splitNativeConsentInvocation(choice.invocation);
|
|
1722
|
+
if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new TypeError("Native consent invocation is not a provider review START");
|
|
1723
|
+
const arguments_ = words.slice(1);
|
|
1724
|
+
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new TypeError("Native consent invocation contract changed");
|
|
1725
|
+
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new TypeError("Native consent invocation repository binding changed");
|
|
1726
|
+
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new TypeError("Native consent invocation target binding changed");
|
|
1727
|
+
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new TypeError("Native consent invocation projection binding changed");
|
|
1728
|
+
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new TypeError("Native consent invocation answer binding changed");
|
|
1729
|
+
return arguments_;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function decodeDeclinedConsentStart(value , expected ) {
|
|
1733
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("Native declined consent result must be an object");
|
|
1734
|
+
const body = value ;
|
|
1735
|
+
if (body.operation !== "review/start" || body.action !== "declined" || body.consent !== "declined_this_candidate") throw new TypeError("Native declined consent result has an invalid identity");
|
|
1736
|
+
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");
|
|
1737
|
+
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");
|
|
1738
|
+
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");
|
|
1739
|
+
if (body.lineage_id !== "" || body.state !== "" || body.correction_budget !== 0) throw new TypeError("Native declined consent result cannot carry review authority");
|
|
1740
|
+
return {
|
|
1741
|
+
kind: "declined",
|
|
1742
|
+
targetIdentity: expected.consent.targetIdentity,
|
|
1743
|
+
projection: expected.consent.projection,
|
|
1744
|
+
riskLevel: expected.consent.riskLevel,
|
|
1745
|
+
changedFiles: body.changed_files,
|
|
1746
|
+
changedLines: body.changed_lines,
|
|
1747
|
+
consent: "declined_this_candidate",
|
|
1748
|
+
raw: body,
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1634
1752
|
|
|
1635
1753
|
const nativeCapabilitiesByDigest = new Map ();
|
|
1636
1754
|
|
|
@@ -1842,14 +1960,12 @@ export class NativeReviewCliV216 {
|
|
|
1842
1960
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1843
1961
|
...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
|
|
1844
1962
|
...(request.focus === undefined ? [] : ["--focus", request.focus]),
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
], true, request.signal, REVIEW_CONSENT_NOTICES);
|
|
1963
|
+
"--consent", "relay",
|
|
1964
|
+
], true, request.signal);
|
|
1848
1965
|
// A negotiated v2 START may answer `consent/v2` (action:
|
|
1849
1966
|
// "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.
|
|
1967
|
+
// explicit answer it cannot infer. Discriminate before decode and surface
|
|
1968
|
+
// the complete envelope; only the caller can map a human answer.
|
|
1853
1969
|
if (execution.body.action === "consent_required") {
|
|
1854
1970
|
throw new NativeReviewConsentRequiredError(decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body)));
|
|
1855
1971
|
}
|
|
@@ -1883,6 +1999,36 @@ export class NativeReviewCliV216 {
|
|
|
1883
1999
|
};
|
|
1884
2000
|
}
|
|
1885
2001
|
|
|
2002
|
+
async answerConsent(request ) {
|
|
2003
|
+
const arguments_ = consentInvocationArguments(request);
|
|
2004
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, arguments_, true, request.signal);
|
|
2005
|
+
if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
|
|
2006
|
+
return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
|
|
2007
|
+
}
|
|
2008
|
+
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
2009
|
+
const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2010
|
+
if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
|
|
2011
|
+
const lineageId = exactConsentOption(arguments_, "--lineage");
|
|
2012
|
+
if (result.lineageId !== lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
|
|
2013
|
+
return { kind: "started", start: {
|
|
2014
|
+
lineageId: result.lineageId,
|
|
2015
|
+
state: result.state ,
|
|
2016
|
+
riskLevel: result.riskLevel,
|
|
2017
|
+
selectedLenses: result.selectedLenses,
|
|
2018
|
+
changedFiles: result.changedFiles,
|
|
2019
|
+
changedLines: result.changedLines,
|
|
2020
|
+
correctionBudget: result.correctionBudget,
|
|
2021
|
+
action: result.action ,
|
|
2022
|
+
lensesRequired: result.lensesRequired,
|
|
2023
|
+
riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
|
|
2024
|
+
...(() => {
|
|
2025
|
+
const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
|
|
2026
|
+
return evidence.length === 0 ? {} : { riskEvidence: evidence };
|
|
2027
|
+
})(),
|
|
2028
|
+
raw: result.raw,
|
|
2029
|
+
} };
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1886
2032
|
async stageDocument(directory , name , document ) {
|
|
1887
2033
|
const path = join(directory, `${name}.json`);
|
|
1888
2034
|
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"),
|