@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,14 +1,40 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import { execSync } from 'node:child_process';
4
- import * as YAML from 'yaml';
5
- import { ulid } from 'ulid';
6
- import { TaskSchema, TasksFileSchema, ManifestSchema, SpecItemSchema, InboxItemSchema, InboxFileSchema, } from '../schema/index.js';
7
- import { ReferenceIndex } from './refs.js';
8
- import { ItemIndex } from './items.js';
9
- import { TraitIndex } from './traits.js';
10
- import { detectShadow, detectRunningFromShadowWorktree, ShadowError, } from './shadow.js';
11
- import { errors } from '../strings/index.js';
1
+ import { execSync } from "node:child_process";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { ulid } from "ulid";
5
+ import * as YAML from "yaml";
6
+ import { InboxFileSchema, InboxItemSchema, ManifestSchema, SpecItemSchema, TaskSchema, TasksFileSchema, TriageFileSchema, TriageRecordSchema, } from "../schema/index.js";
7
+ import { errors } from "../strings/index.js";
8
+ import { ItemIndex } from "./items.js";
9
+ import { ReferenceIndex } from "./refs.js";
10
+ import { detectRunningFromShadowWorktree, detectShadow, ShadowError, } from "./shadow.js";
11
+ import { loadProjectConfig, } from "./config.js";
12
+ import { TraitIndex } from "./traits.js";
13
+ /**
14
+ * Log a debug message (only when KSPEC_DEBUG=1)
15
+ */
16
+ function debugLog(prefix, message) {
17
+ if (process.env.KSPEC_DEBUG === "1") {
18
+ console.error(`[DEBUG] ${prefix}: ${message}`);
19
+ }
20
+ }
21
+ /**
22
+ * Parse a manifest and emit deprecation warnings for deprecated fields.
23
+ *
24
+ * AC: @config-manifest-cleanup ac-4 — debug-level deprecation note for daemon block
25
+ */
26
+ function parseManifestWithWarnings(rawManifest) {
27
+ const manifest = ManifestSchema.parse(rawManifest);
28
+ // AC: @config-manifest-cleanup ac-4 — log debug-level deprecation note for daemon block
29
+ if (manifest.daemon) {
30
+ debugLog("manifest", 'Deprecated "daemon" block found in manifest. Use kspec.config.yaml instead.');
31
+ }
32
+ // Also warn for deprecated config block
33
+ if (manifest.config) {
34
+ debugLog("manifest", 'Deprecated "config" block found in manifest. Use kspec.config.yaml instead.');
35
+ }
36
+ return manifest;
37
+ }
12
38
  /**
13
39
  * Parse YAML content into an object
14
40
  * Uses the modern yaml library which has consistent type handling
@@ -35,16 +61,16 @@ export function toYaml(obj) {
35
61
  // Post-process to fix yaml library blank line accumulation bug.
36
62
  // Filter out lines that contain only spaces/tabs (not truly empty lines).
37
63
  yamlString = yamlString
38
- .split('\n')
39
- .filter(line => !/^[ \t]+$/.test(line))
40
- .join('\n');
64
+ .split("\n")
65
+ .filter((line) => !/^[ \t]+$/.test(line))
66
+ .join("\n");
41
67
  return yamlString;
42
68
  }
43
69
  /**
44
70
  * Read and parse a YAML file
45
71
  */
46
72
  export async function readYamlFile(filePath) {
47
- const content = await fs.readFile(filePath, 'utf-8');
73
+ const content = await fs.readFile(filePath, "utf-8");
48
74
  return parseYaml(content);
49
75
  }
50
76
  /**
@@ -52,7 +78,7 @@ export async function readYamlFile(filePath) {
52
78
  */
53
79
  export async function writeYamlFile(filePath, data) {
54
80
  const content = toYaml(data);
55
- await fs.writeFile(filePath, content, 'utf-8');
81
+ await fs.writeFile(filePath, content, "utf-8");
56
82
  }
57
83
  /**
58
84
  * Write object to YAML file while preserving formatting and comments.
@@ -63,7 +89,7 @@ export async function writeYamlFile(filePath, data) {
63
89
  */
64
90
  export async function writeYamlFilePreserveFormat(filePath, data) {
65
91
  const content = toYaml(data);
66
- await fs.writeFile(filePath, content, 'utf-8');
92
+ await fs.writeFile(filePath, content, "utf-8");
67
93
  }
68
94
  /**
69
95
  * Find task files in a directory
@@ -79,45 +105,47 @@ export async function findTaskFiles(dir) {
79
105
  const subFiles = await findTaskFiles(fullPath);
80
106
  files.push(...subFiles);
81
107
  }
82
- else if (entry.isFile() && entry.name.endsWith('.tasks.yaml')) {
108
+ else if (entry.isFile() && entry.name.endsWith(".tasks.yaml")) {
83
109
  files.push(fullPath);
84
110
  }
85
111
  }
86
112
  }
87
- catch (error) {
113
+ catch (_error) {
88
114
  // Directory doesn't exist or not readable
89
115
  }
90
116
  return files;
91
117
  }
92
118
  /**
93
- * Find the manifest file (kynetic.yaml or kynetic.spec.yaml)
119
+ * Find the manifest file.
120
+ *
121
+ * Discovery algorithm per directory:
122
+ * 1. Check for explicit names: kynetic.yaml, kynetic.spec.yaml (backward compat)
123
+ * 2. If not found, scan for *.yaml files with 'kynetic:' version field
124
+ *
125
+ * Searches current dir, then spec/ subdir, then parent directories.
94
126
  */
