@prestyj/cli 5.28.1 → 5.29.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/assets/motion/bin/contact-sheet.mjs +9 -3
  2. package/assets/motion/bin/cues.mjs +337 -0
  3. package/assets/motion/bin/library.mjs +53 -10
  4. package/assets/motion/bin/motion-blur.mjs +943 -0
  5. package/assets/motion/bin/motion-check.mjs +354 -3
  6. package/assets/motion/bin/music-fit.mjs +436 -0
  7. package/assets/motion/bin/pdf-extract.mjs +22 -10
  8. package/assets/motion/bin/reference-study.mjs +346 -0
  9. package/assets/motion/bin/score-synth.mjs +1052 -93
  10. package/assets/motion/library/README.md +50 -14
  11. package/assets/motion/library/kit/moves.js +1981 -0
  12. package/assets/motion/library/library.json +232 -0
  13. package/assets/motion/library/pieces/camera-rig/meta.json +13 -0
  14. package/assets/motion/library/pieces/camera-rig/piece.html +153 -0
  15. package/assets/motion/library/pieces/camera-rig/preview.jpg +0 -0
  16. package/assets/motion/library/pieces/chain-knock/meta.json +13 -0
  17. package/assets/motion/library/pieces/chain-knock/piece.html +195 -0
  18. package/assets/motion/library/pieces/chain-knock/preview.jpg +0 -0
  19. package/assets/motion/library/pieces/gather-to-logo/meta.json +13 -0
  20. package/assets/motion/library/pieces/gather-to-logo/piece.html +159 -0
  21. package/assets/motion/library/pieces/gather-to-logo/preview.jpg +0 -0
  22. package/assets/motion/library/pieces/morph-carry/meta.json +13 -0
  23. package/assets/motion/library/pieces/morph-carry/piece.html +173 -0
  24. package/assets/motion/library/pieces/morph-carry/preview.jpg +0 -0
  25. package/assets/motion/library/pieces/one-shape-journey/meta.json +13 -0
  26. package/assets/motion/library/pieces/one-shape-journey/piece.html +195 -0
  27. package/assets/motion/library/pieces/one-shape-journey/preview.jpg +0 -0
  28. package/assets/motion/library/pieces/open-from-subject/meta.json +13 -0
  29. package/assets/motion/library/pieces/open-from-subject/piece.html +168 -0
  30. package/assets/motion/library/pieces/open-from-subject/preview.jpg +0 -0
  31. package/assets/motion/library/pieces/request-to-result/meta.json +13 -0
  32. package/assets/motion/library/pieces/request-to-result/piece.html +212 -0
  33. package/assets/motion/library/pieces/request-to-result/preview.jpg +0 -0
  34. package/assets/motion/library/pieces/scale-dive/meta.json +13 -0
  35. package/assets/motion/library/pieces/scale-dive/piece.html +321 -0
  36. package/assets/motion/library/pieces/scale-dive/preview.jpg +0 -0
  37. package/assets/motion/library/pieces/screen-replica-steps/meta.json +13 -0
  38. package/assets/motion/library/pieces/screen-replica-steps/piece.html +366 -0
  39. package/assets/motion/library/pieces/screen-replica-steps/preview.jpg +0 -0
  40. package/assets/motion/library/pieces/zoom-into-card/meta.json +13 -0
  41. package/assets/motion/library/pieces/zoom-into-card/piece.html +179 -0
  42. package/assets/motion/library/pieces/zoom-into-card/preview.jpg +0 -0
  43. package/assets/motion/library/sheets/diagram.jpg +0 -0
  44. package/assets/motion/library/sheets/frame.jpg +0 -0
  45. package/assets/motion/library/sheets/transition.jpg +0 -0
  46. package/assets/motion/library/sheets/ui.jpg +0 -0
  47. package/assets/motion/references/build-sheet.md +206 -0
  48. package/assets/motion/references/runtime/determinism-rules.md +1 -1
  49. package/assets/motion/references/runtime/gsap-easing-and-stagger.md +29 -29
  50. package/assets/motion/references/runtime/inputs-and-assets.md +7 -12
  51. package/assets/motion/references/runtime/lint-validate-inspect.md +3 -3
  52. package/assets/motion/references/runtime/minimal-composition.md +1 -1
  53. package/assets/motion/references/runtime/preview-render.md +3 -3
  54. package/assets/motion/skills/app-walkthrough/SKILL.md +66 -0
  55. package/assets/motion/skills/before-after/SKILL.md +53 -0
  56. package/assets/motion/skills/brand-kit/SKILL.md +3 -3
  57. package/assets/motion/skills/dev-tool-video/SKILL.md +57 -0
  58. package/assets/motion/skills/launch-video/SKILL.md +62 -0
  59. package/assets/motion/skills/match-reference/SKILL.md +58 -0
  60. package/assets/motion/skills/motion/SKILL.md +78 -85
  61. package/assets/motion/skills/source-ingest/SKILL.md +17 -6
  62. package/assets/motion/skills/website-video/SKILL.md +59 -0
  63. package/assets/skills/bulletproof/SKILL.md +36 -11
  64. package/assets/skills/bulletproof/references/agent-surface.md +19 -9
  65. package/assets/skills/bulletproof/references/audit-protocol.md +20 -5
  66. package/assets/skills/bulletproof/references/platform-playbooks.md +5 -4
  67. package/assets/skills/bulletproof/references/provenance.md +26 -1
  68. package/assets/skills/bulletproof/references/secure-defaults.md +6 -5
  69. package/assets/skills/bulletproof/references/supply-chain.md +21 -17
  70. package/assets/skills/bulletproof/references/threat-landscape.md +28 -26
  71. package/assets/skills/bulletproof/references/verification.md +2 -0
  72. package/assets/skills/clarify/SKILL.md +25 -16
  73. package/assets/skills/code-review/SKILL.md +71 -13
  74. package/assets/skills/code-review/references/agent-diffs.md +27 -0
  75. package/assets/skills/code-review/references/tests.md +19 -0
  76. package/assets/skills/compliance-guard/SKILL.md +20 -5
  77. package/assets/skills/compliance-guard/references/artifacts.md +1 -1
  78. package/assets/skills/compliance-guard/references/eu-uk.md +16 -16
  79. package/assets/skills/compliance-guard/references/lawsuit-vectors.md +5 -5
  80. package/assets/skills/compliance-guard/references/provenance.md +41 -2
  81. package/assets/skills/compliance-guard/references/sector-gates.md +3 -3
  82. package/assets/skills/compliance-guard/references/security-baseline.md +2 -2
  83. package/assets/skills/compliance-guard/references/trigger-map.md +5 -5
  84. package/assets/skills/compliance-guard/references/us.md +27 -21
  85. package/assets/skills/durable/SKILL.md +87 -79
  86. package/assets/skills/durable/references/agent-db-safety.md +69 -0
  87. package/assets/skills/durable/references/backups-and-runtime.md +19 -12
  88. package/assets/skills/durable/references/migrations-and-schema.md +13 -6
  89. package/assets/skills/evidence-led-ui/SKILL.md +69 -127
  90. package/assets/skills/evidence-led-ui/references/anti-defaults.md +107 -208
  91. package/assets/skills/evidence-led-ui/references/direction.md +124 -0
  92. package/assets/skills/evidence-led-ui/references/production-contract.md +8 -0
  93. package/assets/skills/evidence-led-ui/references/provenance.md +24 -1
  94. package/assets/skills/lean/SKILL.md +90 -71
  95. package/assets/skills/lean/references/memory-and-processes.md +3 -2
  96. package/assets/skills/lean/references/playbooks.md +37 -12
  97. package/assets/skills/refactoring/SKILL.md +24 -3
  98. package/assets/skills/refactoring/references/agent-pitfalls.md +4 -1
  99. package/assets/skills/refactoring/references/legacy.md +21 -0
  100. package/assets/skills/root-cause/SKILL.md +20 -10
  101. package/assets/skills/shared-language/SKILL.md +16 -14
  102. package/assets/skills/tdd/SKILL.md +27 -15
  103. package/dist/app-sidecar.js +203 -47
  104. package/dist/app-sidecar.js.map +1 -1
  105. package/dist/cli.js +17 -26
  106. package/dist/cli.js.map +1 -1
  107. package/dist/core/acceptance-checks.d.ts +48 -0
  108. package/dist/core/acceptance-checks.js +144 -0
  109. package/dist/core/acceptance-checks.js.map +1 -0
  110. package/dist/core/agent-session.d.ts +106 -72
  111. package/dist/core/agent-session.js +539 -399
  112. package/dist/core/agent-session.js.map +1 -1
  113. package/dist/core/agents.d.ts +6 -5
  114. package/dist/core/agents.js.map +1 -1
  115. package/dist/core/ask-user.d.ts +90 -8
  116. package/dist/core/ask-user.js +124 -13
  117. package/dist/core/ask-user.js.map +1 -1
  118. package/dist/core/bundled-agents.js +1 -3
  119. package/dist/core/bundled-agents.js.map +1 -1
  120. package/dist/core/cache-diagnostics.d.ts +68 -0
  121. package/dist/core/cache-diagnostics.js +196 -0
  122. package/dist/core/cache-diagnostics.js.map +1 -0
  123. package/dist/core/cache-expiry.d.ts +87 -0
  124. package/dist/core/cache-expiry.js +111 -0
  125. package/dist/core/cache-expiry.js.map +1 -0
  126. package/dist/core/compaction/compactor.js +78 -48
  127. package/dist/core/compaction/compactor.js.map +1 -1
  128. package/dist/core/compaction/plan-step-policy.d.ts +46 -0
  129. package/dist/core/compaction/plan-step-policy.js +57 -0
  130. package/dist/core/compaction/plan-step-policy.js.map +1 -0
  131. package/dist/core/destructive-git-guard.d.ts +90 -0
  132. package/dist/core/destructive-git-guard.js +871 -0
  133. package/dist/core/destructive-git-guard.js.map +1 -0
  134. package/dist/core/event-bus.d.ts +3 -0
  135. package/dist/core/event-bus.js +5 -0
  136. package/dist/core/event-bus.js.map +1 -1
  137. package/dist/core/injection-detect.d.ts +38 -0
  138. package/dist/core/injection-detect.js +232 -0
  139. package/dist/core/injection-detect.js.map +1 -0
  140. package/dist/core/keep-awake.d.ts +88 -0
  141. package/dist/core/keep-awake.js +251 -0
  142. package/dist/core/keep-awake.js.map +1 -0
  143. package/dist/core/mcp/client.d.ts +72 -0
  144. package/dist/core/mcp/client.js +264 -41
  145. package/dist/core/mcp/client.js.map +1 -1
  146. package/dist/core/mcp/content.js +6 -2
  147. package/dist/core/mcp/content.js.map +1 -1
  148. package/dist/core/mcp/store.d.ts +6 -1
  149. package/dist/core/mcp/store.js +12 -1
  150. package/dist/core/mcp/store.js.map +1 -1
  151. package/dist/core/mcp/types.d.ts +18 -0
  152. package/dist/core/model-unavailable.d.ts +14 -0
  153. package/dist/core/model-unavailable.js +23 -0
  154. package/dist/core/model-unavailable.js.map +1 -0
  155. package/dist/core/node-debugger.d.ts +148 -0
  156. package/dist/core/node-debugger.js +642 -0
  157. package/dist/core/node-debugger.js.map +1 -0
  158. package/dist/core/package-threats.d.ts +18 -0
  159. package/dist/core/package-threats.js +168 -0
  160. package/dist/core/package-threats.js.map +1 -0
  161. package/dist/core/persistent-shell.d.ts +58 -6
  162. package/dist/core/persistent-shell.js +331 -49
  163. package/dist/core/persistent-shell.js.map +1 -1
  164. package/dist/core/process-manager.d.ts +14 -0
  165. package/dist/core/process-manager.js +61 -0
  166. package/dist/core/process-manager.js.map +1 -1
  167. package/dist/core/progress/git-xp.js +8 -14
  168. package/dist/core/progress/git-xp.js.map +1 -1
  169. package/dist/core/session-history.d.ts +12 -0
  170. package/dist/core/session-history.js +27 -0
  171. package/dist/core/session-history.js.map +1 -1
  172. package/dist/core/session-manager.d.ts +13 -1
  173. package/dist/core/session-manager.js +38 -18
  174. package/dist/core/session-manager.js.map +1 -1
  175. package/dist/core/session-summary-index.d.ts +37 -0
  176. package/dist/core/session-summary-index.js +172 -0
  177. package/dist/core/session-summary-index.js.map +1 -0
  178. package/dist/core/settings-manager.d.ts +2 -0
  179. package/dist/core/settings-manager.js +10 -0
  180. package/dist/core/settings-manager.js.map +1 -1
  181. package/dist/core/shell-threats-popular-packages.d.ts +11 -0
  182. package/dist/core/shell-threats-popular-packages.js +675 -0
  183. package/dist/core/shell-threats-popular-packages.js.map +1 -0
  184. package/dist/core/shell-threats.d.ts +8 -0
  185. package/dist/core/shell-threats.js +186 -0
  186. package/dist/core/shell-threats.js.map +1 -0
  187. package/dist/core/skills.js +3 -1
  188. package/dist/core/skills.js.map +1 -1
  189. package/dist/core/stream-rules.d.ts +30 -0
  190. package/dist/core/stream-rules.js +151 -0
  191. package/dist/core/stream-rules.js.map +1 -0
  192. package/dist/core/subagent-manager.d.ts +20 -5
  193. package/dist/core/subagent-manager.js +22 -8
  194. package/dist/core/subagent-manager.js.map +1 -1
  195. package/dist/core/subagent-receipt.d.ts +54 -0
  196. package/dist/core/subagent-receipt.js +276 -0
  197. package/dist/core/subagent-receipt.js.map +1 -0
  198. package/dist/core/subagent-turn-record.d.ts +2 -0
  199. package/dist/core/subagent-turn-record.js.map +1 -1
  200. package/dist/core/test-impact.d.ts +73 -0
  201. package/dist/core/test-impact.js +467 -0
  202. package/dist/core/test-impact.js.map +1 -0
  203. package/dist/core/thinking-level.d.ts +1 -1
  204. package/dist/core/thinking-level.js +1 -1
  205. package/dist/core/thinking-level.js.map +1 -1
  206. package/dist/core/verification-gate.d.ts +2 -0
  207. package/dist/core/verification-gate.js +4 -0
  208. package/dist/core/verification-gate.js.map +1 -1
  209. package/dist/core/verification-snapshot.js +3 -5
  210. package/dist/core/verification-snapshot.js.map +1 -1
  211. package/dist/core/workspace-guard.d.ts +18 -7
  212. package/dist/core/workspace-guard.js +227 -60
  213. package/dist/core/workspace-guard.js.map +1 -1
  214. package/dist/interactive.js +2 -1
  215. package/dist/interactive.js.map +1 -1
  216. package/dist/modes/subagent-worker-mode.js +34 -6
  217. package/dist/modes/subagent-worker-mode.js.map +1 -1
  218. package/dist/motion-agent/motion-agent.d.ts +6 -2
  219. package/dist/motion-agent/motion-agent.js +5 -7
  220. package/dist/motion-agent/motion-agent.js.map +1 -1
  221. package/dist/motion-agent/motion-prompt.d.ts +1 -1
  222. package/dist/motion-agent/motion-prompt.js +14 -19
  223. package/dist/motion-agent/motion-prompt.js.map +1 -1
  224. package/dist/motion-agent/motion-review.d.ts +10 -3
  225. package/dist/motion-agent/motion-review.js +15 -7
  226. package/dist/motion-agent/motion-review.js.map +1 -1
  227. package/dist/motion-agent/motion-studio-context.js +1 -1
  228. package/dist/motion-agent/motion-studio-context.js.map +1 -1
  229. package/dist/system-prompt.js +3 -1
  230. package/dist/system-prompt.js.map +1 -1
  231. package/dist/test-support/keep-alive.d.ts +14 -0
  232. package/dist/test-support/keep-alive.js +17 -0
  233. package/dist/test-support/keep-alive.js.map +1 -0
  234. package/dist/tools/ask-user.js +3 -3
  235. package/dist/tools/ask-user.js.map +1 -1
  236. package/dist/tools/bash-read-evidence.d.ts +10 -0
  237. package/dist/tools/bash-read-evidence.js +133 -0
  238. package/dist/tools/bash-read-evidence.js.map +1 -0
  239. package/dist/tools/bash.d.ts +10 -1
  240. package/dist/tools/bash.js +115 -7
  241. package/dist/tools/bash.js.map +1 -1
  242. package/dist/tools/debug.d.ts +54 -0
  243. package/dist/tools/debug.js +233 -0
  244. package/dist/tools/debug.js.map +1 -0
  245. package/dist/tools/edit.js +12 -4
  246. package/dist/tools/edit.js.map +1 -1
  247. package/dist/tools/goals.d.ts +1 -1
  248. package/dist/tools/index.d.ts +19 -2
  249. package/dist/tools/index.js +47 -7
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/prompt-hints.js +2 -0
  252. package/dist/tools/prompt-hints.js.map +1 -1
  253. package/dist/tools/read-tracker.d.ts +5 -0
  254. package/dist/tools/read-tracker.js +19 -9
  255. package/dist/tools/read-tracker.js.map +1 -1
  256. package/dist/tools/read.js +3 -2
  257. package/dist/tools/read.js.map +1 -1
  258. package/dist/tools/skill.js +5 -0
  259. package/dist/tools/skill.js.map +1 -1
  260. package/dist/tools/subagent-control.js +44 -8
  261. package/dist/tools/subagent-control.js.map +1 -1
  262. package/dist/tools/subagent-shared.d.ts +29 -8
  263. package/dist/tools/subagent-shared.js +45 -14
  264. package/dist/tools/subagent-shared.js.map +1 -1
  265. package/dist/tools/subagent.d.ts +8 -2
  266. package/dist/tools/subagent.js +28 -10
  267. package/dist/tools/subagent.js.map +1 -1
  268. package/dist/tools/task-output.js +3 -2
  269. package/dist/tools/task-output.js.map +1 -1
  270. package/dist/tools/task-send.d.ts +1 -1
  271. package/dist/tools/task-send.js +15 -1
  272. package/dist/tools/task-send.js.map +1 -1
  273. package/dist/tools/tool-tiers.d.ts +2 -2
  274. package/dist/tools/tool-tiers.js +3 -2
  275. package/dist/tools/tool-tiers.js.map +1 -1
  276. package/dist/tools/truncate.d.ts +21 -0
  277. package/dist/tools/truncate.js +187 -0
  278. package/dist/tools/truncate.js.map +1 -1
  279. package/dist/tools/ui-adopt.js +2 -0
  280. package/dist/tools/ui-adopt.js.map +1 -1
  281. package/dist/ui/App.d.ts +0 -4
  282. package/dist/ui/App.js +5 -28
  283. package/dist/ui/App.js.map +1 -1
  284. package/dist/ui/components/ActivityIndicator.js +1 -0
  285. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  286. package/dist/ui/hooks/useAgentLoop.d.ts +1 -8
  287. package/dist/ui/hooks/useAgentLoop.js +1 -119
  288. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  289. package/dist/ui/render.d.ts +0 -4
  290. package/dist/ui/render.js +0 -2
  291. package/dist/ui/render.js.map +1 -1
  292. package/dist/utils/git.d.ts +77 -0
  293. package/dist/utils/git.js +285 -21
  294. package/dist/utils/git.js.map +1 -1
  295. package/dist/utils/github-ci.js +2 -1
  296. package/dist/utils/github-ci.js.map +1 -1
  297. package/dist/utils/github.js +11 -9
  298. package/dist/utils/github.js.map +1 -1
  299. package/dist/utils/image.d.ts +14 -0
  300. package/dist/utils/image.js +16 -0
  301. package/dist/utils/image.js.map +1 -1
  302. package/dist/utils/process.d.ts +20 -0
  303. package/dist/utils/process.js +98 -0
  304. package/dist/utils/process.js.map +1 -1
  305. package/package.json +5 -5
  306. package/assets/motion/references/motion-language.md +0 -128
  307. package/assets/motion/skills/video-qa/SKILL.md +0 -89
  308. package/dist/core/ideal-review-subagent.d.ts +0 -56
  309. package/dist/core/ideal-review-subagent.js +0 -112
  310. package/dist/core/ideal-review-subagent.js.map +0 -1
  311. package/dist/core/ideal-review.d.ts +0 -82
  312. package/dist/core/ideal-review.js +0 -242
  313. package/dist/core/ideal-review.js.map +0 -1
  314. package/dist/motion-agent/motion-check-tool.d.ts +0 -35
  315. package/dist/motion-agent/motion-check-tool.js +0 -514
  316. package/dist/motion-agent/motion-check-tool.js.map +0 -1
