gentle-pi 2.4.0 → 2.6.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 (227) hide show
  1. package/README.md +292 -25
  2. package/assets/agents/gentle-ai-worker.md +13 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +39 -11
  25. package/assets/orchestrator.md +5 -5
  26. package/assets/sdd-orchestrator-workflow.md +54 -21
  27. package/assets/support/sdd-status-contract.md +34 -90
  28. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  29. package/docs/delegated-verification.md +25 -0
  30. package/docs/telemetry.md +94 -0
  31. package/docs/windows-startup-console-visibility.md +18 -0
  32. package/extensions/ask-user-choice.ts +159 -25
  33. package/extensions/codegraph-tools.ts +95 -5
  34. package/extensions/gentle-agents.ts +1337 -0
  35. package/extensions/gentle-ai.ts +2916 -386
  36. package/extensions/gentle-shell.ts +650 -0
  37. package/extensions/gentle-todo.ts +234 -0
  38. package/extensions/quiet-tools.ts +2 -1
  39. package/extensions/runtime-metrics.ts +130 -0
  40. package/extensions/sdd-init.ts +2 -2
  41. package/extensions/startup-banner.ts +52 -75
  42. package/lib/agent-profiles.ts +550 -0
  43. package/lib/agents-completion-delivery.ts +72 -0
  44. package/lib/agents-config.ts +315 -0
  45. package/lib/agents-history.ts +88 -0
  46. package/lib/agents-messaging.ts +187 -0
  47. package/lib/agents-protocol.ts +501 -0
  48. package/lib/agents-runner.ts +1012 -0
  49. package/lib/agents-thread-view.ts +57 -0
  50. package/lib/agents-transcript.ts +87 -0
  51. package/lib/agents-view-layout.ts +40 -0
  52. package/lib/agents-view.ts +914 -0
  53. package/lib/agents-widget.ts +241 -0
  54. package/lib/gentle-ai-binary.ts +3 -1
  55. package/lib/gentle-ai-renderer.ts +143 -25
  56. package/lib/native-choice-list.ts +194 -0
  57. package/lib/native-fullscreen-interaction.ts +47 -0
  58. package/lib/native-pointer-region.ts +164 -0
  59. package/lib/native-review-cli.ts +371 -13
  60. package/lib/orchestrator-presence.ts +337 -0
  61. package/lib/profiles-orchestrator.ts +203 -0
  62. package/lib/review-candidate-view-owner.ts +427 -0
  63. package/lib/review-candidate-view.ts +150 -48
  64. package/lib/review-consent-component.ts +247 -0
  65. package/lib/review-consent-ui.ts +110 -0
  66. package/lib/review-host-relay.ts +28 -0
  67. package/lib/review-integration-v2.ts +243 -11
  68. package/lib/review-last-event-controller.ts +8 -4
  69. package/lib/review-relay-contract.ts +11 -0
  70. package/lib/review-reminder-receipt.ts +74 -0
  71. package/lib/review-repository.ts +2 -2
  72. package/lib/review-risk-assessment.ts +339 -0
  73. package/lib/review-session-standing-permission-ipc.ts +309 -0
  74. package/lib/review-session-standing-permission.ts +240 -0
  75. package/lib/runtime-metrics-children.ts +199 -0
  76. package/lib/runtime-metrics-delivery.ts +68 -0
  77. package/lib/runtime-metrics-native.ts +166 -0
  78. package/lib/runtime-metrics-pi-identity.ts +113 -0
  79. package/lib/runtime-metrics-policy.ts +51 -0
  80. package/lib/runtime-metrics.ts +255 -0
  81. package/lib/sdd-preflight.ts +362 -81
  82. package/lib/sdd-research-capabilities.ts +228 -0
  83. package/lib/sdd-status.ts +29 -7
  84. package/lib/session-worktree-registry.ts +118 -0
  85. package/lib/shell-bar.ts +184 -0
  86. package/lib/shell-card.ts +133 -0
  87. package/lib/shell-changes-view.ts +530 -0
  88. package/lib/shell-changes.ts +290 -0
  89. package/lib/shell-gauge.ts +40 -0
  90. package/lib/shell-prompt.ts +115 -0
  91. package/lib/shell-sidebar-banner.ts +11 -0
  92. package/lib/shell-sidebar-layout.ts +213 -0
  93. package/lib/shell-sidebar.ts +41 -0
  94. package/lib/shell-todo.ts +297 -0
  95. package/lib/shell-usage-view.ts +76 -0
  96. package/lib/shell-usage.ts +246 -0
  97. package/lib/telemetry-trigger.ts +153 -0
  98. package/package.json +8 -5
  99. package/runtime/gentle-ai-binary.mjs +3 -1
  100. package/runtime/native-review-cli.mjs +370 -12
  101. package/runtime/review-integration-v2.mjs +243 -11
  102. package/runtime/review-relay-contract.mjs +11 -0
  103. package/runtime/review-risk-assessment.mjs +340 -0
  104. package/runtime/telemetry-trigger.mjs +154 -0
  105. package/scripts/build-runtime-modules.mjs +11 -1
  106. package/scripts/check-types.mjs +125 -0
  107. package/scripts/gentle-ai-installer.mjs +10 -10
  108. package/scripts/install-gentle-ai.mjs +12 -0
  109. package/scripts/install-tui-mode-setting.mjs +114 -0
  110. package/scripts/test-packed-runner.mjs +38 -2
  111. package/scripts/types-baseline.json +99 -0
  112. package/scripts/verify-package-files.mjs +8 -2
  113. package/skills/_shared/review-ledger-contract.md +20 -2
  114. package/skills/issue-creation/SKILL.md +3 -3
  115. package/skills/judgment-day/SKILL.md +17 -3
  116. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  117. package/tests/agent-profiles.test.ts +722 -0
  118. package/tests/agents-completion-delivery.test.ts +94 -0
  119. package/tests/agents-config.test.ts +205 -0
  120. package/tests/agents-fake-child.ts +66 -0
  121. package/tests/agents-grouping.test.ts +179 -0
  122. package/tests/agents-history.test.ts +54 -0
  123. package/tests/agents-integration.test.ts +100 -0
  124. package/tests/agents-messaging.test.ts +94 -0
  125. package/tests/agents-protocol.test.ts +198 -0
  126. package/tests/agents-queries.test.ts +190 -0
  127. package/tests/agents-responsive.test.ts +43 -0
  128. package/tests/agents-runner-process.test.ts +111 -0
  129. package/tests/agents-runner.test.ts +959 -0
  130. package/tests/agents-thread-view.test.ts +45 -0
  131. package/tests/agents-transcript.test.ts +30 -0
  132. package/tests/agents-view.test.ts +685 -0
  133. package/tests/agents-widget.test.ts +141 -0
  134. package/tests/artifact-language.test.ts +25 -2
  135. package/tests/ask-user-choice.test.ts +325 -5
  136. package/tests/asset-installation-runtime.test.ts +108 -0
  137. package/tests/autonomous-guard.test.ts +116 -1
  138. package/tests/codegraph-tools.test.ts +112 -2
  139. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  140. package/tests/devbinary/native-review-parity.devtest.ts +110 -0
  141. package/tests/feature-request-form.test.ts +67 -0
  142. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  143. package/tests/fixtures/agents-process-child.mjs +23 -0
  144. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  145. package/tests/gentle-agents.test.ts +2168 -0
  146. package/tests/gentle-ai-binary.test.ts +7 -2
  147. package/tests/gentle-ai-installer.test.ts +47 -47
  148. package/tests/gentle-ai-renderer.test.ts +103 -0
  149. package/tests/gentle-ai.test.ts +971 -15
  150. package/tests/gentle-card-text.ts +35 -0
  151. package/tests/gentle-shell.test.ts +818 -0
  152. package/tests/gentle-todo.test.ts +226 -0
  153. package/tests/install-tui-mode-setting.test.ts +324 -0
  154. package/tests/issue-creation-skill.test.ts +22 -0
  155. package/tests/model-routing-authority.test.ts +12 -0
  156. package/tests/native-choice-list.test.ts +202 -0
  157. package/tests/native-fullscreen-interaction.test.ts +125 -0
  158. package/tests/native-pointer-region.test.ts +245 -0
  159. package/tests/native-review-capability-contract.test.ts +27 -1
  160. package/tests/native-review-cli.test.ts +317 -3
  161. package/tests/native-review-consent.test.ts +91 -0
  162. package/tests/native-review-parity-runtime.test.ts +8 -2
  163. package/tests/native-review-parity.test.ts +43 -29
  164. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  165. package/tests/orchestrator-budget.test.ts +69 -0
  166. package/tests/orchestrator-presence.test.ts +389 -0
  167. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  168. package/tests/package-manifest.test.ts +243 -7
  169. package/tests/profiles-orchestrator.test.ts +208 -0
  170. package/tests/quiet-tool-rendering.test.ts +97 -37
  171. package/tests/rdd-aware-verification-contract.test.ts +226 -0
  172. package/tests/rdd-status-line.test.ts +286 -0
  173. package/tests/review-agent-end-preflight.test.ts +332 -24
  174. package/tests/review-candidate-view.test.ts +751 -7
  175. package/tests/review-consent-ui.test.ts +352 -0
  176. package/tests/review-contract-prompt.test.ts +17 -0
  177. package/tests/review-controller-native-recovery.test.ts +29 -4
  178. package/tests/review-controller-native-routing.test.ts +884 -7
  179. package/tests/review-controller-workspace-root.test.ts +45 -2
  180. package/tests/review-controller.test.ts +26 -1
  181. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  182. package/tests/review-host-relay-routing.test.ts +384 -8
  183. package/tests/review-host-relay.test.ts +29 -0
  184. package/tests/review-integration-v2-forward.test.ts +44 -0
  185. package/tests/review-integration-v2.test.ts +276 -0
  186. package/tests/review-last-event-closure.test.ts +112 -3
  187. package/tests/review-ledger-contract.test.ts +61 -6
  188. package/tests/review-relay-contract.test.ts +26 -0
  189. package/tests/review-reminder-receipt.test.ts +62 -0
  190. package/tests/review-repository.test.ts +28 -1
  191. package/tests/review-risk-assessment.test.ts +626 -0
  192. package/tests/review-session-standing-permission-controller.test.ts +656 -0
  193. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  194. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  195. package/tests/review-session-standing-permission.test.ts +156 -0
  196. package/tests/runtime-harness.mjs +447 -39
  197. package/tests/runtime-metrics-children.test.ts +206 -0
  198. package/tests/runtime-metrics-delivery.test.ts +85 -0
  199. package/tests/runtime-metrics-extension.test.ts +187 -0
  200. package/tests/runtime-metrics-native.test.ts +209 -0
  201. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  202. package/tests/runtime-metrics-policy.test.ts +62 -0
  203. package/tests/runtime-metrics.test.ts +184 -0
  204. package/tests/sdd-agent-tools.test.ts +10 -1
  205. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  206. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  207. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  208. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  209. package/tests/sdd-preflight.test.ts +252 -8
  210. package/tests/sdd-research-capabilities.test.ts +256 -0
  211. package/tests/sdd-research-live.test.ts +241 -0
  212. package/tests/sdd-selection-transport.test.ts +504 -0
  213. package/tests/sdd-status.test.ts +51 -0
  214. package/tests/session-worktree-registry.test.ts +135 -0
  215. package/tests/shell-bar.test.ts +176 -0
  216. package/tests/shell-card.test.ts +139 -0
  217. package/tests/shell-changes-view.test.ts +609 -0
  218. package/tests/shell-changes.test.ts +350 -0
  219. package/tests/shell-prompt.test.ts +140 -0
  220. package/tests/shell-sidebar-banner.test.ts +23 -0
  221. package/tests/shell-sidebar-layout.test.ts +387 -0
  222. package/tests/shell-sidebar.test.ts +50 -0
  223. package/tests/shell-todo.test.ts +259 -0
  224. package/tests/shell-usage-view.test.ts +62 -0
  225. package/tests/shell-usage.test.ts +197 -0
  226. package/tests/startup-banner.test.ts +126 -0
  227. package/tests/telemetry-trigger.test.ts +351 -0
