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
@@ -3,6 +3,7 @@ import { realpathSync } from "node:fs";
3
3
  import test from "node:test";
4
4
  import { initTheme, keyHint } from "@earendil-works/pi-coding-agent";
5
5
  import { imageFallback, visibleWidth } from "@earendil-works/pi-tui";
6
+ import { cardBody, cardHint, cardTitle, cardTone } from "./gentle-card-text.ts";
6
7
  import piPretty from "../extensions/pi-pretty.ts";
7
8
  import quietTools, {
8
9
  countNonEmptyLines,
@@ -172,6 +173,7 @@ test("quiet tool rendering registers noisy built-in tools", () => {
172
173
  for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"]) {
173
174
  const tool = tools.get(toolName);
174
175
  assert.ok(tool, `missing quiet renderer for ${toolName}`);
176
+ assert.equal(tool.renderShell, "self", `${toolName} must opt out of Pi's painted Box`);
175
177
  assert.equal(typeof tool.execute, "function", `${toolName} must delegate execution`);
176
178
  assert.ok(tool.parameters, `${toolName} must preserve built-in parameters`);
177
179
  }
@@ -180,7 +182,16 @@ test("quiet tool rendering registers noisy built-in tools", () => {
180
182
 
181
183
  test("quiet tool execution uses the tool-call cwd", async () => {
182
184
  const tool = registeredQuietTools().get("bash");
183
- const output = extractTextContent(await tool.execute("tool-call", { command: "pwd" }, new AbortController().signal, undefined, { cwd: "/tmp" })).trim();
185
+ const context = {
186
+ cwd: "/tmp",
187
+ sessionManager: {
188
+ getSessionId: () => "quiet-tool-test",
189
+ getSessionFile: () => undefined,
190
+ },
191
+ };
192
+ const output = extractTextContent(
193
+ await tool.execute("tool-call", { command: "pwd" }, new AbortController().signal, undefined, context),
194
+ ).trim();
184
195
  // pwd prints the physical directory: on macOS /tmp is a symlink to /private/tmp.
185
196
  assert.equal(output, realpathSync("/tmp"));
186
197
  assert.notEqual(output, process.cwd());
@@ -472,21 +483,21 @@ test("quiet tool rendering hides every collapsed direct Gentle AI result and pre
472
483
  const nonText = renderToString(tool.renderResult({ content: [{ type: "image", data: "opaque", mimeType: "image/png" }] }, { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
473
484
 
474
485
  assert.equal([...textRose].length, 2);
475
- assert.equal(call.trimEnd(), `${textRose} Gentle AI · running · review status`);
476
- assert.equal(collapsed, expandHint);
477
- assert.equal(collapsed.split("\n")[0], expandHint);
486
+ assert.equal(cardTitle(call), `🌹︎ Gentle AI · running · review status`);
487
+ assert.match(cardBody(collapsed), /\d+ lines?\b/);
488
+ assert.match(cardBody(collapsed), /\d+ lines?\b/);
478
489
  assert.doesNotMatch(collapsed, /next_transition|stop/);
479
- assert.equal(expanded.split("\n")[0], '{"next_transition":"stop"}');
490
+ assert.equal(cardBody(expanded).split("\n")[0], '{"next_transition":"stop"}');
480
491
  assert.match(expanded, /"next_transition":"stop"/);
481
492
  assert.doesNotMatch(expanded, /to expand/);
482
- assert.equal(failure, expandHint);
493
+ assert.match(cardBody(failure), /\d+ lines?\b/);
483
494
  assert.doesNotMatch(failure, /review status failed|authority unavailable|lineage=secret/);
484
495
  assert.match(expandedFailure, /review status failed: authority unavailable/);
485
496
  assert.match(expandedFailure, /lineage=secret/);
486
497
  assert.doesNotMatch(expandedFailure, /to expand/);
487
- assert.doesNotMatch(expandedFailure, /\x1b\[/);
488
- assert.equal(empty, "");
489
- assert.equal(nonText, "");
498
+ assert.doesNotMatch(cardBody(expandedFailure), /\x1b\[/);
499
+ assert.equal(cardBody(empty), "");
500
+ assert.equal(cardBody(nonText), "");
490
501
  });
491
502
 
492
503
  test("quiet tool rendering transitions one Gentle AI header through lifecycle states", () => {
@@ -528,16 +539,16 @@ test("quiet tool rendering transitions one Gentle AI header through lifecycle st
528
539
  assert.strictEqual(initial, running);
529
540
  assert.strictEqual(running, completed);
530
541
  assert.strictEqual(completed, failed);
531
- assert.equal(initialText, "<warning>🌹︎ Gentle AI · running · review status</warning>");
532
- assert.equal(runningText, "<warning>🌹︎ Gentle AI · running · review status</warning>");
533
- assert.equal(completedText, "<success>🌹︎ Gentle AI · completed · review status</success>");
534
- assert.equal(failedText, "<error>🌹︎ Gentle AI · failed · review status</error>");
542
+ assert.equal(cardTitle(initialText), "🌹︎ Gentle AI · running · review status"); assert.equal(cardTone(initialText), "warning");
543
+ assert.equal(cardTitle(runningText), "🌹︎ Gentle AI · running · review status"); assert.equal(cardTone(runningText), "warning");
544
+ assert.equal(cardTitle(completedText), "🌹︎ Gentle AI · completed · review status"); assert.equal(cardTone(completedText), "success");
545
+ assert.equal(cardTitle(failedText), "🌹︎ Gentle AI · failed · review status"); assert.equal(cardTone(failedText), "error");
535
546
  assert.doesNotMatch(failedText, /private-change/);
536
547
  });
537
548
 
538
549
  test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
539
550
  const tool = registeredQuietTools().get("bash");
540
- const rose = /🌹︎/;
551
+ const rose = /🌹︎ Gentle AI/;
541
552
  const render = (command: string, state: Record<string, unknown>, overrides: Record<string, unknown> = {}) => {
542
553
  const component = tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, state, argsComplete: false, ...overrides }));
543
554
  return [component, renderToString(component)] as const;
@@ -545,14 +556,14 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
545
556
  const command = "gentle-ai review status --token 'lineage-secret";
546
557
  const state = {};
547
558
  const [preparing, collapsed] = render(command, state);
548
- assert.equal(collapsed, "🌹︎ Gentle AI · preparing · review status");
559
+ assert.equal(cardTitle(collapsed), "🌹︎ Gentle AI · preparing · review status");
549
560
  assert.doesNotMatch(collapsed, /token|lineage-secret/);
550
561
  const expanded = render(command, state, { expanded: true, lastComponent: preparing })[1];
551
- assert.match(expanded, /^🌹︎ Gentle AI · preparing · review status\n\$ gentle-ai review status --token 'lineage-secret$/);
562
+ assert.equal(cardTitle(expanded), "🌹︎ Gentle AI · preparing · review status"); assert.equal(cardBody(expanded), "$ gentle-ai review status --token 'lineage-secret");
552
563
 
553
564
  const direct = "gentle-ai review status";
554
565
  const [running, runningText] = render(direct, state, { argsComplete: true, executionStarted: true, lastComponent: preparing });
555
- assert.strictEqual(running, preparing); assert.equal(runningText, "🌹︎ Gentle AI · running · review status");
566
+ assert.strictEqual(running, preparing); assert.equal(cardTitle(runningText), "🌹︎ Gentle AI · running · review status");
556
567
  const genericState = {};
557
568
  const [generic, genericText] = render("gentle-ai review status | cat", genericState);
558
569
  assert.doesNotMatch(genericText, rose); assert.match(genericText, /\| cat/);
@@ -569,7 +580,7 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
569
580
  assert.equal(genericResult.split("\n")[0], "generic result");
570
581
  const directState = {}; render(direct, directState, { argsComplete: true });
571
582
  const directResult = renderToolResult(tool, textResult("direct result"), { expanded: false, isPartial: false }, { args: { command: direct }, state: directState, argsComplete: true });
572
- assert.equal(directResult, keyHint("app.tools.expand", "to expand"));
583
+ assert.match(cardBody(directResult), /\d+ lines?\b/);
573
584
  });
574
585
 
575
586
  test("quiet tool rendering displays only finite safe Gentle AI operation paths", () => {
@@ -635,7 +646,7 @@ test("quiet tool rendering displays only finite safe Gentle AI operation paths",
635
646
  routineRenderContext({ args: { command } }),
636
647
  ),
637
648
  );
638
- assert.equal(rendered.trimEnd(), `${rose} Gentle AI · running · ${path}`, command);
649
+ assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${path}`, command);
639
650
  assert.doesNotMatch(rendered, /change-123|private|secret|lineage|sha256|result\.json|incident\.json/);
640
651
  }
641
652
  });
@@ -658,7 +669,7 @@ test("quiet tool rendering covers version and future standalone Gentle AI comman
658
669
  const rendered = renderToString(
659
670
  tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command } })),
660
671
  );
661
- assert.equal(rendered.trimEnd(), `${rose} Gentle AI · running · ${path}`, command);
672
+ assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${path}`, command);
662
673
  assert.doesNotMatch(rendered, /authorization-root|secret-change|private|C:\\\\private/);
663
674
  }
664
675
  });
@@ -686,7 +697,7 @@ test("quiet tool rendering recognizes exact quoted, escaped, Windows, and comman
686
697
 
687
698
  for (const command of routineCommands) {
688
699
  const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
689
- assert.equal(call.trimEnd(), "🌹︎ Gentle AI · running · version", command);
700
+ assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · version", command);
690
701
  }
691
702
 
692
703
  for (const command of ["'gentle-ai-copy' version", '"gentle-ai-copy.exe" version', "'gentle-ai' version && echo done"] as const) {
@@ -713,7 +724,7 @@ test("quiet tool rendering recognizes only the exact resolved dev binary", () =>
713
724
  ] as const;
714
725
  for (const [command, operationPath] of cases) {
715
726
  const call = renderToString(tool.renderCall({ command }, statusTheme, routineRenderContext({ args: { command } })));
716
- assert.equal(call.trimEnd(), `<warning>🌹︎ Gentle AI · running · ${operationPath}</warning>`, command);
727
+ assert.equal(cardTitle(call), `🌹︎ Gentle AI · running · ${operationPath}`, command); assert.equal(cardTone(call), "warning", command);
717
728
  assert.doesNotMatch(call, /gentle-ai-main|private|secret|hidden/);
718
729
  }
719
730
  const command = `${devPath} review status --prompt hidden-prompt --lineage lineage-secret --body private-body`;
@@ -728,11 +739,11 @@ test("quiet tool rendering recognizes only the exact resolved dev binary", () =>
728
739
  const expanded = renderToolResult(tool, textResult(text), { expanded: true, isPartial: false, isError: true }, lifecycleContext);
729
740
  const refreshed = renderToolResult(tool, textResult("result-secret"), { expanded: false, isPartial: false }, { args: { command: refreshedCommand } });
730
741
  const hint = keyHint("app.tools.expand", "to expand");
731
- assert.equal(collapsed, hint);
732
- assert.equal(collapsed.split(hint).length - 1, 1);
742
+ assert.match(cardBody(collapsed), /\d+ lines?\b/);
743
+ assert.equal(cardBody(collapsed).split(hint).length - 1, 0);
733
744
  assert.doesNotMatch(collapsed, /private|lineage|secret|hidden|error/);
734
745
  assert.match(expanded, /private failure|lineage=secret body=hidden/);
735
- assert.doesNotMatch(expanded, /to expand|\x1b\[/);
746
+ assert.doesNotMatch(cardBody(expanded), /to expand|\x1b\[/);
736
747
  assert.doesNotMatch(refreshed, /result-secret/);
737
748
  assert.ok(resolutions > 1);
738
749
  });
@@ -794,12 +805,12 @@ test("quiet tool rendering hides the routine partial result because the header o
794
805
  ),
795
806
  );
796
807
 
797
- assert.equal(partial, expandHint);
798
- assert.equal(partial.split(expandHint).length - 1, 1);
808
+ assert.match(cardBody(partial), /\d+ lines?\b/);
809
+ assert.equal(cardBody(partial).split(expandHint).length - 1, 0);
799
810
  assert.match(partialExpanded, /"status":"running"/);
800
811
  assert.doesNotMatch(partialExpanded, /to expand/);
801
- assert.equal(partialFailure, expandHint);
802
- assert.equal(completed, expandHint);
812
+ assert.match(cardBody(partialFailure), /\d+ lines?\b/);
813
+ assert.match(cardBody(completed), /\d+ lines?\b/);
803
814
 
804
815
  const partialExpandedFailure = renderToString(
805
816
  tool.renderResult(
@@ -810,7 +821,7 @@ test("quiet tool rendering hides the routine partial result because the header o
810
821
  ),
811
822
  );
812
823
  assert.match(partialExpandedFailure, /review status failed: authority unavailable/);
813
- assert.doesNotMatch(partialExpandedFailure, /\x1b\[/);
824
+ assert.doesNotMatch(cardBody(partialExpandedFailure), /\x1b\[/);
814
825
  });
815
826
 
816
827
  test("quiet tool rendering collapses grant calls to action and authorization-root cardinality", () => {
@@ -844,8 +855,8 @@ test("quiet tool rendering collapses grant calls to action and authorization-roo
844
855
  assert.strictEqual(running, completed);
845
856
  assert.strictEqual(completed, failed);
846
857
  const rendered = renderToString(failed);
847
- assert.equal(rendered, "<error>🌹︎ Gentle AI · failed · sdd attempt grant · 2 roots</error>");
848
- assert.doesNotMatch(rendered, /authorization-root|secret-change|repo\/root|other\/root|audit:|\x1b\[/);
858
+ assert.equal(cardTitle(rendered), "🌹︎ Gentle AI · failed · sdd attempt grant · 2 roots"); assert.equal(cardTone(rendered), "error");
859
+ assert.doesNotMatch(rendered.split(keyHint("app.tools.expand", "to expand")).join(""), /authorization-root|secret-change|repo\/root|other\/root|audit:|\x1b\[/);
849
860
  });
850
861
 
851
862
  test("quiet tool rendering counts grant authorization roots without rendering values", () => {
@@ -863,7 +874,7 @@ test("quiet tool rendering counts grant authorization roots without rendering va
863
874
 
864
875
  for (const [command, expected] of cases) {
865
876
  const rendered = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
866
- assert.equal(rendered.trimEnd(), `🌹︎ Gentle AI · running · ${expected}`, command);
877
+ assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${expected}`, command);
867
878
  assert.doesNotMatch(rendered, /authorization-root|\/repo\/root|\/one|\/two|\/three|change/);
868
879
  }
869
880
  });
@@ -883,8 +894,8 @@ test("quiet tool rendering hides invocation secrets from collapsed Gentle AI cal
883
894
  ),
884
895
  );
885
896
 
886
- assert.equal(call.trimEnd(), "🌹︎ Gentle AI · running · review finalize");
887
- assert.equal(collapsed, keyHint("app.tools.expand", "to expand"));
897
+ assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · review finalize");
898
+ assert.match(cardBody(collapsed), /\d+ lines?\b/);
888
899
  for (const forbidden of ["hidden-prompt", "lineage-secret", "private-body", "/private/root", "audit:"]) {
889
900
  assert.doesNotMatch(call, new RegExp(forbidden));
890
901
  assert.doesNotMatch(collapsed, new RegExp(forbidden));
@@ -973,7 +984,7 @@ test("quiet tool rendering sanitizes collapsed output and call rows", () => {
973
984
 
974
985
  const carriageReturn = renderToolResult(tools.get("bash"), textResult("prefix\rSECRET\r\nnext"), { expanded: false, isPartial: false }, { args: { command: "printf output" } });
975
986
  assert.match(collapsed, /safered/);
976
- assert.doesNotMatch(collapsed, /\x1b\[31m|\x1b\[0m/);
987
+ assert.doesNotMatch(cardBody(collapsed), /\x1b\[31m|\x1b\[0m/);
977
988
  assert.equal(call.trimEnd(), "$ echo red");
978
989
  assert.match(carriageReturn, /prefixSECRET\nnext/);
979
990
  assert.doesNotMatch(carriageReturn, /\r/);
@@ -1064,7 +1075,7 @@ test("quiet tool rendering recognizes a quoted exact dev override path containin
1064
1075
  const command = `"${devPath}" review status "literal \\$|#;"`;
1065
1076
  const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
1066
1077
  const collapsed = renderToolResult(tool, textResult("private result"), { expanded: false, isPartial: false }, { args: { command } });
1067
- assert.equal(call.trimEnd(), "🌹︎ Gentle AI · running · review status");
1078
+ assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · review status");
1068
1079
  assert.doesNotMatch(call, /\/opt\/Gentle AI\/gentle-ai|literal/);
1069
1080
  assert.doesNotMatch(collapsed, /private result/);
1070
1081
  });
@@ -1274,3 +1285,52 @@ test("quiet tool rendering respects command and env wrapper order", () => {
1274
1285
  assert.equal(gentleAiRoutineCommand({ command }), undefined);
1275
1286
  assertGenericBash(bash, command);
1276
1287
  });
1288
+
1289
+ test("quiet tool rendering lets a final result promote a replayed call card to its outcome", async () => {
1290
+ // The promotion invalidates after the render returns (a microtask), never inside it.
1291
+ const flush = () => new Promise((resolve) => queueMicrotask(() => resolve(undefined)));
1292
+ const { pi, tools } = createPi();
1293
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
1294
+ const tool = tools.get("bash");
1295
+ const command = "gentle-ai review status";
1296
+ const state = {};
1297
+ let invalidations = 0;
1298
+ const replayed = routineRenderContext({ args: { command }, state, argsComplete: false, executionStarted: false, isPartial: false, invalidate: () => { invalidations += 1; } });
1299
+
1300
+ const call = tool.renderCall({ command }, statusTheme, replayed);
1301
+ assert.equal(cardTitle(renderToString(call)), "🌹︎ Gentle AI · preparing · review status");
1302
+ renderToString(tool.renderResult(textResult("done"), { expanded: false, isPartial: false }, statusTheme, replayed));
1303
+ assert.equal(invalidations, 0, "never reentrant");
1304
+ await flush();
1305
+ assert.equal(invalidations, 1);
1306
+ const promoted = tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call });
1307
+ assert.strictEqual(promoted, call);
1308
+ assert.equal(cardTitle(renderToString(promoted)), "🌹︎ Gentle AI · completed · review status");
1309
+ assert.equal(cardTone(renderToString(promoted)), "success");
1310
+
1311
+ renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
1312
+ await flush();
1313
+ assert.equal(invalidations, 2);
1314
+ assert.equal(cardTitle(renderToString(tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call }))), "🌹︎ Gentle AI · failed · review status");
1315
+ renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
1316
+ await flush();
1317
+ assert.equal(invalidations, 2, "an unchanged outcome must not request another render");
1318
+ });
1319
+
1320
+ test("quiet tool rendering puts the expand key in the finished call card's top rule, not in the result", () => {
1321
+ const { pi, tools } = createPi();
1322
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
1323
+ const tool = tools.get("bash");
1324
+ const command = "gentle-ai review status";
1325
+ const running = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: true })));
1326
+ assert.equal(cardHint(running), undefined, "a running call has nothing to expand yet");
1327
+ const completed = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: false })));
1328
+ assert.match(cardHint(completed) ?? "", /to expand$/);
1329
+ assert.equal(cardTitle(completed), "🌹︎ Gentle AI · completed · review status");
1330
+ const expanded = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: true })));
1331
+ assert.match(cardHint(expanded) ?? "", /to collapse$/);
1332
+ const collapsedResult = renderToolResult(tool, textResult("{\"next_transition\":\"stop\"}"), { expanded: false, isPartial: false }, { args: { command } });
1333
+ assert.equal(cardBody(collapsedResult), "1 line");
1334
+ assert.equal(cardBody(renderToolResult(tool, textResult("a\nb\nc"), { expanded: false, isPartial: false }, { args: { command } })), "3 lines");
1335
+ assert.match(collapsedResult.split("\n").pop() ?? "", /╰─+╯$/);
1336
+ });
@@ -0,0 +1,226 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // gentle-pi#661/#662: RDD-aware verification rule for delegated work.
8
+ //
9
+ // The bounded writer always self-verifies: it runs the parent-authorized
10
+ // `## Verification` commands itself and reports observed output. Whether a
11
+ // SEPARATE `gentle-ai-verify` delegation is also required depends on the
12
+ // rendered `Receipt-driven development:` line, stated normatively exactly
13
+ // once in trigger 5 (Verification rule) and referenced -- not restated --
14
+ // everywhere else in this asset:
15
+ // - `on` -> the writer's own report is the verification of
16
+ // record; `gentle-ai-verify` is on-demand, except
17
+ // passive risk, which gets a structural readback.
18
+ // - `off`/`unknown` -> gentle-pi#662: the parent calls `gentle_review` with
19
+ // `{"operation":"assess"}` over the writer's diff and
20
+ // follows the returned plan by native risk tier
21
+ // (passive/medium/high/unassessable), instead of a
22
+ // blanket non-trivial judgment. An unknown RDD line
23
+ // never lowers a tier below `off`.
24
+ // These tests assert the exact distinctive sentences (not bare words like
25
+ // `off`/`unknown`/`partial`/`blocked`), that the tier table is stated exactly
26
+ // once, that the routing ladder paragraph references trigger 5 rather than
27
+ // restating it, and that `## Known environmental failures` has one canonical
28
+ // definition (owned by the worker asset) that the delegation asset
29
+ // references rather than duplicates.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const ROOT = join(import.meta.dirname, "..");
33
+
34
+ function read(relativePath: string): string {
35
+ return readFileSync(join(ROOT, relativePath), "utf8");
36
+ }
37
+
38
+ function countOccurrences(haystack: string, needle: string): number {
39
+ return haystack.split(needle).length - 1;
40
+ }
41
+
42
+ const delegation = read("assets/orchestrator-delegation.md");
43
+ const worker = read("assets/agents/gentle-ai-worker.md");
44
+
45
+ const ON_SENTENCE =
46
+ "When the line reads `on`, that writer report is the verification of record, and the native review is the independent check the writer cannot influence";
47
+ const OFF_UNKNOWN_SENTENCE =
48
+ 'When the line reads `off` or `unknown`, after the writer returns, call `gentle_review` with `{"operation":"assess"}` over the writer\'s diff and follow the returned plan instead of judging non-triviality from the task description: the operation resolves the native risk tier and states exactly who verifies next.';
49
+ const TIER_TABLE_HEADER = "| Native risk tier | Verification when RDD is `off`/`unknown` |";
50
+ const PASSIVE_TIER_ROW = "| passive | structural readback by the parent; no separate verifier, no tests |";
51
+ const MEDIUM_TIER_ROW = "| medium | writer self-verification stands; a separate `gentle-ai-verify` run is added only when the writer profile is a small model (mini or low effort) |";
52
+ const HIGH_TIER_ROW = "| high | writer self-verification plus a separate `gentle-ai-verify` run, always |";
53
+ const UNASSESSABLE_TIER_ROW = "| unknown / assess failed | treated as high |";
54
+ const SMALL_MODEL_BIAS_SENTENCE =
55
+ "The small-model bias raises the tier by one for verification purposes (medium becomes high); an unknown `Receipt-driven development:` line never lowers a tier below `off`.";
56
+ const SPOT_CHECK_SENTENCE =
57
+ "The parent spot check (re-running one reported command before delivery) stays required in every tier.";
58
+ // gentle-pi#668: the `on` branch of trigger 5 holds only while the native
59
+ // review actually reaches a terminal outcome for this candidate -- a decline,
60
+ // a clone-local disable, or a refused START/STATUS all fall back to the exact
61
+ // same risk-gated path as `off`.
62
+ const ON_BRANCH_FALLBACK_SENTENCE =
63
+ 'That `on` branch holds only while the native review actually reaches a terminal outcome for this candidate (gentle-pi#668): a human decline of the consent envelope for this candidate (candidate-scoped, never the RDD kill switch), a clone-local RDD disable discovered mid-flow, or a refused START/STATUS all fall back to the risk-gated path exactly as `off` -- call `gentle_review` with `{"operation":"assess"}` (pass `nativeReviewOutcome` when the parent already knows it; the tool derives it from what it itself observed for the candidate otherwise, failing closed to `unknown` when it cannot) and follow the returned plan.';
64
+
65
+ test("trigger 5 (Verification rule) states the exact on-line routing: writer report is the verification of record", () => {
66
+ assert.ok(delegation.includes(ON_SENTENCE), "trigger 5 is missing the exact on-line sentence");
67
+ });
68
+
69
+ test("trigger 5 states the exact off/unknown-line routing: the parent calls gentle_review's assess operation and follows the returned plan", () => {
70
+ assert.ok(delegation.includes(OFF_UNKNOWN_SENTENCE), "trigger 5 is missing the exact off/unknown-line sentence");
71
+ });
72
+
73
+ test("trigger 5 states the native risk tier table exactly once, with all four rows", () => {
74
+ for (const row of [TIER_TABLE_HEADER, PASSIVE_TIER_ROW, MEDIUM_TIER_ROW, HIGH_TIER_ROW, UNASSESSABLE_TIER_ROW]) {
75
+ assert.equal(countOccurrences(delegation, row), 1, `expected exactly one occurrence of tier table row: ${row}`);
76
+ }
77
+ });
78
+
79
+ test("trigger 5 states the small-model bias and the unknown-never-lowers-a-tier rule", () => {
80
+ assert.ok(delegation.includes(SMALL_MODEL_BIAS_SENTENCE), "trigger 5 is missing the small-model bias sentence");
81
+ });
82
+
83
+ test("trigger 5 keeps the parent spot check requirement in every tier", () => {
84
+ assert.ok(delegation.includes(SPOT_CHECK_SENTENCE), "trigger 5 is missing the parent spot check sentence");
85
+ });
86
+
87
+ test("trigger 5 states that the on branch holds only while the native review closes for this candidate, falling back to the off path exactly once (gentle-pi#668)", () => {
88
+ assert.equal(countOccurrences(delegation, ON_BRANCH_FALLBACK_SENTENCE), 1, "trigger 5 is missing (or duplicates) the on-branch fallback sentence");
89
+ });
90
+
91
+ test("the on-branch fallback sentence names all three non-closed triggers and never introduces a forbidden native RDD marker", () => {
92
+ for (const clause of ["decline", "clone-local", "refused START/STATUS"]) {
93
+ assert.ok(ON_BRANCH_FALLBACK_SENTENCE.includes(clause), `on-branch fallback sentence missing: ${clause}`);
94
+ }
95
+ for (const marker of ["gentle-ai review status", "next_transition", "review.capture-result", "review.validate", "reviewGate.result"]) {
96
+ assert.ok(!delegation.includes(marker), `stale/forbidden RDD marker introduced: ${marker}`);
97
+ }
98
+ });
99
+
100
+ test("the on-line and off/unknown-line routing sentences appear exactly once each (normative statement lives only in trigger 5)", () => {
101
+ for (const sentence of [ON_SENTENCE, OFF_UNKNOWN_SENTENCE]) {
102
+ assert.equal(countOccurrences(delegation, sentence), 1, `expected exactly one occurrence of: ${sentence.slice(0, 60)}...`);
103
+ }
104
+ });
105
+
106
+ test("trigger 5 never restates the retired #661 off/unknown non-trivial judgment", () => {
107
+ assert.doesNotMatch(delegation, /non-trivial change, in addition to the writer's own report/);
108
+ assert.doesNotMatch(delegation, /purely passive documentation with no behavior to verify/);
109
+ });
110
+
111
+ test("the Simple Delegation paragraph references trigger 5 instead of restating the on/off/unknown routing", () => {
112
+ assert.match(
113
+ delegation,
114
+ /per the RDD-aware Verification rule \(trigger 5 under Mandatory Delegation Triggers, gentle-pi#661\)/,
115
+ );
116
+ assert.match(delegation, /the normative on\/off\/unknown routing lives there, not here/);
117
+ });
118
+
119
+ test("delegation overlay's trigger 5 (Verification rule) is RDD-aware", () => {
120
+ assert.match(delegation, /\*\*Verification rule\*\*.*RDD-aware/);
121
+ });
122
+
123
+ test("delegation overlay reserves separate exploration for parent routing decisions", () => {
124
+ assert.match(delegation, /exploration stays reserved for when the parent needs the map to decide or route/i);
125
+ assert.match(delegation, /reading that prepares a write belongs with the writer/i);
126
+ });
127
+
128
+ test("delegation overlay keeps the required headings", () => {
129
+ for (const heading of [
130
+ "### Delegation Rules",
131
+ "#### Background Subagent Policy",
132
+ "#### Allowed edit surfaces (MANDATORY)",
133
+ "### 3. SDD (optional)",
134
+ ]) {
135
+ assert.ok(delegation.includes(heading), `delegation overlay lost required heading: ${heading}`);
136
+ }
137
+ });
138
+
139
+ test("worker asset declares the Verification section after Test discipline", () => {
140
+ const testDisciplineIndex = worker.indexOf("## Test discipline");
141
+ const verificationIndex = worker.indexOf("## Verification");
142
+ const interactionIndex = worker.indexOf("## Interaction contract");
143
+ assert.ok(testDisciplineIndex >= 0, "worker asset lost ## Test discipline");
144
+ assert.ok(verificationIndex >= 0, "worker asset is missing ## Verification");
145
+ assert.ok(interactionIndex >= 0, "worker asset lost ## Interaction contract");
146
+ assert.ok(
147
+ testDisciplineIndex < verificationIndex && verificationIndex < interactionIndex,
148
+ "## Verification must sit between ## Test discipline and ## Interaction contract",
149
+ );
150
+ });
151
+
152
+ test("worker asset requires foreground, one-at-a-time verification with nothing left unreported", () => {
153
+ for (const clause of [
154
+ "in the foreground",
155
+ "one at a time",
156
+ "Never launch a verification command in the background",
157
+ "never end the task with a listed command unreported",
158
+ ]) {
159
+ assert.ok(worker.includes(clause), `worker asset is missing: ${clause}`);
160
+ }
161
+ });
162
+
163
+ test("worker asset reports each verification command as <exact command>: <observed result> in validation", () => {
164
+ assert.ok(worker.includes("`<exact command>: <observed result>`"));
165
+ assert.ok(worker.includes("in `validation`"));
166
+ });
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Contract consistency (`## Known environmental failures`): defined exactly
170
+ // once, in the worker asset, as "exact test names (or exact command lines)
171
+ // that already fail on the base"; the writer reports those as evidence, but
172
+ // any OTHER failing required command still forces `status: partial`. The
173
+ // delegation asset must reference this same definition, not restate it.
174
+ // ---------------------------------------------------------------------------
175
+
176
+ test("worker asset owns the canonical Known environmental failures definition", () => {
177
+ assert.ok(worker.includes("## Known environmental failures"));
178
+ assert.match(worker, /this is the canonical definition; other assets reference it, they do not restate it/i);
179
+ assert.match(worker, /lists exact test names or exact command lines that already fail on the base/i);
180
+ assert.match(worker, /Any OTHER required command that fails -- one not named under that heading -- still forces `status: partial`\./);
181
+ });
182
+
183
+ test("delegation asset references the worker's Known environmental failures definition instead of restating it", () => {
184
+ assert.match(
185
+ delegation,
186
+ /`## Known environmental failures` follows the same definition as `gentle-ai-worker`'s Verification contract: exact pre-existing base failures reported as evidence, never blockers -- any other failing required command still forces `status: partial`\./,
187
+ );
188
+ // The full canonical wording ("lists exact test names or exact command
189
+ // lines that already fail on the base") must not be duplicated here.
190
+ assert.doesNotMatch(delegation, /lists exact test names or exact command lines that already fail on the base/i);
191
+ });
192
+
193
+ test("worker asset never claims completion while a required verification command fails under RDD, except a named environmental failure", () => {
194
+ assert.match(worker, /this report is the verification of record/i);
195
+ assert.match(worker, /native review remains the independent check/i);
196
+ assert.match(
197
+ worker,
198
+ /never report `status: completed` while a required command under `## Verification` is failing, unless that exact failure is named under `## Known environmental failures`\./i,
199
+ );
200
+ });
201
+
202
+ test("worker keeps candidate review disposition and lifecycle parent-owned", () => {
203
+ for (const clause of [
204
+ "The primary parent owns candidate review disposition and lifecycle, including preflight and any explicit candidate-level opt-out.",
205
+ "Never search for, request, or invoke review tools, including `gentle_review`.",
206
+ "Missing review tools never block this worker's implementation or verification handoff.",
207
+ ]) {
208
+ assert.ok(worker.includes(clause), `worker asset is missing: ${clause}`);
209
+ }
210
+ });
211
+
212
+ test("worker asset keeps the existing Return contract fields", () => {
213
+ for (const field of [
214
+ "status: completed | partial | blocked | interaction_required",
215
+ "summary:",
216
+ "files_changed:",
217
+ "tdd_evidence:",
218
+ "validation:",
219
+ "risks:",
220
+ "review_focus:",
221
+ "skill_resolution:",
222
+ "interaction_required:",
223
+ ]) {
224
+ assert.ok(worker.includes(field), `worker asset lost Return contract field: ${field}`);
225
+ }
226
+ });