@@ -1,30 +1,32 @@
1
1
  ---
2
2
  name: shared-language
3
- description: Use when a project's domain vocabulary is fuzzy or drifting, naming decisions keep recurring, or a hard-to-reverse architectural decision needs recording so it is not re-litigated or re-suggested later. Do NOT use for throwaway scripts or projects too small to have recurring vocabulary.
3
+ description: Use when a project's domain vocabulary is fuzzy or drifting (two names for one thing, one name for two), naming decisions keep recurring, or a hard-to-reverse architectural decision needs recording so it is not re-litigated or re-suggested — including mid-build when a term settles or a change contradicts an ADR. Do NOT use for throwaway scripts, projects too small to have recurring vocabulary, interviewing for requirements (clarify), or renames with no vocabulary decision (refactoring).
4
4
  ---
5
5
 
6
6
  # Shared Language
7
7
 
8
- A repo's terms are its compression. When you and the user mean the same thing by "reservation", every prompt, name, and doc gets shorter and sharper — and the agent stops spending thinking tokens re-deriving what a single word encodes. This skill builds and maintains that vocabulary.
8
+ Two artifacts: the glossary (`CONTEXT.md`) and decision records (`docs/adr/`). On entry, read both if they exist, then act on the trigger: term settled → glossary; hard call made → offer ADR; change contradicts an ADR → surface it.
9
9
 