@@ -11,8 +11,10 @@ import { matchesKey } from "@earendil-works/pi-tui";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { stripAnsi } from "../lib/terminal-theme.ts";
13
13
  import { domainHashV1 } from "../lib/review-canonical.ts";
14
+ import { canonicalHash } from "../lib/review-transaction.ts";
14
15
 
15
16
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
17
+ const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
16
18
  const EXTENSIONS = [
17
19
  "extensions/gentle-ai.ts",
18
20
  "extensions/quiet-tools.ts",
@@ -29,6 +31,8 @@ const EXPECTED_BANNER_COMMANDS = [
29
31
  ];
30
32
 
31
33
  const EXPECTED_COMMANDS = [
34
+ "gentle:install-delegation",
35
+ "gentle:install-review",
32
36
  "gentle:install-sdd",
33
37
  "gentle:sdd-preflight",
34
38
  "gentle-sdd-status",
@@ -43,6 +47,7 @@ const EXPECTED_COMMANDS = [
43
47
  ];
44
48
 
45
49
  const FORBIDDEN_COMPAT_COMMANDS = [
50
+ "gentle:install-assets",
46
51
  // The SDD entry points carry the gentle- prefix so they read identically in
47
52
  // Claude Code and Pi. The bare names are retired without an alias.
48
53
  "sdd-init",
@@ -308,7 +313,6 @@ async function run() {
308
313
  },
309
314
  };
310
315
  const lastEventPi = createPi();
311
- const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
312
316
  createGentleAiExtension({ nativeReviewCli })(lastEventPi.pi);
313
317
  const controller = lastEventPi.tools.get("gentle_review");
314
318
  const capture = lastEventPi.tools.get("gentle_review_capture");
@@ -489,11 +493,19 @@ async function run() {
489
493
  assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
490
494
  const statusCtx = createCtx(promptCwd, true);
491
495
  await commands.get("gentle-sdd-status").handler("status-demo --json", statusCtx);
492
- assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-pi\.sdd-status"/);
496
+ assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-ai\.sdd-status"/);
493
497
  const continueCtx = createCtx(promptCwd, true);
498
+ let markerConfirmations = 0;
499
+ continueCtx.ui.confirm = async (_title, message) => {
500
+ assert.ok(message.includes(join(promptCwd, "openspec/changes/status-demo/.gentle-ai-instance")));
501
+ assert.match(message, /no source roots and no persistent authority/);
502
+ markerConfirmations += 1;
503
+ return true;
504
+ };
494
505
  await commands.get("gentle-sdd-continue").handler("status-demo", continueCtx);
495
- assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Dispatcher/);
496
- assert.match(continueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
506
+ assert.equal(markerConfirmations, 1);
507
+ assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Status Engine/);
508
+ assert.match(continueCtx.ui.notifications.at(-1).message, /"nextRecommended": "apply"/);
497
509
  const { execFileSync } = await import("node:child_process");
498
510
  execFileSync("git", ["init"], { cwd: promptCwd, stdio: "ignore" });
499
511
  const recoveryRequiredDirectory = join(promptCwd, ".git", "gentle-ai", "reviews", "control", "recovery-required-v1");
@@ -505,7 +517,7 @@ async function run() {
505
517
  const blockedContinueCtx = createCtx(promptCwd, true);
506
518
  await commands.get("gentle-sdd-continue").handler("status-demo", blockedContinueCtx);
507
519
  assert.doesNotMatch(blockedContinueCtx.ui.notifications.at(-1).message, /resolve-review:/);
508
- assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
520
+ assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /"nextRecommended": "apply"/);
509
521
  } finally {
510
522
  await rm(promptCwd, { recursive: true, force: true });
511
523
  }
@@ -583,6 +595,124 @@ async function run() {
583
595
  undefined,
584
596
  "a writer may dispatch when context carries narrow task-scoped repository-relative paths",
585
597
  );
598
+
599
+ const interactiveSddDispatch = {
600
+ agent: "sdd-remediate",
601
+ task: "Correct the bound failed verification evidence.",
602
+ context: "Retain the parent-supplied remediation scope.",
603
+ mode: "task",
604
+ };
605
+ assert.equal(
606
+ await toolHook({ toolName: "subagent_run", input: interactiveSddDispatch }, createCtx(toolCwd, true, "interactive-sdd-parent")),
607
+ undefined,
608
+ "an interactive parent may dispatch an SDD child only after preflight resolves",
609
+ );
610
+ assert.match(
611
+ interactiveSddDispatch.context,
612
+ /^## SDD Session Preflight\nThese SDD preferences are explicit current-session choices\./,
613
+ "the parent-confirmed rendered preflight must be transported through the RPC child's context",
614
+ );
615
+ const rpcChildCwd = await tempWorkspace();
616
+ try {
617
+ const rpcChild = createPi();
618
+ createGentleAiExtension({ processEnv: { GENTLE_PI_AGENTS_CHILD: "1" } })(rpcChild.pi);
619
+ const rpcChildCtx = createCtx(rpcChildCwd, false, "delegated-rpc-sdd-child");
620
+ rpcChildCtx.mode = "rpc";
621
+ const rpcChildPrompt = await rpcChild.hooks.get("before_agent_start")[0](
622
+ { agentName: "sdd-remediate", systemPrompt: "You are the SDD remediate executor for Gentle AI." },
623
+ rpcChildCtx,
624
+ );
625
+ assert.doesNotMatch(rpcChildPrompt.systemPrompt, /## SDD Session Preflight/);
626
+ assert.equal(
627
+ existsSync(join(rpcChildCwd, ".pi", "gentle-ai", "sdd-preflight.json")),
628
+ false,
629
+ "a delegated RPC child must not silently originate or persist defaults",
630
+ );
631
+ } finally {
632
+ await rm(rpcChildCwd, { recursive: true, force: true });
633
+ }
634
+
635
+ const canonicalFrozenFindingRow = '{"id":"JD-A-001","lens":"judgment-day","location":"extensions/gentle-ai.ts:1","severity":"CRITICAL","status_at_freeze":"open","evidence_class":"deterministic","evidence_claim":"The frozen finding has concrete user impact."}';
636
+ const canonicalFrozenFindingRows = [JSON.parse(canonicalFrozenFindingRow)];
637
+ const canonicalFrozenLedgerHash = canonicalHash(canonicalFrozenFindingRows);
638
+ const canonicalJdFixTask = [
639
+ "Apply the controller-authorized fix.",
640
+ "",
641
+ "## Judgment Day activation",
642
+ "User explicitly requested Judgment Day.",
643
+ "",
644
+ "## Exact authorized severe IDs",
645
+ "- `JD-A-001`",
646
+ "",
647
+ "## Judgment Day correction batch",
648
+ "Round: 1 of 2.",
649
+ `Frozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\``,
650
+ "",
651
+ "## Exact frozen finding rows",
652
+ canonicalFrozenFindingRow,
653
+ "",
654
+ "## Allowed edit surfaces",
655
+ "extensions/gentle-ai.ts",
656
+ "tests/runtime-harness.mjs",
657
+ ].join("\n");
658
+ const orderedFrozenFindingRows = [
659
+ ...canonicalFrozenFindingRows,
660
+ { ...canonicalFrozenFindingRows[0], id: "JD-B-002", location: "tests/runtime-harness.mjs:1" },
661
+ ];
662
+ const reversedFrozenFindingRows = [...orderedFrozenFindingRows].reverse();
663
+ const rowOrderMismatchJdFixTask = canonicalJdFixTask
664
+ .replace("- `JD-A-001`", "- `JD-A-001`\n- `JD-B-002`")
665
+ .replace(canonicalFrozenLedgerHash, canonicalHash(reversedFrozenFindingRows))
666
+ .replace(canonicalFrozenFindingRow, reversedFrozenFindingRows.map((row) => JSON.stringify(row)).join("\n"));
667
+ const incorrectFrozenLedgerHash = `${canonicalFrozenLedgerHash.slice(0, -1)}${canonicalFrozenLedgerHash.endsWith("0") ? "1" : "0"}`;
668
+ for (const [label, input] of [
669
+ ["missing activation", { agent: "jd-fix-agent", task: "## Allowed edit surfaces\nextensions/gentle-ai.ts", mode: "task" }],
670
+ ["duplicate activation", { agent: "jd-fix-agent", task: `${canonicalJdFixTask}\n\n## Judgment Day activation\nUser explicitly requested Judgment Day.`, mode: "task" }],
671
+ ["missing severe IDs", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("## Exact authorized severe IDs\n- `JD-A-001`\n", ""), mode: "task" }],
672
+ ["duplicate severe ID bindings", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("- `JD-A-001`", "- `JD-A-001`\n- `JD-A-001`"), mode: "task" }],
673
+ ["duplicate severe ID sections", { agent: "jd-fix-agent", task: canonicalJdFixTask, context: "## Exact authorized severe IDs\n- `JD-B-002`", mode: "task" }],
674
+ ["malformed activation", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("User explicitly requested Judgment Day.", "Judgment Day is explicitly activated for this dispatch."), mode: "task" }],
675
+ ["extra activation line", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("User explicitly requested Judgment Day.", "User explicitly requested Judgment Day.\nUnexpected activation detail."), mode: "task" }],
676
+ ["malformed severe ID", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("- `JD-A-001`", "JD-A-001"), mode: "task" }],
677
+ ["empty severe IDs", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("- `JD-A-001`", ""), mode: "task" }],
678
+ ["missing correction batch", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(`## Judgment Day correction batch\nRound: 1 of 2.\nFrozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\`\n`, ""), mode: "task" }],
679
+ ["duplicate correction batch", { agent: "jd-fix-agent", task: `${canonicalJdFixTask}\n## Judgment Day correction batch\nRound: 2 of 2.\nFrozen ledger SHA-256: \`bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\``, mode: "task" }],
680
+ ["out-of-order correction batch", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(`## Judgment Day correction batch\nRound: 1 of 2.\nFrozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\`\n\n## Exact frozen finding rows\n${canonicalFrozenFindingRow}`, `## Exact frozen finding rows\n${canonicalFrozenFindingRow}\n\n## Judgment Day correction batch\nRound: 1 of 2.\nFrozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\``), mode: "task" }],
681
+ ["invalid correction round", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("Round: 1 of 2.", "Round: 3 of 2."), mode: "task" }],
682
+ ["uppercase frozen ledger hash", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenLedgerHash, canonicalFrozenLedgerHash.toUpperCase()), mode: "task" }],
683
+ ["incorrect frozen ledger hash", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenLedgerHash, incorrectFrozenLedgerHash), mode: "task" }],
684
+ ["extra correction batch line", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("Round: 1 of 2.", "Round: 1 of 2.\nUnexpected line"), mode: "task" }],
685
+ ["missing frozen rows", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(`## Exact frozen finding rows\n${canonicalFrozenFindingRow}\n`, ""), mode: "task" }],
686
+ ["malformed frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, "{"), mode: "task" }],
687
+ ["duplicate frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, `${canonicalFrozenFindingRow}\n${canonicalFrozenFindingRow}`), mode: "task" }],
688
+ ["mismatched frozen row ID", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, canonicalFrozenFindingRow.replace("JD-A-001", "JD-B-002")), mode: "task" }],
689
+ ["frozen row order mismatch", { agent: "jd-fix-agent", task: rowOrderMismatchJdFixTask, mode: "task" }],
690
+ ["extra frozen row field", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, canonicalFrozenFindingRow.replace("}", ",\"extra\":true}")), mode: "task" }],
691
+ ["non-Judgment-Day frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"lens\":\"judgment-day\"", "\"lens\":\"review-risk\""), mode: "task" }],
692
+ ["non-open frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"status_at_freeze\":\"open\"", "\"status_at_freeze\":\"closed\""), mode: "task" }],
693
+ ["non-severe frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"severity\":\"CRITICAL\"", "\"severity\":\"WARNING\""), mode: "task" }],
694
+ ["empty frozen evidence", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"evidence_claim\":\"The frozen finding has concrete user impact.\"", "\"evidence_claim\":\"\""), mode: "task" }],
695
+ ["missing edit surface", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs", ""), mode: "task" }],
696
+ ["invalid edit surface", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("## Allowed edit surfaces\nextensions/gentle-ai.ts", "## Allowed edit surfaces\n."), mode: "task" }],
697
+ ["mixed", { agent: ["jd-fix-agent", "gentle-ai-worker"], task: canonicalJdFixTask, mode: "task" }],
698
+ ["agent array", { agent: ["jd-fix-agent"], task: canonicalJdFixTask, mode: "task" }],
699
+ ["agents array", { agents: ["jd-fix-agent"], task: canonicalJdFixTask, mode: "task" }],
700
+ ["duplicate agent binding", { agent: "jd-fix-agent", agents: "jd-fix-agent", task: canonicalJdFixTask, mode: "task" }],
701
+ ]) {
702
+ const original = structuredClone(input);
703
+ const result = await toolHook({ toolName: "subagent_run", input }, createCtx(toolCwd));
704
+ assert.equal(result?.block, true, `${label} jd-fix-agent dispatch must be blocked`);
705
+ assert.match(result?.reason ?? "", /Judgment Day fix dispatch/i);
706
+ assert.deepEqual(input, original, `${label} jd-fix-agent rejection must not mutate input`);
707
+ }
708
+ assert.equal(
709
+ await toolHook(
710
+ { toolName: "subagent_run", input: { agent: "jd-fix-agent", task: canonicalJdFixTask, mode: "task" } },
711
+ createCtx(toolCwd),
712
+ ),
713
+ undefined,
714
+ "a canonical explicit Judgment Day fix dispatch with narrow edit surfaces may run",
715
+ );
586
716
  for (const [label, input] of [
587
717
  [
588
718
  "valid scope followed by a repository-root scope",
@@ -883,7 +1013,10 @@ async function run() {
883
1013
  }
884
1014
 
885
1015
  const noUiCwd = await tempWorkspace();
1016
+ const startupAgentHome = join(noUiCwd, "agent-home");
1017
+ process.env.GENTLE_PI_AGENT_HOME = startupAgentHome;
886
1018
  try {
1019
+ const globalAgentHome = startupAgentHome;
887
1020
  for (const handler of hooks.get("session_start")) {
888
1021
  await handler({ reason: "startup" }, createCtx(noUiCwd, false));
889
1022
  }
@@ -897,7 +1030,15 @@ async function run() {
897
1030
  false,
898
1031
  "session_start must not install project-local SDD chains",
899
1032
  );
900
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1033
+ assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
1034
+ assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), false);
1035
+ assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support")), false);
1036
+ for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1037
+ const ctx = createCtx(noUiCwd, true);
1038
+ await commands.get(diagnostic).handler("", ctx);
1039
+ assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets: on demand/);
1040
+ assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
1041
+ }
901
1042
  // gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
