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.
@@ -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
- function nativeRiskEvidenceSubject(reason ) {
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`. Pi stays headless and never
1622
- // sends `--consent relay`, so this is a blocking question surfaced to the
1623
- // caller rather than answered on its behalf.
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
- // A headless START that reviews without asking still succeeds; its
1846
- // consent notices are console output, never a failure signal.
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. Discriminated BEFORE decode: the two
1851
- // envelopes share no required shape, and Pi never sends `--consent
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: sha256(body.target_identity, "consent.target_identity"),
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"),