10
10
  ## The glossary — CONTEXT.md
11
11
 
12
- At the repo root. **A glossary and nothing else**: term — a one-to-three-line definition, no implementation details, no history. If a definition names a file path, it has become documentation; move that out.
12
+ At the repo root. **A glossary and nothing else**: `term — 1–3 line definition`, plus `Avoid: <synonyms>` where drift exists. No file paths, implementation, or history.
13
13
 
14
- - Challenge fuzzy terms against it: "the glossary defines *cancellation* as pre-charge; this change reads as post-charge — which is meant?"
15
- - Stress-test a new term with an invented edge case before recording it ("is a no-show a cancellation?").
16
- - Update **inline, the moment a term settles** — never batch glossary edits for "later"; later never comes.
17
- - Name files, functions, variables, and tests with glossary terms verbatim. When code and glossary disagree, one of them is wrong — find out which.
18
- - Create the file lazily on the first settled term, never as an empty template.
19
- - On first creation, add one line to the repo's instruction file (AGENTS.md, or CLAUDE.md if that is what the repo uses): `Read CONTEXT.md before naming anything.` CONTEXT.md is not auto-loaded — the pointer is what makes the glossary ambient in every session.
14
+ - Challenge fuzzy usage against it: "glossary says *cancellation* is pre-charge; this reads post-charge — which?"
15
+ - Test a candidate term with an invented edge case before recording it ("is a no-show a cancellation?").
16
+ - Update the moment a term settles; create the file lazily on the first settled term, never as an empty template.
17
+ - Code, tests, and UI use glossary terms verbatim. Code and glossary disagree → one is wrong; ask which.
18
+ - On creation, add to the repo's instruction file (AGENTS.md, or CLAUDE.md if that is what it uses): `Read CONTEXT.md before naming anything.` CONTEXT.md is not auto-loaded — that pointer is what makes it count in later sessions.
20
19
 
