@kynetic-ai/spec 0.1.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (540) hide show
  1. package/README.md +250 -17
  2. package/dist/acp/client.d.ts +18 -4
  3. package/dist/acp/client.d.ts.map +1 -1
  4. package/dist/acp/client.js +44 -26
  5. package/dist/acp/client.js.map +1 -1
  6. package/dist/acp/framing.d.ts +2 -2
  7. package/dist/acp/framing.d.ts.map +1 -1
  8. package/dist/acp/framing.js +37 -29
  9. package/dist/acp/framing.js.map +1 -1
  10. package/dist/acp/index.d.ts +6 -7
  11. package/dist/acp/index.d.ts.map +1 -1
  12. package/dist/acp/index.js +3 -3
  13. package/dist/acp/index.js.map +1 -1
  14. package/dist/acp/types.d.ts +5 -5
  15. package/dist/acp/types.d.ts.map +1 -1
  16. package/dist/acp/types.js +18 -18
  17. package/dist/acp/types.js.map +1 -1
  18. package/dist/agents/adapters.d.ts.map +1 -1
  19. package/dist/agents/adapters.js +24 -13
  20. package/dist/agents/adapters.js.map +1 -1
  21. package/dist/agents/index.d.ts +2 -2
  22. package/dist/agents/index.js +2 -2
  23. package/dist/agents/spawner.d.ts +4 -4
  24. package/dist/agents/spawner.d.ts.map +1 -1
  25. package/dist/agents/spawner.js +6 -6
  26. package/dist/agents/spawner.js.map +1 -1
  27. package/dist/cli/batch-context.d.ts +43 -0
  28. package/dist/cli/batch-context.d.ts.map +1 -0
  29. package/dist/cli/batch-context.js +93 -0
  30. package/dist/cli/batch-context.js.map +1 -0
  31. package/dist/cli/batch-exec.d.ts +107 -0
  32. package/dist/cli/batch-exec.d.ts.map +1 -0
  33. package/dist/cli/batch-exec.js +706 -0
  34. package/dist/cli/batch-exec.js.map +1 -0
  35. package/dist/cli/batch.d.ts +4 -2
  36. package/dist/cli/batch.d.ts.map +1 -1
  37. package/dist/cli/batch.js +15 -14
  38. package/dist/cli/batch.js.map +1 -1
  39. package/dist/cli/command-annotations.d.ts +23 -0
  40. package/dist/cli/command-annotations.d.ts.map +1 -0
  41. package/dist/cli/command-annotations.js +27 -0
  42. package/dist/cli/command-annotations.js.map +1 -0
  43. package/dist/cli/commands/agents.d.ts +46 -0
  44. package/dist/cli/commands/agents.d.ts.map +1 -0
  45. package/dist/cli/commands/agents.js +377 -0
  46. package/dist/cli/commands/agents.js.map +1 -0
  47. package/dist/cli/commands/batch.d.ts +20 -0
  48. package/dist/cli/commands/batch.d.ts.map +1 -0
  49. package/dist/cli/commands/batch.js +214 -0
  50. package/dist/cli/commands/batch.js.map +1 -0
  51. package/dist/cli/commands/clone-for-testing.d.ts +1 -1
  52. package/dist/cli/commands/clone-for-testing.d.ts.map +1 -1
  53. package/dist/cli/commands/clone-for-testing.js +37 -47
  54. package/dist/cli/commands/clone-for-testing.js.map +1 -1
  55. package/dist/cli/commands/derive.d.ts +1 -1
  56. package/dist/cli/commands/derive.d.ts.map +1 -1
  57. package/dist/cli/commands/derive.js +141 -88
  58. package/dist/cli/commands/derive.js.map +1 -1
  59. package/dist/cli/commands/doctor.d.ts +11 -0
  60. package/dist/cli/commands/doctor.d.ts.map +1 -0
  61. package/dist/cli/commands/doctor.js +152 -0
  62. package/dist/cli/commands/doctor.js.map +1 -0
  63. package/dist/cli/commands/export.d.ts +12 -0
  64. package/dist/cli/commands/export.d.ts.map +1 -0
  65. package/dist/cli/commands/export.js +134 -0
  66. package/dist/cli/commands/export.js.map +1 -0
  67. package/dist/cli/commands/help.d.ts +1 -1
  68. package/dist/cli/commands/help.d.ts.map +1 -1
  69. package/dist/cli/commands/help.js +163 -37
  70. package/dist/cli/commands/help.js.map +1 -1
  71. package/dist/cli/commands/inbox.d.ts +1 -1
  72. package/dist/cli/commands/inbox.d.ts.map +1 -1
  73. package/dist/cli/commands/inbox.js +178 -56
  74. package/dist/cli/commands/inbox.js.map +1 -1
  75. package/dist/cli/commands/index.d.ts +31 -19
  76. package/dist/cli/commands/index.d.ts.map +1 -1
  77. package/dist/cli/commands/index.js +31 -19
  78. package/dist/cli/commands/index.js.map +1 -1
  79. package/dist/cli/commands/init.d.ts +5 -1
  80. package/dist/cli/commands/init.d.ts.map +1 -1
  81. package/dist/cli/commands/init.js +108 -57
  82. package/dist/cli/commands/init.js.map +1 -1
  83. package/dist/cli/commands/item.d.ts +1 -1
  84. package/dist/cli/commands/item.d.ts.map +1 -1
  85. package/dist/cli/commands/item.js +557 -274
  86. package/dist/cli/commands/item.js.map +1 -1
  87. package/dist/cli/commands/link.d.ts +1 -1
  88. package/dist/cli/commands/link.d.ts.map +1 -1
  89. package/dist/cli/commands/link.js +55 -46
  90. package/dist/cli/commands/link.js.map +1 -1
  91. package/dist/cli/commands/log.d.ts +1 -1
  92. package/dist/cli/commands/log.d.ts.map +1 -1
  93. package/dist/cli/commands/log.js +58 -51
  94. package/dist/cli/commands/log.js.map +1 -1
  95. package/dist/cli/commands/merge-driver.d.ts +19 -0
  96. package/dist/cli/commands/merge-driver.d.ts.map +1 -0
  97. package/dist/cli/commands/merge-driver.js +398 -0
  98. package/dist/cli/commands/merge-driver.js.map +1 -0
  99. package/dist/cli/commands/meta.d.ts +1 -1
  100. package/dist/cli/commands/meta.d.ts.map +1 -1
  101. package/dist/cli/commands/meta.js +534 -399
  102. package/dist/cli/commands/meta.js.map +1 -1
  103. package/dist/cli/commands/module.d.ts +1 -1
  104. package/dist/cli/commands/module.d.ts.map +1 -1
  105. package/dist/cli/commands/module.js +30 -25
  106. package/dist/cli/commands/module.js.map +1 -1
  107. package/dist/cli/commands/plan-import.d.ts +11 -0
  108. package/dist/cli/commands/plan-import.d.ts.map +1 -0
  109. package/dist/cli/commands/plan-import.js +547 -0
  110. package/dist/cli/commands/plan-import.js.map +1 -0
  111. package/dist/cli/commands/plan.d.ts +10 -0
  112. package/dist/cli/commands/plan.d.ts.map +1 -0
  113. package/dist/cli/commands/plan.js +421 -0
  114. package/dist/cli/commands/plan.js.map +1 -0
  115. package/dist/cli/commands/ralph.d.ts +1 -1
  116. package/dist/cli/commands/ralph.d.ts.map +1 -1
  117. package/dist/cli/commands/ralph.js +1109 -170
  118. package/dist/cli/commands/ralph.js.map +1 -1
  119. package/dist/cli/commands/refs.d.ts +13 -0
  120. package/dist/cli/commands/refs.d.ts.map +1 -0
  121. package/dist/cli/commands/refs.js +283 -0
  122. package/dist/cli/commands/refs.js.map +1 -0
  123. package/dist/cli/commands/search.d.ts +1 -1
  124. package/dist/cli/commands/search.d.ts.map +1 -1
  125. package/dist/cli/commands/search.js +199 -37
  126. package/dist/cli/commands/search.js.map +1 -1
  127. package/dist/cli/commands/serve.d.ts +10 -0
  128. package/dist/cli/commands/serve.d.ts.map +1 -0
  129. package/dist/cli/commands/serve.js +491 -0
  130. package/dist/cli/commands/serve.js.map +1 -0
  131. package/dist/cli/commands/session.d.ts +25 -6
  132. package/dist/cli/commands/session.d.ts.map +1 -1
  133. package/dist/cli/commands/session.js +810 -127
  134. package/dist/cli/commands/session.js.map +1 -1
  135. package/dist/cli/commands/setup-seeding.d.ts +81 -0
  136. package/dist/cli/commands/setup-seeding.d.ts.map +1 -0
  137. package/dist/cli/commands/setup-seeding.js +292 -0
  138. package/dist/cli/commands/setup-seeding.js.map +1 -0
  139. package/dist/cli/commands/setup.d.ts +77 -3
  140. package/dist/cli/commands/setup.d.ts.map +1 -1
  141. package/dist/cli/commands/setup.js +1267 -274
  142. package/dist/cli/commands/setup.js.map +1 -1
  143. package/dist/cli/commands/shadow.d.ts +1 -1
  144. package/dist/cli/commands/shadow.d.ts.map +1 -1
  145. package/dist/cli/commands/shadow.js +70 -50
  146. package/dist/cli/commands/shadow.js.map +1 -1
  147. package/dist/cli/commands/skill-crud.d.ts +58 -0
  148. package/dist/cli/commands/skill-crud.d.ts.map +1 -0
  149. package/dist/cli/commands/skill-crud.js +753 -0
  150. package/dist/cli/commands/skill-crud.js.map +1 -0
  151. package/dist/cli/commands/skill-diff.d.ts +27 -0
  152. package/dist/cli/commands/skill-diff.d.ts.map +1 -0
  153. package/dist/cli/commands/skill-diff.js +840 -0
  154. package/dist/cli/commands/skill-diff.js.map +1 -0
  155. package/dist/cli/commands/skill-install.d.ts +56 -0
  156. package/dist/cli/commands/skill-install.d.ts.map +1 -0
  157. package/dist/cli/commands/skill-install.js +509 -0
  158. package/dist/cli/commands/skill-install.js.map +1 -0
  159. package/dist/cli/commands/skill.d.ts +20 -0
  160. package/dist/cli/commands/skill.d.ts.map +1 -0
  161. package/dist/cli/commands/skill.js +36 -0
  162. package/dist/cli/commands/skill.js.map +1 -0
  163. package/dist/cli/commands/task.d.ts +1 -1
  164. package/dist/cli/commands/task.d.ts.map +1 -1
  165. package/dist/cli/commands/task.js +584 -350
  166. package/dist/cli/commands/task.js.map +1 -1
  167. package/dist/cli/commands/tasks.d.ts +26 -1
  168. package/dist/cli/commands/tasks.d.ts.map +1 -1
  169. package/dist/cli/commands/tasks.js +225 -122
  170. package/dist/cli/commands/tasks.js.map +1 -1
  171. package/dist/cli/commands/trait.d.ts +1 -1
  172. package/dist/cli/commands/trait.d.ts.map +1 -1
  173. package/dist/cli/commands/trait.js +166 -101
  174. package/dist/cli/commands/trait.js.map +1 -1
  175. package/dist/cli/commands/triage.d.ts +7 -0
  176. package/dist/cli/commands/triage.d.ts.map +1 -0
  177. package/dist/cli/commands/triage.js +483 -0
  178. package/dist/cli/commands/triage.js.map +1 -0
  179. package/dist/cli/commands/util.d.ts +7 -0
  180. package/dist/cli/commands/util.d.ts.map +1 -0
  181. package/dist/cli/commands/util.js +30 -0
  182. package/dist/cli/commands/util.js.map +1 -0
  183. package/dist/cli/commands/validate.d.ts +1 -1
  184. package/dist/cli/commands/validate.d.ts.map +1 -1
  185. package/dist/cli/commands/validate.js +264 -83
  186. package/dist/cli/commands/validate.js.map +1 -1
  187. package/dist/cli/commands/workflow.d.ts +16 -0
  188. package/dist/cli/commands/workflow.d.ts.map +1 -0
  189. package/dist/cli/commands/workflow.js +851 -0
  190. package/dist/cli/commands/workflow.js.map +1 -0
  191. package/dist/cli/exit-codes.d.ts +7 -0
  192. package/dist/cli/exit-codes.d.ts.map +1 -1
  193. package/dist/cli/exit-codes.js +26 -18
  194. package/dist/cli/exit-codes.js.map +1 -1
  195. package/dist/cli/help/content.d.ts.map +1 -1
  196. package/dist/cli/help/content.js +86 -71
  197. package/dist/cli/help/content.js.map +1 -1
  198. package/dist/cli/index.d.ts +1 -1
  199. package/dist/cli/index.d.ts.map +1 -1
  200. package/dist/cli/index.js +131 -19
  201. package/dist/cli/index.js.map +1 -1
  202. package/dist/cli/introspection.d.ts +6 -2
  203. package/dist/cli/introspection.d.ts.map +1 -1
  204. package/dist/cli/introspection.js +11 -8
  205. package/dist/cli/introspection.js.map +1 -1
  206. package/dist/cli/output.d.ts +64 -4
  207. package/dist/cli/output.d.ts.map +1 -1
  208. package/dist/cli/output.js +237 -85
  209. package/dist/cli/output.js.map +1 -1
  210. package/dist/cli/parse-utils.d.ts +21 -0
  211. package/dist/cli/parse-utils.d.ts.map +1 -0
  212. package/dist/cli/parse-utils.js +32 -0
  213. package/dist/cli/parse-utils.js.map +1 -0
  214. package/dist/cli/pid-utils.d.ts +72 -0
  215. package/dist/cli/pid-utils.d.ts.map +1 -0
  216. package/dist/cli/pid-utils.js +174 -0
  217. package/dist/cli/pid-utils.js.map +1 -0
  218. package/dist/cli/suggest.d.ts.map +1 -1
  219. package/dist/cli/suggest.js +1 -2
  220. package/dist/cli/suggest.js.map +1 -1
  221. package/dist/cli/validators.d.ts +43 -0
  222. package/dist/cli/validators.d.ts.map +1 -0
  223. package/dist/cli/validators.js +84 -0
  224. package/dist/cli/validators.js.map +1 -0
  225. package/dist/daemon/index.ts +52 -0
  226. package/dist/daemon/middleware/project-context.ts +126 -0
  227. package/dist/daemon/pid.ts +179 -0
  228. package/dist/daemon/project-context.ts +343 -0
  229. package/dist/daemon/routes/inbox.ts +164 -0
  230. package/dist/daemon/routes/items.ts +322 -0
  231. package/dist/daemon/routes/meta.ts +118 -0
  232. package/dist/daemon/routes/projects.ts +162 -0
  233. package/dist/daemon/routes/tasks.ts +327 -0
  234. package/dist/daemon/routes/triage.ts +402 -0
  235. package/dist/daemon/routes/validation.ts +248 -0
  236. package/dist/daemon/server.ts +408 -0
  237. package/dist/daemon/watcher.ts +195 -0
  238. package/dist/daemon/websocket/handler.ts +138 -0
  239. package/dist/daemon/websocket/heartbeat.ts +71 -0
  240. package/dist/daemon/websocket/pubsub.ts +125 -0
  241. package/dist/daemon/websocket/types.ts +66 -0
  242. package/dist/export/html.d.ts +19 -0
  243. package/dist/export/html.d.ts.map +1 -0
  244. package/dist/export/html.js +239 -0
  245. package/dist/export/html.js.map +1 -0
  246. package/dist/export/index.d.ts +10 -0
  247. package/dist/export/index.d.ts.map +1 -0
  248. package/dist/export/index.js +10 -0
  249. package/dist/export/index.js.map +1 -0
  250. package/dist/export/json.d.ts +24 -0
  251. package/dist/export/json.d.ts.map +1 -0
  252. package/dist/export/json.js +198 -0
  253. package/dist/export/json.js.map +1 -0
  254. package/dist/export/triage.d.ts +51 -0
  255. package/dist/export/triage.d.ts.map +1 -0
  256. package/dist/export/triage.js +83 -0
  257. package/dist/export/triage.js.map +1 -0
  258. package/dist/export/types.d.ts +122 -0
  259. package/dist/export/types.d.ts.map +1 -0
  260. package/dist/export/types.js +9 -0
  261. package/dist/export/types.js.map +1 -0
  262. package/dist/index.d.ts +2 -2
  263. package/dist/index.js +2 -2
  264. package/dist/lib/claude-plugin-registry.d.ts +66 -0
  265. package/dist/lib/claude-plugin-registry.d.ts.map +1 -0
  266. package/dist/lib/claude-plugin-registry.js +318 -0
  267. package/dist/lib/claude-plugin-registry.js.map +1 -0
  268. package/dist/merge/arrays.d.ts +87 -0
  269. package/dist/merge/arrays.d.ts.map +1 -0
  270. package/dist/merge/arrays.js +164 -0
  271. package/dist/merge/arrays.js.map +1 -0
  272. package/dist/merge/file-type.d.ts +32 -0
  273. package/dist/merge/file-type.d.ts.map +1 -0
  274. package/dist/merge/file-type.js +70 -0
  275. package/dist/merge/file-type.js.map +1 -0
  276. package/dist/merge/index.d.ts +14 -0
  277. package/dist/merge/index.d.ts.map +1 -0
  278. package/dist/merge/index.js +11 -0
  279. package/dist/merge/index.js.map +1 -0
  280. package/dist/merge/objects.d.ts +46 -0
  281. package/dist/merge/objects.d.ts.map +1 -0
  282. package/dist/merge/objects.js +193 -0
  283. package/dist/merge/objects.js.map +1 -0
  284. package/dist/merge/parse.d.ts +23 -0
  285. package/dist/merge/parse.d.ts.map +1 -0
  286. package/dist/merge/parse.js +78 -0
  287. package/dist/merge/parse.js.map +1 -0
  288. package/dist/merge/resolve.d.ts +66 -0
  289. package/dist/merge/resolve.d.ts.map +1 -0
  290. package/dist/merge/resolve.js +189 -0
  291. package/dist/merge/resolve.js.map +1 -0
  292. package/dist/merge/types.d.ts +82 -0
  293. package/dist/merge/types.d.ts.map +1 -0
  294. package/dist/merge/types.js +8 -0
  295. package/dist/merge/types.js.map +1 -0
  296. package/dist/parser/agent-data-sections.d.ts +53 -0
  297. package/dist/parser/agent-data-sections.d.ts.map +1 -0
  298. package/dist/parser/agent-data-sections.js +118 -0
  299. package/dist/parser/agent-data-sections.js.map +1 -0
  300. package/dist/parser/alignment.d.ts +4 -4
  301. package/dist/parser/alignment.d.ts.map +1 -1
  302. package/dist/parser/alignment.js +27 -22
  303. package/dist/parser/alignment.js.map +1 -1
  304. package/dist/parser/assess.d.ts +5 -5
  305. package/dist/parser/assess.d.ts.map +1 -1
  306. package/dist/parser/assess.js +36 -32
  307. package/dist/parser/assess.js.map +1 -1
  308. package/dist/parser/config.d.ts +457 -0
  309. package/dist/parser/config.d.ts.map +1 -0
  310. package/dist/parser/config.js +373 -0
  311. package/dist/parser/config.js.map +1 -0
  312. package/dist/parser/convention-validation.d.ts +1 -1
  313. package/dist/parser/convention-validation.d.ts.map +1 -1
  314. package/dist/parser/convention-validation.js +21 -16
  315. package/dist/parser/convention-validation.js.map +1 -1
  316. package/dist/parser/coverage-cache.d.ts +49 -0
  317. package/dist/parser/coverage-cache.d.ts.map +1 -0
  318. package/dist/parser/coverage-cache.js +123 -0
  319. package/dist/parser/coverage-cache.js.map +1 -0
  320. package/dist/parser/daemon-status.d.ts +37 -0
  321. package/dist/parser/daemon-status.d.ts.map +1 -0
  322. package/dist/parser/daemon-status.js +67 -0
  323. package/dist/parser/daemon-status.js.map +1 -0
  324. package/dist/parser/doctor.d.ts +107 -0
  325. package/dist/parser/doctor.d.ts.map +1 -0
  326. package/dist/parser/doctor.js +366 -0
  327. package/dist/parser/doctor.js.map +1 -0
  328. package/dist/parser/fix.d.ts +1 -1
  329. package/dist/parser/fix.d.ts.map +1 -1
  330. package/dist/parser/fix.js +31 -27
  331. package/dist/parser/fix.js.map +1 -1
  332. package/dist/parser/index.d.ts +16 -11
  333. package/dist/parser/index.d.ts.map +1 -1
  334. package/dist/parser/index.js +16 -11
  335. package/dist/parser/index.js.map +1 -1
  336. package/dist/parser/items.d.ts +8 -2
  337. package/dist/parser/items.d.ts.map +1 -1
  338. package/dist/parser/items.js +71 -35
  339. package/dist/parser/items.js.map +1 -1
  340. package/dist/parser/meta.d.ts +167 -9
  341. package/dist/parser/meta.d.ts.map +1 -1
  342. package/dist/parser/meta.js +379 -46
  343. package/dist/parser/meta.js.map +1 -1
  344. package/dist/parser/plan-document.d.ts +197 -0
  345. package/dist/parser/plan-document.d.ts.map +1 -0
  346. package/dist/parser/plan-document.js +341 -0
  347. package/dist/parser/plan-document.js.map +1 -0
  348. package/dist/parser/plans.d.ts +59 -0
  349. package/dist/parser/plans.d.ts.map +1 -0
  350. package/dist/parser/plans.js +239 -0
  351. package/dist/parser/plans.js.map +1 -0
  352. package/dist/parser/refs.d.ts +22 -9
  353. package/dist/parser/refs.d.ts.map +1 -1
  354. package/dist/parser/refs.js +102 -50
  355. package/dist/parser/refs.js.map +1 -1
  356. package/dist/parser/setup-status.d.ts +71 -0
  357. package/dist/parser/setup-status.d.ts.map +1 -0
  358. package/dist/parser/setup-status.js +269 -0
  359. package/dist/parser/setup-status.js.map +1 -0
  360. package/dist/parser/shadow.d.ts +150 -19
  361. package/dist/parser/shadow.d.ts.map +1 -1
  362. package/dist/parser/shadow.js +548 -187
  363. package/dist/parser/shadow.js.map +1 -1
  364. package/dist/parser/skill-render.d.ts +317 -0
  365. package/dist/parser/skill-render.d.ts.map +1 -0
  366. package/dist/parser/skill-render.js +943 -0
  367. package/dist/parser/skill-render.js.map +1 -0
  368. package/dist/parser/traits.d.ts +3 -3
  369. package/dist/parser/traits.d.ts.map +1 -1
  370. package/dist/parser/traits.js +2 -2
  371. package/dist/parser/traits.js.map +1 -1
  372. package/dist/parser/validate-skills.d.ts +32 -0
  373. package/dist/parser/validate-skills.d.ts.map +1 -0
  374. package/dist/parser/validate-skills.js +202 -0
  375. package/dist/parser/validate-skills.js.map +1 -0
  376. package/dist/parser/validate.d.ts +45 -3
  377. package/dist/parser/validate.d.ts.map +1 -1
  378. package/dist/parser/validate.js +622 -105
  379. package/dist/parser/validate.js.map +1 -1
  380. package/dist/parser/yaml.d.ts +83 -19
  381. package/dist/parser/yaml.d.ts.map +1 -1
  382. package/dist/parser/yaml.js +478 -173
  383. package/dist/parser/yaml.js.map +1 -1
  384. package/dist/ralph/cli-renderer.d.ts +8 -1
  385. package/dist/ralph/cli-renderer.d.ts.map +1 -1
  386. package/dist/ralph/cli-renderer.js +105 -34
  387. package/dist/ralph/cli-renderer.js.map +1 -1
  388. package/dist/ralph/events.d.ts +10 -10
  389. package/dist/ralph/events.d.ts.map +1 -1
  390. package/dist/ralph/events.js +301 -98
  391. package/dist/ralph/events.js.map +1 -1
  392. package/dist/ralph/index.d.ts +5 -2
  393. package/dist/ralph/index.d.ts.map +1 -1
  394. package/dist/ralph/index.js +9 -3
  395. package/dist/ralph/index.js.map +1 -1
  396. package/dist/ralph/loop-errors.d.ts +83 -0
  397. package/dist/ralph/loop-errors.d.ts.map +1 -0
  398. package/dist/ralph/loop-errors.js +150 -0
  399. package/dist/ralph/loop-errors.js.map +1 -0
  400. package/dist/ralph/subagent.d.ts +94 -0
  401. package/dist/ralph/subagent.d.ts.map +1 -0
  402. package/dist/ralph/subagent.js +193 -0
  403. package/dist/ralph/subagent.js.map +1 -0
  404. package/dist/ralph/wrap-up.d.ts +125 -0
  405. package/dist/ralph/wrap-up.d.ts.map +1 -0
  406. package/dist/ralph/wrap-up.js +270 -0
  407. package/dist/ralph/wrap-up.js.map +1 -0
  408. package/dist/schema/batch.d.ts +97 -0
  409. package/dist/schema/batch.d.ts.map +1 -0
  410. package/dist/schema/batch.js +24 -0
  411. package/dist/schema/batch.js.map +1 -0
  412. package/dist/schema/common.d.ts +8 -2
  413. package/dist/schema/common.d.ts.map +1 -1
  414. package/dist/schema/common.js +42 -31
  415. package/dist/schema/common.js.map +1 -1
  416. package/dist/schema/inbox.d.ts +12 -12
  417. package/dist/schema/inbox.js +4 -4
  418. package/dist/schema/inbox.js.map +1 -1
  419. package/dist/schema/index.d.ts +8 -5
  420. package/dist/schema/index.d.ts.map +1 -1
  421. package/dist/schema/index.js +8 -5
  422. package/dist/schema/index.js.map +1 -1
  423. package/dist/schema/meta.d.ts +1454 -27
  424. package/dist/schema/meta.d.ts.map +1 -1
  425. package/dist/schema/meta.js +198 -21
  426. package/dist/schema/meta.js.map +1 -1
  427. package/dist/schema/plan.d.ts +285 -0
  428. package/dist/schema/plan.d.ts.map +1 -0
  429. package/dist/schema/plan.js +81 -0
  430. package/dist/schema/plan.js.map +1 -0
  431. package/dist/schema/spec.d.ts +72 -33
  432. package/dist/schema/spec.d.ts.map +1 -1
  433. package/dist/schema/spec.js +22 -9
  434. package/dist/schema/spec.js.map +1 -1
  435. package/dist/schema/task.d.ts +172 -161
  436. package/dist/schema/task.d.ts.map +1 -1
  437. package/dist/schema/task.js +21 -12
  438. package/dist/schema/task.js.map +1 -1
  439. package/dist/schema/triage.d.ts +266 -0
  440. package/dist/schema/triage.d.ts.map +1 -0
  441. package/dist/schema/triage.js +134 -0
  442. package/dist/schema/triage.js.map +1 -0
  443. package/dist/sessions/index.d.ts +2 -2
  444. package/dist/sessions/index.d.ts.map +1 -1
  445. package/dist/sessions/index.js +3 -3
  446. package/dist/sessions/index.js.map +1 -1
  447. package/dist/sessions/store.d.ts +241 -1
  448. package/dist/sessions/store.d.ts.map +1 -1
  449. package/dist/sessions/store.js +810 -31
  450. package/dist/sessions/store.js.map +1 -1
  451. package/dist/sessions/types.d.ts +10 -10
  452. package/dist/sessions/types.d.ts.map +1 -1
  453. package/dist/sessions/types.js +10 -9
  454. package/dist/sessions/types.js.map +1 -1
  455. package/dist/strings/errors.d.ts +55 -0
  456. package/dist/strings/errors.d.ts.map +1 -1
  457. package/dist/strings/errors.js +138 -106
  458. package/dist/strings/errors.js.map +1 -1
  459. package/dist/strings/guidance.d.ts.map +1 -1
  460. package/dist/strings/guidance.js +16 -16
  461. package/dist/strings/guidance.js.map +1 -1
  462. package/dist/strings/index.d.ts +4 -4
  463. package/dist/strings/index.d.ts.map +1 -1
  464. package/dist/strings/index.js +4 -4
  465. package/dist/strings/index.js.map +1 -1
  466. package/dist/strings/labels.d.ts +4 -0
  467. package/dist/strings/labels.d.ts.map +1 -1
  468. package/dist/strings/labels.js +45 -41
  469. package/dist/strings/labels.js.map +1 -1
  470. package/dist/strings/validation.d.ts.map +1 -1
  471. package/dist/strings/validation.js +71 -71
  472. package/dist/strings/validation.js.map +1 -1
  473. package/dist/triage/actions.d.ts +27 -0
  474. package/dist/triage/actions.d.ts.map +1 -0
  475. package/dist/triage/actions.js +95 -0
  476. package/dist/triage/actions.js.map +1 -0
  477. package/dist/triage/constants.d.ts +6 -0
  478. package/dist/triage/constants.d.ts.map +1 -0
  479. package/dist/triage/constants.js +7 -0
  480. package/dist/triage/constants.js.map +1 -0
  481. package/dist/triage/index.d.ts +3 -0
  482. package/dist/triage/index.d.ts.map +1 -0
  483. package/dist/triage/index.js +3 -0
  484. package/dist/triage/index.js.map +1 -0
  485. package/dist/utils/commit.d.ts +1 -1
  486. package/dist/utils/commit.d.ts.map +1 -1
  487. package/dist/utils/commit.js +28 -26
  488. package/dist/utils/commit.js.map +1 -1
  489. package/dist/utils/git.d.ts +1 -1
  490. package/dist/utils/git.d.ts.map +1 -1
  491. package/dist/utils/git.js +40 -38
  492. package/dist/utils/git.js.map +1 -1
  493. package/dist/utils/grep.js +11 -11
  494. package/dist/utils/grep.js.map +1 -1
  495. package/dist/utils/index.d.ts +7 -7
  496. package/dist/utils/index.d.ts.map +1 -1
  497. package/dist/utils/index.js +4 -4
  498. package/dist/utils/index.js.map +1 -1
  499. package/dist/utils/time.d.ts.map +1 -1
  500. package/dist/utils/time.js +10 -10
  501. package/dist/utils/time.js.map +1 -1
  502. package/package.json +28 -5
  503. package/plugin/.claude-plugin/marketplace.json +17 -0
  504. package/plugin/.claude-plugin/plugin.json +5 -0
  505. package/plugin/plugins/kspec/skills/create-workflow/SKILL.md +235 -0
  506. package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
  507. package/plugin/plugins/kspec/skills/observations/SKILL.md +143 -0
  508. package/plugin/plugins/kspec/skills/plan/SKILL.md +343 -0
  509. package/plugin/plugins/kspec/skills/reflect/SKILL.md +161 -0
  510. package/plugin/plugins/kspec/skills/review/SKILL.md +193 -0
  511. package/plugin/plugins/kspec/skills/task-work/SKILL.md +303 -0
  512. package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
  513. package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
  514. package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
  515. package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
  516. package/plugin/plugins/kspec/skills/triage-automation/SKILL.md +140 -0
  517. package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +232 -0
  518. package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +340 -0
  519. package/templates/agents-sections/01-quick-start.md +22 -0
  520. package/templates/agents-sections/02-shadow-branch.md +34 -0
  521. package/templates/agents-sections/03-task-lifecycle.md +48 -0
  522. package/templates/agents-sections/04-pr-workflow.md +17 -0
  523. package/templates/agents-sections/05-commit-convention.md +27 -0
  524. package/templates/agents-sections/06-ralph-loop.md +45 -0
  525. package/templates/hooks/pre-commit +34 -0
  526. package/templates/skills/create-workflow/SKILL.md +228 -0
  527. package/templates/skills/help/SKILL.md +37 -0
  528. package/templates/skills/manifest.yaml +60 -0
  529. package/templates/skills/observations/SKILL.md +137 -0
  530. package/templates/skills/plan/SKILL.md +336 -0
  531. package/templates/skills/reflect/SKILL.md +155 -0
  532. package/templates/skills/review/SKILL.md +186 -0
  533. package/templates/skills/task-work/SKILL.md +296 -0
  534. package/templates/skills/triage/SKILL.md +199 -0
  535. package/templates/skills/triage/docs/automation.md +120 -0
  536. package/templates/skills/triage/docs/inbox.md +144 -0
  537. package/templates/skills/triage/docs/observations.md +85 -0
  538. package/templates/skills/triage-automation/SKILL.md +134 -0
  539. package/templates/skills/triage-inbox/SKILL.md +225 -0
  540. package/templates/skills/writing-specs/SKILL.md +333 -0
