@wrongstack/core 0.313.0 → 0.316.1

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 (343) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/legacy-journal-import.d.ts +0 -6
  3. package/dist/chronicle/metrics-ingest.d.ts +14 -0
  4. package/dist/chronicle/metrics-schema.d.ts +10 -1
  5. package/dist/chronicle/metrics-store.d.ts +20 -2
  6. package/dist/chronicle/project-server.js +98 -4874
  7. package/dist/chronicle/sink.d.ts +13 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +20 -2
  9. package/dist/chunk-25LSLC3G.js +334 -0
  10. package/dist/chunk-27QIURLN.js +1073 -0
  11. package/dist/chunk-2FK4WTFF.js +207 -0
  12. package/dist/chunk-2YU4WVBW.js +134 -0
  13. package/dist/chunk-3AGWVSS7.js +4608 -0
  14. package/dist/chunk-3BDV7RL3.js +211 -0
  15. package/dist/chunk-3ECENVHL.js +325 -0
  16. package/dist/chunk-3XPVB7BA.js +135 -0
  17. package/dist/chunk-45WI72ZB.js +16884 -0
  18. package/dist/chunk-4BMVPOHO.js +3748 -0
  19. package/dist/chunk-4I7HGVVA.js +363 -0
  20. package/dist/chunk-4LCZVJVF.js +317 -0
  21. package/dist/chunk-4SJNS7OB.js +217 -0
  22. package/dist/chunk-4VWO3ZXL.js +342 -0
  23. package/dist/chunk-57GEWK5X.js +26 -0
  24. package/dist/chunk-5BMIE7QG.js +46 -0
  25. package/dist/chunk-5S437PRC.js +50 -0
  26. package/dist/chunk-5XDUGTKN.js +511 -0
  27. package/dist/chunk-5YPSXQXM.js +1443 -0
  28. package/dist/chunk-6FHWXLPT.js +10 -0
  29. package/dist/chunk-6H5GEOET.js +2406 -0
  30. package/dist/chunk-6NWEVFEL.js +151 -0
  31. package/dist/chunk-AYW5YINX.js +58 -0
  32. package/dist/chunk-B4AMLZPB.js +9 -0
  33. package/dist/chunk-B7F4WVP4.js +65 -0
  34. package/dist/chunk-BJHVULHO.js +55 -0
  35. package/dist/chunk-BPBKKBUK.js +2085 -0
  36. package/dist/chunk-BYD345OX.js +589 -0
  37. package/dist/chunk-C27KDLOX.js +279 -0
  38. package/dist/chunk-CU546ESW.js +597 -0
  39. package/dist/chunk-DLHHXUAM.js +172 -0
  40. package/dist/chunk-DOTHQSJ2.js +429 -0
  41. package/dist/chunk-DYLPMV4Q.js +711 -0
  42. package/dist/chunk-EODDVJBM.js +82 -0
  43. package/dist/chunk-F3JWNST7.js +361 -0
  44. package/dist/chunk-FC2ZGRF3.js +140 -0
  45. package/dist/chunk-FCBTO76Z.js +5786 -0
  46. package/dist/chunk-FHZVFYSL.js +396 -0
  47. package/dist/chunk-FNXVT7HF.js +52 -0
  48. package/dist/chunk-FWNLEZYG.js +259 -0
  49. package/dist/chunk-FXQLIX3K.js +211 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GDQ7CZRQ.js +116 -0
  52. package/dist/chunk-GJJGJB2R.js +1374 -0
  53. package/dist/chunk-GTCU6DZ3.js +1 -0
  54. package/dist/chunk-HA4ZYA6T.js +178 -0
  55. package/dist/chunk-HI3JGZR2.js +188 -0
  56. package/dist/chunk-HMLIGAU7.js +395 -0
  57. package/dist/chunk-HXKZ7OBQ.js +303 -0
  58. package/dist/chunk-I7DALG7J.js +292 -0
  59. package/dist/chunk-IRUTEHCE.js +960 -0
  60. package/dist/chunk-IYFBFX34.js +86 -0
  61. package/dist/chunk-J2WRWR37.js +1104 -0
  62. package/dist/chunk-JFPJ6LLQ.js +129 -0
  63. package/dist/chunk-JMULFFLN.js +9 -0
  64. package/dist/chunk-JO4MGB6Z.js +1264 -0
  65. package/dist/chunk-JVKB2TBL.js +48 -0
  66. package/dist/chunk-JX7VBZDK.js +31 -0
  67. package/dist/chunk-K6BXGITA.js +931 -0
  68. package/dist/chunk-KDQJCT2J.js +45 -0
  69. package/dist/chunk-KJ2ZTQSL.js +47 -0
  70. package/dist/chunk-KZMSZV32.js +187 -0
  71. package/dist/chunk-LA7CLDRW.js +129 -0
  72. package/dist/chunk-LHOA5DWA.js +28 -0
  73. package/dist/chunk-LSMMOS2Z.js +119 -0
  74. package/dist/chunk-LX5QWLBL.js +27 -0
  75. package/dist/chunk-LXWCMQNJ.js +789 -0
  76. package/dist/chunk-M27LIEKV.js +2829 -0
  77. package/dist/chunk-MJPHJRAL.js +663 -0
  78. package/dist/chunk-MTL4O5YR.js +228 -0
  79. package/dist/chunk-NDBIPFNY.js +102 -0
  80. package/dist/chunk-NEMIWEXC.js +1199 -0
  81. package/dist/chunk-NHJ75HXY.js +1 -0
  82. package/dist/chunk-NIOAGFPB.js +1510 -0
  83. package/dist/chunk-NS52ISWS.js +355 -0
  84. package/dist/chunk-NSOFPKGT.js +5352 -0
  85. package/dist/chunk-NVIZG63R.js +757 -0
  86. package/dist/chunk-O37BQ65S.js +21 -0
  87. package/dist/chunk-OOYZPOXM.js +308 -0
  88. package/dist/chunk-OSGLYW5J.js +28 -0
  89. package/dist/chunk-PFCMSSUF.js +1640 -0
  90. package/dist/chunk-PFZO3C6H.js +14 -0
  91. package/dist/chunk-PGRVXIMY.js +158 -0
  92. package/dist/chunk-PHH7LD45.js +113 -0
  93. package/dist/chunk-PISBFBJN.js +109 -0
  94. package/dist/chunk-POOOIWE5.js +9 -0
  95. package/dist/chunk-PQNS3U7Z.js +1240 -0
  96. package/dist/chunk-QSTSJOCK.js +402 -0
  97. package/dist/chunk-RFQMOKWK.js +282 -0
  98. package/dist/chunk-RJIZEJ42.js +30 -0
  99. package/dist/chunk-RTOTNK3P.js +484 -0
  100. package/dist/chunk-RXKSDUAU.js +1172 -0
  101. package/dist/chunk-S45JPKQN.js +3317 -0
  102. package/dist/chunk-SAFHACFA.js +152 -0
  103. package/dist/chunk-SQJVFEBV.js +41 -0
  104. package/dist/chunk-SVT4RNTR.js +49 -0
  105. package/dist/chunk-SWUFN7XM.js +43 -0
  106. package/dist/chunk-SXMVYC5H.js +174 -0
  107. package/dist/chunk-SZ6AYUIZ.js +23 -0
  108. package/dist/chunk-TBRL2RCC.js +9 -0
  109. package/dist/chunk-TCCHINEF.js +655 -0
  110. package/dist/chunk-THEGTRAG.js +316 -0
  111. package/dist/chunk-TR4LQE7L.js +2256 -0
  112. package/dist/chunk-TRT2HRWZ.js +18 -0
  113. package/dist/chunk-TWJMGXCI.js +900 -0
  114. package/dist/chunk-U45AXT5D.js +1023 -0
  115. package/dist/chunk-U4FWBSO3.js +509 -0
  116. package/dist/chunk-UBGDX6FI.js +147 -0
  117. package/dist/chunk-UJILKYES.js +75 -0
  118. package/dist/chunk-ULMAQV3T.js +234 -0
  119. package/dist/chunk-VVBP6KGT.js +9 -0
  120. package/dist/chunk-W5VITDM2.js +168 -0
  121. package/dist/chunk-WAHQVNWT.js +194 -0
  122. package/dist/chunk-WBPVKEEE.js +227 -0
  123. package/dist/chunk-WGGYRBXH.js +5434 -0
  124. package/dist/chunk-WLYKU45P.js +5770 -0
  125. package/dist/chunk-WP7BVANJ.js +53 -0
  126. package/dist/chunk-WQQON3SV.js +473 -0
  127. package/dist/chunk-WQTOVMGV.js +119 -0
  128. package/dist/chunk-XBDP4QPW.js +7 -0
  129. package/dist/chunk-XEBHSJLT.js +103 -0
  130. package/dist/chunk-XF4VUMKW.js +231 -0
  131. package/dist/chunk-XQ2ECNK3.js +1 -0
  132. package/dist/chunk-Y5YXDM7D.js +74 -0
  133. package/dist/chunk-YOPAUVU4.js +2588 -0
  134. package/dist/chunk-YPT4A46F.js +362 -0
  135. package/dist/chunk-YWXT3JNM.js +425 -0
  136. package/dist/chunk-YY6APYUM.js +49 -0
  137. package/dist/chunk-Z2IJUO45.js +691 -0
  138. package/dist/chunk-ZHZ2Y2DH.js +1643 -0
  139. package/dist/chunk-ZT7JNIGX.js +152 -0
  140. package/dist/coordination/agent-monitor.d.ts +9 -1
  141. package/dist/coordination/agent-subagent-runner.d.ts +1 -1
  142. package/dist/coordination/agents/agent-prompts.d.ts +8 -0
  143. package/dist/coordination/agents/index.d.ts +33 -1
  144. package/dist/coordination/agents/index.js +184 -6269
  145. package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
  146. package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
  147. package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
  148. package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
  149. package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
  150. package/dist/coordination/agents/types.d.ts +13 -2
  151. package/dist/coordination/brain-monitor.d.ts +30 -7
  152. package/dist/coordination/collab-debug.d.ts +1 -1
  153. package/dist/coordination/collab-pause.d.ts +1 -1
  154. package/dist/coordination/delegate-tool.d.ts +7 -5
  155. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
  156. package/dist/coordination/director-options.d.ts +7 -0
  157. package/dist/coordination/director-spawn-model.d.ts +17 -0
  158. package/dist/coordination/director-tools.d.ts +2 -2
  159. package/dist/coordination/director.d.ts +12 -0
  160. package/dist/coordination/dispatcher.d.ts +8 -0
  161. package/dist/coordination/fleet-bus.d.ts +35 -4
  162. package/dist/coordination/fleet-supervisor.d.ts +10 -2
  163. package/dist/coordination/fleet.d.ts +16 -7
  164. package/dist/coordination/index.d.ts +4 -0
  165. package/dist/coordination/index.js +402 -34324
  166. package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
  167. package/dist/coordination/mailbox-http-router.d.ts +4 -3
  168. package/dist/coordination/mailbox-http-validation.d.ts +1 -1
  169. package/dist/coordination/mailbox-project-server.js +48 -3276
  170. package/dist/coordination/mailbox-types.d.ts +24 -6
  171. package/dist/coordination/model-tier-leader.d.ts +122 -0
  172. package/dist/coordination/model-tier.d.ts +122 -0
  173. package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
  174. package/dist/coordination/origin-session.d.ts +16 -0
  175. package/dist/coordination/provider-status-tracker.d.ts +41 -5
  176. package/dist/coordination/session-note-hub.d.ts +44 -0
  177. package/dist/coordination/session-note-tool.d.ts +10 -0
  178. package/dist/core/agent.d.ts +15 -2
  179. package/dist/core/context.d.ts +22 -3
  180. package/dist/core/index.d.ts +2 -0
  181. package/dist/core/index.js +239 -12360
  182. package/dist/core/model-ref.js +5 -23
  183. package/dist/core/provider-runner.d.ts +36 -1
  184. package/dist/core/request-conversation-binding.d.ts +37 -0
  185. package/dist/core/session-notes.d.ts +25 -0
  186. package/dist/core/system-prompt-builder.d.ts +11 -2
  187. package/dist/core/system-prompt-variants.d.ts +78 -0
  188. package/dist/design/index.js +50 -1087
  189. package/dist/execution/auto-compaction-middleware.d.ts +11 -0
  190. package/dist/execution/autonomy-brain.d.ts +2 -1
  191. package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
  192. package/dist/execution/compaction-core.d.ts +4 -4
  193. package/dist/execution/eternal-autonomy.d.ts +1 -1
  194. package/dist/execution/index.d.ts +1 -1
  195. package/dist/execution/index.js +1615 -21895
  196. package/dist/execution/model-runtime.d.ts +0 -7
  197. package/dist/execution/parallel-eternal-engine.d.ts +7 -7
  198. package/dist/execution/prompt-enhancer.js +16 -535
  199. package/dist/execution/retry-policy.d.ts +31 -1
  200. package/dist/extension/index.js +7 -311
  201. package/dist/goal/index.js +33 -3435
  202. package/dist/hooks/index.js +14 -1138
  203. package/dist/hq/index.js +114 -5139
  204. package/dist/hq/persistence/event-log.d.ts +0 -1
  205. package/dist/hq/persistence.d.ts +4 -4
  206. package/dist/hq/protocol.js +21 -668
  207. package/dist/hq/publisher.d.ts +2 -0
  208. package/dist/index.d.ts +14 -11
  209. package/dist/index.js +1502 -92655
  210. package/dist/infrastructure/index.d.ts +4 -4
  211. package/dist/infrastructure/index.js +504 -5230
  212. package/dist/infrastructure/token-counter.d.ts +9 -0
  213. package/dist/kernel/events/agent-events.d.ts +77 -3
  214. package/dist/kernel/events/brain-events.d.ts +12 -0
  215. package/dist/kernel/events/provider-events.d.ts +22 -1
  216. package/dist/kernel/events/session-events.d.ts +14 -0
  217. package/dist/kernel/events/tool-events.d.ts +13 -0
  218. package/dist/kernel/index.js +14 -992
  219. package/dist/mailbox-attach.d.ts +1 -1
  220. package/dist/models/index.js +41 -1117
  221. package/dist/notifications/index.js +3 -108
  222. package/dist/observability/event-bridge.d.ts +25 -5
  223. package/dist/observability/index.d.ts +9 -8
  224. package/dist/observability/index.js +477 -631
  225. package/dist/observability/tool-usage-source.d.ts +65 -0
  226. package/dist/plugin/index.d.ts +5 -0
  227. package/dist/plugin/index.js +600 -14835
  228. package/dist/plugins/auto-review-plugin.d.ts +3 -2
  229. package/dist/plugins/review-finding-commands.d.ts +0 -8
  230. package/dist/plugins/review-finding-store.js +22 -0
  231. package/dist/plugins/review-finding-types.js +19 -0
  232. package/dist/plugins/review-report-integration.js +21 -0
  233. package/dist/plugins/review-report-store.js +20 -0
  234. package/dist/plugins/review-report-types.js +13 -0
  235. package/dist/prompts/index.js +14 -948
  236. package/dist/registry/index.js +9 -822
  237. package/dist/registry/tool-registry.d.ts +59 -9
  238. package/dist/registry-7N4O3E7W.js +14 -0
  239. package/dist/replay/index.js +6 -84
  240. package/dist/security/capabilities.d.ts +6 -0
  241. package/dist/security/file-permissions.d.ts +1 -1
  242. package/dist/security/index.js +161 -2235
  243. package/dist/security/permission-policy.d.ts +12 -1
  244. package/dist/security/secret-scrubber.d.ts +15 -0
  245. package/dist/session-catalog/index.d.ts +1 -0
  246. package/dist/session-catalog/index.js +41 -2103
  247. package/dist/session-catalog/project-server.js +59 -1435
  248. package/dist/session-catalog/protocol.d.ts +32 -1
  249. package/dist/session-catalog/registry.d.ts +17 -1
  250. package/dist/session-catalog/session-agents.d.ts +56 -0
  251. package/dist/session-catalog/store-rebuild.d.ts +14 -0
  252. package/dist/session-catalog/store-schema.d.ts +26 -0
  253. package/dist/session-catalog/store.d.ts +27 -1
  254. package/dist/session-note-attach.d.ts +9 -0
  255. package/dist/skills/index.js +35 -1578
  256. package/dist/statusline/index.d.ts +43 -0
  257. package/dist/statusline/index.js +102 -0
  258. package/dist/storage/board-store-port.d.ts +2 -2
  259. package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
  260. package/dist/storage/config-loader.d.ts +3 -2
  261. package/dist/storage/file-session-writer.d.ts +4 -2
  262. package/dist/storage/goal-coordination.d.ts +6 -1
  263. package/dist/storage/goal-kanban.d.ts +4 -1
  264. package/dist/storage/index.d.ts +12 -4
  265. package/dist/storage/index.js +198 -15601
  266. package/dist/storage/input-history-store.d.ts +2 -4
  267. package/dist/storage/memory-curator.d.ts +133 -0
  268. package/dist/storage/session-agent-attribution.d.ts +39 -0
  269. package/dist/storage/session-doctor.d.ts +109 -0
  270. package/dist/storage/session-event-bridge.d.ts +5 -7
  271. package/dist/storage/session-outcome.d.ts +42 -0
  272. package/dist/storage/session-recovery.d.ts +55 -0
  273. package/dist/storage/session-store/load-session-data.d.ts +11 -0
  274. package/dist/storage/session-store/prune-helpers.d.ts +1 -1
  275. package/dist/storage/session-store/resume-session.d.ts +0 -2
  276. package/dist/storage/session-store/types.d.ts +8 -0
  277. package/dist/storage/session-store.d.ts +3 -3
  278. package/dist/storage/session-summary-tracker.d.ts +8 -1
  279. package/dist/storage/session-write-buffer.d.ts +18 -0
  280. package/dist/storage/session-writer-truncate.d.ts +2 -0
  281. package/dist/tasking/index.js +10 -676
  282. package/dist/tools/fallback-manage-tools.d.ts +6 -4
  283. package/dist/tools/index.js +52 -9590
  284. package/dist/tools/model-tier-set-tool.d.ts +66 -0
  285. package/dist/types/agent-bridge.d.ts +2 -1
  286. package/dist/types/config/autonomy.d.ts +5 -0
  287. package/dist/types/config/mcp-features.d.ts +7 -0
  288. package/dist/types/config/model-tiers.d.ts +148 -0
  289. package/dist/types/config/root.d.ts +9 -0
  290. package/dist/types/config/tools.d.ts +73 -0
  291. package/dist/types/config/ui.d.ts +1 -1
  292. package/dist/types/config.d.ts +1 -0
  293. package/dist/types/context.d.ts +2 -0
  294. package/dist/types/default-config.d.ts +6 -0
  295. package/dist/types/errors.d.ts +20 -1
  296. package/dist/types/index.d.ts +11 -9
  297. package/dist/types/index.js +131 -1342
  298. package/dist/types/multi-agent.d.ts +43 -0
  299. package/dist/types/plugin.d.ts +1 -1
  300. package/dist/types/provider-runner.d.ts +8 -0
  301. package/dist/types/provider.d.ts +15 -0
  302. package/dist/types/runtime-capability-manifest.d.ts +1 -1
  303. package/dist/types/secret-scrubber.d.ts +15 -0
  304. package/dist/types/session-markers.d.ts +37 -10
  305. package/dist/types/session-markers.js +8 -144
  306. package/dist/types/session-timeline.d.ts +197 -0
  307. package/dist/types/session-timeline.js +12 -0
  308. package/dist/types/session.d.ts +172 -11
  309. package/dist/types/system-prompt.d.ts +27 -0
  310. package/dist/utils/config-backup.d.ts +9 -1
  311. package/dist/utils/error.js +3 -4
  312. package/dist/utils/expect-defined.js +3 -9
  313. package/dist/utils/heap-watchdog.js +6 -706
  314. package/dist/utils/index.js +271 -5685
  315. package/dist/utils/message-invariants.d.ts +4 -1
  316. package/dist/utils/regex-guard.d.ts +1 -1
  317. package/dist/utils/sage-output-block.js +5 -42
  318. package/dist/utils/socket-path.d.ts +1 -1
  319. package/dist/utils/token-estimate.d.ts +6 -2
  320. package/dist/utils/wstack-paths.d.ts +2 -0
  321. package/dist/wiring/proxy-rewrite.d.ts +46 -0
  322. package/dist/wiring/proxy-rewrite.js +24 -121
  323. package/dist/worktree/index.js +6 -887
  324. package/instructions/agents/explore-companion.md +45 -18
  325. package/instructions/agents/memory-curator.md +34 -22
  326. package/instructions/coordination/director-preamble.md +12 -7
  327. package/instructions/coordination/subagent-baseline.md +26 -5
  328. package/instructions/leader-after-task.md +4 -1
  329. package/instructions/llm/chimera-review.md +30 -9
  330. package/instructions/llm/memory-curator.md +31 -0
  331. package/instructions/sections/tool/delegation-compact.md +2 -0
  332. package/instructions/sections/tool/delegation-full.md +5 -1
  333. package/instructions/sections/tool/mailbox-compact.md +5 -1
  334. package/instructions/sections/tool/mailbox-full.md +3 -0
  335. package/instructions/sections/tool/session-note-compact.md +3 -0
  336. package/instructions/sections/tool/session-note-full.md +15 -0
  337. package/instructions/system-lite.md +9 -4
  338. package/instructions/system-pro.md +66 -143
  339. package/instructions/system.md +73 -175
  340. package/package.json +32 -4
  341. package/dist/coordination/agents.d.ts +0 -8
  342. package/dist/coordination/transport.d.ts +0 -2
  343. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
