gentle-pi 3.6.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 (320) 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 +95 -30
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +117 -235
  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 +509 -275
  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 +105 -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/foreign-target-grants.ts +32 -0
  55. package/lib/gentle-ai-elapsed-store.ts +87 -0
  56. package/lib/gentle-ai-renderer.ts +196 -39
  57. package/lib/gentle-shell-launcher.ts +24 -13
  58. package/lib/gentle-shell-resume-hint.ts +176 -0
  59. package/lib/history-capture-policy.ts +95 -0
  60. package/lib/model-routing-authority.ts +5 -1
  61. package/lib/nan-provider.ts +227 -0
  62. package/lib/native-review-cli.ts +53 -108
  63. package/lib/odd-phase-inference.ts +231 -0
  64. package/lib/odd-phase.ts +141 -0
  65. package/lib/overlay-repaint.ts +26 -0
  66. package/lib/pi-tui-keys.ts +53 -0
  67. package/lib/review-candidate-view-owner.ts +67 -17
  68. package/lib/review-candidate-view.ts +112 -25
  69. package/lib/review-reminder-receipt.ts +48 -8
  70. package/lib/review-risk-assessment.ts +156 -11
  71. package/lib/review-sidebar-state.ts +223 -0
  72. package/lib/selection-engine.ts +515 -0
  73. package/lib/session-change-capture.ts +10 -1
  74. package/lib/session-messaging-grants.ts +135 -0
  75. package/lib/session-worktree-registry.ts +14 -2
  76. package/lib/shell-bar.ts +179 -24
  77. package/lib/shell-card.ts +287 -18
  78. package/lib/shell-changes-view.ts +2 -1
  79. package/lib/shell-prompt.ts +102 -6
  80. package/lib/shell-sidebar-layout.ts +78 -14
  81. package/lib/shell-sidebar.ts +15 -1
  82. package/lib/shell-todo.ts +23 -13
  83. package/lib/shell-usage-view.ts +9 -4
  84. package/lib/shell-usage.ts +66 -10
  85. package/lib/stats-collector.ts +381 -0
  86. package/lib/stats-view.ts +431 -0
  87. package/lib/theme-customization.ts +52 -0
  88. package/lib/vim-editor-adapter.ts +379 -0
  89. package/lib/vim-normal-engine.ts +154 -0
  90. package/lib/vim-operator-engine.ts +416 -0
  91. package/lib/vim-policy.ts +49 -0
  92. package/lib/vim-visual-engine.ts +107 -0
  93. package/lib/visual-customization-policy.ts +108 -0
  94. package/lib/visual-customize-view.ts +330 -0
  95. package/lib/visual-profiles.ts +228 -0
  96. package/lib/yolo-session-policy.ts +240 -0
  97. package/package.json +20 -8
  98. package/runtime/gentle-shell-launcher.mjs +23 -12
  99. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  100. package/runtime/native-review-cli.mjs +53 -108
  101. package/runtime/review-risk-assessment.mjs +154 -9
  102. package/scripts/build-runtime-modules.mjs +1 -0
  103. package/scripts/gentle-ai-installer.mjs +14 -13
  104. package/scripts/mirror-odd-routing.mjs +2 -2
  105. package/scripts/run-test-suite.mjs +76 -0
  106. package/scripts/test-packed-runner.mjs +31 -14
  107. package/scripts/verify-package-files.mjs +11 -22
  108. package/skills/branch-pr/SKILL.md +24 -52
  109. package/skills/chained-pr/SKILL.md +31 -15
  110. package/skills/chained-pr/references/chaining-details.md +31 -20
  111. package/skills/gentle-ai/SKILL.md +9 -15
  112. package/skills/issue-creation/SKILL.md +8 -2
  113. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  114. package/skills/work-unit-commits/SKILL.md +4 -3
  115. package/tests/agent-profiles.test.ts +18 -0
  116. package/tests/agents-fake-child.ts +2 -2
  117. package/tests/agents-message-delivery.test.ts +106 -0
  118. package/tests/agents-protocol.test.ts +40 -0
  119. package/tests/agents-runner.test.ts +400 -89
  120. package/tests/agents-view-thread-identity.test.ts +169 -0
  121. package/tests/agents-view.test.ts +8 -2
  122. package/tests/agents-widget.test.ts +154 -15
  123. package/tests/append-system-prompt-route.test.ts +160 -0
  124. package/tests/append-system-prompt.test.ts +46 -0
  125. package/tests/artifact-language.test.ts +19 -213
  126. package/tests/ask-user-question.test.ts +44 -1
  127. package/tests/asset-installation-runtime.test.ts +5 -16
  128. package/tests/autonomous-guard.test.ts +69 -1
  129. package/tests/bounded-writer-admission.test.ts +95 -0
  130. package/tests/branch-pr-skill.test.ts +43 -0
  131. package/tests/card-style-policy.test.ts +55 -0
  132. package/tests/chained-pr-skill.test.ts +124 -0
  133. package/tests/child-context-files.test.ts +255 -0
  134. package/tests/child-safety.test.ts +82 -0
  135. package/tests/codemode-rendering.test.ts +491 -0
  136. package/tests/command-palette.test.ts +39 -3
  137. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  138. package/tests/destructive-command-guard.test.ts +84 -0
  139. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  140. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  145. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  146. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  148. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  149. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  150. package/tests/foreign-target-grants.test.ts +58 -0
  151. package/tests/generic-agent-tools.test.ts +54 -0
  152. package/tests/gentle-agents.test.ts +1411 -256
  153. package/tests/gentle-ai-binary.test.ts +3 -3
  154. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  155. package/tests/gentle-ai-installer.test.ts +68 -54
  156. package/tests/gentle-ai-renderer.test.ts +487 -8
  157. package/tests/gentle-ai.test.ts +288 -65
  158. package/tests/gentle-card-text.ts +2 -1
  159. package/tests/gentle-shell-bin.test.ts +651 -114
  160. package/tests/gentle-shell-launcher.test.ts +99 -52
  161. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  162. package/tests/gentle-shell.test.ts +3839 -187
  163. package/tests/gentle-stats.test.ts +152 -0
  164. package/tests/gentle-theme.test.ts +4 -1
  165. package/tests/gentle-todo.test.ts +80 -8
  166. package/tests/history-atomic-write.test.ts +57 -0
  167. package/tests/history-capture-policy.test.ts +102 -0
  168. package/tests/history-command-registration.test.ts +164 -0
  169. package/tests/history-dedupe-entries.test.ts +123 -0
  170. package/tests/history-delete-backfill.test.ts +190 -0
  171. package/tests/history-delete-confirm.test.ts +460 -0
  172. package/tests/history-dispatch.test.ts +180 -0
  173. package/tests/history-drain-hidden.test.ts +110 -0
  174. package/tests/history-drain-order.test.ts +98 -0
  175. package/tests/history-expanded-globals.test.ts +62 -0
  176. package/tests/history-gc.test.ts +832 -0
  177. package/tests/history-header-layout.test.ts +265 -0
  178. package/tests/history-hide-prompts.test.ts +275 -0
  179. package/tests/history-lazy-windowing.test.ts +508 -0
  180. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  181. package/tests/history-load-shared-history.test.ts +53 -0
  182. package/tests/history-max-results-cap.test.ts +76 -0
  183. package/tests/history-multi-reader.test.ts +203 -0
  184. package/tests/history-off-path.test.ts +170 -0
  185. package/tests/history-openflow-integration.test.ts +173 -0
  186. package/tests/history-overlay-margin.test.ts +326 -0
  187. package/tests/history-preview-layout.test.ts +93 -0
  188. package/tests/history-registry.test.ts +143 -0
  189. package/tests/history-scope-delete.test.ts +411 -0
  190. package/tests/history-search-caret-keys.test.ts +142 -0
  191. package/tests/history-seed-bootstrap.test.ts +170 -0
  192. package/tests/history-seed-regen.test.ts +129 -0
  193. package/tests/history-selector-windowing.test.ts +94 -0
  194. package/tests/history-session-scan-directory.test.ts +87 -0
  195. package/tests/history-session-scan-extract.test.ts +583 -0
  196. package/tests/history-session-writer.test.ts +351 -0
  197. package/tests/history-store-paths.test.ts +79 -0
  198. package/tests/history-tombstone-exact.test.ts +139 -0
  199. package/tests/history-wheel-mouse.test.ts +242 -0
  200. package/tests/inprocess-reviewer.test.ts +29 -19
  201. package/tests/issue-creation-skill.test.ts +61 -0
  202. package/tests/model-routing-authority.test.ts +16 -0
  203. package/tests/nan-provider.test.ts +471 -0
  204. package/tests/native-review-capability-contract.test.ts +13 -1
  205. package/tests/native-review-cli.test.ts +6 -120
  206. package/tests/native-review-parity-runtime.test.ts +100 -3
  207. package/tests/odd-integration.test.ts +67 -0
  208. package/tests/odd-phase-inference.test.ts +213 -0
  209. package/tests/odd-phase-loader.test.ts +253 -0
  210. package/tests/odd-phase.test.ts +307 -0
  211. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  212. package/tests/odd-routing-contract.test.ts +86 -35
  213. package/tests/orchestrator-budget.test.ts +14 -39
  214. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  215. package/tests/overlay-repaint.test.ts +74 -0
  216. package/tests/package-manifest.test.ts +252 -115
  217. package/tests/packed-runner-owned-path.test.ts +46 -0
  218. package/tests/persona-single-channel.test.ts +6 -6
  219. package/tests/provider-defect-handoff.test.ts +3 -11
  220. package/tests/quiet-bash-runtime.test.ts +76 -0
  221. package/tests/quiet-tool-rendering.test.ts +409 -184
  222. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  223. package/tests/rdd-status-line.test.ts +9 -4
  224. package/tests/resume-hint-extension.test.ts +122 -0
  225. package/tests/review-agent-end-preflight.test.ts +176 -12
  226. package/tests/review-candidate-owner-retry.test.ts +22 -1
  227. package/tests/review-candidate-view.test.ts +298 -0
  228. package/tests/review-contract-prompt.test.ts +108 -43
  229. package/tests/review-controller-lock-status.test.ts +0 -1
  230. package/tests/review-controller-native-routing.test.ts +611 -5
  231. package/tests/review-controller-workspace-root.test.ts +163 -4
  232. package/tests/review-host-relay-routing.test.ts +338 -2
  233. package/tests/review-integration-v2-forward.test.ts +200 -0
  234. package/tests/review-ledger-contract.test.ts +10 -34
  235. package/tests/review-reminder-receipt.test.ts +47 -1
  236. package/tests/review-risk-assessment.test.ts +498 -6
  237. package/tests/review-sidebar-state.test.ts +402 -0
  238. package/tests/run-test-suite.test.ts +124 -0
  239. package/tests/runtime-harness.mjs +145 -786
  240. package/tests/runtime-metrics-children.test.ts +16 -23
  241. package/tests/selection-engine.test.ts +421 -0
  242. package/tests/session-change-capture.test.ts +12 -1
  243. package/tests/session-messaging-grants.test.ts +255 -0
  244. package/tests/session-worktree-registry.test.ts +77 -0
  245. package/tests/shell-bar.test.ts +382 -1
  246. package/tests/shell-card.test.ts +353 -1
  247. package/tests/shell-changes-view.test.ts +52 -0
  248. package/tests/shell-prompt.test.ts +94 -2
  249. package/tests/shell-sidebar-layout.test.ts +325 -21
  250. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  251. package/tests/shell-todo.test.ts +87 -1
  252. package/tests/shell-usage-view.test.ts +27 -0
  253. package/tests/shell-usage.test.ts +73 -0
  254. package/tests/skill-registry.test.ts +50 -1
  255. package/tests/startup-banner.test.ts +130 -2
  256. package/tests/stats-collector.test.ts +195 -0
  257. package/tests/stats-view.test.ts +202 -0
  258. package/tests/telemetry-trigger.test.ts +81 -20
  259. package/tests/theme-customization.test.ts +72 -0
  260. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  261. package/tests/vim-editor-adapter.test.ts +804 -0
  262. package/tests/vim-normal-engine.test.ts +101 -0
  263. package/tests/vim-operator-engine.test.ts +215 -0
  264. package/tests/vim-policy.test.ts +19 -0
  265. package/tests/vim-visual-engine.test.ts +52 -0
  266. package/tests/visual-customization-policy.test.ts +110 -0
  267. package/tests/visual-customize-view.test.ts +418 -0
  268. package/tests/visual-profiles.test.ts +87 -0
  269. package/tests/yolo-customize.test.ts +256 -0
  270. package/tests/yolo-mode-runtime.test.ts +161 -0
  271. package/tests/yolo-mode.test.ts +261 -0
  272. package/tests/yolo-session-policy.test.ts +59 -0
  273. package/themes/Gentle.json +2 -1
  274. package/themes/Gentleman-Cute.json +2 -1
  275. package/themes/Gentleman-Sexy.json +2 -1
  276. package/assets/agents/sdd-apply.md +0 -159
  277. package/assets/agents/sdd-archive.md +0 -228
  278. package/assets/agents/sdd-design.md +0 -49
  279. package/assets/agents/sdd-explore.md +0 -48
  280. package/assets/agents/sdd-init.md +0 -56
  281. package/assets/agents/sdd-onboard.md +0 -52
  282. package/assets/agents/sdd-proposal.md +0 -64
  283. package/assets/agents/sdd-remediate.md +0 -37
  284. package/assets/agents/sdd-research.md +0 -49
  285. package/assets/agents/sdd-spec.md +0 -192
  286. package/assets/agents/sdd-status.md +0 -54
  287. package/assets/agents/sdd-tasks.md +0 -108
  288. package/assets/agents/sdd-verify.md +0 -124
  289. package/assets/chains/sdd-full.chain.md +0 -83
  290. package/assets/chains/sdd-plan.chain.md +0 -56
  291. package/assets/chains/sdd-verify.chain.md +0 -43
  292. package/assets/sdd-orchestrator-workflow.md +0 -319
  293. package/assets/support/sdd-status-contract.md +0 -77
  294. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  295. package/extensions/sdd-init.ts +0 -816
  296. package/lib/openspec-deltas.ts +0 -156
  297. package/lib/sdd-preflight.ts +0 -1066
  298. package/lib/sdd-research-capabilities.ts +0 -94
  299. package/lib/sdd-status.ts +0 -26
  300. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  301. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  302. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  303. package/tests/openspec-deltas.test.ts +0 -209
  304. package/tests/sdd-agent-tools.test.ts +0 -156
  305. package/tests/sdd-archive-replay.test.ts +0 -82
  306. package/tests/sdd-classical-continuation.test.ts +0 -74
  307. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  308. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  309. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  310. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  311. package/tests/sdd-odd-integration.test.ts +0 -33
  312. package/tests/sdd-optional-research.test.ts +0 -124
  313. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  314. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  315. package/tests/sdd-preflight.test.ts +0 -541
  316. package/tests/sdd-research-capabilities.test.ts +0 -114
  317. package/tests/sdd-research-live.test.ts +0 -241
  318. package/tests/sdd-selection-transport.test.ts +0 -653
  319. package/tests/sdd-status.test.ts +0 -9
  320. package/tests/sdd-task-truth.test.ts +0 -43