902
1043
  // installation must not (re)create them.
903
1044
  const installedRefuterPath = join(globalAgentHome, "agents", "review-refuter.md");
@@ -920,6 +1061,8 @@ async function run() {
920
1061
  );
921
1062
  assert.match(installedRiskSource, /exactly once against the supplied `initial_review_tree`/);
922
1063
  assert.match(installedRiskSource, /cannot authorize transitions, fixes, receipts, gates, or delivery/);
1064
+ await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-install"));
1065
+ assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
923
1066
  assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
924
1067
  assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
925
1068
  const managedAssetsManifestPath = join(globalAgentHome, "gentle-ai", "managed-assets.json");
@@ -968,10 +1111,24 @@ async function run() {
968
1111
  for (const handler of hooks.get("session_start")) {
969
1112
  await handler({ reason: "startup" }, createCtx(noUiCwd, false));
970
1113
  }
1114
+ for (const [key, stale] of [
1115
+ ["agents/sdd-apply.md", previousManagedApply],
1116
+ ["chains/sdd-full.chain.md", previousManagedChain],
1117
+ ["gentle-ai/support/sdd-status-contract.md", previousManagedSupport],
1118
+ ]) {
1119
+ assert.equal(await readFile(join(globalAgentHome, key), "utf8"), stale,
1120
+ "startup must preserve existing SDD package content without explicit routing");
1121
+ }
1122
+ assert.equal(existsSync(retiredManagedValidatorPath), false, "review retirement still runs at startup");
1123
+ const driftCtx = createCtx(noUiCwd, true);
1124
+ await commands.get("gentle:status").handler("", driftCtx);
1125
+ assert.match(driftCtx.ui.notifications.at(-1).message, /Global SDD assets stale: 3 file\(s\).*install-sdd --force/);
1126
+ assert.doesNotMatch(driftCtx.ui.notifications.at(-1).message, /install-(delegation|review) --force/);
1127
+ await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-refresh"));
971
1128
  assert.notEqual(
972
1129
  await readFile(join(globalAgentHome, "agents", "sdd-apply.md"), "utf8"),
973
1130
  "stale global apply\n",
974
- "session_start must refresh stale global SDD agents",
1131
+ "SDD preflight must refresh stale global SDD agents",
975
1132
  );
976
1133
  assert.equal(
977
1134
  await readFile(installedRefuterPath, "utf8"),
@@ -981,12 +1138,12 @@ async function run() {
981
1138
  assert.notEqual(
982
1139
  await readFile(join(globalAgentHome, "chains", "sdd-full.chain.md"), "utf8"),
983
1140
  "stale global chain\n",
984
- "session_start must refresh stale global SDD chains",
1141
+ "SDD preflight must refresh stale global SDD chains",
985
1142
  );
986
1143
  assert.notEqual(
987
1144
  await readFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), "utf8"),
988
1145
  "stale global status contract\n",
989
- "session_start must refresh stale global SDD support files",
1146
+ "SDD preflight must refresh stale global SDD support files",
990
1147
  );
991
1148
  assert.equal(
992
1149
  await readFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "utf8"),
@@ -1022,11 +1179,16 @@ async function run() {
1022
1179
  "a user-authored same-path retired asset must stay unowned",
1023
1180
  );
1024
1181
  } finally {
1182
+ process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1025
1183
  await rm(noUiCwd, { recursive: true, force: true });
1026
1184
  }
