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,35 @@
1
+ import type { NativeReviewCli } from "./native-review-cli.ts";
2
+ import type { ReviewLastEventClosureBinding, ReviewStatusV3 } from "./review-integration-v2.ts";
3
+
4
+ export interface ReviewLastEventCaptureSelector {
5
+ readonly baseRef?: string;
6
+ readonly committedOnly?: true;
7
+ }
8
+
9
+ /**
10
+ * Reconcile exactly one ambiguous native capture outcome. Successful captures
11
+ * return their native artifact or closure directly; this helper is never a
12
+ * post-success lifecycle step and never replays a capture.
13
+ */
14
+ export async function reconcileUnknownReviewLastEventCapture(
15
+ nativeReviewCli: NativeReviewCli,
16
+ cwd: string,
17
+ binding: ReviewLastEventClosureBinding,
18
+ selector?: ReviewLastEventCaptureSelector,
19
+ ): Promise<ReviewStatusV3> {
20
+ if (nativeReviewCli.targetStatus === undefined) {
21
+ throw new TypeError("native target-scoped STATUS is required to reconcile an ambiguous capture outcome");
22
+ }
23
+ const status = await nativeReviewCli.targetStatus({
24
+ cwd,
25
+ lineageId: binding.lineageId,
26
+ ...(selector === undefined ? {} : selector),
27
+ });
28
+ if (binding.targetIdentity !== undefined && status.targetIdentity !== binding.targetIdentity) {
29
+ throw new TypeError("capture reconciliation returned a different target");
30
+ }
31
+ if (status.authority?.lineageId !== undefined && status.authority.lineageId !== binding.lineageId) {
32
+ throw new TypeError("capture reconciliation returned a different lineage");
33
+ }
34
+ return status;
35
+ }
@@ -0,0 +1,16 @@
1
+ // gentle-pi.review-relay/v1 — the Pi host relay handshake (gentle-pi#311 P4,
2
+ // provider side gentle-ai#3249/#3254).
3
+ //
4
+ // gentle-ai admits `pi` as a host-mediated runtime identity only when the
5
+ // launcher that relays the invocation declares this exact contract through
6
+ // this exact environment variable. Any other value (or none) keeps Pi
7
+ // fail-closed at admission, before any repository, target, or authority work.
8
+ // That refusal is correct provider behavior and is never worked around here;
9
+ // the single central gentle-ai process runner injects this declaration on
10
+ // every invocation the extension makes.
11
+ //
12
+ // Both values are compiled constants: the contract identity is versioned by
13
+ // release, never by configuration.
14
+
15
+ export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
16
+ export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -23,19 +23,36 @@ function gentlePiAgentHome(): string {
23
23
  }
24
24
 
25
25
  export type SddExecutionMode = "interactive" | "auto";
26
+ export type SddDeliveryStrategy =
27
+ | "ask-on-risk"
28
+ | "auto-chain"
29
+ | "single-pr"
30
+ | "exception-ok";
31
+ /** @deprecated Use SddDeliveryStrategy; legacy values normalize at persistence boundaries. */
26
32
  export type SddChainedPrStrategy =
33
+ | SddDeliveryStrategy
27
34
  | "auto-forecast"
28
35
  | "ask-always"
29
36
  | "single-pr-default"
30
37
  | "force-chained";
38
+ export type SddPreflightField = "executionMode" | "artifactStore" | "chainedPrStrategy" | "reviewBudgetLines";
39
+ export const SDD_PREFLIGHT_FIELDS = ["executionMode", "artifactStore", "chainedPrStrategy", "reviewBudgetLines"] as const;
31
40
 
32
41
  export interface SddPreflightPreferences {
33
42
  executionMode: SddExecutionMode;
34
43
  artifactStore: SddArtifactStore;
44
+ /** Runtime values are canonical; legacy assignments normalize at persistence/render boundaries. */
35
45
  chainedPrStrategy: SddChainedPrStrategy;
36
46
  reviewBudgetLines: number;
37
47
  engramAvailable: boolean;
38
48
  prompted: boolean;
49
+ sizeExceptionAccepted?: true;
50
+ }
51
+
52
+ export interface SddPreflightResolutionOptions {
53
+ persisted?: Partial<SddPreflightPreferences>;
54
+ promptFields?: readonly SddPreflightField[];
55
+ acceptSizeException?: true;
39
56
  }
40
57
 
