gentle-pi 2.4.0 → 2.5.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 (116) hide show
  1. package/README.md +170 -12
  2. package/assets/agents/gentle-ai-worker.md +9 -0
  3. package/assets/orchestrator-delegation.md +19 -9
  4. package/assets/orchestrator.md +5 -5
  5. package/docs/delegated-verification.md +25 -0
  6. package/docs/telemetry.md +38 -0
  7. package/extensions/ask-user-choice.ts +26 -20
  8. package/extensions/codegraph-tools.ts +94 -5
  9. package/extensions/gentle-agents.ts +588 -0
  10. package/extensions/gentle-ai.ts +898 -79
  11. package/extensions/gentle-shell.ts +547 -0
  12. package/extensions/gentle-todo.ts +199 -0
  13. package/extensions/quiet-tools.ts +1 -1
  14. package/lib/agents-config.ts +318 -0
  15. package/lib/agents-history.ts +80 -0
  16. package/lib/agents-protocol.ts +429 -0
  17. package/lib/agents-runner.ts +490 -0
  18. package/lib/agents-transcript.ts +87 -0
  19. package/lib/agents-view.ts +557 -0
  20. package/lib/agents-widget.ts +222 -0
  21. package/lib/gentle-ai-renderer.ts +142 -26
  22. package/lib/native-choice-list.ts +194 -0
  23. package/lib/native-fullscreen-interaction.ts +47 -0
  24. package/lib/native-pointer-region.ts +164 -0
  25. package/lib/native-review-cli.ts +88 -12
  26. package/lib/review-candidate-view-owner.ts +177 -0
  27. package/lib/review-candidate-view.ts +127 -35
  28. package/lib/review-consent-ui.ts +65 -0
  29. package/lib/review-integration-v2.ts +58 -8
  30. package/lib/review-last-event-controller.ts +1 -0
  31. package/lib/review-relay-contract.ts +11 -0
  32. package/lib/review-repository.ts +2 -2
  33. package/lib/review-risk-assessment.ts +339 -0
  34. package/lib/review-session-standing-permission-ipc.ts +309 -0
  35. package/lib/review-session-standing-permission.ts +219 -0
  36. package/lib/shell-bar.ts +138 -0
  37. package/lib/shell-card.ts +136 -0
  38. package/lib/shell-changes-view.ts +205 -0
  39. package/lib/shell-changes.ts +210 -0
  40. package/lib/shell-gauge.ts +40 -0
  41. package/lib/shell-prompt.ts +119 -0
  42. package/lib/shell-todo.ts +280 -0
  43. package/lib/shell-usage-view.ts +76 -0
  44. package/lib/shell-usage.ts +246 -0
  45. package/lib/telemetry-trigger.ts +151 -0
  46. package/package.json +4 -4
  47. package/runtime/native-review-cli.mjs +87 -11
  48. package/runtime/review-integration-v2.mjs +58 -8
  49. package/runtime/review-relay-contract.mjs +11 -0
  50. package/runtime/review-risk-assessment.mjs +340 -0
  51. package/runtime/telemetry-trigger.mjs +152 -0
  52. package/scripts/build-runtime-modules.mjs +2 -0
  53. package/scripts/gentle-ai-installer.mjs +10 -10
  54. package/scripts/test-packed-runner.mjs +22 -0
  55. package/scripts/verify-package-files.mjs +6 -2
  56. package/skills/_shared/review-ledger-contract.md +3 -1
  57. package/tests/agents-config.test.ts +143 -0
  58. package/tests/agents-fake-child.ts +52 -0
  59. package/tests/agents-history.test.ts +54 -0
  60. package/tests/agents-protocol.test.ts +153 -0
  61. package/tests/agents-runner-process.test.ts +111 -0
  62. package/tests/agents-runner.test.ts +402 -0
  63. package/tests/agents-transcript.test.ts +30 -0
  64. package/tests/agents-view.test.ts +274 -0
  65. package/tests/agents-widget.test.ts +111 -0
  66. package/tests/ask-user-choice.test.ts +157 -3
  67. package/tests/codegraph-tools.test.ts +110 -1
  68. package/tests/devbinary/native-review-parity.devtest.ts +108 -0
  69. package/tests/fixtures/agents-process-child.mjs +23 -0
  70. package/tests/gentle-agents.test.ts +741 -0
  71. package/tests/gentle-ai-binary.test.ts +1 -1
  72. package/tests/gentle-ai-installer.test.ts +47 -47
  73. package/tests/gentle-ai-renderer.test.ts +65 -0
  74. package/tests/gentle-ai.test.ts +28 -12
  75. package/tests/gentle-card-text.ts +35 -0
  76. package/tests/gentle-shell.test.ts +527 -0
  77. package/tests/gentle-todo.test.ts +182 -0
  78. package/tests/native-choice-list.test.ts +202 -0
  79. package/tests/native-fullscreen-interaction.test.ts +125 -0
  80. package/tests/native-pointer-region.test.ts +245 -0
  81. package/tests/native-review-capability-contract.test.ts +16 -1
  82. package/tests/native-review-cli.test.ts +40 -0
  83. package/tests/native-review-consent.test.ts +91 -0
  84. package/tests/native-review-parity-runtime.test.ts +8 -2
  85. package/tests/native-review-parity.test.ts +29 -22
  86. package/tests/orchestrator-budget.test.ts +69 -0
  87. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  88. package/tests/package-manifest.test.ts +17 -6
  89. package/tests/quiet-tool-rendering.test.ts +96 -37
  90. package/tests/rdd-aware-verification-contract.test.ts +216 -0
  91. package/tests/rdd-status-line.test.ts +286 -0
  92. package/tests/review-candidate-view.test.ts +452 -6
  93. package/tests/review-contract-prompt.test.ts +3 -0
  94. package/tests/review-controller-native-recovery.test.ts +29 -4
  95. package/tests/review-controller-native-routing.test.ts +321 -4
  96. package/tests/review-controller-workspace-root.test.ts +45 -2
  97. package/tests/review-controller.test.ts +25 -0
  98. package/tests/review-host-relay-routing.test.ts +20 -4
  99. package/tests/review-integration-v2.test.ts +112 -0
  100. package/tests/review-last-event-closure.test.ts +7 -2
  101. package/tests/review-relay-contract.test.ts +26 -0
  102. package/tests/review-repository.test.ts +28 -1
  103. package/tests/review-risk-assessment.test.ts +626 -0
  104. package/tests/review-session-standing-permission-controller.test.ts +608 -0
  105. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  106. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  107. package/tests/review-session-standing-permission.test.ts +126 -0
  108. package/tests/shell-bar.test.ts +176 -0
  109. package/tests/shell-card.test.ts +118 -0
  110. package/tests/shell-changes-view.test.ts +146 -0
  111. package/tests/shell-changes.test.ts +182 -0
  112. package/tests/shell-prompt.test.ts +118 -0
  113. package/tests/shell-todo.test.ts +170 -0
  114. package/tests/shell-usage-view.test.ts +62 -0
  115. package/tests/shell-usage.test.ts +197 -0
  116. package/tests/telemetry-trigger.test.ts +349 -0