1027
1185
 
1028
1186
  const lazySddCwd = await tempWorkspace();
1187
+ const lazyAgentHome = join(lazySddCwd, "agent-home");
1188
+ process.env.GENTLE_PI_AGENT_HOME = lazyAgentHome;
1029
1189
  try {
1190
+ // This scenario must not inherit SDD definitions from earlier startup tests.
1191
+ const globalAgentHome = lazyAgentHome;
1030
1192
  await writeFile(
1031
1193
  globalModelsPath,
1032
1194
  JSON.stringify({ "sdd-apply": { model: "openai/gpt-5", thinking: "high" } }, null, 2),
@@ -1082,6 +1244,7 @@ async function run() {
1082
1244
  { action: "continue" },
1083
1245
  );
1084
1246
  assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
1247
+ assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
1085
1248
 
1086
1249
  assert.deepEqual(
1087
1250
  await inputHook({ text: "vamos con sdd", source: "interactive" }, ctx),
@@ -1094,13 +1257,13 @@ async function run() {
1094
1257
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
1095
1258
  assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1096
1259
  assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1097
- assert.equal(ctx.ui.selections.length, 0, "automatic SDD triggers must not render confirmation-only selectors");
1098
- assert.match(ctx.ui.notifications.at(-1).message, /Preference source: canonical default or persisted preference/);
1260
+ assert.equal(ctx.ui.selections.length, 1, "first interactive SDD trigger confirms session suggestions");
1261
+ assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
1099
1262
  assert.deepEqual(
1100
1263
  await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
1101
1264
  { action: "continue" },
1102
1265
  );
1103
- assert.equal(ctx.ui.selections.length, 0, "natural SDD triggers reuse preferences without prompts");
1266
+ assert.equal(ctx.ui.selections.length, 1, "natural SDD triggers reuse confirmed session choices");
1104
1267
  assert.deepEqual(
1105
1268
  await inputHook({ text: "/sdd", source: "interactive" }, ctx),
1106
1269
  { action: "continue" },
@@ -1113,7 +1276,7 @@ async function run() {
1113
1276
  await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
1114
1277
  { action: "continue" },
1115
1278
  );
1116
- assert.equal(ctx.ui.selections.length, 0, "slash SDD triggers use automatic defaults without prompts");
1279
+ assert.equal(ctx.ui.selections.length, 1, "slash SDD triggers reuse confirmed session choices");
1117
1280
 
1118
1281
  assert.deepEqual(
1119
1282
  await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
@@ -1141,14 +1304,14 @@ async function run() {
1141
1304
  "global SDD model routing must be materialized in agent frontmatter, not project settings overrides",
1142
1305
  );
1143
1306
  }
1144
- assert.equal(ctx.ui.selections.length, 0, "automatic SDD routing must not render confirmation-only selectors");
1145
- assert.match(ctx.ui.notifications.at(-1).message, /Preference source: canonical default or persisted preference/);
1307
+ assert.equal(ctx.ui.selections.length, 1, "automatic SDD routing reuses session confirmation");
1308
+ assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
1146
1309
  await commands.get("gentle:status").handler("", ctx);
1147
1310
  assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
1148
1311
  assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
1149
1312
 
1150
1313
  await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
1151
- assert.equal(ctx.ui.selections.length, 0, "automatic preflight should remain prompt-free for the session");
1314
+ assert.equal(ctx.ui.selections.length, 1, "resolved preflight must not prompt again in this session");
1152
1315
  const promptHook = hooks.get("before_agent_start")[0];
1153
1316
  const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
1154
1317
  assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
@@ -1163,6 +1326,7 @@ async function run() {
1163
1326
  "non-SDD subagents must not receive parent harness or SDD preflight prompts",
1164
1327
  );
1165
1328
  } finally {
1329
+ process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1166
1330
  await rm(lazySddCwd, { recursive: true, force: true });
1167
1331
  await rm(globalModelsPath, { force: true });
1168
1332
  }
@@ -1177,23 +1341,63 @@ async function run() {
1177
1341
  });
1178
1342
  assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
1179
1343
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1180
- assert.equal(ctx.ui.selections.length, 0, `${text} should use automatic preflight without selectors`);
1344
+ assert.equal(ctx.ui.selections.length, 1, `${text} must confirm the fresh interactive session`);
1181
1345
  } finally {
1182
1346
  await rm(slashSddCwd, { recursive: true, force: true });
1183
1347
  }
1184
1348
  }
1185
1349
 
1350
+ const commandPreflightCwd = await tempWorkspace();
1351
+ try {
1352
+ const command = commands.get("gentle:sdd-preflight");
1353
+ const interactive = createCtx(commandPreflightCwd, true, "explicit-command-regression");
1354
+ await command.handler("", interactive);
1355
+ const first = interactive.ui.selections.length;
1356
+ await command.handler("", interactive);
1357
+ const reused = interactive.ui.selections.length;
1358
+ const select = interactive.ui.select;
1359
+ interactive.ui.select = async (label, options) => {
1360
+ const value = await select(label, options);
1361
+ return label === "SDD execution mode" ? "interactive" : value;
1362
+ };
1363
+ interactive.ui.input = async () => "650";
1364
+ await command.handler("--edit", interactive);
1365
+ const edited = interactive.ui.selections.length;
1366
+ await command.handler("", interactive);
1367
+ assert.equal(interactive.ui.selections.length, edited, "edited choices also reuse without prompting");
1368
+ const saved = JSON.parse(await readFile(join(commandPreflightCwd, ".pi", "gentle-ai", "sdd-preflight.json"), "utf8"));
1369
+ assert.equal(saved.executionMode, "interactive");
1370
+ assert.equal(saved.reviewBudgetLines, 650);
1371
+ const rpc = createCtx(commandPreflightCwd, true, "explicit-command-rpc-regression");
1372
+ rpc.mode = "rpc";
1373
+ let rpcInputs = 0;
1374
+ rpc.ui.input = async (_label, placeholder) => { rpcInputs += 1; return placeholder; };
1375
+ await command.handler("", rpc);
1376
+ await command.handler("--edit", rpc);
1377
+ const noUi = createCtx(commandPreflightCwd, false, "explicit-command-no-ui-regression");
1378
+ let noUiInputs = 0;
1379
+ noUi.ui.input = async (_label, placeholder) => { noUiInputs += 1; return placeholder; };
1380
+ await command.handler("", noUi);
1381
+ await command.handler("--edit", noUi);
1382
+ assert.deepEqual({ first, reused, edited, rpc: rpc.ui.selections.length, noUi: noUi.ui.selections.length },
1383
+ { first: 1, reused: 1, edited: 3, rpc: 0, noUi: 0 },
1384
+ "explicit command confirms once, reuses unless --edit, and never prompts headless callers");
1385
+ assert.deepEqual({ rpcInputs, noUiInputs }, { rpcInputs: 0, noUiInputs: 0 });
1386
+ } finally {
1387
+ await rm(commandPreflightCwd, { recursive: true, force: true });
1388
+ }
1389
+
1186
1390
  const commandSddCwd = await tempWorkspace();
1187
1391
  try {
1188
1392
  const ctx = createCtx(commandSddCwd, true, "command-session");
1189
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1393
+ await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1190
1394
  assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
1191
1395
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1192
1396
  assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts intentional choice fields");
1193
1397
  assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "one-option artifact store must be elided");
1194
1398
  assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
1195
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1196
- assert.equal(ctx.ui.selections.length, 4, "explicit preflight remains an intentional re-prompt");
1399
+ await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1400
+ assert.equal(ctx.ui.selections.length, 4, "--edit remains an intentional re-prompt");
1197
1401
  } finally {
1198
1402
  await rm(commandSddCwd, { recursive: true, force: true });
1199
1403
  }
