@cmetech/otto 1.1.1 → 1.2.5

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 (433) hide show
  1. package/dist/coworker/persona-commands.d.ts +1 -0
  2. package/dist/coworker/persona-commands.js +5 -0
  3. package/dist/coworker/persona-commands.test.d.ts +1 -0
  4. package/dist/coworker/persona-commands.test.js +45 -0
  5. package/dist/resources/.managed-resources-content-hash +1 -1
  6. package/dist/resources/extensions/coworker-artifacts/artifacts-command.js +31 -0
  7. package/dist/resources/extensions/coworker-artifacts/artifacts-singleton.js +17 -0
  8. package/dist/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
  9. package/dist/resources/extensions/coworker-artifacts/index.js +125 -0
  10. package/dist/resources/extensions/coworker-artifacts/list-tool.js +27 -0
  11. package/dist/resources/extensions/coworker-artifacts/open-tool.js +25 -0
  12. package/dist/resources/extensions/coworker-memory/extension-manifest.json +13 -0
  13. package/dist/resources/extensions/coworker-memory/index.js +219 -0
  14. package/dist/resources/extensions/coworker-memory/memorize-tool.js +10 -0
  15. package/dist/resources/extensions/coworker-memory/memory-command.js +157 -0
  16. package/dist/resources/extensions/coworker-memory/memory-singleton.js +55 -0
  17. package/dist/resources/extensions/coworker-memory/recall-tool.js +18 -0
  18. package/dist/resources/extensions/coworker-memory/session-hooks.js +45 -0
  19. package/dist/resources/extensions/coworker-scratchpad/attach-banners.js +53 -0
  20. package/dist/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
  21. package/dist/resources/extensions/coworker-scratchpad/format-age.js +9 -0
  22. package/dist/resources/extensions/coworker-scratchpad/helpers.js +38 -0
  23. package/dist/resources/extensions/coworker-scratchpad/index.js +199 -0
  24. package/dist/resources/extensions/coworker-scratchpad/mime-bundle.js +20 -0
  25. package/dist/resources/extensions/coworker-scratchpad/scratchpad-tool.js +118 -0
  26. package/dist/resources/extensions/coworker-scratchpad/session-sidecar.js +60 -0
  27. package/dist/resources/extensions/coworker-scratchpad/sp-command.js +597 -0
  28. package/dist/resources/extensions/coworker-scratchpad/workspace-pointer.js +41 -0
  29. package/dist/resources/extensions/coworker-scratchpad/workspace-root.js +17 -0
  30. package/dist/resources/extensions/coworker-vault/audit-command.js +35 -0
  31. package/dist/resources/extensions/coworker-vault/connect-command.js +42 -0
  32. package/dist/resources/extensions/coworker-vault/datasource-command.js +50 -0
  33. package/dist/resources/extensions/coworker-vault/extension-manifest.json +12 -0
  34. package/dist/resources/extensions/coworker-vault/index.js +171 -0
  35. package/dist/resources/extensions/coworker-vault/test-helpers.js +86 -0
  36. package/dist/resources/extensions/coworker-vault/vault-singleton.js +24 -0
  37. package/dist/resources/extensions/otto/commands/release-notes/_data.js +83 -0
  38. package/dist/resources/extensions/otto/commands/release-notes/command.js +15 -4
  39. package/dist/resources/extensions/otto/index.js +31 -6
  40. package/dist/resources/extensions/shared/coworker-paths.js +8 -0
  41. package/dist/resources/extensions/slash-commands/{audit.js → audit-codebase.js} +4 -4
  42. package/dist/resources/extensions/slash-commands/extension-manifest.json +1 -1
  43. package/dist/resources/extensions/slash-commands/index.js +2 -2
  44. package/dist/resources/extensions/subagent/index.js +8 -1
  45. package/dist/resources/extensions/subagent/launch.js +37 -5
  46. package/dist/resources/extensions/subagent/run-store.js +1 -0
  47. package/dist/resources/extensions/workflow/bootstrap/register-extension.js +2 -0
  48. package/dist/resources/extensions/workflow/bootstrap/register-hooks.js +10 -0
  49. package/dist/resources/extensions/workflow/health-widget-core.js +1 -1
  50. package/dist/resources/extensions/workflow/persona-status.js +87 -0
  51. package/package.json +26 -10
  52. package/packages/contracts/package.json +1 -1
  53. package/packages/coworker-artifacts/dist/artifact-store.d.ts +25 -0
  54. package/packages/coworker-artifacts/dist/artifact-store.js +187 -0
  55. package/packages/coworker-artifacts/dist/dir-snapshot.d.ts +7 -0
  56. package/packages/coworker-artifacts/dist/dir-snapshot.js +54 -0
  57. package/packages/coworker-artifacts/dist/errors.d.ts +18 -0
  58. package/packages/coworker-artifacts/dist/errors.js +37 -0
  59. package/packages/coworker-artifacts/dist/index.d.ts +7 -0
  60. package/packages/coworker-artifacts/dist/index.js +7 -0
  61. package/packages/coworker-artifacts/dist/readme-renderer.d.ts +5 -0
  62. package/packages/coworker-artifacts/dist/readme-renderer.js +47 -0
  63. package/packages/coworker-artifacts/dist/resolve-uri.d.ts +3 -0
  64. package/packages/coworker-artifacts/dist/resolve-uri.js +29 -0
  65. package/packages/coworker-artifacts/dist/slug.d.ts +4 -0
  66. package/packages/coworker-artifacts/dist/slug.js +32 -0
  67. package/packages/coworker-artifacts/dist/types.d.ts +52 -0
  68. package/packages/coworker-artifacts/dist/types.js +1 -0
  69. package/packages/coworker-artifacts/package.json +20 -0
  70. package/packages/coworker-artifacts/src/artifact-store.test.ts +188 -0
  71. package/packages/coworker-artifacts/src/artifact-store.ts +206 -0
  72. package/packages/coworker-artifacts/src/artifacts-integration.test.ts +109 -0
  73. package/packages/coworker-artifacts/src/dir-snapshot.test.ts +71 -0
  74. package/packages/coworker-artifacts/src/dir-snapshot.ts +52 -0
  75. package/packages/coworker-artifacts/src/errors.test.ts +37 -0
  76. package/packages/coworker-artifacts/src/errors.ts +28 -0
  77. package/packages/coworker-artifacts/src/index.test.ts +22 -0
  78. package/packages/coworker-artifacts/src/index.ts +7 -0
  79. package/packages/coworker-artifacts/src/readme-renderer.test.ts +72 -0
  80. package/packages/coworker-artifacts/src/readme-renderer.ts +56 -0
  81. package/packages/coworker-artifacts/src/resolve-uri.test.ts +46 -0
  82. package/packages/coworker-artifacts/src/resolve-uri.ts +29 -0
  83. package/packages/coworker-artifacts/src/slug.test.ts +47 -0
  84. package/packages/coworker-artifacts/src/slug.ts +31 -0
  85. package/packages/coworker-artifacts/src/types.ts +61 -0
  86. package/packages/coworker-artifacts/tsconfig.json +15 -0
  87. package/packages/coworker-artifacts/tsconfig.publish.json +4 -0
  88. package/packages/coworker-memory/dist/context-injection.d.ts +9 -0
  89. package/packages/coworker-memory/dist/context-injection.js +41 -0
  90. package/packages/coworker-memory/dist/errors.d.ts +25 -0
  91. package/packages/coworker-memory/dist/errors.js +51 -0
  92. package/packages/coworker-memory/dist/index.d.ts +12 -0
  93. package/packages/coworker-memory/dist/index.js +12 -0
  94. package/packages/coworker-memory/dist/layer-a-store.d.ts +16 -0
  95. package/packages/coworker-memory/dist/layer-a-store.js +78 -0
  96. package/packages/coworker-memory/dist/local-sqlite-backend.d.ts +28 -0
  97. package/packages/coworker-memory/dist/local-sqlite-backend.js +167 -0
  98. package/packages/coworker-memory/dist/memory-backend.d.ts +14 -0
  99. package/packages/coworker-memory/dist/memory-backend.js +1 -0
  100. package/packages/coworker-memory/dist/memory-recorder.d.ts +50 -0
  101. package/packages/coworker-memory/dist/memory-recorder.js +69 -0
  102. package/packages/coworker-memory/dist/migrations/001-init.sql +38 -0
  103. package/packages/coworker-memory/dist/migrations/002-artifact-kind.sql +50 -0
  104. package/packages/coworker-memory/dist/paste-detector.d.ts +5 -0
  105. package/packages/coworker-memory/dist/paste-detector.js +14 -0
  106. package/packages/coworker-memory/dist/persona-seed.d.ts +10 -0
  107. package/packages/coworker-memory/dist/persona-seed.js +38 -0
  108. package/packages/coworker-memory/dist/recall-formatter.d.ts +2 -0
  109. package/packages/coworker-memory/dist/recall-formatter.js +14 -0
  110. package/packages/coworker-memory/dist/scope-resolver.d.ts +9 -0
  111. package/packages/coworker-memory/dist/scope-resolver.js +10 -0
  112. package/packages/coworker-memory/dist/types.d.ts +51 -0
  113. package/packages/coworker-memory/dist/types.js +2 -0
  114. package/packages/coworker-memory/dist/workspace-id.d.ts +3 -0
  115. package/packages/coworker-memory/dist/workspace-id.js +54 -0
  116. package/packages/coworker-memory/package.json +35 -0
  117. package/packages/coworker-memory/src/activator-integration.test.ts +141 -0
  118. package/packages/coworker-memory/src/context-injection.test.ts +72 -0
  119. package/packages/coworker-memory/src/context-injection.ts +57 -0
  120. package/packages/coworker-memory/src/errors.test.ts +45 -0
  121. package/packages/coworker-memory/src/errors.ts +42 -0
  122. package/packages/coworker-memory/src/index.test.ts +21 -0
  123. package/packages/coworker-memory/src/index.ts +12 -0
  124. package/packages/coworker-memory/src/layer-a-store.test.ts +85 -0
  125. package/packages/coworker-memory/src/layer-a-store.ts +88 -0
  126. package/packages/coworker-memory/src/local-sqlite-backend.test.ts +110 -0
  127. package/packages/coworker-memory/src/local-sqlite-backend.ts +185 -0
  128. package/packages/coworker-memory/src/memory-backend.ts +10 -0
  129. package/packages/coworker-memory/src/memory-integration.test.ts +89 -0
  130. package/packages/coworker-memory/src/memory-recorder.test.ts +101 -0
  131. package/packages/coworker-memory/src/memory-recorder.ts +95 -0
  132. package/packages/coworker-memory/src/migrations/001-init.sql +38 -0
  133. package/packages/coworker-memory/src/migrations/002-artifact-kind.sql +50 -0
  134. package/packages/coworker-memory/src/paste-detector.test.ts +23 -0
  135. package/packages/coworker-memory/src/paste-detector.ts +18 -0
  136. package/packages/coworker-memory/src/persona-seed.test.ts +57 -0
  137. package/packages/coworker-memory/src/persona-seed.ts +46 -0
  138. package/packages/coworker-memory/src/recall-formatter.test.ts +34 -0
  139. package/packages/coworker-memory/src/recall-formatter.ts +15 -0
  140. package/packages/coworker-memory/src/scope-resolver.test.ts +23 -0
  141. package/packages/coworker-memory/src/scope-resolver.ts +18 -0
  142. package/packages/coworker-memory/src/types.ts +61 -0
  143. package/packages/coworker-memory/src/workspace-id.test.ts +48 -0
  144. package/packages/coworker-memory/src/workspace-id.ts +56 -0
  145. package/packages/coworker-memory/tsconfig.json +15 -0
  146. package/packages/coworker-memory/tsconfig.publish.json +4 -0
  147. package/packages/coworker-persona/dist/commands.d.ts +7 -0
  148. package/packages/coworker-persona/dist/commands.js +35 -0
  149. package/packages/coworker-persona/dist/defaults/manifest.yaml +12 -0
  150. package/packages/coworker-persona/dist/defaults/steering/identity.md +3 -0
  151. package/packages/coworker-persona/dist/index.d.ts +3 -0
  152. package/packages/coworker-persona/dist/index.js +3 -0
  153. package/packages/coworker-persona/dist/manifest.d.ts +24 -0
  154. package/packages/coworker-persona/dist/manifest.js +21 -0
  155. package/packages/coworker-persona/dist/registry.d.ts +22 -0
  156. package/packages/coworker-persona/dist/registry.js +142 -0
  157. package/packages/coworker-persona/package.json +28 -0
  158. package/packages/coworker-persona/scripts/copy-defaults.cjs +17 -0
  159. package/packages/coworker-persona/src/commands.ts +47 -0
  160. package/packages/coworker-persona/src/defaults/manifest.yaml +12 -0
  161. package/packages/coworker-persona/src/defaults/steering/identity.md +3 -0
  162. package/packages/coworker-persona/src/index.ts +3 -0
  163. package/packages/coworker-persona/src/manifest.test.ts +67 -0
  164. package/packages/coworker-persona/src/manifest.ts +49 -0
  165. package/packages/coworker-persona/src/registry.test.ts +89 -0
  166. package/packages/coworker-persona/src/registry.ts +147 -0
  167. package/packages/coworker-persona/tsconfig.json +15 -0
  168. package/packages/coworker-persona/tsconfig.publish.json +4 -0
  169. package/packages/coworker-scratchpad/dist/cell-archive.d.ts +39 -0
  170. package/packages/coworker-scratchpad/dist/cell-archive.js +77 -0
  171. package/packages/coworker-scratchpad/dist/cell-tree.d.ts +14 -0
  172. package/packages/coworker-scratchpad/dist/cell-tree.js +72 -0
  173. package/packages/coworker-scratchpad/dist/child-process-runtime.d.ts +129 -0
  174. package/packages/coworker-scratchpad/dist/child-process-runtime.js +427 -0
  175. package/packages/coworker-scratchpad/dist/collector-registry.d.ts +12 -0
  176. package/packages/coworker-scratchpad/dist/collector-registry.js +29 -0
  177. package/packages/coworker-scratchpad/dist/detect-kind.d.ts +3 -0
  178. package/packages/coworker-scratchpad/dist/detect-kind.js +19 -0
  179. package/packages/coworker-scratchpad/dist/file-collector.d.ts +15 -0
  180. package/packages/coworker-scratchpad/dist/file-collector.js +99 -0
  181. package/packages/coworker-scratchpad/dist/index.d.ts +13 -0
  182. package/packages/coworker-scratchpad/dist/index.js +13 -0
  183. package/packages/coworker-scratchpad/dist/kernel-bindings.d.ts +49 -0
  184. package/packages/coworker-scratchpad/dist/kernel-bindings.js +220 -0
  185. package/packages/coworker-scratchpad/dist/kernel-entry.d.ts +1 -0
  186. package/packages/coworker-scratchpad/dist/kernel-entry.js +355 -0
  187. package/packages/coworker-scratchpad/dist/kernel-protocol.d.ts +171 -0
  188. package/packages/coworker-scratchpad/dist/kernel-protocol.js +48 -0
  189. package/packages/coworker-scratchpad/dist/kernel-spawn.d.ts +3 -0
  190. package/packages/coworker-scratchpad/dist/kernel-spawn.js +54 -0
  191. package/packages/coworker-scratchpad/dist/namespace-codec.d.ts +22 -0
  192. package/packages/coworker-scratchpad/dist/namespace-codec.js +61 -0
  193. package/packages/coworker-scratchpad/dist/scratchpad-lock.d.ts +24 -0
  194. package/packages/coworker-scratchpad/dist/scratchpad-lock.js +86 -0
  195. package/packages/coworker-scratchpad/dist/scratchpad-manager.d.ts +193 -0
  196. package/packages/coworker-scratchpad/dist/scratchpad-manager.js +866 -0
  197. package/packages/coworker-scratchpad/dist/staleness-banner.d.ts +12 -0
  198. package/packages/coworker-scratchpad/dist/staleness-banner.js +27 -0
  199. package/packages/coworker-scratchpad/package.json +31 -0
  200. package/packages/coworker-scratchpad/src/cell-archive.test.ts +150 -0
  201. package/packages/coworker-scratchpad/src/cell-archive.ts +97 -0
  202. package/packages/coworker-scratchpad/src/cell-tree.test.ts +105 -0
  203. package/packages/coworker-scratchpad/src/cell-tree.ts +90 -0
  204. package/packages/coworker-scratchpad/src/child-process-runtime.test.ts +413 -0
  205. package/packages/coworker-scratchpad/src/child-process-runtime.ts +493 -0
  206. package/packages/coworker-scratchpad/src/collector-registry.test.ts +69 -0
  207. package/packages/coworker-scratchpad/src/collector-registry.ts +33 -0
  208. package/packages/coworker-scratchpad/src/detect-kind.test.ts +33 -0
  209. package/packages/coworker-scratchpad/src/detect-kind.ts +22 -0
  210. package/packages/coworker-scratchpad/src/file-collector.test.ts +109 -0
  211. package/packages/coworker-scratchpad/src/file-collector.ts +114 -0
  212. package/packages/coworker-scratchpad/src/index.ts +74 -0
  213. package/packages/coworker-scratchpad/src/kernel-bindings.test.ts +188 -0
  214. package/packages/coworker-scratchpad/src/kernel-bindings.ts +279 -0
  215. package/packages/coworker-scratchpad/src/kernel-entry.test.ts +123 -0
  216. package/packages/coworker-scratchpad/src/kernel-entry.ts +390 -0
  217. package/packages/coworker-scratchpad/src/kernel-protocol.test.ts +105 -0
  218. package/packages/coworker-scratchpad/src/kernel-protocol.ts +230 -0
  219. package/packages/coworker-scratchpad/src/kernel-spawn.test.ts +60 -0
  220. package/packages/coworker-scratchpad/src/kernel-spawn.ts +54 -0
  221. package/packages/coworker-scratchpad/src/namespace-codec.test.ts +102 -0
  222. package/packages/coworker-scratchpad/src/namespace-codec.ts +90 -0
  223. package/packages/coworker-scratchpad/src/scratchpad-lock.test.ts +98 -0
  224. package/packages/coworker-scratchpad/src/scratchpad-lock.ts +102 -0
  225. package/packages/coworker-scratchpad/src/scratchpad-manager.test.ts +1343 -0
  226. package/packages/coworker-scratchpad/src/scratchpad-manager.ts +891 -0
  227. package/packages/coworker-scratchpad/src/staleness-banner.test.ts +53 -0
  228. package/packages/coworker-scratchpad/src/staleness-banner.ts +33 -0
  229. package/packages/coworker-scratchpad/src/vault-integration.test.ts +221 -0
  230. package/packages/coworker-scratchpad/tsconfig.json +15 -0
  231. package/packages/coworker-scratchpad/tsconfig.publish.json +4 -0
  232. package/packages/coworker-types/dist/artifacts.d.ts +31 -0
  233. package/packages/coworker-types/dist/artifacts.js +2 -0
  234. package/packages/coworker-types/dist/contracts.d.ts +32 -0
  235. package/packages/coworker-types/dist/contracts.js +1 -0
  236. package/packages/coworker-types/dist/index.d.ts +5 -0
  237. package/packages/coworker-types/dist/index.js +5 -0
  238. package/packages/coworker-types/dist/memory.d.ts +61 -0
  239. package/packages/coworker-types/dist/memory.js +3 -0
  240. package/packages/coworker-types/dist/scratchpad.d.ts +43 -0
  241. package/packages/coworker-types/dist/scratchpad.js +2 -0
  242. package/packages/coworker-types/dist/vault.d.ts +34 -0
  243. package/packages/coworker-types/dist/vault.js +2 -0
  244. package/packages/coworker-types/package.json +24 -0
  245. package/packages/coworker-types/src/artifacts.test.ts +52 -0
  246. package/packages/coworker-types/src/artifacts.ts +35 -0
  247. package/packages/coworker-types/src/contracts.test.ts +43 -0
  248. package/packages/coworker-types/src/contracts.ts +36 -0
  249. package/packages/coworker-types/src/index.ts +5 -0
  250. package/packages/coworker-types/src/memory.test.ts +50 -0
  251. package/packages/coworker-types/src/memory.ts +79 -0
  252. package/packages/coworker-types/src/scratchpad.test.ts +46 -0
  253. package/packages/coworker-types/src/scratchpad.ts +51 -0
  254. package/packages/coworker-types/src/smoke.test.ts +34 -0
  255. package/packages/coworker-types/src/vault.test.ts +49 -0
  256. package/packages/coworker-types/src/vault.ts +40 -0
  257. package/packages/coworker-types/tsconfig.json +15 -0
  258. package/packages/coworker-types/tsconfig.publish.json +4 -0
  259. package/packages/coworker-utils/dist/audit-log.d.ts +34 -0
  260. package/packages/coworker-utils/dist/audit-log.js +88 -0
  261. package/packages/coworker-utils/dist/index.d.ts +6 -0
  262. package/packages/coworker-utils/dist/index.js +6 -0
  263. package/packages/coworker-utils/dist/lease.d.ts +7 -0
  264. package/packages/coworker-utils/dist/lease.js +67 -0
  265. package/packages/coworker-utils/dist/logger.d.ts +13 -0
  266. package/packages/coworker-utils/dist/logger.js +26 -0
  267. package/packages/coworker-utils/dist/migration-runner.d.ts +7 -0
  268. package/packages/coworker-utils/dist/migration-runner.js +36 -0
  269. package/packages/coworker-utils/dist/ndjson-channel.d.ts +3 -0
  270. package/packages/coworker-utils/dist/ndjson-channel.js +38 -0
  271. package/packages/coworker-utils/dist/secret-scanner.d.ts +10 -0
  272. package/packages/coworker-utils/dist/secret-scanner.js +42 -0
  273. package/packages/coworker-utils/package.json +24 -0
  274. package/packages/coworker-utils/src/audit-log.test.ts +140 -0
  275. package/packages/coworker-utils/src/audit-log.ts +107 -0
  276. package/packages/coworker-utils/src/index.ts +6 -0
  277. package/packages/coworker-utils/src/lease.test.ts +64 -0
  278. package/packages/coworker-utils/src/lease.ts +76 -0
  279. package/packages/coworker-utils/src/logger.test.ts +50 -0
  280. package/packages/coworker-utils/src/logger.ts +45 -0
  281. package/packages/coworker-utils/src/migration-runner.test.ts +65 -0
  282. package/packages/coworker-utils/src/migration-runner.ts +50 -0
  283. package/packages/coworker-utils/src/ndjson-channel.test.ts +76 -0
  284. package/packages/coworker-utils/src/ndjson-channel.ts +41 -0
  285. package/packages/coworker-utils/src/secret-scanner.test.ts +61 -0
  286. package/packages/coworker-utils/src/secret-scanner.ts +56 -0
  287. package/packages/coworker-utils/tsconfig.json +15 -0
  288. package/packages/coworker-utils/tsconfig.publish.json +4 -0
  289. package/packages/coworker-vault/dist/data-vault.d.ts +41 -0
  290. package/packages/coworker-vault/dist/data-vault.js +223 -0
  291. package/packages/coworker-vault/dist/engine-registry.d.ts +34 -0
  292. package/packages/coworker-vault/dist/engine-registry.js +90 -0
  293. package/packages/coworker-vault/dist/engines/jira.yaml +17 -0
  294. package/packages/coworker-vault/dist/errors.d.ts +28 -0
  295. package/packages/coworker-vault/dist/errors.js +57 -0
  296. package/packages/coworker-vault/dist/index.d.ts +6 -0
  297. package/packages/coworker-vault/dist/index.js +6 -0
  298. package/packages/coworker-vault/dist/injector.d.ts +19 -0
  299. package/packages/coworker-vault/dist/injector.js +77 -0
  300. package/packages/coworker-vault/dist/types.d.ts +28 -0
  301. package/packages/coworker-vault/dist/types.js +1 -0
  302. package/packages/coworker-vault/dist/vault-keep.d.ts +4 -0
  303. package/packages/coworker-vault/dist/vault-keep.js +21 -0
  304. package/packages/coworker-vault/package.json +29 -0
  305. package/packages/coworker-vault/src/data-vault.test.ts +199 -0
  306. package/packages/coworker-vault/src/data-vault.ts +257 -0
  307. package/packages/coworker-vault/src/engine-registry.test.ts +120 -0
  308. package/packages/coworker-vault/src/engine-registry.ts +107 -0
  309. package/packages/coworker-vault/src/engines/jira.yaml +17 -0
  310. package/packages/coworker-vault/src/errors.test.ts +58 -0
  311. package/packages/coworker-vault/src/errors.ts +50 -0
  312. package/packages/coworker-vault/src/index.test.ts +24 -0
  313. package/packages/coworker-vault/src/index.ts +6 -0
  314. package/packages/coworker-vault/src/injector.test.ts +109 -0
  315. package/packages/coworker-vault/src/injector.ts +98 -0
  316. package/packages/coworker-vault/src/types.ts +33 -0
  317. package/packages/coworker-vault/src/vault-keep.test.ts +49 -0
  318. package/packages/coworker-vault/src/vault-keep.ts +31 -0
  319. package/packages/coworker-vault/tsconfig.json +15 -0
  320. package/packages/coworker-vault/tsconfig.publish.json +4 -0
  321. package/packages/daemon/package.json +3 -3
  322. package/packages/mcp-server/package.json +3 -3
  323. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  324. package/packages/native/package.json +1 -1
  325. package/packages/native/tsconfig.tsbuildinfo +1 -1
  326. package/packages/pi-agent-core/package.json +1 -1
  327. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  328. package/packages/pi-ai/package.json +1 -1
  329. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  330. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +6 -1
  331. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  332. package/packages/pi-coding-agent/dist/core/extensions/runner.js +22 -3
  333. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  334. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +11 -0
  335. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  336. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts +47 -0
  337. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts.map +1 -0
  338. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js +107 -0
  339. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js.map +1 -0
  340. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts +19 -0
  341. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts.map +1 -0
  342. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js +121 -0
  343. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js.map +1 -0
  344. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  345. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +17 -1
  346. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  347. package/packages/pi-coding-agent/package.json +2 -2
  348. package/packages/pi-coding-agent/src/core/extensions/runner.ts +22 -3
  349. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +11 -0
  350. package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.regression.test.ts +129 -0
  351. package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.ts +117 -0
  352. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +18 -1
  353. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  354. package/packages/pi-tui/package.json +1 -1
  355. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  356. package/packages/rpc-client/package.json +2 -2
  357. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  358. package/pkg/package.json +1 -1
  359. package/scripts/install.js +6 -5
  360. package/src/resources/extensions/coworker-artifacts/artifacts-command.test.ts +54 -0
  361. package/src/resources/extensions/coworker-artifacts/artifacts-command.ts +43 -0
  362. package/src/resources/extensions/coworker-artifacts/artifacts-singleton.test.ts +25 -0
  363. package/src/resources/extensions/coworker-artifacts/artifacts-singleton.ts +29 -0
  364. package/src/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
  365. package/src/resources/extensions/coworker-artifacts/index.test.ts +46 -0
  366. package/src/resources/extensions/coworker-artifacts/index.ts +154 -0
  367. package/src/resources/extensions/coworker-artifacts/list-tool.test.ts +29 -0
  368. package/src/resources/extensions/coworker-artifacts/list-tool.ts +53 -0
  369. package/src/resources/extensions/coworker-artifacts/open-tool.test.ts +30 -0
  370. package/src/resources/extensions/coworker-artifacts/open-tool.ts +43 -0
  371. package/src/resources/extensions/coworker-memory/extension-manifest.json +13 -0
  372. package/src/resources/extensions/coworker-memory/index.test.ts +137 -0
  373. package/src/resources/extensions/coworker-memory/index.ts +257 -0
  374. package/src/resources/extensions/coworker-memory/memorize-tool.test.ts +41 -0
  375. package/src/resources/extensions/coworker-memory/memorize-tool.ts +20 -0
  376. package/src/resources/extensions/coworker-memory/memory-command.test.ts +134 -0
  377. package/src/resources/extensions/coworker-memory/memory-command.ts +131 -0
  378. package/src/resources/extensions/coworker-memory/memory-singleton.test.ts +41 -0
  379. package/src/resources/extensions/coworker-memory/memory-singleton.ts +89 -0
  380. package/src/resources/extensions/coworker-memory/recall-tool.test.ts +50 -0
  381. package/src/resources/extensions/coworker-memory/recall-tool.ts +35 -0
  382. package/src/resources/extensions/coworker-memory/session-hooks.test.ts +77 -0
  383. package/src/resources/extensions/coworker-memory/session-hooks.ts +61 -0
  384. package/src/resources/extensions/coworker-scratchpad/attach-banners.test.ts +124 -0
  385. package/src/resources/extensions/coworker-scratchpad/attach-banners.ts +67 -0
  386. package/src/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
  387. package/src/resources/extensions/coworker-scratchpad/format-age.test.ts +30 -0
  388. package/src/resources/extensions/coworker-scratchpad/format-age.ts +6 -0
  389. package/src/resources/extensions/coworker-scratchpad/helpers.test.ts +93 -0
  390. package/src/resources/extensions/coworker-scratchpad/helpers.ts +42 -0
  391. package/src/resources/extensions/coworker-scratchpad/index.test.ts +514 -0
  392. package/src/resources/extensions/coworker-scratchpad/index.ts +207 -0
  393. package/src/resources/extensions/coworker-scratchpad/mime-bundle.test.ts +61 -0
  394. package/src/resources/extensions/coworker-scratchpad/mime-bundle.ts +23 -0
  395. package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.test.ts +137 -0
  396. package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.ts +165 -0
  397. package/src/resources/extensions/coworker-scratchpad/session-sidecar.test.ts +133 -0
  398. package/src/resources/extensions/coworker-scratchpad/session-sidecar.ts +68 -0
  399. package/src/resources/extensions/coworker-scratchpad/sp-command.test.ts +836 -0
  400. package/src/resources/extensions/coworker-scratchpad/sp-command.ts +602 -0
  401. package/src/resources/extensions/coworker-scratchpad/workspace-pointer.test.ts +74 -0
  402. package/src/resources/extensions/coworker-scratchpad/workspace-pointer.ts +55 -0
  403. package/src/resources/extensions/coworker-scratchpad/workspace-root.test.ts +51 -0
  404. package/src/resources/extensions/coworker-scratchpad/workspace-root.ts +16 -0
  405. package/src/resources/extensions/coworker-vault/audit-command.test.ts +109 -0
  406. package/src/resources/extensions/coworker-vault/audit-command.ts +56 -0
  407. package/src/resources/extensions/coworker-vault/connect-command.test.ts +103 -0
  408. package/src/resources/extensions/coworker-vault/connect-command.ts +69 -0
  409. package/src/resources/extensions/coworker-vault/datasource-command.test.ts +80 -0
  410. package/src/resources/extensions/coworker-vault/datasource-command.ts +81 -0
  411. package/src/resources/extensions/coworker-vault/extension-manifest.json +12 -0
  412. package/src/resources/extensions/coworker-vault/index.test.ts +82 -0
  413. package/src/resources/extensions/coworker-vault/index.ts +181 -0
  414. package/src/resources/extensions/coworker-vault/test-helpers.ts +120 -0
  415. package/src/resources/extensions/coworker-vault/vault-singleton.test.ts +27 -0
  416. package/src/resources/extensions/coworker-vault/vault-singleton.ts +40 -0
  417. package/src/resources/extensions/otto/commands/release-notes/_data.ts +97 -0
  418. package/src/resources/extensions/otto/commands/release-notes/command.ts +16 -3
  419. package/src/resources/extensions/otto/index.ts +29 -6
  420. package/src/resources/extensions/shared/coworker-paths.test.ts +40 -0
  421. package/src/resources/extensions/shared/coworker-paths.ts +10 -0
  422. package/src/resources/extensions/slash-commands/{audit.ts → audit-codebase.ts} +4 -4
  423. package/src/resources/extensions/slash-commands/extension-manifest.json +1 -1
  424. package/src/resources/extensions/slash-commands/index.ts +2 -2
  425. package/src/resources/extensions/subagent/index.ts +9 -0
  426. package/src/resources/extensions/subagent/launch.test.ts +97 -0
  427. package/src/resources/extensions/subagent/launch.ts +42 -5
  428. package/src/resources/extensions/subagent/run-store.ts +3 -1
  429. package/src/resources/extensions/workflow/bootstrap/register-extension.ts +2 -0
  430. package/src/resources/extensions/workflow/bootstrap/register-hooks.ts +10 -0
  431. package/src/resources/extensions/workflow/health-widget-core.ts +1 -1
  432. package/src/resources/extensions/workflow/persona-status.ts +109 -0
  433. package/src/resources/extensions/workflow/tests/auto-recovery.test.ts +34 -0