@@ -30,7 +30,7 @@ Use it for:
30
30
  | Keep docs with the user-visible change | Docs belong with the feature or workflow they explain. |
31
31
  | Tell a story | A reviewer should understand why each commit exists from its diff and message. |
32
32
  | Future PR-ready | Each commit should be a candidate chained PR when the change grows. |
33
- | SDD workload guard | If SDD tasks forecast a >400-line change, group commits into chained PR slices before implementation. |
33
+ | SDD workload guard | If SDD tasks forecast a >400-line change, honor the selected `delivery_strategy`. On chaining paths only, group commits into chained PR slices before implementation. For explicit `single-pr` or `exception-ok`, keep one PR, report the review load, and follow the destination repository's documented contribution/size policy; selection alone does not accept an exception. |
34
34
  | Budget is not code-golf | Never shrink a diff by deleting comments, blank lines, docs, or tests, or by compressing code, to fit the review budget (400 by default, or the session `review_budget_lines`). Slice by work unit or report the overage. |
35
35
 
36
36
  ## Work Unit Checklist
@@ -67,8 +67,9 @@ When `sdd-tasks` produces a Review Workload Forecast:
67
67
 
68
68
  - Low risk: keep work-unit commits inside one PR.
69
69
  - Medium risk: commit by work unit and monitor changed lines before PR creation.
