@kynetic-ai/spec 0.10.0 → 0.12.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 (487) hide show
  1. package/README.md +55 -455
  2. package/dist/agent-runtime/bootstrap.d.ts +31 -0
  3. package/dist/agent-runtime/bootstrap.d.ts.map +1 -0
  4. package/dist/agent-runtime/bootstrap.js +302 -0
  5. package/dist/agent-runtime/bootstrap.js.map +1 -0
  6. package/dist/agent-runtime/dispatch.d.ts +150 -10
  7. package/dist/agent-runtime/dispatch.d.ts.map +1 -1
  8. package/dist/agent-runtime/dispatch.js +1248 -244
  9. package/dist/agent-runtime/dispatch.js.map +1 -1
  10. package/dist/agent-runtime/invocation.d.ts +28 -1
  11. package/dist/agent-runtime/invocation.d.ts.map +1 -1
  12. package/dist/agent-runtime/invocation.js +172 -60
  13. package/dist/agent-runtime/invocation.js.map +1 -1
  14. package/dist/agent-runtime/prompts.d.ts +9 -0
  15. package/dist/agent-runtime/prompts.d.ts.map +1 -1
  16. package/dist/agent-runtime/prompts.js +42 -7
  17. package/dist/agent-runtime/prompts.js.map +1 -1
  18. package/dist/agent-runtime/session-event-accumulator.d.ts +83 -0
  19. package/dist/agent-runtime/session-event-accumulator.d.ts.map +1 -0
  20. package/dist/agent-runtime/session-event-accumulator.js +203 -0
  21. package/dist/agent-runtime/session-event-accumulator.js.map +1 -0
  22. package/dist/agent-runtime/session-event-types.d.ts +67 -0
  23. package/dist/agent-runtime/session-event-types.d.ts.map +1 -0
  24. package/dist/agent-runtime/session-event-types.js +13 -0
  25. package/dist/agent-runtime/session-event-types.js.map +1 -0
  26. package/dist/agent-runtime/workspace.d.ts +244 -0
  27. package/dist/agent-runtime/workspace.d.ts.map +1 -0
  28. package/dist/agent-runtime/workspace.js +2025 -0
  29. package/dist/agent-runtime/workspace.js.map +1 -0
  30. package/dist/agents/adapters.d.ts.map +1 -1
  31. package/dist/agents/adapters.js +58 -13
  32. package/dist/agents/adapters.js.map +1 -1
  33. package/dist/agents/spawner.d.ts +8 -0
  34. package/dist/agents/spawner.d.ts.map +1 -1
  35. package/dist/agents/spawner.js +25 -3
  36. package/dist/agents/spawner.js.map +1 -1
  37. package/dist/cli/batch-exec.js +1 -1
  38. package/dist/cli/batch-exec.js.map +1 -1
  39. package/dist/cli/command-annotations.d.ts +15 -3
  40. package/dist/cli/command-annotations.d.ts.map +1 -1
  41. package/dist/cli/command-annotations.js +23 -3
  42. package/dist/cli/command-annotations.js.map +1 -1
  43. package/dist/cli/commands/agent.d.ts +2 -0
  44. package/dist/cli/commands/agent.d.ts.map +1 -1
  45. package/dist/cli/commands/agent.js +144 -27
  46. package/dist/cli/commands/agent.js.map +1 -1
  47. package/dist/cli/commands/agents.d.ts.map +1 -1
  48. package/dist/cli/commands/agents.js +5 -5
  49. package/dist/cli/commands/agents.js.map +1 -1
  50. package/dist/cli/commands/derive.d.ts.map +1 -1
  51. package/dist/cli/commands/derive.js +118 -3
  52. package/dist/cli/commands/derive.js.map +1 -1
  53. package/dist/cli/commands/guard.d.ts.map +1 -1
  54. package/dist/cli/commands/guard.js +8 -6
  55. package/dist/cli/commands/guard.js.map +1 -1
  56. package/dist/cli/commands/index.d.ts +1 -0
  57. package/dist/cli/commands/index.d.ts.map +1 -1
  58. package/dist/cli/commands/index.js +1 -0
  59. package/dist/cli/commands/index.js.map +1 -1
  60. package/dist/cli/commands/init.d.ts.map +1 -1
  61. package/dist/cli/commands/init.js +20 -0
  62. package/dist/cli/commands/init.js.map +1 -1
  63. package/dist/cli/commands/item.d.ts.map +1 -1
  64. package/dist/cli/commands/item.js +205 -47
  65. package/dist/cli/commands/item.js.map +1 -1
  66. package/dist/cli/commands/log.d.ts.map +1 -1
  67. package/dist/cli/commands/log.js +24 -10
  68. package/dist/cli/commands/log.js.map +1 -1
  69. package/dist/cli/commands/meta.d.ts.map +1 -1
  70. package/dist/cli/commands/meta.js +10 -1
  71. package/dist/cli/commands/meta.js.map +1 -1
  72. package/dist/cli/commands/plan-import.d.ts +3 -3
  73. package/dist/cli/commands/plan-import.d.ts.map +1 -1
  74. package/dist/cli/commands/plan-import.js +213 -528
  75. package/dist/cli/commands/plan-import.js.map +1 -1
  76. package/dist/cli/commands/plan.d.ts.map +1 -1
  77. package/dist/cli/commands/plan.js +533 -83
  78. package/dist/cli/commands/plan.js.map +1 -1
  79. package/dist/cli/commands/review.d.ts +14 -0
  80. package/dist/cli/commands/review.d.ts.map +1 -0
  81. package/dist/cli/commands/review.js +1142 -0
  82. package/dist/cli/commands/review.js.map +1 -0
  83. package/dist/cli/commands/serve.d.ts +1 -0
  84. package/dist/cli/commands/serve.d.ts.map +1 -1
  85. package/dist/cli/commands/serve.js +33 -10
  86. package/dist/cli/commands/serve.js.map +1 -1
  87. package/dist/cli/commands/session/checkpoint.d.ts +2 -4
  88. package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
  89. package/dist/cli/commands/session/checkpoint.js +6 -107
  90. package/dist/cli/commands/session/checkpoint.js.map +1 -1
  91. package/dist/cli/commands/session/commands.d.ts.map +1 -1
  92. package/dist/cli/commands/session/commands.js +33 -23
  93. package/dist/cli/commands/session/commands.js.map +1 -1
  94. package/dist/cli/commands/session/compact.js +4 -4
  95. package/dist/cli/commands/session/compact.js.map +1 -1
  96. package/dist/cli/commands/session/create.js +2 -2
  97. package/dist/cli/commands/session/create.js.map +1 -1
  98. package/dist/cli/commands/session/format.d.ts.map +1 -1
  99. package/dist/cli/commands/session/format.js +1 -6
  100. package/dist/cli/commands/session/format.js.map +1 -1
  101. package/dist/cli/commands/session/log.d.ts +32 -7
  102. package/dist/cli/commands/session/log.d.ts.map +1 -1
  103. package/dist/cli/commands/session/log.js +166 -60
  104. package/dist/cli/commands/session/log.js.map +1 -1
  105. package/dist/cli/commands/session/migrate.d.ts +9 -0
  106. package/dist/cli/commands/session/migrate.d.ts.map +1 -0
  107. package/dist/cli/commands/session/migrate.js +46 -0
  108. package/dist/cli/commands/session/migrate.js.map +1 -0
  109. package/dist/cli/commands/session/stale-close.d.ts.map +1 -1
  110. package/dist/cli/commands/session/stale-close.js +5 -8
  111. package/dist/cli/commands/session/stale-close.js.map +1 -1
  112. package/dist/cli/commands/session/types.d.ts +1 -1
  113. package/dist/cli/commands/session/types.d.ts.map +1 -1
  114. package/dist/cli/commands/setup.d.ts +2 -2
  115. package/dist/cli/commands/setup.d.ts.map +1 -1
  116. package/dist/cli/commands/setup.js +287 -257
  117. package/dist/cli/commands/setup.js.map +1 -1
  118. package/dist/cli/commands/shadow.d.ts.map +1 -1
  119. package/dist/cli/commands/shadow.js +147 -31
  120. package/dist/cli/commands/shadow.js.map +1 -1
  121. package/dist/cli/commands/skill-crud.d.ts +7 -0
  122. package/dist/cli/commands/skill-crud.d.ts.map +1 -1
  123. package/dist/cli/commands/skill-crud.js +41 -18
  124. package/dist/cli/commands/skill-crud.js.map +1 -1
  125. package/dist/cli/commands/skill-diff.d.ts.map +1 -1
  126. package/dist/cli/commands/skill-diff.js +29 -3
  127. package/dist/cli/commands/skill-diff.js.map +1 -1
  128. package/dist/cli/commands/skill-install.d.ts.map +1 -1
  129. package/dist/cli/commands/skill-install.js +5 -4
  130. package/dist/cli/commands/skill-install.js.map +1 -1
  131. package/dist/cli/commands/task.d.ts.map +1 -1
  132. package/dist/cli/commands/task.js +359 -49
  133. package/dist/cli/commands/task.js.map +1 -1
  134. package/dist/cli/commands/trait.d.ts.map +1 -1
  135. package/dist/cli/commands/trait.js +5 -27
  136. package/dist/cli/commands/trait.js.map +1 -1
  137. package/dist/cli/commands/validate.d.ts.map +1 -1
  138. package/dist/cli/commands/validate.js +113 -52
  139. package/dist/cli/commands/validate.js.map +1 -1
  140. package/dist/cli/index.d.ts.map +1 -1
  141. package/dist/cli/index.js +69 -2
  142. package/dist/cli/index.js.map +1 -1
  143. package/dist/cli/output.d.ts +26 -0
  144. package/dist/cli/output.d.ts.map +1 -1
  145. package/dist/cli/output.js +108 -1
  146. package/dist/cli/output.js.map +1 -1
  147. package/dist/cli/sync-mode.d.ts +44 -0
  148. package/dist/cli/sync-mode.d.ts.map +1 -0
  149. package/dist/cli/sync-mode.js +64 -0
  150. package/dist/cli/sync-mode.js.map +1 -0
  151. package/dist/daemon/middleware/project-context.ts +25 -7
  152. package/dist/daemon/project-context.ts +18 -0
  153. package/dist/daemon/routes/agent-dispatch.ts +107 -23
  154. package/dist/daemon/routes/aggregation.ts +184 -0
  155. package/dist/daemon/routes/inbox.ts +5 -0
  156. package/dist/daemon/routes/items.ts +167 -0
  157. package/dist/daemon/routes/meta.ts +141 -1
  158. package/dist/daemon/routes/plans.ts +147 -0
  159. package/dist/daemon/routes/projects.ts +28 -6
  160. package/dist/daemon/routes/ref-resolution.ts +119 -0
  161. package/dist/daemon/routes/refs.ts +42 -0
  162. package/dist/daemon/routes/session-related.ts +140 -0
  163. package/dist/daemon/routes/sessions.ts +581 -0
  164. package/dist/daemon/routes/tasks.ts +257 -2
  165. package/dist/daemon/routes/triage.ts +40 -1
  166. package/dist/daemon/routes/validation.ts +1 -1
  167. package/dist/daemon/server.ts +165 -50
  168. package/dist/daemon/session-sync.ts +11 -0
  169. package/dist/daemon/shadow-sync.ts +11 -0
  170. package/dist/daemon/watcher.ts +56 -5
  171. package/dist/daemon/websocket/project-resolution.ts +77 -0
  172. package/dist/export/json.d.ts.map +1 -1
  173. package/dist/export/json.js +104 -1
  174. package/dist/export/json.js.map +1 -1
  175. package/dist/export/types.d.ts +52 -1
  176. package/dist/export/types.d.ts.map +1 -1
  177. package/dist/index.d.ts +1 -0
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +1 -0
  180. package/dist/index.js.map +1 -1
  181. package/dist/parser/agent-detection.d.ts +1 -1
  182. package/dist/parser/agent-detection.d.ts.map +1 -1
  183. package/dist/parser/agent-detection.js +10 -0
  184. package/dist/parser/agent-detection.js.map +1 -1
  185. package/dist/parser/alignment.d.ts.map +1 -1
  186. package/dist/parser/alignment.js +4 -2
  187. package/dist/parser/alignment.js.map +1 -1
  188. package/dist/parser/config.d.ts +397 -2
  189. package/dist/parser/config.d.ts.map +1 -1
  190. package/dist/parser/config.js +125 -3
  191. package/dist/parser/config.js.map +1 -1
  192. package/dist/parser/dispatch-workspaces.d.ts +18 -0
  193. package/dist/parser/dispatch-workspaces.d.ts.map +1 -0
  194. package/dist/parser/dispatch-workspaces.js +209 -0
  195. package/dist/parser/dispatch-workspaces.js.map +1 -0
  196. package/dist/parser/doctor.d.ts.map +1 -1
  197. package/dist/parser/doctor.js +27 -8
  198. package/dist/parser/doctor.js.map +1 -1
  199. package/dist/parser/file-lock.d.ts.map +1 -1
  200. package/dist/parser/file-lock.js +9 -2
  201. package/dist/parser/file-lock.js.map +1 -1
  202. package/dist/parser/index.d.ts +6 -0
  203. package/dist/parser/index.d.ts.map +1 -1
  204. package/dist/parser/index.js +6 -0
  205. package/dist/parser/index.js.map +1 -1
  206. package/dist/parser/plans.d.ts.map +1 -1
  207. package/dist/parser/plans.js +1 -0
  208. package/dist/parser/plans.js.map +1 -1
  209. package/dist/parser/refs.d.ts +8 -1
  210. package/dist/parser/refs.d.ts.map +1 -1
  211. package/dist/parser/refs.js +27 -1
  212. package/dist/parser/refs.js.map +1 -1
  213. package/dist/parser/review-operations.d.ts +72 -0
  214. package/dist/parser/review-operations.d.ts.map +1 -0
  215. package/dist/parser/review-operations.js +185 -0
  216. package/dist/parser/review-operations.js.map +1 -0
  217. package/dist/parser/review-task-integration.d.ts +78 -0
  218. package/dist/parser/review-task-integration.d.ts.map +1 -0
  219. package/dist/parser/review-task-integration.js +173 -0
  220. package/dist/parser/review-task-integration.js.map +1 -0
  221. package/dist/parser/review-threads.d.ts +101 -0
  222. package/dist/parser/review-threads.d.ts.map +1 -0
  223. package/dist/parser/review-threads.js +222 -0
  224. package/dist/parser/review-threads.js.map +1 -0
  225. package/dist/parser/review-validation.d.ts +69 -0
  226. package/dist/parser/review-validation.d.ts.map +1 -0
  227. package/dist/parser/review-validation.js +207 -0
  228. package/dist/parser/review-validation.js.map +1 -0
  229. package/dist/parser/reviews.d.ts +58 -0
  230. package/dist/parser/reviews.d.ts.map +1 -0
  231. package/dist/parser/reviews.js +230 -0
  232. package/dist/parser/reviews.js.map +1 -0
  233. package/dist/parser/session-branch.d.ts +91 -0
  234. package/dist/parser/session-branch.d.ts.map +1 -0
  235. package/dist/parser/session-branch.js +565 -0
  236. package/dist/parser/session-branch.js.map +1 -0
  237. package/dist/parser/session-sync-scheduler.d.ts +53 -0
  238. package/dist/parser/session-sync-scheduler.d.ts.map +1 -0
  239. package/dist/parser/session-sync-scheduler.js +100 -0
  240. package/dist/parser/session-sync-scheduler.js.map +1 -0
  241. package/dist/parser/setup-status.d.ts +7 -1
  242. package/dist/parser/setup-status.d.ts.map +1 -1
  243. package/dist/parser/setup-status.js +104 -39
  244. package/dist/parser/setup-status.js.map +1 -1
  245. package/dist/parser/shadow-sync-scheduler.d.ts +71 -0
  246. package/dist/parser/shadow-sync-scheduler.d.ts.map +1 -0
  247. package/dist/parser/shadow-sync-scheduler.js +139 -0
  248. package/dist/parser/shadow-sync-scheduler.js.map +1 -0
  249. package/dist/parser/shadow.d.ts +121 -14
  250. package/dist/parser/shadow.d.ts.map +1 -1
  251. package/dist/parser/shadow.js +752 -27
  252. package/dist/parser/shadow.js.map +1 -1
  253. package/dist/parser/skill-render.d.ts +24 -0
  254. package/dist/parser/skill-render.d.ts.map +1 -1
  255. package/dist/parser/skill-render.js +98 -26
  256. package/dist/parser/skill-render.js.map +1 -1
  257. package/dist/parser/validate.d.ts +43 -3
  258. package/dist/parser/validate.d.ts.map +1 -1
  259. package/dist/parser/validate.js +204 -30
  260. package/dist/parser/validate.js.map +1 -1
  261. package/dist/parser/yaml.d.ts +47 -11
  262. package/dist/parser/yaml.d.ts.map +1 -1
  263. package/dist/parser/yaml.js +329 -149
  264. package/dist/parser/yaml.js.map +1 -1
  265. package/dist/review/checks.d.ts +97 -0
  266. package/dist/review/checks.d.ts.map +1 -0
  267. package/dist/review/checks.js +175 -0
  268. package/dist/review/checks.js.map +1 -0
  269. package/dist/review/index.d.ts +3 -0
  270. package/dist/review/index.d.ts.map +1 -0
  271. package/dist/review/index.js +3 -0
  272. package/dist/review/index.js.map +1 -0
  273. package/dist/review/subject-bindings.d.ts +83 -0
  274. package/dist/review/subject-bindings.d.ts.map +1 -0
  275. package/dist/review/subject-bindings.js +175 -0
  276. package/dist/review/subject-bindings.js.map +1 -0
  277. package/dist/schema/common.d.ts +26 -0
  278. package/dist/schema/common.d.ts.map +1 -1
  279. package/dist/schema/common.js +13 -0
  280. package/dist/schema/common.js.map +1 -1
  281. package/dist/schema/dispatch-workspace.d.ts +2643 -0
  282. package/dist/schema/dispatch-workspace.d.ts.map +1 -0
  283. package/dist/schema/dispatch-workspace.js +187 -0
  284. package/dist/schema/dispatch-workspace.js.map +1 -0
  285. package/dist/schema/inbox.d.ts +8 -8
  286. package/dist/schema/index.d.ts +2 -0
  287. package/dist/schema/index.d.ts.map +1 -1
  288. package/dist/schema/index.js +2 -0
  289. package/dist/schema/index.js.map +1 -1
  290. package/dist/schema/meta.d.ts +663 -116
  291. package/dist/schema/meta.d.ts.map +1 -1
  292. package/dist/schema/meta.js +28 -0
  293. package/dist/schema/meta.js.map +1 -1
  294. package/dist/schema/plan.d.ts +30 -19
  295. package/dist/schema/plan.d.ts.map +1 -1
  296. package/dist/schema/plan.js +3 -1
  297. package/dist/schema/plan.js.map +1 -1
  298. package/dist/schema/review-records.d.ts +2676 -0
  299. package/dist/schema/review-records.d.ts.map +1 -0
  300. package/dist/schema/review-records.js +232 -0
  301. package/dist/schema/review-records.js.map +1 -0
  302. package/dist/schema/spec.d.ts +32 -14
  303. package/dist/schema/spec.d.ts.map +1 -1
  304. package/dist/schema/spec.js +5 -0
  305. package/dist/schema/spec.js.map +1 -1
  306. package/dist/schema/task.d.ts +187 -29
  307. package/dist/schema/task.d.ts.map +1 -1
  308. package/dist/schema/task.js +12 -2
  309. package/dist/schema/task.js.map +1 -1
  310. package/dist/schema/triage.d.ts +22 -22
  311. package/dist/sessions/cache.d.ts +119 -0
  312. package/dist/sessions/cache.d.ts.map +1 -0
  313. package/dist/sessions/cache.js +284 -0
  314. package/dist/sessions/cache.js.map +1 -0
  315. package/dist/sessions/index.d.ts +1 -0
  316. package/dist/sessions/index.d.ts.map +1 -1
  317. package/dist/sessions/index.js +2 -0
  318. package/dist/sessions/index.js.map +1 -1
  319. package/dist/sessions/legacy.d.ts +77 -0
  320. package/dist/sessions/legacy.d.ts.map +1 -0
  321. package/dist/sessions/legacy.js +146 -0
  322. package/dist/sessions/legacy.js.map +1 -0
  323. package/dist/sessions/store.d.ts +115 -71
  324. package/dist/sessions/store.d.ts.map +1 -1
  325. package/dist/sessions/store.js +357 -182
  326. package/dist/sessions/store.js.map +1 -1
  327. package/dist/sessions/types.d.ts +44 -16
  328. package/dist/sessions/types.d.ts.map +1 -1
  329. package/dist/sessions/types.js +11 -2
  330. package/dist/sessions/types.js.map +1 -1
  331. package/dist/strings/errors.d.ts +32 -0
  332. package/dist/strings/errors.d.ts.map +1 -1
  333. package/dist/strings/errors.js +17 -0
  334. package/dist/strings/errors.js.map +1 -1
  335. package/dist/strings/labels.d.ts +1 -0
  336. package/dist/strings/labels.d.ts.map +1 -1
  337. package/dist/strings/labels.js +1 -0
  338. package/dist/strings/labels.js.map +1 -1
  339. package/dist/utils/activity.d.ts +101 -0
  340. package/dist/utils/activity.d.ts.map +1 -0
  341. package/dist/utils/activity.js +408 -0
  342. package/dist/utils/activity.js.map +1 -0
  343. package/dist/utils/git.d.ts +31 -0
  344. package/dist/utils/git.d.ts.map +1 -1
  345. package/dist/utils/git.js +87 -0
  346. package/dist/utils/git.js.map +1 -1
  347. package/dist/utils/index.d.ts +2 -0
  348. package/dist/utils/index.d.ts.map +1 -1
  349. package/dist/utils/index.js +1 -0
  350. package/dist/utils/index.js.map +1 -1
  351. package/dist/web-ui/_app/immutable/assets/0.tmlwn-Ih.css +1 -0
  352. package/dist/web-ui/_app/immutable/assets/9.BwwJybWx.css +1 -0
  353. package/dist/web-ui/_app/immutable/chunks/2KqE8gtn.js +1 -0
  354. package/dist/web-ui/_app/immutable/chunks/70-t_QvE.js +1 -0
  355. package/dist/web-ui/_app/immutable/chunks/AiWQj974.js +1 -0
  356. package/dist/web-ui/_app/immutable/chunks/B25nWFyA.js +5 -0
  357. package/dist/web-ui/_app/immutable/chunks/B2bcA_Q_.js +1 -0
  358. package/dist/web-ui/_app/immutable/chunks/B5e5HYyB.js +1 -0
  359. package/dist/web-ui/_app/immutable/chunks/B7-5z6eA.js +1 -0
  360. package/dist/web-ui/_app/immutable/chunks/B7bGmhK0.js +1 -0
  361. package/dist/web-ui/_app/immutable/chunks/B8tYZKAE.js +1 -0
  362. package/dist/web-ui/_app/immutable/chunks/BFGAyJjD.js +1 -0
  363. package/dist/web-ui/_app/immutable/chunks/BG0850zf.js +1 -0
  364. package/dist/web-ui/_app/immutable/chunks/BG8eSzAd.js +1 -0
  365. package/dist/web-ui/_app/immutable/chunks/BIMxXS8I.js +1 -0
  366. package/dist/web-ui/_app/immutable/chunks/BSzL1fpU.js +1 -0
  367. package/dist/web-ui/_app/immutable/chunks/BYtjHfeq.js +1 -0
  368. package/dist/web-ui/_app/immutable/chunks/{D1ArdqNb.js → Bp5pFYXL.js} +1 -1
  369. package/dist/web-ui/_app/immutable/chunks/BsJFsuAT.js +1 -0
  370. package/dist/web-ui/_app/immutable/chunks/BvpNHcD6.js +1 -0
  371. package/dist/web-ui/_app/immutable/chunks/BypqA25-.js +1 -0
  372. package/dist/web-ui/_app/immutable/chunks/C0w6WDm5.js +1 -0
  373. package/dist/web-ui/_app/immutable/chunks/C5_PAZ0y.js +1 -0
  374. package/dist/web-ui/_app/immutable/chunks/CDRO15Iv.js +1 -0
  375. package/dist/web-ui/_app/immutable/chunks/CF1CoqD5.js +1 -0
  376. package/dist/web-ui/_app/immutable/chunks/CS2sa4_m.js +1 -0
  377. package/dist/web-ui/_app/immutable/chunks/CWUQwB9H.js +1 -0
  378. package/dist/web-ui/_app/immutable/chunks/CY5FDdSU.js +1 -0
  379. package/dist/web-ui/_app/immutable/chunks/C_7MTDoj.js +1 -0
  380. package/dist/web-ui/_app/immutable/chunks/CaAJD3dl.js +1 -0
  381. package/dist/web-ui/_app/immutable/chunks/{i-XnOIX0.js → ChB5iyEL.js} +1 -1
  382. package/dist/web-ui/_app/immutable/chunks/ChQD-6N8.js +1 -0
  383. package/dist/web-ui/_app/immutable/chunks/{BCkp8Hs8.js → CqbsoCwA.js} +1 -1
  384. package/dist/web-ui/_app/immutable/chunks/DCeJW50p.js +1 -0
  385. package/dist/web-ui/_app/immutable/chunks/DJtZNgcs.js +1 -0
  386. package/dist/web-ui/_app/immutable/chunks/DKIeaprD.js +1 -0
  387. package/dist/web-ui/_app/immutable/chunks/DLd2uVIA.js +1 -0
  388. package/dist/web-ui/_app/immutable/chunks/DW_subyT.js +2 -0
  389. package/dist/web-ui/_app/immutable/chunks/DbU6lVn0.js +1 -0
  390. package/dist/web-ui/_app/immutable/chunks/Dc7ZCC5m.js +1 -0
  391. package/dist/web-ui/_app/immutable/chunks/Dd5umPsk.js +2 -0
  392. package/dist/web-ui/_app/immutable/chunks/Dg_zDpDS.js +1 -0
  393. package/dist/web-ui/_app/immutable/chunks/Dgqu8Yuc.js +1 -0
  394. package/dist/web-ui/_app/immutable/chunks/DmxsPZTB.js +1 -0
  395. package/dist/web-ui/_app/immutable/chunks/DphTaFUB.js +1 -0
  396. package/dist/web-ui/_app/immutable/chunks/DqK4iHp0.js +1 -0
  397. package/dist/web-ui/_app/immutable/chunks/DqT6OH_u.js +2 -0
  398. package/dist/web-ui/_app/immutable/chunks/Ds9I9wQb.js +1 -0
  399. package/dist/web-ui/_app/immutable/chunks/Du5ng3u4.js +1 -0
  400. package/dist/web-ui/_app/immutable/chunks/DxJw79Wi.js +1 -0
  401. package/dist/web-ui/_app/immutable/chunks/GFTX8GgV.js +1 -0
  402. package/dist/web-ui/_app/immutable/chunks/HNjs76Zz.js +1 -0
  403. package/dist/web-ui/_app/immutable/chunks/HVMjDi4_.js +1 -0
  404. package/dist/web-ui/_app/immutable/chunks/P0A_fJvS.js +1 -0
  405. package/dist/web-ui/_app/immutable/chunks/T3vGWjIL.js +1 -0
  406. package/dist/web-ui/_app/immutable/chunks/VTmrX9Qu.js +1 -0
  407. package/dist/web-ui/_app/immutable/chunks/Xvwhx_F1.js +1 -0
  408. package/dist/web-ui/_app/immutable/chunks/Yyz1XMQA.js +1 -0
  409. package/dist/web-ui/_app/immutable/chunks/dh5HeqUr.js +1 -0
  410. package/dist/web-ui/_app/immutable/chunks/fZMteyca.js +62 -0
  411. package/dist/web-ui/_app/immutable/chunks/{D28BF5MJ.js → gPrj-hqC.js} +1 -1
  412. package/dist/web-ui/_app/immutable/chunks/htcWMiYN.js +1 -0
  413. package/dist/web-ui/_app/immutable/chunks/oTsvd9y4.js +1 -0
  414. package/dist/web-ui/_app/immutable/chunks/qJfLUwU4.js +1 -0
  415. package/dist/web-ui/_app/immutable/chunks/xCtiO_JE.js +1 -0
  416. package/dist/web-ui/_app/immutable/chunks/y4GeEH6k.js +1 -0
  417. package/dist/web-ui/_app/immutable/entry/app.C4h_eOn6.js +2 -0
  418. package/dist/web-ui/_app/immutable/entry/start.CQFTf9ep.js +1 -0
  419. package/dist/web-ui/_app/immutable/nodes/0.Dh1xO970.js +1 -0
  420. package/dist/web-ui/_app/immutable/nodes/1.l75D3Opx.js +1 -0
  421. package/dist/web-ui/_app/immutable/nodes/10.DBidBPc-.js +1 -0
  422. package/dist/web-ui/_app/immutable/nodes/11.Ab0gUKWe.js +1 -0
  423. package/dist/web-ui/_app/immutable/nodes/12.CMsnoxfs.js +1 -0
  424. package/dist/web-ui/_app/immutable/nodes/13.D8YKuknB.js +1 -0
  425. package/dist/web-ui/_app/immutable/nodes/14.DZ0aan7y.js +1 -0
  426. package/dist/web-ui/_app/immutable/nodes/15.CUIKreDL.js +2 -0
  427. package/dist/web-ui/_app/immutable/nodes/16.BWc8--BO.js +1 -0
  428. package/dist/web-ui/_app/immutable/nodes/2.CDUonbuh.js +1 -0
  429. package/dist/web-ui/_app/immutable/nodes/3.Ctg3M00i.js +1 -0
  430. package/dist/web-ui/_app/immutable/nodes/4.Ci-JDwbA.js +2 -0
  431. package/dist/web-ui/_app/immutable/nodes/5.CTyEDAq0.js +1 -0
  432. package/dist/web-ui/_app/immutable/nodes/6.BTZZqsAb.js +1 -0
  433. package/dist/web-ui/_app/immutable/nodes/7.BI52g_Jo.js +137 -0
  434. package/dist/web-ui/_app/immutable/nodes/8.3hZPaB9x.js +1 -0
  435. package/dist/web-ui/_app/immutable/nodes/9.DS49kvwl.js +29 -0
  436. package/dist/web-ui/_app/version.json +1 -1
  437. package/dist/web-ui/favicon-192.png +0 -0
  438. package/dist/web-ui/favicon-32.png +0 -0
  439. package/dist/web-ui/favicon.ico +0 -0
  440. package/dist/web-ui/index.html +14 -11
  441. package/package.json +14 -7
  442. package/plugin/.claude-plugin/marketplace.json +1 -1
  443. package/plugin/.claude-plugin/plugin.json +1 -1
  444. package/plugin/plugins/kspec/skills/merge/SKILL.md +127 -0
  445. package/plugin/plugins/kspec/skills/plan/SKILL.md +55 -26
  446. package/plugin/plugins/kspec/skills/review/SKILL.md +350 -133
  447. package/plugin/plugins/kspec/skills/task-work/SKILL.md +96 -106
  448. package/templates/agents-sections/04-pr-workflow.md +15 -12
  449. package/templates/agents-sections/06-ralph-loop.md +15 -10
  450. package/templates/skills/manifest.yaml +25 -7
  451. package/templates/skills/merge/SKILL.md +120 -0
  452. package/templates/skills/plan/SKILL.md +55 -26
  453. package/templates/skills/review/SKILL.md +346 -130
  454. package/templates/skills/task-work/SKILL.md +93 -103
  455. package/dist/web-ui/_app/immutable/assets/0.BxCxvrZR.css +0 -1
  456. package/dist/web-ui/_app/immutable/chunks/B-CZR0q8.js +0 -1
  457. package/dist/web-ui/_app/immutable/chunks/B1IR5Su5.js +0 -1
  458. package/dist/web-ui/_app/immutable/chunks/B_Cvvtc4.js +0 -1
  459. package/dist/web-ui/_app/immutable/chunks/BtFaGGII.js +0 -1
  460. package/dist/web-ui/_app/immutable/chunks/Bu8JVsCH.js +0 -1
  461. package/dist/web-ui/_app/immutable/chunks/C87u-CNA.js +0 -1
  462. package/dist/web-ui/_app/immutable/chunks/CrFkBTYp.js +0 -1
  463. package/dist/web-ui/_app/immutable/chunks/D6RtLpzL.js +0 -1
  464. package/dist/web-ui/_app/immutable/chunks/D7FHSgx2.js +0 -1
  465. package/dist/web-ui/_app/immutable/chunks/DBXrsxZQ.js +0 -2
  466. package/dist/web-ui/_app/immutable/chunks/Da_hHMuA.js +0 -1
  467. package/dist/web-ui/_app/immutable/chunks/Do6LchSF.js +0 -1
  468. package/dist/web-ui/_app/immutable/chunks/DoNPtcAw.js +0 -1
  469. package/dist/web-ui/_app/immutable/chunks/DtUbXRZz.js +0 -1
  470. package/dist/web-ui/_app/immutable/chunks/DyFPRlLl.js +0 -1
  471. package/dist/web-ui/_app/immutable/chunks/DzAP8lRM.js +0 -1
  472. package/dist/web-ui/_app/immutable/chunks/DzVXElzN.js +0 -2
  473. package/dist/web-ui/_app/immutable/chunks/aoPBFken.js +0 -1
  474. package/dist/web-ui/_app/immutable/chunks/laxtrUO3.js +0 -1
  475. package/dist/web-ui/_app/immutable/chunks/q1nIWgqB.js +0 -1
  476. package/dist/web-ui/_app/immutable/chunks/sTLbk5Nm.js +0 -1
  477. package/dist/web-ui/_app/immutable/chunks/vwKgQu5P.js +0 -5
  478. package/dist/web-ui/_app/immutable/entry/app.BCwMcqnT.js +0 -2
  479. package/dist/web-ui/_app/immutable/entry/start.wKCQH-tt.js +0 -1
  480. package/dist/web-ui/_app/immutable/nodes/0.CjGVMG74.js +0 -1
  481. package/dist/web-ui/_app/immutable/nodes/1.B6_AIPan.js +0 -1
  482. package/dist/web-ui/_app/immutable/nodes/2.q4oCS7Ws.js +0 -1
  483. package/dist/web-ui/_app/immutable/nodes/3.rTKZf9o2.js +0 -1
  484. package/dist/web-ui/_app/immutable/nodes/4.DVIDRu1d.js +0 -1
  485. package/dist/web-ui/_app/immutable/nodes/5.8PtPXIOd.js +0 -1
  486. package/dist/web-ui/_app/immutable/nodes/6.ZZrTemy_.js +0 -1
  487. package/dist/web-ui/_app/immutable/nodes/7.IP-gxCxi.js +0 -1