95
127
  export async function findManifest(startDir) {
96
128
  let dir = startDir;
97
129
  while (true) {
98
- const candidates = ['kynetic.yaml', 'kynetic.spec.yaml'];
99
- for (const candidate of candidates) {
100
- const filePath = path.join(dir, candidate);
101
- try {
102
- await fs.access(filePath);
103
- return filePath;
104
- }
105
- catch {
106
- // File doesn't exist, try next
107
- }
130
+ // Check current directory
131
+ const manifestInDir = await findManifestInDir(dir);
132
+ if (manifestInDir) {
133
+ return manifestInDir;
108
134
  }
109
135
  // Also check in spec/ subdirectory
110
- const specDir = path.join(dir, 'spec');
111
- for (const candidate of candidates) {
112
- const filePath = path.join(specDir, candidate);
113
- try {
114
- await fs.access(filePath);
115
- return filePath;
116
- }
117
- catch {
118
- // File doesn't exist, try next
136
+ const specDir = path.join(dir, "spec");
137
+ try {
138
+ const stat = await fs.stat(specDir);
139
+ if (stat.isDirectory()) {
140
+ const manifestInSpec = await findManifestInDir(specDir);
141
+ if (manifestInSpec) {
142
+ return manifestInSpec;
143
+ }
119
144
  }
120
145
  }
146
+ catch {
147
+ // spec/ doesn't exist
148
+ }
121
149
  const parentDir = path.dirname(dir);
122
150
  if (parentDir === dir) {
123
151
  // Reached root
@@ -130,20 +158,60 @@ export async function findManifest(startDir) {
130
158
  * Initialize context by finding manifest.
131
159
  *
132
160
  * Detection order:
133
- * 1. Check for shadow branch (.kspec/ directory)
134
- * 2. Fall back to traditional spec/ directory
161
+ * 1. Load project config from git root (before shadow detection)
162
+ * 2. Check for shadow branch (.kspec/ directory)
163
+ * 3. Fall back to traditional spec/ directory
135
164
  *
136
165
  * When shadow is detected, all operations use .kspec/ as specDir.
166
+ *
167
+ * AC: @project-config ac-2 — config loaded before shadow detection
137
168
  */
138
169
  export async function initContext(startDir) {
139
170
  const cwd = startDir || process.cwd();
171
+ // AC: @project-config ac-2, ac-6, ac-7 — load config before shadow detection
172
+ // Config is loaded from git root, not cwd or KSPEC_SPEC_DIR temp dir
173
+ const configResult = await loadProjectConfig(cwd);
174
+ // AC: @project-config ac-3 — emit warning to stderr if config had issues
175
+ if (configResult.warning) {
176
+ console.error(`Warning: ${configResult.warning}`);
177
+ }
178
+ const { config } = configResult;
179
+ // KSPEC_SPEC_DIR override: used by batch atomic mode to redirect to temp copy
180
+ const specDirOverride = process.env.KSPEC_SPEC_DIR;
181
+ if (specDirOverride) {
182
+ const specDir = path.resolve(specDirOverride);
183
+ const manifestPath = await findManifestInDir(specDir);
184
+ let manifest = null;
185
+ if (manifestPath) {
186
+ try {
187
+ const rawManifest = await readYamlFile(manifestPath);
188
+ manifest = parseManifestWithWarnings(rawManifest);
189
+ }
190
+ catch {
191
+ // Manifest exists but may be invalid
192
+ }
193
+ }
194
+ return {
195
+ rootDir: path.dirname(specDir),
196
+ specDir,
197
+ manifestPath,
198
+ manifest,
199
+ shadow: null, // No shadow in overridden context
200
+ config,
201
+ };
202
+ }
140
203
  // Check if running from inside the shadow worktree
141
- const mainProjectRoot = await detectRunningFromShadowWorktree(cwd);
204
+ // AC: @config-shadow ac-8 — pass configured directory for detection
205
+ const mainProjectRoot = await detectRunningFromShadowWorktree(cwd, config.shadow.directory);
142
206
  if (mainProjectRoot) {
143
- throw new ShadowError(errors.project.runningFromShadow, 'RUNNING_FROM_SHADOW', `Run from project root: cd ${path.relative(cwd, mainProjectRoot) || mainProjectRoot}`);
207
+ throw new ShadowError(errors.project.runningFromShadow, "RUNNING_FROM_SHADOW", `Run from project root: cd ${path.relative(cwd, mainProjectRoot) || mainProjectRoot}`);
144
208
  }
145
209
  // Try to detect shadow branch first
146
- const shadow = await detectShadow(cwd);
210
+ // AC: @config-shadow ac-1 ac-2 — use configured branch/directory names
211
+ const shadow = await detectShadow(cwd, {
212
+ branchName: config.shadow.branch,
213
+ directory: config.shadow.directory,
214
+ });
147
215
  if (shadow?.enabled) {
148
216
  // Shadow mode: use .kspec/ for everything
149
217
  const specDir = shadow.worktreeDir;
@@ -152,7 +220,7 @@ export async function initContext(startDir) {
152
220
  if (manifestPath) {
153
221
  try {
154
222
  const rawManifest = await readYamlFile(manifestPath);
155
- manifest = ManifestSchema.parse(rawManifest);
223
+ manifest = parseManifestWithWarnings(rawManifest);
156
224
  }
157
225
  catch {
158
226
  // Manifest exists but may be invalid
@@ -164,6 +232,7 @@ export async function initContext(startDir) {
164
232
  manifestPath,
165
233
  manifest,
166
234
  shadow,
235
+ config,
167
236
  };
168
237
  }
169
238
  // Traditional mode: find manifest in spec/ or current directory
@@ -174,7 +243,7 @@ export async function initContext(startDir) {
174
243
  if (manifestPath) {
175
244
  const manifestDir = path.dirname(manifestPath);
176
245
  // Handle spec/ subdirectory
177
- if (path.basename(manifestDir) === 'spec') {
246
+ if (path.basename(manifestDir) === "spec") {
178
247
  rootDir = path.dirname(manifestDir);
179
248
  specDir = manifestDir;
180
249
  }
@@ -184,21 +253,42 @@ export async function initContext(startDir) {
184
253
  }
185
254
  try {
186
255
  const rawManifest = await readYamlFile(manifestPath);
187
- manifest = ManifestSchema.parse(rawManifest);
256
+ manifest = parseManifestWithWarnings(rawManifest);
188
257
  }
189
258
  catch {
190
259
  // Manifest exists but may be invalid
191
260
  }
192
261
  }
193
- return { rootDir, specDir, manifestPath, manifest, shadow: null };
262
+ return { rootDir, specDir, manifestPath, manifest, shadow: null, config };
263
+ }
264
+ /**
265
+ * Check if a filename is a potential manifest file.
266
+ * Excludes files with suffixes that indicate other kspec file types.
267
+ *
268
+ * AC: @manifest-discovery ac-5 (excludes task/inbox/meta/runs files)
269
+ */
270
+ function isManifestCandidate(filename) {
271
+ if (!filename.endsWith(".yaml"))
272
+ return false;
273
+ const exclusions = [".tasks.yaml", ".inbox.yaml", ".meta.yaml", ".runs.yaml"];
274
+ return !exclusions.some((excl) => filename.endsWith(excl));
194
275
  }
195
276
  /**
196
277
  * Find manifest file within a specific directory (no parent traversal).
197
278
  * Used for shadow mode where we know exactly where to look.
279
+ *
280
+ * Discovery algorithm:
281
+ * 1. Check for explicit names: kynetic.yaml, kynetic.spec.yaml (backward compat)
282
+ * 2. If not found, scan directory for *.yaml files (excluding other kspec types)
283
+ * 3. For each candidate, validate it contains a 'kynetic:' version field
284
+ * 4. Return first valid match (alphabetically after explicit names)
285
+ *
286
+ * AC: @manifest-discovery ac-1, ac-2, ac-3, ac-4, ac-5
198
287
  */
199
288
  async function findManifestInDir(dir) {
200
- const candidates = ['kynetic.yaml', 'kynetic.spec.yaml'];
201
- for (const candidate of candidates) {
289
+ // AC: @manifest-discovery ac-1, ac-2 - explicit names have priority
290
+ const priorityCandidates = ["kynetic.yaml", "kynetic.spec.yaml"];
291
+ for (const candidate of priorityCandidates) {
202
292
  const filePath = path.join(dir, candidate);
203
293
  try {
204
294
  await fs.access(filePath);
@@ -208,6 +298,28 @@ async function findManifestInDir(dir) {
208
298
  // File doesn't exist, try next
209
299
  }
210
300
  }
301
+ // AC: @manifest-discovery ac-3, ac-4, ac-5 - glob fallback with validation
302
+ try {
303
+ const entries = await fs.readdir(dir);
304
+ // AC: @manifest-discovery ac-4 - alphabetical order
305
+ const candidates = entries.filter(isManifestCandidate).sort();
306
+ for (const candidate of candidates) {
307
+ const filePath = path.join(dir, candidate);
308
+ try {
309
+ const raw = await readYamlFile(filePath);
310
+ // AC: @manifest-discovery ac-5 - validate kynetic version field
311
+ if (raw && typeof raw === "object" && "kynetic" in raw) {
312
+ return filePath;
313
+ }
314
+ }
315
+ catch {
316
+ // Skip invalid files
317
+ }
318
+ }
319
+ }
320
+ catch {
321
+ // Directory read failed
322
+ }
211
323
  return null;
212
324
  }
213
325
  /**
@@ -223,7 +335,7 @@ async function loadTasksFromFile(filePath) {
223
335
  if (Array.isArray(raw)) {
224
336
  taskList = raw;
225
337
  }
226
- else if (raw && typeof raw === 'object' && 'tasks' in raw) {
338
+ else if (raw && typeof raw === "object" && "tasks" in raw) {
227
339
  const parsed = TasksFileSchema.safeParse(raw);
228
340
  if (parsed.success) {
229
341
  // Add _sourceFile to each task from this file
@@ -265,11 +377,11 @@ export async function loadAllTasks(ctx) {
265
377
  const taskFiles = await findTaskFiles(ctx.specDir);
266
378
  // Also check for standalone files in specDir
267
379
  const standaloneLocations = [
268
- path.join(ctx.specDir, 'tasks.yaml'),
269
- path.join(ctx.specDir, 'project.tasks.yaml'),
270
- path.join(ctx.specDir, 'kynetic.tasks.yaml'),
271
- path.join(ctx.specDir, 'backlog.tasks.yaml'),
272
- path.join(ctx.specDir, 'active.tasks.yaml'),
380
+ path.join(ctx.specDir, "tasks.yaml"),
381
+ path.join(ctx.specDir, "project.tasks.yaml"),
382
+ path.join(ctx.specDir, "kynetic.tasks.yaml"),
383
+ path.join(ctx.specDir, "backlog.tasks.yaml"),
384
+ path.join(ctx.specDir, "active.tasks.yaml"),
273
385
  ];
274
386
  for (const loc of standaloneLocations) {
275
387
  try {
@@ -294,8 +406,8 @@ export async function loadAllTasks(ctx) {
294
406
  const taskFiles = await findTaskFiles(ctx.rootDir);
295
407
  // Also check common locations
296
408
  const additionalPaths = [
297
- path.join(ctx.rootDir, 'tasks'),
298
- path.join(ctx.rootDir, 'spec'),
409
+ path.join(ctx.rootDir, "tasks"),
410
+ path.join(ctx.rootDir, "spec"),
299
411
  ];
300
412
  for (const additionalPath of additionalPaths) {
301
413
  const files = await findTaskFiles(additionalPath);
@@ -303,11 +415,11 @@ export async function loadAllTasks(ctx) {
303
415
  }
304
416
  // Also look for standalone tasks.yaml and project.tasks.yaml
305
417
  const standaloneLocations = [
306
- path.join(ctx.rootDir, 'tasks.yaml'),
307
- path.join(ctx.rootDir, 'project.tasks.yaml'),
308
- path.join(ctx.rootDir, 'spec', 'project.tasks.yaml'),
309
- path.join(ctx.rootDir, 'backlog.tasks.yaml'),
310
- path.join(ctx.rootDir, 'active.tasks.yaml'),
418
+ path.join(ctx.rootDir, "tasks.yaml"),
419
+ path.join(ctx.rootDir, "project.tasks.yaml"),
420
+ path.join(ctx.rootDir, "spec", "project.tasks.yaml"),
421
+ path.join(ctx.rootDir, "backlog.tasks.yaml"),
422
+ path.join(ctx.rootDir, "active.tasks.yaml"),
311
423
  ];
312
424
  for (const loc of standaloneLocations) {
313
425
  try {
@@ -333,8 +445,8 @@ export async function loadAllTasks(ctx) {
333
445
  */
334
446
  export function findTaskByRef(tasks, ref) {
335
447
  // Remove @ prefix if present
336
- const cleanRef = ref.startsWith('@') ? ref.slice(1) : ref;
337
- return tasks.find(task => {
448
+ const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
449
+ return tasks.find((task) => {
338
450
  // Match full ULID
339
451
  if (task._ulid === cleanRef)
340
452
  return true;
@@ -354,7 +466,7 @@ export function findTaskByRef(tasks, ref) {
354
466
  * Otherwise: spec/project.tasks.yaml
355
467
  */
356
468
  export function getDefaultTaskFilePath(ctx) {
357
- return path.join(ctx.specDir, 'project.tasks.yaml');
469
+ return path.join(ctx.specDir, "project.tasks.yaml");
358
470
  }
359
471
  /**
360
472
  * Strip runtime metadata before serialization
@@ -379,7 +491,9 @@ export async function saveTask(ctx, task) {
379
491
  try {
380
492
  existingRaw = await readYamlFile(taskFilePath);
381
493
  // Detect if file uses { tasks: [...] } format
382
- if (existingRaw && typeof existingRaw === 'object' && 'tasks' in existingRaw) {
494
+ if (existingRaw &&
495
+ typeof existingRaw === "object" &&
496
+ "tasks" in existingRaw) {
383
497
  useTasksWrapper = true;
384
498
  }
385
499
  }
@@ -420,7 +534,7 @@ export async function saveTask(ctx, task) {
420
534
  // Strip runtime metadata before saving
421
535
  const cleanTask = stripRuntimeMetadata(task);
422
536
  // Update existing or add new
423
- const existingIndex = fileTasks.findIndex(t => t._ulid === task._ulid);
537
+ const existingIndex = fileTasks.findIndex((t) => t._ulid === task._ulid);
424
538
  if (existingIndex >= 0) {
425
539
  fileTasks[existingIndex] = cleanTask;
426
540
  }
@@ -440,9 +554,9 @@ export async function saveTask(ctx, task) {
440
554
  * Delete a task from its source file.
441
555
  * Requires _sourceFile to know which file to modify.
442
556
  */
443
- export async function deleteTask(ctx, task) {
557
+ export async function deleteTask(_ctx, task) {
444
558
  if (!task._sourceFile) {
445
- throw new Error('Cannot delete task without _sourceFile metadata');
559
+ throw new Error("Cannot delete task without _sourceFile metadata");
446
560
  }
447
561
  const taskFilePath = task._sourceFile;
448
562
  // Load existing file
@@ -450,7 +564,9 @@ export async function deleteTask(ctx, task) {
450
564
  let useTasksWrapper = false;
451
565
  try {
452
566
  existingRaw = await readYamlFile(taskFilePath);
453
- if (existingRaw && typeof existingRaw === 'object' && 'tasks' in existingRaw) {
567
+ if (existingRaw &&
568
+ typeof existingRaw === "object" &&
569
+ "tasks" in existingRaw) {
454
570
  useTasksWrapper = true;
455
571
  }
456
572
  }
@@ -488,7 +604,7 @@ export async function deleteTask(ctx, task) {
488
604
  }
489
605
  // Remove the task
490
606
  const originalCount = fileTasks.length;
491
- fileTasks = fileTasks.filter(t => t._ulid !== task._ulid);
607
+ fileTasks = fileTasks.filter((t) => t._ulid !== task._ulid);
492
608
  if (fileTasks.length === originalCount) {
493
609
  throw new Error(`Task not found in file: ${task._ulid}`);
494
610
  }
@@ -509,8 +625,8 @@ export function createTask(input) {
509
625
  ...input,
510
626
  _ulid: input._ulid || ulid(),
511
627
  slugs: input.slugs || [],
512
- type: input.type || 'task',
513
- status: input.status || 'pending',
628
+ type: input.type || "task",
629
+ status: input.status || "pending",
514
630
  blocked_by: input.blocked_by || [],
515
631
  depends_on: input.depends_on || [],
516
632
  context: input.context || [],
@@ -526,23 +642,32 @@ export function createTask(input) {
526
642
  * Get author from environment with fallback chain.
527
643
  * Priority:
528
644
  * 1. KSPEC_AUTHOR env var (explicit config, agent-agnostic)
529
- * 2. git user.name (developer identity)
530
- * 3. USER/USERNAME env var (system user)
531
- * 4. undefined (will show as 'unknown' in output)
645
+ * 2. kspec.config.yaml identity.author (project-level default)
646
+ * 3. git user.name (developer identity)
647
+ * 4. USER/USERNAME env var (system user)
648
+ * 5. undefined (will show as 'unknown' in output)
532
649
  *
533
650
  * For Claude Code integration, add to ~/.claude/settings.json:
534
651
  * { "env": { "KSPEC_AUTHOR": "@claude" } }
652
+ *
653
+ * AC: @config-author ac-1 ac-2 ac-3 — author priority chain
654
+ *
655
+ * @param configAuthor Optional author from kspec.config.yaml identity.author
535
656
  */
536
- export function getAuthor() {
537
- // 1. Explicit config (works for any agent)
657
+ export function getAuthor(configAuthor) {
658
+ // 1. Explicit env var (works for any agent) — AC: ac-2
538
659
  if (process.env.KSPEC_AUTHOR) {
539
660
  return process.env.KSPEC_AUTHOR;
540
661
  }
541
- // 2. Git user.name
662
+ // 2. Project config author — AC: ac-1
663
+ if (configAuthor) {
664
+ return configAuthor;
665
+ }
666
+ // 3. Git user.name — AC: ac-3
542
667
  try {
543
- const gitUser = execSync('git config user.name', {
544
- encoding: 'utf-8',
545
- stdio: ['pipe', 'pipe', 'ignore'],
668
+ const gitUser = execSync("git config user.name", {
669
+ encoding: "utf-8",
670
+ stdio: ["pipe", "pipe", "ignore"],
546
671
  }).trim();
547
672
  if (gitUser) {
548
673
  return gitUser;
@@ -551,12 +676,12 @@ export function getAuthor() {
551
676
  catch {
552
677
  // git not available or not in a repo
553
678
  }
554
- // 3. System user
679
+ // 4. System user — AC: ac-3
555
680
  const systemUser = process.env.USER || process.env.USERNAME;
556
681
  if (systemUser) {
557
682
  return systemUser;
558
683
  }
559
- // 4. No author available
684
+ // 5. No author available
560
685
  return undefined;
561
686
  }
562
687
  /**
@@ -597,7 +722,7 @@ export function areDependenciesMet(task, allTasks) {
597
722
  return true;
598
723
  for (const depRef of task.depends_on) {
599
724
  const depTask = findTaskByRef(allTasks, depRef);
600
- if (!depTask || depTask.status !== 'completed') {
725
+ if (!depTask || depTask.status !== "completed") {
601
726
  return false;
602
727
  }
603
728
  }
@@ -607,7 +732,7 @@ export function areDependenciesMet(task, allTasks) {
607
732
  * Check if task is ready (pending + deps met + not blocked)
608
733
  */
609
734
  export function isTaskReady(task, allTasks) {
610
- if (task.status !== 'pending')
735
+ if (task.status !== "pending")
611
736
  return false;
612
737
  if (task.blocked_by.length > 0)
613
738
  return false;
@@ -619,14 +744,14 @@ export function isTaskReady(task, allTasks) {
619
744
  */
620
745
  export function getReadyTasks(tasks) {
621
746
  return tasks
622
- .filter(task => isTaskReady(task, tasks))
747
+ .filter((task) => isTaskReady(task, tasks))
623
748
  .sort((a, b) => {
624
749
  // Primary: priority (lower number = higher priority)
625
750
  if (a.priority !== b.priority) {
626
751
  return a.priority - b.priority;
627
752
  }
628
753
  // Secondary: creation time (older first - FIFO within priority)
629
- return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
754
+ return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
630
755
  });
631
756
  }
632
757
  // ============================================================
@@ -637,9 +762,11 @@ export function getReadyTasks(tasks) {
637
762
  * Supports simple patterns like "modules/*.yaml" or "**\/*.yaml"
638
763
  */
639
764
  export async function expandIncludePattern(pattern, baseDir) {
640
- const fullPattern = path.isAbsolute(pattern) ? pattern : path.join(baseDir, pattern);
765
+ const fullPattern = path.isAbsolute(pattern)
766
+ ? pattern
767
+ : path.join(baseDir, pattern);
641
768
  // If no glob characters, just return the path if it exists
642
- if (!pattern.includes('*')) {
769
+ if (!pattern.includes("*")) {
643
770
  try {
644
771
  await fs.access(fullPattern);
645
772
  return [fullPattern];
@@ -649,17 +776,17 @@ export async function expandIncludePattern(pattern, baseDir) {
649
776
  }
650
777
  }
651
778
  // Split pattern into directory part and file pattern
652
- const parts = pattern.split('/');
779
+ const parts = pattern.split("/");
653
780
  let currentDir = baseDir;
654
781
  const result = [];
655
782
  // Find the first part with a glob
656
- let globIndex = parts.findIndex(p => p.includes('*'));
783
+ const globIndex = parts.findIndex((p) => p.includes("*"));
657
784
  // Navigate to the directory before the glob
658
785
  if (globIndex > 0) {
659
786
  currentDir = path.join(baseDir, ...parts.slice(0, globIndex));
660
787
  }
661
788
  // Get the remaining pattern
662
- const remainingPattern = parts.slice(globIndex).join('/');
789
+ const remainingPattern = parts.slice(globIndex).join("/");
663
790
  await expandGlobRecursive(currentDir, remainingPattern, result);
664
791
  return result;
665
792
  }
@@ -667,9 +794,9 @@ export async function expandIncludePattern(pattern, baseDir) {
667
794
  * Recursively expand glob patterns
668
795
  */
669
796
  async function expandGlobRecursive(dir, pattern, result) {
670
- const parts = pattern.split('/');
797
+ const parts = pattern.split("/");
671
798
  const currentPattern = parts[0];
672
- const remainingPattern = parts.slice(1).join('/');
799
+ const remainingPattern = parts.slice(1).join("/");
673
800
  try {
674
801
  const entries = await fs.readdir(dir, { withFileTypes: true });
675
802
  for (const entry of entries) {
@@ -684,10 +811,10 @@ async function expandGlobRecursive(dir, pattern, result) {
684
811
  }
685
812
  else {
686
813
  // This is the final pattern part
687
- if (currentPattern === '**') {
814
+ if (currentPattern === "**") {
688
815
  // ** matches any depth - need special handling
689
816
  if (entry.isDirectory()) {
690
- await expandGlobRecursive(fullPath, '**', result);
817
+ await expandGlobRecursive(fullPath, "**", result);
691
818
  }
692
819
  // Also match files at this level
693
820
  result.push(fullPath);
@@ -698,7 +825,7 @@ async function expandGlobRecursive(dir, pattern, result) {
698
825
  }
699
826
  }
700
827
  // Handle ** - also recurse into directories without consuming the pattern
701
- if (currentPattern === '**' && entry.isDirectory()) {
828
+ if (currentPattern === "**" && entry.isDirectory()) {
702
829
  const fullPath = path.join(dir, entry.name);
703
830
  await expandGlobRecursive(fullPath, pattern, result);
704
831
  }
@@ -712,15 +839,15 @@ async function expandGlobRecursive(dir, pattern, result) {
712
839
  * Match a single path component against a glob pattern part
713
840
  */
714
841
  function matchGlobPart(name, pattern) {
715
- if (pattern === '*')
842
+ if (pattern === "*")
716
843
  return true;
717
- if (pattern === '**')
844
+ if (pattern === "**")
718
845
  return true;
719
846
  // Convert glob pattern to regex
720
847
  const regexPattern = pattern
721
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
722
- .replace(/\*/g, '.*') // * matches anything
723
- .replace(/\?/g, '.'); // ? matches single char
848
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape special regex chars
849
+ .replace(/\*/g, ".*") // * matches anything
850
+ .replace(/\?/g, "."); // ? matches single char
724
851
  const regex = new RegExp(`^${regexPattern}$`);
725
852
  return regex.test(name);
726
853
  }
@@ -728,26 +855,42 @@ function matchGlobPart(name, pattern) {
728
855
  * Fields that may contain nested spec items
729
856
  */
730
857
  const NESTED_ITEM_FIELDS = [
731
- 'modules',
732
- 'features',
733
- 'requirements',
734
- 'constraints',
735
- 'decisions',
736
- 'traits',
737
- 'acceptance_criteria',
858
+ "modules",
859
+ "features",
860
+ "requirements",
861
+ "constraints",
862
+ "decisions",
863
+ "traits",
864
+ "acceptance_criteria",
738
865
  ];
739
866
  /**
740
867
  * Recursively extract all spec items from a raw YAML structure.
741
868
  * Items can be nested under modules/features/requirements/etc.
742
869
  * Tracks the path within the file for each item.
743
870
  */
744
- export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = '') {
745
- if (!raw || typeof raw !== 'object') {
871
+ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = "") {
872
+ if (!raw || typeof raw !== "object") {
746
873
  return items;
747
874
  }
748
875
  // Check if this object is itself a spec item (has _ulid)
749
- if ('_ulid' in raw && typeof raw._ulid === 'string') {
750
- const result = SpecItemSchema.safeParse(raw);
876
+ if ("_ulid" in raw &&
877
+ typeof raw._ulid === "string") {
878
+ // Strip nested item arrays before validation since they're processed separately
879
+ // and the SpecItemSchema expects refs (strings), not nested objects
880
+ const rawObj = raw;
881
+ const cleanedForValidation = { ...rawObj };
882
+ for (const field of NESTED_ITEM_FIELDS) {
883
+ if (field in cleanedForValidation && Array.isArray(cleanedForValidation[field])) {
884
+ const arr = cleanedForValidation[field];
885
+ // Check if array contains nested items (objects with _ulid) vs refs (strings)
886
+ const hasNestedItems = arr.some((item) => item && typeof item === "object" && "_ulid" in item);
887
+ if (hasNestedItems) {
888
+ // Strip nested items - they'll be extracted recursively
889
+ delete cleanedForValidation[field];
890
+ }
891
+ }
892
+ }
893
+ const result = SpecItemSchema.safeParse(cleanedForValidation);
751
894
  if (result.success) {
752
895
  items.push({
753
896
  ...result.data,
@@ -756,12 +899,13 @@ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = '
756
899
  });
757
900
  }
758
901
  // Even if the item itself was added, also extract nested items
759
- const rawObj = raw;
760
902
  for (const field of NESTED_ITEM_FIELDS) {
761
903
  if (field in rawObj && Array.isArray(rawObj[field])) {
762
904
  const arr = rawObj[field];
763
905
  for (let i = 0; i < arr.length; i++) {
764
- const nestedPath = currentPath ? `${currentPath}.${field}[${i}]` : `${field}[${i}]`;
906
+ const nestedPath = currentPath
907
+ ? `${currentPath}.${field}[${i}]`
908
+ : `${field}[${i}]`;
765
909
  extractItemsFromRaw(arr[i], sourceFile, items, nestedPath);
766
910
  }
767
911
  }
@@ -781,7 +925,9 @@ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = '
781
925
  if (field in rawObj && Array.isArray(rawObj[field])) {
782
926
  const arr = rawObj[field];
783
927
  for (let i = 0; i < arr.length; i++) {
784
- const nestedPath = currentPath ? `${currentPath}.${field}[${i}]` : `${field}[${i}]`;
928
+ const nestedPath = currentPath
929
+ ? `${currentPath}.${field}[${i}]`
930
+ : `${field}[${i}]`;
785
931
  extractItemsFromRaw(arr[i], sourceFile, items, nestedPath);
786
932
  }
787
933
  }
@@ -795,7 +941,7 @@ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = '
795
941
  */
796
942
  export async function loadSpecFile(filePath) {
797
943
  try {
798
- const content = await fs.readFile(filePath, 'utf-8');
944
+ const content = await fs.readFile(filePath, "utf-8");
799
945
  const items = [];
800
946
  // Parse all YAML documents in the file (handles files with ---)
801
947
  const documents = YAML.parseAllDocuments(content);
@@ -812,7 +958,7 @@ export async function loadSpecFile(filePath) {
812
958
  }
813
959
  return items;
814
960
  }
815
- catch (error) {
961
+ catch (_error) {
816
962
  // File doesn't exist or parse error
817
963
  return [];
818
964
  }
@@ -846,8 +992,8 @@ export async function loadAllItems(ctx) {
846
992
  */
847
993
  export function findItemByRef(items, ref) {
848
994
  // Remove @ prefix if present
849
- const cleanRef = ref.startsWith('@') ? ref.slice(1) : ref;
850
- return items.find(item => {
995
+ const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
996
+ return items.find((item) => {
851
997
  // Match full ULID
852
998
  if (item._ulid === cleanRef)
853
999
  return true;
@@ -884,11 +1030,13 @@ export async function buildReferenceIndex(ctx) {
884
1030
  /**
885
1031
  * Build both ReferenceIndex and ItemIndex from context.
886
1032
  * Use this when you need query capabilities in addition to reference resolution.
1033
+ * Pass plans for cross-namespace slug collision detection (plans aren't loaded by default
1034
+ * to avoid circular dependency with plans.ts).
887
1035
  */
888
- export async function buildIndexes(ctx) {
1036
+ export async function buildIndexes(ctx, plans = []) {
889
1037
  const tasks = await loadAllTasks(ctx);
890
1038
  const items = await loadAllItems(ctx);
891
- const refIndex = new ReferenceIndex(tasks, items);
1039
+ const refIndex = new ReferenceIndex(tasks, items, [], plans);
892
1040
  const itemIndex = new ItemIndex(tasks, items);
893
1041
  const traitIndex = new TraitIndex(items, refIndex);
894
1042
  return { refIndex, itemIndex, traitIndex, tasks, items };
@@ -930,7 +1078,7 @@ function navigateToPath(root, pathStr) {
930
1078
  // Navigate to the parent of the last segment
931
1079
  for (let i = 0; i < segments.length - 1; i++) {
932
1080
  const [field, index] = segments[i];
933
- if (typeof current !== 'object' || current === null)
1081
+ if (typeof current !== "object" || current === null)
934
1082
  return null;
935
1083
  const obj = current;
936
1084
  if (!Array.isArray(obj[field]))
@@ -939,7 +1087,7 @@ function navigateToPath(root, pathStr) {
939
1087
  }
940
1088
  // Get the final array and index
941
1089
  const [finalField, finalIndex] = segments[segments.length - 1];
942
- if (typeof current !== 'object' || current === null)
1090
+ if (typeof current !== "object" || current === null)
943
1091
  return null;
944
1092
  const parent = current;
945
1093
  if (!Array.isArray(parent[finalField]))
@@ -954,8 +1102,8 @@ function navigateToPath(root, pathStr) {
954
1102
  * Find an item by ULID in a nested YAML structure.
955
1103
  * Returns the path segments to reach it.
956
1104
  */
957
- function findItemInStructure(root, ulid, currentPath = '') {
958
- if (!root || typeof root !== 'object')
1105
+ function findItemInStructure(root, ulid, currentPath = "") {
1106
+ if (!root || typeof root !== "object")
959
1107
  return null;
960
1108
  const obj = root;
961
1109
  // Check if this is the item we're looking for
@@ -967,7 +1115,9 @@ function findItemInStructure(root, ulid, currentPath = '') {
967
1115
  if (Array.isArray(obj[field])) {
968
1116
  const arr = obj[field];
969
1117
  for (let i = 0; i < arr.length; i++) {
970
- const nestedPath = currentPath ? `${currentPath}.${field}[${i}]` : `${field}[${i}]`;
1118
+ const nestedPath = currentPath
1119
+ ? `${currentPath}.${field}[${i}]`
1120
+ : `${field}[${i}]`;
971
1121
  const result = findItemInStructure(arr[i], ulid, nestedPath);
972
1122
  if (result)
973
1123
  return result;
@@ -989,6 +1139,7 @@ export function createSpecItem(input) {
989
1139
  priority: input.priority,
990
1140
  tags: input.tags || [],
991
1141
  description: input.description,
1142
+ acceptance_criteria: input.acceptance_criteria,
992
1143
  depends_on: input.depends_on || [],
993
1144
  implements: input.implements || [],
994
1145
  relates_to: input.relates_to || [],
@@ -1003,12 +1154,12 @@ export function createSpecItem(input) {
1003
1154
  * Map from item type to the field name used to store children of that type.
1004
1155
  */
1005
1156
  const TYPE_TO_CHILD_FIELD = {
1006
- feature: 'features',
1007
- requirement: 'requirements',
1008
- constraint: 'constraints',
1009
- decision: 'decisions',
1010
- module: 'modules',
1011
- trait: 'traits',
1157
+ feature: "features",
1158
+ requirement: "requirements",
1159
+ constraint: "constraints",
1160
+ decision: "decisions",
1161
+ module: "modules",
1162
+ trait: "traits",
1012
1163
  };
1013
1164
  /**
1014
1165
  * Add a spec item as a child of a parent item.
@@ -1016,11 +1167,11 @@ const TYPE_TO_CHILD_FIELD = {
1016
1167
  * @param child The new child item to add
1017
1168
  * @param childField Optional field name override (defaults based on child.type)
1018
1169
  */
1019
- export async function addChildItem(ctx, parent, child, childField) {
1170
+ export async function addChildItem(_ctx, parent, child, childField) {
1020
1171
  if (!parent._sourceFile) {
1021
- throw new Error('Parent item has no source file');
1172
+ throw new Error("Parent item has no source file");
1022
1173
  }
1023
- const field = childField || TYPE_TO_CHILD_FIELD[child.type || 'feature'] || 'features';
1174
+ const field = childField || TYPE_TO_CHILD_FIELD[child.type || "feature"] || "features";
1024
1175
  // Load the raw YAML
1025
1176
  const raw = await readYamlFile(parent._sourceFile);
1026
1177
  // Find the parent in the structure
@@ -1037,7 +1188,7 @@ export async function addChildItem(ctx, parent, child, childField) {
1037
1188
  else {
1038
1189
  // Parent is the root item
1039
1190
  parentObj = raw;
1040
- parentPath = '';
1191
+ parentPath = "";
1041
1192
  }
1042
1193
  // Ensure the child field array exists
1043
1194
  if (!Array.isArray(parentObj[field])) {
@@ -1049,7 +1200,9 @@ export async function addChildItem(ctx, parent, child, childField) {
1049
1200
  childArray.push(cleanChild);
1050
1201
  // Calculate the new child's path
1051
1202
  const childIndex = childArray.length - 1;
1052
- const childPath = parentPath ? `${parentPath}.${field}[${childIndex}]` : `${field}[${childIndex}]`;
1203
+ const childPath = parentPath
1204
+ ? `${parentPath}.${field}[${childIndex}]`
1205
+ : `${field}[${childIndex}]`;
1053
1206
  // Write back with format preservation
1054
1207
  await writeYamlFilePreserveFormat(parent._sourceFile, raw);
1055
1208
  return { item: cleanChild, path: childPath };
@@ -1058,9 +1211,9 @@ export async function addChildItem(ctx, parent, child, childField) {
1058
1211
  * Update a spec item in place within its source file.
1059
1212
  * Works with nested structures using the _path field.
1060
1213
  */
1061
- export async function updateSpecItem(ctx, item, updates) {
1214
+ export async function updateSpecItem(_ctx, item, updates) {
1062
1215
  if (!item._sourceFile) {
1063
- throw new Error('Item has no source file');
1216
+ throw new Error("Item has no source file");
1064
1217
  }
1065
1218
  // Load the raw YAML
1066
1219
  const raw = await readYamlFile(item._sourceFile);
@@ -1088,7 +1241,7 @@ export async function updateSpecItem(ctx, item, updates) {
1088
1241
  }
1089
1242
  // Apply updates (but never change _ulid)
1090
1243
  for (const [key, value] of Object.entries(updates)) {
1091
- if (key !== '_ulid' && key !== '_sourceFile' && key !== '_path') {
1244
+ if (key !== "_ulid" && key !== "_sourceFile" && key !== "_path") {
1092
1245
  targetObj[key] = value;
1093
1246
  }
1094
1247
  }
@@ -1102,12 +1255,12 @@ export async function updateSpecItem(ctx, item, updates) {
1102
1255
  */
1103
1256
  export function findTraitImplementors(trait, allItems) {
1104
1257
  // Check if the item is actually a trait
1105
- if (trait.type !== 'trait') {
1258
+ if (trait.type !== "trait") {
1106
1259
  return [];
1107
1260
  }
1108
1261
  // Find all items that reference this trait in their 'traits' array
1109
- const traitRefs = ['@' + trait._ulid, ...trait.slugs.map(s => '@' + s)];
1110
- return allItems.filter(item => {
1262
+ const traitRefs = [`@${trait._ulid}`, ...trait.slugs.map((s) => `@${s}`)];
1263
+ return allItems.filter((item) => {
1111
1264
  if (!item.traits || item.traits.length === 0)
1112
1265
  return false;
1113
1266
  return item.traits.some((traitRef) => traitRefs.includes(traitRef));
@@ -1117,7 +1270,7 @@ export function findTraitImplementors(trait, allItems) {
1117
1270
  * Delete a spec item from its source file.
1118
1271
  * Works with nested structures using the _path field.
1119
1272
  */
1120
- export async function deleteSpecItem(ctx, item) {
1273
+ export async function deleteSpecItem(_ctx, item) {
1121
1274
  if (!item._sourceFile) {
1122
1275
  return false;
1123
1276
  }
@@ -1136,7 +1289,7 @@ export async function deleteSpecItem(ctx, item) {
1136
1289
  }
1137
1290
  // No path - try to find it by ULID
1138
1291
  const found = findItemInStructure(raw, item._ulid);
1139
- if (found && found.path) {
1292
+ if (found?.path) {
1140
1293
  const nav = navigateToPath(raw, found.path);
1141
1294
  if (nav) {
1142
1295
  nav.array.splice(nav.index, 1);
@@ -1146,7 +1299,9 @@ export async function deleteSpecItem(ctx, item) {
1146
1299
  }
1147
1300
  // Maybe it's a root-level array item
1148
1301
  if (Array.isArray(raw)) {
1149
- const index = raw.findIndex((i) => typeof i === 'object' && i !== null && i._ulid === item._ulid);
1302
+ const index = raw.findIndex((i) => typeof i === "object" &&
1303
+ i !== null &&
1304
+ i._ulid === item._ulid);
1150
1305
  if (index >= 0) {
1151
1306
  raw.splice(index, 1);
1152
1307
  await writeYamlFilePreserveFormat(item._sourceFile, raw);
@@ -1170,7 +1325,7 @@ export async function saveSpecItem(ctx, item) {
1170
1325
  return;
1171
1326
  }
1172
1327
  // Otherwise, this is more complex - would need a parent
1173
- throw new Error('Cannot save new item without parent. Use addChildItem instead.');
1328
+ throw new Error("Cannot save new item without parent. Use addChildItem instead.");
1174
1329
  }
1175
1330
  /**
1176
1331
  * Get the inbox file path.
@@ -1179,7 +1334,7 @@ export async function saveSpecItem(ctx, item) {
1179
1334
  * Otherwise: spec/project.inbox.yaml
1180
1335
  */
1181
1336
  export function getInboxFilePath(ctx) {
1182
- return path.join(ctx.specDir, 'project.inbox.yaml');
1337
+ return path.join(ctx.specDir, "project.inbox.yaml");
1183
1338
  }
1184
1339
  /**
1185
1340
  * Load all inbox items from the project.
@@ -1189,10 +1344,13 @@ export async function loadInboxItems(ctx) {
1189
1344
  try {
1190
1345
  const raw = await readYamlFile(inboxPath);
1191
1346
  // Handle { inbox: [...] } format
1192
- if (raw && typeof raw === 'object' && 'inbox' in raw) {
1347
+ if (raw && typeof raw === "object" && "inbox" in raw) {
1193
1348
  const parsed = InboxFileSchema.safeParse(raw);
1194
1349
  if (parsed.success) {
1195
- return parsed.data.inbox.map(item => ({ ...item, _sourceFile: inboxPath }));
1350
+ return parsed.data.inbox.map((item) => ({
1351
+ ...item,
1352
+ _sourceFile: inboxPath,
1353
+ }));
1196
1354
  }
1197
1355
  }
1198
1356
  // Handle plain array format
@@ -1215,14 +1373,19 @@ export async function loadInboxItems(ctx) {
1215
1373
  }
1216
1374
  /**
1217
1375
  * Create a new inbox item with auto-generated fields.
1376
+ *
1377
+ * AC: @config-author — supports config author in fallback chain
1378
+ *
1379
+ * @param input Inbox item input
1380
+ * @param configAuthor Optional author from kspec.config.yaml identity.author
1218
1381
  */
1219
- export function createInboxItem(input) {
1382
+ export function createInboxItem(input, configAuthor) {
1220
1383
  return {
1221
1384
  _ulid: input._ulid || ulid(),
1222
1385
  text: input.text,
1223
1386
  created_at: input.created_at || new Date().toISOString(),
1224
1387
  tags: input.tags || [],
1225
- added_by: input.added_by ?? getAuthor(),
1388
+ added_by: input.added_by ?? getAuthor(configAuthor),
1226
1389
  };
1227
1390
  }
1228
1391
  /**
@@ -1244,7 +1407,7 @@ export async function saveInboxItem(ctx, item) {
1244
1407
  let existingItems = [];
1245
1408
  try {
1246
1409
  const raw = await readYamlFile(inboxPath);
1247
- if (raw && typeof raw === 'object' && 'inbox' in raw) {
1410
+ if (raw && typeof raw === "object" && "inbox" in raw) {
1248
1411
  const parsed = InboxFileSchema.safeParse(raw);
1249
1412
  if (parsed.success) {
1250
1413
  existingItems = parsed.data.inbox;
@@ -1264,7 +1427,7 @@ export async function saveInboxItem(ctx, item) {
1264
1427
  }
1265
1428
  const cleanItem = stripInboxMetadata(item);
1266
1429
  // Update existing or add new
1267
- const existingIndex = existingItems.findIndex(i => i._ulid === item._ulid);
1430
+ const existingIndex = existingItems.findIndex((i) => i._ulid === item._ulid);
1268
1431
  if (existingIndex >= 0) {
1269
1432
  existingItems[existingIndex] = cleanItem;
1270
1433
  }
@@ -1282,13 +1445,13 @@ export async function deleteInboxItem(ctx, ulid) {
1282
1445
  try {
1283
1446
  const raw = await readYamlFile(inboxPath);
1284
1447
  let existingItems = [];
1285
- if (raw && typeof raw === 'object' && 'inbox' in raw) {
1448
+ if (raw && typeof raw === "object" && "inbox" in raw) {
1286
1449
  const parsed = InboxFileSchema.safeParse(raw);
1287
1450
  if (parsed.success) {
1288
1451
  existingItems = parsed.data.inbox;
1289
1452
  }
1290
1453
  }
1291
- const index = existingItems.findIndex(i => i._ulid === ulid);
1454
+ const index = existingItems.findIndex((i) => i._ulid === ulid);
1292
1455
  if (index < 0) {
1293
1456
  return false;
1294
1457
  }
@@ -1304,8 +1467,8 @@ export async function deleteInboxItem(ctx, ulid) {
1304
1467
  * Find an inbox item by reference (ULID or short ULID).
1305
1468
  */
1306
1469
  export function findInboxItemByRef(items, ref) {
1307
- const cleanRef = ref.startsWith('@') ? ref.slice(1) : ref;
1308
- return items.find(item => {
1470
+ const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
1471
+ return items.find((item) => {
1309
1472
  // Match full ULID
1310
1473
  if (item._ulid === cleanRef)
1311
1474
  return true;
@@ -1315,6 +1478,144 @@ export function findInboxItemByRef(items, ref) {
1315
1478
  return false;
1316
1479
  });
1317
1480
  }
1481
+ /**
1482
+ * Get the triage file path.
1483
+ * AC: @triage-record-schema ac-6
1484
+ *
1485
+ * When shadow enabled: .kspec/project.triage.yaml
1486
+ * Otherwise: spec/project.triage.yaml
1487
+ */
1488
+ export function getTriageFilePath(ctx) {
1489
+ return path.join(ctx.specDir, "project.triage.yaml");
1490
+ }
1491
+ /**
1492
+ * Load all triage records from the project.
1493
+ * AC: @triage-record-schema ac-6, ac-7
1494
+ */
1495
+ export async function loadTriageRecords(ctx) {
1496
+ const triagePath = getTriageFilePath(ctx);
1497
+ try {
1498
+ const raw = await readYamlFile(triagePath);
1499
+ // Handle { kynetic_triage: "1.0", triage: [...] } format
1500
+ if (raw && typeof raw === "object" && "triage" in raw) {
1501
+ const parsed = TriageFileSchema.safeParse(raw);
1502
+ if (parsed.success) {
1503
+ return parsed.data.triage.map((record) => ({
1504
+ ...record,
1505
+ _sourceFile: triagePath,
1506
+ }));
1507
+ }
1508
+ }
1509
+ // Handle plain array format
1510
+ if (Array.isArray(raw)) {
1511
+ const records = [];
1512
+ for (const item of raw) {
1513
+ const result = TriageRecordSchema.safeParse(item);
1514
+ if (result.success) {
1515
+ records.push({ ...result.data, _sourceFile: triagePath });
1516
+ }
1517
+ }
1518
+ return records;
1519
+ }
1520
+ return [];
1521
+ }
1522
+ catch {
1523
+ // File doesn't exist or parse error
1524
+ return [];
1525
+ }
1526
+ }
1527
+ /**
1528
+ * Strip runtime metadata before serialization.
1529
+ */
1530
+ function stripTriageMetadata(record) {
1531
+ const { _sourceFile, ...cleanRecord } = record;
1532
+ return cleanRecord;
1533
+ }
1534
+ /**
1535
+ * Save a triage record (add or update).
1536
+ * AC: @triage-record-schema ac-8 — upsert on inbox_ref (one record per inbox item)
1537
+ * AC: @triage-record-schema ac-9 — sets updated_at on every mutation
1538
+ */
1539
+ export async function saveTriageRecord(ctx, record) {
1540
+ const triagePath = getTriageFilePath(ctx);
1541
+ // Ensure directory exists
1542
+ const dir = path.dirname(triagePath);
1543
+ await fs.mkdir(dir, { recursive: true });
1544
+ // Load existing records
1545
+ let existingRecords = [];
1546
+ try {
1547
+ const raw = await readYamlFile(triagePath);
1548
+ if (raw && typeof raw === "object" && "triage" in raw) {
1549
+ const parsed = TriageFileSchema.safeParse(raw);
1550
+ if (parsed.success) {
1551
+ existingRecords = parsed.data.triage;
1552
+ }
1553
+ }
1554
+ else if (Array.isArray(raw)) {
1555
+ for (const item of raw) {
1556
+ const result = TriageRecordSchema.safeParse(item);
1557
+ if (result.success) {
1558
+ existingRecords.push(result.data);
1559
+ }
1560
+ }
1561
+ }
1562
+ }
1563
+ catch {
1564
+ // File doesn't exist, start fresh
1565
+ }
1566
+ const cleanRecord = stripTriageMetadata(record);
1567
+ // AC: ac-9 — set updated_at on every mutation
1568
+ cleanRecord.updated_at = new Date().toISOString();
1569
+ // AC: ac-8 — upsert: check for existing record by ULID first, then by inbox_ref
1570
+ const existingByUlid = existingRecords.findIndex((r) => r._ulid === record._ulid);
1571
+ if (existingByUlid >= 0) {
1572
+ existingRecords[existingByUlid] = cleanRecord;
1573
+ }
1574
+ else {
1575
+ // Check for existing record with same inbox_ref (uniqueness constraint)
1576
+ // Preserve existing identity (_ulid, created_at) when upserting by inbox_ref
1577
+ const existingByInboxRef = existingRecords.findIndex((r) => r.inbox_ref === record.inbox_ref);
1578
+ if (existingByInboxRef >= 0) {
1579
+ const existing = existingRecords[existingByInboxRef];
1580
+ existingRecords[existingByInboxRef] = {
1581
+ ...cleanRecord,
1582
+ _ulid: existing._ulid,
1583
+ created_at: existing.created_at,
1584
+ };
1585
+ }
1586
+ else {
1587
+ existingRecords.push(cleanRecord);
1588
+ }
1589
+ }
1590
+ // Save with { kynetic_triage: "1.0", triage: [...] } format
1591
+ await writeYamlFilePreserveFormat(triagePath, {
1592
+ kynetic_triage: "1.0",
1593
+ triage: existingRecords,
1594
+ });
1595
+ }
1596
+ /**
1597
+ * Find a triage record by reference (ULID or short ULID).
1598
+ */
1599
+ export function findTriageRecordByRef(records, ref) {
1600
+ const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
1601
+ return records.find((record) => {
1602
+ // Match full ULID
1603
+ if (record._ulid === cleanRef)
1604
+ return true;
1605
+ // Match short ULID (prefix)
1606
+ if (record._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()))
1607
+ return true;
1608
+ return false;
1609
+ });
1610
+ }
1611
+ /**
1612
+ * Find a triage record by inbox item reference.
1613
+ * AC: @triage-record-schema ac-8 — lookup by inbox_ref for upsert
1614
+ */
1615
+ export function findTriageRecordByInboxRef(records, inboxRef) {
1616
+ const cleanRef = inboxRef.startsWith("@") ? inboxRef.slice(1) : inboxRef;
1617
+ return records.find((record) => record.inbox_ref === cleanRef);
1618
+ }
1318
1619
  /**
1319
1620
  * Bulk patch spec items.
1320
1621
  * Resolves refs, validates data, applies patches.
@@ -1325,28 +1626,32 @@ export async function patchSpecItems(ctx, refIndex, items, patches, options = {}
1325
1626
  let stopProcessing = false;
1326
1627
  for (const patch of patches) {
1327
1628
  if (stopProcessing) {
1328
- results.push({ ref: patch.ref, status: 'skipped' });
1629
+ results.push({ ref: patch.ref, status: "skipped" });
1329
1630
  continue;
1330
1631
  }
1331
1632
  // Resolve ref
1332
1633
  const resolved = refIndex.resolve(patch.ref);
1333
1634
  if (!resolved.ok) {
1334
- const errorMsg = resolved.error === 'not_found'
1635
+ const errorMsg = resolved.error === "not_found"
1335
1636
  ? `Item not found: ${patch.ref}`
1336
- : resolved.error === 'ambiguous'
1637
+ : resolved.error === "ambiguous"
1337
1638
  ? `Ambiguous ref: ${patch.ref}`
1338
1639
  : `Duplicate slug: ${patch.ref}`;
1339
- results.push({ ref: patch.ref, status: 'error', error: errorMsg });
1640
+ results.push({ ref: patch.ref, status: "error", error: errorMsg });
1340
1641
  if (options.failFast) {
1341
1642
  stopProcessing = true;
1342
1643
  }
1343
1644
  continue;
1344
1645
  }
1345
1646
  // Find the item
1346
- const item = items.find(i => i._ulid === resolved.ulid);
1647
+ const item = items.find((i) => i._ulid === resolved.ulid);
1347
1648
  if (!item) {
1348
1649
  // Ref resolved but it's not a spec item (might be a task)
1349
- results.push({ ref: patch.ref, status: 'error', error: 'Not a spec item' });
1650
+ results.push({
1651
+ ref: patch.ref,
1652
+ status: "error",
1653
+ error: "Not a spec item",
1654
+ });
1350
1655
  if (options.failFast) {
1351
1656
  stopProcessing = true;
1352
1657
  }
@@ -1354,17 +1659,17 @@ export async function patchSpecItems(ctx, refIndex, items, patches, options = {}
1354
1659
  }
1355
1660
  // Dry run - just record what would happen
1356
1661
  if (options.dryRun) {
1357
- results.push({ ref: patch.ref, status: 'updated', ulid: item._ulid });
1662
+ results.push({ ref: patch.ref, status: "updated", ulid: item._ulid });
1358
1663
  continue;
1359
1664
  }
1360
1665
  // Apply the patch
1361
1666
  try {
1362
1667
  await updateSpecItem(ctx, item, patch.data);
1363
- results.push({ ref: patch.ref, status: 'updated', ulid: item._ulid });
1668
+ results.push({ ref: patch.ref, status: "updated", ulid: item._ulid });
1364
1669
  }
1365
1670
  catch (err) {
1366
1671
  const errorMsg = err instanceof Error ? err.message : String(err);
1367
- results.push({ ref: patch.ref, status: 'error', error: errorMsg });
1672
+ results.push({ ref: patch.ref, status: "error", error: errorMsg });
1368
1673
  if (options.failFast) {
1369
1674
  stopProcessing = true;
1370
1675
  }
@@ -1374,9 +1679,9 @@ export async function patchSpecItems(ctx, refIndex, items, patches, options = {}
1374
1679
  results,
1375
1680
  summary: {
1376
1681
  total: patches.length,
1377
- updated: results.filter(r => r.status === 'updated').length,
1378
- failed: results.filter(r => r.status === 'error').length,
1379
- skipped: results.filter(r => r.status === 'skipped').length,
1682
+ updated: results.filter((r) => r.status === "updated").length,
1683
+ failed: results.filter((r) => r.status === "error").length,
1684
+ skipped: results.filter((r) => r.status === "skipped").length,
1380
1685
  },
1381
1686
  };
1382
1687
  }