@@ -1212,7 +1416,7 @@ async function run() {
1212
1416
  assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
1213
1417
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1214
1418
  assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1215
- assert.equal(ctx.ui.selections.length, 0, "automatic SDD-agent startup must not render selectors");
1419
+ assert.equal(ctx.ui.selections.length, 1, "fresh interactive SDD-agent startup requires confirmation");
1216
1420
  assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
1217
1421
  assert.doesNotMatch(
1218
1422
  promptResult.systemPrompt,
@@ -1233,7 +1437,7 @@ async function run() {
1233
1437
  },
1234
1438
  ctx,
1235
1439
  );
1236
- assert.equal(ctx.ui.selections.length, 0, "SDD-agent startup should reuse automatic preferences");
1440
+ assert.equal(ctx.ui.selections.length, 1, "SDD-agent startup reuses confirmed session preferences");
1237
1441
  assert.doesNotMatch(
1238
1442
  reusedPromptResult.systemPrompt,
1239
1443
  /el Gentleman Identity and Harness/,
@@ -1243,6 +1447,21 @@ async function run() {
1243
1447
  await rm(sddAgentGuardCwd, { recursive: true, force: true });
1244
1448
  }
1245
1449
 
1450
+ const unresolvedSddCwd = await tempWorkspace();
1451
+ try {
1452
+ const ctx = createCtx(unresolvedSddCwd, true, "unresolved-preflight-session");
1453
+ ctx.ui.select = async () => undefined;
1454
+ assert.deepEqual(await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, ctx), { action: "handled" });
1455
+ const result = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "SDD executor" }, ctx);
1456
+ assert.match(result.systemPrompt, /SDD preflight unresolved/);
1457
+ assert.match(result.systemPrompt, /STOP: Do not initialize/);
1458
+ assert.doesNotMatch(result.systemPrompt, /## SDD Session Preflight/);
1459
+ assert.equal(existsSync(join(unresolvedSddCwd, "openspec", "config.yaml")), false);
1460
+ assert.equal(existsSync(join(unresolvedSddCwd, ".pi", "gentle-ai", "sdd-preflight.json")), false);
1461
+ } finally {
1462
+ await rm(unresolvedSddCwd, { recursive: true, force: true });
1463
+ }
1464
+
1246
1465
  const noUiSddAgentCwd = await tempWorkspace();
