gentle-pi 3.7.0 → 4.0.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 (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -0,0 +1,242 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // Unit 4 — L6 wheel slice (spec C5, design §D6).
7
+ //
8
+ // Source-parse structural pins on extensions/history/index.ts (no pi-tui
9
+ // runtime graph — the same discipline as the other source-parse suites).
10
+ // The overlay renders only through pi-tui, so the unit-level contract is the
11
+ // SHAPE of the handleMouse override:
12
+ //
13
+ // - wheel-only: every non-wheel event type returns undefined (press/click/
14
+ // drag stay host-owned) and the dispatch table gains no extra entry (wheel
15
+ // is not a keybinding — dispatch.test.ts remains the authoritative
16
+ // untouched pin);
17
+ // - ONE consumed wheel return: `handled: true` plus the synthetic target
18
+ // enrichment, reached by every wheel path including the no-op regions —
19
+ // this closes the pre-existing fullscreen SGR-fallthrough hazard by
20
+ // construction;
21
+ // - fixed 30-row geometry routing: list region y 5–14, preview region y 17–26,
22
+ // all other rows consumed no-ops;
23
+ // - list wheel: sign × |wheelDelta| steps through moveDown (the arrow grow
24
+ // path applies per step) / moveUp, magnitude clamped to the filtered list,
25
+ // zero/absent delta a no-op move — the override itself never re-implements
26
+ // growth;
27
+ // - preview wheel: 1 line per notch toward the delta direction via the
28
+ // existing clampPreviewOffset semantics + rebuildPreview.
29
+
30
+ const selectorSource = fs.readFileSync(
31
+ fileURLToPath(new URL("../extensions/history/index.ts", import.meta.url)),
32
+ "utf8",
33
+ );
34
+
35
+ // T13 — AC-L6-1: wheel-only override + no extra dispatch entry.
36
+
37
+ test("handleMouse override is wheel-only and the dispatch table keeps 12 entries (AC-L6-1)", () => {
38
+ const decl = selectorSource.indexOf("override handleMouse(");
39
+ assert.ok(decl >= 0, "PromptHistorySelector should override handleMouse");
40
+ const end = selectorSource.indexOf("\n }", decl);
41
+ assert.ok(end > decl, "handleMouse's body should close");
42
+ const body = selectorSource.slice(decl, end);
43
+
44
+ assert.ok(
45
+ body.includes('if (event.type !== "wheel") return undefined;'),
46
+ "non-wheel event types must return undefined (press/click/drag stay host-owned)",
47
+ );
48
+ assert.ok(
49
+ body.includes('ReturnType<Container["handleMouse"]>'),
50
+ 'the return type must name the base contract via ReturnType<Container["handleMouse"]>',
51
+ );
52
+
53
+ const tableAt = selectorSource.indexOf(
54
+ "private readonly dispatch: readonly DispatchEntry[] = [",
55
+ );
56
+ assert.ok(tableAt >= 0, "the dispatch table should exist");
57
+ const tableEnd = selectorSource.indexOf("\n ];", tableAt);
58
+ assert.ok(tableEnd > tableAt, "the dispatch table should close");
59
+ const table = selectorSource.slice(tableAt, tableEnd);
60
+ const entries = table.split("match:").length - 1;
61
+ assert.equal(
62
+ entries,
63
+ 12,
64
+ "wheel is not a keybinding: exactly 12 dispatch entries, no 13th",
65
+ );
66
+ });
67
+
68
+ // T13 — AC-L6-2: ONE consumed wheel return with the target enrichment,
69
+ // reached by every wheel path including the no-op regions.
70
+
71
+ test("every wheel path reaches the single handled:true return with target enrichment (AC-L6-2)", () => {
72
+ const decl = selectorSource.indexOf("override handleMouse(");
73
+ assert.ok(decl >= 0, "handleMouse should exist");
74
+ const end = selectorSource.indexOf("\n }", decl);
75
+ const body = selectorSource.slice(decl, end);
76
+
77
+ const returns = body.split("return").length - 1;
78
+ assert.equal(
79
+ returns,
80
+ 2,
81
+ "exactly two returns: the guard's undefined and the ONE consumed wheel return",
82
+ );
83
+ assert.equal(
84
+ body.split("handled: true").length - 1,
85
+ 1,
86
+ "exactly one handled:true — the single wheel return",
87
+ );
88
+ assert.equal(
89
+ body.split("return {").length - 1,
90
+ 1,
91
+ "exactly one object return, so list, preview, and no-op regions all reach it",
92
+ );
93
+
94
+ // Synthetic target mirroring dispatchMouseEvent's enrichment math
95
+ // (pi-tui tui.js dispatchMouseEvent: originX = screenX - x, originY =
96
+ // screenY - y, bounds from the event) — the result carries `target`, so
97
+ // dispatch passes it through verbatim.
98
+ assert.ok(body.includes("component: this,"), "target.component: this");
99
+ assert.ok(
100
+ body.includes("originX: event.screenX - event.x,"),
101
+ "target.originX mirrors the dispatch enrichment math",
102
+ );
103
+ assert.ok(
104
+ body.includes("originY: event.screenY - event.y,"),
105
+ "target.originY mirrors the dispatch enrichment math",
106
+ );
107
+ assert.ok(body.includes("width: event.width,"), "target bounds width");
108
+ assert.ok(body.includes("height: event.height,"), "target bounds height");
109
+
110
+ // No manual render: pi-tui re-renders handled wheels by default.
111
+ assert.ok(
112
+ !body.includes("requestRender"),
113
+ "handleMouse must not call requestRender (wheel results render by default)",
114
+ );
115
+ });
116
+
117
+ // T13 — AC-L6-3: region routing truth table — the fixed 30-row geometry's
118
+ // list band 5–14 and preview band 17–26 appear as the y comparisons, all
119
+ // other rows fall through to the consumed no-op return.
120
+
121
+ test("region constants 5-14 / 17-26 route the y comparisons (AC-L6-3)", () => {
122
+ assert.ok(
123
+ selectorSource.includes("const LIST_WHEEL_Y_FIRST = 5;"),
124
+ "LIST_WHEEL_Y_FIRST = 5 (list container rows)",
125
+ );
126
+ assert.ok(
127
+ selectorSource.includes("const LIST_WHEEL_Y_LAST = 14;"),
128
+ "LIST_WHEEL_Y_LAST = 14",
129
+ );
130
+ assert.ok(
131
+ selectorSource.includes("const PREVIEW_WHEEL_Y_FIRST = 17;"),
132
+ "PREVIEW_WHEEL_Y_FIRST = 17 (preview container rows)",
133
+ );
134
+ assert.ok(
135
+ selectorSource.includes("const PREVIEW_WHEEL_Y_LAST = 26;"),
136
+ "PREVIEW_WHEEL_Y_LAST = 26",
137
+ );
138
+
139
+ const decl = selectorSource.indexOf("override handleMouse(");
140
+ assert.ok(decl >= 0, "handleMouse should exist");
141
+ const end = selectorSource.indexOf("\n }", decl);
142
+ const body = selectorSource.slice(decl, end);
143
+
144
+ assert.ok(
145
+ body.includes("event.y >= this.listWheelFirstRow") &&
146
+ body.includes("event.y <= LIST_WHEEL_Y_LAST"),
147
+ "the list branch must compare y against the list band",
148
+ );
149
+ assert.ok(
150
+ body.includes("event.y >= PREVIEW_WHEEL_Y_FIRST") &&
151
+ body.includes("event.y <= PREVIEW_WHEEL_Y_LAST"),
152
+ "the preview branch must compare y against the preview band",
153
+ );
154
+ });
155
+
156
+ // T13 — AC-L6-4: list wheel semantics — sign picks the direction, magnitude
157
+ // is clamped to the filtered list, zero/absent delta is a no-op, and the
158
+ // routing goes THROUGH moveDown's own grow path (never a re-implementation).
159
+
160
+ test("list wheel routes sign-clamped steps through moveDown/moveUp (AC-L6-4)", () => {
161
+ const decl = selectorSource.indexOf("override handleMouse(");
162
+ assert.ok(decl >= 0, "handleMouse should exist");
163
+ const end = selectorSource.indexOf("\n }", decl);
164
+ const body = selectorSource.slice(decl, end);
165
+
166
+ // Absent wheelDelta normalizes to 0 → zero steps → no-op move.
167
+ assert.ok(
168
+ body.includes("const delta = event.wheelDelta ?? 0;"),
169
+ "delta must default an absent wheelDelta to 0",
170
+ );
171
+
172
+ const listStart = body.indexOf("if (event.y >= this.listWheelFirstRow");
173
+ const listEnd = body.indexOf("} else if (", listStart);
174
+ assert.ok(
175
+ listStart >= 0 && listEnd > listStart,
176
+ "the list branch should exist",
177
+ );
178
+ const listBranch = body.slice(listStart, listEnd);
179
+
180
+ assert.ok(
181
+ listBranch.includes(
182
+ "const steps = Math.min(Math.abs(delta), this.filteredRecords.length);",
183
+ ),
184
+ "magnitude must clamp to the filtered list length",
185
+ );
186
+ assert.ok(
187
+ listBranch.includes("for (let i = 0; i < steps; i++) {"),
188
+ "steps must move one row at a time (0 for a zero delta — the no-op)",
189
+ );
190
+ assert.ok(
191
+ listBranch.includes("if (delta > 0) this.moveDown();") &&
192
+ listBranch.includes("else this.moveUp();"),
193
+ "sign semantics: positive delta moves down through moveDown, else up",
194
+ );
195
+
196
+ // Prefetch interplay intact: growth belongs to moveDown itself — the
197
+ // override must not re-implement the trigger.
198
+ assert.ok(
199
+ !body.includes("shouldGrowWindow") && !body.includes("nextLoadedCount"),
200
+ "the override must not re-implement growth (wheel-down grows via moveDown)",
201
+ );
202
+ });
203
+
204
+ // T13 — AC-L6-5: preview wheel semantics — one line per notch toward the
205
+ // delta direction through the existing clamp, zero-delta no-op.
206
+
207
+ test("preview wheel scrolls one clamped line per notch (AC-L6-5)", () => {
208
+ const decl = selectorSource.indexOf("override handleMouse(");
209
+ assert.ok(decl >= 0, "handleMouse should exist");
210
+ const end = selectorSource.indexOf("\n }", decl);
211
+ const body = selectorSource.slice(decl, end);
212
+
213
+ const previewStart = body.indexOf("} else if (");
214
+ const previewEnd = body.indexOf("return {", previewStart);
215
+ assert.ok(
216
+ previewStart >= 0 && previewEnd > previewStart,
217
+ "the preview branch should exist",
218
+ );
219
+ const previewBranch = body.slice(previewStart, previewEnd);
220
+
221
+ assert.ok(
222
+ previewBranch.includes("if (delta !== 0) {"),
223
+ "a zero delta must be a no-op in the preview band too",
224
+ );
225
+ assert.ok(
226
+ previewBranch.includes("this.previewScrollOffset = clampPreviewOffset("),
227
+ "the preview must scroll through the existing clamp semantics",
228
+ );
229
+ assert.ok(
230
+ previewBranch.includes("this.previewScrollOffset + (delta > 0 ? 1 : -1)"),
231
+ "exactly one line per notch toward the delta direction",
232
+ );
233
+ assert.ok(
234
+ previewBranch.includes("this.wrappedPreviewLines.length") &&
235
+ previewBranch.includes("PREVIEW_ROWS"),
236
+ "the clamp must run against the wrapped length and the viewport rows",
237
+ );
238
+ assert.ok(
239
+ previewBranch.includes("this.rebuildPreview()"),
240
+ "the preview must re-render after the offset change",
241
+ );
242
+ });
@@ -88,15 +88,20 @@ function capturingComplete(assistant: AssistantMessage) {
88
88
  */
89
89
  function signalAwaitingComplete(): typeof completeSimple {
90
90
  return (async (_model: Model<Api>, _context: Context, options?: SimpleStreamOptions) => {
91
- return await new Promise<AssistantMessage>((_resolve, reject) => {
92
- const signal = options?.signal;
93
- if (signal === undefined) return;
94
- if (signal.aborted) {
95
- reject(new Error("aborted"));
96
- return;
97
- }
98
- signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
99
- });
91
+ const keepalive = setTimeout(() => {}, 60_000);
92
+ try {
93
+ return await new Promise<AssistantMessage>((_resolve, reject) => {
94
+ const signal = options?.signal;
95
+ if (signal === undefined) return;
96
+ if (signal.aborted) {
97
+ reject(new Error("aborted"));
98
+ return;
99
+ }
100
+ signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
101
+ });
102
+ } finally {
103
+ clearTimeout(keepalive);
104
+ }
100
105
  }) as typeof completeSimple;
101
106
  }
102
107
 
@@ -107,16 +112,21 @@ function signalAwaitingComplete(): typeof completeSimple {
107
112
  */
108
113
  function signalResolvingAbortedComplete(partialText = "partial revi"): typeof completeSimple {
109
114
  return (async (_model, _context, options) => {
110
- return await new Promise<AssistantMessage>((resolve) => {
111
- const signal = options?.signal;
112
- const settle = () => resolve(assistantText(partialText, { stopReason: "aborted" }));
113
- if (signal === undefined) return;
114
- if (signal.aborted) {
115
- settle();
116
- return;
117
- }
118
- signal.addEventListener("abort", settle, { once: true });
119
- });
115
+ const keepalive = setTimeout(() => {}, 60_000);
116
+ try {
117
+ return await new Promise<AssistantMessage>((resolve) => {
118
+ const signal = options?.signal;
119
+ const settle = () => resolve(assistantText(partialText, { stopReason: "aborted" }));
120
+ if (signal === undefined) return;
121
+ if (signal.aborted) {
122
+ settle();
123
+ return;
124
+ }
125
+ signal.addEventListener("abort", settle, { once: true });
126
+ });
127
+ } finally {
128
+ clearTimeout(keepalive);
129
+ }
120
130
  }) as typeof completeSimple;
121
131
  }
122
132
 
@@ -69,6 +69,35 @@ test("publishes reviewed Issue Form bodies through a private body file", () => {
69
69
  assert.match(skill, /target-host read-back/i);
70
70
  });
71
71
 
72
+ function assertRemoteDiscoveryGate(candidate: string) {
73
+ const gate = candidate.indexOf("## Safe Discovery");
74
+ const probe = candidate.indexOf("\ngh auth status\n", gate);
75
+ assert.ok(gate >= 0 && probe > gate);
76
+ const beforeProbe = candidate.slice(gate, probe);
77
+ assert.match(beforeProbe, /explicit (?:human|user) authorization.*destination.*operation.*credential\/session/is);
78
+ assert.match(beforeProbe, /(?:do not|never).*probe.*(?:credential|session).*ambiguity/is);
79
+ }
80
+
81
+ test("requires remote authorization before authentication or target discovery", () => {
82
+ assertRemoteDiscoveryGate(skill);
83
+ assert.throws(() => assertRemoteDiscoveryGate(skill.replace(/explicit (?:human|user) authorization[^\n]*/i, "Remote checks are read-only.")));
84
+ });
85
+
86
+ function assertProtectedFormLabels(candidate: string) {
87
+ const creation = candidate.slice(candidate.indexOf("## Safe Discovery"), candidate.indexOf("## Duplicate And Form Decision"));
88
+ assert.match(creation, /(?:YAML|form).*labels/is);
89
+ assert.match(creation, /protected.*(?:label|labels)/is);
90
+ assert.match(creation, /(?:exact|label-specific).*direct (?:human|user) instruction/is);
91
+ assert.match(creation, /`MAINTAIN` or `ADMIN`/);
92
+ assert.match(creation, /(?:skip|omit).*protected.*(?:optional|permitted)/is);
93
+ assert.match(creation, /(?:required|mandatory).*fail closed/is);
94
+ }
95
+
96
+ test("gates protected form labels during issue creation, including required labels", () => {
97
+ assertProtectedFormLabels(skill);
98
+ assert.throws(() => assertProtectedFormLabels(skill.replace(/`MAINTAIN` or `ADMIN`/, "`WRITE`")));
99
+ });
100
+
72
101
  function assertBrowserContract(candidate: string) {
73
102
  assert.match(candidate, /gh issue create --repo "\$TARGET" --title "\$TITLE" --body-file "\$BODY_FILE"/);
74
103
  assert.match(
@@ -101,6 +130,38 @@ test("keeps automated and conditional browser Issue Form paths distinct", () =>
101
130
  }
102
131
  });
103
132
 
133
+ const delegated = readFileSync(
134
+ join(repoRoot, "skills", "issue-creation", "references", "delegated-workflow-actions.md"),
135
+ "utf8",
136
+ );
137
+
138
+ function assertDelegatedAuthority(candidate: string) {
139
+ assert.match(candidate, /current direct human instruction.*exact `HOST`, `REPO`, issue-or-PR number, and action/i);
140
+ assert.match(candidate, /target-host `viewerPermission`.*`MAINTAIN` or `ADMIN`/i);
141
+ assert.match(candidate, /no separate target-host proof of the instruction-giver's identity/i);
142
+ assert.match(candidate, /`size:exception`.*current human-approved rationale/i);
143
+ assert.match(candidate, /one mutation attempt.*never retry timeouts or uncertain responses/i);
144
+ assert.match(candidate, /target-host post-readback.*`no_write`.*`unknown`/i);
145
+ }
146
+
147
+ test("delegates only separately authorized, bounded post-publication actions", () => {
148
+ assert.match(skill, /\[delegated workflow actions\]\(references\/delegated-workflow-actions\.md\)/);
149
+ assert.match(skill, /Publication is not authorization for a later action/i);
150
+ assertDelegatedAuthority(delegated);
151
+ assert.throws(() => assertDelegatedAuthority(delegated.replace(/`MAINTAIN` or `ADMIN`/, "`WRITE`")));
152
+ assert.throws(() => assertDelegatedAuthority(delegated.replace(/current human-approved rationale/, "inferred from diff size")));
153
+ });
154
+
155
+ test("keeps checkbox agency distinct from later label authority", () => {
156
+ assertCheckboxAttestationContract(skill);
157
+ assert.match(delegated, /operational checkboxes.*retained agent evidence/i);
158
+ assert.match(delegated, /personal, legal and user-consent declarations.*explicit first-person user affirmation/i);
159
+ assert.match(delegated, /never a delegated label action or the publication request/i);
160
+ assert.match(delegated, /do not create, rename, or substitute a label/i);
161
+ assert.match(delegated, /atomic update.*conflicting status labels/i);
162
+ assert.doesNotMatch(delegated, /(?:Strict TDD|sdd-init|TUI)/i);
163
+ });
164
+
104
165
  test("discovers support routing and keeps all issue files private until cleanup", () => {
105
166
  assert.match(skill, /README\.md/);
106
167
  assert.match(skill, /\.github\/ISSUE_TEMPLATE\/config\.yml/);
@@ -32,6 +32,22 @@ test("model routing authority normalizes and preserves sync/async source status"
32
32
  authority.normalizeRoutingEntry({ model: " anthropic/opus ", thinking: "high" }),
33
33
  { model: "anthropic/opus", thinking: "high" },
34
34
  );
35
+ assert.deepEqual(
36
+ authority.normalizeRoutingEntry({ model: "anthropic/opus", effort: "high" }),
37
+ { model: "anthropic/opus", thinking: "high" },
38
+ );
39
+ assert.deepEqual(
40
+ authority.normalizeRoutingEntry({ effort: "medium" }),
41
+ { model: undefined, thinking: "medium" },
42
+ );
43
+ assert.deepEqual(
44
+ authority.normalizeRoutingEntry({ model: "anthropic/opus", thinking: "high", effort: "low" }),
45
+ { model: "anthropic/opus", thinking: "high" },
46
+ );
47
+ assert.deepEqual(
48
+ authority.normalizeRoutingEntry({ model: "anthropic/opus", effort: "invalid" }),
49
+ { model: "anthropic/opus", thinking: undefined },
50
+ );
35
51
  assert.deepEqual(authority.normalizeRoutingEntry(null), undefined);
36
52
  assert.deepEqual(
37
53
  authority.normalizeModelConfig({