@@ -0,0 +1,207 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
+ import type { ExtensionAPI, ExtensionContext } from '@otto/pi-coding-agent';
4
+ import { ScratchpadManager, type DataLoadDrawer, type ArtifactCreateDrawer } from '@otto/coworker-scratchpad';
5
+ import { getScratchpadsRoot } from '../shared/coworker-paths.js';
6
+ import { getMemoryRecorder } from '../coworker-memory/index.js';
7
+ import { getArtifactStore } from '../coworker-artifacts/index.js';
8
+ import { registerSpCommand } from './sp-command.js';
9
+ import { registerScratchpadTool } from './scratchpad-tool.js';
10
+ import { sessionSidecarPath, readSessionSidecar, deleteSessionSidecar, sweepStaleSidecars } from './session-sidecar.js';
11
+ import { detectWorkspaceRoot } from './workspace-root.js';
12
+ import { workspaceHash, workspacePointerPath, readWorkspacePointer, isPointerFresh } from './workspace-pointer.js';
13
+ import { formatRelativeAge } from './format-age.js';
14
+
15
+ function deriveSessionId(ctx: ExtensionContext): string {
16
+ const file = ctx.sessionManager.getSessionFile() as string | undefined;
17
+ if (!file) return 'default';
18
+ const base = basename(file);
19
+ return base.endsWith('.jsonl') ? base.slice(0, -6) : base;
20
+ }
21
+
22
+ /**
23
+ * Computes restore precedence with sidecar-fallback. Returns the scratchpad to restore
24
+ * (if any) and the notice string to surface to the user. Does NOT call ctx.ui.notify —
25
+ * the caller does.
26
+ *
27
+ * Side-effect: deletes a broken sidecar (one whose referenced scratchpad no longer
28
+ * exists) so subsequent fresh sessions don't repeatedly re-check it. Not a pure
29
+ * function — a cleanup-closure refactor was considered and rejected as over-engineering
30
+ * for a single in-process call site.
31
+ *
32
+ * Precedence:
33
+ * (a) per-session sidecar — wins when its scratchpad still exists on disk.
34
+ * (b) broken sidecar — silently deleted, falls through to (c).
35
+ * (c) workspace pointer — fresh (≤ 7d) AND its scratchpad still exists on disk.
36
+ * (d) no restore.
37
+ */
38
+ export function tryRestoreCurrentName(
39
+ root: string,
40
+ sessionId: string,
41
+ cwd: string,
42
+ now: number,
43
+ ): { name: string | null; notice: string | null } {
44
+ // (a) Sidecar restore
45
+ const sidecarPath = sessionSidecarPath(root, sessionId);
46
+ const sidecar = readSessionSidecar(sidecarPath);
47
+ if (sidecar) {
48
+ const meta = join(root, sidecar.current_name, 'meta.json');
49
+ if (existsSync(meta)) {
50
+ return { name: sidecar.current_name, notice: `attached to ${sidecar.current_name} (restored)` };
51
+ }
52
+ deleteSessionSidecar(sidecarPath); // broken sidecar — clean up silently and fall through
53
+ }
54
+
55
+ // (b) Workspace-pointer fallback
56
+ const wsRoot = detectWorkspaceRoot(cwd);
57
+ const ptr = readWorkspacePointer(workspacePointerPath(root, workspaceHash(wsRoot)));
58
+ if (ptr && isPointerFresh(ptr, now)) {
59
+ const meta = join(root, ptr.last_current_name, 'meta.json');
60
+ if (existsSync(meta)) {
61
+ const rel = formatRelativeAge(now - Date.parse(ptr.last_attached_at));
62
+ return {
63
+ name: ptr.last_current_name,
64
+ notice: `attached to ${ptr.last_current_name} (from workspace, last used ${rel})`,
65
+ };
66
+ }
67
+ }
68
+
69
+ // (c) No restore
70
+ return { name: null, notice: null };
71
+ }
72
+
73
+ const SUBAGENT_NAME_REGEX = /^subagent-[a-z0-9-]+$/;
74
+
75
+ /**
76
+ * Phase 4.5: When the OTTO_SUBAGENT_SCRATCHPAD env var is set, force-attach
77
+ * to the named scratchpad (creating it if needed) BEFORE the sidecar/pointer
78
+ * restore path runs. Idempotent: re-running with same name attaches without
79
+ * overwriting existing state.
80
+ */
81
+ export function forceSubagentAttach(name: string, scratchpadsRoot: string): { ok: true } | { ok: false; reason: string } {
82
+ if (!SUBAGENT_NAME_REGEX.test(name) || name.length > 80) {
83
+ return { ok: false, reason: `invalid subagent scratchpad name: ${name}` };
84
+ }
85
+ const dir = join(scratchpadsRoot, name);
86
+ try {
87
+ if (!existsSync(dir)) {
88
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
89
+ }
90
+ const metaPath = join(dir, 'meta.json');
91
+ if (!existsSync(metaPath)) {
92
+ const meta = {
93
+ name,
94
+ schema_version: 1,
95
+ created_at: new Date().toISOString(),
96
+ source: 'subagent-dispatch',
97
+ };
98
+ writeFileSync(metaPath, JSON.stringify(meta, null, 2), { mode: 0o600 });
99
+ }
100
+ return { ok: true };
101
+ } catch (err) {
102
+ return { ok: false, reason: (err as Error).message };
103
+ }
104
+ }
105
+
106
+ export default function coworkerScratchpadExtension(pi: ExtensionAPI): void {
107
+ let manager: ScratchpadManager | null = null;
108
+ let workspaceCwd: string | null = null;
109
+ let sessionId: string | null = null;
110
+ let currentName: string | null = null;
111
+ const root = getScratchpadsRoot();
112
+
113
+ const getManager = (): ScratchpadManager => {
114
+ if (!manager) {
115
+ if (!workspaceCwd) throw new Error('scratchpad: manager requested before session_start');
116
+ manager = new ScratchpadManager({
117
+ workspace: workspaceCwd,
118
+ root,
119
+ sessionId: sessionId ?? 'default',
120
+ // Phase 4 Task 11/12: lazy getter for the artifact store — returns null
121
+ // before artifacts session_start or after session_shutdown. Cross-pillar
122
+ // wiring is one-way (scratchpad → artifacts), so activator load order
123
+ // is indifferent.
124
+ getArtifactStore,
125
+ // Phase 3.1 Task 4: production hop into memory. `getMemoryRecorder()` is
126
+ // called lazily on each event so activator load order is indifferent —
127
+ // returns null before memory session_start or after session_shutdown,
128
+ // which is the correct no-op signal. Failures are swallowed silently
129
+ // (spec §3.8): file loads are frequent and a memory backend hiccup must
130
+ // not break the kernel runtime; surfaces in /audit instead.
131
+ onDataLoad: (drawer: DataLoadDrawer, scratchpadName: string): void => {
132
+ const recorder = getMemoryRecorder();
133
+ if (!recorder) return;
134
+ void recorder.recordFileLoad({
135
+ scratchpadName,
136
+ collector: drawer.collector,
137
+ uri: drawer.uri,
138
+ bytes: drawer.bytes ?? 0,
139
+ rows_loaded: drawer.rows_loaded ?? undefined,
140
+ schema: drawer.schema ?? undefined,
141
+ turnId: '',
142
+ }).catch(() => { /* silent: file loads are frequent; failures visible in /audit */ });
143
+ },
144
+ // Phase 4 Task 12: production hop into memory for artifact-create events.
145
+ // Mirrors the onDataLoad pattern: lazy recorder lookup, silent failures
146
+ // (spec §3.9). drawer.artifact_kind is the actual artifact kind
147
+ // (e.g. 'report'); drawer.kind is the discriminator literal 'artifact'.
148
+ onArtifactCreate: (drawer: ArtifactCreateDrawer, scratchpadName: string): void => {
149
+ const recorder = getMemoryRecorder();
150
+ if (!recorder) return;
151
+ void recorder.recordArtifact({
152
+ scratchpadName,
153
+ slug: drawer.slug,
154
+ kind: drawer.artifact_kind,
155
+ uri: drawer.uri,
156
+ turnId: '',
157
+ }).catch(() => { /* silent per spec §3.9 */ });
158
+ },
159
+ });
160
+ }
161
+ return manager;
162
+ };
163
+ const getCurrentName = (): string | null => currentName;
164
+ const setCurrentName = (n: string | null): void => { currentName = n; };
165
+ const rootDir = (): string => root;
166
+ const getSessionId = (): string => sessionId ?? 'default';
167
+ const getWorkspaceCwd = (): string => workspaceCwd ?? process.cwd();
168
+
169
+ registerSpCommand(pi, { getManager, getCurrentName, setCurrentName, rootDir, getSessionId, getWorkspaceCwd });
170
+ registerScratchpadTool(pi, { getManager, getCurrentName, setCurrentName, rootDir });
171
+
172
+ pi.on('session_start', async (_event, ctx) => {
173
+ workspaceCwd = ctx.cwd;
174
+ sessionId = deriveSessionId(ctx);
175
+
176
+ // Phase 4.5: force-attach to subagent scratchpad if OTTO_SUBAGENT_SCRATCHPAD is set.
177
+ const subagentName = process.env.OTTO_SUBAGENT_SCRATCHPAD;
178
+ if (subagentName) {
179
+ const result = forceSubagentAttach(subagentName, root);
180
+ if (result.ok) {
181
+ currentName = subagentName;
182
+ ctx.ui.notify(`attached to ${subagentName} (subagent dispatch)`, 'info');
183
+ try { sweepStaleSidecars(root, sessionId, Date.now()); } catch { /* silent */ }
184
+ return;
185
+ }
186
+ ctx.ui.notify(`subagent scratchpad attach failed: ${result.reason}; continuing without`, 'warning');
187
+ // fall through to normal restore
188
+ }
189
+
190
+ const restore = tryRestoreCurrentName(root, sessionId, ctx.cwd ?? process.cwd(), Date.now());
191
+ if (restore.name) {
192
+ currentName = restore.name;
193
+ ctx.ui.notify(restore.notice!, 'info');
194
+ }
195
+ // When neither sidecar nor fresh pointer resolves to an existing scratchpad,
196
+ // we stay silent — the user gets a clean session_start with no noise.
197
+ try { sweepStaleSidecars(root, sessionId, Date.now()); } catch { /* sweep failures are silent */ }
198
+ });
199
+
200
+ pi.on('session_shutdown', async () => {
201
+ if (manager) {
202
+ await manager.disposeAll();
203
+ manager = null;
204
+ }
205
+ // Sidecar deliberately NOT deleted here — survives so /resume restores.
206
+ });
207
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { deriveMimeBundle } from './mime-bundle.js';
4
+
5
+ describe('deriveMimeBundle', () => {
6
+ it('returns an empty bundle when value is undefined and stdout is empty', () => {
7
+ assert.deepEqual(deriveMimeBundle(undefined, ''), {});
8
+ });
9
+
10
+ it('returns only text/plain when stdout is non-empty and value is undefined', () => {
11
+ const b = deriveMimeBundle(undefined, 'hello\nworld');
12
+ assert.deepEqual(b, { 'text/plain': 'hello\nworld' });
13
+ });
14
+
15
+ it('returns only application/json when value is a number and stdout is empty', () => {
16
+ assert.deepEqual(deriveMimeBundle(42, ''), { 'application/json': 42 });
17
+ });
18
+
19
+ it('drops application/json when value is null', () => {
20
+ assert.deepEqual(deriveMimeBundle(null, 'log'), { 'text/plain': 'log' });
21
+ });
22
+
23
+ it('returns text/plain AND application/json when both present', () => {
24
+ assert.deepEqual(deriveMimeBundle({ rows: 3 }, 'loaded'), {
25
+ 'text/plain': 'loaded',
26
+ 'application/json': { rows: 3 },
27
+ });
28
+ });
29
+
30
+ it('adds text/markdown when value is a string starting with # (heading)', () => {
31
+ const b = deriveMimeBundle('# Title\n\nbody', '');
32
+ assert.equal(b['application/json'], '# Title\n\nbody');
33
+ assert.equal(b['text/markdown'], '# Title\n\nbody');
34
+ });
35
+
36
+ it('adds text/markdown when value is a string starting with | (table)', () => {
37
+ const md = '| a | b |\n|---|---|\n| 1 | 2 |';
38
+ const b = deriveMimeBundle(md, '');
39
+ assert.equal(b['text/markdown'], md);
40
+ });
41
+
42
+ it('adds text/markdown when value contains a GFM table separator row mid-string', () => {
43
+ const md = 'preamble\n\n| a | b |\n|---|---|\n| 1 | 2 |\n';
44
+ const b = deriveMimeBundle(md, '');
45
+ assert.equal(b['text/markdown'], md);
46
+ });
47
+
48
+ it('does NOT add text/markdown for plain prose strings', () => {
49
+ const b = deriveMimeBundle('just a sentence', '');
50
+ assert.equal(b['application/json'], 'just a sentence');
51
+ assert.equal(b['text/markdown'], undefined);
52
+ });
53
+
54
+ it('keeps the value in application/json when also tagged markdown', () => {
55
+ // A future LLM consumer that always reads application/json must not lose data
56
+ // just because the string looked markdown-shaped.
57
+ const b = deriveMimeBundle('# h', '');
58
+ assert.equal(b['application/json'], '# h');
59
+ assert.equal(b['text/markdown'], '# h');
60
+ });
61
+ });
@@ -0,0 +1,23 @@
1
+ export interface MimeBundle {
2
+ 'text/plain'?: string;
3
+ 'application/json'?: unknown;
4
+ 'text/markdown'?: string;
5
+ }
6
+
7
+ export function deriveMimeBundle(value: unknown, stdout: string): MimeBundle {
8
+ const bundle: MimeBundle = {};
9
+ if (stdout.length > 0) bundle['text/plain'] = stdout;
10
+ if (value !== undefined && value !== null) bundle['application/json'] = value;
11
+ if (typeof value === 'string' && looksLikeMarkdown(value)) {
12
+ bundle['text/markdown'] = value;
13
+ }
14
+ return bundle;
15
+ }
16
+
17
+ function looksLikeMarkdown(s: string): boolean {
18
+ const trimmed = s.trimStart();
19
+ if (trimmed.startsWith('#') || trimmed.startsWith('|')) return true;
20
+ // GFM table separator row: a line that is just |---|---|...
21
+ if (/\n\s*\|[-:|\s]+\|\s*\n/.test(s)) return true;
22
+ return false;
23
+ }
@@ -0,0 +1,137 @@
1
+ import { describe, it, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { registerScratchpadTool } from './scratchpad-tool.js';
7
+
8
+ interface StubMgr {
9
+ runCell(name: string, code: string): Promise<{ value: unknown; stdout: string }>;
10
+ calls: Array<['runCell', string, string]>;
11
+ nextResult: { value: unknown; stdout: string } | { throw: Error };
12
+ }
13
+ function makeStub(): StubMgr {
14
+ const calls: StubMgr['calls'] = [];
15
+ return {
16
+ calls,
17
+ nextResult: { value: undefined, stdout: '' },
18
+ async runCell(name, code) {
19
+ calls.push(['runCell', name, code]);
20
+ if ('throw' in this.nextResult) throw this.nextResult.throw;
21
+ return this.nextResult;
22
+ },
23
+ };
24
+ }
25
+
26
+ interface FakePi {
27
+ tools: Map<string, { name: string; execute: (id: string, params: unknown, signal: unknown, onUpdate: unknown, ctx: unknown) => Promise<{ details: unknown }> }>;
28
+ registerTool(opts: { name: string; execute: (id: string, params: unknown, signal: unknown, onUpdate: unknown, ctx: unknown) => Promise<{ details: unknown }>; [k: string]: unknown }): void;
29
+ }
30
+ function makePi(): FakePi {
31
+ const tools = new Map();
32
+ return { tools, registerTool(opts) { tools.set(opts.name, opts); } };
33
+ }
34
+
35
+ let root: string;
36
+
37
+ describe('scratchpad-tool dispatch (stubbed manager)', () => {
38
+ beforeEach(async () => {
39
+ root = await mkdtemp(join(tmpdir(), 'stool-root-'));
40
+ });
41
+ afterEach(async () => {
42
+ await rm(root, { recursive: true, force: true });
43
+ });
44
+
45
+ function wire(currentName: string | null = null): { pi: FakePi; mgr: StubMgr; current: { name: string | null } } {
46
+ const pi = makePi();
47
+ const mgr = makeStub();
48
+ const current = { name: currentName };
49
+ registerScratchpadTool(pi as unknown as Parameters<typeof registerScratchpadTool>[0], {
50
+ getManager: () => mgr as unknown as Parameters<typeof registerScratchpadTool>[1]['getManager'] extends () => infer T ? T : never,
51
+ getCurrentName: () => current.name,
52
+ setCurrentName: (n) => { current.name = n; },
53
+ rootDir: () => root,
54
+ } as Parameters<typeof registerScratchpadTool>[1]);
55
+ return { pi, mgr, current };
56
+ }
57
+
58
+ it('exec without name uses currentName (or auto-default)', async () => {
59
+ const { pi, mgr, current } = wire(null);
60
+ mgr.nextResult = { value: 42, stdout: '' };
61
+ const res = (await pi.tools.get('cw_scratchpad')!.execute('', { action: 'exec', code: 'return 42;' }, undefined, undefined, {})).details as { ok: boolean; mime: Record<string, unknown> };
62
+ assert.equal(current.name, 'default');
63
+ assert.deepEqual(mgr.calls, [['runCell', 'default', 'return 42;']]);
64
+ assert.equal(res.ok, true);
65
+ assert.equal(res.mime['application/json'], 42);
66
+ });
67
+
68
+ it('exec with explicit name does NOT change currentName', async () => {
69
+ const { pi, mgr, current } = wire('p1');
70
+ mgr.nextResult = { value: 'ok', stdout: '' };
71
+ await pi.tools.get('cw_scratchpad')!.execute('', { action: 'exec', name: 'side', code: 'return "ok";' }, undefined, undefined, {}).then(r => r.details);
72
+ assert.deepEqual(mgr.calls, [['runCell', 'side', 'return "ok";']]);
73
+ assert.equal(current.name, 'p1');
74
+ });
75
+
76
+ it('exec returns ok:false when manager throws', async () => {
77
+ const { pi, mgr } = wire('p1');
78
+ mgr.nextResult = { throw: Object.assign(new Error('boom'), { name: 'BoomError' }) } as any;
79
+ const res = (await pi.tools.get('cw_scratchpad')!.execute('', { action: 'exec', code: 'throw new Error("boom");' }, undefined, undefined, {})).details as { ok: boolean; error: { name: string; message: string } };
80
+ assert.equal(res.ok, false);
81
+ assert.equal(res.error.name, 'BoomError');
82
+ assert.match(res.error.message, /boom/);
83
+ });
84
+
85
+ it('view returns tail-5 by default and the right total_cells', async () => {
86
+ const { pi } = wire('p1');
87
+ await mkdir(join(root, 'p1'), { recursive: true });
88
+ const lines = [JSON.stringify({ type: 'header', version: 1 })];
89
+ for (let i = 1; i <= 8; i++) {
90
+ lines.push(JSON.stringify({ id: i, parentId: i === 1 ? null : i - 1, code: `return ${i};`, ok: true, value: i, stdout: '', ts: `t${i}` }));
91
+ }
92
+ await writeFile(join(root, 'p1', 'cells.jsonl'), lines.join('\n') + '\n');
93
+ const res = (await pi.tools.get('cw_scratchpad')!.execute('', { action: 'view' }, undefined, undefined, {})).details as { cells: Array<{ id: number }>; total_cells: number };
94
+ assert.equal(res.total_cells, 8);
95
+ assert.equal(res.cells.length, 5);
96
+ assert.deepEqual(res.cells.map((c) => c.id), [4, 5, 6, 7, 8]);
97
+ });
98
+
99
+ it('view caps tail at 20', async () => {
100
+ const { pi } = wire('p1');
101
+ await mkdir(join(root, 'p1'), { recursive: true });
102
+ const lines = [JSON.stringify({ type: 'header', version: 1 })];
103
+ for (let i = 1; i <= 30; i++) {
104
+ lines.push(JSON.stringify({ id: i, parentId: i === 1 ? null : i - 1, code: 'x', ok: true, value: i, stdout: '', ts: 't' }));
105
+ }
106
+ await writeFile(join(root, 'p1', 'cells.jsonl'), lines.join('\n') + '\n');
107
+ const res = (await pi.tools.get('cw_scratchpad')!.execute('', { action: 'view', tail: 100 }, undefined, undefined, {})).details as { cells: unknown[] };
108
+ assert.equal(res.cells.length, 20);
109
+ });
110
+
111
+ it('view with from_id returns cells with id >= from_id', async () => {
112
+ const { pi } = wire('p1');
113
+ await mkdir(join(root, 'p1'), { recursive: true });
114
+ const lines = [JSON.stringify({ type: 'header', version: 1 })];
115
+ for (let i = 1; i <= 10; i++) {
116
+ lines.push(JSON.stringify({ id: i, parentId: i === 1 ? null : i - 1, code: 'x', ok: true, value: i, stdout: '', ts: 't' }));
117
+ }
118
+ await writeFile(join(root, 'p1', 'cells.jsonl'), lines.join('\n') + '\n');
119
+ const res = (await pi.tools.get('cw_scratchpad')!.execute('', { action: 'view', from_id: 7 }, undefined, undefined, {})).details as { cells: Array<{ id: number }> };
120
+ assert.deepEqual(res.cells.map((c) => c.id), [7, 8, 9, 10]);
121
+ });
122
+
123
+ it('view truncates value strings to 200 chars and stdout to 500 chars', async () => {
124
+ const { pi } = wire('p1');
125
+ await mkdir(join(root, 'p1'), { recursive: true });
126
+ const longValue = 'x'.repeat(500);
127
+ const longStdout = 'y'.repeat(1000);
128
+ const lines = [
129
+ JSON.stringify({ type: 'header', version: 1 }),
130
+ JSON.stringify({ id: 1, parentId: null, code: 'long', ok: true, value: longValue, stdout: longStdout, ts: 't' }),
131
+ ];
132
+ await writeFile(join(root, 'p1', 'cells.jsonl'), lines.join('\n') + '\n');
133
+ const res = (await pi.tools.get('cw_scratchpad')!.execute('', { action: 'view' }, undefined, undefined, {})).details as { cells: Array<{ value: string; stdout: string }> };
134
+ assert.equal(res.cells[0].value.length, 200);
135
+ assert.equal(res.cells[0].stdout.length, 500);
136
+ });
137
+ });
@@ -0,0 +1,165 @@
1
+ import { join } from 'node:path';
2
+ import { StringEnum } from '@otto/pi-ai';
3
+ import type { ExtensionAPI } from '@otto/pi-coding-agent';
4
+ import { Type } from '@sinclair/typebox';
5
+ import type { ScratchpadManager, CellEntry } from '@otto/coworker-scratchpad';
6
+ import { validateName, readCellsJsonl } from './helpers.js';
7
+ import { deriveMimeBundle, type MimeBundle } from './mime-bundle.js';
8
+
9
+ export interface ScratchpadToolDeps {
10
+ getManager: () => ScratchpadManager;
11
+ getCurrentName: () => string | null;
12
+ setCurrentName: (name: string | null) => void;
13
+ rootDir: () => string;
14
+ }
15
+
16
+ const VIEW_DEFAULT_TAIL = 5;
17
+ const VIEW_MAX_TAIL = 20;
18
+ const VALUE_TRUNCATE = 200;
19
+ const STDOUT_TRUNCATE = 500;
20
+
21
+ interface ExecResultOk {
22
+ ok: true;
23
+ cell_id: number;
24
+ total_cells: number;
25
+ mime: MimeBundle;
26
+ }
27
+ interface ExecResultErr {
28
+ ok: false;
29
+ cell_id: number;
30
+ total_cells: number;
31
+ error: { name: string; message: string };
32
+ }
33
+ type ExecResult = ExecResultOk | ExecResultErr;
34
+
35
+ interface ViewCell {
36
+ id: number;
37
+ parentId: number | null;
38
+ ts: string;
39
+ code: string;
40
+ ok: boolean;
41
+ value?: unknown;
42
+ error?: { name: string; message: string };
43
+ stdout: string;
44
+ }
45
+ interface ViewResult {
46
+ name: string;
47
+ cells: ViewCell[];
48
+ total_cells: number;
49
+ }
50
+
51
+ function ensureCurrent(deps: ScratchpadToolDeps): string {
52
+ let current = deps.getCurrentName();
53
+ if (!current) {
54
+ current = 'default';
55
+ deps.setCurrentName(current);
56
+ }
57
+ return current;
58
+ }
59
+
60
+ function resolveName(deps: ScratchpadToolDeps, explicit?: string): string {
61
+ if (explicit) {
62
+ validateName(explicit);
63
+ return explicit;
64
+ }
65
+ return ensureCurrent(deps);
66
+ }
67
+
68
+ function truncateValue(value: unknown): unknown {
69
+ if (typeof value === 'string' && value.length > VALUE_TRUNCATE) {
70
+ return value.slice(0, VALUE_TRUNCATE);
71
+ }
72
+ return value;
73
+ }
74
+
75
+ function projectViewCell(c: CellEntry): ViewCell {
76
+ return {
77
+ id: c.id,
78
+ parentId: c.parentId,
79
+ ts: c.ts,
80
+ code: c.code,
81
+ ok: c.ok,
82
+ value: c.ok ? truncateValue(c.value) : undefined,
83
+ error: c.ok ? undefined : c.error,
84
+ stdout: (c.stdout ?? '').slice(0, STDOUT_TRUNCATE),
85
+ };
86
+ }
87
+
88
+ export function registerScratchpadTool(pi: ExtensionAPI, deps: ScratchpadToolDeps): void {
89
+ pi.registerTool({
90
+ name: 'cw_scratchpad',
91
+ label: 'Co-worker scratchpad',
92
+ description:
93
+ 'USE FOR: loading or analyzing files (CSV, Excel, JSON, Parquet), tabular data manipulation with polars or DuckDB, multi-step data exploration where state should persist across turns, or anything that calls otto.collectors. ' +
94
+ 'DO NOT USE FOR: simple arithmetic, lookups answerable from reasoning alone, pure prose, code review, or one-off calculations with no file or data source involved. ' +
95
+ 'IF the user explicitly names this tool ("use cw_scratchpad", "run this in the scratchpad", "exec a cell"), use it regardless of the rules above. ' +
96
+ 'IF UNSURE whether a request belongs here, ASK the user once: "Do you want me to run this in cw_scratchpad so you can inspect the cells via /sp view, or answer inline?" Wait for their reply before deciding. ' +
97
+ 'Runs TypeScript cells in a persistent JS kernel scoped to a named scratchpad. State persists across cells via globalThis.* and across Otto sessions via on-disk kernel.db + namespace.json. ' +
98
+ 'Pre-bound libs in every cell: polars, DuckDB, ExcelJS, dateFns, lodash, zod, axios. otto.collectors.{list,open} enumerates and loads data sources. ' +
99
+ 'Actions: exec (run a TypeScript cell), view (return the last N cells). ' +
100
+ 'Distinct from the analyst extension\'s SQL-only `scratchpad` tool — this one runs arbitrary TypeScript.',
101
+ promptSnippet:
102
+ 'cw_scratchpad — run TypeScript cells in a persistent JS kernel. USE for files (CSV/Excel/JSON/Parquet), polars/DuckDB analysis, otto.collectors, or multi-step data work. NOT for arithmetic or pure prose. If unsure, ASK the user first.',
103
+ promptGuidelines: [
104
+ 'Trigger criteria: the request involves loading a file (CSV/Excel/JSON/Parquet/etc.), querying tabular data via polars or DuckDB, calling otto.collectors, or building state that must survive across turns.',
105
+ 'Skip the tool for: trivial arithmetic, lookups you can answer from reasoning, prose generation, code review, and pure-explanation tasks.',
106
+ 'If the user names the tool explicitly (e.g. "use cw_scratchpad", "in the scratchpad", "exec a cell"), always honor that request and skip the unsure-prompt.',
107
+ 'IF UNSURE whether a request fits the trigger criteria, ask the user once before deciding: "Do you want me to run this in cw_scratchpad (you can inspect the cells via /sp view) or answer inline?" Wait for the user\'s reply before either calling the tool or answering inline.',
108
+ 'Use action="exec" to run TypeScript code in the current scratchpad. State persists across calls.',
109
+ 'The cell body is wrapped in (async () => { ... })(). let/const/var are local to the cell. To persist, assign to globalThis.foo = ...',
110
+ 'For DuckDB tables that survive across Otto sessions, use `await otto.duckdb.connect()`. For ephemeral in-memory, use `DuckDB.DuckDBInstance.create(":memory:")`.',
111
+ 'For polars→DuckDB: prefer `otto.duckdb.registerDf(name, df)` over manual API discovery. If inference picks the wrong column type, pass `{ schema: { col: \'TYPE\' } }` as the third argument. Falls back to polars\' own SQL (`df.sql(...)`) for one-off aggregations.',
112
+ 'Pre-bound libs available in every cell: polars, DuckDB, ExcelJS, dateFns, lodash, zod, axios. No imports needed.',
113
+ 'Use otto.collectors.list() to discover data sources and otto.collectors.open(uri) to load one.',
114
+ 'The `name` parameter defaults to the currently attached scratchpad. Omit it unless you want to operate on a different one (this does NOT switch the user attachment).',
115
+ 'A returned string that looks markdown-shaped will appear in the response as text/markdown automatically. Return a markdown table or heading to render it.',
116
+ 'Use action="view" to see the last 5 cells (default). Pass tail=20 or from_id to see more.',
117
+ 'A failed cell IS recorded; the next view call will show it. Use this to recover.',
118
+ 'Default cell timeout is 120s. Long operations should call progress("status") periodically to reset the inactivity timer.',
119
+ ],
120
+ parameters: Type.Object({
121
+ action: StringEnum(['exec', 'view'] as const),
122
+ name: Type.Optional(Type.String({ description: "Scratchpad name; defaults to the current session attachment, auto-creating 'default' if none." })),
123
+ code: Type.Optional(Type.String({ description: "TypeScript cell code (action='exec' only)." })),
124
+ tail: Type.Optional(Type.Number({ description: `How many trailing cells to return (action='view' only). Default ${VIEW_DEFAULT_TAIL}, max ${VIEW_MAX_TAIL}.` })),
125
+ from_id: Type.Optional(Type.Number({ description: "If set, view returns cells with id >= from_id (overrides tail)." })),
126
+ }),
127
+ async execute(_toolCallId: string, params: { action: 'exec' | 'view'; name?: string; code?: string; tail?: number; from_id?: number }, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown): Promise<{ content: { type: 'text'; text: string }[]; details: ExecResult | ViewResult }> {
128
+ const name = resolveName(deps, params.name);
129
+
130
+ let result: ExecResult | ViewResult;
131
+
132
+ if (params.action === 'exec') {
133
+ if (!params.code) {
134
+ throw new Error('code is required for action="exec"');
135
+ }
136
+ const mgr = deps.getManager();
137
+ try {
138
+ const { value, stdout } = await mgr.runCell(name, params.code);
139
+ const { total_cells } = readCellsJsonl(join(deps.rootDir(), name));
140
+ result = { ok: true, cell_id: total_cells, total_cells, mime: deriveMimeBundle(value, stdout) };
141
+ } catch (err) {
142
+ const e = err as Error;
143
+ const { total_cells } = readCellsJsonl(join(deps.rootDir(), name));
144
+ result = { ok: false, cell_id: total_cells, total_cells, error: { name: e.name, message: e.message } };
145
+ }
146
+ } else {
147
+ // view
148
+ const { cells, total_cells } = readCellsJsonl(join(deps.rootDir(), name));
149
+ let selected: CellEntry[];
150
+ if (typeof params.from_id === 'number') {
151
+ selected = cells.filter((c) => c.id >= params.from_id!);
152
+ } else {
153
+ const tail = Math.min(params.tail ?? VIEW_DEFAULT_TAIL, VIEW_MAX_TAIL);
154
+ selected = cells.slice(-tail);
155
+ }
156
+ result = { name, cells: selected.map(projectViewCell), total_cells };
157
+ }
158
+
159
+ return {
160
+ content: [{ type: 'text' as const, text: JSON.stringify(result) }],
161
+ details: result,
162
+ };
163
+ },
164
+ });
165
+ }