21
20
  ## Decision records — docs/adr/
22
21
 
23
- An ADR earns its file only when a decision is **hard to reverse**, **surprising without context**, and **a real tradeoff** — all three. One file per decision: title, 1–3 sentences of context, the decision, the main rejected alternative and why. Numbered; immutable once accepted — supersede, never edit.
22
+ Only when a decision is **hard to reverse**, **surprising without context**, and **a real tradeoff** — all three. Offer it; don't write unasked.
24
23
 
25
- Read the ADRs before proposing a change that contradicts one: honor it, or surface the conflict to the user. Never silently relitigate a recorded decision, and never re-suggest a rejected alternative without new facts.
24
+ - Follow the repo's existing ADR format. None? Use the MADR 4 minimal shape: `NNNN-title-with-dashes.md` with Context and Problem Statement, Considered Options, Decision Outcome (chosen option + why), Consequences; `status: accepted` in front matter.
25
+ - Immutable once accepted — supersede, never edit: new ADR notes `Supersedes NNNN`; the old one's status becomes `superseded by NNNN` (the only allowed change).
26
+ - Before proposing a change that contradicts an ADR: honour it or raise the conflict. Never re-suggest a rejected option without new facts.
26
27
 
27
- ## Integration
28
+ ## Scaling: one agent or several
28
29
 
29
- - During a clarify session: settled terms go to the glossary; qualifying hard calls get an ADR offer.
30
- - Before naming anything: read the glossary if it exists.
30
+ Main thread only. On a large repo, one `owl` may inventory competing terms across packages (occurrences with file:line); the main thread decides and edits.
31
+
32
+ Sources (accessed 3 October 2026): MADR 4.0.0, latest release (2024-09-17) — https://adr.github.io/madr/; ubiquitous language — https://martinfowler.com/bliki/UbiquitousLanguage.html
@@ -1,30 +1,42 @@
1
1
  ---
2
2
  name: tdd
3
- description: Use when the user asks for test-driven development, red-green-refactor, or "write the test first", or wants a feature built test-first. Do NOT use when tests are a verification step after the build, the project has no suite and the user has not asked for one, or the change is throwaway probe code.
3
+ description: Use when the user asks for test-driven development, red-green-refactor, "write the test first", or wants a feature or bug fix built test-first — including mid-build when they ask to switch to test-first. Do NOT use when tests are a verification step after the build, the project has no suite and the user has not asked for one, the change is throwaway probe code, or the goal is restructuring without behaviour change (refactoring) or finding an unknown cause (root-cause).
4
4
  ---
5
5
 
6
6
  # TDD
7
7
 
8
- Red → green, one slice at a time. This skill is the reference that makes the loop produce tests worth keeping — most of it applies on every cycle, so consult it before and during the loop, not after.
8
+ Red → green, one vertical slice at a time. Before the first test: agree seams (below). Then loop.
9
9
 
10
10
  ## Seams — agree before writing
11
11
 
12
- A **seam** is the public boundary where behavior is observable: an exported function, an HTTP route, a CLI invocation. Tests live at seams; they never reach into internals.
13
-
14
- Before the first test, write down the seams under test and confirm them with the user — which boundaries get tests and which stay untested is a decision, and settling it up front is what keeps effort on critical paths instead of every edge case. No test at an unagreed seam.
12
+ A **seam** is a public boundary where behaviour is observable: exported function, HTTP route, CLI invocation. Tests live at seams, never in internals. List the seams under test and confirm them with the user (one `ask_user` call, recommended set marked). No test at an unagreed seam.
15
13
 
16
14
  ## The loop
17
15
 
