gentle-pi 2.2.0 → 2.4.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 (160) hide show
  1. package/README.md +76 -79
  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-explore.md +1 -0
  8. package/assets/agents/sdd-proposal.md +2 -0
  9. package/assets/agents/sdd-research.md +54 -0
  10. package/assets/agents/sdd-status.md +4 -4
  11. package/assets/agents/sdd-tasks.md +2 -3
  12. package/assets/agents/sdd-verify.md +21 -1
  13. package/assets/chains/sdd-full.chain.md +1 -1
  14. package/assets/chains/sdd-verify.chain.md +1 -1
  15. package/assets/orchestrator-delegation.md +56 -249
  16. package/assets/orchestrator-memory.md +2 -0
  17. package/assets/orchestrator.md +20 -40
  18. package/assets/sdd-orchestrator-workflow.md +30 -37
  19. package/assets/support/sdd-status-contract.md +7 -7
  20. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  21. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  22. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  23. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  24. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  25. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  26. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  27. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  28. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  29. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  30. package/docs/native-authority-architecture.md +9 -11
  31. package/docs/review-integration.md +27 -373
  32. package/extensions/ask-user-choice.ts +151 -0
  33. package/extensions/gentle-ai.ts +2349 -3785
  34. package/extensions/quiet-tools.ts +515 -32
  35. package/extensions/sdd-init.ts +4 -8
  36. package/lib/agent-home.ts +8 -0
  37. package/lib/gentle-ai-renderer.ts +70 -0
  38. package/lib/model-routing-authority.ts +133 -0
  39. package/lib/native-review-cli.ts +387 -940
  40. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  41. package/lib/provider-contract-bundle.ts +88 -6
  42. package/lib/review-candidate-view.ts +341 -132
  43. package/lib/review-host-relay.ts +324 -96
  44. package/lib/review-integration-v2.ts +868 -307
  45. package/lib/review-last-event-controller.ts +35 -0
  46. package/lib/sdd-preflight.ts +179 -68
  47. package/lib/sdd-status.ts +66 -111
  48. package/lib/terminal-theme.ts +1 -1
  49. package/package.json +83 -82
  50. package/runtime/gentle-ai-binary.mjs +1 -1
  51. package/runtime/native-review-cli.mjs +337 -890
  52. package/runtime/review-integration-v2.mjs +816 -255
  53. package/runtime/review-relay-contract.mjs +1 -1
  54. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  55. package/scripts/gentle-ai-installer.mjs +75 -22
  56. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  57. package/scripts/test-packed-runner.mjs +4 -7
  58. package/scripts/verify-package-files.mjs +23 -23
  59. package/skills/_shared/review-ledger-contract.md +14 -14
  60. package/skills/chained-pr/SKILL.md +3 -0
  61. package/skills/cognitive-doc-design/SKILL.md +1 -1
  62. package/skills/comment-writer/SKILL.md +1 -1
  63. package/skills/gentle-ai/SKILL.md +7 -74
  64. package/skills/issue-creation/SKILL.md +53 -93
  65. package/skills/judgment-day/SKILL.md +5 -7
  66. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  67. package/skills/release/SKILL.md +3 -3
  68. package/skills/skill-registry/SKILL.md +1 -1
  69. package/skills/work-unit-commits/SKILL.md +3 -1
  70. package/tests/artifact-language.test.ts +24 -11
  71. package/tests/ask-user-choice.test.ts +264 -0
  72. package/tests/codegraph-tools.test.ts +3 -3
  73. package/tests/crosslane/cross-lane.mjs +15 -1168
  74. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  75. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  76. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  77. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  78. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  79. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  80. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  81. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  82. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  83. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  84. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  85. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  86. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  87. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  89. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  92. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  93. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  94. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  95. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  96. package/tests/gentle-ai-binary.test.ts +83 -13
  97. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  98. package/tests/gentle-ai-installer.test.ts +137 -47
  99. package/tests/gentle-ai.test.ts +821 -131
  100. package/tests/gentle-theme.test.ts +133 -0
  101. package/tests/issue-creation-skill.test.ts +103 -0
  102. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  103. package/tests/model-routing-authority.test.ts +257 -0
  104. package/tests/native-review-capability-contract.test.ts +45 -1
  105. package/tests/native-review-cli.test.ts +586 -962
  106. package/tests/native-review-consent.test.ts +150 -66
  107. package/tests/native-review-parity-runtime.test.ts +80 -359
  108. package/tests/native-review-parity.test.ts +674 -925
  109. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  110. package/tests/orchestrator-budget.test.ts +132 -88
  111. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  112. package/tests/package-manifest.test.ts +152 -36
  113. package/tests/provider-contract-bundle.test.ts +76 -0
  114. package/tests/provider-contract-mirror.test.ts +19 -0
  115. package/tests/provider-defect-handoff.test.ts +15 -118
  116. package/tests/quiet-tool-rendering.test.ts +1055 -28
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-authority-recovery-docs.test.ts +1 -2
  119. package/tests/review-candidate-view.test.ts +409 -3
  120. package/tests/review-compact-contract.test.ts +4 -30
  121. package/tests/review-contract-prompt.test.ts +139 -0
  122. package/tests/review-controller-lock-status.test.ts +2 -2
  123. package/tests/review-controller-native-recovery.test.ts +363 -1450
  124. package/tests/review-controller-native-routing.test.ts +1251 -5642
  125. package/tests/review-controller-retired-ops.test.ts +1 -1
  126. package/tests/review-controller-workspace-root.test.ts +222 -71
  127. package/tests/review-controller.test.ts +26 -816
  128. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  129. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  130. package/tests/review-gate.test.ts +0 -45
  131. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  132. package/tests/review-host-relay-routing.test.ts +328 -91
  133. package/tests/review-host-relay.test.ts +436 -14
  134. package/tests/review-integration-v2-forward.test.ts +623 -553
  135. package/tests/review-integration-v2.test.ts +239 -165
  136. package/tests/review-last-event-closure.test.ts +408 -0
  137. package/tests/review-ledger-contract.test.ts +97 -35
  138. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  139. package/tests/review-relay-transport-agent.test.ts +119 -46
  140. package/tests/review-snapshot.test.ts +3 -2
  141. package/tests/runtime-harness.mjs +362 -196
  142. package/tests/sdd-agent-tools.test.ts +36 -0
  143. package/tests/sdd-preflight.test.ts +81 -15
  144. package/tests/sdd-status.test.ts +109 -110
  145. package/tests/skill-collision-prefixes.test.ts +5 -8
  146. package/tests/writer-edit-surface-scope.test.ts +366 -0
  147. package/themes/Gentleman-Cute.json +94 -0
  148. package/themes/Gentleman-Sexy.json +92 -0
  149. package/lib/git-commit-transaction.ts +0 -861
  150. package/runtime/git-commit-transaction.mjs +0 -862
  151. package/scripts/run-git-commit-transaction.mjs +0 -35
  152. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  153. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  154. package/tests/git-commit-transaction.test.ts +0 -530
  155. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  156. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  157. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  158. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  159. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  160. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { createHash } from "node:crypto";