@@ -0,0 +1,4608 @@
1
+ import {
2
+ SkillInstaller,
3
+ bodyLineAdvisory,
4
+ createSkillsShAdapter,
5
+ extractSkillFromPrompt,
6
+ generateSkillSkeleton,
7
+ githubDirectAdapter,
8
+ openInEditor,
9
+ validateSkillNameAvailable,
10
+ writeSkeletonSkill
11
+ } from "./chunk-RXKSDUAU.js";
12
+ import {
13
+ ALL_SYNC_CATEGORIES,
14
+ CloudConfigSync,
15
+ CloudSync,
16
+ PromptUsageStore
17
+ } from "./chunk-GJJGJB2R.js";
18
+ import {
19
+ decryptConfigSecrets,
20
+ encryptConfigSecrets
21
+ } from "./chunk-YY6APYUM.js";
22
+ import {
23
+ resolvePluginManifestConfig,
24
+ validatePluginConfigMetadata
25
+ } from "./chunk-3XPVB7BA.js";
26
+ import {
27
+ createChronicleProjectAccess
28
+ } from "./chunk-LXWCMQNJ.js";
29
+ import {
30
+ syncReportCompletion,
31
+ syncReportReopen
32
+ } from "./chunk-YWXT3JNM.js";
33
+ import {
34
+ FINDING_DEFAULT_PAGE_SIZE
35
+ } from "./chunk-4LCZVJVF.js";
36
+ import {
37
+ DefaultPromptLoader,
38
+ renderPrompt
39
+ } from "./chunk-XF4VUMKW.js";
40
+ import {
41
+ FOREIGN_SKILL_TOOLS,
42
+ securityScoreToTier
43
+ } from "./chunk-JVKB2TBL.js";
44
+ import {
45
+ DefaultPromptStore,
46
+ migratePromptEntry
47
+ } from "./chunk-PGRVXIMY.js";
48
+ import {
49
+ readJsonObjectFile,
50
+ validateAgainstSchema
51
+ } from "./chunk-YPT4A46F.js";
52
+ import {
53
+ readBundledInstructionText,
54
+ renderInstructionTemplate
55
+ } from "./chunk-5S437PRC.js";
56
+ import {
57
+ ExtensionRegistry
58
+ } from "./chunk-MTL4O5YR.js";
59
+ import {
60
+ FallbackProfileManager
61
+ } from "./chunk-WQQON3SV.js";
62
+ import {
63
+ capSkillBody
64
+ } from "./chunk-M27LIEKV.js";
65
+ import {
66
+ color
67
+ } from "./chunk-4VWO3ZXL.js";
68
+ import {
69
+ isPidAlive
70
+ } from "./chunk-TRT2HRWZ.js";
71
+ import {
72
+ atomicWrite
73
+ } from "./chunk-OSGLYW5J.js";
74
+ import {
75
+ stripFrontmatter
76
+ } from "./chunk-FC2ZGRF3.js";
77
+ import {
78
+ projectSlug,
79
+ resolveWstackPaths
80
+ } from "./chunk-SAFHACFA.js";
81
+ import {
82
+ parseModelRef
83
+ } from "./chunk-RJIZEJ42.js";
84
+ import {
85
+ expectDefined
86
+ } from "./chunk-PFZO3C6H.js";
87
+ import {
88
+ ERROR_CODES,
89
+ PluginError
90
+ } from "./chunk-25LSLC3G.js";
91
+ import {
92
+ toErrorMessage
93
+ } from "./chunk-TBRL2RCC.js";
94
+
95
+ // src/plugin/loader.ts
96
+ var legacyRegistrations = /* @__PURE__ */ new WeakMap();
97
+ var PluginLifecycleTimeoutError = class extends Error {
98
+ constructor(pluginName, phase, timeoutMs) {
99
+ super(`Plugin "${pluginName}" ${phase} exceeded ${timeoutMs} ms`);
100
+ this.pluginName = pluginName;
101
+ this.phase = phase;
102
+ this.timeoutMs = timeoutMs;
103
+ this.name = "PluginLifecycleTimeoutError";
104
+ }
105
+ pluginName;
106
+ phase;
107
+ timeoutMs;
108
+ };
109
+ var KERNEL_API_VERSION = "0.1.10";
110
+ function parseSemver(v) {
111
+ const parts = v.replace(/^[^0-9]*/, "").split(".").map((s) => Number.parseInt(s, 10) || 0);
112
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
113
+ }
114
+ function satisfies(range, version) {
115
+ const [vMaj, vMin, vPatch] = parseSemver(version);
116
+ const trimmed = range.trim();
117
+ const op = trimmed.startsWith("^") ? "^" : trimmed.startsWith("~") ? "~" : "=";
118
+ const ver = trimmed.replace(/^[\^~=]/, "");
119
+ const [rMaj, rMin, rPatch] = parseSemver(ver);
120
+ if (op === "^") {
121
+ if (rMaj === 0) return vMaj === 0 && vMin === rMin && vPatch >= rPatch;
122
+ return vMaj === rMaj && (vMin > rMin || vMin === rMin && vPatch >= rPatch);
123
+ }
124
+ if (op === "~") {
125
+ return vMaj === rMaj && vMin === rMin && vPatch >= rPatch;
126
+ }
127
+ return vMaj === rMaj && vMin === rMin && vPatch === rPatch;
128
+ }
129
+ function normalizeDep(d) {
130
+ return typeof d === "string" ? { name: d } : d;
131
+ }
132
+ function topoSort(plugins) {
133
+ const map = /* @__PURE__ */ new Map();
134
+ for (const p of plugins) map.set(p.name, p);
135
+ const visited = /* @__PURE__ */ new Set();
136
+ const visiting = /* @__PURE__ */ new Set();
137
+ const order = [];
138
+ const visit = (p, stack) => {
139
+ if (visited.has(p.name)) return;
140
+ if (visiting.has(p.name)) {
141
+ throw new PluginError({
142
+ message: `Plugin dependency cycle: ${[...stack, p.name].join(" -> ")}`,
143
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
144
+ pluginName: p.name
145
+ });
146
+ }
147
+ visiting.add(p.name);
148
+ for (const raw of p.dependsOn ?? []) {
149
+ const dep = normalizeDep(raw);
150
+ const d = map.get(dep.name);
151
+ if (!d) {
152
+ throw new PluginError({
153
+ message: `Plugin "${p.name}" depends on missing plugin "${dep.name}"`,
154
+ code: ERROR_CODES.PLUGIN_MISSING_DEPENDENCY,
155
+ pluginName: p.name,
156
+ context: { dependency: dep.name }
157
+ });
158
+ }
159
+ if (dep.version && d.version && !satisfies(dep.version, d.version)) {
160
+ throw new PluginError({
161
+ message: `Plugin "${p.name}" requires "${dep.name}@${dep.version}", found ${d.version}`,
162
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
163
+ pluginName: p.name,
164
+ context: { dependency: dep.name, required: dep.version, found: d.version }
165
+ });
166
+ }
167
+ visit(d, [...stack, p.name]);
168
+ }
169
+ for (const raw of p.optionalDeps ?? []) {
170
+ const dep = normalizeDep(raw);
171
+ const d = map.get(dep.name);
172
+ if (d) {
173
+ if (dep.version && d.version && !satisfies(dep.version, d.version)) {
174
+ throw new PluginError({
175
+ message: `Plugin "${p.name}" optional dep "${dep.name}@${dep.version}" found ${d.version}`,
176
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
177
+ pluginName: p.name,
178
+ context: { dependency: dep.name, required: dep.version, found: d.version }
179
+ });
180
+ }
181
+ visit(d, [...stack, p.name]);
182
+ }
183
+ }
184
+ visiting.delete(p.name);
185
+ visited.add(p.name);
186
+ order.push(p);
187
+ };
188
+ for (const p of plugins) visit(p, []);
189
+ return order;
190
+ }
191
+ function lifecycleTimeoutMs(configured, fallback, phase) {
192
+ const value = configured ?? fallback;
193
+ if (!Number.isFinite(value) || value < 0) {
194
+ throw new RangeError(`Plugin ${phase} timeout must be a finite non-negative number`);
195
+ }
196
+ return value;
197
+ }
198
+ async function runWithDeadline(plugin, phase, timeoutMs, operation) {
199
+ const controller = new AbortController();
200
+ let timer;
201
+ const timeoutError = new PluginLifecycleTimeoutError(plugin.name, phase, timeoutMs);
202
+ const deadline = new Promise((_resolve, reject) => {
203
+ timer = setTimeout(() => {
204
+ controller.abort(timeoutError);
205
+ reject(timeoutError);
206
+ }, timeoutMs);
207
+ });
208
+ const work = Promise.resolve().then(() => operation(controller.signal));
209
+ try {
210
+ await Promise.race([work, deadline]);
211
+ } finally {
212
+ if (timer !== void 0) clearTimeout(timer);
213
+ }
214
+ }
215
+ function registerLegacy(registration) {
216
+ const registrations = legacyRegistrations.get(registration.plugin) ?? [];
217
+ registrations.push(registration);
218
+ legacyRegistrations.set(registration.plugin, registrations);
219
+ }
220
+ function unregisterLegacy(registration) {
221
+ const registrations = legacyRegistrations.get(registration.plugin);
222
+ if (!registrations) return;
223
+ const index = registrations.indexOf(registration);
224
+ if (index >= 0) registrations.splice(index, 1);
225
+ if (registrations.length === 0) legacyRegistrations.delete(registration.plugin);
226
+ }
227
+ function drainCleanup(registration, log) {
228
+ if (registration.cleanupDrained) return;
229
+ registration.cleanupDrained = true;
230
+ try {
231
+ registration.cleanupApi.drainCleanup?.call(registration.cleanupApi);
232
+ } catch (err) {
233
+ log.error(`Plugin "${registration.plugin.name}" cleanup drain failed`, { err });
234
+ }
235
+ }
236
+ async function disposeRegistration(registration, opts, context) {
237
+ if (registration.disposed) return;
238
+ registration.disposed = true;
239
+ const { plugin, api } = registration;
240
+ try {
241
+ if (typeof plugin.teardown === "function") {
242
+ const timeoutMs = lifecycleTimeoutMs(opts.teardownTimeoutMs, 1e4, "teardown");
243
+ await runWithDeadline(
244
+ plugin,
245
+ "teardown",
246
+ timeoutMs,
247
+ (signal) => plugin.teardown(api, { signal })
248
+ );
249
+ }
250
+ opts.log.info(`Plugin "${plugin.name}" disposed (${context})`);
251
+ } catch (err) {
252
+ opts.log.error(`Plugin "${plugin.name}" teardown failed during ${context}`, {
253
+ err,
254
+ reason: err instanceof PluginLifecycleTimeoutError ? "teardown deadline exceeded" : void 0
255
+ });
256
+ } finally {
257
+ drainCleanup(registration, opts.log);
258
+ unregisterLegacy(registration);
259
+ }
260
+ }
261
+ async function disposeRegistrations(registrations, opts) {
262
+ for (const registration of [...registrations].reverse()) {
263
+ await disposeRegistration(registration, opts, "shutdown");
264
+ }
265
+ }
266
+ async function loadPlugins(plugins, opts) {
267
+ const kernelVersion = opts.kernelApiVersion ?? KERNEL_API_VERSION;
268
+ const loaded = [];
269
+ const failed = [];
270
+ const registrations = [];
271
+ const names = new Set(plugins.map((p) => p.name));
272
+ for (const p of plugins) {
273
+ for (const c of p.conflictsWith ?? []) {
274
+ if (names.has(c)) {
275
+ throw new PluginError({
276
+ message: `Plugin "${p.name}" conflicts with loaded plugin "${c}"`,
277
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
278
+ pluginName: p.name,
279
+ context: { conflictsWith: c }
280
+ });
281
+ }
282
+ }
283
+ }
284
+ let sorted;
285
+ try {
286
+ sorted = topoSort(plugins);
287
+ } catch (err) {
288
+ const message = toErrorMessage(err);
289
+ opts.log.error("Plugin dependency sort failed", message);
290
+ throw new PluginError({
291
+ message: `Plugin dependency sort failed: ${message}`,
292
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
293
+ pluginName: "(topological sort)",
294
+ context: { pluginCount: plugins.length },
295
+ cause: err
296
+ });
297
+ }
298
+ for (const plugin of sorted) {
299
+ if (!satisfies(plugin.apiVersion, kernelVersion)) {
300
+ const err = new PluginError({
301
+ message: `Plugin "${plugin.name}" requires apiVersion ${plugin.apiVersion}; kernel is ${kernelVersion}`,
302
+ code: ERROR_CODES.PLUGIN_API_MISMATCH,
303
+ pluginName: plugin.name,
304
+ context: { required: plugin.apiVersion, kernel: kernelVersion }
305
+ });
306
+ opts.log.error(err.message);
307
+ failed.push({ plugin, err });
308
+ continue;
309
+ }
310
+ const metadataIssues = validatePluginConfigMetadata(plugin);
311
+ if (metadataIssues.length > 0) {
312
+ const err = new PluginError({
313
+ message: `Plugin "${plugin.name}" config metadata invalid \u2014 ${metadataIssues.join("; ")}`,
314
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
315
+ pluginName: plugin.name,
316
+ context: { issues: metadataIssues }
317
+ });
318
+ opts.log.error(err.message);
319
+ failed.push({ plugin, err });
320
+ continue;
321
+ }
322
+ const resolution = resolvePluginManifestConfig(
323
+ plugin,
324
+ void 0,
325
+ opts.pluginOptions?.[plugin.name]
326
+ );
327
+ const hasResolvedOptions = plugin.defaultConfig !== void 0 || resolution.configured;
328
+ if (opts.pluginOptions && hasResolvedOptions) {
329
+ opts.pluginOptions[plugin.name] = resolution.options;
330
+ }
331
+ if (plugin.configSchema && hasResolvedOptions) {
332
+ const result = validateAgainstSchema(resolution.options, plugin.configSchema);
333
+ if (!result.ok) {
334
+ const firstErr = result.errors[0];
335
+ const detail = firstErr ? `${firstErr.path}: ${firstErr.message}` : "config invalid";
336
+ const err = new PluginError({
337
+ message: `Plugin "${plugin.name}" config invalid \u2014 ${detail}`,
338
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
339
+ pluginName: plugin.name,
340
+ context: { errors: result.errors }
341
+ });
342
+ opts.log.error(err.message);
343
+ failed.push({ plugin, err });
344
+ continue;
345
+ }
346
+ }
347
+ let registration;
348
+ try {
349
+ const rawApi = opts.apiFactory(
350
+ plugin,
351
+ resolution.options
352
+ );
353
+ const enforceForPlugin = typeof opts.enforceCapabilities === "function" ? opts.enforceCapabilities(plugin) : opts.enforceCapabilities ?? false;
354
+ const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log, enforceForPlugin) : rawApi;
355
+ registration = {
356
+ plugin,
357
+ api,
358
+ cleanupApi: rawApi,
359
+ cleanupDrained: false,
360
+ disposed: false
361
+ };
362
+ const setupTimeoutMs = lifecycleTimeoutMs(opts.setupTimeoutMs, 3e4, "setup");
363
+ await runWithDeadline(
364
+ plugin,
365
+ "setup",
366
+ setupTimeoutMs,
367
+ (signal) => plugin.setup(api, { signal })
368
+ );
369
+ registrations.push(registration);
370
+ registerLegacy(registration);
371
+ loaded.push(plugin);
372
+ opts.log.info(`Plugin "${plugin.name}" loaded`);
373
+ } catch (err) {
374
+ opts.log.error(`Plugin "${plugin.name}" setup failed`, {
375
+ err,
376
+ reason: err instanceof PluginLifecycleTimeoutError ? "setup deadline exceeded" : void 0
377
+ });
378
+ failed.push({ plugin, err });
379
+ if (registration) await disposeRegistration(registration, opts, "setup rollback");
380
+ }
381
+ }
382
+ let disposed = false;
383
+ let disposePromise;
384
+ return {
385
+ loaded,
386
+ failed,
387
+ get disposed() {
388
+ return disposed;
389
+ },
390
+ dispose() {
391
+ if (!disposePromise) {
392
+ disposed = true;
393
+ disposePromise = disposeRegistrations(registrations, opts);
394
+ }
395
+ return disposePromise;
396
+ }
397
+ };
398
+ }
399
+ async function unloadPlugins(loadedPlugins, opts) {
400
+ const ordered = [...loadedPlugins].reverse();
401
+ for (const plugin of ordered) {
402
+ const candidates = legacyRegistrations.get(plugin);
403
+ let registration;
404
+ if (candidates) {
405
+ for (let index = candidates.length - 1; index >= 0; index -= 1) {
406
+ if (!candidates[index].disposed) {
407
+ registration = candidates[index];
408
+ break;
409
+ }
410
+ }
411
+ }
412
+ if (!registration) {
413
+ opts.log.error(`Plugin "${plugin.name}" has no active loader registration`);
414
+ continue;
415
+ }
416
+ await disposeRegistration(registration, opts, "shutdown");
417
+ }
418
+ }
419
+ function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
420
+ const caps = plugin.capabilities ?? {};
421
+ const violate = (subsystem, detail) => {
422
+ const msg = `Plugin "${plugin.name}" used ${subsystem} without declaring capabilities.${subsystem} \u2014 ${detail}`;
423
+ if (enforce) {
424
+ throw new PluginError({
425
+ message: msg,
426
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
427
+ pluginName: plugin.name,
428
+ context: { subsystem, detail }
429
+ });
430
+ }
431
+ if (typeof log.warn === "function") log.warn(msg);
432
+ else log.error(msg);
433
+ };
434
+ const wrappedTools = caps.tools !== false ? api.tools : new Proxy(api.tools, {
435
+ get(target, prop, receiver) {
436
+ if (prop === "register") {
437
+ return (t) => {
438
+ violate(
439
+ "tools",
440
+ `register(${t?.name ?? "<unknown>"})`
441
+ );
442
+ return target.register(t);
443
+ };
444
+ }
445
+ return Reflect.get(target, prop, receiver);
446
+ }
447
+ });
448
+ const wrappedProviders = caps.providers !== false ? api.providers : new Proxy(api.providers, {
449
+ get(target, prop, receiver) {
450
+ if (prop === "register") {
451
+ return (f) => {
452
+ violate(
453
+ "providers",
454
+ `register(${f?.type ?? "<unknown>"})`
455
+ );
456
+ return target.register(f);
457
+ };
458
+ }
459
+ return Reflect.get(target, prop, receiver);
460
+ }
461
+ });
462
+ const wrappedSlash = caps.slashCommands !== false ? api.slashCommands : new Proxy(api.slashCommands, {
463
+ get(target, prop, receiver) {
464
+ if (prop === "register") {
465
+ return (c) => {
466
+ violate(
467
+ "slashCommands",
468
+ `register(${c?.name ?? "<unknown>"})`
469
+ );
470
+ return target.register(c);
471
+ };
472
+ }
473
+ return Reflect.get(target, prop, receiver);
474
+ }
475
+ });
476
+ const wrappedMcp = caps.mcp !== false ? api.mcp : new Proxy(api.mcp, {
477
+ get(target, prop, receiver) {
478
+ if (prop === "start") {
479
+ return (cfg) => {
480
+ violate(
481
+ "mcp",
482
+ `start(${cfg?.name ?? "<unknown>"})`
483
+ );
484
+ return target.start(cfg);
485
+ };
486
+ }
487
+ return Reflect.get(target, prop, receiver);
488
+ }
489
+ });
490
+ const wrappedLlm = caps.llm !== false || !api.llm ? api.llm : new Proxy(api.llm, {
491
+ get(target, prop, receiver) {
492
+ if (prop === "complete" || prop === "council") {
493
+ return (prompt, options) => {
494
+ violate("llm", `${String(prop)}(${prompt.length} chars)`);
495
+ const method = target[prop];
496
+ if (!method) return void 0;
497
+ return options === void 0 ? method.call(target, prompt) : method.call(target, prompt, options);
498
+ };
499
+ }
500
+ return Reflect.get(target, prop, receiver);
501
+ }
502
+ });
503
+ const wrappedHooks = caps.hooks !== false ? api : new Proxy(api, {
504
+ get(target, prop, receiver) {
505
+ if (prop === "registerHook") {
506
+ return (event, matcher, hook, options) => {
507
+ const ev = typeof event === "string" ? event : "<unknown>";
508
+ const m = typeof matcher === "string" ? matcher : "*";
509
+ violate("hooks", `registerHook(${ev}, ${m})`);
510
+ const register = target.registerHook;
511
+ return options === void 0 ? register(event, matcher, hook) : register(event, matcher, hook, options);
512
+ };
513
+ }
514
+ return Reflect.get(target, prop, receiver);
515
+ }
516
+ });
517
+ return new Proxy(api, {
518
+ get(target, prop, receiver) {
519
+ switch (prop) {
520
+ case "tools":
521
+ return wrappedTools;
522
+ case "providers":
523
+ return wrappedProviders;
524
+ case "slashCommands":
525
+ return wrappedSlash;
526
+ case "mcp":
527
+ return wrappedMcp;
528
+ case "llm":
529
+ return wrappedLlm;
530
+ case "registerHook":
531
+ return wrappedHooks[prop];
532
+ default:
533
+ return Reflect.get(target, prop, receiver);
534
+ }
535
+ }
536
+ });
537
+ }
538
+
539
+ // src/plugin/api.ts
540
+ var DefaultPluginAPI = class {
541
+ container;
542
+ events;
543
+ pipelines;
544
+ tools;
545
+ providers;
546
+ mcp;
547
+ slashCommands;
548
+ extensions;
549
+ session;
550
+ metrics;
551
+ config;
552
+ log;
553
+ modelsRegistry;
554
+ mailbox;
555
+ notifier;
556
+ llm;
557
+ configStore;
558
+ hookRegistry;
559
+ ownerName;
560
+ pluginCleanupFns = [];
561
+ constructor(init) {
562
+ const owner = init.ownerName;
563
+ this.ownerName = owner;
564
+ this.hookRegistry = init.hookRegistry;
565
+ this.container = init.container;
566
+ this.events = init.events;
567
+ this.config = init.config;
568
+ this.configStore = init.configStore;
569
+ this.log = init.log.child({ plugin: owner });
570
+ this.extensions = init.extensions ?? new ExtensionRegistry();
571
+ this.session = init.sessionWriter ?? noopSession;
572
+ this.metrics = init.metricsSink ? scopedMetrics(init.metricsSink, owner) : noopMetrics;
573
+ this.modelsRegistry = init.modelsRegistry;
574
+ this.mailbox = init.mailbox;
575
+ this.notifier = init.notifier;
576
+ let liveConfig;
577
+ if (init.configStore) {
578
+ const off = init.configStore.watch((next) => {
579
+ const n = next;
580
+ if (n && typeof n === "object") liveConfig = n;
581
+ });
582
+ this.pluginCleanupFns.push(off);
583
+ }
584
+ this.llm = init.llm ? makePluginLLM(
585
+ owner,
586
+ init.llm,
587
+ init.providerRegistry,
588
+ init.config,
589
+ () => liveConfig,
590
+ this.metrics,
591
+ this.log
592
+ ) : void 0;
593
+ const pipelines = init.pipelines;
594
+ const readonlyPipelines = {};
595
+ for (const [key, pipeline] of Object.entries(pipelines)) {
596
+ readonlyPipelines[key] = pipeline.asReadonly();
597
+ }
598
+ this.pipelines = readonlyPipelines;
599
+ const tr = init.toolRegistry;
600
+ const isOfficial = init.official === true;
601
+ const capabilities = init.capabilities;
602
+ const assertCanMutateTool = (name, op) => {
603
+ if (isOfficial) return;
604
+ const currentOwner = tr.ownerOf(name);
605
+ if (currentOwner === void 0) return;
606
+ const ownedSolelyByMe = currentOwner.split("+").every((seg) => seg === owner);
607
+ if (ownedSolelyByMe) return;
608
+ const toolCaps = tr.get(name)?.capabilities ?? [];
609
+ const pluginMutateCaps = capabilities?.toolMutateCapabilities ?? [];
610
+ const hasRequiredCap = toolCaps.some((c) => pluginMutateCaps.includes(c));
611
+ if (!hasRequiredCap) {
612
+ throw new Error(
613
+ `Plugin "${owner}" may not ${op} tool "${name}" \u2014 it is owned by "${currentOwner}". Tool capabilities: [${toolCaps.join(", ") || "none"}]. Plugin toolMutateCapabilities: [${pluginMutateCaps.join(", ") || "none"}]. Missing required capability to mutate this tool.`
614
+ );
615
+ }
616
+ };
617
+ this.tools = {
618
+ register: (t) => {
619
+ tr.register(t, owner);
620
+ tr.exposeToProvider(t.name);
621
+ },
622
+ unregister: (name) => {
623
+ assertCanMutateTool(name, "unregister");
624
+ return tr.unregister(name);
625
+ },
626
+ wrap: (name, wrapper) => {
627
+ assertCanMutateTool(name, "wrap");
628
+ tr.wrap(name, wrapper, owner);
629
+ },
630
+ get: (name) => tr.get(name),
631
+ list: () => tr.list()
632
+ };
633
+ const pr = init.providerRegistry;
634
+ const providerTypesIOwn = /* @__PURE__ */ new Set();
635
+ const assertCanMutateProvider = (type, op) => {
636
+ if (isOfficial) return;
637
+ if (providerTypesIOwn.has(type)) return;
638
+ if (!pr.has(type)) return;
639
+ throw new Error(
640
+ `Plugin "${owner}" may not ${op} provider "${type}" \u2014 it was not registered by this plugin. Replacing an existing provider would route prompts and credentials through plugin code.`
641
+ );
642
+ };
643
+ this.providers = {
644
+ register: (f) => {
645
+ assertCanMutateProvider(f.type, "replace");
646
+ pr.register(f);
647
+ providerTypesIOwn.add(f.type);
648
+ },
649
+ unregister: (type) => {
650
+ assertCanMutateProvider(type, "unregister");
651
+ providerTypesIOwn.delete(type);
652
+ return pr.unregister(type);
653
+ },
654
+ create: (cfg) => pr.create(cfg),
655
+ list: () => pr.list()
656
+ };
657
+ const mcpRegistry = init.mcpRegistry;
658
+ if (!mcpRegistry) {
659
+ this.mcp = noopMcp;
660
+ } else {
661
+ const mcpServersIStarted = /* @__PURE__ */ new Set();
662
+ const assertOwnsMcpServer = (name, op) => {
663
+ if (isOfficial) return;
664
+ if (mcpServersIStarted.has(name)) return;
665
+ if (!mcpRegistry.list().some((s) => s.name === name)) return;
666
+ throw new Error(
667
+ `Plugin "${owner}" may not ${op} MCP server "${name}" \u2014 it was not started by this plugin.`
668
+ );
669
+ };
670
+ this.mcp = {
671
+ start: async (cfg) => {
672
+ const name = cfg?.name;
673
+ if (typeof name === "string" && mcpRegistry.list().some((s) => s.name === name)) {
674
+ assertOwnsMcpServer(name, "start");
675
+ }
676
+ await mcpRegistry.start(cfg);
677
+ if (typeof name === "string") mcpServersIStarted.add(name);
678
+ },
679
+ stop: async (name) => {
680
+ assertOwnsMcpServer(name, "stop");
681
+ await mcpRegistry.stop(name);
682
+ },
683
+ restart: async (name) => {
684
+ assertOwnsMcpServer(name, "restart");
685
+ await mcpRegistry.restart(name);
686
+ },
687
+ list: () => mcpRegistry.list()
688
+ };
689
+ }
690
+ const scr = init.slashCommandRegistry;
691
+ const official = init.official === true;
692
+ const commandsIOwn = /* @__PURE__ */ new Set();
693
+ this.slashCommands = scr ? {
694
+ register: (cmd) => {
695
+ scr.register(cmd, owner, { official });
696
+ for (const key of [cmd.name, ...cmd.aliases ?? []]) {
697
+ commandsIOwn.add(key);
698
+ commandsIOwn.add(`${owner}:${key}`);
699
+ }
700
+ },
701
+ unregister: (name) => {
702
+ if (!official && !commandsIOwn.has(name) && scr.get(name) !== void 0) {
703
+ throw new Error(
704
+ `Plugin "${owner}" may not unregister slash command "${name}" \u2014 it was not registered by this plugin.`
705
+ );
706
+ }
707
+ for (const key of [
708
+ name,
709
+ `${owner}:${name}`,
710
+ name.startsWith(`${owner}:`) ? name.slice(owner.length + 1) : name
711
+ ]) {
712
+ commandsIOwn.delete(key);
713
+ }
714
+ return scr.unregister(name);
715
+ },
716
+ get: (name) => scr.get(name),
717
+ list: () => scr.list()
718
+ } : noopSlashCommands;
719
+ }
720
+ onEvent(event, handler) {
721
+ const off = this.events.on(event, handler);
722
+ this.pluginCleanupFns.push(off);
723
+ return off;
724
+ }
725
+ onPattern(pattern, handler) {
726
+ const off = this.events.onPattern(pattern, handler);
727
+ this.pluginCleanupFns.push(off);
728
+ return off;
729
+ }
730
+ emitCustom(event, payload) {
731
+ this.events.emitCustom(event, payload);
732
+ }
733
+ onConfigChange(handler) {
734
+ if (!this.configStore) return () => {
735
+ };
736
+ return this.configStore.watch(handler);
737
+ }
738
+ /** Called by the plugin loader when uninstalling the plugin. */
739
+ drainCleanup() {
740
+ for (const fn of this.pluginCleanupFns.splice(0)) {
741
+ try {
742
+ fn();
743
+ } catch {
744
+ }
745
+ }
746
+ if (this.hookRegistry) this.hookRegistry.drainByOwner(this.ownerName);
747
+ }
748
+ registerSystemPromptContributor(c) {
749
+ return this.extensions.registerSystemPromptContributor(c);
750
+ }
751
+ registerHook(event, matcher, hook, options) {
752
+ if (!this.hookRegistry) return () => {
753
+ };
754
+ const off = this.hookRegistry.registerInProcess(event, matcher, hook, this.ownerName, options);
755
+ this.pluginCleanupFns.push(off);
756
+ return off;
757
+ }
758
+ };
759
+ var noopMcp = {
760
+ start: async () => void 0,
761
+ stop: async () => void 0,
762
+ restart: async () => void 0,
763
+ list: () => []
764
+ };
765
+ var noopSlashCommands = {
766
+ register() {
767
+ },
768
+ unregister() {
769
+ return false;
770
+ },
771
+ get() {
772
+ return void 0;
773
+ },
774
+ list() {
775
+ return [];
776
+ }
777
+ };
778
+ var noopSession = {
779
+ append: async () => {
780
+ }
781
+ };
782
+ var noopMetrics = {
783
+ counter() {
784
+ },
785
+ histogram() {
786
+ },
787
+ gauge() {
788
+ }
789
+ };
790
+ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig, metrics, log) {
791
+ const providerCache = /* @__PURE__ */ new Map();
792
+ const PROVIDER_CACHE_MAX = 32;
793
+ let providerCacheConfigRef = config;
794
+ const currentConfig = () => getLiveConfig() ?? config;
795
+ const currentProvider = () => hostLLM.getProvider?.() ?? hostLLM.provider;
796
+ const currentModel = () => hostLLM.getModel?.() ?? hostLLM.model;
797
+ const DEFAULT_MAX_TOKENS = 2048;
798
+ const HARD_MAX_TOKENS = 32768;
799
+ const DEFAULT_TIMEOUT_MS = 3e4;
800
+ const HARD_TIMEOUT_MS = 12e4;
801
+ const pluginDefaults = () => {
802
+ const extensions = currentConfig().extensions;
803
+ const raw = extensions?.[owner]?.["llm"];
804
+ if (!raw || typeof raw !== "object") return {};
805
+ const r = raw;
806
+ return {
807
+ ...typeof r["provider"] === "string" && r["provider"] ? { provider: r["provider"] } : {},
808
+ ...typeof r["model"] === "string" && r["model"] ? { model: r["model"] } : {},
809
+ ...typeof r["maxTokens"] === "number" ? { maxTokens: r["maxTokens"] } : {},
810
+ ...typeof r["temperature"] === "number" ? { temperature: r["temperature"] } : {},
811
+ ...typeof r["role"] === "string" && r["role"] ? { role: r["role"] } : {},
812
+ ...Array.isArray(r["fallbackModels"]) && r["fallbackModels"].every((v) => typeof v === "string") ? { fallbackModels: [...r["fallbackModels"]] } : {},
813
+ ...typeof r["timeoutMs"] === "number" ? { timeoutMs: r["timeoutMs"] } : {},
814
+ ...typeof r["councilProfile"] === "string" && r["councilProfile"] ? { councilProfile: r["councilProfile"] } : {}
815
+ };
816
+ };
817
+ const resolveProvider = (name, model) => {
818
+ const defaults = pluginDefaults();
819
+ const liveProvider = currentProvider();
820
+ const liveConfig = currentConfig();
821
+ if (liveConfig !== providerCacheConfigRef) {
822
+ providerCache.clear();
823
+ providerCacheConfigRef = liveConfig;
824
+ }
825
+ const providerName = name ?? defaults.provider ?? liveProvider.id;
826
+ if (providerName === liveProvider.id) {
827
+ return { provider: liveProvider, providerName };
828
+ }
829
+ const cacheKey = `${providerName}|${model}`;
830
+ const cached = providerCache.get(cacheKey);
831
+ if (cached) return { provider: cached, providerName };
832
+ let created;
833
+ if (hostLLM.createProvider) {
834
+ created = hostLLM.createProvider(providerName, model);
835
+ } else {
836
+ const savedCfg = liveConfig.providers?.[providerName];
837
+ const factoryType = savedCfg?.type ?? providerName;
838
+ created = providerRegistry.create(
839
+ { ...savedCfg ?? {}, type: providerName, model },
840
+ factoryType
841
+ );
842
+ }
843
+ while (providerCache.size >= PROVIDER_CACHE_MAX) {
844
+ const oldest = providerCache.keys().next().value;
845
+ if (oldest === void 0) break;
846
+ providerCache.delete(oldest);
847
+ }
848
+ providerCache.set(cacheKey, created);
849
+ return { provider: created, providerName };
850
+ };
851
+ const pluginLlm = {
852
+ defaults() {
853
+ const d = pluginDefaults();
854
+ return {
855
+ provider: d.provider ?? currentProvider().id,
856
+ model: d.model ?? currentModel()
857
+ };
858
+ },
859
+ async complete(prompt, opts) {
860
+ const defaults = pluginDefaults();
861
+ const model = opts?.model ?? defaults.model ?? currentModel();
862
+ const providerName = opts?.provider ?? defaults.provider ?? currentProvider().id;
863
+ const maxTokens = Math.min(
864
+ HARD_MAX_TOKENS,
865
+ opts?.maxTokens ?? defaults.maxTokens ?? DEFAULT_MAX_TOKENS
866
+ );
867
+ const temperature = opts?.temperature ?? defaults.temperature;
868
+ const timeoutMs = Math.min(
869
+ HARD_TIMEOUT_MS,
870
+ Math.max(1, opts?.timeoutMs ?? defaults.timeoutMs ?? DEFAULT_TIMEOUT_MS)
871
+ );
872
+ if (hostLLM.oneShot) {
873
+ metrics.counter("llm.calls", 1, { provider: providerName, model, engine: "one-shot" });
874
+ const result = await hostLLM.oneShot({
875
+ userPrompt: prompt,
876
+ providerId: providerName,
877
+ model,
878
+ maxTokens,
879
+ timeoutMs,
880
+ ...temperature !== void 0 ? { temperature } : {},
881
+ ...opts?.system ? { system: opts.system } : {},
882
+ ...opts?.responseFormat === "json" ? { responseFormat: { type: "json_object" } } : {},
883
+ ...opts?.signal ? { signal: opts.signal } : {},
884
+ ...opts?.role ?? defaults.role ? { role: opts?.role ?? defaults.role } : {},
885
+ ...opts?.fallbackModels ?? defaults.fallbackModels ? { fallbackModels: [...opts?.fallbackModels ?? defaults.fallbackModels ?? []] } : {}
886
+ });
887
+ if (result.error) {
888
+ metrics.counter("llm.errors", 1, { provider: result.provider, model: result.model });
889
+ throw new Error(result.error);
890
+ }
891
+ metrics.counter("llm.tokens_in", result.tokens.input);
892
+ metrics.counter("llm.tokens_out", result.tokens.output);
893
+ return {
894
+ text: result.text,
895
+ model: result.model,
896
+ provider: result.provider,
897
+ usage: { input: result.tokens.input, output: result.tokens.output },
898
+ stopReason: result.stopReason ?? "end_turn",
899
+ fromFallback: result.fromFallback,
900
+ attempts: result.attempts,
901
+ durationMs: result.durationMs
902
+ };
903
+ }
904
+ const { provider } = resolveProvider(providerName, model);
905
+ const request = {
906
+ model,
907
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
908
+ maxTokens,
909
+ ...temperature !== void 0 ? { temperature } : {},
910
+ ...opts?.system ? { system: [{ type: "text", text: opts.system }] } : {},
911
+ ...opts?.responseFormat === "json" ? { responseFormat: { type: "json_object" } } : {}
912
+ };
913
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
914
+ const signal = opts?.signal ? AbortSignal.any([opts.signal, timeoutSignal]) : timeoutSignal;
915
+ metrics.counter("llm.calls", 1, { provider: providerName, model });
916
+ try {
917
+ const response = await provider.complete(request, { signal });
918
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
919
+ metrics.counter("llm.tokens_in", response.usage.input);
920
+ metrics.counter("llm.tokens_out", response.usage.output);
921
+ return {
922
+ text,
923
+ model: response.model,
924
+ provider: providerName,
925
+ usage: { input: response.usage.input, output: response.usage.output },
926
+ stopReason: response.stopReason
927
+ };
928
+ } catch (err) {
929
+ metrics.counter("llm.errors", 1, { provider: providerName, model });
930
+ log.warn(`plugin "${owner}" llm.complete failed`, {
931
+ provider: providerName,
932
+ model,
933
+ error: err instanceof Error ? err.message : String(err)
934
+ });
935
+ throw err;
936
+ }
937
+ }
938
+ };
939
+ const council = hostLLM.council;
940
+ if (council) {
941
+ pluginLlm.council = async (question, opts) => {
942
+ const trimmed = question.trim();
943
+ if (!trimmed) throw new Error("Plugin Council question must not be empty.");
944
+ if (trimmed.length > 2e4) {
945
+ throw new Error("Plugin Council question must not exceed 20000 characters.");
946
+ }
947
+ if ((opts?.context?.length ?? 0) > 8e4) {
948
+ throw new Error("Plugin Council context must not exceed 80000 characters.");
949
+ }
950
+ const defaults = pluginDefaults();
951
+ metrics.counter("llm.council.calls", 1, { plugin: owner });
952
+ const result = await council({
953
+ question: trimmed,
954
+ ...opts?.context ? { context: opts.context } : {},
955
+ ...opts?.options ? { options: opts.options } : {},
956
+ ...opts?.profile ?? defaults.councilProfile ? { profile: opts?.profile ?? defaults.councilProfile } : {},
957
+ ...opts?.signal ? { signal: opts.signal } : {}
958
+ });
959
+ metrics.counter("llm.tokens_in", result.usage.inputTokens);
960
+ metrics.counter("llm.tokens_out", result.usage.outputTokens);
961
+ if (result.status === "failed") metrics.counter("llm.errors", 1, { provider: "council" });
962
+ return result;
963
+ };
964
+ }
965
+ return pluginLlm;
966
+ }
967
+ function scopedMetrics(sink, pluginName) {
968
+ const prefix = `plugin.${pluginName}.`;
969
+ return {
970
+ counter(name, value, labels) {
971
+ sink.counter(`${prefix}${name}`, value, labels);
972
+ },
973
+ histogram(name, value, labels) {
974
+ sink.histogram(`${prefix}${name}`, value, labels);
975
+ },
976
+ gauge(name, value, labels) {
977
+ sink.gauge(`${prefix}${name}`, value, labels);
978
+ }
979
+ };
980
+ }
981
+ function definePlugin(metadata, factory) {
982
+ return {
983
+ name: metadata.name,
984
+ version: metadata.version,
985
+ description: metadata.description,
986
+ capabilities: metadata.capabilities,
987
+ configSchema: metadata.configSchema,
988
+ defaultConfig: metadata.defaultConfig,
989
+ dependsOn: metadata.dependsOn,
990
+ optionalDeps: metadata.optionalDeps,
991
+ conflictsWith: metadata.conflictsWith,
992
+ apiVersion: KERNEL_API_VERSION,
993
+ // Cast the factory to match the Plugin.setup signature.
994
+ // The opts parameter ({ signal }) is accepted by Plugin.setup but the
995
+ // definePlugin factory doesn't need to declare it — the host always passes
996
+ // it at the call site; the factory just doesn't have to care about it.
997
+ setup: (api) => factory(api, metadata.defaultConfig)
998
+ };
999
+ }
1000
+
1001
+ // src/plugins/auto-review-config.ts
1002
+ var DEFAULT_DEBOUNCE_MS = 15e3;
1003
+ var DEFAULT_MAX_FILES_PER_BATCH = 15;
1004
+ var DEFAULT_MAX_CONCURRENT_REVIEWS = 2;
1005
+ var DEFAULT_MAX_CASCADE_DEPTH = 2;
1006
+ var MAX_KNOWN_FINGERPRINTS = 5e3;
1007
+ function trimKnownFingerprints(map, max) {
1008
+ while (map.size > max) {
1009
+ const oldest = map.keys().next().value;
1010
+ if (oldest === void 0) break;
1011
+ map.delete(oldest);
1012
+ }
1013
+ }
1014
+ function buildReviewerModelPool(provider, model, fallbackModels = [], statusTracker) {
1015
+ const primaryProvider = provider.trim();
1016
+ const primaryModel = model.trim();
1017
+ const primaryRef = primaryProvider && primaryModel ? `${primaryProvider}/${primaryModel}` : "";
1018
+ const seen = /* @__PURE__ */ new Set();
1019
+ const pool = [];
1020
+ for (const raw of [primaryRef, ...fallbackModels]) {
1021
+ const ref = raw.trim();
1022
+ if (!ref || seen.has(ref)) continue;
1023
+ const parsed = parseModelRef(ref);
1024
+ if (!parsed.provider?.trim() || !parsed.model.trim()) continue;
1025
+ const normalized = `${parsed.provider.trim()}/${parsed.model.trim()}`;
1026
+ if (seen.has(normalized)) continue;
1027
+ seen.add(ref);
1028
+ seen.add(normalized);
1029
+ if (statusTracker && !statusTracker.isAvailable(parsed.provider.trim(), parsed.model.trim())) {
1030
+ continue;
1031
+ }
1032
+ pool.push(normalized);
1033
+ }
1034
+ return pool;
1035
+ }
1036
+ function selectRoundRobinReviewerAssignment(pool, cursor, fallbackProvider = "", fallbackModel = "", statusTracker) {
1037
+ const filteredPool = statusTracker ? pool.filter((ref) => {
1038
+ const parsed2 = parseModelRef(ref);
1039
+ if (!parsed2.provider?.trim() || !parsed2.model.trim()) return false;
1040
+ return statusTracker.isAvailable(parsed2.provider.trim(), parsed2.model.trim());
1041
+ }) : pool;
1042
+ if (filteredPool.length === 0) {
1043
+ return {
1044
+ provider: fallbackProvider,
1045
+ model: fallbackModel,
1046
+ fallbackModels: [],
1047
+ nextCursor: cursor + 1
1048
+ };
1049
+ }
1050
+ const len = filteredPool.length;
1051
+ const idx = (cursor % len + len) % len;
1052
+ const primaryRef = filteredPool[idx];
1053
+ const parsed = parseModelRef(primaryRef);
1054
+ const provider = parsed.provider?.trim() || fallbackProvider;
1055
+ const model = parsed.model.trim() || fallbackModel;
1056
+ const fallbackModels = [...filteredPool.slice(idx + 1), ...filteredPool.slice(0, idx)];
1057
+ return {
1058
+ provider,
1059
+ model,
1060
+ fallbackModels,
1061
+ nextCursor: (idx + 1) % len
1062
+ };
1063
+ }
1064
+ function resolveAutoReviewConfig(cfg, sessionConfig) {
1065
+ const mgr = new FallbackProfileManager(sessionConfig);
1066
+ const chain = mgr.resolveEffective({
1067
+ ...cfg.fallbackProfile ? { fallbackProfile: cfg.fallbackProfile, fallbackAuto: false } : {
1068
+ fallbackModels: sessionConfig.fallbackModels,
1069
+ fallbackAuto: sessionConfig.fallbackAuto
1070
+ }
1071
+ });
1072
+ const rawProvider = cfg.provider?.trim();
1073
+ const rawModel = cfg.model?.trim();
1074
+ const resolvedProvider = rawProvider || (chain.length > 0 ? chain[0].providerId : sessionConfig.provider);
1075
+ const resolvedModel = rawModel || (chain.length > 0 ? chain[0].model : sessionConfig.model);
1076
+ const profileFallbackModels = chain.filter((entry) => entry.providerId !== resolvedProvider || entry.model !== resolvedModel).map((entry) => `${entry.providerId}/${entry.model}`);
1077
+ const sessionRef = `${sessionConfig.provider}/${sessionConfig.model}`;
1078
+ const fallbackModels = [
1079
+ ...profileFallbackModels.filter((ref) => ref !== sessionRef),
1080
+ sessionRef
1081
+ ].filter((ref) => ref !== `${resolvedProvider}/${resolvedModel}`);
1082
+ return {
1083
+ enabled: cfg.enabled === true,
1084
+ provider: resolvedProvider,
1085
+ model: resolvedModel,
1086
+ fallbackModels,
1087
+ modelSelection: cfg.modelSelection === "random" ? "random" : "round-robin",
1088
+ debounceMs: cfg.debounceMs ?? DEFAULT_DEBOUNCE_MS,
1089
+ maxFilesPerBatch: cfg.maxFilesPerBatch ?? DEFAULT_MAX_FILES_PER_BATCH,
1090
+ maxConcurrentReviews: cfg.maxConcurrentReviews ?? DEFAULT_MAX_CONCURRENT_REVIEWS,
1091
+ cascadeOn: cfg.cascadeOn ?? "off",
1092
+ maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH
1093
+ };
1094
+ }
1095
+ function severitiesFromFindings(findings) {
1096
+ const severities = { critical: 0, high: 0, medium: 0 };
1097
+ for (const finding of findings) {
1098
+ if (finding.severity === "critical") severities.critical++;
1099
+ else if (finding.severity === "high") severities.high++;
1100
+ else if (finding.severity === "medium") severities.medium++;
1101
+ }
1102
+ return severities;
1103
+ }
1104
+ function parseReviewSeverity(text) {
1105
+ const result = { critical: 0, high: 0, medium: 0 };
1106
+ if (!text) return result;
1107
+ for (const level of ["critical", "high", "medium"]) {
1108
+ const countMatch = text.match(new RegExp(`###\\s*${level}\\s*\\((\\d+)\\)`, "i"));
1109
+ if (countMatch?.[1]) {
1110
+ result[level] = Number.parseInt(countMatch[1], 10);
1111
+ continue;
1112
+ }
1113
+ const section = text.match(
1114
+ new RegExp(`###\\s*${level}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`, "i")
1115
+ )?.[1];
1116
+ result[level] = section?.match(/^\s*\d+\.\s/gm)?.length ?? 0;
1117
+ }
1118
+ return result;
1119
+ }
1120
+ function decideCascadeAgents(text, severities, findings) {
1121
+ const agents = /* @__PURE__ */ new Set();
1122
+ if (severities.critical > 0 || severities.high > 0) agents.add("bug-hunter");
1123
+ if (findings && findings.length > 0) {
1124
+ const highPlus = findings.filter(
1125
+ (finding) => finding.severity === "critical" || finding.severity === "high"
1126
+ );
1127
+ if (highPlus.some((finding) => finding.category === "security")) {
1128
+ agents.add("security-scanner");
1129
+ }
1130
+ if (highPlus.every((finding) => finding.category !== void 0)) {
1131
+ return [...agents];
1132
+ }
1133
+ }
1134
+ const securityKeywords = [
1135
+ "injection",
1136
+ "xss",
1137
+ "csrf",
1138
+ "ssrf",
1139
+ "sql",
1140
+ "secret",
1141
+ "credential",
1142
+ "password",
1143
+ "api key",
1144
+ "token",
1145
+ "auth",
1146
+ "shell injection",
1147
+ "command injection",
1148
+ "innerhtml",
1149
+ "deserialization",
1150
+ "path traversal",
1151
+ "hardcoded",
1152
+ "privilege",
1153
+ "owasp"
1154
+ ];
1155
+ const criticalHighSections = text.toLowerCase().matchAll(/###\s*(?:critical|high)[^\n]*\n([\s\S]*?)(?=###|$)/gi);
1156
+ for (const section of criticalHighSections) {
1157
+ const body = section[1] ?? "";
1158
+ if (securityKeywords.some((keyword) => body.includes(keyword))) {
1159
+ agents.add("security-scanner");
1160
+ break;
1161
+ }
1162
+ }
1163
+ return [...agents];
1164
+ }
1165
+ function shouldCascade(cascadeOn, severities) {
1166
+ if (cascadeOn === "off") return null;
1167
+ if (severities.critical > 0) return "critical";
1168
+ if (cascadeOn === "high" && severities.high > 0) return "high";
1169
+ return null;
1170
+ }
1171
+
1172
+ // src/plugins/review-claim-registry.ts
1173
+ import { createHash, randomUUID } from "node:crypto";
1174
+ import * as fsp from "node:fs/promises";
1175
+ import { hostname } from "node:os";
1176
+ import * as path from "node:path";
1177
+ var CLAIMS_FILE = "review-claims.jsonl";
1178
+ var DEFAULT_CLAIM_TTL_MS = 30 * 60 * 1e3;
1179
+ var LOCK_WAIT_MS = 5e3;
1180
+ var LOCK_RETRY_MAX_MS = 50;
1181
+ var LOCK_STALE_MS = 3e4;
1182
+ var SAME_HOST_STALE_MS = 2 * LOCK_STALE_MS;
1183
+ var LOCK_RETRY_BUDGET_MS = SAME_HOST_STALE_MS + 5e3;
1184
+ var CLAIM_STORE_LOCK_TIMEOUT = "CLAIM_STORE_LOCK_TIMEOUT";
1185
+ var HOST_NAME = hostname();
1186
+ function lockOwnerStamp() {
1187
+ return `${HOST_NAME}:${process.pid}`;
1188
+ }
1189
+ var lastFallbackWarnAt = 0;
1190
+ var FALLBACK_WARN_INTERVAL_MS = 6e4;
1191
+ function warnFallbackOnce(message) {
1192
+ const now = Date.now();
1193
+ if (now - lastFallbackWarnAt < FALLBACK_WARN_INTERVAL_MS) return;
1194
+ lastFallbackWarnAt = now;
1195
+ console.warn(message);
1196
+ }
1197
+ async function breakStaleLockVerified(lockPath, verify) {
1198
+ if (!await verify()) return false;
1199
+ return await breakLockAtomically(lockPath) === true;
1200
+ }
1201
+ async function breakStaleLock(lockPath) {
1202
+ try {
1203
+ const ownerRaw = await fsp.readFile(lockPath, "utf8").catch(() => null);
1204
+ if (ownerRaw !== null) {
1205
+ const [ownerHost, pidRaw] = ownerRaw.trim().split(":", 2);
1206
+ const ownerPid = Number.parseInt(pidRaw ?? "", 10);
1207
+ if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
1208
+ if (isPidAlive(ownerPid)) {
1209
+ const st2 = await fsp.stat(lockPath);
1210
+ if (Date.now() - st2.mtimeMs > SAME_HOST_STALE_MS) {
1211
+ return await breakStaleLockVerified(lockPath, async () => {
1212
+ const st22 = await fsp.stat(lockPath);
1213
+ return Date.now() - st22.mtimeMs > SAME_HOST_STALE_MS;
1214
+ });
1215
+ }
1216
+ return false;
1217
+ }
1218
+ return await breakStaleLockVerified(lockPath, async () => {
1219
+ const content = await fsp.readFile(lockPath, "utf8").catch(() => "");
1220
+ return content.trim() === ownerRaw.trim();
1221
+ });
1222
+ }
1223
+ }
1224
+ const st = await fsp.stat(lockPath);
1225
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
1226
+ return await breakStaleLockVerified(lockPath, async () => {
1227
+ const st2 = await fsp.stat(lockPath);
1228
+ return Date.now() - st2.mtimeMs > LOCK_STALE_MS;
1229
+ });
1230
+ }
1231
+ } catch {
1232
+ }
1233
+ return false;
1234
+ }
1235
+ async function breakLockAtomically(lockPath) {
1236
+ const tombstone = `${lockPath}.stale-${randomUUID()}.tmp`;
1237
+ try {
1238
+ await fsp.rename(lockPath, tombstone);
1239
+ } catch {
1240
+ return false;
1241
+ }
1242
+ void fsp.unlink(tombstone).catch(() => void 0);
1243
+ return true;
1244
+ }
1245
+ async function withStoreLock(storeDir, fn, waitMs = LOCK_WAIT_MS) {
1246
+ const lockPath = lockFilePath(storeDir);
1247
+ await fsp.mkdir(storeDir, { recursive: true });
1248
+ const deadline = Date.now() + waitMs;
1249
+ let attempt = 0;
1250
+ for (; ; ) {
1251
+ let handle = await fsp.open(lockPath, "wx").catch(() => null);
1252
+ if (!handle && await breakStaleLock(lockPath)) {
1253
+ handle = await fsp.open(lockPath, "wx").catch(() => null);
1254
+ }
1255
+ if (handle) {
1256
+ let stampFailed = false;
1257
+ try {
1258
+ let stamped = false;
1259
+ for (let stampAttempt = 0; stampAttempt < 3 && !stamped; stampAttempt++) {
1260
+ try {
1261
+ await handle.writeFile(lockOwnerStamp());
1262
+ stamped = true;
1263
+ } catch {
1264
+ if (stampAttempt < 2) await new Promise((resolve3) => setTimeout(resolve3, 5));
1265
+ }
1266
+ }
1267
+ if (!stamped) {
1268
+ stampFailed = true;
1269
+ throw new Error("failed to stamp claim lock owner");
1270
+ }
1271
+ return await fn();
1272
+ } finally {
1273
+ await handle.close().catch(() => void 0);
1274
+ if (stampFailed) {
1275
+ await fsp.unlink(lockPath).catch(() => void 0);
1276
+ } else {
1277
+ const owner = await fsp.readFile(lockPath, "utf8").catch(() => null);
1278
+ if (owner !== null && owner.trim() === lockOwnerStamp()) {
1279
+ await fsp.unlink(lockPath).catch(() => void 0);
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1284
+ const remaining = deadline - Date.now();
1285
+ if (remaining <= 0) {
1286
+ throw Object.assign(new Error("review claim store lock timeout"), {
1287
+ code: CLAIM_STORE_LOCK_TIMEOUT
1288
+ });
1289
+ }
1290
+ const delay = Math.min(LOCK_RETRY_MAX_MS * (attempt + 1), remaining);
1291
+ await new Promise((resolve3) => setTimeout(resolve3, delay));
1292
+ attempt += 1;
1293
+ }
1294
+ }
1295
+ var MAX_LEDGER_LINES = 1e4;
1296
+ var HOST_SID = randomUUID();
1297
+ var claimsByEventBus = /* @__PURE__ */ new WeakMap();
1298
+ var startedReviews = /* @__PURE__ */ new WeakMap();
1299
+ var pendingStartedReviews = /* @__PURE__ */ new WeakMap();
1300
+ function fingerprint(content) {
1301
+ return createHash("sha256").update(content).digest("hex");
1302
+ }
1303
+ function normalizeKeyPart(p) {
1304
+ const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
1305
+ return process.platform === "win32" ? forward.toLowerCase() : forward;
1306
+ }
1307
+ function claimKey(cwd, filePath) {
1308
+ return `${normalizeKeyPart(cwd)}\0${normalizeKeyPart(filePath)}`;
1309
+ }
1310
+ function resolveStore(opts, cwd) {
1311
+ return {
1312
+ storeDir: opts?.storeDir ?? path.join(cwd, ".wrongstack"),
1313
+ ttlMs: opts?.ttlMs ?? DEFAULT_CLAIM_TTL_MS,
1314
+ maxLedgerLines: opts?.maxLedgerLines ?? MAX_LEDGER_LINES
1315
+ };
1316
+ }
1317
+ function claimsFilePath(storeDir) {
1318
+ return path.join(storeDir, CLAIMS_FILE);
1319
+ }
1320
+ function lockFilePath(storeDir) {
1321
+ return path.join(storeDir, `${CLAIMS_FILE}.lock`);
1322
+ }
1323
+ async function readLedger(storeDir, now) {
1324
+ const active = /* @__PURE__ */ new Map();
1325
+ const raw = await fsp.readFile(claimsFilePath(storeDir), "utf8").catch((err) => {
1326
+ const code = err.code;
1327
+ if (code === "ENOENT") return "";
1328
+ throw err;
1329
+ });
1330
+ for (const line of raw.split("\n")) {
1331
+ if (!line.trim()) continue;
1332
+ let entry;
1333
+ try {
1334
+ const parsed = JSON.parse(line);
1335
+ if (parsed && typeof parsed === "object" && (parsed.op === "claim" || parsed.op === "release") && typeof parsed.key === "string" && typeof parsed.fp === "string" && typeof parsed.sid === "string" && typeof parsed.ts === "number" && (typeof parsed.exp === "undefined" || typeof parsed.exp === "number")) {
1336
+ entry = parsed;
1337
+ }
1338
+ } catch {
1339
+ }
1340
+ if (!entry) continue;
1341
+ let bucket = active.get(entry.key);
1342
+ if (!bucket) {
1343
+ bucket = /* @__PURE__ */ new Map();
1344
+ active.set(entry.key, bucket);
1345
+ }
1346
+ if (entry.op === "claim") {
1347
+ const exp = entry.exp ?? entry.ts + DEFAULT_CLAIM_TTL_MS;
1348
+ if (now > exp) continue;
1349
+ bucket.set(entry.fp, { sid: entry.sid, ts: entry.ts, exp });
1350
+ } else {
1351
+ const existing = bucket.get(entry.fp);
1352
+ if (existing?.sid === entry.sid && existing.ts === entry.ts) bucket.delete(entry.fp);
1353
+ }
1354
+ }
1355
+ return active;
1356
+ }
1357
+ async function ledgerLineCount(storeDir) {
1358
+ const raw = await fsp.readFile(claimsFilePath(storeDir), "utf8").catch(() => "");
1359
+ return raw.split("\n").filter((l) => l.trim()).length;
1360
+ }
1361
+ async function compactLedger(storeDir, active) {
1362
+ const lines = [];
1363
+ for (const [key, fps] of active) {
1364
+ for (const [fp, claim] of fps) {
1365
+ lines.push(
1366
+ JSON.stringify({
1367
+ op: "claim",
1368
+ key,
1369
+ fp,
1370
+ sid: claim.sid,
1371
+ ts: claim.ts,
1372
+ exp: claim.exp
1373
+ })
1374
+ );
1375
+ }
1376
+ }
1377
+ const tmp = `${claimsFilePath(storeDir)}.tmp-${randomUUID()}`;
1378
+ await fsp.writeFile(tmp, lines.length > 0 ? `${lines.join("\n")}
1379
+ ` : "", "utf8");
1380
+ let replaced = false;
1381
+ for (let attempt = 0; attempt < 3 && !replaced; attempt++) {
1382
+ try {
1383
+ await fsp.rename(tmp, claimsFilePath(storeDir));
1384
+ replaced = true;
1385
+ } catch {
1386
+ if (attempt === 2) {
1387
+ await fsp.copyFile(tmp, claimsFilePath(storeDir)).catch(async (copyErr) => {
1388
+ throw copyErr;
1389
+ });
1390
+ await fsp.unlink(tmp).catch(() => void 0);
1391
+ replaced = true;
1392
+ } else {
1393
+ await new Promise((resolve3) => setTimeout(resolve3, 10 * (attempt + 1)));
1394
+ }
1395
+ }
1396
+ }
1397
+ }
1398
+ async function acquireClaims(storeDir, ttlMs, maxLedgerLines, cwd, files, waitMs = LOCK_WAIT_MS) {
1399
+ return withStoreLock(
1400
+ storeDir,
1401
+ async () => {
1402
+ const now = Date.now();
1403
+ const active = await readLedger(storeDir, now);
1404
+ const claims = [];
1405
+ const lines = [];
1406
+ for (const file of files) {
1407
+ const key = claimKey(cwd, file.path);
1408
+ const fp = fingerprint(file.content);
1409
+ let bucket = active.get(key);
1410
+ if (bucket?.has(fp)) continue;
1411
+ if (!bucket) {
1412
+ bucket = /* @__PURE__ */ new Map();
1413
+ active.set(key, bucket);
1414
+ }
1415
+ bucket.set(fp, { sid: HOST_SID, ts: now, exp: now + ttlMs });
1416
+ claims.push({ key, fingerprint: fp, sid: HOST_SID, ts: now });
1417
+ lines.push(
1418
+ JSON.stringify({
1419
+ op: "claim",
1420
+ key,
1421
+ fp,
1422
+ sid: HOST_SID,
1423
+ ts: now,
1424
+ exp: now + ttlMs
1425
+ })
1426
+ );
1427
+ }
1428
+ if (lines.length > 0) {
1429
+ await fsp.appendFile(claimsFilePath(storeDir), `${lines.join("\n")}
1430
+ `, "utf8");
1431
+ try {
1432
+ if (await ledgerLineCount(storeDir) > maxLedgerLines) {
1433
+ await compactLedger(storeDir, active);
1434
+ }
1435
+ } catch (err) {
1436
+ warnFallbackOnce(
1437
+ `[review-claim-registry] ledger compaction failed (${err instanceof Error ? err.message : String(err)}); will retry on a future acquire`
1438
+ );
1439
+ }
1440
+ }
1441
+ return claims;
1442
+ },
1443
+ waitMs
1444
+ );
1445
+ }
1446
+ async function releaseSharedClaims(storeDir, claims, waitMs = LOCK_WAIT_MS) {
1447
+ if (claims.length === 0) return;
1448
+ await withStoreLock(
1449
+ storeDir,
1450
+ async () => {
1451
+ const now = Date.now();
1452
+ const active = await readLedger(storeDir, now);
1453
+ const lines = [];
1454
+ for (const claim of claims) {
1455
+ const bucket = active.get(claim.key);
1456
+ const live = bucket?.get(claim.fingerprint);
1457
+ if (live && live.sid === claim.sid && live.ts === claim.ts) {
1458
+ lines.push(
1459
+ JSON.stringify({
1460
+ op: "release",
1461
+ key: claim.key,
1462
+ fp: claim.fingerprint,
1463
+ sid: claim.sid,
1464
+ // Target the ORIGINAL claim time so a late release only ever
1465
+ // removes the claim it owns (see readLedger release matching).
1466
+ ts: claim.ts
1467
+ })
1468
+ );
1469
+ }
1470
+ }
1471
+ if (lines.length > 0) {
1472
+ await fsp.appendFile(claimsFilePath(storeDir), `${lines.join("\n")}
1473
+ `, "utf8");
1474
+ }
1475
+ },
1476
+ waitMs
1477
+ );
1478
+ }
1479
+ function ledgerFor(events) {
1480
+ let ledger = claimsByEventBus.get(events);
1481
+ if (!ledger) {
1482
+ ledger = /* @__PURE__ */ new Map();
1483
+ claimsByEventBus.set(events, ledger);
1484
+ }
1485
+ return ledger;
1486
+ }
1487
+ function claimFilesInMemory(events, cwd, files, ttlMs) {
1488
+ const ledger = ledgerFor(events);
1489
+ const now = Date.now();
1490
+ const claims = [];
1491
+ for (const file of files) {
1492
+ const key = claimKey(cwd, file.path);
1493
+ const fileFingerprint = fingerprint(file.content);
1494
+ let activeFingerprints = ledger.get(key);
1495
+ if (activeFingerprints) {
1496
+ for (const [fp, entry] of activeFingerprints) {
1497
+ if (now > entry.exp) activeFingerprints.delete(fp);
1498
+ }
1499
+ if (activeFingerprints.size === 0) {
1500
+ ledger.delete(key);
1501
+ activeFingerprints = void 0;
1502
+ }
1503
+ }
1504
+ if (!activeFingerprints) {
1505
+ activeFingerprints = /* @__PURE__ */ new Map();
1506
+ ledger.set(key, activeFingerprints);
1507
+ }
1508
+ if (activeFingerprints.has(fileFingerprint)) continue;
1509
+ activeFingerprints.set(fileFingerprint, { ts: now, exp: now + ttlMs });
1510
+ claims.push({ key, fingerprint: fileFingerprint, sid: HOST_SID, ts: now });
1511
+ }
1512
+ return claims;
1513
+ }
1514
+ function releaseClaimsInMemory(events, claims) {
1515
+ const ledger = ledgerFor(events);
1516
+ for (const claim of claims) {
1517
+ const activeFingerprints = ledger.get(claim.key);
1518
+ if (!activeFingerprints) continue;
1519
+ if (activeFingerprints.get(claim.fingerprint)?.ts === claim.ts) {
1520
+ activeFingerprints.delete(claim.fingerprint);
1521
+ }
1522
+ if (activeFingerprints.size === 0) ledger.delete(claim.key);
1523
+ }
1524
+ }
1525
+ async function claimFiles(events, cwd, files, opts) {
1526
+ const { storeDir, ttlMs, maxLedgerLines } = resolveStore(opts, cwd);
1527
+ try {
1528
+ const claims = await acquireClaims(storeDir, ttlMs, maxLedgerLines, cwd, files);
1529
+ return { claims, storeDir };
1530
+ } catch (err) {
1531
+ const code = err.code;
1532
+ if (code === CLAIM_STORE_LOCK_TIMEOUT) {
1533
+ try {
1534
+ const claims = await acquireClaims(
1535
+ storeDir,
1536
+ ttlMs,
1537
+ maxLedgerLines,
1538
+ cwd,
1539
+ files,
1540
+ LOCK_RETRY_BUDGET_MS
1541
+ );
1542
+ return { claims, storeDir };
1543
+ } catch (err2) {
1544
+ warnFallbackOnce(
1545
+ `[review-claim-registry] claim lock still contended after ${LOCK_WAIT_MS + LOCK_RETRY_BUDGET_MS}ms (${err2 instanceof Error ? err2.message : String(err2)}); falling back to in-memory dedup`
1546
+ );
1547
+ return { claims: claimFilesInMemory(events, cwd, files, ttlMs), storeDir: null };
1548
+ }
1549
+ }
1550
+ warnFallbackOnce(
1551
+ `[review-claim-registry] shared claim store unavailable (${err instanceof Error ? err.message : String(err)}); falling back to in-memory dedup`
1552
+ );
1553
+ return { claims: claimFilesInMemory(events, cwd, files, ttlMs), storeDir: null };
1554
+ }
1555
+ }
1556
+ async function releaseClaims(events, claims, storeDir) {
1557
+ if (storeDir === null) {
1558
+ releaseClaimsInMemory(events, claims);
1559
+ return;
1560
+ }
1561
+ try {
1562
+ await releaseSharedClaims(storeDir, claims);
1563
+ } catch (err) {
1564
+ if (err.code === CLAIM_STORE_LOCK_TIMEOUT) {
1565
+ try {
1566
+ await releaseSharedClaims(storeDir, claims, LOCK_RETRY_BUDGET_MS);
1567
+ return;
1568
+ } catch {
1569
+ }
1570
+ }
1571
+ warnFallbackOnce(
1572
+ `[review-claim-registry] shared claim release failed (${err instanceof Error ? err.message : String(err)}); claim will expire via its stored TTL`
1573
+ );
1574
+ }
1575
+ }
1576
+ async function recordStartedReview(events, payload, opts) {
1577
+ if (!payload || typeof payload !== "object") return;
1578
+ const bundle = payload;
1579
+ if (startedReviews.has(bundle) || pendingStartedReviews.has(bundle)) return;
1580
+ const candidate = payload;
1581
+ if (typeof candidate.cwd !== "string" || !Array.isArray(candidate.files)) return;
1582
+ const cwd = candidate.cwd;
1583
+ const files = candidate.files.filter(
1584
+ (file) => Boolean(file) && typeof file === "object" && typeof file.path === "string" && typeof file.content === "string"
1585
+ );
1586
+ const pending = (async () => {
1587
+ try {
1588
+ const { claims, storeDir } = await claimFiles(events, cwd, files, opts);
1589
+ if (claims.length === 0) return null;
1590
+ const entry = { events, storeDir, claims };
1591
+ startedReviews.set(bundle, entry);
1592
+ return entry;
1593
+ } catch {
1594
+ return null;
1595
+ }
1596
+ })();
1597
+ pendingStartedReviews.set(bundle, pending);
1598
+ try {
1599
+ await pending;
1600
+ } finally {
1601
+ if (pendingStartedReviews.get(bundle) === pending) {
1602
+ pendingStartedReviews.delete(bundle);
1603
+ }
1604
+ }
1605
+ }
1606
+ async function recordCompletedReview(events, payload) {
1607
+ if (!payload || typeof payload !== "object") return;
1608
+ const bundle = payload.bundle;
1609
+ if (!bundle || typeof bundle !== "object") return;
1610
+ let startedReview = startedReviews.get(bundle);
1611
+ if (!startedReview) {
1612
+ const pending = pendingStartedReviews.get(bundle);
1613
+ if (pending) startedReview = await pending ?? void 0;
1614
+ }
1615
+ if (!startedReview || startedReview.events !== events) return;
1616
+ await releaseClaims(events, startedReview.claims, startedReview.storeDir);
1617
+ startedReviews.delete(bundle);
1618
+ }
1619
+ async function emitReviewIfChanged(api, bundle, opts) {
1620
+ const { claims, storeDir } = await claimFiles(api.events, bundle.cwd, bundle.files, opts);
1621
+ if (claims.length === 0) return null;
1622
+ const claimed = new Set(claims.map((claim) => `${claim.key}\0${claim.fingerprint}`));
1623
+ const claimedBundle = {
1624
+ ...bundle,
1625
+ // Filter on key+FINGERPRINT pairs, not just key: two entries for the same
1626
+ // path in one batch can differ in content, and a fingerprint claimed by
1627
+ // another session must not ride along in the emitted bundle.
1628
+ files: bundle.files.filter(
1629
+ (file) => claimed.has(`${claimKey(bundle.cwd, file.path)}\0${fingerprint(file.content)}`)
1630
+ )
1631
+ };
1632
+ startedReviews.set(claimedBundle, {
1633
+ events: api.events,
1634
+ storeDir,
1635
+ claims
1636
+ });
1637
+ try {
1638
+ api.emitCustom("chimera.review_needed", claimedBundle);
1639
+ return claimedBundle;
1640
+ } catch (err) {
1641
+ startedReviews.delete(claimedBundle);
1642
+ await releaseClaims(api.events, claims, storeDir);
1643
+ throw err;
1644
+ }
1645
+ }
1646
+
1647
+ // src/plugins/auto-review-plugin.ts
1648
+ import * as fsp3 from "node:fs/promises";
1649
+ import * as path3 from "node:path";
1650
+
1651
+ // src/plugins/auto-review-git.ts
1652
+ import { spawn } from "node:child_process";
1653
+ import { createHash as createHash2 } from "node:crypto";
1654
+ import * as fsp2 from "node:fs/promises";
1655
+ import * as path2 from "node:path";
1656
+ var MAX_SNAPSHOT_FILE_BYTES = 256 * 1024;
1657
+ var MAX_SNAPSHOT_TOTAL_BYTES = 8 * 1024 * 1024;
1658
+ async function runGit(args, cwd) {
1659
+ return new Promise((resolve3, reject) => {
1660
+ let child;
1661
+ try {
1662
+ child = spawn("git", args, {
1663
+ cwd,
1664
+ stdio: ["ignore", "pipe", "pipe"],
1665
+ signal: AbortSignal.timeout(15e3),
1666
+ windowsHide: true
1667
+ });
1668
+ } catch (err) {
1669
+ reject(err);
1670
+ return;
1671
+ }
1672
+ let stdout = "";
1673
+ let stderr = "";
1674
+ child.stdout?.on("data", (d) => {
1675
+ stdout += d;
1676
+ });
1677
+ child.stderr?.on("data", (d) => {
1678
+ stderr += d;
1679
+ });
1680
+ child.on("error", () => resolve3({ stdout, stderr, code: 1 }));
1681
+ child.on("close", (code) => resolve3({ stdout, stderr, code: code ?? 0 }));
1682
+ });
1683
+ }
1684
+ async function isGitRepo(cwd) {
1685
+ const r = await runGit(["rev-parse", "--git-dir"], cwd);
1686
+ return r.code === 0;
1687
+ }
1688
+ async function getChangedFiles(cwd) {
1689
+ const r = await runGit(["status", "--porcelain", "--untracked-files=no"], cwd);
1690
+ if (r.code !== 0) return [];
1691
+ const files = [];
1692
+ for (const line of r.stdout.split("\n")) {
1693
+ if (!line.trim()) continue;
1694
+ const x = line[0] ?? " ";
1695
+ const y = line[1] ?? " ";
1696
+ const rawPath = line.slice(3).trim();
1697
+ const filePath = x === "R" || x === "C" ? rawPath.split(" -> ").pop()?.trim() ?? rawPath : rawPath;
1698
+ if (x === "A" || y === "A") {
1699
+ files.push({ path: filePath, status: "added" });
1700
+ } else if (x === "M" || y === "M" || x === "R" || x === "C") {
1701
+ files.push({ path: filePath, status: "modified" });
1702
+ }
1703
+ }
1704
+ return files;
1705
+ }
1706
+ async function snapshotChangedFiles(cwd) {
1707
+ const snapshots = [];
1708
+ let budget = MAX_SNAPSHOT_TOTAL_BYTES;
1709
+ for (const file of await getChangedFiles(cwd)) {
1710
+ if (file.path.startsWith(".wrongstack/")) continue;
1711
+ if (budget <= 0) break;
1712
+ try {
1713
+ const stat3 = await fsp2.stat(path2.join(cwd, file.path));
1714
+ if (!stat3.isFile() || stat3.size > MAX_SNAPSHOT_FILE_BYTES) continue;
1715
+ const content = await fsp2.readFile(path2.join(cwd, file.path), "utf8");
1716
+ budget -= content.length;
1717
+ snapshots.push({
1718
+ ...file,
1719
+ content,
1720
+ fingerprint: createHash2("sha256").update(content).digest("hex")
1721
+ });
1722
+ } catch {
1723
+ }
1724
+ }
1725
+ return snapshots;
1726
+ }
1727
+
1728
+ // src/plugins/review-context-builder.ts
1729
+ import { spawn as spawn2 } from "node:child_process";
1730
+ var GIT_TIMEOUT_MS = 15e3;
1731
+ var MAX_DIFF_BYTES = 1e5;
1732
+ var MAX_RECENT_COMMITS = 10;
1733
+ async function runGit2(args, cwd) {
1734
+ return new Promise((resolve3, reject) => {
1735
+ let child;
1736
+ try {
1737
+ child = spawn2("git", args, {
1738
+ cwd,
1739
+ stdio: ["ignore", "pipe", "pipe"],
1740
+ signal: AbortSignal.timeout(GIT_TIMEOUT_MS),
1741
+ windowsHide: true
1742
+ });
1743
+ } catch (err) {
1744
+ reject(err);
1745
+ return;
1746
+ }
1747
+ let stdout = "";
1748
+ let stderr = "";
1749
+ child.stdout?.on("data", (d) => {
1750
+ stdout += d;
1751
+ });
1752
+ child.stderr?.on("data", (d) => {
1753
+ stderr += d;
1754
+ });
1755
+ child.on("error", () => resolve3({ stdout, stderr, code: 1 }));
1756
+ child.on("close", (code) => resolve3({ stdout, stderr, code: code ?? 0 }));
1757
+ });
1758
+ }
1759
+ async function getFileDiff(cwd, filePath) {
1760
+ const r = await runGit2(["diff", "HEAD", "--", filePath], cwd);
1761
+ if (r.code !== 0 || !r.stdout.trim()) return void 0;
1762
+ return r.stdout.length > MAX_DIFF_BYTES ? `${r.stdout.slice(0, MAX_DIFF_BYTES)}
1763
+ ... (diff truncated at ${MAX_DIFF_BYTES} bytes \u2014 please use the read tool to view full file content)` : r.stdout;
1764
+ }
1765
+ function parsePorcelainStatusZ(stdout) {
1766
+ const records = stdout.split("\0");
1767
+ const out = [];
1768
+ for (let index = 0; index < records.length; index++) {
1769
+ const record = records[index];
1770
+ if (!record || record.length < 3) continue;
1771
+ const statusCode = record.slice(0, 2).trim();
1772
+ const filePath = record.slice(3);
1773
+ if (filePath) out.push({ path: filePath, status: statusCode });
1774
+ if (statusCode.includes("R") || statusCode.includes("C")) index++;
1775
+ }
1776
+ return out;
1777
+ }
1778
+ async function getAllChangedFiles(cwd) {
1779
+ const r = await runGit2(["status", "--porcelain", "-z"], cwd);
1780
+ return r.code === 0 ? parsePorcelainStatusZ(r.stdout) : [];
1781
+ }
1782
+ async function getRecentCommits(cwd) {
1783
+ const r = await runGit2(["log", `-${MAX_RECENT_COMMITS}`, "--oneline", "--format=%s"], cwd);
1784
+ if (r.code !== 0) return [];
1785
+ return r.stdout.split("\n").filter(Boolean);
1786
+ }
1787
+ async function buildReviewContext(opts) {
1788
+ const { cwd, config } = opts;
1789
+ const filesWithDiffs = [];
1790
+ for (const f of opts.files) {
1791
+ const entry = {
1792
+ path: f.path,
1793
+ status: f.status,
1794
+ content: f.content
1795
+ };
1796
+ if (f.status === "modified") {
1797
+ try {
1798
+ entry.diff = await getFileDiff(cwd, f.path);
1799
+ } catch {
1800
+ }
1801
+ }
1802
+ filesWithDiffs.push(entry);
1803
+ }
1804
+ let allChangedFiles;
1805
+ try {
1806
+ allChangedFiles = await getAllChangedFiles(cwd);
1807
+ } catch {
1808
+ }
1809
+ let recentCommits;
1810
+ try {
1811
+ recentCommits = await getRecentCommits(cwd);
1812
+ if (recentCommits.length === 0) recentCommits = void 0;
1813
+ } catch {
1814
+ }
1815
+ const activeTodos = opts.activeTodos && opts.activeTodos.length > 0 ? opts.activeTodos : void 0;
1816
+ const cascadeOn = opts.cascadeOn ?? void 0;
1817
+ const cascadeDepth = opts.cascadeDepth ?? void 0;
1818
+ const maxCascadeDepth = opts.maxCascadeDepth ?? void 0;
1819
+ let kanbanCard;
1820
+ try {
1821
+ kanbanCard = await findActiveKanbanCard(cwd);
1822
+ } catch {
1823
+ }
1824
+ let fileProvenance;
1825
+ try {
1826
+ const paths = opts.files.map((f) => f.path);
1827
+ fileProvenance = await findFileProvenance(cwd, paths);
1828
+ if (fileProvenance.length === 0) fileProvenance = void 0;
1829
+ } catch {
1830
+ }
1831
+ return {
1832
+ config,
1833
+ cwd,
1834
+ files: filesWithDiffs,
1835
+ allChangedFiles,
1836
+ recentCommits,
1837
+ activeTodos,
1838
+ cascadeOn,
1839
+ cascadeDepth,
1840
+ maxCascadeDepth,
1841
+ kanbanCard,
1842
+ fileProvenance
1843
+ };
1844
+ }
1845
+ async function findActiveKanbanCard(projectRoot) {
1846
+ let kanbanApi;
1847
+ try {
1848
+ kanbanApi = await import("@wrongstack/kanban");
1849
+ } catch {
1850
+ return void 0;
1851
+ }
1852
+ const activeTasks = [];
1853
+ const boards = await kanbanApi.listBoards(projectRoot);
1854
+ for (const board of boards) {
1855
+ const data = await kanbanApi.getBoard(projectRoot, board.id);
1856
+ if (!data?.tasks) continue;
1857
+ activeTasks.push(
1858
+ ...data.tasks.filter(
1859
+ (task2) => task2.status === "in_progress" || task2.status === "review" || task2.lifecycle?.currentStage === "running" || task2.lifecycle?.currentStage === "review"
1860
+ )
1861
+ );
1862
+ }
1863
+ if (activeTasks.length !== 1) return void 0;
1864
+ const task = activeTasks[0];
1865
+ if (!task) return void 0;
1866
+ return {
1867
+ id: task.id,
1868
+ title: task.title,
1869
+ description: task.description,
1870
+ successCriteria: task.successCriteria?.map((criterion) => criterion.description)
1871
+ };
1872
+ }
1873
+ async function findFileProvenance(projectRoot, filePaths) {
1874
+ if (filePaths.length === 0) return [];
1875
+ let access3;
1876
+ try {
1877
+ access3 = createChronicleProjectAccess({ projectRoot });
1878
+ } catch {
1879
+ return [];
1880
+ }
1881
+ const provenance = [];
1882
+ try {
1883
+ const uniquePaths = [...new Set(filePaths)];
1884
+ const result = await access3.call("metrics", {
1885
+ view: "files",
1886
+ refresh: false,
1887
+ files: {
1888
+ paths: uniquePaths,
1889
+ latestPerPath: true,
1890
+ limit: Math.min(1e4, uniquePaths.length)
1891
+ }
1892
+ });
1893
+ const rows = result.data;
1894
+ const byPath = new Map(rows.map((row) => [normalizeReviewPath(row.path), row]));
1895
+ for (const filePath of uniquePaths) {
1896
+ const row = byPath.get(normalizeReviewPath(filePath));
1897
+ if (!row) continue;
1898
+ provenance.push({
1899
+ path: filePath,
1900
+ agentId: row.agentId || void 0,
1901
+ taskId: row.taskId || void 0,
1902
+ eventType: lineageEventType(row),
1903
+ observedAt: row.occurredAt
1904
+ });
1905
+ }
1906
+ } finally {
1907
+ await access3.close();
1908
+ }
1909
+ return provenance;
1910
+ }
1911
+ function normalizeReviewPath(filePath) {
1912
+ return filePath.replaceAll("\\", "/").toLocaleLowerCase();
1913
+ }
1914
+ function lineageEventType(row) {
1915
+ return row.source === "external" ? `file.external.${row.operation || "modified"}` : "file.event";
1916
+ }
1917
+
1918
+ // src/plugins/auto-review-plugin.ts
1919
+ function buildAutoReviewCommand(getConfig, getInFlightCount) {
1920
+ return {
1921
+ name: "auto-review",
1922
+ category: "Session",
1923
+ description: "Show continuous auto-review status and configuration.",
1924
+ help: [
1925
+ "\u2554\u2550\u2550\u2550 Auto Review \u2550\u2550\u2550\u2557",
1926
+ "",
1927
+ "Continuous code review that fires on every change during a session.",
1928
+ "Detects git-tracked file edits and dispatches review subagents",
1929
+ "with configurable provider/model/fallback and profile selection.",
1930
+ "",
1931
+ "Reports are persisted and shown as passive notifications.",
1932
+ "They never wake the leader or spawn mutating follow-up agents.",
1933
+ "",
1934
+ "Commands:",
1935
+ " /auto-review Show current status and config",
1936
+ " /auto-review on Enable auto-review",
1937
+ " /auto-review off Disable auto-review",
1938
+ "",
1939
+ "Configuration (edit config.json extensions.wstack-auto-review):",
1940
+ " enabled true | false",
1941
+ " provider provider id for review agents",
1942
+ " model model id for review agents",
1943
+ " fallbackProfile named profile from config.fallbackProfiles",
1944
+ " modelSelection round-robin | random (default round-robin)",
1945
+ " debounceMs debounce window (default 15000)",
1946
+ " maxFilesPerBatch max files per review (default 15)",
1947
+ " maxConcurrentReviews max parallel reviews (default 2)",
1948
+ " cascadeOn off | critical | high (default off)",
1949
+ " maxCascadeDepth max fix+re-review cycles (default 2)"
1950
+ ].join("\n"),
1951
+ async run(args) {
1952
+ const cfg = getConfig();
1953
+ const trimmed = (args ?? "").trim().toLowerCase();
1954
+ if (trimmed === "on" || trimmed === "enable") {
1955
+ return {
1956
+ message: "Auto-review enable/disable via config.json extensions.wstack-auto-review.enabled"
1957
+ };
1958
+ }
1959
+ if (trimmed === "off" || trimmed === "disable") {
1960
+ return {
1961
+ message: "Auto-review enable/disable via config.json extensions.wstack-auto-review.enabled"
1962
+ };
1963
+ }
1964
+ const inFlight = getInFlightCount();
1965
+ return {
1966
+ message: [
1967
+ `\u{1F4CB} Auto Review \u2014 ${cfg.enabled ? "\u2705 enabled" : "\u23F8\uFE0F disabled"}`,
1968
+ "",
1969
+ ` Provider: ${cfg.provider}`,
1970
+ ` Model: ${cfg.model}`,
1971
+ ` Fallback / RR: ${cfg.fallbackModels.length > 0 ? cfg.fallbackModels.join(", ") : "(none)"}`,
1972
+ ` Selection: ${cfg.modelSelection}`,
1973
+ ` Debounce: ${cfg.debounceMs} ms`,
1974
+ ` Max files: ${cfg.maxFilesPerBatch}`,
1975
+ ` Max parallel: ${cfg.maxConcurrentReviews}`,
1976
+ ` Cascade: ${cfg.cascadeOn === "off" ? "off" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`}`,
1977
+ ` Max depth: ${cfg.maxCascadeDepth} re-review cycle(s)`,
1978
+ ` In-flight: ${inFlight} review(s)`,
1979
+ "",
1980
+ "Fires on every git-tracked file change during the session.",
1981
+ "Disabled by default. Enable via config.json."
1982
+ ].join("\n")
1983
+ };
1984
+ }
1985
+ };
1986
+ }
1987
+ function createAutoReviewPlugin() {
1988
+ return {
1989
+ name: "wstack-auto-review",
1990
+ version: "1.0.0",
1991
+ description: "Continuous auto-review that fires on every code change during a session.",
1992
+ apiVersion: "^0.1",
1993
+ capabilities: { slashCommands: true },
1994
+ defaultConfig: {},
1995
+ setup(api) {
1996
+ const recompute = () => {
1997
+ const raw = api.config.extensions?.["wstack-auto-review"] ?? {};
1998
+ return resolveAutoReviewConfig(raw, api.config);
1999
+ };
2000
+ let resolved = recompute();
2001
+ const inFlight = [];
2002
+ const expireInFlight = (entry) => {
2003
+ const timer = setTimeout(
2004
+ () => {
2005
+ const idx = inFlight.indexOf(entry);
2006
+ if (idx !== -1) inFlight.splice(idx, 1);
2007
+ },
2008
+ 5 * 60 * 1e3
2009
+ );
2010
+ timer.unref();
2011
+ };
2012
+ api.onConfigChange(() => {
2013
+ const old = resolved;
2014
+ resolved = recompute();
2015
+ if (old.enabled !== resolved.enabled || old.provider !== resolved.provider || old.model !== resolved.model) {
2016
+ api.log.info(
2017
+ `[auto-review] config changed \u2014 enabled=${resolved.enabled} provider=${resolved.provider} model=${resolved.model}`
2018
+ );
2019
+ }
2020
+ });
2021
+ if (!resolved.enabled) {
2022
+ api.log.info("[auto-review] disabled by config");
2023
+ return;
2024
+ }
2025
+ api.log.info(
2026
+ `[auto-review] loaded \u2014 provider=${resolved.provider} model=${resolved.model} debounce=${resolved.debounceMs}ms cascade=${resolved.cascadeOn}`
2027
+ );
2028
+ const knownFingerprints = /* @__PURE__ */ new Map();
2029
+ const pendingFiles = /* @__PURE__ */ new Map();
2030
+ let reviewCounter = 0;
2031
+ let pendingReviewTimer;
2032
+ let pendingReviewWork;
2033
+ let sessionEnded = false;
2034
+ let latestIterationPayload;
2035
+ const rememberKnownFingerprint = (filePath, fingerprint2) => {
2036
+ if (knownFingerprints.has(filePath)) knownFingerprints.delete(filePath);
2037
+ knownFingerprints.set(filePath, fingerprint2);
2038
+ trimKnownFingerprints(knownFingerprints, MAX_KNOWN_FINGERPRINTS);
2039
+ };
2040
+ let snapshotInFlight = false;
2041
+ const withSnapshotLock = async (fn) => {
2042
+ if (snapshotInFlight) {
2043
+ api.log.info("[auto-review] snapshot pass already in flight, skipping");
2044
+ return void 0;
2045
+ }
2046
+ snapshotInFlight = true;
2047
+ try {
2048
+ return await fn();
2049
+ } finally {
2050
+ snapshotInFlight = false;
2051
+ }
2052
+ };
2053
+ api.onEvent("agent.run.started", async () => {
2054
+ try {
2055
+ const cwd = api.config.cwd ?? process.cwd();
2056
+ if (!await isGitRepo(cwd)) return;
2057
+ const snapshots = await withSnapshotLock(() => snapshotChangedFiles(cwd));
2058
+ if (!snapshots) return;
2059
+ for (const file of snapshots) {
2060
+ rememberKnownFingerprint(file.path, file.fingerprint);
2061
+ }
2062
+ api.log.info(`[auto-review] seeded ${knownFingerprints.size} known file snapshot(s)`);
2063
+ } catch {
2064
+ }
2065
+ });
2066
+ api.slashCommands.register(
2067
+ buildAutoReviewCommand(
2068
+ () => resolved,
2069
+ () => inFlight.length
2070
+ )
2071
+ );
2072
+ const cancelPendingReviewTimer = () => {
2073
+ if (!pendingReviewTimer) return;
2074
+ clearTimeout(pendingReviewTimer);
2075
+ pendingReviewTimer = void 0;
2076
+ };
2077
+ let handleIterationCompleted;
2078
+ const schedulePendingReview = (delayMs) => {
2079
+ cancelPendingReviewTimer();
2080
+ if (sessionEnded || pendingFiles.size === 0) return;
2081
+ const timer = setTimeout(
2082
+ async () => {
2083
+ if (pendingReviewTimer === timer) pendingReviewTimer = void 0;
2084
+ if (sessionEnded) return;
2085
+ const work = handleIterationCompleted?.(latestIterationPayload, true);
2086
+ if (!work) return;
2087
+ pendingReviewWork = work;
2088
+ try {
2089
+ await work;
2090
+ } finally {
2091
+ if (pendingReviewWork === work) pendingReviewWork = void 0;
2092
+ }
2093
+ },
2094
+ Math.max(0, delayMs)
2095
+ );
2096
+ pendingReviewTimer = timer;
2097
+ timer.unref();
2098
+ };
2099
+ const refreshPendingFiles = (snapshots) => {
2100
+ let pendingChanged = false;
2101
+ const observedPaths = /* @__PURE__ */ new Set();
2102
+ for (const file of snapshots) {
2103
+ if (file.path.startsWith(".wrongstack/")) continue;
2104
+ observedPaths.add(file.path);
2105
+ if (knownFingerprints.get(file.path) !== file.fingerprint) {
2106
+ if (pendingFiles.get(file.path)?.fingerprint !== file.fingerprint) {
2107
+ pendingChanged = true;
2108
+ }
2109
+ pendingFiles.set(file.path, file);
2110
+ } else if (pendingFiles.delete(file.path)) {
2111
+ pendingChanged = true;
2112
+ }
2113
+ }
2114
+ for (const pendingPath of pendingFiles.keys()) {
2115
+ if (!observedPaths.has(pendingPath)) {
2116
+ pendingFiles.delete(pendingPath);
2117
+ pendingChanged = true;
2118
+ }
2119
+ }
2120
+ return pendingChanged;
2121
+ };
2122
+ handleIterationCompleted = async (payload, fromQuietTimer = false) => {
2123
+ try {
2124
+ const cfg = resolved;
2125
+ if (!cfg.enabled || sessionEnded) return;
2126
+ if (payload) latestIterationPayload = payload;
2127
+ const cwd = api.config.cwd ?? process.cwd();
2128
+ if (!await isGitRepo(cwd)) return;
2129
+ const ctxTodos = payload?.ctx?.todos;
2130
+ const snapshots = await withSnapshotLock(() => snapshotChangedFiles(cwd));
2131
+ if (!snapshots) {
2132
+ if (fromQuietTimer) schedulePendingReview(cfg.debounceMs);
2133
+ return;
2134
+ }
2135
+ if (sessionEnded) return;
2136
+ const now = Date.now();
2137
+ const pendingChanged = refreshPendingFiles(snapshots);
2138
+ if (pendingFiles.size === 0) {
2139
+ cancelPendingReviewTimer();
2140
+ return;
2141
+ }
2142
+ if (cfg.debounceMs > 0) {
2143
+ if (!fromQuietTimer) {
2144
+ if (pendingChanged || !pendingReviewTimer) {
2145
+ schedulePendingReview(cfg.debounceMs);
2146
+ api.log.info(
2147
+ `[auto-review] waiting ${cfg.debounceMs}ms for ${pendingFiles.size} pending file(s) to settle`
2148
+ );
2149
+ }
2150
+ return;
2151
+ }
2152
+ if (pendingChanged) {
2153
+ schedulePendingReview(cfg.debounceMs);
2154
+ api.log.info(
2155
+ `[auto-review] file activity continued, restarting ${cfg.debounceMs}ms quiet window for ${pendingFiles.size} pending file(s)`
2156
+ );
2157
+ return;
2158
+ }
2159
+ }
2160
+ if (inFlight.length >= cfg.maxConcurrentReviews) {
2161
+ api.log.info(
2162
+ `[auto-review] at max concurrent (${cfg.maxConcurrentReviews}), retaining ${pendingFiles.size} pending file(s)`
2163
+ );
2164
+ return;
2165
+ }
2166
+ const toReview = [...pendingFiles.values()].slice(0, cfg.maxFilesPerBatch);
2167
+ if (toReview.length === 0) return;
2168
+ const filesWithContent = toReview.map((file) => ({
2169
+ path: file.path,
2170
+ status: file.status,
2171
+ content: file.content
2172
+ }));
2173
+ reviewCounter++;
2174
+ const bundle = await buildReviewContext({
2175
+ cwd,
2176
+ config: {
2177
+ enabled: true,
2178
+ provider: cfg.provider,
2179
+ model: cfg.model,
2180
+ maxFiles: cfg.maxFilesPerBatch,
2181
+ autoFix: "off",
2182
+ cascadeOn: "off",
2183
+ maxCascadeDepth: 0,
2184
+ fallbackModels: [...cfg.fallbackModels],
2185
+ fallbackProfile: void 0
2186
+ },
2187
+ files: filesWithContent,
2188
+ activeTodos: ctxTodos,
2189
+ cascadeOn: cfg.cascadeOn,
2190
+ maxCascadeDepth: cfg.maxCascadeDepth
2191
+ });
2192
+ bundle.reviewFallbackModels = [...cfg.fallbackModels];
2193
+ bundle.reviewModelSelection = cfg.modelSelection;
2194
+ const trackedChangedPaths = new Set(
2195
+ snapshots.filter((file) => !file.path.startsWith(".wrongstack/")).map((file) => file.path)
2196
+ );
2197
+ bundle.allChangedFiles = bundle.allChangedFiles?.filter(
2198
+ (file) => trackedChangedPaths.has(file.path)
2199
+ );
2200
+ if (sessionEnded) return;
2201
+ api.log.info(
2202
+ `[auto-review] #${reviewCounter} emitting review_needed (${filesWithContent.length} files, provider=${cfg.provider} model=${cfg.model})`
2203
+ );
2204
+ const emittedBundle = await emitReviewIfChanged(api, bundle);
2205
+ if (!emittedBundle) {
2206
+ api.log.info(
2207
+ `[auto-review] #${reviewCounter} skipped \u2014 file content already has a review in progress`
2208
+ );
2209
+ return;
2210
+ }
2211
+ const emittedPaths = new Set(emittedBundle.files.map((file) => file.path));
2212
+ const inflightEntry = {
2213
+ files: [...emittedPaths],
2214
+ startedAt: now,
2215
+ subagentType: "review"
2216
+ };
2217
+ inFlight.push(inflightEntry);
2218
+ for (const file of toReview) {
2219
+ if (!emittedPaths.has(file.path)) continue;
2220
+ rememberKnownFingerprint(file.path, file.fingerprint);
2221
+ pendingFiles.delete(file.path);
2222
+ }
2223
+ api.log.info(
2224
+ `[auto-review] #${reviewCounter} event emitted \u2014 ${emittedBundle.files.length}/${filesWithContent.length} files; requires Director (--director) for subagent spawning`
2225
+ );
2226
+ expireInFlight(inflightEntry);
2227
+ if (pendingFiles.size > 0 && cfg.debounceMs > 0) {
2228
+ schedulePendingReview(cfg.debounceMs);
2229
+ }
2230
+ } catch (err) {
2231
+ api.log.warn(`[auto-review] iteration.completed handler failed: ${toErrorMessage(err)}`);
2232
+ }
2233
+ };
2234
+ api.onEvent("iteration.completed", async (payload) => {
2235
+ await handleIterationCompleted?.(payload);
2236
+ });
2237
+ api.onEvent("session.ended", (event) => {
2238
+ const work = (async () => {
2239
+ sessionEnded = true;
2240
+ const handedOffFiles = pendingFiles.size;
2241
+ cancelPendingReviewTimer();
2242
+ if (handedOffFiles > 0) {
2243
+ api.log.info(
2244
+ `[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
2245
+ );
2246
+ }
2247
+ await pendingReviewWork;
2248
+ try {
2249
+ const cfg = resolved;
2250
+ if (!cfg.enabled) return;
2251
+ const cwd = api.config.cwd ?? process.cwd();
2252
+ if (!await isGitRepo(cwd)) return;
2253
+ const allChanged = await getChangedFiles(cwd);
2254
+ const existing = [];
2255
+ for (const f of allChanged) {
2256
+ if (f.path.startsWith(".wrongstack/")) continue;
2257
+ try {
2258
+ await fsp3.access(path3.join(cwd, f.path));
2259
+ existing.push(f);
2260
+ } catch {
2261
+ }
2262
+ }
2263
+ if (existing.length === 0) return;
2264
+ const toReview = existing.slice(0, cfg.maxFilesPerBatch);
2265
+ const filesWithContent = [];
2266
+ for (const f of toReview) {
2267
+ try {
2268
+ const absPath = path3.join(cwd, f.path);
2269
+ const content = await fsp3.readFile(absPath, "utf8");
2270
+ filesWithContent.push({ path: f.path, status: f.status, content });
2271
+ } catch {
2272
+ }
2273
+ }
2274
+ if (filesWithContent.length === 0) return;
2275
+ if (inFlight.length >= cfg.maxConcurrentReviews) {
2276
+ api.log.info(
2277
+ `[auto-review] session end \u2014 at max concurrent (${cfg.maxConcurrentReviews}), skipping final review`
2278
+ );
2279
+ return;
2280
+ }
2281
+ const bundle = await buildReviewContext({
2282
+ cwd,
2283
+ config: {
2284
+ enabled: true,
2285
+ provider: cfg.provider,
2286
+ model: cfg.model,
2287
+ maxFiles: cfg.maxFilesPerBatch,
2288
+ autoFix: "off",
2289
+ cascadeOn: "off",
2290
+ maxCascadeDepth: 0,
2291
+ fallbackModels: [...cfg.fallbackModels],
2292
+ fallbackProfile: void 0
2293
+ },
2294
+ files: filesWithContent,
2295
+ cascadeOn: cfg.cascadeOn,
2296
+ maxCascadeDepth: cfg.maxCascadeDepth
2297
+ });
2298
+ bundle.reviewFallbackModels = [...cfg.fallbackModels];
2299
+ bundle.reviewModelSelection = cfg.modelSelection;
2300
+ const trackedChangedPaths = new Set(existing.map((file) => file.path));
2301
+ bundle.allChangedFiles = bundle.allChangedFiles?.filter(
2302
+ (file) => trackedChangedPaths.has(file.path)
2303
+ );
2304
+ const emittedBundle = await emitReviewIfChanged(api, bundle);
2305
+ if (!emittedBundle) {
2306
+ api.log.info(
2307
+ "[auto-review] session end \u2014 unchanged files already have reviews in progress"
2308
+ );
2309
+ return;
2310
+ }
2311
+ const inflightEntry = {
2312
+ files: emittedBundle.files.map((file) => file.path),
2313
+ startedAt: Date.now(),
2314
+ subagentType: "review"
2315
+ };
2316
+ inFlight.push(inflightEntry);
2317
+ expireInFlight(inflightEntry);
2318
+ api.log.info(
2319
+ `[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
2320
+ );
2321
+ } catch (err) {
2322
+ api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
2323
+ }
2324
+ })();
2325
+ event?.waitUntil?.(work);
2326
+ return work;
2327
+ });
2328
+ api.onPattern("chimera.review_complete", (_event, payload) => {
2329
+ try {
2330
+ const p = payload;
2331
+ if (!p.reviewText) return;
2332
+ const cascadeOn = p.bundle.cascadeOn ?? "off";
2333
+ if (cascadeOn === "off") return;
2334
+ const parsed = p.parsedReport;
2335
+ const verifiedFindings = parsed?.findings.filter((f) => f.verification?.status === "verified") ?? [];
2336
+ const severities = parsed ? severitiesFromFindings(verifiedFindings) : parseReviewSeverity(p.reviewText);
2337
+ const threshold = shouldCascade(cascadeOn, severities);
2338
+ if (!threshold) return;
2339
+ const agents = decideCascadeAgents(
2340
+ p.reviewText,
2341
+ severities,
2342
+ parsed ? verifiedFindings : void 0
2343
+ );
2344
+ if (agents.length === 0) return;
2345
+ const cascadePayload = {
2346
+ bundle: p.bundle,
2347
+ ...p.reportId ? { reportId: p.reportId } : {},
2348
+ reviewText: p.reviewText,
2349
+ severities,
2350
+ threshold,
2351
+ agents,
2352
+ ...parsed ? { verifiedFindings } : {}
2353
+ };
2354
+ api.emitCustom("chimera.cascade_needed", cascadePayload);
2355
+ api.log.info(
2356
+ `[auto-review] cascade_needed emitted \u2014 ${severities.critical} critical, ${severities.high} high, ${severities.medium} medium; agents: ${agents.join(", ")}${parsed ? ` (gated on ${verifiedFindings.length} verified finding(s))` : ""}`
2357
+ );
2358
+ } catch (err) {
2359
+ api.log.warn(
2360
+ `[auto-review] review_complete cascade handler failed: ${toErrorMessage(err)}`
2361
+ );
2362
+ }
2363
+ });
2364
+ },
2365
+ teardown(api) {
2366
+ api.slashCommands.unregister("auto-review");
2367
+ api.log.info("[auto-review] unloaded");
2368
+ },
2369
+ async health() {
2370
+ return { ok: true, message: "auto-review ready" };
2371
+ }
2372
+ };
2373
+ }
2374
+
2375
+ // src/plugins/review-finding-commands.ts
2376
+ async function executeFindingCommand(args, ctx) {
2377
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
2378
+ return helpText();
2379
+ }
2380
+ const subcommand = args[0];
2381
+ if (subcommand === "findings" || subcommand === "list") {
2382
+ return listFindings(args.slice(1), ctx);
2383
+ }
2384
+ if (subcommand === "finding") {
2385
+ const id = args[1];
2386
+ if (!id) return "Usage: /review finding <id> [resolve|ignore|reopen|triage|start]";
2387
+ const action = args[2];
2388
+ if (action === "resolve" || action === "ignore" || action === "reopen" || action === "triage" || action === "start") {
2389
+ return transitionFinding(id, action, args.slice(3), ctx);
2390
+ }
2391
+ return showFinding(id, ctx);
2392
+ }
2393
+ if (subcommand === "reports") {
2394
+ return listReports(args.slice(1), ctx);
2395
+ }
2396
+ if (subcommand === "report") {
2397
+ const id = args[1];
2398
+ if (!id) return "Usage: /review report <id> [complete|skip|action|note]";
2399
+ const action = args[2];
2400
+ if (action === "complete" || action === "skip" || action === "action") {
2401
+ return transitionReport(id, action, args.slice(3), ctx);
2402
+ }
2403
+ if (action === "note") {
2404
+ return addReportNote(id, args.slice(3), ctx);
2405
+ }
2406
+ return showReport(id, ctx);
2407
+ }
2408
+ if (subcommand === "status") {
2409
+ return showStatus(ctx);
2410
+ }
2411
+ return `Unknown subcommand: ${subcommand}. Use /review findings --help for usage.`;
2412
+ }
2413
+ function helpText() {
2414
+ return [
2415
+ "## /review \u2014 Chimera Review Store",
2416
+ "",
2417
+ "| Command | Description |",
2418
+ "|---|---|",
2419
+ "| `/review findings` | List all active findings grouped by severity |",
2420
+ "| `/review findings --status <s>` | Filter by status (active, triaged, in_progress, resolved, ignored) |",
2421
+ "| `/review findings --severity <s>` | Filter by severity (critical, high, medium, low) |",
2422
+ "| `/review findings --limit <n>` | Max results (default 20) |",
2423
+ "| `/review finding <id>` | Show a single finding with full lifecycle |",
2424
+ '| `/review finding <id> resolve --outcome <fixed|wontfix|duplicate|false_positive|stale> [--reason "..."]` | Mark a finding resolved |',
2425
+ '| `/review finding <id> ignore [--reason "..."]` | Ignore a finding (wontfix) |',
2426
+ '| `/review finding <id> reopen [--reason "..."]` | Reopen a resolved/ignored finding back to active |',
2427
+ '| `/review finding <id> triage [--reason "..."]` | Acknowledge a finding (active \u2192 triaged) |',
2428
+ '| `/review finding <id> start [--reason "..."]` | Start work on a finding (triaged \u2192 in_progress) |',
2429
+ "| `/review reports` | List recent review reports |",
2430
+ "| `/review reports --status <s>` | Filter reports by lifecycle (open, actioned, completed, skipped) |",
2431
+ "| `/review report <id>` | Show a report with raw text, counts, and lifecycle events |",
2432
+ '| `/review report <id> complete [--reason "..."]` | Mark a report completed |',
2433
+ '| `/review report <id> skip [--reason "..."]` | Skip a report (wontfix) |',
2434
+ '| `/review report <id> action [--reason "..."]` | Mark a report as being actioned |',
2435
+ "| `/review report <id> note <message>` | Add an annotation without changing lifecycle |",
2436
+ "| `/review status` | Show store statistics |",
2437
+ "",
2438
+ "Findings: `~/.wrongstack/projects/<slug>/review-findings.jsonl`",
2439
+ "Reports: `~/.wrongstack/projects/<slug>/review-reports.jsonl`"
2440
+ ].join("\n");
2441
+ }
2442
+ async function loadStore(ctx) {
2443
+ const { JsonlFindingStore } = await import("./plugins/review-finding-store.js");
2444
+ const store = new JsonlFindingStore(ctx.projectDir);
2445
+ return store;
2446
+ }
2447
+ async function listFindings(args, ctx) {
2448
+ let severityFilter;
2449
+ let statusFilter;
2450
+ let limit = 20;
2451
+ for (let i = 0; i < args.length; i++) {
2452
+ if (args[i] === "--severity" && i + 1 < args.length) {
2453
+ severityFilter = args[++i];
2454
+ } else if (args[i] === "--status" && i + 1 < args.length) {
2455
+ statusFilter = args[++i];
2456
+ } else if (args[i] === "--limit" && i + 1 < args.length) {
2457
+ limit = Number.parseInt(args[++i], 10) || 20;
2458
+ }
2459
+ }
2460
+ const store = await loadStore(ctx);
2461
+ const all = await store.list({
2462
+ ...severityFilter ? { severities: [severityFilter] } : {},
2463
+ ...statusFilter ? { statuses: [statusFilter] } : {},
2464
+ limit
2465
+ });
2466
+ if (all.length === 0) {
2467
+ const filters = [severityFilter && `severity=${severityFilter}`, statusFilter && `status=${statusFilter}`].filter(Boolean).join(", ");
2468
+ return `No findings found${filters ? ` (${filters})` : ""}.`;
2469
+ }
2470
+ const lines = [
2471
+ "| Severity | Status | File | Title |",
2472
+ "|---|---|---|---|"
2473
+ ];
2474
+ for (const f of all) {
2475
+ const location = f.location?.file ?? "unknown";
2476
+ const file = location.substring(location.lastIndexOf("/") + 1).substring(location.lastIndexOf("\\") + 1) || location;
2477
+ const severityEmoji = f.severity === "critical" ? "\u{1F534}" : f.severity === "high" ? "\u{1F7E0}" : f.severity === "medium" ? "\u{1F7E1}" : "\u26AA";
2478
+ const title = f.title.length > 60 ? f.title.substring(0, 57) + "..." : f.title;
2479
+ lines.push(`| ${severityEmoji} ${f.severity} | ${f.status} | \`${file}\` | ${title} |`);
2480
+ }
2481
+ lines.push("", `_${all.length} finding(s) shown. Use \`/review finding <id>\` for details._`);
2482
+ return lines.join("\n");
2483
+ }
2484
+ async function showFinding(id, ctx) {
2485
+ const store = await loadStore(ctx);
2486
+ const finding = await store.get(id);
2487
+ if (!finding) return `Finding not found: ${id}`;
2488
+ const lines = [
2489
+ `## ${finding.severity.toUpperCase()}: ${finding.title}`,
2490
+ "",
2491
+ `**ID:** \`${finding.id}\``,
2492
+ `**File:** \`${finding.location?.file ?? "unknown"}\`${finding.location?.line ? `:${finding.location.line}` : ""}`,
2493
+ `**Severity:** ${finding.severity}`,
2494
+ `**Status:** ${finding.status}`,
2495
+ `**Source:** ${finding.source}`,
2496
+ `**Created:** ${finding.createdAt}`,
2497
+ "",
2498
+ finding.description
2499
+ ];
2500
+ if (finding.suggestedFix) {
2501
+ lines.push("", "**Suggested fix:**", finding.suggestedFix);
2502
+ }
2503
+ const events = await store.getEvents(id);
2504
+ if (events.length > 0) {
2505
+ lines.push("", "**Lifecycle:**", "");
2506
+ for (const ev of events) {
2507
+ const eventLine = `- \`${ev.timestamp}\` ${ev.fromStatus ?? "none"} \u2192 ${ev.toStatus} (${ev.eventType}) by ${ev.actorId}`;
2508
+ lines.push(eventLine);
2509
+ if (ev.reason) lines.push(` _${ev.reason}_`);
2510
+ }
2511
+ }
2512
+ lines.push("", `_Fingerprint: \`${finding.fingerprint.substring(0, 16)}\u2026\`_`);
2513
+ return lines.join("\n");
2514
+ }
2515
+ var VALID_RESOLUTION_OUTCOMES = ["fixed", "wontfix", "duplicate", "false_positive", "stale"];
2516
+ async function transitionFinding(id, action, extraArgs, ctx) {
2517
+ let reason;
2518
+ let outcome;
2519
+ for (let i = 0; i < extraArgs.length; i++) {
2520
+ if (extraArgs[i] === "--reason" && i + 1 < extraArgs.length) {
2521
+ reason = extraArgs[++i];
2522
+ } else if (extraArgs[i] === "--outcome" && i + 1 < extraArgs.length) {
2523
+ outcome = extraArgs[++i];
2524
+ }
2525
+ }
2526
+ const store = await loadStore(ctx);
2527
+ const finding = await store.get(id);
2528
+ if (!finding) return `Finding not found: ${id}`;
2529
+ const cleanReason = reason && reason.trim().length > 0 ? reason : void 0;
2530
+ const cleanOutcome = outcome && outcome.trim().length > 0 ? outcome : void 0;
2531
+ if (action === "reopen") {
2532
+ if (cleanOutcome) {
2533
+ return `\u26A0\uFE0F \`--outcome\` only applies to \`resolve\`. Usage: \`/review finding ${id} reopen --reason "..."\``;
2534
+ }
2535
+ try {
2536
+ await store.transition(finding.id, "active", { id: "operator", kind: "operator" }, {
2537
+ ...cleanReason !== void 0 ? { reason: cleanReason } : {}
2538
+ });
2539
+ } catch (err) {
2540
+ const msg = err instanceof Error ? err.message : String(err);
2541
+ return `Could not reopen finding ${id}: ${msg}`;
2542
+ }
2543
+ const lines2 = [
2544
+ `\u2705 Finding \`${id}\` reopened as **active**.`
2545
+ ];
2546
+ const reportId2 = finding.originReport?.reportId;
2547
+ if (reportId2) {
2548
+ try {
2549
+ const reopenResult = await syncReportReopen(reportId2, ctx.projectDir, {
2550
+ id: "operator",
2551
+ kind: "operator"
2552
+ }, cleanReason);
2553
+ if (reopenResult.reopened) {
2554
+ lines2.push("", `\u{1F535} Report \`${reportId2}\` auto-reopened \u2014 was ${reopenResult.previousLifecycle}.`);
2555
+ }
2556
+ } catch (err) {
2557
+ const msg = err instanceof Error ? err.message : String(err);
2558
+ console.warn(JSON.stringify({ level: "warn", event: "review_auto_reopen_failed", reportId: reportId2, message: msg }));
2559
+ }
2560
+ }
2561
+ return lines2.join("\n");
2562
+ }
2563
+ if (action === "triage") {
2564
+ if (cleanOutcome) {
2565
+ return `\u26A0\uFE0F \`--outcome\` only applies to \`resolve\`. Usage: \`/review finding ${id} triage --reason "..."\``;
2566
+ }
2567
+ try {
2568
+ await store.transition(finding.id, "triaged", { id: "operator", kind: "operator" }, {
2569
+ ...cleanReason !== void 0 ? { reason: cleanReason } : {}
2570
+ });
2571
+ } catch (err) {
2572
+ const msg = err instanceof Error ? err.message : String(err);
2573
+ return `Could not triage finding ${id}: ${msg}`;
2574
+ }
2575
+ return `\u2705 Finding \`${id}\` marked as **triaged**.`;
2576
+ }
2577
+ if (action === "start") {
2578
+ if (cleanOutcome) {
2579
+ return `\u26A0\uFE0F \`--outcome\` only applies to \`resolve\`. Usage: \`/review finding ${id} start --reason "..."\``;
2580
+ }
2581
+ try {
2582
+ await store.transition(finding.id, "in_progress", { id: "operator", kind: "operator" }, {
2583
+ ...cleanReason !== void 0 ? { reason: cleanReason } : {}
2584
+ });
2585
+ } catch (err) {
2586
+ const msg = err instanceof Error ? err.message : String(err);
2587
+ return `Could not start finding ${id}: ${msg}`;
2588
+ }
2589
+ return `\u2705 Finding \`${id}\` marked as **in_progress**.`;
2590
+ }
2591
+ const targetStatus = action === "resolve" ? "resolved" : "ignored";
2592
+ if (action === "ignore" && cleanOutcome) {
2593
+ return `\u26A0\uFE0F \`--outcome\` only applies to \`resolve\`. Usage: \`/review finding ${id} ignore --reason "..."\``;
2594
+ }
2595
+ if (action === "resolve" && cleanOutcome && !VALID_RESOLUTION_OUTCOMES.includes(cleanOutcome)) {
2596
+ return `Invalid outcome "${cleanOutcome}". Valid values: ${VALID_RESOLUTION_OUTCOMES.join(", ")}`;
2597
+ }
2598
+ const resolvedOutcome = action === "resolve" ? cleanOutcome ?? "fixed" : void 0;
2599
+ try {
2600
+ await store.transition(finding.id, targetStatus, { id: "operator", kind: "operator" }, {
2601
+ ...cleanReason !== void 0 ? { reason: cleanReason } : {},
2602
+ ...resolvedOutcome ? { outcome: resolvedOutcome } : {}
2603
+ });
2604
+ } catch (err) {
2605
+ const msg = err instanceof Error ? err.message : String(err);
2606
+ return `Could not transition finding ${id} to ${targetStatus}: ${msg}`;
2607
+ }
2608
+ const lines = [
2609
+ `\u2705 Finding \`${id}\` marked as **${targetStatus}**.`
2610
+ ];
2611
+ const reportId = finding.originReport?.reportId;
2612
+ if (reportId) {
2613
+ try {
2614
+ const syncResult = await syncReportCompletion(reportId, ctx.projectDir, {
2615
+ id: "operator",
2616
+ kind: "operator"
2617
+ });
2618
+ if (syncResult.completed) {
2619
+ lines.push("", `\u{1F7E2} Report \`${reportId}\` auto-completed \u2014 all ${syncResult.totalFindings} finding(s) resolved or ignored.`);
2620
+ } else if (syncResult.totalFindings > 0 && syncResult.remaining > 0) {
2621
+ lines.push(`_Report \`${reportId}\`: ${syncResult.remaining} of ${syncResult.totalFindings} finding(s) still open._`);
2622
+ }
2623
+ } catch (err) {
2624
+ const msg = err instanceof Error ? err.message : String(err);
2625
+ console.warn(JSON.stringify({ level: "warn", event: "review_auto_completion_failed", reportId, message: msg }));
2626
+ }
2627
+ }
2628
+ return lines.join("\n");
2629
+ }
2630
+ async function loadReportStore(ctx) {
2631
+ const { JsonlReportStore } = await import("./plugins/review-report-store.js");
2632
+ return new JsonlReportStore(ctx.projectDir);
2633
+ }
2634
+ var LIFECYCLE_EMOJI = {
2635
+ open: "\u{1F535}",
2636
+ actioned: "\u{1F7E1}",
2637
+ completed: "\u{1F7E2}",
2638
+ skipped: "\u26AA"
2639
+ };
2640
+ async function listReports(args, ctx) {
2641
+ let statusFilter;
2642
+ let limit = 15;
2643
+ for (let i = 0; i < args.length; i++) {
2644
+ if (args[i] === "--status" && i + 1 < args.length) {
2645
+ statusFilter = args[++i];
2646
+ } else if (args[i] === "--limit" && i + 1 < args.length) {
2647
+ limit = Number.parseInt(args[++i], 10) || 15;
2648
+ }
2649
+ }
2650
+ const store = await loadReportStore(ctx);
2651
+ const reports = await store.list({
2652
+ ...statusFilter ? { statuses: [statusFilter] } : {},
2653
+ limit
2654
+ });
2655
+ if (reports.length === 0) {
2656
+ return `No reports found${statusFilter ? ` (status=${statusFilter})` : ""}.`;
2657
+ }
2658
+ const lines = [
2659
+ "| Lifecycle | Source | Findings | Reviewed | Report |",
2660
+ "|---|---|---|---|---|"
2661
+ ];
2662
+ for (const r of reports) {
2663
+ const emoji = LIFECYCLE_EMOJI[r.lifecycle] ?? "\u26AA";
2664
+ const total = r.counts.critical + r.counts.high + r.counts.medium + r.counts.low;
2665
+ const sevSummary = total === 0 ? "clean" : [
2666
+ r.counts.critical > 0 && `${r.counts.critical}\u{1F534}`,
2667
+ r.counts.high > 0 && `${r.counts.high}\u{1F7E0}`,
2668
+ r.counts.medium > 0 && `${r.counts.medium}\u{1F7E1}`,
2669
+ r.counts.low > 0 && `${r.counts.low}\u26AA`
2670
+ ].filter(Boolean).join(" ");
2671
+ const date = r.reviewedAt.substring(0, 10);
2672
+ const id = r.id.substring(0, 8);
2673
+ lines.push(`| ${emoji} ${r.lifecycle} | ${r.source} | ${sevSummary} | ${date} | \`${id}\` |`);
2674
+ }
2675
+ lines.push("", `_${reports.length} report(s). Use \`/review report <id>\` for details._`);
2676
+ return lines.join("\n");
2677
+ }
2678
+ async function resolveReportId(store, id) {
2679
+ const report = await store.get(id);
2680
+ if (report) return report;
2681
+ if (id.length >= 8) {
2682
+ const all = await store.list({ limit: 9999 });
2683
+ const match = all.find((r) => r.id.startsWith(id));
2684
+ if (match) return match;
2685
+ }
2686
+ return null;
2687
+ }
2688
+ async function showReport(id, ctx) {
2689
+ const store = await loadReportStore(ctx);
2690
+ const report = await resolveReportId(store, id);
2691
+ if (!report) return `Report not found: ${id}`;
2692
+ const emoji = LIFECYCLE_EMOJI[report.lifecycle] ?? "\u26AA";
2693
+ const lines = [
2694
+ `## ${emoji} Review Report \u2014 ${report.lifecycle}`,
2695
+ "",
2696
+ `**ID:** \`${report.id}\``,
2697
+ `**Reviewed:** ${report.reviewedAt}`,
2698
+ `**Source:** ${report.source}`,
2699
+ `**Reviewer model:** ${report.reviewerModel}`,
2700
+ `**Review status:** ${report.reviewStatus}`,
2701
+ ...report.cascadeDepth !== void 0 ? [`**Cascade depth:** ${report.cascadeDepth}`] : [],
2702
+ ...report.durationSeconds !== void 0 ? [`**Duration:** ${report.durationSeconds}s`] : [],
2703
+ ...report.evidenceStatus !== void 0 ? [
2704
+ `**Evidence:** ${report.evidenceStatus === "verified" ? "\u2705 verified" : report.evidenceStatus === "failed" ? "\u274C failed" : "\u26A0\uFE0F missing"}`,
2705
+ ...report.evidenceChecks && report.evidenceChecks.length > 0 ? [
2706
+ "",
2707
+ ...report.evidenceChecks.map((check) => {
2708
+ const mark = check.ok ? "\u2713" : "\u2717";
2709
+ const claimed = check.claimedExitCode ?? "\u2014";
2710
+ const actual = check.actualExitCode ?? "\u2014";
2711
+ return ` ${mark} \`${check.name}\` \u2014 \`${check.command}\` (claimed ${claimed}, observed ${actual})`;
2712
+ })
2713
+ ] : []
2714
+ ] : [],
2715
+ "",
2716
+ "**Severity counts:**",
2717
+ ` \u{1F534} Critical: ${report.counts.critical}`,
2718
+ ` \u{1F7E0} High: ${report.counts.high}`,
2719
+ ` \u{1F7E1} Medium: ${report.counts.medium}`,
2720
+ ` \u26AA Low: ${report.counts.low}`,
2721
+ ` **Total findings:** ${report.totalFindings}`,
2722
+ ...report.unparseableCount > 0 ? [` _${report.unparseableCount} unparseable_`] : []
2723
+ ];
2724
+ if (report.files.length > 0) {
2725
+ lines.push("", "**Files reviewed:**");
2726
+ for (const f of report.files.slice(0, 20)) {
2727
+ lines.push(` - \`${f.path}\` (${f.status})`);
2728
+ }
2729
+ if (report.files.length > 20) {
2730
+ lines.push(` _...and ${report.files.length - 20} more_`);
2731
+ }
2732
+ }
2733
+ const events = await store.getEvents(report.id);
2734
+ if (events.length > 0) {
2735
+ lines.push("", "**Lifecycle events:**", "");
2736
+ for (const ev of events) {
2737
+ const transition = ev.fromLifecycle ? `${ev.fromLifecycle} \u2192 ${ev.toLifecycle}` : `\u2192 ${ev.toLifecycle}`;
2738
+ lines.push(`- \`${ev.timestamp}\` ${transition} (${ev.eventType}) by ${ev.actorId}`);
2739
+ if (ev.reason) lines.push(` _${ev.reason}_`);
2740
+ }
2741
+ }
2742
+ if (report.rawText?.trim()) {
2743
+ lines.push("", "<details><summary>Raw report text</summary>", "", report.rawText, "", "</details>");
2744
+ }
2745
+ return lines.join("\n");
2746
+ }
2747
+ async function transitionReport(id, action, extraArgs, ctx) {
2748
+ let reason;
2749
+ for (let i = 0; i < extraArgs.length; i++) {
2750
+ if (extraArgs[i] === "--reason" && i + 1 < extraArgs.length) {
2751
+ reason = extraArgs[++i];
2752
+ }
2753
+ }
2754
+ const cleanReason = reason && reason.trim().length > 0 ? reason : void 0;
2755
+ const targetMap = { complete: "completed", skip: "skipped", action: "actioned" };
2756
+ const targetStatus = targetMap[action];
2757
+ const store = await loadReportStore(ctx);
2758
+ const report = await resolveReportId(store, id);
2759
+ if (!report) return `Report not found: ${id}`;
2760
+ try {
2761
+ await store.transition(report.id, targetStatus, { id: "operator", kind: "operator" }, {
2762
+ ...cleanReason !== void 0 ? { reason: cleanReason } : {}
2763
+ });
2764
+ } catch (err) {
2765
+ const msg = err instanceof Error ? err.message : String(err);
2766
+ return `Could not transition report \`${report.id}\` to ${targetStatus}: ${msg}`;
2767
+ }
2768
+ const emoji = LIFECYCLE_EMOJI[targetStatus] ?? "\u26AA";
2769
+ return `${emoji} Report \`${report.id}\` marked as **${targetStatus}**.`;
2770
+ }
2771
+ async function addReportNote(id, extraArgs, ctx) {
2772
+ const note = extraArgs.join(" ").trim();
2773
+ if (!note) {
2774
+ return "Usage: /review report <id> note <message>";
2775
+ }
2776
+ const store = await loadReportStore(ctx);
2777
+ const report = await resolveReportId(store, id);
2778
+ if (!report) return `Report not found: ${id}`;
2779
+ try {
2780
+ await store.addNote(report.id, { id: "operator", kind: "operator" }, note);
2781
+ } catch (err) {
2782
+ const msg = err instanceof Error ? err.message : String(err);
2783
+ return `Could not add note to report ${id}: ${msg}`;
2784
+ }
2785
+ return `\u{1F4DD} Note added to report \`${report.id}\`.`;
2786
+ }
2787
+ async function showStatus(ctx) {
2788
+ const store = await loadStore(ctx);
2789
+ const all = await store.list({ limit: 9999 });
2790
+ const counts = {};
2791
+ const severityCounts = {};
2792
+ for (const f of all) {
2793
+ counts[f.status] = (counts[f.status] || 0) + 1;
2794
+ severityCounts[f.severity] = (severityCounts[f.severity] || 0) + 1;
2795
+ }
2796
+ return [
2797
+ "## Finding Store Status",
2798
+ "",
2799
+ `**Total findings:** ${all.length}`,
2800
+ "",
2801
+ "**By status:**",
2802
+ ...Object.entries(counts).map(([k, v]) => ` - ${k}: ${v}`),
2803
+ "",
2804
+ "**By severity:**",
2805
+ ...Object.entries(severityCounts).map(([k, v]) => ` - ${k}: ${v}`),
2806
+ "",
2807
+ `_Page size: ${FINDING_DEFAULT_PAGE_SIZE}_`
2808
+ ].join("\n");
2809
+ }
2810
+
2811
+ // src/plugins/chimera-plugin.ts
2812
+ import { spawn as spawn3 } from "node:child_process";
2813
+ import * as fsp4 from "node:fs/promises";
2814
+ import * as os from "node:os";
2815
+ import * as path4 from "node:path";
2816
+ var DEFAULT_MAX_FILES = 15;
2817
+ var DEFAULT_CASCADE_ON = "off";
2818
+ var DEFAULT_MAX_CASCADE_DEPTH2 = 2;
2819
+ function resolveChimeraConfig(cfg, sessionProvider, sessionModel) {
2820
+ return {
2821
+ enabled: cfg.enabled !== false,
2822
+ provider: cfg.provider ?? sessionProvider,
2823
+ model: cfg.model ?? sessionModel,
2824
+ maxFiles: cfg.maxFiles ?? DEFAULT_MAX_FILES,
2825
+ autoFix: cfg.autoFix ?? "off",
2826
+ cascadeOn: cfg.cascadeOn ?? DEFAULT_CASCADE_ON,
2827
+ maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH2,
2828
+ fallbackModels: cfg.fallbackModels ? [...cfg.fallbackModels] : [],
2829
+ fallbackProfile: cfg.fallbackProfile
2830
+ };
2831
+ }
2832
+ var CHIMERA_REVIEW_PROMPT = readBundledInstructionText("llm/chimera-review.md");
2833
+ async function runGit3(args, cwd) {
2834
+ return new Promise((resolve3, reject) => {
2835
+ let child;
2836
+ try {
2837
+ child = spawn3("git", args, {
2838
+ cwd,
2839
+ stdio: ["ignore", "pipe", "pipe"],
2840
+ signal: AbortSignal.timeout(15e3),
2841
+ windowsHide: true
2842
+ });
2843
+ } catch (err) {
2844
+ reject(err);
2845
+ return;
2846
+ }
2847
+ let stdout = "";
2848
+ let stderr = "";
2849
+ child.stdout?.on("data", (d) => {
2850
+ stdout += d;
2851
+ });
2852
+ child.stderr?.on("data", (d) => {
2853
+ stderr += d;
2854
+ });
2855
+ child.on("error", () => resolve3({ stdout, stderr, code: 1 }));
2856
+ child.on("close", (code) => resolve3({ stdout, stderr, code: code ?? 0 }));
2857
+ });
2858
+ }
2859
+ async function isGitRepo2(cwd) {
2860
+ const r = await runGit3(["rev-parse", "--git-dir"], cwd);
2861
+ return r.code === 0;
2862
+ }
2863
+ async function getChangedFiles2(cwd) {
2864
+ const r = await runGit3(["status", "--porcelain"], cwd);
2865
+ if (r.code !== 0) return [];
2866
+ const files = [];
2867
+ for (const line of r.stdout.split("\n")) {
2868
+ if (!line.trim()) continue;
2869
+ const statusCode = line.slice(0, 2).trim();
2870
+ const filePath = line.slice(3).trim();
2871
+ if (statusCode === "A" || statusCode === "A " || statusCode === " A" || statusCode === "??") {
2872
+ files.push({ path: filePath, status: "added" });
2873
+ } else if (statusCode.includes("M")) {
2874
+ files.push({ path: filePath, status: "modified" });
2875
+ }
2876
+ }
2877
+ return files;
2878
+ }
2879
+ function buildChimeraCommand(getConfig, events) {
2880
+ return {
2881
+ name: "chimera",
2882
+ category: "Session",
2883
+ description: "Show Chimera post-session review agent status and configuration.",
2884
+ help: [
2885
+ "\u2554\u2550\u2550\u2550 Chimera \u2550\u2550\u2550\u2557",
2886
+ "",
2887
+ "Post-session code quality guardian. Reviews files changed during",
2888
+ "each session using a dedicated read-only subagent.",
2889
+ "",
2890
+ "Commands:",
2891
+ " /chimera Show current status",
2892
+ " /review Manually review changed files now",
2893
+ "",
2894
+ "Configuration (edit config.json):",
2895
+ " extensions.wstack-chimera.enabled true | false",
2896
+ " extensions.wstack-chimera.provider provider id",
2897
+ " extensions.wstack-chimera.model model id",
2898
+ " extensions.wstack-chimera.maxFiles max files (default 15)",
2899
+ " extensions.wstack-chimera.autoFix off | ask | auto (default off)",
2900
+ " extensions.wstack-chimera.cascadeOn off | critical | high (default off)",
2901
+ " extensions.wstack-chimera.maxCascadeDepth max fix+re-review cycles (default 2)",
2902
+ "",
2903
+ "Output cap: the subagent runs up to the provider's model-native",
2904
+ "output ceiling (Anthropic 64K, OpenAI 16K, Gemini 8K). No manual cap."
2905
+ ].join("\n"),
2906
+ async run(args) {
2907
+ const cfg = getConfig();
2908
+ const trimmed = (args ?? "").trim();
2909
+ if (trimmed.startsWith("autoFix") || trimmed.startsWith("autofix")) {
2910
+ const mode = trimmed.split(/\s+/)[1]?.toLowerCase();
2911
+ if (!mode || !["off", "ask", "auto"].includes(mode)) {
2912
+ return {
2913
+ message: [
2914
+ `Usage: /chimera autoFix <off|ask|auto>`,
2915
+ `Current: ${cfg.autoFix}`,
2916
+ "",
2917
+ " off \u2014 send review result to mailbox; leader waits for your command.",
2918
+ " ask \u2014 send review as an ask; leader prompts you for permission.",
2919
+ " auto \u2014 send review result AND spawn a fix subagent automatically."
2920
+ ].join("\n")
2921
+ };
2922
+ }
2923
+ events.emit(
2924
+ "chimera.set_autofix",
2925
+ { mode }
2926
+ );
2927
+ return {
2928
+ message: `\u2705 Chimera autoFix set to "${mode}" for this session (config file unchanged).`
2929
+ };
2930
+ }
2931
+ return {
2932
+ message: [
2933
+ `\u{1F982} Chimera \u2014 ${cfg.enabled ? "enabled" : "disabled"}`,
2934
+ "",
2935
+ ` Provider: ${cfg.provider}`,
2936
+ ` Model: ${cfg.model}`,
2937
+ ` Max files: ${cfg.maxFiles}`,
2938
+ ` Auto-fix: ${cfg.autoFix}`,
2939
+ ` Cascade: ${cfg.cascadeOn === "off" ? "disabled" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`}`,
2940
+ ` Max depth: ${cfg.maxCascadeDepth} re-review cycle(s)`,
2941
+ ` Max output: model-native (no cap)`,
2942
+ "",
2943
+ cfg.enabled ? "Auto-review runs after each session. /review triggers manually." : "Set extensions.wstack-chimera.enabled = true to enable."
2944
+ ].join("\n")
2945
+ };
2946
+ }
2947
+ };
2948
+ }
2949
+ function buildReviewCommand(_getConfig) {
2950
+ return {
2951
+ name: "review",
2952
+ category: "Session",
2953
+ description: "Query the Chimera Finding Store.",
2954
+ help: [
2955
+ "Usage: /review findings [--severity <s>] [--status <s>] [--limit <n>]",
2956
+ " /review finding <id>",
2957
+ " /review status",
2958
+ "",
2959
+ "The finding store persists structured findings from every Chimera review.",
2960
+ "Use this to track what issues were found, their lifecycle, and outcomes."
2961
+ ].join("\n"),
2962
+ async run(args) {
2963
+ const cwd = process.cwd();
2964
+ const slug = projectSlug(cwd);
2965
+ const globalRoot = path4.join(os.homedir(), ".wrongstack");
2966
+ const projectDir = path4.join(globalRoot, "projects", slug);
2967
+ const trimmed = (args ?? "").trim();
2968
+ const parts = trimmed.split(/\s+/).filter(Boolean);
2969
+ return {
2970
+ message: await executeFindingCommand(parts, { projectDir })
2971
+ };
2972
+ }
2973
+ };
2974
+ }
2975
+ function createChimeraPlugin() {
2976
+ return {
2977
+ name: "wstack-chimera",
2978
+ version: "1.0.0",
2979
+ description: "Post-session code quality guardian with subagent review.",
2980
+ apiVersion: "^0.1",
2981
+ capabilities: { slashCommands: true },
2982
+ defaultConfig: {},
2983
+ setup(api) {
2984
+ const recompute = () => {
2985
+ const raw = api.config.extensions?.["wstack-chimera"] ?? {};
2986
+ return resolveChimeraConfig(raw, api.config.provider, api.config.model);
2987
+ };
2988
+ let resolved = recompute();
2989
+ api.onConfigChange(() => {
2990
+ const old = resolved;
2991
+ resolved = recompute();
2992
+ if (old.enabled !== resolved.enabled || old.provider !== resolved.provider || old.model !== resolved.model) {
2993
+ api.log.info(
2994
+ `[chimera] config changed \u2014 enabled=${resolved.enabled} provider=${resolved.provider} model=${resolved.model}`
2995
+ );
2996
+ }
2997
+ });
2998
+ api.onPattern("chimera.review_needed", (_event, payload) => {
2999
+ void recordStartedReview(api.events, payload).catch(() => void 0);
3000
+ });
3001
+ api.onPattern("chimera.review_complete", (_event, payload) => {
3002
+ void recordCompletedReview(api.events, payload).catch(() => void 0);
3003
+ });
3004
+ if (!resolved.enabled) {
3005
+ api.log.info("[chimera] disabled by config");
3006
+ return;
3007
+ }
3008
+ api.log.info(
3009
+ `[chimera] loaded \u2014 provider=${resolved.provider} model=${resolved.model} maxFiles=${resolved.maxFiles}`
3010
+ );
3011
+ api.slashCommands.register(buildChimeraCommand(() => resolved, api.events));
3012
+ api.slashCommands.register(buildReviewCommand(() => resolved));
3013
+ api.onEvent("session.ended", (event) => {
3014
+ const work = (async () => {
3015
+ try {
3016
+ const cfg = resolved;
3017
+ if (!cfg.enabled) return;
3018
+ const cwd = api.config.cwd ?? process.cwd();
3019
+ if (!await isGitRepo2(cwd)) {
3020
+ api.log.info("[chimera] skipped \u2014 not a git repo");
3021
+ return;
3022
+ }
3023
+ const allChanged = await getChangedFiles2(cwd);
3024
+ const existing = [];
3025
+ for (const f of allChanged) {
3026
+ if (f.path.startsWith(".wrongstack/")) continue;
3027
+ try {
3028
+ await fsp4.access(path4.join(cwd, f.path));
3029
+ existing.push(f);
3030
+ } catch {
3031
+ }
3032
+ }
3033
+ if (existing.length === 0) {
3034
+ api.log.info("[chimera] no changed files to review");
3035
+ return;
3036
+ }
3037
+ const toReview = existing.slice(0, cfg.maxFiles);
3038
+ if (existing.length > cfg.maxFiles) {
3039
+ api.log.info(
3040
+ `[chimera] capping review at ${cfg.maxFiles} of ${existing.length} files`
3041
+ );
3042
+ }
3043
+ const filesWithContent = [];
3044
+ for (const f of toReview) {
3045
+ try {
3046
+ const absPath = path4.join(cwd, f.path);
3047
+ const content = await fsp4.readFile(absPath, "utf8");
3048
+ filesWithContent.push({ path: f.path, status: f.status, content });
3049
+ } catch {
3050
+ }
3051
+ }
3052
+ if (filesWithContent.length === 0) {
3053
+ api.log.info("[chimera] could not read changed files");
3054
+ return;
3055
+ }
3056
+ const bundle = await buildReviewContext({
3057
+ cwd,
3058
+ config: cfg,
3059
+ files: filesWithContent,
3060
+ cascadeOn: cfg.cascadeOn,
3061
+ maxCascadeDepth: cfg.maxCascadeDepth
3062
+ });
3063
+ const emittedBundle = await emitReviewIfChanged(api, bundle);
3064
+ if (!emittedBundle) {
3065
+ api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
3066
+ return;
3067
+ }
3068
+ api.log.info(
3069
+ `[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
3070
+ );
3071
+ } catch (err) {
3072
+ api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
3073
+ }
3074
+ })();
3075
+ event?.waitUntil?.(work);
3076
+ return work;
3077
+ });
3078
+ },
3079
+ teardown(api) {
3080
+ api.slashCommands.unregister("chimera");
3081
+ api.slashCommands.unregister("review");
3082
+ api.log.info("[chimera] unloaded");
3083
+ },
3084
+ async health() {
3085
+ return { ok: true, message: "chimera ready" };
3086
+ }
3087
+ };
3088
+ }
3089
+
3090
+ // src/plugins/cloud-config-sync-plugin.ts
3091
+ import * as path5 from "node:path";
3092
+ var DEFAULT_INTERVAL_SECONDS = 300;
3093
+ var MIN_INTERVAL_SECONDS = 60;
3094
+ function createCloudConfigSyncPlugin(opts) {
3095
+ let engine = null;
3096
+ let timer = null;
3097
+ let passRunning = false;
3098
+ return {
3099
+ name: "wstack-cloud-config-sync",
3100
+ version: "1.0.0",
3101
+ description: "my.wrongstack.com config synchronization (namespaced sync API v1)",
3102
+ apiVersion: "^0.1",
3103
+ capabilities: { slashCommands: true },
3104
+ defaultConfig: {},
3105
+ setup(api) {
3106
+ const rawConfig = api.config;
3107
+ const paths = opts?.paths ?? rawConfig.paths;
3108
+ const configStore = opts?.configStore ?? rawConfig.configStore;
3109
+ const vault = opts?.vault ?? rawConfig.vault;
3110
+ const appVersion = opts?.appVersion ?? rawConfig.appVersion;
3111
+ if (!paths?.configDir || !configStore || !vault) {
3112
+ api.log.warn(
3113
+ "[cloud-config-sync] paths, configStore, or vault not available \u2014 /cloudsync disabled"
3114
+ );
3115
+ return;
3116
+ }
3117
+ const profileConfigPath = path5.join(paths.configDir, "config.json");
3118
+ const statePath = path5.join(paths.configDir, "cloud-sync-state.json");
3119
+ const warn = (msg) => api.log.warn(msg);
3120
+ const readLocalConfig = async () => {
3121
+ const raw = await readJsonObjectFile(profileConfigPath).catch(() => ({}));
3122
+ return decryptConfigSecrets(raw, vault, { warn });
3123
+ };
3124
+ const writeLocalConfig = async (mutator) => {
3125
+ const current = await readLocalConfig();
3126
+ const next = mutator(current);
3127
+ const encrypted = encryptConfigSecrets(next, vault);
3128
+ await atomicWrite(profileConfigPath, `${JSON.stringify(encrypted, null, 2)}
3129
+ `, {
3130
+ mode: 384
3131
+ });
3132
+ configStore.update({ cloudSync: next.cloudSync });
3133
+ };
3134
+ const getSettings = () => {
3135
+ const cfg = configStore.get().cloudSync;
3136
+ if (!cfg?.enabled || !cfg.url || !cfg.token) return null;
3137
+ const token = cfg.token.startsWith("enc:") && vault.decrypt ? vault.decrypt(cfg.token) : cfg.token;
3138
+ return { enabled: true, url: cfg.url, token };
3139
+ };
3140
+ engine = new CloudConfigSync({
3141
+ statePath,
3142
+ readLocalConfig,
3143
+ writeLocalConfig,
3144
+ getSettings,
3145
+ appVersion,
3146
+ logger: { info: (m) => api.log.info(m), warn }
3147
+ });
3148
+ const runPass = async () => {
3149
+ if (!engine) return "Cloud config sync is not initialized.";
3150
+ if (passRunning) return "A sync pass is already running.";
3151
+ passRunning = true;
3152
+ try {
3153
+ const result = await engine.syncOnce();
3154
+ return result.message;
3155
+ } finally {
3156
+ passRunning = false;
3157
+ }
3158
+ };
3159
+ const scheduleLoop = () => {
3160
+ if (timer) {
3161
+ clearInterval(timer);
3162
+ timer = null;
3163
+ }
3164
+ const cfg = configStore.get().cloudSync;
3165
+ if (!cfg?.enabled) return;
3166
+ const seconds = Math.max(
3167
+ MIN_INTERVAL_SECONDS,
3168
+ cfg.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS
3169
+ );
3170
+ timer = setInterval(() => {
3171
+ void runPass().catch((err) => warn(`[cloud-config-sync] ${toErrorMessage(err)}`));
3172
+ }, seconds * 1e3);
3173
+ timer.unref?.();
3174
+ };
3175
+ const setCloudSyncConfig = async (patch) => {
3176
+ await writeLocalConfig((config) => ({
3177
+ ...config,
3178
+ cloudSync: { ...config.cloudSync, ...patch }
3179
+ }));
3180
+ scheduleLoop();
3181
+ };
3182
+ api.slashCommands.register(
3183
+ buildCloudSyncCommand(engine, configStore, setCloudSyncConfig, runPass)
3184
+ );
3185
+ scheduleLoop();
3186
+ api.log.info("[cloud-config-sync] loaded \u2014 /cloudsync available.");
3187
+ },
3188
+ teardown(api) {
3189
+ if (timer) {
3190
+ clearInterval(timer);
3191
+ timer = null;
3192
+ }
3193
+ api.slashCommands.unregister("cloudsync");
3194
+ api.log.info("[cloud-config-sync] unloaded");
3195
+ },
3196
+ async health() {
3197
+ return { ok: true, message: "CloudConfigSync ready" };
3198
+ }
3199
+ };
3200
+ }
3201
+ function buildCloudSyncCommand(engine, configStore, setCloudSyncConfig, runPass) {
3202
+ return {
3203
+ name: "cloudsync",
3204
+ description: "Portal config sync: /cloudsync [status|on|off|now|set url|set token]",
3205
+ async run(args, _ctx) {
3206
+ const [verb, ...rest] = args.trim().split(/\s+/);
3207
+ const restJoined = rest.join(" ").trim();
3208
+ switch (verb) {
3209
+ case "":
3210
+ case "status":
3211
+ return { message: await engine.statusText() };
3212
+ case "on": {
3213
+ const cfg = configStore.get().cloudSync;
3214
+ if (!cfg?.url || !cfg.token) {
3215
+ return {
3216
+ message: [
3217
+ "Set the portal first:",
3218
+ " /cloudsync set url https://my.wrongstack.com",
3219
+ ' /cloudsync set token wst_\u2026 (from the dashboard "Connect client" flow)'
3220
+ ].join("\n")
3221
+ };
3222
+ }
3223
+ await setCloudSyncConfig({ enabled: true });
3224
+ const first = await runPass();
3225
+ return { message: `Cloud config sync enabled.
3226
+ ${first}` };
3227
+ }
3228
+ case "off": {
3229
+ await setCloudSyncConfig({ enabled: false });
3230
+ return { message: "Cloud config sync disabled. Local config kept as-is." };
3231
+ }
3232
+ case "now": {
3233
+ try {
3234
+ return { message: await runPass() };
3235
+ } catch (err) {
3236
+ return { message: `Sync failed: ${toErrorMessage(err)}` };
3237
+ }
3238
+ }
3239
+ case "set": {
3240
+ const [field, ...valueParts] = restJoined.split(/\s+/);
3241
+ const value = valueParts.join(" ").trim();
3242
+ if (field === "url") {
3243
+ if (!/^https?:\/\//.test(value)) {
3244
+ return { message: "Usage: /cloudsync set url https://my.wrongstack.com" };
3245
+ }
3246
+ await setCloudSyncConfig({ url: value.replace(/\/+$/, "") });
3247
+ return { message: `Portal URL set to ${value}.` };
3248
+ }
3249
+ if (field === "token") {
3250
+ if (!/^wst_[A-Za-z0-9_-]{43}$/.test(value)) {
3251
+ return {
3252
+ message: 'That does not look like a machine token (wst_ + 43 chars). Create one in the dashboard "Connect client" flow.'
3253
+ };
3254
+ }
3255
+ await setCloudSyncConfig({ token: value });
3256
+ return { message: "Machine token stored (encrypted at rest)." };
3257
+ }
3258
+ if (field === "interval") {
3259
+ const seconds = Number.parseInt(value, 10);
3260
+ if (!Number.isFinite(seconds) || seconds < MIN_INTERVAL_SECONDS) {
3261
+ return {
3262
+ message: `Usage: /cloudsync set interval <seconds \u2265 ${MIN_INTERVAL_SECONDS}>`
3263
+ };
3264
+ }
3265
+ await setCloudSyncConfig({ intervalSeconds: seconds });
3266
+ return { message: `Sync interval set to ${seconds}s.` };
3267
+ }
3268
+ return { message: "Usage: /cloudsync set url|token|interval <value>" };
3269
+ }
3270
+ default:
3271
+ return {
3272
+ message: [
3273
+ "/cloudsync \u2014 my.wrongstack.com config sync",
3274
+ "",
3275
+ " /cloudsync status Show sync state",
3276
+ " /cloudsync set url <url> Set the portal URL",
3277
+ " /cloudsync set token <wst_\u2026> Store the machine token (encrypted)",
3278
+ " /cloudsync set interval <sec> Background pass interval",
3279
+ " /cloudsync on Enable + run the first pass",
3280
+ " /cloudsync off Disable background sync",
3281
+ " /cloudsync now Run one pass immediately",
3282
+ "",
3283
+ "Secrets never sync: providers keys, hq binding, and bot tokens stay local."
3284
+ ].join("\n")
3285
+ };
3286
+ }
3287
+ }
3288
+ };
3289
+ }
3290
+
3291
+ // src/plugins/prompts-plugin.ts
3292
+ import * as fs from "node:fs/promises";
3293
+ import * as path6 from "node:path";
3294
+ function createPromptsPlugin(opts) {
3295
+ let store = null;
3296
+ let loader = null;
3297
+ let usage = null;
3298
+ return {
3299
+ name: "wstack-prompts",
3300
+ version: "1.0.0",
3301
+ description: "Prompt library with 100+ builtin prompts, search, and AI authoring",
3302
+ apiVersion: "^0.1",
3303
+ capabilities: { slashCommands: true, llm: true },
3304
+ defaultConfig: {},
3305
+ setup(api) {
3306
+ const rawConfig = api.config;
3307
+ const paths = opts?.paths ?? rawConfig["paths"];
3308
+ store = opts?.store ?? (paths ? new DefaultPromptStore(paths) : null);
3309
+ loader = opts?.loader ?? rawConfig["promptLoader"] ?? (paths ? new DefaultPromptLoader({
3310
+ paths,
3311
+ bundledDir: rawConfig["bundledPromptsDir"]
3312
+ }) : null);
3313
+ usage = opts?.usage ?? (paths ? new PromptUsageStore(paths.promptUsage) : null);
3314
+ api.slashCommands.register(buildPromptsCommand(() => store, () => loader, () => api.llm));
3315
+ api.slashCommands.register(buildPromptSearchCommand(() => loader, () => usage));
3316
+ api.slashCommands.register(buildPromptGenCommand(() => loader));
3317
+ api.log.info("[prompts] loaded \u2014 /prompts, /prompt, /prompt-gen available");
3318
+ },
3319
+ teardown(api) {
3320
+ api.slashCommands.unregister("prompts");
3321
+ api.slashCommands.unregister("prompt");
3322
+ api.slashCommands.unregister("prompt-gen");
3323
+ api.log.info("[prompts] unloaded");
3324
+ },
3325
+ async health() {
3326
+ return { ok: true, message: "Prompt store accessible" };
3327
+ }
3328
+ };
3329
+ }
3330
+ function buildPromptsCommand(getStore, getLoader, getLlm) {
3331
+ return {
3332
+ name: "prompts",
3333
+ description: "Manage your prompt library: /prompts [list|view|add|edit|delete|favorite|extend]",
3334
+ async run(args, ctx) {
3335
+ const store = getStore();
3336
+ const loader = getLoader();
3337
+ if (!store) return { message: "Prompt store not available." };
3338
+ const [verb, ...rest] = args.trim().split(/\s+/);
3339
+ const restJoined = rest.join(" ");
3340
+ switch (verb) {
3341
+ case "":
3342
+ case "list":
3343
+ case "ls": {
3344
+ const entries = await store.list();
3345
+ if (entries.length === 0) {
3346
+ return { message: 'Prompt library empty. Add: /prompts add "title" "content"' };
3347
+ }
3348
+ const lines = entries.map(
3349
+ (e) => ` ${e.favorite ? "\u2605 " : ""}${e.title} ${dim(e.id)} ${e.tags.join(", ") || ""}`
3350
+ );
3351
+ return { message: `Prompt library (${entries.length}):
3352
+ ${lines.join("\n")}
3353
+ ` };
3354
+ }
3355
+ case "view":
3356
+ case "show": {
3357
+ if (!restJoined) return { message: "Usage: /prompts view <title>" };
3358
+ const matches = await store.find(restJoined);
3359
+ if (matches.length === 0) return { message: `No prompt matching "${restJoined}".` };
3360
+ const entry = matches.find((m) => m.title.toLowerCase() === restJoined.toLowerCase()) ?? expectDefined(matches[0]);
3361
+ return { message: formatPrompt(entry) };
3362
+ }
3363
+ case "add":
3364
+ case "new": {
3365
+ const { flags, positional } = parseFlags(restJoined);
3366
+ const parsed = parseTitleContent(positional);
3367
+ if (!parsed.title) return { message: 'Usage: /prompts add "title" "prompt content"' };
3368
+ const category = flags["category"] ?? flags["cat"];
3369
+ const variables = parseVarFlags(flags["var"]);
3370
+ const entry = store.createNew(parsed.title, parsed.content, parseTags(flags["tags"]), {
3371
+ description: flags["description"] ?? flags["desc"] ?? "",
3372
+ ...category ? { category } : {},
3373
+ ...variables ? { variables } : {}
3374
+ });
3375
+ await store.save(entry);
3376
+ getLoader()?.invalidateCache();
3377
+ return { message: `Added prompt "${entry.title}" (${entry.slug}).` };
3378
+ }
3379
+ case "delete":
3380
+ case "rm": {
3381
+ if (!restJoined) return { message: "Usage: /prompts delete <title>" };
3382
+ const matches = await store.find(restJoined);
3383
+ if (matches.length === 0) return { message: `No prompt matching "${restJoined}".` };
3384
+ const exact = matches.find((m) => m.title.toLowerCase() === restJoined.toLowerCase()) ?? expectDefined(matches[0]);
3385
+ const deleted = await store.delete(exact.id);
3386
+ getLoader()?.invalidateCache();
3387
+ return { message: deleted ? `Deleted "${exact.title}".` : "Delete failed." };
3388
+ }
3389
+ case "edit":
3390
+ case "update": {
3391
+ const parsed = parseTitleContent(restJoined);
3392
+ if (!parsed.title) return { message: 'Usage: /prompts edit "title" "new content"' };
3393
+ const matches = await store.find(parsed.title);
3394
+ if (matches.length === 0) return { message: `No prompt matching "${parsed.title}".` };
3395
+ const exact = matches.find((m) => m.title.toLowerCase() === parsed.title?.toLowerCase()) ?? expectDefined(matches[0]);
3396
+ exact.content = parsed.content;
3397
+ exact.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3398
+ await store.save(exact);
3399
+ getLoader()?.invalidateCache();
3400
+ return { message: `Updated "${exact.title}".` };
3401
+ }
3402
+ case "favorite":
3403
+ case "star": {
3404
+ if (!restJoined) return { message: "Usage: /prompts favorite <slug-or-title>" };
3405
+ if (loader) {
3406
+ const updated = await loader.setFavorite(restJoined, true);
3407
+ if (!updated) return { message: `No prompt matching "${restJoined}".` };
3408
+ return { message: `\u2605 Favorited "${updated.title}" (${updated.slug}).` };
3409
+ }
3410
+ const matches = await store.find(restJoined);
3411
+ if (matches.length === 0) return { message: `No prompt matching "${restJoined}".` };
3412
+ const exact = expectDefined(matches[0]);
3413
+ exact.favorite = true;
3414
+ exact.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3415
+ await store.save(exact);
3416
+ return { message: `\u2605 Favorited "${exact.title}".` };
3417
+ }
3418
+ case "extend": {
3419
+ if (!restJoined) return { message: 'Usage: /prompts extend "title" <instructions>' };
3420
+ const parsed = parseTitleContent(restJoined);
3421
+ if (!parsed.title) return { message: 'Usage: /prompts extend "title" <instructions>' };
3422
+ const matches = await store.find(parsed.title);
3423
+ if (matches.length === 0) return { message: `No prompt matching "${parsed.title}".` };
3424
+ const exact = matches.find((m) => m.title.toLowerCase() === parsed.title?.toLowerCase()) ?? expectDefined(matches[0]);
3425
+ const llm = getLlm();
3426
+ if (!llm) return { message: "LLM not available. Configure a provider first." };
3427
+ let enhanced;
3428
+ try {
3429
+ enhanced = await llm.complete(
3430
+ renderInstructionTemplate(readBundledInstructionText("llm/prompt-extend.md"), {
3431
+ existingPrompt: exact.content,
3432
+ additionalInstructions: parsed.content
3433
+ }),
3434
+ {
3435
+ system: "You improve reusable prompts while preserving their intent and variables.",
3436
+ role: "prompt-refiner",
3437
+ maxTokens: 2048
3438
+ }
3439
+ );
3440
+ } catch {
3441
+ return { message: "LLM enhancement failed. The saved prompt was not changed." };
3442
+ }
3443
+ exact.content = enhanced.text.trim();
3444
+ exact.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3445
+ await store.save(exact);
3446
+ getLoader()?.invalidateCache();
3447
+ return { message: `Extended "${exact.title}".
3448
+
3449
+ ${dim("New content:")}
3450
+ ${exact.content}` };
3451
+ }
3452
+ case "export": {
3453
+ if (!loader) return { message: "Prompt library not available." };
3454
+ const own = (await loader.list()).filter((e) => e.source !== "builtin");
3455
+ if (own.length === 0) {
3456
+ return { message: "No user prompts to export. (Builtin prompts ship with WrongStack.)" };
3457
+ }
3458
+ const target = resolveIoPath(restJoined || "wrongstack-prompts.json", ctx);
3459
+ const payload = JSON.stringify(
3460
+ { version: 2, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), prompts: own },
3461
+ null,
3462
+ 2
3463
+ );
3464
+ try {
3465
+ await fs.writeFile(target, payload, "utf8");
3466
+ return { message: `Exported ${own.length} prompt(s) \u2192 ${target}` };
3467
+ } catch (err) {
3468
+ return { message: `Export failed: ${err instanceof Error ? err.message : String(err)}` };
3469
+ }
3470
+ }
3471
+ case "import": {
3472
+ if (!loader) return { message: "Prompt library not available." };
3473
+ if (!restJoined) return { message: "Usage: /prompts import <path-to.json>" };
3474
+ const src = resolveIoPath(restJoined, ctx);
3475
+ let raw;
3476
+ try {
3477
+ raw = JSON.parse(await fs.readFile(src, "utf8"));
3478
+ } catch (err) {
3479
+ return { message: `Import failed: ${err instanceof Error ? err.message : String(err)}` };
3480
+ }
3481
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw.prompts) ? raw.prompts : null;
3482
+ if (!list) return { message: "Import failed: expected a JSON array or { prompts: [...] }." };
3483
+ let imported = 0;
3484
+ let skipped = 0;
3485
+ for (const item of list) {
3486
+ const entry = migratePromptEntry(item);
3487
+ if (!entry) {
3488
+ skipped++;
3489
+ continue;
3490
+ }
3491
+ entry.source = "user";
3492
+ entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3493
+ const existing = await loader.find(entry.slug);
3494
+ if (existing && existing.source !== "builtin") entry.id = existing.id;
3495
+ await loader.save(entry);
3496
+ imported++;
3497
+ }
3498
+ return {
3499
+ message: `Imported ${imported} prompt(s)${skipped ? ` (${skipped} skipped \u2014 invalid)` : ""} from ${src}.`
3500
+ };
3501
+ }
3502
+ default:
3503
+ return {
3504
+ message: `Unknown subcommand "${verb}". Try: list | view | add | edit | delete | favorite | export | import | extend`
3505
+ };
3506
+ }
3507
+ }
3508
+ };
3509
+ }
3510
+ function buildPromptSearchCommand(getLoader, getUsage) {
3511
+ return {
3512
+ name: "prompt",
3513
+ description: "Search the prompt library and insert one: /prompt <query> | /prompt insert <slug>",
3514
+ argsHint: "<query> | insert <slug> [var=value \u2026] | recent | favorites",
3515
+ async run(args) {
3516
+ const loader = getLoader();
3517
+ if (!loader) return { message: "Prompt library not available." };
3518
+ const trimmed = args.trim();
3519
+ if (trimmed.startsWith("insert ") || trimmed.startsWith("use ")) {
3520
+ const rest = trimmed.slice(trimmed.indexOf(" ") + 1).trim();
3521
+ const [slug, ...kvs] = rest.split(/\s+/);
3522
+ if (!slug) return { message: "Usage: /prompt insert <slug> [var=value \u2026]" };
3523
+ const entry = await loader.find(slug);
3524
+ if (!entry) return { message: `No prompt with slug/id "${slug}". Try /prompt ${slug}` };
3525
+ const values = parseKeyValues(kvs);
3526
+ const { text, missing, invalid } = renderPrompt(entry, values);
3527
+ if (invalid.length > 0) {
3528
+ const hint = (entry.variables ?? []).filter((v) => invalid.includes(v.name)).map((v) => ` ${v.name}: one of ${(v.enum ?? []).join(" | ")}`).join("\n");
3529
+ return {
3530
+ message: `"${entry.title}" got an out-of-range value for: ${invalid.join(", ")}
3531
+ ${hint}`
3532
+ };
3533
+ }
3534
+ if (missing.length > 0) {
3535
+ const hint = (entry.variables ?? []).filter((v) => missing.includes(v.name)).map((v) => {
3536
+ const opts = v.enum && v.enum.length > 0 ? ` [${v.enum.join(" | ")}]` : "";
3537
+ return ` ${v.name}=\u2026${v.description ? ` (${v.description})` : ""}${opts}`;
3538
+ }).join("\n");
3539
+ return {
3540
+ message: `"${entry.title}" needs values for: ${missing.join(", ")}
3541
+ ${hint}
3542
+
3543
+ Re-run: /prompt insert ${entry.slug} ${missing.map((m) => `${m}=\u2026`).join(" ")}`
3544
+ };
3545
+ }
3546
+ try {
3547
+ await getUsage()?.record(entry.slug);
3548
+ } catch {
3549
+ }
3550
+ return { message: `Inserted "${entry.title}".`, runText: text };
3551
+ }
3552
+ if (trimmed === "recent" || trimmed === "popular") {
3553
+ const u = getUsage();
3554
+ if (!u) return { message: "Usage tracking not available." };
3555
+ const top = trimmed === "popular" ? await u.top(15) : await u.recent(15);
3556
+ if (top.length === 0) {
3557
+ return { message: "No prompt usage yet. Insert one with /prompt insert <slug>." };
3558
+ }
3559
+ const lines2 = [];
3560
+ for (const { slug, usage } of top) {
3561
+ const e = await loader.find(slug);
3562
+ const title = e?.title ?? slug;
3563
+ lines2.push(` ${e ? sourceGlyph(e) : "\u2022"} ${title} ${dim(slug)} ${dim(`\xD7${usage.count}`)}`);
3564
+ }
3565
+ return {
3566
+ message: `${trimmed === "popular" ? "Most-used" : "Recent"} prompts:
3567
+ ${lines2.join("\n")}
3568
+
3569
+ Insert: /prompt insert <slug>`
3570
+ };
3571
+ }
3572
+ if (trimmed === "favorites" || trimmed === "fav" || trimmed === "starred") {
3573
+ const favs = (await loader.list()).filter((e) => e.favorite);
3574
+ if (favs.length === 0) {
3575
+ return { message: "No favorites yet. Star one with /prompts favorite <slug>." };
3576
+ }
3577
+ const lines2 = favs.map(
3578
+ (e) => ` ${sourceGlyph(e)} \u2605 ${e.title} ${dim(e.slug)} ${dim(`[${e.category}]`)}`
3579
+ );
3580
+ return { message: `Favorites (${favs.length}):
3581
+ ${lines2.join("\n")}
3582
+
3583
+ Insert: /prompt insert <slug>` };
3584
+ }
3585
+ if (!trimmed) {
3586
+ const cats = await loader.categories();
3587
+ const total = cats.reduce((n, c) => n + c.count, 0);
3588
+ const catLine = cats.map((c) => `${c.label} (${c.count})`).join(" \xB7 ");
3589
+ return {
3590
+ message: [
3591
+ `Prompt library: ${total} prompts.`,
3592
+ catLine,
3593
+ "",
3594
+ "Search: /prompt <query> \xB7 Recent: /prompt recent \xB7 Favorites: /prompt favorites \xB7 Insert: /prompt insert <slug>"
3595
+ ].join("\n")
3596
+ };
3597
+ }
3598
+ const results = await loader.search(trimmed, { limit: 15 });
3599
+ if (results.length === 0) return { message: `No prompts matching "${trimmed}".` };
3600
+ const lines = results.map(
3601
+ (e) => ` ${sourceGlyph(e)} ${e.favorite ? "\u2605 " : ""}${e.title} ${dim(e.slug)} ${dim(`[${e.category}]`)}
3602
+ ${e.description}`
3603
+ );
3604
+ return {
3605
+ message: `${results.length} match(es) for "${trimmed}":
3606
+ ${lines.join("\n")}
3607
+
3608
+ Insert: /prompt insert <slug>`
3609
+ };
3610
+ }
3611
+ };
3612
+ }
3613
+ function buildPromptGenCommand(getLoader) {
3614
+ return {
3615
+ name: "prompt-gen",
3616
+ description: "Create a new reusable prompt with AI guidance. The AI will interview you.",
3617
+ help: [
3618
+ "\u2554\u2550\u2550\u2550 Prompt Generator \u2550\u2550\u2550\u2557",
3619
+ "",
3620
+ "Create a high-quality, reusable prompt with AI guidance.",
3621
+ "",
3622
+ "Usage:",
3623
+ " /prompt-gen Start AI-guided authoring",
3624
+ " /prompt-gen list List prompts in your library",
3625
+ " /prompt-gen edit <slug> View an existing prompt",
3626
+ "",
3627
+ "The AI asks about purpose, task and variables, drafts a strong",
3628
+ "prompt, then saves it via /prompts add."
3629
+ ].join("\n"),
3630
+ async run(args) {
3631
+ const loader = getLoader();
3632
+ const trimmed = args.trim();
3633
+ if (trimmed === "list" || trimmed === "ls") {
3634
+ if (!loader) return { message: "Prompt library not available." };
3635
+ const all = await loader.list();
3636
+ if (all.length === 0) return { message: "No prompts found." };
3637
+ const lines = all.slice(0, 50).map((e) => ` ${sourceGlyph(e)} ${e.title} ${dim(e.slug)}`);
3638
+ return { message: `Prompts (${all.length}):
3639
+ ${lines.join("\n")}` };
3640
+ }
3641
+ if (trimmed.startsWith("edit ") || trimmed.startsWith("view ")) {
3642
+ if (!loader) return { message: "Prompt library not available." };
3643
+ const slug = trimmed.slice(trimmed.indexOf(" ") + 1).trim();
3644
+ const entry = await loader.find(slug);
3645
+ if (!entry) return { message: `Prompt "${slug}" not found.` };
3646
+ return { message: formatPrompt(entry) };
3647
+ }
3648
+ return {
3649
+ message: "\u2554\u2550\u2550\u2550 Prompt Generator \u2550\u2550\u2550\u2557\n\nThe AI will guide you through creating a new prompt.\nAnswer its questions naturally.",
3650
+ runText: [
3651
+ "I want to create a new reusable prompt for my prompt library.",
3652
+ "Read the `prompt-engineering` skill and use it as your playbook.",
3653
+ "Interview me ONE question at a time to elicit:",
3654
+ " 1. the purpose and the exact task the prompt should accomplish,",
3655
+ " 2. the inputs that change each time (these become {{variables}}),",
3656
+ " 3. the desired output format and tone.",
3657
+ "Then draft a high-quality prompt that uses {{double-brace}} placeholders for the variables.",
3658
+ "Show me the draft for approval, then SAVE it by calling:",
3659
+ ' /prompts add --category <category> --description "<one-line summary>" --tags <comma,separated> --var <name:description> "<Title>" "<prompt content>"',
3660
+ "Pick the best-fitting category from: coding, debugging, refactoring, testing, code-review, architecture, devops, documentation, data-analysis, writing, research, product, agentic-workflows, meta-prompting."
3661
+ ].join("\n")
3662
+ };
3663
+ }
3664
+ };
3665
+ }
3666
+ function formatPrompt(entry) {
3667
+ const tags = entry.tags.length > 0 ? ` [${entry.tags.join(", ")}]` : "";
3668
+ const vars = entry.variables && entry.variables.length > 0 ? `
3669
+
3670
+ Variables: ${entry.variables.map((v) => `{{${v.name}}}${v.required ? "*" : ""}`).join(", ")}` : "";
3671
+ return [
3672
+ `# ${entry.title}${tags}`,
3673
+ entry.description ? `
3674
+ ${entry.description}` : "",
3675
+ `
3676
+ ${entry.content}${vars}`,
3677
+ `
3678
+ ${dim(`slug: ${entry.slug} | category: ${entry.category} | source: ${entry.source}`)}`
3679
+ ].join("\n");
3680
+ }
3681
+ function resolveIoPath(p, ctx) {
3682
+ const cleaned = p.trim().replace(/^["']|["']$/g, "");
3683
+ if (path6.isAbsolute(cleaned)) return cleaned;
3684
+ const base = ctx.projectRoot ?? process.cwd();
3685
+ return path6.resolve(base, cleaned);
3686
+ }
3687
+ function sourceGlyph(e) {
3688
+ return e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : e.source === "synced" ? "\u2601" : "\u{1F4E6}";
3689
+ }
3690
+ function parseFlags(input) {
3691
+ const flags = {};
3692
+ let s = input.trim();
3693
+ const flagRe = /^--([a-zA-Z][\w-]*)(?:=("[^"]*"|'[^']*'|\S+)|\s+("[^"]*"|'[^']*'|\S+))?\s*/;
3694
+ let m;
3695
+ while (s.startsWith("--") && (m = flagRe.exec(s))) {
3696
+ const name = expectDefined(m[1]);
3697
+ const rawVal = m[2] ?? m[3] ?? "true";
3698
+ flags[name] = unquote(rawVal);
3699
+ s = s.slice(m[0].length);
3700
+ }
3701
+ return { flags, positional: s.trim() };
3702
+ }
3703
+ function unquote(v) {
3704
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
3705
+ return v.slice(1, -1);
3706
+ }
3707
+ return v;
3708
+ }
3709
+ function parseTags(raw) {
3710
+ if (!raw) return [];
3711
+ return raw.split(",").map((t) => t.trim()).filter(Boolean);
3712
+ }
3713
+ function parseVarFlags(raw) {
3714
+ if (!raw) return void 0;
3715
+ const out = [];
3716
+ for (const part of raw.split(",")) {
3717
+ const trimmed = part.trim();
3718
+ if (!trimmed) continue;
3719
+ const metaAt = trimmed.indexOf("::");
3720
+ const head = metaAt === -1 ? trimmed : trimmed.slice(0, metaAt).trim();
3721
+ const meta = metaAt === -1 ? "" : trimmed.slice(metaAt + 2).trim();
3722
+ const colon = head.indexOf(":");
3723
+ const name = colon === -1 ? head : head.slice(0, colon).trim();
3724
+ const description = colon === -1 ? void 0 : head.slice(colon + 1).trim();
3725
+ if (!name) continue;
3726
+ const v = { name, description, required: true };
3727
+ for (const token of meta.split("::").map((t) => t.trim()).filter(Boolean)) {
3728
+ if (token === "multiline") v.multiline = true;
3729
+ else if (token.startsWith("enum=")) {
3730
+ const opts = token.slice(5).split("|").map((o) => o.trim()).filter(Boolean);
3731
+ if (opts.length > 0) v.enum = opts;
3732
+ }
3733
+ }
3734
+ out.push(v);
3735
+ }
3736
+ return out.length > 0 ? out : void 0;
3737
+ }
3738
+ function parseKeyValues(kvs) {
3739
+ const out = {};
3740
+ for (const kv of kvs) {
3741
+ const eq = kv.indexOf("=");
3742
+ if (eq > 0) out[kv.slice(0, eq)] = unquote(kv.slice(eq + 1));
3743
+ }
3744
+ return out;
3745
+ }
3746
+ function parseTitleContent(args) {
3747
+ const trimmed = args.trim();
3748
+ if (!trimmed) return { title: "", content: "" };
3749
+ const doubleMatch = /^"([^"]+)"\s+"([^"]+)"$/.exec(trimmed) || /^'([^']+)'\s+'([^']+)'$/.exec(trimmed);
3750
+ if (doubleMatch)
3751
+ return { title: expectDefined(doubleMatch[1]), content: expectDefined(doubleMatch[2]) };
3752
+ const quotedTitleMatch = /^"([^"]+)"\s+(.+)$/.exec(trimmed) || /^'([^']+)'\s+(.+)$/.exec(trimmed);
3753
+ if (quotedTitleMatch)
3754
+ return { title: expectDefined(quotedTitleMatch[1]), content: expectDefined(quotedTitleMatch[2]) };
3755
+ const firstSpace = trimmed.indexOf(" ");
3756
+ if (firstSpace === -1) return { title: trimmed, content: "" };
3757
+ return { title: trimmed.slice(0, firstSpace), content: trimmed.slice(firstSpace + 1) };
3758
+ }
3759
+ function dim(s) {
3760
+ return `\x1B[2m${s}\x1B[0m`;
3761
+ }
3762
+
3763
+ // src/plugins/skills-plugin.ts
3764
+ import * as os2 from "node:os";
3765
+ import * as path7 from "node:path";
3766
+ function createSkillsPlugin(opts) {
3767
+ return {
3768
+ name: "wstack-skills",
3769
+ version: "1.1.0",
3770
+ description: "Skill library, registry search, installer and authoring toolkit: /skill, /skill-gen, /skill-search, /skill-install, ...",
3771
+ apiVersion: "^0.1",
3772
+ capabilities: { slashCommands: true },
3773
+ defaultConfig: {},
3774
+ setup(api) {
3775
+ const rawConfig = api.config;
3776
+ const skillLoader = opts?.skillLoader ?? rawConfig.skillLoader;
3777
+ const registryUrl = rawConfig.skills?.registryUrl?.trim() || void 0;
3778
+ const registryAdapters = opts?.registryAdapters ?? [
3779
+ githubDirectAdapter,
3780
+ createSkillsShAdapter(registryUrl ? { baseUrl: registryUrl } : {})
3781
+ ];
3782
+ api.slashCommands.register(buildSkillCommand(skillLoader));
3783
+ api.slashCommands.register(buildSkillGeneratorCommand(skillLoader));
3784
+ api.slashCommands.register(buildSkillSearchCommand(skillLoader, registryAdapters));
3785
+ api.slashCommands.register(buildSkillInstallCommand(skillLoader, registryAdapters));
3786
+ api.slashCommands.register(buildSkillImportCommand(skillLoader));
3787
+ api.slashCommands.register(buildSkillUpdateCommand(skillLoader, registryAdapters));
3788
+ api.slashCommands.register(buildSkillUninstallCommand(skillLoader));
3789
+ api.log.info(
3790
+ "[skills] loaded \u2014 /skill, /skill-gen, /skill-search, /skill-install, /skill-import, /skill-update/uninstall available"
3791
+ );
3792
+ },
3793
+ teardown(api) {
3794
+ for (const name of [
3795
+ "skill",
3796
+ "skill-gen",
3797
+ "skill-search",
3798
+ "skill-install",
3799
+ "skill-import",
3800
+ "skill-update",
3801
+ "skill-uninstall"
3802
+ ]) {
3803
+ api.slashCommands.unregister(name);
3804
+ }
3805
+ api.log.info("[skills] unloaded");
3806
+ },
3807
+ async health() {
3808
+ return { ok: true, message: "skills ready" };
3809
+ }
3810
+ };
3811
+ }
3812
+ function makeInstaller(skillLoader, projectRoot, registryAdapters) {
3813
+ const paths = resolveWstackPaths({ projectRoot });
3814
+ return new SkillInstaller({
3815
+ manifestPath: path7.join(paths.configDir, "installed-skills.json"),
3816
+ projectSkillsDir: paths.inProjectSkills,
3817
+ globalSkillsDir: paths.globalSkills,
3818
+ projectHash: paths.projectHash,
3819
+ skillLoader,
3820
+ ...registryAdapters ? { registryAdapters } : {}
3821
+ });
3822
+ }
3823
+ function buildSkillCommand(skillLoader) {
3824
+ return {
3825
+ name: "skill",
3826
+ description: "Show skill details or list available skills. Use /skill-gen to create new skills.",
3827
+ async run(args) {
3828
+ if (!skillLoader) return { message: "No skill loader configured." };
3829
+ if (!args.trim()) {
3830
+ const entries = await skillLoader.listEntries();
3831
+ if (entries.length === 0) return { message: "No skills found." };
3832
+ const lines = entries.map((e) => {
3833
+ const scopeTag = e.scope.length > 0 ? ` ${color.dim(`(${e.scope.slice(0, 3).join(", ")})`)}` : "";
3834
+ return ` ${color.bold(e.name)}${scopeTag}
3835
+ Use when: ${e.trigger}`;
3836
+ });
3837
+ return { message: `Available skills:
3838
+ ${lines.join("\n\n")}
3839
+ ` };
3840
+ }
3841
+ const skill = await skillLoader.find(args.trim());
3842
+ if (!skill) return { message: `Skill "${args.trim()}" not found.` };
3843
+ const body = capSkillBody(stripFrontmatter(await skillLoader.readBody(skill.name)));
3844
+ return { message: body };
3845
+ }
3846
+ };
3847
+ }
3848
+ function buildSkillGeneratorCommand(skillLoader) {
3849
+ return {
3850
+ name: "skill-gen",
3851
+ description: "Create or validate skills. /skill-gen (wizard) | validate <name> | skeleton <name> | from-prompt <text> | view <name> | edit <name> | list",
3852
+ help: [
3853
+ "\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557",
3854
+ "",
3855
+ "Create and validate AI skills.",
3856
+ "",
3857
+ "Usage:",
3858
+ " /skill-gen Start the interactive AI-guided wizard",
3859
+ " /skill-gen list List existing skills (with source)",
3860
+ " /skill-gen validate <name> Validate a skill name (format + collisions)",
3861
+ " /skill-gen skeleton <name> Generate a SKILL.md skeleton to draft",
3862
+ ' [--desc "..."] [--trigger a,b,c] [--global] [--force]',
3863
+ " /skill-gen from-prompt <text> Turn a prompt into a skill draft",
3864
+ " [--global] [--force]",
3865
+ " /skill-gen view <name> Show a skill's body (read-only)",
3866
+ " /skill-gen edit <name> Open a skill in $EDITOR/$VISUAL",
3867
+ "",
3868
+ "Skeletons are written to .wrongstack/skills/<name>/SKILL.md (--global: active profile/skills/).",
3869
+ "The interactive wizard reads the bundled `skill-creator` skill and guides you step by step."
3870
+ ].join("\n"),
3871
+ async run(args, ctx) {
3872
+ const trimmed = args.trim();
3873
+ const [sub, ...rest] = trimmed.split(/\s+/);
3874
+ if (sub === "list" || sub === "ls") {
3875
+ if (!skillLoader) return { message: "No skill loader configured." };
3876
+ const entries = await skillLoader.listEntries();
3877
+ if (entries.length === 0) return { message: "No skills found." };
3878
+ const lines = entries.map((e) => {
3879
+ const src = e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : e.source === "claude-project" || e.source === "claude-user" ? "\u{1F310}" : e.source === "foreign" ? `\u{1F310}(${e.originTool ?? "?"})` : e.source === "extra" ? "\u2795" : "\u{1F4E6}";
3880
+ return ` ${src} ${e.name}
3881
+ ${e.trigger}`;
3882
+ });
3883
+ let message = `Available Skills:
3884
+ ${lines.join("\n\n")}
3885
+ `;
3886
+ const diag = skillLoader.diagnostics?.();
3887
+ if (diag && (diag.shadowed.length > 0 || diag.skipped.length > 0)) {
3888
+ const notes = [];
3889
+ if (diag.shadowed.length > 0) {
3890
+ notes.push(color.dim("Shadowed (hidden by a higher-priority layer):"));
3891
+ for (const s of diag.shadowed) {
3892
+ notes.push(
3893
+ color.dim(` \u26A0 ${s.name} (${s.source}) \u2190 overridden by ${s.shadowedBy}`)
3894
+ );
3895
+ }
3896
+ }
3897
+ if (diag.skipped.length > 0) {
3898
+ notes.push(color.dim("Skipped (malformed SKILL.md):"));
3899
+ for (const k of diag.skipped) {
3900
+ const name = k.name ? ` "${k.name}"` : "";
3901
+ notes.push(color.dim(` \u2717 ${k.entry}${name} \u2014 ${k.reason}`));
3902
+ }
3903
+ }
3904
+ message += `
3905
+ ${notes.join("\n")}
3906
+ `;
3907
+ }
3908
+ return { message };
3909
+ }
3910
+ if (sub === "validate") {
3911
+ const name = rest[0];
3912
+ if (!name) return { message: "Usage: /skill-gen validate <name>" };
3913
+ const result = await validateSkillNameAvailable(name, skillLoader);
3914
+ const lines = [`Validating "${name}":`];
3915
+ if (result.formatViolations.length > 0) {
3916
+ lines.push(` \u2717 Format issues:`);
3917
+ for (const v of result.formatViolations) lines.push(` - ${v}`);
3918
+ } else {
3919
+ lines.push(` \u2713 Format: valid kebab-case`);
3920
+ }
3921
+ if (result.conflicts.length > 0) {
3922
+ const sameLayer = result.conflicts.filter(
3923
+ (c) => c.source === "project" || c.source === "user"
3924
+ );
3925
+ const shadowing = result.conflicts.filter(
3926
+ (c) => c.source !== "project" && c.source !== "user"
3927
+ );
3928
+ if (sameLayer.length > 0) {
3929
+ lines.push(` \u2717 Collisions (same layer \u2014 would overwrite):`);
3930
+ for (const c of sameLayer) lines.push(` - ${c.name} (${c.source})`);
3931
+ }
3932
+ if (shadowing.length > 0) {
3933
+ lines.push(` \u26A0 Shadows lower-priority skills (intentional override):`);
3934
+ for (const c of shadowing) lines.push(` - ${c.name} (${c.source})`);
3935
+ }
3936
+ } else {
3937
+ lines.push(` \u2713 No collisions`);
3938
+ }
3939
+ lines.push(result.ok ? ` \u2192 Ready to use.` : ` \u2192 Fix the issues above first.`);
3940
+ return { message: lines.join("\n") };
3941
+ }
3942
+ if (sub === "skeleton") {
3943
+ return runSkeleton(rest, ctx, skillLoader);
3944
+ }
3945
+ if (sub === "from-prompt") {
3946
+ return runFromPrompt(rest, ctx, skillLoader);
3947
+ }
3948
+ if (sub === "view") {
3949
+ const skillName = rest[0];
3950
+ if (!skillName) return { message: "Usage: /skill-gen view <name>" };
3951
+ if (!skillLoader) return { message: "No skill loader configured." };
3952
+ const skill = await skillLoader.find(skillName);
3953
+ if (!skill) return { message: `Skill "${skillName}" not found.` };
3954
+ const body = await skillLoader.readBody(skillName);
3955
+ return { message: [`Skill: ${skillName}`, `Path: ${skill.path}`, "", body].join("\n") };
3956
+ }
3957
+ if (sub === "edit") {
3958
+ const skillName = rest[0];
3959
+ if (!skillName) return { message: "Usage: /skill-gen edit <name>" };
3960
+ if (!skillLoader) return { message: "No skill loader configured." };
3961
+ const skill = await skillLoader.find(skillName);
3962
+ if (!skill) return { message: `Skill "${skillName}" not found.` };
3963
+ try {
3964
+ await openInEditor(skill.path);
3965
+ skillLoader.invalidateCache();
3966
+ return {
3967
+ message: `Opening ${skill.path} in your editor\u2026 (skill cache cleared \u2014 changes load on next use)`
3968
+ };
3969
+ } catch (err) {
3970
+ return { message: `\u2717 ${toErrorMessage(err)}` };
3971
+ }
3972
+ }
3973
+ if (trimmed.startsWith("edit ")) {
3974
+ return buildSkillGeneratorCommand(skillLoader).run(`view ${rest.join(" ")}`, ctx);
3975
+ }
3976
+ return {
3977
+ message: "\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557\n\nThe AI will guide you through creating a new skill.\nAnswer its questions naturally. (Or use /skill-gen skeleton <name> for a quick scaffold.)",
3978
+ runText: "I want to create a new AI skill. Read the skill-creator skill and guide me through the process. Ask me questions one at a time \u2014 name, description, what to cover \u2014 then create the SKILL.md file. After writing it, run /skill-gen validate <name> to confirm."
3979
+ };
3980
+ }
3981
+ };
3982
+ }
3983
+ async function runSkeleton(rest, ctx, skillLoader) {
3984
+ const name = rest.find((p) => !p.startsWith("--"));
3985
+ if (!name) {
3986
+ return {
3987
+ message: 'Usage: /skill-gen skeleton <name> [--desc "..."] [--trigger a,b,c] [--global] [--force]'
3988
+ };
3989
+ }
3990
+ const desc = parseFlagValue(rest, "--desc");
3991
+ const triggers = parseFlagValue(rest, "--trigger")?.split(",").map((t) => t.trim()).filter(Boolean);
3992
+ const isGlobal = rest.includes("--global");
3993
+ const force = rest.includes("--force");
3994
+ const validation = await validateSkillNameAvailable(name, skillLoader);
3995
+ if (!validation.ok) {
3996
+ const issues = [...validation.formatViolations];
3997
+ for (const c of validation.conflicts) {
3998
+ if (c.source === "project" || c.source === "user") {
3999
+ issues.push(
4000
+ `collides with existing ${c.source} skill "${c.name}" (use --force to overwrite)`
4001
+ );
4002
+ }
4003
+ }
4004
+ return { message: `\u2717 Cannot scaffold "${name}":
4005
+ - ${issues.join("\n - ")}` };
4006
+ }
4007
+ const body = generateSkillSkeleton({ name, description: desc ?? "", triggerKeywords: triggers });
4008
+ const paths = resolveWstackPaths({ projectRoot: ctx.projectRoot });
4009
+ const skillsDir = isGlobal ? paths.globalSkills : paths.inProjectSkills;
4010
+ try {
4011
+ const written = await writeSkeletonSkill(skillsDir, body, { overwrite: force });
4012
+ const advisory = bodyLineAdvisory(body);
4013
+ return {
4014
+ message: [
4015
+ `\u2713 Scaffolded ${name} \u2192 ${written}`,
4016
+ advisory.over ? ` \u26A0 Body is ${advisory.lines} lines (skill-creator recommends \u2264500).` : "",
4017
+ ` Next: edit the body, then /skill-gen validate ${name}.`
4018
+ ].filter(Boolean).join("\n")
4019
+ };
4020
+ } catch (err) {
4021
+ return { message: `\u2717 ${toErrorMessage(err)}` };
4022
+ }
4023
+ }
4024
+ async function runFromPrompt(rest, ctx, skillLoader) {
4025
+ const promptText = rest.filter((p) => !p.startsWith("--")).join(" ").trim();
4026
+ if (!promptText) {
4027
+ return { message: "Usage: /skill-gen from-prompt <text> [--global] [--force]" };
4028
+ }
4029
+ const isGlobal = rest.includes("--global");
4030
+ const force = rest.includes("--force");
4031
+ const draft = extractSkillFromPrompt(promptText);
4032
+ if (!draft.suggestedName) {
4033
+ return { message: "\u2717 Could not derive a skill name from the prompt. Provide a # heading." };
4034
+ }
4035
+ const validation = await validateSkillNameAvailable(draft.suggestedName, skillLoader);
4036
+ if (!validation.ok) {
4037
+ return {
4038
+ message: `\u2717 Derived name "${draft.suggestedName}" has issues:
4039
+ - ` + [
4040
+ ...validation.formatViolations,
4041
+ ...validation.conflicts.map((c) => `collides with ${c.name}`)
4042
+ ].join("\n - ")
4043
+ };
4044
+ }
4045
+ const body = generateSkillSkeleton({
4046
+ name: draft.suggestedName,
4047
+ description: draft.description,
4048
+ triggerKeywords: draft.triggerKeywords
4049
+ });
4050
+ const withBody = draft.body ? body.replace("## Overview\n", `## Overview
4051
+
4052
+ ${draft.body}
4053
+ `) : body;
4054
+ const paths = resolveWstackPaths({ projectRoot: ctx.projectRoot });
4055
+ const skillsDir = isGlobal ? paths.globalSkills : paths.inProjectSkills;
4056
+ try {
4057
+ const written = await writeSkeletonSkill(skillsDir, withBody, { overwrite: force });
4058
+ return {
4059
+ message: `\u2713 Drafted ${draft.suggestedName} from prompt \u2192 ${written}
4060
+ Triggers: ${draft.triggerKeywords.join(", ") || "(none detected)"}
4061
+ Next: review and edit the body, then /skill-gen validate ${draft.suggestedName}.`
4062
+ };
4063
+ } catch (err) {
4064
+ return { message: `\u2717 ${toErrorMessage(err)}` };
4065
+ }
4066
+ }
4067
+ function parseFlagValue(tokens, flag) {
4068
+ const idx = tokens.indexOf(flag);
4069
+ if (idx === -1) return void 0;
4070
+ const next = tokens[idx + 1];
4071
+ return next && !next.startsWith("--") ? next : void 0;
4072
+ }
4073
+ function buildSkillSearchCommand(_skillLoader, registryAdapters) {
4074
+ return {
4075
+ name: "skill-search",
4076
+ description: "Search the skill registry (skills.sh) for installable skills.",
4077
+ argsHint: "<query> [--page N] [--pageSize N]",
4078
+ help: [
4079
+ "\u2554\u2550\u2550\u2550 Skill Search \u2550\u2550\u2550\u2557",
4080
+ "",
4081
+ "Search the skill registry for installable skills.",
4082
+ "Results show name, author, installs, security score, and the install ref.",
4083
+ "",
4084
+ "Usage:",
4085
+ ' /skill-search react Search for "react" skills',
4086
+ ' /skill-search "code review" --page 2 Second page of results',
4087
+ "",
4088
+ `Registries: ${registryAdapters.map((a) => a.id).join(", ")}.`,
4089
+ "To install a hit: /skill-install <owner/repo> (or the full install ref shown)."
4090
+ ].join("\n"),
4091
+ async run(args) {
4092
+ const parts = args.trim().split(/\s+/).filter(Boolean);
4093
+ const query = parts.filter((p) => !p.startsWith("--")).join(" ").trim();
4094
+ if (!query) return { message: "Usage: /skill-search <query> [--page N]" };
4095
+ const page = parseFlagValue(parts, "--page");
4096
+ const pageSize = parseFlagValue(parts, "--pageSize");
4097
+ const tmpInstaller = new SkillInstaller({
4098
+ manifestPath: ":memory:",
4099
+ // never written — search() is read-only
4100
+ projectSkillsDir: "/dev/null",
4101
+ globalSkillsDir: "/dev/null",
4102
+ projectHash: "search",
4103
+ registryAdapters
4104
+ });
4105
+ try {
4106
+ const perAdapter = await tmpInstaller.search(query, {
4107
+ ...page ? { page: Number(page) } : {},
4108
+ ...pageSize ? { pageSize: Number(pageSize) } : {}
4109
+ });
4110
+ const total = perAdapter.reduce((n, b) => n + b.results.length, 0);
4111
+ if (total === 0) {
4112
+ return {
4113
+ message: `No skills found for "${query}" in ${registryAdapters.map((a) => a.id).join(", ")}.`
4114
+ };
4115
+ }
4116
+ const lines = [`Found ${total} skill(s) for "${query}":`];
4117
+ for (const block of perAdapter) {
4118
+ if (block.results.length === 0) continue;
4119
+ lines.push(`
4120
+ ${color.bold(block.adapterId)}:`);
4121
+ for (const r of block.results) {
4122
+ const tier = r.securityScore != null ? securityScoreToTier(r.securityScore) : void 0;
4123
+ const scoreMark = tier === "low" ? ` \u26A0${r.securityScore}` : r.securityScore != null ? ` \u2713${r.securityScore}` : "";
4124
+ const installs = r.installs != null ? ` \xB7 ${formatCount(r.installs)} installs` : "";
4125
+ const author = r.author ? ` ${color.dim(r.author)}` : "";
4126
+ lines.push(` ${color.bold(r.name)}${author}${scoreMark}${installs}`);
4127
+ if (r.description) lines.push(` ${truncate(r.description, 90)}`);
4128
+ lines.push(` ${color.dim("\u2192 /skill-install " + r.installRef)}`);
4129
+ }
4130
+ }
4131
+ return { message: lines.join("\n") };
4132
+ } catch (err) {
4133
+ return { message: `\u2717 Search failed: ${toErrorMessage(err)}` };
4134
+ }
4135
+ }
4136
+ };
4137
+ }
4138
+ function buildSkillInstallCommand(skillLoader, registryAdapters) {
4139
+ return {
4140
+ name: "skill-install",
4141
+ description: "Install skills from a GitHub repository or a registry hit (skills.sh:<id>).",
4142
+ argsHint: "<user/repo[@ref] | skills.sh:<owner/repo> | registry:<id>> [--global]",
4143
+ help: [
4144
+ "\u2554\u2550\u2550\u2550 Skill Install \u2550\u2550\u2550\u2557",
4145
+ "",
4146
+ "Install skills from GitHub (direct) or a registry (resolved to GitHub).",
4147
+ "",
4148
+ "Usage:",
4149
+ " /skill-install <user/repo> Install from default branch (main)",
4150
+ " /skill-install <user/repo@ref> Install specific tag/branch/commit",
4151
+ " /skill-install <user/repo> --global Install to user-global skills",
4152
+ " /skill-install skills.sh:<owner/repo> Resolve a skills.sh hit and install",
4153
+ "",
4154
+ "Supports single-skill repos (SKILL.md at root) and multi-skill repos (skills/).",
4155
+ "Use /skill-search to find skills, then install with the shown install ref.",
4156
+ "",
4157
+ "Private repos: set GITHUB_TOKEN (or GH_TOKEN) in your environment."
4158
+ ].join("\n"),
4159
+ async run(args, ctx) {
4160
+ const parts = args.trim().split(/\s+/);
4161
+ const ref = parts.find((p) => !p.startsWith("--"));
4162
+ const isGlobal = parts.includes("--global");
4163
+ if (!ref) {
4164
+ return {
4165
+ message: "Usage: /skill-install <user/repo[@ref] | skills.sh:<owner/repo>> [--global]\nUse /skill-search to find skills."
4166
+ };
4167
+ }
4168
+ const installer = makeInstaller(skillLoader, ctx.projectRoot, registryAdapters);
4169
+ try {
4170
+ const results = await installer.install(ref, { global: isGlobal });
4171
+ if (results.length === 0) return { message: "No skills found in the repository." };
4172
+ const scope = isGlobal ? "user-global" : "project";
4173
+ const lines = [`Installed ${results.length} skill(s) [${scope}]:`];
4174
+ for (const r of results) {
4175
+ lines.push(` \u2713 ${r.name} (${r.source}@${r.ref})`);
4176
+ lines.push(` \u2192 ${r.path}`);
4177
+ }
4178
+ return { message: lines.join("\n") };
4179
+ } catch (err) {
4180
+ const msg = toErrorMessage(err);
4181
+ return { message: `\u2717 Install failed: ${msg}` };
4182
+ }
4183
+ }
4184
+ };
4185
+ }
4186
+ var IMPORT_SOURCE_TOOLS = [
4187
+ { id: "claude", subdir: "skills" },
4188
+ ...FOREIGN_SKILL_TOOLS
4189
+ ];
4190
+ function resolveImportSourceDir(tool, opts) {
4191
+ const entry = IMPORT_SOURCE_TOOLS.find((t) => t.id === tool);
4192
+ if (!entry) return void 0;
4193
+ const base = opts.global ? opts.homeDir ?? os2.homedir() : opts.projectRoot;
4194
+ return path7.join(base, "." + entry.id, entry.subdir);
4195
+ }
4196
+ function buildSkillImportCommand(skillLoader) {
4197
+ return {
4198
+ name: "skill-import",
4199
+ description: "Import skills from a local directory or another agent into .wrongstack/skills.",
4200
+ argsHint: "[<src-dir> | --from <tool> | --from-claude] [--global] [--link]",
4201
+ help: [
4202
+ "\u2554\u2550\u2550\u2550 Skill Import \u2550\u2550\u2550\u2557",
4203
+ "",
4204
+ "Copy (or symlink) skills into .wrongstack/skills so you can edit and commit",
4205
+ "them. Foreign skills are already readable without importing \u2014 this takes ownership.",
4206
+ "",
4207
+ "Usage:",
4208
+ " /skill-import --from cursor Import project .cursor/skills",
4209
+ " /skill-import --from codex --global Import ~/.codex/skills",
4210
+ " /skill-import --from claude Import project .claude/skills (--from-claude alias)",
4211
+ " /skill-import /path/to/skills Import from any directory",
4212
+ " /skill-import --from trae --link Symlink instead of copy",
4213
+ "",
4214
+ `Known tools: ${IMPORT_SOURCE_TOOLS.map((t) => t.id).join(", ")}.`,
4215
+ "Each subdirectory with a valid SKILL.md is imported."
4216
+ ].join("\n"),
4217
+ async run(args, ctx) {
4218
+ const parts = args.trim().split(/\s+/).filter(Boolean);
4219
+ const isGlobal = parts.includes("--global");
4220
+ const link = parts.includes("--link");
4221
+ const fromIdx = parts.indexOf("--from");
4222
+ let tool;
4223
+ if (fromIdx !== -1) tool = parts[fromIdx + 1];
4224
+ else if (parts.includes("--from-claude")) tool = "claude";
4225
+ const positional = parts.find((p) => !p.startsWith("--") && p !== tool);
4226
+ let srcDir;
4227
+ if (tool) {
4228
+ srcDir = resolveImportSourceDir(tool, { global: isGlobal, projectRoot: ctx.projectRoot });
4229
+ if (!srcDir) {
4230
+ return {
4231
+ message: `Unknown tool "${tool}". Known: ${IMPORT_SOURCE_TOOLS.map((t) => t.id).join(", ")}`
4232
+ };
4233
+ }
4234
+ } else if (positional) {
4235
+ srcDir = path7.resolve(ctx.projectRoot, positional);
4236
+ } else {
4237
+ return {
4238
+ message: "Usage: /skill-import <src-dir> | --from <tool> | --from-claude [--global] [--link]"
4239
+ };
4240
+ }
4241
+ const installer = makeInstaller(skillLoader, ctx.projectRoot);
4242
+ try {
4243
+ const results = await installer.importFromDir(srcDir, { global: isGlobal, link });
4244
+ if (results.length === 0) {
4245
+ return { message: `No valid skills found in ${srcDir}.` };
4246
+ }
4247
+ const scope = isGlobal ? "user-global" : "project";
4248
+ const lines = [
4249
+ `Imported ${results.length} skill(s) [${scope}]${link ? " (symlinked)" : ""}:`
4250
+ ];
4251
+ for (const r of results) {
4252
+ lines.push(` \u2713 ${r.name}`);
4253
+ lines.push(` \u2192 ${r.path}`);
4254
+ }
4255
+ return { message: lines.join("\n") };
4256
+ } catch (err) {
4257
+ return { message: `\u2717 Import failed: ${toErrorMessage(err)}` };
4258
+ }
4259
+ }
4260
+ };
4261
+ }
4262
+ function buildSkillUpdateCommand(skillLoader, registryAdapters) {
4263
+ return {
4264
+ name: "skill-update",
4265
+ description: "Update installed skills from their GitHub source.",
4266
+ argsHint: "[name|ref] [--global]",
4267
+ help: [
4268
+ "\u2554\u2550\u2550\u2550 Skill Update \u2550\u2550\u2550\u2557",
4269
+ "",
4270
+ "Update installed skills from their GitHub source.",
4271
+ "",
4272
+ "Usage:",
4273
+ " /skill-update Update all installed skills",
4274
+ " /skill-update <name> Update a specific skill",
4275
+ " /skill-update <user/repo@ref> Update to a different ref",
4276
+ " /skill-update <name> --global Update a global skill"
4277
+ ].join("\n"),
4278
+ async run(args, ctx) {
4279
+ const parts = args.trim().split(/\s+/);
4280
+ const nameOrRef = parts.find((p) => !p.startsWith("--"));
4281
+ const isGlobal = parts.includes("--global");
4282
+ const installer = makeInstaller(skillLoader, ctx.projectRoot, registryAdapters);
4283
+ try {
4284
+ const result = await installer.update(nameOrRef, { global: isGlobal });
4285
+ const lines = [];
4286
+ if (result.updated.length > 0) {
4287
+ lines.push(`Updated ${result.updated.length} skill(s):`);
4288
+ for (const u of result.updated) {
4289
+ lines.push(
4290
+ u.oldRef !== u.newRef ? ` \u2713 ${u.name} (${u.oldRef} \u2192 ${u.newRef})` : ` \u2713 ${u.name} (refreshed)`
4291
+ );
4292
+ }
4293
+ }
4294
+ if (result.unchanged.length > 0) lines.push(`Up to date: ${result.unchanged.join(", ")}`);
4295
+ if (result.errors.length > 0) {
4296
+ for (const e of result.errors) lines.push(` \u2717 ${e.name}: ${e.error}`);
4297
+ }
4298
+ if (lines.length === 0) return { message: "No installed skills to update." };
4299
+ return { message: lines.join("\n") };
4300
+ } catch (err) {
4301
+ const msg = toErrorMessage(err);
4302
+ return { message: `\u2717 Update failed: ${msg}` };
4303
+ }
4304
+ }
4305
+ };
4306
+ }
4307
+ function buildSkillUninstallCommand(skillLoader) {
4308
+ return {
4309
+ name: "skill-uninstall",
4310
+ description: "Remove an installed skill.",
4311
+ argsHint: "<name> [--global]",
4312
+ help: [
4313
+ "\u2554\u2550\u2550\u2550 Skill Uninstall \u2550\u2550\u2550\u2557",
4314
+ "",
4315
+ "Remove an installed skill and its files.",
4316
+ "",
4317
+ "Usage:",
4318
+ " /skill-uninstall <name> Remove from project skills",
4319
+ " /skill-uninstall <name> --global Remove from user-global skills"
4320
+ ].join("\n"),
4321
+ async run(args, ctx) {
4322
+ const parts = args.trim().split(/\s+/);
4323
+ const name = parts.find((p) => !p.startsWith("--"));
4324
+ const isGlobal = parts.includes("--global");
4325
+ if (!name) {
4326
+ const installer2 = makeInstaller(skillLoader, ctx.projectRoot);
4327
+ const installed = await installer2.listInstalled();
4328
+ if (installed.length === 0) return { message: "No installed skills found." };
4329
+ const scope = isGlobal ? "user" : "project";
4330
+ const filtered = installed.filter((s) => s.scope === scope);
4331
+ if (filtered.length === 0) {
4332
+ return { message: `No installed skills found (${scope} scope).` };
4333
+ }
4334
+ const lines = [`Installed skills (${scope}):`];
4335
+ for (const s of filtered) {
4336
+ lines.push(` ${s.name} ${s.source}@${s.ref} (${s.installedAt.slice(0, 10)})`);
4337
+ }
4338
+ lines.push("", "Use /skill-uninstall <name> to remove.");
4339
+ return { message: lines.join("\n") };
4340
+ }
4341
+ const installer = makeInstaller(skillLoader, ctx.projectRoot);
4342
+ try {
4343
+ await installer.uninstall(name, { global: isGlobal });
4344
+ return { message: `\u2713 Skill "${name}" uninstalled.` };
4345
+ } catch (err) {
4346
+ const msg = toErrorMessage(err);
4347
+ return { message: `\u2717 Uninstall failed: ${msg}` };
4348
+ }
4349
+ }
4350
+ };
4351
+ }
4352
+ function formatCount(n) {
4353
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
4354
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
4355
+ return String(n);
4356
+ }
4357
+ function truncate(s, max) {
4358
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
4359
+ }
4360
+
4361
+ // src/plugins/sync-plugin.ts
4362
+ import * as path8 from "node:path";
4363
+ function createSyncPlugin(opts) {
4364
+ let cloud = null;
4365
+ let configStore;
4366
+ let vault;
4367
+ return {
4368
+ name: "wstack-sync",
4369
+ version: "1.0.0",
4370
+ description: "GitHub cloud sync for prompts, skills, settings, memory and history",
4371
+ apiVersion: "^0.1",
4372
+ capabilities: { slashCommands: true },
4373
+ defaultConfig: {},
4374
+ setup(api) {
4375
+ const rawConfig = api.config;
4376
+ const paths = opts?.paths ?? rawConfig.paths;
4377
+ configStore = opts?.configStore ?? rawConfig.configStore;
4378
+ vault = opts?.vault ?? rawConfig.vault;
4379
+ if (!paths || !configStore || !paths.configDir) {
4380
+ api.log.warn("[sync] paths, configStore, or configDir not available \u2014 /sync disabled");
4381
+ return;
4382
+ }
4383
+ const syncConfigPath = paths.syncConfig ?? path8.join(paths.configDir, "sync.json");
4384
+ cloud = new CloudSync(
4385
+ paths,
4386
+ () => {
4387
+ const cfg = configStore?.get();
4388
+ return cfg.sync;
4389
+ },
4390
+ async (cfg) => {
4391
+ await persistSyncConfig(syncConfigPath, cfg, vault);
4392
+ configStore?.update({ sync: cfg });
4393
+ },
4394
+ () => path8.join(paths.configDir, "config.json")
4395
+ );
4396
+ void cloud.loadState();
4397
+ api.slashCommands.register(buildSyncCommand(cloud, configStore, vault, syncConfigPath));
4398
+ api.log.info("[sync] loaded \u2014 /sync available. Run /sync to get started.");
4399
+ },
4400
+ teardown(api) {
4401
+ api.slashCommands.unregister("sync");
4402
+ api.log.info("[sync] unloaded");
4403
+ },
4404
+ async health() {
4405
+ return { ok: true, message: "CloudSync ready" };
4406
+ }
4407
+ };
4408
+ }
4409
+ async function persistSyncConfig(syncConfigPath, cfg, vault) {
4410
+ let githubToken = cfg.githubToken;
4411
+ if (githubToken && !githubToken.startsWith("enc:")) {
4412
+ if (!vault) {
4413
+ throw new Error("Secure token storage is unavailable; refusing to persist a plaintext token.");
4414
+ }
4415
+ const encryptedToken = vault.encrypt(githubToken);
4416
+ if (encryptedToken === githubToken || !encryptedToken.startsWith("enc:")) {
4417
+ throw new Error("Secure token storage failed; refusing to persist a plaintext token.");
4418
+ }
4419
+ githubToken = encryptedToken;
4420
+ }
4421
+ await atomicWrite(syncConfigPath, JSON.stringify({ ...cfg, githubToken }, null, 2), { mode: 384 });
4422
+ }
4423
+ function buildSyncCommand(cloud, configStore, vault, syncConfigPath) {
4424
+ const setSyncConfig = async (cfg) => {
4425
+ await persistSyncConfig(syncConfigPath, cfg, vault);
4426
+ configStore.update({ sync: cfg });
4427
+ };
4428
+ return {
4429
+ name: "sync",
4430
+ description: "Cloud sync: /sync [status|enable|disable|push|pull|categories]",
4431
+ async run(args, _ctx) {
4432
+ const [verb, ...rest] = args.trim().split(/\s+/);
4433
+ const restJoined = rest.join(" ");
4434
+ switch (verb) {
4435
+ case "":
4436
+ case "status": {
4437
+ const msg = await cloud.status();
4438
+ return { message: msg };
4439
+ }
4440
+ case "enable": {
4441
+ const parts = restJoined.trim().split(/\s+/);
4442
+ if (parts.length < 2) {
4443
+ return {
4444
+ message: [
4445
+ "Usage: /sync enable owner/repo TOKEN [cat1 cat2 ...]",
4446
+ " e.g. /sync enable myname/wrongstack-data ghp_xxx",
4447
+ ` Categories (default: all): ${ALL_SYNC_CATEGORIES.join(", ")}`
4448
+ ].join("\n")
4449
+ };
4450
+ }
4451
+ const [repo, token, ...cats] = parts;
4452
+ if (!repo?.includes("/")) {
4453
+ return { message: 'Invalid repo format. Expected "owner/repo".' };
4454
+ }
4455
+ if (!vault) {
4456
+ return { message: "Secure token storage is unavailable; refusing to enable CloudSync." };
4457
+ }
4458
+ const invalidCategories = cats.filter(
4459
+ (cat) => !ALL_SYNC_CATEGORIES.includes(cat)
4460
+ );
4461
+ if (invalidCategories.length > 0) {
4462
+ return {
4463
+ message: `Unknown categories: ${invalidCategories.join(", ")}. Available: ${ALL_SYNC_CATEGORIES.join(", ")}`
4464
+ };
4465
+ }
4466
+ const syncConfig = {
4467
+ enabled: true,
4468
+ repo,
4469
+ githubToken: expectDefined(token),
4470
+ categories: cats.length > 0 ? cats : ALL_SYNC_CATEGORIES,
4471
+ lastSyncedAt: void 0
4472
+ };
4473
+ await setSyncConfig(syncConfig);
4474
+ await cloud.loadState();
4475
+ return {
4476
+ message: [
4477
+ `CloudSync enabled for ${repo}`,
4478
+ ` categories: ${syncConfig.categories.join(", ")}`,
4479
+ "",
4480
+ "Run `/sync push` to upload your data to GitHub."
4481
+ ].join("\n")
4482
+ };
4483
+ }
4484
+ case "disable": {
4485
+ const msg = await cloud.disable();
4486
+ return { message: msg };
4487
+ }
4488
+ case "push": {
4489
+ const cfg = configStore.get().sync;
4490
+ if (!cfg?.enabled) return { message: "CloudSync not enabled. Run `/sync enable`." };
4491
+ if (!cfg?.githubToken) return { message: "No GitHub token found. Run `/sync enable owner/repo TOKEN`." };
4492
+ let result;
4493
+ try {
4494
+ result = await cloud.push(cfg.githubToken);
4495
+ } catch (err) {
4496
+ return { message: `Push failed: ${toErrorMessage(err)}` };
4497
+ }
4498
+ if (result.ok) {
4499
+ try {
4500
+ await setSyncConfig({ ...cfg, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
4501
+ } catch (err) {
4502
+ return {
4503
+ message: `${result.message}
4504
+ Warning: sync completed, but metadata persistence failed: ${toErrorMessage(err)}`
4505
+ };
4506
+ }
4507
+ }
4508
+ return { message: result.message };
4509
+ }
4510
+ case "pull": {
4511
+ const cfg = configStore.get().sync;
4512
+ if (!cfg?.enabled) return { message: "CloudSync not enabled. Run `/sync enable`." };
4513
+ if (!cfg?.githubToken) return { message: "No GitHub token. Run `/sync enable owner/repo TOKEN`." };
4514
+ let result;
4515
+ try {
4516
+ result = await cloud.pull(cfg.githubToken);
4517
+ } catch (err) {
4518
+ return { message: `Pull failed: ${toErrorMessage(err)}` };
4519
+ }
4520
+ if (result.ok) {
4521
+ try {
4522
+ await setSyncConfig({ ...cfg, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
4523
+ } catch (err) {
4524
+ return {
4525
+ message: `${result.message}
4526
+ Warning: sync completed, but metadata persistence failed: ${toErrorMessage(err)}`
4527
+ };
4528
+ }
4529
+ }
4530
+ return { message: result.message };
4531
+ }
4532
+ case "categories": {
4533
+ const [action, ...catRest] = restJoined.trim().split(/\s+/);
4534
+ const cfg = configStore.get().sync;
4535
+ if (!cfg?.enabled) return { message: "CloudSync not enabled. Run `/sync enable`." };
4536
+ if (action === "list" || !action) {
4537
+ return {
4538
+ message: [
4539
+ `Synced categories: ${cfg.categories.join(", ")}`,
4540
+ `Available: ${ALL_SYNC_CATEGORIES.join(", ")}`,
4541
+ "Update: /sync categories add <name> | remove <name>"
4542
+ ].join("\n")
4543
+ };
4544
+ }
4545
+ if (action === "add") {
4546
+ const catName = catRest[0];
4547
+ if (!ALL_SYNC_CATEGORIES.includes(catName)) {
4548
+ return { message: `Unknown. Available: ${ALL_SYNC_CATEGORIES.join(", ")}` };
4549
+ }
4550
+ if (cfg.categories.includes(catName)) return { message: `"${catName}" already synced.` };
4551
+ await setSyncConfig({ ...cfg, categories: [...cfg.categories, catName] });
4552
+ return { message: `Added "${catName}" to sync categories.` };
4553
+ }
4554
+ if (action === "remove") {
4555
+ const catName = catRest[0];
4556
+ if (!cfg.categories.includes(catName)) return { message: `"${catName}" not in sync categories.` };
4557
+ await setSyncConfig({
4558
+ ...cfg,
4559
+ categories: cfg.categories.filter((c) => c !== catName)
4560
+ });
4561
+ return { message: `Removed "${catName}" from sync categories.` };
4562
+ }
4563
+ return { message: "Usage: /sync categories list | add <name> | remove <name>" };
4564
+ }
4565
+ default:
4566
+ return {
4567
+ message: [
4568
+ "/sync \u2014 Cloud Sync",
4569
+ "",
4570
+ " /sync status Show current sync status",
4571
+ " /sync enable owner/repo TOKEN Enable for a repo (TOKEN = fine-grained PAT)",
4572
+ " /sync disable Disable sync (keeps local data)",
4573
+ " /sync push Upload selected categories",
4574
+ " /sync pull Download from repo",
4575
+ " /sync categories list|add|remove Manage synced categories",
4576
+ "",
4577
+ `Categories: ${ALL_SYNC_CATEGORIES.join(", ")}`
4578
+ ].join("\n")
4579
+ };
4580
+ }
4581
+ }
4582
+ };
4583
+ }
4584
+
4585
+ export {
4586
+ KERNEL_API_VERSION,
4587
+ loadPlugins,
4588
+ unloadPlugins,
4589
+ DefaultPluginAPI,
4590
+ definePlugin,
4591
+ buildReviewerModelPool,
4592
+ selectRoundRobinReviewerAssignment,
4593
+ parseReviewSeverity,
4594
+ recordStartedReview,
4595
+ recordCompletedReview,
4596
+ emitReviewIfChanged,
4597
+ createAutoReviewPlugin,
4598
+ executeFindingCommand,
4599
+ transitionReport,
4600
+ resolveChimeraConfig,
4601
+ CHIMERA_REVIEW_PROMPT,
4602
+ createChimeraPlugin,
4603
+ createCloudConfigSyncPlugin,
4604
+ createPromptsPlugin,
4605
+ createSkillsPlugin,
4606
+ createSyncPlugin
4607
+ };
4608
+ //# sourceMappingURL=chunk-3AGWVSS7.js.map