@@ -1,11 +1,45 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import * as os from 'node:os';
4
- import * as readline from 'node:readline/promises';
5
- import { success, error, warn } from '../output.js';
6
- import { errors } from '../../strings/index.js';
7
- import { getShadowStatus, repairShadow, getGitRoot, SHADOW_BRANCH_NAME, } from '../../parser/shadow.js';
8
- import { EXIT_CODES } from '../exit-codes.js';
1
+ /**
2
+ * Enhanced setup command for kspec agent integration.
3
+ *
4
+ * Orchestrates the full onboarding pipeline:
5
+ * - Detect agent environment (Claude Code, Cline, etc.)
6
+ * - Install hooks (UserPromptSubmit, Stop, PreToolUse guards)
7
+ * - Render skills from .kspec/skills/ to .claude/skills/
8
+ * - Generate kspec-agents.md
9
+ *
10
+ * AC: @enhanced-setup ac-1 - summary displayed listing each step performed
11
+ * AC: @enhanced-setup ac-2 - all hook entries (UserPromptSubmit, Stop, PreToolUse) present
12
+ * AC: @enhanced-setup ac-3 - rendered skill files exist for each skill targeting claude-code
13
+ * AC: @enhanced-setup ac-4 - kspec-agents.md exists after setup
14
+ * AC: @enhanced-setup ac-5 - --skip-skills flag skips skill rendering
15
+ * AC: @enhanced-setup ac-6 - --dry-run displays planned actions without changes
16
+ * AC: @enhanced-setup ac-7 - --status reports current state including agent detected
17
+ * AC: @enhanced-setup ac-8 - --status shows hooks status, skills rendered count, agents.md freshness
18
+ * AC: @enhanced-setup ac-9 - skills referenced by ralph (task-work, reflect) are present
19
+ */
20
+ import * as fs from "node:fs/promises";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+ import * as readline from "node:readline/promises";
24
+ import chalk from "chalk";
25
+ import { getGitRoot, getShadowStatus, repairShadow, SHADOW_BRANCH_NAME, } from "../../parser/shadow.js";
26
+ import { errors } from "../../strings/index.js";
27
+ import { EXIT_CODES } from "../exit-codes.js";
28
+ import { error, output, success, warn } from "../output.js";
29
+ /**
30
+ * Log a message at debug level (only when KSPEC_DEBUG=1)
31
+ * AC: @setup-pipeline-unification ac-4
32
+ */
33
+ function debugLog(message, detail) {
34
+ if (process.env.KSPEC_DEBUG === "1") {
35
+ if (detail) {
36
+ console.error(`[DEBUG] setup: ${message}`, detail);
37
+ }
38
+ else {
39
+ console.error(`[DEBUG] setup: ${message}`);
40
+ }
41
+ }
42
+ }
9
43
  /**
10
44
  * Detect which agent environment we're running in.
11
45
  * Returns the detected agent type and confidence level.
@@ -17,23 +51,29 @@ export function detectAgent() {
17
51
  // CLAUDECODE=1 is set in CLI sessions
18
52
  // CLAUDE_CODE_ENTRYPOINT indicates entry point (cli, etc.)
19
53
  // CLAUDE_PROJECT_DIR is set in some contexts
20
- if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_PROJECT_DIR) {
54
+ if (process.env.CLAUDECODE === "1" ||
55
+ process.env.CLAUDE_CODE_ENTRYPOINT ||
56
+ process.env.CLAUDE_PROJECT_DIR) {
21
57
  return {
22
- type: 'claude-code',
23
- confidence: 'high',
24
- configPath: path.join(os.homedir(), '.claude', 'settings.json'),
58
+ type: "claude-code",
59
+ confidence: "high",
60
+ configPath: path.join(os.homedir(), ".claude", "settings.json"),
25
61
  envVars: {
26
62
  ...(process.env.CLAUDECODE && { CLAUDECODE: process.env.CLAUDECODE }),
27
- ...(process.env.CLAUDE_CODE_ENTRYPOINT && { CLAUDE_CODE_ENTRYPOINT: process.env.CLAUDE_CODE_ENTRYPOINT }),
28
- ...(process.env.CLAUDE_PROJECT_DIR && { CLAUDE_PROJECT_DIR: process.env.CLAUDE_PROJECT_DIR }),
63
+ ...(process.env.CLAUDE_CODE_ENTRYPOINT && {
64
+ CLAUDE_CODE_ENTRYPOINT: process.env.CLAUDE_CODE_ENTRYPOINT,
65
+ }),
66
+ ...(process.env.CLAUDE_PROJECT_DIR && {
67
+ CLAUDE_PROJECT_DIR: process.env.CLAUDE_PROJECT_DIR,
68
+ }),
29
69
  },
30
70
  };
31
71
  }
32
72
  // Cline: CLINE_ACTIVE is set when running in Cline terminal
33
73
  if (process.env.CLINE_ACTIVE) {
34
74
  return {
35
- type: 'cline',
36
- confidence: 'high',
75
+ type: "cline",
76
+ confidence: "high",
37
77
  // Cline uses VS Code settings, but env vars should be in shell profile
38
78
  configPath: undefined,
39
79
  envVars: { CLINE_ACTIVE: process.env.CLINE_ACTIVE },
@@ -42,63 +82,64 @@ export function detectAgent() {
42
82
  // GitHub Copilot CLI: Check for copilot-specific markers
43
83
  if (process.env.COPILOT_MODEL || process.env.GH_TOKEN) {
44
84
  return {
45
- type: 'copilot-cli',
46
- confidence: 'medium',
47
- configPath: path.join(os.homedir(), '.copilot', 'config.json'),
85
+ type: "copilot-cli",
86
+ confidence: "medium",
87
+ configPath: path.join(os.homedir(), ".copilot", "config.json"),
48
88
  };
49
89
  }
50
90
  // Aider: Check for AIDER_* env vars
51
91
  if (process.env.AIDER_MODEL || process.env.AIDER_DARK_MODE !== undefined) {
52
92
  return {
53
- type: 'aider',
54
- confidence: 'high',
55
- configPath: path.join(os.homedir(), '.aider.conf.yml'),
93
+ type: "aider",
94
+ confidence: "high",
95
+ configPath: path.join(os.homedir(), ".aider.conf.yml"),
56
96
  };
57
97
  }
58
98
  // OpenCode: Check for OPENCODE_* env vars
59
99
  if (process.env.OPENCODE_CONFIG_DIR || process.env.OPENCODE_CONFIG) {
60
100
  return {
61
- type: 'opencode',
62
- confidence: 'high',
63
- configPath: process.env.OPENCODE_CONFIG || path.join(os.homedir(), '.config', 'opencode', 'opencode.json'),
101
+ type: "opencode",
102
+ confidence: "high",
103
+ configPath: process.env.OPENCODE_CONFIG ||
104
+ path.join(os.homedir(), ".config", "opencode", "opencode.json"),
64
105
  };
65
106
  }
66
107
  // Gemini CLI: GEMINI_CLI=1 is set when running in Gemini CLI
67
- if (process.env.GEMINI_CLI === '1') {
108
+ if (process.env.GEMINI_CLI === "1") {
68
109
  return {
69
- type: 'gemini-cli',
70
- confidence: 'high',
71
- configPath: path.join(os.homedir(), '.gemini', 'settings.json'),
72
- envVars: { GEMINI_CLI: '1' },
110
+ type: "gemini-cli",
111
+ confidence: "high",
112
+ configPath: path.join(os.homedir(), ".gemini", "settings.json"),
113
+ envVars: { GEMINI_CLI: "1" },
73
114
  };
74
115
  }
75
116
  // Codex CLI: CODEX_SANDBOX is set when running in sandbox
76
117
  if (process.env.CODEX_SANDBOX) {
77
118
  return {
78
- type: 'codex-cli',
79
- confidence: 'high',
80
- configPath: path.join(os.homedir(), '.codex', 'config.toml'),
119
+ type: "codex-cli",
120
+ confidence: "high",
121
+ configPath: path.join(os.homedir(), ".codex", "config.toml"),
81
122
  envVars: { CODEX_SANDBOX: process.env.CODEX_SANDBOX },
82
123
  };
83
124
  }
84
125
  // Amp (Sourcegraph): Check for AMP_API_KEY or AMP_TOOLBOX
85
126
  if (process.env.AMP_API_KEY || process.env.AMP_TOOLBOX) {
86
127
  return {
87
- type: 'amp',
88
- confidence: 'medium',
89
- configPath: path.join(os.homedir(), '.config', 'amp', 'settings.json'),
128
+ type: "amp",
129
+ confidence: "medium",
130
+ configPath: path.join(os.homedir(), ".config", "amp", "settings.json"),
90
131
  };
91
132
  }
92
133
  return {
93
- type: 'unknown',
94
- confidence: 'low',
134
+ type: "unknown",
135
+ confidence: "low",
95
136
  };
96
137
  }
97
138
  /**
98
139
  * Install KSPEC_AUTHOR config for Claude Code (global settings)
99
140
  */