70
- - High risk: follow SDD `delivery_strategy` — ask on `ask-on-risk`, auto-slice on `auto-chain`, require `size:exception` on over-budget `single-pr`, or record accepted `size:exception` on `exception-ok`.
71
- - Splitting is bounded: after one honest slicing pass, if no cohesive work-unit split fits the budget, stop and report the smallest honest count with a `size:exception` recommendation. Do not iterate shrinking the code to reach the number.
70
+ - High risk: follow SDD `delivery_strategy` — ask on `ask-on-risk`, auto-slice with the cached chain choice on `auto-chain`, follow the destination repository's documented contribution/size policy on over-budget `single-pr`, or record an explicitly accepted policy exception on `exception-ok`. A single-PR selection does not itself accept an exception.
71
+ - `size:exception` is a Gentle-owned repository policy, not a universal label requirement. Never request, create, or add the label for generic users unless the destination policy uses it; preserve its maintainer acceptance and protected-label authorization gates.
72
+ - Splitting is bounded: after one honest slicing pass, if no cohesive work-unit split fits the budget, stop and report the smallest honest count and rationale. Recommend an exception only where destination policy provides it. Do not iterate shrinking the code to reach the number.
72
73
 
73
74
  Each SDD work unit should map cleanly to a commit or PR with:
74
75
 
@@ -437,6 +437,24 @@ test("normalizeProfilesFile drops an active marker that points nowhere", () => {
437
437
  assert.equal(normalized?.drops.droppedProfiles.length, 0);
438
438
  });
439
439
 
440
+ test("normalizeProfilesFile accepts effort as alias for thinking in profile entries (#1403)", () => {
441
+ const normalized = normalizeProfilesFile(
442
+ JSON.parse(
443
+ profilesText({
444
+ hybrid: {
445
+ "example-agent": { model: "anthropic/claude-sonnet-4", effort: "medium" },
446
+ "worker": { effort: "high" },
447
+ },
448
+ }),
449
+ ),
450
+ );
451
+ assert.deepEqual(normalized?.file.profiles.hybrid, {
452
+ "example-agent": { model: "anthropic/claude-sonnet-4", thinking: "medium" },
453
+ "worker": { model: undefined, thinking: "high" },
454
+ });
455
+ assert.equal(normalized?.drops.droppedAgents.length, 0);
456
+ });
457
+
440
458
  test("bootstrapProfilesFile seeds a current profile, active only when routing is non-empty", () => {
441
459
  const empty = bootstrapProfilesFile({});
442
460
  assert.deepEqual(empty, {
@@ -9,7 +9,7 @@ export interface FakeChild {
9
9
  child: ChildLike;
10
10
  written: Array<Record<string, unknown>>;
11
11
  emit(event: Record<string, unknown>): void;
12
- exit(code: number): void;
12
+ exit(code: number | null, signal?: string | null): void;
13
13
  fail(message: string): void;
14
14
  killed: string[];
15
15
  sent: Array<Record<string, unknown>>;
@@ -62,5 +62,5 @@ export function fakeChild(options: { exitOnKill?: boolean; pid?: number } = {}):
62
62
  return child;
63
63
  },
64
64
  };
65
- return { child, written, killed, sent, get disconnects() { return disconnects; }, message: (event) => emitter.emit("message", event), emit: (event) => stdout.write(`${JSON.stringify(event)}\n`), exit: (code) => emitter.emit("exit", code, null), fail: (message) => emitter.emit("error", new Error(message)) };
65
+ return { child, written, killed, sent, get disconnects() { return disconnects; }, message: (event) => emitter.emit("message", event), emit: (event) => stdout.write(`${JSON.stringify(event)}\n`), exit: (code, signal = null) => emitter.emit("exit", code, signal), fail: (message) => emitter.emit("error", new Error(message)) };
66
66
  }
@@ -0,0 +1,106 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createAgentMessageQueue, STALE_NOTIFICATION_MS } from "../lib/agents-message-delivery.ts";
4
+
5
+ test("delivers live notifications and queries in order for the active session", () => {
6
+ const queue = createAgentMessageQueue();
7
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "First progress", 1000);
8
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Which branch?", 1050);
9
+
10
+ assert.equal(queue.pendingCount(), 2);
11
+ const deliverable = queue.takeDeliverable(1100, "s1", () => true);
12
+ assert.equal(deliverable.length, 2);
13
+
14
+ assert.equal(deliverable[0]?.kind, "notification");
15
+ assert.equal(deliverable[0]?.content, "First progress");
16
+ assert.equal(deliverable[0]?.display, false);
17
+
18
+ assert.equal(deliverable[1]?.kind, "query");
19
+ assert.match(deliverable[1]?.content ?? "", /Which branch\?/);
20
+ assert.equal(deliverable[1]?.display, true);
21
+ assert.equal(deliverable[1]?.requestId, "q1");
22
+
23
+ assert.equal(queue.pendingCount(), 0);
24
+ assert.equal(queue.takeDeliverable(1200, "s1", () => true).length, 0, "messages are delivered at most once");
25
+ });
26
+
27
+ test("a notification from a task that settled before delivery is dropped silently", () => {
28
+ const queue = createAgentMessageQueue();
29
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Late notice", 1000);
30
+
31
+ // The task finishes and is invalidated before the parent turn boundary.
32
+ queue.invalidateTask("task-1");
33
+
34
+ const deliverable = queue.takeDeliverable(1050, "s1", () => false);
35
+ assert.equal(deliverable.length, 0, "notification from settled task must never enter conversation");
36
+ });
37
+
38
+ test("an answered query is consumed and never delivered", () => {
39
+ const queue = createAgentMessageQueue();
40
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Which target?", 1000);
41
+
42
+ // Parent replies before queue flush.
43
+ queue.consumeQuery("task-1", "q1");
44
+
45
+ const deliverable = queue.takeDeliverable(1050, "s1", () => true);
46
+ assert.equal(deliverable.length, 0, "answered query must not be delivered");
47
+ });
48
+
49
+ test("an expired query is dropped and never delivered", () => {
50
+ const queue = createAgentMessageQueue();
51
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Need approval", 1000);
52
+
53
+ // Query times out.
54
+ queue.expireQuery("task-1", "q1");
55
+
56
+ const deliverable = queue.takeDeliverable(1050, "s1", () => true);
57
+ assert.equal(deliverable.length, 0, "expired query must not be delivered");
58
+ });
59
+
60
+ test("a notification exceeding the stale window is dropped", () => {
61
+ const queue = createAgentMessageQueue();
62
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Slow notice", 1000);
63
+
64
+ // Held past the 90-second threshold.
65
+ const deliverable = queue.takeDeliverable(1000 + STALE_NOTIFICATION_MS + 10, "s1", () => true);
66
+ assert.equal(deliverable.length, 0, "stale notification must not be delivered");
67
+ });
68
+
69
+ test("isTaskLive returning false drops messages for non-live tasks", () => {
70
+ const queue = createAgentMessageQueue();
71
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Dead task notice", 1000);
72
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Dead task query", 1000);
73
+
74
+ const deliverable = queue.takeDeliverable(1050, "s1", (id) => id !== "task-1");
75
+ assert.equal(deliverable.length, 0, "non-live task messages must be dropped");
76
+ });
77
+
78
+ test("session mismatch drops foreign session messages", () => {
79
+ const queue = createAgentMessageQueue();
80
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "session-old" }, "Old session notice", 1000);
81
+
82
+ const deliverable = queue.takeDeliverable(1050, "session-new", () => true);
83
+ assert.equal(deliverable.length, 0, "foreign session message must be dropped");
84
+ });
85
+
86
+ test("deduplicates identical query requests for the same task and requestId", () => {
87
+ const queue = createAgentMessageQueue();
88
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "First ask", 1000);
89
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Duplicate ask", 1001);
90
+
91
+ assert.equal(queue.pendingCount(), 1);
92
+ const deliverable = queue.takeDeliverable(1050, "s1", () => true);
93
+ assert.equal(deliverable.length, 1);
94
+ assert.match(deliverable[0]!.content, /First ask/);
95
+ });
96
+
97
+ test("dropAll clears everything across session reset", () => {
98
+ const queue = createAgentMessageQueue();
99
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Notice", 1000);
100
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Query", 1000);
101
+ assert.equal(queue.pendingCount(), 2);
102
+
103
+ queue.dropAll();
104
+ assert.equal(queue.pendingCount(), 0);
105
+ assert.equal(queue.takeDeliverable(1050, "s1", () => true).length, 0);
106
+ });
@@ -4,6 +4,7 @@ import {
4
4
  applyTaskEvent,
5
5
  emptyThread,
6
6
  normalizeRpcEvent,
7
+ ToolArgumentProgress,
7
8
  TASK_EVENT,
8
9
  TASK_STATUS,
9
10
  taskLabel,
@@ -67,6 +68,45 @@ test("normalizeRpcEvent maps pi RPC events to task deltas and ignores the rest",
67
68
  assert.deepEqual(normalizeRpcEvent("garbage"), []);
68
69
  });