18
- 1. **Red.** One failing test at an agreed seam, for the next smallest real behavior. Run it; watch it fail for the right reason.
19
- 2. **Green.** The least code that passes — nothing speculative. Run it.
20
- 3. **Repeat.** The next test responds to what the last cycle taught. Work in vertical slices, never layers: writing all tests then all implementation verifies *imagined* behavior and locks in structure before understanding.
21
- 4. **Refactor outside the loop.** Cleanup happens at a checkpoint after a green cycle or in a review pass — not interleaved guesswork inside red-green.
16
+ 1. **Red.** One failing test at an agreed seam for the next smallest real behaviour. Run it; confirm it fails for the right reason (assertion, not import/syntax error).
17
+ 2. **Green.** Least code that passes. Run the test and the touched suite.
18
+ 3. **Repeat.** The next test follows from what the last cycle taught. Never write all tests first, then all code.
19
+ 4. **Refactor** only on green, as a separate step; rerun tests after.
20
+
21
+ ## Test integrity (agent guard)
22
+
23
+ - **Tests are frozen during green.** Never edit, delete, skip, loosen, or `.only` a test to make it pass. If a test is wrong, stop, say why, and get agreement before changing it.
24
+ - Never special-case test inputs in production code (hard-coded returns for the test's literal).
25
+ - Before claiming done, `git diff` the test files; explain any change made during a green step.
26
+
27
+ ## Test quality
28
+
29
+ - **Expected values from an outside source of truth** — a known literal, worked example, or the spec. Never recompute the expected value the way the code does.
30
+ - **Behaviour, not structure.** A test that breaks under a behaviour-preserving refactor is coupled to internals; rewrite it at the seam.
31
+ - **Mock only external boundaries** (network, clock, filesystem), and only when slow or stateful.
32
+ - **Name like a spec**: "user can check out with an empty cart".
33
+ - **Property tests for rules over many inputs** (parsers, round-trips, invariants) when the project already has a property library (fast-check, Hypothesis, proptest); otherwise a table of examples.
34
+ - **Optional strength check:** if a mutation tool is already installed (Stryker, mutmut, cargo-mutants), run it on the changed module; surviving mutants point to missing assertions. Do not install one unasked. Manual alternative: break one line of the new code, confirm a test goes red, revert.
35
+
36
+ Never claim a cycle green that you did not run.
22
37
 
23
- ## Test quality rules
38
+ ## Scaling: one agent or several
24
39
 
25
- - **Expected values from an outside source of truth** — a known literal, a worked example, the spec. Never recompute the expected value the same way the code does; that test can only agree with itself and can never catch the bug.
26
- - **Behavior, not structure.** Assert on what the interface does. A test that breaks under a refactor with unchanged behavior is coupled to implementation — rewrite it at the seam, never patch it with mocks of internals.
27
- - **Real paths over mocks.** Exercise real code; mock only external boundaries (network, clock, filesystem) and only when they are slow or stateful.
28
- - **Named like a spec.** "user can check out with an empty cart" — the name alone states the capability.
40
+ Main thread only — the loop is sequential by design. Never parallelise red/green steps or let a child edit tests.
29
41
 
30
- The loop inherits the verification gate: never claim a cycle green that you did not run.
42
+ Sources (accessed 3 October 2026): https://code.claude.com/docs/en/best-practices (failing test before the fix); https://stryker-mutator.io/docs/ (killed vs surviving mutants); https://hypothesis.readthedocs.io/
@@ -39,11 +39,12 @@ import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycl
39
39
  import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
40
40
  import { describeRunVerification, describeTurnVerification } from "./core/run-status.js";
41
41
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
42
- import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, getHistoryMessageVisibility, replayMessagesInOrder, restoreUserRow, restoreAssistantTexts, resolveRestoredCommand, autopilotMarkerCopySeed, } from "./core/session-history.js";
42
+ import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, getHistoryMessageVisibility, replayMessagesInOrder, restoreUserRow, restoreAssistantTexts, resolveRestoredCommand, autopilotMarkerCopySeed, extractToolImagePaths, } from "./core/session-history.js";
43
43
  import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
44
44
  import { AuthStorage } from "./core/auth-storage.js";
45
45
  import { advancesPhase, explainPullFailure, isGgufShard, isValidHfRepoId, parseOllamaPullLine, pickGgufQuant, SHARDED_MESSAGE, toHfSearchRow, } from "./hf-pull.js";
46
46
  import { cleanupToolOutputs } from "./tools/overflow.js";
47
+ import { spawnedTasks } from "./tools/subagent-shared.js";
47
48
  import { readCappedBody } from "./utils/http-body.js";
48
49
  import { fetchSubscriptionUsage, SubscriptionUsageError, XIAOMI_CREDITS_KEY, dualAuthProvider, oauthStorageKey, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
49
50
  import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
@@ -70,20 +71,25 @@ import { listSidecarSessions } from "./app-sidecar-sessions.js";
70
71
  import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
71
72
  import { initLogger, log } from "./core/logger.js";
72
73
  import { installTerminationHandlers } from "./core/shutdown.js";
74
+ import { KeepAwake } from "./core/keep-awake.js";
73
75
  import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
74
76
  import { enrichProcessPath } from "./core/shell-path.js";
75
- import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
77
+ import { downscaleForPreview, IMAGE_MEDIA_TYPES, shrinkToFit, validateVisionImage, } from "./utils/image.js";
78
+ import { readFileBounded } from "./tools/operations.js";
76
79
  import { startServeMode } from "./modes/serve-mode.js";
77
80
  import { installSteroids, probeSteroids } from "./core/steroids.js";
78
81
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
79
82
  import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, createElicitationBridge, } from "./core/mcp/index.js";
80
- import { createAskUserBridge } from "./core/ask-user.js";
83
+ import { askSoftDeadlineMs, createAskUserBridge, deliverLateAnswer, } from "./core/ask-user.js";
81
84
  import { createAskUserTool } from "./tools/ask-user.js";
82
85
  import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
83
86
  import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
84
87
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
85
88
  import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
86
89
  import { rebuildFromSessions } from "./core/progress/rebuild.js";
