@cmetech/otto 1.1.1 → 1.2.4

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 (423) 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-paths.js +8 -0
  7. package/dist/resources/extensions/coworker-artifacts/artifacts-command.js +31 -0
  8. package/dist/resources/extensions/coworker-artifacts/artifacts-singleton.js +17 -0
  9. package/dist/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
  10. package/dist/resources/extensions/coworker-artifacts/index.js +125 -0
  11. package/dist/resources/extensions/coworker-artifacts/list-tool.js +27 -0
  12. package/dist/resources/extensions/coworker-artifacts/open-tool.js +25 -0
  13. package/dist/resources/extensions/coworker-memory/extension-manifest.json +13 -0
  14. package/dist/resources/extensions/coworker-memory/index.js +219 -0
  15. package/dist/resources/extensions/coworker-memory/memorize-tool.js +10 -0
  16. package/dist/resources/extensions/coworker-memory/memory-command.js +157 -0
  17. package/dist/resources/extensions/coworker-memory/memory-singleton.js +55 -0
  18. package/dist/resources/extensions/coworker-memory/recall-tool.js +18 -0
  19. package/dist/resources/extensions/coworker-memory/session-hooks.js +45 -0
  20. package/dist/resources/extensions/coworker-scratchpad/attach-banners.js +53 -0
  21. package/dist/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
  22. package/dist/resources/extensions/coworker-scratchpad/format-age.js +9 -0
  23. package/dist/resources/extensions/coworker-scratchpad/helpers.js +38 -0
  24. package/dist/resources/extensions/coworker-scratchpad/index.js +199 -0
  25. package/dist/resources/extensions/coworker-scratchpad/mime-bundle.js +20 -0
  26. package/dist/resources/extensions/coworker-scratchpad/scratchpad-tool.js +118 -0
  27. package/dist/resources/extensions/coworker-scratchpad/session-sidecar.js +60 -0
  28. package/dist/resources/extensions/coworker-scratchpad/sp-command.js +597 -0
  29. package/dist/resources/extensions/coworker-scratchpad/workspace-pointer.js +41 -0
  30. package/dist/resources/extensions/coworker-scratchpad/workspace-root.js +17 -0
  31. package/dist/resources/extensions/coworker-vault/audit-command.js +35 -0
  32. package/dist/resources/extensions/coworker-vault/connect-command.js +42 -0
  33. package/dist/resources/extensions/coworker-vault/datasource-command.js +50 -0
  34. package/dist/resources/extensions/coworker-vault/extension-manifest.json +12 -0
  35. package/dist/resources/extensions/coworker-vault/index.js +171 -0
  36. package/dist/resources/extensions/coworker-vault/test-helpers.js +86 -0
  37. package/dist/resources/extensions/coworker-vault/vault-singleton.js +24 -0
  38. package/dist/resources/extensions/otto/commands/release-notes/_data.js +71 -0
  39. package/dist/resources/extensions/otto/commands/release-notes/command.js +15 -4
  40. package/dist/resources/extensions/subagent/index.js +8 -1
  41. package/dist/resources/extensions/subagent/launch.js +37 -5
  42. package/dist/resources/extensions/subagent/run-store.js +1 -0
  43. package/dist/resources/extensions/workflow/bootstrap/register-extension.js +2 -0
  44. package/dist/resources/extensions/workflow/bootstrap/register-hooks.js +10 -0
  45. package/dist/resources/extensions/workflow/persona-status.js +87 -0
  46. package/package.json +25 -10
  47. package/packages/contracts/package.json +1 -1
  48. package/packages/coworker-artifacts/dist/artifact-store.d.ts +25 -0
  49. package/packages/coworker-artifacts/dist/artifact-store.js +187 -0
  50. package/packages/coworker-artifacts/dist/dir-snapshot.d.ts +7 -0
  51. package/packages/coworker-artifacts/dist/dir-snapshot.js +54 -0
  52. package/packages/coworker-artifacts/dist/errors.d.ts +18 -0
  53. package/packages/coworker-artifacts/dist/errors.js +37 -0
  54. package/packages/coworker-artifacts/dist/index.d.ts +7 -0
  55. package/packages/coworker-artifacts/dist/index.js +7 -0
  56. package/packages/coworker-artifacts/dist/readme-renderer.d.ts +5 -0
  57. package/packages/coworker-artifacts/dist/readme-renderer.js +47 -0
  58. package/packages/coworker-artifacts/dist/resolve-uri.d.ts +3 -0
  59. package/packages/coworker-artifacts/dist/resolve-uri.js +29 -0
  60. package/packages/coworker-artifacts/dist/slug.d.ts +4 -0
  61. package/packages/coworker-artifacts/dist/slug.js +32 -0
  62. package/packages/coworker-artifacts/dist/types.d.ts +52 -0
  63. package/packages/coworker-artifacts/dist/types.js +1 -0
  64. package/packages/coworker-artifacts/package.json +20 -0
  65. package/packages/coworker-artifacts/src/artifact-store.test.ts +188 -0
  66. package/packages/coworker-artifacts/src/artifact-store.ts +206 -0
  67. package/packages/coworker-artifacts/src/artifacts-integration.test.ts +109 -0
  68. package/packages/coworker-artifacts/src/dir-snapshot.test.ts +71 -0
  69. package/packages/coworker-artifacts/src/dir-snapshot.ts +52 -0
  70. package/packages/coworker-artifacts/src/errors.test.ts +37 -0
  71. package/packages/coworker-artifacts/src/errors.ts +28 -0
  72. package/packages/coworker-artifacts/src/index.test.ts +22 -0
  73. package/packages/coworker-artifacts/src/index.ts +7 -0
  74. package/packages/coworker-artifacts/src/readme-renderer.test.ts +72 -0
  75. package/packages/coworker-artifacts/src/readme-renderer.ts +56 -0
  76. package/packages/coworker-artifacts/src/resolve-uri.test.ts +46 -0
  77. package/packages/coworker-artifacts/src/resolve-uri.ts +29 -0
  78. package/packages/coworker-artifacts/src/slug.test.ts +47 -0
  79. package/packages/coworker-artifacts/src/slug.ts +31 -0
  80. package/packages/coworker-artifacts/src/types.ts +61 -0
  81. package/packages/coworker-artifacts/tsconfig.json +15 -0
  82. package/packages/coworker-artifacts/tsconfig.publish.json +4 -0
  83. package/packages/coworker-memory/dist/context-injection.d.ts +9 -0
  84. package/packages/coworker-memory/dist/context-injection.js +41 -0
  85. package/packages/coworker-memory/dist/errors.d.ts +25 -0
  86. package/packages/coworker-memory/dist/errors.js +51 -0
  87. package/packages/coworker-memory/dist/index.d.ts +12 -0
  88. package/packages/coworker-memory/dist/index.js +12 -0
  89. package/packages/coworker-memory/dist/layer-a-store.d.ts +16 -0
  90. package/packages/coworker-memory/dist/layer-a-store.js +78 -0
  91. package/packages/coworker-memory/dist/local-sqlite-backend.d.ts +28 -0
  92. package/packages/coworker-memory/dist/local-sqlite-backend.js +167 -0
  93. package/packages/coworker-memory/dist/memory-backend.d.ts +14 -0
  94. package/packages/coworker-memory/dist/memory-backend.js +1 -0
  95. package/packages/coworker-memory/dist/memory-recorder.d.ts +50 -0
  96. package/packages/coworker-memory/dist/memory-recorder.js +69 -0
  97. package/packages/coworker-memory/dist/migrations/001-init.sql +38 -0
  98. package/packages/coworker-memory/dist/migrations/002-artifact-kind.sql +50 -0
  99. package/packages/coworker-memory/dist/paste-detector.d.ts +5 -0
  100. package/packages/coworker-memory/dist/paste-detector.js +14 -0
  101. package/packages/coworker-memory/dist/persona-seed.d.ts +10 -0
  102. package/packages/coworker-memory/dist/persona-seed.js +38 -0
  103. package/packages/coworker-memory/dist/recall-formatter.d.ts +2 -0
  104. package/packages/coworker-memory/dist/recall-formatter.js +14 -0
  105. package/packages/coworker-memory/dist/scope-resolver.d.ts +9 -0
  106. package/packages/coworker-memory/dist/scope-resolver.js +10 -0
  107. package/packages/coworker-memory/dist/types.d.ts +51 -0
  108. package/packages/coworker-memory/dist/types.js +2 -0
  109. package/packages/coworker-memory/dist/workspace-id.d.ts +3 -0
  110. package/packages/coworker-memory/dist/workspace-id.js +54 -0
  111. package/packages/coworker-memory/package.json +35 -0
  112. package/packages/coworker-memory/src/activator-integration.test.ts +141 -0
  113. package/packages/coworker-memory/src/context-injection.test.ts +72 -0
  114. package/packages/coworker-memory/src/context-injection.ts +57 -0
  115. package/packages/coworker-memory/src/errors.test.ts +45 -0
  116. package/packages/coworker-memory/src/errors.ts +42 -0
  117. package/packages/coworker-memory/src/index.test.ts +21 -0
  118. package/packages/coworker-memory/src/index.ts +12 -0
  119. package/packages/coworker-memory/src/layer-a-store.test.ts +85 -0
  120. package/packages/coworker-memory/src/layer-a-store.ts +88 -0
  121. package/packages/coworker-memory/src/local-sqlite-backend.test.ts +110 -0
  122. package/packages/coworker-memory/src/local-sqlite-backend.ts +185 -0
  123. package/packages/coworker-memory/src/memory-backend.ts +10 -0
  124. package/packages/coworker-memory/src/memory-integration.test.ts +89 -0
  125. package/packages/coworker-memory/src/memory-recorder.test.ts +101 -0
  126. package/packages/coworker-memory/src/memory-recorder.ts +95 -0
  127. package/packages/coworker-memory/src/migrations/001-init.sql +38 -0
  128. package/packages/coworker-memory/src/migrations/002-artifact-kind.sql +50 -0
  129. package/packages/coworker-memory/src/paste-detector.test.ts +23 -0
  130. package/packages/coworker-memory/src/paste-detector.ts +18 -0
  131. package/packages/coworker-memory/src/persona-seed.test.ts +57 -0
  132. package/packages/coworker-memory/src/persona-seed.ts +46 -0
  133. package/packages/coworker-memory/src/recall-formatter.test.ts +34 -0
  134. package/packages/coworker-memory/src/recall-formatter.ts +15 -0
  135. package/packages/coworker-memory/src/scope-resolver.test.ts +23 -0
  136. package/packages/coworker-memory/src/scope-resolver.ts +18 -0
  137. package/packages/coworker-memory/src/types.ts +61 -0
  138. package/packages/coworker-memory/src/workspace-id.test.ts +48 -0
  139. package/packages/coworker-memory/src/workspace-id.ts +56 -0
  140. package/packages/coworker-memory/tsconfig.json +15 -0
  141. package/packages/coworker-memory/tsconfig.publish.json +4 -0
  142. package/packages/coworker-persona/dist/commands.d.ts +7 -0
  143. package/packages/coworker-persona/dist/commands.js +35 -0
  144. package/packages/coworker-persona/dist/defaults/manifest.yaml +12 -0
  145. package/packages/coworker-persona/dist/defaults/steering/identity.md +3 -0
  146. package/packages/coworker-persona/dist/index.d.ts +3 -0
  147. package/packages/coworker-persona/dist/index.js +3 -0
  148. package/packages/coworker-persona/dist/manifest.d.ts +24 -0
  149. package/packages/coworker-persona/dist/manifest.js +21 -0
  150. package/packages/coworker-persona/dist/registry.d.ts +22 -0
  151. package/packages/coworker-persona/dist/registry.js +142 -0
  152. package/packages/coworker-persona/package.json +28 -0
  153. package/packages/coworker-persona/scripts/copy-defaults.cjs +17 -0
  154. package/packages/coworker-persona/src/commands.ts +47 -0
  155. package/packages/coworker-persona/src/defaults/manifest.yaml +12 -0
  156. package/packages/coworker-persona/src/defaults/steering/identity.md +3 -0
  157. package/packages/coworker-persona/src/index.ts +3 -0
  158. package/packages/coworker-persona/src/manifest.test.ts +67 -0
  159. package/packages/coworker-persona/src/manifest.ts +49 -0
  160. package/packages/coworker-persona/src/registry.test.ts +89 -0
  161. package/packages/coworker-persona/src/registry.ts +147 -0
  162. package/packages/coworker-persona/tsconfig.json +15 -0
  163. package/packages/coworker-persona/tsconfig.publish.json +4 -0
  164. package/packages/coworker-scratchpad/dist/cell-archive.d.ts +39 -0
  165. package/packages/coworker-scratchpad/dist/cell-archive.js +77 -0
  166. package/packages/coworker-scratchpad/dist/cell-tree.d.ts +14 -0
  167. package/packages/coworker-scratchpad/dist/cell-tree.js +72 -0
  168. package/packages/coworker-scratchpad/dist/child-process-runtime.d.ts +129 -0
  169. package/packages/coworker-scratchpad/dist/child-process-runtime.js +427 -0
  170. package/packages/coworker-scratchpad/dist/collector-registry.d.ts +12 -0
  171. package/packages/coworker-scratchpad/dist/collector-registry.js +29 -0
  172. package/packages/coworker-scratchpad/dist/detect-kind.d.ts +3 -0
  173. package/packages/coworker-scratchpad/dist/detect-kind.js +19 -0
  174. package/packages/coworker-scratchpad/dist/file-collector.d.ts +15 -0
  175. package/packages/coworker-scratchpad/dist/file-collector.js +99 -0
  176. package/packages/coworker-scratchpad/dist/index.d.ts +13 -0
  177. package/packages/coworker-scratchpad/dist/index.js +13 -0
  178. package/packages/coworker-scratchpad/dist/kernel-bindings.d.ts +49 -0
  179. package/packages/coworker-scratchpad/dist/kernel-bindings.js +220 -0
  180. package/packages/coworker-scratchpad/dist/kernel-entry.d.ts +1 -0
  181. package/packages/coworker-scratchpad/dist/kernel-entry.js +355 -0
  182. package/packages/coworker-scratchpad/dist/kernel-protocol.d.ts +171 -0
  183. package/packages/coworker-scratchpad/dist/kernel-protocol.js +48 -0
  184. package/packages/coworker-scratchpad/dist/kernel-spawn.d.ts +3 -0
  185. package/packages/coworker-scratchpad/dist/kernel-spawn.js +54 -0
  186. package/packages/coworker-scratchpad/dist/namespace-codec.d.ts +22 -0
  187. package/packages/coworker-scratchpad/dist/namespace-codec.js +61 -0
  188. package/packages/coworker-scratchpad/dist/scratchpad-lock.d.ts +24 -0
  189. package/packages/coworker-scratchpad/dist/scratchpad-lock.js +86 -0
  190. package/packages/coworker-scratchpad/dist/scratchpad-manager.d.ts +193 -0
  191. package/packages/coworker-scratchpad/dist/scratchpad-manager.js +866 -0
  192. package/packages/coworker-scratchpad/dist/staleness-banner.d.ts +12 -0
  193. package/packages/coworker-scratchpad/dist/staleness-banner.js +27 -0
  194. package/packages/coworker-scratchpad/package.json +31 -0
  195. package/packages/coworker-scratchpad/src/cell-archive.test.ts +150 -0
  196. package/packages/coworker-scratchpad/src/cell-archive.ts +97 -0
  197. package/packages/coworker-scratchpad/src/cell-tree.test.ts +105 -0
  198. package/packages/coworker-scratchpad/src/cell-tree.ts +90 -0
  199. package/packages/coworker-scratchpad/src/child-process-runtime.test.ts +413 -0
  200. package/packages/coworker-scratchpad/src/child-process-runtime.ts +493 -0
  201. package/packages/coworker-scratchpad/src/collector-registry.test.ts +69 -0
  202. package/packages/coworker-scratchpad/src/collector-registry.ts +33 -0
  203. package/packages/coworker-scratchpad/src/detect-kind.test.ts +33 -0
  204. package/packages/coworker-scratchpad/src/detect-kind.ts +22 -0
  205. package/packages/coworker-scratchpad/src/file-collector.test.ts +109 -0
  206. package/packages/coworker-scratchpad/src/file-collector.ts +114 -0
  207. package/packages/coworker-scratchpad/src/index.ts +74 -0
  208. package/packages/coworker-scratchpad/src/kernel-bindings.test.ts +188 -0
  209. package/packages/coworker-scratchpad/src/kernel-bindings.ts +279 -0
  210. package/packages/coworker-scratchpad/src/kernel-entry.test.ts +123 -0
  211. package/packages/coworker-scratchpad/src/kernel-entry.ts +390 -0
  212. package/packages/coworker-scratchpad/src/kernel-protocol.test.ts +105 -0
  213. package/packages/coworker-scratchpad/src/kernel-protocol.ts +230 -0
  214. package/packages/coworker-scratchpad/src/kernel-spawn.test.ts +60 -0
  215. package/packages/coworker-scratchpad/src/kernel-spawn.ts +54 -0
  216. package/packages/coworker-scratchpad/src/namespace-codec.test.ts +102 -0
  217. package/packages/coworker-scratchpad/src/namespace-codec.ts +90 -0
  218. package/packages/coworker-scratchpad/src/scratchpad-lock.test.ts +98 -0
  219. package/packages/coworker-scratchpad/src/scratchpad-lock.ts +102 -0
  220. package/packages/coworker-scratchpad/src/scratchpad-manager.test.ts +1343 -0
  221. package/packages/coworker-scratchpad/src/scratchpad-manager.ts +891 -0
  222. package/packages/coworker-scratchpad/src/staleness-banner.test.ts +53 -0
  223. package/packages/coworker-scratchpad/src/staleness-banner.ts +33 -0
  224. package/packages/coworker-scratchpad/src/vault-integration.test.ts +221 -0
  225. package/packages/coworker-scratchpad/tsconfig.json +15 -0
  226. package/packages/coworker-scratchpad/tsconfig.publish.json +4 -0
  227. package/packages/coworker-types/dist/artifacts.d.ts +31 -0
  228. package/packages/coworker-types/dist/artifacts.js +2 -0
  229. package/packages/coworker-types/dist/contracts.d.ts +32 -0
  230. package/packages/coworker-types/dist/contracts.js +1 -0
  231. package/packages/coworker-types/dist/index.d.ts +5 -0
  232. package/packages/coworker-types/dist/index.js +5 -0
  233. package/packages/coworker-types/dist/memory.d.ts +61 -0
  234. package/packages/coworker-types/dist/memory.js +3 -0
  235. package/packages/coworker-types/dist/scratchpad.d.ts +43 -0
  236. package/packages/coworker-types/dist/scratchpad.js +2 -0
  237. package/packages/coworker-types/dist/vault.d.ts +34 -0
  238. package/packages/coworker-types/dist/vault.js +2 -0
  239. package/packages/coworker-types/package.json +24 -0
  240. package/packages/coworker-types/src/artifacts.test.ts +52 -0
  241. package/packages/coworker-types/src/artifacts.ts +35 -0
  242. package/packages/coworker-types/src/contracts.test.ts +43 -0
  243. package/packages/coworker-types/src/contracts.ts +36 -0
  244. package/packages/coworker-types/src/index.ts +5 -0
  245. package/packages/coworker-types/src/memory.test.ts +50 -0
  246. package/packages/coworker-types/src/memory.ts +79 -0
  247. package/packages/coworker-types/src/scratchpad.test.ts +46 -0
  248. package/packages/coworker-types/src/scratchpad.ts +51 -0
  249. package/packages/coworker-types/src/smoke.test.ts +34 -0
  250. package/packages/coworker-types/src/vault.test.ts +49 -0
  251. package/packages/coworker-types/src/vault.ts +40 -0
  252. package/packages/coworker-types/tsconfig.json +15 -0
  253. package/packages/coworker-types/tsconfig.publish.json +4 -0
  254. package/packages/coworker-utils/dist/audit-log.d.ts +34 -0
  255. package/packages/coworker-utils/dist/audit-log.js +88 -0
  256. package/packages/coworker-utils/dist/index.d.ts +6 -0
  257. package/packages/coworker-utils/dist/index.js +6 -0
  258. package/packages/coworker-utils/dist/lease.d.ts +7 -0
  259. package/packages/coworker-utils/dist/lease.js +67 -0
  260. package/packages/coworker-utils/dist/logger.d.ts +13 -0
  261. package/packages/coworker-utils/dist/logger.js +26 -0
  262. package/packages/coworker-utils/dist/migration-runner.d.ts +7 -0
  263. package/packages/coworker-utils/dist/migration-runner.js +36 -0
  264. package/packages/coworker-utils/dist/ndjson-channel.d.ts +3 -0
  265. package/packages/coworker-utils/dist/ndjson-channel.js +38 -0
  266. package/packages/coworker-utils/dist/secret-scanner.d.ts +10 -0
  267. package/packages/coworker-utils/dist/secret-scanner.js +42 -0
  268. package/packages/coworker-utils/package.json +24 -0
  269. package/packages/coworker-utils/src/audit-log.test.ts +140 -0
  270. package/packages/coworker-utils/src/audit-log.ts +107 -0
  271. package/packages/coworker-utils/src/index.ts +6 -0
  272. package/packages/coworker-utils/src/lease.test.ts +64 -0
  273. package/packages/coworker-utils/src/lease.ts +76 -0
  274. package/packages/coworker-utils/src/logger.test.ts +50 -0
  275. package/packages/coworker-utils/src/logger.ts +45 -0
  276. package/packages/coworker-utils/src/migration-runner.test.ts +65 -0
  277. package/packages/coworker-utils/src/migration-runner.ts +50 -0
  278. package/packages/coworker-utils/src/ndjson-channel.test.ts +76 -0
  279. package/packages/coworker-utils/src/ndjson-channel.ts +41 -0
  280. package/packages/coworker-utils/src/secret-scanner.test.ts +61 -0
  281. package/packages/coworker-utils/src/secret-scanner.ts +56 -0
  282. package/packages/coworker-utils/tsconfig.json +15 -0
  283. package/packages/coworker-utils/tsconfig.publish.json +4 -0
  284. package/packages/coworker-vault/dist/data-vault.d.ts +41 -0
  285. package/packages/coworker-vault/dist/data-vault.js +223 -0
  286. package/packages/coworker-vault/dist/engine-registry.d.ts +34 -0
  287. package/packages/coworker-vault/dist/engine-registry.js +90 -0
  288. package/packages/coworker-vault/dist/engines/jira.yaml +17 -0
  289. package/packages/coworker-vault/dist/errors.d.ts +28 -0
  290. package/packages/coworker-vault/dist/errors.js +57 -0
  291. package/packages/coworker-vault/dist/index.d.ts +6 -0
  292. package/packages/coworker-vault/dist/index.js +6 -0
  293. package/packages/coworker-vault/dist/injector.d.ts +19 -0
  294. package/packages/coworker-vault/dist/injector.js +77 -0
  295. package/packages/coworker-vault/dist/types.d.ts +28 -0
  296. package/packages/coworker-vault/dist/types.js +1 -0
  297. package/packages/coworker-vault/dist/vault-keep.d.ts +4 -0
  298. package/packages/coworker-vault/dist/vault-keep.js +21 -0
  299. package/packages/coworker-vault/package.json +29 -0
  300. package/packages/coworker-vault/src/data-vault.test.ts +199 -0
  301. package/packages/coworker-vault/src/data-vault.ts +257 -0
  302. package/packages/coworker-vault/src/engine-registry.test.ts +120 -0
  303. package/packages/coworker-vault/src/engine-registry.ts +107 -0
  304. package/packages/coworker-vault/src/engines/jira.yaml +17 -0
  305. package/packages/coworker-vault/src/errors.test.ts +58 -0
  306. package/packages/coworker-vault/src/errors.ts +50 -0
  307. package/packages/coworker-vault/src/index.test.ts +24 -0
  308. package/packages/coworker-vault/src/index.ts +6 -0
  309. package/packages/coworker-vault/src/injector.test.ts +109 -0
  310. package/packages/coworker-vault/src/injector.ts +98 -0
  311. package/packages/coworker-vault/src/types.ts +33 -0
  312. package/packages/coworker-vault/src/vault-keep.test.ts +49 -0
  313. package/packages/coworker-vault/src/vault-keep.ts +31 -0
  314. package/packages/coworker-vault/tsconfig.json +15 -0
  315. package/packages/coworker-vault/tsconfig.publish.json +4 -0
  316. package/packages/daemon/package.json +3 -3
  317. package/packages/mcp-server/package.json +3 -3
  318. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  319. package/packages/native/package.json +1 -1
  320. package/packages/native/tsconfig.tsbuildinfo +1 -1
  321. package/packages/pi-agent-core/package.json +1 -1
  322. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  323. package/packages/pi-ai/package.json +1 -1
  324. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  325. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +6 -1
  326. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  327. package/packages/pi-coding-agent/dist/core/extensions/runner.js +22 -3
  328. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  329. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +11 -0
  330. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  331. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts +47 -0
  332. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts.map +1 -0
  333. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js +107 -0
  334. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js.map +1 -0
  335. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts +19 -0
  336. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts.map +1 -0
  337. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js +121 -0
  338. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js.map +1 -0
  339. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  340. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +17 -1
  341. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  342. package/packages/pi-coding-agent/package.json +2 -2
  343. package/packages/pi-coding-agent/src/core/extensions/runner.ts +22 -3
  344. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +11 -0
  345. package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.regression.test.ts +129 -0
  346. package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.ts +117 -0
  347. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +18 -1
  348. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  349. package/packages/pi-tui/package.json +1 -1
  350. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  351. package/packages/rpc-client/package.json +2 -2
  352. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  353. package/pkg/package.json +1 -1
  354. package/scripts/install.js +6 -5
  355. package/src/resources/extensions/_coworker-paths.test.ts +40 -0
  356. package/src/resources/extensions/_coworker-paths.ts +10 -0
  357. package/src/resources/extensions/coworker-artifacts/artifacts-command.test.ts +54 -0
  358. package/src/resources/extensions/coworker-artifacts/artifacts-command.ts +43 -0
  359. package/src/resources/extensions/coworker-artifacts/artifacts-singleton.test.ts +25 -0
  360. package/src/resources/extensions/coworker-artifacts/artifacts-singleton.ts +29 -0
  361. package/src/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
  362. package/src/resources/extensions/coworker-artifacts/index.test.ts +46 -0
  363. package/src/resources/extensions/coworker-artifacts/index.ts +154 -0
  364. package/src/resources/extensions/coworker-artifacts/list-tool.test.ts +29 -0
  365. package/src/resources/extensions/coworker-artifacts/list-tool.ts +53 -0
  366. package/src/resources/extensions/coworker-artifacts/open-tool.test.ts +30 -0
  367. package/src/resources/extensions/coworker-artifacts/open-tool.ts +43 -0
  368. package/src/resources/extensions/coworker-memory/extension-manifest.json +13 -0
  369. package/src/resources/extensions/coworker-memory/index.test.ts +137 -0
  370. package/src/resources/extensions/coworker-memory/index.ts +257 -0
  371. package/src/resources/extensions/coworker-memory/memorize-tool.test.ts +41 -0
  372. package/src/resources/extensions/coworker-memory/memorize-tool.ts +20 -0
  373. package/src/resources/extensions/coworker-memory/memory-command.test.ts +134 -0
  374. package/src/resources/extensions/coworker-memory/memory-command.ts +131 -0
  375. package/src/resources/extensions/coworker-memory/memory-singleton.test.ts +41 -0
  376. package/src/resources/extensions/coworker-memory/memory-singleton.ts +89 -0
  377. package/src/resources/extensions/coworker-memory/recall-tool.test.ts +50 -0
  378. package/src/resources/extensions/coworker-memory/recall-tool.ts +35 -0
  379. package/src/resources/extensions/coworker-memory/session-hooks.test.ts +77 -0
  380. package/src/resources/extensions/coworker-memory/session-hooks.ts +61 -0
  381. package/src/resources/extensions/coworker-scratchpad/attach-banners.test.ts +124 -0
  382. package/src/resources/extensions/coworker-scratchpad/attach-banners.ts +67 -0
  383. package/src/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
  384. package/src/resources/extensions/coworker-scratchpad/format-age.test.ts +30 -0
  385. package/src/resources/extensions/coworker-scratchpad/format-age.ts +6 -0
  386. package/src/resources/extensions/coworker-scratchpad/helpers.test.ts +93 -0
  387. package/src/resources/extensions/coworker-scratchpad/helpers.ts +42 -0
  388. package/src/resources/extensions/coworker-scratchpad/index.test.ts +514 -0
  389. package/src/resources/extensions/coworker-scratchpad/index.ts +207 -0
  390. package/src/resources/extensions/coworker-scratchpad/mime-bundle.test.ts +61 -0
  391. package/src/resources/extensions/coworker-scratchpad/mime-bundle.ts +23 -0
  392. package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.test.ts +137 -0
  393. package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.ts +165 -0
  394. package/src/resources/extensions/coworker-scratchpad/session-sidecar.test.ts +133 -0
  395. package/src/resources/extensions/coworker-scratchpad/session-sidecar.ts +68 -0
  396. package/src/resources/extensions/coworker-scratchpad/sp-command.test.ts +836 -0
  397. package/src/resources/extensions/coworker-scratchpad/sp-command.ts +602 -0
  398. package/src/resources/extensions/coworker-scratchpad/workspace-pointer.test.ts +74 -0
  399. package/src/resources/extensions/coworker-scratchpad/workspace-pointer.ts +55 -0
  400. package/src/resources/extensions/coworker-scratchpad/workspace-root.test.ts +51 -0
  401. package/src/resources/extensions/coworker-scratchpad/workspace-root.ts +16 -0
  402. package/src/resources/extensions/coworker-vault/audit-command.test.ts +109 -0
  403. package/src/resources/extensions/coworker-vault/audit-command.ts +56 -0
  404. package/src/resources/extensions/coworker-vault/connect-command.test.ts +103 -0
  405. package/src/resources/extensions/coworker-vault/connect-command.ts +69 -0
  406. package/src/resources/extensions/coworker-vault/datasource-command.test.ts +80 -0
  407. package/src/resources/extensions/coworker-vault/datasource-command.ts +81 -0
  408. package/src/resources/extensions/coworker-vault/extension-manifest.json +12 -0
  409. package/src/resources/extensions/coworker-vault/index.test.ts +82 -0
  410. package/src/resources/extensions/coworker-vault/index.ts +181 -0
  411. package/src/resources/extensions/coworker-vault/test-helpers.ts +120 -0
  412. package/src/resources/extensions/coworker-vault/vault-singleton.test.ts +27 -0
  413. package/src/resources/extensions/coworker-vault/vault-singleton.ts +40 -0
  414. package/src/resources/extensions/otto/commands/release-notes/_data.ts +85 -0
  415. package/src/resources/extensions/otto/commands/release-notes/command.ts +16 -3
  416. package/src/resources/extensions/subagent/index.ts +9 -0
  417. package/src/resources/extensions/subagent/launch.test.ts +97 -0
  418. package/src/resources/extensions/subagent/launch.ts +42 -5
  419. package/src/resources/extensions/subagent/run-store.ts +3 -1
  420. package/src/resources/extensions/workflow/bootstrap/register-extension.ts +2 -0
  421. package/src/resources/extensions/workflow/bootstrap/register-hooks.ts +10 -0
  422. package/src/resources/extensions/workflow/persona-status.ts +109 -0
  423. package/src/resources/extensions/workflow/tests/auto-recovery.test.ts +34 -0
