gentle-pi 1.2.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.
Files changed (111) hide show
  1. package/README.md +32 -10
  2. package/assets/agents/review-readability.md +0 -1
  3. package/assets/agents/review-reliability.md +0 -1
  4. package/assets/agents/review-resilience.md +0 -1
  5. package/assets/agents/review-risk.md +0 -1
  6. package/contracts/review-integration/v1/fixtures/binding-revision-conflict.fixture.json +25 -0
  7. package/contracts/review-integration/v1/fixtures/capabilities-v1.2.fixture.json +263 -0
  8. package/contracts/review-integration/v1/fixtures/capabilities-v1.3.fixture.json +301 -0
  9. package/contracts/review-integration/v1/fixtures/capabilities-v1.4.fixture.json +312 -0
  10. package/contracts/review-integration/v1/fixtures/capabilities-v1.5.fixture.json +321 -0
  11. package/contracts/review-integration/v1/fixtures/consent.fixture.json +36 -0
  12. package/contracts/review-integration/v1/fixtures/final-verification-incident.fixture.json +1 -0
  13. package/contracts/review-integration/v1/fixtures/operation.fixture.json +4 -0
  14. package/contracts/review-integration/v1/fixtures/repair-preflight.fixture.json +54 -0
  15. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +97 -0
  16. package/contracts/review-integration/v1/fixtures/start.fixture.json +25 -1
  17. package/contracts/review-integration/v1/fixtures/status-ambiguous.fixture.json +4 -0
  18. package/contracts/review-integration/v1/fixtures/status-corrupted.fixture.json +4 -0
  19. package/contracts/review-integration/v1/fixtures/status-recover.fixture.json +4 -0
  20. package/contracts/review-integration/v1/fixtures/status-unrelated.fixture.json +4 -0
  21. package/contracts/review-integration/v1/fixtures/status-v2-ambiguous.fixture.json +110 -0
  22. package/contracts/review-integration/v1/fixtures/status-v2-corrupted.fixture.json +107 -0
  23. package/contracts/review-integration/v1/fixtures/status-v2-final-verification-retry.fixture.json +192 -0
  24. package/contracts/review-integration/v1/fixtures/status-v2-recover.fixture.json +130 -0
  25. package/contracts/review-integration/v1/fixtures/status-v2-repair.fixture.json +161 -0
  26. package/contracts/review-integration/v1/fixtures/status-v2-unrelated.fixture.json +103 -0
  27. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +193 -0
  28. package/contracts/review-integration/v1/fixtures/status.fixture.json +19 -5
  29. package/contracts/review-integration/v1/fixtures/verification-evidence.fixture.json +1 -0
  30. package/contracts/review-integration/v1/schemas/admitted-result.schema.json +35 -0
  31. package/contracts/review-integration/v1/schemas/artifact-subject.schema.json +33 -0
  32. package/contracts/review-integration/v1/schemas/authority-repair-assessment.schema.json +247 -0
  33. package/contracts/review-integration/v1/schemas/capabilities-v1.2.schema.json +249 -0
  34. package/contracts/review-integration/v1/schemas/capabilities-v1.3.schema.json +429 -0
  35. package/contracts/review-integration/v1/schemas/capabilities-v1.4.schema.json +432 -0
  36. package/contracts/review-integration/v1/schemas/capabilities-v1.5.schema.json +154 -0
  37. package/contracts/review-integration/v1/schemas/consent.schema.json +86 -0
  38. package/contracts/review-integration/v1/schemas/failure.schema.json +54 -8
  39. package/contracts/review-integration/v1/schemas/final-verification-incident.schema.json +51 -0
  40. package/contracts/review-integration/v1/schemas/operation.schema.json +204 -31
  41. package/contracts/review-integration/v1/schemas/repair.schema.json +251 -0
  42. package/contracts/review-integration/v1/schemas/result-artifact-v2.schema.json +38 -0
  43. package/contracts/review-integration/v1/schemas/start-v2.schema.json +216 -0
  44. package/contracts/review-integration/v1/schemas/start.schema.json +79 -3
  45. package/contracts/review-integration/v1/schemas/status-v2.schema.json +313 -0
  46. package/contracts/review-integration/v1/schemas/status.schema.json +37 -10
  47. package/contracts/review-integration/v1/schemas/targeted-validation-request.schema.json +46 -0
  48. package/contracts/review-integration/v1/schemas/verification-evidence.schema.json +64 -0
  49. package/contracts/review-integration/v2/fixtures/capabilities.fixture.json +111 -0
  50. package/contracts/review-integration/v2/fixtures/consent.fixture.json +36 -0
  51. package/contracts/review-integration/v2/fixtures/start.fixture.json +93 -0
  52. package/contracts/review-integration/v2/fixtures/status.fixture.json +113 -0
  53. package/contracts/review-integration/v2/schemas/admitted-result.schema.json +14 -0
  54. package/contracts/review-integration/v2/schemas/artifact-subject.schema.json +36 -0
  55. package/contracts/review-integration/v2/schemas/capabilities.schema.json +111 -0
  56. package/contracts/review-integration/v2/schemas/consent.schema.json +70 -0
  57. package/contracts/review-integration/v2/schemas/failure.schema.json +35 -0
  58. package/contracts/review-integration/v2/schemas/operation.schema.json +46 -0
  59. package/contracts/review-integration/v2/schemas/repair.schema.json +22 -0
  60. package/contracts/review-integration/v2/schemas/start.schema.json +106 -0
  61. package/contracts/review-integration/v2/schemas/status.schema.json +104 -0
  62. package/docs/native-authority-architecture.md +18 -3
  63. package/docs/review-integration.md +175 -27
  64. package/extensions/gentle-ai.ts +647 -58
  65. package/lib/gentle-ai-binary.ts +65 -18
  66. package/lib/native-review-cli.ts +824 -56
  67. package/lib/review-candidate-view.ts +135 -1
  68. package/lib/review-compact-contract.ts +12 -3
  69. package/lib/review-consent-latch.ts +51 -0
  70. package/lib/review-correction-lifecycle.ts +170 -0
  71. package/lib/review-integration-v2.ts +1567 -0
  72. package/package.json +15 -11
  73. package/runtime/gentle-ai-binary.mjs +65 -18
  74. package/runtime/native-review-cli.mjs +808 -40
  75. package/runtime/review-integration-v2.mjs +1568 -0
  76. package/scripts/build-git-commit-transaction-runner.mjs +1 -1
  77. package/scripts/gentle-ai-installer.mjs +464 -76
  78. package/scripts/install-gentle-ai.mjs +7 -3
  79. package/scripts/test-packed-runner.mjs +3 -3
  80. package/scripts/verify-package-files.mjs +216 -64
  81. package/skills/_shared/review-ledger-contract.md +3 -1
  82. package/skills/release/SKILL.md +38 -11
  83. package/tests/devbinary/native-review-parity.devtest.ts +348 -0
  84. package/tests/gentle-ai-binary.test.ts +77 -3
  85. package/tests/gentle-ai-installer.test.ts +561 -22
  86. package/tests/gentle-ai.test.ts +2 -2
  87. package/tests/native-binary-gate.test.ts +145 -0
  88. package/tests/native-review-capability-contract.test.ts +87 -0
  89. package/tests/native-review-cli.test.ts +44 -1
  90. package/tests/native-review-consent.test.ts +128 -0
  91. package/tests/native-review-parity-runtime.test.ts +85 -29
  92. package/tests/native-review-parity.test.ts +782 -0
  93. package/tests/native-risk-evidence-parity.test.ts +91 -0
  94. package/tests/package-manifest.test.ts +80 -8
  95. package/tests/review-actor-tool-deny.test.ts +20 -5
  96. package/tests/review-candidate-view.test.ts +374 -1
  97. package/tests/review-compact-contract.test.ts +5 -0
  98. package/tests/review-consent-latch.test.ts +67 -0
  99. package/tests/review-controller-native-recovery.test.ts +180 -5
  100. package/tests/review-controller-native-routing.test.ts +632 -18
  101. package/tests/review-controller-workspace-root.test.ts +26 -6
  102. package/tests/review-controller.test.ts +1 -1
  103. package/tests/review-correction-lifecycle.test.ts +174 -0
  104. package/tests/review-integration-v2.test.ts +548 -0
  105. package/tests/runtime-harness.mjs +211 -12
  106. package/tests/support/native-binary-gate.ts +86 -0
  107. package/tests/verify-package-files.test.ts +119 -0
  108. package/lib/review-integration-v1.ts +0 -795
  109. package/runtime/review-integration-v1.mjs +0 -796
  110. package/tests/native-review-integration-v1.test.ts +0 -102
  111. package/tests/review-integration-v1.test.ts +0 -530
