@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (359) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +8 -11
  3. package/docs/prefix-cache-architecture.md +14 -0
  4. package/package.json +22 -24
  5. package/packages/ask-user/README.md +55 -107
  6. package/packages/ask-user/ask-ui.ts +306 -732
  7. package/packages/ask-user/clipboard.ts +50 -0
  8. package/packages/ask-user/config.ts +74 -78
  9. package/packages/ask-user/package.json +2 -2
  10. package/packages/ask-user/questions.ts +151 -0
  11. package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
  12. package/packages/ask-user/tools.ts +198 -432
  13. package/packages/autocomplete/package.json +1 -1
  14. package/packages/autocomplete/src/constants.ts +38 -43
  15. package/packages/autocomplete/src/sorting.ts +8 -0
  16. package/packages/background-tasks/package.json +2 -2
  17. package/packages/background-tasks/src/config.ts +0 -73
  18. package/packages/background-tasks/src/index.ts +20 -16
  19. package/packages/background-tasks/src/registry.ts +1 -171
  20. package/packages/background-tasks/src/types.ts +0 -32
  21. package/packages/btw/README.md +13 -56
  22. package/packages/btw/extensions/btw.ts +403 -1796
  23. package/packages/btw/package.json +5 -2
  24. package/packages/btw/skills/btw/SKILL.md +7 -143
  25. package/packages/compactor/README.md +56 -287
  26. package/packages/compactor/package.json +5 -9
  27. package/packages/compactor/skills/compactor/SKILL.md +13 -20
  28. package/packages/compactor/src/card.ts +90 -0
  29. package/packages/compactor/src/commands/index.ts +73 -130
  30. package/packages/compactor/src/compaction/compact-args.ts +5 -3
  31. package/packages/compactor/src/compaction/filter-noise.ts +1 -1
  32. package/packages/compactor/src/compaction/hooks.ts +487 -311
  33. package/packages/compactor/src/compaction/source.ts +178 -0
  34. package/packages/compactor/src/compaction/summarize.ts +586 -52
  35. package/packages/compactor/src/config/manager.ts +130 -127
  36. package/packages/compactor/src/config/schema.ts +32 -48
  37. package/packages/compactor/src/index.ts +40 -431
  38. package/packages/compactor/src/info-screen.ts +44 -190
  39. package/packages/compactor/src/session/recall-blocks.ts +1 -1
  40. package/packages/compactor/src/stats.ts +76 -0
  41. package/packages/compactor/src/tools/context-budget.ts +17 -34
  42. package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
  43. package/packages/compactor/src/tools/register.ts +29 -269
  44. package/packages/compactor/src/types.ts +47 -186
  45. package/packages/core/README.md +25 -0
  46. package/packages/core/child-agent.ts +1 -0
  47. package/packages/core/command-echo.ts +63 -0
  48. package/packages/core/compaction-context.ts +41 -0
  49. package/packages/core/constants.ts +8 -38
  50. package/packages/core/events.ts +0 -53
  51. package/packages/core/fusion-status.ts +4 -0
  52. package/packages/core/global.d.ts +10 -0
  53. package/packages/core/harness-messages.ts +392 -0
  54. package/packages/core/index.ts +14 -0
  55. package/packages/core/kanboard-status.ts +49 -0
  56. package/packages/core/long-horizon-owner-status.ts +46 -0
  57. package/packages/core/model-cache.ts +81 -7
  58. package/packages/core/package.json +6 -3
  59. package/packages/core/src/attach/detect.ts +163 -0
  60. package/packages/core/src/child-agent/child-args.ts +31 -0
  61. package/packages/core/src/child-agent/delivery.ts +41 -0
  62. package/packages/core/src/child-agent/index.ts +5 -0
  63. package/packages/core/src/child-agent/readers.ts +203 -0
  64. package/packages/core/src/child-agent/runtime.ts +584 -0
  65. package/packages/core/src/evidence.ts +78 -0
  66. package/packages/core/src/hints/browser.ts +278 -0
  67. package/packages/core/src/hints/crab-data.ts +19 -0
  68. package/packages/core/src/hints/index.ts +865 -0
  69. package/packages/core/src/hints/lines.ts +607 -0
  70. package/packages/core/src/hints/store.ts +120 -0
  71. package/packages/core/src/jev/chatter.ts +24 -0
  72. package/packages/core/src/jev/settings.ts +177 -14
  73. package/packages/core/src/package-colors.ts +2 -0
  74. package/packages/core/src/settings/catalog.ts +39 -15
  75. package/packages/core/src/settings/engine.ts +48 -2
  76. package/packages/core/src/settings/hub.ts +83 -20
  77. package/packages/core/src/settings/open.ts +49 -0
  78. package/packages/core/src/settings/schema.ts +27 -7
  79. package/packages/core/src/tui/hub-kit.ts +18 -5
  80. package/packages/core/src/tui/kit.ts +198 -0
  81. package/packages/core/src/tui/progress.ts +73 -0
  82. package/packages/core/src/turn/arbiter.ts +290 -0
  83. package/packages/footer/README.md +4 -6
  84. package/packages/footer/package.json +3 -3
  85. package/packages/footer/src/glance-editor.ts +19 -11
  86. package/packages/footer/src/help.ts +1 -1
  87. package/packages/footer/src/index.ts +36 -3
  88. package/packages/footer/src/presets.ts +3 -4
  89. package/packages/footer/src/rendering/icons.ts +0 -20
  90. package/packages/footer/src/segments/compactor.ts +52 -142
  91. package/packages/footer/src/segments/status-ext.ts +0 -1
  92. package/packages/fusion/README.md +14 -4
  93. package/packages/fusion/package.json +4 -3
  94. package/packages/fusion/src/child-args.ts +1 -0
  95. package/packages/fusion/src/index.ts +161 -8
  96. package/packages/fusion/src/pi-settings.ts +57 -0
  97. package/packages/fusion/src/picker.ts +186 -51
  98. package/packages/fusion/src/preset.ts +56 -2
  99. package/packages/fusion/src/prompts.ts +1 -1
  100. package/packages/fusion/src/sidekick-runtime.ts +1 -420
  101. package/packages/fusion/src/tools.ts +107 -128
  102. package/packages/fusion/src/transcript.ts +27 -85
  103. package/packages/info-screen/package.json +2 -2
  104. package/packages/input-shortcuts/README.md +18 -30
  105. package/packages/input-shortcuts/package.json +2 -2
  106. package/packages/input-shortcuts/src/burst.ts +88 -0
  107. package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
  108. package/packages/input-shortcuts/src/cleared-input.ts +97 -0
  109. package/packages/input-shortcuts/src/index.ts +99 -193
  110. package/packages/input-shortcuts/src/last-response.ts +40 -0
  111. package/packages/input-shortcuts/src/registers.ts +8 -23
  112. package/packages/input-shortcuts/src/types.ts +4 -16
  113. package/packages/input-shortcuts/src/undo-redo.ts +59 -47
  114. package/packages/kanboard/README.md +51 -47
  115. package/packages/kanboard/index.ts +293 -55
  116. package/packages/kanboard/package.json +8 -8
  117. package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
  118. package/packages/kanboard/src/badges.ts +93 -0
  119. package/packages/kanboard/src/bin.ts +4 -2
  120. package/packages/kanboard/src/commands.ts +218 -84
  121. package/packages/kanboard/src/debug.ts +20 -0
  122. package/packages/kanboard/src/guard.ts +267 -109
  123. package/packages/kanboard/src/monitor.ts +330 -0
  124. package/packages/kanboard/src/notice-buffer.ts +74 -0
  125. package/packages/kanboard/src/progress.ts +27 -0
  126. package/packages/kanboard/src/reminders.ts +234 -0
  127. package/packages/kanboard/src/settings.ts +69 -15
  128. package/packages/kanboard/src/shapes.ts +5 -0
  129. package/packages/long-horizon/index.ts +190 -13
  130. package/packages/long-horizon/package.json +3 -3
  131. package/packages/long-horizon/src/commands.ts +103 -7
  132. package/packages/long-horizon/src/compaction-brief.ts +35 -0
  133. package/packages/long-horizon/src/engine/continuation.ts +84 -15
  134. package/packages/long-horizon/src/engine/goal-state.ts +64 -13
  135. package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
  136. package/packages/long-horizon/src/engine/ralph.ts +3 -2
  137. package/packages/long-horizon/src/engine/runaway.ts +68 -11
  138. package/packages/long-horizon/src/engine/verifier.ts +19 -0
  139. package/packages/long-horizon/src/gate.ts +37 -12
  140. package/packages/long-horizon/src/judge/resolve.ts +1 -0
  141. package/packages/long-horizon/src/owner.ts +35 -2
  142. package/packages/long-horizon/src/progress.ts +87 -0
  143. package/packages/long-horizon/src/prompts/goal.ts +1 -1
  144. package/packages/long-horizon/src/runtime.ts +172 -18
  145. package/packages/long-horizon/src/settings.ts +54 -39
  146. package/packages/long-horizon/src/tools/goal.ts +6 -0
  147. package/packages/long-horizon/src/tools/graph.ts +1 -1
  148. package/packages/long-horizon/src/tools/swarm.ts +3 -3
  149. package/packages/mcp/package.json +2 -2
  150. package/packages/memory/commands.ts +7 -2
  151. package/packages/memory/index.ts +210 -32
  152. package/packages/memory/package.json +3 -3
  153. package/packages/memory/save-session.ts +264 -0
  154. package/packages/memory/settings.ts +17 -0
  155. package/packages/memory/tools.ts +253 -203
  156. package/packages/notify/package.json +2 -2
  157. package/packages/skill-registry/README.md +53 -16
  158. package/packages/skill-registry/index.ts +230 -0
  159. package/packages/skill-registry/package.json +19 -8
  160. package/packages/skill-registry/src/editor.ts +206 -0
  161. package/packages/skill-registry/src/judge.ts +289 -0
  162. package/packages/skill-registry/src/registry.ts +87 -0
  163. package/packages/skill-registry/src/settings.ts +259 -0
  164. package/packages/skill-registry/src/vault.ts +80 -0
  165. package/packages/subagents/README.md +39 -119
  166. package/packages/subagents/package.json +6 -13
  167. package/packages/subagents/skills/subagents/SKILL.md +18 -42
  168. package/packages/subagents/src/agents.ts +213 -0
  169. package/packages/subagents/src/cards.ts +182 -0
  170. package/packages/subagents/src/index.ts +609 -1084
  171. package/packages/subagents/src/manager.ts +429 -0
  172. package/packages/subagents/src/profiles.ts +130 -0
  173. package/packages/subagents/src/transcript.ts +154 -0
  174. package/packages/subagents/src/ui.ts +389 -0
  175. package/packages/unipi/bundled.js +39756 -45569
  176. package/packages/unipi/index.ts +55 -10
  177. package/packages/updater/package.json +2 -2
  178. package/packages/utility/README.md +76 -69
  179. package/packages/utility/package.json +2 -2
  180. package/packages/utility/skills/image/SKILL.md +37 -0
  181. package/packages/utility/skills/utility/SKILL.md +17 -60
  182. package/packages/utility/src/answer/extract.ts +128 -0
  183. package/packages/utility/src/answer/index.ts +213 -0
  184. package/packages/utility/src/answer/reply.ts +133 -0
  185. package/packages/utility/src/answer/web.ts +167 -0
  186. package/packages/utility/src/attach/index.ts +205 -0
  187. package/packages/utility/src/commands.ts +38 -142
  188. package/packages/utility/src/diagnostics/engine.ts +50 -5
  189. package/packages/utility/src/image/index.ts +33 -0
  190. package/packages/utility/src/image/models.ts +221 -0
  191. package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
  192. package/packages/utility/src/image/settings.ts +178 -0
  193. package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
  194. package/packages/utility/src/image/tools.ts +168 -0
  195. package/packages/utility/src/image/transports.ts +186 -0
  196. package/packages/utility/src/index.ts +68 -389
  197. package/packages/utility/src/lifecycle/cleanup.ts +116 -311
  198. package/packages/utility/src/rename/gate.ts +102 -0
  199. package/packages/utility/src/rename/index.ts +191 -0
  200. package/packages/utility/src/rename/session.ts +170 -0
  201. package/packages/utility/src/render/delegated.ts +285 -0
  202. package/packages/utility/src/render/harness.ts +683 -0
  203. package/packages/utility/src/render/parse.ts +156 -0
  204. package/packages/utility/src/render/pi-settings.ts +38 -0
  205. package/packages/utility/src/render/reply-bg.ts +307 -0
  206. package/packages/utility/src/render/simple.ts +724 -0
  207. package/packages/utility/src/render/spacing.ts +235 -0
  208. package/packages/utility/src/render/styled.ts +251 -0
  209. package/packages/utility/src/render/tools.ts +353 -0
  210. package/packages/utility/src/settings.ts +166 -233
  211. package/packages/utility/src/types.ts +1 -222
  212. package/packages/watchdog/index.ts +17 -6
  213. package/packages/watchdog/package.json +3 -3
  214. package/packages/watchdog/src/config.ts +3 -2
  215. package/packages/web-api/package.json +2 -2
  216. package/packages/workflow/README.md +3 -0
  217. package/packages/workflow/index.ts +0 -2
  218. package/packages/workflow/package.json +2 -2
  219. package/packages/workflow/src/permission/decide.ts +6 -4
  220. package/packages/workflow/src/permission/index.ts +17 -7
  221. package/packages/workflow/src/permission/settings.ts +39 -4
  222. package/packages/workflow/src/plan/index.ts +19 -4
  223. package/packages/background-tasks/extensions/delegate-child.ts +0 -1
  224. package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
  225. package/packages/background-tasks/src/context-token-budget.ts +0 -903
  226. package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
  227. package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
  228. package/packages/background-tasks/src/delegate/budget.ts +0 -415
  229. package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
  230. package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
  231. package/packages/background-tasks/src/delegate/launch.ts +0 -484
  232. package/packages/background-tasks/src/delegate/result-package.ts +0 -459
  233. package/packages/background-tasks/src/delegate/runner.ts +0 -430
  234. package/packages/background-tasks/src/delegate/seed.ts +0 -423
  235. package/packages/background-tasks/src/delegate/types.ts +0 -323
  236. package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
  237. package/packages/background-tasks/src/delegate-extension.ts +0 -654
  238. package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
  239. package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
  240. package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
  241. package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
  242. package/packages/compactor/src/compaction/build-sections.ts +0 -76
  243. package/packages/compactor/src/compaction/extract/goals.ts +0 -36
  244. package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
  245. package/packages/compactor/src/compaction/format.ts +0 -53
  246. package/packages/compactor/src/compaction/merge.ts +0 -143
  247. package/packages/compactor/src/config/presets.ts +0 -160
  248. package/packages/compactor/src/executor/executor.ts +0 -233
  249. package/packages/compactor/src/executor/runtime.ts +0 -115
  250. package/packages/compactor/src/security/evaluator.ts +0 -89
  251. package/packages/compactor/src/security/policy.ts +0 -96
  252. package/packages/compactor/src/security/scanner.ts +0 -65
  253. package/packages/compactor/src/session/auto-inject.ts +0 -60
  254. package/packages/compactor/src/session/db.ts +0 -325
  255. package/packages/compactor/src/session/extract.ts +0 -144
  256. package/packages/compactor/src/session/resume-inject.ts +0 -64
  257. package/packages/compactor/src/session/snapshot.ts +0 -327
  258. package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
  259. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
  260. package/packages/compactor/src/tools/ctx-execute.ts +0 -23
  261. package/packages/compactor/src/tools/ctx-stats.ts +0 -96
  262. package/packages/image/README.md +0 -99
  263. package/packages/image/package.json +0 -61
  264. package/packages/image/skills/image/SKILL.md +0 -76
  265. package/packages/image/src/generate.ts +0 -256
  266. package/packages/image/src/index.ts +0 -135
  267. package/packages/image/src/models.ts +0 -519
  268. package/packages/image/src/openai-images-api.ts +0 -282
  269. package/packages/image/src/register-providers.ts +0 -209
  270. package/packages/image/src/settings.ts +0 -173
  271. package/packages/image/src/tools.ts +0 -326
  272. package/packages/input-shortcuts/src/clipboard.ts +0 -119
  273. package/packages/kanboard/src/runner.ts +0 -817
  274. package/packages/skill-registry/skills/auto/SKILL.md +0 -465
  275. package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
  276. package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
  277. package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
  278. package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
  279. package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
  280. package/packages/skill-registry/skills/debug/SKILL.md +0 -228
  281. package/packages/skill-registry/skills/document/SKILL.md +0 -120
  282. package/packages/skill-registry/skills/fix/SKILL.md +0 -212
  283. package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
  284. package/packages/skill-registry/skills/plan/SKILL.md +0 -231
  285. package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
  286. package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
  287. package/packages/skill-registry/skills/research/SKILL.md +0 -251
  288. package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
  289. package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
  290. package/packages/skill-registry/skills/work/SKILL.md +0 -218
  291. package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
  292. package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
  293. package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
  294. package/packages/subagents/agents/delegate.md +0 -14
  295. package/packages/subagents/agents/oracle.md +0 -78
  296. package/packages/subagents/agents/researcher.md +0 -52
  297. package/packages/subagents/agents/reviewer.md +0 -80
  298. package/packages/subagents/agents/scout.md +0 -50
  299. package/packages/subagents/agents/worker.md +0 -59
  300. package/packages/subagents/prompts/council.md +0 -48
  301. package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
  302. package/packages/subagents/prompts/parallel-cleanup.md +0 -59
  303. package/packages/subagents/prompts/parallel-research.md +0 -50
  304. package/packages/subagents/prompts/parallel-review.md +0 -54
  305. package/packages/subagents/prompts/review-loop.md +0 -43
  306. package/packages/subagents/skills/council-mode/SKILL.md +0 -230
  307. package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
  308. package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
  309. package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
  310. package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
  311. package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
  312. package/packages/subagents/src/acceptance.ts +0 -331
  313. package/packages/subagents/src/agent-manager.ts +0 -405
  314. package/packages/subagents/src/agent-memory.ts +0 -300
  315. package/packages/subagents/src/agent-overrides.ts +0 -183
  316. package/packages/subagents/src/agent-runner.ts +0 -271
  317. package/packages/subagents/src/async-runner.ts +0 -422
  318. package/packages/subagents/src/authority-policy.ts +0 -47
  319. package/packages/subagents/src/budgets.ts +0 -285
  320. package/packages/subagents/src/child-safety.ts +0 -140
  321. package/packages/subagents/src/config.ts +0 -249
  322. package/packages/subagents/src/conversation-viewer.ts +0 -281
  323. package/packages/subagents/src/core-compat.ts +0 -94
  324. package/packages/subagents/src/custom-agents.ts +0 -343
  325. package/packages/subagents/src/file-system-retry.ts +0 -50
  326. package/packages/subagents/src/fleet-data.ts +0 -105
  327. package/packages/subagents/src/fleet-view.ts +0 -322
  328. package/packages/subagents/src/foreground-detach.ts +0 -59
  329. package/packages/subagents/src/fork-context.ts +0 -285
  330. package/packages/subagents/src/global.d.ts +0 -7
  331. package/packages/subagents/src/guide.ts +0 -129
  332. package/packages/subagents/src/mission-state.ts +0 -133
  333. package/packages/subagents/src/mission-store.ts +0 -436
  334. package/packages/subagents/src/output-limits.ts +0 -142
  335. package/packages/subagents/src/parity-types.ts +0 -315
  336. package/packages/subagents/src/pi-args.ts +0 -167
  337. package/packages/subagents/src/result-files.ts +0 -232
  338. package/packages/subagents/src/result-watcher.ts +0 -221
  339. package/packages/subagents/src/retained-children.ts +0 -176
  340. package/packages/subagents/src/run-fanout-budget.ts +0 -356
  341. package/packages/subagents/src/scheduled-runs.ts +0 -349
  342. package/packages/subagents/src/schemas.ts +0 -263
  343. package/packages/subagents/src/slash-commands.ts +0 -80
  344. package/packages/subagents/src/supervisor-channel.ts +0 -249
  345. package/packages/subagents/src/tool-handler.ts +0 -1144
  346. package/packages/subagents/src/types.ts +0 -238
  347. package/packages/subagents/src/widget.ts +0 -457
  348. package/packages/subagents/src/workflow-script.ts +0 -737
  349. package/packages/subagents/src/workflow-worker.ts +0 -384
  350. package/packages/subagents/src/worktree.ts +0 -614
  351. package/packages/utility/src/analytics/collector.ts +0 -293
  352. package/packages/utility/src/info-screen.ts +0 -80
  353. package/packages/utility/src/lifecycle/process.ts +0 -162
  354. package/packages/utility/src/prefix-cache.ts +0 -263
  355. package/packages/utility/src/skill-discovery.ts +0 -542
  356. package/packages/utility/src/tools/env.ts +0 -113
  357. package/packages/utility/src/tui/name-badge-state.ts +0 -315
  358. package/packages/utility/src/tui/name-badge.ts +0 -109
  359. /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * @pi-unipi/kanboard — the turn arbiter's kanboard nudge provider.
