@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
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { execFile, spawn, spawnSync } from "node:child_process";
12
12
  import * as fs from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
13
14
  import * as path from "node:path";
14
15
  import { promisify } from "node:util";
15
16
  import { fileURLToPath } from "node:url";
@@ -38,6 +39,28 @@ async function runGitAsync(cwd, args, env) {
38
39
  });
39
40
  return { stdout: stdout.toString(), stderr: stderr.toString() };
40
41
  }
42
+ async function stashBrokenWorktreeDir(worktreeDir) {
43
+ const stat = await fs.stat(worktreeDir).catch(() => null);
44
+ if (!stat) {
45
+ return null;
46
+ }
47
+ const backupDir = `${worktreeDir}.repair-backup-${Date.now()}`;
48
+ await fs.rename(worktreeDir, backupDir);
49
+ return backupDir;
50
+ }
51
+ async function restoreStashedWorktreeDir(backupDir, worktreeDir) {
52
+ if (!backupDir) {
53
+ return;
54
+ }
55
+ await fs.rm(worktreeDir, { recursive: true, force: true });
56
+ await fs.rename(backupDir, worktreeDir);
57
+ }
58
+ async function discardStashedWorktreeDir(backupDir) {
59
+ if (!backupDir) {
60
+ return;
61
+ }
62
+ await fs.rm(backupDir, { recursive: true, force: true });
63
+ }
41
64
  function runGitSync(cwd, args) {
42
65
  const result = spawnSync("git", args, {
43
66
  cwd,
@@ -49,6 +72,129 @@ function runGitSync(cwd, args) {
49
72
  stdout: (result.stdout || "").toString(),
50
73
  };
51
74
  }
75
+ /**
76
+ * Parse git version from `git --version` output.
77
+ * Returns [major, minor, patch] or null if unparseable.
78
+ */
79
+ export function getGitVersion(cwd) {
80
+ const result = spawnSync("git", ["--version"], {
81
+ cwd,
82
+ stdio: ["ignore", "pipe", "pipe"],
83
+ encoding: "utf-8",
84
+ });
85
+ if (result.error || result.status !== 0)
86
+ return null;
87
+ const match = (result.stdout || "").match(/(\d+)\.(\d+)\.(\d+)/);
88
+ if (!match)
89
+ return null;
90
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
91
+ }
92
+ /**
93
+ * Check if the installed git supports `git worktree add --orphan` (requires >= 2.42.0).
94
+ */
95
+ export function gitSupportsOrphanWorktree(cwd) {
96
+ const version = getGitVersion(cwd);
97
+ if (!version)
98
+ return false;
99
+ const [major, minor] = version;
100
+ return major > 2 || (major === 2 && minor >= 42);
101
+ }
102
+ /**
103
+ * Fallback for creating an orphan branch when git < 2.42.
104
+ *
105
+ * Strategy:
106
+ * 1. Create a temp bare repo in the OS temp directory
107
+ * 2. Create an orphan branch with an empty commit there
108
+ * 3. Push that branch to the project repo (via file:// protocol)
109
+ * 4. Clean up the temp repo
110
+ * 5. Attach using standard `git worktree add <dir> <branch>`
111
+ *
112
+ * This approach NEVER modifies the project's working tree.
113
+ *
114
+ * AC: @config-shadow ac-10
115
+ */
116
+ export async function createOrphanBranchFallback(projectRoot, branchName, directoryName) {
117
+ const tmpDir = await fs.mkdtemp(path.join(tmpdir(), "kspec-orphan-"));
118
+ try {
119
+ // 1. Init a bare repo in the temp dir
120
+ await runGitAsync(tmpDir, ["init", "--bare"]);
121
+ // 2. Create the orphan branch using a temporary non-bare clone.
122
+ // We need a working tree to make a commit, so clone the bare repo.
123
+ const workDir = await fs.mkdtemp(path.join(tmpdir(), "kspec-orphan-work-"));
124
+ try {
125
+ await runGitAsync(workDir, ["clone", tmpDir, "."]);
126
+ await runGitAsync(workDir, [
127
+ "config",
128
+ "user.email",
129
+ "kspec@localhost",
130
+ ]);
131
+ await runGitAsync(workDir, [
132
+ "config",
133
+ "user.name",
134
+ "kspec",
135
+ ]);
136
+ // Create an orphan branch (checkout --orphan works on all git versions)
137
+ await runGitAsync(workDir, [
138
+ "checkout",
139
+ "--orphan",
140
+ branchName,
141
+ ]);
142
+ // Remove any files that might have been staged
143
+ try {
144
+ await runGitAsync(workDir, ["rm", "-rf", "."]);
145
+ }
146
+ catch {
147
+ // May fail if nothing to remove (empty repo) - that's fine
148
+ }
149
+ // Create an empty initial commit
150
+ await runGitAsync(workDir, [
151
+ "commit",
152
+ "--allow-empty",
153
+ "-m",
154
+ `Initialize ${branchName}`,
155
+ ]);
156
+ // Push the orphan branch back to the bare repo
157
+ await runGitAsync(workDir, [
158
+ "push",
159
+ "origin",
160
+ branchName,
161
+ ]);
162
+ }
163
+ finally {
164
+ await fs.rm(workDir, { recursive: true, force: true });
165
+ }
166
+ // 3. Push from the temp bare repo to the project repo
167
+ // Use file:// protocol to ensure git treats it as a proper remote
168
+ await runGitAsync(tmpDir, [
169
+ "push",
170
+ `file://${path.resolve(projectRoot)}`,
171
+ branchName,
172
+ ]);
173
+ }
174
+ finally {
175
+ // 4. Clean up the temp bare repo
176
+ await fs.rm(tmpDir, { recursive: true, force: true });
177
+ }
178
+ // 5. Attach worktree using standard git worktree add (no --orphan flag)
179
+ await runGitAsync(projectRoot, [
180
+ "worktree",
181
+ "add",
182
+ directoryName,
183
+ branchName,
184
+ ]);
185
+ // 6. Remove all tracked files from the worktree since the fallback
186
+ // created an empty commit but `git worktree add` may still populate
187
+ // the index from the branch. Clear anything that appeared.
188
+ try {
189
+ const { stdout } = await runGitAsync(path.join(projectRoot, directoryName), ["ls-files"]);
190
+ if (stdout.trim()) {
191
+ await runGitAsync(path.join(projectRoot, directoryName), ["rm", "-rf", "."]);
192
+ }
193
+ }
194
+ catch {
195
+ // Nothing to remove — expected for an empty commit
196
+ }
197
+ }
52
198
  // Import getVerboseMode for checking CLI --debug-shadow flag
53
199
  // We use a getter function to avoid issues with circular dependencies
54
200
  let getVerboseModeFunc = null;
@@ -76,6 +222,11 @@ export const SHADOW_BRANCH_NAME = "kspec-meta";
76
222
  * Default shadow worktree directory
77
223
  */
78
224
  export const SHADOW_WORKTREE_DIR = ".kspec";
225
+ /**
226
+ * Sessions storage directory name (at project root, separate from shadow worktree).
227
+ * AC: @session-storage-modes ac-sessions-dir
228
+ */
229
+ export const SESSIONS_WORKTREE_DIR = ".kspec-sessions";
79
230
  /**
80
231
  * Get effective branch name from options or default.
81
232
  * AC: @config-shadow ac-7 — backward compat when called without config
@@ -90,6 +241,54 @@ function getBranchName(options) {
90
241
  function getDirectoryName(options) {
91
242
  return options?.directory ?? SHADOW_WORKTREE_DIR;
92
243
  }
244
+ /**
245
+ * Get effective remote name from options or default.
246
+ * AC: @config-shadow ac-3 ac-7 — resolves configured remote for fetch/push/pull.
247
+ * Named remotes use the name directly; path/URL remotes use the auto-created "kspec-specs".
248
+ */
249
+ export function getRemoteName(options) {
250
+ if (!options?.remote)
251
+ return "origin";
252
+ const remoteType = options.remoteType ?? "named";
253
+ if (remoteType === "path" || remoteType === "url")
254
+ return "kspec-specs";
255
+ return options.remote;
256
+ }
257
+ /**
258
+ * Resolve the remote target used for direct ls-remote/fetch queries.
259
+ * Named remotes use the configured name, while path remotes expand "~".
260
+ */
261
+ function getRemoteQueryTarget(options) {
262
+ if (!options?.remote)
263
+ return "origin";
264
+ const remoteType = options.remoteType ?? "named";
265
+ if (remoteType !== "path") {
266
+ return options.remote;
267
+ }
268
+ if (options.remote.startsWith("~")) {
269
+ return options.remote.replace(/^~/, process.env.HOME || process.env.USERPROFILE || "~");
270
+ }
271
+ return options.remote;
272
+ }
273
+ /**
274
+ * Check whether the shadow branch exists on the configured/default remote.
275
+ */
276
+ export async function remoteShadowBranchExists(projectRoot, options) {
277
+ const branchName = getBranchName(options);
278
+ const remoteType = options?.remoteType ?? "named";
279
+ const remoteName = getRemoteName(options);
280
+ const remoteQueryTarget = getRemoteQueryTarget(options);
281
+ if (options?.remote) {
282
+ if (remoteType === "named" && !(await hasRemote(projectRoot, remoteName))) {
283
+ return false;
284
+ }
285
+ return remoteBranchExists(projectRoot, branchName, remoteQueryTarget);
286
+ }
287
+ if (!(await hasRemote(projectRoot, remoteName))) {
288
+ return false;
289
+ }
290
+ return remoteBranchExists(projectRoot, branchName, remoteQueryTarget);
291
+ }
93
292
  /**
94
293
  * Check if debug mode is enabled.
95
294
  * Debug mode can be enabled via:
@@ -127,6 +326,45 @@ export function getGitRoot(dir) {
127
326
  }
128
327
  return result.stdout.trim();
129
328
  }
329
+ function isSubmoduleCommonDir(commonDir) {
330
+ const segments = path.normalize(commonDir).split(path.sep).filter(Boolean);
331
+ const gitIndex = segments.lastIndexOf(".git");
332
+ return gitIndex >= 0 && segments[gitIndex + 1] === "modules";
333
+ }
334
+ export function resolveProjectRoots(dir) {
335
+ const result = runGitSync(dir, [
336
+ "rev-parse",
337
+ "--show-toplevel",
338
+ "--git-common-dir",
339
+ ]);
340
+ if (!result.ok) {
341
+ return null;
342
+ }
343
+ const [rawTopLevel, rawCommonDir] = result.stdout
344
+ .split(/\r?\n/)
345
+ .map((line) => line.trim())
346
+ .filter(Boolean);
347
+ if (!rawTopLevel || !rawCommonDir) {
348
+ return null;
349
+ }
350
+ const worktreeRoot = path.resolve(rawTopLevel);
351
+ const commonDir = path.isAbsolute(rawCommonDir)
352
+ ? path.resolve(rawCommonDir)
353
+ : path.resolve(worktreeRoot, rawCommonDir);
354
+ if (isSubmoduleCommonDir(commonDir) ||
355
+ commonDir === path.join(worktreeRoot, ".git")) {
356
+ return {
357
+ mainRoot: worktreeRoot,
358
+ worktreeRoot,
359
+ isWorktree: false,
360
+ };
361
+ }
362
+ return {
363
+ mainRoot: path.dirname(commonDir),
364
+ worktreeRoot,
365
+ isWorktree: true,
366
+ };
367
+ }
130
368
  /**
131
369
  * Check if a branch exists
132
370
  */
@@ -188,20 +426,18 @@ export async function detectRunningFromShadowWorktree(cwd, configuredDirectory)
188
426
  if (!match) {
189
427
  return null;
190
428
  }
191
- const gitdir = match[1];
429
+ const gitdir = path.resolve(cwd, match[1]);
192
430
  // Check if this is a worktree (pattern: <project>/.git/worktrees/<name>)
193
431
  if (gitdir.includes(".git/worktrees/")) {
194
- const worktreesMatch = gitdir.match(/^(.+)\/\.git\/worktrees\//);
432
+ const worktreesMatch = gitdir.match(/^(.*?)[/\\]\.git[/\\]worktrees[/\\]/);
195
433
  if (worktreesMatch) {
196
434
  const mainProjectRoot = worktreesMatch[1];
197
435
  const cwdBase = path.basename(cwd);
198
- const worktreeName = path.basename(gitdir);
199
436
  // AC: ac-8 — check multiple patterns for shadow worktree detection
200
437
  const directoryToCheck = configuredDirectory || SHADOW_WORKTREE_DIR;
201
- // Check if directory name matches default, configured, or worktree contains "kspec"
438
+ // Exact shadow directory names are always considered shadow worktrees.
202
439
  if (cwdBase === SHADOW_WORKTREE_DIR ||
203
- cwdBase === directoryToCheck ||
204
- worktreeName.includes("kspec")) {
440
+ cwdBase === directoryToCheck) {
205
441
  return mainProjectRoot;
206
442
  }
207
443
  // Additional check: see if this directory has a kspec manifest
@@ -239,8 +475,8 @@ export async function detectRunningFromShadowWorktree(cwd, configuredDirectory)
239
475
  * @param startDir Directory to start detection from
240
476
  * @param options Optional shadow configuration (branch name, directory)
241
477
  */
242
- export async function detectShadow(startDir, options) {
243
- const gitRoot = getGitRoot(startDir);
478
+ export async function detectShadow(startDir, options, mainRoot) {
479
+ const gitRoot = mainRoot ?? getGitRoot(startDir);
244
480
  if (!gitRoot) {
245
481
  return null;
246
482
  }
@@ -493,7 +729,12 @@ export function generateCommitMessage(operation, ref, detail) {
493
729
  parts.push(`Note on @${ref}`);
494
730
  break;
495
731
  case "task-add":
496
- parts.push(`Add task: ${detail || ref}`);
732
+ if (ref && detail) {
733
+ parts.push(`Add task @${ref}: ${detail}`);
734
+ }
735
+ else {
736
+ parts.push(`Add task: ${detail || ref}`);
737
+ }
497
738
  break;
498
739
  case "inbox-add":
499
740
  parts.push(`Inbox: ${detail?.slice(0, 50)}${(detail?.length || 0) > 50 ? "..." : ""}`);
@@ -573,6 +814,7 @@ export async function commitIfShadow(shadowConfig, operation, ref, detail, verbo
573
814
  const message = generateCommitMessage(operation, ref, detail);
574
815
  const committed = await shadowAutoCommit(shadowConfig.worktreeDir, message, verbose);
575
816
  // AC: @shadow-sync ac-1 - Fire-and-forget push after each commit
817
+ // AC: @shadow-write-sync ac-write-always-syncs — writes always sync via push path
576
818
  if (committed) {
577
819
  shadowPushAsync(shadowConfig.worktreeDir, verbose);
578
820
  }
@@ -650,6 +892,30 @@ export async function fetchRemote(projectRoot, remoteName = "origin") {
650
892
  return false;
651
893
  }
652
894
  }
895
+ /**
896
+ * Check if the local shadow branch has unpushed commits ahead of upstream.
897
+ * Returns true if local is ahead, false otherwise (including when upstream
898
+ * ref doesn't exist or an error occurs).
899
+ *
900
+ * @param worktreeDir Path to shadow worktree
901
+ */
902
+ export async function isAheadOfUpstream(worktreeDir) {
903
+ try {
904
+ const { stdout } = await runGitAsync(worktreeDir, [
905
+ "rev-list",
906
+ "--left-right",
907
+ "--count",
908
+ "HEAD...@{u}",
909
+ ]);
910
+ const [aheadStr] = stdout.trim().split("\t");
911
+ const ahead = parseInt(aheadStr, 10);
912
+ return ahead > 0;
913
+ }
914
+ catch {
915
+ // No upstream ref or other error — not ahead
916
+ return false;
917
+ }
918
+ }
653
919
  /**
654
920
  * Push shadow branch to remote with tracking.
655
921
  * Returns true if push succeeded, false otherwise.
@@ -817,9 +1083,117 @@ function noteShadowPushSuccess(worktreeDir) {
817
1083
  shadowPushFailureCounts.delete(worktreeDir);
818
1084
  }
819
1085
  /**
820
- * Fire-and-forget push to remote.
1086
+ * Pull-rebase from remote before pushing, using the kspec merge driver for
1087
+ * YAML conflict resolution.
1088
+ *
1089
+ * AC: @config-shadow ac-11 — pull-rebase before push prevents divergence
1090
+ *
1091
+ * @returns true if pull succeeded (or was unnecessary), false on conflict
1092
+ */
1093
+ async function pullRebaseBeforePush(worktreeDir, branchName, debug, options) {
1094
+ try {
1095
+ // Fetch latest remote state for the shadow branch specifically.
1096
+ // Using the worktree dir for fetch ensures we use the branch's tracking config.
1097
+ try {
1098
+ await runGitAsync(worktreeDir, ["fetch"]);
1099
+ }
1100
+ catch {
1101
+ if (debug) {
1102
+ console.error("[DEBUG] Shadow pull-rebase: fetch failed, skipping pull");
1103
+ }
1104
+ // Fetch failure is non-fatal — push may still succeed if already up to date
1105
+ return true;
1106
+ }
1107
+ // AC: @config-shadow ac-3 — resolve the configured remote name from git config
1108
+ // instead of hardcoding "origin", so custom shadow.remote setups work correctly
1109
+ const projectRoot = path.dirname(worktreeDir);
1110
+ let remoteName = "origin";
1111
+ try {
1112
+ const { stdout } = await runGitAsync(worktreeDir, [
1113
+ "config",
1114
+ `branch.${branchName}.remote`,
1115
+ ]);
1116
+ const configured = stdout.trim();
1117
+ if (configured) {
1118
+ remoteName = configured;
1119
+ }
1120
+ }
1121
+ catch {
1122
+ // Fall back to origin if config lookup fails
1123
+ }
1124
+ const remoteHasBranch = await remoteBranchExists(projectRoot, branchName, remoteName);
1125
+ if (!remoteHasBranch) {
1126
+ if (debug) {
1127
+ console.error("[DEBUG] Shadow pull-rebase: no remote branch yet, skipping pull");
1128
+ }
1129
+ return true;
1130
+ }
1131
+ // Check if there are any upstream changes to integrate.
1132
+ // If local is already at or ahead of remote, skip the pull.
1133
+ try {
1134
+ const { stdout } = await runGitAsync(worktreeDir, [
1135
+ "rev-list",
1136
+ "--count",
1137
+ `${branchName}..@{upstream}`,
1138
+ ]);
1139
+ const behindCount = parseInt(stdout.trim(), 10);
1140
+ if (behindCount === 0) {
1141
+ if (debug) {
1142
+ console.error("[DEBUG] Shadow pull-rebase: already up to date with remote");
1143
+ }
1144
+ return true;
1145
+ }
1146
+ }
1147
+ catch {
1148
+ // rev-list may fail if upstream isn't set — proceed with pull attempt
1149
+ }
1150
+ // Try fast-forward first (cleanest, no rebase needed)
1151
+ try {
1152
+ await runGitAsync(worktreeDir, ["pull", "--ff-only"]);
1153
+ if (debug) {
1154
+ console.error("[DEBUG] Shadow pull-rebase: fast-forward succeeded");
1155
+ }
1156
+ return true;
1157
+ }
1158
+ catch {
1159
+ // FF failed, need rebase
1160
+ }
1161
+ // Fall back to rebase — the kspec merge driver handles YAML conflicts
1162
+ try {
1163
+ await runGitAsync(worktreeDir, ["pull", "--rebase"]);
1164
+ if (debug) {
1165
+ console.error("[DEBUG] Shadow pull-rebase: rebase succeeded");
1166
+ }
1167
+ return true;
1168
+ }
1169
+ catch {
1170
+ // Rebase failed — abort and report
1171
+ }
1172
+ // Abort the failed rebase so local state is clean
1173
+ try {
1174
+ await runGitAsync(worktreeDir, ["rebase", "--abort"]);
1175
+ }
1176
+ catch {
1177
+ // May not be in rebase state
1178
+ }
1179
+ if (debug) {
1180
+ console.error("[DEBUG] Shadow pull-rebase: conflict detected, push skipped");
1181
+ }
1182
+ return false;
1183
+ }
1184
+ catch (err) {
1185
+ if (debug) {
1186
+ console.error("[DEBUG] Shadow pull-rebase error:", err);
1187
+ }
1188
+ // Pull failure is non-fatal — still attempt push
1189
+ return true;
1190
+ }
1191
+ }
1192
+ /**
1193
+ * Fire-and-forget push to remote with pull-rebase-before-push.
821
1194
  * AC-1: Called after each auto-commit when tracking is configured.
822
1195
  * AC-8: Automatically sets up tracking if main branch has remote.
1196
+ * AC: @config-shadow ac-11 — pull-rebase before push for bidirectional sync.
823
1197
  * Push failures are surfaced as warnings, but local commits still succeed.
824
1198
  *
825
1199
  * AC: @config-shadow ac-7 — backward compat when called without config
@@ -847,6 +1221,14 @@ export async function shadowPushAsync(worktreeDir, verbose, options) {
847
1221
  }
848
1222
  return; // AC: @shadow-sync ac-4 - silently skip if no tracking
849
1223
  }
1224
+ // AC: @config-shadow ac-11 — pull-rebase before pushing to integrate remote changes
1225
+ // AC: @shadow-write-sync ac-write-always-syncs — writes always perform full sync
1226
+ const branchName = getBranchName(options);
1227
+ const pullOk = await pullRebaseBeforePush(worktreeDir, branchName, debug, options);
1228
+ if (!pullOk) {
1229
+ noteShadowPushFailure(worktreeDir, "Pull-rebase failed due to conflicts. Run `kspec shadow resolve` to fix.");
1230
+ return;
1231
+ }
850
1232
  try {
851
1233
  if (debug) {
852
1234
  console.error(`[DEBUG] Shadow push: git push (cwd: ${worktreeDir})`);
@@ -915,7 +1297,22 @@ export async function shadowPushAsync(worktreeDir, verbose, options) {
915
1297
  * @param worktreeDir Path to shadow worktree
916
1298
  * @param options Optional shadow configuration
917
1299
  */
918
- export async function shadowPull(worktreeDir, options) {
1300
+ // In-flight dedup: if a pull is already running for this worktree, piggyback
1301
+ // on its result instead of starting a concurrent stash/pull/pop sequence.
1302
+ const pullInflight = new Map();
1303
+ export function shadowPull(worktreeDir, options) {
1304
+ const key = path.resolve(worktreeDir);
1305
+ const existing = pullInflight.get(key);
1306
+ if (existing) {
1307
+ return existing;
1308
+ }
1309
+ const promise = shadowPullImpl(worktreeDir, options).finally(() => {
1310
+ pullInflight.delete(key);
1311
+ });
1312
+ pullInflight.set(key, promise);
1313
+ return promise;
1314
+ }
1315
+ async function shadowPullImpl(worktreeDir, options) {
919
1316
  const branchName = getBranchName(options);
920
1317
  const result = {
921
1318
  success: false,
@@ -937,17 +1334,41 @@ export async function shadowPull(worktreeDir, options) {
937
1334
  return result;
938
1335
  }
939
1336
  // Check if remote branch exists before attempting pull
1337
+ // AC: @config-shadow ac-3 — use configured remote instead of hardcoded origin
1338
+ const remoteName = getRemoteName(options);
940
1339
  // Fetch first to ensure refs are up to date
941
- await fetchRemote(projectRoot);
942
- const remoteHasBranch = await remoteBranchExists(projectRoot, branchName);
1340
+ await fetchRemote(projectRoot, remoteName);
1341
+ const remoteHasBranch = await remoteBranchExists(projectRoot, branchName, remoteName);
943
1342
  if (!remoteHasBranch) {
944
1343
  // Remote branch doesn't exist yet - nothing to pull, but success
945
1344
  result.success = true;
946
1345
  return result;
947
1346
  }
1347
+ // Stash uncommitted changes before pulling to avoid false conflict reports
1348
+ let stashed = false;
1349
+ try {
1350
+ const { stdout } = await runGitAsync(worktreeDir, ["stash", "push", "-m", "shadow-sync-auto"]);
1351
+ stashed = !stdout.includes("No local changes");
1352
+ }
1353
+ catch {
1354
+ // If stash fails, skip the pull entirely — don't risk reporting a false conflict
1355
+ result.success = true;
1356
+ return result;
1357
+ }
1358
+ const unstash = async () => {
1359
+ if (stashed) {
1360
+ try {
1361
+ await runGitAsync(worktreeDir, ["stash", "pop"]);
1362
+ }
1363
+ catch {
1364
+ // Stash pop conflict is unlikely but leave stash intact if it happens
1365
+ }
1366
+ }
1367
+ };
948
1368
  try {
949
1369
  // Try fast-forward only first (cleanest)
950
1370
  await runGitAsync(worktreeDir, ["pull", "--ff-only"]);
1371
+ await unstash();
951
1372
  result.success = true;
952
1373
  result.pulled = true;
953
1374
  return result;
@@ -958,6 +1379,7 @@ export async function shadowPull(worktreeDir, options) {
958
1379
  try {
959
1380
  // AC: @shadow-sync ac-6 - Fall back to rebase
960
1381
  await runGitAsync(worktreeDir, ["pull", "--rebase"]);
1382
+ await unstash();
961
1383
  result.success = true;
962
1384
  result.pulled = true;
963
1385
  return result;
@@ -972,6 +1394,7 @@ export async function shadowPull(worktreeDir, options) {
972
1394
  catch {
973
1395
  // May not be in rebase state, ignore
974
1396
  }
1397
+ await unstash();
975
1398
  result.hadConflict = true;
976
1399
  result.error = "Sync conflict detected. Run `kspec shadow resolve` to fix.";
977
1400
  return result;
@@ -1004,6 +1427,135 @@ export async function shadowSync(worktreeDir, options) {
1004
1427
  }
1005
1428
  return pullResult;
1006
1429
  }
1430
+ // ─── Lazy Drift Check ────────────────────────────────────────────────────────
1431
+ const FETCH_TIMEOUT_MS = 5000;
1432
+ /**
1433
+ * Spawn a git command with a hard timeout. Returns stdout/stderr on success,
1434
+ * throws on non-zero exit or timeout. On timeout, sends SIGTERM then SIGKILL.
1435
+ *
1436
+ * Used for drift check fetch only — other git ops continue using runGitAsync.
1437
+ *
1438
+ * AC: @shadow-lazy-read-sync ac-fetch-timeout
1439
+ */
1440
+ export function spawnGitWithTimeout(cwd, args, timeoutMs) {
1441
+ return new Promise((resolve, reject) => {
1442
+ const child = spawn("git", args, {
1443
+ cwd,
1444
+ stdio: ["ignore", "pipe", "pipe"],
1445
+ });
1446
+ let stdout = "";
1447
+ let stderr = "";
1448
+ child.stdout.on("data", (d) => {
1449
+ stdout += d;
1450
+ });
1451
+ child.stderr.on("data", (d) => {
1452
+ stderr += d;
1453
+ });
1454
+ let promiseSettled = false;
1455
+ let processExited = false;
1456
+ const timer = setTimeout(() => {
1457
+ child.kill("SIGTERM");
1458
+ setTimeout(() => {
1459
+ if (!processExited)
1460
+ child.kill("SIGKILL");
1461
+ }, 1000);
1462
+ promiseSettled = true;
1463
+ reject(new Error(`git ${args[0]} timed out after ${timeoutMs}ms`));
1464
+ }, timeoutMs);
1465
+ child.on("close", (code) => {
1466
+ processExited = true;
1467
+ clearTimeout(timer);
1468
+ if (promiseSettled)
1469
+ return;
1470
+ promiseSettled = true;
1471
+ if (code === 0)
1472
+ resolve({ stdout, stderr });
1473
+ else
1474
+ reject(new Error(`git ${args[0]} exited ${code}: ${stderr}`));
1475
+ });
1476
+ });
1477
+ }
1478
+ /**
1479
+ * Lightweight drift check: determine whether local shadow branch needs
1480
+ * to pull from remote. Uses FETCH_HEAD mtime to avoid redundant fetches,
1481
+ * and ahead/behind counts to decide if a pull is needed.
1482
+ *
1483
+ * AC: @shadow-lazy-read-sync ac-drift-check
1484
+ * AC: @shadow-lazy-read-sync ac-fetch-head-location
1485
+ * AC: @shadow-lazy-read-sync ac-fetch-head-freshness
1486
+ * AC: @shadow-lazy-read-sync ac-fetch-when-stale
1487
+ * AC: @shadow-lazy-read-sync ac-fetch-timeout-no-error
1488
+ * AC: @shadow-lazy-read-sync ac-fetch-timeout-debug-log
1489
+ * AC: @shadow-lazy-read-sync ac-pull-when-behind
1490
+ * AC: @shadow-lazy-read-sync ac-no-pull-when-ahead
1491
+ * AC: @shadow-lazy-read-sync ac-pull-when-diverged
1492
+ * AC: @shadow-lazy-read-sync ac-upstream-ref-missing
1493
+ * AC: @shadow-lazy-read-sync ac-no-drift-fast-path
1494
+ * AC: @shadow-lazy-read-sync ac-threshold-from-config
1495
+ *
1496
+ * @returns true if shadowPull() should be called, false if local state is current
1497
+ */
1498
+ export async function shadowNeedsSync(worktreeDir, remoteName, thresholdMs) {
1499
+ // 1. Resolve FETCH_HEAD path for this worktree
1500
+ // AC: ac-fetch-head-location — use rev-parse --git-path from worktree dir
1501
+ const { stdout: fetchHeadRaw } = await runGitAsync(worktreeDir, [
1502
+ "rev-parse",
1503
+ "--git-path",
1504
+ "FETCH_HEAD",
1505
+ ]);
1506
+ const fetchHeadPath = path.resolve(worktreeDir, fetchHeadRaw.trim());
1507
+ // 2. Check freshness — if stale or missing, fetch with timeout
1508
+ // AC: ac-fetch-head-freshness, ac-fetch-when-stale
1509
+ let fetchNeeded = true;
1510
+ try {
1511
+ const stat = await fs.stat(fetchHeadPath);
1512
+ fetchNeeded = Date.now() - stat.mtimeMs > thresholdMs;
1513
+ }
1514
+ catch {
1515
+ // No FETCH_HEAD — need to fetch
1516
+ }
1517
+ if (fetchNeeded) {
1518
+ try {
1519
+ // AC: ac-fetch-timeout — kill if exceeds FETCH_TIMEOUT_MS
1520
+ await spawnGitWithTimeout(worktreeDir, ["fetch", remoteName], FETCH_TIMEOUT_MS);
1521
+ }
1522
+ catch (err) {
1523
+ // AC: ac-fetch-timeout-no-error — no error surfaced to user
1524
+ // AC: ac-fetch-timeout-debug-log — debug log if enabled
1525
+ if (isDebugEnabled()) {
1526
+ console.error(`[DEBUG] shadow drift-check: fetch failed: ${err instanceof Error ? err.message : String(err)}`);
1527
+ }
1528
+ return false;
1529
+ }
1530
+ }
1531
+ // 3. Check ahead/behind — only sync when behind or diverged
1532
+ // AC: ac-pull-when-behind, ac-no-pull-when-ahead, ac-pull-when-diverged
1533
+ try {
1534
+ const { stdout } = await runGitAsync(worktreeDir, [
1535
+ "rev-list",
1536
+ "--left-right",
1537
+ "--count",
1538
+ "HEAD...@{u}",
1539
+ ]);
1540
+ const [, behind] = stdout.trim().split("\t").map(Number);
1541
+ // AC: ac-no-drift-fast-path — behind === 0 means no pull needed
1542
+ return behind > 0;
1543
+ }
1544
+ catch {
1545
+ // AC: ac-upstream-ref-missing — force sync as safer default
1546
+ return true;
1547
+ }
1548
+ }
1549
+ /**
1550
+ * Check if debug logging is enabled (KSPEC_DEBUG=1 or --debug-shadow).
1551
+ */
1552
+ function isDebugEnabled() {
1553
+ if (process.env.KSPEC_DEBUG === "1")
1554
+ return true;
1555
+ if (getVerboseModeFunc?.())
1556
+ return true;
1557
+ return false;
1558
+ }
1007
1559
  /**
1008
1560
  * Check if .gitignore has uncommitted changes
1009
1561
  */
@@ -1092,6 +1644,123 @@ async function ensureGitignore(projectRoot, options) {
1092
1644
  throw new ShadowError(`Failed to update .gitignore: ${error}`, "GIT_ERROR", "Check file permissions for .gitignore");
1093
1645
  }
1094
1646
  }
1647
+ /**
1648
+ * Add .kspec-sessions/ to root .gitignore if not already present.
1649
+ * Does NOT commit — caller is responsible for committing if needed.
1650
+ *
1651
+ * AC: @session-storage-modes ac-gitignore
1652
+ *
1653
+ * @param projectRoot Git repository root
1654
+ * @returns true if entry was added, false if already present
1655
+ */
1656
+ export async function needsSessionsGitignore(projectRoot) {
1657
+ const gitignorePath = path.join(projectRoot, ".gitignore");
1658
+ let content = "";
1659
+ try {
1660
+ content = await fs.readFile(gitignorePath, "utf-8");
1661
+ }
1662
+ catch {
1663
+ // File doesn't exist — entry is needed
1664
+ return true;
1665
+ }
1666
+ const lines = content.split("\n");
1667
+ const patterns = [
1668
+ SESSIONS_WORKTREE_DIR,
1669
+ `${SESSIONS_WORKTREE_DIR}/`,
1670
+ `/${SESSIONS_WORKTREE_DIR}`,
1671
+ `/${SESSIONS_WORKTREE_DIR}/`,
1672
+ ];
1673
+ for (const line of lines) {
1674
+ const trimmed = line.trim();
1675
+ if (patterns.includes(trimmed)) {
1676
+ return false; // Already present
1677
+ }
1678
+ }
1679
+ return true;
1680
+ }
1681
+ export async function ensureSessionsGitignore(projectRoot) {
1682
+ const gitignorePath = path.join(projectRoot, ".gitignore");
1683
+ const entry = `${SESSIONS_WORKTREE_DIR}/`;
1684
+ try {
1685
+ const needed = await needsSessionsGitignore(projectRoot);
1686
+ if (!needed) {
1687
+ return false;
1688
+ }
1689
+ let content = "";
1690
+ try {
1691
+ content = await fs.readFile(gitignorePath, "utf-8");
1692
+ }
1693
+ catch {
1694
+ // File doesn't exist, will create
1695
+ }
1696
+ // Add to gitignore
1697
+ const newContent = content.endsWith("\n") || content === ""
1698
+ ? `${content}${entry}\n`
1699
+ : `${content}\n${entry}\n`;
1700
+ await fs.writeFile(gitignorePath, newContent, "utf-8");
1701
+ return true;
1702
+ }
1703
+ catch (error) {
1704
+ throw new ShadowError(`Failed to update .gitignore with sessions directory: ${error}`, "GIT_ERROR", "Check file permissions for .gitignore");
1705
+ }
1706
+ }
1707
+ /**
1708
+ * Add sessions/ to .kspec/.gitignore to prevent legacy session data
1709
+ * from being tracked on kspec-meta shadow branch.
1710
+ *
1711
+ * AC: @session-legacy-migration ac-shadow-gitignore
1712
+ *
1713
+ * @param projectRoot Git repository root
1714
+ * @param options Optional shadow configuration for directory name
1715
+ * @returns true if entry was added, false if already present
1716
+ */
1717
+ export async function needsShadowSessionsGitignore(projectRoot, options) {
1718
+ const directoryName = getDirectoryName(options);
1719
+ const shadowGitignorePath = path.join(projectRoot, directoryName, ".gitignore");
1720
+ const entry = "sessions/";
1721
+ try {
1722
+ const content = await fs.readFile(shadowGitignorePath, "utf-8");
1723
+ const lines = content.split("\n");
1724
+ if (lines.some((line) => line.trim() === entry || line.trim() === "sessions")) {
1725
+ return false; // Already present
1726
+ }
1727
+ return true;
1728
+ }
1729
+ catch {
1730
+ // File doesn't exist — can't add to non-existent file
1731
+ return false;
1732
+ }
1733
+ }
1734
+ export async function ensureShadowSessionsGitignore(projectRoot, options) {
1735
+ const directoryName = getDirectoryName(options);
1736
+ const shadowGitignorePath = path.join(projectRoot, directoryName, ".gitignore");
1737
+ const entry = "sessions/";
1738
+ try {
1739
+ const needed = await needsShadowSessionsGitignore(projectRoot, options);
1740
+ if (!needed) {
1741
+ return false;
1742
+ }
1743
+ let content = "";
1744
+ try {
1745
+ content = await fs.readFile(shadowGitignorePath, "utf-8");
1746
+ }
1747
+ catch {
1748
+ // File doesn't exist — this shouldn't happen since init creates it,
1749
+ // but handle gracefully
1750
+ return false;
1751
+ }
1752
+ // Add to gitignore
1753
+ const newContent = content.endsWith("\n") || content === ""
1754
+ ? `${content}${entry}\n`
1755
+ : `${content}\n${entry}\n`;
1756
+ await fs.writeFile(shadowGitignorePath, newContent, "utf-8");
1757
+ return true;
1758
+ }
1759
+ catch (error) {
1760
+ // Non-fatal — shadow gitignore update is best-effort
1761
+ return false;
1762
+ }
1763
+ }
1095
1764
  /**
1096
1765
  * Generate initial manifest content for shadow branch
1097
1766
  */
@@ -1350,14 +2019,32 @@ export async function initializeShadow(projectRoot, options = {}) {
1350
2019
  if (remoteExists) {
1351
2020
  remoteHasShadow = await remoteBranchExists(projectRoot, branchName, remoteName);
1352
2021
  }
2022
+ let stashedWorktreeDir = null;
1353
2023
  try {
1354
2024
  // Step 1: Update .gitignore first (before creating worktree)
1355
2025
  result.gitignoreUpdated = await ensureGitignore(projectRoot, options.shadow);
2026
+ // Step 1b: Also add .kspec-sessions/ to .gitignore
2027
+ // AC: @session-storage-modes ac-gitignore
2028
+ const sessionsAdded = await ensureSessionsGitignore(projectRoot);
2029
+ if (sessionsAdded) {
2030
+ // Commit .kspec-sessions/ gitignore entry (may be separate commit if .kspec/ was already present)
2031
+ await runGitAsync(projectRoot, ["add", ".gitignore"]);
2032
+ await runGitAsync(projectRoot, [
2033
+ "commit",
2034
+ "-m",
2035
+ `chore: add ${SESSIONS_WORKTREE_DIR}/ to .gitignore for session storage`,
2036
+ ]);
2037
+ }
2038
+ // Step 1c: Create .kspec-sessions/ directory
2039
+ // AC: @session-storage-modes ac-sessions-dir-autocreate
2040
+ const sessionsDir = path.join(projectRoot, SESSIONS_WORKTREE_DIR);
2041
+ await fs.mkdir(sessionsDir, { recursive: true });
2042
+ result.sessionsDirectoryCreated = true;
1356
2043
  // Step 2: Create worktree with orphan branch (or attach to existing branch)
1357
2044
  if (!status.worktreeExists || !status.worktreeLinked) {
1358
2045
  // Remove existing directory if present but not linked
1359
2046
  if (status.worktreeExists && !status.worktreeLinked) {
1360
- await fs.rm(worktreeDir, { recursive: true, force: true });
2047
+ stashedWorktreeDir = await stashBrokenWorktreeDir(worktreeDir);
1361
2048
  }
1362
2049
  // Remove stale worktree reference if any
1363
2050
  try {
@@ -1405,14 +2092,20 @@ export async function initializeShadow(projectRoot, options = {}) {
1405
2092
  else if (!status.branchExists) {
1406
2093
  // AC: @shadow-init-remote ac-2 ac-3 - No remote branch or no remote - create orphan branch
1407
2094
  // AC: @config-shadow ac-1 — use configured branch name
1408
- await runGitAsync(projectRoot, [
1409
- "worktree",
1410
- "add",
1411
- "--orphan",
1412
- "-b",
1413
- branchName,
1414
- directoryName,
1415
- ]);
2095
+ // AC: @config-shadow ac-10 — fallback for git < 2.42
2096
+ if (gitSupportsOrphanWorktree(projectRoot)) {
2097
+ await runGitAsync(projectRoot, [
2098
+ "worktree",
2099
+ "add",
2100
+ "--orphan",
2101
+ "-b",
2102
+ branchName,
2103
+ directoryName,
2104
+ ]);
2105
+ }
2106
+ else {
2107
+ await createOrphanBranchFallback(projectRoot, branchName, directoryName);
2108
+ }
1416
2109
  result.branchCreated = true;
1417
2110
  }
1418
2111
  else {
@@ -1454,7 +2147,8 @@ export async function initializeShadow(projectRoot, options = {}) {
1454
2147
  // AC: @artifacts-directory ac-init-creates, ac-gitignore-entry
1455
2148
  const artifactsDir = path.join(worktreeDir, "artifacts");
1456
2149
  await fs.mkdir(artifactsDir, { recursive: true });
1457
- await fs.writeFile(path.join(worktreeDir, ".gitignore"), "# Ephemeral artifacts - reports, exports, generated files\n# Not tracked in shadow branch\nartifacts/\n", "utf-8");
2150
+ // AC: @session-legacy-migration ac-shadow-gitignore sessions/ not tracked on kspec-meta
2151
+ await fs.writeFile(path.join(worktreeDir, ".gitignore"), "# Ephemeral artifacts - reports, exports, generated files\n# Not tracked in shadow branch\nartifacts/\n\n# Sessions stored in .kspec-sessions/ at project root, not on shadow branch\nsessions/\n", "utf-8");
1458
2152
  filesCreated = true;
1459
2153
  }
1460
2154
  // Step 4: Initial commit if files were created
@@ -1470,10 +2164,23 @@ export async function initializeShadow(projectRoot, options = {}) {
1470
2164
  // Step 7: Configure merge driver for semantic YAML merging
1471
2165
  // AC: @yaml-merge-driver ac-12
1472
2166
  await configureMergeDriver(projectRoot, worktreeDir);
2167
+ // Step 8: Initialize session branch worktree if sessions.storage is "branch"
2168
+ // AC: @session-branch-worktree ac-init
2169
+ if (options.sessions?.storage === "branch") {
2170
+ const { initializeSessionBranch } = await import("./session-branch.js");
2171
+ const sessionBranchName = options.sessions.branch || "kspec-sessions";
2172
+ const sessionResult = await initializeSessionBranch(projectRoot, sessionBranchName);
2173
+ if (sessionResult.success) {
2174
+ result.sessionBranchCreated = true;
2175
+ }
2176
+ // Non-fatal: session branch failure doesn't block shadow init
2177
+ }
2178
+ await discardStashedWorktreeDir(stashedWorktreeDir);
1473
2179
  result.success = true;
1474
2180
  return result;
1475
2181
  }
1476
2182
  catch (error) {
2183
+ await restoreStashedWorktreeDir(stashedWorktreeDir, worktreeDir).catch(() => { });
1477
2184
  result.error = error instanceof Error ? error.message : String(error);
1478
2185
  return result;
1479
2186
  }
@@ -1492,6 +2199,8 @@ export async function repairShadow(projectRoot, options) {
1492
2199
  const branchName = getBranchName(options);
1493
2200
  const directoryName = getDirectoryName(options);
1494
2201
  const status = await getShadowStatus(projectRoot, options);
2202
+ const remoteQueryTarget = getRemoteQueryTarget(options);
2203
+ const remoteHasShadow = await remoteShadowBranchExists(projectRoot, options);
1495
2204
  if (status.healthy) {
1496
2205
  return {
1497
2206
  success: true,
@@ -1504,7 +2213,7 @@ export async function repairShadow(projectRoot, options) {
1504
2213
  pushedToRemote: false,
1505
2214
  };
1506
2215
  }
1507
- if (!status.branchExists) {
2216
+ if (!status.branchExists && !remoteHasShadow) {
1508
2217
  // Can't repair without a branch - need full init
1509
2218
  return {
1510
2219
  success: false,
@@ -1518,8 +2227,9 @@ export async function repairShadow(projectRoot, options) {
1518
2227
  error: "Shadow branch does not exist. Run `kspec init` instead.",
1519
2228
  };
1520
2229
  }
1521
- // Branch exists but worktree is broken - repair it
2230
+ // The branch exists locally or remotely, but the worktree is broken - repair it.
1522
2231
  const worktreeDir = path.join(projectRoot, directoryName);
2232
+ let stashedWorktreeDir = null;
1523
2233
  try {
1524
2234
  // Remove stale worktree reference
1525
2235
  try {
@@ -1534,7 +2244,7 @@ export async function repairShadow(projectRoot, options) {
1534
2244
  // Ignore - worktree may not be in git's list
1535
2245
  }
1536
2246
  // Remove directory if exists (handles corrupted .git file case)
1537
- await fs.rm(worktreeDir, { recursive: true, force: true });
2247
+ stashedWorktreeDir = await stashBrokenWorktreeDir(worktreeDir);
1538
2248
  // Prune stale worktree references (cleans up orphaned entries)
1539
2249
  try {
1540
2250
  await runGitAsync(projectRoot, ["worktree", "prune"]);
@@ -1542,6 +2252,13 @@ export async function repairShadow(projectRoot, options) {
1542
2252
  catch {
1543
2253
  // Ignore - prune is best-effort
1544
2254
  }
2255
+ if (!status.branchExists && remoteHasShadow) {
2256
+ await runGitAsync(projectRoot, [
2257
+ "fetch",
2258
+ remoteQueryTarget,
2259
+ `${branchName}:${branchName}`,
2260
+ ]);
2261
+ }
1545
2262
  // Recreate worktree
1546
2263
  await runGitAsync(projectRoot, [
1547
2264
  "worktree",
@@ -1549,6 +2266,13 @@ export async function repairShadow(projectRoot, options) {
1549
2266
  directoryName,
1550
2267
  branchName,
1551
2268
  ]);
2269
+ if (remoteHasShadow) {
2270
+ const tracking = await ensureRemoteTracking(worktreeDir, projectRoot, options);
2271
+ if (!tracking.success) {
2272
+ throw new Error(tracking.guidance || "Failed to configure shadow branch remote tracking");
2273
+ }
2274
+ }
2275
+ await discardStashedWorktreeDir(stashedWorktreeDir);
1552
2276
  // Install pre-commit hook
1553
2277
  await installShadowHook(projectRoot);
1554
2278
  // AC: @artifacts-directory ac-repair-recreates
@@ -1561,11 +2285,12 @@ export async function repairShadow(projectRoot, options) {
1561
2285
  gitignoreUpdated: false,
1562
2286
  initialCommit: false,
1563
2287
  alreadyExists: false,
1564
- createdFromRemote: false,
2288
+ createdFromRemote: !status.branchExists && remoteHasShadow,
1565
2289
  pushedToRemote: false,
1566
2290
  };
1567
2291
  }
1568
2292
  catch (error) {
2293
+ await restoreStashedWorktreeDir(stashedWorktreeDir, worktreeDir).catch(() => { });
1569
2294
  return {
1570
2295
  success: false,
1571
2296
  branchCreated: false,