90
+ /** App-wide idle-sleep guard: every window's runs share one OS assertion. The
91
+ * `keepAwake` setting is applied at daemon start and live via /keep-awake. */
92
+ const keepAwake = new KeepAwake();
87
93
  const AUTOMATION_PROVENANCE = {
88
94
  source: "runtime",
89
95
  kind: "automation",
@@ -570,6 +576,8 @@ async function main() {
570
576
  // ~/.ezcoder/debug.log (initLogger truncates on each start).
571
577
  const sidecarLog = path.join(paths.agentDir, "ezcoder-app-sidecar.log");
572
578
  initLogger(sidecarLog);
579
+ // Apply the saved keepAwake setting before any run can acquire the guard.
580
+ keepAwake.setEnabled((await new SettingsManager(paths.settingsFile).load()).keepAwake);
573
581
  // The desktop sidecar previously omitted the stream diagnostic hook used by
574
582
  // the CLI, leaving device-specific provider stalls impossible to distinguish from
575
583
  // event-loop starvation or a broken streaming network path. Keep routine
@@ -908,6 +916,40 @@ async function main() {
908
916
  daemonJson(res, 200, { ok: true });
909
917
  return;
910
918
  }
919
+ // Keep-awake is app-wide (one OS assertion for every window), so its
920
+ // setting lives at the daemon level and applies live to in-flight runs.
921
+ if (method === "GET" && url === "/keep-awake") {
922
+ daemonJson(res, 200, { enabled: keepAwake.isEnabled });
923
+ return;
924
+ }
925
+ if (method === "POST" && url === "/keep-awake") {
926
+ void daemonReadBody(req, res).then(async (raw) => {
927
+ if (raw === null)
928
+ return;
929
+ let enabled;
930
+ try {
931
+ enabled = JSON.parse(raw).enabled;
932
+ }
933
+ catch {
934
+ enabled = undefined;
935
+ }
936
+ if (typeof enabled !== "boolean") {
937
+ daemonJson(res, 400, { error: "enabled must be a boolean" });
938
+ return;
939
+ }
940
+ keepAwake.setEnabled(enabled);
941
+ try {
942
+ const sm = new SettingsManager(paths.settingsFile);
943
+ await sm.load();
944
+ await sm.set("keepAwake", enabled);
945
+ }
946
+ catch (err) {
947
+ log("WARN", "app-sidecar", "failed to persist keepAwake", { err: String(err) });
948
+ }
949
+ daemonJson(res, 200, { enabled });
950
+ });
951
+ return;
952
+ }
911
953
  // Progress is daemon-level so the Home screen can paint before a project
912
954
  // session exists; per-session callers still work through the same endpoint.
913
955
  if (method === "GET" && url === "/progress") {
@@ -955,9 +997,16 @@ async function main() {
955
997
  const shutdown = installTerminationHandlers({
956
998
  scope: "app-sidecar",
957
999
  teardown: async () => {
1000
+ // Before any await: background commands live outside the daemon's process
1001
+ // group, and the app force-kills that group after 3 s. If a slow session
1002
+ // teardown is still pending then, these would never be stopped.
1003
+ for (const c of sessions.values())
1004
+ c.stopBackgroundProcesses();
958
1005
  clearInterval(parentWatch);
959
1006
  // Radio playback is app-wide (one stream across all windows).
960
1007
  stopRadio();
1008
+ // Drop the idle-sleep assertion first: session teardown may hang.
1009
+ keepAwake.dispose();
961
1010
  // Close the ~/.ezcoder progress fs.watch handle.
962
1011
  progress.dispose();
963
1012
  await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
@@ -1312,9 +1361,33 @@ async function createSession(deps, opts) {
1312
1361
  // MCP bridge above: broadcast over SSE, resolved when the webview POSTs
1313
1362
  // /ask/:id. Registered ONLY here — a TUI/headless/subagent run has nobody to
1314
1363
  // answer, so the tool is absent there rather than hanging on a dead channel.
1364
+ //
1365
+ // Soft deadline ("async ask"): past it the tool returns "no answer yet —
1366
+ // proceed on your best guess" and the question STAYS OPEN. A later answer
1367
+ // rides the ordinary user queue: steering into a live run, or the next turn
1368
+ // when idle. The deadline is short when nobody is watching (autopilot, task
1369
+ // run-all, a scheduled prompt).
1370
+ let scheduledRunActive = false;
1315
1371
  const asks = createAskUserBridge({
1316
1372
  broadcast: (prompt) => broadcast("ask_user", prompt),
1317
- onTimeout: (prompt) => log("WARN", "app-sidecar", "ask_user timed out", { id: prompt.id }),
1373
+ timeoutMs: () => askSoftDeadlineMs(autopilot || autopilotActive || taskRunAll || scheduledRunActive),
1374
+ onTimeout: (prompt) => {
1375
+ log("WARN", "app-sidecar", "ask_user deadline passed; agent proceeding", { id: prompt.id });
1376
+ broadcast("ask_user_deferred", { id: prompt.id });
1377
+ },
1378
+ onClosed: (ids) => broadcast("ask_user_closed", { ids }),
1379
+ onLateAnswer: (late) => {
1380
+ log("INFO", "app-sidecar", "ask_user late answer queued", { id: late.prompt.id });
1381
+ deliverLateAnswer(late, {
1382
+ queueMessage: (text) => session.queueMessage(text),
1383
+ isBusy: () => running || runClaim.active || autopilotActive,
1384
+ onQueued: () => broadcast("queued", {
1385
+ count: session.getQueuedCount(),
1386
+ messages: session.listQueuedMessages(),
1387
+ }),
1388
+ startIdleRun: () => void runStrandedQueue(),
1389
+ });
1390
+ },
1318
1391
  });
1319
1392
  const askUserTool = createAskUserTool(asks.park);
1320
1393
  // The session file path to resume (passed by the daemon's POST /session);
@@ -1867,11 +1940,14 @@ async function createSession(deps, opts) {
1867
1940
  if (!d.silent)
1868
1941
  broadcast("retry", { reason: d.reason, attempt: d.attempt, delayMs: d.delayMs });
1869
1942
  });
1943
+ session.eventBus.on("stream_rule_triggered", (d) => broadcast("stream_rule_triggered", { rules: d.rules, source: d.source, toolName: d.toolName }));
1870
1944
  session.eventBus.on("max_turns", (d) => broadcast("max_turns", d));
1871
1945
  // The agent consumed queued steering at a turn boundary. Re-broadcast as the
1872
1946
  // usual `queued` depth update so the webview drops the pending affordance the
1873
1947
  // moment the message lands in the loop, not at run_end.
1874
1948
  session.eventBus.on("queue_drained", (d) => broadcast("queued", { count: d.count, messages: session.listQueuedMessages() }));
1949
+ // A fresh/loaded session must not receive late answers to the old one's questions.
1950
+ session.eventBus.on("session_start", () => asks.closeDeferred());
1875
1951
  session.eventBus.on("tool_call_start", (d) => {
1876
1952
  toolCallNames.set(d.toolCallId, d.name);
1877
1953
  broadcast("tool_call_start", d);
@@ -1931,12 +2007,38 @@ async function createSession(deps, opts) {
1931
2007
  session.eventBus.on("hook", (d) => broadcast("hook", d));
1932
2008
  session.eventBus.on("diagnostics", (d) => broadcast("diagnostics", d));
1933
2009
  // Fires BEFORE the candidate final answer streams. The webview holds assistant
1934
- // text back while armed, so an Ideal review supersedes a draft that was never
2010
+ // text back while armed, so an injected follow-up supersedes a draft that was never
1935
2011
  // painted instead of deleting one the user already started reading.
1936
2012
  session.eventBus.on("hook_armed", (d) => broadcast("hook_armed", d));
1937
2013
  session.eventBus.on("subagent_state", (d) => broadcast("subagent_state", d));
1938
2014
  session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
1939
2015
  session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
2016
+ // Cold-prompt-cache notice: push the fresh TTL anchor + context size whenever
2017
+ // a run or compaction settles. Expiry itself is time-based, so the webview
2018
+ // also re-reads `cacheExpiry` from /state when the user returns or types.
2019
+ for (const ev of ["agent_done", "compaction_end", "model_change"]) {
2020
+ session.eventBus.on(ev, () => broadcast("cache_expiry", session.getCacheExpiryStatus()));
2021
+ }
2022
+ // Keep the computer awake while this window's agent works: owned runs and
2023
+ // autopilot cycles (RunLifecycle state), Nolan replies, and background
2024
+ // sub-agents that outlive their parent run. Released on settle (completed,
2025
+ // failed or cancelled) and on session dispose.
2026
+ let releaseRunAwake = null;
2027
+ let releaseSubagentAwake = null;
2028
+ const activeSubagents = new Set();
2029
+ session.eventBus.on("subagent_state", (d) => {
2030
+ if (d.state === "starting" || d.state === "running")
2031
+ activeSubagents.add(d.agent_id);
2032
+ else
2033
+ activeSubagents.delete(d.agent_id);
2034
+ if (activeSubagents.size > 0) {
2035
+ releaseSubagentAwake ??= keepAwake.acquire("subagent");
2036
+ }
2037
+ else {
2038
+ releaseSubagentAwake?.();
2039
+ releaseSubagentAwake = null;
2040
+ }
2041
+ });
1940
2042
  let running = false;
1941
2043
  // Closes the window between `/prompt` deciding to start a run and `runAgent`
1942
2044
  // flipping `running` — that stretch awaits, so Node yields inside it. See
@@ -1944,6 +2046,13 @@ async function createSession(deps, opts) {
1944
2046
  const runClaim = new RunClaim();
1945
2047
  const runLifecycle = new RunLifecycle((runState) => {
1946
2048
  running = runState !== "idle";
2049
+ if (running) {
2050
+ releaseRunAwake ??= keepAwake.acquire("run");
2051
+ }
2052
+ else {
2053
+ releaseRunAwake?.();
2054
+ releaseRunAwake = null;
2055
+ }
1947
2056
  if (runState === "cancelling")
1948
2057
  broadcast("run_cancelling", { runState });
1949
2058
  },
@@ -1970,10 +2079,8 @@ async function createSession(deps, opts) {
1970
2079
  // Autopilot (auto-review) toggle for THIS window's project. Loaded from
1971
2080
  // ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
1972
2081
  // runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
1973
- // and drives the review→prompt→review loop. Nolan is the sole verification
1974
- // owner in this mode, so suppress the build session's redundant Ideal hook.
2082
+ // and drives the review→prompt→review loop.
1975
2083
  let autopilot = mode === "code" && (await loadAutopilot(cwd));
1976
- session.setIdealReviewSuppressed(autopilot);
1977
2084
  // True while an autopilot review is in flight (used to defer nolanAuto model
1978
2085
  // switches, like nolanRunning does for chat Nolan, and to drive the spinner).
1979
2086
  let autopilotReviewing = false;
@@ -2173,9 +2280,6 @@ async function createSession(deps, opts) {
2173
2280
  // (often >5 min) regardless of the user's global speedProfile pick.
2174
2281
  forceLongCacheRetention: true,
2175
2282
  });
2176
- // Nolan is already the independent autopilot reviewer; recursively running
2177
- // his own Ideal self-review adds latency and can corrupt the verdict shape.
2178
- nolanAgent.setIdealReviewSuppressed(true);
2179
2283
  await nolanAgent.initialize();
2180
2284
  // Keep review text/tools silent; report usage only for whole-task accounting.
2181
2285
  nolanAgent.eventBus.on("turn_end", (d) => {
@@ -2444,7 +2548,6 @@ async function createSession(deps, opts) {
2444
2548
  const generation = runLifecycle.begin(abortOwnedWork).generation;
2445
2549
  pendingCancelDrain = null;
2446
2550
  autopilotActive = true;
2447
- session.setIdealReviewSuppressed(true);
2448
2551
  // Generation captured by the last plan review; acceptPlan re-checks it so
2449
2552
  // a user Accept/Reject landing mid-review always wins.
2450
2553
  let planGenAtReview = -1;
@@ -2562,7 +2665,6 @@ async function createSession(deps, opts) {
2562
2665
  }
2563
2666
  finally {
2564
2667
  autopilotActive = false;
2565
- session.setIdealReviewSuppressed(autopilot);
2566
2668
  finishOwnedGeneration(generation, true, session.getVerificationProblem() ? "unverified" : "completed");
2567
2669
  queueMicrotask(() => void runStrandedQueue());
2568
2670
  }
@@ -2861,6 +2963,7 @@ async function createSession(deps, opts) {
2861
2963
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
2862
2964
  supportsVideo: getModel(st.model)?.supportsVideo ?? false,
2863
2965
  autopilot,
2966
+ cacheExpiry: session.getCacheExpiryStatus(),
2864
2967
  ...nolanStatePayload(),
2865
2968
  ...footerExtras(),
2866
2969
  });
@@ -3340,7 +3443,16 @@ async function createSession(deps, opts) {
3340
3443
  // Pre-index tool results by toolCallId so we can pair tool calls with
3341
3444
  // their results (for sub-agent status + image extraction).
3342
3445
  const toolResultMap = new Map();
3446
+ // Tool name per call, so restore only trusts generate_image's own text
3447
+ // when it reads extra images from disk (an MCP server could fake it).
3448
+ const toolNameById = new Map();
3343
3449
  for (const msg of messages) {
3450
+ if (msg.role === "assistant" && typeof msg.content !== "string") {
3451
+ for (const c of msg.content) {
3452
+ if (c.type === "tool_call")
3453
+ toolNameById.set(c.id, c.name);
3454
+ }
3455
+ }
3344
3456
  if (msg.role !== "tool")
3345
3457
  continue;
3346
3458
  for (const tr of msg.content) {
@@ -3500,31 +3612,43 @@ async function createSession(deps, opts) {
3500
3612
  const imageBlocks = tr.content.filter((c) => c.type === "image");
3501
3613
  if (imageBlocks.length === 0)
3502
3614
  continue;
3503
- // Extract the path from the text block (e.g. "Generated image → /path").
3615
+ // Extract the path from the text block so the restored image
3616
+ // stays clickable (read / screenshot / generate_image formats).
3504
3617
  const textBlock = tr.content.find((c) => c.type === "text" && "text" in c && typeof c.text === "string");
3505
3618
  const textContent = textBlock && textBlock.type === "text" ? textBlock.text : "";
3506
- const pathMatch = textContent.match(/→\s*(\S+)/);
3507
- const imgPath = pathMatch?.[1];
3508
- // Downscale each image for the webview preview.
3619
+ const imgPaths = extractToolImagePaths(textContent);
3620
+ // Downscale each image for the webview preview. Paths pair with
3621
+ // image blocks in order (a tool saves one file per image).
3509
3622
  const toolImages = [];
3623
+ let blockIndex = 0;
3510
3624
  for (const block of imageBlocks) {
3511
3625
  if (block.type !== "image")
3512
3626
  continue;
3513
- try {
3514
- const rawBuf = Buffer.from(block.data, "base64");
3515
- const previewBuf = await downscaleForPreview(rawBuf);
3516
- toolImages.push({
3517
- src: `data:${block.mediaType};base64,${previewBuf.toString("base64")}`,
3518
- path: imgPath,
3519
- });
3520
- }
3521
- catch {
3522
- // Downscale failed — use the raw data.
3523
- toolImages.push({
3524
- src: `data:${block.mediaType};base64,${block.data}`,
3525
- path: imgPath,
3526
- });
3527
- }
3627
+ const imgPath = imgPaths[blockIndex++];
3628
+ const previewBuf = await downscaleForPreview(Buffer.from(block.data, "base64"));
3629
+ toolImages.push({
3630
+ src: `data:${block.mediaType};base64,${previewBuf.toString("base64")}`,
3631
+ path: imgPath,
3632
+ });
3633
+ }
3634
+ // generate_image persists only its first image's pixels; the
3635
+ // rest exist only on disk, so preview them from their files.
3636
+ // Skip silently if a file was moved or deleted since.
3637
+ const extraPaths = toolNameById.get(tr.toolCallId) === "generate_image"
3638
+ ? imgPaths.slice(blockIndex)
3639
+ : [];
3640
+ for (const extraPath of extraPaths) {
3641
+ const mediaType = IMAGE_MEDIA_TYPES[path.extname(extraPath).toLowerCase()];
3642
+ if (!mediaType)
3643
+ continue;
3644
+ const raw = await readFileBounded(extraPath).catch(() => null);
3645
+ if (!raw)
3646
+ continue;
3647
+ const previewBuf = await downscaleForPreview(raw);
3648
+ toolImages.push({
3649
+ src: `data:${mediaType};base64,${previewBuf.toString("base64")}`,
3650
+ path: extraPath,
3651
+ });
3528
3652
  }
3529
3653
  if (toolImages.length > 0) {
3530
3654
  history.push({
@@ -3609,18 +3733,25 @@ async function createSession(deps, opts) {
3609
3733
  if (msg.role === "assistant" && typeof msg.content !== "string") {
3610
3734
  const subagentCalls = msg.content.filter((c) => c.type === "tool_call" && (c.name === "subagent" || c.name === "spawn_agent"));
3611
3735
  if (subagentCalls.length > 0) {
3612
- const agents = subagentCalls.map((c) => {
3736
+ const agents = subagentCalls.flatMap((c) => {
3613
3737
  const result = toolResultMap.get(c.id);
3614
- return {
3615
- agentName: c.name === "spawn_agent" && typeof c.args?.task_name === "string"
3616
- ? c.args.task_name
3617
- : typeof c.args?.agent === "string"
3618
- ? c.args.agent
3619
- : undefined,
3620
- // Async workers are intentionally non-resumable; restored rows are historical.
3621
- status: result?.isError ? "error" : "done",
3622
- toolUseCount: 0,
3623
- };
3738
+ // Async workers are intentionally non-resumable; restored rows are historical.
3739
+ const status = result?.isError ? "error" : "done";
3740
+ if (c.name === "spawn_agent") {
3741
+ // One row per child: a batch call starts several.
3742
+ return spawnedTasks(c.args).map((spawn) => ({
3743
+ agentName: spawn.task_name ?? spawn.agent,
3744
+ status,
3745
+ toolUseCount: 0,
3746
+ }));
3747
+ }
3748
+ return [
3749
+ {
3750
+ agentName: typeof c.args?.agent === "string" ? c.args.agent : undefined,
3751
+ status,
3752
+ toolUseCount: 0,
3753
+ },
3754
+ ];
3624
3755
  });
3625
3756
  history.push({
3626
3757
  role: "assistant",
@@ -3754,6 +3885,8 @@ async function createSession(deps, opts) {
3754
3885
  // Claim the run NOW, synchronously. Everything below this line may
3755
3886
  // yield, and `running` does not flip until runAgent begins.
3756
3887
  claimedStart = runClaim.claim();
3888
+ // A scheduled prompt has nobody watching: short ask deadline.
3889
+ scheduledRunActive = meta?.scheduled === true;
3757
3890
  json(res, 202, { accepted: true });
3758
3891
  // Gate inputs captured around the run: whether this turn is a workflow
3759
3892
  // slash command (attachment prompts skip slash expansion entirely), and
@@ -3852,8 +3985,10 @@ async function createSession(deps, opts) {
3852
3985
  await runStrandedQueue();
3853
3986
  })
3854
3987
  .finally(() => {
3855
- if (claimedStart)
3988
+ if (claimedStart) {
3989
+ scheduledRunActive = false;
3856
3990
  runClaim.release();
3991
+ }
3857
3992
  });
3858
3993
  return;
3859
3994
  }
@@ -3889,6 +4024,7 @@ async function createSession(deps, opts) {
3889
4024
  }
3890
4025
  json(res, 202, { accepted: true });
3891
4026
  nolanRunning = true;
4027
+ const releaseNolanAwake = keepAwake.acquire("nolan");
3892
4028
  broadcast("nolan_run_start", { text });
3893
4029
  try {
3894
4030
  const nolanAgent = await ensureNolanSession();
@@ -3905,6 +4041,7 @@ async function createSession(deps, opts) {
3905
4041
  broadcastError("nolan_error", "nolan run failed", err);
3906
4042
  }
3907
4043
  finally {
4044
+ releaseNolanAwake();
3908
4045
  nolanRunning = false;
3909
4046
  broadcast("nolan_run_end", {});
3910
4047
  const pending = pendingNolanModel;
@@ -3943,9 +4080,6 @@ async function createSession(deps, opts) {
3943
4080
  return;
3944
4081
  }
3945
4082
  autopilot = enabled;
3946
- // A toggle-off during an active cycle takes effect after Nolan finishes;
3947
- // until then, injected build runs must not re-enable Ideal self-review.
3948
- session.setIdealReviewSuppressed(enabled || autopilotActive);
3949
4083
  await saveAutopilot(cwd, enabled);
3950
4084
  log("INFO", "app-sidecar", "autopilot toggled", { enabled: String(enabled) });
3951
4085
  broadcast("autopilot", { autopilot: enabled });
@@ -4471,6 +4605,18 @@ async function createSession(deps, opts) {
4471
4605
  json(res, 200, payload);
4472
4606
  return;
4473
4607
  }
4608
+ if (method === "POST" && url === "/prewarm") {
4609
+ // Best-effort Anthropic cache prewarm on the composer's first keystroke.
4610
+ // Fire-and-forget: AgentSession.prewarm() applies its own gating (provider,
4611
+ // setting, history size, TTL, in-flight) and swallows request errors.
4612
+ if (!running) {
4613
+ void session.prewarm().catch((err) => {
4614
+ log("WARN", "app-sidecar", "prewarm failed", { err: String(err) });
4615
+ });
4616
+ }
4617
+ json(res, 202, { accepted: !running });
4618
+ return;
4619
+ }
4474
4620
  if (method === "POST" && url === "/cancel") {
4475
4621
  void (async () => {
4476
4622
  // Even between task runs, cancellation stops the sweep. Active provider
@@ -5327,6 +5473,10 @@ async function createSession(deps, opts) {
5327
5473
  json(res, 404, { error: "not found" });
5328
5474
  }
5329
5475
  async function dispose() {
5476
+ releaseRunAwake?.();
5477
+ releaseRunAwake = null;
5478
+ releaseSubagentAwake?.();
5479
+ releaseSubagentAwake = null;
5330
5480
  elicitations.cancelAll();
5331
5481
  asks.cancelAll();
5332
5482
  tasksPollStopped = true;
@@ -5350,6 +5500,11 @@ async function createSession(deps, opts) {
5350
5500
  await nolanAutoSession?.dispose().catch(() => { });
5351
5501
  await session.dispose().catch(() => { });
5352
5502
  }
5503
+ function stopBackgroundProcesses() {
5504
+ session.stopBackgroundProcesses();
5505
+ nolanSession?.stopBackgroundProcesses();
5506
+ nolanAutoSession?.stopBackgroundProcesses();
5507
+ }
5353
5508
  return {
5354
5509
  id: opts.id,
5355
5510
  mode,
@@ -5363,6 +5518,7 @@ async function createSession(deps, opts) {
5363
5518
  broadcast,
5364
5519
  handle,
5365
5520
  dispose,
5521
+ stopBackgroundProcesses,
5366
5522
  };
5367
5523
  }
5368
5524
  main().catch(async (err) => {