5
5
  import { existsSync } from "node:fs";
6
- import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
6
+ import { chmod, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { dirname, join } from "node:path";
9
9
  import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent";
@@ -11,8 +11,6 @@ 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 { resolveGentleAiBinary } from "../lib/gentle-ai-binary.ts";
15
- import { NativeReviewCliV216, NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError } from "../lib/native-review-cli.ts";
16
14
 
17
15
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
18
16
  const EXTENSIONS = [
@@ -33,18 +31,23 @@ const EXPECTED_BANNER_COMMANDS = [
33
31
  const EXPECTED_COMMANDS = [
34
32
  "gentle:install-sdd",
35
33
  "gentle:sdd-preflight",
36
- "sdd-status",
37
- "sdd-continue",
34
+ "gentle-sdd-status",
35
+ "gentle-sdd-continue",
38
36
  "gentle:models",
39
37
  "gentle:persona",
40
38
  "gentle:status",
41
39
  "gentle:doctor",
42
- "sdd-init",
40
+ "gentle-sdd-init",
43
41
  "skill-registry:refresh",
44
42
  ...EXPECTED_BANNER_COMMANDS,
45
43
  ];
46
44
 
47
45
  const FORBIDDEN_COMPAT_COMMANDS = [
46
+ // The SDD entry points carry the gentle- prefix so they read identically in
47
+ // Claude Code and Pi. The bare names are retired without an alias.
48
+ "sdd-init",
49
+ "sdd-continue",
50
+ "sdd-status",
48
51
  "gentle-ai:install-sdd",
49
52
  "gentle-ai:sdd-preflight",
50
53
  "gentle-ai:sdd-status",
@@ -66,10 +69,28 @@ function createPi() {
66
69
  const commands = new Map();
67
70
  const flags = new Map();
68
71
  const tools = new Map();
72
+ const eventHandlers = new Map();
73
+ const emittedEvents = [];
69
74
  const flagValues = new Map([["no-skill-registry", true]]);
75
+ const events = {
76
+ emit(channel, data) {
77
+ emittedEvents.push({ channel, data });
78
+ for (const handler of eventHandlers.get(channel) ?? []) handler(data);
79
+ },
80
+ on(channel, handler) {
81
+ const handlers = eventHandlers.get(channel) ?? new Set();
82
+ handlers.add(handler);
83
+ eventHandlers.set(channel, handlers);
84
+ return () => {
85
+ handlers.delete(handler);
86
+ if (handlers.size === 0) eventHandlers.delete(channel);
87
+ };
88
+ },
89
+ };
70
90
  let activeTools = ["read", "bash", "edit", "write"];
71
91
 
72
92
  const pi = {
93
+ events,
73
94
  on(name, handler) {
74
95
  const list = hooks.get(name) ?? [];
75
96
  list.push(handler);
@@ -110,7 +131,7 @@ function createPi() {
110
131
  },
111
132
  };
112
133
 
113
- return { pi, hooks, commands, flags, tools };
134
+ return { pi, hooks, commands, flags, tools, emittedEvents };
114
135
  }
115
136
 
116
137
  function createUi() {
@@ -182,6 +203,15 @@ async function tempWorkspace() {
182
203
  return mkdtemp(join(tmpdir(), "gentle-pi-runtime-"));
183
204
  }
184
205
 
206
+ function restoreWorkspaceWritePermissions(cwd) {
207
+ if (process.platform === "win32") return;
208
+ try {
209
+ execFileSync("chmod", ["-R", "u+w", cwd], { stdio: "ignore" });
210
+ } catch {
211
+ // A prior candidate-view cleanup may already have removed the workspace.
212
+ }
213
+ }
214
+
185
215
  async function loadExtensions(pi) {
186
216
  for (const [index, rel] of EXTENSIONS.entries()) {
187
217
  const mod = await import(`${pathToFileURL(join(ROOT, rel)).href}?runtime-harness=${index}`);
@@ -197,28 +227,125 @@ async function run() {
197
227
  process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
198
228
  process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
199
229
  process.env.GENTLE_PI_TEST_ASSETS_DIR = ambientTestAssetsDir;
200
- // The harness already sandboxes its config and agent homes; the review kill
201
- // switch is the one piece of machine state it was still inheriting. Several
202
- // lifecycle-gate assertions below only reach their subject while
203
- // receipt-driven development is on — with it off, gateLifecycleCommand
204
- // returns undefined before deriving any target, which is correct behavior
205
- // but not what those assertions are about.
206
- //
207
- // gentle-ai v2.4.0 made the switch opt-in, so an unconfigured machine now
208
- // resolves to off. Until then the harness passed or failed on whatever the
209
- // operator happened to have set, which made it green locally and red on a
210
- // fresh CI runner from identical bytes. It now owns a sandbox HOME and opts
211
- // in explicitly, the same way a user does, so the gates are what is under
212
- // test rather than the machine.
213
- const reviewHome = await tempWorkspace();
214
- process.env.HOME = reviewHome;
215
- const reviewModeEnable = execFileSync(resolveGentleAiBinary(ROOT, process.platform), ["review", "mode", "enable", "--scope", "global", "--json"], { cwd: ROOT, encoding: "utf8", env: { ...process.env, HOME: reviewHome } });
216
- assert.match(reviewModeEnable, /"effective": "on"/, "the harness sandbox HOME must have receipt-driven development explicitly enabled");
217
230
  const globalModelsPath = join(globalConfigHome, "models.json");
218
231
  const globalSubagentsPath = join(globalAgentHome, "subagents.json");
219
- const { pi, hooks, commands, flags, tools } = createPi();
232
+ const { pi, hooks, commands, flags, tools, emittedEvents } = createPi();
220
233
  await loadExtensions(pi);
221
234
 
235
+ // gentle-pi#404: a collect binding that returns the native last-event
236
+ // closure must terminate after one capture. It must not re-enter a public
237
+ // lifecycle mutation or synthesize a follow-up transition.
238
+ {
239
+ const lineageId = "runtime-last-event";
240
+ const sha = `sha256:${"a".repeat(64)}`;
241
+ const tree = "b".repeat(40);
242
+ const repositoryContext = `rctx1_${"c".repeat(64)}`;
243
+ const calls = [];
244
+ const arguments_ = [
245
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
246
+ { name: "expected-revision", value: sha, token: `--expected-revision=${sha}` },
247
+ { name: "target", value: sha, token: `--target=${sha}` },
248
+ { name: "repository-context", value: repositoryContext, token: `--repository-context=${repositoryContext}` },
249
+ { name: "lens", value: "review-risk", token: "--lens=review-risk" },
250
+ { name: "order", value: "0", token: "--order=0" },
251
+ { name: "subject-hash", value: sha, token: `--subject-hash=${sha}` },
252
+ ];
253
+ const input = {
254
+ name: "correction_plan",
255
+ schema: "gentle-ai.review-correction-plan/v1",
256
+ captureOperation: "review.capture-correction-plan",
257
+ arguments: arguments_,
258
+ submission: {
259
+ operationToken: "capture-correction-plan",
260
+ argumentTokens: [...arguments_.map((argument) => argument.token), "--correction-lines={{value}}"],
261
+ values: [{ slot: "correction_lines", domain: "positive_integer", substitutionLocation: 7, minimum: 1, maximum: 1 }],
262
+ },
263
+ };
264
+ const status = {
265
+ contract: "gentle-ai.review-integration/v2",
266
+ applicability: "current_target",
267
+ authority: { version: "compact-v2", lineageId, state: "correction_required", generation: 1, revision: sha },
268
+ receipt: { status: "expected_missing" },
269
+ action: "stop",
270
+ replayability: "not_replayable",
271
+ targetIdentity: sha,
272
+ projection: {
273
+ schema: "gentle-ai.review-candidate-projection/v1",
274
+ kind: "current-changes",
275
+ projection: "workspace",
276
+ baseTree: tree,
277
+ initialReviewTree: tree,
278
+ currentCandidateTree: tree,
279
+ pathsDigest: sha,
280
+ paths: ["app.ts"],
281
+ intendedUntracked: [],
282
+ intendedUntrackedProof: sha,
283
+ initialSnapshotIdentity: sha,
284
+ currentSnapshotIdentity: sha,
285
+ },
286
+ repair: { schema: "gentle-ai.review-authority-repair-assessment/v1", status: "unsupported", counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 }, supportedOperations: ["review/complete-fix", "review/validate-fix"], authorizationSchema: "gentle-ai.review-repair-authorization/v1" },
287
+ candidates: [],
288
+ nextTransition: { kind: "collect", reasonCode: "correction_plan_required", collect: { inputs: [input] } },
289
+ raw: { schema: "gentle-ai.review-integration.status/v5" },
290
+ };
291
+ const nativeReviewCli = {
292
+ async targetStatus(request) {
293
+ calls.push({ operation: "status", request });
294
+ return status;
295
+ },
296
+ async captureCorrectionPlan(request) {
297
+ calls.push({ operation: "capture-correction-plan", request });
298
+ return {
299
+ schema: "gentle-ai.review-last-event-closure/v1",
300
+ operation: "review.capture-correction-plan",
301
+ lineageId,
302
+ state: "correction_required",
303
+ targetIdentity: sha,
304
+ requestHash: sha,
305
+ correctionLines: 1,
306
+ storeRevision: sha,
307
+ };
308
+ },
309
+ };
310
+ const lastEventPi = createPi();
311
+ const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
312
+ createGentleAiExtension({ nativeReviewCli })(lastEventPi.pi);
313
+ const controller = lastEventPi.tools.get("gentle_review");
314
+ const capture = lastEventPi.tools.get("gentle_review_capture");
315
+ assert.ok(controller, "runtime must register the public status controller");
316
+ assert.ok(capture, "runtime must register the one-slot capture tool");
317
+ assert.equal(lastEventPi.tools.get("gentle_review_capture_group")?.executionMode, "sequential", "runtime must register grouped capture with bounded foreground concurrency");
318
+ assert.equal(controller.parameters.properties.operation.enum.includes("finalize"), false);
319
+ assert.equal(controller.parameters.properties.operation.enum.includes("validate"), false);
320
+
321
+ const publicStatus = await controller.execute(
322
+ "runtime-status",
323
+ { operation: "status", lineageId },
324
+ undefined,
325
+ undefined,
326
+ createCtx(ROOT, false, lineageId),
327
+ );
328
+ const collectBindings = publicStatus.details.collectBindings;
329
+ assert.equal(publicStatus.details.status, "blocked");
330
+ assert.equal(collectBindings.length, 1);
331
+
332
+ const captured = await capture.execute(
333
+ "runtime-capture",
334
+ { lineageId, collectBinding: collectBindings[0].collectBinding, correctionLines: 1 },
335
+ undefined,
336
+ undefined,
337
+ createCtx(ROOT, false, lineageId),
338
+ );
339
+ assert.equal(captured.details.status, "closed");
340
+ assert.equal(captured.details.outcome, "native-last-event-closure");
341
+ assert.equal(captured.details.closure.operation, "review.capture-correction-plan");
342
+ assert.deepEqual(
343
+ calls.map(({ operation }) => operation),
344
+ ["status", "status", "capture-correction-plan"],
345
+ "last-event closure must make no follow-up lifecycle mutation",
346
+ );
347
+ }
348
+
222
349
  for (const name of EXPECTED_COMMANDS) {
223
350
  assert.ok(commands.has(name), `missing command ${name}`);
224
351
  }
@@ -281,10 +408,13 @@ async function run() {
281
408
  assert.doesNotMatch(promptResult.systemPrompt, /default\s*\|\s*sonnet\s*\|\s*Non-SDD general delegation/);
282
409
  assert.match(promptResult.systemPrompt, /openspec\/config\.yaml.*not session preflight/s);
283
410
  assert.match(promptResult.systemPrompt, /Do not mark SDD preflight complete/);
284
- assert.match(
285
- promptResult.systemPrompt,
286
- new RegExp(`${ROOT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*assets.*sdd-orchestrator-workflow\\.md`),
411
+ assert.ok(
412
+ promptResult.systemPrompt.includes(
413
+ `Package assets root: \`${join(ROOT, "assets")}\`. Lazy asset paths below are relative to this root.`,
414
+ ),
415
+ "parent prompt must declare the one absolute root for relative lazy asset paths",
287
416
  );
417
+ assert.match(promptResult.systemPrompt, /`sdd-orchestrator-workflow\.md`/);
288
418
  assert.doesNotMatch(
289
419
  promptResult.systemPrompt,
290
420
  new RegExp(ambientTestAssetsDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
@@ -358,10 +488,10 @@ async function run() {
358
488
  assert.match(applyPromptResult.systemPrompt, /"changeName": "status-demo"/);
359
489
  assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
360
490
  const statusCtx = createCtx(promptCwd, true);
361
- await commands.get("sdd-status").handler("status-demo --json", statusCtx);
491
+ await commands.get("gentle-sdd-status").handler("status-demo --json", statusCtx);
362
492
  assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-pi\.sdd-status"/);
363
493
  const continueCtx = createCtx(promptCwd, true);
364
- await commands.get("sdd-continue").handler("status-demo", continueCtx);
494
+ await commands.get("gentle-sdd-continue").handler("status-demo", continueCtx);
365
495
  assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Dispatcher/);
366
496
  assert.match(continueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
367
497
  const { execFileSync } = await import("node:child_process");
@@ -373,7 +503,7 @@ async function run() {
373
503
  '{"schema":"gentle-ai.recovery-required/v1","change_name":"status-demo"}',
374
504
  );
375
505
  const blockedContinueCtx = createCtx(promptCwd, true);
376
- await commands.get("sdd-continue").handler("status-demo", blockedContinueCtx);
506
+ await commands.get("gentle-sdd-continue").handler("status-demo", blockedContinueCtx);
377
507
  assert.doesNotMatch(blockedContinueCtx.ui.notifications.at(-1).message, /resolve-review:/);
378
508
  assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /nextPhase: sdd-apply/);
379
509
  } finally {
@@ -386,12 +516,11 @@ async function run() {
386
516
  const ghPrCwd = await tempWorkspace();
387
517
  try {
388
518
  execFileSync("git", ["init"], { cwd: ghPrCwd, stdio: "ignore" });
389
- const receiptGate = await toolHook(
519
+ const deliveryResult = await toolHook(
390
520
  { toolName: "bash", input: { command: "gh pr create --draft" } },
391
- createCtx(ghPrCwd, true, "receipt-gate-session"),
521
+ createCtx(ghPrCwd, true, "ordinary-delivery-session"),
392
522
  );
393
- assert.equal(receiptGate.block, true);
394
- assert.match(receiptGate.reason, /exactly derive.*--base/i);
523
+ assert.equal(deliveryResult, undefined, "ordinary delivery policy, not review authority, governs pull-request creation");
395
524
  } finally {
396
525
  await rm(ghPrCwd, { recursive: true, force: true });
397
526
  }
@@ -404,6 +533,110 @@ async function run() {
404
533
  assert.equal(missingReviewView.block, true);
405
534
  assert.match(missingReviewView.reason, /candidate view/i);
406
535
  assert.equal(reviewDispatch.task, "review", "blocked review dispatch must not mutate child input");
536
+
537
+ for (const [agent, label, task] of [
538
+ ["gentle-ai-worker", "missing", "Implement the requested change."],
539
+ ["gentle-ai-worker", "absolute", "## Allowed edit surfaces\n/tmp/outside.ts"],
540
+ ["gentle-ai-worker", "Windows absolute", "## Allowed edit surfaces\nC:\\outside.ts"],
541
+ ["gentle-ai-worker", "prose instead of paths", "## Allowed edit surfaces\nThe parent will determine the paths."],
542
+ ["gentle-ai-worker", "repository root", "## Allowed edit surfaces\n."],
543
+ ["gentle-ai-worker", "bare repository root", "## Allowed edit surfaces\n./"],
544
+ ["gentle-ai-worker", "normalized bare repository root", "## Allowed edit surfaces\n.//"],
545
+ ["gentle-ai-worker", "equivalent normalized bare repository root", "## Allowed edit surfaces\n././/"],
546
+ ["worker", "generic writer missing", "Implement the requested change."],
547
+ ]) {
548
+ const writerDispatch = { agent, task, mode: "task" };
549
+ const writerResult = await toolHook(
550
+ { toolName: "subagent_run", input: writerDispatch },
551
+ createCtx(toolCwd),
552
+ );
553
+ assert.equal(writerResult?.block, true, `${label} writer scope must be blocked before dispatch`);
554
+ assert.match(writerResult?.reason ?? "", /derive|map/i);
555
+ assert.match(writerResult?.reason ?? "", /relaunch/i);
556
+ assert.match(writerResult?.reason ?? "", /do not ask.*human.*paths or globs/i);
557
+ assert.equal(writerDispatch.task, task, "writer guard must not mutate child input");
558
+ }
559
+
560
+ const scopedWriterDispatch = {
561
+ agent: "gentle-ai-worker",
562
+ task: "Implement the requested change.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs",
563
+ mode: "task",
564
+ };
565
+ assert.equal(
566
+ await toolHook({ toolName: "subagent_run", input: scopedWriterDispatch }, createCtx(toolCwd)),
567
+ undefined,
568
+ "a writer may dispatch with narrow task-scoped repository-relative paths",
569
+ );
570
+ assert.equal(
571
+ await toolHook(
572
+ {
573
+ toolName: "subagent_run",
574
+ input: {
575
+ agent: "worker",
576
+ task: "Implement the requested change.",
577
+ context: "## Allowed edit surfaces\n- assets/orchestrator.md",
578
+ mode: "task",
579
+ },
580
+ },
581
+ createCtx(toolCwd),
582
+ ),
583
+ undefined,
584
+ "a writer may dispatch when context carries narrow task-scoped repository-relative paths",
585
+ );
586
+ for (const [label, input] of [
587
+ [
588
+ "valid scope followed by a repository-root scope",
589
+ {
590
+ agent: "gentle-ai-worker",
591
+ task: "## Allowed edit surfaces\nextensions/gentle-ai.ts\n\n## Allowed edit surfaces\n.",
592
+ mode: "task",
593
+ },
594
+ ],
595
+ [
596
+ "valid task scope plus invalid context scope",
597
+ {
598
+ agent: "gentle-ai-worker",
599
+ task: "## Allowed edit surfaces\nextensions/gentle-ai.ts",
600
+ context: "## Allowed edit surfaces\n.",
601
+ mode: "task",
602
+ },
603
+ ],
604
+ [
605
+ "conflicting valid task and context scopes",
606
+ {
607
+ agent: "gentle-ai-worker",
608
+ task: "## Allowed edit surfaces\nextensions/gentle-ai.ts",
609
+ context: "## Allowed edit surfaces\ntests/runtime-harness.mjs",
610
+ mode: "task",
611
+ },
612
+ ],
613
+ ]) {
614
+ const writerResult = await toolHook({ toolName: "subagent_run", input }, createCtx(toolCwd));
615
+ assert.equal(writerResult?.block, true, `${label} must block writer dispatch`);
616
+ }
617
+ assert.equal(
618
+ await toolHook(
619
+ {
620
+ toolName: "subagent_run",
621
+ input: {
622
+ agent: "gentle-ai-worker",
623
+ task: "## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs\n\n## Allowed edit surfaces\n- `tests/runtime-harness.mjs`\n- `extensions/gentle-ai.ts`",
624
+ mode: "task",
625
+ },
626
+ },
627
+ createCtx(toolCwd),
628
+ ),
629
+ undefined,
630
+ "a writer may dispatch when multiple allowed edit surface sections are compatible",
631
+ );
632
+ assert.equal(
633
+ await toolHook(
634
+ { toolName: "subagent_run", input: { agent: "scout", task: "Map the repository.", mode: "task" } },
635
+ createCtx(toolCwd),
636
+ ),
637
+ undefined,
638
+ "non-writer subagents must remain unaffected",
639
+ );
407
640
  const sensitiveRead = await toolHook({ toolName: "read", input: { path: join(toolCwd, ".env.local") } }, createCtx(toolCwd));
408
641
  assert.equal(sensitiveRead.block, true);
409
642
  assert.match(sensitiveRead.reason, /sensitive path/);
@@ -419,20 +652,74 @@ async function run() {
419
652
  );
420
653
  assert.equal(needsConfirm.block, true);
421
654
  assert.match(needsConfirm.reason, /not confirmed/);
655
+ assert.deepEqual(
656
+ emittedEvents.map(({ channel, data }) => ({
657
+ channel,
658
+ state: data.state,
659
+ active: data.active,
660
+ label: data.label,
661
+ })),
662
+ [
663
+ {
664
+ channel: "pi-permission-system:permission-request",
665
+ state: "waiting",
666
+ active: undefined,
667
+ label: undefined,
668
+ },
669
+ {
670
+ channel: "herdr:blocked",
671
+ state: undefined,
672
+ active: true,
673
+ label: "Guarded command confirmation",
674
+ },
675
+ {
676
+ channel: "pi-permission-system:permission-request",
677
+ state: "denied",
678
+ active: undefined,
679
+ label: undefined,
680
+ },
681
+ {
682
+ channel: "herdr:blocked",
683
+ state: undefined,
684
+ active: false,
685
+ label: undefined,
686
+ },
687
+ ],
688
+ "guarded confirmation emits both lifecycle channels in order",
689
+ );
690
+ assert.equal(emittedEvents[0].data.requestId, emittedEvents[2].data.requestId);
691
+ emittedEvents.length = 0;
692
+ pi.events.emit("rpiv:ask-user:blocked", {
693
+ active: true,
694
+ question: "private runtime questionnaire",
695
+ answer: "private runtime answer",
696
+ path: "/private/runtime-path",
697
+ command: "private runtime command",
698
+ });
699
+ pi.events.emit("rpiv:ask-user:blocked", { active: true, duplicate: true });
700
+ pi.events.emit("rpiv:ask-user:blocked", { active: "true" });
701
+ pi.events.emit("rpiv:ask-user:other", { active: false });
702
+ pi.events.emit("rpiv:ask-user:blocked", { active: false });
703
+ const rpivHerdrEvents = emittedEvents.filter(({ channel }) => channel === "herdr:blocked");
704
+ assert.deepEqual(rpivHerdrEvents, [
705
+ { channel: "herdr:blocked", data: { active: true, label: "Questionnaire awaiting input" } },
706
+ { channel: "herdr:blocked", data: { active: false } },
707
+ ]);
708
+ assert.doesNotMatch(JSON.stringify(rpivHerdrEvents), /private runtime/i);
709
+ emittedEvents.length = 0;
422
710
  assert.equal(
423
711
  dangerousReviewCtx.ui.notifications.length,
424
712
  0,
425
- "receipt validation must not launch or announce review actors",
713
+ "dangerous-command confirmation must not launch or announce review actors",
426
714
  );
427
715
  const commitCwd = await tempWorkspace();
428
716
  try {
429
717
  execFileSync("git", ["init"], { cwd: commitCwd, stdio: "ignore" });
430
- const commitGate = await toolHook(
718
+ const deliveryResult = await toolHook(
431
719
  { toolName: "bash", input: { command: "git commit -m bounded tracked.txt" } },
432
720
  createCtx(commitCwd),
433
721
  );
434
- assert.equal(commitGate.block, true);
435
- assert.match(commitGate.reason, /exactly derive/i);
722
+ assert.equal(deliveryResult, undefined, "ordinary delivery policy, not review authority, governs commits");
436
723
  } finally {
437
724
  await rm(commitCwd, { recursive: true, force: true });
438
725
  }
@@ -496,131 +783,13 @@ async function run() {
496
783
  /Controller-owned review lineage/,
497
784
  "a failed-closed dispatch must never inject a substituted candidate view into the lens sub-agent's task",
498
785
  );
786
+ await chmod(view.root, 0o700);
499
787
  registry.cleanup(view.token);
500
788
  } finally {
789
+ restoreWorkspaceWritePermissions(candidateDriftCwd);
501
790
  await rm(candidateDriftCwd, { recursive: true, force: true });
502
791
  }
503
792
 
504
- // Corrective v2 lifecycle settling test: the real controller must call the
505
- // native evidence surface before it accepts a targeted-validation document.
506
- // This is intentionally controller-level rather than a pure resolver test;
507
- // the ordered call trace proves production wiring and the fail-closed gate.
508
- const correctionCwd = await tempWorkspace();
509
- try {
510
- const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
511
- const { CandidateViewRegistry } = await import(pathToFileURL(join(ROOT, "lib/review-candidate-view.ts")).href);
512
- gitSync(correctionCwd, "init", "-b", "main");
513
- await writeFile(join(correctionCwd, "app.ts"), "export const value = 1;\n");
514
- gitSync(correctionCwd, "add", "app.ts");
515
- gitSync(correctionCwd, "-c", "user.name=Runtime Harness", "-c", "user.email=runtime-harness@example.invalid", "commit", "-m", "base");
516
- await writeFile(join(correctionCwd, "app.ts"), "export const value = 2;\n");
517
- const frozen = new CandidateViewRegistry().create({ contributorRoot: correctionCwd });
518
- const sha = (digit) => `sha256:${digit.repeat(64)}`;
519
- const repair = { schema: "gentle-ai.review-authority-repair-assessment/v1", status: "unsupported", counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 }, supportedOperations: ["review/complete-fix", "review/validate-fix"], authorizationSchema: "gentle-ai.review-repair-authorization/v1" };
520
- const projection = { schema: "gentle-ai.review-integration.projection/v1", kind: "current-changes", projection: "workspace", baseTree: frozen.baseTree, initialReviewTree: frozen.candidateTree, currentCandidateTree: frozen.candidateTree, pathsDigest: sha("a"), paths: frozen.paths, intendedUntracked: [], intendedUntrackedProof: sha("b"), initialSnapshotIdentity: sha("c"), currentSnapshotIdentity: sha("c") };
521
- const status = { contract: "gentle-ai.review-integration/v2", applicability: "current_target", authority: { version: "compact-v2", lineageId: "runtime-correction", state: "correction_required", generation: 1, revision: sha("d") }, receipt: { status: "expected_missing" }, action: "finalize", replayability: "not_replayable", frozen: { tier: "medium", originalChangedLines: 1, correctionBudget: 1 }, targetIdentity: sha("e"), projection, repair, candidates: [], nextTransition: { kind: "collect", reasonCode: "verification_evidence_required", collect: { inputs: [{ name: "verification_evidence", schema: "gentle-ai.review-verification-evidence/v2", captureOperation: "review.capture-evidence", arguments: [{ name: "lineage", value: "runtime-correction" }] }] } }, raw: {} };
522
- const validationRequest = { schema: "gentle-ai.review-targeted-validation-request/v1", requestHash: sha("f"), lineageId: "runtime-correction", expectedRevision: sha("d"), targetIdentity: sha("e"), fixFindingIds: [], projection: "workspace", correctionCandidateTree: frozen.candidateTree, correctionTargetIdentity: sha("e"), correctionPaths: frozen.paths, correctionPathsDigest: sha("a") };
523
- const afterCapture = { ...status, authority: { ...status.authority, state: "validating" }, validationRequest, nextTransition: { kind: "collect", reasonCode: "targeted_validation_required", collect: { inputs: [{ name: "targeted_validation", schema: validationRequest.schema, captureOperation: "external.run_targeted_validation", arguments: [{ name: "lineage", value: "runtime-correction" }], validationRequest }] } } };
524
- frozen.cleanup();
525
- const calls = [];
526
- let statuses = 0;
527
- const nativeReviewCli = {
528
- async targetStatus() { calls.push("status"); statuses += 1; return statuses === 1 ? status : afterCapture; },
529
- async captureEvidence(request) { calls.push("capture-evidence"); return { schema: "gentle-ai.review-verification-evidence/v2", version: 2, lineageId: "runtime-correction", authorityRevision: sha("d"), targetIdentity: sha("e"), candidateTree: frozen.candidateTree, pathsDigest: sha("a"), paths: frozen.paths, ledgerIds: [], rawPayloadSha256: sha("1"), rawPayloadBytes: request.evidenceDocument.length, outcome: "passed", recordDigest: sha("2") }; },
530
- async finalize() { calls.push("finalize"); return { lineageId: "runtime-correction", state: "approved", action: "approved", storeRevision: sha("3") }; },
531
- async start() { throw new Error("not expected"); }, async validate() { throw new Error("not expected"); }, async bindSdd() { throw new Error("not expected"); }, async sddStatus() { return { ready: false }; }, async reviewStatus() { throw new Error("not expected"); },
532
- };
533
- const correctionPi = createPi();
534
- createGentleAiExtension({ nativeReviewCli, candidateViews: new CandidateViewRegistry() })(correctionPi.pi);
535
- const controller = correctionPi.tools.get("gentle_review");
536
- const response = await controller.execute("runtime-correction", { operation: "finalize", lineageId: "runtime-correction", input: JSON.stringify({ final_evidence: "focused verification passed", final_verification_outcome: "passed", validation: { request_hash: "f".repeat(64), correction_ids: [], original_criteria: { passed: true, evidence: ["acceptance passes"] }, correction_regression: { passed: true, evidence: ["regression passes"] }, fix_caused_findings: [], follow_ups: [] } }) }, undefined, undefined, createCtx(correctionCwd));
537
- assert.deepEqual(calls, ["status", "capture-evidence", "status", "finalize"]);
538
- assert.equal(response.details.result.state, "approved");
539
- } finally {
540
- await rm(correctionCwd, { recursive: true, force: true });
541
- }
542
-
543
- // Task 11.2 (migrate-review-integration-v2): an unimplemented
544
- // next_transition.execute.operation (e.g. a future "dispose-result") must
545
- // raise a typed, named unsupported-transition-operation refusal and stop —
546
- // never fall through to a generic schema-incompatible error, and never
547
- // synthesize an invocation for it. Drives the real negotiated STATUS decode
548
- // path (NativeReviewCliV216.targetStatus) against a stubbed adapter, so no
549
- // live binary needs to advertise the unimplemented operation to prove this.
550
- {
551
- const capabilitiesFixturePath = join(
552
- ROOT, "contracts", "review-integration", "v2", "fixtures", "capabilities.fixture.json",
553
- );
554
- const capabilitiesFixture = JSON.parse(await readFile(capabilitiesFixturePath, "utf8"));
555
- const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
556
- const capabilitiesBody = {
557
- ...capabilitiesFixture,
558
- package: { ...capabilitiesFixture.package, version: "2.4.0" },
559
- };
560
- const statusBody = {
561
- schema: "gentle-ai.review-integration.status/v3",
562
- contract: "gentle-ai.review-integration/v2",
563
- operation: "review.status",
564
- applicability: "unrelated",
565
- receipt: { status: "not_applicable" },
566
- action: "start",
567
- replayability: "not_replayable",
568
- target_identity: `sha256:${"a".repeat(64)}`,
569
- repair: {
570
- schema: "gentle-ai.review-authority-repair-assessment/v1",
571
- status: "unsupported",
572
- counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
573
- supported_operations: ["review/complete-fix", "review/validate-fix"],
574
- authorization_schema: "gentle-ai.review-repair-authorization/v1",
575
- },
576
- projection: {
577
- schema: "gentle-ai.review-integration.projection/v1",
578
- kind: "current-changes",
579
- projection: "workspace",
580
- base_tree: "b".repeat(40),
581
- initial_review_tree: "b".repeat(40),
582
- current_candidate_tree: "b".repeat(40),
583
- paths_digest: `sha256:${"a".repeat(64)}`,
584
- paths: [],
585
- intended_untracked: [],
586
- intended_untracked_proof: `sha256:${"a".repeat(64)}`,
587
- initial_snapshot_identity: `sha256:${"a".repeat(64)}`,
588
- current_snapshot_identity: `sha256:${"a".repeat(64)}`,
589
- },
590
- candidates: [],
591
- next_transition: {
592
- kind: "execute",
593
- reason_code: "disposable_result_pending",
594
- execute: {
595
- operation: "review.dispose-result",
596
- arguments: [],
597
- preconditions: [],
598
- binding: { target_identity: `sha256:${"a".repeat(64)}` },
599
- },
600
- },
601
- };
602
- const calls = [];
603
- const adapter = async (request) => {
604
- calls.push(request.arguments);
605
- const body = calls.length === 1 ? capabilitiesBody : statusBody;
606
- return { stdout: JSON.stringify(body), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
607
- };
608
- const client = new NativeReviewCliV216(
609
- adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024,
610
- async () => undefined, () => executableDigest,
611
- );
612
- let thrown;
613
- try {
614
- await client.targetStatus({ cwd: "/repo" });
615
- } catch (error) {
616
- thrown = error;
617
- }
618
- assert.ok(thrown instanceof NativeReviewCliError, "an unimplemented next_transition.execute.operation must raise a typed NativeReviewCliError");
619
- assert.equal(thrown.code, NATIVE_REVIEW_ERROR_CODE.UNSUPPORTED_TRANSITION_OPERATION);
620
- assert.match(thrown.message, /review\.dispose-result/, "the refusal must name the specific unimplemented operation");
621
- assert.equal(calls.length, 2, "capabilities negotiation plus one STATUS call — never a third call synthesizing the unimplemented operation");
622
- }
623
-
624
793
  const bannerCwd = await tempWorkspace();
625
794
  try {
626
795
  const ctx = createCtx(bannerCwd, true);
@@ -925,17 +1094,13 @@ async function run() {
925
1094
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
926
1095
  assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
927
1096
  assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
928
- assert.equal(ctx.ui.selections.length, 3);
929
- assert.equal(ctx.ui.selections[0].label, "SDD execution mode");
930
- assert.equal(ctx.ui.selections[1].label, "SDD artifact store");
931
- assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
932
- assert.equal(ctx.ui.selections[2].label, "SDD PR chaining");
933
- assert.match(ctx.ui.notifications.at(-1).message, /Preference source: user prompt/);
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/);
934
1099
  assert.deepEqual(
935
1100
  await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
936
1101
  { action: "continue" },
937
1102
  );
938
- assert.equal(ctx.ui.selections.length, 3, "natural SDD trigger should reuse session choices");
1103
+ assert.equal(ctx.ui.selections.length, 0, "natural SDD triggers reuse preferences without prompts");
939
1104
  assert.deepEqual(
940
1105
  await inputHook({ text: "/sdd", source: "interactive" }, ctx),
941
1106
  { action: "continue" },
@@ -948,7 +1113,7 @@ async function run() {
948
1113
  await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
949
1114
  { action: "continue" },
950
1115
  );
951
- assert.equal(ctx.ui.selections.length, 3);
1116
+ assert.equal(ctx.ui.selections.length, 0, "slash SDD triggers use automatic defaults without prompts");
952
1117
 
953
1118
  assert.deepEqual(
954
1119
  await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
@@ -976,19 +1141,18 @@ async function run() {
976
1141
  "global SDD model routing must be materialized in agent frontmatter, not project settings overrides",
977
1142
  );
978
1143
  }
979
- assert.equal(ctx.ui.selections.length, 3);
980
- assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
981
- assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
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/);
982
1146
  await commands.get("gentle:status").handler("", ctx);
983
1147
  assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
984
1148
  assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
985
1149
 
986
1150
  await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
987
- assert.equal(ctx.ui.selections.length, 3, "preflight should run only once per session");
1151
+ assert.equal(ctx.ui.selections.length, 0, "automatic preflight should remain prompt-free for the session");
988
1152
  const promptHook = hooks.get("before_agent_start")[0];
989
1153
  const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
990
1154
  assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
991
- assert.match(promptResult.systemPrompt, /Execution mode: interactive/);
1155
+ assert.match(promptResult.systemPrompt, /Execution mode: auto/);
992
1156
  const workerPromptResult = await promptHook(
993
1157
  { agentName: "worker", systemPrompt: "worker base" },
994
1158
  ctx,
@@ -1003,7 +1167,7 @@ async function run() {
1003
1167
  await rm(globalModelsPath, { force: true });
1004
1168
  }
1005
1169
 
1006
- for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change"].entries()) {
1170
+ for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change", "/gentle-sdd-continue", "/gentle-sdd-status fix-rose --json", "/gentle-sdd-init"].entries()) {
1007
1171
  const slashSddCwd = await tempWorkspace();
1008
1172
  try {
1009
1173
  const ctx = createCtx(slashSddCwd, true, `slash-sdd-session-${index}`);
@@ -1013,7 +1177,7 @@ async function run() {
1013
1177
  });
1014
1178
  assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
1015
1179
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1016
- assert.equal(ctx.ui.selections.length, 3, `${text} should run canonical preflight`);
1180
+ assert.equal(ctx.ui.selections.length, 0, `${text} should use automatic preflight without selectors`);
1017
1181
  } finally {
1018
1182
  await rm(slashSddCwd, { recursive: true, force: true });
1019
1183
  }
@@ -1025,9 +1189,11 @@ async function run() {
1025
1189
  await commands.get("gentle:sdd-preflight").handler("", ctx);
1026
1190
  assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
1027
1191
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1028
- assert.equal(ctx.ui.selections.length, 3);
1192
+ assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts intentional choice fields");
1193
+ assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "one-option artifact store must be elided");
1194
+ assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
1029
1195
  await commands.get("gentle:sdd-preflight").handler("", ctx);
1030
- assert.equal(ctx.ui.selections.length, 3, "manual preflight command should reuse session choices");
1196
+ assert.equal(ctx.ui.selections.length, 4, "explicit preflight remains an intentional re-prompt");
1031
1197
  } finally {
1032
1198
  await rm(commandSddCwd, { recursive: true, force: true });
1033
1199
  }
@@ -1046,7 +1212,7 @@ async function run() {
1046
1212
  assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
1047
1213
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1048
1214
  assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1049
- assert.equal(ctx.ui.selections.length, 3);
1215
+ assert.equal(ctx.ui.selections.length, 0, "automatic SDD-agent startup must not render selectors");
1050
1216
  assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
1051
1217
  assert.doesNotMatch(
1052
1218
  promptResult.systemPrompt,
@@ -1067,7 +1233,7 @@ async function run() {
1067
1233
  },
1068
1234
  ctx,
1069
1235
  );
1070
- assert.equal(ctx.ui.selections.length, 3, "SDD agent guard should reuse session choices");
1236
+ assert.equal(ctx.ui.selections.length, 0, "SDD-agent startup should reuse automatic preferences");
1071
1237
  assert.doesNotMatch(
1072
1238
  reusedPromptResult.systemPrompt,
1073
1239
  /el Gentleman Identity and Harness/,
@@ -1089,7 +1255,7 @@ async function run() {
1089
1255
  ctx,
1090
1256
  );
1091
1257
  assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
1092
- assert.match(promptResult.systemPrompt, /No interactive UI was available/);
1258
+ assert.match(promptResult.systemPrompt, /canonical defaults or persisted choices/);
1093
1259
  assert.equal(ctx.ui.selections.length, 0);
1094
1260
  assert.equal(existsSync(join(noUiSddAgentCwd, ".pi", "agents", "sdd-apply.md")), false);
1095
1261
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
@@ -1115,14 +1281,14 @@ async function run() {
1115
1281
  pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1116
1282
  const ctx = createCtx(engramSddCwd, true, "engram-session");
1117
1283
  await commands.get("gentle:sdd-preflight").handler("", ctx);
1118
- assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "both"]);
1284
+ assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "hybrid"]);
1119
1285
  } finally {
1120
1286
  pi.setActiveTools(["read", "bash", "edit", "write"]);
1121
1287
  await rm(engramSddCwd, { recursive: true, force: true });
1122
1288
  }
1123
1289
 
1124
1290
  // Issue #64: selecting engram as the artifact store must not cause
1125
- // /sdd-init to create openspec/ or openspec/config.yaml.
1291
+ // /gentle-sdd-init to create openspec/ or openspec/config.yaml.
1126
1292
  const engramSddInitCwd = await tempWorkspace();
1127
1293
  try {
1128
1294
  pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
@@ -1132,21 +1298,21 @@ async function run() {
1132
1298
  return options[0];
1133
1299
  };
1134
1300
  await commands.get("gentle:sdd-preflight").handler("", ctx);
1135
- await commands.get("sdd-init").handler("", ctx);
1301
+ await commands.get("gentle-sdd-init").handler("", ctx);
1136
1302
  assert.equal(
1137
1303
  existsSync(join(engramSddInitCwd, "openspec")),
1138
1304
  false,
1139
- "/sdd-init must not create openspec/ when artifactStore is engram",
1305
+ "/gentle-sdd-init must not create openspec/ when artifactStore is engram",
1140
1306
  );
1141
1307
  assert.equal(
1142
1308
  existsSync(join(engramSddInitCwd, "openspec", "config.yaml")),
1143
1309
  false,
1144
- "/sdd-init must not write openspec/config.yaml when artifactStore is engram",
1310
+ "/gentle-sdd-init must not write openspec/config.yaml when artifactStore is engram",
1145
1311
  );
1146
1312
  assert.doesNotMatch(
1147
1313
  ctx.ui.notifications.at(-1).message,
1148
1314
  /Wrote openspec\/config\.yaml/,
1149
- "/sdd-init must not announce openspec/config.yaml when artifactStore is engram",
1315
+ "/gentle-sdd-init must not announce openspec/config.yaml when artifactStore is engram",
1150
1316
  );
1151
1317
  assert.match(
1152
1318
  ctx.ui.notifications.at(-1).message,
@@ -1166,30 +1332,30 @@ async function run() {
1166
1332
  pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1167
1333
  const ctx = createCtx(bothSddInitCwd, true, "both-sdd-init-session");
1168
1334
  ctx.ui.select = async (label, options) => {
1169
- if (label === "SDD artifact store") return "both";
1335
+ if (label === "SDD artifact store") return "hybrid";
1170
1336
  return options[0];
1171
1337
  };
1172
1338
  await commands.get("gentle:sdd-preflight").handler("", ctx);
1173
- await commands.get("sdd-init").handler("", ctx);
1339
+ await commands.get("gentle-sdd-init").handler("", ctx);
1174
1340
  assert.equal(
1175
1341
  existsSync(join(bothSddInitCwd, "openspec", "specs")),
1176
1342
  true,
1177
- "/sdd-init must create openspec/specs when artifactStore is both",
1343
+ "/gentle-sdd-init must create openspec/specs when artifactStore is both",
1178
1344
  );
1179
1345
  assert.equal(
1180
1346
  existsSync(join(bothSddInitCwd, "openspec", "changes", "archive")),
1181
1347
  true,
1182
- "/sdd-init must create openspec/changes/archive when artifactStore is both",
1348
+ "/gentle-sdd-init must create openspec/changes/archive when artifactStore is both",
1183
1349
  );
1184
1350
  assert.equal(
1185
1351
  existsSync(join(bothSddInitCwd, "openspec", "config.yaml")),
1186
1352
  true,
1187
- "/sdd-init must write openspec/config.yaml when artifactStore is both",
1353
+ "/gentle-sdd-init must write openspec/config.yaml when artifactStore is both",
1188
1354
  );
1189
1355
  assert.match(
1190
1356
  ctx.ui.notifications.at(-1).message,
1191
1357
  /Wrote openspec\/config\.yaml/,
1192
- "/sdd-init must announce openspec/config.yaml when artifactStore is both",
1358
+ "/gentle-sdd-init must announce openspec/config.yaml when artifactStore is both",
1193
1359
  );
1194
1360
  assert.equal(ctx.ui.notifications.at(-1).level, "info");
1195
1361
  } finally {
@@ -1202,7 +1368,7 @@ async function run() {
1202
1368
  pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
1203
1369
  const ctx = createCtx(directEngramToolCwd, true, "direct-engram-session");
1204
1370
  await commands.get("gentle:sdd-preflight").handler("", ctx);
1205
- assert.deepEqual(ctx.ui.selections[1].options, ["openspec"]);
1371
+ assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "unrecognized Engram capability must elide the artifact selector");
1206
1372
  } finally {
1207
1373
  pi.setActiveTools(["read", "bash", "edit", "write"]);
1208
1374
  await rm(directEngramToolCwd, { recursive: true, force: true });
@@ -1252,7 +1418,7 @@ async function run() {
1252
1418
  const sddCwd = await tempWorkspace();
1253
1419
  try {
1254
1420
  const ctx = createCtx(sddCwd, true);
1255
- await commands.get("sdd-init").handler("", ctx);
1421
+ await commands.get("gentle-sdd-init").handler("", ctx);
1256
1422
  assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
1257
1423
  assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
1258
1424
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
@@ -1260,12 +1426,12 @@ async function run() {
1260
1426
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
1261
1427
  assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1262
1428
  assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1263
- assert.equal(ctx.ui.selections.length, 3);
1429
+ assert.equal(ctx.ui.selections.length, 0, "sdd-init uses automatic preflight defaults");
1264
1430
  assert.match(ctx.ui.notifications[0].message, /SDD preflight complete/);
1265
1431
  assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
1266
1432
 
1267
1433
  await commands.get("gentle:sdd-preflight").handler("", ctx);
1268
- assert.equal(ctx.ui.selections.length, 3, "/sdd-init preflight should be reused by later manual preflight");
1434
+ assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts after automatic sdd-init");
1269
1435
  } finally {
1270
1436
  await rm(sddCwd, { recursive: true, force: true });
1271
1437
  }
@@ -1279,7 +1445,7 @@ async function run() {
1279
1445
  );
1280
1446
  await writeFile(globalModelsPath, "{ invalid json");
1281
1447
  const ctx = createCtx(invalidSddInitCwd, true, "invalid-sdd-init-session");
1282
- await commands.get("sdd-init").handler("", ctx);
1448
+ await commands.get("gentle-sdd-init").handler("", ctx);
1283
1449
  assert.equal(ctx.ui.notifications[0].level, "warning");
1284
1450
  assert.match(ctx.ui.notifications[0].message, /Model routing skipped:/);
1285
1451
  assert.match(ctx.ui.notifications[0].message, /models\.json/);