100
141
  async function installClaudeCodeConfig(author) {
101
- const configPath = path.join(os.homedir(), '.claude', 'settings.json');
142
+ const configPath = path.join(os.homedir(), ".claude", "settings.json");
102
143
  const configDir = path.dirname(configPath);
103
144
  try {
104
145
  // Ensure directory exists
@@ -106,56 +147,255 @@ async function installClaudeCodeConfig(author) {
106
147
  // Read existing config or start fresh
107
148
  let config = {};
108
149
  try {
109
- const existing = await fs.readFile(configPath, 'utf-8');
150
+ const existing = await fs.readFile(configPath, "utf-8");
110
151
  config = JSON.parse(existing);
111
152
  }
112
- catch {
113
- // File doesn't exist or invalid JSON, start fresh
153
+ catch (err) {
154
+ debugLog("No existing Claude Code config, starting fresh", err);
114
155
  }
115
156
  // Merge env settings
116
157
  const env = config.env || {};
117
158
  env.KSPEC_AUTHOR = author;
118
159
  config.env = env;
119
160
  // Write back
120
- await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
161
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
121
162
  return true;
122
163
  }
123
164
  catch (err) {
165
+ debugLog("Failed to install Claude Code config", err);
124
166
  return false;
125
167
  }
126
168
  }
169
+ /**
170
+ * PreToolUse guard hook scripts
171
+ * These are the shell scripts that will be installed to .claude/hooks/
172
+ */
173
+ const GUARD_SCRIPTS = {
174
+ "kspec-worktree-guard.sh": `#!/bin/bash
175
+ # Guard against dangerous git operations in .kspec worktree
176
+ #
177
+ # This hook prevents accidentally creating branches or switching
178
+ # branches in the .kspec worktree, which should always stay on kspec-meta.
179
+
180
+ # Read the tool input from stdin
181
+ INPUT=$(cat)
182
+
183
+ # Extract the command from the JSON input
184
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
185
+
186
+ # If no command, allow (not a Bash tool call)
187
+ if [ -z "$COMMAND" ]; then
188
+ echo '{"decision": "allow"}'
189
+ exit 0
190
+ fi
191
+
192
+ # Two views of the command for safe matching:
193
+ # 1. UNQUOTED: remove quote chars (keeps content) to catch split-quote
194
+ # bypasses like: git "reset" --hard → git reset --hard
195
+ # 2. STRIPPED: remove entire quoted strings to ignore patterns in args
196
+ # like: echo "git reset" → echo
197
+ UNQUOTED=$(echo "$COMMAND" | sed 's/["\\x27]//g')
198
+ STRIPPED=$(echo "$COMMAND" | sed -e "s/\\x27[^\\x27]*\\x27//g" -e 's/"[^"]*"//g')
199
+ # First command word (handles leading whitespace)
200
+ FIRST_CMD=$(echo "$COMMAND" | sed 's/^[[:space:]]*//' | cut -d' ' -f1)
201
+
202
+ # Block deleting kspec-meta from anywhere (check unquoted to catch bypasses)
203
+ if [[ "$UNQUOTED" == *"git branch -d kspec-meta"* || "$UNQUOTED" == *"git branch -D kspec-meta"* ]]; then
204
+ cat <<EOF
205
+ {
206
+ "decision": "block",
207
+ "reason": "[kspec-worktree-guard] BLOCKED: Cannot delete kspec-meta branch. This is the main branch for the .kspec worktree."
208
+ }
209
+ EOF
210
+ exit 0
211
+ fi
212
+
213
+ # Get cwd from hook input (not pwd - hook runs in different context)
214
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
215
+ IN_KSPEC=false
216
+
217
+ if [[ "$CWD" == *"/.kspec"* || "$CWD" == *"/.kspec" ]]; then
218
+ IN_KSPEC=true
219
+ fi
220
+
221
+ # Also check if command contains cd to .kspec
222
+ if [[ "$COMMAND" == *"cd "*".kspec"* || "$COMMAND" == *"cd .kspec"* ]]; then
223
+ IN_KSPEC=true
224
+ fi
225
+
226
+ if [ "$IN_KSPEC" = false ]; then
227
+ echo '{"decision": "allow"}'
228
+ exit 0
229
+ fi
230
+
231
+ # Dangerous patterns in .kspec (branch creation/modification/history rewriting)
232
+ # Note: "git checkout kspec-meta" is safe and allowed
233
+ DANGEROUS_PATTERNS=(
234
+ # Branch creation
235
+ "git checkout -b"
236
+ "git checkout -B"
237
+ "git branch -c"
238
+ "git branch -C"
239
+ "git branch -m"
240
+ "git branch -M"
241
+ "git switch -c"
242
+ "git switch -C"
243
+ "git switch --create"
244
+ # History rewriting - these can cause conflicts with active sessions
245
+ "git reset"
246
+ "git rebase"
247
+ "git cherry-pick"
248
+ "git commit --amend"
249
+ # Force push
250
+ "git push --force"
251
+ "git push -f"
252
+ # Discarding changes
253
+ "git stash"
254
+ "git clean"
255
+ "git checkout -- "
256
+ "git restore"
257
+ )
258
+
259
+ for pattern in "\${DANGEROUS_PATTERNS[@]}"; do
260
+ # Block if:
261
+ # - Pattern matches UNQUOTED AND first command is "git" (catches split-quote bypasses), OR
262
+ # - Pattern matches STRIPPED (actual command outside any quotes)
263
+ # This allows: echo "git reset", grep "git stash" (first cmd is echo/grep, pattern not in STRIPPED)
264
+ # This blocks: git reset, git "reset" --hard, git st'ash' (first cmd is git OR pattern in STRIPPED)
265
+ if [[ "$STRIPPED" == *"$pattern"* ]] || { [[ "$UNQUOTED" == *"$pattern"* ]] && [[ "$FIRST_CMD" == "git" ]]; }; then
266
+ cat <<EOF
267
+ {
268
+ "decision": "block",
269
+ "reason": "[kspec-worktree-guard] BLOCKED: Dangerous git operation in .kspec worktree. This worktree contains active session data and must stay on kspec-meta. Operations like reset, rebase, stash, and clean can corrupt session files."
270
+ }
271
+ EOF
272
+ exit 0
273
+ fi
274
+ done
275
+
276
+ # Allow all other commands
277
+ echo '{"decision": "allow"}'
278
+ `,
279
+ "ralph-task-limit-guard.sh": `#!/bin/bash
280
+ # Ralph task limit guard - blocks task start when limit reached
281
+ #
282
+ # This hook provides hard enforcement of the --max-tasks limit.
283
+ # Ralph writes a marker file when the limit is reached; this hook
284
+ # blocks 'kspec task start' commands when that marker exists.
285
+
286
+ # Marker file location (relative to project root)
287
+ MARKER_FILE=".claude/ralph-task-limit.json"
288
+
289
+ # Read the tool input from stdin
290
+ INPUT=$(cat)
291
+
292
+ # Extract the command from the JSON input
293
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
294
+
295
+ # If no command, allow (not a Bash tool call)
296
+ if [ -z "$COMMAND" ]; then
297
+ echo '{"decision": "allow"}'
298
+ exit 0
299
+ fi
300
+
301
+ # Only check commands that match "kspec task start"
302
+ if [[ ! "$COMMAND" =~ kspec[[:space:]]+task[[:space:]]+start ]]; then
303
+ echo '{"decision": "allow"}'
304
+ exit 0
305
+ fi
306
+
307
+ # Get cwd from hook input to find marker file
308
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
309
+ if [ -z "$CWD" ]; then
310
+ CWD="$PWD"
311
+ fi
312
+
313
+ # Find project root by walking up looking for .claude directory
314
+ PROJECT_ROOT="$CWD"
315
+ while [ "$PROJECT_ROOT" != "/" ]; do
316
+ if [ -d "$PROJECT_ROOT/.claude" ]; then
317
+ break
318
+ fi
319
+ PROJECT_ROOT=$(dirname "$PROJECT_ROOT")
320
+ done
321
+
322
+ if [ "$PROJECT_ROOT" = "/" ]; then
323
+ # No .claude directory found, allow
324
+ echo '{"decision": "allow"}'
325
+ exit 0
326
+ fi
327
+
328
+ MARKER_PATH="$PROJECT_ROOT/$MARKER_FILE"
329
+
330
+ # Check if marker file exists
331
+ if [ ! -f "$MARKER_PATH" ]; then
332
+ echo '{"decision": "allow"}'
333
+ exit 0
334
+ fi
335
+
336
+ # Read marker file and check if active
337
+ ACTIVE=$(jq -r '.active // false' "$MARKER_PATH" 2>/dev/null)
338
+ if [ "$ACTIVE" != "true" ]; then
339
+ echo '{"decision": "allow"}'
340
+ exit 0
341
+ fi
342
+
343
+ # Extract limit info for error message
344
+ MAX=$(jq -r '.max // "?"' "$MARKER_PATH" 2>/dev/null)
345
+ COMPLETED=$(jq -r '.completed // "?"' "$MARKER_PATH" 2>/dev/null)
346
+
347
+ # Block the command
348
+ cat <<EOF
349
+ {
350
+ "decision": "block",
351
+ "reason": "[ralph-task-limit-guard] BLOCKED: Task limit reached (\${COMPLETED}/\${MAX} tasks completed this iteration). This limit was set by --max-tasks. Please wrap up current work and let the iteration end naturally. Do not attempt to start new tasks."
352
+ }
353
+ EOF
354
+ exit 0
355
+ `,
356
+ };
127
357
  /**
128
358
  * Install hooks to project-level Claude Code settings (.claude/settings.json)
359
+ * AC: @enhanced-setup ac-2 - all hook entries present
129
360
  */
130
- async function installClaudeCodeHooks(projectDir) {
131
- const configPath = path.join(projectDir, '.claude', 'settings.json');
361
+ async function installClaudeCodeHooks(projectDir, dryRun = false) {
362
+ const configPath = path.join(projectDir, ".claude", "settings.json");
132
363
  const configDir = path.dirname(configPath);
133
- const result = { stop: false, promptCheck: false };
364
+ const hooksDir = path.join(projectDir, ".claude", "hooks");
365
+ const result = {
366
+ promptCheck: false,
367
+ stop: false,
368
+ preToolUse: false,
369
+ guardsCreated: [],
370
+ };
134
371
  try {
135
- // Ensure directory exists
136
- await fs.mkdir(configDir, { recursive: true });
372
+ // Ensure directories exist
373
+ if (!dryRun) {
374
+ await fs.mkdir(configDir, { recursive: true });
375
+ await fs.mkdir(hooksDir, { recursive: true });
376
+ }
137
377
  // Read existing config or start fresh
138
378
  let config = {};
139
379
  try {
140
- const existing = await fs.readFile(configPath, 'utf-8');
380
+ const existing = await fs.readFile(configPath, "utf-8");
141
381
  config = JSON.parse(existing);
142
382
  }
143
- catch {
144
- // File doesn't exist or invalid JSON, start fresh
383
+ catch (err) {
384
+ debugLog("No existing hooks config, starting fresh", err);
145
385
  }
146
386
  // Get or create hooks object
147
387
  const hooks = config.hooks || {};
148
388
  // Install UserPromptSubmit hook (spec-first reminder)
149
- const promptCheckCommand = 'npx kspec session prompt-check';
389
+ const promptCheckCommand = "kspec session prompt-check";
150
390
  const existingPromptHooks = hooks.UserPromptSubmit;
151
- const promptAlreadyInstalled = existingPromptHooks?.some((entry) => entry.hooks?.some((hook) => hook.command?.includes('session prompt-check')));
391
+ const promptAlreadyInstalled = existingPromptHooks?.some((entry) => entry.hooks?.some((hook) => hook.command?.includes("session prompt-check")));
152
392
  if (!promptAlreadyInstalled) {
153
393
  hooks.UserPromptSubmit = [
154
394
  ...(existingPromptHooks || []),
155
395
  {
156
396
  hooks: [
157
397
  {
158
- type: 'command',
398
+ type: "command",
159
399
  command: promptCheckCommand,
160
400
  },
161
401
  ],
@@ -167,17 +407,17 @@ async function installClaudeCodeHooks(projectDir) {
167
407
  result.promptCheck = true; // Already configured
168
408
  }
169
409
  // Install Stop hook (checkpoint)
170
- const stopHookCommand = 'npx kspec session checkpoint --json';
410
+ const stopHookCommand = "kspec session checkpoint --json";
171
411
  const existingStopHooks = hooks.Stop;
172
- const stopAlreadyInstalled = existingStopHooks?.some((entry) => entry.hooks?.some((hook) => hook.command?.includes('session checkpoint')));
412
+ const stopAlreadyInstalled = existingStopHooks?.some((entry) => entry.hooks?.some((hook) => hook.command?.includes("session checkpoint")));
173
413
  if (!stopAlreadyInstalled) {
174
414
  hooks.Stop = [
175
415
  ...(existingStopHooks || []),
176
416
  {
177
- matcher: '',
417
+ matcher: "",
178
418
  hooks: [
179
419
  {
180
- type: 'command',
420
+ type: "command",
181
421
  command: stopHookCommand,
182
422
  },
183
423
  ],
@@ -188,65 +428,56 @@ async function installClaudeCodeHooks(projectDir) {
188
428
  else {
189
429
  result.stop = true; // Already configured
190
430
  }
191
- config.hooks = hooks;
192
- // Write back
193
- await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
194
- return result;
195
- }
196
- catch {
197
- return result;
198
- }
199
- }
200
- /**
201
- * Install stop hook to project-level Claude Code settings (.claude/settings.json)
202
- * @deprecated Use installClaudeCodeHooks instead
203
- */
204
- async function installClaudeCodeStopHook(projectDir) {
205
- const configPath = path.join(projectDir, '.claude', 'settings.json');
206
- const configDir = path.dirname(configPath);
207
- try {
208
- // Ensure directory exists
209
- await fs.mkdir(configDir, { recursive: true });
210
- // Read existing config or start fresh
211
- let config = {};
212
- try {
213
- const existing = await fs.readFile(configPath, 'utf-8');
214
- config = JSON.parse(existing);
431
+ // AC: @enhanced-setup ac-2 - Install PreToolUse hooks with guards
432
+ const existingPreToolUseHooks = hooks.PreToolUse;
433
+ // Check if our guards are already installed
434
+ const guardHookCommands = Object.keys(GUARD_SCRIPTS).map((name) => `.claude/hooks/${name}`);
435
+ const guardsAlreadyInstalled = existingPreToolUseHooks?.some((entry) => entry.hooks?.some((hook) => guardHookCommands.some((cmd) => hook.command?.includes(cmd))));
436
+ if (!guardsAlreadyInstalled) {
437
+ // Create guard script files, skipping when content unchanged
438
+ for (const [name, content] of Object.entries(GUARD_SCRIPTS)) {
439
+ const scriptPath = path.join(hooksDir, name);
440
+ // Check if existing script already has the same content
441
+ let existingContent = null;
442
+ try {
443
+ existingContent = await fs.readFile(scriptPath, "utf-8");
444
+ }
445
+ catch (_err) {
446
+ // File doesn't exist yet
447
+ }
448
+ if (existingContent !== content) {
449
+ if (!dryRun) {
450
+ await fs.writeFile(scriptPath, content, { mode: 0o755 });
451
+ }
452
+ result.guardsCreated.push(name);
453
+ }
454
+ }
455
+ // Add PreToolUse hook entry
456
+ hooks.PreToolUse = [
457
+ ...(existingPreToolUseHooks || []),
458
+ {
459
+ matcher: "Bash",
460
+ hooks: Object.keys(GUARD_SCRIPTS).map((name) => ({
461
+ type: "command",
462
+ command: `.claude/hooks/${name}`,
463
+ })),
464
+ },
465
+ ];
466
+ result.preToolUse = true;
215
467
  }
216
- catch {
217
- // File doesn't exist or invalid JSON, start fresh
468
+ else {
469
+ result.preToolUse = true; // Already configured
218
470
  }
219
- // Build the stop hook command
220
- const stopHookCommand = 'npx kspec session checkpoint --json';
221
- // Get or create hooks object
222
- const hooks = config.hooks || {};
223
- // Check if Stop hook already exists with our command
224
- const existingStopHooks = hooks.Stop;
225
- const alreadyInstalled = existingStopHooks?.some((entry) => entry.hooks?.some((hook) => hook.command?.includes('session checkpoint')));
226
- if (alreadyInstalled) {
227
- return true; // Already configured
228
- }
229
- // Add our stop hook using Claude Code hooks format
230
- // Note: matcher field is required even if empty string
231
- hooks.Stop = [
232
- ...(existingStopHooks || []),
233
- {
234
- matcher: '',
235
- hooks: [
236
- {
237
- type: 'command',
238
- command: stopHookCommand,
239
- },
240
- ],
241
- },
242
- ];
243
471
  config.hooks = hooks;
244
472
  // Write back
245
- await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
246
- return true;
473
+ if (!dryRun) {
474
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
475
+ }
476
+ return result;
247
477
  }
248
- catch {
249
- return false;
478
+ catch (err) {
479
+ debugLog("installClaudeCodeHooks failed", err);
480
+ return result;
250
481
  }
251
482
  }
252
483
  /**
@@ -254,23 +485,23 @@ async function installClaudeCodeStopHook(projectDir) {
254
485
  * Aider uses `set-env:` for environment variables in list format
255
486
  */
256
487
  async function installAiderConfig(author) {
257
- const configPath = path.join(os.homedir(), '.aider.conf.yml');
488
+ const configPath = path.join(os.homedir(), ".aider.conf.yml");
258
489
  try {
259
- let content = '';
490
+ let content = "";
260
491
  try {
261
- content = await fs.readFile(configPath, 'utf-8');
492
+ content = await fs.readFile(configPath, "utf-8");
262
493
  }
263
- catch {
264
- // File doesn't exist, start fresh
494
+ catch (err) {
495
+ debugLog("No existing Aider config, starting fresh", err);
265
496
  }
266
497
  // Check if KSPEC_AUTHOR is already set
267
- if (content.includes('KSPEC_AUTHOR')) {
498
+ if (content.includes("KSPEC_AUTHOR")) {
268
499
  // Replace existing value (handles both old and new format)
269
500
  content = content.replace(/^(\s*-?\s*KSPEC_AUTHOR\s*[=:]\s*).*$/m, ` - KSPEC_AUTHOR=${author}`);
270
501
  }
271
502
  else {
272
503
  // Add to set-env section or create it
273
- if (content.includes('set-env:')) {
504
+ if (content.includes("set-env:")) {
274
505
  // Append to existing set-env section
275
506
  content = content.replace(/(set-env:\s*\n)/m, `$1 - KSPEC_AUTHOR=${author}\n`);
276
507
  }
@@ -279,10 +510,11 @@ async function installAiderConfig(author) {
279
510
  content += `\n# kspec author for note attribution\nset-env:\n - KSPEC_AUTHOR=${author}\n`;
280
511
  }
281
512
  }
282
- await fs.writeFile(configPath, content, 'utf-8');
513
+ await fs.writeFile(configPath, content, "utf-8");
283
514
  return true;
284
515
  }
285
- catch {
516
+ catch (err) {
517
+ debugLog("installAiderConfig failed", err);
286
518
  return false;
287
519
  }
288
520
  }
@@ -295,19 +527,20 @@ async function installGenericJsonConfig(configPath, author) {
295
527
  await fs.mkdir(configDir, { recursive: true });
296
528
  let config = {};
297
529
  try {
298
- const existing = await fs.readFile(configPath, 'utf-8');
530
+ const existing = await fs.readFile(configPath, "utf-8");
299
531
  config = JSON.parse(existing);
300
532
  }
301
- catch {
302
- // Start fresh
533
+ catch (err) {
534
+ debugLog(`No existing config at ${configPath}, starting fresh`, err);
303
535
  }
304
536
  const env = config.env || {};
305
537
  env.KSPEC_AUTHOR = author;
306
538
  config.env = env;
307
- await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
539
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
308
540
  return true;
309
541
  }
310
- catch {
542
+ catch (err) {
543
+ debugLog(`installGenericJsonConfig failed for ${configPath}`, err);
311
544
  return false;
312
545
  }
313
546
  }
@@ -316,26 +549,26 @@ async function installGenericJsonConfig(configPath, author) {
316
549
  */
317
550
  function getDefaultAuthor(agentType) {
318
551
  switch (agentType) {
319
- case 'claude-code':
320
- return '@claude';
321
- case 'cline':
322
- return '@cline';
323
- case 'roo-code':
324
- return '@roo';
325
- case 'copilot-cli':
326
- return '@copilot';
327
- case 'gemini-cli':
328
- return '@gemini';
329
- case 'codex-cli':
330
- return '@codex';
331
- case 'aider':
332
- return '@aider';
333
- case 'opencode':
334
- return '@opencode';
335
- case 'amp':
336
- return '@amp';
552
+ case "claude-code":
553
+ return "@claude";
554
+ case "cline":
555
+ return "@cline";
556
+ case "roo-code":
557
+ return "@roo";
558
+ case "copilot-cli":
559
+ return "@copilot";
560
+ case "gemini-cli":
561
+ return "@gemini";
562
+ case "codex-cli":
563
+ return "@codex";
564
+ case "aider":
565
+ return "@aider";
566
+ case "opencode":
567
+ return "@opencode";
568
+ case "amp":
569
+ return "@amp";
337
570
  default:
338
- return '@agent';
571
+ return "@agent";
339
572
  }
340
573
  }
341
574
  /**
@@ -348,7 +581,7 @@ async function promptYesNo(question) {
348
581
  });
349
582
  try {
350
583
  const answer = await rl.question(`${question} `);
351
- return answer.toLowerCase() === 'y';
584
+ return answer.toLowerCase() === "y";
352
585
  }
353
586
  finally {
354
587
  rl.close();
@@ -379,7 +612,7 @@ async function ensureWorktree(autoWorktree) {
379
612
  console.log(`Detected ${SHADOW_BRANCH_NAME} branch without .kspec worktree. Creating...`);
380
613
  const result = await repairShadow(projectRoot);
381
614
  if (result.success) {
382
- success('Created .kspec worktree');
615
+ success("Created .kspec worktree");
383
616
  return true;
384
617
  }
385
618
  else {
@@ -390,10 +623,10 @@ async function ensureWorktree(autoWorktree) {
390
623
  // AC: detect-existing-repo - prompt user
391
624
  const shouldCreate = await promptYesNo(`${SHADOW_BRANCH_NAME} branch exists but .kspec worktree is missing. Create it? (y/N)`);
392
625
  if (shouldCreate) {
393
- console.log('Creating .kspec worktree...');
626
+ console.log("Creating .kspec worktree...");
394
627
  const result = await repairShadow(projectRoot);
395
628
  if (result.success) {
396
- success('Created .kspec worktree');
629
+ success("Created .kspec worktree");
397
630
  return true;
398
631
  }
399
632
  else {
@@ -402,7 +635,7 @@ async function ensureWorktree(autoWorktree) {
402
635
  }
403
636
  }
404
637
  else {
405
- warn('Skipping worktree creation');
638
+ warn("Skipping worktree creation");
406
639
  return false;
407
640
  }
408
641
  }
@@ -414,76 +647,861 @@ async function ensureWorktree(autoWorktree) {
414
647
  */
415
648
  function printManualInstructions(agentType) {
416
649
  const author = getDefaultAuthor(agentType);
417
- console.log('\nManual setup instructions:\n');
650
+ console.log("\nManual setup instructions:\n");
418
651
  switch (agentType) {
419
- case 'claude-code':
420
- console.log('Add to ~/.claude/settings.json:');
421
- console.log('```json');
652
+ case "claude-code":
653
+ console.log("Add to ~/.claude/settings.json:");
654
+ console.log("```json");
422
655
  console.log(JSON.stringify({ env: { KSPEC_AUTHOR: author } }, null, 2));
423
- console.log('```');
656
+ console.log("```");
424
657
  break;
425
- case 'cline':
426
- case 'roo-code':
427
- console.log('Add to your shell profile (~/.bashrc, ~/.zshrc):');
428
- console.log('```bash');
658
+ case "cline":
659
+ case "roo-code":
660
+ console.log("Add to your shell profile (~/.bashrc, ~/.zshrc):");
661
+ console.log("```bash");
429
662
  console.log(`export KSPEC_AUTHOR="${author}"`);
430
- console.log('```');
431
- console.log('\nThis will be inherited by terminals spawned by the VS Code extension.');
663
+ console.log("```");
664
+ console.log("\nThis will be inherited by terminals spawned by the VS Code extension.");
432
665
  break;
433
- case 'copilot-cli':
434
- console.log('Add to ~/.copilot/config.json:');
435
- console.log('```json');
666
+ case "copilot-cli":
667
+ console.log("Add to ~/.copilot/config.json:");
668
+ console.log("```json");
436
669
  console.log(JSON.stringify({ env: { KSPEC_AUTHOR: author } }, null, 2));
437
- console.log('```');
670
+ console.log("```");
438
671
  break;
439
- case 'aider':
440
- console.log('Add to ~/.aider.conf.yml:');
441
- console.log('```yaml');
442
- console.log('set-env:');
672
+ case "aider":
673
+ console.log("Add to ~/.aider.conf.yml:");
674
+ console.log("```yaml");
675
+ console.log("set-env:");
443
676
  console.log(` - KSPEC_AUTHOR=${author}`);
444
- console.log('```');
677
+ console.log("```");
445
678
  break;
446
- case 'codex-cli':
447
- console.log('Add to ~/.codex/config.toml:');
448
- console.log('```toml');
449
- console.log('[shell_environment_policy]');
679
+ case "codex-cli":
680
+ console.log("Add to ~/.codex/config.toml:");
681
+ console.log("```toml");
682
+ console.log("[shell_environment_policy]");
450
683
  console.log(`set = { KSPEC_AUTHOR = "${author}" }`);
451
- console.log('```');
684
+ console.log("```");
452
685
  break;
453
- case 'opencode':
454
- console.log('Add to ~/.config/opencode/opencode.json:');
455
- console.log('```json');
686
+ case "opencode":
687
+ console.log("Add to ~/.config/opencode/opencode.json:");
688
+ console.log("```json");
456
689
  console.log(JSON.stringify({ env: { KSPEC_AUTHOR: author } }, null, 2));
457
- console.log('```');
690
+ console.log("```");
458
691
  break;
459
- case 'amp':
460
- console.log('Add to ~/.config/amp/settings.json:');
461
- console.log('```json');
692
+ case "amp":
693
+ console.log("Add to ~/.config/amp/settings.json:");
694
+ console.log("```json");
462
695
  console.log(JSON.stringify({ env: { KSPEC_AUTHOR: author } }, null, 2));
463
- console.log('```');
696
+ console.log("```");
464
697
  break;
465
698
  default:
466
- console.log('Set the KSPEC_AUTHOR environment variable:');
467
- console.log('```bash');
699
+ console.log("Set the KSPEC_AUTHOR environment variable:");
700
+ console.log("```bash");
468
701
  console.log(`export KSPEC_AUTHOR="${author}"`);
469
- console.log('```');
470
- console.log('\nOr add to your shell profile (~/.bashrc, ~/.zshrc, etc.)');
702
+ console.log("```");
703
+ console.log("\nOr add to your shell profile (~/.bashrc, ~/.zshrc, etc.)");
704
+ }
705
+ }
706
+ /**
707
+ * Check the current setup status
708
+ * AC: @enhanced-setup ac-7, ac-8
709
+ */
710
+ async function getSetupStatus(projectDir) {
711
+ const detected = detectAgent();
712
+ const configPath = path.join(projectDir, ".claude", "settings.json");
713
+ const hooksDir = path.join(projectDir, ".claude", "hooks");
714
+ const agentsMdPath = path.join(projectDir, "kspec-agents.md");
715
+ const hashPath = path.join(projectDir, ".kspec", ".kspec-agents-hash");
716
+ const skillsDir = path.join(projectDir, ".claude", "skills");
717
+ // Check hooks
718
+ const hooks = {
719
+ promptCheck: false,
720
+ stop: false,
721
+ preToolUse: false,
722
+ guardsPresent: [],
723
+ };
724
+ try {
725
+ const configContent = await fs.readFile(configPath, "utf-8");
726
+ const config = JSON.parse(configContent);
727
+ const hooksConfig = config.hooks || {};
728
+ // Check UserPromptSubmit
729
+ const promptHooks = hooksConfig.UserPromptSubmit;
730
+ hooks.promptCheck = promptHooks?.some((entry) => entry.hooks?.some((h) => h.command?.includes("prompt-check"))) ?? false;
731
+ // Check Stop
732
+ const stopHooks = hooksConfig.Stop;
733
+ hooks.stop = stopHooks?.some((entry) => entry.hooks?.some((h) => h.command?.includes("checkpoint"))) ?? false;
734
+ // Check PreToolUse
735
+ const preToolUseHooks = hooksConfig.PreToolUse;
736
+ hooks.preToolUse = preToolUseHooks?.some((entry) => entry.hooks?.some((h) => h.command?.includes(".claude/hooks/"))) ?? false;
737
+ }
738
+ catch (err) {
739
+ debugLog("Failed to read hooks config for status", err);
740
+ }
741
+ // Check guard scripts
742
+ try {
743
+ const guardFiles = await fs.readdir(hooksDir);
744
+ for (const name of Object.keys(GUARD_SCRIPTS)) {
745
+ if (guardFiles.includes(name)) {
746
+ hooks.guardsPresent.push(name);
747
+ }
748
+ }
749
+ }
750
+ catch (err) {
751
+ debugLog("Hooks dir doesn't exist", err);
752
+ }
753
+ // Check skills
754
+ const skills = {
755
+ total: 0,
756
+ rendered: 0,
757
+ drifted: 0,
758
+ };
759
+ // Helper to scan a directory for skill subdirs with kspec-managed SKILL.md
760
+ async function scanForSkills(baseDir) {
761
+ try {
762
+ const dirs = await fs.readdir(baseDir, { withFileTypes: true });
763
+ for (const dir of dirs) {
764
+ if (dir.isDirectory()) {
765
+ const skillMdPath = path.join(baseDir, dir.name, "SKILL.md");
766
+ try {
767
+ const content = await fs.readFile(skillMdPath, "utf-8");
768
+ if (content.includes("<!-- kspec-managed -->")) {
769
+ skills.total++;
770
+ skills.rendered++;
771
+ }
772
+ }
773
+ catch (_noSkillMd) {
774
+ // No SKILL.md
775
+ }
776
+ }
777
+ }
778
+ }
779
+ catch (_notReadable) {
780
+ // Directory doesn't exist
781
+ }
782
+ }
783
+ // Scan .claude/skills/ (project/local skills)
784
+ await scanForSkills(skillsDir);
785
+ // Check plugin marketplace health
786
+ // AC: @enhanced-setup ac-7, ac-8
787
+ const plugin = {
788
+ marketplaceRegistered: false,
789
+ marketplaceHealthy: false,
790
+ pluginEnabled: false,
791
+ };
792
+ try {
793
+ const { checkMarketplaceHealth } = await import("../../lib/claude-plugin-registry.js");
794
+ const health = await checkMarketplaceHealth();
795
+ plugin.marketplaceRegistered = health.status !== "missing";
796
+ plugin.marketplaceHealthy = health.status === "healthy";
797
+ plugin.registeredPath = health.registeredPath;
798
+ plugin.healthMessage = health.message;
799
+ }
800
+ catch (err) {
801
+ debugLog("Could not check marketplace health", err);
802
+ plugin.healthMessage = "Health check unavailable";
803
+ }
804
+ // Check if plugin is enabled in project settings
805
+ try {
806
+ const configContent = await fs.readFile(configPath, "utf-8");
807
+ const config = JSON.parse(configContent);
808
+ plugin.pluginEnabled = config.enabledPlugins?.["kspec@kspec-plugins"] === true;
809
+ }
810
+ catch (err) {
811
+ debugLog("Could not check plugin enablement", err);
812
+ }
813
+ // Check agents.md
814
+ const agentsMd = {
815
+ exists: false,
816
+ status: "missing",
817
+ };
818
+ try {
819
+ await fs.access(agentsMdPath);
820
+ agentsMd.exists = true;
821
+ try {
822
+ const hashContent = await fs.readFile(hashPath, "utf-8");
823
+ const hashData = JSON.parse(hashContent);
824
+ agentsMd.generatedAt = hashData.generatedAt;
825
+ // AC: @cross-platform-and-version-robustness ac-4
826
+ // Compare stored hash against current meta to detect staleness
827
+ try {
828
+ const { initContext, loadMetaContext } = await import("../../parser/index.js");
829
+ const { computeMetaHash, loadTemplateSections, getPackageRoot } = await import("./agents.js");
830
+ const ctx = await initContext();
831
+ if (ctx.manifestPath) {
832
+ const metaCtx = await loadMetaContext(ctx);
833
+ let templateSections = [];
834
+ try {
835
+ templateSections = await loadTemplateSections(getPackageRoot());
836
+ }
837
+ catch (err) {
838
+ debugLog("Templates not available for staleness check", err);
839
+ }
840
+ const currentHash = computeMetaHash(metaCtx.skills, metaCtx.conventions, metaCtx.workflows, templateSections);
841
+ agentsMd.status = hashData.metaHash === currentHash ? "current" : "stale";
842
+ }
843
+ else {
844
+ // AC: @doctor-command ac-staleness-unknown — no manifest means we can't determine staleness
845
+ agentsMd.status = "unknown";
846
+ }
847
+ }
848
+ catch (err) {
849
+ // AC: @doctor-command ac-staleness-unknown — hash computation failed
850
+ debugLog("Could not compute meta hash for staleness check", err);
851
+ agentsMd.status = "unknown";
852
+ }
853
+ }
854
+ catch (err) {
855
+ debugLog("Hash file missing or invalid, marking stale", err);
856
+ agentsMd.status = "stale";
857
+ }
858
+ }
859
+ catch (err) {
860
+ debugLog("kspec-agents.md doesn't exist", err);
861
+ }
862
+ // Check seeding state
863
+ const seeding = {
864
+ permissionsSeeded: false,
865
+ memorySeeded: false,
866
+ };
867
+ try {
868
+ const configContent = await fs.readFile(path.join(projectDir, ".claude", "settings.json"), "utf-8");
869
+ const config = JSON.parse(configContent);
870
+ seeding.permissionsSeeded = !!config.permissions;
871
+ }
872
+ catch (err) {
873
+ debugLog("Could not check permissions seeding state", err);
874
+ }
875
+ if (detected.type === "claude-code") {
876
+ try {
877
+ const { claudeCodeMemoryWriter } = await import("./setup-seeding.js");
878
+ const memoryExists = await claudeCodeMemoryWriter.exists(projectDir);
879
+ seeding.memorySeeded = memoryExists;
880
+ if (memoryExists) {
881
+ seeding.memoryPath = claudeCodeMemoryWriter.getMemoryPath(projectDir);
882
+ }
883
+ }
884
+ catch (err) {
885
+ debugLog("Could not check memory seeding state", err);
886
+ }
887
+ }
888
+ return {
889
+ agent: {
890
+ detected: detected.type,
891
+ confidence: detected.confidence,
892
+ },
893
+ hooks,
894
+ skills,
895
+ plugin,
896
+ agentsMd,
897
+ seeding,
898
+ };
899
+ }
900
+ /**
901
+ * Render skills using the platform renderer registry
902
+ * AC: @setup-pipeline-unification ac-2 - uses getRenderer/getAllRenderers, not legacy renderClaudeCodeSkill
903
+ * AC: @setup-pipeline-unification ac-4 - errors logged at debug level
904
+ */
905
+ async function renderSkillsForSetup(projectDir, dryRun) {
906
+ // Dynamically import to avoid circular dependencies
907
+ const { initContext, loadMetaContext } = await import("../../parser/index.js");
908
+ const { getRenderer } = await import("../../parser/skill-render.js");
909
+ try {
910
+ const ctx = await initContext();
911
+ if (!ctx.manifestPath) {
912
+ return { rendered: 0, skipped: 0, pluginProvided: 0, skillIds: [] };
913
+ }
914
+ const metaCtx = await loadMetaContext(ctx);
915
+ // Collect all skills that have a registered renderer for their platform
916
+ const skillsToRender = [];
917
+ for (const skill of metaCtx.skills) {
918
+ for (const platform of skill.platforms) {
919
+ const renderer = getRenderer(platform);
920
+ if (renderer) {
921
+ skillsToRender.push({ skill, platform });
922
+ }
923
+ }
924
+ }
925
+ if (skillsToRender.length === 0) {
926
+ return { rendered: 0, skipped: 0, pluginProvided: 0, skillIds: [] };
927
+ }
928
+ let rendered = 0;
929
+ let skipped = 0;
930
+ let pluginProvided = 0;
931
+ const skillIds = [];
932
+ for (const { skill, platform } of skillsToRender) {
933
+ const renderer = getRenderer(platform);
934
+ try {
935
+ const result = await renderer.render(ctx, projectDir, skill, {
936
+ dryRun,
937
+ });
938
+ if (result.action === "created" || result.action === "updated") {
939
+ rendered++;
940
+ if (!skillIds.includes(skill.id)) {
941
+ skillIds.push(skill.id);
942
+ }
943
+ }
944
+ else if (result.action === "skipped" && result.skipCode === "plugin-provided") {
945
+ pluginProvided++;
946
+ }
947
+ else {
948
+ skipped++;
949
+ }
950
+ }
951
+ catch (err) {
952
+ debugLog(`Failed to render skill ${skill.id} for ${platform}`, err);
953
+ skipped++;
954
+ }
955
+ }
956
+ return { rendered, skipped, pluginProvided, skillIds };
957
+ }
958
+ catch (err) {
959
+ debugLog("renderSkillsForSetup failed", err);
960
+ return { rendered: 0, skipped: 0, pluginProvided: 0, skillIds: [] };
961
+ }
962
+ }
963
+ /**
964
+ * Generate kspec-agents.md using the canonical implementation from agents.ts
965
+ * AC: @setup-pipeline-unification ac-1 - calls generateAgentsContent() from agents.ts
966
+ * AC: @setup-pipeline-unification ac-4 - errors logged at debug level
967
+ */
968
+ async function generateAgentInstructions(projectDir, dryRun) {
969
+ const outputPath = path.join(projectDir, "kspec-agents.md");
970
+ const hashPath = path.join(projectDir, ".kspec", ".kspec-agents-hash");
971
+ // Dynamically import to avoid circular dependencies
972
+ const { initContext, loadMetaContext } = await import("../../parser/index.js");
973
+ const { generateAgentsContent, loadTemplateSections, getPackageRoot, computeMetaHash, } = await import("./agents.js");
974
+ try {
975
+ const ctx = await initContext();
976
+ if (!ctx.manifestPath) {
977
+ return { success: false, path: outputPath };
978
+ }
979
+ const metaCtx = await loadMetaContext(ctx);
980
+ const timestamp = new Date().toISOString();
981
+ // Load templates using the canonical implementation
982
+ let templateSections = [];
983
+ try {
984
+ templateSections = await loadTemplateSections(getPackageRoot());
985
+ }
986
+ catch (err) {
987
+ debugLog("Failed to load template sections", err);
988
+ }
989
+ // Generate content using the canonical implementation from agents.ts
990
+ const content = await generateAgentsContent(metaCtx.skills, metaCtx.conventions, metaCtx.workflows, timestamp, templateSections);
991
+ if (!dryRun) {
992
+ // Compute meta hash for freshness tracking
993
+ const metaHash = computeMetaHash(metaCtx.skills, metaCtx.conventions, metaCtx.workflows, templateSections);
994
+ // Skip regeneration when content unchanged (same pattern as kspec agents generate)
995
+ let storedHash;
996
+ try {
997
+ const hashContent = await fs.readFile(hashPath, "utf-8");
998
+ const hashData = JSON.parse(hashContent);
999
+ storedHash = hashData.metaHash;
1000
+ }
1001
+ catch (_err) {
1002
+ // No hash file or invalid — regenerate
1003
+ }
1004
+ // Only skip if hash matches AND the output file actually exists
1005
+ let outputExists = false;
1006
+ try {
1007
+ await fs.access(outputPath);
1008
+ outputExists = true;
1009
+ }
1010
+ catch (_e) {
1011
+ // File missing — must regenerate even if hash matches
1012
+ }
1013
+ if (storedHash === metaHash && outputExists) {
1014
+ return { success: true, path: outputPath, skipped: true };
1015
+ }
1016
+ await fs.writeFile(outputPath, content, "utf-8");
1017
+ await fs.mkdir(path.dirname(hashPath), { recursive: true });
1018
+ // Dynamically import version to avoid top-level require
1019
+ const { createRequire } = await import("node:module");
1020
+ const req = createRequire(import.meta.url);
1021
+ const { version } = req("../../../package.json");
1022
+ await fs.writeFile(hashPath, JSON.stringify({
1023
+ metaHash,
1024
+ generatedAt: timestamp,
1025
+ version,
1026
+ }, null, 2), "utf-8");
1027
+ }
1028
+ return { success: true, path: outputPath };
1029
+ }
1030
+ catch (err) {
1031
+ debugLog("generateAgentInstructions failed", err);
1032
+ return { success: false, path: outputPath };
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Install core skills for the setup pipeline
1037
+ * AC: @init-setup-integration ac-2 - core skills installed
1038
+ */
1039
+ async function installCoreSkillsForSetup(projectDir, dryRun) {
1040
+ // Dynamically import to avoid circular dependencies
1041
+ const { initContext, loadMetaContext, saveMetaItem, getSkillContentPath, } = await import("../../parser/index.js");
1042
+ const { commitIfShadow } = await import("../../parser/shadow.js");
1043
+ const { SkillSchema } = await import("../../schema/index.js");
1044
+ const { loadCoreSkillsManifest, copyCoreSkillFiles, getKspecPackageVersion, } = await import("./skill.js");
1045
+ const { ulid } = await import("ulid");
1046
+ let installed = 0;
1047
+ let skipped = 0;
1048
+ try {
1049
+ const ctx = await initContext();
1050
+ if (!ctx.manifestPath) {
1051
+ return { installed: 0, skipped: 0 };
1052
+ }
1053
+ const metaCtx = await loadMetaContext(ctx);
1054
+ const coreSkills = await loadCoreSkillsManifest();
1055
+ // AC: @cross-platform-and-version-robustness ac-3
1056
+ const kspecVersion = await getKspecPackageVersion();
1057
+ if (!kspecVersion) {
1058
+ debugLog("Could not determine kspec version — skills installed without version tracking");
1059
+ }
1060
+ for (const coreSkill of coreSkills) {
1061
+ // Check if skill exists
1062
+ const existingSkill = metaCtx.skills.find((s) => s.id === coreSkill.id);
1063
+ if (existingSkill && existingSkill.origin !== "core") {
1064
+ // Custom/project skill exists, skip
1065
+ skipped++;
1066
+ continue;
1067
+ }
1068
+ // Build skill data
1069
+ const skillData = {
1070
+ _ulid: existingSkill?._ulid || ulid(),
1071
+ id: coreSkill.id,
1072
+ name: coreSkill.name,
1073
+ description: coreSkill.description,
1074
+ origin: "core",
1075
+ ...(kspecVersion && { version: kspecVersion }),
1076
+ platforms: coreSkill.platforms || ["claude-code"],
1077
+ depends_on: [],
1078
+ tags: ["core"],
1079
+ };
1080
+ const parsed = SkillSchema.safeParse(skillData);
1081
+ if (!parsed.success) {
1082
+ skipped++;
1083
+ continue;
1084
+ }
1085
+ if (!dryRun) {
1086
+ await saveMetaItem(ctx, parsed.data, "skill");
1087
+ // Copy skill files (SKILL.md + supporting dirs)
1088
+ const targetDir = path.dirname(getSkillContentPath(ctx, parsed.data.id));
1089
+ await copyCoreSkillFiles(coreSkill.id, targetDir);
1090
+ }
1091
+ installed++;
1092
+ }
1093
+ // Commit changes
1094
+ if (!dryRun && installed > 0) {
1095
+ const ctx2 = await initContext();
1096
+ await commitIfShadow(ctx2.shadow, "skill-install-core", `${installed} core skills`);
1097
+ }
1098
+ // AC: @core-skill-install ac-6, ac-7 - Register marketplace and enable plugin
1099
+ let marketplaceResult;
1100
+ let enableResult;
1101
+ if (!dryRun) {
1102
+ const { registerCorePluginMarketplace, enablePluginInProject, } = await import("../../lib/claude-plugin-registry.js");
1103
+ marketplaceResult = await registerCorePluginMarketplace();
1104
+ enableResult = await enablePluginInProject(projectDir);
1105
+ }
1106
+ return {
1107
+ installed,
1108
+ skipped,
1109
+ marketplaceRegistered: marketplaceResult?.success ?? false,
1110
+ pluginEnabled: enableResult?.success ?? false,
1111
+ marketplaceMessage: marketplaceResult?.message,
1112
+ enableMessage: enableResult?.message,
1113
+ };
1114
+ }
1115
+ catch (err) {
1116
+ debugLog("installCoreSkillsForSetup failed", err);
1117
+ return { installed: 0, skipped: 0 };
1118
+ }
1119
+ }
1120
+ /**
1121
+ * Run the full setup pipeline programmatically.
1122
+ * Used by both 'kspec setup' command and 'kspec init --setup'.
1123
+ * AC: @init-setup-integration ac-2, ac-3
1124
+ */
1125
+ export async function runSetupPipeline(projectDir, options) {
1126
+ const dryRun = options.dryRun ?? false;
1127
+ const skipSkills = options.skipSkills ?? false;
1128
+ const installHooksFlag = options.installHooks ?? true;
1129
+ const steps = [];
1130
+ let coreSkillsInstalled = 0;
1131
+ let skillsRendered = 0;
1132
+ let hooksInstalled = false;
1133
+ let agentsMdGenerated = false;
1134
+ let permissionsSeeded = false;
1135
+ let memorySeeded = false;
1136
+ try {
1137
+ const detected = detectAgent();
1138
+ // Step 1: Agent detection
1139
+ steps.push({
1140
+ name: "Agent detection",
1141
+ status: "done",
1142
+ message: `${detected.type} (${detected.confidence} confidence)`,
1143
+ });
1144
+ // Step 2: Install core skills
1145
+ // AC: @init-setup-integration ac-2 - core skills installed in .kspec/skills/
1146
+ const coreResult = await installCoreSkillsForSetup(projectDir, dryRun);
1147
+ coreSkillsInstalled = coreResult.installed;
1148
+ if (coreResult.installed > 0 || coreResult.skipped > 0) {
1149
+ steps.push({
1150
+ name: "Install core skills",
1151
+ status: "done",
1152
+ message: `${coreResult.installed} installed, ${coreResult.skipped} skipped`,
1153
+ });
1154
+ }
1155
+ else {
1156
+ steps.push({
1157
+ name: "Install core skills",
1158
+ status: "skipped",
1159
+ message: "No core skills found in package",
1160
+ });
1161
+ }
1162
+ // Step 2b: Register plugin marketplace (reports result from installCoreSkillsForSetup)
1163
+ // AC: @core-skill-install ac-6, ac-7
1164
+ if (!dryRun && (coreResult.installed > 0 || coreResult.skipped > 0)) {
1165
+ const bothOk = coreResult.marketplaceRegistered && coreResult.pluginEnabled;
1166
+ if (bothOk) {
1167
+ steps.push({
1168
+ name: "Register plugin marketplace",
1169
+ status: "done",
1170
+ message: "marketplace registered, plugin enabled",
1171
+ });
1172
+ }
1173
+ else {
1174
+ const failures = [];
1175
+ if (!coreResult.marketplaceRegistered) {
1176
+ failures.push(coreResult.marketplaceMessage || "marketplace registration failed");
1177
+ }
1178
+ if (!coreResult.pluginEnabled) {
1179
+ failures.push(coreResult.enableMessage || "plugin enablement failed");
1180
+ }
1181
+ steps.push({
1182
+ name: "Register plugin marketplace",
1183
+ status: "failed",
1184
+ message: failures.join("; "),
1185
+ });
1186
+ }
1187
+ }
1188
+ // Step 3: Install hooks (Claude Code only)
1189
+ // AC: @init-setup-integration ac-3 - hooks present
1190
+ if (detected.type === "claude-code" && installHooksFlag) {
1191
+ const hooksResult = await installClaudeCodeHooks(projectDir, dryRun);
1192
+ const installedHooks = [];
1193
+ if (hooksResult.promptCheck)
1194
+ installedHooks.push("UserPromptSubmit");
1195
+ if (hooksResult.stop)
1196
+ installedHooks.push("Stop");
1197
+ if (hooksResult.preToolUse)
1198
+ installedHooks.push("PreToolUse");
1199
+ hooksInstalled =
1200
+ hooksResult.promptCheck || hooksResult.stop || hooksResult.preToolUse;
1201
+ steps.push({
1202
+ name: "Install hooks",
1203
+ status: "done",
1204
+ message: installedHooks.join(", "),
1205
+ details: {
1206
+ guards: hooksResult.guardsCreated,
1207
+ },
1208
+ });
1209
+ }
1210
+ else if (!installHooksFlag) {
1211
+ steps.push({
1212
+ name: "Install hooks",
1213
+ status: "skipped",
1214
+ message: "--no-hooks flag",
1215
+ });
1216
+ }
1217
+ else {
1218
+ steps.push({
1219
+ name: "Install hooks",
1220
+ status: "skipped",
1221
+ message: `not applicable for ${detected.type}`,
1222
+ });
1223
+ }
1224
+ // Step 3a: Ensure artifacts directory exists
1225
+ // AC: @artifacts-directory ac-setup-ensures
1226
+ {
1227
+ const artifactsDir = path.join(projectDir, ".kspec", "artifacts");
1228
+ let artifactsCreated = false;
1229
+ try {
1230
+ await fs.access(artifactsDir);
1231
+ }
1232
+ catch (_e) {
1233
+ if (!dryRun) {
1234
+ await fs.mkdir(artifactsDir, { recursive: true });
1235
+ }
1236
+ artifactsCreated = true;
1237
+ }
1238
+ steps.push({
1239
+ name: "Ensure artifacts directory",
1240
+ status: artifactsCreated ? "done" : "skipped",
1241
+ message: artifactsCreated ? "created .kspec/artifacts/" : "already exists",
1242
+ });
1243
+ }
1244
+ // Step 3b: Seed permissions (Claude Code only)
1245
+ // AC: @new-project-bootstrapping ac-1
1246
+ {
1247
+ const { seedPermissions } = await import("./setup-seeding.js");
1248
+ const permResult = await seedPermissions(projectDir, detected.type, {
1249
+ dryRun,
1250
+ force: options.force,
1251
+ });
1252
+ permissionsSeeded = permResult.seeded;
1253
+ steps.push({
1254
+ name: "Seed permissions",
1255
+ status: permResult.seeded ? "done" : "skipped",
1256
+ message: permResult.message,
1257
+ });
1258
+ }
1259
+ // Step 3c: Seed memory (platform-extensible)
1260
+ // AC: @new-project-bootstrapping ac-2
1261
+ {
1262
+ const { seedMemory } = await import("./setup-seeding.js");
1263
+ const memResult = await seedMemory(projectDir, detected.type, {
1264
+ dryRun,
1265
+ force: options.force,
1266
+ });
1267
+ memorySeeded = memResult.seeded;
1268
+ steps.push({
1269
+ name: "Seed memory",
1270
+ status: memResult.seeded ? "done" : "skipped",
1271
+ message: memResult.seeded ? memResult.path : memResult.message,
1272
+ });
1273
+ }
1274
+ // Step 4: Render skills
1275
+ // AC: @init-setup-integration ac-3 - rendered skill files present
1276
+ if (!skipSkills) {
1277
+ const skillsResult = await renderSkillsForSetup(projectDir, dryRun);
1278
+ skillsRendered = skillsResult.rendered;
1279
+ if (skillsResult.rendered > 0 || skillsResult.skipped > 0 || skillsResult.pluginProvided > 0) {
1280
+ const parts = [];
1281
+ if (skillsResult.rendered > 0)
1282
+ parts.push(`${skillsResult.rendered} rendered`);
1283
+ if (skillsResult.pluginProvided > 0)
1284
+ parts.push(`${skillsResult.pluginProvided} plugin-provided`);
1285
+ if (skillsResult.skipped > 0)
1286
+ parts.push(`${skillsResult.skipped} unchanged`);
1287
+ steps.push({
1288
+ name: "Render skills",
1289
+ status: "done",
1290
+ message: parts.join(", "),
1291
+ details: {
1292
+ skillIds: skillsResult.skillIds,
1293
+ },
1294
+ });
1295
+ }
1296
+ else {
1297
+ steps.push({
1298
+ name: "Render skills",
1299
+ status: "skipped",
1300
+ message: "No claude-code skills in meta",
1301
+ });
1302
+ }
1303
+ }
1304
+ else {
1305
+ steps.push({
1306
+ name: "Render skills",
1307
+ status: "skipped",
1308
+ message: "--skip-skills flag",
1309
+ });
1310
+ }
1311
+ // Step 5: Generate kspec-agents.md
1312
+ // AC: @init-setup-integration ac-3 - kspec-agents.md present
1313
+ const agentsResult = await generateAgentInstructions(projectDir, dryRun);
1314
+ agentsMdGenerated = agentsResult.success;
1315
+ if (agentsResult.success) {
1316
+ steps.push({
1317
+ name: "Generate kspec-agents.md",
1318
+ status: "done",
1319
+ message: agentsResult.skipped
1320
+ ? "already up to date"
1321
+ : agentsResult.path,
1322
+ });
1323
+ }
1324
+ else {
1325
+ steps.push({
1326
+ name: "Generate kspec-agents.md",
1327
+ status: "failed",
1328
+ message: "No kspec project found",
1329
+ });
1330
+ }
1331
+ // Step 6: Configure author (optional, used by setup command)
1332
+ if (options.configureAuthor) {
1333
+ const author = options.author || getDefaultAuthor(detected.type);
1334
+ if (!options.force && process.env.KSPEC_AUTHOR) {
1335
+ steps.push({
1336
+ name: "Configure author",
1337
+ status: "skipped",
1338
+ message: `KSPEC_AUTHOR already set to "${process.env.KSPEC_AUTHOR}"`,
1339
+ });
1340
+ }
1341
+ else {
1342
+ let authorInstalled = false;
1343
+ switch (detected.type) {
1344
+ case "claude-code":
1345
+ if (!dryRun) {
1346
+ authorInstalled = await installClaudeCodeConfig(author);
1347
+ }
1348
+ else {
1349
+ authorInstalled = true;
1350
+ }
1351
+ break;
1352
+ case "aider":
1353
+ if (!dryRun) {
1354
+ authorInstalled = await installAiderConfig(author);
1355
+ }
1356
+ else {
1357
+ authorInstalled = true;
1358
+ }
1359
+ break;
1360
+ default:
1361
+ break;
1362
+ }
1363
+ if (authorInstalled) {
1364
+ steps.push({
1365
+ name: "Configure author",
1366
+ status: "done",
1367
+ message: `KSPEC_AUTHOR="${author}"`,
1368
+ });
1369
+ }
1370
+ }
1371
+ }
1372
+ // Output summary
1373
+ if (!dryRun) {
1374
+ console.log(chalk.bold("kspec Setup Summary\n"));
1375
+ for (const step of steps) {
1376
+ const icon = step.status === "done"
1377
+ ? chalk.green("✓")
1378
+ : step.status === "skipped"
1379
+ ? chalk.gray("○")
1380
+ : chalk.red("✗");
1381
+ const statusText = step.status === "done"
1382
+ ? ""
1383
+ : step.status === "skipped"
1384
+ ? chalk.gray(" (skipped)")
1385
+ : chalk.red(" (failed)");
1386
+ console.log(`${icon} ${step.name}${statusText}`);
1387
+ if (step.message) {
1388
+ console.log(chalk.gray(` ${step.message}`));
1389
+ }
1390
+ }
1391
+ }
1392
+ const success = steps.every((s) => s.status !== "failed");
1393
+ return {
1394
+ success,
1395
+ steps,
1396
+ coreSkillsInstalled,
1397
+ skillsRendered,
1398
+ hooksInstalled,
1399
+ agentsMdGenerated,
1400
+ permissionsSeeded,
1401
+ memorySeeded,
1402
+ };
1403
+ }
1404
+ catch (err) {
1405
+ debugLog("runSetupPipeline failed", err);
1406
+ return {
1407
+ success: false,
1408
+ steps,
1409
+ coreSkillsInstalled,
1410
+ skillsRendered,
1411
+ hooksInstalled,
1412
+ agentsMdGenerated,
1413
+ permissionsSeeded,
1414
+ memorySeeded,
1415
+ };
471
1416
  }
472
1417
  }
473
1418
  /**
474
1419
  * Register the 'setup' command
1420
+ * AC: @enhanced-setup ac-1 through ac-9
475
1421
  */
476
1422
  export function registerSetupCommand(program) {
477
1423
  program
478
- .command('setup')
479
- .description('Configure agent environment for kspec')
480
- .option('--dry-run', 'Show what would be done without making changes')
481
- .option('--author <author>', 'Custom author string (default: auto-detected based on agent)')
482
- .option('--no-hooks', 'Skip installing Claude Code stop hook')
483
- .option('--force', 'Overwrite existing configuration')
484
- .option('--auto-worktree', 'Automatically create .kspec worktree if kspec-meta branch exists')
1424
+ .command("setup")
1425
+ .description("Configure agent environment for kspec (orchestrated pipeline)")
1426
+ .option("--dry-run", "Show what would be done without making changes")
1427
+ .option("--author <author>", "Custom author string (default: auto-detected based on agent)")
1428
+ .option("--no-hooks", "Skip installing hooks")
1429
+ .option("--skip-skills", "Skip rendering skills")
1430
+ .option("--status", "Report current setup state without making changes")
1431
+ .option("--force", "Overwrite existing configuration")
1432
+ .option("--auto-worktree", "Automatically create .kspec worktree if kspec-meta branch exists")
485
1433
  .action(async (options) => {
486
1434
  try {
1435
+ const projectDir = process.cwd();
1436
+ // AC: @enhanced-setup ac-7, ac-8 - --status mode
1437
+ if (options.status) {
1438
+ const status = await getSetupStatus(projectDir);
1439
+ output(status, () => {
1440
+ console.log(chalk.bold("kspec Setup Status\n"));
1441
+ // Agent detection
1442
+ console.log(chalk.gray("Agent:"));
1443
+ console.log(` Detected: ${status.agent.detected} (${status.agent.confidence} confidence)`);
1444
+ console.log();
1445
+ // Hooks status
1446
+ console.log(chalk.gray("Hooks:"));
1447
+ console.log(` UserPromptSubmit: ${status.hooks.promptCheck ? chalk.green("✓") : chalk.red("✗")}`);
1448
+ console.log(` Stop: ${status.hooks.stop ? chalk.green("✓") : chalk.red("✗")}`);
1449
+ console.log(` PreToolUse: ${status.hooks.preToolUse ? chalk.green("✓") : chalk.red("✗")}`);
1450
+ if (status.hooks.guardsPresent.length > 0) {
1451
+ console.log(` Guards: ${status.hooks.guardsPresent.join(", ")}`);
1452
+ }
1453
+ console.log();
1454
+ // Skills status
1455
+ console.log(chalk.gray("Skills:"));
1456
+ console.log(` Rendered: ${status.skills.rendered}`);
1457
+ if (status.skills.drifted > 0) {
1458
+ console.log(` Drifted: ${chalk.yellow(status.skills.drifted.toString())}`);
1459
+ }
1460
+ console.log();
1461
+ // Plugin marketplace status
1462
+ // AC: @enhanced-setup ac-7, ac-8
1463
+ console.log(chalk.gray("Plugin:"));
1464
+ console.log(` Marketplace: ${status.plugin.marketplaceRegistered ? (status.plugin.marketplaceHealthy ? chalk.green("healthy") : chalk.yellow("registered")) : chalk.red("not registered")}`);
1465
+ console.log(` Enabled: ${status.plugin.pluginEnabled ? chalk.green("✓") : chalk.red("✗")}`);
1466
+ if (status.plugin.registeredPath) {
1467
+ console.log(chalk.gray(` Path: ${status.plugin.registeredPath}`));
1468
+ }
1469
+ if (status.plugin.healthMessage && !status.plugin.marketplaceHealthy) {
1470
+ console.log(chalk.yellow(` ${status.plugin.healthMessage}`));
1471
+ }
1472
+ console.log();
1473
+ // Agents.md status
1474
+ console.log(chalk.gray("kspec-agents.md:"));
1475
+ if (status.agentsMd.exists) {
1476
+ // AC: @doctor-command ac-staleness-unknown — show appropriate color for unknown status
1477
+ const statusColor = status.agentsMd.status === "current"
1478
+ ? chalk.green
1479
+ : status.agentsMd.status === "unknown"
1480
+ ? chalk.yellow
1481
+ : chalk.yellow;
1482
+ console.log(` Status: ${statusColor(status.agentsMd.status)}`);
1483
+ if (status.agentsMd.status === "unknown") {
1484
+ console.log(chalk.gray(" Could not determine staleness (no manifest or hash unavailable)"));
1485
+ }
1486
+ if (status.agentsMd.generatedAt) {
1487
+ console.log(` Generated: ${status.agentsMd.generatedAt}`);
1488
+ }
1489
+ }
1490
+ else {
1491
+ console.log(` Status: ${chalk.red("missing")}`);
1492
+ console.log(chalk.gray(" Run 'kspec setup' to generate"));
1493
+ }
1494
+ console.log();
1495
+ // Seeding status
1496
+ console.log(chalk.gray("Seeding:"));
1497
+ console.log(` Permissions: ${status.seeding.permissionsSeeded ? chalk.green("✓") : chalk.gray("○")}`);
1498
+ console.log(` Memory: ${status.seeding.memorySeeded ? chalk.green("✓") : chalk.gray("○")}`);
1499
+ if (status.seeding.memoryPath) {
1500
+ console.log(chalk.gray(` Path: ${status.seeding.memoryPath}`));
1501
+ }
1502
+ });
1503
+ return;
1504
+ }
487
1505
  // AC: detect-existing-repo, auto-worktree-flag, worktree-already-exists
488
1506
  const worktreeReady = await ensureWorktree(options.autoWorktree || false);
489
1507
  if (!worktreeReady) {
@@ -491,91 +1509,66 @@ export function registerSetupCommand(program) {
491
1509
  process.exit(EXIT_CODES.ERROR);
492
1510
  }
493
1511
  const detected = detectAgent();
494
- const projectDir = process.cwd();
495
- console.log(`Detected agent: ${detected.type} (confidence: ${detected.confidence})`);
496
- if (detected.type === 'unknown') {
497
- warn('Could not auto-detect agent environment');
498
- printManualInstructions('unknown');
1512
+ const dryRun = options.dryRun || false;
1513
+ if (detected.type === "unknown") {
1514
+ warn("Could not auto-detect agent environment");
1515
+ printManualInstructions("unknown");
499
1516
  return;
500
1517
  }
501
- const author = options.author || getDefaultAuthor(detected.type);
502
- const installHooks = options.hooks !== false && detected.type === 'claude-code';
503
- if (options.dryRun) {
504
- console.log(`\nWould configure:`);
505
- console.log(` Agent: ${detected.type}`);
506
- console.log(` Author: ${author}`);
507
- if (detected.configPath) {
508
- console.log(` Global config: ${detected.configPath}`);
1518
+ // AC: @setup-pipeline-unification ac-3 - delegate to runSetupPipeline()
1519
+ // One code path for both 'kspec setup' and 'kspec init --setup'
1520
+ const result = await runSetupPipeline(projectDir, {
1521
+ dryRun,
1522
+ skipSkills: options.skipSkills || false,
1523
+ installHooks: options.hooks !== false,
1524
+ force: options.force || false,
1525
+ author: options.author,
1526
+ configureAuthor: true,
1527
+ });
1528
+ // AC: @enhanced-setup ac-1 - Display summary
1529
+ // AC: @enhanced-setup ac-6 - dry-run displays planned actions
1530
+ output({
1531
+ dry_run: dryRun,
1532
+ steps: result.steps.map((s) => ({
1533
+ name: s.name,
1534
+ status: s.status,
1535
+ message: s.message,
1536
+ details: s.details,
1537
+ })),
1538
+ }, () => {
1539
+ if (dryRun) {
1540
+ console.log(chalk.yellow("DRY RUN - No changes made\n"));
509
1541
  }
510
- if (installHooks) {
511
- console.log(` Project config: ${path.join(projectDir, '.claude', 'settings.json')}`);
512
- console.log(` Hooks: UserPromptSubmit (prompt-check), Stop (checkpoint)`);
513
- }
514
- return;
515
- }
516
- // Check if already configured
517
- if (!options.force && process.env.KSPEC_AUTHOR) {
518
- warn(`KSPEC_AUTHOR is already set to "${process.env.KSPEC_AUTHOR}"`);
519
- console.log('Use --force to overwrite');
520
- return;
521
- }
522
- // Install config based on agent type
523
- let installed = false;
524
- let hooksInstalled = false;
525
- let hooksResult = null;
526
- switch (detected.type) {
527
- case 'claude-code':
528
- installed = await installClaudeCodeConfig(author);
529
- if (installHooks) {
530
- hooksResult = await installClaudeCodeHooks(projectDir);
531
- hooksInstalled = hooksResult.stop || hooksResult.promptCheck;
1542
+ // Pipeline already prints the summary when not dry-run
1543
+ // For dry-run, print it here since the pipeline skips output
1544
+ if (dryRun) {
1545
+ console.log(chalk.bold("kspec Setup Summary\n"));
1546
+ for (const step of result.steps) {
1547
+ const icon = step.status === "done"
1548
+ ? chalk.green("✓")
1549
+ : step.status === "skipped"
1550
+ ? chalk.gray("○")
1551
+ : chalk.red("✗");
1552
+ const statusText = step.status === "done"
1553
+ ? ""
1554
+ : step.status === "skipped"
1555
+ ? chalk.gray(" (skipped)")
1556
+ : chalk.red(" (failed)");
1557
+ console.log(`${icon} ${step.name}${statusText}`);
1558
+ if (step.message) {
1559
+ console.log(chalk.gray(` ${step.message}`));
1560
+ }
532
1561
  }
533
- break;
534
- case 'aider':
535
- installed = await installAiderConfig(author);
536
- break;
537
- case 'cline':
538
- case 'roo-code':
539
- // These VS Code extensions use shell env vars, not config files
540
- // Can't auto-install, must print instructions
541
- printManualInstructions(detected.type);
542
- return;
543
- case 'copilot-cli':
544
- case 'gemini-cli':
545
- case 'opencode':
546
- case 'amp':
547
- if (detected.configPath) {
548
- installed = await installGenericJsonConfig(detected.configPath, author);
549
- }
550
- break;
551
- case 'codex-cli':
552
- // Codex uses TOML config, would need special handling
553
- // For now, print manual instructions
554
- printManualInstructions(detected.type);
555
- return;
556
- }
557
- if (installed) {
558
- success(`Configured ${detected.type} with KSPEC_AUTHOR="${author}"`, {
559
- agent: detected.type,
560
- author,
561
- configPath: detected.configPath,
562
- });
563
- if (hooksInstalled && hooksResult) {
564
- const installedHooks = [];
565
- if (hooksResult.promptCheck)
566
- installedHooks.push('UserPromptSubmit (prompt-check)');
567
- if (hooksResult.stop)
568
- installedHooks.push('Stop (checkpoint)');
569
- success(`Installed hooks to .claude/settings.json`, {
570
- hooks: installedHooks,
571
- });
572
1562
  }
573
- console.log('\nRestart your agent session for changes to take effect.');
574
- }
575
- else {
576
- error(errors.failures.installConfig(detected.type));
577
- printManualInstructions(detected.type);
578
- }
1563
+ console.log();
1564
+ if (dryRun) {
1565
+ console.log(chalk.yellow("Run without --dry-run to apply changes."));
1566
+ }
1567
+ else {
1568
+ console.log(chalk.green("Setup complete."));
1569
+ console.log(chalk.gray("Restart your agent session for changes to take effect."));
1570
+ }
1571
+ });
579
1572
  }
580
1573
  catch (err) {
581
1574
  error(errors.failures.setupFailed, err);