@@ -0,0 +1,602 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { ExtensionAPI } from '@otto/pi-coding-agent';
4
+ import { ScratchpadBusyError, StalenessBanner } from '@otto/coworker-scratchpad';
5
+ import type { ScratchpadManager, RecoveryNote } from '@otto/coworker-scratchpad';
6
+ import { LocalDataVault } from '@otto/coworker-vault';
7
+ import { validateName, readCellsJsonl, readPersistedLeaf } from './helpers.js';
8
+ import { projectTree, formatTreeText } from '@otto/coworker-scratchpad';
9
+ import { sessionSidecarPath, writeSessionSidecar, deleteSessionSidecar, readSessionSidecar } from './session-sidecar.js';
10
+ import { detectWorkspaceRoot } from './workspace-root.js';
11
+ import { workspaceHash, workspacePointerPath, writeWorkspacePointer, type WorkspacePointer } from './workspace-pointer.js';
12
+ import { showRecoveryNotesBanner, showDivergenceBanner, formatNoteLine } from './attach-banners.js';
13
+ import { formatRelativeAge } from './format-age.js';
14
+
15
+ export interface SpDeps {
16
+ getManager: () => ScratchpadManager;
17
+ getCurrentName: () => string | null;
18
+ setCurrentName: (name: string | null) => void;
19
+ rootDir: () => string;
20
+ getSessionId: () => string;
21
+ /**
22
+ * Returns the workspace cwd captured at session_start. Using this instead of
23
+ * process.cwd() ensures the workspace pointer is anchored to the same root that
24
+ * session_start used, even if the process chdirs between session_start and a
25
+ * /sp command.
26
+ */
27
+ getWorkspaceCwd: () => string;
28
+ /**
29
+ * Phase 2 Task 16: optional vault hook for staleness-banner emission on
30
+ * attach. When provided, /sp attach reads meta.bindings and asks the vault
31
+ * for each binding's last-modified timestamp; refs that were modified after
32
+ * the spawned kernel's start time produce a one-shot banner (per scratchpad,
33
+ * per (session, ref)). Absent => /sp attach skips the staleness check
34
+ * silently, which is the right behavior for sessions running without vault.
35
+ *
36
+ * Wiring contract: the extension activator (or the vault-extension counterpart)
37
+ * is responsible for plumbing a closure that returns the live VaultBundle's
38
+ * `lookupLastModified` reference. Decoupled here so the scratchpad extension
39
+ * does not have to know about VaultBundle construction order.
40
+ */
41
+ getStalenessVault?: () => { lookupLastModified: (ref: string) => Promise<string | null> } | null;
42
+ }
43
+
44
+ type SpVerb = 'list' | 'new' | 'attach' | 'reset' | 'view' | 'remove' | 'tree' | 'fork' | 'save' | 'detach' | 'clear-history' | 'notes' | 'evict' | 'use' | 'unuse';
45
+ const VERBS: SpVerb[] = ['list', 'new', 'attach', 'reset', 'view', 'remove', 'tree', 'fork', 'save', 'detach', 'clear-history', 'notes', 'evict', 'use', 'unuse'];
46
+
47
+ /**
48
+ * Phase 2 Task 16: module-level singleton so banner one-shot state survives
49
+ * across multiple /sp invocations within one process. Reset on /sp reset for
50
+ * the affected scratchpad (the kernel respawns ⇒ stale tracking restarts).
51
+ *
52
+ * Per-test isolation: tests create distinct scratchpad names (or new tmp
53
+ * roots) so the per-(scratchpad, session, ref) key set never collides across
54
+ * tests. No reset-all escape hatch is exposed.
55
+ */
56
+ const stalenessBanner = new StalenessBanner();
57
+
58
+ function readBindingsFromMeta(metaPath: string): string[] {
59
+ if (!existsSync(metaPath)) return [];
60
+ try {
61
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8')) as { bindings?: unknown };
62
+ return Array.isArray(meta.bindings) ? (meta.bindings as string[]) : [];
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ function ensureCurrent(deps: SpDeps): string {
69
+ let current = deps.getCurrentName();
70
+ if (!current) {
71
+ current = 'default';
72
+ deps.setCurrentName(current);
73
+ }
74
+ return current;
75
+ }
76
+
77
+ function listExistingScratchpads(root: string): string[] {
78
+ if (!existsSync(root)) return [];
79
+ const names: string[] = [];
80
+ for (const entry of readdirSync(root)) {
81
+ const dir = join(root, entry);
82
+ try {
83
+ if (statSync(dir).isDirectory() && existsSync(join(dir, 'meta.json'))) names.push(entry);
84
+ } catch {
85
+ // entry vanished -> skip
86
+ }
87
+ }
88
+ return names.sort();
89
+ }
90
+
91
+ function formatCellSummary(rec: { id: number; ok: boolean; code: string; value?: unknown; error?: { message: string } }): string {
92
+ const head = rec.ok ? `cell ${rec.id} [ok]` : `cell ${rec.id} [err]`;
93
+ const value = rec.ok ? ` value=${JSON.stringify(rec.value)}` : ` error=${rec.error?.message ?? ''}`;
94
+ return `${head} ${rec.code.split('\n')[0].slice(0, 80)} ${value}`;
95
+ }
96
+
97
+ interface UiCtx {
98
+ hasUI: boolean;
99
+ ui: {
100
+ notify: (msg: string, level: 'info' | 'warning' | 'error') => void;
101
+ confirm: (title: string, msg: string) => Promise<boolean>;
102
+ input: (title: string, placeholder?: string) => Promise<string | undefined>;
103
+ };
104
+ }
105
+
106
+ function persistWorkspacePointer(deps: SpDeps, name: string): void {
107
+ const wsRoot = detectWorkspaceRoot(deps.getWorkspaceCwd());
108
+ const wsHash = workspaceHash(wsRoot);
109
+ const wsPath = workspacePointerPath(deps.rootDir(), wsHash);
110
+ const wsPayload: WorkspacePointer = {
111
+ schema_version: 1,
112
+ workspace_hash: wsHash,
113
+ workspace_root: wsRoot,
114
+ last_session_id: deps.getSessionId(),
115
+ last_current_name: name,
116
+ last_attached_at: new Date().toISOString(),
117
+ };
118
+ writeWorkspacePointer(wsPath, wsPayload);
119
+ }
120
+
121
+ function joinQuotedArg(parts: string[], startIdx: number): string | null {
122
+ if (startIdx >= parts.length) return null;
123
+ const first = parts[startIdx];
124
+ if (!first) return null;
125
+ if (!first.startsWith('"')) return first;
126
+ // Quoted: walk forward until we find a part ending with "
127
+ if (first.length > 1 && first.endsWith('"')) {
128
+ return first.slice(1, -1); // single-token quoted reason
129
+ }
130
+ const collected: string[] = [first.slice(1)]; // strip opening quote
131
+ for (let i = startIdx + 1; i < parts.length; i++) {
132
+ const p = parts[i] ?? '';
133
+ if (p.endsWith('"')) {
134
+ collected.push(p.slice(0, -1));
135
+ return collected.join(' ');
136
+ }
137
+ collected.push(p);
138
+ }
139
+ return collected.join(' '); // no closing quote — take rest
140
+ }
141
+
142
+ export function registerSpCommand(pi: ExtensionAPI, deps: SpDeps): void {
143
+ pi.registerCommand('sp', {
144
+ description: 'Manage scratchpads: /sp [list|new|attach|reset|view|remove|tree|fork|save|detach|clear-history|notes|evict|use|unuse] [name]',
145
+ getArgumentCompletions: (prefix: string) => {
146
+ // Split on whitespace but preserve whether the prefix ends with a space
147
+ // (trailing space = user typed the verb and hit space, ready for name completion).
148
+ const trimmed = prefix.trimStart();
149
+ const parts = trimmed.split(/\s+/);
150
+ // If trailing space: user has finished typing the verb, want name completions.
151
+ const trailingSpace = prefix.endsWith(' ');
152
+ if (parts.length <= 1 && !trailingSpace) {
153
+ return VERBS.filter((v) => v.startsWith(parts[0] ?? '')).map((v) => ({ value: v, label: v }));
154
+ }
155
+ const verb = parts[0];
156
+ if (verb === 'attach' || verb === 'reset' || verb === 'view' || verb === 'remove') {
157
+ const namePrefix = trailingSpace && parts.length === 1 ? '' : (parts[1] ?? '');
158
+ return listExistingScratchpads(deps.rootDir())
159
+ .filter((n) => n.startsWith(namePrefix))
160
+ .map((n) => ({ value: `${verb} ${n}`, label: n }));
161
+ }
162
+ return [];
163
+ },
164
+ handler: async (args: string, ctx: UiCtx) => {
165
+ const trimmed = args.trim();
166
+ const parts = trimmed.length === 0 ? [] : trimmed.split(/\s+/);
167
+ const verb = (parts[0] as SpVerb | undefined) ?? 'list';
168
+ const name = parts[1];
169
+
170
+ try {
171
+ switch (verb) {
172
+ case 'list': {
173
+ const mgr = deps.getManager();
174
+ const live = mgr.list();
175
+ const liveByName = new Map(live.map((e) => [e.name, e]));
176
+ const onDisk = listExistingScratchpads(deps.rootDir());
177
+ const all = Array.from(new Set([...liveByName.keys(), ...onDisk])).sort();
178
+ const cur = deps.getCurrentName();
179
+ if (all.length === 0) {
180
+ ctx.ui.notify('No scratchpads yet. Use /sp new <name> to create one.', 'info');
181
+ return;
182
+ }
183
+ // Task D: render idle-age column for warm entries. Compute `now` once so
184
+ // every row shares the same baseline.
185
+ const now = Date.now();
186
+ // Pad name column so the age column lines up. Cold entries have no age.
187
+ const maxNameLen = all.reduce((m, n) => Math.max(m, n.length), 0);
188
+ const lines = all.map((n) => {
189
+ const l = liveByName.get(n);
190
+ const state = l?.live ? '● live' : '○ cold';
191
+ const marker = n === cur ? ' (current)' : '';
192
+ let age = '';
193
+ if (l?.live) {
194
+ age = l.hasActiveCell ? 'active' : formatRelativeAge(now - l.lastUsedAt);
195
+ }
196
+ const namePadded = age ? n.padEnd(maxNameLen) : n;
197
+ const ageCol = age ? ` ${age}` : '';
198
+ // Phase 2 Task 16: binding count read from meta.json. Renders as
199
+ // `uses:N` after the age column so the existing live/cold and
200
+ // active/idle assertions stay intact. Hidden when 0 to avoid
201
+ // visual noise for scratchpads that aren't bound.
202
+ const bindingCount = readBindingsFromMeta(join(deps.rootDir(), n, 'meta.json')).length;
203
+ const bindingsCol = bindingCount > 0 ? ` uses:${bindingCount}` : '';
204
+ return ` ${state} ${namePadded}${ageCol}${bindingsCol}${marker}`;
205
+ });
206
+ ctx.ui.notify(['scratchpads:', ...lines].join('\n'), 'info');
207
+ return;
208
+ }
209
+ case 'new': {
210
+ if (!name) { ctx.ui.notify('Usage: /sp new <name> [--use <engine:name>] ...', 'error'); return; }
211
+ validateName(name);
212
+ // Phase 2 Task 16: parse --use flag. Repeatable: `--use jira:prod --use foo:bar`.
213
+ // Each ref is validated via LocalDataVault.parseRef before persisting; a single
214
+ // malformed ref aborts the whole create so we don't end up with a half-bound
215
+ // scratchpad on disk.
216
+ const bindings: string[] = [];
217
+ for (let i = 2; i < parts.length; i++) {
218
+ if (parts[i] === '--use') {
219
+ const ref = parts[i + 1];
220
+ if (!ref) { ctx.ui.notify('Usage: /sp new <name> --use <engine:name>', 'error'); return; }
221
+ try {
222
+ LocalDataVault.parseRef(ref);
223
+ } catch (err) {
224
+ ctx.ui.notify((err as Error).message, 'error');
225
+ return;
226
+ }
227
+ bindings.push(ref);
228
+ i++; // skip ref token
229
+ }
230
+ }
231
+ await deps.getManager().create(name, bindings.length > 0 ? { bindings } : {});
232
+ deps.setCurrentName(name);
233
+ writeSessionSidecar(sessionSidecarPath(deps.rootDir(), deps.getSessionId()), {
234
+ schema_version: 1,
235
+ session_id: deps.getSessionId(),
236
+ current_name: name,
237
+ attached_at: new Date().toISOString(),
238
+ });
239
+ persistWorkspacePointer(deps, name);
240
+ const suffix = bindings.length > 0 ? ` (bindings: ${bindings.join(', ')})` : '';
241
+ ctx.ui.notify(`created scratchpad: ${name} (now current)${suffix}`, 'info');
242
+ return;
243
+ }
244
+ case 'use': {
245
+ const target = name;
246
+ const ref = parts[2];
247
+ if (!target || !ref) { ctx.ui.notify('Usage: /sp use <name> <engine:name>', 'error'); return; }
248
+ validateName(target);
249
+ try {
250
+ LocalDataVault.parseRef(ref);
251
+ } catch (err) {
252
+ ctx.ui.notify((err as Error).message, 'error');
253
+ return;
254
+ }
255
+ try {
256
+ const { added } = await deps.getManager().addBinding(target, ref);
257
+ if (!added) {
258
+ ctx.ui.notify(`binding already present: ${ref} → ${target}`, 'info');
259
+ } else {
260
+ // Hint /sp reset because the live kernel was spawned without this
261
+ // env block; the binding only takes effect on the next respawn.
262
+ ctx.ui.notify(`binding added: ${ref} → ${target}. /sp reset to inject into the live kernel.`, 'info');
263
+ }
264
+ } catch (err) {
265
+ ctx.ui.notify((err as Error).message, 'error');
266
+ }
267
+ return;
268
+ }
269
+ case 'unuse': {
270
+ const target = name;
271
+ const ref = parts[2];
272
+ if (!target || !ref) { ctx.ui.notify('Usage: /sp unuse <name> <engine:name>', 'error'); return; }
273
+ validateName(target);
274
+ try {
275
+ const { removed } = await deps.getManager().removeBinding(target, ref);
276
+ if (!removed) {
277
+ ctx.ui.notify(`binding not present: ${ref} → ${target}`, 'info');
278
+ } else {
279
+ ctx.ui.notify(`binding removed: ${ref} from ${target}. /sp reset to drop the env block from the live kernel.`, 'info');
280
+ }
281
+ } catch (err) {
282
+ ctx.ui.notify((err as Error).message, 'error');
283
+ }
284
+ return;
285
+ }
286
+ case 'attach': {
287
+ if (!name) {
288
+ ctx.ui.notify('Usage: /sp attach <name> [--force-takeover] [--reason "<text>"]', 'error');
289
+ return;
290
+ }
291
+ validateName(name);
292
+ // Slash-command path is strict: error on typo instead of silently auto-creating
293
+ // (the LLM-tool path via cw_scratchpad action=exec stays permissive).
294
+ const metaPath = join(deps.rootDir(), name, 'meta.json');
295
+ if (!existsSync(metaPath)) {
296
+ ctx.ui.notify(
297
+ `scratchpad not found: ${name}. Use /sp new ${name} to create it.`,
298
+ 'error',
299
+ );
300
+ return;
301
+ }
302
+ const forceFlag = parts.includes('--force-takeover');
303
+ const reasonIdx = parts.indexOf('--reason');
304
+ const reasonArg = reasonIdx >= 0 ? joinQuotedArg(parts, reasonIdx + 1) : null;
305
+
306
+ let attached = false;
307
+ // Phase 2 Task 16: capture the runtime returned by getOrAttach so we
308
+ // can use its spawnTime as the staleness banner's clock. Defensive
309
+ // typing because some tests return null from a stubbed manager.
310
+ let runtime: { spawnTime?: Date } | null = null;
311
+ try {
312
+ runtime = (await deps.getManager().getOrAttach(name)) as { spawnTime?: Date } | null;
313
+ attached = true;
314
+ } catch (err) {
315
+ if (!(err instanceof ScratchpadBusyError)) {
316
+ ctx.ui.notify((err as Error).message, 'error');
317
+ return;
318
+ }
319
+ const holder = err.holder;
320
+ const proceed = forceFlag || await ctx.ui.confirm(
321
+ 'Force takeover?',
322
+ `${name}: lock held by pid ${holder.pid} on host ${holder.host} (acquired ${holder.acquired_at}). Take it?`,
323
+ );
324
+ if (!proceed) { ctx.ui.notify('cancelled', 'info'); return; }
325
+
326
+ let reason: string | null = reasonArg;
327
+ if (reason === null) {
328
+ const input = await ctx.ui.input('Takeover reason', 'why are you taking over?');
329
+ if (input === undefined) { ctx.ui.notify('cancelled', 'info'); return; }
330
+ reason = input.trim() || '(no reason given)';
331
+ }
332
+ try {
333
+ runtime = (await deps.getManager().getOrAttach(name, { forceTakeover: true, takeoverReason: reason })) as { spawnTime?: Date } | null;
334
+ attached = true;
335
+ } catch (retryErr) {
336
+ ctx.ui.notify((retryErr as Error).message, 'error');
337
+ return;
338
+ }
339
+ }
340
+ if (!attached) return;
341
+
342
+ deps.setCurrentName(name);
343
+ writeSessionSidecar(sessionSidecarPath(deps.rootDir(), deps.getSessionId()), {
344
+ schema_version: 1,
345
+ session_id: deps.getSessionId(),
346
+ current_name: name,
347
+ attached_at: new Date().toISOString(),
348
+ });
349
+ persistWorkspacePointer(deps, name);
350
+ ctx.ui.notify(`attached to scratchpad: ${name}`, 'info');
351
+
352
+ // §2 + §4 banners (1g2):
353
+ const { markSeen } = showRecoveryNotesBanner(name, deps.rootDir(), ctx.ui);
354
+ if (markSeen) {
355
+ await deps.getManager().markRecoveryNotesSeen(name);
356
+ }
357
+ showDivergenceBanner(name, deps.rootDir(), ctx.ui);
358
+
359
+ // Phase 2 Task 16: staleness banner. If a vault is wired AND this
360
+ // scratchpad has bindings, check each ref's last-modified vs the
361
+ // kernel's spawnTime; emit one-shot warning if any are stale.
362
+ // Skipped silently when no vault is configured — the scratchpad
363
+ // extension still works without coworker-vault present.
364
+ //
365
+ // spawnTime source: prefer the runtime's stamped Date from
366
+ // ChildProcessRuntime.start() (Task 13). Fallback to meta.json's
367
+ // mtime — also rewritten on every attach by writeMeta — when the
368
+ // runtime is null (stubbed manager paths in tests) or missing the
369
+ // spawnTime field. Both anchors converge on the same user-visible
370
+ // semantic: "your creds changed since you started this kernel."
371
+ if (deps.getStalenessVault) {
372
+ const vault = deps.getStalenessVault();
373
+ const bindings = readBindingsFromMeta(join(deps.rootDir(), name, 'meta.json'));
374
+ if (vault && bindings.length > 0) {
375
+ try {
376
+ const spawnTime = runtime?.spawnTime instanceof Date && runtime.spawnTime.getTime() > 0
377
+ ? runtime.spawnTime
378
+ : new Date(statSync(join(deps.rootDir(), name, 'meta.json')).mtime);
379
+ const banner = await stalenessBanner.check({
380
+ scratchpadName: name,
381
+ sessionId: deps.getSessionId(),
382
+ bindings,
383
+ spawnTime,
384
+ lookupLastModified: (ref) => vault.lookupLastModified(ref),
385
+ });
386
+ if (banner) ctx.ui.notify(banner, 'warning');
387
+ } catch {
388
+ // Banner emission must never block /sp attach — swallow errors.
389
+ }
390
+ }
391
+ }
392
+ return;
393
+ }
394
+ case 'reset': {
395
+ const target = name ?? ensureCurrent(deps);
396
+ validateName(target);
397
+ const mgr = deps.getManager();
398
+ // Phase 2 Task 16: preserve bindings across reset. remove() wipes
399
+ // the dir, then create() rewrites a fresh meta.json — without this
400
+ // pre-read the new meta.bindings would be []. Resetting is "respawn
401
+ // the kernel with current config", so the binding list is part of
402
+ // config that must survive.
403
+ const preservedBindings = mgr.readBindings(target);
404
+ await mgr.remove(target);
405
+ await mgr.create(target, preservedBindings.length > 0 ? { bindings: preservedBindings } : {});
406
+ // Phase 2 Task 16: clear the staleness-banner one-shot state for
407
+ // this scratchpad — the new kernel has a fresh spawnTime, so old
408
+ // "stale" status is no longer relevant.
409
+ stalenessBanner.resetForRespawn(target);
410
+ // currentName preserved if it was the reset target; otherwise unchanged
411
+ ctx.ui.notify(`reset scratchpad: ${target}`, 'info');
412
+ return;
413
+ }
414
+ case 'view': {
415
+ const target = name ?? ensureCurrent(deps);
416
+ validateName(target);
417
+ const { cells, total_cells } = readCellsJsonl(join(deps.rootDir(), target));
418
+ if (total_cells === 0) {
419
+ ctx.ui.notify(`${target}: no cells yet`, 'info');
420
+ return;
421
+ }
422
+ const tail = cells.slice(-10);
423
+ const lines = tail.map((c) => formatCellSummary(c));
424
+ ctx.ui.notify([`${target} (${total_cells} cells, last 10):`, ...lines].join('\n'), 'info');
425
+ return;
426
+ }
427
+ case 'remove': {
428
+ if (!name) { ctx.ui.notify('Usage: /sp remove <name> [--yes]', 'error'); return; }
429
+ const force = parts.includes('--yes');
430
+ validateName(name);
431
+ if (name === deps.getCurrentName() && !force) {
432
+ const confirmed = await ctx.ui.confirm(
433
+ 'Remove current scratchpad?',
434
+ `${name} is your current scratchpad. Remove it? This deletes kernel.db, namespace.json, and the cell journal.`,
435
+ );
436
+ if (!confirmed) { ctx.ui.notify('cancelled', 'info'); return; }
437
+ }
438
+ const wasCurrent = name === deps.getCurrentName();
439
+ await deps.getManager().remove(name);
440
+ if (wasCurrent) {
441
+ deleteSessionSidecar(sessionSidecarPath(deps.rootDir(), deps.getSessionId()));
442
+ deps.setCurrentName(null);
443
+ }
444
+ ctx.ui.notify(`removed scratchpad: ${name}`, 'info');
445
+ return;
446
+ }
447
+ case 'evict': {
448
+ if (!name) { ctx.ui.notify('Usage: /sp evict <name> [--force]', 'error'); return; }
449
+ const force = parts.includes('--force');
450
+ validateName(name);
451
+ try {
452
+ const { interrupted } = await deps.getManager().evict(name, { force });
453
+ const msg = interrupted
454
+ ? `interrupted active cell and evicted ${name}`
455
+ : `evicted ${name} (still on disk; /sp attach ${name} to re-warm)`;
456
+ ctx.ui.notify(msg, 'info');
457
+ } catch (e) {
458
+ ctx.ui.notify((e as Error).message, 'error');
459
+ }
460
+ return;
461
+ }
462
+ case 'tree': {
463
+ // Usage: /sp tree [<name>] [--to <id>]
464
+ const flagIdx = parts.indexOf('--to');
465
+ let target: string;
466
+ if (flagIdx === -1) {
467
+ target = name ?? ensureCurrent(deps);
468
+ } else {
469
+ target = flagIdx === 1 ? ensureCurrent(deps) : (parts[1] as string);
470
+ const toId = Number(parts[flagIdx + 1]);
471
+ if (!Number.isInteger(toId) || toId <= 0) {
472
+ ctx.ui.notify('Usage: /sp tree [<name>] --to <id>', 'error');
473
+ return;
474
+ }
475
+ validateName(target);
476
+ await deps.getManager().setLeaf(target, toId);
477
+ ctx.ui.notify(`set leaf of ${target} to cell ${toId}`, 'info');
478
+ return;
479
+ }
480
+ validateName(target);
481
+ const { cells } = readCellsJsonl(join(deps.rootDir(), target));
482
+ if (cells.length === 0) {
483
+ ctx.ui.notify(`${target}: no cells yet`, 'info');
484
+ return;
485
+ }
486
+ const tree = projectTree(cells);
487
+ const leaf = readPersistedLeaf(join(deps.rootDir(), target, 'meta.json'));
488
+ ctx.ui.notify(`${target} cell tree:\n${formatTreeText(tree, leaf)}`, 'info');
489
+ return;
490
+ }
491
+ case 'fork': {
492
+ // Usage: /sp fork <src> <dst>
493
+ if (parts.length < 3) { ctx.ui.notify('Usage: /sp fork <src> <dst>', 'error'); return; }
494
+ const src = parts[1]!;
495
+ const dst = parts[2]!;
496
+ validateName(src);
497
+ validateName(dst);
498
+ await deps.getManager().fork(src, dst);
499
+ ctx.ui.notify(`forked ${src} → ${dst}`, 'info');
500
+ return;
501
+ }
502
+ case 'save': {
503
+ const target = name ?? deps.getCurrentName();
504
+ if (!target) { ctx.ui.notify('Usage: /sp save [<name>] — no current scratchpad', 'error'); return; }
505
+ validateName(target);
506
+ await deps.getManager().save(target);
507
+ ctx.ui.notify(`saved ${target}`, 'info');
508
+ return;
509
+ }
510
+ case 'detach': {
511
+ const target = deps.getCurrentName();
512
+ if (!target) { ctx.ui.notify('not attached to any scratchpad', 'error'); return; }
513
+ await deps.getManager().detach(target, deps.getSessionId());
514
+ deleteSessionSidecar(sessionSidecarPath(deps.rootDir(), deps.getSessionId()));
515
+ deps.setCurrentName(null);
516
+ ctx.ui.notify(`detached from ${target}`, 'info');
517
+ return;
518
+ }
519
+ case 'clear-history': {
520
+ const target = name ?? deps.getCurrentName();
521
+ if (!target) { ctx.ui.notify('Usage: /sp clear-history [<name>] — no current scratchpad', 'error'); return; }
522
+ validateName(target);
523
+ const confirmed = await ctx.ui.confirm(
524
+ 'Clear cell history?',
525
+ `Clear cell history for ${target}? kernel.db + namespace.json are preserved.`,
526
+ );
527
+ if (!confirmed) { ctx.ui.notify('cancelled', 'info'); return; }
528
+ await deps.getManager().clearHistory(target);
529
+ ctx.ui.notify(`cleared cell history for ${target}`, 'info');
530
+ return;
531
+ }
532
+ case 'notes': {
533
+ const target = name ?? deps.getCurrentName();
534
+ if (!target) {
535
+ ctx.ui.notify('Usage: /sp notes [<name>] (no current scratchpad)', 'error');
536
+ return;
537
+ }
538
+ validateName(target);
539
+ const metaPath = join(deps.rootDir(), target, 'meta.json');
540
+ if (!existsSync(metaPath)) {
541
+ ctx.ui.notify(`scratchpad not found: ${target}`, 'error');
542
+ return;
543
+ }
544
+ let meta: Record<string, unknown>;
545
+ try {
546
+ meta = JSON.parse(readFileSync(metaPath, 'utf8')) as Record<string, unknown>;
547
+ } catch {
548
+ ctx.ui.notify(`${target}: meta.json unreadable`, 'error');
549
+ return;
550
+ }
551
+ type RecoveryNoteEntry = RecoveryNote & { at: string };
552
+ const notes = Array.isArray(meta.recovery_notes) ? (meta.recovery_notes as RecoveryNoteEntry[]) : [];
553
+ if (notes.length === 0) {
554
+ ctx.ui.notify(`no recovery notes for ${target}`, 'info');
555
+ return;
556
+ }
557
+ const lines = notes.map(formatNoteLine);
558
+ ctx.ui.notify(`${target} recovery notes (${notes.length}):\n${lines.join('\n')}`, 'info');
559
+ // Deliberately does NOT update recovery_notes_seen_at — re-view path is read-only.
560
+ return;
561
+ }
562
+ default: {
563
+ ctx.ui.notify(`unknown verb: ${verb}. Try one of: ${VERBS.join(', ')}`, 'error');
564
+ }
565
+ }
566
+ } catch (err) {
567
+ ctx.ui.notify((err as Error).message, 'error');
568
+ }
569
+ },
570
+ });
571
+ }
572
+
573
+ /**
574
+ * Phase 3 Task 18: exposes a session→scratchpad-name lookup for cross-pillar
575
+ * consumers (memory's MemoryRecorder uses this to derive the default Room).
576
+ *
577
+ * Reads the per-session sidecar written by /sp new and /sp attach. Returns
578
+ * null when no scratchpad is currently attached for `sessionId`, when
579
+ * `sessionId` is empty/undefined, or on any IO/parse error — callers must
580
+ * tolerate null (the caller-side fallback is "use workspace as Room").
581
+ *
582
+ * Decoupled from `SpDeps` so consumers outside the slash-command lifecycle
583
+ * (e.g. recorders constructed at extension activation) can call it without
584
+ * holding a reference to the scratchpad manager.
585
+ *
586
+ * Note on the sidecar reader's actual signature: `readSessionSidecar(path)`
587
+ * takes a fully-resolved path, not `{scratchpadsRoot, sessionId}`. We compose
588
+ * with `sessionSidecarPath(rootDir, sessionId)` to bridge.
589
+ */
590
+ export function createCurrentScratchpadProvider(opts: {
591
+ scratchpadsRoot: string;
592
+ }): (sessionId: string) => string | null {
593
+ return (sessionId: string) => {
594
+ if (!sessionId) return null;
595
+ try {
596
+ const sidecar = readSessionSidecar(sessionSidecarPath(opts.scratchpadsRoot, sessionId));
597
+ return sidecar?.current_name ?? null;
598
+ } catch {
599
+ return null;
600
+ }
601
+ };
602
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import {
7
+ workspaceHash,
8
+ workspacePointerPath,
9
+ readWorkspacePointer,
10
+ writeWorkspacePointer,
11
+ isPointerFresh,
12
+ WORKSPACE_POINTER_STALE_MS,
13
+ type WorkspacePointer,
14
+ } from './workspace-pointer.js';
15
+
16
+ describe('workspace-pointer', () => {
17
+ it('writes and round-trip reads a pointer', () => {
18
+ const root = mkdtempSync(join(tmpdir(), 'wsp-'));
19
+ try {
20
+ const hash = workspaceHash('/home/me/project');
21
+ const path = workspacePointerPath(root, hash);
22
+ const payload: WorkspacePointer = {
23
+ schema_version: 1,
24
+ workspace_hash: hash,
25
+ workspace_root: '/home/me/project',
26
+ last_session_id: 'sess-A',
27
+ last_current_name: 't04-tree',
28
+ last_attached_at: '2026-06-01T12:00:00.000Z',
29
+ };
30
+ writeWorkspacePointer(path, payload);
31
+ assert.deepEqual(readWorkspacePointer(path), payload);
32
+ } finally {
33
+ rmSync(root, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ it('isPointerFresh respects the 7-day boundary', () => {
38
+ const base: WorkspacePointer = {
39
+ schema_version: 1,
40
+ workspace_hash: 'h',
41
+ workspace_root: '/x',
42
+ last_session_id: 's',
43
+ last_current_name: 'n',
44
+ last_attached_at: '2026-06-01T00:00:00.000Z',
45
+ };
46
+ const at0 = Date.parse(base.last_attached_at);
47
+ assert.equal(isPointerFresh(base, at0 + WORKSPACE_POINTER_STALE_MS - 1), true);
48
+ assert.equal(isPointerFresh(base, at0 + WORKSPACE_POINTER_STALE_MS), false);
49
+ assert.equal(isPointerFresh(base, at0 + WORKSPACE_POINTER_STALE_MS + 1000), false);
50
+ });
51
+
52
+ it('returns null for corrupt JSON or missing schema_version', () => {
53
+ const root = mkdtempSync(join(tmpdir(), 'wsp-bad-'));
54
+ try {
55
+ const path = workspacePointerPath(root, 'abc');
56
+ mkdirSync(dirname(path), { recursive: true });
57
+ writeFileSync(path, 'not valid json at all');
58
+ assert.equal(readWorkspacePointer(path), null);
59
+ writeFileSync(path, JSON.stringify({ schema_version: 99, foo: 'bar' }));
60
+ assert.equal(readWorkspacePointer(path), null);
61
+ } finally {
62
+ rmSync(root, { recursive: true, force: true });
63
+ }
64
+ });
65
+
66
+ it('workspaceHash is deterministic 16-char hex and varies by input', () => {
67
+ const a = workspaceHash('/home/me/projA');
68
+ const b = workspaceHash('/home/me/projB');
69
+ assert.equal(a.length, 16);
70
+ assert.match(a, /^[0-9a-f]{16}$/);
71
+ assert.notEqual(a, b);
72
+ assert.equal(workspaceHash('/home/me/projA'), a);
73
+ });
74
+ });