1247
1466
  try {
1248
1467
  const ctx = createCtx(noUiSddAgentCwd, false, "no-ui-sdd-agent-session");
@@ -1280,7 +1499,7 @@ async function run() {
1280
1499
  try {
1281
1500
  pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1282
1501
  const ctx = createCtx(engramSddCwd, true, "engram-session");
1283
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1502
+ await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1284
1503
  assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "hybrid"]);
1285
1504
  } finally {
1286
1505
  pi.setActiveTools(["read", "bash", "edit", "write"]);
@@ -1297,7 +1516,7 @@ async function run() {
1297
1516
  if (label === "SDD artifact store") return "engram";
1298
1517
  return options[0];
1299
1518
  };
1300
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1519
+ await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1301
1520
  await commands.get("gentle-sdd-init").handler("", ctx);
1302
1521
  assert.equal(
1303
1522
  existsSync(join(engramSddInitCwd, "openspec")),
@@ -1335,7 +1554,7 @@ async function run() {
1335
1554
  if (label === "SDD artifact store") return "hybrid";
1336
1555
  return options[0];
1337
1556
  };
1338
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1557
+ await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1339
1558
  await commands.get("gentle-sdd-init").handler("", ctx);
1340
1559
  assert.equal(
1341
1560
  existsSync(join(bothSddInitCwd, "openspec", "specs")),
@@ -1374,6 +1593,149 @@ async function run() {
1374
1593
  await rm(directEngramToolCwd, { recursive: true, force: true });
1375
1594
  }
1376
1595
 
1596
+ for (const owner of ["sdd", "delegation", "review"]) {
1597
+ const fixture = await tempWorkspace();
1598
+ const agentHome = join(fixture, "agent-home");
1599
+ const representatives = {
1600
+ sdd: "sdd-apply.md",
1601
+ delegation: "gentle-ai-worker.md",
1602
+ review: "review-risk.md",
1603
+ };
1604
+ try {
1605
+ process.env.GENTLE_PI_AGENT_HOME = agentHome;
1606
+ process.env.GENTLE_PI_CONFIG_HOME = join(fixture, "config");
1607
+ await mkdir(process.env.GENTLE_PI_CONFIG_HOME);
1608
+ await writeFile(join(process.env.GENTLE_PI_CONFIG_HOME, "models.json"),
1609
+ JSON.stringify({ [representatives[owner].replace(/\.md$/, "")]: "test/installer-must-not-apply" }));
1610
+ const ctx = createCtx(fixture, true);
1611
+ const command = commands.get(`gentle:install-${owner}`);
1612
+ assert.ok(command, `missing owner installer: ${owner}`);
1613
+ await command.handler("", ctx);
1614
+ for (const [candidate, name] of Object.entries(representatives)) {
1615
+ assert.equal(existsSync(join(agentHome, "agents", name)), candidate === owner,
1616
+ `${owner} installation must not install ${candidate} assets`);
1617
+ }
1618
+ assert.equal(existsSync(join(fixture, ".pi", "agents")), false);
1619
+ assert.match(ctx.ui.notifications.at(-1).message, /assets installed: \d+ agent\(s\), \d+ chain\(s\), \d+ support file\(s\)/);
1620
+ const selectedPath = join(agentHome, "agents", representatives[owner]);
1621
+ const packaged = await readFile(selectedPath, "utf8");
1622
+ assert.doesNotMatch(packaged, /test\/installer-must-not-apply/);
1623
+ const manifestPath = join(agentHome, "gentle-ai", "managed-assets.json");
1624
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1625
+ for (const [candidate, name] of Object.entries(representatives)) {
1626
+ const stale = `stale managed ${candidate}\n`;
1627
+ await writeFile(join(agentHome, "agents", name), stale);
1628
+ manifest.assets[`agents/${name}`] = sha256(stale);
1629
+ }
1630
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1631
+ await command.handler("", ctx);
1632
+ assert.equal(await readFile(selectedPath, "utf8"), `stale managed ${owner}\n`,
1633
+ "repair without --force must preserve existing files");
1634
+ await command.handler("--force", ctx);
1635
+ assert.equal(await readFile(selectedPath, "utf8"), packaged);
1636
+ const refreshed = JSON.parse(await readFile(manifestPath, "utf8"));
1637
+ for (const [candidate, name] of Object.entries(representatives)) {
1638
+ if (candidate === owner) continue;
1639
+ assert.equal(await readFile(join(agentHome, "agents", name), "utf8"), `stale managed ${candidate}\n`);
1640
+ assert.equal(refreshed.assets[`agents/${name}`], manifest.assets[`agents/${name}`]);
1641
+ }
1642
+ const userEdit = `${packaged}\nUser-owned instructions must survive.\n`;
1643
+ await writeFile(selectedPath, userEdit);
1644
+ await command.handler("--force", ctx);
1645
+ assert.equal(await readFile(selectedPath, "utf8"), userEdit);
1646
+ assert.equal(existsSync(join(agentHome, "subagents.json")), false,
1647
+ "owner installers must not apply model routing");
1648
+ await commands.get("gentle:status").handler("", ctx);
1649
+ const label = owner === "sdd" ? "SDD" : owner;
1650
+ assert.ok(ctx.ui.notifications.at(-1).message.includes(`Global ${label} user overrides: 1 file(s)`));
1651
+ } finally {
1652
+ process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
1653
+ process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1654
+ await rm(fixture, { recursive: true, force: true });
1655
+ }
1656
+ }
1657
+
1658
+ const repairFixture = await tempWorkspace();
1659
+ try {
1660
+ process.env.GENTLE_PI_AGENT_HOME = join(repairFixture, "agent-home");
1661
+ const ctx = createCtx(repairFixture, true);
1662
+ await commands.get("gentle:install-sdd").handler("", ctx);
1663
+ for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1664
+ await commands.get(diagnostic).handler("", ctx);
1665
+ const message = ctx.ui.notifications.at(-1).message;
1666
+ assert.match(message, /\/gentle:install-delegation --force/,
1667
+ `${diagnostic} must provide a repair for missing delegation assets`);
1668
+ assert.match(message, /\/gentle:install-review --force/,
1669
+ `${diagnostic} must provide a repair for missing review assets`);
1670
+ assert.doesNotMatch(message, /install-sdd --force/);
1671
+ }
1672
+ const manifest = JSON.parse(await readFile(join(process.env.GENTLE_PI_AGENT_HOME, "gentle-ai", "managed-assets.json"), "utf8"));
1673
+ for (const key of Object.keys(manifest.assets)) {
1674
+ await rm(join(process.env.GENTLE_PI_AGENT_HOME, key));
1675
+ }
1676
+ for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1677
+ await commands.get(diagnostic).handler("", ctx);
1678
+ assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: [1-9]\d* file\(s\).*install-sdd --force/);
1679
+ assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /on demand/,
1680
+ "managed installation evidence must survive missing SDD files");
1681
+ }
1682
+ } finally {
1683
+ process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1684
+ await rm(repairFixture, { recursive: true, force: true });
1685
+ }
1686
+
1687
+ for (const trigger of ["gentle:sdd-preflight", "gentle-sdd-init"]) {
1688
+ const fixture = await tempWorkspace();
1689
+ const agentHome = join(fixture, "agent-home");
1690
+ try {
1691
+ process.env.GENTLE_PI_AGENT_HOME = agentHome;
1692
+ const command = commands.get(trigger);
1693
+ await command.handler("", createCtx(fixture, true, `${trigger}-install`));
1694
+ assert.equal(existsSync(join(agentHome, "agents", "gentle-ai-worker.md")), false,
1695
+ `${trigger} must not install delegation assets`);
1696
+ assert.equal(existsSync(join(agentHome, "agents", "review-risk.md")), false,
1697
+ `${trigger} must not install review assets`);
1698
+ const manifestPath = join(agentHome, "gentle-ai", "managed-assets.json");
1699
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1700
+ const managedKeys = [
1701
+ "agents/sdd-apply.md",
1702
+ "chains/sdd-full.chain.md",
1703
+ "gentle-ai/support/sdd-status-contract.md",
1704
+ ];
1705
+ const originals = new Map();
1706
+ for (const key of managedKeys) {
1707
+ originals.set(key, await readFile(join(agentHome, key), "utf8"));
1708
+ }
1709
+ const untouchedKeys = ["agents/gentle-ai-worker.md", "agents/review-risk.md"];
1710
+ for (const key of [...managedKeys, ...untouchedKeys]) {
1711
+ await writeFile(join(agentHome, key), `stale ${key}\n`);
1712
+ manifest.assets[key] = sha256(`stale ${key}\n`);
1713
+ }
1714
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1715
+ const userPath = join(agentHome, "agents", "sdd-spec.md");
1716
+ const userEdit = `${await readFile(userPath, "utf8")}\nUser-owned instructions.\n`;
1717
+ await writeFile(userPath, userEdit);
1718
+ const projectOverride = join(fixture, ".pi", "agents", "sdd-apply.md");
1719
+ await mkdir(dirname(projectOverride), { recursive: true });
1720
+ await writeFile(projectOverride, "Project override must survive.\n");
1721
+ await command.handler("", createCtx(fixture, true, `${trigger}-refresh`));
1722
+ for (const key of managedKeys) {
1723
+ assert.equal(await readFile(join(agentHome, key), "utf8"), originals.get(key),
1724
+ `${trigger} must refresh managed ${key} on demand`);
1725
+ }
1726
+ const refreshed = JSON.parse(await readFile(manifestPath, "utf8"));
1727
+ for (const key of untouchedKeys) {
1728
+ assert.equal(await readFile(join(agentHome, key), "utf8"), `stale ${key}\n`);
1729
+ assert.equal(refreshed.assets[key], manifest.assets[key]);
1730
+ }
1731
+ assert.equal(await readFile(userPath, "utf8"), userEdit);
1732
+ assert.equal(await readFile(projectOverride, "utf8"), "Project override must survive.\n");
1733
+ } finally {
1734
+ process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1735
+ await rm(fixture, { recursive: true, force: true });
1736
+ }
1737
+ }
1738
+
1377
1739
  const installCwd = await tempWorkspace();