@@ -8,7 +8,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
8
8
  import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
9
9
  import type { NativeReviewCli } from "../lib/native-review-cli.ts";
10
10
  import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
11
- import type { ReviewStatusV1 } from "../lib/review-integration-v1.ts";
11
+ import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
12
12
 
13
13
  interface RegisteredTool {
14
14
  execute: (
@@ -90,11 +90,19 @@ function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
90
90
  };
91
91
  }
92
92
 
93
+ const UNSUPPORTED_REPAIR_ASSESSMENT: AuthorityRepairAssessmentV1 = {
94
+ schema: "gentle-ai.review-authority-repair-assessment/v1",
95
+ status: "unsupported",
96
+ counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 },
97
+ supportedOperations: ["review/complete-fix", "review/validate-fix"],
98
+ authorizationSchema: "gentle-ai.review-repair-authorization/v1",
99
+ };
100
+
93
101
  function targetStatusFixture(options: {
94
102
  applicability?: "current_target" | "unrelated";
95
- action?: ReviewStatusV1["action"];
103
+ action?: ReviewStatusV3["action"];
96
104
  lineageId?: string;
97
- } = {}): ReviewStatusV1 {
105
+ } = {}): ReviewStatusV3 {
98
106
  const applicability = options.applicability ?? "current_target";
99
107
  const action = options.action ?? (applicability === "current_target" ? "finalize" : "start");
100
108
  const lineageId = options.lineageId ?? "native-lineage";
@@ -114,15 +122,26 @@ function targetStatusFixture(options: {
114
122
  initialSnapshotIdentity: sha,
115
123
  currentSnapshotIdentity: sha,
116
124
  };
125
+ const rawRepair = {
126
+ schema: UNSUPPORTED_REPAIR_ASSESSMENT.schema,
127
+ status: UNSUPPORTED_REPAIR_ASSESSMENT.status,
128
+ counts: {
129
+ lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0,
130
+ eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0,
131
+ },
132
+ supported_operations: UNSUPPORTED_REPAIR_ASSESSMENT.supportedOperations,
133
+ authorization_schema: UNSUPPORTED_REPAIR_ASSESSMENT.authorizationSchema,
134
+ };
117
135
  const raw: Record<string, unknown> = {
118
- schema: "gentle-ai.review-integration.status/v1",
119
- contract: "gentle-ai.review-integration/v1",
136
+ schema: "gentle-ai.review-integration.status/v3",
137
+ contract: "gentle-ai.review-integration/v2",
120
138
  operation: "review.status",
121
139
  applicability,
122
140
  receipt: { status: applicability === "current_target" ? "expected_missing" : "not_applicable" },
123
141
  action,
124
142
  replayability: "not_replayable",
125
143
  target_identity: sha,
144
+ repair: rawRepair,
126
145
  projection: {
127
146
  schema: projection.schema,
128
147
  kind: projection.kind,
@@ -144,7 +163,7 @@ function targetStatusFixture(options: {
144
163
  raw.frozen = { tier: "medium", original_changed_lines: 2, correction_budget: 1 };
145
164
  }
146
165
  return {
147
- contract: "gentle-ai.review-integration/v1",
166
+ contract: "gentle-ai.review-integration/v2",
148
167
  applicability,
149
168
  ...(applicability === "current_target" ? { authority: { version: "compact-v2" as const, lineageId, state: "reviewing", generation: 1, revision: sha } } : {}),
150
169
  receipt: { status: applicability === "current_target" ? "expected_missing" : "not_applicable" },
@@ -153,6 +172,7 @@ function targetStatusFixture(options: {
153
172
  ...(applicability === "current_target" ? { frozen: { tier: "medium" as const, originalChangedLines: 2, correctionBudget: 1 } } : {}),
154
173
  targetIdentity: sha,
155
174
  projection,
175
+ repair: UNSUPPORTED_REPAIR_ASSESSMENT,
156
176
  candidates: [],
157
177
  raw,
158
178
  };
@@ -482,7 +482,7 @@ test("shipped controller and orchestrator contracts specify inspect-first compac
482
482
  assert.match(toolContract, /RESET.*reclaim.*RECOVER.*recover/s);
483
483
  assert.match(toolContract, /native-input-required.*never.*invent/is);
484
484
  assert.match(toolContract, /output.*lost|response.*lost|ambiguous.*START/is);
485
- assert.match(toolContract, /ambiguous START or FINALIZE.*target-scoped native status.*declared action/is);
485
+ assert.match(toolContract, /ambiguous START, answer-consent, or FINALIZE.*target-scoped native status.*declared action/is);
486
486
  assert.doesNotMatch(toolContract, /START throws.*lineage does not exist/is);
487
487
 
488
488
  for (const path of ["assets/orchestrator-delegation.md", "skills/gentle-ai/SKILL.md"]) {
@@ -0,0 +1,174 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ CORRECTION_OUTCOMES,
5
+ CorrectionEvidenceReplacedError,
6
+ CorrectionOutcomeError,
7
+ assertDistinctCorrectionEvidence,
8
+ resolveCorrectionStep,
9
+ } from "../lib/review-correction-lifecycle.ts";
10
+
11
+ // The provider owns the evidence directory and the budget ledger. Pi's job is
12
+ // narrower and entirely decidable from data: given a status and one captured
13
+ // evidence record, which step comes next, and is it ever allowed to reuse a
14
+ // prior evidence identity. Keeping that a pure function is what makes these
15
+ // branches testable without spawning the binary — the alternative the design
16
+ // rejected was inlining them beside the existing conditionals, where the
17
+ // single-correction invariant would be enforced by control flow instead.
18
+
19
+ const STATUS = Object.freeze({
20
+ lineageId: "review-b39d803b68a90767",
21
+ targetIdentity: "sha256:b39d803b68a90767c822c6bac906c2ff512034df2a2432d4c2d8e48f45c11b0a",
22
+ authorityRevision: "sha256:1298b2f5dd1ab7208bd7dfbf2e5802b21380617050f7ae3d7786bffcf0c6bb29",
23
+ correctionBudget: 56,
24
+ changedLinesCharged: 0,
25
+ });
26
+
27
+ function evidence(outcome: string, overrides: Record<string, unknown> = {}) {
28
+ return {
29
+ outcome,
30
+ evidenceIdentity: "sha256:a077148ccb9f1c6e8b65fbfa984309676eee3e35311caaca31d6d61f6351f9fa",
31
+ recordDigest: "sha256:a077148ccb9f1c6e8b65fbfa984309676eee3e35311caaca31d6d61f6351f9fa",
32
+ candidateTree: "0542f90ab7b6af3fe79340509ec6420f0cc9ef68",
33
+ rawPayloadSha256: "sha256:9013edfc64295a20c3f4ed41a7cf61d81a6617bb7180fd95b129cb77ff53fa00",
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ test("the outcome domain is exactly the three closed provider values", () => {
39
+ assert.deepEqual([...CORRECTION_OUTCOMES], ["passed", "verification_failed", "procedural_tooling_failed"]);
40
+ });
41
+
42
+ test("an outcome outside the closed domain is rejected with no step and no charge", () => {
43
+ for (const rejected of ["failed", "PASSED", "pass", "ok", "", "verification-failed", "skipped"]) {
44
+ assert.throws(
45
+ () => resolveCorrectionStep(STATUS, evidence(rejected)),
46
+ (error: unknown) => error instanceof CorrectionOutcomeError && /outcome/.test((error as Error).message),
47
+ `outcome ${JSON.stringify(rejected)} must be refused`,
48
+ );
49
+ }
50
+ });
51
+
52
+ test("a missing or non-string outcome is refused rather than defaulted", () => {
53
+ for (const rejected of [undefined, null, 1, true, {}, []]) {
54
+ assert.throws(() => resolveCorrectionStep(STATUS, evidence(rejected as unknown as string)), CorrectionOutcomeError);
55
+ }
56
+ });
57
+
58
+ test("passed is the only outcome that unlocks targeted validation", () => {
59
+ const step = resolveCorrectionStep(STATUS, evidence("passed"));
60
+
61
+ assert.equal(step.kind, "run-targeted-validation");
62
+ assert.equal(step.unlocksTargetedValidation, true);
63
+ assert.equal(step.transactionOpen, false);
64
+ assert.equal(step.escalation, undefined);
65
+ // The provider issues the request; Pi must re-query STATUS for it rather
66
+ // than inventing one, which is why the step names the operation instead of
67
+ // carrying a fabricated payload.
68
+ assert.equal(step.expectCaptureOperation, "external.run_targeted_validation");
69
+ });
70
+
71
+ test("verification_failed keeps the transaction open and charges nothing", () => {
72
+ const step = resolveCorrectionStep(STATUS, evidence("verification_failed"));
73
+
74
+ assert.equal(step.kind, "recapture-required");
75
+ assert.equal(step.transactionOpen, true);
76
+ assert.equal(step.unlocksTargetedValidation, false);
77
+ assert.equal(step.attemptConsumed, false);
78
+ assert.equal(step.changedLinesCharged, 0);
79
+ assert.equal(step.budgetConsumed, 0);
80
+ assert.equal(step.autoRetry, false);
81
+ });
82
+
83
+ test("verification_failed carries the prior identity as supersedes and demands a NEW capture", () => {
84
+ const first = evidence("verification_failed");
85
+ const step = resolveCorrectionStep(STATUS, first);
86
+
87
+ assert.equal(step.kind, "recapture-required");
88
+ assert.equal(step.supersedes, first.evidenceIdentity);
89
+ assert.equal(step.requiresNewCapture, true);
90
+ // It must never hand back a step that resubmits the same candidate bytes.
91
+ assert.notEqual(step.supersedes, undefined);
92
+ assert.equal(Object.hasOwn(step, "resubmitCandidate"), false);
93
+ });
94
+
95
+ test("verification_failed never consumes budget even when lines were already charged", () => {
96
+ const charged = { ...STATUS, changedLinesCharged: 40 };
97
+ const step = resolveCorrectionStep(charged, evidence("verification_failed"));
98
+
99
+ // The invariant is about what THIS outcome adds, not about resetting prior
100
+ // accounting: a failed verification must not move the needle at all.
101
+ assert.equal(step.budgetConsumed, 0);
102
+ assert.equal(step.changedLinesCharged, 40);
103
+ assert.equal(step.attemptConsumed, false);
104
+ });
105
+
106
+ test("procedural_tooling_failed escalates terminally before any retry is eligible", () => {
107
+ const step = resolveCorrectionStep(STATUS, evidence("procedural_tooling_failed"));
108
+
109
+ assert.equal(step.kind, "terminal-escalation");
110
+ assert.equal(step.retryEligible, false);
111
+ assert.equal(step.transactionOpen, false);
112
+ assert.equal(step.unlocksTargetedValidation, false);
113
+ assert.equal(typeof step.escalation, "string");
114
+ assert.match(step.escalation as string, /\S/);
115
+ // No further actor may run after a terminal escalation.
116
+ assert.equal(step.launchesReviewer, false);
117
+ assert.equal(step.launchesValidator, false);
118
+ assert.equal(step.launchesCorrection, false);
119
+ });
120
+
121
+ test("every closed outcome resolves to exactly one distinct step kind", () => {
122
+ const kinds = CORRECTION_OUTCOMES.map((outcome) => resolveCorrectionStep(STATUS, evidence(outcome)).kind);
123
+
124
+ assert.equal(new Set(kinds).size, kinds.length, "each outcome must map to its own step kind");
125
+ assert.deepEqual(kinds, ["run-targeted-validation", "recapture-required", "terminal-escalation"]);
126
+ });
127
+
128
+ test("a second capture with a distinct identity and an unchanged prior record is accepted", () => {
129
+ const prior = evidence("verification_failed");
130
+ const next = evidence("passed", { evidenceIdentity: "sha256:" + "b".repeat(64), recordDigest: "sha256:" + "b".repeat(64) });
131
+
132
+ assert.doesNotThrow(() => assertDistinctCorrectionEvidence({ prior, next, priorStillResolvable: true, priorRecordDigestNow: prior.recordDigest }));
133
+ });
134
+
135
+ test("a second capture reusing the prior evidence identity is correction-evidence-replaced", () => {
136
+ const prior = evidence("verification_failed");
137
+ const next = evidence("passed");
138
+
139
+ assert.throws(
140
+ () => assertDistinctCorrectionEvidence({ prior, next, priorStillResolvable: true, priorRecordDigestNow: prior.recordDigest }),
141
+ (error: unknown) => error instanceof CorrectionEvidenceReplacedError && /correction-evidence-replaced/.test((error as Error).message),
142
+ );
143
+ });
144
+
145
+ test("a prior evidence record whose bytes changed is correction-evidence-replaced", () => {
146
+ const prior = evidence("verification_failed");
147
+ const next = evidence("passed", { evidenceIdentity: "sha256:" + "c".repeat(64), recordDigest: "sha256:" + "c".repeat(64) });
148
+
149
+ assert.throws(
150
+ () => assertDistinctCorrectionEvidence({ prior, next, priorStillResolvable: true, priorRecordDigestNow: "sha256:" + "d".repeat(64) }),
151
+ CorrectionEvidenceReplacedError,
152
+ );
153
+ });
154
+
155
+ test("a prior evidence record that no longer resolves is correction-evidence-replaced", () => {
156
+ const prior = evidence("verification_failed");
157
+ const next = evidence("passed", { evidenceIdentity: "sha256:" + "e".repeat(64), recordDigest: "sha256:" + "e".repeat(64) });
158
+
159
+ assert.throws(
160
+ () => assertDistinctCorrectionEvidence({ prior, next, priorStillResolvable: false, priorRecordDigestNow: prior.recordDigest }),
161
+ CorrectionEvidenceReplacedError,
162
+ );
163
+ });
164
+
165
+ test("the step machine is pure: repeated calls on frozen input return equal results and mutate nothing", () => {
166
+ const input = Object.freeze(evidence("verification_failed"));
167
+ const snapshot = JSON.stringify({ status: STATUS, input });
168
+
169
+ const first = resolveCorrectionStep(STATUS, input);
170
+ const second = resolveCorrectionStep(STATUS, input);
171
+
172
+ assert.deepEqual(first, second);
173
+ assert.equal(JSON.stringify({ status: STATUS, input }), snapshot, "inputs must not be mutated");
174
+ });