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
@@ -0,0 +1,235 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Native SDD Runtime Attempt Authority contract tests (issue #256 track 3).
8
+ //
9
+ // Locks the Pi-owned lazy-loaded orchestration/status contract that ports the
10
+ // released Gentle AI v2.4.0 compact SDD attempt ledger authority. Pi must NOT
11
+ // implement a local attempt runtime; it must route every runtime-bearing
12
+ // sdd-apply/sdd-verify/remediation launch through the provider compact CLI.
13
+ //
14
+ // Both assets are Markdown contracts consumed by orchestrators; these tests
15
+ // read the real repo files and assert the essential semantics, not merely
16
+ // keyword presence. Negative controls guard against caller-authored
17
+ // counters, OpenSpec/Engram attempt ledgers, automatic reset, and legacy
18
+ // status|begin|finish|reset normal-flow routing.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const ROOT = join(import.meta.dirname, "..");
22
+ const WORKFLOW = "assets/sdd-orchestrator-workflow.md";
23
+ const STATUS_CONTRACT = "assets/support/sdd-status-contract.md";
24
+ const SECTION = "Native Runtime Attempt Authority";
25
+
26
+ function read(path: string): string {
27
+ return readFileSync(join(ROOT, path), "utf8");
28
+ }
29
+
30
+ function readMarkdownSection(source: string, heading: string): string {
31
+ const lines = source.split(/\r?\n/);
32
+ const matches = lines.flatMap((line, index) => {
33
+ const match = line.match(/^(#{1,6})\s+(.+?)\s*$/);
34
+ return match?.[2] === heading ? [{ index, level: match[1].length }] : [];
35
+ });
36
+ assert.equal(
37
+ matches.length,
38
+ 1,
39
+ `Markdown must contain exactly one "${heading}" section (found ${matches.length})`,
40
+ );
41
+ const [{ index: start, level }] = matches;
42
+ const relativeEnd = lines.slice(start + 1).findIndex((line) => {
43
+ const match = line.match(/^(#{1,6})\s+/);
44
+ return match !== null && match[1].length <= level;
45
+ });
46
+ const end = relativeEnd === -1 ? lines.length : start + 1 + relativeEnd;
47
+ return lines.slice(start + 1, end).join("\n").trim();
48
+ }
49
+
50
+ // Extracts one command line beginning with `commandPrefix` so payload args bind
51
+ // to the correct command shape; `--cwd`, `--change`, `--request-id` appear in
52
+ // both acquire and settle, so a section-wide match stays green on arg removal.
53
+ function extractCommandLine(section: string, commandPrefix: string): string {
54
+ const escaped = commandPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ const re = new RegExp(`^[ \t]*${escaped}.*$`, "gm");
56
+ const matches = section.match(re) ?? [];
57
+ assert.equal(
58
+ matches.length,
59
+ 1,
60
+ `Native Runtime Attempt Authority section must contain exactly one command line starting with "${commandPrefix}" (found ${matches.length})`,
61
+ );
62
+ const [line] = matches;
63
+ return line;
64
+ }
65
+
66
+ test("extractCommandLine rejects duplicate matching command lines (negative control)", () => {
67
+ for (const prefix of [
68
+ "gentle-ai sdd-attempt acquire ",
69
+ "gentle-ai sdd-attempt settle ",
70
+ ]) {
71
+ const duplicate = [
72
+ `${prefix}--cwd repo --change change --request-id id`,
73
+ `${prefix}--cwd repo2 --change change2 --request-id id2`,
74
+ ].join("\n");
75
+ assert.throws(
76
+ () => extractCommandLine(duplicate, prefix),
77
+ /exactly one command line/,
78
+ `duplicate ${prefix.trim()} lines must be rejected`,
79
+ );
80
+ }
81
+ });
82
+
83
+ test("workflow asset has exactly one Native Runtime Attempt Authority section", () => {
84
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
85
+ assert.ok(section.length > 0, "section must be non-empty");
86
+ });
87
+
88
+ test("status contract asset has exactly one Native Runtime Attempt Authority section", () => {
89
+ const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
90
+ assert.ok(section.length > 0, "section must be non-empty");
91
+ });
92
+
93
+ test("workflow names acquire and settle as sole provider authority for OpenSpec and Engram", () => {
94
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
95
+ assert.match(section, /sdd-attempt acquire/);
96
+ assert.match(section, /sdd-attempt settle/);
97
+ assert.match(section, /sole attempt and changed-line budget authority/i);
98
+ assert.match(section, /Git-common-dir/);
99
+ assert.match(section, /OpenSpec and Engram/);
100
+ });
101
+
102
+ test("status contract names acquire and settle and forbids OpenSpec/Engram attempt ledgers", () => {
103
+ const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
104
+ assert.match(section, /acquire/);
105
+ assert.match(section, /settle/);
106
+ assert.match(section, /OpenSpec or Engram attempt ledger/i);
107
+ });
108
+
109
+ test("workflow launches only on proceed; blocked and complete stop the launch", () => {
110
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
111
+ assert.match(section, /proceed/);
112
+ assert.match(section, /blocked/);
113
+ assert.match(section, /complete/);
114
+ assert.match(section, /launch only on `proceed`/i);
115
+ assert.match(section, /blocked.*do not launch/i);
116
+ assert.match(section, /complete.*do not launch/i);
117
+ });
118
+
119
+ test("workflow requires distinct request IDs with own-ID-only idempotent replay", () => {
120
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
121
+ assert.match(section, /distinct from acquire/i);
122
+ assert.match(section, /own ID only for idempotent replay/i);
123
+ });
124
+
125
+ test("workflow acquire command line binds every mandatory payload argument", () => {
126
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
127
+ const acquire = extractCommandLine(section, "gentle-ai sdd-attempt acquire ");
128
+ for (const arg of [
129
+ "--cwd <repo>",
130
+ "--change <change>",
131
+ "--request-id <id>",
132
+ "--work-unit <label>",
133
+ "--evidence-goal <goal>",
134
+ "--max-attempts <count>",
135
+ "--max-changed-lines <count>",
136
+ ]) {
137
+ assert.ok(acquire.includes(arg), `acquire command is missing ${arg}; command: ${acquire}`);
138
+ }
139
+ });
140
+
141
+ test("workflow settle command line binds mandatory arguments and routing invariants", () => {
142
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
143
+ const settle = extractCommandLine(section, "gentle-ai sdd-attempt settle ");
144
+ for (const arg of [
145
+ "--cwd <repo>",
146
+ "--change <change>",
147
+ "--token <token>",
148
+ "--request-id <id>",
149
+ "--outcome <failed|interrupted|passed>",
150
+ "--evidence-revision <sha256:...>",
151
+ "--diagnosis <text>",
152
+ "--harness-disposition <reused|invalidated>",
153
+ "--cleanup-evidence <text>",
154
+ "--process-evidence <text>",
155
+ ]) {
156
+ assert.ok(settle.includes(arg), `settle command is missing ${arg}; command: ${settle}`);
157
+ }
158
+ assert.match(section, /never `none`/);
159
+ assert.match(section, /proceed\|blocked\|complete/);
160
+ assert.match(section, /--successor-lineage/);
161
+ assert.match(section, /--remediates-evidence-revision/);
162
+ });
163
+
164
+ test("workflow forbids caller-authored counters and Pi-owned attempt state", () => {
165
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
166
+ assert.match(section, /never persist caller-authored/i);
167
+ assert.match(section, /OpenSpec artifacts, Engram memory/);
168
+ assert.match(section, /counter|token store|state machine|interception/i);
169
+ });
170
+
171
+ test("workflow states reset is never automatic and names compatibility surfaces", () => {
172
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
173
+ assert.match(section, /`status`, `begin`, `finish`, and `reset`/);
174
+ assert.match(section, /diagnostic\/compatibility surfaces/);
175
+ assert.match(section, /not the normal runtime route/i);
176
+ assert.match(section, /reset.*never automatic/i);
177
+ assert.match(section, /explicit maintainer scope decision/);
178
+ });
179
+
180
+ test("workflow gatekeeper rerun is subordinate to a fresh native acquire", () => {
181
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
182
+ assert.match(section, /rerun never bypasses native attempt authority/i);
183
+ assert.match(section, /fresh compact acquire/i);
184
+ // The gatekeeper quality rule is preserved (not deleted).
185
+ const gatekeeper = readMarkdownSection(read(WORKFLOW), "Automatic Mode Gatekeeper");
186
+ assert.match(gatekeeper, /rerun the same SDD phase once with corrective feedback/);
187
+ assert.match(gatekeeper, /Validate the rerun/);
188
+ });
189
+
190
+ test("workflow does not present status|begin|finish|reset as the primary route", () => {
191
+ const section = readMarkdownSection(read(WORKFLOW), SECTION);
192
+ assert.match(section, /not the normal runtime route/i);
193
+ assert.doesNotMatch(
194
+ section,
195
+ /primary route.*sdd-attempt (?:status|begin|finish|reset)/i,
196
+ );
197
+ });
198
+
199
+ test("status contract authority is artifact-store agnostic and excluded from SDD v1 status", () => {
200
+ const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
201
+ assert.match(section, /artifact-store agnostic/i);
202
+ assert.match(section, /MUST NOT be embedded in the SDD v1 status/i);
203
+ assert.match(section, /separate from artifact dispatch/i);
204
+ });
205
+
206
+ test("status contract continuation rule: acquire before launch, settle after run, distinct IDs, provider states route, reset never automatic", () => {
207
+ const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
208
+ assert.match(section, /before every runtime-bearing/i);
209
+ assert.match(section, /after the external run completes it MUST settle/i);
210
+ assert.match(section, /distinct/i);
211
+ assert.match(section, /proceed\|blocked\|complete/);
212
+ assert.match(section, /launch only on `proceed`/i);
213
+ assert.match(section, /reset.*never automatic/i);
214
+ });
215
+
216
+ test("status contract schema is unchanged (no attempt authority payload added)", () => {
217
+ const schema = readMarkdownSection(read(STATUS_CONTRACT), "Status Schema");
218
+ assert.doesNotMatch(
219
+ schema,
220
+ /attemptToken|attempt_token|attemptCount|attempt_counter|sddAttempt|sdd_attempt|nativeAttempt/i,
221
+ "Status Schema must not embed the runtime attempt authority payload",
222
+ );
223
+ });
224
+
225
+ test("status contract uses a runtime-safe semantic reference, not a repo-source assets/ path", () => {
226
+ const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
227
+ assert.doesNotMatch(
228
+ section,
229
+ /See `assets\/sdd-orchestrator-workflow\.md`|see `assets\//i,
230
+ "status contract must not point installed consumers at a repo-source assets/ path; those are package source paths before installation",
231
+ );
232
+ assert.match(section, /lazy-loaded `SDD Orchestrator Workflow`/i);
233
+ assert.match(section, /Native Runtime Attempt Authority/);
234
+ assert.match(section, /Do not look up `assets\/\.\.\.` paths at runtime/i);
235
+ });
@@ -0,0 +1,266 @@
1
+ import assert from "node:assert/strict";
2
+ import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { fileURLToPath } from "node:url";
7
+ import {
8
+ OPAQUE_PI_REVIEWER_ARGV,
9
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE,
10
+ OpaquePiReviewerTransportError,
11
+ resolvePiLaunch,
12
+ runOpaquePiReviewer,
13
+ } from "../lib/opaque-pi-reviewer-adapter.ts";
14
+
15
+ const FAKE_PI = `#!/usr/bin/env node
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+ const argv = process.argv.slice(2);
19
+ const chunks = [];
20
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
21
+ process.stdin.on("end", () => {
22
+ const stdin = Buffer.concat(chunks);
23
+ if (process.env.OPAQUE_PI_STDIN_CAPTURE) fs.writeFileSync(process.env.OPAQUE_PI_STDIN_CAPTURE, stdin);
24
+ const mode = process.env.OPAQUE_PI_MODE || "ok";
25
+ const log = {
26
+ argv,
27
+ cwd: process.cwd(),
28
+ entries_before: fs.readdirSync(process.cwd()),
29
+ entries_after: fs.readdirSync(process.cwd()),
30
+ };
31
+ if (process.env.OPAQUE_PI_LOG) fs.appendFileSync(process.env.OPAQUE_PI_LOG, JSON.stringify(log) + "\\n");
32
+ if (mode === "break-cleanup" || process.env.OPAQUE_PI_BREAK_CLEANUP === "true") {
33
+ fs.chmodSync(path.dirname(process.cwd()), 0o500);
34
+ }
35
+ if (mode === "empty") process.exit(0);
36
+ if (mode === "hang") { setTimeout(() => process.exit(0), 10_000); return; }
37
+ if (mode === "nonzero") { process.stderr.write("opaque pi failed\\n"); process.exit(7); }
38
+ process.stdout.write(Buffer.from(process.env.OPAQUE_PI_OUTPUT_B64 || "", "base64"));
39
+ });
40
+ `;
41
+
42
+ const PROMPT_BYTES = Buffer.concat([
43
+ Buffer.from("opaque prompt\r\n\u0000", "utf8"),
44
+ Buffer.from([0x01, 0xff, 0xfe, 0x00]),
45
+ ]);
46
+ const OUTPUT_BYTES = Buffer.concat([
47
+ Buffer.from("opaque output\r\n\u0000", "utf8"),
48
+ Buffer.from([0x07, 0xff, 0xfe, 0x00]),
49
+ ]);
50
+
51
+ interface OpaqueHarness {
52
+ directory: string;
53
+ pi: string;
54
+ logPath: string;
55
+ stdinCapturePath: string;
56
+ environment: NodeJS.ProcessEnv;
57
+ }
58
+
59
+ interface OpaquePiLog {
60
+ argv: string[];
61
+ cwd: string;
62
+ entries_before: string[];
63
+ entries_after: string[];
64
+ }
65
+
66
+ function harness(t: test.TestContext, overrides: Record<string, string> = {}): OpaqueHarness {
67
+ const directory = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-"));
68
+ t.after(() => rmSync(directory, { recursive: true, force: true }));
69
+ const pi = join(directory, "pi");
70
+ writeFileSync(pi, FAKE_PI);
71
+ chmodSync(pi, 0o755);
72
+ const logPath = join(directory, "pi.log");
73
+ const stdinCapturePath = join(directory, "stdin.bin");
74
+ return {
75
+ directory,
76
+ pi,
77
+ logPath,
78
+ stdinCapturePath,
79
+ environment: {
80
+ ...process.env,
81
+ OPAQUE_PI_LOG: logPath,
82
+ OPAQUE_PI_STDIN_CAPTURE: stdinCapturePath,
83
+ OPAQUE_PI_OUTPUT_B64: OUTPUT_BYTES.toString("base64"),
84
+ ...overrides,
85
+ },
86
+ };
87
+ }
88
+
89
+ function readLog(path: string): OpaquePiLog[] {
90
+ if (!existsSync(path)) return [];
91
+ return readFileSync(path, "utf8")
92
+ .split("\n")
93
+ .filter((line) => line.length > 0)
94
+ .map((line) => JSON.parse(line) as OpaquePiLog);
95
+ }
96
+
97
+ async function rejectsWithTransportError(
98
+ promise: Promise<unknown>,
99
+ kind: (typeof OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE)[keyof typeof OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE],
100
+ ): Promise<OpaquePiReviewerTransportError> {
101
+ let caught: OpaquePiReviewerTransportError | undefined;
102
+ await assert.rejects(promise, (error: unknown) => {
103
+ assert.ok(error instanceof OpaquePiReviewerTransportError, `expected OpaquePiReviewerTransportError, received ${String(error)}`);
104
+ caught = error;
105
+ return error.kind === kind;
106
+ });
107
+ return caught!;
108
+ }
109
+
110
+ test("the opaque adapter streams arbitrary prompt and stdout bytes verbatim through the fixed Pi subprocess", async (t) => {
111
+ const fixture = harness(t);
112
+ const result = await runOpaquePiReviewer(PROMPT_BYTES, {
113
+ piExecutable: fixture.pi,
114
+ environment: fixture.environment,
115
+ timeoutMs: 10_000,
116
+ });
117
+
118
+ assert.equal(result.promptByteLength, PROMPT_BYTES.length);
119
+ assert.equal(result.stdoutByteLength, OUTPUT_BYTES.length);
120
+ assert.deepEqual(result.stdout, OUTPUT_BYTES);
121
+ assert.deepEqual(readFileSync(fixture.stdinCapturePath), PROMPT_BYTES);
122
+
123
+ const calls = readLog(fixture.logPath);
124
+ assert.equal(calls.length, 1);
125
+ assert.deepEqual(calls[0]!.argv, [...OPAQUE_PI_REVIEWER_ARGV]);
126
+ assert.deepEqual(calls[0]!.entries_before, []);
127
+ assert.deepEqual(calls[0]!.entries_after, []);
128
+ assert.notEqual(calls[0]!.cwd, process.cwd());
129
+ assert.equal(existsSync(calls[0]!.cwd), false, "the empty scratch directory is removed after success");
130
+ });
131
+
132
+ test("the opaque adapter returns typed transport errors for launch, nonzero, empty, timeout, and cancellation", async (t) => {
133
+ const fixture = harness(t);
134
+ await rejectsWithTransportError(
135
+ runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: join(fixture.directory, "missing-pi"), environment: fixture.environment, timeoutMs: 10_000 }),
136
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.LAUNCH_FAILED,
137
+ );
138
+
139
+ const nonzero = await rejectsWithTransportError(
140
+ runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "nonzero" }, timeoutMs: 10_000 }),
141
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.NONZERO_EXIT,
142
+ );
143
+ assert.equal(nonzero.exitCode, 7);
144
+ assert.deepEqual(nonzero.stderr, Buffer.from("opaque pi failed\n"));
145
+
146
+ await rejectsWithTransportError(
147
+ runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "empty" }, timeoutMs: 10_000 }),
148
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.EMPTY_OUTPUT,
149
+ );
150
+
151
+ const timedOut = await rejectsWithTransportError(
152
+ runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang" }, timeoutMs: 300 }),
153
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.TIMED_OUT,
154
+ );
155
+ assert.equal(timedOut.timedOut, true);
156
+
157
+ const controller = new AbortController();
158
+ const cancellation = runOpaquePiReviewer(PROMPT_BYTES, {
159
+ piExecutable: fixture.pi,
160
+ environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang" },
161
+ timeoutMs: 10_000,
162
+ signal: controller.signal,
163
+ });
164
+ setTimeout(() => controller.abort(), 100).unref();
165
+ const cancelled = await rejectsWithTransportError(cancellation, OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.CANCELLED);
166
+ assert.equal(cancelled.cancelled, true);
167
+ });
168
+
169
+ test("the opaque adapter leaves no prompt or result file in scratch and reports cleanup failure as transport-only", async (t) => {
170
+ const fixture = harness(t);
171
+ const scratchParent = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-cleanup-"));
172
+ const originalTmpdir = process.env.TMPDIR;
173
+ process.env.TMPDIR = scratchParent;
174
+ try {
175
+ const error = await rejectsWithTransportError(
176
+ runOpaquePiReviewer(PROMPT_BYTES, {
177
+ piExecutable: fixture.pi,
178
+ environment: { ...fixture.environment, OPAQUE_PI_MODE: "break-cleanup" },
179
+ timeoutMs: 10_000,
180
+ }),
181
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.CLEANUP_FAILED,
182
+ );
183
+ assert.match(error.message, /scratch directory/i);
184
+ } finally {
185
+ if (originalTmpdir === undefined) delete process.env.TMPDIR;
186
+ else process.env.TMPDIR = originalTmpdir;
187
+ chmodSync(scratchParent, 0o700);
188
+ rmSync(scratchParent, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ test("the opaque adapter preserves primary nonzero and timeout errors when scratch cleanup also fails", async (t) => {
193
+ const fixture = harness(t);
194
+ const scratchParent = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-primary-failure-"));
195
+ const originalTmpdir = process.env.TMPDIR;
196
+ process.env.TMPDIR = scratchParent;
197
+ try {
198
+ const nonzero = await rejectsWithTransportError(
199
+ runOpaquePiReviewer(PROMPT_BYTES, {
200
+ piExecutable: fixture.pi,
201
+ environment: { ...fixture.environment, OPAQUE_PI_MODE: "nonzero", OPAQUE_PI_BREAK_CLEANUP: "true" },
202
+ timeoutMs: 10_000,
203
+ }),
204
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.NONZERO_EXIT,
205
+ );
206
+ assert.equal(nonzero.exitCode, 7);
207
+ assert.deepEqual(nonzero.stderr, Buffer.from("opaque pi failed\n"));
208
+ chmodSync(scratchParent, 0o700);
209
+
210
+ const timedOut = await rejectsWithTransportError(
211
+ runOpaquePiReviewer(PROMPT_BYTES, {
212
+ piExecutable: fixture.pi,
213
+ environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang", OPAQUE_PI_BREAK_CLEANUP: "true" },
214
+ timeoutMs: 300,
215
+ }),
216
+ OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.TIMED_OUT,
217
+ );
218
+ assert.equal(timedOut.timedOut, true);
219
+ } finally {
220
+ if (originalTmpdir === undefined) delete process.env.TMPDIR;
221
+ else process.env.TMPDIR = originalTmpdir;
222
+ chmodSync(scratchParent, 0o700);
223
+ rmSync(scratchParent, { recursive: true, force: true });
224
+ }
225
+ });
226
+
227
+ test("the opaque adapter has no review lifecycle imports or identifiers", () => {
228
+ const adapterPath = fileURLToPath(new URL("../lib/opaque-pi-reviewer-adapter.ts", import.meta.url));
229
+ const source = readFileSync(adapterPath, "utf8");
230
+ assert.doesNotMatch(source, /review-integration|gentle-ai|materialize|submit/i);
231
+ for (const identifier of ["lineage", "target", "revision", "receipt", "lens", "order", "subject", "schema", "capture", "submission", "status", "model", "provider", "profile"]) {
232
+ assert.doesNotMatch(source, new RegExp(`\\b${identifier}\\b`, "i"), `adapter must not contain lifecycle identifier ${identifier}`);
233
+ }
234
+ });
235
+
236
+ // #468 / #519: on Windows a bare `pi` resolves to pi.cmd, pi.ps1, or a POSIX
237
+ // shim, none of which Node can spawn with shell:false (EINVAL or ENOENT). The
238
+ // relay already runs inside Pi, so the host's own JavaScript entry is spawned
239
+ // through process.execPath instead. Other platforms keep today's exact shape.
240
+ test("the Pi launch shape spawns the host entry through process.execPath on win32 and stays byte-identical elsewhere", () => {
241
+ const host = {
242
+ execPath: "C:\\Program Files\\nodejs\\node.exe",
243
+ entry: "C:\\Users\\dev\\AppData\\Local\\pnpm\\global\\5\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\bundle\\cli.js",
244
+ };
245
+ const windows = resolvePiLaunch(undefined, "win32", host);
246
+ assert.deepEqual(windows, { file: host.execPath, arguments: [host.entry, ...OPAQUE_PI_REVIEWER_ARGV] });
247
+ assert.notEqual(windows.file, "pi");
248
+ assert.equal(windows.arguments.includes("pi"), false);
249
+ for (const platform of ["linux", "darwin", "freebsd"] as const) {
250
+ assert.deepEqual(resolvePiLaunch(undefined, platform, host), { file: "pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] }, platform);
251
+ assert.deepEqual(resolvePiLaunch("/opt/pi/bin/pi", platform, host), { file: "/opt/pi/bin/pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] }, platform);
252
+ }
253
+ assert.deepEqual(resolvePiLaunch("C:\\tools\\pi.exe", "win32", host), { file: "C:\\tools\\pi.exe", arguments: [...OPAQUE_PI_REVIEWER_ARGV] });
254
+ for (const entry of [undefined, "", "cli.js", "dist/bundle/cli.js"]) {
255
+ assert.throws(() => resolvePiLaunch(undefined, "win32", { execPath: host.execPath, entry }), /host entry/, `entry ${JSON.stringify(entry)} must fail closed instead of spawning a bare pi`);
256
+ }
257
+ });
258
+
259
+ test("the default Pi launch resolves from the running host process", () => {
260
+ const launch = resolvePiLaunch(undefined);
261
+ if (process.platform === "win32") {
262
+ assert.deepEqual(launch, { file: process.execPath, arguments: [process.argv[1]!, ...OPAQUE_PI_REVIEWER_ARGV] });
263
+ } else {
264
+ assert.deepEqual(launch, { file: "pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] });
265
+ }
266
+ });