gentle-pi 2.1.2 → 2.3.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 (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -13,7 +13,7 @@ function scratchDir(prefix: string): string {
13
13
 
14
14
  async function runRetiredOperation(parameters: Record<string, unknown>): Promise<Record<string, unknown>> {
15
15
  const cwd = scratchDir("gentle-pi-retired-ops-");
16
- return await __testing.executeReviewControllerOperation(parameters, cwd, new Map(), null);
16
+ return await __testing.executeReviewControllerOperation(parameters, cwd, null);
17
17
  }
18
18
 
19
19
  function assertRetiredEnvelope(details: Record<string, unknown>, operation: string): void {
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import test from "node:test";
7
7
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
- import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
8
+ import { __testing, 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
11
  import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
@@ -55,8 +55,13 @@ function context(cwd: string): ExtensionContext {
55
55
  }
56
56
 
57
57
  function repository(t: test.TestContext, prefix = "gentle-pi-workspace-root-"): string {
58
- const cwd = mkdtempSync(join(tmpdir(), prefix));
59
- t.after(() => rmSync(cwd, { recursive: true, force: true }));
58
+ const cwd = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
59
+ t.after(() => {
60
+ if (process.platform !== "win32") {
61
+ try { execFileSync("chmod", ["-R", "u+w", cwd], { stdio: "ignore" }); } catch { /* best effort */ }
62
+ }
63
+ rmSync(cwd, { recursive: true, force: true });
64
+ });
60
65
  execFileSync("git", ["init", "-b", "main"], { cwd });
61
66
  writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
62
67
  execFileSync("git", ["add", "."], { cwd });
@@ -65,7 +70,7 @@ function repository(t: test.TestContext, prefix = "gentle-pi-workspace-root-"):
65
70
  }
66
71
 
67
72
  function addWorktree(t: test.TestContext, cwd: string, branch: string): string {
68
- const parent = mkdtempSync(join(tmpdir(), "gentle-pi-workspace-worktrees-"));
73
+ const parent = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-workspace-worktrees-")));
69
74
  t.after(() => {
70
75
  try { execFileSync("git", ["worktree", "remove", "--force", join(parent, branch)], { cwd }); } catch {}
71
76
  rmSync(parent, { recursive: true, force: true });
@@ -78,14 +83,11 @@ function addWorktree(t: test.TestContext, cwd: string, branch: string): string {
78
83
  function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
79
84
  return {
80
85
  start: async () => ({ lineageId: "native-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 7, correctionBudget: 4, action: "created", lensesRequired: true }),
81
- finalize: async () => ({ lineageId: "native-lineage", state: "approved", action: "approved", storeRevision: "r1", receiptPath: "/opaque/receipt" }),
82
- validate: async () => { throw new Error("validate is not expected in workspace-root tests"); },
83
- bindSdd: async () => { throw new Error("bind-sdd is not expected in workspace-root tests"); },
84
86
  sddStatus: async () => ({ ready: false }),
85
87
  reviewStatus: async () => ({ schema: "gentle-ai.review-authority-status/v1", repository: "/repo", complete: true, authoritative: true, status: "clean", entries: [], locks: [], diagnostics: [], raw: { schema: "gentle-ai.review-authority-status/v1", operation: "review/status", repository: "/repo", complete: true, authoritative: true, status: "clean", entries: [], locks: [], diagnostics: [] } }),
86
88
  targetStatus: async (request) => request.lineageId === undefined
87
89
  ? candidateStartTargetStatus(request)
88
- : targetStatusFixture({ lineageId: request.lineageId }),
90
+ : candidateTargetStatus(request, request.lineageId),
89
91
  ...overrides,
90
92
  };
91
93
  }
@@ -102,17 +104,20 @@ function targetStatusFixture(options: {
102
104
  applicability?: "current_target" | "unrelated";
103
105
  action?: ReviewStatusV3["action"];
104
106
  lineageId?: string;
107
+ authorityState?: NonNullable<ReviewStatusV3["authority"]>["state"];
105
108
  baseTree?: string;
106
109
  currentCandidateTree?: string;
107
110
  paths?: readonly string[];
111
+ intendedUntracked?: readonly string[];
108
112
  } = {}): ReviewStatusV3 {
109
113
  const applicability = options.applicability ?? "current_target";
110
- const action = options.action ?? (applicability === "current_target" ? "finalize" : "start");
111
- const lineageId = options.lineageId ?? "native-lineage";
114
+ const action = options.action ?? (applicability === "current_target" ? "stop" : "start"); const lineageId = options.lineageId ?? "native-lineage";
115
+ const authorityState = options.authorityState ?? "reviewing";
112
116
  const sha = `sha256:${"a".repeat(64)}`;
113
117
  const tree = options.currentCandidateTree ?? "b".repeat(40);
114
118
  const baseTree = options.baseTree ?? tree;
115
119
  const paths = options.paths ?? ["app.ts"];
120
+ const intendedUntracked = options.intendedUntracked ?? [];
116
121
  const projection = {
117
122
  schema: "gentle-ai.review-integration.projection/v1" as const,
118
123
  kind: "current-changes" as const,
@@ -122,7 +127,7 @@ function targetStatusFixture(options: {
122
127
  currentCandidateTree: tree,
123
128
  pathsDigest: sha,
124
129
  paths,
125
- intendedUntracked: [],
130
+ intendedUntracked,
126
131
  intendedUntrackedProof: sha,
127
132
  initialSnapshotIdentity: sha,
128
133
  currentSnapshotIdentity: sha,
@@ -156,7 +161,7 @@ function targetStatusFixture(options: {
156
161
  current_candidate_tree: tree,
157
162
  paths_digest: sha,
158
163
  paths,
159
- intended_untracked: [],
164
+ intended_untracked: intendedUntracked,
160
165
  intended_untracked_proof: sha,
161
166
  initial_snapshot_identity: sha,
162
167
  current_snapshot_identity: sha,
@@ -164,13 +169,13 @@ function targetStatusFixture(options: {
164
169
  candidates: [],
165
170
  };
166
171
  if (applicability === "current_target") {
167
- raw.authority = { version: "compact-v2", lineage_id: lineageId, state: "reviewing", generation: 1, revision: sha };
172
+ raw.authority = { version: "compact-v2", lineage_id: lineageId, state: authorityState, generation: 1, revision: sha };
168
173
  raw.frozen = { tier: "medium", original_changed_lines: 2, correction_budget: 1 };
169
174
  }
170
175
  return {
171
176
  contract: "gentle-ai.review-integration/v2",
172
177
  applicability,
173
- ...(applicability === "current_target" ? { authority: { version: "compact-v2" as const, lineageId, state: "reviewing", generation: 1, revision: sha } } : {}),
178
+ ...(applicability === "current_target" ? { authority: { version: "compact-v2" as const, lineageId, state: authorityState, generation: 1, revision: sha } } : {}),
174
179
  receipt: { status: applicability === "current_target" ? "expected_missing" : "not_applicable" },
175
180
  action,
176
181
  replayability: "not_replayable",
@@ -189,6 +194,7 @@ function candidateStartTargetStatus(request: Parameters<NonNullable<NativeReview
189
194
  candidate = new CandidateViewRegistry().create({
190
195
  contributorRoot: request.cwd,
191
196
  ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef, committedOnly: true }),
197
+ ...(request.intendedUntracked === undefined ? {} : { intendedUntracked: request.intendedUntracked }),
192
198
  });
193
199
  return targetStatusFixture({
194
200
  applicability: "unrelated",
@@ -196,6 +202,22 @@ function candidateStartTargetStatus(request: Parameters<NonNullable<NativeReview
196
202
  baseTree: candidate.baseTree,
197
203
  currentCandidateTree: candidate.candidateTree,
198
204
  paths: candidate.paths,
205
+ intendedUntracked: request.intendedUntracked,
206
+ });
207
+ } finally {
208
+ candidate?.cleanup();
209
+ }
210
+ }
211
+
212
+ function candidateTargetStatus(request: Parameters<NonNullable<NativeReviewCli["targetStatus"]>>[0], lineageId: string): ReviewStatusV3 {
213
+ let candidate: ReturnType<CandidateViewRegistry["create"]> | undefined;
214
+ try {
215
+ candidate = new CandidateViewRegistry().create({ contributorRoot: request.cwd });
216
+ return targetStatusFixture({
217
+ lineageId,
218
+ baseTree: candidate.baseTree,
219
+ currentCandidateTree: candidate.candidateTree,
220
+ paths: candidate.paths,
199
221
  });
200
222
  } finally {
201
223
  candidate?.cleanup();
@@ -255,39 +277,164 @@ test("START freezes the candidate from the explicit workspace root and returns t
255
277
  candidateViews.cleanup(view.token);
256
278
  });
257
279
 
258
- test("absent workspaceRoot keeps the session-cwd flow and still reports the actor binding envelope", async (t) => {
259
- const sessionCwd = repository(t);
260
- writeFileSync(join(sessionCwd, "app.ts"), "export const value = 3;\n");
280
+ test("an omitted workspaceRoot canonicalizes a nested same-worktree session before supported operations", async (t) => {
281
+ const root = repository(t);
282
+ const sessionCwd = join(root, "nested");
283
+ mkdirSync(sessionCwd);
284
+ writeFileSync(join(root, "app.ts"), "export const value = 3;\n");
261
285
  const candidateViews = new CandidateViewRegistry();
286
+ const targetStatusCwds: string[] = [];
262
287
  const startRequests: Parameters<NativeReviewCli["start"]>[0][] = [];
263
288
  const { controller } = runtime(fakeNative({
289
+ targetStatus: async (request) => { targetStatusCwds.push(request.cwd); return request.lineageId === undefined ? candidateStartTargetStatus(request) : candidateTargetStatus(request, request.lineageId); },
264
290
  start: async (request) => {
265
291
  startRequests.push(request);
266
292
  return { lineageId: "session-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
267
293
  },
268
294
  }), candidateViews);
269
- const started = await controller.execute("start-session", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(sessionCwd));
270
- const details = started.details as {
271
- workspace_root: string;
272
- actor_binding: { workspace_root: string; candidate_root: string; candidate_paths: readonly string[] };
273
- };
274
- assert.equal(details.workspace_root, sessionCwd);
275
- assert.equal(details.actor_binding.workspace_root, sessionCwd);
295
+ const ctx = context(sessionCwd);
296
+ await controller.execute("inspect-session", { operation: "inspect" }, undefined, undefined, ctx);
297
+ const started = await controller.execute("start-session", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, ctx);
298
+ const status = await controller.execute("status-session", { operation: "status", lineageId: "session-lineage" }, undefined, undefined, ctx);
299
+ const details = started.details as { workspace_root: string; actor_binding: { workspace_root: string; candidate_root: string; candidate_paths: readonly string[] } };
300
+ assert.equal(details.workspace_root, root);
301
+ assert.equal(details.actor_binding.workspace_root, root);
302
+ assert.equal((status.details as { workspace_root: string }).workspace_root, root);
276
303
  assert.deepEqual(details.actor_binding.candidate_paths, ["app.ts"]);
277
- assert.deepEqual(startRequests, [{ cwd: sessionCwd, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" }]);
304
+ assert.deepEqual(startRequests, [{ cwd: root, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" }]);
278
305
  const view = candidateViews.resolveForLens("session-lineage", "review-reliability");
279
306
  assert.equal(details.actor_binding.candidate_root, view.root);
307
+ assert.ok(targetStatusCwds.every((cwd) => cwd === root), JSON.stringify(targetStatusCwds));
280
308
  candidateViews.cleanup(view.token);
281
309
  });
282
310
 
283
- test("workspaceRoot fails closed before any native call for non-worktree and foreign targets", async (t) => {
311
+ test("an explicit nested foreign workspace root canonically owns inspect, status, and START", async (t) => {
312
+ const sessionCwd = repository(t, "gentle-pi-session-a-");
313
+ const target = repository(t, "gentle-pi-target-b-");
314
+ const nested = join(target, "nested");
315
+ mkdirSync(nested);
316
+ writeFileSync(join(target, "app.ts"), "export const value = 2; // target B\n");
317
+ const root = realpathSync(target);
318
+ const targetStatusCwds: string[] = [];
319
+ const targetStatusRequests: Parameters<NonNullable<NativeReviewCli["targetStatus"]>>[0][] = [];
320
+ const startCwds: string[] = [];
321
+ const candidateViews = new CandidateViewRegistry();
322
+ let initialCrossRootStatus = true;
323
+ const { controller } = runtime(fakeNative({
324
+ targetStatus: async (request) => {
325
+ targetStatusCwds.push(request.cwd);
326
+ targetStatusRequests.push(request);
327
+ if (request.lineageId === "cross-root-lineage" && initialCrossRootStatus) {
328
+ initialCrossRootStatus = false;
329
+ return request.untrackedScope === "select"
330
+ ? targetStatusFixture({ lineageId: request.lineageId, authorityState: "correction_required" })
331
+ : targetStatusFixture({ applicability: "unrelated", action: "start" });
332
+ }
333
+ return request.lineageId === undefined
334
+ ? candidateStartTargetStatus(request)
335
+ : candidateTargetStatus(request, request.lineageId);
336
+ },
337
+ start: async (request) => {
338
+ startCwds.push(request.cwd);
339
+ return { lineageId: "cross-root-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
340
+ },
341
+ }), candidateViews);
342
+
343
+ const digest = `sha256:${"b".repeat(64)}`;
344
+ const intendedUntracked = ["selected-b.ts", "selected-a.ts"];
345
+ const inspect = await controller.execute("inspect-target-b", { operation: "inspect", workspaceRoot: nested }, undefined, undefined, context(sessionCwd));
346
+ const status = await controller.execute("status-target-b", { operation: "status", lineageId: "cross-root-lineage", workspaceRoot: nested, input: JSON.stringify({ untrackedScope: "select", expectedUntrackedInventory: digest, intendedUntracked }) }, undefined, undefined, context(sessionCwd));
347
+ const started = await controller.execute("start-target-b", { operation: "start", workspaceRoot: nested, input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(sessionCwd));
348
+
349
+ for (const result of [inspect, status, started]) {
350
+ assert.equal((result.details as { workspace_root?: string }).workspace_root, root);
351
+ }
352
+ const statusDetails = status.details as { status?: string; result?: { authority?: { state?: string } } };
353
+ assert.equal(statusDetails.status, "blocked");
354
+ assert.equal(statusDetails.result?.authority?.state, "correction_required");
355
+ assert.deepEqual(targetStatusRequests[1], { cwd: root, lineageId: "cross-root-lineage", untrackedScope: "select", expectedUntrackedInventory: digest, intendedUntracked, agent: "pi" });
356
+ assert.ok(targetStatusCwds.every((cwd) => cwd === root), JSON.stringify(targetStatusCwds));
357
+ assert.deepEqual(startCwds, [root]);
358
+ candidateViews.cleanup(candidateViews.resolveForLens("cross-root-lineage", "review-reliability").token);
359
+ });
360
+
361
+
362
+ test("STATUS relays exclude selection and rejects malformed input before native status", async (t) => {
363
+ const cwd = repository(t);
364
+ const requests: Parameters<NonNullable<NativeReviewCli["targetStatus"]>>[0][] = [];
365
+ const { controller } = runtime(fakeNative({
366
+ targetStatus: async (request) => {
367
+ requests.push(request);
368
+ return targetStatusFixture({ lineageId: request.lineageId });
369
+ },
370
+ }));
371
+ const digest = `sha256:${"c".repeat(64)}`;
372
+ const excluded = await controller.execute("status-exclude", { operation: "status", lineageId: "excluded-lineage", input: JSON.stringify({ untrackedScope: "exclude", expectedUntrackedInventory: digest, intendedUntracked: [] }) }, undefined, undefined, context(cwd));
373
+ assert.equal((excluded.details as { status?: string }).status, "blocked");
374
+ assert.deepEqual(requests, [{ cwd, lineageId: "excluded-lineage", untrackedScope: "exclude", expectedUntrackedInventory: digest, intendedUntracked: [], agent: "pi" }]);
375
+
376
+ for (const [input, reason] of [
377
+ [{}, "untracked-selection-invalid"],
378
+ [{ untrackedScope: "select", expectedUntrackedInventory: digest, intendedUntracked: [] }, "untracked-selection-invalid"],
379
+ [{ untrackedScope: "select" }, "untracked-selection-invalid"],
380
+ [{ untrackedScope: "exclude", expectedUntrackedInventory: digest, intendedUntracked: [], unexpected: true }, "unknown-field"],
381
+ ] as const) {
382
+ const rejected = await controller.execute("status-invalid", { operation: "status", input: JSON.stringify(input) }, undefined, undefined, context(cwd));
383
+ assert.equal((rejected.details as { reason?: string }).reason, reason);
384
+ }
385
+ assert.equal(requests.length, 1);
386
+ });
387
+
388
+ test("an explicit foreign workspace root works when the Pi session cwd is not a Git repository", async (t) => {
389
+ const target = repository(t, "gentle-pi-target-b-non-git-session-");
390
+ const nested = join(target, "nested");
391
+ mkdirSync(nested);
392
+ const nonGit = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-non-git-session-")));
393
+ t.after(() => rmSync(nonGit, { recursive: true, force: true }));
394
+ const observed: string[] = [];
395
+ const { controller } = runtime(fakeNative({
396
+ targetStatus: async (request) => {
397
+ observed.push(request.cwd);
398
+ return targetStatusFixture();
399
+ },
400
+ }));
401
+ const result = await controller.execute("inspect-target-from-non-git", { operation: "inspect", workspaceRoot: nested }, undefined, undefined, context(nonGit));
402
+ assert.equal((result.details as { workspace_root?: string }).workspace_root, realpathSync(target));
403
+ assert.deepEqual(observed, [realpathSync(target)]);
404
+ });
405
+
406
+ test("omitted workspaceRoot fails closed for the same lineage bound to two target repositories", async (t) => {
407
+ const rootA = repository(t, "gentle-pi-ambiguous-a-");
408
+ const rootB = repository(t, "gentle-pi-ambiguous-b-");
409
+ const candidateViews = new CandidateViewRegistry();
410
+ const viewA = candidateViews.create({ contributorRoot: rootA });
411
+ const viewB = candidateViews.create({ contributorRoot: rootB });
412
+ candidateViews.bindCurrent({ token: viewA.token, lineageId: "shared-lineage", selectedLenses: ["review-reliability"] });
413
+ candidateViews.bindCurrent({ token: viewB.token, lineageId: "shared-lineage", selectedLenses: ["review-reliability"] });
414
+ let nativeCalls = 0;
415
+ const { controller } = runtime(fakeNative({
416
+ targetStatus: async (request) => {
417
+ nativeCalls += 1;
418
+ return targetStatusFixture({ lineageId: request.lineageId });
419
+ },
420
+ }), candidateViews);
421
+ await assert.rejects(
422
+ controller.execute("ambiguous-omission", { operation: "status", lineageId: "shared-lineage" }, undefined, undefined, context(rootA)),
423
+ /workspaceRoot/,
424
+ );
425
+ assert.equal(nativeCalls, 0);
426
+ const explicit = await controller.execute("explicit-b", { operation: "status", lineageId: "shared-lineage", workspaceRoot: rootB }, undefined, undefined, context(rootA));
427
+ assert.equal((explicit.details as { workspace_root?: string }).workspace_root, realpathSync(rootB));
428
+ assert.equal(nativeCalls, 1);
429
+ candidateViews.cleanupTerminal("shared-lineage", "approved", rootA);
430
+ candidateViews.cleanupTerminal("shared-lineage", "approved", rootB);
431
+ });
432
+
433
+ test("workspaceRoot fails closed before any native call for invalid target paths", async (t) => {
284
434
  const sessionCwd = repository(t);
285
435
  const worktree = addWorktree(t, sessionCwd, "feat-guard");
286
- const foreign = repository(t, "gentle-pi-foreign-root-");
287
- const nonGit = mkdtempSync(join(tmpdir(), "gentle-pi-non-git-"));
436
+ const nonGit = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-non-git-")));
288
437
  t.after(() => rmSync(nonGit, { recursive: true, force: true }));
289
- const nested = join(worktree, "nested");
290
- mkdirSync(nested);
291
438
  const filePath = join(worktree, "app.ts");
292
439
  let nativeCalls = 0;
293
440
  const counting = fakeNative({
@@ -296,16 +443,13 @@ test("workspaceRoot fails closed before any native call for non-worktree and for
296
443
  reviewStatus: async () => { nativeCalls += 1; throw new Error("native review status must not run"); },
297
444
  });
298
445
  const { controller } = runtime(counting);
299
- const rejected: Array<{ label: string; workspaceRoot: string; ctx: string }> = [
300
- { label: "non-git directory", workspaceRoot: nonGit, ctx: sessionCwd },
301
- { label: "missing directory", workspaceRoot: join(nonGit, "missing"), ctx: sessionCwd },
302
- { label: "file path", workspaceRoot: filePath, ctx: sessionCwd },
303
- { label: "relative path", workspaceRoot: "relative/worktree", ctx: sessionCwd },
304
- { label: "worktree subdirectory", workspaceRoot: nested, ctx: sessionCwd },
305
- { label: "foreign repository", workspaceRoot: foreign, ctx: sessionCwd },
306
- { label: "session outside a repository", workspaceRoot: worktree, ctx: nonGit },
446
+ const rejected: Array<{ label: string; workspaceRoot: string }> = [
447
+ { label: "non-git directory", workspaceRoot: nonGit },
448
+ { label: "missing directory", workspaceRoot: join(nonGit, "missing") },
449
+ { label: "file path", workspaceRoot: filePath },
450
+ { label: "relative path", workspaceRoot: "relative/worktree" },
307
451
  ];
308
- for (const { label, workspaceRoot, ctx } of rejected) {
452
+ for (const { label, workspaceRoot } of rejected) {
309
453
  for (const operation of ["inspect", "start", "status"] as const) {
310
454
  await assert.rejects(
311
455
  controller.execute(`${operation}-${label}`, {
@@ -313,7 +457,7 @@ test("workspaceRoot fails closed before any native call for non-worktree and for
313
457
  ...(operation === "start" ? { input: JSON.stringify({ mode: "ordinary" }) } : {}),
314
458
  ...(operation === "status" ? { lineageId: "native-lineage" } : {}),
315
459
  workspaceRoot,
316
- }, undefined, undefined, context(ctx)),
460
+ }, undefined, undefined, context(sessionCwd)),
317
461
  /workspaceRoot/,
318
462
  `${operation} must fail closed for ${label}`,
319
463
  );
@@ -322,40 +466,62 @@ test("workspaceRoot fails closed before any native call for non-worktree and for
322
466
  assert.equal(nativeCalls, 0);
323
467
  });
324
468
 
325
- test("workspaceRoot rejection reports both roots for a shared-common-dir mismatch", async (t) => {
469
+ function providerRoleBinding(lineageId: string) {
470
+ return {
471
+ name: "provider_targeted_validator",
472
+ schema: "https://gentle-ai.dev/schema/review/targeted-validator/v1",
473
+ captureOperation: "review.capture-validation",
474
+ arguments: [
475
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
476
+ { name: "target", value: `sha256:${"a".repeat(64)}`, token: `--target=sha256:${"a".repeat(64)}` },
477
+ { name: "agent", value: "pi", token: "--agent=pi" },
478
+ { name: "execute", value: "true", token: "--execute=true" },
479
+ ],
480
+ };
481
+ }
482
+
483
+ test("same-session START binding migrates to one validation capture without a FINALIZE operation", async (t) => {
326
484
  const sessionCwd = repository(t);
327
- const foreign = repository(t, "gentle-pi-foreign-report-");
328
- const { controller } = runtime(fakeNative());
329
- await assert.rejects(
330
- controller.execute("inspect-foreign", { operation: "inspect", workspaceRoot: foreign }, undefined, undefined, context(sessionCwd)),
331
- (error: Error) => {
332
- assert.match(error.message, /workspaceRoot/);
333
- assert.ok(error.message.includes(realpathSync(foreign)));
334
- assert.ok(error.message.includes(sessionCwd));
335
- return true;
485
+ const worktree = addWorktree(t, sessionCwd, "feat-last-event");
486
+ writeFileSync(join(worktree, "app.ts"), "export const value = 2;\n");
487
+ const candidateViews = new CandidateViewRegistry();
488
+ const lineageId = "workspace-last-event";
489
+ const binding = providerRoleBinding(lineageId);
490
+ let captureCalls = 0;
491
+ const native = fakeNative({
492
+ targetStatus: async (request) => request.lineageId === undefined
493
+ ? candidateStartTargetStatus(request)
494
+ : ({ ...candidateTargetStatus(request, lineageId), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [binding] } } } as unknown as ReviewStatusV3),
495
+ start: async () => ({ lineageId, state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true }),
496
+ captureProviderRole: async (request) => {
497
+ captureCalls += 1;
498
+ assert.equal(request.captureOperation, "review.capture-validation");
499
+ return { schema: "gentle-ai.review-last-event-closure/v1", operation: "review.capture-validation", lineageId, state: "approved", storeRevision: `sha256:${"a".repeat(64)}` };
336
500
  },
337
- );
501
+ });
502
+ const { controller } = runtime(native, candidateViews);
503
+ await controller.execute("start", { operation: "start", workspaceRoot: worktree, input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(sessionCwd));
504
+ const captured = await __testing.executeReviewCaptureOperation({ lineageId, workspaceRoot: worktree, collectBinding: JSON.stringify(binding) }, sessionCwd, native, undefined, candidateViews);
505
+ assert.equal(captured.outcome, "native-last-event-closure");
506
+ assert.equal(captured.status, "closed");
507
+ assert.equal(captureCalls, 1);
508
+ candidateViews.cleanupTerminal(lineageId, "approved", realpathSync(worktree));
338
509
  });
339
510
 
340
- test("FINALIZE fails closed when the frozen projection belongs to a different workspace than requested", async (t) => {
511
+ test("an explicit linked-worktree root owns STATUS collect capture over the session candidate root", async (t) => {
341
512
  const sessionCwd = repository(t);
342
- const worktree = addWorktree(t, sessionCwd, "feat-finalize");
343
- writeFileSync(join(worktree, "app.ts"), "export const value = 4;\n");
344
- const candidateViews = new CandidateViewRegistry();
345
- let finalizes = 0;
346
- const { controller } = runtime(fakeNative({
347
- start: async () => ({ lineageId: "finalize-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true }),
348
- finalize: async () => {
349
- finalizes += 1;
350
- return { lineageId: "finalize-lineage", state: "approved", action: "approved", storeRevision: "r1" };
513
+ const worktree = addWorktree(t, sessionCwd, "feat-explicit-capture");
514
+ const lineageId = "explicit-worktree-capture";
515
+ const binding = providerRoleBinding(lineageId);
516
+ const observedRoots: string[] = [];
517
+ const native = {
518
+ targetStatus: async (request: { cwd: string }) => {
519
+ observedRoots.push(request.cwd);
520
+ return { ...targetStatusFixture({ lineageId }), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [binding] } } } as unknown as ReviewStatusV3;
351
521
  },
352
- }), candidateViews);
353
- await controller.execute("start-finalize-b", { operation: "start", workspaceRoot: worktree, input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(sessionCwd));
354
- const input = JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } });
355
- const mismatch = await controller.execute("finalize-wrong-root", { operation: "finalize", lineageId: "finalize-lineage", input }, undefined, undefined, context(sessionCwd));
356
- assert.equal((mismatch.details as { outcome?: string }).outcome, "native-operation-failed");
357
- assert.equal(finalizes, 0);
358
- const matched = await controller.execute("finalize-right-root", { operation: "finalize", lineageId: "finalize-lineage", workspaceRoot: worktree, input }, undefined, undefined, context(sessionCwd));
359
- assert.equal((matched.details as { result?: { state?: string } }).result?.state, "approved");
360
- assert.equal(finalizes, 1);
522
+ captureProviderRole: async () => ({ schema: "gentle-ai.review-last-event-closure/v1", operation: "review.capture-validation", lineageId, state: "approved", storeRevision: `sha256:${"a".repeat(64)}` }),
523
+ } as unknown as NativeReviewCli;
524
+ const result = await __testing.executeReviewCaptureOperation({ lineageId, workspaceRoot: worktree, collectBinding: JSON.stringify(binding) }, sessionCwd, native);
525
+ assert.equal(result.outcome, "native-last-event-closure");
526
+ assert.deepEqual(observedRoots, [realpathSync(worktree)]);
361
527
  });