@@ -9,8 +9,10 @@ import { InboxFileSchema, InboxItemSchema, ManifestSchema, SpecItemSchema, TaskS
9
9
  import { errors } from "../strings/index.js";
10
10
  import { ItemIndex } from "./items.js";
11
11
  import { ReferenceIndex } from "./refs.js";
12
- import { detectRunningFromShadowWorktree, detectShadow, ShadowError, } from "./shadow.js";
12
+ import { loadReviewRecords } from "./reviews.js";
13
+ import { createShadowError, detectRunningFromShadowWorktree, detectShadow, getShadowStatus, hasRemoteTracking, resolveProjectRoots, shadowNeedsSync, shadowPull, ShadowError, } from "./shadow.js";
13
14
  import { loadProjectConfig, } from "./config.js";
15
+ import { consumeSyncMode } from "../cli/sync-mode.js";
14
16
  import { TraitIndex } from "./traits.js";
15
17
  /**
16
18
  * Log a debug message (only when KSPEC_DEBUG=1)
@@ -54,11 +56,98 @@ export function parseYaml(content) {
54
56
  * causes lines containing only spaces to grow. We post-process the output to filter
55
57
  * these whitespace-only lines. See: https://github.com/eemeli/yaml - stringifyString.ts
56
58
  */
59
+ /**
60
+ * Canonical key priority tiers for YAML serialization.
61
+ * Keys are sorted by tier first, then alphabetically within each tier.
62
+ * _ulid is always first (tier 0). Keys not listed default to tier 50.
63
+ */
64
+ const KEY_PRIORITY = {
65
+ // Tier 0: ULID — always first for record boundary detection
66
+ _ulid: 0,
67
+ // Tier 1: Identity fields
68
+ slugs: 1,
69
+ title: 2,
70
+ type: 3,
71
+ // Tier 2: Content / description
72
+ description: 10,
73
+ text: 10,
74
+ content: 10,
75
+ // Tier 3: Spec relationships
76
+ spec_ref: 15,
77
+ derivation: 16,
78
+ meta_ref: 17,
79
+ plan_ref: 18,
80
+ origin: 19,
81
+ // Tier 4: Status / state
82
+ status: 20,
83
+ maturity: 20,
84
+ blocked_by: 21,
85
+ closed_reason: 22,
86
+ disposition: 23,
87
+ // Tier 5: Relationships
88
+ depends_on: 25,
89
+ context: 26,
90
+ implements: 27,
91
+ relates_to: 28,
92
+ tests: 29,
93
+ traits: 30,
94
+ supersedes: 31,
95
+ acceptance_criteria: 32,
96
+ // Tier 6: Work metadata
97
+ priority: 35,
98
+ complexity: 36,
99
+ tags: 37,
100
+ assignee: 38,
101
+ // Tier 7: VCS / review
102
+ vcs_refs: 40,
103
+ review_url: 41,
104
+ review_ref: 42,
105
+ submission_linkage: 43,
106
+ session_id: 44,
107
+ // Tier 8: Timestamps
108
+ created: 60,
109
+ created_at: 60,
110
+ created_by: 61,
111
+ started_at: 62,
112
+ submitted_at: 63,
113
+ completed_at: 64,
114
+ updated_at: 65,
115
+ acted_at: 66,
116
+ deprecated_in: 67,
117
+ superseded_by: 68,
118
+ verified_at: 69,
119
+ verified_by: 70,
120
+ // Tier 9: Audit / append-only
121
+ notes: 80,
122
+ todos: 81,
123
+ // Tier 10: Automation / config
124
+ automation: 90,
125
+ traceability: 91,
126
+ };
127
+ /**
128
+ * Compare two YAML map entries for canonical field ordering.
129
+ * _ulid is always first. Known keys are ordered by priority tier,
130
+ * with alphabetical tiebreaking within the same tier.
131
+ * Unknown keys sort after tier 50 (alphabetically among themselves).
132
+ */
133
+ export function canonicalKeyComparator(a, b) {
134
+ const aKey = String(a.key);
135
+ const bKey = String(b.key);
136
+ const aPriority = KEY_PRIORITY[aKey] ?? 50;
137
+ const bPriority = KEY_PRIORITY[bKey] ?? 50;
138
+ if (aPriority !== bPriority)
139
+ return aPriority - bPriority;
140
+ return aKey.localeCompare(bKey);
141
+ }
57
142
  export function toYaml(obj) {
58
- let yamlString = YAML.stringify(obj, {
143
+ // JSON round-trip breaks shared object references so the yaml library
144
+ // won't generate anchors/aliases that crash when sortMapEntries reorders keys.
145
+ // structuredClone preserves shared refs, so JSON.parse(JSON.stringify()) is needed.
146
+ const cloned = JSON.parse(JSON.stringify(obj));
147
+ let yamlString = YAML.stringify(cloned, {
59
148
  indent: 2,
60
149
  lineWidth: 100,
61
- sortMapEntries: false,
150
+ sortMapEntries: canonicalKeyComparator,
62
151
  });
63
152
  // Post-process to fix yaml library blank line accumulation bug.
64
153
  // Filter out lines that contain only spaces/tabs (not truly empty lines).
@@ -108,7 +197,7 @@ export async function writeYamlFile(filePath, data) {
108
197
  buffer.write(filePath, content);
109
198
  return;
110
199
  }
111
- await fs.writeFile(filePath, content, "utf-8");
200
+ await writeFileAtomic(filePath, content);
112
201
  }
113
202
  /**
114
203
  * Write object to YAML file while preserving formatting and comments.
@@ -125,7 +214,14 @@ export async function writeYamlFilePreserveFormat(filePath, data) {
125
214
  buffer.write(filePath, content);
126
215
  return;
127
216
  }
128
- await fs.writeFile(filePath, content, "utf-8");
217
+ await writeFileAtomic(filePath, content);
218
+ }
219
+ async function writeFileAtomic(filePath, content) {
220
+ const dir = path.dirname(filePath);
221
+ await fs.mkdir(dir, { recursive: true });
222
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
223
+ await fs.writeFile(tmpPath, content, "utf-8");
224
+ await fs.rename(tmpPath, filePath);
129
225
  }
130
226
  /**
131
227
  * Find task files in a directory
@@ -204,9 +300,10 @@ export async function findManifest(startDir) {
204
300
  */
205
301
  export async function initContext(startDir) {
206
302
  const cwd = startDir || process.cwd();
303
+ const projectRoots = resolveProjectRoots(cwd);
207
304
  // AC: @project-config ac-2, ac-6, ac-7 — load config before shadow detection
208
305
  // Config is loaded from git root, not cwd or KSPEC_SPEC_DIR temp dir
209
- const configResult = await loadProjectConfig(cwd);
306
+ const configResult = await loadProjectConfig(cwd, projectRoots?.mainRoot);
210
307
  // AC: @project-config ac-3 — emit warning to stderr if config had issues
211
308
  if (configResult.warning) {
212
309
  console.error(`Warning: ${configResult.warning}`);
@@ -227,9 +324,12 @@ export async function initContext(startDir) {
227
324
  // Manifest exists but may be invalid
228
325
  }
229
326
  }
327
+ const rootDir = path.dirname(specDir);
230
328
  return {
231
- rootDir: path.dirname(specDir),
329
+ rootDir,
330
+ projectRoot: rootDir,
232
331
  specDir,
332
+ sessionsDir: path.join(rootDir, ".kspec-sessions"),
233
333
  manifestPath,
234
334
  manifest,
235
335
  shadow: null, // No shadow in overridden context
@@ -247,10 +347,54 @@ export async function initContext(startDir) {
247
347
  const shadow = await detectShadow(cwd, {
248
348
  branchName: config.shadow.branch,
249
349
  directory: config.shadow.directory,
250
- });
350
+ }, projectRoots?.mainRoot);
251
351
  if (shadow?.enabled) {
252
352
  // Shadow mode: use .kspec/ for everything
253
353
  const specDir = shadow.worktreeDir;
354
+ // AC: @shadow-lazy-read-sync ac-no-sync-env — KSPEC_NO_SYNC disables all sync
355
+ // AC: @shadow-lazy-read-sync ac-syncmode-consume-once — consume-once prevents double-pull
356
+ // AC: @shadow-lazy-read-sync ac-drift-check — drift check replaces unconditional pull
357
+ if (!process.env.KSPEC_NO_SYNC) {
358
+ const syncMode = consumeSyncMode();
359
+ // AC: @config-shadow ac-3 ac-7 — pass configured shadow options so sync
360
+ // uses the right branch name and remote instead of hardcoded defaults
361
+ const shadowOpts = {
362
+ branchName: config.shadow.branch,
363
+ directory: config.shadow.directory,
364
+ remote: config.shadow.remote?.value,
365
+ remoteType: config.shadow.remote?.type,
366
+ };
367
+ // AC: @shadow-write-sync ac-write-skips-read-check — skip pre-read sync for mutating commands
368
+ if (syncMode !== "skip") {
369
+ try {
370
+ const tracked = await hasRemoteTracking(specDir, shadowOpts);
371
+ if (tracked) {
372
+ let shouldPull = false;
373
+ if (syncMode === "always") {
374
+ // AC: @shadow-lazy-read-sync ac-session-start-always-pulls
375
+ shouldPull = true;
376
+ }
377
+ else {
378
+ // AC: @shadow-lazy-read-sync ac-drift-check — lightweight drift check
379
+ // AC: @shadow-lazy-read-sync ac-threshold-from-config
380
+ const remoteName = config.shadow.remote?.value ?? "origin";
381
+ const thresholdMs = config.shadow.sync_interval * 1000;
382
+ shouldPull = await shadowNeedsSync(specDir, remoteName, thresholdMs);
383
+ }
384
+ if (shouldPull) {
385
+ const syncResult = await shadowPull(specDir, shadowOpts);
386
+ if (syncResult.hadConflict) {
387
+ console.error("Warning: Shadow sync conflict detected. Run `kspec shadow resolve` to fix.");
388
+ console.error("Continuing with local state...");
389
+ }
390
+ }
391
+ }
392
+ }
393
+ catch {
394
+ // Pre-read sync is best-effort — don't fail the command
395
+ }
396
+ }
397
+ }
254
398
  const manifestPath = await findManifestInDir(specDir);
255
399
  let manifest = null;
256
400
  if (manifestPath) {
@@ -263,19 +407,33 @@ export async function initContext(startDir) {
263
407
  }
264
408
  }
265
409
  return {
266
- rootDir: shadow.projectRoot,
410
+ rootDir: projectRoots?.worktreeRoot ?? shadow.projectRoot,
411
+ projectRoot: shadow.projectRoot,
267
412
  specDir,
413
+ sessionsDir: path.join(shadow.projectRoot, ".kspec-sessions"),
268
414
  manifestPath,
269
415
  manifest,
270
416
  shadow,
271
417
  config,
272
418
  };
273
419
  }
420
+ // Fail closed when a repo already has shadow state but the configured worktree
421
+ // is missing or disconnected, rather than silently degrading into repo-root mode.
422
+ if (projectRoots?.mainRoot) {
423
+ const shadowStatus = await getShadowStatus(projectRoots.mainRoot, {
424
+ branchName: config.shadow.branch,
425
+ directory: config.shadow.directory,
426
+ });
427
+ if (shadowStatus.branchExists && !shadowStatus.healthy) {
428
+ throw createShadowError(shadowStatus);
429
+ }
430
+ }
274
431
  // Traditional mode: find manifest in spec/ or current directory
275
432
  const manifestPath = await findManifest(cwd);
276
433
  let manifest = null;
277
- let rootDir = cwd;
278
- let specDir = cwd;
434
+ let rootDir = projectRoots?.worktreeRoot ?? cwd;
435
+ const projectRoot = projectRoots?.mainRoot ?? rootDir;
436
+ let specDir = rootDir;
279
437
  if (manifestPath) {
280
438
  const manifestDir = path.dirname(manifestPath);
281
439
  // Handle spec/ subdirectory
@@ -295,7 +453,16 @@ export async function initContext(startDir) {
295
453
  // Manifest exists but may be invalid
296
454
  }
297
455
  }
298
- return { rootDir, specDir, manifestPath, manifest, shadow: null, config };
456
+ return {
457
+ rootDir,
458
+ projectRoot,
459
+ specDir,
460
+ sessionsDir: path.join(projectRoot, ".kspec-sessions"),
461
+ manifestPath,
462
+ manifest,
463
+ shadow: null,
464
+ config,
465
+ };
299
466
  }
300
467
  /**
301
468
  * Check if a filename is a potential manifest file.
@@ -321,7 +488,7 @@ function isManifestCandidate(filename) {
321
488
  *
322
489
  * AC: @manifest-discovery ac-1, ac-2, ac-3, ac-4, ac-5
323
490
  */
324
- async function findManifestInDir(dir) {
491
+ export async function findManifestInDir(dir) {
325
492
  // AC: @manifest-discovery ac-1, ac-2 - explicit names have priority
326
493
  const priorityCandidates = ["kynetic.yaml", "kynetic.spec.yaml"];
327
494
  for (const candidate of priorityCandidates) {
@@ -513,9 +680,96 @@ function stripRuntimeMetadata(task) {
513
680
  const { _sourceFile, ...cleanTask } = task;
514
681
  return cleanTask;
515
682
  }
683
+ /**
684
+ * Extract the raw task array and format info from a YAML file.
685
+ * Does NOT run schema validation — preserves original data for round-trip stability.
686
+ */
687
+ async function extractRawTaskArray(filePath) {
688
+ let existingRaw = null;
689
+ let useTasksWrapper = false;
690
+ try {
691
+ existingRaw = await readYamlFile(filePath);
692
+ if (existingRaw &&
693
+ typeof existingRaw === "object" &&
694
+ "tasks" in existingRaw) {
695
+ useTasksWrapper = true;
696
+ }
697
+ }
698
+ catch {
699
+ // File doesn't exist
700
+ return { rawTasks: [], useTasksWrapper: false };
701
+ }
702
+ if (!existingRaw) {
703
+ return { rawTasks: [], useTasksWrapper: false };
704
+ }
705
+ if (Array.isArray(existingRaw)) {
706
+ return { rawTasks: existingRaw, useTasksWrapper: false };
707
+ }
708
+ if (useTasksWrapper) {
709
+ const wrapper = existingRaw;
710
+ const tasks = wrapper.tasks;
711
+ return {
712
+ rawTasks: Array.isArray(tasks) ? tasks : [],
713
+ useTasksWrapper: true,
714
+ wrapperObj: wrapper,
715
+ };
716
+ }
717
+ return { rawTasks: [], useTasksWrapper: false };
718
+ }
719
+ /**
720
+ * Write raw task array back to file, preserving the wrapper format.
721
+ */
722
+ async function writeRawTaskArray(filePath, rawTasks, useTasksWrapper, wrapperObj) {
723
+ if (useTasksWrapper) {
724
+ // Preserve any extra top-level fields (e.g. kynetic_tasks version)
725
+ const output = wrapperObj ? { ...wrapperObj, tasks: rawTasks } : { tasks: rawTasks };
726
+ await writeYamlFilePreserveFormat(filePath, output);
727
+ }
728
+ else {
729
+ await writeYamlFilePreserveFormat(filePath, rawTasks);
730
+ }
731
+ }
732
+ /**
733
+ * Find task index in a raw array by ULID match.
734
+ */
735
+ function findRawTaskIndex(rawTasks, ulid) {
736
+ return rawTasks.findIndex((t) => t && typeof t === "object" && t._ulid === ulid);
737
+ }
738
+ /**
739
+ * Merge a schema-normalized task onto the original raw task data.
740
+ * Only adds fields that were in the original raw data or that contain
741
+ * non-default values. This prevents Zod defaults from polluting YAML
742
+ * output with fields that weren't originally present.
743
+ *
744
+ * Fields present in rawTask are always updated with the new value.
745
+ * Fields NOT in rawTask are only added if they carry meaningful data
746
+ * (i.e. non-empty arrays, non-null values, etc.).
747
+ */
748
+ function mergeTaskPreservingRawShape(rawTask, normalizedTask) {
749
+ const result = {};
750
+ for (const [key, value] of Object.entries(normalizedTask)) {
751
+ if (key in rawTask) {
752
+ // Field existed in raw — always include (even if value changed)
753
+ result[key] = value;
754
+ }
755
+ else {
756
+ // Field was added by schema normalization — only include if non-trivial
757
+ const isEmptyArray = Array.isArray(value) && value.length === 0;
758
+ const isNull = value === null || value === undefined;
759
+ if (!isEmptyArray && !isNull) {
760
+ result[key] = value;
761
+ }
762
+ }
763
+ }
764
+ return result;
765
+ }
516
766
  /**
517
767
  * Save a task to its source file (or default location for new tasks).
518
768
  * Preserves file format (tasks: [...] wrapper vs plain array).
769
+ *
770
+ * Non-target tasks are preserved as raw data (no schema parsing) to ensure
771
+ * round-trip stability — fields not present in the original YAML won't be
772
+ * added by Zod defaults.
519
773
  */
520
774
  export async function saveTask(ctx, task) {
521
775
  // Determine target file: use _sourceFile if present, otherwise default
@@ -525,70 +779,21 @@ export async function saveTask(ctx, task) {
525
779
  // Ensure directory exists
526
780
  const dir = path.dirname(taskFilePath);
527
781
  await fs.mkdir(dir, { recursive: true });
528
- // Load existing tasks from the target file
529
- let existingRaw = null;
530
- let useTasksWrapper = false;
531
- try {
532
- existingRaw = await readYamlFile(taskFilePath);
533
- // Detect if file uses { tasks: [...] } format
534
- if (existingRaw &&
535
- typeof existingRaw === "object" &&
536
- "tasks" in existingRaw) {
537
- useTasksWrapper = true;
538
- }
539
- }
540
- catch {
541
- // File doesn't exist, start fresh
542
- }
543
- // Parse existing tasks from file
544
- let fileTasks = [];
545
- if (existingRaw) {
546
- if (Array.isArray(existingRaw)) {
547
- for (const t of existingRaw) {
548
- const result = TaskSchema.safeParse(t);
549
- if (result.success) {
550
- fileTasks.push(result.data);
551
- }
552
- }
553
- }
554
- else if (useTasksWrapper) {
555
- // Try TasksFileSchema first (has kynetic_tasks version)
556
- const parsed = TasksFileSchema.safeParse(existingRaw);
557
- if (parsed.success) {
558
- fileTasks = parsed.data.tasks;
559
- }
560
- else {
561
- // Fall back to raw tasks array (common format without version field)
562
- const rawTasks = existingRaw.tasks;
563
- if (Array.isArray(rawTasks)) {
564
- for (const t of rawTasks) {
565
- const result = TaskSchema.safeParse(t);
566
- if (result.success) {
567
- fileTasks.push(result.data);
568
- }
569
- }
570
- }
571
- }
572
- }
573
- }
782
+ // Load raw task data without schema normalization
783
+ const { rawTasks, useTasksWrapper, wrapperObj } = await extractRawTaskArray(taskFilePath);
574
784
  // Strip runtime metadata before saving
575
785
  const cleanTask = stripRuntimeMetadata(task);
576
- // Update existing or add new
577
- const existingIndex = fileTasks.findIndex((t) => t._ulid === task._ulid);
786
+ // Update existing or add new — replace only the target task
787
+ const existingIndex = findRawTaskIndex(rawTasks, task._ulid);
578
788
  if (existingIndex >= 0) {
579
- fileTasks[existingIndex] = cleanTask;
580
- }
581
- else {
582
- fileTasks.push(cleanTask);
583
- }
584
- // Save in the same format as original (or tasks: wrapper for new files)
585
- // Use format-preserving write to maintain formatting and comments
586
- if (useTasksWrapper) {
587
- await writeYamlFilePreserveFormat(taskFilePath, { tasks: fileTasks });
789
+ // Merge onto raw data to avoid adding Zod defaults for absent fields
790
+ const rawTarget = rawTasks[existingIndex];
791
+ rawTasks[existingIndex] = mergeTaskPreservingRawShape(rawTarget, cleanTask);
588
792
  }
589
793
  else {
590
- await writeYamlFilePreserveFormat(taskFilePath, fileTasks);
794
+ rawTasks.push(cleanTask);
591
795
  }
796
+ await writeRawTaskArray(taskFilePath, rawTasks, useTasksWrapper, wrapperObj);
592
797
  });
593
798
  }
594
799
  /**
@@ -596,6 +801,9 @@ export async function saveTask(ctx, task) {
596
801
  *
597
802
  * The callback receives the current task value while holding the task file lock,
598
803
  * so concurrent writers cannot clobber unrelated fields (for example status vs notes).
804
+ *
805
+ * Non-target tasks are preserved as raw data (no schema parsing) to ensure
806
+ * round-trip stability.
599
807
  */
600
808
  export async function mutateTaskAtomically(ctx, task, mutate) {
601
809
  const taskFilePath = task._sourceFile || getDefaultTaskFilePath(ctx);
@@ -604,35 +812,24 @@ export async function mutateTaskAtomically(ctx, task, mutate) {
604
812
  // Ensure directory exists (important for default path in new repos)
605
813
  const dir = path.dirname(taskFilePath);
606
814
  await fs.mkdir(dir, { recursive: true });
607
- // Preserve existing file format (tasks wrapper vs plain array)
608
- let existingRaw = null;
609
- let useTasksWrapper = false;
610
- try {
611
- existingRaw = await readYamlFile(taskFilePath);
612
- if (existingRaw &&
613
- typeof existingRaw === "object" &&
614
- "tasks" in existingRaw) {
615
- useTasksWrapper = true;
616
- }
617
- }
618
- catch {
619
- throw new Error(`Task file not found: ${taskFilePath}`);
620
- }
621
- const fileTasks = await loadTasksFromFile(taskFilePath);
622
- const taskIndex = fileTasks.findIndex((t) => t._ulid === task._ulid);
815
+ // Load raw task data without schema normalization for non-target tasks
816
+ const { rawTasks, useTasksWrapper, wrapperObj } = await extractRawTaskArray(taskFilePath);
817
+ const taskIndex = findRawTaskIndex(rawTasks, task._ulid);
623
818
  if (taskIndex === -1) {
624
819
  throw new Error(`Task not found in file: ${task._ulid}`);
625
820
  }
626
- const latestTask = fileTasks[taskIndex];
821
+ // Schema-parse only the target task for the mutation callback
822
+ const rawTarget = rawTasks[taskIndex];
823
+ const parsed = TaskSchema.safeParse(rawTarget);
824
+ if (!parsed.success) {
825
+ throw new Error(`Invalid task data for ${task._ulid}: ${parsed.error.message}`);
826
+ }
827
+ const latestTask = { ...parsed.data, _sourceFile: taskFilePath };
627
828
  const mutatedTask = await mutate(latestTask);
628
829
  const cleanMutatedTask = stripRuntimeMetadata(mutatedTask);
629
- const serializedTasks = fileTasks.map((fileTask, index) => index === taskIndex ? cleanMutatedTask : stripRuntimeMetadata(fileTask));
630
- if (useTasksWrapper) {
631
- await writeYamlFilePreserveFormat(taskFilePath, { tasks: serializedTasks });
632
- }
633
- else {
634
- await writeYamlFilePreserveFormat(taskFilePath, serializedTasks);
635
- }
830
+ // Merge onto raw data to avoid adding Zod defaults for absent fields
831
+ rawTasks[taskIndex] = mergeTaskPreservingRawShape(rawTarget, cleanMutatedTask);
832
+ await writeRawTaskArray(taskFilePath, rawTasks, useTasksWrapper, wrapperObj);
636
833
  updatedTask = {
637
834
  ...cleanMutatedTask,
638
835
  _sourceFile: taskFilePath,
@@ -654,62 +851,18 @@ export async function deleteTask(_ctx, task) {
654
851
  const taskFilePath = task._sourceFile;
655
852
  // Lock the file to prevent concurrent read-modify-write races
656
853
  await withFileLock(taskFilePath, async () => {
657
- // Load existing file
658
- let existingRaw = null;
659
- let useTasksWrapper = false;
660
- try {
661
- existingRaw = await readYamlFile(taskFilePath);
662
- if (existingRaw &&
663
- typeof existingRaw === "object" &&
664
- "tasks" in existingRaw) {
665
- useTasksWrapper = true;
666
- }
667
- }
668
- catch {
854
+ // Load raw task data without schema normalization for round-trip stability
855
+ const { rawTasks, useTasksWrapper, wrapperObj } = await extractRawTaskArray(taskFilePath);
856
+ if (rawTasks.length === 0) {
669
857
  throw new Error(`Task file not found: ${taskFilePath}`);
670
858
  }
671
- // Parse existing tasks
672
- let fileTasks = [];
673
- if (existingRaw) {
674
- if (Array.isArray(existingRaw)) {
675
- for (const t of existingRaw) {
676
- const result = TaskSchema.safeParse(t);
677
- if (result.success) {
678
- fileTasks.push(result.data);
679
- }
680
- }
681
- }
682
- else if (useTasksWrapper) {
683
- const parsed = TasksFileSchema.safeParse(existingRaw);
684
- if (parsed.success) {
685
- fileTasks = parsed.data.tasks;
686
- }
687
- else {
688
- const rawTasks = existingRaw.tasks;
689
- if (Array.isArray(rawTasks)) {
690
- for (const t of rawTasks) {
691
- const result = TaskSchema.safeParse(t);
692
- if (result.success) {
693
- fileTasks.push(result.data);
694
- }
695
- }
696
- }
697
- }
698
- }
699
- }
700
- // Remove the task
701
- const originalCount = fileTasks.length;
702
- fileTasks = fileTasks.filter((t) => t._ulid !== task._ulid);
703
- if (fileTasks.length === originalCount) {
859
+ // Remove the task by ULID match on raw data
860
+ const taskIndex = findRawTaskIndex(rawTasks, task._ulid);
861
+ if (taskIndex === -1) {
704
862
  throw new Error(`Task not found in file: ${task._ulid}`);
705
863
  }
706
- // Save the modified file with format preservation
707
- if (useTasksWrapper) {
708
- await writeYamlFilePreserveFormat(taskFilePath, { tasks: fileTasks });
709
- }
710
- else {
711
- await writeYamlFilePreserveFormat(taskFilePath, fileTasks);
712
- }
864
+ rawTasks.splice(taskIndex, 1);
865
+ await writeRawTaskArray(taskFilePath, rawTasks, useTasksWrapper, wrapperObj);
713
866
  });
714
867
  }
715
868
  /**
@@ -1126,7 +1279,8 @@ export function findAnyItemByRef(tasks, items, ref) {
1126
1279
  export async function buildReferenceIndex(ctx) {
1127
1280
  const tasks = await loadAllTasks(ctx);
1128
1281
  const items = await loadAllItems(ctx);
1129
- const index = new ReferenceIndex(tasks, items);
1282
+ const reviews = await loadReviewRecords(ctx);
1283
+ const index = new ReferenceIndex(tasks, items, [], [], reviews);
1130
1284
  return { index, tasks, items };
1131
1285
  }
1132
1286
  /**
@@ -1138,7 +1292,8 @@ export async function buildReferenceIndex(ctx) {
1138
1292
  export async function buildIndexes(ctx, plans = []) {
1139
1293
  const tasks = await loadAllTasks(ctx);
1140
1294
  const items = await loadAllItems(ctx);
1141
- const refIndex = new ReferenceIndex(tasks, items, [], plans);
1295
+ const reviews = await loadReviewRecords(ctx);
1296
+ const refIndex = new ReferenceIndex(tasks, items, [], plans, reviews);
1142
1297
  const itemIndex = new ItemIndex(tasks, items);
1143
1298
  const traitIndex = new TraitIndex(items, refIndex);
1144
1299
  return { refIndex, itemIndex, traitIndex, tasks, items };
@@ -1318,6 +1473,31 @@ export async function addChildItem(_ctx, parent, child, childField) {
1318
1473
  return { item: cleanChild, path: childPath };
1319
1474
  });
1320
1475
  }
1476
+ /**
1477
+ * Add a trait item to the project-level traits array in kynetic.yaml.
1478
+ */
1479
+ export async function addProjectLevelTraitItem(ctx, item) {
1480
+ if (!ctx.manifestPath) {
1481
+ throw new Error("Could not find kynetic.yaml");
1482
+ }
1483
+ return withFileLock(ctx.manifestPath, async () => {
1484
+ const manifest = await readYamlFile(ctx.manifestPath);
1485
+ if (!manifest) {
1486
+ throw new Error("Could not load kynetic.yaml");
1487
+ }
1488
+ if (!Array.isArray(manifest.traits)) {
1489
+ manifest.traits = [];
1490
+ }
1491
+ const cleanItem = stripSpecItemMetadata(item);
1492
+ manifest.traits.push(cleanItem);
1493
+ await writeYamlFilePreserveFormat(ctx.manifestPath, manifest);
1494
+ const traitIndex = manifest.traits.length - 1;
1495
+ return {
1496
+ item: cleanItem,
1497
+ path: `traits[${traitIndex}]`,
1498
+ };
1499
+ });
1500
+ }
1321
1501
  /**
1322
1502
  * Update a spec item in place within its source file.
1323
1503
  * Works with nested structures using the _path field.