gentle-pi 2.2.0 → 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 (130) hide show
  1. package/README.md +51 -80
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-proposal.md +2 -0
  8. package/assets/agents/sdd-research.md +54 -0
  9. package/assets/agents/sdd-status.md +4 -4
  10. package/assets/agents/sdd-tasks.md +2 -3
  11. package/assets/agents/sdd-verify.md +21 -1
  12. package/assets/chains/sdd-full.chain.md +1 -1
  13. package/assets/chains/sdd-verify.chain.md +1 -1
  14. package/assets/orchestrator-delegation.md +54 -248
  15. package/assets/orchestrator-memory.md +2 -0
  16. package/assets/orchestrator.md +22 -38
  17. package/assets/sdd-orchestrator-workflow.md +30 -37
  18. package/assets/support/sdd-status-contract.md +7 -7
  19. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  20. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  21. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  22. package/docs/native-authority-architecture.md +9 -11
  23. package/docs/review-integration.md +27 -373
  24. package/extensions/ask-user-choice.ts +151 -0
  25. package/extensions/gentle-ai.ts +1778 -3673
  26. package/extensions/quiet-tools.ts +515 -32
  27. package/extensions/sdd-init.ts +4 -8
  28. package/lib/gentle-ai-renderer.ts +70 -0
  29. package/lib/model-routing-authority.ts +133 -0
  30. package/lib/native-review-cli.ts +372 -940
  31. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  32. package/lib/review-candidate-view.ts +341 -132
  33. package/lib/review-host-relay.ts +210 -68
  34. package/lib/review-integration-v2.ts +839 -307
  35. package/lib/review-last-event-controller.ts +35 -0
  36. package/lib/sdd-preflight.ts +177 -66
  37. package/lib/sdd-status.ts +66 -111
  38. package/lib/terminal-theme.ts +1 -1
  39. package/package.json +83 -82
  40. package/runtime/gentle-ai-binary.mjs +1 -1
  41. package/runtime/native-review-cli.mjs +322 -890
  42. package/runtime/review-integration-v2.mjs +789 -257
  43. package/runtime/review-relay-contract.mjs +1 -1
  44. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  45. package/scripts/gentle-ai-installer.mjs +75 -22
  46. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  47. package/scripts/test-packed-runner.mjs +4 -7
  48. package/scripts/verify-package-files.mjs +12 -13
  49. package/skills/_shared/review-ledger-contract.md +8 -14
  50. package/skills/chained-pr/SKILL.md +3 -0
  51. package/skills/cognitive-doc-design/SKILL.md +1 -1
  52. package/skills/comment-writer/SKILL.md +1 -1
  53. package/skills/gentle-ai/SKILL.md +7 -74
  54. package/skills/judgment-day/SKILL.md +5 -7
  55. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  56. package/skills/release/SKILL.md +3 -3
  57. package/skills/skill-registry/SKILL.md +1 -1
  58. package/skills/work-unit-commits/SKILL.md +3 -1
  59. package/tests/artifact-language.test.ts +24 -11
  60. package/tests/ask-user-choice.test.ts +264 -0
  61. package/tests/codegraph-tools.test.ts +3 -3
  62. package/tests/crosslane/cross-lane.mjs +15 -1168
  63. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  64. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  65. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  66. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  67. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  68. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  69. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  70. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  71. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  72. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  73. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  74. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  75. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  76. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  77. package/tests/gentle-ai-binary.test.ts +83 -13
  78. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  79. package/tests/gentle-ai-installer.test.ts +137 -47
  80. package/tests/gentle-ai.test.ts +820 -131
  81. package/tests/gentle-theme.test.ts +133 -0
  82. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  83. package/tests/model-routing-authority.test.ts +257 -0
  84. package/tests/native-review-capability-contract.test.ts +28 -1
  85. package/tests/native-review-cli.test.ts +586 -962
  86. package/tests/native-review-consent.test.ts +150 -66
  87. package/tests/native-review-parity-runtime.test.ts +80 -359
  88. package/tests/native-review-parity.test.ts +674 -925
  89. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  90. package/tests/orchestrator-budget.test.ts +132 -88
  91. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  92. package/tests/package-manifest.test.ts +37 -35
  93. package/tests/provider-defect-handoff.test.ts +15 -118
  94. package/tests/quiet-tool-rendering.test.ts +1055 -28
  95. package/tests/review-authority-recovery-docs.test.ts +1 -2
  96. package/tests/review-candidate-view.test.ts +409 -3
  97. package/tests/review-compact-contract.test.ts +4 -30
  98. package/tests/review-controller-lock-status.test.ts +2 -2
  99. package/tests/review-controller-native-recovery.test.ts +363 -1450
  100. package/tests/review-controller-native-routing.test.ts +1251 -5642
  101. package/tests/review-controller-retired-ops.test.ts +1 -1
  102. package/tests/review-controller-workspace-root.test.ts +222 -71
  103. package/tests/review-controller.test.ts +26 -816
  104. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  105. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  106. package/tests/review-gate.test.ts +0 -45
  107. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  108. package/tests/review-host-relay-routing.test.ts +117 -82
  109. package/tests/review-host-relay.test.ts +242 -8
  110. package/tests/review-integration-v2-forward.test.ts +576 -553
  111. package/tests/review-integration-v2.test.ts +239 -165
  112. package/tests/review-last-event-closure.test.ts +408 -0
  113. package/tests/review-ledger-contract.test.ts +97 -35
  114. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  115. package/tests/review-relay-transport-agent.test.ts +119 -46
  116. package/tests/review-snapshot.test.ts +3 -2
  117. package/tests/runtime-harness.mjs +361 -196
  118. package/tests/sdd-agent-tools.test.ts +36 -0
  119. package/tests/sdd-preflight.test.ts +81 -15
  120. package/tests/sdd-status.test.ts +109 -110
  121. package/tests/skill-collision-prefixes.test.ts +5 -8
  122. package/tests/writer-edit-surface-scope.test.ts +230 -0
  123. package/themes/Gentleman-Cute.json +94 -0
  124. package/themes/Gentleman-Sexy.json +92 -0
  125. package/lib/git-commit-transaction.ts +0 -861
  126. package/runtime/git-commit-transaction.mjs +0 -862
  127. package/scripts/run-git-commit-transaction.mjs +0 -35
  128. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  129. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  130. package/tests/git-commit-transaction.test.ts +0 -530