69
70
 
71
+ test("argument liveness accepts current and legacy Pi starts without display or usage deltas", () => {
72
+ for (const start of [{ type: "toolcall_start", contentIndex: 0, id: "c1", toolName: "write" },
73
+ { type: "toolcall_start", contentIndex: 0, partial: { content: [{ type: "toolCall", id: "c1", name: "write", arguments: {} }] } }]) {
74
+ const progress = new ToolArgumentProgress();
75
+ const update = (inner: Record<string, unknown>) => ({ type: "message_update", assistantMessageEvent: inner });
76
+ const delta = update({ type: "toolcall_delta", contentIndex: 0, delta: "private" });
77
+ assert.equal(progress.observe(delta), false, "no active generation");
78
+ progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
79
+ assert.equal(progress.observe(delta), false, "no announced block");
80
+ assert.equal(progress.observe(update(start)), false, "announcement alone is not progress");
81
+ assert.equal(progress.observe(delta), true);
82
+ assert.equal(progress.observe(delta), false);
83
+ assert.deepEqual(normalizeRpcEvent(delta), [], "no arguments in display events or provisional totals");
84
+ progress.observe(update({ type: "toolcall_end", contentIndex: 0 }));
85
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "late" })), false);
86
+ progress.observe(update(start));
87
+ assert.equal(progress.observe(delta), false, "a closed block cannot be reopened");
88
+ }
89
+ });
90
+
91
+ test("argument liveness fails closed on malformed starts, stale generations and fingerprint exhaustion", () => {
92
+ const progress = new ToolArgumentProgress();
93
+ const update = (inner: Record<string, unknown>) => ({ type: "message_update", assistantMessageEvent: inner });
94
+ for (const timestamp of [undefined, "1", -1, 1.5]) progress.observe({ type: "message_start", message: { role: "assistant", timestamp } });
95
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "x" })), false);
96
+ progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
97
+ for (const fields of [{ id: 1, toolName: "write" }, { id: "c", toolName: "" }, { partial: null }]) {
98
+ progress.observe(update({ type: "toolcall_start", contentIndex: 0, ...fields }));
99
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "x" })), false);
100
+ }
101
+ progress.observe(update({ type: "toolcall_start", contentIndex: 0, id: "c", toolName: "write" }));
102
+ for (let index = 0; index < 4096; index++) assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: String(index) })), true);
103
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "over-cap" })), false);
104
+ progress.observe({ type: "message_end" });
105
+ progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
106
+ progress.observe(update({ type: "toolcall_start", contentIndex: 0, id: "c", toolName: "write" }));
107
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "stale" })), false);
108
+ });
109
+
70
110
  test("response observations are opt-in, finalized, field-specific and content-free", () => {
71
111
  const message = { role: "assistant", provider: "openai", model: "gpt-4o", responseModel: "gpt-4o-2024-08-06",
72
112
  providerThinkingLevel: "high", stopReason: "stop", content: [{ type: "text", text: "private response" }],