@@ -184,6 +184,44 @@ test("failure enforces exact keys, enums, and identifiers", () => {
184
184
  assert.throws(() => decodeReviewFailureV2(badCode), /code/);
185
185
  });
186
186
 
187
+ test("failure with code managed_assets_outdated decodes its continuation, degrades without one, and forbids it elsewhere", () => {
188
+ const source: JsonObject = {
189
+ schema: "gentle-ai.review-integration.failure/v2",
190
+ contract: REVIEW_INTEGRATION_CONTRACT,
191
+ operation: "review.start",
192
+ phase: "preflight",
193
+ code: "managed_assets_outdated",
194
+ message: "the managed asset set is stale",
195
+ mutation_outcome: "not_started",
196
+ authority_applicability: "current_target",
197
+ retry_safe: true,
198
+ replayability: "manual_action_required",
199
+ required_inputs: [],
200
+ next_action: "explicit-maintainer-action",
201
+ continuation: { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", stale_assets: ["orchestration/claude-code.md"] },
202
+ };
203
+ assert.deepEqual(decodeReviewFailureV2(source).continuation, { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", staleAssets: ["orchestration/claude-code.md"] });
204
+
205
+ const missingCommand = clone(source);
206
+ delete (missingCommand.continuation as JsonObject).command;
207
+ assert.throws(() => decodeReviewFailureV2(missingCommand), /command.*required|required.*command/);
208
+
209
+ // an older gentle-ai may emit managed_assets_outdated with no continuation
210
+ // at all; decode must not refuse the whole envelope for that
211
+ const missingContinuation = clone(source);
212
+ delete missingContinuation.continuation;
213
+ assert.equal(decodeReviewFailureV2(missingContinuation).continuation, undefined);
214
+
215
+ const unexpectedContinuation: JsonObject = { ...clone(source), code: "gate_scope_changed" };
216
+ assert.throws(() => decodeReviewFailureV2(unexpectedContinuation), /continuation is only valid/);
217
+
218
+ // an envelope without continuation, on a code that does not require one, is unchanged
219
+ const withoutContinuation = clone(source);
220
+ withoutContinuation.code = "gate_scope_changed";
221
+ delete withoutContinuation.continuation;
222
+ assert.equal(decodeReviewFailureV2(withoutContinuation).continuation, undefined);
223
+ });
224
+
187
225
  test("net-new decoders: consent, next-transition, and artifact-subject reject malformed payloads", () => {
188
226
  const consentSource = fixture<JsonObject>("consent.fixture.json");
189
227
  assertRequired(decodeReviewConsentV2, consentSource, ["schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path"]);
@@ -405,6 +443,41 @@ test("next_transition decodes an execute variant and rejects a stop that carries
405
443
  assert.throws(() => decodeReviewNextTransitionV3(stopWithExecute), /stop cannot carry/);
406
444
  });
407
445
 
446
+ // gentle-pi#627: gentle-ai reports a stale managed-asset set as a typed stop
447
+ // carrying the exact `gentle-ai sync` invocation that resolves it.
448
+ function managedAssetsContinuation(overrides: Partial<JsonObject> = {}): JsonObject {
449
+ return { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", stale_assets: ["orchestration/claude-code.md"], ...overrides };
450
+ }
451
+
452
+ test("next_transition stop decodes a managed_assets_outdated continuation and rejects a malformed one", () => {
453
+ const stop: JsonObject = { kind: "stop", reason_code: "managed_assets_outdated", continuation: managedAssetsContinuation() };
454
+ const decoded = decodeReviewNextTransitionV3(stop);
455
+ assert.deepEqual(decoded.continuation, { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", staleAssets: ["orchestration/claude-code.md"] });
456
+
457
+ const bare: JsonObject = { kind: "stop", reason_code: "managed_assets_outdated", continuation: { operation: "sync", command: "gentle-ai sync" } };
458
+ assert.deepEqual(decodeReviewNextTransitionV3(bare).continuation, { operation: "sync", command: "gentle-ai sync" });
459
+
460
+ const missingCommand = clone(stop);
461
+ delete (missingCommand.continuation as JsonObject).command;
462
+ assert.throws(() => decodeReviewNextTransitionV3(missingCommand), /command.*required|required.*command/);
463
+
464
+ const emptyCommand = clone(stop);
465
+ (emptyCommand.continuation as JsonObject).command = "";
466
+ assert.throws(() => decodeReviewNextTransitionV3(emptyCommand), /command/);
467
+
468
+ // an older gentle-ai may emit this stop with no continuation at all; decode
469
+ // must not refuse the whole envelope for that
470
+ const missingForReasonCode = { kind: "stop", reason_code: "managed_assets_outdated" };
471
+ assert.equal(decodeReviewNextTransitionV3(missingForReasonCode).continuation, undefined);
472
+
473
+ const unexpectedOnOtherReasonCode = { kind: "stop", reason_code: "rdd_disabled", continuation: managedAssetsContinuation() };
474
+ assert.throws(() => decodeReviewNextTransitionV3(unexpectedOnOtherReasonCode), /continuation is only valid/);
475
+
476
+ // envelopes without continuation stay unchanged
477
+ const plainStop = decodeReviewNextTransitionV3({ kind: "stop", reason_code: "rdd_disabled" });
478
+ assert.equal(plainStop.continuation, undefined);
479
+ });
480
+
408
481
  function approvedAcknowledgementTransition(): JsonObject {
409
482
  return {
410
483
  kind: "execute",
@@ -700,6 +773,40 @@ test("v5 targeted-validator collect inputs carry the exact provider-owned valida
700
773
  assert.throws(() => decodeReviewNextTransitionV3(weakenedRequest, { v5: true }), /policy_content/);
701
774
  });
702
775
 
776
+ test("status.frozen carries the optional frozen manifest digest", () => {
777
+ const current = devFixture<JsonObject>("status-v5-repository-context.captured.json");
778
+ delete current.receipt;
779
+ if (current.action === "finalize") {
780
+ current.action = "stop";
781
+ delete current.next_transition;
782
+ delete current.forecast;
783
+ }
784
+ assert.equal(typeof (current.frozen as JsonObject | undefined)?.tier, "string", "the captured status carries frozen");
785
+ const digest = `sha256:${"ab".repeat(32)}`;
786
+ (current.frozen as JsonObject).changed_path_manifest_sha256 = digest;
787
+ assert.equal(decodeReviewStatusV3(current).frozen?.changedPathManifestSha256, digest);
788
+ (current.frozen as JsonObject).changed_path_manifest_sha256 = "not-a-digest";
789
+ assert.throws(() => decodeReviewStatusV3(current), /changed_path_manifest_sha256/);
790
+ delete (current.frozen as JsonObject).changed_path_manifest_sha256;
791
+ assert.equal(decodeReviewStatusV3(current).frozen?.changedPathManifestSha256, undefined);
792
+ });
793
+
794
+ test("a capture-result collect input decodes without changed_path_manifest, which the artifact subject digest already binds", () => {
795
+ const status = fixture<JsonObject>("status.fixture.json");
796
+ const transition = clone((status.next_transition ?? {}) as JsonObject);
797
+ const inputs = ((transition.collect as JsonObject | undefined)?.inputs ?? []) as JsonObject[];
798
+ const captureInput = inputs.find((input) => input.capture_operation === "review.capture-result");
799
+ assert.ok(captureInput, "the status fixture carries a capture-result collect input");
800
+ assert.doesNotThrow(() => decodeReviewNextTransitionV3(transition, { v5: true, v6: true }), "with the manifest");
801
+ delete captureInput.changed_path_manifest;
802
+ const decoded = decodeReviewNextTransitionV3(transition, { v5: true, v6: true });
803
+ const decodedInput = decoded.collect?.inputs.find((input) => input.captureOperation === "review.capture-result");
804
+ assert.ok(decodedInput?.artifactSubject?.changedPathManifestSha256, "the digest on the artifact subject is the binding");
805
+ assert.equal(decodedInput?.changedPathManifest, undefined);
806
+ delete captureInput.artifact_subject;
807
+ assert.throws(() => decodeReviewNextTransitionV3(transition, { v5: true, v6: true }), /requires artifact_subject, base_tree, and candidate_tree/);
808
+ });
809
+
703
810
  test("status enforces authority/frozen/receipt conditionals and decodes the required repair field", () => {
704
811
  const current = devFixture<JsonObject>("status-v5-repository-context.captured.json");
705
812
  delete current.receipt;
@@ -711,6 +818,11 @@ test("status enforces authority/frozen/receipt conditionals and decodes the requ
711
818
  const decoded = decodeReviewStatusV3(current);
712
819
  assert.equal(decoded.repair.status, "unsupported");
713
820
  assert.equal(decoded.authority?.state, "reviewing");
821
+ for (const action of ["collect", "execute"]) {
822
+ const projected = clone(current);
823
+ projected.action = action;
824
+ assert.equal(decodeReviewStatusV3(projected).action, action, "a live transaction's root action names its mandated transition kind");
825
+ }
714
826
 
715
827
  const missingAuthority = clone(current);
716
828
  delete missingAuthority.authority;
@@ -331,7 +331,7 @@ test("terminal capture closes directly, nonterminal capture does not auto-follow
331
331
  assert.equal(statusCalls, 2, "only initial STATUS plus one ambiguity reconciliation");
332
332
  });
333
333
 
334
- test("unknown-capture reconciliation validates the target-scoped lineage without replay", async () => {
334
+ test("unknown-capture reconciliation preserves agentless legacy callers and forwards an explicit Pi agent", async () => {
335
335
  const calls: Array<Record<string, unknown>> = [];
336
336
  const native = {
337
337
  targetStatus: async (request: Record<string, unknown>) => {
@@ -342,7 +342,12 @@ test("unknown-capture reconciliation validates the target-scoped lineage without
342
342
  } as unknown as NativeReviewCli;
343
343
  const result = await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA });
344
344
  await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, { baseRef: "refs/heads/main", committedOnly: true });
345
- assert.deepEqual(calls, [{ cwd: "/repo", lineageId: "reconcile-lineage" }, { cwd: "/repo", lineageId: "reconcile-lineage", baseRef: "refs/heads/main", committedOnly: true }]);
345
+ await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, { agent: "pi" });
346
+ assert.deepEqual(calls, [
347
+ { cwd: "/repo", lineageId: "reconcile-lineage" },
348
+ { cwd: "/repo", lineageId: "reconcile-lineage", baseRef: "refs/heads/main", committedOnly: true },
349
+ { cwd: "/repo", lineageId: "reconcile-lineage", agent: "pi" },
350
+ ]);
346
351
  assert.equal(result.targetIdentity, SHA);
347
352
  let launches = 0;
348
353
  const strictNative = new nativeReviewCliModule.NativeReviewCliV216(async () => { launches += 1; throw new Error("must not launch"); }, "/package/.gentle-ai/gentle-ai");
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
5
+ import { declareReviewRelayHandshake, GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "../lib/review-relay-contract.ts";
6
+
7
+ // gentle-pi#550: the session shell must inherit the relay handshake, so a
8
+ // `gentle-ai review ... --agent pi` typed in a shell tool is eligible without
9
+ // the operator reading package source for the value.
10
+
11
+ test("declareReviewRelayHandshake exports the exact contract and reports whether it changed anything", () => {
12
+ const env: NodeJS.ProcessEnv = {};
13
+ assert.equal(declareReviewRelayHandshake(env), true);
14
+ assert.equal(env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
15
+ assert.equal(declareReviewRelayHandshake(env), false, "already declared");
16
+ const stale: NodeJS.ProcessEnv = { [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: "gentle-pi.review-relay/v0" };
17
+ assert.equal(declareReviewRelayHandshake(stale), true, "a wrong value is replaced: this extension is the host");
18
+ assert.equal(stale[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
19
+ });
20
+
21
+ test("loading the extension declares the handshake in the session environment", () => {
22
+ const processEnv: NodeJS.ProcessEnv = { PATH: "/bin" };
23
+ createGentleAiExtension({ nativeReviewCli: null, processEnv })({ on() {}, registerTool() {}, registerCommand() {} } as unknown as ExtensionAPI);
24
+ assert.equal(processEnv[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
25
+ assert.equal(processEnv.PATH, "/bin");
26
+ });
@@ -4,7 +4,7 @@ import { cpSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import test from "node:test";
7
- import { resolveRepositoryAuthorityV1, setReviewRepositoryIdentityRetryHookForTesting } from "../lib/review-repository.ts";
7
+ import { resolveRepositoryAuthorityV1, reviewGitEnvironment, setReviewRepositoryIdentityRetryHookForTesting } from "../lib/review-repository.ts";
8
8
  import { REVIEW_MODE, ReviewTransactionStore, createReviewState, setReviewMutationLockPlatformForTesting } from "../lib/review-transaction.ts";
9
9
  import { REVIEW_LENS, REVIEW_ROUTE } from "../lib/review-triggers.ts";
10
10
  import { qualifiedReviewLockPlatform, testSnapshot } from "./review-test-fixtures.ts";
@@ -49,6 +49,33 @@ function addOrphanRoot(root: string, branch: string, file: string): void {
49
49
  git(root, "checkout", "main");
50
50
  }
51
51
 
52
+ test("review Git environment uses a Git-compatible null config path on Windows", () => {
53
+ const environment = reviewGitEnvironment();
54
+ assert.equal(environment.GIT_CONFIG_GLOBAL, "/dev/null");
55
+ assert.equal(environment.GIT_CONFIG_SYSTEM, "/dev/null");
56
+ });
57
+
58
+ test("review Git environment runs a real Git authority probe", (t) => {
59
+ const root = repository(t);
60
+ const commonDirectory = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
61
+ cwd: root,
62
+ encoding: "utf8",
63
+ env: reviewGitEnvironment(),
64
+ }).trim();
65
+ assert.equal(resolve(commonDirectory), resolve(git(root, "rev-parse", "--path-format=absolute", "--git-common-dir")));
66
+ });
67
+
68
+ test("review Git environment rejects inherited unsafe Git overrides", () => {
69
+ const prior = process.env.GIT_DIR;
70
+ try {
71
+ process.env.GIT_DIR = "/unsafe";
72
+ assert.throws(() => reviewGitEnvironment(), /REVIEW_GIT_ENV_UNSAFE/);
73
+ } finally {
74
+ if (prior === undefined) delete process.env.GIT_DIR;
75
+ else process.env.GIT_DIR = prior;
76
+ }
77
+ });
78
+
52
79
  test("a Windows drive-letter Git common directory resolves repository authority", { skip: process.platform !== "win32" }, (t) => {
53
80
  const root = repository(t);
54
81
  const commonDirectory = git(root, "rev-parse", "--path-format=absolute", "--git-common-dir");