@@ -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
+ }
@@ -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
  }
@@ -495,7 +586,7 @@ export function installSddAssets(
495
586
 
496
587
  export function isSddPreflightTrigger(text: string): boolean {
497
588
  const trimmed = text.trim();
498
- if (/^\/sdd(?:[-:][^\s]*)?(?:\s|$)/i.test(trimmed)) return true;
589
+ if (/^\/(?:gentle-)?sdd(?:[-:][^\s]*)?(?:\s|$)/i.test(trimmed)) return true;
499
590
  if (/[??]\s*$/.test(trimmed)) return false;
500
591
  if (
501
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(
@@ -550,56 +641,67 @@ function hasWritableEngramTool(pi: ExtensionAPI): boolean {
550
641
  }
551
642
  }
552
643
 
553
- function normalizeSddReviewBudget(value: string): number {
554
- const parsed = Number.parseInt(value.trim(), 10);
555
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 400;
556
- }
557
-
558
- async function collectSddPreflightPreferences(
644
+ export async function collectSddPreflightPreferences(
559
645
  ctx: ExtensionContext,
560
646
  engramAvailable: boolean,
647
+ options: SddPreflightResolutionOptions = {},
561
648
  ): Promise<SddPreflightPreferences> {
562
- if (!ctx.hasUI) return { ...DEFAULT_SDD_PREFLIGHT, engramAvailable };
563
- const executionMode = await ctx.ui.select("SDD execution mode", [
564
- "interactive",
565
- "auto",
566
- ]);
567
- const artifactOptions = engramAvailable
568
- ? ["openspec", "engram", "both"]
569
- : ["openspec"];
570
- const artifactStore = await ctx.ui.select("SDD artifact store", artifactOptions);
571
- const chainedPrStrategy = await ctx.ui.select("SDD PR chaining", [
572
- "auto-forecast",
573
- "ask-always",
574
- "single-pr-default",
575
- "force-chained",
576
- ]);
577
- const reviewBudgetLines = normalizeSddReviewBudget(
578
- (await ctx.ui.input("SDD review budget lines", "400")) ?? "400",
579
- );
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;
580
686
  return {
581
- executionMode:
582
- executionMode === "auto" ? "auto" : DEFAULT_SDD_PREFLIGHT.executionMode,
583
- artifactStore:
584
- artifactStore === "engram" || artifactStore === "both"
585
- ? artifactStore
586
- : DEFAULT_SDD_PREFLIGHT.artifactStore,
587
- chainedPrStrategy:
588
- chainedPrStrategy === "ask-always" ||
589
- chainedPrStrategy === "single-pr-default" ||
590
- chainedPrStrategy === "force-chained"
591
- ? chainedPrStrategy
592
- : DEFAULT_SDD_PREFLIGHT.chainedPrStrategy,
593
- 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),
594
691
  engramAvailable,
595
- prompted: true,
692
+ prompted,
693
+ ...(sizeExceptionAccepted ? { sizeExceptionAccepted: true as const } : {}),
596
694
  };
597
695
  }
598
696
 
599
697
  export function renderSddPreflightPrompt(prefs: SddPreflightPreferences): string {
698
+ const deliveryStrategy = normalizeSddChainedPrStrategy(
699
+ prefs.chainedPrStrategy,
700
+ prefs.sizeExceptionAccepted === true,
701
+ );
600
702
  const sourceLine = prefs.prompted
601
- ? "The user already chose these SDD preferences for this Pi session. Reuse them unless the user explicitly changes them."
602
- : "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.";
603
705
  const interactiveRules =
604
706
  prefs.executionMode === "interactive"
605
707
  ? [
@@ -615,25 +717,34 @@ export function renderSddPreflightPrompt(prefs: SddPreflightPreferences): string
615
717
  sourceLine,
616
718
  `- Execution mode: ${prefs.executionMode}`,
617
719
  `- Artifact store: ${prefs.artifactStore}${prefs.engramAvailable ? "" : " (Engram unavailable in this session)"}`,
618
- `- Chained PR strategy: ${prefs.chainedPrStrategy}`,
619
- `- 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`.",
620
725
  ...interactiveRules,
621
- "- 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.",
622
728
  ].join("\n");
623
729
  }
624
730
 
625
731
  export async function ensureSddPreflight(
626
732
  ctx: ExtensionContext,
627
733
  callbacks: SddPreflightCallbacks,
734
+ resolutionOptions: SddPreflightResolutionOptions = {},
628
735
  ): Promise<SddPreflightPreferences> {
629
736
  const sessionKey = sddPreflightSessionKey(ctx);
630
737
  const existing = sddPreflightBySession.get(sessionKey);
631
- if (existing) return existing;
738
+ if (existing && !(resolutionOptions.promptFields?.length ?? 0)) return existing;
632
739
  const inFlight = sddPreflightInFlight.get(sessionKey);
633
- if (inFlight) return inFlight;
740
+ if (inFlight && !(resolutionOptions.promptFields?.length ?? 0)) return inFlight;
634
741
  const promise = (async () => {
635
742
  const engramAvailable = hasWritableEngramTool(callbacks.pi);
636
- 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
+ });
637
748
  const result =
638
749
  (await callbacks.installAssets?.(ctx.cwd)) ??
639
750
  installSddAssets(ctx.cwd, false);
@@ -650,9 +761,9 @@ export async function ensureSddPreflight(
650
761
  "Gentle AI SDD preflight complete.",
651
762
  `Mode: ${prefs.executionMode}`,
652
763
  `Artifacts: ${prefs.artifactStore}`,
653
- `PR chaining: ${prefs.chainedPrStrategy}`,
764
+ `Delivery strategy: ${prefs.chainedPrStrategy}`,
654
765
  `Review budget: ${prefs.reviewBudgetLines} changed lines`,
655
- `Preference source: ${prefs.prompted ? "user prompt" : "defaults (no interactive UI available)"}`,
766
+ `Preference source: ${prefs.prompted ? "explicit session choice" : "canonical default or persisted preference"}`,
656
767
  `Global SDD assets ready: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s), ${result.skipped} already present.`,
657
768
  modelRoutingLine,
658
769
  ].join("\n"),