1378
1740
  try {
1379
1741
  const ctx = createCtx(installCwd, true);
@@ -1388,19 +1750,51 @@ async function run() {
1388
1750
  }
1389
1751
 
1390
1752
  const staleAssetsCwd = await tempWorkspace();
1753
+ const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
1754
+ const previousHome = process.env.HOME;
1755
+ const previousUserProfile = process.env.USERPROFILE;
1391
1756
  try {
1392
- await mkdir(join(staleAssetsCwd, ".pi", "agents"), { recursive: true });
1757
+ const diagnosticsAgentHome = join(staleAssetsCwd, "agent-home");
1758
+ const diagnosticsHome = staleAssetsCwd;
1759
+ process.env.GENTLE_PI_AGENT_HOME = diagnosticsAgentHome;
1760
+ process.env.HOME = diagnosticsHome;
1761
+ process.env.USERPROFILE = diagnosticsHome;
1762
+ for (const [dir, name] of [
1763
+ [join(diagnosticsAgentHome, "subagents"), "sdd-apply.md"],
1764
+ [join(diagnosticsHome, ".agents"), "sdd-archive.md"],
1765
+ [join(staleAssetsCwd, ".agents"), "sdd-design.md"],
1766
+ [join(staleAssetsCwd, ".pi", "agents"), "sdd-spec.md"],
1767
+ [join(staleAssetsCwd, ".pi", "subagents"), "sdd-sync.md"],
1768
+ ]) {
1769
+ await mkdir(dir, { recursive: true });
1770
+ await writeFile(
1771
+ join(dir, name),
1772
+ `---\nname: ${name.replace(/\.md$/, "")}\n---\nintentional SDD override\n`,
1773
+ );
1774
+ }
1775
+ await mkdir(join(diagnosticsAgentHome, "subagents", "nested"), { recursive: true });
1776
+ await mkdir(join(diagnosticsAgentHome, "agents", "nested"), { recursive: true });
1777
+ await mkdir(join(staleAssetsCwd, ".agents", "skills", "ignored"), { recursive: true });
1778
+ await writeFile(
1779
+ join(diagnosticsAgentHome, "subagents", "nested", "renamed-agent.md"),
1780
+ "---\nname: sdd-apply\n---\nintentional nested SDD override\n",
1781
+ );
1782
+ await writeFile(
1783
+ join(diagnosticsAgentHome, "agents", "nested", "managed-copy.md"),
1784
+ "---\nname: sdd-apply\n---\npackage-managed root is excluded\n",
1785
+ );
1786
+ await writeFile(
1787
+ join(staleAssetsCwd, ".agents", "skills", "ignored", "SKILL.md"),
1788
+ "---\nname: sdd-apply\n---\nskills are not agent definitions\n",
1789
+ );
1393
1790
  await mkdir(join(staleAssetsCwd, ".pi", "chains"), { recursive: true });
1394
1791
  await mkdir(join(staleAssetsCwd, ".pi", "gentle-ai", "support"), { recursive: true });
1395
- await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-apply.md"), "stale apply\n");
1396
- await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-spec.md"), "stale spec\n");
1397
- await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-custom-debug.md"), "custom debug agent\n");
1398
1792
  await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
1399
1793
  await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
1400
1794
  const ctx = createCtx(staleAssetsCwd, true);
1401
1795
  await commands.get("gentle:status").handler("", ctx);
1402
- assert.match(ctx.ui.notifications.at(-1).message, /Project-local SDD agent overrides: 2 file\(s\)/);
1403
- assert.match(ctx.ui.notifications.at(-1).message, /local SDD agents shadow package assets/);
1796
+ assert.match(ctx.ui.notifications.at(-1).message, /Active SDD agent overrides: 6 file\(s\)/);
1797
+ assert.match(ctx.ui.notifications.at(-1).message, /active non-builtin SDD agents shadow package assets/);
1404
1798
  await commands.get("gentle:doctor").handler("", ctx);
1405
1799
  assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
1406
1800
  assert.match(ctx.ui.notifications.at(-1).message, /Sensitive-path guard active/);
@@ -1412,6 +1806,12 @@ async function run() {
1412
1806
  assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools not active in this session/);
1413
1807
  pi.setActiveTools(["read", "bash", "edit", "write"]);
1414
1808
  } finally {
1809
+ if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
1810
+ else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
1811
+ if (previousHome === undefined) delete process.env.HOME;
1812
+ else process.env.HOME = previousHome;
1813
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
1814
+ else process.env.USERPROFILE = previousUserProfile;
1415
1815
  await rm(staleAssetsCwd, { recursive: true, force: true });
1416
1816
  }
1417
1817
 
@@ -1426,12 +1826,20 @@ async function run() {
1426
1826
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
1427
1827
  assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1428
1828
  assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1429
- assert.equal(ctx.ui.selections.length, 0, "sdd-init uses automatic preflight defaults");
1430
- assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
1829
+ assert.equal(ctx.ui.selections.length, 1, "sdd-init confirms session preflight before project initialization");
1830
+ assert.match(ctx.ui.notifications[1].message, /SDD preflight complete/);
1431
1831
  assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
1432
-
1433
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1434
- assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts after automatic sdd-init");
1832
+ const initializedConfig = await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8");
1833
+ const explore = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "You are the SDD explore executor for Gentle AI." }, ctx);
1834
+ assert.match(explore.systemPrompt, /explicit current-session choices/);
1835
+ assert.equal(ctx.ui.selections.length, 1, "confirmation -> sdd-init -> explore must reuse the resolved preflight");
1836
+ const nextSession = createCtx(sddCwd, true, "cold-start-with-saved-preferences");
1837
+ await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, nextSession);
1838
+ assert.equal(nextSession.ui.selections.length, 1, "saved preferences still require confirmation in a new session");
1839
+ assert.equal(await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8"), initializedConfig, "new session confirmation must not reset project initialization");
1840
+
1841
+ await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1842
+ assert.equal(ctx.ui.selections.length, 3, "--edit permits changes after confirmed sdd-init");
1435
1843
  } finally {
1436
1844
  await rm(sddCwd, { recursive: true, force: true });
1437
1845
  }
@@ -1446,9 +1854,9 @@ async function run() {
1446
1854
  await writeFile(globalModelsPath, "{ invalid json");
1447
1855
  const ctx = createCtx(invalidSddInitCwd, true, "invalid-sdd-init-session");
1448
1856
  await commands.get("gentle-sdd-init").handler("", ctx);
1449
- assert.equal(ctx.ui.notifications[0].level, "warning");
1450
- assert.match(ctx.ui.notifications[0].message, /Model routing skipped:/);
1451
- assert.match(ctx.ui.notifications[0].message, /models\.json/);
1857
+ assert.equal(ctx.ui.notifications[1].level, "warning");
1858
+ assert.match(ctx.ui.notifications[1].message, /Model routing skipped:/);
1859
+ assert.match(ctx.ui.notifications[1].message, /models\.json/);
1452
1860
  assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
1453
1861
  const preservedAgent = await readFile(
1454
1862
  join(invalidSddInitCwd, ".pi", "agents", "sdd-apply.md"),