41
58
  interface SddPreflightCallbacks {
@@ -69,14 +86,14 @@ interface LegacyManagedAssetsManifest extends ManagedAssetsManifest {
69
86
  packageVersion: string;
70
87
  }
71
88
 
72
- const DEFAULT_SDD_PREFLIGHT: SddPreflightPreferences = {
73
- executionMode: "interactive",
89
+ export const DEFAULT_SDD_PREFLIGHT: SddPreflightPreferences = Object.freeze({
90
+ executionMode: "auto",
74
91
  artifactStore: "openspec",
75
- chainedPrStrategy: "auto-forecast",
92
+ chainedPrStrategy: "ask-on-risk",
76
93
  reviewBudgetLines: 400,
77
94
  engramAvailable: false,
78
95
  prompted: false,
79
- };
96
+ });
80
97
 
81
98
  const sddPreflightBySession = new Map<string, SddPreflightPreferences>();
82
99
  const sddPreflightInFlight = new Map<string, Promise<SddPreflightPreferences>>();
@@ -85,6 +102,69 @@ function isRecord(value: unknown): value is Record<string, unknown> {
85
102
  return typeof value === "object" && value !== null && !Array.isArray(value);
86
103
  }
87
104
 
105
+ function isSddExecutionMode(value: unknown): value is SddExecutionMode {
106
+ return value === "interactive" || value === "auto";
107
+ }
108
+
109
+ function isSddArtifactStore(value: unknown): value is SddArtifactStore {
110
+ return value === "openspec" || value === "engram" || value === "hybrid" || value === "none";
111
+ }
112
+
113
+ // normalizeSddArtifactStore accepts the canonical names and the legacy "both"
114
+ // spelling of the dual-store mode. Operator preflight files written before the
115
+ // rename carry "both" on disk; rejecting it would silently drop the operator's
116
+ // choice back to the default, so it maps forward instead.
117
+ export function normalizeSddArtifactStore(value: unknown): SddArtifactStore | undefined {
118
+ if (value === "both") return "hybrid";
119
+ return isSddArtifactStore(value) ? value : undefined;
120
+ }
121
+
122
+ function normalizeReviewBudgetValue(value: unknown): number | undefined {
123
+ const parsed = typeof value === "number"
124
+ ? value
125
+ : typeof value === "string"
126
+ ? Number.parseInt(value.trim(), 10)
127
+ : Number.NaN;
128
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : undefined;
129
+ }
130
+
131
+ function normalizeSddStrategySelection(
132
+ value: unknown,
133
+ allowExceptionOk: boolean,
134
+ ): SddDeliveryStrategy | undefined {
135
+ if (value === "exception-ok") return allowExceptionOk ? value : undefined;
136
+ if (value === "auto-forecast" || value === "ask-always") return "ask-on-risk";
137
+ if (value === "single-pr-default") return "single-pr";
138
+ if (value === "force-chained") return "auto-chain";
139
+ return value === "ask-on-risk" || value === "auto-chain" || value === "single-pr"
140
+ ? value
141
+ : undefined;
142
+ }
143
+
144
+ export function normalizeSddChainedPrStrategy(
145
+ value: unknown,
146
+ allowExceptionOk = false,
147
+ ): SddDeliveryStrategy {
148
+ return normalizeSddStrategySelection(value, allowExceptionOk) ?? "ask-on-risk";
149
+ }
150
+
151
+ function normalizedSelections(
152
+ value: Partial<Record<SddPreflightField, unknown>> | undefined,
153
+ engramAvailable: boolean,
154
+ allowExceptionOk: boolean,
155
+ ): Partial<Record<SddPreflightField, unknown>> {
156
+ if (!value) return {};
157
+ const result: Partial<Record<SddPreflightField, unknown>> = {};
158
+ if (isSddExecutionMode(value.executionMode)) result.executionMode = value.executionMode;
159
+ const artifactStore = normalizeSddArtifactStore(value.artifactStore);
160
+ if (artifactStore !== undefined && (engramAvailable || artifactStore === "openspec" || artifactStore === "none")) result.artifactStore = artifactStore;
161
+ const strategy = normalizeSddStrategySelection(value.chainedPrStrategy, allowExceptionOk);
162
+ if (strategy) result.chainedPrStrategy = strategy;
163
+ const reviewBudgetLines = normalizeReviewBudgetValue(value.reviewBudgetLines);
164
+ if (reviewBudgetLines !== undefined) result.reviewBudgetLines = reviewBudgetLines;
165
+ return result;
166
+ }
167
+
88
168
  function emptyManagedAssetsManifest(): ManagedAssetsManifest {
89
169
  return {
90
170
  schemaVersion: MANAGED_ASSETS_SCHEMA_VERSION,
@@ -273,28 +353,28 @@ export function readSddPreflightFromDisk(cwd: string): SddPreflightPreferences |
273
353
  try {
274
354
  const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
275
355
  if (!isRecord(parsed)) return undefined;
276
- // Validate required fields to guard against stale/corrupt writes
356
+ // Validate required fields to guard against stale/corrupt writes.
277
357
  const { executionMode, artifactStore, chainedPrStrategy, reviewBudgetLines, engramAvailable, prompted } = parsed;
358
+ // Normalize before validating: a preflight file written before the
359
+ // dual-store rename carries "both", and discarding it would silently
360
+ // drop the operator's choice back to the default.
361
+ const canonicalArtifactStore = normalizeSddArtifactStore(artifactStore);
278
362
  if (
279
- (executionMode !== "interactive" && executionMode !== "auto") ||
280
- (artifactStore !== "openspec" && artifactStore !== "engram" && artifactStore !== "both" && artifactStore !== "none") ||
363
+ !isSddExecutionMode(executionMode) ||
364
+ canonicalArtifactStore === undefined ||
281
365
  typeof reviewBudgetLines !== "number" ||
366
+ !Number.isFinite(reviewBudgetLines) ||
367
+ reviewBudgetLines <= 0 ||
282
368
  typeof engramAvailable !== "boolean" ||
283
369
  typeof prompted !== "boolean"
284
370
  ) {
285
371
  return undefined;
286
372
  }
287
- const normalizedChain: SddChainedPrStrategy =
288
- chainedPrStrategy === "ask-always" ||
289
- chainedPrStrategy === "single-pr-default" ||
290
- chainedPrStrategy === "force-chained"
291
- ? (chainedPrStrategy as SddChainedPrStrategy)
292
- : "auto-forecast";
293
373
  return {
294
374
  executionMode,
295
- artifactStore,
296
- chainedPrStrategy: normalizedChain,
297
- reviewBudgetLines,
375
+ artifactStore: canonicalArtifactStore,
376
+ chainedPrStrategy: normalizeSddChainedPrStrategy(chainedPrStrategy),
377
+ reviewBudgetLines: normalizeReviewBudgetValue(reviewBudgetLines) ?? DEFAULT_SDD_PREFLIGHT.reviewBudgetLines,
298
378
  engramAvailable,
299
379
  prompted,
300
380
  };
@@ -306,8 +386,19 @@ export function readSddPreflightFromDisk(cwd: string): SddPreflightPreferences |
306
386
  export function writeSddPreflightToDisk(cwd: string, prefs: SddPreflightPreferences): void {
307
387
  try {
308
388
  const path = sddPreflightDiskPath(cwd);
389
+ const prompted = prefs.prompted === true;
390
+ const canonical: SddPreflightPreferences = {
391
+ executionMode: isSddExecutionMode(prefs.executionMode) ? prefs.executionMode : DEFAULT_SDD_PREFLIGHT.executionMode,
392
+ artifactStore: normalizeSddArtifactStore(prefs.artifactStore) ?? DEFAULT_SDD_PREFLIGHT.artifactStore,
393
+ chainedPrStrategy: normalizeSddChainedPrStrategy(prefs.chainedPrStrategy),
394
+ reviewBudgetLines:
395
+ normalizeReviewBudgetValue(prefs.reviewBudgetLines) ??
396
+ DEFAULT_SDD_PREFLIGHT.reviewBudgetLines,
397
+ engramAvailable: prefs.engramAvailable === true,
398
+ prompted,
399
+ };
309
400
  mkdirSync(dirname(path), { recursive: true });
310
- writeFileSync(path, JSON.stringify(prefs, null, 2));
401
+ writeFileSync(path, JSON.stringify(canonical, null, 2));
311
402
  } catch {
312
403
  // Disk write failures are non-fatal; in-memory cache is the primary store
313
404
  }
@@ -394,6 +485,57 @@ function copyDirectoryFiles(
394
485
  return { copied, skipped };
395
486
  }
396
487
 
488
+ // Assets retired by gentle-pi#311 P5: the Pi-owned adversarial review actors.
489
+ // The refuter and validator verdicts now execute through Go-owned pi
490
+ // processes via provider-rendered self-contained vectors, so these agent
491
+ // definitions have no runtime consumer. The migration manifests under
492
+ // assets/migrations are append-only legacy-hash HISTORY (adoption evidence
493
+ // for force-installs) with no removal semantics, so history stays untouched
494
+ // and retirement happens here: an installed copy is deleted only when its
495
+ // content hash proves package ownership (current manifest or legacy
496
+ // history); user-modified copies are left in place and only lose managed
497
+ // ownership.
498
+ const RETIRED_MANAGED_ASSETS = Object.freeze([
499
+ "agents/review-refuter.md",
500
+ "agents/review-validator.md",
501
+ ]);
502
+
503
+ function removeRetiredManagedAssets(
504
+ agentHome: string,
505
+ manifest: ManagedAssetsManifest,
506
+ ): void {
507
+ let legacyHashes: Record<string, readonly string[]> | undefined;
508
+ for (const ownershipKey of RETIRED_MANAGED_ASSETS) {
509
+ const installedPath = join(agentHome, ...ownershipKey.split("/"));
510
+ if (!existsSync(installedPath)) {
511
+ delete manifest.assets[ownershipKey];
512
+ continue;
513
+ }
514
+ let installedContent: string | undefined;
515
+ try {
516
+ installedContent = readFileSync(installedPath, "utf8");
517
+ } catch {
518
+ installedContent = undefined;
519
+ }
520
+ if (installedContent === undefined) continue;
521
+ const installedHash = managedAssetHash(installedContent);
522
+ const managed = manifest.assets[ownershipKey] === installedHash;
523
+ const legacy = (legacyHashes ??= readLegacyManagedAssetHashes())[ownershipKey]?.includes(
524
+ managedAssetHash(legacyComparableAssetContent(ownershipKey, installedContent)),
525
+ ) === true;
526
+ if (managed || legacy) {
527
+ try {
528
+ rmSync(installedPath);
529
+ } catch {
530
+ continue;
531
+ }
532
+ }
533
+ // Managed copies are gone; user-modified copies stay but stop being
534
+ // package-managed either way.
535
+ delete manifest.assets[ownershipKey];
536
+ }
537
+ }
538
+
397
539
  export function installSddAssets(
398
540
  _cwd: string,
399
541
  force: boolean,
@@ -407,6 +549,7 @@ export function installSddAssets(
407
549
  (cachedLegacyAssetHashes ??= readLegacyManagedAssetHashes());
408
550
  }
409
551
  const manifest = readManagedAssetsManifest(manifestPath);
552
+ removeRetiredManagedAssets(agentHome, manifest);
410
553
  const agents = copyDirectoryFiles(
411
554
  join(ASSETS_DIR, "agents"),
412
555
  join(agentHome, "agents"),
@@ -443,7 +586,7 @@ export function installSddAssets(
443
586
 
444
587
  export function isSddPreflightTrigger(text: string): boolean {
445
588
  const trimmed = text.trim();
446
- if (/^\/sdd(?:[-:][^\s]*)?(?:\s|$)/i.test(trimmed)) return true;
589
+ if (/^\/(?:gentle-)?sdd(?:[-:][^\s]*)?(?:\s|$)/i.test(trimmed)) return true;
447
590
  if (/[??]\s*$/.test(trimmed)) return false;
448
591
  if (
449
592
  /\b(?:don't|do\s+not|not\s+use|never\s+use|without\s+using|sin\s+usar|no\s+(?:quiero|queremos|vamos\s+a)?\s*usar)\s+sdd\b/i.test(
@@ -498,56 +641,67 @@ function hasWritableEngramTool(pi: ExtensionAPI): boolean {
498
641
  }
499
642
  }
500
643
 
501
- function normalizeSddReviewBudget(value: string): number {
502
- const parsed = Number.parseInt(value.trim(), 10);
503
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 400;
504
- }
505
-
506
- async function collectSddPreflightPreferences(
644
+ export async function collectSddPreflightPreferences(
507
645
  ctx: ExtensionContext,
508
646
  engramAvailable: boolean,
647
+ options: SddPreflightResolutionOptions = {},
509
648
  ): Promise<SddPreflightPreferences> {
510
- if (!ctx.hasUI) return { ...DEFAULT_SDD_PREFLIGHT, engramAvailable };
511
- const executionMode = await ctx.ui.select("SDD execution mode", [
512
- "interactive",
513
- "auto",
514
- ]);
515
- const artifactOptions = engramAvailable
516
- ? ["openspec", "engram", "both"]
517
- : ["openspec"];
518
- const artifactStore = await ctx.ui.select("SDD artifact store", artifactOptions);
519
- const chainedPrStrategy = await ctx.ui.select("SDD PR chaining", [
520
- "auto-forecast",
521
- "ask-always",
522
- "single-pr-default",
523
- "force-chained",
524
- ]);
525
- const reviewBudgetLines = normalizeSddReviewBudget(
526
- (await ctx.ui.input("SDD review budget lines", "400")) ?? "400",
527
- );
649
+ const persistedPrompted = options.persisted?.prompted === true;
650
+ const allowExceptionOk = options.acceptSizeException === true;
651
+ const persisted = normalizedSelections(options.persisted, engramAvailable, allowExceptionOk);
652
+ const resolved: Partial<Record<SddPreflightField, unknown>> = { ...persisted };
653
+ let prompted = persistedPrompted;
654
+ let sizeExceptionAccepted = allowExceptionOk && persisted.chainedPrStrategy === "exception-ok";
655
+ const promptFields = new Set(options.promptFields ?? []);
656
+
657
+ const usePromptedValue = (
658
+ field: SddPreflightField,
659
+ value: unknown,
660
+ ): void => {
661
+ const candidate = normalizedSelections({ [field]: value }, engramAvailable, allowExceptionOk);
662
+ if (candidate[field] !== undefined) {
663
+ resolved[field] = candidate[field];
664
+ if (field === "chainedPrStrategy" && candidate[field] === "exception-ok") sizeExceptionAccepted = true;
665
+ prompted = true;
666
+ }
667
+ };
668
+
669
+ const promptField = async (
670
+ field: SddPreflightField,
671
+ read: () => Promise<unknown>,
672
+ enabled = true,
673
+ ): Promise<void> => {
674
+ if (ctx.hasUI && enabled && promptFields.has(field)) {
675
+ usePromptedValue(field, await read());
676
+ }
677
+ };
678
+ const artifactOptions = engramAvailable ? ["openspec", "engram", "hybrid"] : ["openspec"];
679
+ await promptField("executionMode", () => ctx.ui.select("SDD execution mode", ["interactive", "auto"]));
680
+ await promptField("artifactStore", () => ctx.ui.select("SDD artifact store", artifactOptions), artifactOptions.length > 1);
681
+ await promptField("chainedPrStrategy", () => ctx.ui.select("SDD delivery strategy", ["ask-on-risk", "auto-chain", "single-pr"]));
682
+ await promptField("reviewBudgetLines", () => ctx.ui.input("SDD review budget lines", String(DEFAULT_SDD_PREFLIGHT.reviewBudgetLines)));
683
+
684
+ const resolvedValue = <T>(field: SddPreflightField, fallback: T): T =>
685
+ (resolved[field] as T | undefined) ?? fallback;
528
686
  return {
529
- executionMode:
530
- executionMode === "auto" ? "auto" : DEFAULT_SDD_PREFLIGHT.executionMode,
531
- artifactStore:
532
- artifactStore === "engram" || artifactStore === "both"
533
- ? artifactStore
534
- : DEFAULT_SDD_PREFLIGHT.artifactStore,
535
- chainedPrStrategy:
536
- chainedPrStrategy === "ask-always" ||
537
- chainedPrStrategy === "single-pr-default" ||
538
- chainedPrStrategy === "force-chained"
539
- ? chainedPrStrategy
540
- : DEFAULT_SDD_PREFLIGHT.chainedPrStrategy,
541
- reviewBudgetLines,
687
+ executionMode: resolvedValue("executionMode", DEFAULT_SDD_PREFLIGHT.executionMode),
688
+ artifactStore: resolvedValue("artifactStore", DEFAULT_SDD_PREFLIGHT.artifactStore),
689
+ chainedPrStrategy: resolvedValue("chainedPrStrategy", DEFAULT_SDD_PREFLIGHT.chainedPrStrategy),
690
+ reviewBudgetLines: resolvedValue("reviewBudgetLines", DEFAULT_SDD_PREFLIGHT.reviewBudgetLines),
542
691
  engramAvailable,
543
- prompted: true,
692
+ prompted,
693
+ ...(sizeExceptionAccepted ? { sizeExceptionAccepted: true as const } : {}),
544
694
  };
545
695
  }
546
696
 
547
697
  export function renderSddPreflightPrompt(prefs: SddPreflightPreferences): string {
698
+ const deliveryStrategy = normalizeSddChainedPrStrategy(
699
+ prefs.chainedPrStrategy,
700
+ prefs.sizeExceptionAccepted === true,
701
+ );
548
702
  const sourceLine = prefs.prompted
549
- ? "The user already chose these SDD preferences for this Pi session. Reuse them unless the user explicitly changes them."
550
- : "No interactive UI was available for SDD preflight, so these default preferences were applied for this Pi session. Ask the user before making delivery decisions that depend on them.";
703
+ ? "These SDD preferences are explicit current-session choices. Reuse them unless the user explicitly changes them."
704
+ : "These SDD preferences are canonical defaults or persisted choices. Treat them as authoritative; do not revisit dependent decisions unless a genuine human-control gate is reached.";
551
705
  const interactiveRules =
552
706
  prefs.executionMode === "interactive"
553
707
  ? [
@@ -563,25 +717,34 @@ export function renderSddPreflightPrompt(prefs: SddPreflightPreferences): string
563
717
  sourceLine,
564
718
  `- Execution mode: ${prefs.executionMode}`,
565
719
  `- Artifact store: ${prefs.artifactStore}${prefs.engramAvailable ? "" : " (Engram unavailable in this session)"}`,
566
- `- Chained PR strategy: ${prefs.chainedPrStrategy}`,
567
- `- Review budget: ${prefs.reviewBudgetLines} changed lines`,
720
+ `- Delivery strategy: ${deliveryStrategy}`,
721
+ "- Delivery strategy domain: `ask-on-risk` | `auto-chain` | `single-pr` | `exception-ok`",
722
+ `- Review budget: ${prefs.reviewBudgetLines} changed lines (400 is the canonical threshold unless explicitly changed)`,
723
+ "- Chain strategy: deferred until chaining is selected.",
724
+ "- `exception-ok` is never inferred; it requires explicit acceptance of `size:exception`.",
568
725
  ...interactiveRules,
569
- "- If task/workload forecasts conflict with these preferences, pause before sdd-apply and ask the user for a delivery decision.",
726
+ "- Preserve human-controlled consent, authorization, security, destructive/publishing, ambiguous-scope, and `size:exception` gates.",
727
+ "- When review-budget risk requires a delivery decision, use `ask-on-risk` to pause and ask; do not invent a chain strategy or an exception.",
570
728
  ].join("\n");
571
729
  }
572
730
 
573
731
  export async function ensureSddPreflight(
574
732
  ctx: ExtensionContext,
575
733
  callbacks: SddPreflightCallbacks,
734
+ resolutionOptions: SddPreflightResolutionOptions = {},
576
735
  ): Promise<SddPreflightPreferences> {
577
736
  const sessionKey = sddPreflightSessionKey(ctx);
578
737
  const existing = sddPreflightBySession.get(sessionKey);
579
- if (existing) return existing;
738
+ if (existing && !(resolutionOptions.promptFields?.length ?? 0)) return existing;
580
739
  const inFlight = sddPreflightInFlight.get(sessionKey);
581
- if (inFlight) return inFlight;
740
+ if (inFlight && !(resolutionOptions.promptFields?.length ?? 0)) return inFlight;
582
741
  const promise = (async () => {
583
742
  const engramAvailable = hasWritableEngramTool(callbacks.pi);
584
- const prefs = await collectSddPreflightPreferences(ctx, engramAvailable);
743
+ const persisted = resolutionOptions.persisted ?? readSddPreflightFromDisk(ctx.cwd);
744
+ const prefs = await collectSddPreflightPreferences(ctx, engramAvailable, {
745
+ ...resolutionOptions,
746
+ persisted,
747
+ });
585
748
  const result =
586
749
  (await callbacks.installAssets?.(ctx.cwd)) ??
587
750
  installSddAssets(ctx.cwd, false);
@@ -598,9 +761,9 @@ export async function ensureSddPreflight(
598
761
  "Gentle AI SDD preflight complete.",
599
762
  `Mode: ${prefs.executionMode}`,
600
763
  `Artifacts: ${prefs.artifactStore}`,
601
- `PR chaining: ${prefs.chainedPrStrategy}`,
764
+ `Delivery strategy: ${prefs.chainedPrStrategy}`,
602
765
  `Review budget: ${prefs.reviewBudgetLines} changed lines`,
603
- `Preference source: ${prefs.prompted ? "user prompt" : "defaults (no interactive UI available)"}`,
766
+ `Preference source: ${prefs.prompted ? "explicit session choice" : "canonical default or persisted preference"}`,
604
767
  `Global SDD assets ready: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s), ${result.skipped} already present.`,
605
768
  modelRoutingLine,
606
769
  ].join("\n"),