3
+ *
4
+ * Replaces the runner and the R2 reminder: when the lead settles with claims
5
+ * of its own still In Progress, the monitor proposes ONE continuation nudge
6
+ * (priority 50, claims; priority 40, autowork-next). The arbiter delivers at
7
+ * most one nudge per settle, so the monitor never fights the long-horizon
8
+ * owner (priority 100) or a pending event. Lead only — children never
9
+ * register it.
10
+ */
11
+
12
+ import type { Nudge, SettleInfo } from "@pi-unipi/core";
13
+ import { harnessMetadata, kanboardGlanceLabel } from "@pi-unipi/core";
14
+
15
+ import { ANTI_POISONING_SUFFIX, taskIdsIn } from "./reminders.js";
16
+ import type { KanboardTask } from "./shapes.js";
17
+
18
+ export const CLAIMS_NUDGE_CUSTOM_TYPE = "unipi:kanboard-continue";
19
+ export const AUTOWORK_NUDGE_CUSTOM_TYPE = "unipi:kanboard-next";
20
+ export const CLAIMS_PRIORITY = 50;
21
+ export const AUTOWORK_PRIORITY = 40;
22
+ /** Hard cap of continuation nudges per task. */
23
+ export const MAX_NUDGES_PER_TASK = 5;
24
+ /** Consecutive nudged runs with zero tool calls before the monitor gives up. */
25
+ export const STALL_LIMIT = 2;
26
+ /** Offers of the SAME ready task before autowork gives up on it. */
27
+ export const MAX_OFFERS_PER_TASK = 3;
28
+
29
+ export interface MonitorDeps {
30
+ /** `list --json` on this session's project (board order). */
31
+ list(): Promise<KanboardTask[]>;
32
+ /** `list --ready --json` (todo tasks whose deps are met), board order. */
33
+ listReady(): Promise<KanboardTask[]>;
34
+ /** This session's id (UNIPI_KANBOARD_SESSION). */
35
+ session(): string;
36
+ /** Shared long-horizon owner status holder. */
37
+ ownerStatus(): { owner?: { kind: string; status: string }; lastStop?: { kind: string; at: number } } | undefined;
38
+ /** Wall clock (injectable in tests). */
39
+ now(): number;
40
+ /** User-only notices (never LLM context) — implementations must only queue. */
41
+ notify(text: string, level?: "info" | "warning"): void;
42
+ /** The monitor itself turned autowork off (done / stalled) — guard + holder follow. */
43
+ onAutoworkOff?(): void;
44
+ /** `<binary> --actor agent --project <slug>` for the nudge text, or null. */
45
+ cliPrefix(): string | null;
46
+ debug?(line: string): void;
47
+ }
48
+
49
+ export interface KanboardMonitor {
50
+ /** agent_start: the run that is about to start; stamps runStartedAt. */
51
+ onAgentStart(): void;
52
+ /** A user prompt: re-arm when it names one of our claimed ids. */
53
+ onUserPrompt(text: string): Promise<void>;
54
+ /** agent_end: an aborted or errored run disarms (kanboard never re-nudges after Esc). */
55
+ onAgentEnd(messages: unknown[] | undefined): void;
56
+ /** Arm the monitor (from -do, autowork start, or a successful lead `start`). */
57
+ arm(reason?: string): void;
58
+ disarm(): void;
59
+ setAutowork(on: boolean): void;
60
+ /** The nudge provider body (throws are caught by the arbiter, not here). */
61
+ propose(info: SettleInfo): Promise<Nudge | null>;
62
+ /** For tests / the status holder. */
63
+ state(): {
64
+ armed: boolean;
65
+ autowork: boolean;
66
+ nudgesPerTask: Record<string, number>;
67
+ noProgressRuns: number;
68
+ };
69
+ }
70
+
71
+ /** Last assistant message aborted or errored (Step 0 Q1d: stopReason). */
72
+ function runFailed(messages: unknown[] | undefined): boolean {
73
+ if (!Array.isArray(messages)) return false;
74
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
75
+ const message = messages[index] as { role?: string; stopReason?: string };
76
+ if (message?.role === "assistant") return message.stopReason === "aborted" || message.stopReason === "error";
77
+ }
78
+ return false;
79
+ }
80
+
81
+ /** The final non-empty paragraph of a text, trimmed. */
82
+ function finalParagraph(text: string): string {
83
+ const paragraphs = text.trim().split(/\n\s*\n/).filter((part) => part.trim().length > 0);
84
+ return (paragraphs.at(-1) ?? "").trim();
85
+ }
86
+
87
+ function ownClaim(task: KanboardTask, session: string): boolean {
88
+ const run = task.run as { session?: string } | null | undefined;
89
+ return task.status === "in_progress" && run?.session === session;
90
+ }
91
+
92
+ /** Our own runtime texts never count as user prompts naming claimed ids. */
93
+ function isOwnRuntimeText(text: string): boolean {
94
+ return (
95
+ text.includes(ANTI_POISONING_SUFFIX) ||
96
+ text.includes(CLAIMS_NUDGE_CUSTOM_TYPE) ||
97
+ text.includes(AUTOWORK_NUDGE_CUSTOM_TYPE)
98
+ );
99
+ }
100
+
101
+ export function createKanboardMonitor(deps: MonitorDeps): KanboardMonitor {
102
+ const debug = (line: string): void => deps.debug?.(`monitor: ${line}`);
103
+ let armed = false;
104
+ let autowork = false;
105
+ let runStartedAt = 0;
106
+ let lastRunWasNudge = false;
107
+ let nudgeDelivered = false;
108
+ let noProgressRuns = 0;
109
+ /** Continuation nudges per task id (delivered, not proposed). */
110
+ const nudgesPerTask = new Map<string, number>();
111
+ /** Autowork offers per ready task id (delivered; reset when it leaves ready). */
112
+ const offersPerTask = new Map<string, number>();
113
+ /** Notice dedupe per condition; cleared on re-arm. */
114
+ const notified = new Set<string>();
115
+ const notifyOnce = (key: string, text: string, level: "info" | "warning" = "info"): void => {
116
+ if (notified.has(key)) return;
117
+ notified.add(key);
118
+ deps.notify(text, level);
119
+ };
120
+ const autoworkOff = (why: string): void => {
121
+ autowork = false;
122
+ try {
123
+ deps.onAutoworkOff?.();
124
+ } catch {
125
+ // A broken callback must never break settlement.
126
+ }
127
+ debug(`autowork off (${why})`);
128
+ };
129
+ const disarmNow = (): void => {
130
+ if (armed) debug("disarmed");
131
+ armed = false;
132
+ };
133
+
134
+ const claimedFrom = (tasks: KanboardTask[]): KanboardTask[] => {
135
+ const session = deps.session();
136
+ return tasks.filter((task) => ownClaim(task, session));
137
+ };
138
+
139
+ return {
140
+ onAgentStart() {
141
+ // The run that just started was nudged iff the previous settle delivered.
142
+ lastRunWasNudge = nudgeDelivered;
143
+ nudgeDelivered = false;
144
+ runStartedAt = deps.now();
145
+ },
146
+
147
+ async onUserPrompt(text) {
148
+ if (isOwnRuntimeText(text)) return;
149
+ const named = taskIdsIn(text);
150
+ if (named.length === 0) return;
151
+ try {
152
+ const claimed = claimedFrom(await deps.list()).map((task) => task.id);
153
+ if (named.some((id) => claimed.includes(id))) {
154
+ armed = true;
155
+ notified.clear();
156
+ debug(`re-armed by user prompt naming ${claimed.join(", ")}`);
157
+ }
158
+ } catch (error) {
159
+ debug(`prompt re-arm list failed: ${error instanceof Error ? error.message : String(error)}`);
160
+ }
161
+ },
162
+
163
+ onAgentEnd(messages) {
164
+ if (runFailed(messages)) {
165
+ // Esc (or a provider error): kanboard never re-nudges over the user.
166
+ disarmNow();
167
+ }
168
+ },
169
+
170
+ arm(reason) {
171
+ armed = true;
172
+ notified.clear();
173
+ debug(`armed${reason ? ` (${reason})` : ""}`);
174
+ },
175
+
176
+ disarm() {
177
+ disarmNow();
178
+ },
179
+
180
+ setAutowork(on) {
181
+ autowork = on;
182
+ },
183
+
184
+ async propose(info) {
185
+ if (!armed) return null;
186
+ let tasks: KanboardTask[];
187
+ try {
188
+ tasks = await deps.list();
189
+ } catch (error) {
190
+ debug(`list failed: ${error instanceof Error ? error.message : String(error)}`);
191
+ return null;
192
+ }
193
+ const claims = claimedFrom(tasks);
194
+
195
+ // A long-horizon owner drives continuation: kanboard stays quiet.
196
+ // An owner that STOPPED this run matters: complete → kanboard takes
197
+ // over again; paused/budget/other → tell the user what was left.
198
+ const owner = deps.ownerStatus();
199
+ if (owner?.owner?.status === "active") return null;
200
+ const stop = owner?.lastStop;
201
+ if (stop && stop.at >= runStartedAt && stop.kind !== "complete") {
202
+ if (claims.length > 0) {
203
+ const ids = claims.map((task) => task.id).join(", ");
204
+ const kindWord = stop.kind === "paused" ? "paused" : stop.kind === "budget" ? "hit its budget" : "stopped";
205
+ notifyOnce(
206
+ `goal-stop:${ids}`,
207
+ `kanboard: goal ${kindWord} — ${ids} left In Progress`,
208
+ );
209
+ }
210
+ disarmNow();
211
+ return null;
212
+ }
213
+
214
+ if (claims.length > 0) {
215
+ // Question heuristic: the agent ended by asking the user something —
216
+ // a nudge would talk over the question. Notice only, no count.
217
+ if (!info.lastAssistantHadToolCalls && finalParagraph(info.lastAssistantText).endsWith("?")) {
218
+ const ids = claims.map((task) => task.id).join(", ");
219
+ notifyOnce(`question:${ids}`, `kanboard: ${ids} left In Progress — the agent asked you a question`);
220
+ return null;
221
+ }
222
+ // Stall guard: nudged runs that did nothing.
223
+ if (lastRunWasNudge && info.toolCallsThisRun === 0) noProgressRuns += 1;
224
+ else if (info.toolCallsThisRun > 0) noProgressRuns = 0;
225
+ if (noProgressRuns >= STALL_LIMIT) {
226
+ const ids = claims.map((task) => task.id).join(", ");
227
+ notifyOnce(`stall:${ids}`, `kanboard: ⚠ ${ids} stalled — no progress after ${String(STALL_LIMIT)} nudges`, "warning");
228
+ disarmNow();
229
+ return null;
230
+ }
231
+ const rest = claims.slice(1).map((task) => task.id);
232
+ const target = claims.find((task) => (nudgesPerTask.get(task.id) ?? 0) < MAX_NUDGES_PER_TASK);
233
+ if (!target) {
234
+ const ids = claims.map((task) => task.id).join(", ");
235
+ notifyOnce(`cap:${ids}`, `kanboard: ⚠ ${ids} stalled — nudge cap (${String(MAX_NUDGES_PER_TASK)}) reached`, "warning");
236
+ disarmNow();
237
+ return null;
238
+ }
239
+ const n = (nudgesPerTask.get(target.id) ?? 0) + 1;
240
+ const prefix = deps.cliPrefix() ?? "unipi-kanboard";
241
+ const also = rest.length > 0 ? ` · also open: ${rest.join(", ")}` : "";
242
+ const content =
243
+ `↻ ${target.id} still In Progress — continue, or finish/block it (${String(n)}/${String(MAX_NUDGES_PER_TASK)})${also}\n` +
244
+ `\`${prefix} finish ${target.id} --comment "<summary>"\` / \`${prefix} move ${target.id} blocked --comment "<what you need>"\` ` +
245
+ ANTI_POISONING_SUFFIX;
246
+ return {
247
+ source: "kanboard",
248
+ priority: CLAIMS_PRIORITY,
249
+ customType: CLAIMS_NUDGE_CUSTOM_TYPE,
250
+ details: { unipiHarness: harnessMetadata({ source: "Kanboard", title: "Unfinished task", synopsis: String(target.id), severity: "warning" }, "boundary") },
251
+ content,
252
+ display: true,
253
+ onDelivered: () => {
254
+ nudgesPerTask.set(target.id, n); // counts on DELIVERY, not proposal
255
+ nudgeDelivered = true;
256
+ },
257
+ };
258
+ }
259
+
260
+ if (autowork) {
261
+ // The same stall rule as claims: an offered run that did nothing.
262
+ if (lastRunWasNudge && info.toolCallsThisRun === 0) noProgressRuns += 1;
263
+ else if (info.toolCallsThisRun > 0) noProgressRuns = 0;
264
+ if (noProgressRuns >= STALL_LIMIT) {
265
+ notifyOnce(
266
+ "autowork-stall",
267
+ `kanboard: ⚠ autowork stalled — no progress after ${String(STALL_LIMIT)} offers`,
268
+ "warning",
269
+ );
270
+ autoworkOff("stalled");
271
+ disarmNow();
272
+ return null;
273
+ }
274
+ let ready: KanboardTask[];
275
+ try {
276
+ ready = (await deps.listReady()).filter((task) => task.status === "todo");
277
+ } catch (error) {
278
+ debug(`listReady failed: ${error instanceof Error ? error.message : String(error)}`);
279
+ return null;
280
+ }
281
+ const next = ready[0];
282
+ if (next) {
283
+ // A task that left the ready list gets a fresh offer budget.
284
+ for (const id of [...offersPerTask.keys()]) {
285
+ if (!ready.some((task) => task.id === id)) offersPerTask.delete(id);
286
+ }
287
+ const offers = (offersPerTask.get(next.id) ?? 0) + 1;
288
+ if (offers > MAX_OFFERS_PER_TASK) {
289
+ notifyOnce(`autowork-cap:${next.id}`, `kanboard: ⚠ autowork stalled on ${next.id}`, "warning");
290
+ autoworkOff(`cap:${next.id}`);
291
+ disarmNow();
292
+ return null;
293
+ }
294
+ return {
295
+ source: "kanboard",
296
+ priority: AUTOWORK_PRIORITY,
297
+ customType: AUTOWORK_NUDGE_CUSTOM_TYPE,
298
+ details: { unipiHarness: harnessMetadata({ source: "Kanboard", title: "Next task", synopsis: String(next.id) }, "boundary") },
299
+ content: `↻ next ready: ${next.id} ${next.displayTitle || next.title || "(untitled)"} — show it, start it, work it (autowork) ${ANTI_POISONING_SUFFIX}`,
300
+ display: true,
301
+ onDelivered: () => {
302
+ offersPerTask.set(next.id, offers); // counts on DELIVERY, not proposal
303
+ nudgeDelivered = true;
304
+ },
305
+ };
306
+ }
307
+ const inReview = tasks.filter((task) => task.status === "in_review").length;
308
+ const blocked = tasks.filter((task) => task.status === "blocked").length;
309
+ notifyOnce("autowork-done", `kanboard: autowork done · ${String(inReview)} in review · ${String(blocked)} blocked`);
310
+ autoworkOff("done");
311
+ disarmNow();
312
+ return null;
313
+ }
314
+
315
+ return null;
316
+ },
317
+
318
+ state() {
319
+ return {
320
+ armed,
321
+ autowork,
322
+ nudgesPerTask: Object.fromEntries(nudgesPerTask),
323
+ noProgressRuns,
324
+ };
325
+ },
326
+ };
327
+ }
328
+
329
+ /** Glance label for the current claims/autowork snapshot (re-exported read). */
330
+ export const monitorGlanceLabel = kanboardGlanceLabel;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Kanboard monitor notices — user-only, never LLM context.
3
+ *
4
+ * The monitor proposes during the settle boundary, where `appendEntry` is
5
+ * dropped and a `sendMessage` custom message would be converted to a
6
+ * user-role LLM message (dist/core/messages.js `case "custom"`). So the
7
+ * monitor only QUEUES text here; kanboard's `agent_settled` handler (post
8
+ * boundary) drains it via `flushNotices`: an `appendEntry` custom entry
9
+ * (rendered by the entry renderer, transcript-only) plus a UI toast.
10
+ */
11
+
12
+ export type NoticeLevel = "info" | "warning";
13
+
14
+ export interface NoticeItem {
15
+ text: string;
16
+ level: NoticeLevel;
17
+ }
18
+
19
+ export const NOTICE_ENTRY = "unipi:kanboard-notice";
20
+
21
+ export class NoticeBuffer {
22
+ private items: NoticeItem[] = [];
23
+
24
+ queue(text: string, level: NoticeLevel = "info"): void {
25
+ this.items.push({ text, level });
26
+ }
27
+
28
+ /** Return everything queued (newest last) and clear the buffer. */
29
+ drain(): NoticeItem[] {
30
+ return this.items.splice(0);
31
+ }
32
+
33
+ get pending(): number {
34
+ return this.items.length;
35
+ }
36
+ }
37
+
38
+ export interface NoticeFlushPi {
39
+ /** Custom entries are transcript-only — never serialized into LLM context. */
40
+ appendEntry(customType: string, data?: unknown): void;
41
+ }
42
+
43
+ export interface NoticeFlushUi {
44
+ hasUI?: boolean;
45
+ notify?(text: string, level?: NoticeLevel): void;
46
+ }
47
+
48
+ /**
49
+ * Drain the buffer: one appendEntry per notice (user-only entry) and, when a
50
+ * dialog-capable UI is attached, a toast. Never sends a message. appendEntry
51
+ * failures are reported to `debug` (the toast still fires, so a notice is
52
+ * never lost even if entries are dropped in odd phases).
53
+ */
54
+ export function flushNotices(
55
+ pi: NoticeFlushPi,
56
+ buffer: NoticeBuffer,
57
+ ui?: NoticeFlushUi,
58
+ debug?: (line: string) => void,
59
+ ): void {
60
+ for (const item of buffer.drain()) {
61
+ try {
62
+ pi.appendEntry(NOTICE_ENTRY, { text: item.text, level: item.level });
63
+ } catch (error) {
64
+ debug?.(`appendEntry dropped: ${error instanceof Error ? error.message : String(error)}`);
65
+ }
66
+ if (ui?.hasUI !== false) {
67
+ try {
68
+ ui?.notify?.(item.text, item.level);
69
+ } catch {
70
+ // A failed toast must never break settlement.
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Board progress bar (user-only entry):
3
+ * ▣ Board · unipi ██████████▒▒░░░░░░░░ 5/10 tasks 1 blocked
4
+ * Solid = in review + done, shade = in progress, light = todo + blocked.
5
+ * Backlog, cancelled and archived tasks are outside the bar.
6
+ */
7
+
8
+ import type { ProgressData } from "@pi-unipi/core";
9
+
10
+ const COUNTED = new Set(["todo", "in_progress", "in_review", "done", "blocked"]);
11
+
12
+ export function boardProgressData(tasks: ReadonlyArray<{ status: string }>, slug: string): ProgressData | undefined {
13
+ const counted = tasks.filter((t) => COUNTED.has(t.status));
14
+ if (counted.length === 0) return undefined;
15
+ const n = (s: string) => counted.filter((t) => t.status === s).length;
16
+ const blocked = n("blocked");
17
+ return {
18
+ icon: "▣",
19
+ label: slug ? `Board · ${slug}` : "Board",
20
+ done: n("in_review") + n("done"),
21
+ active: n("in_progress"),
22
+ total: counted.length,
23
+ unit: "tasks",
24
+ detail: blocked > 0 ? `${String(blocked)} blocked` : undefined,
25
+ color: "success",
26
+ };
27
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @pi-unipi/kanboard — progress reminders (no LLM, never blocking).
3
+ *
4
+ * When the agent works board tasks by hand ("do UNI-5 and UNI-8"), the board
5
+ * should show it: `start <ID>` moves a task to In Progress for this session.
6
+ * Continuation after that is the arbiter's job (src/monitor.ts); what stays
7
+ * here is:
8
+ *
9
+ * R1 every file-changing tool call of an agent turn, for each mentioned
10
+ * task that is still Todo and has not been reminded this turn (one
11
+ * steer per task per turn, at most twice per task).
12
+ *
13
+ * "Mentioned" = task ids in the user's prompts plus ids the agent `show`ed;
14
+ * only ids that exist on the board count. R1 is silent in child sessions
15
+ * (children only read the board) and when the setting `kanboard.reminders`
16
+ * is off.
17
+ */
18
+
19
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
+ import { isChildProcess, harnessToolResultDetails } from "@pi-unipi/core";
21
+
22
+ import { kanboardInvocations, shellSegments } from "./guard.js";
23
+ import type { KanboardTask } from "./shapes.js";
24
+
25
+ export const REMINDER_CUSTOM_TYPE = "unipi:kanboard-reminder";
26
+ /** R1 reminders per task id. */
27
+ export const MAX_REMINDERS_PER_TASK = 2;
28
+
29
+ /** Same wording long-horizon's runaway guard uses: the reminder must not stick. */
30
+ export const ANTI_POISONING_SUFFIX =
31
+ "This is a temporary runtime reminder for the current Turn only, not a user preference " +
32
+ "or a durable rule; do not save this reminder or generalize it into Memory, Skills, or " +
33
+ "other persistent instruction files.";
34
+
35
+ /** Board task ids: PREFIX-123 (the prefix is letters/digits, starting with a letter). */
36
+ const TASK_ID = /\b[A-Z][A-Z0-9]{0,15}-\d+\b/g;
37
+
38
+ /** Tools that change files. bash/powershell count unless the command only reads. */
39
+ const EDIT_TOOLS = new Set(["edit", "write", "multi_edit", "multiedit", "apply_patch", "notebook_edit"]);
40
+ const SHELL_TOOLS = new Set(["bash", "powershell"]);
41
+
42
+ /** First words of shell segments that never change files. */
43
+ const READ_COMMANDS = new Set([
44
+ "ls", "pwd", "cat", "head", "tail", "wc", "echo", "printf", "which", "type", "rg", "grep", "egrep",
45
+ "fgrep", "find", "fd", "stat", "file", "du", "df", "env", "printenv", "date", "whoami", "tree", "less",
46
+ "more", "true", "false", "test", "[", "diff", "cmp", "jq", "realpath", "dirname", "basename", "uname",
47
+ "hostname", "id", "ps", "cd",
48
+ ]);
49
+ /** Interpreters/package managers whose version-check arms never write. */
50
+ const VERSION_CHECKED = new Set(["node", "npm", "npx", "python", "python3"]);
51
+ const isVersionFlag = (word: string): boolean => word === "--version" || word === "-v" || word === "-V";
52
+ const GIT_READS = new Set(["status", "log", "diff", "show", "branch", "remote", "rev-parse", "blame", "ls-files", "describe", "tag"]);
53
+
54
+ export function taskIdsIn(text: string): string[] {
55
+ return [...new Set(text.match(TASK_ID) ?? [])];
56
+ }
57
+
58
+ /**
59
+ * Whether a shell command may change files. Conservative toward "yes": any
60
+ * segment that is not a known read (or a kanboard CLI call) counts, and so
61
+ * does an output redirection. Quoted spans are masked before both the
62
+ * redirect test and the segment split, so `grep -c "a;b" f` stays one
63
+ * read-only segment.
64
+ */
65
+ export function shellChangesFiles(command: string): boolean {
66
+ const trimmed = command.trim();
67
+ if (!trimmed) return false;
68
+ // `>`/`>>` into a file (but not `2>&1` / `>/dev/null`).
69
+ if (/(^|[^0-9&>])>{1,2}\s*(?!&|\/dev\/null)[^\s|;&]/.test(trimmed.replace(/(["'])(?:\\.|(?!\1).)*\1/g, '""'))) return true;
70
+ const segments = shellSegments(trimmed).map((part) => part.trim()).filter(Boolean);
71
+ return segments.some((segment) => {
72
+ const words = segment.split(/\s+/).filter((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word));
73
+ const first = (words[0] ?? "").replace(/^.*\//, "");
74
+ if (!first) return false;
75
+ // Board writes go through the binary, not files.
76
+ if (/^unipi-kanboard(\.exe)?$/.test(first)) return false;
77
+ if (READ_COMMANDS.has(first)) return false;
78
+ if (first === "git") return !GIT_READS.has(words[1] ?? "");
79
+ if (first === "sed") return words.includes("-i") || words.some((word) => word.startsWith("-i"));
80
+ // `sort` writes only through -o/--output (a `>` redirect is caught above).
81
+ if (first === "sort") return words.some((word) => /^-[^-]*o/.test(word) || word.startsWith("--output"));
82
+ // node/npm/npx/python are reads only as version checks (`node --version`,
83
+ // `npx tsx --version`); scripts, installs and builds still count.
84
+ if (VERSION_CHECKED.has(first)) return !words.slice(1).some(isVersionFlag);
85
+ return true;
86
+ });
87
+ }
88
+
89
+ export function isFileChangingCall(toolName: string, input: Record<string, unknown> | undefined): boolean {
90
+ if (EDIT_TOOLS.has(toolName)) return true;
91
+ if (!SHELL_TOOLS.has(toolName)) return false;
92
+ return shellChangesFiles(String(input?.command ?? ""));
93
+ }
94
+
95
+ export function r1Text(todo: string[], cli: string | null): string {
96
+ const ids = todo.join(", ");
97
+ const verb = todo.length === 1 ? "is" : "are";
98
+ const how = cli ? `\`${cli} start <ID>\`` : "`start <ID>`";
99
+ return (
100
+ `[kanboard] ${ids} ${verb} still Todo. ${how} the one you're on before changing files ` +
101
+ `(it moves it to In Progress for this session), and \`finish <ID> --comment "<summary>"\` when done. ` +
102
+ ANTI_POISONING_SUFFIX
103
+ );
104
+ }
105
+
106
+ export interface TrackerDeps {
107
+ /** Reminders on (setting `kanboard.reminders`). */
108
+ enabled(): boolean;
109
+ /** This session's id (UNIPI_KANBOARD_SESSION). */
110
+ session(): string;
111
+ /** `list` on the current project; [] when unavailable. */
112
+ list(): Promise<KanboardTask[]>;
113
+ /** `<binary> --actor agent --project <slug>` for the reminder text, or null. */
114
+ cliPrefix(): string | null;
115
+ debug?(message: string): void;
116
+ }
117
+
118
+ type ToolResultContent = Array<{ type: string; text?: string; [key: string]: unknown }>;
119
+
120
+ export interface ToolResultLike {
121
+ toolName?: string;
122
+ input?: Record<string, unknown>;
123
+ content?: ToolResultContent;
124
+ isError?: boolean;
125
+ }
126
+
127
+ export interface ProgressTracker {
128
+ /** A user prompt (or -do request): remember the task ids it names. */
129
+ onPrompt(text: string): void;
130
+ /** A new agent turn (agent_start): re-arm R1. */
131
+ onTurnStart(): void;
132
+ /** tool_result: record `show`/`start`, and return R1 content when due. */
133
+ onToolResult(event: ToolResultLike): Promise<{ content: ToolResultContent } | undefined>;
134
+ /** For tests / status. */
135
+ state(): { mentioned: string[]; started: string[]; r1: Record<string, number> };
136
+ }
137
+
138
+ export function createProgressTracker(deps: TrackerDeps): ProgressTracker {
139
+ const mentioned = new Set<string>();
140
+ /** Ids this session ran `start` on (bookkeeping; the board is the truth). */
141
+ const started = new Set<string>();
142
+ const r1Count = new Map<string, number>();
143
+ /** Task ids already reminded this turn (per-task re-arm, not one shot). */
144
+ const remindedThisTurn = new Set<string>();
145
+ const debug = (message: string): void => deps.debug?.(`reminders: ${message}`);
146
+ const silent = (): boolean => !deps.enabled() || isChildProcess() || Boolean(process.env.UNIPI_KANBOARD_CHILD);
147
+
148
+ const safeList = async (): Promise<KanboardTask[] | null> => {
149
+ try {
150
+ return await deps.list();
151
+ } catch (error) {
152
+ debug(`list failed: ${error instanceof Error ? error.message : String(error)}`);
153
+ return null;
154
+ }
155
+ };
156
+
157
+ return {
158
+ onPrompt(text) {
159
+ // Our own reminders name started tasks; they are not new mentions. (A
160
+ // -do request counts: its text carries the user's request verbatim.)
161
+ if (text.includes(ANTI_POISONING_SUFFIX)) return;
162
+ for (const id of taskIdsIn(text)) mentioned.add(id);
163
+ },
164
+
165
+ onTurnStart() {
166
+ remindedThisTurn.clear();
167
+ },
168
+
169
+ async onToolResult(event) {
170
+ const toolName = String(event.toolName ?? "");
171
+ if (SHELL_TOOLS.has(toolName)) {
172
+ for (const invocation of kanboardInvocations(String(event.input?.command ?? ""))) {
173
+ const id = invocation.args.find((arg) => !arg.startsWith("-"));
174
+ if (!id) continue;
175
+ if (invocation.sub === "show") mentioned.add(id);
176
+ if (invocation.sub === "start" && !event.isError) started.add(id);
177
+
178
+ }
179
+ }
180
+ if (!isFileChangingCall(toolName, event.input)) return undefined;
181
+ if (silent() || mentioned.size === 0) return undefined;
182
+ // Per task, at most once per turn: candidates are mentioned ids not yet
183
+ // reminded this turn and under the per-task cap (a started task leaves
184
+ // Todo and is never re-reminded).
185
+ const candidates = [...mentioned].filter(
186
+ (id) => !remindedThisTurn.has(id) && (r1Count.get(id) ?? 0) < MAX_REMINDERS_PER_TASK,
187
+ );
188
+ if (candidates.length === 0) return undefined;
189
+ const tasks = await safeList();
190
+ if (!tasks) return undefined;
191
+ const byId = new Map(tasks.map((task) => [task.id, task]));
192
+ const todo = candidates.filter((id) => byId.get(id)?.status === "todo");
193
+ if (todo.length === 0) return undefined;
194
+ for (const id of todo) {
195
+ remindedThisTurn.add(id);
196
+ r1Count.set(id, (r1Count.get(id) ?? 0) + 1);
197
+ }
198
+ debug(`R1 for ${todo.join(", ")}`);
199
+ const annotation = `\n\n${r1Text(todo, deps.cliPrefix())}`;
200
+ return {
201
+ content: [...(event.content ?? []), { type: "text", text: annotation }],
202
+ details: harnessToolResultDetails(
203
+ (event as { details?: unknown }).details,
204
+ { source: "Kanboard", title: "R1 progress reminder", synopsis: `${todo.join(", ")} still Todo`, severity: "warning" },
205
+ "boundary",
206
+ annotation,
207
+ ),
208
+ };
209
+ },
210
+
211
+ state() {
212
+ return {
213
+ mentioned: [...mentioned],
214
+ started: [...started],
215
+ r1: Object.fromEntries(r1Count),
216
+ };
217
+ },
218
+ };
219
+ }
220
+
221
+ /** Wire the tracker into pi's events. */
222
+ export function registerProgressReminders(pi: ExtensionAPI, tracker: ProgressTracker): void {
223
+ pi.on("before_agent_start", (event) => {
224
+ tracker.onPrompt(String((event as { prompt?: unknown }).prompt ?? ""));
225
+ return undefined;
226
+ });
227
+ pi.on("agent_start", () => {
228
+ tracker.onTurnStart();
229
+ });
230
+ pi.on("tool_result", async (event) => {
231
+ const result = await tracker.onToolResult(event as unknown as ToolResultLike);
232
+ return result as never;
233
+ });
234
+ }